From 1fe01e421b9c15771d9275111f1cbf70685e2a98 Mon Sep 17 00:00:00 2001 From: kai392 Date: Thu, 30 Jul 2026 06:50:26 +0800 Subject: [PATCH] fix(orb): one shared token-redaction pattern across the five public surfaces (#9697) Four hand-written token-redaction patterns guarded public-facing text in src/services/**, and every one missed something another caught. The worst gap: three surfaces (miner-dashboard-recommendations, control-panel-roles, weekly-value-report) matched only `ghp_`, so `ghs_` -- the GitHub App INSTALLATION token this Worker mints on every pass -- passed through a public dashboard string, role summary, and weekly report verbatim. The two path surfaces (score-breakdown, agent-action-explanation-card) matched the correct `gh[pousr]_` GitHub class but not the Orb/GitLab/Slack prefixes the others caught. - src/signals/redaction.ts: add PUBLIC_TOKEN_INLINE (the alternation source of every prefix -- `gh[pousr]_`, github_pat_, gts_, orbenr_, orbsec_, glpat-, sk-, xox[baprs]-) and publicTokenPattern() returning a FRESH /g RegExp each call, the credential analogue of the existing PUBLIC_LOCAL_PATH_INLINE / PUBLIC_LOCAL_PATH_SCRUB_PATTERN. - All five surfaces now compose from that single source, each keeping its own replacement string and its own trailing body class / path alternation; no hand-written prefix literal remains under src/services/. Tests: a REGRESSION ghs_ case on miner-dashboard; an it.each over every prefix on control-panel-roles, weekly-value-report, score-breakdown, and a new agent-action-explanation-card test file; publicTokenPattern() fresh- object + repeated-.replace() idempotency in redaction.test. Each site also asserts a non-token string is left unmodified. Closes #9697 Co-Authored-By: Claude Opus 4.8 --- src/services/agent-action-explanation-card.ts | 13 +++-- src/services/control-panel-roles.ts | 4 +- .../miner-dashboard-recommendations.ts | 5 +- src/services/score-breakdown.ts | 11 ++++- src/services/weekly-value-report.ts | 7 ++- src/signals/redaction.ts | 19 ++++++++ .../agent-action-explanation-card.test.ts | 29 +++++++++++ test/unit/control-panel-roles.test.ts | 25 ++++++++++ .../miner-dashboard-recommendations.test.ts | 25 ++++++++++ test/unit/redaction.test.ts | 48 +++++++++++++++++++ test/unit/score-breakdown.test.ts | 25 +++++++++- test/unit/weekly-value-report.test.ts | 26 +++++++++- 12 files changed, 222 insertions(+), 15 deletions(-) create mode 100644 test/unit/agent-action-explanation-card.test.ts diff --git a/src/services/agent-action-explanation-card.ts b/src/services/agent-action-explanation-card.ts index 9230d0d1dc..28ad8fa73d 100644 --- a/src/services/agent-action-explanation-card.ts +++ b/src/services/agent-action-explanation-card.ts @@ -1,5 +1,5 @@ import type { AgentActionBlockerCategory, AgentActionExplanationCard, AgentActionRecord } from "../types"; -import { PUBLIC_LOCAL_PATH_INLINE } from "../signals/redaction"; +import { PUBLIC_LOCAL_PATH_INLINE, PUBLIC_TOKEN_INLINE } from "../signals/redaction"; type AgentActionExplanationInput = Pick< AgentActionRecord, @@ -10,9 +10,11 @@ const BLOCKER_CATEGORY_ORDER: AgentActionBlockerCategory[] = ["branch", "account const PUBLIC_FORBIDDEN_PATTERN = /\b(wallets?|hotkeys?|coldkeys?|seed phrases?|mnemonics?|private keys?|raw[-_\s]?trust scores?|trust scores?|private reviewability|reviewability internals?|private scoreability|scoreability|projected scores?|score(?:d|s|ability)?|public score estimates?|estimated scores?|score estimates?|score previews?|reward estimates?|payouts?|farming|reward optimization|private rankings?)\b/gi; const PUBLIC_SCORE_DELTA_PATTERN = /\b(?:projected\s+)?score\w*(?:\s+\w+){0,4}\s+[-+]?\d+(?:\.\d+)?\s*->\s*[-+]?\d+(?:\.\d+)?\b/gi; -// Token alternatives stay local; the local-path alternatives compose from the canonical PUBLIC_LOCAL_PATH_INLINE -// in redaction.ts (adds the previously-missed /root/ and /var/, plus the forward-slash Windows form C:/Users/). -const TOKEN_OR_PATH_PATTERN = new RegExp(`\\bgithub_pat_[A-Za-z0-9_]+|\\bgh[pousr]_[A-Za-z0-9_]+|(?:${PUBLIC_LOCAL_PATH_INLINE})\\S+`, "gi"); +// #9697: token alternatives compose from the canonical PUBLIC_TOKEN_INLINE — one shared source across all five +// public surfaces so none can drift (this surface's old hand-written list missed the +// gts_/orbenr_/orbsec_/glpat-/sk-/xox prefixes the sibling surfaces caught); the local-path alternatives compose +// from PUBLIC_LOCAL_PATH_INLINE. +const TOKEN_OR_PATH_PATTERN = new RegExp(`\\b(?:${PUBLIC_TOKEN_INLINE})[A-Za-z0-9_]+|(?:${PUBLIC_LOCAL_PATH_INLINE})\\S+`, "gi"); export function withAgentActionExplanationCard(action: AgentActionRecord): AgentActionRecord { return { ...action, explanationCard: buildAgentActionExplanationCard(action) }; @@ -119,6 +121,9 @@ function categorizeBlocker(blocker: string): AgentActionBlockerCategory { return "unknown"; } +/** Test-only handle on the surface's public-text sanitizer (mirrors control-panel-roles' internals export). */ +export const __agentActionExplanationCardInternals = { sanitizePublicCardText }; + function sanitizePublicCardText(value: string): string { return compactText(value) .replace(TOKEN_OR_PATH_PATTERN, "") diff --git a/src/services/control-panel-roles.ts b/src/services/control-panel-roles.ts index f21a9e0148..f906613fe4 100644 --- a/src/services/control-panel-roles.ts +++ b/src/services/control-panel-roles.ts @@ -2,7 +2,7 @@ import { isAuthorizedGitHubSessionLogin } from "../auth/security"; import { getFreshOfficialMinerDetection, getRepository, listAllPullRequests, listInstallations, listRepositories } from "../db/repositories"; import type { ControlPanelRoleCard, ControlPanelRoleName, ControlPanelRoleSummary, InstallationRecord, PullRequestRecord, RepositoryRecord } from "../types"; import { nowIso } from "../utils/json"; -import { PUBLIC_LOCAL_PATH_SCRUB_PATTERN } from "../signals/redaction"; +import { PUBLIC_LOCAL_PATH_SCRUB_PATTERN, publicTokenPattern } from "../signals/redaction"; export type RoleSummaryInputs = { login: string; @@ -301,7 +301,7 @@ function isMaintainerAssociation(value: string | null | undefined): boolean { export function sanitizeRoleText(value: string): string { const redacted = value .replace(PUBLIC_LOCAL_PATH_SCRUB_PATTERN, "") - .replace(/\b(?:ghp_|github_pat_|gts_|orbenr_|orbsec_|glpat-|sk-)[A-Za-z0-9_=-]{8,}/g, "") + .replace(publicTokenPattern(), "") .replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi, "Bearer "); if (/\b(seed phrase|mnemonic|private key|raw trust|trust score|wallet|hotkey|coldkey|payout|reward estimate|farming|private reviewability|public score estimate)\b/i.test(redacted)) return ""; return redacted.slice(0, 200); diff --git a/src/services/miner-dashboard-recommendations.ts b/src/services/miner-dashboard-recommendations.ts index 33b1fb5afe..d8e0dde333 100644 --- a/src/services/miner-dashboard-recommendations.ts +++ b/src/services/miner-dashboard-recommendations.ts @@ -1,6 +1,6 @@ import type { ContributorDecisionPack } from "./decision-pack"; import type { SignalSnapshotRecord } from "../types"; -import { PUBLIC_LOCAL_PATH_INLINE } from "../signals/redaction"; +import { PUBLIC_LOCAL_PATH_INLINE, publicTokenPattern } from "../signals/redaction"; export type MinerDashboardSignalGroup = "repo_state" | "contributor_state" | "validation_state" | "policy_context"; export type MinerDashboardChangeStatus = "new" | "changed" | "unchanged"; @@ -46,7 +46,6 @@ const FORBIDDEN_PUBLIC_TEXT = // Compose the roots from the canonical PUBLIC_LOCAL_PATH_INLINE in redaction.ts (so this surface cannot drift) // while preserving this surface's own trailing class and its case-sensitive `/g` (Windows form via `[A-Z]`). const LOCAL_PATH = new RegExp(`(?:${PUBLIC_LOCAL_PATH_INLINE})[^\\s,;:)]+`, "g"); -const FORBIDDEN_TOKEN = /\b(?:ghp_|github_pat_|gts_|orbenr_|orbsec_|glpat-|sk-)[A-Za-z0-9_=-]{8,}/g; export function previousDecisionPackFromSnapshots(currentPack: ContributorDecisionPack, snapshots: SignalSnapshotRecord[]): ContributorDecisionPack | undefined { const current = asRecord(currentPack); @@ -386,7 +385,7 @@ function numberValue(record: DashboardRecord | undefined, key: string): number | function sanitizePublicText(value: string): string { return value .replace(LOCAL_PATH, "[local path]") - .replace(FORBIDDEN_TOKEN, "private context") + .replace(publicTokenPattern(), "private context") .replace(FORBIDDEN_PUBLIC_TEXT, "private context") .replace(/\s+/g, " ") .trim(); diff --git a/src/services/score-breakdown.ts b/src/services/score-breakdown.ts index 7c59eeec09..cc29099eb0 100644 --- a/src/services/score-breakdown.ts +++ b/src/services/score-breakdown.ts @@ -1,4 +1,4 @@ -import { PUBLIC_LOCAL_PATH_INLINE } from "../signals/redaction"; +import { PUBLIC_LOCAL_PATH_INLINE, PUBLIC_TOKEN_INLINE } from "../signals/redaction"; import type { ScoreGateDelta, ScorePreviewResult } from "../scoring/preview"; // This endpoint (POST /v1/scoring/explain-breakdown, gated by requireContributorAccess) is authenticated and @@ -12,12 +12,19 @@ import type { ScoreGateDelta, ScorePreviewResult } from "../scoring/preview"; // computed, structured score data (numbers and gate deltas), so the only genuine residual risk is an // accidentally-embedded token or local filesystem path -- keep just that minimal safety net rather than the // full gittensor-economic-vocabulary substitution. -const TOKEN_OR_PATH_PATTERN = new RegExp(`\\bgithub_pat_[A-Za-z0-9_]+|\\bgh[pousr]_[A-Za-z0-9_]+|(?:${PUBLIC_LOCAL_PATH_INLINE})\\S+`, "gi"); +// #9697: token alternatives compose from the canonical PUBLIC_TOKEN_INLINE — one shared source across all five +// public surfaces, so none can drift and miss a prefix (this surface's old hand-written list also missed the +// gts_/orbenr_/orbsec_/glpat-/sk-/xox prefixes the sibling surfaces caught); the local-path alternatives compose +// from PUBLIC_LOCAL_PATH_INLINE. Module-local /g const is safe here — .replace resets lastIndex after each call. +const TOKEN_OR_PATH_PATTERN = new RegExp(`\\b(?:${PUBLIC_TOKEN_INLINE})[A-Za-z0-9_]+|(?:${PUBLIC_LOCAL_PATH_INLINE})\\S+`, "gi"); function sanitizeScoreBreakdownText(value: string): string { return value.replace(TOKEN_OR_PATH_PATTERN, ""); } +/** Test-only handle on the surface's token/path scrubber (mirrors control-panel-roles' internals export). */ +export const __scoreBreakdownInternals = { sanitizeScoreBreakdownText }; + export type ScoreMultiplierBand = "full" | "reduced" | "neutral" | "blocked"; export type ScoreMultiplierBreakdown = { diff --git a/src/services/weekly-value-report.ts b/src/services/weekly-value-report.ts index c65dd3d5cd..0585b35076 100644 --- a/src/services/weekly-value-report.ts +++ b/src/services/weekly-value-report.ts @@ -28,7 +28,7 @@ import type { WeeklyValueReportVariant, } from "../types"; import { nowIso } from "../utils/json"; -import { PUBLIC_LOCAL_PATH_SCRUB_PATTERN } from "../signals/redaction"; +import { PUBLIC_LOCAL_PATH_SCRUB_PATTERN, publicTokenPattern } from "../signals/redaction"; type WeeklyValueReportInputs = { generatedAt: string; @@ -411,7 +411,7 @@ function normalizeReportDays(value: number | null | undefined): number { function sanitizeReportText(value: string): string { const redacted = value .replace(PUBLIC_LOCAL_PATH_SCRUB_PATTERN, "") - .replace(/\b(?:ghp_|github_pat_|gts_|orbenr_|orbsec_|glpat-|sk-|xox[baprs]-)[A-Za-z0-9_=-]{8,}/g, "") + .replace(publicTokenPattern(), "") .replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi, "Bearer "); if ( /\b(seed phrase|mnemonic|private key|raw[-\s]?trust|trust[-\s]?score|wallet|hotkey|coldkey|payout|reward(?:[-\s]?(?:estimate|prediction|claim|score|payout|risk))?|farming|private[-\s]?reviewability|private[-\s]?scoreability|scoreability|public[-\s]?score[-\s]?(?:estimate|prediction|claim)|score[-\s]?(?:estimate|prediction|preview))\b/i.test( @@ -421,3 +421,6 @@ function sanitizeReportText(value: string): string { return ""; return redacted.slice(0, 240); } + +/** Test-only handle on the report text sanitizer (mirrors control-panel-roles' internals export). */ +export const __weeklyValueReportInternals = { sanitizeReportText }; diff --git a/src/signals/redaction.ts b/src/signals/redaction.ts index db945898a2..ce73e7db5a 100644 --- a/src/signals/redaction.ts +++ b/src/signals/redaction.ts @@ -44,6 +44,25 @@ export const PUBLIC_LOCAL_PATH_PREFIX_PATTERN = new RegExp(String.raw`^(?:${PUBL export const PUBLIC_UNSAFE_PATTERN = new RegExp(String.raw`\b(${PUBLIC_UNSAFE_TERMS})\b|${PUBLIC_LOCAL_PATH_INLINE}`, "i"); +// #9697: the canonical PUBLIC token-prefix vocabulary (alternation source only — no flags, no `\b` anchors, no +// trailing token-body class), the credential analogue of `PUBLIC_UNSAFE_TERMS` / `PUBLIC_LOCAL_PATH_INLINE`. +// Every public surface that scrubs a leaked credential composes from this one source, so a surface cannot drift +// and miss a prefix — the concrete gap that motivated this: `ghs_`, the GitHub App INSTALLATION token this +// Worker mints on every pass, was passing through three surfaces that only matched `ghp_`. `gh[pousr]_` is the +// correct GitHub class (the same one `src/review/secret-patterns.ts` uses), covering ghp_/gho_/ghu_/ghs_/ghr_ +// at once. Adding a new provider's prefix here updates every surface at once. +export const PUBLIC_TOKEN_INLINE = String.raw`gh[pousr]_|github_pat_|gts_|orbenr_|orbsec_|glpat-|sk-|xox[baprs]-`; + +/** + * A FRESH `/g` token scrubber for `.replace()` surfaces that swap a leaked credential for a placeholder: a prefix + * from `PUBLIC_TOKEN_INLINE` plus its opaque token body (`[A-Za-z0-9_=-]{8,}`, anchored on a word boundary). + * Returns a NEW `RegExp` on every call so no module-level `/g` object (and its `lastIndex`) is ever shared across + * call sites — a shared stateful global would carry `lastIndex` between surfaces and skip matches. + */ +export function publicTokenPattern(): RegExp { + return new RegExp(String.raw`\b(?:${PUBLIC_TOKEN_INLINE})[A-Za-z0-9_=-]{8,}`, "g"); +} + /** True iff `text` contains nothing that must stay private — i.e. it is safe to surface on a public GitHub surface. */ export function isPublicSafeText(text: string): boolean { return !PUBLIC_UNSAFE_PATTERN.test(text); diff --git a/test/unit/agent-action-explanation-card.test.ts b/test/unit/agent-action-explanation-card.test.ts new file mode 100644 index 0000000000..6cbf925463 --- /dev/null +++ b/test/unit/agent-action-explanation-card.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; +import { __agentActionExplanationCardInternals } from "../../src/services/agent-action-explanation-card"; + +const { sanitizePublicCardText } = __agentActionExplanationCardInternals; + +describe("agent-action explanation-card public-text sanitizer — token redaction (#9697)", () => { + it.each([ + ["ghp_", `ghp_${"A".repeat(24)}`], + ["gho_", `gho_${"A".repeat(24)}`], + ["ghu_", `ghu_${"A".repeat(24)}`], + ["ghs_", `ghs_${"A".repeat(24)}`], + ["ghr_", `ghr_${"A".repeat(24)}`], + ["github_pat_", `github_pat_${"A".repeat(24)}`], + ["gts_", `gts_${"A".repeat(24)}`], + ["orbenr_", `orbenr_${"A".repeat(24)}`], + ["orbsec_", `orbsec_${"A".repeat(24)}`], + ["glpat-", `glpat-${"A".repeat(24)}`], + ["sk-", `sk-${"A".repeat(24)}`], + ["xoxb-", `xoxb-${"A".repeat(24)}`], + ])("REGRESSION (#9697): redacts a %s token (unified PUBLIC_TOKEN_INLINE)", (_prefix, token) => { + const out = sanitizePublicCardText(`blocked until ${token} rotates`); + expect(out).toContain(""); + expect(out).not.toContain(token); + }); + + it("leaves non-token card text unmodified by the token scrubber (#9697)", () => { + expect(sanitizePublicCardText("waiting on branch to be ready")).toBe("waiting on branch to be ready"); + }); +}); diff --git a/test/unit/control-panel-roles.test.ts b/test/unit/control-panel-roles.test.ts index cf8ea3e6ff..5a09f26493 100644 --- a/test/unit/control-panel-roles.test.ts +++ b/test/unit/control-panel-roles.test.ts @@ -80,6 +80,31 @@ describe("control panel role summaries", () => { expect(sanitizeRoleText("see C:\\Users\\me\\repo")).toBe("see "); }); + it.each([ + ["ghp_", `ghp_${"A".repeat(24)}`], + ["gho_", `gho_${"A".repeat(24)}`], + ["ghu_", `ghu_${"A".repeat(24)}`], + ["ghs_", `ghs_${"A".repeat(24)}`], + ["ghr_", `ghr_${"A".repeat(24)}`], + ["github_pat_", `github_pat_${"A".repeat(24)}`], + ["gts_", `gts_${"A".repeat(24)}`], + ["orbenr_", `orbenr_${"A".repeat(24)}`], + ["orbsec_", `orbsec_${"A".repeat(24)}`], + ["glpat-", `glpat-${"A".repeat(24)}`], + ["sk-", `sk-${"A".repeat(24)}`], + ["xoxb-", `xoxb-${"A".repeat(24)}`], + ])("REGRESSION (#9697): sanitizeRoleText redacts a %s token (unified PUBLIC_TOKEN_INLINE)", (_prefix, token) => { + const { sanitizeRoleText } = __controlPanelRolesInternals; + const out = sanitizeRoleText(`role evidence ${token} note`); + expect(out).toContain(""); + expect(out).not.toContain(token); + }); + + it("leaves non-token role evidence text unmodified (#9697)", () => { + const { sanitizeRoleText } = __controlPanelRolesInternals; + expect(sanitizeRoleText("collaborator on JSONbored/loopover since May")).toBe("collaborator on JSONbored/loopover since May"); + }); + it("recognizes account installations even before an owned repo is cached", () => { const summary = buildControlPanelRoleSummary({ login: "repo-owner", diff --git a/test/unit/miner-dashboard-recommendations.test.ts b/test/unit/miner-dashboard-recommendations.test.ts index b9069218f7..daa6ffb300 100644 --- a/test/unit/miner-dashboard-recommendations.test.ts +++ b/test/unit/miner-dashboard-recommendations.test.ts @@ -367,6 +367,31 @@ describe("miner dashboard recommendation metadata", () => { expect(JSON.stringify(enriched?.rerunReasons)).not.toContain(fakeSecret); }); + // REGRESSION (#9697): ghs_ is the GitHub App INSTALLATION token this Worker mints on every pass. The old + // hand-written list matched only ghp_, so this exact prefix passed through verbatim to a public dashboard + // string. The unified PUBLIC_TOKEN_INLINE's `gh[pousr]_` class closes that gap. + it("REGRESSION (#9697): redacts a ghs_ installation token from rerun reasons", () => { + const installationToken = `ghs_${"a".repeat(24)}`; + const current = decisionPack({ + generatedAt: "2026-06-02T00:00:00.000Z", + topActions: [action()], + actionPortfolio: { + topActions: [ + { + repoFullName: "JSONbored/loopover", + actionKind: "open_new_direct_pr", + rerunWhen: `Rerun once the installation token ${installationToken} has rotated.`, + }, + ], + }, + }); + + const [enriched] = buildMinerDashboardNextActions(current); + const repoStateReasons = enriched?.rerunReasons.find((group) => group.group === "repo_state")?.reasons.join(" ") ?? ""; + expect(repoStateReasons).toContain("private context"); + expect(JSON.stringify(enriched?.rerunReasons)).not.toContain(installationToken); + }); + it("selects the previous ready decision-pack snapshot", () => { const current = decisionPack({ generatedAt: "2026-06-02T00:00:00.000Z" }); const previous = decisionPack({ generatedAt: "2026-06-01T00:00:00.000Z" }); diff --git a/test/unit/redaction.test.ts b/test/unit/redaction.test.ts index 43a6cef8cd..4e523e45f8 100644 --- a/test/unit/redaction.test.ts +++ b/test/unit/redaction.test.ts @@ -4,9 +4,57 @@ import { PUBLIC_LOCAL_PATH_INLINE, PUBLIC_LOCAL_PATH_PREFIX_PATTERN, PUBLIC_LOCAL_PATH_SCRUB_PATTERN, + PUBLIC_TOKEN_INLINE, PUBLIC_UNSAFE_PATTERN, + publicTokenPattern, } from "../../src/signals/redaction"; +// Every prefix in PUBLIC_TOKEN_INLINE, as a concrete leaked-token sample (body is pure alphanumerics so it is +// fully consumed by both the `[A-Za-z0-9_=-]{8,}` and `[A-Za-z0-9_]+` body classes the call sites use). +const PUBLIC_TOKEN_SAMPLES: [string, string][] = [ + ["ghp_", `ghp_${"A".repeat(24)}`], + ["gho_", `gho_${"A".repeat(24)}`], + ["ghu_", `ghu_${"A".repeat(24)}`], + ["ghs_", `ghs_${"A".repeat(24)}`], + ["ghr_", `ghr_${"A".repeat(24)}`], + ["github_pat_", `github_pat_${"A".repeat(24)}`], + ["gts_", `gts_${"A".repeat(24)}`], + ["orbenr_", `orbenr_${"A".repeat(24)}`], + ["orbsec_", `orbsec_${"A".repeat(24)}`], + ["glpat-", `glpat-${"A".repeat(24)}`], + ["sk-", `sk-${"A".repeat(24)}`], + ["xoxb-", `xoxb-${"A".repeat(24)}`], +]; + +describe("publicTokenPattern / PUBLIC_TOKEN_INLINE (#9697)", () => { + it("returns a FRESH /g RegExp on each call, never a shared stateful object", () => { + const a = publicTokenPattern(); + const b = publicTokenPattern(); + expect(a).not.toBe(b); + expect(a.global).toBe(true); + }); + + it("stays idempotent across repeated .replace() on the same input (no shared lastIndex carry-over)", () => { + const input = `leak ghs_${"A".repeat(24)} and gho_${"B".repeat(24)} here`; + const first = input.replace(publicTokenPattern(), ""); + const second = input.replace(publicTokenPattern(), ""); + expect(first).toBe(second); + expect(first).toContain(""); + expect(first).not.toMatch(/gh[so]_/); + }); + + it.each(PUBLIC_TOKEN_SAMPLES)("redacts a %s token via publicTokenPattern()", (_prefix, token) => { + const out = `x ${token} y`.replace(publicTokenPattern(), ""); + expect(out).toBe("x y"); + expect(out).not.toContain(token); + }); + + it("leaves a non-token string untouched", () => { + expect("plain reviewable summary text".replace(publicTokenPattern(), "")).toBe("plain reviewable summary text"); + expect(PUBLIC_TOKEN_INLINE).toContain("gh[pousr]_"); // ghs_/gho_/ghu_/ghr_ covered by the GitHub class + }); +}); + describe("isPublicSafeText (#542 shared public/private boundary)", () => { it("accepts text with no private signals", () => { expect(isPublicSafeText("Add a retry to the cache reconnect path.")).toBe(true); diff --git a/test/unit/score-breakdown.test.ts b/test/unit/score-breakdown.test.ts index 92a60ce838..d4cd034c42 100644 --- a/test/unit/score-breakdown.test.ts +++ b/test/unit/score-breakdown.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import { buildScorePreview } from "../../src/scoring/preview"; -import { explainScoreBreakdown } from "../../src/services/score-breakdown"; +import { __scoreBreakdownInternals, explainScoreBreakdown } from "../../src/services/score-breakdown"; import type { RepositoryRecord, ScoringModelSnapshotRecord } from "../../src/types"; const FORBIDDEN = /\b(wallet|hotkey|coldkey|mnemonic|farming|payout|raw[-_\s]?trust)\b/i; @@ -737,6 +737,29 @@ describe("explainScoreBreakdown", () => { // "score"/"credibility" into "private context" -- fine for a genuinely public GitHub comment, but this // endpoint is authenticated and per-contributor, and "score"/"credibility" are its entire point. Asserts // the feature's own core vocabulary survives byte-for-byte, not just "no forbidden wallet/hotkey word leaked". + it.each([ + ["ghp_", `ghp_${"A".repeat(24)}`], + ["gho_", `gho_${"A".repeat(24)}`], + ["ghu_", `ghu_${"A".repeat(24)}`], + ["ghs_", `ghs_${"A".repeat(24)}`], + ["ghr_", `ghr_${"A".repeat(24)}`], + ["github_pat_", `github_pat_${"A".repeat(24)}`], + ["gts_", `gts_${"A".repeat(24)}`], + ["orbenr_", `orbenr_${"A".repeat(24)}`], + ["orbsec_", `orbsec_${"A".repeat(24)}`], + ["glpat-", `glpat-${"A".repeat(24)}`], + ["sk-", `sk-${"A".repeat(24)}`], + ["xoxb-", `xoxb-${"A".repeat(24)}`], + ])("REGRESSION (#9697): sanitizeScoreBreakdownText redacts a %s token (unified PUBLIC_TOKEN_INLINE)", (_prefix, token) => { + const out = __scoreBreakdownInternals.sanitizeScoreBreakdownText(`saturated near ${token} cap`); + expect(out).toContain(""); + expect(out).not.toContain(token); + }); + + it("leaves this feature's own computed score text unmodified by the token scrubber (#9697)", () => { + expect(__scoreBreakdownInternals.sanitizeScoreBreakdownText("credibility leverage saturated near the cap")).toBe("credibility leverage saturated near the cap"); + }); + it("REGRESSION: never mangles this feature's own core vocabulary (score/credibility/leverage) into private context", () => { const preview = buildScorePreview({ repo, diff --git a/test/unit/weekly-value-report.test.ts b/test/unit/weekly-value-report.test.ts index 3bfc90405d..7999e485d6 100644 --- a/test/unit/weekly-value-report.test.ts +++ b/test/unit/weekly-value-report.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { buildWeeklyValueReport, formatWeeklyValueReportMarkdown, generateWeeklyValueReport } from "../../src/services/weekly-value-report"; +import { __weeklyValueReportInternals, buildWeeklyValueReport, formatWeeklyValueReportMarkdown, generateWeeklyValueReport } from "../../src/services/weekly-value-report"; import type { InstallationHealthRecord, InstallationRecord, @@ -211,6 +211,30 @@ describe("weekly value reports", () => { expect(JSON.stringify(report)).not.toMatch(/\/root\/work|\/var\/folders/); }); + it.each([ + ["ghp_", `ghp_${"A".repeat(24)}`], + ["gho_", `gho_${"A".repeat(24)}`], + ["ghu_", `ghu_${"A".repeat(24)}`], + ["ghs_", `ghs_${"A".repeat(24)}`], + ["ghr_", `ghr_${"A".repeat(24)}`], + ["github_pat_", `github_pat_${"A".repeat(24)}`], + ["gts_", `gts_${"A".repeat(24)}`], + ["orbenr_", `orbenr_${"A".repeat(24)}`], + ["orbsec_", `orbsec_${"A".repeat(24)}`], + ["glpat-", `glpat-${"A".repeat(24)}`], + ["sk-", `sk-${"A".repeat(24)}`], + ["xoxb-", `xoxb-${"A".repeat(24)}`], + ])("REGRESSION (#9697): sanitizeReportText redacts a %s token (unified PUBLIC_TOKEN_INLINE)", (_prefix, token) => { + const { sanitizeReportText } = __weeklyValueReportInternals; + const out = sanitizeReportText(`repo ${token}`); + expect(out).toContain(""); + expect(out).not.toContain(token); + }); + + it("leaves non-token report text unmodified (#9697)", () => { + expect(__weeklyValueReportInternals.sanitizeReportText("JSONbored/loopover")).toBe("JSONbored/loopover"); + }); + it("redacts Slack bot tokens in operator rollup dimensions", () => { const slackToken = ["xoxb", "1234567890", "ABCDEFabcdef"].join("-"); const report = buildWeeklyValueReport({