/**
 * 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 { concatenateUrl, Convert, logger, nowMs, timed } from '@enbox/common';
import { CryptoUtils, Ed25519, EdDsaAlgorithm, Hkdf, X25519, XChaCha20Poly1305, } from '@enbox/crypto';
import { Did, DidJwk } from '@enbox/dids';
import { DwnInterfaceName, DwnMethodName, PermissionsProtocol, Time } from '@enbox/dwn-sdk-js';
import { AgentPermissionsApi } from './permissions-api.js';
import { createGrantKeyRecordsForGrants as createGrantKeyRecordsForGrantsFn } from './dwn-encryption.js';
import { DwnInterface } from './types/dwn.js';
import { isRecordPermissionScope } from './dwn-api.js';
import { mapConcurrent, mapConcurrentSettled } from './utils.js';
// ---------------------------------------------------------------------------
// Tunables
// ---------------------------------------------------------------------------
/**
 * Maximum number of in-flight DWN-endpoint sends issued by the connect flow
 * (permission grants + revocation grants). Caps total concurrency across all
 * `(grant, endpoint)` pairs so that a request with many permissions and/or a
 * tenant with many DWN endpoints cannot stampede the network. Tuned to be
 * generous enough to hide endpoint latency while staying well under typical
 * per-host browser connection limits and server-side rate limits.
 */
const CONNECT_FANOUT_CONCURRENCY = 8;
/**
 * Per-request abort budget applied to every DWN-endpoint `sendDwnRequest`
 * issued during the connect flow. The HttpDwnRpcClient's default per-attempt
 * timeout is 30 s with 3 retries (~120 s worst-case per request) — that
 * scales unacceptably when bounded fan-out has to wait for every settled
 * task. With this budget, an unhealthy / cold endpoint short-circuits the
 * retry loop within a few seconds (AbortError is non-retryable), keeping
 * the user-visible "Authorizing…" wait bounded even when one of N DWN
 * endpoints is misbehaving.
 *
 * Sync delivers any missed copies eventually, so aborting fast is safe:
 * the connect-flow fan-outs are best-effort and tolerate per-task failure.
 */
const CONNECT_REQUEST_TIMEOUT_MS = 10000;
/** Log namespace used for wallet-side connect critical-path timings. */
const CONNECT_PERF_LOG_PREFIX = '[connect.perf]';
/**
 * Default lifetime for app delegate grants created by a connect approval.
 *
 * This is a hard grant expiry. There is intentionally no renewal path in this
 * protocol layer yet; clients should treat expired connect grants as
 * reconnect-required.
 */
export const CONNECT_SESSION_DEFAULT_TTL_SECONDS = 24 * 60 * 60;
/** Maximum lifetime a wallet will stamp onto relay connect session grants. */
export const CONNECT_SESSION_MAX_TTL_SECONDS = 90 * 24 * 60 * 60;
const CONNECT_SESSION_METADATA_LIMITS = {
    id: 128,
    appName: 128,
    appIcon: 2048,
    origin: 512,
    userAgent: 512,
    platform: 128,
    language: 64,
    languages: 16,
    timezone: 128,
};
// ---------------------------------------------------------------------------
// 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.
 *
 * `data` is constrained to `object` so callers don't have to widen
 * typed payload shapes (e.g. `EnboxConnectResponse`) to
 * `Record<string, unknown>` at the call site. `Convert.object(data)`
 * stringifies whatever JSON-serializable shape is passed.
 */
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 as an untyped object.
 *
 * The return type is intentionally `Record<string, unknown>` rather than a
 * caller-supplied generic — a JWT payload is bytes from a remote party, and
 * we can't soundly assert its shape without runtime validation. Callers must
 * apply one of the {@link assertConnectRequest} / {@link assertConnectResponse}
 * assertion helpers (or their own type guard) to narrow the payload before
 * accessing fields.
 */
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.');
        }
        const decoded = Convert.base64Url(payloadB64U).toObject();
        if (typeof decoded !== 'object' || decoded === null || Array.isArray(decoded)) {
            throw new Error('Connect: JWT verification failed — payload must be a JSON object.');
        }
        return decoded;
    });
}
// ─── Field-level validation helpers ─────────────────────────────────────
//
// The connect-request / connect-response assertions below describe their
// expected shape declaratively in terms of these primitive checks. Each
// helper throws a consistent `Connect: <context> — \`<field>\` <reason>`
// message on mismatch, so per-field error formatting is centralized here
// rather than duplicated at every call site.
/**
 * Throws a shape-validation error during connect JWT assertion.
 *
 * Deliberately throws plain `Error` rather than `TypeError` even though
 * the failures are runtime type-shape mismatches. The reason is layered
 * error handling: boundary-validation failures need to propagate through
 * the same `try/catch` paths as every other connect-flow error — vault
 * lock failure, JWT signature failure, DWN request failure, etc. —
 * without `catch` blocks having to special-case a `TypeError` subclass.
 *
 * SonarCloud's `typescript:S7786` flags this as "too unspecific for a
 * type check" and prefers `TypeError`. We suppress it at the file level
 * (see `sonar-project.properties`) because every `require*` helper goes
 * through this single throw site.
 */
function fail(context, field, reason) {
    throw new Error(`Connect: ${context} — \`${field}\` ${reason}.`);
}
function requireString(payload, field, context) {
    if (typeof payload[field] !== 'string') {
        fail(context, field, 'must be a string');
    }
}
function requireNumber(payload, field, context) {
    if (typeof payload[field] !== 'number') {
        fail(context, field, 'must be a number');
    }
}
function requireOptionalNumber(payload, field, context) {
    if (payload[field] !== undefined && typeof payload[field] !== 'number') {
        fail(context, field, 'must be a number when present');
    }
}
function requireArray(payload, field, context) {
    if (!Array.isArray(payload[field])) {
        fail(context, field, 'must be an array');
    }
}
function requireLiteral(payload, field, expected, context) {
    if (payload[field] !== expected) {
        fail(context, field, `must be ${JSON.stringify(expected)}`);
    }
}
function requireStringArray(payload, field, context) {
    const value = payload[field];
    if (!Array.isArray(value) || !value.every((item) => typeof item === 'string')) {
        fail(context, field, 'must be a string[]');
    }
}
function requireOptionalString(payload, field, context) {
    if (payload[field] !== undefined && typeof payload[field] !== 'string') {
        fail(context, field, 'must be a string when present');
    }
}
function requireOptionalObject(payload, field, context) {
    const value = payload[field];
    if (value !== undefined && (typeof value !== 'object' || value === null || Array.isArray(value))) {
        fail(context, field, 'must be an object when present');
    }
}
function requireOptionalArray(payload, field, context) {
    if (payload[field] !== undefined && !Array.isArray(payload[field])) {
        fail(context, field, 'must be an array when present');
    }
}
// ─── Boundary assertions ────────────────────────────────────────────────
//
// Each `assertConnect*` describes the shape of its target type as a list
// of per-field requirements. The structural existence/primitive checks
// are the only validation done at the JWT-payload boundary; deeper
// validation of nested arrays/objects (permission scopes, grants,
// portable DID structure) happens downstream in DWN-aware validators
// where the richer logic already lives.
/**
 * Runtime assertion that a verified JWT payload has the shape of an
 * {@link EnboxConnectRequest}. Use immediately after `verifyJwt()` to narrow
 * a `Record<string, unknown>` payload before accessing fields.
 */
function assertConnectRequest(payload) {
    const ctx = 'invalid connect request';
    requireString(payload, 'clientDid', ctx);
    requireString(payload, 'appName', ctx);
    requireOptionalString(payload, 'appIcon', ctx);
    requireOptionalObject(payload, 'clientMetadata', ctx);
    requireOptionalNumber(payload, 'requestedSessionTtlSeconds', ctx);
    requireOptionalString(payload, 'delegateDid', ctx);
    requireArray(payload, 'permissionRequests', ctx);
    requireString(payload, 'nonce', ctx);
    requireString(payload, 'state', ctx);
    requireString(payload, 'callbackUrl', ctx);
    requireLiteral(payload, 'responseMode', 'direct_post', ctx);
    requireStringArray(payload, 'supportedDidMethods', ctx);
}
/**
 * Runtime assertion that a verified JWT payload has the shape of an
 * {@link EnboxConnectResponse}. Use immediately after `verifyJwt()` to narrow
 * a `Record<string, unknown>` payload before accessing fields.
 */
function assertConnectResponse(payload) {
    const ctx = 'invalid connect response';
    requireString(payload, 'providerDid', ctx);
    requireString(payload, 'delegateDid', ctx);
    requireString(payload, 'aud', ctx);
    requireNumber(payload, 'iat', ctx);
    requireNumber(payload, 'exp', ctx);
    requireOptionalString(payload, 'nonce', ctx);
    requireArray(payload, 'delegateGrants', ctx);
    requireOptionalObject(payload, 'delegatePortableDid', ctx);
    requireOptionalArray(payload, 'sessionRevocations', ctx);
}
// ---------------------------------------------------------------------------
// 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)
// ---------------------------------------------------------------------------
/**
 * Core ECDH key derivation from a raw public key JWK.
 *
 * Converts both keys to X25519, performs ECDH, and derives the final
 * symmetric key via HKDF-SHA-256.
 */
function deriveSharedKeyFromJwk(privateKeyDid, publicKeyJwk) {
    return __awaiter(this, void 0, void 0, function* () {
        var _a;
        const privatePortableDid = yield privateKeyDid.export();
        const privateJwk = (_a = privatePortableDid.privateKeys) === null || _a === void 0 ? void 0 : _a[0];
        const pubJwk = Object.assign(Object.assign({}, publicKeyJwk), { alg: 'EdDSA' });
        const publicX25519 = yield Ed25519.convertPublicKeyToX25519({ publicKey: pubJwk });
        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,
        });
    });
}
/** 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;
        const publicJwk = (_a = publicKeyDid.verificationMethod) === null || _a === void 0 ? void 0 : _a[0].publicKeyJwk;
        return deriveSharedKeyFromJwk(privateKeyDid, publicJwk);
    });
}
/**
 * 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, delegatePublicKeyJwk, pin, }) {
        // Include only the minimum key material (kty, crv, x) in the ephemeral
        // public key header.  This avoids leaking DID-level identifiers (kid,
        // alg, etc.) that would let the relay correlate sessions or resolve
        // the delegate DID.  See https://github.com/enboxorg/enbox/issues/890
        const epk = {
            kty: delegatePublicKeyJwk.kty,
            crv: delegatePublicKeyJwk.crv,
            x: delegatePublicKeyJwk.x,
        };
        const protectedHeader = {
            alg: 'dir',
            cty: 'JWT',
            enc: 'XC20P',
            typ: 'JWT',
            epk,
        };
        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.epk || typeof header.epk !== 'object') {
            throw new Error('Connect: JWE protected header is missing required "epk" property.');
        }
        const sharedKey = yield deriveSharedKeyFromJwk(clientDid, header.epk);
        // 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 });
        const payload = yield verifyJwt({ jwt });
        assertConnectRequest(payload);
        return payload;
    });
}
// ---------------------------------------------------------------------------
// 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 assertConnectGrantScope(scope) {
    if (isRecordPermissionScope(scope)) {
        const method = scope.method;
        if (method === DwnMethodName.Query ||
            method === DwnMethodName.Subscribe ||
            method === DwnMethodName.Count) {
            throw new Error(`Records.${method} grants are not supported by connect; request Records.Read instead.`);
        }
        return;
    }
    if (scope.interface === DwnInterfaceName.Protocols && scope.method === DwnMethodName.Configure) {
        throw new Error('Protocols.Configure cannot be delegated through connect; the wallet configures protocols during approval.');
    }
}
function shouldUseDelegatePermission(scope) {
    return isRecordPermissionScope(scope);
}
function isConnectReadScope(scope) {
    return isRecordPermissionScope(scope) && scope.method === DwnMethodName.Read;
}
function randomSessionId() {
    return Convert.uint8Array(CryptoUtils.randomBytes(16)).toBase64Url();
}
function boundedSessionString(value, maxLength) {
    if (typeof value !== 'string' || value.length === 0) {
        return undefined;
    }
    return value.slice(0, maxLength);
}
function boundedSessionStringArray(values) {
    if (!Array.isArray(values)) {
        return undefined;
    }
    const bounded = values
        .filter((value) => typeof value === 'string' && value.length > 0)
        .slice(0, CONNECT_SESSION_METADATA_LIMITS.languages)
        .map((value) => value.slice(0, CONNECT_SESSION_METADATA_LIMITS.language));
    return bounded.length > 0 ? bounded : undefined;
}
function createConnectSessionMetadata(options = {}) {
    var _a, _b, _c, _d, _e;
    const createdAt = (_a = options.createdAt) !== null && _a !== void 0 ? _a : Time.getCurrentTimestamp();
    const expiresAt = (_b = options.expiresAt) !== null && _b !== void 0 ? _b : Time.createOffsetTimestamp({
        seconds: (_c = options.ttlSeconds) !== null && _c !== void 0 ? _c : CONNECT_SESSION_DEFAULT_TTL_SECONDS,
    }, createdAt);
    const clientMetadata = (_d = options.clientMetadata) !== null && _d !== void 0 ? _d : {};
    const appName = boundedSessionString(options.appName, CONNECT_SESSION_METADATA_LIMITS.appName);
    const appIcon = boundedSessionString(options.appIcon, CONNECT_SESSION_METADATA_LIMITS.appIcon);
    const origin = boundedSessionString(clientMetadata.origin, CONNECT_SESSION_METADATA_LIMITS.origin);
    const userAgent = boundedSessionString(clientMetadata.userAgent, CONNECT_SESSION_METADATA_LIMITS.userAgent);
    const platform = boundedSessionString(clientMetadata.platform, CONNECT_SESSION_METADATA_LIMITS.platform);
    const language = boundedSessionString(clientMetadata.language, CONNECT_SESSION_METADATA_LIMITS.language);
    const languages = boundedSessionStringArray(clientMetadata.languages);
    const timezone = boundedSessionString(clientMetadata.timezone, CONNECT_SESSION_METADATA_LIMITS.timezone);
    return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ id: (_e = boundedSessionString(options.id, CONNECT_SESSION_METADATA_LIMITS.id)) !== null && _e !== void 0 ? _e : randomSessionId(), createdAt,
        expiresAt }, (appName ? { appName } : {})), (appIcon ? { appIcon } : {})), (origin ? { origin } : {})), (userAgent ? { userAgent } : {})), (platform ? { platform } : {})), (language ? { language } : {})), (languages ? { languages } : {})), (timezone ? { timezone } : {})), (options.transport ? { transport: options.transport } : {}));
}
function resolveRequestedSessionTtlSeconds(requestedSessionTtlSeconds) {
    if (requestedSessionTtlSeconds === undefined) {
        return CONNECT_SESSION_DEFAULT_TTL_SECONDS;
    }
    const requestedWholeSeconds = Math.floor(requestedSessionTtlSeconds);
    if (!Number.isFinite(requestedSessionTtlSeconds) || requestedWholeSeconds <= 0) {
        throw new Error('Connect requestedSessionTtlSeconds must resolve to at least one whole second.');
    }
    return Math.min(requestedWholeSeconds, CONNECT_SESSION_MAX_TTL_SECONDS);
}
function hasEncryptedProtocolTypes(protocolDefinition) {
    var _a;
    return Object.values((_a = protocolDefinition.types) !== null && _a !== void 0 ? _a : {})
        .some((type) => (type === null || type === void 0 ? void 0 : type.encryptionRequired) === true);
}
function hasEncryptedReadScope(permissionRequest) {
    return hasEncryptedProtocolTypes(permissionRequest.protocolDefinition)
        && permissionRequest.permissionScopes.some(isConnectReadScope);
}
function resolvePreSuppliedDelegateDid(delegateDid) {
    if (delegateDid === undefined) {
        return undefined;
    }
    if (delegateDid.trim() === '' || delegateDid.trim() !== delegateDid) {
        throw new Error('Connect delegateDid must be a non-empty DID URI.');
    }
    const parsedDelegateDid = Did.parse(delegateDid);
    if (parsedDelegateDid === null || parsedDelegateDid.uri !== delegateDid) {
        throw new Error('Connect delegateDid must be a valid DID URI.');
    }
    return delegateDid;
}
function resolveConnectPermissionGrantOptions(options) {
    var _a, _b, _c;
    if ((options === null || options === void 0 ? void 0 : options.dateExpires) && ((_a = options.connectSession) === null || _a === void 0 ? void 0 : _a.expiresAt) && options.dateExpires !== options.connectSession.expiresAt) {
        throw new Error('Connect grant dateExpires must match connectSession.expiresAt.');
    }
    const connectSession = (_b = options === null || options === void 0 ? void 0 : options.connectSession) !== null && _b !== void 0 ? _b : createConnectSessionMetadata({
        expiresAt: options === null || options === void 0 ? void 0 : options.dateExpires,
    });
    return {
        dateExpires: (_c = options === null || options === void 0 ? void 0 : options.dateExpires) !== null && _c !== void 0 ? _c : connectSession.expiresAt,
        connectSession,
    };
}
/**
 * Creates permission grants that assign the requested scopes to a delegate DID.
 */
function createPermissionGrants(selectedDid, delegateDid, agent, scopes, options) {
    return __awaiter(this, void 0, void 0, function* () {
        const permissionsApi = new AgentPermissionsApi({ agent });
        const { dateExpires, connectSession } = resolveConnectPermissionGrantOptions(options);
        logger.log(`Creating permission grants for ${scopes.length} scopes...`);
        for (const scope of scopes) {
            assertConnectGrantScope(scope);
        }
        const permissionGrants = yield Promise.all(scopes.map((scope) => {
            const delegated = shouldUseDelegatePermission(scope);
            return permissionsApi.createGrant({
                delegated,
                store: true,
                grantedTo: delegateDid,
                scope,
                dateExpires,
                author: selectedDid,
                connectSession,
            });
        }));
        // 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)...`);
        // Flatten (grant, endpoint) tuples into a single list of sends so that one
        // global concurrency cap governs total in-flight requests during the
        // connect flow — important when either dimension grows large.
        const sendTasks = permissionGrants.flatMap((grant, grantIndex) => {
            const _a = grant.message, { encodedData } = _a, rawMessage = __rest(_a, ["encodedData"]);
            const data = Convert.base64Url(encodedData).toUint8Array();
            return dwnEndpointUrls.map((dwnUrl) => ({ grantIndex, dwnUrl, rawMessage, data }));
        });
        const settled = yield mapConcurrentSettled(sendTasks, CONNECT_FANOUT_CONCURRENCY, (_a) => __awaiter(this, [_a], void 0, function* ({ grantIndex, dwnUrl, rawMessage, data }) {
            const reply = yield agent.rpc.sendDwnRequest({
                dwnUrl,
                targetDid: selectedDid,
                message: rawMessage,
                data: new Blob([data]),
                signal: AbortSignal.timeout(CONNECT_REQUEST_TIMEOUT_MS),
            });
            return { grantIndex, dwnUrl, reply };
        }));
        // Aggregate results back per grant: each grant must have at least one
        // endpoint accept it (status 202 or 409 — already-stored is acceptable).
        const successPerGrant = new Array(permissionGrants.length).fill(false);
        for (let i = 0; i < settled.length; i++) {
            const result = settled[i];
            if (result.status === 'rejected') {
                const reason = result.reason instanceof Error ? result.reason.message : String(result.reason);
                logger.error(`Grant send to ${sendTasks[i].dwnUrl} failed: ${reason}`);
                continue;
            }
            const { grantIndex, dwnUrl, reply } = result.value;
            if (reply.status.code === 202 || reply.status.code === 409) {
                successPerGrant[grantIndex] = true;
            }
            else {
                logger.error(`Grant send to ${dwnUrl} returned ${reply.status.code}: ${reply.status.detail}`);
            }
        }
        for (let g = 0; g < permissionGrants.length; g++) {
            if (!successPerGrant[g]) {
                logger.error(`Error during batch-send of permission grants: grant ${g} reached no DWN endpoint.`);
                throw new Error('Could not send permission grant to any DWN endpoint.');
            }
        }
        return permissionGrants.map((g) => g.message);
    });
}
function fanOutDataEncodedRecords(ownerDid, agent, records) {
    return __awaiter(this, void 0, void 0, function* () {
        if (records.length === 0) {
            return;
        }
        const dwnEndpointUrls = yield agent.dwn.getDwnEndpointUrlsForTarget(ownerDid);
        const sendTasks = records.flatMap((record, recordIndex) => {
            const { encodedData } = record, rawMessage = __rest(record, ["encodedData"]);
            const data = Convert.base64Url(encodedData).toUint8Array();
            return dwnEndpointUrls.map((dwnUrl) => ({ recordIndex, dwnUrl, rawMessage, data }));
        });
        const settled = yield mapConcurrentSettled(sendTasks, CONNECT_FANOUT_CONCURRENCY, (_a) => __awaiter(this, [_a], void 0, function* ({ recordIndex, dwnUrl, rawMessage, data }) {
            const reply = yield agent.rpc.sendDwnRequest({
                dwnUrl,
                targetDid: ownerDid,
                message: rawMessage,
                data: new Blob([data]),
                signal: AbortSignal.timeout(CONNECT_REQUEST_TIMEOUT_MS),
            });
            return { recordIndex, dwnUrl, reply };
        }));
        const successPerRecord = new Array(records.length).fill(false);
        for (let i = 0; i < settled.length; i++) {
            const result = settled[i];
            if (result.status === 'rejected') {
                const reason = result.reason instanceof Error ? result.reason.message : String(result.reason);
                logger.error(`Record send to ${sendTasks[i].dwnUrl} failed: ${reason}`);
                continue;
            }
            const { recordIndex, dwnUrl, reply } = result.value;
            if (reply.status.code === 202 || reply.status.code === 409) {
                successPerRecord[recordIndex] = true;
            }
            else {
                logger.error(`Record send to ${dwnUrl} returned ${reply.status.code}: ${reply.status.detail}`);
            }
        }
        for (let i = 0; i < successPerRecord.length; i++) {
            if (!successPerRecord[i]) {
                throw new Error('Could not send grantKey record to any DWN endpoint.');
            }
        }
    });
}
// ---------------------------------------------------------------------------
// Protocol installation
// ---------------------------------------------------------------------------
/**
 * Ensures the protocol is installed on the provider's local DWN so that the
 * agent can sign and (when applicable) encrypt grants for it during
 * `submitConnectResponse`.
 *
 * Remote installation (push to every owner DWN endpoint) is the
 * responsibility of the calling client (the wallet's own `prepareProtocol`
 * runs *before* `submitConnectResponse` and fans out to every endpoint in
 * parallel). When the protocol already exists locally — the common case —
 * this function performs a single local `ProtocolsQuery` and returns: there
 * is no remote send, so a slow/unhealthy DWN endpoint cannot block the
 * "Authorizing…" hot path.
 *
 * When the protocol is *not* installed locally — a safety fallback for
 * callers that did not pre-install — the protocol is configured locally
 * (with `encryption: true` when any type declares `encryptionRequired: true`,
 * so the agent injects `$keyAgreement` keys derived from the owner's X25519
 * root key) and then fanned out to every owner DWN endpoint with bounded
 * concurrency and a short per-request budget. Endpoint failures are
 * non-fatal — sync delivers any missing copies eventually.
 */
function prepareProtocol(selectedDid, agent, protocolDefinition) {
    return __awaiter(this, void 0, void 0, function* () {
        var _a;
        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}`);
        }
        const isInstalledLocally = queryMessage.reply.entries !== undefined
            && queryMessage.reply.entries.length > 0;
        if (isInstalledLocally) {
            // Already installed locally. The wallet's pre-call `prepareProtocol`
            // is responsible for fanning the protocol out to every owner DWN
            // endpoint; sync delivers any missing copies eventually. Skipping the
            // remote send here turns this hot path into a single local DB read
            // (~10 ms) instead of a sequential per-endpoint network round-trip
            // with retries — the latter could take minutes if any endpoint was
            // slow or unreachable.
            logger.log(`Protocol already installed locally: ${protocolDefinition.protocol}`);
            return;
        }
        // Safety fallback — protocol is missing locally, so the caller did not
        // pre-install. Configure it locally (with encryption derivation if any
        // type requires it) so the agent can sign/encrypt grants, then push to
        // every owner DWN endpoint in parallel with a short per-request budget.
        logger.log(`Protocol not installed, configuring locally: ${protocolDefinition.protocol}`);
        const needsEncryption = Object.values((_a = protocolDefinition.types) !== null && _a !== void 0 ? _a : {})
            .some((type) => (type === null || type === void 0 ? void 0 : type.encryptionRequired) === true);
        const { reply: configureReply, message: configureMessage } = yield agent.processDwnRequest({
            author: selectedDid,
            target: selectedDid,
            messageType: DwnInterface.ProtocolsConfigure,
            messageParams: { definition: protocolDefinition },
            encryption: needsEncryption || undefined,
        });
        if (configureReply.status.code !== 202 && configureReply.status.code !== 409) {
            throw new Error(`Could not configure protocol locally: ${configureReply.status.detail}`);
        }
        let dwnEndpointUrls = [];
        try {
            dwnEndpointUrls = yield agent.dwn.getDwnEndpointUrlsForTarget(selectedDid);
        }
        catch (_b) {
            // Endpoint resolution failure — protocol stays local-only until sync.
        }
        if (dwnEndpointUrls.length === 0) {
            return;
        }
        // Best-effort remote fan-out with bounded concurrency and a per-request
        // abort signal. Failures are tolerated (sync delivers eventually).
        yield mapConcurrentSettled(dwnEndpointUrls, CONNECT_FANOUT_CONCURRENCY, (dwnUrl) => agent.rpc.sendDwnRequest({
            dwnUrl,
            targetDid: selectedDid,
            message: configureMessage,
            signal: AbortSignal.timeout(CONNECT_REQUEST_TIMEOUT_MS),
        }));
    });
}
// ---------------------------------------------------------------------------
// Full wallet-side flow (provider submits response)
// ---------------------------------------------------------------------------
/**
 * Executes the full wallet-side (provider) flow:
 * 1. Uses a requester-supplied delegate DID, or creates one when omitted
 * 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 submitStart = nowMs();
        const numProtocols = connectRequest.permissionRequests.length;
        const numScopes = connectRequest.permissionRequests.reduce((sum, req) => sum + req.permissionScopes.length, 0);
        // Tracked across the try/finally so the aggregate `submitConnectResponse.total`
        // log emits on both success and failure paths — operators bisecting wall-time
        // from wallet debug logs need the total even when a phase throws.
        let sessionGrantCount = 0;
        let outcome = 'ok';
        logger.log(`${CONNECT_PERF_LOG_PREFIX} submitConnectResponse.start `
            + `(protocols=${numProtocols}, scopes=${numScopes})`);
        try {
            const sessionTtlSeconds = resolveRequestedSessionTtlSeconds(connectRequest.requestedSessionTtlSeconds);
            const preSuppliedDelegateDid = resolvePreSuppliedDelegateDid(connectRequest.delegateDid);
            if (preSuppliedDelegateDid !== undefined && connectRequest.permissionRequests.some(hasEncryptedReadScope)) {
                throw new Error('Connect pre-supplied delegate DID cannot be used with encrypted read scopes yet; use a wallet-minted delegate for encrypted protocols.');
            }
            let delegatePortableDid;
            let delegateRootPrivateKey;
            let responseBearerDid;
            let grantedDelegateDid;
            if (preSuppliedDelegateDid !== undefined) {
                grantedDelegateDid = preSuppliedDelegateDid;
                responseBearerDid = yield timed(`${CONNECT_PERF_LOG_PREFIX} responseDid.create`, () => DidJwk.create());
            }
            else {
                const delegateBearerDid = yield timed(`${CONNECT_PERF_LOG_PREFIX} delegateDid.create`, () => DidJwk.create());
                delegatePortableDid = yield delegateBearerDid.export();
                // Add X25519 key derived from the delegate's Ed25519 key.
                // did:jwk only supports one verification method, but DWN encryption
                // requires X25519 for key agreement. Including the derived X25519
                // private key in the PortableDid ensures the delegate agent's KMS
                // has both keys after import. The Ed25519→X25519 conversion is a
                // standard cryptographic operation (RFC 8032 / libsodium).
                const delegateEdPrivateKey = delegatePortableDid.privateKeys[0];
                delegateRootPrivateKey = (yield Ed25519.convertPrivateKeyToX25519({
                    privateKey: delegateEdPrivateKey,
                }));
                delegatePortableDid.privateKeys.push(delegateRootPrivateKey);
                responseBearerDid = delegateBearerDid;
                grantedDelegateDid = delegateBearerDid.uri;
            }
            const connectSession = createConnectSessionMetadata({
                appName: connectRequest.appName,
                appIcon: connectRequest.appIcon,
                ttlSeconds: sessionTtlSeconds,
                clientMetadata: connectRequest.clientMetadata,
                transport: 'relay',
            });
            const durableGrantKeyRecords = [];
            const delegateGrants = yield timed(`${CONNECT_PERF_LOG_PREFIX} permissionGrants.fanout (protocols=${numProtocols})`, () => __awaiter(this, void 0, void 0, function* () {
                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);
                    const hasEncryptedTypes = hasEncryptedProtocolTypes(protocolDefinition);
                    const hasEncryptedReadScopes = hasEncryptedTypes && permissionScopes.some(isConnectReadScope);
                    const grants = yield EnboxConnectProtocol.createPermissionGrants(selectedDid, grantedDelegateDid, agent, permissionScopes, { connectSession });
                    if (hasEncryptedReadScopes) {
                        if (delegateRootPrivateKey === undefined) {
                            throw new Error('Connect encrypted read grants require a wallet-minted delegate DID.');
                        }
                        const grantKeyRecords = yield EnboxConnectProtocol.createGrantKeyRecordsForGrants({
                            agent,
                            ownerDid: selectedDid,
                            granteeDid: grantedDelegateDid,
                            granteeRootPrivateKey: delegateRootPrivateKey,
                            grantMessages: grants,
                            protocolDefinitions: [protocolDefinition],
                        });
                        durableGrantKeyRecords.push(...grantKeyRecords);
                    }
                    return grants;
                }));
                return (yield Promise.all(delegateGrantPromises)).flat();
            }));
            yield timed(`${CONNECT_PERF_LOG_PREFIX} grantKeys.fanout (n=${durableGrantKeyRecords.length})`, () => fanOutDataEncodedRecords(selectedDid, agent, durableGrantKeyRecords));
            // Create per-grant contextId-scoped revocation grants.
            // Each revocation grant authorizes the delegate to write a revocation
            // ONLY for the specific session grant it corresponds to.
            const permissionsApi = new AgentPermissionsApi({ agent });
            const sessionRevocations = [];
            let revGrantEndpoints = [];
            try {
                revGrantEndpoints = yield agent.dwn.getDwnEndpointUrlsForTarget(selectedDid);
            }
            catch (_a) {
                // Endpoint resolution failure — revocation grants will be local-only until sync.
            }
            // Snapshot the current length — revocation grants are appended to delegateGrants
            // below, but we must NOT iterate over them (they are meta-grants, not session grants).
            sessionGrantCount = delegateGrants.length;
            // Create all revocation grants locally with bounded concurrency.
            // createGrant is local-only (storage + signing) so it's cheap, but we still
            // cap parallelism to avoid head-of-line blocking when sessionGrantCount is
            // large (e.g. dapp requesting many scopes at once).
            // Revocation grants share the same hard expiry but do not duplicate
            // connectSession display metadata; session grouping should use the
            // user-facing permission grants.
            const revGrantResults = yield timed(`${CONNECT_PERF_LOG_PREFIX} revocationGrants.create (n=${sessionGrantCount})`, () => mapConcurrent(delegateGrants.slice(0, sessionGrantCount), CONNECT_FANOUT_CONCURRENCY, (grantMessage) => permissionsApi.createGrant({
                delegated: true,
                store: true,
                grantedTo: grantedDelegateDid,
                scope: {
                    interface: DwnInterfaceName.Records,
                    method: DwnMethodName.Write,
                    protocol: PermissionsProtocol.uri,
                    contextId: grantMessage.recordId,
                },
                dateExpires: connectSession.expiresAt,
                author: selectedDid,
            }).then((revGrant) => ({ grantMessage, revGrant }))));
            // Fan out every revocation grant to every owner DWN endpoint with a single
            // global concurrency cap so that (grants × endpoints) cannot blow up. This
            // is best-effort (sync delivers eventually) so individual failures are
            // tolerated by `mapConcurrentSettled`.
            const revSendTasks = revGrantResults.flatMap(({ grantMessage, revGrant }) => {
                sessionRevocations.push({
                    grantId: grantMessage.recordId,
                    revocationGrantId: revGrant.message.recordId,
                });
                const _a = revGrant.message, { encodedData: revEncoded } = _a, revRawMessage = __rest(_a, ["encodedData"]);
                const revData = Uint8Array.from(Convert.base64Url(revEncoded).toUint8Array());
                // Include the revocation grant in the delegate grants for distribution.
                delegateGrants.push(revGrant.message);
                return revGrantEndpoints.map((dwnUrl) => ({ revRawMessage, revData, dwnUrl }));
            });
            if (revSendTasks.length > 0) {
                yield timed(`${CONNECT_PERF_LOG_PREFIX} revocationGrants.fanout (sends=${revSendTasks.length}, endpoints=${revGrantEndpoints.length})`, () => mapConcurrentSettled(revSendTasks, CONNECT_FANOUT_CONCURRENCY, ({ revRawMessage, revData, dwnUrl }) => agent.rpc.sendDwnRequest({
                    dwnUrl,
                    targetDid: selectedDid,
                    message: revRawMessage,
                    data: new Blob([revData]),
                    signal: AbortSignal.timeout(CONNECT_REQUEST_TIMEOUT_MS),
                })));
            }
            const responseObject = yield timed(`${CONNECT_PERF_LOG_PREFIX} response.build`, () => EnboxConnectProtocol.createConnectResponse({
                providerDid: selectedDid,
                delegateDid: grantedDelegateDid,
                aud: connectRequest.clientDid,
                nonce: connectRequest.nonce,
                delegateGrants,
                delegatePortableDid,
                sessionRevocations: sessionRevocations.length > 0 ? sessionRevocations : undefined,
            }));
            const responseObjectJwt = yield timed(`${CONNECT_PERF_LOG_PREFIX} response.sign`, () => EnboxConnectProtocol.signJwt({
                did: responseBearerDid,
                data: responseObject,
            }));
            const clientDid = yield timed(`${CONNECT_PERF_LOG_PREFIX} clientDid.resolve`, () => DidJwk.resolve(connectRequest.clientDid));
            const sharedKey = yield timed(`${CONNECT_PERF_LOG_PREFIX} response.deriveSharedKey`, () => EnboxConnectProtocol.deriveSharedKey(responseBearerDid, clientDid === null || clientDid === void 0 ? void 0 : clientDid.didDocument));
            const encryptedResponse = yield timed(`${CONNECT_PERF_LOG_PREFIX} response.encrypt`, () => EnboxConnectProtocol.encryptResponse({
                jwt: responseObjectJwt,
                encryptionKey: sharedKey,
                delegatePublicKeyJwk: responseBearerDid.document.verificationMethod[0].publicKeyJwk,
                pin,
            }));
            const formEncodedRequest = new URLSearchParams({
                id_token: encryptedResponse,
                state: connectRequest.state,
            }).toString();
            yield timed(`${CONNECT_PERF_LOG_PREFIX} response.callbackPost`, () => __awaiter(this, void 0, void 0, function* () {
                const response = yield fetch(connectRequest.callbackUrl, {
                    body: formEncodedRequest,
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/x-www-form-urlencoded',
                    },
                    signal: AbortSignal.timeout(30000),
                });
                if (!response.ok) {
                    // NOTE: delegate grants have already been written/fanned out by this
                    // point, so callers may observe partial owner-side state when the
                    // callback server rejects the final response delivery.
                    throw new Error(`Connect: callback POST failed with HTTP ${response.status}.`);
                }
                return response;
            }));
        }
        catch (err) {
            outcome = 'fail';
            throw err;
        }
        finally {
            const totalElapsed = nowMs() - submitStart;
            logger.log(`${CONNECT_PERF_LOG_PREFIX} submitConnectResponse.total ${outcome} in ${totalElapsed.toFixed(1)}ms `
                + `(protocols=${numProtocols}, scopes=${numScopes}, sessionGrants=${sessionGrantCount})`);
        }
    });
}
// ---------------------------------------------------------------------------
// Namespace export
// ---------------------------------------------------------------------------
export const EnboxConnectProtocol = {
    buildConnectUrl,
    signJwt,
    verifyJwt,
    assertConnectRequest,
    assertConnectResponse,
    encryptRequest,
    decryptRequest,
    encryptResponse,
    decryptResponse,
    deriveSharedKey,
    createConnectRequest,
    getConnectRequest,
    createConnectResponse,
    createConnectSessionMetadata,
    createPermissionGrants,
    createGrantKeyRecordsForGrants: createGrantKeyRecordsForGrantsFn,
    submitConnectResponse,
};
//# sourceMappingURL=enbox-connect-protocol.js.map