"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CWIStyleWalletManager = exports.OverlayUMPTokenInteractor = exports.DEFAULT_PROFILE_ID = exports.ARGON2ID_DEFAULT_HASH_LENGTH = exports.ARGON2ID_DEFAULT_PARALLELISM = exports.ARGON2ID_DEFAULT_MEMORY_KIB = exports.ARGON2ID_DEFAULT_ITERATIONS = exports.PBKDF2_NUM_ROUNDS = void 0;
const sdk_1 = require("@bsv/sdk");
const hash_wasm_1 = require("hash-wasm");
const PrivilegedKeyManager_1 = require("./sdk/PrivilegedKeyManager");
/**
 * Number of rounds used in PBKDF2 for deriving password keys.
 */
exports.PBKDF2_NUM_ROUNDS = 7777;
/**
 * Default Argon2id parameters for password-key derivation (UMP v3).
 */
exports.ARGON2ID_DEFAULT_ITERATIONS = 7;
exports.ARGON2ID_DEFAULT_MEMORY_KIB = 131072;
exports.ARGON2ID_DEFAULT_PARALLELISM = 1;
exports.ARGON2ID_DEFAULT_HASH_LENGTH = 32;
function isLikelyDerSignatureField(field) {
    if (!field || field.length < 8 || field.length > 80) {
        return false;
    }
    if (field[0] !== 0x30) {
        return false;
    }
    const declaredLen = field[1];
    return declaredLen === field.length - 2;
}
function stripVerifiedPushDropSignature(fields, lockingPublicKey) {
    if (fields.length <= 11) {
        return fields;
    }
    const protocolFieldCount = fields.length - 1;
    const trailingField = fields[protocolFieldCount];
    if (!trailingField || !isLikelyDerSignatureField(trailingField)) {
        return fields;
    }
    try {
        let dataLength = 0;
        for (let i = 0; i < protocolFieldCount; i++) {
            dataLength += fields[i].length;
        }
        const dataToVerify = new Array(dataLength);
        let writeOffset = 0;
        for (let i = 0; i < protocolFieldCount; i++) {
            const field = fields[i];
            for (let j = 0; j < field.length; j++) {
                dataToVerify[writeOffset++] = field[j];
            }
        }
        const signature = sdk_1.Signature.fromDER(trailingField);
        if (signature.verify(dataToVerify, lockingPublicKey)) {
            return fields.slice(0, protocolFieldCount);
        }
    }
    catch (_a) {
        return fields;
    }
    return fields;
}
/**
 * PBKDF-2 that prefers WebCrypto and falls back to hash-wasm.
 *
 * @param passwordBytes   Raw password bytes.
 * @param salt            Salt bytes.
 * @param iterations      Number of rounds.
 * @param keyLen          Desired key length in bytes.
 * @param hash            Digest algorithm (default "sha512").
 * @returns               Derived key bytes.
 */
async function pbkdf2NativeOrWasm(passwordBytes, salt, iterations, keyLen, hash = 'sha512') {
    var _a;
    // ----- fast-path: WebCrypto (both browser & recent Node expose globalThis.crypto.subtle)
    const subtle = (_a = globalThis === null || globalThis === void 0 ? void 0 : globalThis.crypto) === null || _a === void 0 ? void 0 : _a.subtle;
    if (subtle) {
        try {
            const baseKey = await subtle.importKey('raw', new Uint8Array(passwordBytes), { name: 'PBKDF2' }, 
            /*extractable*/ false, ['deriveBits']);
            const bits = await subtle.deriveBits({
                name: 'PBKDF2',
                salt: new Uint8Array(salt),
                iterations,
                hash: hash.toUpperCase()
            }, baseKey, keyLen * 8);
            return Array.from(new Uint8Array(bits));
        }
        catch (err) {
            //console.warn('[pbkdf2] WebCrypto path failed → falling back to JS implementation', err)
            /* fall through */
        }
    }
    const hashFunction = hash === 'sha256' ? (0, hash_wasm_1.createSHA256)() : (0, hash_wasm_1.createSHA512)();
    const derived = await (0, hash_wasm_1.pbkdf2)({
        password: new Uint8Array(passwordBytes),
        salt: new Uint8Array(salt),
        iterations,
        hashLength: keyLen,
        hashFunction,
        outputType: 'binary'
    });
    return Array.from(derived);
}
/**
 * Derives the password key from a password using the KDF specified in the UMP token.
 * For legacy tokens (no KDF metadata), uses PBKDF2 with fixed rounds (7777).
 * For v3 tokens, uses the algorithm specified in passwordKdf metadata.
 *
 * @param token          The UMP token containing KDF metadata and salt.
 * @param passwordBytes  Raw password bytes.
 * @param overrideKdf    Optional KDF config to override token/default settings.
 * @returns              Derived password key bytes.
 */
async function derivePasswordKey(token, passwordBytes, overrideKdf) {
    var _a, _b, _c, _d, _e;
    const kdf = overrideKdf || token.passwordKdf;
    // Legacy token or explicit PBKDF2 request
    if (!kdf || kdf.algorithm === 'pbkdf2-sha512') {
        return pbkdf2NativeOrWasm(passwordBytes, token.passwordSalt, (_a = kdf === null || kdf === void 0 ? void 0 : kdf.iterations) !== null && _a !== void 0 ? _a : exports.PBKDF2_NUM_ROUNDS, 32, 'sha512');
    }
    // Argon2id path (UMP v3)
    if (kdf.algorithm === 'argon2id') {
        const iterations = (_b = kdf.iterations) !== null && _b !== void 0 ? _b : exports.ARGON2ID_DEFAULT_ITERATIONS;
        const memorySize = (_c = kdf.memoryKiB) !== null && _c !== void 0 ? _c : exports.ARGON2ID_DEFAULT_MEMORY_KIB;
        const parallelism = (_d = kdf.parallelism) !== null && _d !== void 0 ? _d : exports.ARGON2ID_DEFAULT_PARALLELISM;
        const hashLength = (_e = kdf.hashLength) !== null && _e !== void 0 ? _e : exports.ARGON2ID_DEFAULT_HASH_LENGTH;
        const hash = await (0, hash_wasm_1.argon2id)({
            password: new Uint8Array(passwordBytes),
            salt: new Uint8Array(token.passwordSalt),
            iterations,
            memorySize,
            parallelism,
            hashLength,
            outputType: 'binary'
        });
        return Array.from(hash);
    }
    throw new Error(`Unsupported KDF algorithm: ${kdf.algorithm}`);
}
/**
 * Unique Identifier for the default profile (16 zero bytes).
 */
exports.DEFAULT_PROFILE_ID = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
/**
 * @class OverlayUMPTokenInteractor
 *
 * A concrete implementation of the UMPTokenInteractor interface that interacts
 * with Overlay Services and the UMP (User Management Protocol) topic. This class
 * is responsible for:
 *
 * 1) Locating UMP tokens via overlay lookups (ls_users).
 * 2) Creating and publishing new or updated UMP token outputs on-chain under
 *    the "tm_users" topic.
 * 3) Consuming (spending) an old token if provided.
 */
class OverlayUMPTokenInteractor {
    /**
     * Construct a new OverlayUMPTokenInteractor.
     *
     * @param resolver     A LookupResolver instance for performing overlay queries (ls_users).
     * @param broadcaster  A SHIPBroadcaster instance for sharing new or updated tokens across the `tm_users` overlay.
     */
    constructor(resolver = new sdk_1.LookupResolver(), broadcaster = new sdk_1.SHIPBroadcaster(['tm_users'])) {
        this.resolver = resolver;
        this.broadcaster = broadcaster;
    }
    /**
     * Finds a UMP token on-chain by the given presentation key hash, if it exists.
     * Uses the ls_users overlay service to perform the lookup.
     *
     * @param hash The 32-byte SHA-256 hash of the presentation key.
     * @returns A UMPToken object (including currentOutpoint) if found, otherwise undefined.
     */
    async findByPresentationKeyHash(hash) {
        // Query ls_users for the given presentationHash
        const question = {
            service: 'ls_users',
            query: { presentationHash: sdk_1.Utils.toHex(hash) }
        };
        const answer = await this.resolver.query(question);
        return this.parseLookupAnswer(answer);
    }
    /**
     * Finds a UMP token on-chain by the given recovery key hash, if it exists.
     * Uses the ls_users overlay service to perform the lookup.
     *
     * @param hash The 32-byte SHA-256 hash of the recovery key.
     * @returns A UMPToken object (including currentOutpoint) if found, otherwise undefined.
     */
    async findByRecoveryKeyHash(hash) {
        const question = {
            service: 'ls_users',
            query: { recoveryHash: sdk_1.Utils.toHex(hash) }
        };
        const answer = await this.resolver.query(question);
        return this.parseLookupAnswer(answer);
    }
    /**
     * Creates or updates (replaces) a UMP token on-chain. If `oldTokenToConsume` is provided,
     * it is spent in the same transaction that creates the new token output. The new token is
     * then broadcast and published under the `tm_users` topic using a SHIP broadcast, ensuring
     * overlay participants see the updated token.
     *
     * @param wallet            The wallet used to build and sign the transaction (MUST be operating under the DEFAULT profile).
     * @param adminOriginator   The domain/FQDN of the administrative originator (wallet operator).
     * @param token             The new UMPToken to create on-chain.
     * @param oldTokenToConsume Optionally, an existing token to consume/spend in the same transaction.
     * @returns The outpoint of the newly created UMP token (e.g. "abcd1234...ef.0").
     */
    async buildAndSend(wallet, // This wallet MUST be the one built for the default profile
    adminOriginator, token, oldTokenToConsume) {
        // 1) Construct the data fields for the new UMP token.
        const fields = [];
        fields[0] = token.passwordSalt;
        fields[1] = token.passwordPresentationPrimary;
        fields[2] = token.passwordRecoveryPrimary;
        fields[3] = token.presentationRecoveryPrimary;
        fields[4] = token.passwordPrimaryPrivileged;
        fields[5] = token.presentationRecoveryPrivileged;
        fields[6] = token.presentationHash;
        fields[7] = token.recoveryHash;
        fields[8] = token.presentationKeyEncrypted;
        fields[9] = token.passwordKeyEncrypted;
        fields[10] = token.recoveryKeyEncrypted;
        // Optional field for encrypted profiles
        if (token.profilesEncrypted) {
            fields[11] = token.profilesEncrypted;
        }
        // V3 fields: umpVersion, kdfAlgorithm, kdfParams
        if (token.umpVersion === 3 && token.passwordKdf) {
            const versionFieldIndex = token.profilesEncrypted ? 12 : 11;
            fields[versionFieldIndex] = [token.umpVersion]; // Single byte
            fields[versionFieldIndex + 1] = sdk_1.Utils.toArray(token.passwordKdf.algorithm, 'utf8');
            // Construct kdfParams JSON
            const kdfParams = {
                iterations: token.passwordKdf.iterations
            };
            if (token.passwordKdf.memoryKiB !== undefined) {
                kdfParams.memoryKiB = token.passwordKdf.memoryKiB;
            }
            if (token.passwordKdf.parallelism !== undefined) {
                kdfParams.parallelism = token.passwordKdf.parallelism;
            }
            if (token.passwordKdf.hashLength !== undefined) {
                kdfParams.hashLength = token.passwordKdf.hashLength;
            }
            fields[versionFieldIndex + 2] = sdk_1.Utils.toArray(JSON.stringify(kdfParams), 'utf8');
        }
        // 2) Create a PushDrop script referencing these fields, locked with the admin key.
        // The signature will be added as trailing metadata by PushDrop (not part of protocol fields).
        const script = await new sdk_1.PushDrop(wallet, adminOriginator).lock(fields, [2, 'admin user management token'], // protocolID
        '1', // keyID
        'self', // counterparty
        /*forSelf=*/ true, 
        /*includeSignature=*/ true);
        // 3) Prepare the createAction call. If oldTokenToConsume is provided, gather the outpoint.
        const inputs = [];
        let inputToken;
        if (oldTokenToConsume === null || oldTokenToConsume === void 0 ? void 0 : oldTokenToConsume.currentOutpoint) {
            inputToken = await this.findByOutpoint(oldTokenToConsume.currentOutpoint);
            // If there is no token on the overlay, we can't consume it. Just start over with a new token.
            if (!inputToken) {
                oldTokenToConsume = undefined;
                // Otherwise, add the input
            }
            else {
                inputs.push({
                    outpoint: oldTokenToConsume.currentOutpoint,
                    unlockingScriptLength: 73, // typical signature length
                    inputDescription: 'Consume old UMP token'
                });
            }
        }
        const outputs = [
            {
                lockingScript: script.toHex(),
                satoshis: 1,
                outputDescription: 'New UMP token output'
            }
        ];
        // 4) Build the partial transaction via createAction.
        let createResult;
        try {
            createResult = await wallet.createAction({
                description: oldTokenToConsume ? 'Renew UMP token (consume old, create new)' : 'Create new UMP token',
                inputs,
                outputs,
                inputBEEF: inputToken === null || inputToken === void 0 ? void 0 : inputToken.beef,
                options: {
                    randomizeOutputs: false,
                    acceptDelayedBroadcast: false
                }
            }, adminOriginator);
        }
        catch (e) {
            console.error('Error with UMP token update. Attempting a last-ditch effort to get a new one', e);
            createResult = await wallet.createAction({
                description: 'Recover UMP token',
                outputs,
                options: {
                    randomizeOutputs: false,
                    acceptDelayedBroadcast: false
                }
            }, adminOriginator);
        }
        // If the transaction is fully processed by the wallet
        if (!createResult.signableTransaction) {
            const finalTxid = createResult.txid || (createResult.tx ? sdk_1.Transaction.fromAtomicBEEF(createResult.tx).id('hex') : undefined);
            if (!finalTxid) {
                throw new Error('No signableTransaction and no final TX found.');
            }
            // Now broadcast to `tm_users` using SHIP
            const broadcastTx = sdk_1.Transaction.fromAtomicBEEF(createResult.tx);
            const result = await this.broadcaster.broadcast(broadcastTx);
            console.log('BROADCAST RESULT', result);
            return `${finalTxid}.0`;
        }
        // 5) If oldTokenToConsume is present, we must sign the input referencing it.
        //    (If there's no old token, there's nothing to sign for the input.)
        let finalTxid = '';
        const reference = createResult.signableTransaction.reference;
        const partialTx = sdk_1.Transaction.fromBEEF(createResult.signableTransaction.tx);
        if (oldTokenToConsume === null || oldTokenToConsume === void 0 ? void 0 : oldTokenToConsume.currentOutpoint) {
            // Unlock the old token with a matching PushDrop unlocker
            const unlocker = new sdk_1.PushDrop(wallet, adminOriginator).unlock([2, 'admin user management token'], '1', 'self');
            const unlockingScript = await unlocker.sign(partialTx, 0);
            // Provide it to the wallet
            const signResult = await wallet.signAction({
                reference,
                spends: {
                    0: {
                        unlockingScript: unlockingScript.toHex()
                    }
                }
            }, adminOriginator);
            finalTxid = signResult.txid || (signResult.tx ? sdk_1.Transaction.fromAtomicBEEF(signResult.tx).id('hex') : '');
            if (!finalTxid) {
                throw new Error('Could not finalize transaction for renewed UMP token.');
            }
            // 6) Broadcast to `tm_users`
            const finalAtomicTx = signResult.tx;
            if (!finalAtomicTx) {
                throw new Error('Final transaction data missing after signing renewed UMP token.');
            }
            const broadcastTx = sdk_1.Transaction.fromAtomicBEEF(finalAtomicTx);
            const result = await this.broadcaster.broadcast(broadcastTx);
            console.log('BROADCAST RESULT', result);
            return `${finalTxid}.0`;
        }
        else {
            // Fallback for creating a new token (no input spending)
            const signResult = await wallet.signAction({ reference, spends: {} }, adminOriginator);
            finalTxid = signResult.txid || (signResult.tx ? sdk_1.Transaction.fromAtomicBEEF(signResult.tx).id('hex') : '');
            if (!finalTxid) {
                throw new Error('Failed to finalize new UMP token transaction.');
            }
            const finalAtomicTx = signResult.tx;
            if (!finalAtomicTx) {
                throw new Error('Final transaction data missing after signing new UMP token.');
            }
            const broadcastTx = sdk_1.Transaction.fromAtomicBEEF(finalAtomicTx);
            const result = await this.broadcaster.broadcast(broadcastTx);
            console.log('BROADCAST RESULT', result);
            return `${finalTxid}.0`;
        }
    }
    /**
     * Attempts to parse a LookupAnswer from the UMP lookup service. If successful,
     * extracts the token fields from the resulting transaction and constructs
     * a UMPToken object.
     *
     * @param answer The LookupAnswer returned by a query to ls_users.
     * @returns The parsed UMPToken or `undefined` if none found/decodable.
     */
    parseLookupAnswer(answer) {
        var _a, _b, _c;
        if (answer.type !== 'output-list') {
            return undefined;
        }
        if (!answer.outputs || answer.outputs.length === 0) {
            return undefined;
        }
        const { beef, outputIndex } = answer.outputs[0];
        try {
            const tx = sdk_1.Transaction.fromBEEF(beef);
            const outpoint = `${tx.id('hex')}.${outputIndex}`;
            const decoded = sdk_1.PushDrop.decode(tx.outputs[outputIndex].lockingScript);
            // Expecting 11 or more fields for UMP
            if (!decoded.fields || decoded.fields.length < 11) {
                console.warn(`Unexpected number of fields in UMP token: ${(_a = decoded.fields) === null || _a === void 0 ? void 0 : _a.length}`);
                return undefined;
            }
            // Parse protocol fields, excluding the trailing PushDrop signature only when
            // it is cryptographically valid for the preceding field payload.
            let protocolFields = stripVerifiedPushDropSignature(decoded.fields, decoded.lockingPublicKey);
            // Detect v3 token metadata in either layout:
            // - with profilesEncrypted present: [0..10]=core, [11]=profiles, [12]=version, [13]=algorithm, [14]=params
            // - without profilesEncrypted:      [0..10]=core, [11]=version, [12]=algorithm, [13]=params
            const hasV3MetadataWithProfiles = protocolFields.length >= 15 && ((_b = protocolFields[12]) === null || _b === void 0 ? void 0 : _b.length) === 1 && protocolFields[12][0] === 3;
            const hasV3MetadataWithoutProfiles = protocolFields.length >= 14 && ((_c = protocolFields[11]) === null || _c === void 0 ? void 0 : _c.length) === 1 && protocolFields[11][0] === 3;
            const kdfVersionFieldIndex = hasV3MetadataWithProfiles ? 12 : hasV3MetadataWithoutProfiles ? 11 : -1;
            // Build the UMP token from these fields, preserving outpoint
            const t = {
                // Core fields (unchanged for all versions)
                passwordSalt: protocolFields[0],
                passwordPresentationPrimary: protocolFields[1],
                passwordRecoveryPrimary: protocolFields[2],
                presentationRecoveryPrimary: protocolFields[3],
                passwordPrimaryPrivileged: protocolFields[4],
                presentationRecoveryPrivileged: protocolFields[5],
                presentationHash: protocolFields[6],
                recoveryHash: protocolFields[7],
                presentationKeyEncrypted: protocolFields[8],
                passwordKeyEncrypted: protocolFields[9],
                recoveryKeyEncrypted: protocolFields[10],
                currentOutpoint: outpoint
            };
            // Encrypted profiles (optional, all versions)
            const hasProfilesField = hasV3MetadataWithProfiles || (!hasV3MetadataWithoutProfiles && !!protocolFields[11]);
            if (hasProfilesField && protocolFields[11] && protocolFields[11].length > 0) {
                t.profilesEncrypted = protocolFields[11];
            }
            // V3 fields: umpVersion, kdfAlgorithm, kdfParams (fields 12, 13, 14)
            if (kdfVersionFieldIndex !== -1) {
                t.umpVersion = protocolFields[kdfVersionFieldIndex][0]; // Single byte value 3
                const kdfAlgorithmField = protocolFields[kdfVersionFieldIndex + 1];
                const kdfParamsField = protocolFields[kdfVersionFieldIndex + 2];
                if (!kdfAlgorithmField || !kdfParamsField) {
                    return t;
                }
                const kdfAlgorithm = sdk_1.Utils.toUTF8(kdfAlgorithmField);
                const kdfParamsJson = sdk_1.Utils.toUTF8(kdfParamsField);
                try {
                    const kdfParams = JSON.parse(kdfParamsJson);
                    t.passwordKdf = {
                        algorithm: kdfAlgorithm,
                        iterations: kdfParams.iterations,
                        memoryKiB: kdfParams.memoryKiB,
                        parallelism: kdfParams.parallelism,
                        hashLength: kdfParams.hashLength
                    };
                }
                catch (e) {
                    console.warn('Failed to parse v3 KDF params:', e);
                }
            }
            return t;
        }
        catch (e) {
            console.error('Failed to parse or decode UMP token:', e);
            return undefined;
        }
    }
    /**
     * Finds by outpoint for unlocking / spending previous tokens.
     * @param outpoint The outpoint we are searching by
     * @returns The result so that we can use it to unlock the transaction
     */
    async findByOutpoint(outpoint) {
        const results = await this.resolver.query({
            service: 'ls_users',
            query: {
                outpoint
            }
        });
        if (results.type !== 'output-list') {
            return undefined;
        }
        if (!results.outputs || !results.outputs.length) {
            return undefined;
        }
        return results.outputs[0];
    }
}
exports.OverlayUMPTokenInteractor = OverlayUMPTokenInteractor;
/**
 * Manages a "CWI-style" wallet that uses a UMP token and a
 * multi-key authentication scheme (password, presentation key, and recovery key),
 * supporting multiple user profiles under a single account.
 */
class CWIStyleWalletManager {
    /**
     * Constructs a new CWIStyleWalletManager.
     *
     * @param adminOriginator   The domain name of the administrative originator.
     * @param walletBuilder     A function that can build an underlying wallet instance for a profile.
     * @param interactor        An instance of UMPTokenInteractor.
     * @param recoveryKeySaver  A function to persist a new recovery key.
     * @param passwordRetriever A function to request the user's password.
     * @param newWalletFunder   Optional function to fund a new wallet.
     * @param stateSnapshot     Optional previously saved state snapshot.
     * @param kdfConfig         Optional KDF configuration for new UMP tokens.
     */
    constructor(adminOriginator, walletBuilder, interactor = new OverlayUMPTokenInteractor(), recoveryKeySaver, passwordRetriever, newWalletFunder, stateSnapshot, kdfConfig) {
        var _a, _b, _c, _d, _e;
        /**
         * Current mode of authentication.
         */
        this.authenticationMode = 'presentation-key-and-password';
        /**
         * Indicates new user or existing user flow.
         */
        this.authenticationFlow = 'new-user';
        /**
         * The currently active profile ID (null or DEFAULT_PROFILE_ID means default profile).
         */
        this.activeProfileId = exports.DEFAULT_PROFILE_ID;
        /**
         * List of loaded non-default profiles.
         */
        this.profiles = [];
        this.adminOriginator = adminOriginator;
        this.walletBuilder = walletBuilder;
        this.UMPTokenInteractor = interactor;
        this.recoveryKeySaver = recoveryKeySaver;
        this.passwordRetriever = passwordRetriever;
        this.authenticated = false;
        this.newWalletFunder = newWalletFunder;
        // Initialize KDF config with Argon2id defaults for v3 tokens
        this.kdfConfig = {
            algorithm: (_a = kdfConfig === null || kdfConfig === void 0 ? void 0 : kdfConfig.algorithm) !== null && _a !== void 0 ? _a : 'argon2id',
            iterations: (_b = kdfConfig === null || kdfConfig === void 0 ? void 0 : kdfConfig.iterations) !== null && _b !== void 0 ? _b : exports.ARGON2ID_DEFAULT_ITERATIONS,
            memoryKiB: (_c = kdfConfig === null || kdfConfig === void 0 ? void 0 : kdfConfig.memoryKiB) !== null && _c !== void 0 ? _c : exports.ARGON2ID_DEFAULT_MEMORY_KIB,
            parallelism: (_d = kdfConfig === null || kdfConfig === void 0 ? void 0 : kdfConfig.parallelism) !== null && _d !== void 0 ? _d : exports.ARGON2ID_DEFAULT_PARALLELISM,
            hashLength: (_e = kdfConfig === null || kdfConfig === void 0 ? void 0 : kdfConfig.hashLength) !== null && _e !== void 0 ? _e : exports.ARGON2ID_DEFAULT_HASH_LENGTH
        };
        // If a saved snapshot is provided, attempt to load it.
        // Note: loadSnapshot now returns a promise. We don't await it here,
        // as the constructor must be synchronous. The caller should check
        // `this.authenticated` after construction if a snapshot was provided.
        if (stateSnapshot) {
            this.loadSnapshot(stateSnapshot).catch(err => {
                console.error('Failed to load snapshot during construction:', err);
                // Clear potentially partially loaded state
                this.destroy();
            });
        }
    }
    // --- Authentication Methods ---
    /**
     * Provides the presentation key.
     */
    async providePresentationKey(key) {
        if (this.authenticated) {
            throw new Error('User is already authenticated');
        }
        if (this.authenticationMode === 'recovery-key-and-password') {
            throw new Error('Presentation key is not needed in this mode');
        }
        const hash = sdk_1.Hash.sha256(key);
        const token = await this.UMPTokenInteractor.findByPresentationKeyHash(hash);
        if (!token) {
            // No token found -> New user
            this.authenticationFlow = 'new-user';
            this.presentationKey = key;
        }
        else {
            // Found token -> existing user
            this.authenticationFlow = 'existing-user';
            this.presentationKey = key;
            this.currentUMPToken = token;
        }
    }
    /**
     * Provides the password.
     */
    async providePassword(password) {
        if (this.authenticated) {
            throw new Error('User is already authenticated');
        }
        if (this.authenticationMode === 'presentation-key-and-recovery-key') {
            throw new Error('Password is not needed in this mode');
        }
        if (this.authenticationFlow === 'existing-user') {
            // Existing user flow
            if (!this.currentUMPToken) {
                throw new Error('Provide presentation or recovery key first.');
            }
            // Use token-driven KDF (legacy PBKDF2 or v3 Argon2id)
            const derivedPasswordKey = await derivePasswordKey(this.currentUMPToken, sdk_1.Utils.toArray(password, 'utf8'));
            let rootPrimaryKey;
            let rootPrivilegedKey; // Only needed for recovery mode
            if (this.authenticationMode === 'presentation-key-and-password') {
                if (!this.presentationKey)
                    throw new Error('No presentation key found!');
                const xorKey = this.XOR(this.presentationKey, derivedPasswordKey);
                rootPrimaryKey = new sdk_1.SymmetricKey(xorKey).decrypt(this.currentUMPToken.passwordPresentationPrimary);
            }
            else {
                // 'recovery-key-and-password'
                if (!this.recoveryKey)
                    throw new Error('No recovery key found!');
                const primaryDecryptionKey = this.XOR(this.recoveryKey, derivedPasswordKey);
                rootPrimaryKey = new sdk_1.SymmetricKey(primaryDecryptionKey).decrypt(this.currentUMPToken.passwordRecoveryPrimary);
                const privilegedDecryptionKey = this.XOR(rootPrimaryKey, derivedPasswordKey);
                rootPrivilegedKey = new sdk_1.SymmetricKey(privilegedDecryptionKey).decrypt(this.currentUMPToken.passwordPrimaryPrivileged);
            }
            // Build root infrastructure, load profiles, and switch to default profile initially
            await this.setupRootInfrastructure(rootPrimaryKey, rootPrivilegedKey);
            await this.switchProfile(this.activeProfileId);
        }
        else {
            // New user flow (only 'presentation-key-and-password')
            if (this.authenticationMode !== 'presentation-key-and-password') {
                throw new Error('New-user flow requires presentation key and password mode.');
            }
            if (!this.presentationKey) {
                throw new Error('No presentation key provided for new-user flow.');
            }
            // Generate new keys/salt
            const recoveryKey = (0, sdk_1.Random)(32);
            await this.recoveryKeySaver(recoveryKey);
            const passwordSalt = (0, sdk_1.Random)(32);
            // Build temporary token with KDF config for password derivation
            const tempTokenForKdf = {
                passwordSalt,
                passwordKdf: this.kdfConfig
            };
            const passwordKey = await derivePasswordKey(tempTokenForKdf, sdk_1.Utils.toArray(password, 'utf8'));
            const rootPrimaryKey = (0, sdk_1.Random)(32);
            const rootPrivilegedKey = (0, sdk_1.Random)(32);
            // Build XOR keys
            const presentationPassword = new sdk_1.SymmetricKey(this.XOR(this.presentationKey, passwordKey));
            const presentationRecovery = new sdk_1.SymmetricKey(this.XOR(this.presentationKey, recoveryKey));
            const recoveryPassword = new sdk_1.SymmetricKey(this.XOR(recoveryKey, passwordKey));
            const primaryPassword = new sdk_1.SymmetricKey(this.XOR(rootPrimaryKey, passwordKey));
            // Temp manager for encryption
            const tempPrivilegedKeyManager = new PrivilegedKeyManager_1.PrivilegedKeyManager(async () => new sdk_1.PrivateKey(rootPrivilegedKey));
            // Build new UMP token (v3 with KDF metadata, no profiles initially)
            const newToken = {
                passwordSalt,
                passwordPresentationPrimary: presentationPassword.encrypt(rootPrimaryKey),
                passwordRecoveryPrimary: recoveryPassword.encrypt(rootPrimaryKey),
                presentationRecoveryPrimary: presentationRecovery.encrypt(rootPrimaryKey),
                passwordPrimaryPrivileged: primaryPassword.encrypt(rootPrivilegedKey),
                presentationRecoveryPrivileged: presentationRecovery.encrypt(rootPrivilegedKey),
                presentationHash: sdk_1.Hash.sha256(this.presentationKey),
                recoveryHash: sdk_1.Hash.sha256(recoveryKey),
                presentationKeyEncrypted: (await tempPrivilegedKeyManager.encrypt({
                    plaintext: this.presentationKey,
                    protocolID: [2, 'admin key wrapping'],
                    keyID: '1'
                })).ciphertext,
                passwordKeyEncrypted: (await tempPrivilegedKeyManager.encrypt({
                    plaintext: passwordKey,
                    protocolID: [2, 'admin key wrapping'],
                    keyID: '1'
                })).ciphertext,
                recoveryKeyEncrypted: (await tempPrivilegedKeyManager.encrypt({
                    plaintext: recoveryKey,
                    protocolID: [2, 'admin key wrapping'],
                    keyID: '1'
                })).ciphertext,
                profilesEncrypted: undefined, // No profiles yet
                umpVersion: 3, // UMP protocol version 3
                passwordKdf: this.kdfConfig // KDF metadata for v3
            };
            this.currentUMPToken = newToken;
            // Setup root infrastructure and switch to default profile
            await this.setupRootInfrastructure(rootPrimaryKey);
            await this.switchProfile(exports.DEFAULT_PROFILE_ID);
            // Fund the *default* wallet if funder provided
            if (this.newWalletFunder && this.underlying) {
                try {
                    await this.newWalletFunder(this.presentationKey, this.underlying, this.adminOriginator);
                }
                catch (e) {
                    console.error('Error funding new wallet:', e);
                    const message = e instanceof Error ? e.message : String(e);
                    throw new Error(`Failed to fund new wallet before publishing UMP token: ${message}`);
                }
            }
            // Publish the new UMP token *after* potentially funding
            // We need the default profile wallet to sign the UMP creation TX
            if (!this.underlying) {
                throw new Error('Default profile wallet not built before attempting to publish UMP token.');
            }
            this.currentUMPToken.currentOutpoint = await this.UMPTokenInteractor.buildAndSend(this.underlying, // Use the default profile wallet
            this.adminOriginator, newToken);
        }
    }
    /**
     * Provides the recovery key.
     */
    async provideRecoveryKey(recoveryKey) {
        if (this.authenticated) {
            throw new Error('Already authenticated');
        }
        if (this.authenticationFlow === 'new-user') {
            throw new Error('Do not submit recovery key in new-user flow');
        }
        if (this.authenticationMode === 'presentation-key-and-password') {
            throw new Error('No recovery key required in this mode');
        }
        else if (this.authenticationMode === 'recovery-key-and-password') {
            // Wait for password
            const hash = sdk_1.Hash.sha256(recoveryKey);
            const token = await this.UMPTokenInteractor.findByRecoveryKeyHash(hash);
            if (!token)
                throw new Error('No user found with this recovery key');
            this.recoveryKey = recoveryKey;
            this.currentUMPToken = token;
        }
        else {
            // 'presentation-key-and-recovery-key'
            if (!this.presentationKey)
                throw new Error('Provide the presentation key first');
            if (!this.currentUMPToken)
                throw new Error('Current UMP token not found');
            const xorKey = this.XOR(this.presentationKey, recoveryKey);
            const rootPrimaryKey = new sdk_1.SymmetricKey(xorKey).decrypt(this.currentUMPToken.presentationRecoveryPrimary);
            const rootPrivilegedKey = new sdk_1.SymmetricKey(xorKey).decrypt(this.currentUMPToken.presentationRecoveryPrivileged);
            // Build root infrastructure, load profiles, switch to default
            await this.setupRootInfrastructure(rootPrimaryKey, rootPrivilegedKey);
            await this.switchProfile(this.activeProfileId);
        }
    }
    // --- State Management Methods ---
    /**
     * Saves the current wallet state (root key, UMP token, active profile) into an encrypted snapshot.
     * Version 2 format: [1 byte version=2] + [32 byte snapshot key] + [16 byte activeProfileId] + [encrypted payload]
     * Encrypted Payload: [32 byte rootPrimaryKey] + [varint token length + serialized UMP token]
     *
     * @returns Encrypted snapshot bytes.
     */
    saveSnapshot() {
        if (!this.rootPrimaryKey || !this.currentUMPToken) {
            throw new Error('No root primary key or current UMP token set');
        }
        const snapshotKey = (0, sdk_1.Random)(32);
        const snapshotPreimageWriter = new sdk_1.Utils.Writer();
        // Write root primary key
        snapshotPreimageWriter.write(this.rootPrimaryKey);
        // Write serialized UMP token (must have outpoint)
        if (!this.currentUMPToken.currentOutpoint) {
            throw new Error('UMP token cannot be saved without a current outpoint.');
        }
        const serializedToken = this.serializeUMPToken(this.currentUMPToken);
        snapshotPreimageWriter.writeVarIntNum(serializedToken.length);
        snapshotPreimageWriter.write(serializedToken);
        // Encrypt the payload
        const snapshotPreimage = snapshotPreimageWriter.toArray();
        const snapshotPayload = new sdk_1.SymmetricKey(snapshotKey).encrypt(snapshotPreimage);
        // Build final snapshot (Version 2)
        const snapshotWriter = new sdk_1.Utils.Writer();
        snapshotWriter.writeUInt8(2); // Version
        snapshotWriter.write(snapshotKey);
        snapshotWriter.write(this.activeProfileId); // Active profile ID
        snapshotWriter.write(snapshotPayload); // Encrypted data
        return snapshotWriter.toArray();
    }
    /**
     * Loads a previously saved state snapshot. Restores root key, UMP token, profiles, and active profile.
     * Handles Version 1 (legacy) and Version 2 formats.
     *
     * @param snapshot Encrypted snapshot bytes.
     */
    async loadSnapshot(snapshot) {
        try {
            const reader = new sdk_1.Utils.Reader(snapshot);
            const version = reader.readUInt8();
            let snapshotKey;
            let encryptedPayload;
            let activeProfileId = exports.DEFAULT_PROFILE_ID; // Default for V1
            if (version === 1) {
                snapshotKey = reader.read(32);
                encryptedPayload = reader.read();
            }
            else if (version === 2) {
                snapshotKey = reader.read(32);
                activeProfileId = reader.read(16); // Read active profile ID
                encryptedPayload = reader.read();
            }
            else {
                throw new Error(`Unsupported snapshot version: ${version}`);
            }
            // Decrypt payload
            const decryptedPayload = new sdk_1.SymmetricKey(snapshotKey).decrypt(encryptedPayload);
            const payloadReader = new sdk_1.Utils.Reader(decryptedPayload);
            // Read root primary key
            const rootPrimaryKey = payloadReader.read(32);
            // Read serialized UMP token
            const tokenLen = payloadReader.readVarIntNum();
            const tokenBytes = payloadReader.read(tokenLen);
            const token = this.deserializeUMPToken(tokenBytes);
            // Assign loaded data
            this.currentUMPToken = token;
            // Setup root infrastructure, load profiles, and switch to the loaded active profile
            await this.setupRootInfrastructure(rootPrimaryKey); // Will automatically load profiles
            await this.switchProfile(activeProfileId); // Switch to the profile saved in the snapshot
            this.authenticationFlow = 'existing-user'; // Loading implies existing user
        }
        catch (error) {
            this.destroy(); // Clear state on error
            throw new Error(`Failed to load snapshot: ${error.message}`);
        }
    }
    async syncUMPToken() {
        if (!this.authenticated || !this.currentUMPToken || !this.rootPrimaryKey) {
            throw new Error('Wallet not authenticated or missing UMP token.');
        }
        const currentToken = this.currentUMPToken;
        let refreshed;
        if (currentToken.presentationHash && currentToken.presentationHash.length) {
            refreshed = await this.UMPTokenInteractor.findByPresentationKeyHash(currentToken.presentationHash);
        }
        if (!refreshed && currentToken.recoveryHash && currentToken.recoveryHash.length) {
            refreshed = await this.UMPTokenInteractor.findByRecoveryKeyHash(currentToken.recoveryHash);
        }
        if (!refreshed) {
            return false;
        }
        if (refreshed.currentOutpoint &&
            currentToken.currentOutpoint &&
            refreshed.currentOutpoint === currentToken.currentOutpoint) {
            return false;
        }
        this.currentUMPToken = refreshed;
        await this.setupRootInfrastructure(this.rootPrimaryKey);
        this.saveSnapshot();
        return true;
    }
    /**
     * Destroys the wallet state, clearing keys, tokens, and profiles.
     */
    destroy() {
        this.underlying = undefined;
        this.rootPrivilegedKeyManager = undefined;
        this.authenticated = false;
        this.rootPrimaryKey = undefined;
        this.currentUMPToken = undefined;
        this.presentationKey = undefined;
        this.recoveryKey = undefined;
        this.profiles = [];
        this.activeProfileId = exports.DEFAULT_PROFILE_ID;
        this.authenticationMode = 'presentation-key-and-password';
        this.authenticationFlow = 'new-user';
    }
    // --- Profile Management Methods ---
    /**
     * Lists all available profiles, including the default profile.
     * @returns Array of profile info objects, including an 'active' flag.
     */
    listProfiles() {
        if (!this.authenticated) {
            throw new Error('Not authenticated.');
        }
        const profileList = [
            // Default profile
            {
                id: exports.DEFAULT_PROFILE_ID,
                name: 'default',
                createdAt: null, // Default profile doesn't have a creation timestamp in the same way
                active: this.activeProfileId.every(x => x === 0),
                identityKey: new sdk_1.PrivateKey(this.rootPrimaryKey).toPublicKey().toString()
            },
            // Other profiles
            ...this.profiles.map(p => ({
                id: p.id,
                name: p.name,
                createdAt: p.createdAt,
                active: this.activeProfileId.every((x, i) => x === p.id[i]),
                identityKey: new sdk_1.PrivateKey(this.XOR(this.rootPrimaryKey, p.primaryPad)).toPublicKey().toString()
            }))
        ];
        return profileList;
    }
    /**
     * Adds a new profile with the given name.
     * Generates necessary pads and updates the UMP token.
     * Does not switch to the new profile automatically.
     *
     * @param name The desired name for the new profile.
     * @returns The ID of the newly created profile.
     */
    async addProfile(name) {
        if (!this.authenticated || !this.rootPrimaryKey || !this.currentUMPToken || !this.rootPrivilegedKeyManager) {
            throw new Error('Wallet not fully initialized or authenticated.');
        }
        // Ensure name is unique (including 'default')
        if (name === 'default' || this.profiles.some(p => p.name.toLowerCase() === name.toLowerCase())) {
            throw new Error(`Profile name "${name}" is already in use.`);
        }
        const newProfile = {
            name,
            id: (0, sdk_1.Random)(16),
            primaryPad: (0, sdk_1.Random)(32),
            privilegedPad: (0, sdk_1.Random)(32),
            createdAt: Math.floor(Date.now() / 1000)
        };
        this.profiles.push(newProfile);
        // Update the UMP token with the new profile list
        await this.updateAuthFactors(this.currentUMPToken.passwordSalt, 
        // Need to re-derive/decrypt factors needed for re-encryption
        await this.getFactor('passwordKey'), await this.getFactor('presentationKey'), await this.getFactor('recoveryKey'), this.rootPrimaryKey, await this.getFactor('privilegedKey'), // Get ROOT privileged key
        this.profiles // Pass the updated profile list
        );
        return newProfile.id;
    }
    /**
     * Deletes a profile by its ID.
     * Cannot delete the default profile. If the active profile is deleted,
     * it switches back to the default profile.
     *
     * @param profileId The 16-byte ID of the profile to delete.
     */
    async deleteProfile(profileId) {
        if (!this.authenticated || !this.rootPrimaryKey || !this.currentUMPToken || !this.rootPrivilegedKeyManager) {
            throw new Error('Wallet not fully initialized or authenticated.');
        }
        if (profileId.every(x => x === 0)) {
            throw new Error('Cannot delete the default profile.');
        }
        const profileIndex = this.profiles.findIndex(p => p.id.every((x, i) => x === profileId[i]));
        if (profileIndex === -1) {
            throw new Error('Profile not found.');
        }
        // Remove the profile
        this.profiles.splice(profileIndex, 1);
        // If the deleted profile was active, switch to default
        if (this.activeProfileId.every((x, i) => x === profileId[i])) {
            await this.switchProfile(exports.DEFAULT_PROFILE_ID); // This rebuilds the wallet
        }
        // Update the UMP token
        await this.updateAuthFactors(this.currentUMPToken.passwordSalt, await this.getFactor('passwordKey'), await this.getFactor('presentationKey'), await this.getFactor('recoveryKey'), this.rootPrimaryKey, await this.getFactor('privilegedKey'), // Get ROOT privileged key
        this.profiles // Pass updated list
        );
    }
    /**
     * Switches the active profile. This re-derives keys and rebuilds the underlying wallet.
     *
     * @param profileId The 16-byte ID of the profile to switch to (use DEFAULT_PROFILE_ID for default).
     */
    async switchProfile(profileId) {
        if (!this.authenticated || !this.rootPrimaryKey || !this.rootPrivilegedKeyManager) {
            throw new Error('Cannot switch profile: Wallet not authenticated or root keys missing.');
        }
        let profilePrimaryKey;
        let profilePrivilegedPad; // Pad for the target profile
        if (profileId.every(x => x === 0)) {
            // Switching to default profile
            profilePrimaryKey = this.rootPrimaryKey;
            profilePrivilegedPad = undefined; // No pad for default
            this.activeProfileId = exports.DEFAULT_PROFILE_ID;
        }
        else {
            // Switching to a non-default profile
            const profile = this.profiles.find(p => p.id.every((x, i) => x === profileId[i]));
            if (!profile) {
                throw new Error('Profile not found.');
            }
            profilePrimaryKey = this.XOR(this.rootPrimaryKey, profile.primaryPad);
            profilePrivilegedPad = profile.privilegedPad;
            this.activeProfileId = profileId;
        }
        // Create a *profile-specific* PrivilegedKeyManager.
        // It uses the ROOT manager internally but applies the profile's pad.
        const profilePrivilegedKeyManager = new PrivilegedKeyManager_1.PrivilegedKeyManager(async (reason) => {
            // Request the ROOT privileged key using the root manager
            const rootPrivileged = await this.rootPrivilegedKeyManager.getPrivilegedKey(reason);
            const rootPrivilegedBytes = rootPrivileged.toArray();
            // Apply the profile's pad if applicable
            const profilePrivilegedBytes = profilePrivilegedPad
                ? this.XOR(rootPrivilegedBytes, profilePrivilegedPad)
                : rootPrivilegedBytes;
            return new sdk_1.PrivateKey(profilePrivilegedBytes);
        });
        // Build the underlying wallet for the specific profile
        this.underlying = await this.walletBuilder(profilePrimaryKey, profilePrivilegedKeyManager, // Pass the profile-specific manager
        this.activeProfileId // Pass the ID of the profile being activated
        );
    }
    // --- Key Management Methods ---
    /**
     * Changes the user's password. Re-wraps keys and updates the UMP token.
     */
    async changePassword(newPassword) {
        var _a;
        if (!this.authenticated || !this.currentUMPToken || !this.rootPrimaryKey || !this.rootPrivilegedKeyManager) {
            throw new Error('Not authenticated or missing required data.');
        }
        const passwordSalt = (0, sdk_1.Random)(32);
        // Preserve current token's KDF metadata (or use manager's kdfConfig for legacy tokens)
        const kdfToUse = (_a = this.currentUMPToken.passwordKdf) !== null && _a !== void 0 ? _a : this.kdfConfig;
        const tempTokenForKdf = {
            passwordSalt,
            passwordKdf: kdfToUse
        };
        const newPasswordKey = await derivePasswordKey(tempTokenForKdf, sdk_1.Utils.toArray(newPassword, 'utf8'));
        // Decrypt existing factors needed for re-encryption, using the *root* privileged key manager
        const recoveryKey = await this.getFactor('recoveryKey');
        const presentationKey = await this.getFactor('presentationKey');
        const rootPrivilegedKey = await this.getFactor('privilegedKey'); // Get ROOT privileged key
        await this.updateAuthFactors(passwordSalt, newPasswordKey, presentationKey, recoveryKey, this.rootPrimaryKey, rootPrivilegedKey, // Pass the explicitly fetched root key
        this.profiles // Preserve existing profiles
        );
    }
    /**
     * Retrieves the current recovery key. Requires privileged access.
     */
    async getRecoveryKey() {
        if (!this.authenticated || !this.currentUMPToken || !this.rootPrivilegedKeyManager) {
            throw new Error('Not authenticated or missing required data.');
        }
        return this.getFactor('recoveryKey');
    }
    /**
     * Changes the user's recovery key. Prompts user to save the new key.
     */
    async changeRecoveryKey() {
        if (!this.authenticated || !this.currentUMPToken || !this.rootPrimaryKey || !this.rootPrivilegedKeyManager) {
            throw new Error('Not authenticated or missing required data.');
        }
        // Decrypt existing factors needed
        const passwordKey = await this.getFactor('passwordKey');
        const presentationKey = await this.getFactor('presentationKey');
        const rootPrivilegedKey = await this.getFactor('privilegedKey'); // Get ROOT privileged key
        // Generate and save new recovery key
        const newRecoveryKey = (0, sdk_1.Random)(32);
        await this.recoveryKeySaver(newRecoveryKey);
        await this.updateAuthFactors(this.currentUMPToken.passwordSalt, passwordKey, presentationKey, newRecoveryKey, // Use the new key
        this.rootPrimaryKey, rootPrivilegedKey, this.profiles // Preserve profiles
        );
    }
    /**
     * Changes the user's presentation key.
     */
    async changePresentationKey(newPresentationKey) {
        if (!this.authenticated || !this.currentUMPToken || !this.rootPrimaryKey || !this.rootPrivilegedKeyManager) {
            throw new Error('Not authenticated or missing required data.');
        }
        if (newPresentationKey.length !== 32) {
            throw new Error('Presentation key must be 32 bytes.');
        }
        // Decrypt existing factors
        const recoveryKey = await this.getFactor('recoveryKey');
        const passwordKey = await this.getFactor('passwordKey');
        const rootPrivilegedKey = await this.getFactor('privilegedKey'); // Get ROOT privileged key
        await this.updateAuthFactors(this.currentUMPToken.passwordSalt, passwordKey, newPresentationKey, // Use the new key
        recoveryKey, this.rootPrimaryKey, rootPrivilegedKey, this.profiles // Preserve profiles
        );
        // Update the temporarily stored key if it was set
        if (this.presentationKey) {
            this.presentationKey = newPresentationKey;
        }
    }
    // --- Internal Helper Methods ---
    /**
     * Performs XOR operation on two byte arrays.
     */
    XOR(n1, n2) {
        if (n1.length !== n2.length) {
            // Provide more context in error
            throw new Error(`XOR length mismatch: ${n1.length} vs ${n2.length}`);
        }
        const r = new Array(n1.length);
        for (let i = 0; i < n1.length; i++) {
            r[i] = n1[i] ^ n2[i];
        }
        return r;
    }
    /**
     * Helper to decrypt a specific factor (key) stored encrypted in the UMP token.
     * Requires the root privileged key manager.
     * @param factorName Name of the factor to decrypt ('passwordKey', 'presentationKey', 'recoveryKey', 'privilegedKey').
     * @param getRoot If true and factorName is 'privilegedKey', returns the root privileged key bytes directly.
     * @returns The decrypted key bytes.
     */
    async getFactor(factorName) {
        if (!this.authenticated || !this.currentUMPToken || !this.rootPrivilegedKeyManager) {
            throw new Error(`Cannot get factor "${factorName}": Wallet not ready.`);
        }
        const protocolID = [2, 'admin key wrapping']; // Protocol used for encrypting factors
        const keyID = '1'; // Key ID used
        try {
            switch (factorName) {
                case 'passwordKey':
                    return (await this.rootPrivilegedKeyManager.decrypt({
                        ciphertext: this.currentUMPToken.passwordKeyEncrypted,
                        protocolID,
                        keyID
                    })).plaintext;
                case 'presentationKey':
                    return (await this.rootPrivilegedKeyManager.decrypt({
                        ciphertext: this.currentUMPToken.presentationKeyEncrypted,
                        protocolID,
                        keyID
                    })).plaintext;
                case 'recoveryKey':
                    return (await this.rootPrivilegedKeyManager.decrypt({
                        ciphertext: this.currentUMPToken.recoveryKeyEncrypted,
                        protocolID,
                        keyID
                    })).plaintext;
                case 'privilegedKey': {
                    // This needs careful handling based on whether the ROOT or PROFILE key is needed.
                    // This helper is mostly used for UMP updates, which need the ROOT key.
                    // We retrieve the PrivateKey object first.
                    const pk = await this.rootPrivilegedKeyManager.getPrivilegedKey('UMP token update', true); // Force retrieval of root key
                    return pk.toArray(); // Return bytes
                }
                default:
                    throw new Error(`Unknown factor name: ${factorName}`);
            }
        }
        catch (error) {
            console.error(`Error decrypting factor ${factorName}:`, error);
            throw new Error(`Failed to decrypt factor "${factorName}": ${error.message}`);
        }
    }
    /**
     * Recomputes UMP token fields with updated factors and profiles, then publishes the update.
     * This operation requires the *root* privileged key and the *default* profile wallet.
     */
    async updateAuthFactors(passwordSalt, passwordKey, presentationKey, recoveryKey, rootPrimaryKey, rootPrivilegedKey, // Explicitly pass the root key bytes
    profiles // Pass current/new profiles list
    ) {
        var _a;
        if (!this.authenticated || !this.rootPrimaryKey || !this.currentUMPToken) {
            throw new Error('Wallet is not properly authenticated or missing data for update.');
        }
        // Ensure we have the OLD token to consume
        const oldTokenToConsume = { ...this.currentUMPToken };
        if (!oldTokenToConsume.currentOutpoint) {
            throw new Error('Cannot update UMP token: Old token has no outpoint.');
        }
        // Derive symmetrical encryption keys using XOR for the *root* keys
        const presentationPassword = new sdk_1.SymmetricKey(this.XOR(presentationKey, passwordKey));
        const presentationRecovery = new sdk_1.SymmetricKey(this.XOR(presentationKey, recoveryKey));
        const recoveryPassword = new sdk_1.SymmetricKey(this.XOR(recoveryKey, passwordKey));
        const primaryPassword = new sdk_1.SymmetricKey(this.XOR(rootPrimaryKey, passwordKey)); // Use rootPrimaryKey
        // Build a temporary privileged key manager using the explicit ROOT privileged key
        const tempRootPrivilegedKeyManager = new PrivilegedKeyManager_1.PrivilegedKeyManager(async () => new sdk_1.PrivateKey(rootPrivilegedKey));
        // Encrypt profiles if provided
        let profilesEncrypted;
        if (profiles && profiles.length > 0) {
            const profilesJson = JSON.stringify(profiles);
            const profilesBytes = sdk_1.Utils.toArray(profilesJson, 'utf8');
            profilesEncrypted = new sdk_1.SymmetricKey(rootPrimaryKey).encrypt(profilesBytes);
        }
        // Construct the new UMP token data (preserve or upgrade to v3 with KDF metadata)
        const kdfMetadata = (_a = this.currentUMPToken.passwordKdf) !== null && _a !== void 0 ? _a : this.kdfConfig;
        const newTokenData = {
            passwordSalt,
            passwordPresentationPrimary: presentationPassword.encrypt(rootPrimaryKey),
            passwordRecoveryPrimary: recoveryPassword.encrypt(rootPrimaryKey),
            presentationRecoveryPrimary: presentationRecovery.encrypt(rootPrimaryKey),
            passwordPrimaryPrivileged: primaryPassword.encrypt(rootPrivilegedKey),
            presentationRecoveryPrivileged: presentationRecovery.encrypt(rootPrivilegedKey),
            presentationHash: sdk_1.Hash.sha256(presentationKey),
            recoveryHash: sdk_1.Hash.sha256(recoveryKey),
            presentationKeyEncrypted: (await tempRootPrivilegedKeyManager.encrypt({
                plaintext: presentationKey,
                protocolID: [2, 'admin key wrapping'],
                keyID: '1'
            })).ciphertext,
            passwordKeyEncrypted: (await tempRootPrivilegedKeyManager.encrypt({
                plaintext: passwordKey,
                protocolID: [2, 'admin key wrapping'],
                keyID: '1'
            })).ciphertext,
            recoveryKeyEncrypted: (await tempRootPrivilegedKeyManager.encrypt({
                plaintext: recoveryKey,
                protocolID: [2, 'admin key wrapping'],
                keyID: '1'
            })).ciphertext,
            profilesEncrypted, // Add encrypted profiles
            umpVersion: 3, // UMP protocol version 3
            passwordKdf: kdfMetadata // Preserve or upgrade KDF metadata
            // currentOutpoint will be set after publishing
        };
        // We need the wallet built for the DEFAULT profile to publish the UMP token.
        // If the current active profile is not default, temporarily switch, publish, then switch back.
        const currentActiveId = this.activeProfileId;
        let walletToUse = this.underlying;
        if (!currentActiveId.every(x => x === 0)) {
            console.log('Temporarily switching to default profile to update UMP token...');
            await this.switchProfile(exports.DEFAULT_PROFILE_ID); // This rebuilds this.underlying
            walletToUse = this.underlying;
        }
        if (!walletToUse) {
            throw new Error('Default profile wallet could not be activated for UMP token update.');
        }
        // Publish the new token on-chain, consuming the old one
        try {
            newTokenData.currentOutpoint = await this.UMPTokenInteractor.buildAndSend(walletToUse, this.adminOriginator, newTokenData, oldTokenToConsume // Consume the previous token
            );
            // Update the manager's state
            this.currentUMPToken = newTokenData;
            // Profiles are already updated in this.profiles if they were passed in
        }
        finally {
            // Switch back if we temporarily switched
            if (!currentActiveId.every(x => x === 0)) {
                console.log('Switching back to original profile...');
                await this.switchProfile(currentActiveId);
            }
        }
    }
    /**
     * Serializes a UMP token to binary format (Version 3 with KDF metadata, Version 2 with profiles).
     * V3 Layout: [1 byte version=3] + [11 * (varint len + bytes) for standard fields] + [1 byte profile_flag] + [IF flag=1 THEN varint len + profile bytes] + [1 byte kdf_flag] + [IF flag=1 THEN kdf metadata] + [varint len + outpoint bytes]
     */
    serializeUMPToken(token) {
        if (!token.currentOutpoint) {
            throw new Error('Token must have outpoint for serialization');
        }
        const writer = new sdk_1.Utils.Writer();
        const hasKdfMetadata = token.umpVersion === 3 && token.passwordKdf;
        writer.writeUInt8(hasKdfMetadata ? 3 : 2); // Version 3 for KDF, 2 for legacy
        const writeArray = (arr) => {
            writer.writeVarIntNum(arr.length);
            writer.write(arr);
        };
        // Write standard fields in specific order
        writeArray(token.passwordSalt); // 0
        writeArray(token.passwordPresentationPrimary); // 1
        writeArray(token.passwordRecoveryPrimary); // 2
        writeArray(token.presentationRecoveryPrimary); // 3
        writeArray(token.passwordPrimaryPrivileged); // 4
        writeArray(token.presentationRecoveryPrivileged); // 5
        writeArray(token.presentationHash); // 6
        writeArray(token.recoveryHash); // 7
        writeArray(token.presentationKeyEncrypted); // 8
        writeArray(token.passwordKeyEncrypted); // 9
        writeArray(token.recoveryKeyEncrypted); // 10
        // Write optional profiles field
        if (token.profilesEncrypted && token.profilesEncrypted.length > 0) {
            writer.writeUInt8(1); // Flag indicating profiles present
            writeArray(token.profilesEncrypted);
        }
        else {
            writer.writeUInt8(0); // Flag indicating no profiles
        }
        // V3: Write KDF metadata
        if (hasKdfMetadata) {
            writer.writeUInt8(1); // Flag indicating KDF metadata present
            writer.writeUInt8(token.umpVersion); // On-chain UMP version
            const algorithmBytes = sdk_1.Utils.toArray(token.passwordKdf.algorithm, 'utf8');
            writeArray(algorithmBytes);
            // Serialize KDF params as JSON
            const kdfParams = {
                iterations: token.passwordKdf.iterations
            };
            if (token.passwordKdf.memoryKiB !== undefined) {
                kdfParams.memoryKiB = token.passwordKdf.memoryKiB;
            }
            if (token.passwordKdf.parallelism !== undefined) {
                kdfParams.parallelism = token.passwordKdf.parallelism;
            }
            if (token.passwordKdf.hashLength !== undefined) {
                kdfParams.hashLength = token.passwordKdf.hashLength;
            }
            const kdfParamsBytes = sdk_1.Utils.toArray(JSON.stringify(kdfParams), 'utf8');
            writeArray(kdfParamsBytes);
        }
        else if (writer.toArray()[0] === 3) {
            // Version 3 without KDF metadata (shouldn't happen, but handle gracefully)
            writer.writeUInt8(0); // Flag indicating no KDF metadata
        }
        // Write outpoint string
        const outpointBytes = sdk_1.Utils.toArray(token.currentOutpoint, 'utf8');
        writer.writeVarIntNum(outpointBytes.length);
        writer.write(outpointBytes);
        return writer.toArray();
    }
    /**
     * Deserializes a UMP token from binary format (Handles Version 1, 2, and 3).
     */
    deserializeUMPToken(bin) {
        const reader = new sdk_1.Utils.Reader(bin);
        const version = reader.readUInt8();
        if (version !== 1 && version !== 2 && version !== 3) {
            throw new Error(`Unsupported UMP token serialization version: ${version}`);
        }
        const readArray = () => {
            const length = reader.readVarIntNum();
            return reader.read(length);
        };
        // Read standard fields (order matches serialization)
        const passwordSalt = readArray(); // 0
        const passwordPresentationPrimary = readArray(); // 1
        const passwordRecoveryPrimary = readArray(); // 2
        const presentationRecoveryPrimary = readArray(); // 3
        const passwordPrimaryPrivileged = readArray(); // 4
        const presentationRecoveryPrivileged = readArray(); // 5
        const presentationHash = readArray(); // 6
        const recoveryHash = readArray(); // 7
        const presentationKeyEncrypted = readArray(); // 8
        const passwordKeyEncrypted = readArray(); // 9
        const recoveryKeyEncrypted = readArray(); // 10
        // Read optional profiles (V2 and V3)
        let profilesEncrypted;
        if (version >= 2) {
            const profilesFlag = reader.readUInt8();
            if (profilesFlag === 1) {
                profilesEncrypted = readArray();
            }
        }
        // Read KDF metadata (V3 only)
        let umpVersion;
        let passwordKdf;
        if (version === 3) {
            const kdfFlag = reader.readUInt8();
            if (kdfFlag === 1) {
                umpVersion = reader.readUInt8(); // On-chain UMP version
                const algorithmBytes = readArray();
                const algorithm = sdk_1.Utils.toUTF8(algorithmBytes);
                const kdfParamsBytes = readArray();
                const kdfParamsJson = sdk_1.Utils.toUTF8(kdfParamsBytes);
                try {
                    const kdfParams = JSON.parse(kdfParamsJson);
                    passwordKdf = {
                        algorithm,
                        iterations: kdfParams.iterations,
                        memoryKiB: kdfParams.memoryKiB,
                        parallelism: kdfParams.parallelism,
                        hashLength: kdfParams.hashLength
                    };
                }
                catch (e) {
                    console.warn('Failed to parse KDF params during deserialization:', e);
                }
            }
        }
        // Read outpoint string
        const outpointLen = reader.readVarIntNum();
        const outpointBytes = reader.read(outpointLen);
        const currentOutpoint = sdk_1.Utils.toUTF8(outpointBytes);
        const token = {
            passwordSalt,
            passwordPresentationPrimary,
            passwordRecoveryPrimary,
            presentationRecoveryPrimary,
            passwordPrimaryPrivileged,
            presentationRecoveryPrivileged,
            presentationHash,
            recoveryHash,
            presentationKeyEncrypted,
            passwordKeyEncrypted,
            recoveryKeyEncrypted,
            profilesEncrypted,
            currentOutpoint,
            umpVersion,
            passwordKdf
        };
        return token;
    }
    /**
     * Sets up the root key infrastructure after authentication or loading from snapshot.
     * Initializes the root primary key, root privileged key manager, loads profiles,
     * and sets the authenticated flag. Does NOT switch profile initially.
     *
     * @param rootPrimaryKey      The user's root primary key (32 bytes).
     * @param ephemeralRootPrivilegedKey Optional root privileged key (e.g., during recovery flows).
     */
    async setupRootInfrastructure(rootPrimaryKey, ephemeralRootPrivilegedKey) {
        if (!this.currentUMPToken) {
            throw new Error('A UMP token must exist before setting up root infrastructure!');
        }
        this.rootPrimaryKey = rootPrimaryKey;
        // Store ephemeral key if provided, for one-time use by the manager
        let oneTimePrivilegedKey = ephemeralRootPrivilegedKey
            ? new sdk_1.PrivateKey(ephemeralRootPrivilegedKey)
            : undefined;
        // Create the ROOT PrivilegedKeyManager
        this.rootPrivilegedKeyManager = new PrivilegedKeyManager_1.PrivilegedKeyManager(async (reason) => {
            // 1. Use one-time key if available (for recovery)
            if (oneTimePrivilegedKey) {
                const tempKey = oneTimePrivilegedKey;
                oneTimePrivilegedKey = undefined; // Consume it
                return tempKey;
            }
            // 2. Otherwise, derive from password
            const password = await this.passwordRetriever(reason, async (passwordCandidate) => {
                try {
                    const derivedPasswordKey = await derivePasswordKey(this.currentUMPToken, sdk_1.Utils.toArray(passwordCandidate, 'utf8'));
                    const privilegedDecryptor = this.XOR(this.rootPrimaryKey, derivedPasswordKey);
                    const decryptedPrivileged = new sdk_1.SymmetricKey(privilegedDecryptor).decrypt(this.currentUMPToken.passwordPrimaryPrivileged);
                    return !!decryptedPrivileged; // Test passes if decryption works
                }
                catch (e) {
                    return false;
                }
            });
            // Decrypt the root privileged key using the confirmed password (with token-driven KDF)
            const derivedPasswordKey = await derivePasswordKey(this.currentUMPToken, sdk_1.Utils.toArray(password, 'utf8'));
            const privilegedDecryptor = this.XOR(this.rootPrimaryKey, derivedPasswordKey);
            const rootPrivilegedBytes = new sdk_1.SymmetricKey(privilegedDecryptor).decrypt(this.currentUMPToken.passwordPrimaryPrivileged);
            return new sdk_1.PrivateKey(rootPrivilegedBytes); // Return the ROOT key object
        });
        // Decrypt and load profiles if present in the token
        this.profiles = []; // Clear existing profiles before loading
        if (this.currentUMPToken.profilesEncrypted && this.currentUMPToken.profilesEncrypted.length > 0) {
            try {
                const decryptedProfileBytes = new sdk_1.SymmetricKey(rootPrimaryKey).decrypt(this.currentUMPToken.profilesEncrypted);
                const profilesJson = sdk_1.Utils.toUTF8(decryptedProfileBytes);
                this.profiles = JSON.parse(profilesJson);
            }
            catch (error) {
                console.error('Failed to decrypt or parse profiles:', error);
                // Decide if this should be fatal or just log and continue without profiles
                this.profiles = []; // Ensure profiles are empty on error
                // Optionally re-throw or handle more gracefully
                throw new Error(`Failed to load profiles: ${error.message}`);
            }
        }
        this.authenticated = true;
        // Note: We don't call switchProfile here anymore.
        // It's called by the auth methods (providePassword/provideRecoveryKey) or loadSnapshot after this.
    }
    /*
     * ---------------------------------------------------------------------------------------
     * Standard WalletInterface methods proxying to the *active* underlying wallet.
     * Includes authentication checks and admin originator protection.
     * ---------------------------------------------------------------------------------------
     */
    checkAuthAndUnderlying(originator) {
        if (!this.authenticated) {
            throw new Error('User is not authenticated.');
        }
        if (!this.underlying) {
            // This might happen if authentication succeeded but profile switching failed
            throw new Error('Underlying wallet for the active profile is not initialized.');
        }
        if (originator === this.adminOriginator) {
            throw new Error('External applications are not allowed to use the admin originator.');
        }
    }
    // Example proxy method (repeat pattern for all others)
    async getPublicKey(args, originator) {
        this.checkAuthAndUnderlying(originator);
        return this.underlying.getPublicKey(args, originator);
    }
    async revealCounterpartyKeyLinkage(args, originator) {
        this.checkAuthAndUnderlying(originator);
        return this.underlying.revealCounterpartyKeyLinkage(args, originator);
    }
    async revealSpecificKeyLinkage(args, originator) {
        this.checkAuthAndUnderlying(originator);
        return this.underlying.revealSpecificKeyLinkage(args, originator);
    }
    async encrypt(args, originator) {
        this.checkAuthAndUnderlying(originator);
        return this.underlying.encrypt(args, originator);
    }
    async decrypt(args, originator) {
        this.checkAuthAndUnderlying(originator);
        return this.underlying.decrypt(args, originator);
    }
    async createHmac(args, originator) {
        this.checkAuthAndUnderlying(originator);
        return this.underlying.createHmac(args, originator);
    }
    async verifyHmac(args, originator) {
        this.checkAuthAndUnderlying(originator);
        return this.underlying.verifyHmac(args, originator);
    }
    async createSignature(args, originator) {
        this.checkAuthAndUnderlying(originator);
        return this.underlying.createSignature(args, originator);
    }
    async verifySignature(args, originator) {
        this.checkAuthAndUnderlying(originator);
        return this.underlying.verifySignature(args, originator);
    }
    async createAction(args, originator) {
        this.checkAuthAndUnderlying(originator);
        return this.underlying.createAction(args, originator);
    }
    async signAction(args, originator) {
        this.checkAuthAndUnderlying(originator);
        return this.underlying.signAction(args, originator);
    }
    async abortAction(args, originator) {
        this.checkAuthAndUnderlying(originator);
        return this.underlying.abortAction(args, originator);
    }
    async listActions(args, originator) {
        this.checkAuthAndUnderlying(originator);
        return this.underlying.listActions(args, originator);
    }
    async internalizeAction(args, originator) {
        this.checkAuthAndUnderlying(originator);
        return this.underlying.internalizeAction(args, originator);
    }
    async listOutputs(args, originator) {
        this.checkAuthAndUnderlying(originator);
        return this.underlying.listOutputs(args, originator);
    }
    async relinquishOutput(args, originator) {
        this.checkAuthAndUnderlying(originator);
        return this.underlying.relinquishOutput(args, originator);
    }
    async acquireCertificate(args, originator) {
        this.checkAuthAndUnderlying(originator);
        return this.underlying.acquireCertificate(args, originator);
    }
    async listCertificates(args, originator) {
        this.checkAuthAndUnderlying(originator);
        return this.underlying.listCertificates(args, originator);
    }
    async proveCertificate(args, originator) {
        this.checkAuthAndUnderlying(originator);
        return this.underlying.proveCertificate(args, originator);
    }
    async relinquishCertificate(args, originator) {
        this.checkAuthAndUnderlying(originator);
        return this.underlying.relinquishCertificate(args, originator);
    }
    async discoverByIdentityKey(args, originator) {
        this.checkAuthAndUnderlying(originator);
        return this.underlying.discoverByIdentityKey(args, originator);
    }
    async discoverByAttributes(args, originator) {
        this.checkAuthAndUnderlying(originator);
        return this.underlying.discoverByAttributes(args, originator);
    }
    async isAuthenticated(_, originator) {
        if (!this.authenticated) {
            throw new Error('User is not authenticated.');
        }
        if (originator === this.adminOriginator) {
            throw new Error('External applications are not allowed to use the admin originator.');
        }
        return { authenticated: true };
    }
    async waitForAuthentication(_, originator) {
        if (originator === this.adminOriginator) {
            throw new Error('External applications are not allowed to use the admin originator.');
        }
        while (!this.authenticated || !this.underlying) {
            await new Promise(resolve => setTimeout(resolve, 100));
        }
        return await this.underlying.waitForAuthentication({}, originator);
    }
    async getHeight(_, originator) {
        this.checkAuthAndUnderlying(originator);
        return this.underlying.getHeight({}, originator);
    }
    async getHeaderForHeight(args, originator) {
        this.checkAuthAndUnderlying(originator);
        return this.underlying.getHeaderForHeight(args, originator);
    }
    async getNetwork(_, originator) {
        this.checkAuthAndUnderlying(originator);
        return this.underlying.getNetwork({}, originator);
    }
    async getVersion(_, originator) {
        this.checkAuthAndUnderlying(originator);
        return this.underlying.getVersion({}, originator);
    }
}
exports.CWIStyleWalletManager = CWIStyleWalletManager;
//# sourceMappingURL=CWIStyleWalletManager.js.map