Files
bitvid/tests/torrent-metadata-service.test.mjs
T
thePR0M3TH3ANandClaude Opus 4.8 d23cfa6697 feat(blossom): P2P for Blossom videos via a WebTorrent-metadata companion event (#30)
Blossom servers reject a .torrent (415), so a Blossom video's magnet is webseed-only and can't bootstrap WebTorrent. This restores P2P without hosting a .torrent:

- Upload hardening: correct Content-Type (overrides empty/generic application/octet-stream), HTTP status surfaced in errors, and best-effort .torrent sidecar so a rejected sidecar no longer sinks the upload (URL-first fallback).

- Publish: when the .torrent can't be hosted, publish the piece-map as an infohash-keyed companion event (kind 30078, d=bitvid:torrent:<infohash>) and ship a ws= magnet. Gated by FEATURE_BLOSSOM_TORRENT_METADATA (default off = no trace); size-capped.

- Playback: torrentMetadataService lazily fetches the companion, VERIFIES the reconstructed infohash === the magnet's btih (untrusted content), sources the webseed from the video event, and hands the .torrent buffer to WebTorrent so it streams with zero peers. Relay reads go through the subscription manager.

Inherited by any upload-based media (shorts, audio-publish); excludes ingest/live. See docs/blossom-torrent-metadata-plan.md. Includes a Prettier reflow in css/tailwind.source.css.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-11 14:08:53 -04:00

129 lines
4.7 KiB
JavaScript

// torrentMetadataService: fetch → decode → VERIFY infohash → cache, plus the
// bencode/SHA-1 infohash computation. Builds a real .torrent in-test so the verify
// is exercised end-to-end (not circularly). See docs/blossom-torrent-metadata-plan.md.
import test from "node:test";
import assert from "node:assert/strict";
import {
TorrentMetadataService,
infoHashFromTorrent,
base64ToBytes,
} from "../js/services/torrentMetadataService.js";
import { torrentMetadataDTag } from "../js/nostrEventSchemas.js";
// --- tiny bencode encoder (test-only) ---
function bstr(s) {
const b = Buffer.from(s);
return Buffer.concat([Buffer.from(`${b.length}:`), b]);
}
function bbytes(buf) {
return Buffer.concat([Buffer.from(`${buf.length}:`), buf]);
}
function bint(n) {
return Buffer.from(`i${n}e`);
}
// A minimal, valid info dict + a full .torrent wrapping it.
const pieces = Buffer.alloc(20, 7); // one 20-byte SHA-1 placeholder
const infoDict = Buffer.concat([
Buffer.from("d"),
bstr("length"), bint(3),
bstr("name"), bstr("clip.mp4"),
bstr("piece length"), bint(16384),
bstr("pieces"), bbytes(pieces),
Buffer.from("e"),
]);
const torrent = Buffer.concat([
Buffer.from("d"),
bstr("announce"), bstr("wss://tracker.example"),
bstr("info"), infoDict,
Buffer.from("e"),
]);
async function sha1Hex(buf) {
const d = await crypto.subtle.digest("SHA-1", buf);
return Buffer.from(new Uint8Array(d)).toString("hex");
}
function companionEvent(infoHash, torrentBuf, created_at = 1000) {
return {
kind: 30078,
created_at,
tags: [["d", torrentMetadataDTag(infoHash)]],
content: JSON.stringify({ v: 1, infohash: infoHash, torrent: torrentBuf.toString("base64") }),
};
}
test("infoHashFromTorrent isolates the info dict and SHA-1s exactly those bytes", async () => {
const expected = await sha1Hex(infoDict); // hash of ONLY the info dict
const actual = await infoHashFromTorrent(new Uint8Array(torrent));
assert.equal(actual, expected, "infohash = SHA-1 of the bencoded info dict");
assert.equal(actual.length, 40);
});
test("infoHashFromTorrent returns '' for non-torrent / malformed bytes", async () => {
assert.equal(await infoHashFromTorrent(new Uint8Array([1, 2, 3])), "");
assert.equal(await infoHashFromTorrent(new Uint8Array(Buffer.from("d4:spam"))), "");
});
test("fetch returns the verified piece-map for a matching companion event", async () => {
const infoHash = await infoHashFromTorrent(new Uint8Array(torrent));
const svc = new TorrentMetadataService();
let queries = 0;
const out = await svc.fetch({
infoHash,
author: "a".repeat(64),
queryEvents: async (filter) => {
queries += 1;
assert.deepEqual(filter["#d"], [torrentMetadataDTag(infoHash)], "queried by infohash key");
assert.deepEqual(filter.authors, ["a".repeat(64)], "author-scoped");
return [companionEvent(infoHash, torrent)];
},
});
assert.ok(out, "returns a result");
assert.equal(out.infoHash, infoHash);
assert.equal(Buffer.from(out.torrentBytes).equals(torrent), true, "exact .torrent bytes");
assert.equal(queries, 1);
});
test("fetch REJECTS a companion whose bytes don't match the requested infohash", async () => {
// Ask for a different infohash than the .torrent actually reconstructs to.
const wrongHash = "1".repeat(40);
const svc = new TorrentMetadataService();
const out = await svc.fetch({
infoHash: wrongHash,
queryEvents: async () => [companionEvent(wrongHash, torrent)], // torrent hashes to something else
});
assert.equal(out, null, "infohash mismatch ⇒ rejected (tamper-resistant)");
});
test("fetch returns null when not found or malformed", async () => {
const infoHash = await infoHashFromTorrent(new Uint8Array(torrent));
const svc = new TorrentMetadataService();
assert.equal(await svc.fetch({ infoHash, queryEvents: async () => [] }), null, "not found");
assert.equal(
await svc.fetch({ infoHash, queryEvents: async () => [{ content: "not json" }] }),
null,
"malformed content",
);
assert.equal(await svc.fetch({ infoHash: "nope", queryEvents: async () => [] }), null, "bad infohash");
});
test("fetch caches the verified result (no second relay query)", async () => {
const infoHash = await infoHashFromTorrent(new Uint8Array(torrent));
const svc = new TorrentMetadataService();
let queries = 0;
const q = async () => {
queries += 1;
return [companionEvent(infoHash, torrent)];
};
await svc.fetch({ infoHash, queryEvents: q });
await svc.fetch({ infoHash, queryEvents: q });
assert.equal(queries, 1, "second fetch served from cache");
});
test("base64ToBytes round-trips", () => {
const bytes = base64ToBytes(Buffer.from([1, 2, 3, 250]).toString("base64"));
assert.deepEqual([...bytes], [1, 2, 3, 250]);
});