/**
 * Phase 11 — centralized log redactor.
 *
 * Single `sanitize(text)` function applied at every log choke point:
 *   - Fastify logger transform
 *   - patched console.* (production only)
 *   - API error response middleware
 *   - pushSeed() git output capture
 *
 * Redaction rules, in order:
 *   1. Registry-driven substring match: every env-var value listed under
 *      a non-`redact: 'never'` registry entry is looked up at call time
 *      and substring-replaced with `[REDACTED:<key.name>]`. The lookup
 *      is dynamic so rotated values track without a process restart.
 *   2. Prefix-matched tokens — `sk-*`, `ghp_*`, `ghu_*`, `ghs_*`,
 *      `xox[bp]-*`, `AKIA*`, `glpat-*`, `Bearer *`, `Basic *`.
 *   3. URL with embedded credentials — `https?://user:pass@…`.
 *   4. JSON-quoted secret-named fields — `"Authorization": "<value>"`,
 *      `"api_key": "…"`, etc.
 *
 * Each replacement uses `[REDACTED:<name>]` when there's a registry hit,
 * `[REDACTED]` otherwise. Replacements are idempotent — running
 * `sanitize()` twice on the same string produces the same output.
 *
 * Performance: every log line walks the registry once. Registry is ~30
 * entries today; tested at ~50µs per call on a typical Node host. The
 * patched-console path means every console.warn in production pays this
 * cost — acceptable given the alternative is shipping secrets to stdout.
 *
 * Escape hatch: callers can pre-check `process.env.STUDIOGRAPH_LOG_RAW === '1'`
 * before invoking sanitize() if they need raw output for local debugging.
 * The patched-console wrapper in serve.ts honours this flag.
 */
import { REGISTRY } from '../../core/secrets/registry.js';
/**
 * Prefix-shape detectors. Each pattern matches a known secret token
 * format. The match is greedy through whitespace boundaries so we
 * redact the whole token, not just its prefix. The patterns include
 * the prefix so the replacement is recognisable in logs.
 */
const TOKEN_PREFIX_PATTERN = /\b(?:sk|ghp|ghu|ghs|glpat|xoxb|xoxp)[-_][A-Za-z0-9_-]{6,}\b|\bAKIA[A-Z0-9]{12,}\b|(?:Bearer|Basic) [A-Za-z0-9._\-+/=]{6,}/g;
/**
 * URL with embedded credentials: `https://user:pass@host/path`.
 * The credentials section is what we redact; we keep the host so logs
 * stay useful for debugging.
 */
const URL_CREDS_PATTERN = /(https?:\/\/)([^:@\s/]+):([^@\s/]+)@/g;
/**
 * JSON-quoted secret-named fields: catches both `"authorization": "value"`
 * and (HTTP header style) `Authorization: value` patterns inside log
 * lines. The field-name regex matches `authorization`, `cookie`, anything
 * containing `key`, `token`, `secret`, `password`, `credential`, or
 * `auth` (case-insensitive).
 */
const JSON_SECRET_FIELD_PATTERN = /("(?:authorization|cookie|proxy-authorization|x-amz-security-token|[a-zA-Z0-9_-]*(?:api[_-]?key|token|secret|password|credential)[a-zA-Z0-9_-]*)"\s*:\s*)"([^"\\]+)"/gi;
/**
 * HTTP-style headers in plain text (Fastify's prod log mode and some
 * stack traces): `Authorization: Bearer abc…`. Different from the JSON
 * pattern — no quotes, colon, then the value until end of line. We
 * keep the field name in the redacted output.
 */
const PLAIN_HEADER_PATTERN = /\b(authorization|cookie|proxy-authorization|x-api-key|x-auth-token|x-amz-security-token):\s*([^\r\n,;]+)/gi;
/**
 * Artifact-asset capability token carried in a URL query string
 * (`/api/assets/…?at=<jwt>`). It's a bearer credential, but it isn't a
 * secret-named field and a JWT carries no recognizable prefix, so the rules
 * above miss it — redact the value wherever a URL is logged. Keeps `at=` so the
 * shape stays debuggable. Idempotent: `at=[REDACTED]` can't re-match (the `[`
 * isn't in the value class).
 */
const ARTIFACT_ASSET_TOKEN_QUERY_PATTERN = /([?&]at=)[A-Za-z0-9._\-+/=%]+/g;
/**
 * Public share-link token carried as a URL PATH segment — `/api/share/<token>`
 * (the API surface) and `/share/<token>` (the page route). Like `?at=`, the
 * token IS the bearer credential, so request-log lines would otherwise hand a
 * live share link to anyone who can read logs (D4: the sanitizer must cover
 * share-token paths, not just the `?at=` query). Tokens are `shr_` + base64url
 * today; the length floor keeps short literal path words out of scope while
 * matching any plausible token shape. Trailing segments (`/asset/...`) stay
 * visible for debuggability. Idempotent: `[REDACTED]`'s `[` isn't in the class.
 */
const SHARE_TOKEN_PATH_PATTERN = /((?:\/api)?\/share\/)(?:shr_[A-Za-z0-9_-]+|[A-Za-z0-9_-]{10,})/g;
/**
 * Build the list of env-var values that should be substring-redacted.
 * Looked up dynamically from `process.env` so rotations track without a
 * restart. Excluded: registry entries with `redact: 'never'` (e.g.
 * `model.id`, `cors.origins` — these are safe to log).
 *
 * Returns pairs of `[value, replacement]` sorted by value length
 * descending so longer values are tried first (a 64-char JWT shouldn't
 * have its prefix nibbled by a shorter substring match later).
 *
 * Note: `redact: 'value-only'` is included because its semantic is
 * "log the key name, redact the value" — i.e. the literal value should
 * still be redacted in free-form log lines.
 */
function buildEnvValueReplacements() {
    const out = [];
    for (const entry of REGISTRY) {
        if (entry.redact === 'never')
            continue;
        for (const alias of entry.envAliases) {
            const value = process.env[alias];
            if (typeof value === 'string' && value.length >= 8) {
                out.push([value, `[REDACTED:${entry.name}]`]);
            }
        }
    }
    // Sort by length descending — longest values redacted first.
    out.sort((a, b) => b[0].length - a[0].length);
    return out;
}
/**
 * Escape a string for use inside a RegExp source. Standard implementation.
 */
function escapeRegExp(s) {
    return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
 * Sanitize a single string. Idempotent — `sanitize(sanitize(s)) === sanitize(s)`.
 *
 * Returns the input untouched when it's not a string. Non-string inputs
 * are passed through so callers can use it as a generic transform.
 */
export function sanitize(text) {
    if (typeof text !== 'string') {
        if (text === null || text === undefined)
            return String(text);
        try {
            return sanitize(JSON.stringify(text));
        }
        catch {
            return String(text);
        }
    }
    let out = text;
    // (1) Registry-driven env-value substring replacement.
    const replacements = buildEnvValueReplacements();
    for (const [value, replacement] of replacements) {
        if (!value)
            continue;
        // Skip very short values that would over-redact (any 4-char hex would
        // collide with timestamps, ids, etc.). 8-char floor matches buildEnvValueReplacements.
        if (value.length < 8)
            continue;
        const pattern = new RegExp(escapeRegExp(value), 'g');
        out = out.replace(pattern, replacement);
    }
    // (2) URL with embedded credentials. Run before token-prefix so the
    // password portion (which might match Bearer/Basic) isn't reported
    // twice. Keeps protocol and host visible.
    out = out.replace(URL_CREDS_PATTERN, (_m, proto, _user, _pass) => {
        return `${proto}[REDACTED]@`;
    });
    // (3) JSON-quoted secret fields. Run before prefix-token so a value
    // that already lives in a quoted field is replaced with a quoted
    // sentinel (preserves the JSON shape for downstream parsers).
    out = out.replace(JSON_SECRET_FIELD_PATTERN, (_m, head, _value) => {
        return `${head}"[REDACTED]"`;
    });
    // (4) Plain HTTP-style headers (Fastify default-mode logs, stack traces).
    out = out.replace(PLAIN_HEADER_PATTERN, (_m, name, _value) => {
        return `${name}: [REDACTED]`;
    });
    // (5) Artifact-asset capability token in a URL query (`?at=<jwt>`).
    out = out.replace(ARTIFACT_ASSET_TOKEN_QUERY_PATTERN, (_m, head) => `${head}[REDACTED]`);
    // (5b) Share-link token in a URL path (`/api/share/<token>`, `/share/<token>`).
    out = out.replace(SHARE_TOKEN_PATH_PATTERN, (_m, head) => `${head}[REDACTED]`);
    // (6) Token-prefix patterns. Last so it can sweep up anything the
    // JSON/header patterns missed (e.g. tokens embedded in error
    // messages or query strings).
    out = out.replace(TOKEN_PREFIX_PATTERN, (m) => {
        // Preserve the recognisable prefix so logs stay useful for
        // debugging which provider's credential leaked.
        const sep = m.match(/^(?:sk|ghp|ghu|ghs|glpat|xoxb|xoxp)[-_]/)?.[0];
        if (sep)
            return `${sep}[REDACTED]`;
        if (/^AKIA/.test(m))
            return 'AKIA[REDACTED]';
        const bearer = m.match(/^(?:Bearer|Basic) /)?.[0];
        if (bearer)
            return `${bearer}[REDACTED]`;
        return '[REDACTED]';
    });
    return out;
}
/**
 * Convenience: sanitize multi-arg console-style invocations. Each arg
 * is sanitized independently and returned as a string array — caller
 * decides how to join.
 */
export function sanitizeArgs(args) {
    return args.map((a) => sanitize(a));
}
//# sourceMappingURL=sanitize.js.map