clone

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

feat/ui-updates
tinyrelay / scripts / blossom-manifests.test.mjs
blossom-manifests.test.mjs 4.3K
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 bytes = hex => Uint8Array.from(hex.match(/../g), x => parseInt(x, 16));13const hash = c => c.repeat(64);1415test("matches the BUD-16 empty and single-entry vectors", async () => {16 const empty = {l: [], t: 2};17 assert.equal(Buffer.from(api.encodeManifest(empty)).toString("hex"), "82a16c90a17402");18 assert.equal(await api.manifestHash(empty), "0218ed9a4fbb0993757f17e5d08d089cb0c6ac851928ba1ba82d337d76c41c0c");19 const one = {l: [{h: bytes("ab".repeat(32)), n: "test.txt", s: 100, t: 0}], t: 2};20 assert.equal(await api.manifestHash(one), "16121fa792b3afc72ec8bfc1dc85060518b6adba1429973ecc12891165cbe67e");21 const decoded = api.decodeManifest(api.encodeManifest(one));22 assert.equal(decoded.t, 2);23 assert.equal(decoded.l[0].n, "test.txt");24 assert.equal(Buffer.from(decoded.l[0].h).toString("hex"), "ab".repeat(32));25});2627test("matches the BUD-17 two-chunk vector and preserves chunk order", async () => {28 const node = {l: [{h: bytes("aa".repeat(32)), s: 100, t: 0}, {h: bytes("bb".repeat(32)), s: 50, t: 0}], t: 1};29 assert.equal(await api.manifestHash(node), "559b726c38295aa0ecbbaef43d438cc86dd63324a0c3e9426dc5f1d0285f483f");30 const decoded = api.decodeManifest(api.encodeManifest(node));31 assert.equal(decoded.t, 1);32 assert.equal(JSON.stringify(decoded.l.map(x => [Buffer.from(x.h).toString("hex"), x.s, x.t])), JSON.stringify([["aa".repeat(32), 100, 0], ["bb".repeat(32), 50, 0]]));33});3435test("canonicalizes directory names by UTF-8 bytes and builds file manifests", async () => {36 const directory = await api.buildDirectory([37 {hash: hash("a"), name: "z", size: 2}, {hash: hash("b"), name: "a", size: 1}38 ]);39 assert.deepEqual(directory.l.map(x => x.n), ["a", "z"]);40 const details = await api.buildFile(Array.from({length: 175}, (_, i) => ({hash: hash((i % 10).toString()), size: 1})), {returnDetails: true});41 assert.equal(details.root.t, 1);42 assert.equal(details.root.l.length, 2);43 assert.equal(details.manifests.length, 3);44 assert.equal(details.root.l.reduce((n, x) => n + x.s, 0), 175);45});4647test("builds typed directory fanout nodes with bounded child metadata", async () => {48 const details = await api.buildDirectory(Array.from({length: 175}, (_, i) => ({hash: hash("a"), name: `file-${String(i).padStart(3, "0")}`, size: 1})), {returnDetails: true});49 assert.equal(details.root.t, 3);50 assert.equal(details.root.l.length, 2);51 assert.equal(JSON.stringify(details.root.l[0].m), JSON.stringify({count: 174, first: "file-000", last: "file-173"}));52 assert.equal(details.root.l[0].t, 2);53});5455test("resolves fanout children only after hash and bound verification", async () => {56 const child = {l: [{h: bytes("aa".repeat(32)), n: "a.txt", s: 1, t: 0}], t: 2};57 const childHash = await api.manifestHash(child);58 const root = {l: [{h: bytes(childHash), m: {count: 1, first: "a.txt", last: "a.txt"}, s: 1, t: 2}], t: 3};59 const resolved = await api.resolveDirectory(root, async hash => hash === childHash ? api.encodeManifest(child) : new Uint8Array());60 assert.equal(resolved.length, 1);61 await assert.rejects(api.resolveDirectory(root, async () => api.encodeManifest({...child, l: []})), /manifest hash mismatch/);62});6364test("rejects traversal, duplicate names, unknown types and malformed bytes", () => {65 const link = {h: bytes("aa".repeat(32)), n: "ok", s: 1, t: 0};66 assert.throws(() => api.encodeManifest({l: [{...link, n: "../secret"}], t: 2}), /invalid directory entry name/);67 assert.throws(() => api.encodeManifest({l: [link, link], t: 2}), /duplicate directory entry/);68 assert.throws(() => api.encodeManifest({l: [{...link, t: 9}], t: 2}), /unsupported link type/);69 assert.throws(() => api.decodeManifest(Uint8Array.from([0x82, 0xa1, 0x6c])), /truncated/);70 assert.throws(() => api.encodeManifest({l: [{...link, n: "b"}, {...link, n: "a"}], t: 2}), /canonical/);71});