/**
 * Key Registry — single source of truth for every configurable key.
 *
 * Every consumer (resolver, importer, validator, redactor, audit logger,
 * exporter, test suite) reads from this module. If a `process.env.<KEY>`
 * appears in `src/`, it MUST be either listed under some entry's
 * `envAliases` or covered by `SYSTEM_ALLOWLIST` / `OPERATIONAL_ALLOWLIST`.
 * The registry-completeness test enforces this — orphans fail CI with
 * their names printed.
 *
 * Adding a new env var? Two-step: pick the right classification, add the
 * KeyDef here. The completeness test will keep you honest.
 */
import { z } from 'zod';
import { probeAnthropic } from './probes/anthropic.js';
import { probeR2 } from './probes/r2.js';
import { probeBrave } from './probes/brave.js';
import { probeTavily } from './probes/tavily.js';
import { probeVoyage } from './probes/voyage.js';
/**
 * System-level env vars provided by the OS or container runtime. Not
 * registry-managed — they do not represent product configuration; we just
 * read them when they happen to be set.
 */
export const SYSTEM_ALLOWLIST = new Set([
    'HOME',
    'PATH',
    'EDITOR',
    'CI',
    'VISUAL',
    'USERPROFILE',
    'PORT',
    'HOST',
]);
/**
 * Operational env vars that toggle runtime behaviour but aren't
 * configurable settings (no UI surface, no persistence, no precedence
 * chain). Not registry-managed.
 */
export const OPERATIONAL_ALLOWLIST = new Set([
    'RAILWAY_ENVIRONMENT',
    'STUDIOGRAPH_LOG',
    'STUDIOGRAPH_VERBOSE',
    'STUDIOGRAPH_NO_OPEN',
    'STUDIOGRAPH_NO_SPINNER',
    // Historical Phase 1 feature flag. Removed once Phase 6 stabilized in
    // production; the encrypted store is now unconditional. Kept on the
    // allowlist so legacy Railway services that still set it don't trip
    // the "unknown env" warning. Setting it has no effect.
    'STUDIOGRAPH_NEW_STORE',
    // Phase 11 log-redaction controls.
    'STUDIOGRAPH_LOG_RAW',
    'NODE_ENV',
    // Phase 9c schema-enforcement toggle. `1` (default when unset) makes the
    // workspace.json + connector validators throw on any violation; `0`
    // drops back to warn-mode for hotfix. Operators flip without restart —
    // the env is read at validation time, not cached at boot.
    'STUDIOGRAPH_SCHEMA_STRICT',
    // Phase 1 of the Y.Doc-routed write plan. Kill switch on the broadcast
    // observer (default `1` / ON). When `0`, server-side writes refuse to
    // mutate live docs via `BROADCAST_DISABLED_LIVE_CLIENTS` instead of
    // silently diverging from connected editors. Read once at YjsManager
    // construction — restart required to flip.
    'STUDIOGRAPH_YJS_BROADCAST',
    // Operator opt-in to accept degraded-mode data loss when broadcast is
    // off AND clients are active. Default off. Required override to bypass
    // the fail-closed `assertWritable` guard.
    'STUDIOGRAPH_ALLOW_DEGRADED_WRITES',
    // Deploy-time-only: lets `studiograph deploy railway` receive the model
    // API key non-interactively (alternative to the --api-key flag). Read
    // once during deploy, never by the running server — not a stored secret.
    'STUDIOGRAPH_DEPLOY_API_KEY',
    // OAuth authorization-server issuer pinning. The canonical public base URL
    // used to build OAuth discovery-metadata + token `aud` (never the inbound
    // Host header). Operational config, not a stored secret — read at request
    // time. RAILWAY_PUBLIC_DOMAIN is the platform-provided fallback.
    'STUDIOGRAPH_PUBLIC_URL',
    'RAILWAY_PUBLIC_DOMAIN',
    // Managed-cloud auth handoff (src/server/routes/auth-api.ts). Operational
    // config for the Studiograph Cloud login bridge — workspace/org identifiers
    // + the cloud base URL + the HMAC verify token. Read at request time, not
    // through the secret store. (These were previously read without being
    // registered, tripping the orphan-env guard — registered here.)
    'STUDIOGRAPH_MANAGED_AUTH',
    'STUDIOGRAPH_CLOUD_URL',
    'STUDIOGRAPH_CLOUD_ORG_ID',
    'STUDIOGRAPH_CLOUD_WORKSPACE_ID',
    'STUDIOGRAPH_CLOUD_TOKEN',
    // Deploy-time opt-in for the post-listen Playwright Chromium bootstrap
    // (src/services/playwright-bootstrap.ts) so inspect_artifact/export work on
    // Railway. `=1` installs the browser in the background after serve binds;
    // BROWSER_ONLY=1 skips the OS dependency step. Operational toggles read once
    // at boot, never stored.
    'STUDIOGRAPH_INSTALL_PLAYWRIGHT',
    'STUDIOGRAPH_PLAYWRIGHT_BROWSER_ONLY',
]);
/**
 * The canonical registry. Order is informative (matches plan §2 grouping)
 * but every consumer should look up by name.
 */
export const REGISTRY = [
    // ── Bootstrap (env-only, no store, no export) ────────────────────────────
    {
        name: 'jwt.secret',
        classification: 'bootstrap',
        envAliases: ['STUDIOGRAPH_JWT_SECRET'],
        validate: z.string().min(16),
        redact: 'always',
        exportable: false,
        rotationHint: 'Set STUDIOGRAPH_JWT_SECRET on the host and restart. All extant tokens invalidate together.',
    },
    {
        name: 'master.key',
        classification: 'bootstrap',
        envAliases: ['STUDIOGRAPH_MASTER_KEY'],
        validate: z.string().regex(/^[A-Za-z0-9+/=_-]{40,}$/, 'expected base64-encoded 32+ bytes'),
        redact: 'always',
        exportable: false,
        rotationHint: 'Use `studiograph secrets rotate-master --new-key <new-base64>`; old key remains in STUDIOGRAPH_MASTER_KEY_PREVIOUS for the transition.',
    },
    {
        name: 'master.key.previous',
        classification: 'bootstrap',
        envAliases: ['STUDIOGRAPH_MASTER_KEY_PREVIOUS'],
        validate: z.string().optional(),
        redact: 'always',
        exportable: false,
    },
    {
        name: 'railway.token',
        classification: 'bootstrap',
        envAliases: ['RAILWAY_TOKEN'],
        validate: z.string(),
        redact: 'always',
        exportable: false,
    },
    // ── Bootstrap — CLI auth credentials ─────────────────────────────────────
    // Pre-approved decision: these are bootstrap because they exist solely to
    // let CLI/agent traffic authenticate without an interactive prompt.
    {
        name: 'cli.email',
        classification: 'bootstrap',
        envAliases: ['STUDIOGRAPH_EMAIL'],
        validate: z.string().email(),
        redact: 'value-only',
        exportable: false,
    },
    {
        name: 'cli.password',
        classification: 'bootstrap',
        envAliases: ['STUDIOGRAPH_PASSWORD'],
        validate: z.string().min(1),
        redact: 'always',
        exportable: false,
    },
    {
        name: 'cli.token',
        classification: 'bootstrap',
        envAliases: ['STUDIOGRAPH_TOKEN'],
        validate: z.string().min(1),
        redact: 'always',
        exportable: false,
    },
    {
        name: 'mcp.token',
        classification: 'bootstrap',
        envAliases: ['STUDIOGRAPH_MCP_TOKEN'],
        validate: z.string().min(1),
        redact: 'always',
        exportable: false,
    },
    // ── Workspace — model & providers ────────────────────────────────────────
    {
        name: 'model.provider',
        classification: 'workspace-setting',
        envAliases: ['MODEL_PROVIDER'],
        validate: z.enum(['anthropic', 'openai', 'google', 'xai', 'groq', 'openrouter']),
        redact: 'never',
        exportable: true,
    },
    {
        name: 'model.id',
        classification: 'workspace-setting',
        envAliases: ['MODEL_ID'],
        validate: z.string(),
        redact: 'never',
        exportable: true,
    },
    // Per-provider model API keys. Splitting the legacy `model.api_key`
    // (decommissioned 2026-05-12, Phase 12c) means switching providers no
    // longer silently re-uses an Anthropic key against OpenAI (or vice
    // versa) — each provider's key survives the switch and the panel
    // renders only the active one. `MODEL_API_KEY` is no longer an alias
    // here; each provider's SDK reads its own env var below.
    {
        name: 'model.anthropic.api_key',
        classification: 'workspace-secret',
        envAliases: ['ANTHROPIC_API_KEY'],
        validate: z.string().min(1),
        redact: 'always',
        exportable: false,
        consoleUrl: 'https://console.anthropic.com/settings/keys',
        testConnection: probeAnthropic,
    },
    {
        name: 'model.openai.api_key',
        classification: 'workspace-secret',
        envAliases: ['OPENAI_API_KEY'],
        validate: z.string().min(1),
        redact: 'always',
        exportable: false,
        consoleUrl: 'https://platform.openai.com/api-keys',
    },
    {
        name: 'model.google.api_key',
        classification: 'workspace-secret',
        envAliases: ['GOOGLE_API_KEY'],
        validate: z.string().min(1),
        redact: 'always',
        exportable: false,
        consoleUrl: 'https://aistudio.google.com/apikey',
    },
    {
        name: 'model.xai.api_key',
        classification: 'workspace-secret',
        envAliases: ['XAI_API_KEY'],
        validate: z.string().min(1),
        redact: 'always',
        exportable: false,
        consoleUrl: 'https://console.x.ai',
    },
    {
        name: 'model.groq.api_key',
        classification: 'workspace-secret',
        envAliases: ['GROQ_API_KEY'],
        validate: z.string().min(1),
        redact: 'always',
        exportable: false,
        consoleUrl: 'https://console.groq.com/keys',
    },
    {
        name: 'model.openrouter.api_key',
        classification: 'workspace-secret',
        envAliases: ['OPENROUTER_API_KEY'],
        validate: z.string().min(1),
        redact: 'always',
        exportable: false,
        consoleUrl: 'https://openrouter.ai/keys',
    },
    // ── Workspace — semantic search (embeddings) ─────────────────────────────
    // Optional Tier-2 hybrid search. Without this key, vector-service falls
    // back to Tier-1 FTS-only. With it, voyage-3 embeddings are generated
    // on index + query for better recall. The first-run importer migrates
    // existing `user.json:voyage_api_key` values into the store on cold
    // boot — no manual action needed for installs that had it configured
    // before this entry landed.
    {
        name: 'voyage.api_key',
        classification: 'workspace-secret',
        envAliases: ['VOYAGE_API_KEY'],
        validate: z.string().min(1),
        redact: 'always',
        exportable: false,
        consoleUrl: 'https://dashboard.voyageai.com/api-keys',
        testConnection: probeVoyage,
    },
    // ── Workspace — storage (R2 / S3-compatible) ─────────────────────────────
    // Structural fields (account_id, bucket, public_url) live in the store
    // post-2026-05-12 alongside the credentials. The one-shot migration in
    // r2-structural-migration.ts mirrors any pre-existing values from
    // `workspace.json:r2_config.*` on first boot so operators who
    // configured R2 via `studiograph r2 setup` see no behavior change.
    // Subsequent writes route through Settings UI or the same CLI (whose
    // workspace.writeConfig path is dual-write-wired in dual-write.ts).
    //
    // We deliberately do NOT register `r2.endpoint`. The S3 client derives
    // its endpoint from account_id (`https://<account>.r2.cloudflarestorage.com`)
    // and no consumer reads an override today. If a future change adds
    // support for non-Cloudflare S3-compatible storage (MinIO, B2,
    // Backblaze, local-dev MinIO, etc.), add the entry then alongside the
    // R2AssetService change that consumes it.
    {
        name: 'r2.account_id',
        classification: 'workspace-setting',
        envAliases: ['R2_ACCOUNT_ID'],
        validate: z.string(),
        redact: 'never',
        exportable: true,
    },
    {
        name: 'r2.bucket',
        classification: 'workspace-setting',
        envAliases: ['R2_BUCKET'],
        validate: z.string(),
        redact: 'never',
        exportable: true,
    },
    {
        name: 'r2.public_url',
        classification: 'workspace-setting',
        envAliases: ['R2_PUBLIC_URL'],
        validate: z.string().url(),
        redact: 'never',
        exportable: true,
    },
    {
        name: 'r2.access_key_id',
        classification: 'workspace-secret',
        envAliases: ['R2_ACCESS_KEY_ID'],
        validate: z.string(),
        redact: 'value-only',
        exportable: false,
        consoleUrl: 'https://dash.cloudflare.com/?to=/:account/r2/api-tokens',
        testConnection: probeR2,
    },
    {
        name: 'r2.secret_access_key',
        classification: 'workspace-secret',
        envAliases: ['R2_SECRET_ACCESS_KEY'],
        validate: z.string(),
        redact: 'always',
        exportable: false,
        consoleUrl: 'https://dash.cloudflare.com/?to=/:account/r2/api-tokens',
    },
    // ── Workspace — search ───────────────────────────────────────────────────
    // search.provider is the explicit picker — when set, the resolver uses
    // ONLY this provider's key (no cascade). When unset, the resolver falls
    // back to the legacy first-key-wins cascade for back-compat.
    {
        name: 'search.provider',
        classification: 'workspace-setting',
        envAliases: ['SEARCH_PROVIDER'],
        validate: z.enum(['brave', 'tavily', 'google']),
        redact: 'never',
        exportable: true,
    },
    {
        name: 'search.brave.api_key',
        classification: 'workspace-secret',
        envAliases: ['BRAVE_SEARCH_API_KEY'],
        validate: z.string(),
        redact: 'always',
        exportable: false,
        consoleUrl: 'https://api.search.brave.com/app/keys',
        testConnection: probeBrave,
    },
    {
        name: 'search.tavily.api_key',
        classification: 'workspace-secret',
        envAliases: ['TAVILY_API_KEY'],
        validate: z.string(),
        redact: 'always',
        exportable: false,
        consoleUrl: 'https://app.tavily.com',
        testConnection: probeTavily,
    },
    {
        name: 'search.google.api_key',
        classification: 'workspace-secret',
        envAliases: ['GOOGLE_SEARCH_API_KEY'],
        validate: z.string(),
        redact: 'always',
        exportable: false,
        consoleUrl: 'https://console.cloud.google.com/apis/credentials',
    },
    {
        name: 'search.google.cx',
        classification: 'workspace-setting',
        envAliases: ['GOOGLE_SEARCH_CX'],
        validate: z.string(),
        redact: 'value-only',
        exportable: true,
    },
    // ── Workspace — server config ────────────────────────────────────────────
    {
        name: 'cors.origins',
        classification: 'workspace-setting',
        envAliases: ['STUDIOGRAPH_CORS_ORIGINS'],
        validate: z.string(),
        redact: 'never',
        exportable: true,
    },
    // ── Workspace — connector secret slots (pattern entries) ────────────────
    // These are TEMPLATE entries — the `<name>` segment is substituted with
    // the connector name at write time (e.g. `connector.linear.auth`).
    // `resolveConnectorKeyDef()` does the pattern match for callers that
    // hand it a concrete key.
    {
        name: 'connector.<name>.auth',
        classification: 'workspace-secret',
        envAliases: [],
        validate: z.string(),
        redact: 'always',
        exportable: false,
    },
    {
        name: 'connector.<name>.oauth_secret',
        classification: 'workspace-secret',
        envAliases: [],
        validate: z.string(),
        redact: 'always',
        exportable: false,
    },
    // ── User-scope (lives in auth.db, encrypted in Phase 8) ──────────────────
    {
        name: 'user.deepgram.api_key',
        classification: 'user-secret',
        envAliases: [],
        validate: z.string(),
        redact: 'always',
        exportable: false,
    },
    {
        name: 'user.mcp.oauth_token',
        classification: 'user-secret',
        envAliases: [],
        validate: z.string(),
        redact: 'always',
        exportable: false,
    },
    // ── Workspace — indexing ────────────────────────────────────────────────
    // Auto-reindex on `studiograph serve` boot. Defaults true. Owners with
    // very large workspaces (millions of entities) where boot latency matters
    // can flip this off and rely on the explicit `studiograph index` command
    // instead. Replaces the auto-index that used to live in `studiograph
    // start`; see cli-owner-audit.md §3 (A1).
    {
        name: 'index.auto_on_boot',
        classification: 'workspace-setting',
        envAliases: ['STUDIOGRAPH_INDEX_AUTO_ON_BOOT'],
        default: true,
        validate: z.boolean(),
        redact: 'never',
        exportable: true,
    },
];
/**
 * Look up a registry entry by canonical name.
 *
 * Handles two cases:
 *   1. Exact match on `name` — the common path.
 *   2. Connector pattern match — concrete keys like
 *      `connector.linear.auth` resolve to the template entry
 *      `connector.<name>.auth`. The template's classification + redaction
 *      flags apply to the concrete key.
 *
 * Returns undefined for genuinely unknown keys; callers (importer, store
 * setters) treat this as "not in the registry, reject the write."
 */
export function getKeyDef(name) {
    const direct = REGISTRY.find((k) => k.name === name);
    if (direct)
        return direct;
    return resolveConnectorKeyDef(name);
}
/**
 * Match a concrete connector key (`connector.<name>.<slot>`) against the
 * template entries in the registry. Returns a synthetic KeyDef carrying
 * the template's classification/redaction/exportable flags but with the
 * concrete `name` so audit and store rows record the actual slot, not
 * the placeholder.
 */
export function resolveConnectorKeyDef(name) {
    const m = /^connector\.([a-z0-9][a-z0-9_-]*)\.(.+)$/.exec(name);
    if (!m)
        return undefined;
    const slot = m[2];
    // Find the template entry whose slot suffix matches.
    const template = REGISTRY.find((k) => k.name === `connector.<name>.${slot}`);
    if (!template)
        return undefined;
    return { ...template, name };
}
/**
 * Reverse lookup: given an env-var spelling, return the registry entry that
 * claims it as an alias. Returns `undefined` for unregistered names — the
 * caller should fall back to the allowlists.
 */
export function getKeyDefByEnvAlias(envName) {
    return REGISTRY.find((k) => k.envAliases.includes(envName));
}
/**
 * Set of every env-var spelling claimed by the registry. Used by tests and
 * by the resolver to decide whether an env-var is "ours" vs ambient.
 */
export function allRegisteredEnvAliases() {
    return new Set(REGISTRY.flatMap((k) => k.envAliases));
}
/**
 * Per-provider recommended model IDs.
 *
 * Surfaced via the `/api/settings/schema` response on the `model.id`
 * entry so the ProvidersPanel combobox can render a non-blocking
 * "Recommended" group. The list is intentionally short (3–4 per
 * provider) — power users can always type a preview-tagged value
 * directly; the combobox never blocks freeform input.
 *
 * Provider keys must align 1:1 with the `model.provider` enum values
 * — there's a registry-completeness test that guards this. Seed
 * values below are best-effort and SHOULD BE USER-VERIFIED; the IDs
 * shift fast and stale entries here just degrade the dropdown UX
 * without breaking anything (free typing still works).
 */
// Verified 2026-05-11 against each provider's docs. Re-verify periodically —
// these IDs drift as providers ship new versions and retire old ones. The
// Test Connection probe's `model_in_list` stage is the runtime canary for
// any list that goes stale.
export const RECOMMENDED_MODELS = {
    anthropic: ['claude-opus-4-7', 'claude-sonnet-4-6', 'claude-haiku-4-5'],
    openai: ['gpt-5.5', 'gpt-5.4', 'gpt-5.4-mini', 'o3', 'o4-mini'],
    google: ['gemini-2.5-pro', 'gemini-2.5-flash'],
    // xAI retired grok-3 + grok-3-mini on 2026-05-15. grok-4 superseded by
    // grok-4.3 as the current reasoning/code workhorse.
    xai: ['grok-4.3', 'grok-4.20-non-reasoning'],
    groq: ['llama-3.3-70b-versatile', 'llama-3.1-8b-instant'],
    openrouter: ['anthropic/claude-opus-4-7', 'openai/gpt-5.5', 'google/gemini-2.5-pro'],
};
/**
 * Introspect a registered key's zod validator to recover what kind of
 * value it accepts. Walks ZodOptional/ZodNullable/ZodDefault wrappers to
 * the inner type. Returns `undefined` for unknown keys and for plain
 * string-ish schemas — the UI treats absence-of-constraint as "render a
 * text input."
 *
 * Implementation note: zod doesn't expose a stable type-introspection
 * API across versions. We peek into `_def` and check `typeName` /
 * `entries`. Wrap in try/catch and degrade to undefined if the shape
 * changes upstream — losing a dropdown is preferable to crashing the
 * settings panel.
 */
export function getKeyConstraint(name) {
    const def = getKeyDef(name);
    if (!def)
        return undefined;
    try {
        return introspectSchema(def.validate);
    }
    catch {
        return undefined;
    }
}
function introspectSchema(schema) {
    const s = schema;
    const inner = s._def;
    if (!inner)
        return undefined;
    // zod v4 uses `_def.type`; zod v3 used `_def.typeName`. Check both so
    // the introspection survives a minor zod version bump.
    const tag = (inner.type ?? inner.typeName ?? '').toLowerCase();
    // Unwrap modifier wrappers — z.string().optional() etc.
    if (tag === 'optional' || tag === 'nullable' || tag === 'default' ||
        tag === 'zodoptional' || tag === 'zodnullable' || tag === 'zoddefault') {
        return introspectSchema(inner.innerType);
    }
    if (tag === 'enum' || tag === 'zodenum') {
        // zod v4: public `.options` getter returns the values array.
        if (Array.isArray(s.options)) {
            return { type: 'enum', values: s.options };
        }
        // zod v4 internal: `_def.entries` is a Record of key→value.
        if (inner.entries && typeof inner.entries === 'object') {
            return { type: 'enum', values: Object.keys(inner.entries) };
        }
        // zod v3 internal: `_def.values` is an array.
        if (Array.isArray(inner.values)) {
            return { type: 'enum', values: inner.values };
        }
    }
    if (tag === 'boolean' || tag === 'zodboolean') {
        return { type: 'boolean' };
    }
    return undefined;
}
/**
 * Enumerate every registered workspace-setting (non-secret) entry for
 * the settings panels. Workspace-secrets are excluded — they go through
 * `GET /api/secrets`, which never returns plaintext values.
 *
 * Connector template entries (`connector.<name>.*`) are excluded too —
 * they're matched dynamically at resolution time, not enumerated in the
 * UI as fixed slots.
 */
export function listWorkspaceSettingsSchema() {
    return REGISTRY
        .filter((k) => k.classification === 'workspace-setting')
        .filter((k) => !k.name.includes('<'))
        .map((k) => {
        const entry = {
            key: k.name,
            classification: k.classification,
            redact: k.redact,
            env_aliases: k.envAliases,
            default: k.default,
            constraint: getKeyConstraint(k.name),
        };
        // Only `model.id` carries recommendations today. We attach the
        // full RECOMMENDED_MODELS map (not just the current provider's
        // slice) because the panel needs to swap the visible group as
        // the provider dropdown changes without a second roundtrip.
        if (k.name === 'model.id') {
            entry.recommendations = { byProvider: { ...RECOMMENDED_MODELS } };
        }
        return entry;
    });
}
/**
 * Flat map of canonical key → console URL for every registered key that
 * carries one. The settings UI uses this to render a "Get an API key →"
 * link below credential fields. The map covers BOTH workspace-secret
 * entries (API keys, R2 credentials) and any workspace-setting that
 * happens to grow a consoleUrl later — same lookup, single roundtrip.
 *
 * Keys without a registered consoleUrl are simply absent from the map.
 */
export function getAllConsoleUrls() {
    const out = {};
    for (const k of REGISTRY) {
        if (k.consoleUrl)
            out[k.name] = k.consoleUrl;
    }
    return out;
}
//# sourceMappingURL=registry.js.map