import { applyRedactionPattern, mergeRedactionFinding, mergeSecretFinding, redactionPatternsForPolicy, scanTextForSecrets, type ShareRedactionPattern, } from "./session-share-redaction-patterns.js"; const SHARE_ERROR_PREFIX = "[share:"; export type ShareSessionErrorCode = | "session_not_persisted" | "gh_not_installed" | "gh_not_authenticated" | "share_timeout" | "gist_create_failed" | "gist_parse_failed" | "share_secret_detected" | "share_export_failed" | "share_unknown"; export interface ShareSecretFinding { kind: string; count: number; } export interface ShareRedactionFinding extends ShareSecretFinding { replacement: string; samples: string[]; } export interface ShareSessionRedactionPolicyInput { secrets?: boolean; emails?: boolean; phones?: boolean; userPaths?: boolean; ipAddresses?: boolean; jwtAndBearer?: boolean; namesHeuristic?: boolean; skills?: boolean; } export interface ShareSessionRedactionPolicy { secrets: true; emails: boolean; phones: boolean; userPaths: boolean; ipAddresses: boolean; jwtAndBearer: boolean; namesHeuristic: boolean; skills: boolean; } export interface ShareSessionRedactionSummary { enabled: boolean; policy: ShareSessionRedactionPolicy; totalReplacements: number; findings: ShareRedactionFinding[]; } export interface ShareHtmlRedactionResult { html: string; findings: ShareRedactionFinding[]; totalReplacements: number; } export class ShareSessionError extends Error { constructor( public readonly code: ShareSessionErrorCode, message: string, public readonly findings: ShareSecretFinding[] = [], ) { super(formatShareErrorMessage(code, message)); this.name = "ShareSessionError"; } } const DEFAULT_SHARE_REDACTION_POLICY: ShareSessionRedactionPolicy = { secrets: true, emails: true, phones: true, userPaths: true, ipAddresses: true, jwtAndBearer: true, namesHeuristic: false, skills: true, }; function parseBoolEnv(name: string, fallback: boolean): boolean { const raw = process.env[name]; if (!raw) return fallback; return !/^(0|false|off|no)$/i.test(raw.trim()); } export function secretScanEnabled(): boolean { return parseBoolEnv("OPPI_SHARE_SECRET_SCAN", true); } export function autoRedactionEnabled(): boolean { return parseBoolEnv("OPPI_SHARE_AUTO_REDACT", true); } export function blockOnSecretFindings(): boolean { return parseBoolEnv("OPPI_SHARE_BLOCK_ON_SECRETS", false); } function defaultRedactionPolicyFromEnv(): ShareSessionRedactionPolicy { return { secrets: true, emails: parseBoolEnv("OPPI_SHARE_REDACT_EMAILS", DEFAULT_SHARE_REDACTION_POLICY.emails), phones: parseBoolEnv("OPPI_SHARE_REDACT_PHONES", DEFAULT_SHARE_REDACTION_POLICY.phones), userPaths: parseBoolEnv( "OPPI_SHARE_REDACT_USER_PATHS", DEFAULT_SHARE_REDACTION_POLICY.userPaths, ), ipAddresses: parseBoolEnv( "OPPI_SHARE_REDACT_IP_ADDRESSES", DEFAULT_SHARE_REDACTION_POLICY.ipAddresses, ), jwtAndBearer: parseBoolEnv( "OPPI_SHARE_REDACT_JWT_AND_BEARER", DEFAULT_SHARE_REDACTION_POLICY.jwtAndBearer, ), namesHeuristic: parseBoolEnv( "OPPI_SHARE_REDACT_NAMES_HEURISTIC", DEFAULT_SHARE_REDACTION_POLICY.namesHeuristic, ), skills: parseBoolEnv("OPPI_SHARE_REDACT_SKILLS", DEFAULT_SHARE_REDACTION_POLICY.skills), }; } export function normalizeRedactionPolicy( input: ShareSessionRedactionPolicyInput | undefined, ): ShareSessionRedactionPolicy { const fallback = defaultRedactionPolicyFromEnv(); return { secrets: true, emails: typeof input?.emails === "boolean" ? input.emails : fallback.emails, phones: typeof input?.phones === "boolean" ? input.phones : fallback.phones, userPaths: typeof input?.userPaths === "boolean" ? input.userPaths : fallback.userPaths, ipAddresses: typeof input?.ipAddresses === "boolean" ? input.ipAddresses : fallback.ipAddresses, jwtAndBearer: typeof input?.jwtAndBearer === "boolean" ? input.jwtAndBearer : fallback.jwtAndBearer, namesHeuristic: typeof input?.namesHeuristic === "boolean" ? input.namesHeuristic : fallback.namesHeuristic, skills: typeof input?.skills === "boolean" ? input.skills : fallback.skills, }; } export function formatShareErrorMessage(code: ShareSessionErrorCode, message: string): string { const cleaned = message.replace(/\s+/g, " ").trim(); return `${SHARE_ERROR_PREFIX}${code}] ${cleaned}`; } export function shareError(code: ShareSessionErrorCode, message: string): ShareSessionError { return new ShareSessionError(code, message); } export function summarizeFindings(findings: ShareSecretFinding[]): string { return findings.map((finding) => `${finding.kind}×${finding.count}`).join(", "); } function visitStringValues(value: unknown, visitor: (text: string) => void): void { if (typeof value === "string") { visitor(value); return; } if (Array.isArray(value)) { for (const item of value) { visitStringValues(item, visitor); } return; } if (!value || typeof value !== "object") { return; } for (const item of Object.values(value)) { visitStringValues(item, visitor); } } const EMBEDDED_SESSION_DATA_SCRIPT_REGEX = /(