feat(nostr): outbox-model fetching, private mutes, contacts, NIP-56 vocabulary

Closes the four spec gaps from the audit, verified against the NIPs repo.

NIP-65 (kind 10002) — the one that was actually breaking things. Mute lists are
published by each contact to their own write relays, so querying a fixed relay
set finds only the subset who happen to write where this deployment reads. The
trust graph was silently under-fed and counts came out low, which looks like
"nobody muted them" rather than like a bug. Adds a relay-list codec, per-author
grouping with a fallback for accounts that have published no list, and
on-demand relay connections in the transport so an outbox query can reach
relays that were never configured.

NIP-51 private entries — the viewer's own private mutes are now decrypted
through their signer (NIP-44) and applied as viewer mutes. Someone who mutes
privately previously got no effect at all. The privacy boundary is asymmetric
and enforced at the codec: another account's private list is never decoded,
and the check lives there rather than in the caller's discipline.

NIP-02 (kind 3) — the runtime can build the trust graph itself, so
<bitgate-provider> no longer needs a host to supply a follow list. Set
trust="manual" to opt out.

NIP-56 vocabulary — SOCIAL_POLICY declared thresholds for "harassment", which
is not a NIP-56 type, so no standard client could ever trigger it. Replaced
with the defined vocabulary and asserted in tests. Commerce keeps its
marketplace-specific categories, now documented as deliberate, with defaults so
unclassified reports are never silently dropped.

Two bugs found by the new tests:
- checkout had no default threshold, so a flood of "other" reports did nothing
  at the one place money changes hands. Now earns review, never auto-denial.
- describe() spread diagnostics after a derived field, so a stale zero shadowed
  the live relay-list count.

Adds docs/nips.md recording coverage, deliberate choices, and remaining gaps —
NIP-32 labelling being the largest.

811 tests.
This commit is contained in:
thePR0M3TH3AN committed 2026-08-09 20:52:21 -04:00
1 parent 015f9c3ad4
commit 7a45f1cf3e
15 files changed
+1399 -17

No files matched your search

+31
View File
@@ -198,6 +198,37 @@ describe("admin-only preset", () => {
});
});
describe("category vocabulary", () => {
// NIP-56 defines exactly these. A threshold for anything else can only fire
// if a bespoke client emits it.
const NIP56 = ["nudity", "malware", "profanity", "illegal", "spam", "impersonation", "other"];
it("uses only NIP-56 categories in the social preset", () => {
for (const category of Object.keys(SOCIAL_POLICY.profiles.feed.reports ?? {})) {
if (category === "default") continue;
expect(NIP56, `social preset declares "${category}"`).toContain(category);
}
});
it("keeps commerce extensions alongside the standard ones", () => {
const categories = Object.keys(COMMERCE_POLICY.profiles.browse.reports ?? {});
expect(categories).toContain("malware");
expect(categories).toContain("illegal");
// Marketplace-specific vocabulary is deliberate; it is documented as such.
expect(categories).toContain("scam");
});
it("gives every preset a default so unknown categories still land somewhere", () => {
for (const [name, policy] of Object.entries(POLICY_PRESETS)) {
for (const [profileName, profile] of Object.entries(policy.profiles)) {
const reports = profile.reports ?? {};
if (Object.keys(reports).length === 0) continue;
expect(reports.default, `${name}/${profileName}`).toBeDefined();
}
}
});
});
describe("preset hygiene", () => {
it("gives every preset a version and a named default profile", () => {
for (const [name, policy] of Object.entries(POLICY_PRESETS)) {