/**
 * Dual-write helpers — Phase 5 of the secrets refactor.
 *
 * Every legacy writer of a configuration file (user.json, workspace.json,
 * connectors/*.json) also mirrors the registry-mapped fields into the
 * encrypted store. The legacy file remains authoritative through Phase 5;
 * Phase 6 flipped the read path so the store is the source of truth at
 * read time.
 *
 * Failure modes are deliberately non-fatal: a dropped dual-write must
 * never block the legacy write, because the file is still the source of
 * truth. Failures route to `console.warn` for now — Phase 11 swaps in the
 * sanitizing log wrapper.
 *
 * Idempotency is provided by the store itself: writes use INSERT … ON
 * CONFLICT DO UPDATE, so writing the same key twice yields a single row
 * whose `updated_at` advances.
 *
 * `looksLikeEnvPlaceholder()` filters `$ENV_VAR` strings out — connector
 * configs use those as placeholders, and the literal `$FOO_AUTH` is not
 * actually a credential value worth mirroring into the store.
 *
 * After the scope-collapse refactor, the store is single-tier (the
 * process IS the workspace), so the mirror helpers no longer thread a
 * scope or scopeId — every write lands in the single secrets.db.
 */
import { getKeyDef } from './registry.js';
import { getSecretStore, hasSecretStore } from './index.js';
/**
 * Returns true when the store singleton was successfully initialized at
 * boot. Every dual-write entry point guards on this before touching the
 * store. Historically gated on the `STUDIOGRAPH_NEW_STORE` env flag too;
 * the flag was removed once Phase 6 stabilized in production.
 */
export function dualWriteEnabled() {
    return hasSecretStore();
}
/**
 * `$ENV_VAR` placeholder values are written into connector configs as the
 * sanitized form of a real secret. Mirroring them into the encrypted store
 * would be pointless and would shadow whatever the env variable actually
 * resolves to. Skip them.
 */
function looksLikeEnvPlaceholder(s) {
    return typeof s === 'string' && /^\$[A-Z_][A-Z0-9_]*$/.test(s);
}
function isNonEmptyString(v) {
    return typeof v === 'string' && v.length > 0;
}
/**
 * Pull a nested value out of a plain object using a dot-separated path.
 * Returns undefined for any missing intermediate.
 */
function readPath(obj, path) {
    if (!obj || typeof obj !== 'object')
        return undefined;
    const parts = path.split('.');
    let cur = obj;
    for (const p of parts) {
        if (cur == null || typeof cur !== 'object')
            return undefined;
        cur = cur[p];
    }
    return cur;
}
/**
 * Whether a registry entry lives in the workspace secrets.db at all
 * (vs bootstrap or user-* which live elsewhere).
 */
function isStoreManaged(c) {
    return c === 'workspace-secret' || c === 'workspace-setting';
}
/**
 * Best-effort wrapper that swallows store errors. Logs to stderr so a
 * lost mirror is visible without bringing the legacy write down with it.
 */
async function safeMirror(label, fn) {
    try {
        await fn();
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        // eslint-disable-next-line no-console
        console.warn(`[secrets] dual-write ${label} failed: ${msg}`);
    }
}
const DEFAULT_ACTOR = { type: 'system', systemId: 'dual-write' };
/**
 * Mirror a single value into the store under a canonical registry key.
 * Routes to the secret or settings table based on classification. Empty
 * strings and undefined skip the write entirely — callers that want to
 * clear a value should call the delete API explicitly. `$ENV_VAR`
 * placeholders are skipped too (see module comment).
 */
async function mirrorOne(def, value, ctx) {
    if (value === undefined || value === null || value === '')
        return;
    if (looksLikeEnvPlaceholder(value))
        return;
    if (!isStoreManaged(def.classification))
        return; // bootstrap or user-* — not the store's job.
    const store = getSecretStore();
    const isSecret = def.classification.endsWith('-secret');
    if (isSecret) {
        if (!isNonEmptyString(value))
            return;
        await store.set({ key: def.name, value }, ctx.actor);
        return;
    }
    // Setting: any JSON-serializable value goes through.
    await store.set({ key: def.name, value }, ctx.actor);
}
/**
 * Dual-write `user.json` fields into the encrypted store. The mapping
 * from user.json path → registry key is inlined below; Phase 12b
 * (2026-05-12) dropped the registry's `migrationSource` annotation
 * that used to drive this iteration, since it was the annotation's
 * sole remaining consumer (the importer was removed in Phase 12a).
 *
 * The actor is `{ type: 'system', systemId: 'dual-write' }` because the
 * caller is a file writer, not a user action; UI-driven changes that
 * route through the Phase 7 APIs supply a real user actor.
 */
export async function mirrorUserConfig(config, ctx = { actor: DEFAULT_ACTOR }) {
    if (!dualWriteEnabled())
        return;
    await safeMirror('user.json', async () => {
        const mappings = [
            { key: 'railway.token', path: 'railway_token' },
            { key: 'model.provider', path: 'model_provider' },
            { key: 'model.id', path: 'model_id' },
            { key: 'voyage.api_key', path: 'voyage_api_key' },
            { key: 'r2.access_key_id', path: 'r2.access_key_id' },
            { key: 'r2.secret_access_key', path: 'r2.secret_access_key' },
        ];
        for (const m of mappings) {
            const def = getKeyDef(m.key);
            if (!def)
                continue;
            const value = readPath(config, m.path);
            await mirrorOne(def, value, ctx);
        }
    });
}
/**
 * Dual-write a connector config. Iterates the connector-secret registry
 * entries and resolves their paths under the specific connector's config
 * (e.g., `headers.Authorization` → `connector.<name>.auth`).
 *
 * The registry uses pattern names like `connector.<name>.auth`; we expand
 * to a concrete key per connector at write time. `<name>` is the connector
 * name verbatim, lowercased for stability — connector names are already
 * url-safe slugs per `parseConfigBlock`.
 */
export async function mirrorConnectorConfig(config, ctx = { actor: DEFAULT_ACTOR }) {
    if (!dualWriteEnabled())
        return;
    if (!config?.name)
        return;
    const connector = String(config.name).toLowerCase();
    await safeMirror(`connector ${connector}`, async () => {
        const store = getSecretStore();
        // Authorization header → connector.<name>.auth
        const auth = config.headers?.['Authorization'] ?? config.headers?.['authorization'];
        if (isNonEmptyString(auth) && !looksLikeEnvPlaceholder(auth)) {
            await store.set({
                key: `connector.${connector}.auth`,
                value: auth,
                provider: connector,
            }, ctx.actor);
        }
        // OAuth client secret → connector.<name>.oauth_secret
        const oauthSecret = config.oauth?.client_secret;
        if (isNonEmptyString(oauthSecret) && !looksLikeEnvPlaceholder(oauthSecret)) {
            await store.set({
                key: `connector.${connector}.oauth_secret`,
                value: oauthSecret,
                provider: connector,
            }, ctx.actor);
        }
    });
}
/**
 * Dual-write workspace.json into the store. Covers the R2 credentials
 * (always-secret), the three R2 structural fields (workspace-setting,
 * added 2026-05-12 so the Settings UI can fully configure R2), and the
 * legacy search.* block.
 *
 * Why structural fields dual-write too: post-2026-05-12 the store is
 * the source of truth for R2 structural config, but `studiograph r2
 * setup` (and any other workspace.writeConfig caller) still writes the
 * file. Without this mirror, every CLI invocation would silently leave
 * the store stale. Pair this with the one-shot r2-structural-migration
 * that covers the first-boot case.
 */
export async function mirrorWorkspaceConfig(config, ctx = { actor: DEFAULT_ACTOR }) {
    if (!dualWriteEnabled())
        return;
    await safeMirror('workspace.json', async () => {
        // workspace.json has its own nesting (`r2_config.access_key_id`) that
        // doesn't match the registry's flat paths. Translate explicitly.
        const r2Mappings = [
            { key: 'r2.access_key_id', path: 'r2_config.access_key_id' },
            { key: 'r2.secret_access_key', path: 'r2_config.secret_access_key' },
            { key: 'r2.account_id', path: 'r2_config.account_id' },
            { key: 'r2.bucket', path: 'r2_config.bucket' },
            { key: 'r2.public_url', path: 'r2_config.public_url' },
        ];
        for (const m of r2Mappings) {
            const def = getKeyDef(m.key);
            if (!def)
                continue;
            const value = readPath(config, m.path);
            if (!isNonEmptyString(value))
                continue;
            await mirrorOne(def, value, ctx);
        }
        // Non-secret search settings live in workspace.json today (provider,
        // api_key, google_cx). Per plan §3 the search api_key is
        // secret-class; mirror it when present and non-placeholder.
        const searchApiKey = readPath(config, 'search.api_key');
        const searchProvider = readPath(config, 'search.provider');
        if (isNonEmptyString(searchApiKey) && !looksLikeEnvPlaceholder(searchApiKey)) {
            const key = searchProviderKey(typeof searchProvider === 'string' ? searchProvider : undefined);
            if (key) {
                const def = getKeyDef(key);
                if (def)
                    await mirrorOne(def, searchApiKey, ctx);
            }
        }
        const googleCx = readPath(config, 'search.google_cx');
        if (isNonEmptyString(googleCx)) {
            const def = getKeyDef('search.google.cx');
            if (def)
                await mirrorOne(def, googleCx, ctx);
        }
    });
}
function searchProviderKey(provider) {
    switch (provider) {
        case 'brave': return 'search.brave.api_key';
        case 'google': return 'search.google.api_key';
        case 'tavily': return 'search.tavily.api_key';
        default: return undefined;
    }
}
//# sourceMappingURL=dual-write.js.map