clone

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

feat/standalone-relay
tinyrelay / scripts / blossom-manifests-review.test.mjs
blossom-manifests-review.test.mjs 4.9K
1import assert from "node:assert/strict";2import {readFile} from "node:fs/promises";3import test from "node:test";4import vm from "node:vm";56const source = await readFile(new URL("../tinyclient/blossom-manifests.js", import.meta.url), "utf8");7const shared = await readFile(new URL("../tinyclient/tiny.js", import.meta.url), "utf8");8const sandbox = {TextEncoder, TextDecoder, Uint8Array, ArrayBuffer, crypto: (await import("node:crypto")).webcrypto};9sandbox.globalThis = sandbox;10vm.runInNewContext(shared + source, sandbox);11const api = sandbox.tiny.blossom.manifests;12const hash = value => value.repeat(64);13const bytes = hex => Uint8Array.from(hex.match(/../g), value => parseInt(value, 16));1415test("rejects duplicate names split across directory fanout children", async () => {16 const entries = Array.from({length: 173}, (_, i) => ({hash: hash("a"), name: `a${String(i).padStart(3, "0")}`, size: 1}));17 entries.push({hash: hash("b"), name: "dup", size: 1}, {hash: hash("c"), name: "dup", size: 1});18 await assert.rejects(api.buildDirectory(entries), /duplicate directory entry/);19});2021test("decrypts encrypted file-manifest children before decoding", async () => {22 const leaf = new TextEncoder().encode("ok");23 const leafHash = api.hex(await api.sha256(leaf));24 const child = {l: [{h: bytes(leafHash), s: leaf.length, t: 0}], t: 1};25 const childBytes = api.encodeManifest(child);26 const encrypted = Uint8Array.from(childBytes, value => value ^ 0xff);27 const childHash = await api.hex(await api.sha256(encrypted));28 const root = {l: [{h: bytes(childHash), k: new Uint8Array(32), s: 2, t: 1}], t: 1};29 const result = await api.readFile(root, async (wantedHash) => {30 if (wantedHash === childHash) return encrypted;31 assert.equal(wantedHash, leafHash);32 return leaf;33 }, {decrypt: async value => {34 assert.equal(api.hex(value), api.hex(encrypted));35 return childBytes;36 }});37 assert.equal(result.length, 2);38});3940test("decrypts encrypted directory-manifest children before decoding", async () => {41 const leaf = new TextEncoder().encode("ok");42 const leafHash = api.hex(await api.sha256(leaf));43 const child = {l: [{h: bytes(leafHash), n: "a.txt", s: leaf.length, t: 0}], t: 2};44 const childBytes = api.encodeManifest(child);45 const encrypted = Uint8Array.from(childBytes, value => value ^ 0xff);46 const childHash = await api.hex(await api.sha256(encrypted));47 const root = {l: [{h: bytes(childHash), k: new Uint8Array(32), m: {count: 1, first: "a.txt", last: "a.txt"}, s: leaf.length, t: 2}], t: 3};48 const result = await api.resolveDirectory(root, async wantedHash => {49 assert.equal(wantedHash, childHash);50 return encrypted;51 }, {decrypt: async value => {52 assert.equal(api.hex(value), api.hex(encrypted));53 return childBytes;54 }});55 assert.equal(result[0].n, "a.txt");56});5758test("matches the BUD-17 directory fanout vector", async () => {59 const node = {l: [60 {h: bytes("11".repeat(32)), m: {count: 2, first: "a.txt", last: "b.txt"}, s: 30, t: 2},61 {h: bytes("22".repeat(32)), m: {count: 1, first: "c.txt", last: "c.txt"}, s: 40, t: 2}62 ], t: 3};63 assert.equal(await api.manifestHash(node), "6626ab03b5468f417d888fa25fa22b48f5bcb7dfafb88eef34c638d167afc0a3");64});6566test("sorts and preserves non-ASCII names by UTF-8 byte order", async () => {67 const node = await api.buildDirectory([68 {hash: hash("a"), name: "é", size: 1},69 {hash: hash("b"), name: "e\u0301", size: 1},70 {hash: hash("c"), name: "😀", size: 1}71 ]);72 assert.deepEqual(node.l.map(link => link.n), ["e\u0301", "é", "😀"]);73});7475test("honors zero traversal budgets", async () => {76 const one = new TextEncoder().encode("x");77 const oneHash = api.hex(await api.sha256(one));78 const file = {l: [{h: bytes(oneHash), s: 1, t: 0}], t: 1};79 await assert.rejects(api.readFile(file, async () => one, {maxManifests: 0}), /manifest count limit exceeded/);80 await assert.rejects(api.readFile(file, async () => one, {maxBytes: 0}), /file byte limit exceeded/);81 await assert.rejects(api.resolveDirectory({l: [], t: 3}, async () => new Uint8Array(), {maxManifests: 0}), /manifest count limit exceeded/);82});8384test("bounds total directory fetches and permits encryption overhead separately", async () => {85 const child = {l: [{h: bytes("aa".repeat(32)), n: "a", s: 2, t: 0}], t: 2};86 const raw = api.encodeManifest(child), digest = api.hex(await api.sha256(raw));87 const root = {l: [{h: bytes(digest), m: {count: 1, first: "a", last: "a"}, s: 2, t: 2}], t: 3};88 await assert.rejects(api.resolveDirectory(root, async () => raw, {maxBytes: 1}), /byte limit/);89 const ciphertext = new Uint8Array(18), hash = api.hex(await api.sha256(ciphertext));90 const file = {l: [{h: bytes(hash), k: new Uint8Array(32), s: 2, t: 0}], t: 1};91 assert.equal((await api.readFile(file, async () => ciphertext, {maxBytes: 2, decrypt: async () => new Uint8Array(2)})).length, 2);92 await assert.rejects(api.readFile(file, async () => ciphertext, {maxBytes: 2, maxFetchedBytes: 1}), /byte limit/);93});