/** * Secret redactor for persisted terminal output. * * Every byte of agent PTY output that Crewly writes to disk * (~/.crewly/logs/sessions/*.log) passes through here, and so do the * one-time scrub of existing logs and shell history files * (`crewly security scrub-logs`). Three layers, applied in this order: * * 1. **Known values** — the exact value of every secret Crewly holds for the * session (backend env + the session's spawn env, see * collectSecretEnvValues). Catches secrets no pattern recognises (a Slack * signing secret is plain hex). Masked as `[REDACTED ]`. * 2. **Secret-named assignments** — `NAME=value` where NAME is a secret name * (isSecretEnvKey: `…API_KEY`, `…TOKEN`, `…SECRET`, `…PASSWORD`, * `…PRIVATE_KEY`, … plus SECRET_REDACTION_CONSTANTS.KNOWN_SECRET_ENV_NAMES). * The value is masked whatever it looks like. Names are matched on the * whole identifier, so `TOKEN_COUNT=5` or `MAX_TOKENS=4096` are left alone * (the name does not END in a secret suffix); `$VAR` references and * already-masked values are left alone too. * 3. **Token shapes** — well-known raw credential formats wherever they * appear: Slack `xoxb-/xoxp-/xoxa-/xoxe-/xapp-…`, OpenAI/DeepSeek `sk-…`, * Anthropic `sk-ant-…`, Google `AIza…`, GitHub `ghp_/gho_/…` and * `github_pat_…`, plus the wiki confidentiality patterns (JWTs, bearer * headers, private-key blocks, connection strings). * * Redaction is idempotent: running it over already-redacted text changes * nothing and reports a count of 0. * * Output is streamed in arbitrary chunks, so a secret can be split between * two chunks. StreamingSecretRedactor holds back the tail after the last * whitespace (secrets never contain whitespace) and prepends it to the next * chunk, which keeps the whole thing linear with a small bounded carry. * * @module utils/secret-redactor */ import { type SecretEnvValue } from './secret-env.js'; /** * Whether an assignment's name marks its value as secret. * * @param name - Variable name as it appeared (any case) * @returns True for secret names (see module docs) */ export declare function isSecretAssignmentName(name: string): boolean; /** Result of a counted redaction. */ export interface RedactionResult { /** The masked text */ text: string; /** Number of secrets masked (0 when the text was already clean) */ count: number; } /** * Masks every secret in a complete piece of text and counts what was masked. * * @param text - Text to mask * @param knownSecrets - Exact secret values to mask (see collectSecretEnvValues) * @returns Masked text and the number of secrets masked * * @example * ```ts * redactSecretsWithCount('export OPENAI_API_KEY=sk-abc…').count; // 1 * redactSecretsWithCount('TOKEN_COUNT=5').count; // 0 * ``` */ export declare function redactSecretsWithCount(text: string, knownSecrets?: readonly SecretEnvValue[]): RedactionResult; /** * Masks every secret in a complete piece of text. * * @param text - Text to mask * @param knownSecrets - Exact secret values to mask * @returns The masked text */ export declare function redactSecrets(text: string, knownSecrets?: readonly SecretEnvValue[]): string; /** * Chunk-boundary-safe redactor for streamed output. * * `push()` returns the redacted text that is safe to emit now and holds back * the tail after the last whitespace; `flush()` returns whatever is held back * (call it when the stream closes). Only the new chunk is scanned for * whitespace (the carry never contains any), so work is linear in the input. * A run of more than MAX_CARRY_CHARS non-whitespace characters is cut at a * non-token character near its end (or hard-cut when there is none), keeping * the carry bounded. * * @example * ```ts * const r = new StreamingSecretRedactor(); * log.write(r.push('export GEMINI_API_KEY=AIzaSy')); * log.write(r.push('…rest of key\n')); * log.write(r.flush()); * ``` */ export declare class StreamingSecretRedactor { private carry; private knownSecrets; private readonly maxCarry; private readonly minCarry; /** * @param knownSecrets - Exact secret values to mask * @param maxCarry - Longest whitespace-free tail held back before forcing a cut */ constructor(knownSecrets?: readonly SecretEnvValue[], maxCarry?: number); /** * Replaces the exact-value secret set (e.g. after the session's env changes). * * @param knownSecrets - Exact secret values to mask */ setKnownSecrets(knownSecrets: readonly SecretEnvValue[]): void; /** * Feeds one chunk and returns the redacted text that can be emitted now. * * @param chunk - Next piece of output * @returns Redacted text (possibly empty while a word is still arriving) */ push(chunk: string): string; /** * Returns (redacted) whatever is still held back and resets the carry. * * @returns Remaining redacted text */ flush(): string; /** * Picks a cut point for a whitespace-free run that outgrew the carry: the * last non-token character before the final minCarry characters, so a * credential near the end is not split; hard cut when there is none. * * @param buf - Buffered text whose tail has no whitespace * @returns Index to cut at */ private forcedCut; } //# sourceMappingURL=secret-redactor.d.ts.map