@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.
- 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 +455 -246
- 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 +556 -298
- 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
|
@@ -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 {
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
420
|
-
*
|
|
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
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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 (
|
|
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) => !
|
|
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
|
-
|
|
550
|
-
|
|
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 (
|
|
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 (
|
|
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
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
//
|
|
724
|
-
//
|
|
725
|
-
//
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
const
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
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
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
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
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
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
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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,
|