/**
 * IframeKeyVault
 *
 * KeyVault implementation that communicates with a cross-origin iframe
 * via postMessage. Private keys never leave the iframe — only signatures,
 * ciphertexts, and commitments cross the boundary.
 */
import { createChainClient } from '../core/chains';
import { IframeManager } from './iframe-manager';
import { MessageBus } from './message-bus';
import { VaultOps, getTimeout, DEFAULT_IFRAME_URL, PROTOCOL_ID, PROTOCOL_VERSION } from './protocol';
import { convertMapsToObjects, parseResponse, AuthenticateResultSchema, StoredSessionSchema, KeyStatusSchema, PublicKeysSchema, ShieldCommitmentResultSchema, DepositCiphertextResultSchema, EcdhKeysResultSchema, TransferCiphertextResultSchema, SpendingSignatureResultSchema, DigestResultSchema, ViewAuthSignatureResultSchema, RegistrationDataSchema, NullifiersResultSchema, HexResultSchema, } from './schemas';
export class IframeKeyVault {
    iframeUrl;
    iframeManager = null;
    messageBus = null;
    _isAuthenticated = false;
    _privacyAddress = null;
    _mpk = null;
    _disposed = false;
    _allowedOrigins = new Set();
    _chainId = 0;
    _rpcUrl;
    _publicClient = null;
    _walletBridge = null;
    _isDevMode = false;
    constructor(iframeUrl = DEFAULT_IFRAME_URL) {
        this.iframeUrl = iframeUrl;
    }
    ensureBus() {
        if (this._disposed)
            throw new Error('IframeKeyVault has been disposed');
        if (!this.messageBus)
            throw new Error('IframeKeyVault not initialized');
        return this.messageBus;
    }
    async send(op, payload, opts) {
        const bus = this.ensureBus();
        const response = await bus.request(op, payload, {
            timeout: opts?.timeout ?? getTimeout(op),
        });
        if (opts?.schema) {
            return parseResponse(opts.schema, response.payload, op);
        }
        return response.payload;
    }
    // -- Lifecycle --
    async initialize(config) {
        this.iframeManager = new IframeManager(this.iframeUrl);
        await this.iframeManager.create();
        this.messageBus = new MessageBus(this.iframeManager.contentWindow, this.iframeManager.origin);
        this._chainId = config.chainId;
        this._rpcUrl = config.rpcUrl;
        this._isDevMode = new URL(this.iframeUrl).hostname === 'localhost' ||
            new URL(this.iframeUrl).hostname === '127.0.0.1';
        // Build API bridge allowlist from configured origins
        try {
            this._allowedOrigins.add(new URL(config.serverUrl).origin);
        }
        catch { /* skip */ }
        if (config.rpcUrl) {
            try {
                this._allowedOrigins.add(new URL(config.rpcUrl).origin);
            }
            catch { /* skip */ }
        }
        // Single handler for all iframe → parent requests (wallet + API bridge)
        this.messageBus.onWalletRequest((type, payload) => this.handleBridgeRequest(type, payload));
        await this.send(VaultOps.INIT, { config });
    }
    dispose() {
        if (this._disposed)
            return;
        this._disposed = true;
        // Best-effort: tell iframe to zeroize keys before teardown.
        // Use postMessage directly to avoid the MessageBus request/response
        // machinery which would be rejected by the immediate destroy() below.
        if (this.messageBus && this.iframeManager) {
            try {
                this.iframeManager.contentWindow.postMessage({
                    protocol: PROTOCOL_ID,
                    version: PROTOCOL_VERSION,
                    id: 'dispose',
                    type: VaultOps.DISPOSE,
                    direction: 'request',
                }, this.iframeManager.origin);
            }
            catch {
                // Ignore — iframe may already be gone
            }
        }
        this.messageBus?.destroy();
        this.iframeManager?.destroy();
        this.messageBus = null;
        this.iframeManager = null;
        this._isAuthenticated = false;
        this._privacyAddress = null;
        this._mpk = null;
    }
    // -- Authentication --
    // Security note: when authenticate() returns 'mnemonicGenerated', the raw
    // mnemonic crosses the postMessage boundary so the parent can display it.
    // This is an intentional exception to the "keys never leave iframe" rule:
    // the mnemonic is a one-time backup artefact shown to the user, not a
    // long-lived key. It is cleared from the result object after proceed().
    async authenticate(keySource, tokenProviderCallback) {
        if (tokenProviderCallback !== null) {
            throw new Error('tokenProviderCallback is not supported in iframe mode. ' +
                'Callbacks cannot be serialized across the postMessage boundary.');
        }
        const result = await this.send(VaultOps.AUTHENTICATE, { keySource }, { schema: AuthenticateResultSchema });
        if (result.status === 'authenticated') {
            this._isAuthenticated = true;
            this._privacyAddress = result.privacyAddress ?? null;
            this._mpk = result.mpk ?? null;
        }
        return result;
    }
    async submitCredential(credential, tokenProviderCallback) {
        if (tokenProviderCallback !== null) {
            throw new Error('tokenProviderCallback is not supported in iframe mode. ' +
                'Callbacks cannot be serialized across the postMessage boundary.');
        }
        const result = await this.send(VaultOps.SUBMIT_CREDENTIAL, { credential }, { schema: AuthenticateResultSchema });
        if (result.status === 'authenticated') {
            this._isAuthenticated = true;
            this._privacyAddress = result.privacyAddress ?? null;
            this._mpk = result.mpk ?? null;
        }
        return result;
    }
    async proceedAfterMnemonic(credential, tokenProviderCallback) {
        if (tokenProviderCallback !== null) {
            throw new Error('tokenProviderCallback is not supported in iframe mode. ' +
                'Callbacks cannot be serialized across the postMessage boundary.');
        }
        const result = await this.send(VaultOps.PROCEED_AFTER_MNEMONIC, { credential }, { schema: AuthenticateResultSchema });
        if (result.status === 'authenticated') {
            this._isAuthenticated = true;
            this._privacyAddress = result.privacyAddress ?? null;
            this._mpk = result.mpk ?? null;
        }
        return result;
    }
    async logout() {
        await this.send(VaultOps.LOGOUT);
        this._isAuthenticated = false;
        this._privacyAddress = null;
        this._mpk = null;
    }
    async clearSession() {
        await this.send(VaultOps.CLEAR_SESSION);
        // Only clear the JWT/session on the iframe side. Keep _isAuthenticated,
        // _privacyAddress, and _mpk so keys remain available for quick re-auth.
    }
    // -- Session Persistence --
    async getStoredSession() {
        try {
            const result = await this.send(VaultOps.GET_STORED_SESSION, undefined, { schema: StoredSessionSchema });
            return result;
        }
        catch {
            return null;
        }
    }
    // -- Key Status --
    async getKeyStatus() {
        return this.send(VaultOps.GET_KEY_STATUS, undefined, {
            schema: KeyStatusSchema,
        });
    }
    async getPublicKeys() {
        return this.send(VaultOps.GET_PUBLIC_KEYS, undefined, {
            schema: PublicKeysSchema,
        });
    }
    isAuthenticated() {
        return this._isAuthenticated;
    }
    getPrivacyAddress() {
        return this._privacyAddress;
    }
    getMpk() {
        return this._mpk;
    }
    async deleteVault() {
        return this.send(VaultOps.DELETE_VAULT, {});
    }
    // -- Deposit Crypto --
    async buildShieldCommitment(params) {
        return this.send(VaultOps.BUILD_SHIELD_COMMITMENT, params, {
            schema: ShieldCommitmentResultSchema,
        });
    }
    async encryptDepositCiphertext(params) {
        return this.send(VaultOps.ENCRYPT_DEPOSIT_CIPHERTEXT, params, {
            schema: DepositCiphertextResultSchema,
        });
    }
    // -- Transfer / Withdraw Crypto --
    async computeNullifiers(notes) {
        return this.send(VaultOps.COMPUTE_NULLIFIERS, { notes }, {
            schema: NullifiersResultSchema,
        });
    }
    async generateNpk(recipientMpk, randomIn) {
        return this.send(VaultOps.GENERATE_NPK, { recipientMpk, randomIn }, {
            schema: HexResultSchema,
        });
    }
    async computeOutputCommitment(npk, tokenId, amount) {
        return this.send(VaultOps.COMPUTE_OUTPUT_COMMITMENT, { npk, tokenId, amount }, {
            schema: HexResultSchema,
        });
    }
    async deriveEcdhKeys(params) {
        return this.send(VaultOps.DERIVE_ECDH_KEYS, params, {
            schema: EcdhKeysResultSchema,
        });
    }
    async encryptTransferCiphertext(params) {
        return this.send(VaultOps.ENCRYPT_TRANSFER_CIPHERTEXT, params, {
            schema: TransferCiphertextResultSchema,
        });
    }
    async signSpending(digestHi, digestLo) {
        return this.send(VaultOps.SIGN_SPENDING, { digestHi, digestLo }, {
            schema: SpendingSignatureResultSchema,
        });
    }
    // -- Digest Computation --
    async computeTransferDigest(params) {
        return this.send(VaultOps.COMPUTE_TRANSFER_DIGEST, params, {
            schema: DigestResultSchema,
        });
    }
    async computeWithdrawalDigest(params) {
        return this.send(VaultOps.COMPUTE_WITHDRAWAL_DIGEST, params, {
            schema: DigestResultSchema,
        });
    }
    async computeWithdrawalCommitment(recipientAddr, tokenId, amount) {
        return this.send(VaultOps.COMPUTE_WITHDRAWAL_COMMITMENT, { recipientAddr, tokenId, amount }, {
            schema: HexResultSchema,
        });
    }
    // -- Auth Crypto --
    async signViewAuth(mpk, nonce) {
        return this.send(VaultOps.SIGN_VIEW_AUTH, { mpk, nonce }, {
            schema: ViewAuthSignatureResultSchema,
        });
    }
    async getRegistrationData() {
        return this.send(VaultOps.GET_REGISTRATION_DATA, undefined, {
            schema: RegistrationDataSchema,
        });
    }
    // -- High-level Operations --
    async shield(tokenAddress, amount, 
    // TODO(progress): pipe progress events through the iframe message bus as a
    // streaming response. For now the iframe path simply doesn't emit per-step
    // progress; callers fall back to a single pending state.
    _onProgress) {
        return this.send(VaultOps.SHIELD, { tokenAddress, amount });
    }
    async sendTransfer(tokenAddress, amount, recipientPrivacyAddress) {
        return this.send(VaultOps.SEND, { tokenAddress, amount, recipientPrivacyAddress });
    }
    async unshield(tokenAddress, amount, recipientAddress, 
    // TODO(progress): see shield() above.
    _onProgress) {
        return this.send(VaultOps.UNSHIELD, { tokenAddress, amount, recipientAddress });
    }
    async consolidateNotes(tokenAddress, maxRounds) {
        return this.send(VaultOps.CONSOLIDATE_NOTES, { tokenAddress, maxRounds });
    }
    async unwrapWeth(amount, recipientAddress) {
        return this.send(VaultOps.UNWRAP_WETH, { amount, recipientAddress });
    }
    getOrCreatePublicClient() {
        if (!this._publicClient) {
            this._publicClient = createChainClient(this._chainId, this._rpcUrl);
        }
        return this._publicClient;
    }
    // -- Bridge Handler (wallet + API fetch) --
    async handleBridgeRequest(type, payload) {
        const p = (typeof payload === 'object' && payload !== null
            ? payload
            : {});
        // API fetch proxy
        if (type === VaultOps.API_FETCH) {
            return this.handleApiFetch(payload);
        }
        // Receipt polling uses the public client, not the wallet adapter
        if (type === VaultOps.WALLET_WAIT_RECEIPT) {
            if (typeof p.txHash !== 'string') {
                throw new Error('wallet.waitForTransactionReceipt: txHash must be a string');
            }
            const client = this.getOrCreatePublicClient();
            const receipt = await client.waitForTransactionReceipt({
                hash: p.txHash,
            });
            return {
                transactionHash: receipt.transactionHash,
                status: receipt.status === 'success',
                logs: receipt.logs.map((log) => ({
                    address: log.address,
                    topics: [...log.topics],
                    data: log.data,
                })),
            };
        }
        // Wallet operations requiring an adapter
        const adapter = this._walletBridge;
        if (!adapter) {
            throw new Error('No wallet connected');
        }
        switch (type) {
            case VaultOps.WALLET_SIGN_MESSAGE: {
                if (typeof p.message !== 'string') {
                    throw new Error('wallet.signMessage: message must be a string');
                }
                return adapter.signMessage(p.message);
            }
            case VaultOps.WALLET_SIGN_TYPED_DATA: {
                if (typeof p.typedData !== 'string') {
                    throw new Error('wallet.signTypedData: typedData must be a string');
                }
                return adapter.signTypedData(p.typedData);
            }
            case VaultOps.WALLET_SEND_TRANSACTION: {
                const tx = p.tx;
                if (typeof tx !== 'object' || tx === null) {
                    throw new Error('wallet.sendTransaction: tx must be an object');
                }
                if (typeof tx.to !== 'string' || !/^0x[0-9a-fA-F]{40}$/.test(tx.to)) {
                    throw new Error('wallet.sendTransaction: tx.to must be a valid hex address');
                }
                return adapter.sendTransaction(tx);
            }
            case VaultOps.WALLET_GET_ADDRESS:
                return adapter.getAddress();
            case VaultOps.WALLET_GET_CHAIN_ID:
                return adapter.getChainId();
            default:
                throw new Error(`Unknown bridge operation: ${type}`);
        }
    }
    static MAX_PROXY_RESPONSE_SIZE = 10 * 1024 * 1024; // 10 MB
    static MAX_PROXY_REQUEST_BODY_SIZE = 1 * 1024 * 1024; // 1 MB
    // Headers that must not be forwarded from iframe requests
    static BLOCKED_REQUEST_HEADERS = new Set([
        'cookie', 'set-cookie', 'host', 'origin', 'referer',
    ]);
    async handleApiFetch(req) {
        const parsed = new URL(req.url);
        const isLocalhost = parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1';
        if (parsed.protocol !== 'https:' && !(parsed.protocol === 'http:' && isLocalhost)) {
            throw new Error(`API bridge: blocked request to ${req.url} (only HTTPS or localhost HTTP allowed)`);
        }
        // Localhost is only allowed in dev mode (iframe served from localhost)
        if (isLocalhost && !this._isDevMode) {
            throw new Error(`API bridge: localhost requests are only allowed in dev mode`);
        }
        if (!isLocalhost && !this._allowedOrigins.has(parsed.origin)) {
            throw new Error(`API bridge: blocked request to ${parsed.origin} (not in allowed origins)`);
        }
        // Validate request body size
        if (req.body && req.body.length > IframeKeyVault.MAX_PROXY_REQUEST_BODY_SIZE) {
            throw new Error(`API bridge: request body too large (${req.body.length} chars, limit ${IframeKeyVault.MAX_PROXY_REQUEST_BODY_SIZE})`);
        }
        // Strip sensitive headers
        const sanitizedHeaders = {};
        for (const [k, v] of Object.entries(req.headers)) {
            if (!IframeKeyVault.BLOCKED_REQUEST_HEADERS.has(k.toLowerCase())) {
                sanitizedHeaders[k] = v;
            }
        }
        const response = await fetch(req.url, {
            method: req.method,
            headers: sanitizedHeaders,
            body: req.body,
        });
        // Check Content-Length before buffering the full response
        const contentLength = response.headers.get('content-length');
        if (contentLength && parseInt(contentLength, 10) > IframeKeyVault.MAX_PROXY_RESPONSE_SIZE) {
            throw new Error(`API bridge: response too large (Content-Length: ${contentLength}, limit ${IframeKeyVault.MAX_PROXY_RESPONSE_SIZE})`);
        }
        const responseHeaders = {};
        response.headers.forEach((v, k) => { responseHeaders[k] = v; });
        const body = await response.text();
        if (body.length > IframeKeyVault.MAX_PROXY_RESPONSE_SIZE) {
            throw new Error(`API bridge: response too large (${body.length} chars, limit ${IframeKeyVault.MAX_PROXY_RESPONSE_SIZE})`);
        }
        return {
            status: response.status,
            statusText: response.statusText,
            headers: responseHeaders,
            body,
        };
    }
    // -- HTTP Proxy --
    /**
     * Proxy an HTTP request through the iframe's WASM encryption layer.
     *
     * In iframe mode, the parent cannot make direct API calls to the server
     * because the server requires encrypted POST envelopes. This method
     * routes the request through the iframe → WASM EncryptedHttpClient →
     * api-bridge → parent (actual HTTP call) → back through the chain.
     */
    async proxyFetch(path, method, body, query, baseUrl, authToken) {
        // Dynamically allow the chain's indexer origin for the API bridge
        if (baseUrl) {
            try {
                this._allowedOrigins.add(new URL(baseUrl).origin);
            }
            catch { /* skip */ }
        }
        const result = await this.send(VaultOps.HTTP_PROXY, { path, method, body, query, baseUrl, authToken: authToken ?? undefined });
        // serde_wasm_bindgen serializes Rust maps as JS Map objects, which survive
        // postMessage's structured clone. Convert them to plain objects so the
        // parent's IndexerClient/AuthClient can work with them normally.
        return convertMapsToObjects(result);
    }
    // -- Wallet Bridge --
    setWalletBridge(bridge) {
        this._walletBridge = bridge;
    }
}
//# sourceMappingURL=IframeKeyVault.js.map