diff --git a/src/signals/extension-contributor-context.ts b/src/signals/extension-contributor-context.ts index 53e7e3a1fd..bf2995e651 100644 --- a/src/signals/extension-contributor-context.ts +++ b/src/signals/extension-contributor-context.ts @@ -1,4 +1,5 @@ import type { ContributorOpportunity, PublicReadinessScore } from "./engine"; +import { PUBLIC_UNSAFE_TERMS } from "./redaction"; // ─── Contributor-context payloads for the browser extension (#556) ─────────────────────────────── // The contributor (miner) side of the extension overlay. Every payload here is PUBLIC-SAFE and self- @@ -20,9 +21,13 @@ export function contributorReadinessBand(total: number): ContributorReadinessBan // Defense-in-depth public-safe redaction for any free-form text that reaches the contributor overlay. // The upstream builders are already contributor-facing, but every string is re-checked here and any // forbidden private term (reward/wallet/key material/raw trust score/etc.) is redacted rather than -// leaked. Kept local (no import) so this module stays cycle-free and the API never 500s on a stray term. -const FORBIDDEN_EXTENSION_TERMS = - /\b(?:rewards?|payouts?|farming|wallets?|hotkeys?|coldkeys?|seed[-\s]?phrases?|mnemonics?|private[-\s]?keys?|raw[-\s]?trust(?:[-\s]?scores?)?|trust[-\s]?scores?|score[-\s]?(?:estimate|preview|prediction)s?|estimated[-\s]?scores?|scoreability|private[-\s]?reviewability|reviewability[-\s]?internals?|private[-\s]?rankings?)\b/gi; +// leaked. Composed from the canonical `PUBLIC_UNSAFE_TERMS` alternation (redaction.ts) plus a couple of +// extension-only terms that aren't part of that shared vocabulary, so this list can't silently drift +// from the canonical public/private boundary again (#5840). Only the plain string constant is imported +// (not the whole redaction module), and redaction.ts has no imports of its own, so this stays cycle-free. +const FORBIDDEN_EXTENSION_ONLY_TERMS = String.raw`seed[-\s]?phrases?|private[-\s]?keys?`; + +const FORBIDDEN_EXTENSION_TERMS = new RegExp(String.raw`\b(?:${PUBLIC_UNSAFE_TERMS}|${FORBIDDEN_EXTENSION_ONLY_TERMS})\b`, "gi"); export function redactExtensionText(text: string): string { return text.replace(FORBIDDEN_EXTENSION_TERMS, "[redacted]").replace(/\s+/g, " ").trim(); diff --git a/test/unit/extension-contributor-context.test.ts b/test/unit/extension-contributor-context.test.ts index 08b8053f27..195402f29e 100644 --- a/test/unit/extension-contributor-context.test.ts +++ b/test/unit/extension-contributor-context.test.ts @@ -6,9 +6,11 @@ import { contributorReadinessBand, redactExtensionText, } from "../../src/signals/extension-contributor-context"; +import { isPublicSafeText } from "../../src/signals/redaction"; import type { ContributorOpportunity, PublicReadinessScore } from "../../src/signals/engine"; -const FORBIDDEN_PUBLIC_TERMS = /wallet|hotkey|coldkey|mnemonic|reward|payout|farming|raw trust|trust score|scoreability|reviewability internals|private ranking/i; +const FORBIDDEN_PUBLIC_TERMS = + /wallet|hotkey|coldkey|mnemonic|reward|payout|farming|raw trust|trust score|scoreability|reviewability|cohort|ranking|miner-originated|human-originated/i; function opportunity(over: Partial = {}): ContributorOpportunity { return { @@ -59,6 +61,52 @@ describe("redactExtensionText", () => { it("leaves safe text untouched", () => { expect(redactExtensionText("Maintainer-created issue, good fit.")).toBe("Maintainer-created issue, good fit."); }); + + it("redacts a bare cohort reference (#5840)", () => { + expect(redactExtensionText("Cohort diagnostics flagged this PR")).toBe("[redacted] diagnostics flagged this PR"); + }); + + it("redacts a bare ranking reference (#5840)", () => { + expect(redactExtensionText("Your ranking dropped this week")).toBe("Your [redacted] dropped this week"); + }); + + it("redacts miner-originated and human-originated (#5840)", () => { + expect(redactExtensionText("This looks miner-originated, not human-originated")).toBe( + "This looks [redacted], not [redacted]", + ); + }); + + it("redacts a standalone reviewability reference, not just the compound phrases (#5840)", () => { + expect(redactExtensionText("Reviewability is limited right now")).toBe("[redacted] is limited right now"); + }); +}); + +describe("FORBIDDEN_EXTENSION_TERMS drift guard (#5840)", () => { + it("redacts a representative sample of every canonical PUBLIC_UNSAFE_TERMS entry, so this module can't silently drift from the shared public/private boundary again", () => { + const samples = [ + "Your reward is pending.", + "Your score is pending.", + "Check your wallet balance.", + "Rotate your hotkey.", + "Rotate your coldkey.", + "Keep the mnemonic safe.", + "The payout is scheduled.", + "Your ranking dropped this week.", + "Cohort diagnostics flagged this PR.", + "This looks miner-originated, not human-originated.", + "We detected farming behavior.", + "This uses raw trust internally.", + "The trust score moved.", + "This is private reviewability data.", + "Reviewability is limited right now.", + ]; + + for (const sample of samples) { + // Sanity check: confirm the sample really is flagged unsafe by the canonical pattern. + expect(isPublicSafeText(sample)).toBe(false); + expect(isPublicSafeText(redactExtensionText(sample))).toBe(true); + } + }); }); describe("buildExtensionIssueFit", () => {