@enbox/agent 0.5.16 → 0.6.0
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/dwn-api.js +433 -33
- package/dist/esm/dwn-api.js.map +1 -1
- package/dist/esm/dwn-encryption.js +131 -12
- package/dist/esm/dwn-encryption.js.map +1 -1
- package/dist/esm/dwn-key-delivery.js +64 -47
- package/dist/esm/dwn-key-delivery.js.map +1 -1
- package/dist/esm/enbox-connect-protocol.js +400 -3
- package/dist/esm/enbox-connect-protocol.js.map +1 -1
- package/dist/esm/permissions-api.js +11 -1
- package/dist/esm/permissions-api.js.map +1 -1
- package/dist/esm/sync-engine-level.js +407 -6
- package/dist/esm/sync-engine-level.js.map +1 -1
- package/dist/esm/sync-messages.js +10 -3
- package/dist/esm/sync-messages.js.map +1 -1
- package/dist/types/dwn-api.d.ts +159 -0
- package/dist/types/dwn-api.d.ts.map +1 -1
- package/dist/types/dwn-encryption.d.ts +39 -2
- package/dist/types/dwn-encryption.d.ts.map +1 -1
- package/dist/types/dwn-key-delivery.d.ts +1 -9
- package/dist/types/dwn-key-delivery.d.ts.map +1 -1
- package/dist/types/enbox-connect-protocol.d.ts +166 -1
- package/dist/types/enbox-connect-protocol.d.ts.map +1 -1
- package/dist/types/permissions-api.d.ts.map +1 -1
- package/dist/types/sync-engine-level.d.ts +45 -1
- package/dist/types/sync-engine-level.d.ts.map +1 -1
- package/dist/types/sync-messages.d.ts +2 -2
- package/dist/types/sync-messages.d.ts.map +1 -1
- package/dist/types/types/permissions.d.ts +9 -0
- package/dist/types/types/permissions.d.ts.map +1 -1
- package/dist/types/types/sync.d.ts +70 -2
- package/dist/types/types/sync.d.ts.map +1 -1
- package/package.json +5 -4
- package/src/dwn-api.ts +494 -38
- package/src/dwn-encryption.ts +160 -11
- package/src/dwn-key-delivery.ts +73 -61
- package/src/enbox-connect-protocol.ts +575 -6
- package/src/permissions-api.ts +13 -1
- package/src/sync-engine-level.ts +368 -4
- package/src/sync-messages.ts +14 -5
- package/src/types/permissions.ts +9 -0
- package/src/types/sync.ts +86 -2
package/src/dwn-encryption.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
RecordsReadReply,
|
|
8
8
|
RecordsWriteMessage,
|
|
9
9
|
} from '@enbox/dwn-sdk-js';
|
|
10
|
-
import type { KeyIdentifier, PublicKeyJwk } from '@enbox/crypto';
|
|
10
|
+
import type { Jwk, KeyIdentifier, PublicKeyJwk } from '@enbox/crypto';
|
|
11
11
|
|
|
12
12
|
import type { EnboxPlatformAgent } from './types/agent.js';
|
|
13
13
|
import type {
|
|
@@ -16,7 +16,6 @@ import type {
|
|
|
16
16
|
SendDwnRequest,
|
|
17
17
|
} from './types/dwn.js';
|
|
18
18
|
|
|
19
|
-
import { X25519 } from '@enbox/crypto';
|
|
20
19
|
import {
|
|
21
20
|
Cid,
|
|
22
21
|
ContentEncryptionAlgorithm,
|
|
@@ -26,6 +25,7 @@ import {
|
|
|
26
25
|
KeyDerivationScheme,
|
|
27
26
|
Records,
|
|
28
27
|
} from '@enbox/dwn-sdk-js';
|
|
28
|
+
import { Ed25519, X25519 } from '@enbox/crypto';
|
|
29
29
|
|
|
30
30
|
import { DwnInterface } from './types/dwn.js';
|
|
31
31
|
import { isDwnRequest } from './dwn-type-guards.js';
|
|
@@ -140,22 +140,33 @@ export async function getEncryptionKeyInfo(
|
|
|
140
140
|
);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
// 4.
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
// 4. Resolve or derive the X25519 key for encryption.
|
|
144
|
+
// Standard case: the keyAgreement VM already has an X25519 key.
|
|
145
|
+
// Delegate case: did:jwk with Ed25519 only — convert to X25519.
|
|
146
|
+
// The Ed25519→X25519 conversion is a standard cryptographic operation
|
|
147
|
+
// (RFC 8032 / libsodium). The converted X25519 key must already be
|
|
148
|
+
// present in the agent's KMS (imported via the delegate PortableDid).
|
|
149
|
+
let resolvedPublicKeyJwk = verificationMethod.publicKeyJwk;
|
|
150
|
+
|
|
151
|
+
if (resolvedPublicKeyJwk.crv === 'Ed25519') {
|
|
152
|
+
resolvedPublicKeyJwk = await Ed25519.convertPublicKeyToX25519({
|
|
153
|
+
publicKey: resolvedPublicKeyJwk,
|
|
154
|
+
});
|
|
155
|
+
} else if (resolvedPublicKeyJwk.crv !== 'X25519') {
|
|
146
156
|
throw new Error(
|
|
147
157
|
`AgentDwnApi: keyAgreement key for '${didUri}' uses curve ` +
|
|
148
|
-
`'${
|
|
158
|
+
`'${resolvedPublicKeyJwk.crv}', but DWN encryption requires ` +
|
|
159
|
+
`'X25519' (or 'Ed25519' which is auto-converted).`
|
|
149
160
|
);
|
|
150
161
|
}
|
|
151
162
|
|
|
152
163
|
// 5. Compute the KMS key URI (does NOT export the key)
|
|
153
|
-
const keyUri = await agent.keyManager.getKeyUri({ key:
|
|
164
|
+
const keyUri = await agent.keyManager.getKeyUri({ key: resolvedPublicKeyJwk });
|
|
154
165
|
|
|
155
166
|
return {
|
|
156
167
|
keyId : verificationMethod.id,
|
|
157
168
|
keyUri,
|
|
158
|
-
publicKeyJwk :
|
|
169
|
+
publicKeyJwk : resolvedPublicKeyJwk as PublicKeyJwk,
|
|
159
170
|
};
|
|
160
171
|
}
|
|
161
172
|
|
|
@@ -295,11 +306,55 @@ export function buildContextKeyDecrypter(
|
|
|
295
306
|
};
|
|
296
307
|
}
|
|
297
308
|
|
|
309
|
+
/** Cache entry shape for scope-aware delegate decryption keys. */
|
|
310
|
+
export type DelegateDecryptionKeyEntry = {
|
|
311
|
+
protocol: string;
|
|
312
|
+
scope: { kind: 'protocol' } | { kind: 'protocolPath'; protocolPath: string; match: 'exact' };
|
|
313
|
+
derivedPrivateKey: DerivedPrivateJwk;
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Builds a KeyDecrypter for an exact-path delegate key that enforces the
|
|
318
|
+
* record's full derivation path matches the key's path exactly — siblings
|
|
319
|
+
* and descendants are NOT accessible.
|
|
320
|
+
*/
|
|
321
|
+
export function buildExactProtocolPathDecrypter(
|
|
322
|
+
key: DerivedPrivateJwk,
|
|
323
|
+
): KeyDecrypter {
|
|
324
|
+
return {
|
|
325
|
+
rootKeyId : key.rootKeyId,
|
|
326
|
+
derivationScheme : key.derivationScheme,
|
|
327
|
+
decrypt : async (
|
|
328
|
+
fullDerivationPath: string[],
|
|
329
|
+
jwePayload: { ephemeralPublicKey: Jwk; encryptedKey: Uint8Array },
|
|
330
|
+
): Promise<Uint8Array> => {
|
|
331
|
+
const keyPath = key.derivationPath ?? [];
|
|
332
|
+
if (keyPath.length !== fullDerivationPath.length ||
|
|
333
|
+
!keyPath.every((seg: string, i: number) => seg === fullDerivationPath[i])) {
|
|
334
|
+
throw new Error(
|
|
335
|
+
'Delegate decryption key is out of scope for this protocol path. ' +
|
|
336
|
+
`Key path: [${keyPath.join(', ')}], ` +
|
|
337
|
+
`record path: [${fullDerivationPath.join(', ')}].`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
const leafPrivateKeyBytes = await Records.derivePrivateKey(key, fullDerivationPath);
|
|
341
|
+
const leafPrivateKeyJwk = await X25519.bytesToPrivateKey({ privateKeyBytes: leafPrivateKeyBytes });
|
|
342
|
+
return Encryption.ecdhEsUnwrapKey(leafPrivateKeyJwk, jwePayload.ephemeralPublicKey, jwePayload.encryptedKey);
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
298
347
|
/**
|
|
299
348
|
* Resolves the appropriate KeyDecrypter for a record's encryption scheme.
|
|
300
349
|
* Handles both single-party (ProtocolPath) and multi-party (ProtocolContext).
|
|
301
350
|
*
|
|
351
|
+
* For ProtocolPath records:
|
|
352
|
+
* - Owner: derives key directly from KMS
|
|
353
|
+
* - Delegate with protocol-wide key: uses ancestor-prefix derivation
|
|
354
|
+
* - Delegate with exact-path key: enforces exact path match
|
|
355
|
+
*
|
|
302
356
|
* For ProtocolContext records:
|
|
357
|
+
* - Delegate: uses delivered context key from the connect flow
|
|
303
358
|
* - Context creator: derives key directly from KMS
|
|
304
359
|
* - Participant: fetches contextKey via key-delivery protocol, caches it
|
|
305
360
|
*
|
|
@@ -309,6 +364,8 @@ export function buildContextKeyDecrypter(
|
|
|
309
364
|
* @param targetDid - The target DID (DWN owner), if known
|
|
310
365
|
* @param contextDerivedKeyCache - Cache for context-derived private keys
|
|
311
366
|
* @param fetchContextKeyRecordFn - Function to fetch context key records from key-delivery protocol
|
|
367
|
+
* @param delegateDecryptionKeyCache - Cache for scope-aware delegate decryption keys
|
|
368
|
+
* @param granteeDid - The delegate DID (if this is a delegated request)
|
|
312
369
|
*/
|
|
313
370
|
export async function resolveKeyDecrypter(
|
|
314
371
|
agent: EnboxPlatformAgent,
|
|
@@ -322,6 +379,9 @@ export async function resolveKeyDecrypter(
|
|
|
322
379
|
sourceProtocol: string;
|
|
323
380
|
sourceContextId: string;
|
|
324
381
|
}) => Promise<DerivedPrivateJwk | undefined>,
|
|
382
|
+
delegateDecryptionKeyCache?: { get(key: string): DelegateDecryptionKeyEntry[] | undefined },
|
|
383
|
+
granteeDid?: string,
|
|
384
|
+
delegateContextKeyCache?: { get(key: string): DerivedPrivateJwk | undefined; set(key: string, value: DerivedPrivateJwk): void },
|
|
325
385
|
): Promise<KeyDecrypter> {
|
|
326
386
|
const { encryption } = recordsWrite;
|
|
327
387
|
|
|
@@ -331,7 +391,37 @@ export async function resolveKeyDecrypter(
|
|
|
331
391
|
);
|
|
332
392
|
|
|
333
393
|
if (!hasContextKey || !recordsWrite.contextId) {
|
|
334
|
-
// Single-party protocol-path encryption
|
|
394
|
+
// Single-party protocol-path encryption.
|
|
395
|
+
// Check for scope-aware delegate decryption keys first — this enables
|
|
396
|
+
// delegates to decrypt without the owner's root X25519 private key.
|
|
397
|
+
if (delegateDecryptionKeyCache && granteeDid) {
|
|
398
|
+
const protocol = recordsWrite.descriptor.protocol;
|
|
399
|
+
const protocolPath = recordsWrite.descriptor.protocolPath;
|
|
400
|
+
if (protocol) {
|
|
401
|
+
const cacheKey = `ddk~${granteeDid}`;
|
|
402
|
+
const allKeys = delegateDecryptionKeyCache.get(cacheKey);
|
|
403
|
+
if (allKeys) {
|
|
404
|
+
const keysForProtocol = allKeys.filter((k) => k.protocol === protocol);
|
|
405
|
+
|
|
406
|
+
// Priority 1: exact-path key matching this record's protocolPath.
|
|
407
|
+
if (protocolPath) {
|
|
408
|
+
const exactKey = keysForProtocol.find(
|
|
409
|
+
(k) => k.scope.kind === 'protocolPath' && k.scope.protocolPath === protocolPath
|
|
410
|
+
);
|
|
411
|
+
if (exactKey) {
|
|
412
|
+
return buildExactProtocolPathDecrypter(exactKey.derivedPrivateKey);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Priority 2: protocol-wide key (ancestor-prefix derivation).
|
|
417
|
+
const wideKey = keysForProtocol.find((k) => k.scope.kind === 'protocol');
|
|
418
|
+
if (wideKey) {
|
|
419
|
+
return buildContextKeyDecrypter(wideKey.derivedPrivateKey);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
335
425
|
return getKeyDecrypter(agent, authorDid);
|
|
336
426
|
}
|
|
337
427
|
|
|
@@ -342,6 +432,60 @@ export async function resolveKeyDecrypter(
|
|
|
342
432
|
|
|
343
433
|
const rootContextId = recordsWrite.contextId.split('/')[0];
|
|
344
434
|
|
|
435
|
+
// Case 0: Delegate with a delivered context key for this rootContextId.
|
|
436
|
+
// First check the in-memory cache (same-process delivery).
|
|
437
|
+
// On cache miss, try to fetch a contextKey record from the owner's DWN
|
|
438
|
+
// (cross-device delivery via the key-delivery protocol).
|
|
439
|
+
//
|
|
440
|
+
// IMPORTANT: If this is a delegated request (granteeDid is set), we must
|
|
441
|
+
// NOT fall through to Cases 1/2 which use authorDid (the owner). Delegates
|
|
442
|
+
// must decrypt via their own delivered context key — never via the owner's
|
|
443
|
+
// KMS. This is fail-closed by design.
|
|
444
|
+
if (delegateContextKeyCache && granteeDid) {
|
|
445
|
+
const protocol = recordsWrite.descriptor.protocol;
|
|
446
|
+
if (protocol) {
|
|
447
|
+
const ctxCacheKey = `dctx~${granteeDid}~${protocol}~${rootContextId}`;
|
|
448
|
+
const delegateCtxKey = delegateContextKeyCache.get(ctxCacheKey);
|
|
449
|
+
if (delegateCtxKey) {
|
|
450
|
+
return buildContextKeyDecrypter(delegateCtxKey);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Cache miss — try fetching a delivered contextKey record.
|
|
454
|
+
// The owner may have written one addressed to this delegate
|
|
455
|
+
// after the initial connect (cross-device delivery).
|
|
456
|
+
//
|
|
457
|
+
// For cross-device (ownerDid !== requesterDid), fetchContextKeyRecord
|
|
458
|
+
// queries the owner's tenant on the delegate's local DWN via
|
|
459
|
+
// processRequest. The delegate identity's connectedDid metadata
|
|
460
|
+
// registers ownerDid as locally-managed, so the query routes locally
|
|
461
|
+
// (in-process node or local-server RPC) — not to the owner's remote
|
|
462
|
+
// endpoint. Sync must have brought the contextKey record locally.
|
|
463
|
+
try {
|
|
464
|
+
const fetchedKey = await fetchContextKeyRecordFn({
|
|
465
|
+
ownerDid : authorDid,
|
|
466
|
+
requesterDid : granteeDid,
|
|
467
|
+
sourceProtocol : protocol,
|
|
468
|
+
sourceContextId : rootContextId,
|
|
469
|
+
});
|
|
470
|
+
if (fetchedKey) {
|
|
471
|
+
delegateContextKeyCache.set(ctxCacheKey, fetchedKey);
|
|
472
|
+
return buildContextKeyDecrypter(fetchedKey);
|
|
473
|
+
}
|
|
474
|
+
} catch {
|
|
475
|
+
// Delegate fetch failed — fail closed below.
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Delegate path exhausted: no cached key, no delivered record.
|
|
480
|
+
// Fail closed — do NOT fall through to the owner KMS path.
|
|
481
|
+
throw new Error(
|
|
482
|
+
`AgentDwnApi: Delegate '${granteeDid}' does not have a context key ` +
|
|
483
|
+
`for context '${rootContextId}'. No delivered contextKey record was ` +
|
|
484
|
+
`found via the key-delivery protocol. The delegate may need to ` +
|
|
485
|
+
`reconnect or wait for sync.`
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
345
489
|
// Case 1: I am the context creator — rootKeyId matches my encryption key
|
|
346
490
|
const { keyId, keyUri } = await getEncryptionKeyInfo(agent, authorDid);
|
|
347
491
|
if (contextKeyEntry.header.kid === keyId) {
|
|
@@ -405,6 +549,7 @@ export async function resolveKeyDecrypter(
|
|
|
405
549
|
* @param agent - The platform agent
|
|
406
550
|
* @param contextDerivedKeyCache - Cache for context-derived private keys
|
|
407
551
|
* @param fetchContextKeyRecordFn - Function to fetch context key records
|
|
552
|
+
* @param delegateDecryptionKeyCache - Cache for scope-aware delegate decryption keys
|
|
408
553
|
*/
|
|
409
554
|
export async function maybeDecryptReply<T extends DwnInterface>(
|
|
410
555
|
request: ProcessDwnRequest<T> | SendDwnRequest<T>,
|
|
@@ -417,6 +562,8 @@ export async function maybeDecryptReply<T extends DwnInterface>(
|
|
|
417
562
|
sourceProtocol: string;
|
|
418
563
|
sourceContextId: string;
|
|
419
564
|
}) => Promise<DerivedPrivateJwk | undefined>,
|
|
565
|
+
delegateDecryptionKeyCache?: { get(key: string): DelegateDecryptionKeyEntry[] | undefined },
|
|
566
|
+
delegateContextKeyCache?: { get(key: string): DerivedPrivateJwk | undefined; set(key: string, value: DerivedPrivateJwk): void },
|
|
420
567
|
): Promise<void> {
|
|
421
568
|
if (!('encryption' in request) || !request.encryption) {
|
|
422
569
|
return;
|
|
@@ -430,7 +577,8 @@ export async function maybeDecryptReply<T extends DwnInterface>(
|
|
|
430
577
|
&& readReply.entry?.data) {
|
|
431
578
|
const keyDecrypter = await resolveKeyDecrypter(
|
|
432
579
|
agent, request.author, readReply.entry.recordsWrite, request.target,
|
|
433
|
-
contextDerivedKeyCache, fetchContextKeyRecordFn,
|
|
580
|
+
contextDerivedKeyCache, fetchContextKeyRecordFn, delegateDecryptionKeyCache,
|
|
581
|
+
(request as any).granteeDid, delegateContextKeyCache,
|
|
434
582
|
);
|
|
435
583
|
|
|
436
584
|
try {
|
|
@@ -457,7 +605,8 @@ export async function maybeDecryptReply<T extends DwnInterface>(
|
|
|
457
605
|
if (entry.encryption && entry.encodedData) {
|
|
458
606
|
const keyDecrypter = await resolveKeyDecrypter(
|
|
459
607
|
agent, request.author, entry as RecordsWriteMessage, request.target,
|
|
460
|
-
contextDerivedKeyCache, fetchContextKeyRecordFn,
|
|
608
|
+
contextDerivedKeyCache, fetchContextKeyRecordFn, delegateDecryptionKeyCache,
|
|
609
|
+
(request as any).granteeDid, delegateContextKeyCache,
|
|
461
610
|
);
|
|
462
611
|
|
|
463
612
|
try {
|
package/src/dwn-key-delivery.ts
CHANGED
|
@@ -2,7 +2,6 @@ import type { PublicKeyJwk } from '@enbox/crypto';
|
|
|
2
2
|
import type {
|
|
3
3
|
DerivedPrivateJwk,
|
|
4
4
|
EncryptionInput,
|
|
5
|
-
RecordsQueryReply,
|
|
6
5
|
RecordsReadReply,
|
|
7
6
|
} from '@enbox/dwn-sdk-js';
|
|
8
7
|
|
|
@@ -19,12 +18,12 @@ import {
|
|
|
19
18
|
KeyDerivationScheme,
|
|
20
19
|
Message,
|
|
21
20
|
Protocols,
|
|
22
|
-
Records,
|
|
23
21
|
} from '@enbox/dwn-sdk-js';
|
|
24
22
|
|
|
23
|
+
import { DwnInterface } from './types/dwn.js';
|
|
25
24
|
import { KeyDeliveryProtocolDefinition } from './store-data-protocols.js';
|
|
26
|
-
|
|
27
|
-
import {
|
|
25
|
+
|
|
26
|
+
import { buildEncryptionInput, encryptAndComputeCid, getEncryptionKeyDeriver, ivLength } from './dwn-encryption.js';
|
|
28
27
|
|
|
29
28
|
/**
|
|
30
29
|
* Parameters for writeContextKeyRecord.
|
|
@@ -255,6 +254,59 @@ export async function eagerSendContextKeyRecord(
|
|
|
255
254
|
});
|
|
256
255
|
}
|
|
257
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Fetches a contextKey record from the owner's tenant via `processRequest`.
|
|
259
|
+
*
|
|
260
|
+
* This works in both agent modes:
|
|
261
|
+
* - In-process DWN node: `processRequest` routes locally via the DWN instance
|
|
262
|
+
* - Remote/local-server mode: `processRequest` routes via RPC to the local DWN server
|
|
263
|
+
*
|
|
264
|
+
* The delegate identity's `connectedDid` metadata registers `ownerDid` as a
|
|
265
|
+
* locally-managed DID, so `processRequest` routes to the local DWN (not the
|
|
266
|
+
* owner's remote endpoint).
|
|
267
|
+
*
|
|
268
|
+
* Requires sync to have brought the contextKey record to the owner's tenant
|
|
269
|
+
* on the delegate's DWN before this is called.
|
|
270
|
+
*/
|
|
271
|
+
async function fetchCrossDeviceContextKey(
|
|
272
|
+
processRequest: ProcessRequestFn,
|
|
273
|
+
requesterDid: string,
|
|
274
|
+
ownerDid: string,
|
|
275
|
+
contextKeyFilter: Record<string, any>,
|
|
276
|
+
parsePayload: (bytes: Uint8Array) => DerivedPrivateJwk,
|
|
277
|
+
): Promise<DerivedPrivateJwk | undefined> {
|
|
278
|
+
try {
|
|
279
|
+
const { reply } = await processRequest({
|
|
280
|
+
author : requesterDid,
|
|
281
|
+
target : ownerDid,
|
|
282
|
+
messageType : DwnInterface.RecordsQuery,
|
|
283
|
+
messageParams : { filter: contextKeyFilter },
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
if (reply.status.code !== 200 || !reply.entries?.length) {
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const recordId = reply.entries[0].recordId;
|
|
291
|
+
const { reply: readReply } = await processRequest({
|
|
292
|
+
author : requesterDid,
|
|
293
|
+
target : ownerDid,
|
|
294
|
+
messageType : DwnInterface.RecordsRead,
|
|
295
|
+
messageParams : { filter: { recordId } },
|
|
296
|
+
encryption : true,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const readResult = readReply as RecordsReadReply;
|
|
300
|
+
if (!readResult.entry?.data) {
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return parsePayload(await DataStream.toBytes(readResult.entry.data));
|
|
305
|
+
} catch {
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
258
310
|
/**
|
|
259
311
|
* Fetches and decrypts a `contextKey` record from a DWN, returning the
|
|
260
312
|
* `DerivedPrivateJwk` payload.
|
|
@@ -265,18 +317,12 @@ export async function eagerSendContextKeyRecord(
|
|
|
265
317
|
* @param agent - The platform agent
|
|
266
318
|
* @param params - The fetch parameters
|
|
267
319
|
* @param processRequest - The agent's processRequest method (bound)
|
|
268
|
-
* @param getSigner - Function to get a signer for a DID
|
|
269
|
-
* @param sendDwnRpcRequest - Function to send a DWN RPC request
|
|
270
|
-
* @param getDwnEndpointUrlsForTarget - Function to resolve DWN endpoint URLs (with local discovery)
|
|
271
320
|
* @returns The decrypted `DerivedPrivateJwk`, or `undefined` if no matching record found
|
|
272
321
|
*/
|
|
273
322
|
export async function fetchContextKeyRecord(
|
|
274
|
-
|
|
323
|
+
_agent: EnboxPlatformAgent,
|
|
275
324
|
params: FetchContextKeyParams,
|
|
276
325
|
processRequest: ProcessRequestFn,
|
|
277
|
-
getSigner: (author: string) => Promise<any>,
|
|
278
|
-
sendDwnRpcRequest: (params: { targetDid: string; dwnEndpointUrls: string[]; message: any; data?: Blob }) => Promise<any>,
|
|
279
|
-
getDwnEndpointUrlsForTarget: (targetDid: string) => Promise<string[]>,
|
|
280
326
|
): Promise<DerivedPrivateJwk | undefined> {
|
|
281
327
|
const { ownerDid, requesterDid, sourceProtocol, sourceContextId } = params;
|
|
282
328
|
const protocolUri = KeyDeliveryProtocolDefinition.protocol;
|
|
@@ -295,7 +341,7 @@ export async function fetchContextKeyRecord(
|
|
|
295
341
|
JSON.parse(new TextDecoder().decode(bytes)) as DerivedPrivateJwk;
|
|
296
342
|
|
|
297
343
|
if (isLocal) {
|
|
298
|
-
// Local query: owner queries their own DWN
|
|
344
|
+
// Local query via processRequest: owner queries their own DWN.
|
|
299
345
|
const { reply } = await processRequest({
|
|
300
346
|
author : requesterDid,
|
|
301
347
|
target : ownerDid,
|
|
@@ -307,13 +353,12 @@ export async function fetchContextKeyRecord(
|
|
|
307
353
|
return undefined;
|
|
308
354
|
}
|
|
309
355
|
|
|
310
|
-
|
|
311
|
-
const recordId = reply.entries[0].recordId;
|
|
356
|
+
const localRecordId = reply.entries[0].recordId;
|
|
312
357
|
const { reply: readReply } = await processRequest({
|
|
313
358
|
author : requesterDid,
|
|
314
359
|
target : ownerDid,
|
|
315
360
|
messageType : DwnInterface.RecordsRead,
|
|
316
|
-
messageParams : { filter: { recordId } },
|
|
361
|
+
messageParams : { filter: { recordId: localRecordId } },
|
|
317
362
|
encryption : true,
|
|
318
363
|
});
|
|
319
364
|
|
|
@@ -323,51 +368,18 @@ export async function fetchContextKeyRecord(
|
|
|
323
368
|
}
|
|
324
369
|
|
|
325
370
|
return parsePayload(await DataStream.toBytes(readResult.entry.data));
|
|
326
|
-
} else {
|
|
327
|
-
// Remote query: participant queries the context owner's DWN
|
|
328
|
-
const signer = await getSigner(requesterDid);
|
|
329
|
-
const dwnEndpointUrls = await getDwnEndpointUrlsForTarget(ownerDid);
|
|
330
|
-
|
|
331
|
-
const recordsQuery = await dwnMessageConstructors[DwnInterface.RecordsQuery].create({
|
|
332
|
-
signer,
|
|
333
|
-
filter: contextKeyFilter,
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
const queryReply = await sendDwnRpcRequest({
|
|
337
|
-
targetDid : ownerDid,
|
|
338
|
-
dwnEndpointUrls,
|
|
339
|
-
message : recordsQuery.message,
|
|
340
|
-
}) as RecordsQueryReply;
|
|
341
|
-
|
|
342
|
-
if (queryReply.status.code !== 200 || !queryReply.entries?.length) {
|
|
343
|
-
return undefined;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Read the full record remotely
|
|
347
|
-
const recordId = queryReply.entries[0].recordId;
|
|
348
|
-
const recordsRead = await dwnMessageConstructors[DwnInterface.RecordsRead].create({
|
|
349
|
-
signer,
|
|
350
|
-
filter: { recordId },
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
const readReply = await sendDwnRpcRequest({
|
|
354
|
-
targetDid : ownerDid,
|
|
355
|
-
dwnEndpointUrls,
|
|
356
|
-
message : recordsRead.message,
|
|
357
|
-
}) as RecordsReadReply;
|
|
358
|
-
|
|
359
|
-
if (!readReply.entry?.data || !readReply.entry?.recordsWrite) {
|
|
360
|
-
return undefined;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Decrypt the contextKey payload using the requester's key-delivery protocol path key
|
|
364
|
-
const keyDecrypter = await getKeyDecrypter(agent, requesterDid);
|
|
365
|
-
const decryptedStream = await Records.decrypt(
|
|
366
|
-
readReply.entry.recordsWrite,
|
|
367
|
-
keyDecrypter,
|
|
368
|
-
readReply.entry.data as ReadableStream<Uint8Array>,
|
|
369
|
-
);
|
|
370
|
-
|
|
371
|
-
return parsePayload(await DataStream.toBytes(decryptedStream));
|
|
372
371
|
}
|
|
372
|
+
|
|
373
|
+
// Cross-device path: the requester is NOT the owner.
|
|
374
|
+
// Query the owner's tenant on the delegate's DWN via processRequest.
|
|
375
|
+
// The delegate identity's connectedDid metadata registers ownerDid as
|
|
376
|
+
// locally-managed, so processRequest routes locally (in-process or
|
|
377
|
+
// local-server RPC) rather than to the owner's remote DWN endpoint.
|
|
378
|
+
//
|
|
379
|
+
// Sync must have brought the contextKey record to the owner's tenant
|
|
380
|
+
// on the delegate's DWN before this is called.
|
|
381
|
+
// If fetch fails, the caller's fail-closed guard will fire.
|
|
382
|
+
return fetchCrossDeviceContextKey(
|
|
383
|
+
processRequest, requesterDid, ownerDid, contextKeyFilter, parsePayload,
|
|
384
|
+
);
|
|
373
385
|
}
|