@enbox/agent 0.6.8 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/browser.mjs +11 -11
  2. package/dist/browser.mjs.map +4 -4
  3. package/dist/esm/agent-session.js +16 -0
  4. package/dist/esm/agent-session.js.map +1 -0
  5. package/dist/esm/dwn-api.js +5 -4
  6. package/dist/esm/dwn-api.js.map +1 -1
  7. package/dist/esm/dwn-encryption.js +11 -4
  8. package/dist/esm/dwn-encryption.js.map +1 -1
  9. package/dist/esm/enbox-connect-protocol.js +455 -246
  10. package/dist/esm/enbox-connect-protocol.js.map +1 -1
  11. package/dist/esm/index.js +1 -0
  12. package/dist/esm/index.js.map +1 -1
  13. package/dist/esm/local-key-manager.js.map +1 -1
  14. package/dist/esm/protocol-utils.js +19 -6
  15. package/dist/esm/protocol-utils.js.map +1 -1
  16. package/dist/esm/store-data.js.map +1 -1
  17. package/dist/esm/store-key.js +1 -1
  18. package/dist/esm/store-key.js.map +1 -1
  19. package/dist/esm/sync-engine-level.js +18 -5
  20. package/dist/esm/sync-engine-level.js.map +1 -1
  21. package/dist/esm/types/dwn.js.map +1 -1
  22. package/dist/esm/utils.js +0 -12
  23. package/dist/esm/utils.js.map +1 -1
  24. package/dist/types/agent-session.d.ts +59 -0
  25. package/dist/types/agent-session.d.ts.map +1 -0
  26. package/dist/types/dwn-api.d.ts.map +1 -1
  27. package/dist/types/dwn-encryption.d.ts.map +1 -1
  28. package/dist/types/enbox-connect-protocol.d.ts +34 -3
  29. package/dist/types/enbox-connect-protocol.d.ts.map +1 -1
  30. package/dist/types/index.d.ts +1 -0
  31. package/dist/types/index.d.ts.map +1 -1
  32. package/dist/types/local-key-manager.d.ts.map +1 -1
  33. package/dist/types/protocol-utils.d.ts.map +1 -1
  34. package/dist/types/sync-engine-level.d.ts +6 -1
  35. package/dist/types/sync-engine-level.d.ts.map +1 -1
  36. package/dist/types/types/dwn.d.ts +10 -1
  37. package/dist/types/types/dwn.d.ts.map +1 -1
  38. package/dist/types/utils.d.ts +0 -2
  39. package/dist/types/utils.d.ts.map +1 -1
  40. package/package.json +6 -6
  41. package/src/agent-session.ts +78 -0
  42. package/src/dwn-api.ts +5 -4
  43. package/src/dwn-encryption.ts +14 -6
  44. package/src/enbox-connect-protocol.ts +556 -298
  45. package/src/index.ts +1 -0
  46. package/src/local-key-manager.ts +7 -3
  47. package/src/protocol-utils.ts +19 -8
  48. package/src/store-data.ts +1 -1
  49. package/src/store-key.ts +1 -1
  50. package/src/sync-engine-level.ts +19 -6
  51. package/src/types/dwn.ts +21 -12
  52. package/src/utils.ts +3 -18
@@ -33,7 +33,7 @@ var __rest = (this && this.__rest) || function (s, e) {
33
33
  return t;
34
34
  };
35
35
  import { DidJwk } from '@enbox/dids';
36
- import { Convert, logger } from '@enbox/common';
36
+ import { concatenateUrl, Convert, logger, nowMs, timed } from '@enbox/common';
37
37
  import { CryptoUtils, Ed25519, EdDsaAlgorithm, Hkdf, X25519, XChaCha20Poly1305, } from '@enbox/crypto';
38
38
  import { DwnInterfaceName, DwnMethodName, HdKey, KeyDerivationScheme, PermissionsProtocol } from '@enbox/dwn-sdk-js';
39
39
  import { AgentPermissionsApi } from './permissions-api.js';
@@ -42,7 +42,7 @@ import { getEncryptionKeyInfo } from './dwn-encryption.js';
42
42
  import { isMultiPartyContext } from './protocol-utils.js';
43
43
  import { isRecordPermissionScope } from './dwn-api.js';
44
44
  import { KeyDeliveryProtocolDefinition } from './store-data-protocols.js';
45
- import { concatenateUrl, mapConcurrent, mapConcurrentSettled } from './utils.js';
45
+ import { mapConcurrent, mapConcurrentSettled } from './utils.js';
46
46
  // ---------------------------------------------------------------------------
47
47
  // Tunables
48
48
  // ---------------------------------------------------------------------------
@@ -55,6 +55,22 @@ import { concatenateUrl, mapConcurrent, mapConcurrentSettled } from './utils.js'
55
55
  * per-host browser connection limits and server-side rate limits.
56
56
  */
57
57
  const CONNECT_FANOUT_CONCURRENCY = 8;
58
+ /**
59
+ * Per-request abort budget applied to every DWN-endpoint `sendDwnRequest`
60
+ * issued during the connect flow. The HttpDwnRpcClient's default per-attempt
61
+ * timeout is 30 s with 3 retries (~120 s worst-case per request) — that
62
+ * scales unacceptably when bounded fan-out has to wait for every settled
63
+ * task. With this budget, an unhealthy / cold endpoint short-circuits the
64
+ * retry loop within a few seconds (AbortError is non-retryable), keeping
65
+ * the user-visible "Authorizing…" wait bounded even when one of N DWN
66
+ * endpoints is misbehaving.
67
+ *
68
+ * Sync delivers any missed copies eventually, so aborting fast is safe:
69
+ * the connect-flow fan-outs are best-effort and tolerate per-task failure.
70
+ */
71
+ const CONNECT_REQUEST_TIMEOUT_MS = 10000;
72
+ /** Log namespace used for wallet-side connect critical-path timings. */
73
+ const CONNECT_PERF_LOG_PREFIX = '[connect.perf]';
58
74
  // ---------------------------------------------------------------------------
59
75
  // URL building
60
76
  // ---------------------------------------------------------------------------
@@ -89,7 +105,14 @@ function buildConnectUrl({ baseURL, endpoint, authParam, tokenParam, }) {
89
105
  // ---------------------------------------------------------------------------
90
106
  // JWT signing and verification
91
107
  // ---------------------------------------------------------------------------
92
- /** Signs an object as a JWT using an Ed25519 DID key. */
108
+ /**
109
+ * Signs an object as a JWT using an Ed25519 DID key.
110
+ *
111
+ * `data` is constrained to `object` so callers don't have to widen
112
+ * typed payload shapes (e.g. `EnboxConnectResponse`) to
113
+ * `Record<string, unknown>` at the call site. `Convert.object(data)`
114
+ * stringifies whatever JSON-serializable shape is passed.
115
+ */
93
116
  function signJwt(_a) {
94
117
  return __awaiter(this, arguments, void 0, function* ({ did, data, }) {
95
118
  const header = Convert.object({
@@ -106,7 +129,17 @@ function signJwt(_a) {
106
129
  return `${header}.${payload}.${signatureBase64Url}`;
107
130
  });
108
131
  }
109
- /** Verifies a JWT signature using the DID in the `kid` header. Returns the parsed payload. */
132
+ /**
133
+ * Verifies a JWT signature using the DID in the `kid` header. Returns the
134
+ * parsed payload as an untyped object.
135
+ *
136
+ * The return type is intentionally `Record<string, unknown>` rather than a
137
+ * caller-supplied generic — a JWT payload is bytes from a remote party, and
138
+ * we can't soundly assert its shape without runtime validation. Callers must
139
+ * apply one of the {@link assertConnectRequest} / {@link assertConnectResponse}
140
+ * assertion helpers (or their own type guard) to narrow the payload before
141
+ * accessing fields.
142
+ */
110
143
  function verifyJwt(_a) {
111
144
  return __awaiter(this, arguments, void 0, function* ({ jwt }) {
112
145
  var _b, _c;
@@ -134,9 +167,124 @@ function verifyJwt(_a) {
134
167
  if (!isValid) {
135
168
  throw new Error('Connect: JWT verification failed — invalid signature.');
136
169
  }
137
- return Convert.base64Url(payloadB64U).toObject();
170
+ const decoded = Convert.base64Url(payloadB64U).toObject();
171
+ if (typeof decoded !== 'object' || decoded === null || Array.isArray(decoded)) {
172
+ throw new Error('Connect: JWT verification failed — payload must be a JSON object.');
173
+ }
174
+ return decoded;
138
175
  });
139
176
  }
177
+ // ─── Field-level validation helpers ─────────────────────────────────────
178
+ //
179
+ // The connect-request / connect-response assertions below describe their
180
+ // expected shape declaratively in terms of these primitive checks. Each
181
+ // helper throws a consistent `Connect: <context> — \`<field>\` <reason>`
182
+ // message on mismatch, so per-field error formatting is centralized here
183
+ // rather than duplicated at every call site.
184
+ /**
185
+ * Throws a shape-validation error during connect JWT assertion.
186
+ *
187
+ * Deliberately throws plain `Error` rather than `TypeError` even though
188
+ * the failures are runtime type-shape mismatches. The reason is layered
189
+ * error handling: boundary-validation failures need to propagate through
190
+ * the same `try/catch` paths as every other connect-flow error — vault
191
+ * lock failure, JWT signature failure, DWN request failure, etc. —
192
+ * without `catch` blocks having to special-case a `TypeError` subclass.
193
+ *
194
+ * SonarCloud's `typescript:S7786` flags this as "too unspecific for a
195
+ * type check" and prefers `TypeError`. We suppress it at the file level
196
+ * (see `sonar-project.properties`) because every `require*` helper goes
197
+ * through this single throw site.
198
+ */
199
+ function fail(context, field, reason) {
200
+ throw new Error(`Connect: ${context} — \`${field}\` ${reason}.`);
201
+ }
202
+ function requireString(payload, field, context) {
203
+ if (typeof payload[field] !== 'string') {
204
+ fail(context, field, 'must be a string');
205
+ }
206
+ }
207
+ function requireNumber(payload, field, context) {
208
+ if (typeof payload[field] !== 'number') {
209
+ fail(context, field, 'must be a number');
210
+ }
211
+ }
212
+ function requireArray(payload, field, context) {
213
+ if (!Array.isArray(payload[field])) {
214
+ fail(context, field, 'must be an array');
215
+ }
216
+ }
217
+ function requireObject(payload, field, context) {
218
+ const value = payload[field];
219
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
220
+ fail(context, field, 'must be an object');
221
+ }
222
+ }
223
+ function requireLiteral(payload, field, expected, context) {
224
+ if (payload[field] !== expected) {
225
+ fail(context, field, `must be ${JSON.stringify(expected)}`);
226
+ }
227
+ }
228
+ function requireStringArray(payload, field, context) {
229
+ const value = payload[field];
230
+ if (!Array.isArray(value) || !value.every((item) => typeof item === 'string')) {
231
+ fail(context, field, 'must be a string[]');
232
+ }
233
+ }
234
+ function requireOptionalString(payload, field, context) {
235
+ if (payload[field] !== undefined && typeof payload[field] !== 'string') {
236
+ fail(context, field, 'must be a string when present');
237
+ }
238
+ }
239
+ function requireOptionalArray(payload, field, context) {
240
+ if (payload[field] !== undefined && !Array.isArray(payload[field])) {
241
+ fail(context, field, 'must be an array when present');
242
+ }
243
+ }
244
+ // ─── Boundary assertions ────────────────────────────────────────────────
245
+ //
246
+ // Each `assertConnect*` describes the shape of its target type as a list
247
+ // of per-field requirements. The structural existence/primitive checks
248
+ // are the only validation done at the JWT-payload boundary; deeper
249
+ // validation of nested arrays/objects (permission scopes, grants,
250
+ // portable DID structure) happens downstream in DWN-aware validators
251
+ // where the richer logic already lives.
252
+ /**
253
+ * Runtime assertion that a verified JWT payload has the shape of an
254
+ * {@link EnboxConnectRequest}. Use immediately after `verifyJwt()` to narrow
255
+ * a `Record<string, unknown>` payload before accessing fields.
256
+ */
257
+ function assertConnectRequest(payload) {
258
+ const ctx = 'invalid connect request';
259
+ requireString(payload, 'clientDid', ctx);
260
+ requireString(payload, 'appName', ctx);
261
+ requireArray(payload, 'permissionRequests', ctx);
262
+ requireString(payload, 'nonce', ctx);
263
+ requireString(payload, 'state', ctx);
264
+ requireString(payload, 'callbackUrl', ctx);
265
+ requireLiteral(payload, 'responseMode', 'direct_post', ctx);
266
+ requireStringArray(payload, 'supportedDidMethods', ctx);
267
+ }
268
+ /**
269
+ * Runtime assertion that a verified JWT payload has the shape of an
270
+ * {@link EnboxConnectResponse}. Use immediately after `verifyJwt()` to narrow
271
+ * a `Record<string, unknown>` payload before accessing fields.
272
+ */
273
+ function assertConnectResponse(payload) {
274
+ const ctx = 'invalid connect response';
275
+ requireString(payload, 'providerDid', ctx);
276
+ requireString(payload, 'delegateDid', ctx);
277
+ requireString(payload, 'aud', ctx);
278
+ requireNumber(payload, 'iat', ctx);
279
+ requireNumber(payload, 'exp', ctx);
280
+ requireOptionalString(payload, 'nonce', ctx);
281
+ requireArray(payload, 'delegateGrants', ctx);
282
+ requireObject(payload, 'delegatePortableDid', ctx);
283
+ requireOptionalArray(payload, 'delegateDecryptionKeys', ctx);
284
+ requireOptionalArray(payload, 'delegateContextKeys', ctx);
285
+ requireOptionalArray(payload, 'delegateMultiPartyProtocols', ctx);
286
+ requireOptionalArray(payload, 'sessionRevocations', ctx);
287
+ }
140
288
  // ---------------------------------------------------------------------------
141
289
  // Encryption: request (symmetric key via QR/deep link)
142
290
  // ---------------------------------------------------------------------------
@@ -309,7 +457,9 @@ function getConnectRequest(requestUri, encryptionKey) {
309
457
  const response = yield fetch(requestUri, { signal: AbortSignal.timeout(30000) });
310
458
  const jwe = yield response.text();
311
459
  const jwt = yield decryptRequest({ jwe, encryptionKey });
312
- return (yield verifyJwt({ jwt }));
460
+ const payload = yield verifyJwt({ jwt });
461
+ assertConnectRequest(payload);
462
+ return payload;
313
463
  });
314
464
  }
315
465
  // ---------------------------------------------------------------------------
@@ -382,6 +532,7 @@ function createPermissionGrants(selectedDid, delegateBearerDid, agent, scopes, d
382
532
  targetDid: selectedDid,
383
533
  message: rawMessage,
384
534
  data: new Blob([data]),
535
+ signal: AbortSignal.timeout(CONNECT_REQUEST_TIMEOUT_MS),
385
536
  });
386
537
  return { grantIndex, dwnUrl, reply };
387
538
  }));
@@ -416,21 +567,29 @@ function createPermissionGrants(selectedDid, delegateBearerDid, agent, scopes, d
416
567
  // Protocol installation
417
568
  // ---------------------------------------------------------------------------
418
569
  /**
419
- * Installs a DWN protocol on the provider's DWN if it doesn't already exist.
420
- * Ensures the protocol is available on both the local and remote DWN.
570
+ * Ensures the protocol is installed on the provider's local DWN so that the
571
+ * agent can sign and (when applicable) encrypt grants for it during
572
+ * `submitConnectResponse`.
573
+ *
574
+ * Remote installation (push to every owner DWN endpoint) is the
575
+ * responsibility of the calling client (the wallet's own `prepareProtocol`
576
+ * runs *before* `submitConnectResponse` and fans out to every endpoint in
577
+ * parallel). When the protocol already exists locally — the common case —
578
+ * this function performs a single local `ProtocolsQuery` and returns: there
579
+ * is no remote send, so a slow/unhealthy DWN endpoint cannot block the
580
+ * "Authorizing…" hot path.
421
581
  *
422
- * When the protocol definition contains types with `encryptionRequired: true`,
423
- * the protocol is installed with `encryption: true` so that the agent injects
424
- * `$encryption` keys (derived from the owner's X25519 root key) into the
425
- * protocol definition. This ensures the protocol is immediately usable for
426
- * encrypted record operations by both the owner and any delegates.
582
+ * When the protocol is *not* installed locally a safety fallback for
583
+ * callers that did not pre-install the protocol is configured locally
584
+ * (with `encryption: true` when any type declares `encryptionRequired: true`,
585
+ * so the agent injects `$encryption` keys derived from the owner's X25519
586
+ * root key) and then fanned out to every owner DWN endpoint with bounded
587
+ * concurrency and a short per-request budget. Endpoint failures are
588
+ * non-fatal — sync delivers any missing copies eventually.
427
589
  */
428
590
  function prepareProtocol(selectedDid, agent, protocolDefinition) {
429
591
  return __awaiter(this, void 0, void 0, function* () {
430
592
  var _a;
431
- // Detect whether any type in the protocol requires encryption.
432
- const needsEncryption = Object.values((_a = protocolDefinition.types) !== null && _a !== void 0 ? _a : {})
433
- .some((type) => (type === null || type === void 0 ? void 0 : type.encryptionRequired) === true);
434
593
  const queryMessage = yield agent.processDwnRequest({
435
594
  author: selectedDid,
436
595
  messageType: DwnInterface.ProtocolsQuery,
@@ -440,38 +599,54 @@ function prepareProtocol(selectedDid, agent, protocolDefinition) {
440
599
  if (queryMessage.reply.status.code !== 200) {
441
600
  throw new Error(`Could not fetch protocol: ${queryMessage.reply.status.detail}`);
442
601
  }
443
- else if (queryMessage.reply.entries === undefined || queryMessage.reply.entries.length === 0) {
444
- logger.log(`Protocol does not exist, creating: ${protocolDefinition.protocol}`);
445
- const { reply: sendReply, message: configureMessage } = yield agent.sendDwnRequest({
446
- author: selectedDid,
447
- target: selectedDid,
448
- messageType: DwnInterface.ProtocolsConfigure,
449
- messageParams: { definition: protocolDefinition },
450
- encryption: needsEncryption || undefined,
451
- });
452
- if (sendReply.status.code !== 202 && sendReply.status.code !== 409) {
453
- throw new Error(`Could not send protocol: ${sendReply.status.detail}`);
454
- }
455
- yield agent.processDwnRequest({
456
- author: selectedDid,
457
- target: selectedDid,
458
- messageType: DwnInterface.ProtocolsConfigure,
459
- rawMessage: configureMessage
460
- });
602
+ const isInstalledLocally = queryMessage.reply.entries !== undefined
603
+ && queryMessage.reply.entries.length > 0;
604
+ if (isInstalledLocally) {
605
+ // Already installed locally. The wallet's pre-call `prepareProtocol`
606
+ // is responsible for fanning the protocol out to every owner DWN
607
+ // endpoint; sync delivers any missing copies eventually. Skipping the
608
+ // remote send here turns this hot path into a single local DB read
609
+ // (~10 ms) instead of a sequential per-endpoint network round-trip
610
+ // with retries — the latter could take minutes if any endpoint was
611
+ // slow or unreachable.
612
+ logger.log(`Protocol already installed locally: ${protocolDefinition.protocol}`);
613
+ return;
461
614
  }
462
- else {
463
- logger.log(`Protocol already exists: ${protocolDefinition.protocol}`);
464
- const configureMessage = queryMessage.reply.entries[0];
465
- const { reply: sendReply } = yield agent.sendDwnRequest({
466
- author: selectedDid,
467
- target: selectedDid,
468
- messageType: DwnInterface.ProtocolsConfigure,
469
- rawMessage: configureMessage,
470
- });
471
- if (sendReply.status.code !== 202 && sendReply.status.code !== 409) {
472
- throw new Error(`Could not send protocol: ${sendReply.status.detail}`);
473
- }
615
+ // Safety fallback — protocol is missing locally, so the caller did not
616
+ // pre-install. Configure it locally (with encryption derivation if any
617
+ // type requires it) so the agent can sign/encrypt grants, then push to
618
+ // every owner DWN endpoint in parallel with a short per-request budget.
619
+ logger.log(`Protocol not installed, configuring locally: ${protocolDefinition.protocol}`);
620
+ const needsEncryption = Object.values((_a = protocolDefinition.types) !== null && _a !== void 0 ? _a : {})
621
+ .some((type) => (type === null || type === void 0 ? void 0 : type.encryptionRequired) === true);
622
+ const { reply: configureReply, message: configureMessage } = yield agent.processDwnRequest({
623
+ author: selectedDid,
624
+ target: selectedDid,
625
+ messageType: DwnInterface.ProtocolsConfigure,
626
+ messageParams: { definition: protocolDefinition },
627
+ encryption: needsEncryption || undefined,
628
+ });
629
+ if (configureReply.status.code !== 202 && configureReply.status.code !== 409) {
630
+ throw new Error(`Could not configure protocol locally: ${configureReply.status.detail}`);
631
+ }
632
+ let dwnEndpointUrls = [];
633
+ try {
634
+ dwnEndpointUrls = yield agent.dwn.getDwnEndpointUrlsForTarget(selectedDid);
474
635
  }
636
+ catch (_b) {
637
+ // Endpoint resolution failure — protocol stays local-only until sync.
638
+ }
639
+ if (dwnEndpointUrls.length === 0) {
640
+ return;
641
+ }
642
+ // Best-effort remote fan-out with bounded concurrency and a per-request
643
+ // abort signal. Failures are tolerated (sync delivers eventually).
644
+ yield mapConcurrentSettled(dwnEndpointUrls, CONNECT_FANOUT_CONCURRENCY, (dwnUrl) => agent.rpc.sendDwnRequest({
645
+ dwnUrl,
646
+ targetDid: selectedDid,
647
+ message: configureMessage,
648
+ signal: AbortSignal.timeout(CONNECT_REQUEST_TIMEOUT_MS),
649
+ }));
475
650
  });
476
651
  }
477
652
  /**
@@ -499,14 +674,16 @@ function deriveScopedDecryptionKeys(agent, ownerDid, protocolUri, scopes, protoc
499
674
  const readMethods = new Set([
500
675
  DwnMethodName.Read, DwnMethodName.Query, DwnMethodName.Subscribe,
501
676
  ]);
502
- // Collect read-like scopes only.
677
+ // Collect read-like scopes only. `isRecordPermissionScope` narrows to
678
+ // `DwnRecordsPermissionScope`, which declares `protocolPath?: string`
679
+ // and `contextId?: string` — no `as any` needed for those reads below.
503
680
  const readScopes = scopes.filter((s) => isRecordPermissionScope(s) && readMethods.has(s.method));
504
681
  if (readScopes.length === 0) {
505
682
  return []; // write/delete only → no decryption keys
506
683
  }
507
684
  // Fail closed: reject contextId-scoped encrypted reads.
508
685
  for (const scope of readScopes) {
509
- if ('contextId' in scope && scope.contextId) {
686
+ if (scope.contextId) {
510
687
  throw new Error(`Encrypted delegate access scoped by contextId is not supported ` +
511
688
  `yet; use protocol-wide permissions for protocol '${protocolUri}'.`);
512
689
  }
@@ -521,7 +698,7 @@ function deriveScopedDecryptionKeys(agent, ownerDid, protocolUri, scopes, protoc
521
698
  `for multi-party protocols.`);
522
699
  }
523
700
  // Check if any scope is protocol-wide (no protocolPath).
524
- const hasProtocolWideRead = readScopes.some((s) => !('protocolPath' in s) || !s.protocolPath);
701
+ const hasProtocolWideRead = readScopes.some((s) => !s.protocolPath);
525
702
  const { keyId, keyUri } = yield getEncryptionKeyInfo(agent, ownerDid);
526
703
  // If any unrestricted read scope exists, emit one protocol-wide key
527
704
  // and skip narrower keys (the protocol-wide key subsumes them).
@@ -546,9 +723,8 @@ function deriveScopedDecryptionKeys(agent, ownerDid, protocolUri, scopes, protoc
546
723
  // Emit one exact-path key per unique protocolPath.
547
724
  const uniquePaths = new Set();
548
725
  for (const scope of readScopes) {
549
- const pp = scope.protocolPath;
550
- if (pp) {
551
- uniquePaths.add(pp);
726
+ if (scope.protocolPath) {
727
+ uniquePaths.add(scope.protocolPath);
552
728
  }
553
729
  }
554
730
  const keys = [];
@@ -627,17 +803,20 @@ function classifyProtocolRoots(definition) {
627
803
  */
628
804
  function deriveContextKeysForDelegate(agent, ownerDid, protocolDefinition, scopes) {
629
805
  return __awaiter(this, void 0, void 0, function* () {
630
- var _a, _b, _c;
806
+ var _a, _b, _c, _d;
631
807
  const readMethods = new Set([
632
808
  DwnMethodName.Read, DwnMethodName.Query, DwnMethodName.Subscribe,
633
809
  ]);
810
+ // `isRecordPermissionScope` narrows to `DwnRecordsPermissionScope`,
811
+ // which declares `protocolPath?: string` and `contextId?: string` —
812
+ // no `as any` needed for the field reads below.
634
813
  const readScopes = scopes.filter((s) => isRecordPermissionScope(s) && readMethods.has(s.method));
635
814
  if (readScopes.length === 0) {
636
815
  return []; // write-only → no context keys
637
816
  }
638
817
  // Fail closed: reject contextId-scoped reads.
639
818
  for (const scope of readScopes) {
640
- if ('contextId' in scope && scope.contextId) {
819
+ if (scope.contextId) {
641
820
  throw new Error(`Encrypted delegate access scoped by contextId is not supported ` +
642
821
  `yet; use protocol-wide permissions for protocol ` +
643
822
  `'${protocolDefinition.protocol}'.`);
@@ -645,7 +824,7 @@ function deriveContextKeysForDelegate(agent, ownerDid, protocolDefinition, scope
645
824
  }
646
825
  // Fail closed: reject protocolPath-scoped reads on multi-party protocols.
647
826
  for (const scope of readScopes) {
648
- if ('protocolPath' in scope && scope.protocolPath) {
827
+ if (scope.protocolPath) {
649
828
  throw new Error(`Encrypted delegate access scoped by protocolPath on multi-party ` +
650
829
  `protocols is not supported yet; use protocol-wide permissions for ` +
651
830
  `protocol '${protocolDefinition.protocol}'.`);
@@ -671,8 +850,7 @@ function deriveContextKeysForDelegate(agent, ownerDid, protocolDefinition, scope
671
850
  },
672
851
  });
673
852
  for (const entry of (_b = reply.entries) !== null && _b !== void 0 ? _b : []) {
674
- const rootContextId = ((_c = entry.contextId) === null || _c === void 0 ? void 0 : _c.split('/')[0])
675
- || entry.recordId;
853
+ const rootContextId = (_d = (_c = entry.contextId) === null || _c === void 0 ? void 0 : _c.split('/')[0]) !== null && _d !== void 0 ? _d : entry.recordId;
676
854
  if (!rootContextId || seenContextIds.has(rootContextId)) {
677
855
  continue;
678
856
  }
@@ -717,200 +895,229 @@ function deriveContextKeysForDelegate(agent, ownerDid, protocolDefinition, scope
717
895
  */
718
896
  function submitConnectResponse(selectedDid, connectRequest, pin, agent) {
719
897
  return __awaiter(this, void 0, void 0, function* () {
720
- const delegateBearerDid = yield DidJwk.create();
721
- const delegatePortableDid = yield delegateBearerDid.export();
722
- // Add X25519 key derived from the delegate's Ed25519 key.
723
- // did:jwk only supports one verification method, but DWN encryption
724
- // requires X25519 for key agreement. Including the derived X25519
725
- // private key in the PortableDid ensures the delegate agent's KMS
726
- // has both keys after import. The Ed25519→X25519 conversion is a
727
- // standard cryptographic operation (RFC 8032 / libsodium).
728
- const delegateEdPrivateKey = delegatePortableDid.privateKeys[0];
729
- const delegateX25519PrivateKey = yield Ed25519.convertPrivateKeyToX25519({
730
- privateKey: delegateEdPrivateKey,
731
- });
732
- delegatePortableDid.privateKeys.push(delegateX25519PrivateKey);
733
- // Derive the delegate's key-delivery ProtocolPath leaf public key.
734
- // This is the pre-derived key that the owner will use later when writing
735
- // contextKey records addressed to this delegate. The owner cannot derive
736
- // this from the delegate's root public key alone (HKDF needs the private
737
- // key), so we compute it now while we have temporary access to the
738
- // delegate's private key material.
739
- const delegateX25519PrivateKeyBytes = yield X25519.privateKeyToBytes({
740
- privateKey: delegateX25519PrivateKey,
741
- });
742
- const keyDeliveryDerivationPath = [
743
- KeyDerivationScheme.ProtocolPath,
744
- KeyDeliveryProtocolDefinition.protocol,
745
- 'contextKey',
746
- ];
747
- const delegateLeafPrivateKeyBytes = yield HdKey.derivePrivateKeyBytes(delegateX25519PrivateKeyBytes, keyDeliveryDerivationPath);
748
- const delegateLeafPrivateKeyJwk = yield X25519.bytesToPrivateKey({
749
- privateKeyBytes: delegateLeafPrivateKeyBytes,
750
- });
751
- const delegateKeyDeliveryLeafPublicKey = yield X25519.getPublicKey({
752
- key: delegateLeafPrivateKeyJwk,
753
- });
754
- // The rootKeyId is the delegate's keyAgreement VM id (e.g. `did:jwk:...#0`).
755
- // For did:jwk this is the Ed25519 VM, but getEncryptionKeyInfo() also returns
756
- // this same id after Ed25519→X25519 conversion. The DWN SDK matches the JWE
757
- // `kid` header against the KeyDecrypter's `rootKeyId`, so both sides must use
758
- // the same id which they do because both derive from verificationMethod.id
759
- // of the keyAgreement relationship.
760
- const delegateKeyAgreementVmId = delegateBearerDid.document.verificationMethod[0].id;
761
- const delegateKeyDeliveryData = {
762
- rootKeyId: delegateKeyAgreementVmId,
763
- publicKeyJwk: delegateKeyDeliveryLeafPublicKey,
764
- };
765
- // Derive scope-aware decryption keys for encrypted protocols.
766
- // Single-party: ProtocolPath keys (protocol-wide or exact-path).
767
- // Multi-party: ProtocolContext keys (per rootContextId).
768
- // Write-only delegates receive no decryption capability.
769
- const delegateDecryptionKeys = [];
770
- const delegateContextKeys = [];
771
- const delegateMultiPartyProtocols = [];
772
- const delegateGrantPromises = connectRequest.permissionRequests.map((permissionRequest) => __awaiter(this, void 0, void 0, function* () {
773
- var _a;
774
- const { protocolDefinition, permissionScopes } = permissionRequest;
775
- const grantsMatchProtocolUri = permissionScopes.every(scope => 'protocol' in scope && scope.protocol === protocolDefinition.protocol);
776
- if (!grantsMatchProtocolUri) {
777
- throw new Error('All permission scopes must match the protocol URI they are provided with.');
778
- }
779
- yield prepareProtocol(selectedDid, agent, protocolDefinition);
780
- const hasEncryptedTypes = Object.values((_a = protocolDefinition.types) !== null && _a !== void 0 ? _a : {})
781
- .some((type) => (type === null || type === void 0 ? void 0 : type.encryptionRequired) === true);
782
- if (hasEncryptedTypes) {
783
- const { multiParty, singleParty } = classifyProtocolRoots(protocolDefinition);
784
- if (multiParty.length > 0 && singleParty.length > 0) {
785
- // Mixed protocol: some roots are multi-party, others single-party.
786
- // We cannot safely model this with either key type alone.
787
- throw new Error(`Encrypted delegate access for protocols with mixed single-party ` +
788
- `and multi-party roots is not supported yet. ` +
789
- `Protocol '${protocolDefinition.protocol}' has multi-party roots ` +
790
- `[${multiParty.join(', ')}] and single-party roots ` +
791
- `[${singleParty.join(', ')}].`);
792
- }
793
- if (multiParty.length > 0) {
794
- // Pure multi-party: derive per-context keys for existing contexts.
795
- // Unsupported scope shapes (protocolPath, contextId) throw.
796
- const ctxKeys = yield deriveContextKeysForDelegate(agent, selectedDid, protocolDefinition, permissionScopes);
797
- delegateContextKeys.push(...ctxKeys);
798
- // Only register the protocol for post-connect delivery if the
799
- // delegate has at least one read-like scope. Write-only delegates
800
- // must NOT receive context keys — they have no decryption need.
801
- const readMethods = new Set([
802
- DwnMethodName.Read, DwnMethodName.Query, DwnMethodName.Subscribe,
803
- ]);
804
- const hasReadLikeScope = permissionScopes.some((s) => isRecordPermissionScope(s) && readMethods.has(s.method));
805
- if (hasReadLikeScope) {
806
- delegateMultiPartyProtocols.push(protocolDefinition.protocol);
898
+ const submitStart = nowMs();
899
+ const numProtocols = connectRequest.permissionRequests.length;
900
+ const numScopes = connectRequest.permissionRequests.reduce((sum, req) => sum + req.permissionScopes.length, 0);
901
+ // Tracked across the try/finally so the aggregate `submitConnectResponse.total`
902
+ // log emits on both success and failure paths — operators bisecting wall-time
903
+ // from wallet debug logs need the total even when a phase throws.
904
+ let sessionGrantCount = 0;
905
+ let outcome = 'ok';
906
+ logger.log(`${CONNECT_PERF_LOG_PREFIX} submitConnectResponse.start `
907
+ + `(protocols=${numProtocols}, scopes=${numScopes})`);
908
+ try {
909
+ const delegateBearerDid = yield timed(`${CONNECT_PERF_LOG_PREFIX} delegateDid.create`, () => DidJwk.create());
910
+ const delegatePortableDid = yield delegateBearerDid.export();
911
+ // Add X25519 key derived from the delegate's Ed25519 key.
912
+ // did:jwk only supports one verification method, but DWN encryption
913
+ // requires X25519 for key agreement. Including the derived X25519
914
+ // private key in the PortableDid ensures the delegate agent's KMS
915
+ // has both keys after import. The Ed25519→X25519 conversion is a
916
+ // standard cryptographic operation (RFC 8032 / libsodium).
917
+ const delegateEdPrivateKey = delegatePortableDid.privateKeys[0];
918
+ const delegateX25519PrivateKey = yield Ed25519.convertPrivateKeyToX25519({
919
+ privateKey: delegateEdPrivateKey,
920
+ });
921
+ delegatePortableDid.privateKeys.push(delegateX25519PrivateKey);
922
+ // Derive the delegate's key-delivery ProtocolPath leaf public key.
923
+ // This is the pre-derived key that the owner will use later when writing
924
+ // contextKey records addressed to this delegate. The owner cannot derive
925
+ // this from the delegate's root public key alone (HKDF needs the private
926
+ // key), so we compute it now while we have temporary access to the
927
+ // delegate's private key material.
928
+ const delegateX25519PrivateKeyBytes = yield X25519.privateKeyToBytes({
929
+ privateKey: delegateX25519PrivateKey,
930
+ });
931
+ const keyDeliveryDerivationPath = [
932
+ KeyDerivationScheme.ProtocolPath,
933
+ KeyDeliveryProtocolDefinition.protocol,
934
+ 'contextKey',
935
+ ];
936
+ const delegateLeafPrivateKeyBytes = yield HdKey.derivePrivateKeyBytes(delegateX25519PrivateKeyBytes, keyDeliveryDerivationPath);
937
+ const delegateLeafPrivateKeyJwk = yield X25519.bytesToPrivateKey({
938
+ privateKeyBytes: delegateLeafPrivateKeyBytes,
939
+ });
940
+ const delegateKeyDeliveryLeafPublicKey = yield X25519.getPublicKey({
941
+ key: delegateLeafPrivateKeyJwk,
942
+ });
943
+ // The rootKeyId is the delegate's keyAgreement VM id (e.g. `did:jwk:...#0`).
944
+ // For did:jwk this is the Ed25519 VM, but getEncryptionKeyInfo() also returns
945
+ // this same id after Ed25519→X25519 conversion. The DWN SDK matches the JWE
946
+ // `kid` header against the KeyDecrypter's `rootKeyId`, so both sides must use
947
+ // the same id — which they do because both derive from verificationMethod.id
948
+ // of the keyAgreement relationship.
949
+ const delegateKeyAgreementVmId = delegateBearerDid.document.verificationMethod[0].id;
950
+ const delegateKeyDeliveryData = {
951
+ rootKeyId: delegateKeyAgreementVmId,
952
+ publicKeyJwk: delegateKeyDeliveryLeafPublicKey,
953
+ };
954
+ // Derive scope-aware decryption keys for encrypted protocols.
955
+ // Single-party: ProtocolPath keys (protocol-wide or exact-path).
956
+ // Multi-party: ProtocolContext keys (per rootContextId).
957
+ // Write-only delegates receive no decryption capability.
958
+ const delegateDecryptionKeys = [];
959
+ const delegateContextKeys = [];
960
+ const delegateMultiPartyProtocols = [];
961
+ const delegateGrants = yield timed(`${CONNECT_PERF_LOG_PREFIX} permissionGrants.fanout (protocols=${numProtocols})`, () => __awaiter(this, void 0, void 0, function* () {
962
+ const delegateGrantPromises = connectRequest.permissionRequests.map((permissionRequest) => __awaiter(this, void 0, void 0, function* () {
963
+ var _a;
964
+ const { protocolDefinition, permissionScopes } = permissionRequest;
965
+ const grantsMatchProtocolUri = permissionScopes.every(scope => 'protocol' in scope && scope.protocol === protocolDefinition.protocol);
966
+ if (!grantsMatchProtocolUri) {
967
+ throw new Error('All permission scopes must match the protocol URI they are provided with.');
807
968
  }
808
- }
809
- else {
810
- // Pure single-party: derive ProtocolPath keys.
811
- // Unsupported scope shapes (contextId) throw.
812
- const keys = yield deriveScopedDecryptionKeys(agent, selectedDid, protocolDefinition.protocol, permissionScopes, protocolDefinition);
813
- delegateDecryptionKeys.push(...keys);
814
- }
969
+ yield prepareProtocol(selectedDid, agent, protocolDefinition);
970
+ const hasEncryptedTypes = Object.values((_a = protocolDefinition.types) !== null && _a !== void 0 ? _a : {})
971
+ .some((type) => (type === null || type === void 0 ? void 0 : type.encryptionRequired) === true);
972
+ if (hasEncryptedTypes) {
973
+ const { multiParty, singleParty } = classifyProtocolRoots(protocolDefinition);
974
+ if (multiParty.length > 0 && singleParty.length > 0) {
975
+ // Mixed protocol: some roots are multi-party, others single-party.
976
+ // We cannot safely model this with either key type alone.
977
+ throw new Error(`Encrypted delegate access for protocols with mixed single-party ` +
978
+ `and multi-party roots is not supported yet. ` +
979
+ `Protocol '${protocolDefinition.protocol}' has multi-party roots ` +
980
+ `[${multiParty.join(', ')}] and single-party roots ` +
981
+ `[${singleParty.join(', ')}].`);
982
+ }
983
+ if (multiParty.length > 0) {
984
+ // Pure multi-party: derive per-context keys for existing contexts.
985
+ // Unsupported scope shapes (protocolPath, contextId) throw.
986
+ const ctxKeys = yield deriveContextKeysForDelegate(agent, selectedDid, protocolDefinition, permissionScopes);
987
+ delegateContextKeys.push(...ctxKeys);
988
+ // Only register the protocol for post-connect delivery if the
989
+ // delegate has at least one read-like scope. Write-only delegates
990
+ // must NOT receive context keys — they have no decryption need.
991
+ const readMethods = new Set([
992
+ DwnMethodName.Read, DwnMethodName.Query, DwnMethodName.Subscribe,
993
+ ]);
994
+ const hasReadLikeScope = permissionScopes.some((s) => isRecordPermissionScope(s) && readMethods.has(s.method));
995
+ if (hasReadLikeScope) {
996
+ delegateMultiPartyProtocols.push(protocolDefinition.protocol);
997
+ }
998
+ }
999
+ else {
1000
+ // Pure single-party: derive ProtocolPath keys.
1001
+ // Unsupported scope shapes (contextId) throw.
1002
+ const keys = yield deriveScopedDecryptionKeys(agent, selectedDid, protocolDefinition.protocol, permissionScopes, protocolDefinition);
1003
+ delegateDecryptionKeys.push(...keys);
1004
+ }
1005
+ }
1006
+ return EnboxConnectProtocol.createPermissionGrants(selectedDid, delegateBearerDid, agent, permissionScopes, delegateKeyDeliveryData);
1007
+ }));
1008
+ return (yield Promise.all(delegateGrantPromises)).flat();
1009
+ }));
1010
+ // Create per-grant contextId-scoped revocation grants.
1011
+ // Each revocation grant authorizes the delegate to write a revocation
1012
+ // ONLY for the specific session grant it corresponds to.
1013
+ const permissionsApi = new AgentPermissionsApi({ agent });
1014
+ const sessionRevocations = [];
1015
+ let revGrantEndpoints = [];
1016
+ try {
1017
+ revGrantEndpoints = yield agent.dwn.getDwnEndpointUrlsForTarget(selectedDid);
815
1018
  }
816
- return EnboxConnectProtocol.createPermissionGrants(selectedDid, delegateBearerDid, agent, permissionScopes, delegateKeyDeliveryData);
817
- }));
818
- const delegateGrants = (yield Promise.all(delegateGrantPromises)).flat();
819
- // Create per-grant contextId-scoped revocation grants.
820
- // Each revocation grant authorizes the delegate to write a revocation
821
- // ONLY for the specific session grant it corresponds to.
822
- const permissionsApi = new AgentPermissionsApi({ agent });
823
- const sessionRevocations = [];
824
- let revGrantEndpoints = [];
825
- try {
826
- revGrantEndpoints = yield agent.dwn.getDwnEndpointUrlsForTarget(selectedDid);
827
- }
828
- catch (_a) {
829
- // Endpoint resolution failure — revocation grants will be local-only until sync.
830
- }
831
- // Snapshot the current length — revocation grants are appended to delegateGrants
832
- // below, but we must NOT iterate over them (they are meta-grants, not session grants).
833
- const sessionGrantCount = delegateGrants.length;
834
- // Phase 1: create all revocation grants locally with bounded concurrency.
835
- // createGrant is local-only (storage + signing) so it's cheap, but we still
836
- // cap parallelism to avoid head-of-line blocking when sessionGrantCount is
837
- // large (e.g. dapp requesting many scopes at once).
838
- const revGrantResults = yield mapConcurrent(delegateGrants.slice(0, sessionGrantCount), CONNECT_FANOUT_CONCURRENCY, (grantMessage) => permissionsApi.createGrant({
839
- delegated: true,
840
- store: true,
841
- grantedTo: delegateBearerDid.uri,
842
- scope: {
843
- interface: DwnInterfaceName.Records,
844
- method: DwnMethodName.Write,
845
- protocol: PermissionsProtocol.uri,
846
- contextId: grantMessage.recordId,
847
- },
848
- dateExpires: '2040-06-25T16:09:16.693356Z',
849
- author: selectedDid,
850
- }).then((revGrant) => ({ grantMessage, revGrant })));
851
- // Phase 2: fan out every revocation grant to every owner DWN endpoint with
852
- // a single global concurrency cap so that (grants × endpoints) cannot blow
853
- // up. This is best-effort (sync delivers eventually) so individual failures
854
- // are tolerated by `mapConcurrentSettled`.
855
- const revSendTasks = revGrantResults.flatMap(({ grantMessage, revGrant }) => {
856
- sessionRevocations.push({
857
- grantId: grantMessage.recordId,
858
- revocationGrantId: revGrant.message.recordId,
1019
+ catch (_a) {
1020
+ // Endpoint resolution failure — revocation grants will be local-only until sync.
1021
+ }
1022
+ // Snapshot the current length — revocation grants are appended to delegateGrants
1023
+ // below, but we must NOT iterate over them (they are meta-grants, not session grants).
1024
+ sessionGrantCount = delegateGrants.length;
1025
+ // Phase 1: create all revocation grants locally with bounded concurrency.
1026
+ // createGrant is local-only (storage + signing) so it's cheap, but we still
1027
+ // cap parallelism to avoid head-of-line blocking when sessionGrantCount is
1028
+ // large (e.g. dapp requesting many scopes at once).
1029
+ const revGrantResults = yield timed(`${CONNECT_PERF_LOG_PREFIX} revocationGrants.create (n=${sessionGrantCount})`, () => mapConcurrent(delegateGrants.slice(0, sessionGrantCount), CONNECT_FANOUT_CONCURRENCY, (grantMessage) => permissionsApi.createGrant({
1030
+ delegated: true,
1031
+ store: true,
1032
+ grantedTo: delegateBearerDid.uri,
1033
+ scope: {
1034
+ interface: DwnInterfaceName.Records,
1035
+ method: DwnMethodName.Write,
1036
+ protocol: PermissionsProtocol.uri,
1037
+ contextId: grantMessage.recordId,
1038
+ },
1039
+ dateExpires: '2040-06-25T16:09:16.693356Z',
1040
+ author: selectedDid,
1041
+ }).then((revGrant) => ({ grantMessage, revGrant }))));
1042
+ // Phase 2: fan out every revocation grant to every owner DWN endpoint with
1043
+ // a single global concurrency cap so that (grants × endpoints) cannot blow
1044
+ // up. This is best-effort (sync delivers eventually) so individual failures
1045
+ // are tolerated by `mapConcurrentSettled`.
1046
+ const revSendTasks = revGrantResults.flatMap(({ grantMessage, revGrant }) => {
1047
+ sessionRevocations.push({
1048
+ grantId: grantMessage.recordId,
1049
+ revocationGrantId: revGrant.message.recordId,
1050
+ });
1051
+ const _a = revGrant.message, { encodedData: revEncoded } = _a, revRawMessage = __rest(_a, ["encodedData"]);
1052
+ const revData = Uint8Array.from(Convert.base64Url(revEncoded).toUint8Array());
1053
+ // Include the revocation grant in the delegate grants for distribution.
1054
+ delegateGrants.push(revGrant.message);
1055
+ return revGrantEndpoints.map((dwnUrl) => ({ revRawMessage, revData, dwnUrl }));
859
1056
  });
860
- const _a = revGrant.message, { encodedData: revEncoded } = _a, revRawMessage = __rest(_a, ["encodedData"]);
861
- const revData = Convert.base64Url(revEncoded).toUint8Array();
862
- // Include the revocation grant in the delegate grants for distribution.
863
- delegateGrants.push(revGrant.message);
864
- return revGrantEndpoints.map((dwnUrl) => ({ revRawMessage, revData, dwnUrl }));
865
- });
866
- if (revSendTasks.length > 0) {
867
- yield mapConcurrentSettled(revSendTasks, CONNECT_FANOUT_CONCURRENCY, ({ revRawMessage, revData, dwnUrl }) => agent.rpc.sendDwnRequest({
868
- dwnUrl,
869
- targetDid: selectedDid,
870
- message: revRawMessage,
871
- data: new Blob([revData]),
1057
+ if (revSendTasks.length > 0) {
1058
+ 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({
1059
+ dwnUrl,
1060
+ targetDid: selectedDid,
1061
+ message: revRawMessage,
1062
+ data: new Blob([revData]),
1063
+ signal: AbortSignal.timeout(CONNECT_REQUEST_TIMEOUT_MS),
1064
+ })));
1065
+ }
1066
+ const responseObject = yield timed(`${CONNECT_PERF_LOG_PREFIX} response.build`, () => EnboxConnectProtocol.createConnectResponse({
1067
+ providerDid: selectedDid,
1068
+ delegateDid: delegateBearerDid.uri,
1069
+ aud: connectRequest.clientDid,
1070
+ nonce: connectRequest.nonce,
1071
+ delegateGrants,
1072
+ delegatePortableDid,
1073
+ delegateDecryptionKeys: delegateDecryptionKeys.length > 0 ? delegateDecryptionKeys : undefined,
1074
+ delegateContextKeys: delegateContextKeys.length > 0 ? delegateContextKeys : undefined,
1075
+ delegateMultiPartyProtocols: delegateMultiPartyProtocols.length > 0 ? delegateMultiPartyProtocols : undefined,
1076
+ sessionRevocations: sessionRevocations.length > 0 ? sessionRevocations : undefined,
1077
+ }));
1078
+ const responseObjectJwt = yield timed(`${CONNECT_PERF_LOG_PREFIX} response.sign`, () => EnboxConnectProtocol.signJwt({
1079
+ did: delegateBearerDid,
1080
+ data: responseObject,
1081
+ }));
1082
+ const clientDid = yield timed(`${CONNECT_PERF_LOG_PREFIX} clientDid.resolve`, () => DidJwk.resolve(connectRequest.clientDid));
1083
+ const sharedKey = yield timed(`${CONNECT_PERF_LOG_PREFIX} response.deriveSharedKey`, () => EnboxConnectProtocol.deriveSharedKey(delegateBearerDid, clientDid === null || clientDid === void 0 ? void 0 : clientDid.didDocument));
1084
+ const encryptedResponse = yield timed(`${CONNECT_PERF_LOG_PREFIX} response.encrypt`, () => EnboxConnectProtocol.encryptResponse({
1085
+ jwt: responseObjectJwt,
1086
+ encryptionKey: sharedKey,
1087
+ delegatePublicKeyJwk: delegateBearerDid.document.verificationMethod[0].publicKeyJwk,
1088
+ pin,
1089
+ }));
1090
+ const formEncodedRequest = new URLSearchParams({
1091
+ id_token: encryptedResponse,
1092
+ state: connectRequest.state,
1093
+ }).toString();
1094
+ yield timed(`${CONNECT_PERF_LOG_PREFIX} response.callbackPost`, () => __awaiter(this, void 0, void 0, function* () {
1095
+ const response = yield fetch(connectRequest.callbackUrl, {
1096
+ body: formEncodedRequest,
1097
+ method: 'POST',
1098
+ headers: {
1099
+ 'Content-Type': 'application/x-www-form-urlencoded',
1100
+ },
1101
+ signal: AbortSignal.timeout(30000),
1102
+ });
1103
+ if (!response.ok) {
1104
+ // NOTE: delegate grants have already been written/fanned out by this
1105
+ // point, so callers may observe partial owner-side state when the
1106
+ // callback server rejects the final response delivery.
1107
+ throw new Error(`Connect: callback POST failed with HTTP ${response.status}.`);
1108
+ }
1109
+ return response;
872
1110
  }));
873
1111
  }
874
- logger.log('Building connect response...');
875
- const responseObject = yield EnboxConnectProtocol.createConnectResponse({
876
- providerDid: selectedDid,
877
- delegateDid: delegateBearerDid.uri,
878
- aud: connectRequest.clientDid,
879
- nonce: connectRequest.nonce,
880
- delegateGrants,
881
- delegatePortableDid,
882
- delegateDecryptionKeys: delegateDecryptionKeys.length > 0 ? delegateDecryptionKeys : undefined,
883
- delegateContextKeys: delegateContextKeys.length > 0 ? delegateContextKeys : undefined,
884
- delegateMultiPartyProtocols: delegateMultiPartyProtocols.length > 0 ? delegateMultiPartyProtocols : undefined,
885
- sessionRevocations: sessionRevocations.length > 0 ? sessionRevocations : undefined,
886
- });
887
- logger.log('Signing connect response...');
888
- const responseObjectJwt = yield EnboxConnectProtocol.signJwt({
889
- did: delegateBearerDid,
890
- data: responseObject,
891
- });
892
- const clientDid = yield DidJwk.resolve(connectRequest.clientDid);
893
- const sharedKey = yield EnboxConnectProtocol.deriveSharedKey(delegateBearerDid, clientDid === null || clientDid === void 0 ? void 0 : clientDid.didDocument);
894
- logger.log('Encrypting connect response...');
895
- const encryptedResponse = yield EnboxConnectProtocol.encryptResponse({
896
- jwt: responseObjectJwt,
897
- encryptionKey: sharedKey,
898
- delegatePublicKeyJwk: delegateBearerDid.document.verificationMethod[0].publicKeyJwk,
899
- pin,
900
- });
901
- const formEncodedRequest = new URLSearchParams({
902
- id_token: encryptedResponse,
903
- state: connectRequest.state,
904
- }).toString();
905
- logger.log(`Sending connect response to: ${connectRequest.callbackUrl}`);
906
- yield fetch(connectRequest.callbackUrl, {
907
- body: formEncodedRequest,
908
- method: 'POST',
909
- headers: {
910
- 'Content-Type': 'application/x-www-form-urlencoded',
911
- },
912
- signal: AbortSignal.timeout(30000),
913
- });
1112
+ catch (err) {
1113
+ outcome = 'fail';
1114
+ throw err;
1115
+ }
1116
+ finally {
1117
+ const totalElapsed = nowMs() - submitStart;
1118
+ logger.log(`${CONNECT_PERF_LOG_PREFIX} submitConnectResponse.total ${outcome} in ${totalElapsed.toFixed(1)}ms `
1119
+ + `(protocols=${numProtocols}, scopes=${numScopes}, sessionGrants=${sessionGrantCount})`);
1120
+ }
914
1121
  });
915
1122
  }
916
1123
  // ---------------------------------------------------------------------------
@@ -920,6 +1127,8 @@ export const EnboxConnectProtocol = {
920
1127
  buildConnectUrl,
921
1128
  signJwt,
922
1129
  verifyJwt,
1130
+ assertConnectRequest,
1131
+ assertConnectResponse,
923
1132
  encryptRequest,
924
1133
  decryptRequest,
925
1134
  encryptResponse,