mirror of
https://github.com/PR0M3TH3AN/bitgate.git
synced 2026-11-07 05:51:04 +00:00
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:
1 parent
015f9c3ad4
commit
7a45f1cf3e
15 files changed
+1399
-17
No files matched your search
@@ -16,9 +16,14 @@ const DAY = 24 * 60 * 60;
|
||||
/**
|
||||
* Report thresholds for a social feed.
|
||||
*
|
||||
* Categories are the NIP-56 vocabulary exactly — `nudity`, `malware`,
|
||||
* `profanity`, `illegal`, `spam`, `impersonation`, `other`. Inventing a
|
||||
* category here would be dead configuration: no standard client emits it, so
|
||||
* the threshold could never fire.
|
||||
*
|
||||
* Malware and impersonation escalate fastest because their harm is immediate
|
||||
* and specific. Broad categories like "spam" need more agreement before they
|
||||
* do anything visible, since they are the easiest to weaponize against someone
|
||||
* and specific. Broad categories like `spam` need more agreement before doing
|
||||
* anything visible, since they are the easiest to weaponize against someone
|
||||
* unpopular.
|
||||
*
|
||||
* @type {Record<string, import('./policy.js').CategoryThresholds>}
|
||||
@@ -26,9 +31,10 @@ const DAY = 24 * 60 * 60;
|
||||
const SOCIAL_REPORTS = {
|
||||
malware: { warn: 1, restrict: 1, hide: 2, interactionDeny: 1 },
|
||||
impersonation: { warn: 1, restrict: 2, hide: 4 },
|
||||
illegal: { warn: 1, restrict: 2, hide: 3 },
|
||||
nudity: { restrict: 2, requireExplicitAction: 2 },
|
||||
profanity: { downrank: 1, warn: 2, restrict: 3, hide: 6 },
|
||||
spam: { downrank: 2, restrict: 4, hide: 8 },
|
||||
harassment: { downrank: 1, warn: 2, restrict: 3, hide: 6 },
|
||||
default: { downrank: 2, warn: 4, restrict: 6 },
|
||||
};
|
||||
|
||||
@@ -97,6 +103,13 @@ export const SOCIAL_POLICY = createPolicyDefinition({
|
||||
/**
|
||||
* Report thresholds for a marketplace.
|
||||
*
|
||||
* `malware`, `illegal`, and `spam` are NIP-56 categories. `scam`,
|
||||
* `counterfeit`, and `not-as-described` are **not**: they are marketplace
|
||||
* vocabulary that only a commerce client will emit. That is a deliberate
|
||||
* trade — commerce harms have no NIP-56 equivalent — but it means reports from
|
||||
* generic Nostr clients arrive as `other` and fall through to the default
|
||||
* thresholds. Publish them as NIP-32 labels if you need cross-client meaning.
|
||||
*
|
||||
* Money changes the calculus: one credible malware report should stop a sale,
|
||||
* because the cost of being wrong runs one way. Visibility stays more generous
|
||||
* than transaction throughout — a listing can remain readable while being
|
||||
@@ -110,6 +123,8 @@ const COMMERCE_REPORTS = {
|
||||
counterfeit: { downrank: 1, warn: 2, restrict: 4, transactionReview: 3 },
|
||||
"not-as-described": { downrank: 2, warn: 4, transactionReview: 5 },
|
||||
misleading: { downrank: 2, warn: 3, restrict: 5, hide: 8 },
|
||||
illegal: { warn: 1, restrict: 2, hide: 3, transactionDeny: 2 },
|
||||
spam: { downrank: 2, restrict: 5 },
|
||||
default: { downrank: 2, warn: 4 },
|
||||
};
|
||||
|
||||
@@ -128,9 +143,15 @@ const COMMERCE_MUTES = {
|
||||
*/
|
||||
const CHECKOUT_REPORTS = {
|
||||
malware: { transactionDeny: 1 },
|
||||
illegal: { transactionDeny: 2 },
|
||||
scam: { transactionReview: 2, transactionDeny: 3 },
|
||||
counterfeit: { transactionReview: 3 },
|
||||
"not-as-described": { transactionReview: 5 },
|
||||
// Reports whose category we cannot interpret — `other`, or a vocabulary from
|
||||
// some client we have never seen — should not silently do nothing at the one
|
||||
// place money changes hands. They do not auto-deny either: an unclassified
|
||||
// complaint is not evidence of fraud. Enough of them earns a human look.
|
||||
default: { transactionReview: 4 },
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
Reference in new issue
Block a user