tinyrelayfile
tinyrelay/scripts/blossom-manifests-review.test.mjs/

scripts/blossom-manifests-review.test.mjs raw
1import assert from "node:assert/strict";
2import {readFile} from "node:fs/promises";
3import test from "node:test";
4import vm from "node:vm";
5
6const 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));
14
15test("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});
20
21test("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});
39
40test("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});
57
58test("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});
65
66test("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});
74
75test("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});
83
84test("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});

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

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

Refs