tinyrelayfile
tinyrelay/scripts/file-messages.test.mjs/

scripts/file-messages.test.mjs raw
1import test from "node:test";
2import assert from "node:assert/strict";
3import fs from "node:fs";
4import vm from "node:vm";
5import { finalizeEvent, generateSecretKey, getEventHash, getPublicKey, nip19, nip44, verifyEvent } from "nostr-tools";
6
7globalThis.NostrSigner = { generateSecretKey, getEventHash, getPublicKey, finalizeEvent, verifyEvent, nip44, decodeNpub: (value) => nip19.decode(value).data };
8globalThis.tiny = {files: {}};
9vm.runInThisContext(fs.readFileSync("tinyclient/tiny.js", "utf8"), { filename: "tiny.js" });
10vm.runInThisContext(fs.readFileSync("tinyclient/file-messages.js", "utf8"), { filename: "file-messages.js" });
11const fileMessages = globalThis.tiny.files.messages;
12
13const alice = generateSecretKey();
14const bob = generateSecretKey();
15const alicePub = getPublicKey(alice);
16const bobPub = getPublicKey(bob);
17const signer = {
18 async getPublicKey() { return alicePub; },
19 async signEvent(event) { return finalizeEvent(event, alice); },
20 async nip44Encrypt(recipient, plaintext) { return nip44.encrypt(plaintext, nip44.getConversationKey(alice, recipient)); },
21};
22const list = (pubkey, relays = ["wss://inbox.example"]) => finalizeEvent({ kind: 10050, tags: relays.map((relay) => ["relay", relay]), content: "", created_at: 100 }, pubkey === alicePub ? alice : bob);
23
24test("build creates independently decryptable NIP-17 file gift wraps", async () => {
25 const result = await fileMessages.build({ fileURL: "https://files.example/abc", ciphertextHash: "a".repeat(64), plaintextHash: "b".repeat(64), mimeType: "image/png", key: "11".repeat(32), nonce: "22".repeat(12), recipient: bobPub }, { signer, now: 200 });
26 assert.equal(result.rumor.pubkey, alicePub);
27 assert.equal(result.rumor.id, getEventHash(result.rumor));
28 assert.equal(result.rumor.kind, 15);
29 assert.equal(result.rumor.tags.find((tag) => tag[0] === "x")[1], "a".repeat(64));
30 assert.equal(result.events.length, 2);
31 const receiver = result.events[0].event;
32 assert.equal(receiver.kind, 1059);
33 assert.deepEqual(receiver.tags, [["p", bobPub]]);
34 assert.notEqual(receiver.pubkey, result.events[1].event.pubkey);
35 const sealJSON = nip44.decrypt(receiver.content, nip44.getConversationKey(bob, receiver.pubkey));
36 const seal = JSON.parse(sealJSON);
37 assert.equal(seal.kind, 13);
38 assert.deepEqual(seal.tags, []);
39 assert.equal(seal.pubkey, alicePub);
40 assert.equal(verifyEvent(seal), true);
41 const rumor = JSON.parse(nip44.decrypt(seal.content, nip44.getConversationKey(bob, alicePub)));
42 assert.equal(rumor.pubkey, alicePub);
43 assert.equal(rumor.content, "https://files.example/abc");
44 assert.equal("" + receiver.content.includes("11".repeat(32)), "false");
45});
46
47test("share requires valid inbox lists and publishes only to those lists", async () => {
48 const published = [];
49 const result = await fileMessages.share({ fileURL: "https://files.example/a", ciphertextHash: "c".repeat(64), key: "11".repeat(32), nonce: "22".repeat(12), recipient: bobPub }, {
50 signer,
51 queryRelayList: async (pubkey) => list(pubkey, pubkey === bobPub ? ["wss://bob.example"] : ["ws://127.0.0.1:7777"]),
52 publish: async (event, relays) => { published.push({ event, relays }); return true; },
53 now: 200,
54 });
55 assert.equal(published.length, 2);
56 assert.deepEqual(published[0].relays, ["wss://bob.example"]);
57 assert.deepEqual(published[1].relays, ["ws://127.0.0.1:7777"]);
58 assert.deepEqual(result.relays.recipient, ["wss://bob.example"]);
59});
60
61test("share refuses to send when either inbox list is absent", async () => {
62 let sends = 0;
63 await assert.rejects(() => fileMessages.share({ fileURL: "https://files.example/a", ciphertextHash: "d".repeat(64), key: "11".repeat(32), nonce: "22".repeat(12), recipient: bobPub }, {
64 signer,
65 queryRelayList: async () => null,
66 publish: async () => { sends++; },
67 }), /valid kind 10050/);
68 assert.equal(sends, 0);
69});
70
71test("share accepts npub recipients and rejects signer identity mismatches", async () => {
72 const published = [];
73 await fileMessages.share({ fileURL: "https://files.example/a", ciphertextHash: "e".repeat(64), key: "11".repeat(32), nonce: "22".repeat(12), recipient: nip19.npubEncode(bobPub) }, {
74 signer,
75 queryRelayList: async (pubkey) => list(pubkey),
76 publish: async (event) => { published.push(event); return true; },
77 });
78 assert.equal(published.length, 2);
79 const mismatched = { ...signer, async signEvent(event) { return finalizeEvent(event, bob); } };
80 await assert.rejects(() => fileMessages.build({ fileURL: "https://files.example/a", ciphertextHash: "f".repeat(64), key: "11".repeat(32), nonce: "22".repeat(12), recipient: bobPub }, { signer: mismatched }), /invalid NIP-17 seal/);
81});
82
83test("share rejects an all-failed publish result", async () => {
84 await assert.rejects(() => fileMessages.share({ fileURL: "https://files.example/a", ciphertextHash: "1".repeat(64), key: "11".repeat(32), nonce: "22".repeat(12), recipient: bobPub }, {
85 signer,
86 queryRelayList: async (pubkey) => list(pubkey),
87 publish: async () => [false, false],
88 }), /every inbox relay/);
89});
90
91test("default transport queries locally and publishes only inbox WebSockets", async () => {
92 const queries = [];
93 const sockets = [];
94 globalThis.tiny = {
95 signedFetch: async (path, method, body) => {
96 queries.push({ path, method, body });
97 const author = JSON.parse(body)[0].authors[0];
98 return { json: async () => [list(author, [author === bobPub ? "wss://bob.example" : "wss://alice.example"]) ] };
99 },
100 };
101 globalThis.WebSocket = class {
102 constructor(url) { this.url = url; sockets.push(this); setTimeout(() => this.onopen?.(), 0); }
103 send(data) { const event = JSON.parse(data)[1]; setTimeout(() => this.onmessage?.({ data: JSON.stringify(["OK", event.id, true, ""]) }), 0); }
104 close() { this.closed = true; }
105 };
106 const result = await fileMessages.share({ fileURL: "https://files.example/a", ciphertextHash: "2".repeat(64), key: "11".repeat(32), nonce: "22".repeat(12), recipient: bobPub }, { signer, timeoutMs: 1000 });
107 assert.equal(queries.length, 2);
108 assert.deepEqual(sockets.map((socket) => socket.url), ["wss://bob.example", "wss://alice.example"]);
109 assert.deepEqual(result.deliveries, [[true], [true]]);
110 delete globalThis.tiny;
111 delete globalThis.WebSocket;
112});
113
114test("default transport answers one NIP-42 challenge and retries the event", async () => {
115 let eventAttempts = 0;
116 globalThis.WebSocket = class {
117 constructor() { setTimeout(() => this.onopen?.(), 0); }
118 send(data) {
119 const frame = JSON.parse(data);
120 if (frame[0] === "EVENT") {
121 eventAttempts++;
122 if (eventAttempts === 1) {
123 setTimeout(() => this.onmessage?.({ data: JSON.stringify(["AUTH", "challenge"]) }), 0);
124 setTimeout(() => this.onmessage?.({ data: JSON.stringify(["OK", frame[1].id, false, "auth-required"]) }), 1);
125 }
126 else setTimeout(() => this.onmessage?.({ data: JSON.stringify(["OK", frame[1].id, true, ""]) }), 0);
127 } else if (frame[0] === "AUTH") setTimeout(() => this.onmessage?.({ data: JSON.stringify(["OK", frame[1].id, true, ""]) }), 0);
128 }
129 close() {}
130 };
131 const result = await fileMessages.publishEvent({ id: "1".repeat(64) }, ["wss://inbox.example"], { signer, timeoutMs: 1000 });
132 assert.deepEqual(result, [true]);
133 assert.equal(eventAttempts, 2);
134 delete globalThis.WebSocket;
135});

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs