@enbox/agent 0.7.0 → 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.
- package/dist/browser.mjs +11 -11
- package/dist/browser.mjs.map +4 -4
- package/dist/esm/agent-session.js +16 -0
- package/dist/esm/agent-session.js.map +1 -0
- package/dist/esm/dwn-api.js +5 -4
- package/dist/esm/dwn-api.js.map +1 -1
- package/dist/esm/dwn-encryption.js +11 -4
- package/dist/esm/dwn-encryption.js.map +1 -1
- package/dist/esm/enbox-connect-protocol.js +376 -207
- package/dist/esm/enbox-connect-protocol.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/local-key-manager.js.map +1 -1
- package/dist/esm/protocol-utils.js +19 -6
- package/dist/esm/protocol-utils.js.map +1 -1
- package/dist/esm/store-data.js.map +1 -1
- package/dist/esm/store-key.js +1 -1
- package/dist/esm/store-key.js.map +1 -1
- package/dist/esm/sync-engine-level.js +18 -5
- package/dist/esm/sync-engine-level.js.map +1 -1
- package/dist/esm/types/dwn.js.map +1 -1
- package/dist/esm/utils.js +0 -12
- package/dist/esm/utils.js.map +1 -1
- package/dist/types/agent-session.d.ts +59 -0
- package/dist/types/agent-session.d.ts.map +1 -0
- package/dist/types/dwn-api.d.ts.map +1 -1
- package/dist/types/dwn-encryption.d.ts.map +1 -1
- package/dist/types/enbox-connect-protocol.d.ts +34 -3
- package/dist/types/enbox-connect-protocol.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/local-key-manager.d.ts.map +1 -1
- package/dist/types/protocol-utils.d.ts.map +1 -1
- package/dist/types/sync-engine-level.d.ts +6 -1
- package/dist/types/sync-engine-level.d.ts.map +1 -1
- package/dist/types/types/dwn.d.ts +10 -1
- package/dist/types/types/dwn.d.ts.map +1 -1
- package/dist/types/utils.d.ts +0 -2
- package/dist/types/utils.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/agent-session.ts +78 -0
- package/src/dwn-api.ts +5 -4
- package/src/dwn-encryption.ts +14 -6
- package/src/enbox-connect-protocol.ts +466 -256
- package/src/index.ts +1 -0
- package/src/local-key-manager.ts +7 -3
- package/src/protocol-utils.ts +19 -8
- package/src/store-data.ts +1 -1
- package/src/store-key.ts +1 -1
- package/src/sync-engine-level.ts +19 -6
- package/src/types/dwn.ts +21 -12
- package/src/utils.ts +3 -18
|
@@ -18,7 +18,7 @@ import type { EnboxPlatformAgent } from './types/agent.js';
|
|
|
18
18
|
import type { PrivateKeyJwk } from '@enbox/crypto';
|
|
19
19
|
import type { RequireOnly } from '@enbox/common';
|
|
20
20
|
import type { DidDocument, PortableDid } from '@enbox/dids';
|
|
21
|
-
import type { DwnDataEncodedRecordsWriteMessage, DwnPermissionScope, DwnProtocolDefinition } from './types/dwn.js';
|
|
21
|
+
import type { DwnDataEncodedRecordsWriteMessage, DwnPermissionScope, DwnProtocolDefinition, DwnRecordsPermissionScope } from './types/dwn.js';
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* The protocols of permissions requested, along with the definition and permission scopes for each protocol.
|
|
@@ -103,7 +103,7 @@ import type {
|
|
|
103
103
|
Jwk } from '@enbox/crypto';
|
|
104
104
|
|
|
105
105
|
import { type BearerDid, DidJwk } from '@enbox/dids';
|
|
106
|
-
import { Convert, logger } from '@enbox/common';
|
|
106
|
+
import { concatenateUrl, Convert, logger, nowMs, timed } from '@enbox/common';
|
|
107
107
|
import {
|
|
108
108
|
CryptoUtils,
|
|
109
109
|
Ed25519,
|
|
@@ -120,7 +120,7 @@ import { getEncryptionKeyInfo } from './dwn-encryption.js';
|
|
|
120
120
|
import { isMultiPartyContext } from './protocol-utils.js';
|
|
121
121
|
import { isRecordPermissionScope } from './dwn-api.js';
|
|
122
122
|
import { KeyDeliveryProtocolDefinition } from './store-data-protocols.js';
|
|
123
|
-
import {
|
|
123
|
+
import { mapConcurrent, mapConcurrentSettled } from './utils.js';
|
|
124
124
|
|
|
125
125
|
// ---------------------------------------------------------------------------
|
|
126
126
|
// Tunables
|
|
@@ -151,6 +151,9 @@ const CONNECT_FANOUT_CONCURRENCY = 8;
|
|
|
151
151
|
*/
|
|
152
152
|
const CONNECT_REQUEST_TIMEOUT_MS = 10_000;
|
|
153
153
|
|
|
154
|
+
/** Log namespace used for wallet-side connect critical-path timings. */
|
|
155
|
+
const CONNECT_PERF_LOG_PREFIX = '[connect.perf]';
|
|
156
|
+
|
|
154
157
|
// ---------------------------------------------------------------------------
|
|
155
158
|
// Types
|
|
156
159
|
// ---------------------------------------------------------------------------
|
|
@@ -333,13 +336,20 @@ function buildConnectUrl({
|
|
|
333
336
|
// JWT signing and verification
|
|
334
337
|
// ---------------------------------------------------------------------------
|
|
335
338
|
|
|
336
|
-
/**
|
|
339
|
+
/**
|
|
340
|
+
* Signs an object as a JWT using an Ed25519 DID key.
|
|
341
|
+
*
|
|
342
|
+
* `data` is constrained to `object` so callers don't have to widen
|
|
343
|
+
* typed payload shapes (e.g. `EnboxConnectResponse`) to
|
|
344
|
+
* `Record<string, unknown>` at the call site. `Convert.object(data)`
|
|
345
|
+
* stringifies whatever JSON-serializable shape is passed.
|
|
346
|
+
*/
|
|
337
347
|
async function signJwt({
|
|
338
348
|
did,
|
|
339
349
|
data,
|
|
340
350
|
}: {
|
|
341
351
|
did: BearerDid;
|
|
342
|
-
data:
|
|
352
|
+
data: object;
|
|
343
353
|
}): Promise<string> {
|
|
344
354
|
const header = Convert.object({
|
|
345
355
|
alg : 'EdDSA',
|
|
@@ -358,7 +368,17 @@ async function signJwt({
|
|
|
358
368
|
return `${header}.${payload}.${signatureBase64Url}`;
|
|
359
369
|
}
|
|
360
370
|
|
|
361
|
-
/**
|
|
371
|
+
/**
|
|
372
|
+
* Verifies a JWT signature using the DID in the `kid` header. Returns the
|
|
373
|
+
* parsed payload as an untyped object.
|
|
374
|
+
*
|
|
375
|
+
* The return type is intentionally `Record<string, unknown>` rather than a
|
|
376
|
+
* caller-supplied generic — a JWT payload is bytes from a remote party, and
|
|
377
|
+
* we can't soundly assert its shape without runtime validation. Callers must
|
|
378
|
+
* apply one of the {@link assertConnectRequest} / {@link assertConnectResponse}
|
|
379
|
+
* assertion helpers (or their own type guard) to narrow the payload before
|
|
380
|
+
* accessing fields.
|
|
381
|
+
*/
|
|
362
382
|
async function verifyJwt({ jwt }: { jwt: string }): Promise<Record<string, unknown>> {
|
|
363
383
|
const [headerB64U, payloadB64U, signatureB64U] = jwt.split('.');
|
|
364
384
|
|
|
@@ -394,7 +414,129 @@ async function verifyJwt({ jwt }: { jwt: string }): Promise<Record<string, unkno
|
|
|
394
414
|
throw new Error('Connect: JWT verification failed — invalid signature.');
|
|
395
415
|
}
|
|
396
416
|
|
|
397
|
-
|
|
417
|
+
const decoded: unknown = Convert.base64Url(payloadB64U).toObject();
|
|
418
|
+
if (typeof decoded !== 'object' || decoded === null || Array.isArray(decoded)) {
|
|
419
|
+
throw new Error('Connect: JWT verification failed — payload must be a JSON object.');
|
|
420
|
+
}
|
|
421
|
+
return decoded as Record<string, unknown>;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ─── Field-level validation helpers ─────────────────────────────────────
|
|
425
|
+
//
|
|
426
|
+
// The connect-request / connect-response assertions below describe their
|
|
427
|
+
// expected shape declaratively in terms of these primitive checks. Each
|
|
428
|
+
// helper throws a consistent `Connect: <context> — \`<field>\` <reason>`
|
|
429
|
+
// message on mismatch, so per-field error formatting is centralized here
|
|
430
|
+
// rather than duplicated at every call site.
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Throws a shape-validation error during connect JWT assertion.
|
|
434
|
+
*
|
|
435
|
+
* Deliberately throws plain `Error` rather than `TypeError` even though
|
|
436
|
+
* the failures are runtime type-shape mismatches. The reason is layered
|
|
437
|
+
* error handling: boundary-validation failures need to propagate through
|
|
438
|
+
* the same `try/catch` paths as every other connect-flow error — vault
|
|
439
|
+
* lock failure, JWT signature failure, DWN request failure, etc. —
|
|
440
|
+
* without `catch` blocks having to special-case a `TypeError` subclass.
|
|
441
|
+
*
|
|
442
|
+
* SonarCloud's `typescript:S7786` flags this as "too unspecific for a
|
|
443
|
+
* type check" and prefers `TypeError`. We suppress it at the file level
|
|
444
|
+
* (see `sonar-project.properties`) because every `require*` helper goes
|
|
445
|
+
* through this single throw site.
|
|
446
|
+
*/
|
|
447
|
+
function fail(context: string, field: string, reason: string): never {
|
|
448
|
+
throw new Error(`Connect: ${context} — \`${field}\` ${reason}.`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function requireString(payload: Record<string, unknown>, field: string, context: string): void {
|
|
452
|
+
if (typeof payload[field] !== 'string') { fail(context, field, 'must be a string'); }
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function requireNumber(payload: Record<string, unknown>, field: string, context: string): void {
|
|
456
|
+
if (typeof payload[field] !== 'number') { fail(context, field, 'must be a number'); }
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function requireArray(payload: Record<string, unknown>, field: string, context: string): void {
|
|
460
|
+
if (!Array.isArray(payload[field])) { fail(context, field, 'must be an array'); }
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function requireObject(payload: Record<string, unknown>, field: string, context: string): void {
|
|
464
|
+
const value = payload[field];
|
|
465
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
466
|
+
fail(context, field, 'must be an object');
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function requireLiteral<L extends string | number | boolean>(
|
|
471
|
+
payload: Record<string, unknown>, field: string, expected: L, context: string,
|
|
472
|
+
): void {
|
|
473
|
+
if (payload[field] !== expected) { fail(context, field, `must be ${JSON.stringify(expected)}`); }
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function requireStringArray(payload: Record<string, unknown>, field: string, context: string): void {
|
|
477
|
+
const value = payload[field];
|
|
478
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === 'string')) {
|
|
479
|
+
fail(context, field, 'must be a string[]');
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function requireOptionalString(payload: Record<string, unknown>, field: string, context: string): void {
|
|
484
|
+
if (payload[field] !== undefined && typeof payload[field] !== 'string') {
|
|
485
|
+
fail(context, field, 'must be a string when present');
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function requireOptionalArray(payload: Record<string, unknown>, field: string, context: string): void {
|
|
490
|
+
if (payload[field] !== undefined && !Array.isArray(payload[field])) {
|
|
491
|
+
fail(context, field, 'must be an array when present');
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ─── Boundary assertions ────────────────────────────────────────────────
|
|
496
|
+
//
|
|
497
|
+
// Each `assertConnect*` describes the shape of its target type as a list
|
|
498
|
+
// of per-field requirements. The structural existence/primitive checks
|
|
499
|
+
// are the only validation done at the JWT-payload boundary; deeper
|
|
500
|
+
// validation of nested arrays/objects (permission scopes, grants,
|
|
501
|
+
// portable DID structure) happens downstream in DWN-aware validators
|
|
502
|
+
// where the richer logic already lives.
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Runtime assertion that a verified JWT payload has the shape of an
|
|
506
|
+
* {@link EnboxConnectRequest}. Use immediately after `verifyJwt()` to narrow
|
|
507
|
+
* a `Record<string, unknown>` payload before accessing fields.
|
|
508
|
+
*/
|
|
509
|
+
function assertConnectRequest(payload: Record<string, unknown>): asserts payload is EnboxConnectRequest {
|
|
510
|
+
const ctx = 'invalid connect request';
|
|
511
|
+
requireString(payload, 'clientDid', ctx);
|
|
512
|
+
requireString(payload, 'appName', ctx);
|
|
513
|
+
requireArray(payload, 'permissionRequests', ctx);
|
|
514
|
+
requireString(payload, 'nonce', ctx);
|
|
515
|
+
requireString(payload, 'state', ctx);
|
|
516
|
+
requireString(payload, 'callbackUrl', ctx);
|
|
517
|
+
requireLiteral(payload, 'responseMode', 'direct_post', ctx);
|
|
518
|
+
requireStringArray(payload, 'supportedDidMethods', ctx);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Runtime assertion that a verified JWT payload has the shape of an
|
|
523
|
+
* {@link EnboxConnectResponse}. Use immediately after `verifyJwt()` to narrow
|
|
524
|
+
* a `Record<string, unknown>` payload before accessing fields.
|
|
525
|
+
*/
|
|
526
|
+
function assertConnectResponse(payload: Record<string, unknown>): asserts payload is EnboxConnectResponse {
|
|
527
|
+
const ctx = 'invalid connect response';
|
|
528
|
+
requireString(payload, 'providerDid', ctx);
|
|
529
|
+
requireString(payload, 'delegateDid', ctx);
|
|
530
|
+
requireString(payload, 'aud', ctx);
|
|
531
|
+
requireNumber(payload, 'iat', ctx);
|
|
532
|
+
requireNumber(payload, 'exp', ctx);
|
|
533
|
+
requireOptionalString(payload, 'nonce', ctx);
|
|
534
|
+
requireArray(payload, 'delegateGrants', ctx);
|
|
535
|
+
requireObject(payload, 'delegatePortableDid', ctx);
|
|
536
|
+
requireOptionalArray(payload, 'delegateDecryptionKeys', ctx);
|
|
537
|
+
requireOptionalArray(payload, 'delegateContextKeys', ctx);
|
|
538
|
+
requireOptionalArray(payload, 'delegateMultiPartyProtocols', ctx);
|
|
539
|
+
requireOptionalArray(payload, 'sessionRevocations', ctx);
|
|
398
540
|
}
|
|
399
541
|
|
|
400
542
|
// ---------------------------------------------------------------------------
|
|
@@ -637,7 +779,9 @@ async function getConnectRequest(requestUri: string, encryptionKey: string): Pro
|
|
|
637
779
|
const response = await fetch(requestUri, { signal: AbortSignal.timeout(30_000) });
|
|
638
780
|
const jwe = await response.text();
|
|
639
781
|
const jwt = await decryptRequest({ jwe, encryptionKey });
|
|
640
|
-
|
|
782
|
+
const payload = await verifyJwt({ jwt });
|
|
783
|
+
assertConnectRequest(payload);
|
|
784
|
+
return payload;
|
|
641
785
|
}
|
|
642
786
|
|
|
643
787
|
// ---------------------------------------------------------------------------
|
|
@@ -903,10 +1047,12 @@ async function deriveScopedDecryptionKeys(
|
|
|
903
1047
|
DwnMethodName.Read, DwnMethodName.Query, DwnMethodName.Subscribe,
|
|
904
1048
|
]);
|
|
905
1049
|
|
|
906
|
-
// Collect read-like scopes only.
|
|
1050
|
+
// Collect read-like scopes only. `isRecordPermissionScope` narrows to
|
|
1051
|
+
// `DwnRecordsPermissionScope`, which declares `protocolPath?: string`
|
|
1052
|
+
// and `contextId?: string` — no `as any` needed for those reads below.
|
|
907
1053
|
const readScopes = scopes.filter(
|
|
908
|
-
(s): s is
|
|
909
|
-
isRecordPermissionScope(s) && readMethods.has(s.method
|
|
1054
|
+
(s): s is DwnRecordsPermissionScope =>
|
|
1055
|
+
isRecordPermissionScope(s) && readMethods.has(s.method),
|
|
910
1056
|
);
|
|
911
1057
|
|
|
912
1058
|
if (readScopes.length === 0) {
|
|
@@ -915,7 +1061,7 @@ async function deriveScopedDecryptionKeys(
|
|
|
915
1061
|
|
|
916
1062
|
// Fail closed: reject contextId-scoped encrypted reads.
|
|
917
1063
|
for (const scope of readScopes) {
|
|
918
|
-
if (
|
|
1064
|
+
if (scope.contextId) {
|
|
919
1065
|
throw new Error(
|
|
920
1066
|
`Encrypted delegate access scoped by contextId is not supported ` +
|
|
921
1067
|
`yet; use protocol-wide permissions for protocol '${protocolUri}'.`,
|
|
@@ -936,9 +1082,7 @@ async function deriveScopedDecryptionKeys(
|
|
|
936
1082
|
}
|
|
937
1083
|
|
|
938
1084
|
// Check if any scope is protocol-wide (no protocolPath).
|
|
939
|
-
const hasProtocolWideRead = readScopes.some(
|
|
940
|
-
(s) => !('protocolPath' in s) || !(s as any).protocolPath,
|
|
941
|
-
);
|
|
1085
|
+
const hasProtocolWideRead = readScopes.some((s) => !s.protocolPath);
|
|
942
1086
|
|
|
943
1087
|
const { keyId, keyUri } = await getEncryptionKeyInfo(agent, ownerDid);
|
|
944
1088
|
|
|
@@ -967,8 +1111,7 @@ async function deriveScopedDecryptionKeys(
|
|
|
967
1111
|
// Emit one exact-path key per unique protocolPath.
|
|
968
1112
|
const uniquePaths = new Set<string>();
|
|
969
1113
|
for (const scope of readScopes) {
|
|
970
|
-
|
|
971
|
-
if (pp) { uniquePaths.add(pp); }
|
|
1114
|
+
if (scope.protocolPath) { uniquePaths.add(scope.protocolPath); }
|
|
972
1115
|
}
|
|
973
1116
|
|
|
974
1117
|
const keys: DelegateDecryptionKey[] = [];
|
|
@@ -1058,9 +1201,12 @@ async function deriveContextKeysForDelegate(
|
|
|
1058
1201
|
DwnMethodName.Read, DwnMethodName.Query, DwnMethodName.Subscribe,
|
|
1059
1202
|
]);
|
|
1060
1203
|
|
|
1204
|
+
// `isRecordPermissionScope` narrows to `DwnRecordsPermissionScope`,
|
|
1205
|
+
// which declares `protocolPath?: string` and `contextId?: string` —
|
|
1206
|
+
// no `as any` needed for the field reads below.
|
|
1061
1207
|
const readScopes = scopes.filter(
|
|
1062
|
-
(s): s is
|
|
1063
|
-
isRecordPermissionScope(s) && readMethods.has(s.method
|
|
1208
|
+
(s): s is DwnRecordsPermissionScope =>
|
|
1209
|
+
isRecordPermissionScope(s) && readMethods.has(s.method),
|
|
1064
1210
|
);
|
|
1065
1211
|
|
|
1066
1212
|
if (readScopes.length === 0) {
|
|
@@ -1069,7 +1215,7 @@ async function deriveContextKeysForDelegate(
|
|
|
1069
1215
|
|
|
1070
1216
|
// Fail closed: reject contextId-scoped reads.
|
|
1071
1217
|
for (const scope of readScopes) {
|
|
1072
|
-
if (
|
|
1218
|
+
if (scope.contextId) {
|
|
1073
1219
|
throw new Error(
|
|
1074
1220
|
`Encrypted delegate access scoped by contextId is not supported ` +
|
|
1075
1221
|
`yet; use protocol-wide permissions for protocol ` +
|
|
@@ -1080,7 +1226,7 @@ async function deriveContextKeysForDelegate(
|
|
|
1080
1226
|
|
|
1081
1227
|
// Fail closed: reject protocolPath-scoped reads on multi-party protocols.
|
|
1082
1228
|
for (const scope of readScopes) {
|
|
1083
|
-
if (
|
|
1229
|
+
if (scope.protocolPath) {
|
|
1084
1230
|
throw new Error(
|
|
1085
1231
|
`Encrypted delegate access scoped by protocolPath on multi-party ` +
|
|
1086
1232
|
`protocols is not supported yet; use protocol-wide permissions for ` +
|
|
@@ -1113,8 +1259,7 @@ async function deriveContextKeysForDelegate(
|
|
|
1113
1259
|
});
|
|
1114
1260
|
|
|
1115
1261
|
for (const entry of reply.entries ?? []) {
|
|
1116
|
-
const rootContextId =
|
|
1117
|
-
|| (entry as any).recordId;
|
|
1262
|
+
const rootContextId = entry.contextId?.split('/')[0] ?? entry.recordId;
|
|
1118
1263
|
|
|
1119
1264
|
if (!rootContextId || seenContextIds.has(rootContextId)) { continue; }
|
|
1120
1265
|
seenContextIds.add(rootContextId);
|
|
@@ -1166,261 +1311,324 @@ async function submitConnectResponse(
|
|
|
1166
1311
|
pin: string | undefined,
|
|
1167
1312
|
agent: EnboxPlatformAgent
|
|
1168
1313
|
): Promise<void> {
|
|
1169
|
-
const
|
|
1170
|
-
const
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
//
|
|
1176
|
-
//
|
|
1177
|
-
//
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
// Derive the delegate's key-delivery ProtocolPath leaf public key.
|
|
1185
|
-
// This is the pre-derived key that the owner will use later when writing
|
|
1186
|
-
// contextKey records addressed to this delegate. The owner cannot derive
|
|
1187
|
-
// this from the delegate's root public key alone (HKDF needs the private
|
|
1188
|
-
// key), so we compute it now while we have temporary access to the
|
|
1189
|
-
// delegate's private key material.
|
|
1190
|
-
const delegateX25519PrivateKeyBytes = await X25519.privateKeyToBytes({
|
|
1191
|
-
privateKey: delegateX25519PrivateKey,
|
|
1192
|
-
});
|
|
1193
|
-
const keyDeliveryDerivationPath = [
|
|
1194
|
-
KeyDerivationScheme.ProtocolPath,
|
|
1195
|
-
KeyDeliveryProtocolDefinition.protocol,
|
|
1196
|
-
'contextKey',
|
|
1197
|
-
];
|
|
1198
|
-
const delegateLeafPrivateKeyBytes = await HdKey.derivePrivateKeyBytes(
|
|
1199
|
-
delegateX25519PrivateKeyBytes, keyDeliveryDerivationPath,
|
|
1314
|
+
const submitStart = nowMs();
|
|
1315
|
+
const numProtocols = connectRequest.permissionRequests.length;
|
|
1316
|
+
const numScopes = connectRequest.permissionRequests.reduce(
|
|
1317
|
+
(sum, req) => sum + req.permissionScopes.length,
|
|
1318
|
+
0,
|
|
1319
|
+
);
|
|
1320
|
+
// Tracked across the try/finally so the aggregate `submitConnectResponse.total`
|
|
1321
|
+
// log emits on both success and failure paths — operators bisecting wall-time
|
|
1322
|
+
// from wallet debug logs need the total even when a phase throws.
|
|
1323
|
+
let sessionGrantCount = 0;
|
|
1324
|
+
let outcome: 'ok' | 'fail' = 'ok';
|
|
1325
|
+
logger.log(
|
|
1326
|
+
`${CONNECT_PERF_LOG_PREFIX} submitConnectResponse.start `
|
|
1327
|
+
+ `(protocols=${numProtocols}, scopes=${numScopes})`,
|
|
1200
1328
|
);
|
|
1201
|
-
const delegateLeafPrivateKeyJwk = await X25519.bytesToPrivateKey({
|
|
1202
|
-
privateKeyBytes: delegateLeafPrivateKeyBytes,
|
|
1203
|
-
});
|
|
1204
|
-
const delegateKeyDeliveryLeafPublicKey = await X25519.getPublicKey({
|
|
1205
|
-
key: delegateLeafPrivateKeyJwk,
|
|
1206
|
-
});
|
|
1207
1329
|
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1330
|
+
try {
|
|
1331
|
+
const delegateBearerDid = await timed(`${CONNECT_PERF_LOG_PREFIX} delegateDid.create`, () => DidJwk.create());
|
|
1332
|
+
const delegatePortableDid = await delegateBearerDid.export();
|
|
1333
|
+
|
|
1334
|
+
// Add X25519 key derived from the delegate's Ed25519 key.
|
|
1335
|
+
// did:jwk only supports one verification method, but DWN encryption
|
|
1336
|
+
// requires X25519 for key agreement. Including the derived X25519
|
|
1337
|
+
// private key in the PortableDid ensures the delegate agent's KMS
|
|
1338
|
+
// has both keys after import. The Ed25519→X25519 conversion is a
|
|
1339
|
+
// standard cryptographic operation (RFC 8032 / libsodium).
|
|
1340
|
+
const delegateEdPrivateKey = delegatePortableDid.privateKeys![0];
|
|
1341
|
+
const delegateX25519PrivateKey = await Ed25519.convertPrivateKeyToX25519({
|
|
1342
|
+
privateKey: delegateEdPrivateKey,
|
|
1343
|
+
});
|
|
1344
|
+
delegatePortableDid.privateKeys!.push(delegateX25519PrivateKey);
|
|
1345
|
+
|
|
1346
|
+
// Derive the delegate's key-delivery ProtocolPath leaf public key.
|
|
1347
|
+
// This is the pre-derived key that the owner will use later when writing
|
|
1348
|
+
// contextKey records addressed to this delegate. The owner cannot derive
|
|
1349
|
+
// this from the delegate's root public key alone (HKDF needs the private
|
|
1350
|
+
// key), so we compute it now while we have temporary access to the
|
|
1351
|
+
// delegate's private key material.
|
|
1352
|
+
const delegateX25519PrivateKeyBytes = await X25519.privateKeyToBytes({
|
|
1353
|
+
privateKey: delegateX25519PrivateKey,
|
|
1354
|
+
});
|
|
1355
|
+
const keyDeliveryDerivationPath = [
|
|
1356
|
+
KeyDerivationScheme.ProtocolPath,
|
|
1357
|
+
KeyDeliveryProtocolDefinition.protocol,
|
|
1358
|
+
'contextKey',
|
|
1359
|
+
];
|
|
1360
|
+
const delegateLeafPrivateKeyBytes = await HdKey.derivePrivateKeyBytes(
|
|
1361
|
+
delegateX25519PrivateKeyBytes, keyDeliveryDerivationPath,
|
|
1362
|
+
);
|
|
1363
|
+
const delegateLeafPrivateKeyJwk = await X25519.bytesToPrivateKey({
|
|
1364
|
+
privateKeyBytes: delegateLeafPrivateKeyBytes,
|
|
1365
|
+
});
|
|
1366
|
+
const delegateKeyDeliveryLeafPublicKey = await X25519.getPublicKey({
|
|
1367
|
+
key: delegateLeafPrivateKeyJwk,
|
|
1368
|
+
});
|
|
1219
1369
|
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1370
|
+
// The rootKeyId is the delegate's keyAgreement VM id (e.g. `did:jwk:...#0`).
|
|
1371
|
+
// For did:jwk this is the Ed25519 VM, but getEncryptionKeyInfo() also returns
|
|
1372
|
+
// this same id after Ed25519→X25519 conversion. The DWN SDK matches the JWE
|
|
1373
|
+
// `kid` header against the KeyDecrypter's `rootKeyId`, so both sides must use
|
|
1374
|
+
// the same id — which they do because both derive from verificationMethod.id
|
|
1375
|
+
// of the keyAgreement relationship.
|
|
1376
|
+
const delegateKeyAgreementVmId = delegateBearerDid.document.verificationMethod![0].id;
|
|
1377
|
+
const delegateKeyDeliveryData = {
|
|
1378
|
+
rootKeyId : delegateKeyAgreementVmId,
|
|
1379
|
+
publicKeyJwk : delegateKeyDeliveryLeafPublicKey,
|
|
1380
|
+
};
|
|
1381
|
+
|
|
1382
|
+
// Derive scope-aware decryption keys for encrypted protocols.
|
|
1383
|
+
// Single-party: ProtocolPath keys (protocol-wide or exact-path).
|
|
1384
|
+
// Multi-party: ProtocolContext keys (per rootContextId).
|
|
1385
|
+
// Write-only delegates receive no decryption capability.
|
|
1386
|
+
const delegateDecryptionKeys: DelegateDecryptionKey[] = [];
|
|
1387
|
+
const delegateContextKeys: DelegateContextKey[] = [];
|
|
1388
|
+
const delegateMultiPartyProtocols: string[] = [];
|
|
1389
|
+
|
|
1390
|
+
const delegateGrants = await timed(
|
|
1391
|
+
`${CONNECT_PERF_LOG_PREFIX} permissionGrants.fanout (protocols=${numProtocols})`,
|
|
1392
|
+
async () => {
|
|
1393
|
+
const delegateGrantPromises = connectRequest.permissionRequests.map(
|
|
1394
|
+
async (permissionRequest) => {
|
|
1395
|
+
const { protocolDefinition, permissionScopes } = permissionRequest;
|
|
1396
|
+
|
|
1397
|
+
const grantsMatchProtocolUri = permissionScopes.every(
|
|
1398
|
+
scope => 'protocol' in scope && scope.protocol === protocolDefinition.protocol
|
|
1399
|
+
);
|
|
1400
|
+
if (!grantsMatchProtocolUri) {
|
|
1401
|
+
throw new Error('All permission scopes must match the protocol URI they are provided with.');
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
await prepareProtocol(selectedDid, agent, protocolDefinition);
|
|
1405
|
+
|
|
1406
|
+
const hasEncryptedTypes = Object.values(protocolDefinition.types ?? {})
|
|
1407
|
+
.some((type: any) => type?.encryptionRequired === true);
|
|
1408
|
+
|
|
1409
|
+
if (hasEncryptedTypes) {
|
|
1410
|
+
const { multiParty, singleParty } = classifyProtocolRoots(protocolDefinition);
|
|
1411
|
+
|
|
1412
|
+
if (multiParty.length > 0 && singleParty.length > 0) {
|
|
1413
|
+
// Mixed protocol: some roots are multi-party, others single-party.
|
|
1414
|
+
// We cannot safely model this with either key type alone.
|
|
1415
|
+
throw new Error(
|
|
1416
|
+
`Encrypted delegate access for protocols with mixed single-party ` +
|
|
1417
|
+
`and multi-party roots is not supported yet. ` +
|
|
1418
|
+
`Protocol '${protocolDefinition.protocol}' has multi-party roots ` +
|
|
1419
|
+
`[${multiParty.join(', ')}] and single-party roots ` +
|
|
1420
|
+
`[${singleParty.join(', ')}].`,
|
|
1421
|
+
);
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
if (multiParty.length > 0) {
|
|
1425
|
+
// Pure multi-party: derive per-context keys for existing contexts.
|
|
1426
|
+
// Unsupported scope shapes (protocolPath, contextId) throw.
|
|
1427
|
+
const ctxKeys = await deriveContextKeysForDelegate(
|
|
1428
|
+
agent, selectedDid, protocolDefinition, permissionScopes,
|
|
1429
|
+
);
|
|
1430
|
+
delegateContextKeys.push(...ctxKeys);
|
|
1431
|
+
|
|
1432
|
+
// Only register the protocol for post-connect delivery if the
|
|
1433
|
+
// delegate has at least one read-like scope. Write-only delegates
|
|
1434
|
+
// must NOT receive context keys — they have no decryption need.
|
|
1435
|
+
const readMethods = new Set([
|
|
1436
|
+
DwnMethodName.Read, DwnMethodName.Query, DwnMethodName.Subscribe,
|
|
1437
|
+
]);
|
|
1438
|
+
const hasReadLikeScope = permissionScopes.some(
|
|
1439
|
+
(s): boolean => isRecordPermissionScope(s) && readMethods.has(s.method as DwnMethodName),
|
|
1440
|
+
);
|
|
1441
|
+
if (hasReadLikeScope) {
|
|
1442
|
+
delegateMultiPartyProtocols.push(protocolDefinition.protocol);
|
|
1443
|
+
}
|
|
1444
|
+
} else {
|
|
1445
|
+
// Pure single-party: derive ProtocolPath keys.
|
|
1446
|
+
// Unsupported scope shapes (contextId) throw.
|
|
1447
|
+
const keys = await deriveScopedDecryptionKeys(
|
|
1448
|
+
agent, selectedDid, protocolDefinition.protocol,
|
|
1449
|
+
permissionScopes, protocolDefinition,
|
|
1450
|
+
);
|
|
1451
|
+
delegateDecryptionKeys.push(...keys);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
return EnboxConnectProtocol.createPermissionGrants(
|
|
1456
|
+
selectedDid,
|
|
1457
|
+
delegateBearerDid,
|
|
1458
|
+
agent,
|
|
1459
|
+
permissionScopes,
|
|
1460
|
+
delegateKeyDeliveryData,
|
|
1461
|
+
);
|
|
1462
|
+
}
|
|
1463
|
+
);
|
|
1227
1464
|
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1465
|
+
return (await Promise.all(delegateGrantPromises)).flat();
|
|
1466
|
+
},
|
|
1467
|
+
);
|
|
1231
1468
|
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1469
|
+
// Create per-grant contextId-scoped revocation grants.
|
|
1470
|
+
// Each revocation grant authorizes the delegate to write a revocation
|
|
1471
|
+
// ONLY for the specific session grant it corresponds to.
|
|
1472
|
+
const permissionsApi = new AgentPermissionsApi({ agent });
|
|
1473
|
+
const sessionRevocations: { grantId: string; revocationGrantId: string }[] = [];
|
|
1474
|
+
let revGrantEndpoints: string[] = [];
|
|
1475
|
+
try {
|
|
1476
|
+
revGrantEndpoints = await agent.dwn.getDwnEndpointUrlsForTarget(selectedDid);
|
|
1477
|
+
} catch {
|
|
1478
|
+
// Endpoint resolution failure — revocation grants will be local-only until sync.
|
|
1479
|
+
}
|
|
1238
1480
|
|
|
1239
|
-
|
|
1481
|
+
// Snapshot the current length — revocation grants are appended to delegateGrants
|
|
1482
|
+
// below, but we must NOT iterate over them (they are meta-grants, not session grants).
|
|
1483
|
+
sessionGrantCount = delegateGrants.length;
|
|
1484
|
+
|
|
1485
|
+
// Phase 1: create all revocation grants locally with bounded concurrency.
|
|
1486
|
+
// createGrant is local-only (storage + signing) so it's cheap, but we still
|
|
1487
|
+
// cap parallelism to avoid head-of-line blocking when sessionGrantCount is
|
|
1488
|
+
// large (e.g. dapp requesting many scopes at once).
|
|
1489
|
+
const revGrantResults = await timed(
|
|
1490
|
+
`${CONNECT_PERF_LOG_PREFIX} revocationGrants.create (n=${sessionGrantCount})`,
|
|
1491
|
+
() => mapConcurrent(
|
|
1492
|
+
delegateGrants.slice(0, sessionGrantCount),
|
|
1493
|
+
CONNECT_FANOUT_CONCURRENCY,
|
|
1494
|
+
(grantMessage) =>
|
|
1495
|
+
permissionsApi.createGrant({
|
|
1496
|
+
delegated : true,
|
|
1497
|
+
store : true,
|
|
1498
|
+
grantedTo : delegateBearerDid.uri,
|
|
1499
|
+
scope : {
|
|
1500
|
+
interface : DwnInterfaceName.Records,
|
|
1501
|
+
method : DwnMethodName.Write,
|
|
1502
|
+
protocol : PermissionsProtocol.uri,
|
|
1503
|
+
contextId : grantMessage.recordId,
|
|
1504
|
+
},
|
|
1505
|
+
dateExpires : '2040-06-25T16:09:16.693356Z',
|
|
1506
|
+
author : selectedDid,
|
|
1507
|
+
}).then((revGrant) => ({ grantMessage, revGrant })),
|
|
1508
|
+
),
|
|
1509
|
+
);
|
|
1240
1510
|
|
|
1241
|
-
|
|
1242
|
-
|
|
1511
|
+
// Phase 2: fan out every revocation grant to every owner DWN endpoint with
|
|
1512
|
+
// a single global concurrency cap so that (grants × endpoints) cannot blow
|
|
1513
|
+
// up. This is best-effort (sync delivers eventually) so individual failures
|
|
1514
|
+
// are tolerated by `mapConcurrentSettled`.
|
|
1515
|
+
const revSendTasks = revGrantResults.flatMap(({ grantMessage, revGrant }) => {
|
|
1516
|
+
sessionRevocations.push({
|
|
1517
|
+
grantId : grantMessage.recordId,
|
|
1518
|
+
revocationGrantId : revGrant.message.recordId,
|
|
1519
|
+
});
|
|
1243
1520
|
|
|
1244
|
-
|
|
1245
|
-
|
|
1521
|
+
const { encodedData: revEncoded, ...revRawMessage } = revGrant.message;
|
|
1522
|
+
const revData = Uint8Array.from(Convert.base64Url(revEncoded).toUint8Array());
|
|
1246
1523
|
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
// We cannot safely model this with either key type alone.
|
|
1250
|
-
throw new Error(
|
|
1251
|
-
`Encrypted delegate access for protocols with mixed single-party ` +
|
|
1252
|
-
`and multi-party roots is not supported yet. ` +
|
|
1253
|
-
`Protocol '${protocolDefinition.protocol}' has multi-party roots ` +
|
|
1254
|
-
`[${multiParty.join(', ')}] and single-party roots ` +
|
|
1255
|
-
`[${singleParty.join(', ')}].`,
|
|
1256
|
-
);
|
|
1257
|
-
}
|
|
1524
|
+
// Include the revocation grant in the delegate grants for distribution.
|
|
1525
|
+
delegateGrants.push(revGrant.message);
|
|
1258
1526
|
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
// Unsupported scope shapes (protocolPath, contextId) throw.
|
|
1262
|
-
const ctxKeys = await deriveContextKeysForDelegate(
|
|
1263
|
-
agent, selectedDid, protocolDefinition, permissionScopes,
|
|
1264
|
-
);
|
|
1265
|
-
delegateContextKeys.push(...ctxKeys);
|
|
1266
|
-
|
|
1267
|
-
// Only register the protocol for post-connect delivery if the
|
|
1268
|
-
// delegate has at least one read-like scope. Write-only delegates
|
|
1269
|
-
// must NOT receive context keys — they have no decryption need.
|
|
1270
|
-
const readMethods = new Set([
|
|
1271
|
-
DwnMethodName.Read, DwnMethodName.Query, DwnMethodName.Subscribe,
|
|
1272
|
-
]);
|
|
1273
|
-
const hasReadLikeScope = permissionScopes.some(
|
|
1274
|
-
(s): boolean => isRecordPermissionScope(s) && readMethods.has(s.method as DwnMethodName),
|
|
1275
|
-
);
|
|
1276
|
-
if (hasReadLikeScope) {
|
|
1277
|
-
delegateMultiPartyProtocols.push(protocolDefinition.protocol);
|
|
1278
|
-
}
|
|
1279
|
-
} else {
|
|
1280
|
-
// Pure single-party: derive ProtocolPath keys.
|
|
1281
|
-
// Unsupported scope shapes (contextId) throw.
|
|
1282
|
-
const keys = await deriveScopedDecryptionKeys(
|
|
1283
|
-
agent, selectedDid, protocolDefinition.protocol,
|
|
1284
|
-
permissionScopes, protocolDefinition,
|
|
1285
|
-
);
|
|
1286
|
-
delegateDecryptionKeys.push(...keys);
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1527
|
+
return revGrantEndpoints.map((dwnUrl) => ({ revRawMessage, revData, dwnUrl }));
|
|
1528
|
+
});
|
|
1289
1529
|
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1530
|
+
if (revSendTasks.length > 0) {
|
|
1531
|
+
await timed(
|
|
1532
|
+
`${CONNECT_PERF_LOG_PREFIX} revocationGrants.fanout (sends=${revSendTasks.length}, endpoints=${revGrantEndpoints.length})`,
|
|
1533
|
+
() => mapConcurrentSettled(
|
|
1534
|
+
revSendTasks,
|
|
1535
|
+
CONNECT_FANOUT_CONCURRENCY,
|
|
1536
|
+
({ revRawMessage, revData, dwnUrl }) =>
|
|
1537
|
+
agent.rpc.sendDwnRequest({
|
|
1538
|
+
dwnUrl,
|
|
1539
|
+
targetDid : selectedDid,
|
|
1540
|
+
message : revRawMessage,
|
|
1541
|
+
data : new Blob([revData]),
|
|
1542
|
+
signal : AbortSignal.timeout(CONNECT_REQUEST_TIMEOUT_MS),
|
|
1543
|
+
}),
|
|
1544
|
+
),
|
|
1296
1545
|
);
|
|
1297
1546
|
}
|
|
1298
|
-
);
|
|
1299
|
-
|
|
1300
|
-
const delegateGrants = (await Promise.all(delegateGrantPromises)).flat();
|
|
1301
1547
|
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1548
|
+
const responseObject = await timed(
|
|
1549
|
+
`${CONNECT_PERF_LOG_PREFIX} response.build`,
|
|
1550
|
+
() => EnboxConnectProtocol.createConnectResponse({
|
|
1551
|
+
providerDid : selectedDid,
|
|
1552
|
+
delegateDid : delegateBearerDid.uri,
|
|
1553
|
+
aud : connectRequest.clientDid,
|
|
1554
|
+
nonce : connectRequest.nonce,
|
|
1555
|
+
delegateGrants,
|
|
1556
|
+
delegatePortableDid,
|
|
1557
|
+
delegateDecryptionKeys : delegateDecryptionKeys.length > 0 ? delegateDecryptionKeys : undefined,
|
|
1558
|
+
delegateContextKeys : delegateContextKeys.length > 0 ? delegateContextKeys : undefined,
|
|
1559
|
+
delegateMultiPartyProtocols : delegateMultiPartyProtocols.length > 0 ? delegateMultiPartyProtocols : undefined,
|
|
1560
|
+
sessionRevocations : sessionRevocations.length > 0 ? sessionRevocations : undefined,
|
|
1561
|
+
}),
|
|
1562
|
+
);
|
|
1317
1563
|
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
(grantMessage) =>
|
|
1326
|
-
permissionsApi.createGrant({
|
|
1327
|
-
delegated : true,
|
|
1328
|
-
store : true,
|
|
1329
|
-
grantedTo : delegateBearerDid.uri,
|
|
1330
|
-
scope : {
|
|
1331
|
-
interface : DwnInterfaceName.Records,
|
|
1332
|
-
method : DwnMethodName.Write,
|
|
1333
|
-
protocol : PermissionsProtocol.uri,
|
|
1334
|
-
contextId : grantMessage.recordId,
|
|
1335
|
-
},
|
|
1336
|
-
dateExpires : '2040-06-25T16:09:16.693356Z',
|
|
1337
|
-
author : selectedDid,
|
|
1338
|
-
}).then((revGrant) => ({ grantMessage, revGrant })),
|
|
1339
|
-
);
|
|
1564
|
+
const responseObjectJwt = await timed(
|
|
1565
|
+
`${CONNECT_PERF_LOG_PREFIX} response.sign`,
|
|
1566
|
+
() => EnboxConnectProtocol.signJwt({
|
|
1567
|
+
did : delegateBearerDid,
|
|
1568
|
+
data : responseObject,
|
|
1569
|
+
}),
|
|
1570
|
+
);
|
|
1340
1571
|
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
const revSendTasks = revGrantResults.flatMap(({ grantMessage, revGrant }) => {
|
|
1346
|
-
sessionRevocations.push({
|
|
1347
|
-
grantId : grantMessage.recordId,
|
|
1348
|
-
revocationGrantId : revGrant.message.recordId,
|
|
1349
|
-
});
|
|
1572
|
+
const clientDid = await timed(
|
|
1573
|
+
`${CONNECT_PERF_LOG_PREFIX} clientDid.resolve`,
|
|
1574
|
+
() => DidJwk.resolve(connectRequest.clientDid),
|
|
1575
|
+
);
|
|
1350
1576
|
|
|
1351
|
-
const
|
|
1352
|
-
|
|
1577
|
+
const sharedKey = await timed(
|
|
1578
|
+
`${CONNECT_PERF_LOG_PREFIX} response.deriveSharedKey`,
|
|
1579
|
+
() => EnboxConnectProtocol.deriveSharedKey(
|
|
1580
|
+
delegateBearerDid,
|
|
1581
|
+
clientDid?.didDocument!,
|
|
1582
|
+
),
|
|
1583
|
+
);
|
|
1353
1584
|
|
|
1354
|
-
|
|
1355
|
-
|
|
1585
|
+
const encryptedResponse = await timed(
|
|
1586
|
+
`${CONNECT_PERF_LOG_PREFIX} response.encrypt`,
|
|
1587
|
+
() => EnboxConnectProtocol.encryptResponse({
|
|
1588
|
+
jwt : responseObjectJwt,
|
|
1589
|
+
encryptionKey : sharedKey,
|
|
1590
|
+
delegatePublicKeyJwk : delegateBearerDid.document.verificationMethod![0].publicKeyJwk!,
|
|
1591
|
+
pin,
|
|
1592
|
+
}),
|
|
1593
|
+
);
|
|
1356
1594
|
|
|
1357
|
-
|
|
1358
|
-
|
|
1595
|
+
const formEncodedRequest = new URLSearchParams({
|
|
1596
|
+
id_token : encryptedResponse,
|
|
1597
|
+
state : connectRequest.state,
|
|
1598
|
+
}).toString();
|
|
1599
|
+
|
|
1600
|
+
await timed(
|
|
1601
|
+
`${CONNECT_PERF_LOG_PREFIX} response.callbackPost`,
|
|
1602
|
+
async () => {
|
|
1603
|
+
const response = await fetch(connectRequest.callbackUrl, {
|
|
1604
|
+
body : formEncodedRequest,
|
|
1605
|
+
method : 'POST',
|
|
1606
|
+
headers : {
|
|
1607
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
1608
|
+
},
|
|
1609
|
+
signal: AbortSignal.timeout(30_000),
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1612
|
+
if (!response.ok) {
|
|
1613
|
+
// NOTE: delegate grants have already been written/fanned out by this
|
|
1614
|
+
// point, so callers may observe partial owner-side state when the
|
|
1615
|
+
// callback server rejects the final response delivery.
|
|
1616
|
+
throw new Error(`Connect: callback POST failed with HTTP ${response.status}.`);
|
|
1617
|
+
}
|
|
1359
1618
|
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
}),
|
|
1619
|
+
return response;
|
|
1620
|
+
},
|
|
1621
|
+
);
|
|
1622
|
+
} catch (err) {
|
|
1623
|
+
outcome = 'fail';
|
|
1624
|
+
throw err;
|
|
1625
|
+
} finally {
|
|
1626
|
+
const totalElapsed = nowMs() - submitStart;
|
|
1627
|
+
logger.log(
|
|
1628
|
+
`${CONNECT_PERF_LOG_PREFIX} submitConnectResponse.total ${outcome} in ${totalElapsed.toFixed(1)}ms `
|
|
1629
|
+
+ `(protocols=${numProtocols}, scopes=${numScopes}, sessionGrants=${sessionGrantCount})`,
|
|
1372
1630
|
);
|
|
1373
1631
|
}
|
|
1374
|
-
|
|
1375
|
-
logger.log('Building connect response...');
|
|
1376
|
-
const responseObject = await EnboxConnectProtocol.createConnectResponse({
|
|
1377
|
-
providerDid : selectedDid,
|
|
1378
|
-
delegateDid : delegateBearerDid.uri,
|
|
1379
|
-
aud : connectRequest.clientDid,
|
|
1380
|
-
nonce : connectRequest.nonce,
|
|
1381
|
-
delegateGrants,
|
|
1382
|
-
delegatePortableDid,
|
|
1383
|
-
delegateDecryptionKeys : delegateDecryptionKeys.length > 0 ? delegateDecryptionKeys : undefined,
|
|
1384
|
-
delegateContextKeys : delegateContextKeys.length > 0 ? delegateContextKeys : undefined,
|
|
1385
|
-
delegateMultiPartyProtocols : delegateMultiPartyProtocols.length > 0 ? delegateMultiPartyProtocols : undefined,
|
|
1386
|
-
sessionRevocations : sessionRevocations.length > 0 ? sessionRevocations : undefined,
|
|
1387
|
-
});
|
|
1388
|
-
|
|
1389
|
-
logger.log('Signing connect response...');
|
|
1390
|
-
const responseObjectJwt = await EnboxConnectProtocol.signJwt({
|
|
1391
|
-
did : delegateBearerDid,
|
|
1392
|
-
data : responseObject as unknown as Record<string, unknown>,
|
|
1393
|
-
});
|
|
1394
|
-
|
|
1395
|
-
const clientDid = await DidJwk.resolve(connectRequest.clientDid);
|
|
1396
|
-
|
|
1397
|
-
const sharedKey = await EnboxConnectProtocol.deriveSharedKey(
|
|
1398
|
-
delegateBearerDid,
|
|
1399
|
-
clientDid?.didDocument!
|
|
1400
|
-
);
|
|
1401
|
-
|
|
1402
|
-
logger.log('Encrypting connect response...');
|
|
1403
|
-
const encryptedResponse = await EnboxConnectProtocol.encryptResponse({
|
|
1404
|
-
jwt : responseObjectJwt,
|
|
1405
|
-
encryptionKey : sharedKey,
|
|
1406
|
-
delegatePublicKeyJwk : delegateBearerDid.document.verificationMethod![0].publicKeyJwk!,
|
|
1407
|
-
pin,
|
|
1408
|
-
});
|
|
1409
|
-
|
|
1410
|
-
const formEncodedRequest = new URLSearchParams({
|
|
1411
|
-
id_token : encryptedResponse,
|
|
1412
|
-
state : connectRequest.state,
|
|
1413
|
-
}).toString();
|
|
1414
|
-
|
|
1415
|
-
logger.log(`Sending connect response to: ${connectRequest.callbackUrl}`);
|
|
1416
|
-
await fetch(connectRequest.callbackUrl, {
|
|
1417
|
-
body : formEncodedRequest,
|
|
1418
|
-
method : 'POST',
|
|
1419
|
-
headers : {
|
|
1420
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
1421
|
-
},
|
|
1422
|
-
signal: AbortSignal.timeout(30_000),
|
|
1423
|
-
});
|
|
1424
1632
|
}
|
|
1425
1633
|
|
|
1426
1634
|
// ---------------------------------------------------------------------------
|
|
@@ -1431,6 +1639,8 @@ export const EnboxConnectProtocol = {
|
|
|
1431
1639
|
buildConnectUrl,
|
|
1432
1640
|
signJwt,
|
|
1433
1641
|
verifyJwt,
|
|
1642
|
+
assertConnectRequest,
|
|
1643
|
+
assertConnectResponse,
|
|
1434
1644
|
encryptRequest,
|
|
1435
1645
|
decryptRequest,
|
|
1436
1646
|
encryptResponse,
|