"use strict";
/**
 * Reads and writes OAuth tokens and wizard-scoped session data under
 * `~/.amplitude/wizard/oauth-session.json` (canonical). Migrates from legacy
 * `~/.ampli.json` on first read when the canonical file has no OAuth entries.
 *
 * JSON shape matches the historical on-disk format (nested `User-*` entries
 * with OAuth* keys) so tokens remain portable across wizard versions.
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.AMPLI_CONFIG_PATH = void 0;
exports.getStoredUser = getStoredUser;
exports.getStoredToken = getStoredToken;
exports.storeToken = storeToken;
exports.replaceStoredUser = replaceStoredUser;
exports.clearStoredCredentials = clearStoredCredentials;
exports.getLastUsedSelection = getLastUsedSelection;
exports.storeLastUsedSelection = storeLastUsedSelection;
exports.updateStoredUserZone = updateStoredUserZone;
const fs = __importStar(require("node:fs"));
const zod_1 = require("zod");
const constants_js_1 = require("../lib/constants.js");
const logger_js_1 = require("../lib/observability/logger.js");
const atomic_write_js_1 = require("./atomic-write.js");
const jwt_exp_js_1 = require("./jwt-exp.js");
const storage_paths_js_1 = require("./storage-paths.js");
const log = (0, logger_js_1.createLogger)('ampli-settings');
/** @deprecated Use {@link getOAuthSettingsFile} — kept for tests and imports. */
exports.AMPLI_CONFIG_PATH = (0, storage_paths_js_1.getLegacyAmpliHomeOAuthPath)();
const AmpliSettingsFileSchema = zod_1.z.record(zod_1.z.string(), zod_1.z.unknown());
function readConfigFileDisk(pathname) {
    try {
        const raw = fs.readFileSync(pathname, 'utf-8');
        const result = AmpliSettingsFileSchema.safeParse(JSON.parse(raw));
        return result.success ? result.data : {};
    }
    catch {
        return {};
    }
}
function oauthEntriesPresent(config) {
    return Object.keys(config).some((k) => isUserKey(k));
}
/**
 * When `explicitPath` is set, only that file is read (tests / special cases).
 * Otherwise merge canonical + legacy: prefer canonical when it already holds
 * OAuth user keys; otherwise load legacy, migrate forward best-effort.
 */
function readConfig(explicitPath) {
    if (explicitPath) {
        return readConfigFileDisk(explicitPath);
    }
    const primaryPath = (0, storage_paths_js_1.getOAuthSettingsFile)();
    const legacyPath = (0, storage_paths_js_1.getLegacyAmpliHomeOAuthPath)();
    const primary = readConfigFileDisk(primaryPath);
    const legacy = readConfigFileDisk(legacyPath);
    if (oauthEntriesPresent(primary)) {
        return primary;
    }
    // Only migrate from legacy when the primary file doesn't exist on disk.
    // If primary exists (even as `{}`), it was written intentionally (e.g. by
    // clearStoredCredentials) and legacy data must not be resurrected.
    const primaryFileExists = fs.existsSync(primaryPath);
    if (!primaryFileExists && oauthEntriesPresent(legacy)) {
        const merged = { ...legacy, ...primary };
        try {
            (0, storage_paths_js_1.ensureDir)((0, storage_paths_js_1.getCacheRoot)());
            (0, atomic_write_js_1.atomicWriteJSON)(primaryPath, merged, 0o600);
        }
        catch (err) {
            log.debug('readConfig: could not migrate legacy OAuth file forward', {
                'error message': err instanceof Error ? err.message : String(err),
            });
        }
        return merged;
    }
    return Object.keys(primary).length > 0 ? primary : legacy;
}
function writeConfig(data, explicitPath) {
    if (explicitPath) {
        (0, atomic_write_js_1.atomicWriteJSON)(explicitPath, data, 0o600);
        return;
    }
    const primaryPath = (0, storage_paths_js_1.getOAuthSettingsFile)();
    const legacyPath = (0, storage_paths_js_1.getLegacyAmpliHomeOAuthPath)();
    try {
        (0, storage_paths_js_1.ensureDir)((0, storage_paths_js_1.getCacheRoot)());
        (0, atomic_write_js_1.atomicWriteJSON)(primaryPath, data, 0o600);
    }
    catch (err) {
        log.warn('writeConfig: failed to write canonical OAuth session file', {
            'error message': err instanceof Error ? err.message : String(err),
        });
    }
    try {
        (0, atomic_write_js_1.atomicWriteJSON)(legacyPath, data, 0o600);
    }
    catch (err) {
        log.debug('writeConfig: legacy OAuth mirror write failed (non-fatal)', {
            'error message': err instanceof Error ? err.message : String(err),
        });
    }
}
function userKey(userId, zone) {
    const safeId = userId.replace(/\./g, '-');
    return zone !== constants_js_1.DEFAULT_AMPLITUDE_ZONE
        ? `User[${zone}]-${safeId}`
        : `User-${safeId}`;
}
function isUserKey(key) {
    return key.startsWith('User-') || key.startsWith('User[');
}
/**
 * True iff a given session-store key belongs to the requested zone.
 *
 * Key shapes:
 *   - US (the default zone): `User-<userId>`
 *   - Non-default zones:     `User[<zone>]-<userId>`
 *
 * Without this filter, `getStoredToken(undefined, 'eu')` would happily return
 * a US session because `isUserKey('User-…')` is true regardless of zone.
 * That made `/region` switches silently reuse the wrong-zone OAuth token,
 * fall back to a fresh browser login at the wrong host, and leave the user
 * staring at a US auth URL after picking EU.
 */
function isUserKeyForZone(key, zone) {
    if (zone === constants_js_1.DEFAULT_AMPLITUDE_ZONE) {
        // US keys are unbracketed: `User-…` but never `User[…]-…`.
        return key.startsWith('User-');
    }
    return key.startsWith(`User[${zone}]-`);
}
const StoredUserSchema = zod_1.z.object({
    id: zod_1.z.string(),
    firstName: zod_1.z.string(),
    lastName: zod_1.z.string(),
    email: zod_1.z.string(),
    zone: zod_1.z.string(),
    tosAccepted: zod_1.z.boolean().optional(),
    tosAcceptedAt: zod_1.z.string().optional(),
});
const UserEntrySchema = zod_1.z
    .object({
    User: StoredUserSchema.optional(),
})
    .passthrough();
/** Returns the first real (non-pending) stored user, or undefined if none. */
function getStoredUser(configPath) {
    const config = readConfig(configPath);
    let fallback;
    for (const [key, value] of Object.entries(config)) {
        if (!isUserKey(key))
            continue;
        const entry = UserEntrySchema.safeParse(value);
        if (!entry.success || !entry.data.User)
            continue;
        const user = entry.data.User;
        // Skip the "pending" placeholder — prefer a real user with an actual ID
        if (user.id === 'pending') {
            fallback = fallback ?? user;
            continue;
        }
        return user;
    }
    return fallback;
}
/** Returns a valid (non-expired) stored token, or undefined. */
function getStoredToken(userId, zone = constants_js_1.DEFAULT_AMPLITUDE_ZONE, configPath) {
    const config = readConfig(configPath);
    const OAuthEntrySchema = zod_1.z
        .object({
        OAuthAccessToken: zod_1.z.string(),
        OAuthIdToken: zod_1.z.string(),
        OAuthRefreshToken: zod_1.z.string(),
        OAuthExpiresAt: zod_1.z.string(),
    })
        .passthrough();
    const findToken = (key) => {
        const parsed = OAuthEntrySchema.safeParse(config[key]);
        if (!parsed.success)
            return undefined;
        const entry = parsed.data;
        const expiresAt = new Date(entry.OAuthExpiresAt);
        const refreshExpiry = new Date(expiresAt.getTime() + 364 * 24 * 60 * 60 * 1000);
        if (new Date() > refreshExpiry)
            return undefined;
        // Drop tokens that were minted against a different OAuth client/issuer
        // than this wizard build is configured to use. Catches the "upgraded
        // from an old wizard version" case where the refresh-window check would
        // otherwise return a token whose audience/issuer no longer matches the
        // current AMPLITUDE_ZONE_SETTINGS — leading to silent 401s mid-run.
        //
        // Only enforced when the token actually carries `iss`/`aud` claims and
        // the expected values are known. Tokens without claims (or where decode
        // fails) fall through to the pre-existing behavior — never strictly
        // worse than before.
        const expected = constants_js_1.AMPLITUDE_ZONE_SETTINGS[zone];
        const claims = (0, jwt_exp_js_1.decodeJwtIssAud)(entry.OAuthIdToken);
        if (claims && expected) {
            // Compare only the host portion of `iss` against `oAuthHost`. Ory
            // mints tokens with `iss = "<oAuthHost>/"` (trailing slash); we strip
            // it for a robust prefix-style check that survives minor path tweaks.
            const expectedHost = (() => {
                try {
                    return new URL(expected.oAuthHost).host;
                }
                catch {
                    return null;
                }
            })();
            const issHost = claims.iss
                ? (() => {
                    try {
                        return new URL(claims.iss).host;
                    }
                    catch {
                        return null;
                    }
                })()
                : null;
            if (expectedHost &&
                issHost &&
                issHost !== expectedHost &&
                // Allow legacy tokens that omit `iss` entirely — only reject explicit
                // mismatches.
                true) {
                log.debug('getStoredToken: dropping token with mismatched issuer', {
                    key,
                    expectedHost,
                    issHost,
                });
                return undefined;
            }
            if (claims.aud &&
                claims.aud.length > 0 &&
                !claims.aud.includes(expected.oAuthClientId)) {
                log.debug('getStoredToken: dropping token with mismatched audience', {
                    key,
                    expectedClientId: expected.oAuthClientId,
                    aud: claims.aud,
                });
                return undefined;
            }
        }
        return {
            accessToken: entry.OAuthAccessToken,
            idToken: entry.OAuthIdToken,
            refreshToken: entry.OAuthRefreshToken,
            expiresAt: entry.OAuthExpiresAt,
        };
    };
    if (userId) {
        return findToken(userKey(userId, zone));
    }
    // Try all stored users for the requested zone. Filtering by zone is what
    // makes `/region` switches actually re-authenticate against the new data
    // center: without it, a stored US token would be returned for an EU lookup
    // (since both `User-…` and `User[eu]-…` pass the generic `isUserKey` test),
    // and `performAmplitudeAuth` would skip the browser entirely.
    for (const key of Object.keys(config)) {
        if (!isUserKeyForZone(key, zone))
            continue;
        const token = findToken(key);
        if (token)
            return token;
    }
    return undefined;
}
/** Persists OAuth tokens to the wizard session store (and legacy mirror). */
function storeToken(user, token, configPath) {
    const config = readConfig(configPath);
    const key = userKey(user.id, user.zone);
    config[key] = {
        ...(config[key] ?? {}),
        User: user,
        OAuthAccessToken: token.accessToken,
        OAuthIdToken: token.idToken,
        OAuthRefreshToken: token.refreshToken,
        OAuthExpiresAt: token.expiresAt,
    };
    writeConfig(config, configPath);
}
/** Persists a user + token as the sole stored account, wiping any prior User entries. */
function replaceStoredUser(user, token, configPath) {
    const config = readConfig(configPath);
    const wiped = [];
    for (const k of Object.keys(config)) {
        if (isUserKey(k)) {
            delete config[k];
            wiped.push(k);
        }
    }
    if (wiped.length > 0) {
        log.debug('replaceStoredUser: wiped prior user entries', {
            count: wiped.length,
            keys: wiped,
        });
    }
    const key = userKey(user.id, user.zone);
    // No spread needed: the loop above deleted every User-* key, so there is
    // nothing to preserve at `config[key]`.
    config[key] = {
        User: user,
        OAuthAccessToken: token.accessToken,
        OAuthIdToken: token.idToken,
        OAuthRefreshToken: token.refreshToken,
        OAuthExpiresAt: token.expiresAt,
    };
    writeConfig(config, configPath);
}
/** Clears all stored credentials by writing an empty config. */
function clearStoredCredentials(configPath) {
    writeConfig({}, configPath);
}
// ── Wizard-scoped settings namespace ──────────────────────────────────
//
// Reserved top-level keys use the `User-*` prefix for OAuth entries.
// Wizard-only settings live under the `wizard` key.
const WizardNamespaceSchema = zod_1.z
    .object({
    lastUsedOrgId: zod_1.z.string().optional(),
    lastUsedWorkspaceId: zod_1.z.string().optional(),
    // Matches `selectedAppId` on the wizard session — both refer to the
    // numeric Amplitude app/environment id. Keep the terminology in sync
    // so a future caller doesn't reintroduce the retired `project` term.
    lastUsedAppId: zod_1.z.string().optional(),
})
    .passthrough();
function readWizardNamespace(configPath) {
    const config = readConfig(configPath);
    const parsed = WizardNamespaceSchema.safeParse(config['wizard'] ?? {});
    return parsed.success ? parsed.data : {};
}
function writeWizardNamespace(next, configPath) {
    const config = readConfig(configPath);
    config['wizard'] = next;
    writeConfig(config, configPath);
}
/**
 * Returns the last-used org/workspace/app selection triple. Each field is
 * individually optional — a user who has never had a workspace picked can
 * still have an orgId from an org-only run.
 *
 * Field naming matches the canonical session shape (`selectedAppId`,
 * `selectedWorkspaceId`, `selectedOrgId`) so callers can spread directly
 * into the picker pre-focus logic without re-keying.
 */
function getLastUsedSelection(configPath) {
    const ns = readWizardNamespace(configPath);
    return {
        orgId: ns.lastUsedOrgId,
        workspaceId: ns.lastUsedWorkspaceId,
        appId: ns.lastUsedAppId,
    };
}
/**
 * Persist the last-used selection triple. Pass undefined to clear a level
 * (e.g. when the user selects a different org, the old workspace/app
 * shouldn't pre-focus the picker anymore). Other wizard-scoped settings
 * inside the `wizard` namespace are preserved.
 */
function storeLastUsedSelection(selection, configPath) {
    const current = readWizardNamespace(configPath);
    writeWizardNamespace({
        ...current,
        lastUsedOrgId: selection.orgId,
        lastUsedWorkspaceId: selection.workspaceId,
        lastUsedAppId: selection.appId,
    }, configPath);
}
/**
 * Updates the stored user's zone in the session store, migrating the entry to the
 * new zone key. Returns the updated user, or undefined if no user is stored.
 */
function updateStoredUserZone(newZone, configPath) {
    const user = getStoredUser(configPath);
    if (!user)
        return undefined;
    const config = readConfig(configPath);
    const oldKey = userKey(user.id, user.zone);
    const newKey = userKey(user.id, newZone);
    const entry = config[oldKey];
    if (entry !== undefined) {
        if (oldKey !== newKey) {
            delete config[oldKey];
        }
        config[newKey] = {
            ...entry,
            User: { ...user, zone: newZone },
        };
    }
    writeConfig(config, configPath);
    return { ...user, zone: newZone };
}
