/**
 * Enbox Connect Protocol
 *
 * A capability delegation protocol for DWN access. Enables apps to request
 * scoped permission grants from a wallet (provider), receiving a delegate DID
 * with the granted permissions.
 *
 * Two transport modes:
 * - Local (`dwn://connect`): same-device, direct HTTP against the local DWN
 * - Remote (`enbox://connect`): cross-device, relay-mediated with QR/deep link
 *
 * The protocol uses JWTs for signing, JWE (XChaCha20-Poly1305) for encryption,
 * and ECDH (Ed25519 → X25519 + HKDF) for key agreement.
 */
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __rest = (this && this.__rest) || function (s, e) {
    var t = {};
    for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
        t[p] = s[p];
    if (s != null && typeof Object.getOwnPropertySymbols === "function")
        for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
            if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
                t[p[i]] = s[p[i]];
        }
    return t;
};
import { DidJwk } from '@enbox/dids';
import { Convert, logger } from '@enbox/common';
import { CryptoUtils, Ed25519, EdDsaAlgorithm, Hkdf, X25519, XChaCha20Poly1305, } from '@enbox/crypto';
import { DwnInterfaceName, DwnMethodName } from '@enbox/dwn-sdk-js';
import { AgentPermissionsApi } from './permissions-api.js';
import { concatenateUrl } from './utils.js';
import { DwnInterface } from './types/dwn.js';
import { isRecordPermissionScope } from './dwn-api.js';
// ---------------------------------------------------------------------------
// URL building
// ---------------------------------------------------------------------------
/**
 * Builds the URL for a connect server endpoint.
 *
 * @param options.baseURL - The connect server base URL (e.g. `http://localhost:3000/connect/`)
 * @param options.endpoint - The endpoint type
 * @param options.authParam - Required for `authorize` endpoint (the request ID)
 * @param options.tokenParam - Required for `token` endpoint (the state value)
 */
function buildConnectUrl({ baseURL, endpoint, authParam, tokenParam, }) {
    switch (endpoint) {
        case 'pushedAuthorizationRequest':
            return concatenateUrl(baseURL, 'par');
        case 'authorize':
            if (!authParam) {
                throw new Error('authParam must be provided when building an authorize URL');
            }
            return concatenateUrl(baseURL, `authorize/${authParam}.jwt`);
        case 'callback':
            return concatenateUrl(baseURL, 'callback');
        case 'token':
            if (!tokenParam) {
                throw new Error('tokenParam must be provided when building a token URL');
            }
            return concatenateUrl(baseURL, `token/${tokenParam}.jwt`);
        default:
            throw new Error(`Unknown connect endpoint: ${endpoint}`);
    }
}
// ---------------------------------------------------------------------------
// JWT signing and verification
// ---------------------------------------------------------------------------
/** Signs an object as a JWT using an Ed25519 DID key. */
function signJwt(_a) {
    return __awaiter(this, arguments, void 0, function* ({ did, data, }) {
        const header = Convert.object({
            alg: 'EdDSA',
            kid: did.document.verificationMethod[0].id,
            typ: 'JWT',
        }).toBase64Url();
        const payload = Convert.object(data).toBase64Url();
        const signer = yield did.getSigner();
        const signature = yield signer.sign({
            data: Convert.string(`${header}.${payload}`).toUint8Array(),
        });
        const signatureBase64Url = Convert.uint8Array(signature).toBase64Url();
        return `${header}.${payload}.${signatureBase64Url}`;
    });
}
/** Verifies a JWT signature using the DID in the `kid` header. Returns the parsed payload. */
function verifyJwt(_a) {
    return __awaiter(this, arguments, void 0, function* ({ jwt }) {
        var _b, _c;
        const [headerB64U, payloadB64U, signatureB64U] = jwt.split('.');
        const header = Convert.base64Url(headerB64U).toObject();
        if (!header.kid) {
            throw new Error('Connect: JWT missing required "kid" header value.');
        }
        const { didDocument } = yield DidJwk.resolve(header.kid.split('#')[0]);
        if (!didDocument) {
            throw new Error('Connect: JWT verification failed — could not resolve DID.');
        }
        const { publicKeyJwk } = (_c = (_b = didDocument.verificationMethod) === null || _b === void 0 ? void 0 : _b.find((method) => {
            return method.id === header.kid;
        })) !== null && _c !== void 0 ? _c : {};
        if (!publicKeyJwk) {
            throw new Error('Connect: JWT verification failed — public key not found in DID document.');
        }
        const EdDsa = new EdDsaAlgorithm();
        const isValid = yield EdDsa.verify({
            key: publicKeyJwk,
            signature: Convert.base64Url(signatureB64U).toUint8Array(),
            data: Convert.string(`${headerB64U}.${payloadB64U}`).toUint8Array(),
        });
        if (!isValid) {
            throw new Error('Connect: JWT verification failed — invalid signature.');
        }
        return Convert.base64Url(payloadB64U).toObject();
    });
}
// ---------------------------------------------------------------------------
// Encryption: request (symmetric key via QR/deep link)
// ---------------------------------------------------------------------------
/** Encrypts the connect request JWT with a symmetric key (shared via QR code or deep link). */
function encryptRequest(_a) {
    return __awaiter(this, arguments, void 0, function* ({ jwt, encryptionKey, }) {
        const protectedHeader = {
            alg: 'dir',
            cty: 'JWT',
            enc: 'XC20P',
            typ: 'JWT',
        };
        const nonce = CryptoUtils.randomBytes(24);
        const additionalData = Convert.object(protectedHeader).toUint8Array();
        const jwtBytes = Convert.string(jwt).toUint8Array();
        const ciphertextAndTag = yield XChaCha20Poly1305.encryptRaw({ data: jwtBytes, keyBytes: encryptionKey, nonce, additionalData });
        const ciphertext = ciphertextAndTag.subarray(0, -16);
        const authenticationTag = ciphertextAndTag.subarray(-16);
        return [
            Convert.object(protectedHeader).toBase64Url(),
            '', // No wrapped key (direct encryption).
            Convert.uint8Array(nonce).toBase64Url(),
            Convert.uint8Array(ciphertext).toBase64Url(),
            Convert.uint8Array(authenticationTag).toBase64Url(),
        ].join('.');
    });
}
/** Decrypts an encrypted connect request JWE using the symmetric key from the QR/deep link. */
function decryptRequest(_a) {
    return __awaiter(this, arguments, void 0, function* ({ jwe, encryptionKey, }) {
        const [protectedHeaderB64U, , nonceB64U, ciphertextB64U, authenticationTagB64U,] = jwe.split('.');
        const encryptionKeyBytes = Convert.base64Url(encryptionKey).toUint8Array();
        const additionalData = Convert.base64Url(protectedHeaderB64U).toUint8Array();
        const nonce = Convert.base64Url(nonceB64U).toUint8Array();
        const ciphertext = Convert.base64Url(ciphertextB64U).toUint8Array();
        const authenticationTag = Convert.base64Url(authenticationTagB64U).toUint8Array();
        const ciphertextAndTag = new Uint8Array([...ciphertext, ...authenticationTag]);
        const decryptedJwtBytes = yield XChaCha20Poly1305.decryptRaw({ data: ciphertextAndTag, keyBytes: encryptionKeyBytes, nonce, additionalData });
        return Convert.uint8Array(decryptedJwtBytes).toString();
    });
}
// ---------------------------------------------------------------------------
// Encryption: response (ECDH shared key + optional PIN)
// ---------------------------------------------------------------------------
/** Derives a shared ECDH key for encrypting/decrypting the connect response. */
function deriveSharedKey(privateKeyDid, publicKeyDid) {
    return __awaiter(this, void 0, void 0, function* () {
        var _a, _b;
        const privatePortableDid = yield privateKeyDid.export();
        const publicJwk = (_a = publicKeyDid.verificationMethod) === null || _a === void 0 ? void 0 : _a[0].publicKeyJwk;
        const privateJwk = (_b = privatePortableDid.privateKeys) === null || _b === void 0 ? void 0 : _b[0];
        publicJwk.alg = 'EdDSA';
        const publicX25519 = yield Ed25519.convertPublicKeyToX25519({ publicKey: publicJwk });
        const privateX25519 = yield Ed25519.convertPrivateKeyToX25519({ privateKey: privateJwk });
        const sharedKey = yield X25519.sharedSecret({
            privateKeyA: privateX25519,
            publicKeyB: publicX25519,
        });
        return Hkdf.deriveKeyBytes({
            baseKeyBytes: new Uint8Array(sharedKey),
            hash: 'SHA-256',
            salt: new Uint8Array(),
            info: new Uint8Array(),
            length: 256,
        });
    });
}
/**
 * Encrypts the connect response JWT.
 *
 * For remote (relay-mediated) flows, `pin` is required — it is added to the
 * AAD to prevent MITM attacks via the untrusted relay.
 *
 * For local (same-device) flows, `pin` may be omitted — the ECDH encryption
 * alone is sufficient when there is no untrusted intermediary.
 */
function encryptResponse(_a) {
    return __awaiter(this, arguments, void 0, function* ({ jwt, encryptionKey, delegateDidKeyId, pin, }) {
        const protectedHeader = {
            alg: 'dir',
            cty: 'JWT',
            enc: 'XC20P',
            typ: 'JWT',
            kid: delegateDidKeyId,
        };
        const nonce = CryptoUtils.randomBytes(24);
        // Build AAD — include PIN if provided (remote flows).
        const aadObject = pin
            ? Object.assign(Object.assign({}, protectedHeader), { pin }) : Object.assign({}, protectedHeader);
        const additionalData = Convert.object(aadObject).toUint8Array();
        const jwtBytes = Convert.string(jwt).toUint8Array();
        const ciphertextAndTag = yield XChaCha20Poly1305.encryptRaw({ data: jwtBytes, keyBytes: encryptionKey, nonce, additionalData });
        const ciphertext = ciphertextAndTag.subarray(0, -16);
        const authenticationTag = ciphertextAndTag.subarray(-16);
        return [
            Convert.object(protectedHeader).toBase64Url(),
            '', // No wrapped key (direct encryption).
            Convert.uint8Array(nonce).toBase64Url(),
            Convert.uint8Array(ciphertext).toBase64Url(),
            Convert.uint8Array(authenticationTag).toBase64Url(),
        ].join('.');
    });
}
/**
 * Decrypts the connect response JWE using ECDH + optional PIN.
 *
 * @param clientDid - The ephemeral DID used at connect initiation (for ECDH).
 * @param jwe - The encrypted response JWE.
 * @param pin - The PIN entered by the user (required for remote flows, omit for local).
 */
function decryptResponse(clientDid, jwe, pin) {
    return __awaiter(this, void 0, void 0, function* () {
        const [protectedHeaderB64U, , nonceB64U, ciphertextB64U, authenticationTagB64U,] = jwe.split('.');
        const header = Convert.base64Url(protectedHeaderB64U).toObject();
        if (!header.kid) {
            throw new Error('Connect: JWE protected header is missing required "kid" property.');
        }
        const delegateResolvedDid = yield DidJwk.resolve(header.kid.split('#')[0]);
        const sharedKey = yield EnboxConnectProtocol.deriveSharedKey(clientDid, delegateResolvedDid.didDocument);
        // Build AAD — include PIN if provided (must match what was used during encryption).
        const aadObject = pin
            ? Object.assign(Object.assign({}, header), { pin }) : Object.assign({}, header);
        const AAD = Convert.object(aadObject).toUint8Array();
        const nonce = Convert.base64Url(nonceB64U).toUint8Array();
        const ciphertext = Convert.base64Url(ciphertextB64U).toUint8Array();
        const authenticationTag = Convert.base64Url(authenticationTagB64U).toUint8Array();
        const ciphertextAndTag = new Uint8Array([...ciphertext, ...authenticationTag]);
        const decryptedJwtBytes = yield XChaCha20Poly1305.decryptRaw({ data: ciphertextAndTag, keyBytes: sharedKey, nonce, additionalData: AAD });
        return Convert.uint8Array(decryptedJwtBytes).toString();
    });
}
// ---------------------------------------------------------------------------
// Request creation and retrieval
// ---------------------------------------------------------------------------
/** Creates an {@link EnboxConnectRequest}. */
function createConnectRequest(options) {
    return __awaiter(this, void 0, void 0, function* () {
        var _a;
        const stateBytes = CryptoUtils.randomBytes(16);
        const nonceBytes = CryptoUtils.randomBytes(16);
        return Object.assign(Object.assign({}, options), { nonce: Convert.uint8Array(nonceBytes).toBase64Url(), responseMode: 'direct_post', state: Convert.uint8Array(stateBytes).toBase64Url(), supportedDidMethods: (_a = options.supportedDidMethods) !== null && _a !== void 0 ? _a : ['did:dht', 'did:jwk'] });
    });
}
/**
 * Fetches an encrypted connect request from the authorize endpoint
 * and decrypts it using the encryption key from the QR/deep link.
 */
function getConnectRequest(requestUri, encryptionKey) {
    return __awaiter(this, void 0, void 0, function* () {
        const response = yield fetch(requestUri, { signal: AbortSignal.timeout(30000) });
        const jwe = yield response.text();
        const jwt = yield decryptRequest({ jwe, encryptionKey });
        return (yield verifyJwt({ jwt }));
    });
}
// ---------------------------------------------------------------------------
// Response creation
// ---------------------------------------------------------------------------
/** Creates an {@link EnboxConnectResponse} with timestamps. */
function createConnectResponse(options) {
    return __awaiter(this, void 0, void 0, function* () {
        const currentTimeInSeconds = Math.floor(Date.now() / 1000);
        return Object.assign(Object.assign({}, options), { iat: currentTimeInSeconds, exp: currentTimeInSeconds + 600 });
    });
}
// ---------------------------------------------------------------------------
// Permission grants
// ---------------------------------------------------------------------------
function shouldUseDelegatePermission(scope) {
    if (isRecordPermissionScope(scope)) {
        return true;
    }
    else if (scope.interface === DwnInterfaceName.Protocols && scope.method === DwnMethodName.Configure) {
        return true;
    }
    return false;
}
/**
 * Creates permission grants that assign the requested scopes to a delegate DID.
 */
function createPermissionGrants(selectedDid, delegateBearerDid, agent, scopes) {
    return __awaiter(this, void 0, void 0, function* () {
        const permissionsApi = new AgentPermissionsApi({ agent });
        logger.log(`Creating permission grants for ${scopes.length} scopes...`);
        const permissionGrants = yield Promise.all(scopes.map((scope) => {
            const delegated = shouldUseDelegatePermission(scope);
            return permissionsApi.createGrant({
                delegated,
                store: true,
                grantedTo: delegateBearerDid.uri,
                scope,
                dateExpires: '2040-06-25T16:09:16.693356Z', // TODO: make dateExpires configurable
                author: selectedDid,
            });
        }));
        // Resolve all DWN endpoints for the selected DID.  `sendDwnRequest` only
        // sends to the first reachable endpoint, but the sync engine may connect
        // to a different one and needs the grant to authenticate.  We send each
        // grant to every endpoint so that sync works regardless of which DWN the
        // agent contacts first.
        const dwnEndpointUrls = yield agent.dwn.getDwnEndpointUrlsForTarget(selectedDid);
        logger.log(`Sending ${permissionGrants.length} permission grants to ${dwnEndpointUrls.length} DWN endpoint(s)...`);
        const messagePromises = permissionGrants.map((grant) => __awaiter(this, void 0, void 0, function* () {
            const _a = grant.message, { encodedData } = _a, rawMessage = __rest(_a, ["encodedData"]);
            const data = Convert.base64Url(encodedData).toUint8Array();
            // The rawMessage is already signed by createGrant(), so we send it
            // directly to each endpoint without re-constructing.
            let atLeastOneSuccess = false;
            for (const dwnUrl of dwnEndpointUrls) {
                try {
                    const reply = yield agent.rpc.sendDwnRequest({
                        dwnUrl,
                        targetDid: selectedDid,
                        message: rawMessage,
                        data: new Blob([data]),
                    });
                    if (reply.status.code === 202 || reply.status.code === 409) {
                        atLeastOneSuccess = true;
                    }
                    else {
                        logger.error(`Grant send to ${dwnUrl} returned ${reply.status.code}: ${reply.status.detail}`);
                    }
                }
                catch (error) {
                    logger.error(`Grant send to ${dwnUrl} failed: ${error.message}`);
                }
            }
            if (!atLeastOneSuccess) {
                throw new Error('Could not send permission grant to any DWN endpoint.');
            }
            return grant.message;
        }));
        try {
            return yield Promise.all(messagePromises);
        }
        catch (error) {
            logger.error(`Error during batch-send of permission grants: ${error}`);
            throw error;
        }
    });
}
// ---------------------------------------------------------------------------
// Protocol installation
// ---------------------------------------------------------------------------
/**
 * Installs a DWN protocol on the provider's DWN if it doesn't already exist.
 * Ensures the protocol is available on both the local and remote DWN.
 */
function prepareProtocol(selectedDid, agent, protocolDefinition) {
    return __awaiter(this, void 0, void 0, function* () {
        const queryMessage = yield agent.processDwnRequest({
            author: selectedDid,
            messageType: DwnInterface.ProtocolsQuery,
            target: selectedDid,
            messageParams: { filter: { protocol: protocolDefinition.protocol } },
        });
        if (queryMessage.reply.status.code !== 200) {
            throw new Error(`Could not fetch protocol: ${queryMessage.reply.status.detail}`);
        }
        else if (queryMessage.reply.entries === undefined || queryMessage.reply.entries.length === 0) {
            logger.log(`Protocol does not exist, creating: ${protocolDefinition.protocol}`);
            const { reply: sendReply, message: configureMessage } = yield agent.sendDwnRequest({
                author: selectedDid,
                target: selectedDid,
                messageType: DwnInterface.ProtocolsConfigure,
                messageParams: { definition: protocolDefinition },
            });
            if (sendReply.status.code !== 202 && sendReply.status.code !== 409) {
                throw new Error(`Could not send protocol: ${sendReply.status.detail}`);
            }
            yield agent.processDwnRequest({
                author: selectedDid,
                target: selectedDid,
                messageType: DwnInterface.ProtocolsConfigure,
                rawMessage: configureMessage
            });
        }
        else {
            logger.log(`Protocol already exists: ${protocolDefinition.protocol}`);
            const configureMessage = queryMessage.reply.entries[0];
            const { reply: sendReply } = yield agent.sendDwnRequest({
                author: selectedDid,
                target: selectedDid,
                messageType: DwnInterface.ProtocolsConfigure,
                rawMessage: configureMessage,
            });
            if (sendReply.status.code !== 202 && sendReply.status.code !== 409) {
                throw new Error(`Could not send protocol: ${sendReply.status.detail}`);
            }
        }
    });
}
// ---------------------------------------------------------------------------
// Full wallet-side flow (provider submits response)
// ---------------------------------------------------------------------------
/**
 * Executes the full wallet-side (provider) flow:
 * 1. Creates a delegate DID
 * 2. Installs requested protocols
 * 3. Creates permission grants
 * 4. Builds, signs, and encrypts the response
 * 5. POSTs the encrypted response to the callback URL
 *
 * @param selectedDid - The provider's DID that is granting access.
 * @param connectRequest - The decoded connect request from the app.
 * @param pin - The PIN for response encryption AAD (required for remote flows).
 * @param agent - The agent instance for DWN operations.
 */
function submitConnectResponse(selectedDid, connectRequest, pin, agent) {
    return __awaiter(this, void 0, void 0, function* () {
        const delegateBearerDid = yield DidJwk.create();
        const delegatePortableDid = yield delegateBearerDid.export();
        const delegateGrantPromises = connectRequest.permissionRequests.map((permissionRequest) => __awaiter(this, void 0, void 0, function* () {
            const { protocolDefinition, permissionScopes } = permissionRequest;
            const grantsMatchProtocolUri = permissionScopes.every(scope => 'protocol' in scope && scope.protocol === protocolDefinition.protocol);
            if (!grantsMatchProtocolUri) {
                throw new Error('All permission scopes must match the protocol URI they are provided with.');
            }
            yield prepareProtocol(selectedDid, agent, protocolDefinition);
            return EnboxConnectProtocol.createPermissionGrants(selectedDid, delegateBearerDid, agent, permissionScopes);
        }));
        const delegateGrants = (yield Promise.all(delegateGrantPromises)).flat();
        logger.log('Building connect response...');
        const responseObject = yield EnboxConnectProtocol.createConnectResponse({
            providerDid: selectedDid,
            delegateDid: delegateBearerDid.uri,
            aud: connectRequest.clientDid,
            nonce: connectRequest.nonce,
            delegateGrants,
            delegatePortableDid,
        });
        logger.log('Signing connect response...');
        const responseObjectJwt = yield EnboxConnectProtocol.signJwt({
            did: delegateBearerDid,
            data: responseObject,
        });
        const clientDid = yield DidJwk.resolve(connectRequest.clientDid);
        const sharedKey = yield EnboxConnectProtocol.deriveSharedKey(delegateBearerDid, clientDid === null || clientDid === void 0 ? void 0 : clientDid.didDocument);
        logger.log('Encrypting connect response...');
        const encryptedResponse = yield EnboxConnectProtocol.encryptResponse({
            jwt: responseObjectJwt,
            encryptionKey: sharedKey,
            delegateDidKeyId: delegateBearerDid.document.verificationMethod[0].id,
            pin,
        });
        const formEncodedRequest = new URLSearchParams({
            id_token: encryptedResponse,
            state: connectRequest.state,
        }).toString();
        logger.log(`Sending connect response to: ${connectRequest.callbackUrl}`);
        yield fetch(connectRequest.callbackUrl, {
            body: formEncodedRequest,
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
            },
            signal: AbortSignal.timeout(30000),
        });
    });
}
// ---------------------------------------------------------------------------
// Namespace export
// ---------------------------------------------------------------------------
export const EnboxConnectProtocol = {
    buildConnectUrl,
    signJwt,
    verifyJwt,
    encryptRequest,
    decryptRequest,
    encryptResponse,
    decryptResponse,
    deriveSharedKey,
    createConnectRequest,
    getConnectRequest,
    createConnectResponse,
    createPermissionGrants,
    submitConnectResponse,
};
//# sourceMappingURL=enbox-connect-protocol.js.map