/**
 * In-memory stores for the OAuth 2.1 authorization server.
 *
 * All state is process-local — matching mailpouch's single-user design.
 * A restart drops outstanding auth codes (short TTL anyway) and tokens
 * (requiring clients to re-auth); registered DCR clients are re-provisioned
 * on demand because MCP hosts will call the register endpoint again.
 *
 * Rationale for not persisting to disk:
 *   - Keeps the attack surface small (no on-disk tokens to leak).
 *   - Tokens in this deployment are long-lived enough for an interactive
 *     session but re-issue is cheap.
 *   - Persistence is a follow-up PR when we tackle multi-instance or
 *     survivable restarts.
 */
import { createHash, randomBytes, randomUUID } from "crypto";
export const OAUTH_CODE_TTL_MS = 60_000; // RFC 6749 §4.1.2: MAX 10 min; we go short
export const OAUTH_ACCESS_TOKEN_TTL_MS = 24 * 60 * 60_000; // 24 h — self-host session
/**
 * Absolute ceilings. The DCR endpoint is rate-limited per IP upstream, so
 * a friendly client won't hit these. The caps exist so a broken or hostile
 * client can't grow the Maps without bound between sweep() calls.
 *
 * When a cap is hit we evict the oldest entry rather than refusing the
 * new one — matches the "self-host, keep the live session working"
 * philosophy. Evicted clients will simply re-register on their next use.
 */
export const OAUTH_MAX_CLIENTS = 1000;
export const OAUTH_MAX_CODES = 500;
export const OAUTH_MAX_TOKENS = 5000;
export class OAuthStore {
    clients = new Map();
    codes = new Map();
    /**
     * XPORT-005: tokens are keyed by sha256(token), never the raw token. The
     * user-supplied bearer is hashed before the Map lookup, so the comparison is
     * over fixed-length digests rather than letting V8's hash-table short-circuit
     * on the first byte of the attacker-controlled string — consistent with the
     * `timingSafeEqual` posture the rest of the codebase uses. The reverse index
     * stores hashes too.
     */
    tokens = new Map();
    /** Reverse index clientId → token-hashes, so revoking a grant can invalidate
     *  all outstanding access tokens for that client immediately rather than
     *  waiting for the 24 h TTL to expire. Kept consistent with `tokens` via the
     *  issueToken / revokeToken / evict / sweep paths. */
    tokensByClient = new Map();
    /** sha256(token) hex — the key under which a token's record lives. */
    static hashToken(token) {
        return createHash("sha256").update(token, "utf-8").digest("hex");
    }
    /** Drop the oldest entry from a Map (relies on Map's insertion-order iteration). */
    evictOldest(m) {
        const first = m.keys().next();
        if (!first.done)
            m.delete(first.value);
    }
    indexToken(rec) {
        let set = this.tokensByClient.get(rec.clientId);
        if (!set) {
            set = new Set();
            this.tokensByClient.set(rec.clientId, set);
        }
        set.add(OAuthStore.hashToken(rec.token));
    }
    /** `hash` is sha256(token) — the same key used in the primary tokens Map. */
    unindexToken(hash, clientId) {
        const set = this.tokensByClient.get(clientId);
        if (!set)
            return;
        set.delete(hash);
        if (set.size === 0)
            this.tokensByClient.delete(clientId);
    }
    registerClient(client) {
        const id = `pmc_${randomUUID().replace(/-/g, "")}`;
        const now = Math.floor(Date.now() / 1000);
        const record = {
            client_id: id,
            client_id_issued_at: now,
            ...client,
        };
        if (this.clients.size >= OAUTH_MAX_CLIENTS)
            this.evictOldest(this.clients);
        this.clients.set(id, record);
        return record;
    }
    getClient(id) {
        return this.clients.get(id);
    }
    /** Allocate a new one-shot authorization code. */
    issueAuthCode(params) {
        const code = randomBytes(32).toString("base64url");
        const record = { code, createdAt: Date.now(), ...params };
        if (this.codes.size >= OAUTH_MAX_CODES)
            this.evictOldest(this.codes);
        this.codes.set(code, record);
        return record;
    }
    /** Consume a code (always deletes, returns record only if still valid). */
    consumeAuthCode(code) {
        const rec = this.codes.get(code);
        if (!rec)
            return null;
        this.codes.delete(code); // single-use — always drop regardless of expiry
        if (Date.now() - rec.createdAt > OAUTH_CODE_TTL_MS)
            return null;
        return rec;
    }
    issueToken(args) {
        const token = randomBytes(32).toString("base64url");
        const rec = {
            token,
            clientId: args.clientId,
            scopes: args.scopes,
            resource: args.resource,
            expiresAt: Date.now() + OAUTH_ACCESS_TOKEN_TTL_MS,
            issuedFromIp: args.issuedFromIp,
        };
        if (this.tokens.size >= OAUTH_MAX_TOKENS) {
            const first = this.tokens.keys().next();
            if (!first.done) {
                const old = this.tokens.get(first.value);
                this.tokens.delete(first.value);
                if (old)
                    this.unindexToken(first.value, old.clientId);
            }
        }
        this.tokens.set(OAuthStore.hashToken(token), rec);
        this.indexToken(rec);
        return rec;
    }
    verifyToken(token) {
        const hash = OAuthStore.hashToken(token);
        const rec = this.tokens.get(hash);
        if (!rec)
            return null;
        if (Date.now() > rec.expiresAt) {
            this.tokens.delete(hash);
            this.unindexToken(hash, rec.clientId);
            return null;
        }
        return rec;
    }
    revokeToken(token) {
        const hash = OAuthStore.hashToken(token);
        const rec = this.tokens.get(hash);
        const removed = this.tokens.delete(hash);
        if (removed && rec)
            this.unindexToken(hash, rec.clientId);
        return removed;
    }
    /** Drop every outstanding access token for `clientId`. Returns the number
     *  of tokens revoked. Called when a per-agent grant is denied / revoked /
     *  expires so that ongoing requests holding the token can't continue past
     *  the policy change. */
    revokeTokensForClient(clientId) {
        const set = this.tokensByClient.get(clientId);
        if (!set || set.size === 0)
            return 0;
        let n = 0;
        for (const token of set) {
            if (this.tokens.delete(token))
                n++;
        }
        this.tokensByClient.delete(clientId);
        return n;
    }
    /**
     * Drop expired codes + tokens. Safe to call periodically — O(n) scan, but
     * n is small in a single-user deployment (usually ≤ dozens).
     */
    sweep(now = Date.now()) {
        let codes = 0;
        let tokens = 0;
        for (const [k, v] of this.codes) {
            if (now - v.createdAt > OAUTH_CODE_TTL_MS) {
                this.codes.delete(k);
                codes++;
            }
        }
        for (const [k, v] of this.tokens) {
            if (now > v.expiresAt) {
                this.tokens.delete(k);
                this.unindexToken(k, v.clientId);
                tokens++;
            }
        }
        return { codes, tokens };
    }
    stats() {
        return { clients: this.clients.size, codes: this.codes.size, tokens: this.tokens.size };
    }
}
//# sourceMappingURL=oauth-store.js.map