/**
 * 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 { concatenateUrl, Convert, logger, nowMs, timed } from '@enbox/common';
import { CryptoUtils, Ed25519, EdDsaAlgorithm, Hkdf, X25519, XChaCha20Poly1305, } from '@enbox/crypto';
import { DwnInterfaceName, DwnMethodName, HdKey, KeyDerivationScheme, PermissionsProtocol } from '@enbox/dwn-sdk-js';
import { AgentPermissionsApi } from './permissions-api.js';
import { DwnInterface } from './types/dwn.js';
import { getEncryptionKeyInfo } from './dwn-encryption.js';
import { isMultiPartyContext } from './protocol-utils.js';
import { isRecordPermissionScope } from './dwn-api.js';
import { KeyDeliveryProtocolDefinition } from './store-data-protocols.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]';
// ---------------------------------------------------------------------------
// 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 requireArray(payload, field, context) {
    if (!Array.isArray(payload[field])) {
        fail(context, field, 'must be an array');
    }
}
function requireObject(payload, field, context) {
    const value = payload[field];
    if (typeof value !== 'object' || value === null || Array.isArray(value)) {
        fail(context, field, 'must be an object');
    }
}
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 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);
    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);
    requireObject(payload, 'delegatePortableDid', ctx);
    requireOptionalArray(payload, 'delegateDecryptionKeys', ctx);
    requireOptionalArray(payload, 'delegateContextKeys', ctx);
    requireOptionalArray(payload, 'delegateMultiPartyProtocols', 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 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, delegateKeyDeliveryData) {
    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);
            // Attach delegate key-delivery tags to read-like grants so the
            // owner can encrypt future contextKey records to the delegate.
            const readMethods = new Set([
                DwnMethodName.Read, DwnMethodName.Query, DwnMethodName.Subscribe,
            ]);
            const isReadLike = isRecordPermissionScope(scope)
                && readMethods.has(scope.method);
            const delegateKeyDelivery = (isReadLike && delegateKeyDeliveryData) ? delegateKeyDeliveryData : undefined;
            return permissionsApi.createGrant({
                delegated,
                store: true,
                grantedTo: delegateBearerDid.uri,
                scope,
                dateExpires: '2040-06-25T16:09:16.693356Z', // TODO: make dateExpires configurable
                author: selectedDid,
                delegateKeyDelivery,
            });
        }));
        // 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);
    });
}
// ---------------------------------------------------------------------------
// 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 `$encryption` 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),
        }));
    });
}
/**
 * Derives the minimal set of decryption keys implied by read-like permission
 * scopes for a single-party encrypted protocol.
 *
 * Rules:
 *   - Only Records.Read / Records.Query / Records.Subscribe scopes contribute.
 *   - Write / Delete / Count scopes produce no decryption keys.
 *   - If any unrestricted (no `protocolPath`) read scope exists, one
 *     protocol-wide key is emitted and narrower keys are dropped.
 *   - Otherwise one exact-path key is emitted per unique `protocolPath`.
 *   - Scopes with `contextId` cause a fail-closed error.
 *   - Multi-party protocols cause a fail-closed error.
 *
 * @param agent - The platform agent (must hold the owner's KMS keys)
 * @param ownerDid - The DID of the protocol owner
 * @param protocolUri - The protocol URI
 * @param scopes - The permission scopes for this protocol
 * @param protocolDefinition - The protocol definition (for multi-party detection)
 * @returns An array of `DelegateDecryptionKey` (may be empty)
 */
function deriveScopedDecryptionKeys(agent, ownerDid, protocolUri, scopes, protocolDefinition) {
    return __awaiter(this, void 0, void 0, function* () {
        const readMethods = new Set([
            DwnMethodName.Read, DwnMethodName.Query, DwnMethodName.Subscribe,
        ]);
        // Collect read-like scopes only. `isRecordPermissionScope` narrows to
        // `DwnRecordsPermissionScope`, which declares `protocolPath?: string`
        // and `contextId?: string` — no `as any` needed for those reads below.
        const readScopes = scopes.filter((s) => isRecordPermissionScope(s) && readMethods.has(s.method));
        if (readScopes.length === 0) {
            return []; // write/delete only → no decryption keys
        }
        // Fail closed: reject contextId-scoped encrypted reads.
        for (const scope of readScopes) {
            if (scope.contextId) {
                throw new Error(`Encrypted delegate access scoped by contextId is not supported ` +
                    `yet; use protocol-wide permissions for protocol '${protocolUri}'.`);
            }
        }
        // Defense-in-depth: reject if any root is multi-party.
        // The caller should have already routed multi-party protocols to
        // deriveContextKeysForDelegate instead.
        const { multiParty } = classifyProtocolRoots(protocolDefinition);
        if (multiParty.length > 0) {
            throw new Error(`deriveScopedDecryptionKeys called for protocol with multi-party ` +
                `roots [${multiParty.join(', ')}]. Use deriveContextKeysForDelegate ` +
                `for multi-party protocols.`);
        }
        // Check if any scope is protocol-wide (no protocolPath).
        const hasProtocolWideRead = readScopes.some((s) => !s.protocolPath);
        const { keyId, keyUri } = yield getEncryptionKeyInfo(agent, ownerDid);
        // If any unrestricted read scope exists, emit one protocol-wide key
        // and skip narrower keys (the protocol-wide key subsumes them).
        if (hasProtocolWideRead) {
            const derivationPath = [KeyDerivationScheme.ProtocolPath, protocolUri];
            const derivedBytes = yield agent.keyManager.derivePrivateKeyBytes({
                keyUri, derivationPath,
            });
            const derivedJwk = yield X25519.bytesToPrivateKey({ privateKeyBytes: derivedBytes });
            return [{
                    protocol: protocolUri,
                    scope: { kind: 'protocol' },
                    derivedPrivateKey: {
                        rootKeyId: keyId,
                        derivationScheme: KeyDerivationScheme.ProtocolPath,
                        derivationPath,
                        derivedPrivateKey: derivedJwk,
                    },
                }];
        }
        // All read scopes are protocolPath-scoped.
        // Emit one exact-path key per unique protocolPath.
        const uniquePaths = new Set();
        for (const scope of readScopes) {
            if (scope.protocolPath) {
                uniquePaths.add(scope.protocolPath);
            }
        }
        const keys = [];
        for (const protocolPath of uniquePaths) {
            const pathSegments = protocolPath.split('/');
            const derivationPath = [KeyDerivationScheme.ProtocolPath, protocolUri, ...pathSegments];
            const derivedBytes = yield agent.keyManager.derivePrivateKeyBytes({
                keyUri, derivationPath,
            });
            const derivedJwk = yield X25519.bytesToPrivateKey({ privateKeyBytes: derivedBytes });
            keys.push({
                protocol: protocolUri,
                scope: { kind: 'protocolPath', protocolPath, match: 'exact' },
                derivedPrivateKey: {
                    rootKeyId: keyId,
                    derivationScheme: KeyDerivationScheme.ProtocolPath,
                    derivationPath,
                    derivedPrivateKey: derivedJwk,
                },
            });
        }
        return keys;
    });
}
/**
 * Detects whether a protocol definition has any root-level type whose subtree
 * triggers multi-party semantics. Delegates to the canonical
 * `isMultiPartyContext()` from `protocol-utils.ts` which checks:
 *
 *   - `$role: true` descendants in the subtree
 *   - Relational `who`/`of` `$actions` rules that grant `read` access
 *
 * These patterns cause the DWN agent to use ProtocolContext encryption at
 * write time, which is not supported in delegate sessions yet.
 */
/**
 * Classifies root-level types in a protocol definition into multi-party
 * and single-party buckets. Used to detect mixed protocols that cannot
 * be safely modeled with a single key type.
 */
function classifyProtocolRoots(definition) {
    const structure = definition.structure;
    if (!structure) {
        return { multiParty: [], singleParty: [] };
    }
    const multiParty = [];
    const singleParty = [];
    for (const rootTypeName of Object.keys(structure)) {
        if (rootTypeName.startsWith('$')) {
            continue;
        }
        if (isMultiPartyContext(definition, rootTypeName)) {
            multiParty.push(rootTypeName);
        }
        else {
            singleParty.push(rootTypeName);
        }
    }
    return { multiParty, singleParty };
}
/**
 * Derives per-context decryption keys for a delegate's access to a multi-party
 * encrypted protocol. Queries the owner's DWN for all root-level records
 * (thread roots, etc.) and derives a `[ProtocolContext, rootContextId]` key
 * for each.
 *
 * Validates scopes first — only protocol-wide read-like scopes are accepted.
 * `protocolPath`-scoped and `contextId`-scoped reads throw (not yet supported).
 * Write-only scopes return empty (no decryption keys needed).
 *
 * @param agent - The platform agent (must hold the owner's KMS keys)
 * @param ownerDid - The DID of the protocol owner
 * @param protocolDefinition - The protocol definition
 * @param scopes - The permission scopes for this protocol
 * @returns An array of `DelegateContextKey` (may be empty)
 */
function deriveContextKeysForDelegate(agent, ownerDid, protocolDefinition, scopes) {
    return __awaiter(this, void 0, void 0, function* () {
        var _a, _b, _c, _d;
        const readMethods = new Set([
            DwnMethodName.Read, DwnMethodName.Query, DwnMethodName.Subscribe,
        ]);
        // `isRecordPermissionScope` narrows to `DwnRecordsPermissionScope`,
        // which declares `protocolPath?: string` and `contextId?: string` —
        // no `as any` needed for the field reads below.
        const readScopes = scopes.filter((s) => isRecordPermissionScope(s) && readMethods.has(s.method));
        if (readScopes.length === 0) {
            return []; // write-only → no context keys
        }
        // Fail closed: reject contextId-scoped reads.
        for (const scope of readScopes) {
            if (scope.contextId) {
                throw new Error(`Encrypted delegate access scoped by contextId is not supported ` +
                    `yet; use protocol-wide permissions for protocol ` +
                    `'${protocolDefinition.protocol}'.`);
            }
        }
        // Fail closed: reject protocolPath-scoped reads on multi-party protocols.
        for (const scope of readScopes) {
            if (scope.protocolPath) {
                throw new Error(`Encrypted delegate access scoped by protocolPath on multi-party ` +
                    `protocols is not supported yet; use protocol-wide permissions for ` +
                    `protocol '${protocolDefinition.protocol}'.`);
            }
        }
        // All read scopes are protocol-wide. Derive context keys for each
        // existing root-level context in the protocol.
        const { keyId, keyUri } = yield getEncryptionKeyInfo(agent, ownerDid);
        const protocolUri = protocolDefinition.protocol;
        // Find root-level types (non-$ keys in structure).
        const rootTypes = Object.keys((_a = protocolDefinition.structure) !== null && _a !== void 0 ? _a : {})
            .filter((k) => !k.startsWith('$'));
        const contextKeys = [];
        const seenContextIds = new Set();
        for (const rootType of rootTypes) {
            // Query all root records for this type.
            const { reply } = yield agent.processDwnRequest({
                author: ownerDid,
                target: ownerDid,
                messageType: DwnInterface.RecordsQuery,
                messageParams: {
                    filter: { protocol: protocolUri, protocolPath: rootType },
                },
            });
            for (const entry of (_b = reply.entries) !== null && _b !== void 0 ? _b : []) {
                const rootContextId = (_d = (_c = entry.contextId) === null || _c === void 0 ? void 0 : _c.split('/')[0]) !== null && _d !== void 0 ? _d : entry.recordId;
                if (!rootContextId || seenContextIds.has(rootContextId)) {
                    continue;
                }
                seenContextIds.add(rootContextId);
                const derivationPath = [KeyDerivationScheme.ProtocolContext, rootContextId];
                const derivedBytes = yield agent.keyManager.derivePrivateKeyBytes({
                    keyUri, derivationPath,
                });
                const derivedJwk = yield X25519.bytesToPrivateKey({
                    privateKeyBytes: derivedBytes,
                });
                contextKeys.push({
                    protocol: protocolUri,
                    contextId: rootContextId,
                    derivedPrivateKey: {
                        rootKeyId: keyId,
                        derivationScheme: KeyDerivationScheme.ProtocolContext,
                        derivationPath,
                        derivedPrivateKey: derivedJwk,
                    },
                });
            }
        }
        return contextKeys;
    });
}
// ---------------------------------------------------------------------------
// 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 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 delegateBearerDid = yield timed(`${CONNECT_PERF_LOG_PREFIX} delegateDid.create`, () => DidJwk.create());
            const 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];
            const delegateX25519PrivateKey = yield Ed25519.convertPrivateKeyToX25519({
                privateKey: delegateEdPrivateKey,
            });
            delegatePortableDid.privateKeys.push(delegateX25519PrivateKey);
            // Derive the delegate's key-delivery ProtocolPath leaf public key.
            // This is the pre-derived key that the owner will use later when writing
            // contextKey records addressed to this delegate. The owner cannot derive
            // this from the delegate's root public key alone (HKDF needs the private
            // key), so we compute it now while we have temporary access to the
            // delegate's private key material.
            const delegateX25519PrivateKeyBytes = yield X25519.privateKeyToBytes({
                privateKey: delegateX25519PrivateKey,
            });
            const keyDeliveryDerivationPath = [
                KeyDerivationScheme.ProtocolPath,
                KeyDeliveryProtocolDefinition.protocol,
                'contextKey',
            ];
            const delegateLeafPrivateKeyBytes = yield HdKey.derivePrivateKeyBytes(delegateX25519PrivateKeyBytes, keyDeliveryDerivationPath);
            const delegateLeafPrivateKeyJwk = yield X25519.bytesToPrivateKey({
                privateKeyBytes: delegateLeafPrivateKeyBytes,
            });
            const delegateKeyDeliveryLeafPublicKey = yield X25519.getPublicKey({
                key: delegateLeafPrivateKeyJwk,
            });
            // The rootKeyId is the delegate's keyAgreement VM id (e.g. `did:jwk:...#0`).
            // For did:jwk this is the Ed25519 VM, but getEncryptionKeyInfo() also returns
            // this same id after Ed25519→X25519 conversion. The DWN SDK matches the JWE
            // `kid` header against the KeyDecrypter's `rootKeyId`, so both sides must use
            // the same id — which they do because both derive from verificationMethod.id
            // of the keyAgreement relationship.
            const delegateKeyAgreementVmId = delegateBearerDid.document.verificationMethod[0].id;
            const delegateKeyDeliveryData = {
                rootKeyId: delegateKeyAgreementVmId,
                publicKeyJwk: delegateKeyDeliveryLeafPublicKey,
            };
            // Derive scope-aware decryption keys for encrypted protocols.
            // Single-party: ProtocolPath keys (protocol-wide or exact-path).
            // Multi-party: ProtocolContext keys (per rootContextId).
            // Write-only delegates receive no decryption capability.
            const delegateDecryptionKeys = [];
            const delegateContextKeys = [];
            const delegateMultiPartyProtocols = [];
            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* () {
                    var _a;
                    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 = Object.values((_a = protocolDefinition.types) !== null && _a !== void 0 ? _a : {})
                        .some((type) => (type === null || type === void 0 ? void 0 : type.encryptionRequired) === true);
                    if (hasEncryptedTypes) {
                        const { multiParty, singleParty } = classifyProtocolRoots(protocolDefinition);
                        if (multiParty.length > 0 && singleParty.length > 0) {
                            // Mixed protocol: some roots are multi-party, others single-party.
                            // We cannot safely model this with either key type alone.
                            throw new Error(`Encrypted delegate access for protocols with mixed single-party ` +
                                `and multi-party roots is not supported yet. ` +
                                `Protocol '${protocolDefinition.protocol}' has multi-party roots ` +
                                `[${multiParty.join(', ')}] and single-party roots ` +
                                `[${singleParty.join(', ')}].`);
                        }
                        if (multiParty.length > 0) {
                            // Pure multi-party: derive per-context keys for existing contexts.
                            // Unsupported scope shapes (protocolPath, contextId) throw.
                            const ctxKeys = yield deriveContextKeysForDelegate(agent, selectedDid, protocolDefinition, permissionScopes);
                            delegateContextKeys.push(...ctxKeys);
                            // Only register the protocol for post-connect delivery if the
                            // delegate has at least one read-like scope. Write-only delegates
                            // must NOT receive context keys — they have no decryption need.
                            const readMethods = new Set([
                                DwnMethodName.Read, DwnMethodName.Query, DwnMethodName.Subscribe,
                            ]);
                            const hasReadLikeScope = permissionScopes.some((s) => isRecordPermissionScope(s) && readMethods.has(s.method));
                            if (hasReadLikeScope) {
                                delegateMultiPartyProtocols.push(protocolDefinition.protocol);
                            }
                        }
                        else {
                            // Pure single-party: derive ProtocolPath keys.
                            // Unsupported scope shapes (contextId) throw.
                            const keys = yield deriveScopedDecryptionKeys(agent, selectedDid, protocolDefinition.protocol, permissionScopes, protocolDefinition);
                            delegateDecryptionKeys.push(...keys);
                        }
                    }
                    return EnboxConnectProtocol.createPermissionGrants(selectedDid, delegateBearerDid, agent, permissionScopes, delegateKeyDeliveryData);
                }));
                return (yield Promise.all(delegateGrantPromises)).flat();
            }));
            // 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).
            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: delegateBearerDid.uri,
                scope: {
                    interface: DwnInterfaceName.Records,
                    method: DwnMethodName.Write,
                    protocol: PermissionsProtocol.uri,
                    contextId: grantMessage.recordId,
                },
                dateExpires: '2040-06-25T16:09:16.693356Z',
                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: delegateBearerDid.uri,
                aud: connectRequest.clientDid,
                nonce: connectRequest.nonce,
                delegateGrants,
                delegatePortableDid,
                delegateDecryptionKeys: delegateDecryptionKeys.length > 0 ? delegateDecryptionKeys : undefined,
                delegateContextKeys: delegateContextKeys.length > 0 ? delegateContextKeys : undefined,
                delegateMultiPartyProtocols: delegateMultiPartyProtocols.length > 0 ? delegateMultiPartyProtocols : undefined,
                sessionRevocations: sessionRevocations.length > 0 ? sessionRevocations : undefined,
            }));
            const responseObjectJwt = yield timed(`${CONNECT_PERF_LOG_PREFIX} response.sign`, () => EnboxConnectProtocol.signJwt({
                did: delegateBearerDid,
                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(delegateBearerDid, 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: delegateBearerDid.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,
    createPermissionGrants,
    submitConnectResponse,
    deriveScopedDecryptionKeys,
    deriveContextKeysForDelegate,
    classifyProtocolRoots,
};
//# sourceMappingURL=enbox-connect-protocol.js.map