Files
thePR0M3TH3AN 5e0fffb32e feat(nostr): implement Phase 3 codecs
Event formats live in codecs so the core never learns which kind stores roles,
which d identifier is used, or how a legacy list is shaped.

- bech32/NIP-19 decoding, verified against the NIP-19 test vector, so the core
  can stay hex-only and dependency-free
- canonical v1 codec on kind:30078 with namespaced d identifiers, role rosters
  carrying both role labels and explicit capabilities, and size-bounded policy
  documents that return null rather than throwing on malformed JSON
- NIP-56 reports, resolving the report type across the three tag layouts
  clients use and excluding relay hints from the type position
- NIP-51 mute lists; encrypted entries are flagged, never decoded
- legacy admin-list codec supporting all five migration stages, with editors
  surfaced separately so a UI permission cannot silently become signing authority
- replaceable selection with a deterministic same-timestamp tiebreak, without
  which two relays returning events in different orders would produce different
  state on different clients

129 codec tests; 402 total.
2026-08-09 17:39:37 -04:00

127 lines
4.1 KiB
JavaScript

import { describe, expect, it } from "vitest";
import {
coordinateOf,
getTagValue,
getTags,
selectLatest,
selectReplaceable,
verifyEvents,
} from "../src/replaceable.js";
const PUBKEY = "a1".repeat(32);
/**
* @param {Partial<import('../src/replaceable.js').NostrEvent>} parts
* @returns {import('../src/replaceable.js').NostrEvent}
*/
const event = (parts) => ({
id: "00".repeat(32),
pubkey: PUBKEY,
kind: 30078,
created_at: 1000,
tags: [],
content: "",
...parts,
});
describe("tag helpers", () => {
it("reads the first value of a tag", () => {
expect(getTagValue(event({ tags: [["d", "x"], ["d", "y"]] }), "d")).toBe("x");
});
it("returns empty for a missing tag", () => {
expect(getTagValue(event({}), "d")).toBe("");
});
it("ignores malformed tags", () => {
expect(getTagValue(event({ tags: [/** @type {any} */ ("d"), ["d", "ok"]] }), "d")).toBe("ok");
});
it("collects repeated tags", () => {
expect(getTags(event({ tags: [["p", "a"], ["p", "b"], ["e", "c"]] }), "p")).toHaveLength(2);
});
});
describe("coordinateOf", () => {
it("builds kind:pubkey:d", () => {
expect(coordinateOf(event({ tags: [["d", "list"]] }))).toBe(`30078:${PUBKEY}:list`);
});
it("handles a missing d tag", () => {
expect(coordinateOf(event({}))).toBe(`30078:${PUBKEY}:`);
});
});
describe("selectReplaceable", () => {
it("keeps the newest event per coordinate", () => {
const older = event({ id: "aa".repeat(32), created_at: 100, tags: [["d", "list"]] });
const newer = event({ id: "bb".repeat(32), created_at: 200, tags: [["d", "list"]] });
const winners = selectReplaceable([older, newer]);
expect(winners.get(`30078:${PUBKEY}:list`)?.id).toBe(newer.id);
});
it("is order-independent", () => {
const older = event({ id: "aa".repeat(32), created_at: 100, tags: [["d", "list"]] });
const newer = event({ id: "bb".repeat(32), created_at: 200, tags: [["d", "list"]] });
expect(selectReplaceable([older, newer]).get(`30078:${PUBKEY}:list`)?.id).toBe(
selectReplaceable([newer, older]).get(`30078:${PUBKEY}:list`)?.id,
);
});
it("breaks same-timestamp ties deterministically", () => {
const a = event({ id: "aa".repeat(32), created_at: 100, tags: [["d", "list"]] });
const b = event({ id: "bb".repeat(32), created_at: 100, tags: [["d", "list"]] });
expect(selectReplaceable([a, b]).get(`30078:${PUBKEY}:list`)?.id).toBe(a.id);
expect(selectReplaceable([b, a]).get(`30078:${PUBKEY}:list`)?.id).toBe(a.id);
});
it("keeps distinct coordinates apart", () => {
const first = event({ tags: [["d", "one"]] });
const second = event({ tags: [["d", "two"]] });
expect(selectReplaceable([first, second]).size).toBe(2);
});
it("ignores malformed events", () => {
expect(selectReplaceable([/** @type {any} */ (null), /** @type {any} */ ({})]).size).toBe(0);
});
});
describe("selectLatest", () => {
it("returns the newest event", () => {
const older = event({ id: "aa".repeat(32), created_at: 1 });
const newer = event({ id: "bb".repeat(32), created_at: 2 });
expect(selectLatest([older, newer])?.id).toBe(newer.id);
});
it("returns null for an empty list", () => {
expect(selectLatest([])).toBeNull();
});
});
describe("verifyEvents", () => {
it("passes everything through when no verifier is supplied", async () => {
const events = [event({}), event({})];
expect(await verifyEvents(events)).toHaveLength(2);
});
it("drops events the verifier rejects", async () => {
const keep = event({ id: "aa".repeat(32) });
const drop = event({ id: "bb".repeat(32) });
const verified = await verifyEvents([keep, drop], (candidate) => candidate.id === keep.id);
expect(verified).toEqual([keep]);
});
it("treats a throwing verifier as a rejection", async () => {
const verified = await verifyEvents([event({})], () => {
throw new Error("boom");
});
expect(verified).toEqual([]);
});
it("supports async verifiers", async () => {
const verified = await verifyEvents([event({})], async () => true);
expect(verified).toHaveLength(1);
});
});