clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/ui-updates
tinyrelay / scripts / file-messages.test.mjs
file-messages.test.mjs 7.2K
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";67globalThis.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;1213const 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);2324test("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});4647test("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});6061test("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});7071test("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});8283test("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});9091test("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});113114test("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});