/**
 * Phase 9a — workspace.json schema (WARN-MODE).
 *
 * Registry-driven validator that flags secret-shaped material anywhere in a
 * workspace.json blob. In 9a the validator runs but only *logs* violations;
 * the write proceeds. Phase 9b will route writers through the validator, and
 * 9c will flip rejections on.
 *
 * The detector reuses `findSecretLeaks()` from `bundle.ts` — the goal is
 * that import-time, export-time, and save-time agree on what "looks like a
 * secret" so leaks can't slip through one path while another rejects them.
 *
 * Output format is the structured single-line shape from the plan §5:
 *   [schema-warn] workspace_config: field '<path>' has secret-shaped value
 *     (registry: <key.name>, classification: <classification>)
 *
 * One line per violation. No multi-line stack traces. Cheap to grep, cheap
 * for downstream log routing to redact further once Phase 11's sanitize()
 * is wired.
 */
import { z } from 'zod';
import { findSecretLeaks } from '../secrets/bundle.js';
import { getKeyDef } from '../secrets/registry.js';
/**
 * Phase 9c — strict-mode error.
 *
 * Thrown when `STUDIOGRAPH_SCHEMA_STRICT` is on (default) and the
 * workspace or connector validator finds at least one violation.
 *
 * **The error message MUST NOT contain the offending value.** Only the
 * field path, registry classification, and a remediation hint are
 * exposed — exactly what a route handler can log and surface to the
 * user without re-leaking the secret.
 *
 * Routes that catch this translate it to:
 *   - HTTP 400 with error code `schema_violation` (config-api, etc.)
 *   - CLI exit 1 with a red message (sync, clone, join, ...)
 *
 * Callers can read `violations` and `scope` to build their own message.
 */
export class SchemaViolationError extends Error {
    static code = 'schema_violation';
    code = SchemaViolationError.code;
    scope;
    violations;
    constructor(scope, violations) {
        super(formatSchemaViolationMessage(scope, violations));
        this.name = 'SchemaViolationError';
        this.scope = scope;
        this.violations = violations;
    }
}
function formatSchemaViolationMessage(scope, violations) {
    const lines = violations.map((v) => {
        const meta = [];
        if (v.registryKey)
            meta.push(`registry: ${v.registryKey}`);
        if (v.classification)
            meta.push(`classification: ${v.classification}`);
        if (!meta.length)
            meta.push(`reason: ${v.reason}`);
        return `  - ${v.path} (${meta.join(', ')})`;
    });
    const remediation = scope === 'workspace_config'
        ? 'Move secret values to environment variables or the encrypted store. ' +
            'workspace.json is committed to the config repo and must not carry literals.'
        : 'Use $ENV_VAR placeholders for secret-named fields (Authorization, ' +
            'oauth.client_secret, token). Literal values belong in ~/.studiograph/connectors/ ' +
            '(homedir layer), not the workspace-layer file.';
    return [
        `${scope} schema violation — secret-shaped content detected:`,
        ...lines,
        remediation,
        'Set STUDIOGRAPH_SCHEMA_STRICT=0 to fall back to warn-mode while fixing.',
    ].join('\n');
}
/**
 * Phase 9c — runtime check for strict mode.
 *
 * Read at validation time (not cached at boot) so operators can flip the
 * env in a hotfix without restarting the process. Default behaviour
 * (env unset) is strict — `'0'` and `'false'` are the only opt-outs.
 */
export function isSchemaStrict() {
    const raw = process.env.STUDIOGRAPH_SCHEMA_STRICT;
    if (raw === undefined || raw === '')
        return true;
    const v = raw.toLowerCase();
    return !(v === '0' || v === 'false' || v === 'off' || v === 'no');
}
/**
 * Top-level shape. Loose by design in 9a — we keep unknown fields so
 * upstream callers (workspace.ts writeConfig, dual-write mirrors) don't
 * regress on perfectly valid existing data. Phase 9b/c can tighten this to
 * `.strict()` once the warn-mode telemetry shows what real data looks like.
 */
export const WorkspaceConfigSchema = z
    .object({
    version: z.string().optional(),
    team_name: z.string().optional(),
    name: z.string().optional(),
    slug: z.string().optional(),
    created_at: z.string().optional(),
    repos: z.array(z.unknown()).optional(),
})
    .passthrough();
/**
 * Run the secret-leak walker over a workspace.json blob and return a list of
 * structured violations. The walker uses `findSecretLeaks()` from bundle.ts,
 * which understands:
 *   - registry-named paths (`r2_config.secret_access_key` → bootstrap)
 *   - bootstrap field names (`jwtSecret`, `apiKey`)
 *   - value-shape patterns (`sk-*`, `Bearer *`, `https://user:pass@…`, etc.)
 *
 * The validator does NOT mutate, throw, or reject — it just produces the
 * violation list. Callers that want strict behaviour wrap it in their own
 * `if (violations.length) throw` check (Phase 9c).
 */
export function validateWorkspaceConfig(input) {
    const violations = [];
    if (!input || typeof input !== 'object')
        return violations;
    for (const leak of findSecretLeaks('', input)) {
        const def = lookupKey(leak.path);
        violations.push({
            path: leak.path,
            registryKey: def?.name,
            classification: def?.classification,
            reason: leak.reason,
        });
    }
    return violations;
}
/**
 * Format a single violation as the warn-mode log line. Stable, greppable,
 * one line per violation. Phase 11's `sanitize()` is responsible for any
 * further redaction once the centralized logger lands.
 */
export function formatViolation(scope, v) {
    const parts = [];
    parts.push(`[schema-warn] ${scope}: field '${v.path}' has secret-shaped value`);
    if (v.registryKey || v.classification) {
        const meta = [];
        if (v.registryKey)
            meta.push(`registry: ${v.registryKey}`);
        if (v.classification)
            meta.push(`classification: ${v.classification}`);
        parts.push(`(${meta.join(', ')})`);
    }
    else {
        parts.push('(registry: <unmapped>, reason: value-shape)');
    }
    return parts.join(' ');
}
/**
 * Warn-mode entry point: validate + log each violation via `console.warn`.
 * Returns the violation list so callers (tests, telemetry) can also
 * inspect what was found.
 *
 * The logger seam is `console.warn` for 9a; Phase 11 will swap this for the
 * centralized `log.warn` once that lands. Using `console.warn` here keeps
 * the import graph small and avoids a Phase-11 dependency.
 */
export function warnOnWorkspaceConfigViolations(input) {
    const violations = validateWorkspaceConfig(input);
    for (const v of violations) {
        // eslint-disable-next-line no-console
        console.warn(formatViolation('workspace_config', v));
    }
    return violations;
}
/**
 * Phase 9c — strict-or-warn entry point used by save sites.
 *
 * When `STUDIOGRAPH_SCHEMA_STRICT` is on (default), throws
 * `SchemaViolationError` on the first non-empty violation list. Otherwise
 * delegates to `warnOnWorkspaceConfigViolations` for log-only behaviour.
 *
 * This is the canonical helper that `Workspace.writeConfig` should call.
 * Tests can flip the env to exercise both modes.
 */
export function enforceWorkspaceConfig(input) {
    if (!isSchemaStrict()) {
        return warnOnWorkspaceConfigViolations(input);
    }
    const violations = validateWorkspaceConfig(input);
    if (violations.length > 0) {
        throw new SchemaViolationError('workspace_config', violations);
    }
    return violations;
}
/**
 * Resolve a leak path to a registry entry. Tries direct match, the
 * `workspace.`/`instance.` prefixes, and tail-walks suffixes — mirrors
 * bundle.ts's lookup logic so the two stay in sync.
 */
function lookupKey(path) {
    const direct = getKeyDef(path) ?? getKeyDef(`workspace.${path}`) ?? getKeyDef(`instance.${path}`);
    if (direct)
        return direct;
    // Try the suffix walk: a.b.c → b.c, then c.
    const parts = path.split('.');
    for (let i = 1; i < parts.length; i++) {
        const tail = parts.slice(i).join('.');
        const m = getKeyDef(tail) ?? getKeyDef(`workspace.${tail}`) ?? getKeyDef(`instance.${tail}`);
        if (m)
            return m;
    }
    return undefined;
}
//# sourceMappingURL=workspace.js.map