@enbox/agent 0.5.16 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/browser.mjs +11 -11
  2. package/dist/browser.mjs.map +4 -4
  3. package/dist/esm/dwn-api.js +468 -36
  4. package/dist/esm/dwn-api.js.map +1 -1
  5. package/dist/esm/dwn-encryption.js +131 -12
  6. package/dist/esm/dwn-encryption.js.map +1 -1
  7. package/dist/esm/dwn-key-delivery.js +64 -47
  8. package/dist/esm/dwn-key-delivery.js.map +1 -1
  9. package/dist/esm/dwn-protocol-cache.js +8 -6
  10. package/dist/esm/dwn-protocol-cache.js.map +1 -1
  11. package/dist/esm/enbox-connect-protocol.js +400 -3
  12. package/dist/esm/enbox-connect-protocol.js.map +1 -1
  13. package/dist/esm/permissions-api.js +11 -1
  14. package/dist/esm/permissions-api.js.map +1 -1
  15. package/dist/esm/sync-engine-level.js +407 -6
  16. package/dist/esm/sync-engine-level.js.map +1 -1
  17. package/dist/esm/sync-messages.js +10 -3
  18. package/dist/esm/sync-messages.js.map +1 -1
  19. package/dist/types/dwn-api.d.ts +159 -0
  20. package/dist/types/dwn-api.d.ts.map +1 -1
  21. package/dist/types/dwn-encryption.d.ts +39 -2
  22. package/dist/types/dwn-encryption.d.ts.map +1 -1
  23. package/dist/types/dwn-key-delivery.d.ts +1 -9
  24. package/dist/types/dwn-key-delivery.d.ts.map +1 -1
  25. package/dist/types/dwn-protocol-cache.d.ts +1 -1
  26. package/dist/types/dwn-protocol-cache.d.ts.map +1 -1
  27. package/dist/types/enbox-connect-protocol.d.ts +166 -1
  28. package/dist/types/enbox-connect-protocol.d.ts.map +1 -1
  29. package/dist/types/permissions-api.d.ts.map +1 -1
  30. package/dist/types/sync-engine-level.d.ts +45 -1
  31. package/dist/types/sync-engine-level.d.ts.map +1 -1
  32. package/dist/types/sync-messages.d.ts +2 -2
  33. package/dist/types/sync-messages.d.ts.map +1 -1
  34. package/dist/types/types/permissions.d.ts +9 -0
  35. package/dist/types/types/permissions.d.ts.map +1 -1
  36. package/dist/types/types/sync.d.ts +70 -2
  37. package/dist/types/types/sync.d.ts.map +1 -1
  38. package/package.json +5 -4
  39. package/src/dwn-api.ts +530 -39
  40. package/src/dwn-encryption.ts +160 -11
  41. package/src/dwn-key-delivery.ts +73 -61
  42. package/src/dwn-protocol-cache.ts +9 -1
  43. package/src/enbox-connect-protocol.ts +575 -6
  44. package/src/permissions-api.ts +13 -1
  45. package/src/sync-engine-level.ts +368 -4
  46. package/src/sync-messages.ts +14 -5
  47. package/src/types/permissions.ts +9 -0
  48. package/src/types/sync.ts +86 -2
@@ -7,10 +7,11 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
- import { TtlCache } from '@enbox/common';
11
10
  import { Cid, ContentEncryptionAlgorithm, DataStoreLevel, DataStream, Dwn, DwnMethodName, EventEmitterEventLog, Jws, KeyDerivationScheme, Message, MessageStoreLevel, Protocols, Records, ResumableTaskStoreLevel, StateIndexLevel, } from '@enbox/dwn-sdk-js';
11
+ import { Convert, TtlCache } from '@enbox/common';
12
12
  import { CryptoUtils, X25519 } from '@enbox/crypto';
13
13
  import { DidDht, DidJwk, DidResolverCacheLevel, UniversalResolver } from '@enbox/dids';
14
+ import { AgentPermissionsApi } from './permissions-api.js';
14
15
  import { DwnDiscoveryFile } from './dwn-discovery-file.js';
15
16
  import { KeyDeliveryProtocolDefinition } from './store-data-protocols.js';
16
17
  import { LocalDwnDiscovery } from './local-dwn.js';
@@ -53,6 +54,41 @@ export class AgentDwnApi {
53
54
  this._contextDerivedKeyCache = new TtlCache({
54
55
  ttl: 30 * 60 * 1000
55
56
  });
57
+ /**
58
+ * Delegate decryption key cache — stores scope-aware decryption keys
59
+ * delivered to delegates during the connect flow. These keys enable
60
+ * delegates to decrypt encrypted records without possessing the owner's
61
+ * root X25519 private key.
62
+ *
63
+ * Keyed by `ddk~${delegateDid}`. Each entry is an array covering all
64
+ * granted read scopes for that delegate session.
65
+ * TTL 24 hours (keys are re-populated on session restore).
66
+ */
67
+ this._delegateDecryptionKeyCache = new TtlCache({
68
+ ttl: 24 * 60 * 60 * 1000
69
+ });
70
+ /**
71
+ * Delegate context key cache — stores ProtocolContext decryption keys for
72
+ * multi-party encrypted protocols. Each key is scoped to one rootContextId.
73
+ * Keyed by `dctx~${delegateDid}~${protocol}~${rootContextId}`.
74
+ * TTL 24 hours (re-populated on session restore).
75
+ */
76
+ this._delegateContextKeyCache = new TtlCache({
77
+ ttl: 24 * 60 * 60 * 1000
78
+ });
79
+ /** Tracks which context key cache entries belong to which delegate DID. */
80
+ this._delegateContextKeyCacheIndex = new Map();
81
+ /**
82
+ * Explicit registry of which multi-party protocols each delegate has
83
+ * protocol-wide read-like access to. Populated at connect time (even
84
+ * when zero contexts exist) and used by postWriteKeyDelivery() to
85
+ * decide whether to deliver new context keys.
86
+ *
87
+ * Keyed by delegateDid. Each entry is a Set of protocol URIs.
88
+ * Unlike the context key cache, this registry is NOT time-limited —
89
+ * it persists for the lifetime of the session.
90
+ */
91
+ this._delegateMultiPartyProtocols = new Map();
56
92
  /**
57
93
  * Cache of locally-managed DIDs (agent DID + identities). Used to decide
58
94
  * whether a target DID should be routed through the local DWN server.
@@ -174,6 +210,16 @@ export class AgentDwnApi {
174
210
  return [...uniqueEndpoints];
175
211
  });
176
212
  }
213
+ /**
214
+ * Returns only the DWN service endpoints from the DID document (no local
215
+ * discovery endpoint). Use this when you need to confirm that a message
216
+ * reached the owner's actual remote DWN, not just the delegate's local server.
217
+ */
218
+ getRemoteDwnEndpointUrls(targetDid) {
219
+ return __awaiter(this, void 0, void 0, function* () {
220
+ return getDwnServiceEndpointUrls(targetDid, this.agent.did);
221
+ });
222
+ }
177
223
  /** Lazily retrieves the local DWN server endpoint via discovery. */
178
224
  getLocalDwnEndpoint() {
179
225
  return __awaiter(this, void 0, void 0, function* () {
@@ -490,11 +536,17 @@ export class AgentDwnApi {
490
536
  tenantDid: request.target,
491
537
  authorDid: isExternallyAuthored ? authorDid : undefined,
492
538
  });
493
- if (newParticipants.size > 0) {
494
- // Derive the context key to deliver to participants
495
- const rootContextId = ((_a = recordsWriteMessage.contextId) === null || _a === void 0 ? void 0 : _a.split('/')[0])
496
- || recordsWriteMessage.contextId
497
- || recordsWriteMessage.recordId;
539
+ // Compute rootContextId for both participant and delegate delivery.
540
+ const rootContextId = ((_a = recordsWriteMessage.contextId) === null || _a === void 0 ? void 0 : _a.split('/')[0])
541
+ || recordsWriteMessage.contextId
542
+ || recordsWriteMessage.recordId;
543
+ // Determine if delegate delivery is needed: new multi-party root
544
+ // record created by the owner with active delegate sessions.
545
+ const needsDelegateDelivery = isMultiParty && isRootRecord
546
+ && !isExternallyAuthored
547
+ && this.hasEligibleDelegatesForProtocol(writeParams.protocol);
548
+ if (newParticipants.size > 0 || needsDelegateDelivery) {
549
+ // Derive the context key once (shared for participant + delegate delivery).
498
550
  const { keyId, keyUri } = yield getEncryptionKeyInfoFn(this.agent, request.target);
499
551
  const contextDerivationPath = [
500
552
  KeyDerivationScheme.ProtocolContext,
@@ -511,32 +563,66 @@ export class AgentDwnApi {
511
563
  derivationPath: contextDerivationPath,
512
564
  derivedPrivateKey: contextDerivedPrivateJwk,
513
565
  };
514
- // Extract the author's key delivery public key from the record
515
- // so we can encrypt the contextKey directly to the external author.
516
- const authorKeyDeliveryPubKey = (_b = recordsWriteMessage.authorization) === null || _b === void 0 ? void 0 : _b.authorKeyDeliveryPublicKey;
517
- for (const participantDid of newParticipants) {
518
- try {
519
- // Use the author's key delivery public key when delivering
520
- // to the external author; for other participants (e.g.
521
- // recipient, role holders) fall back to owner-key encryption.
522
- const recipientKey = (participantDid === authorDid && authorKeyDeliveryPubKey)
523
- ? authorKeyDeliveryPubKey
524
- : undefined;
525
- yield this.writeContextKeyRecord({
526
- tenantDid: request.target,
527
- recipientDid: participantDid,
528
- contextKeyData: contextKeyPayload,
529
- sourceProtocol: writeParams.protocol,
530
- sourceContextId: rootContextId,
531
- recipientKeyDeliveryPublicKey: recipientKey,
532
- });
533
- }
534
- catch (keyDeliveryError) {
535
- console.warn(`AgentDwnApi: Key delivery to '${participantDid}' for context ` +
536
- `'${rootContextId}' failed: ${keyDeliveryError.message}. ` +
537
- `The participant may not be able to decrypt records in this context.`);
566
+ // --- Participant key delivery (existing) ---
567
+ if (newParticipants.size > 0) {
568
+ // Extract the author's key delivery public key from the record
569
+ // so we can encrypt the contextKey directly to the external author.
570
+ const authorKeyDeliveryPubKey = (_b = recordsWriteMessage.authorization) === null || _b === void 0 ? void 0 : _b.authorKeyDeliveryPublicKey;
571
+ for (const participantDid of newParticipants) {
572
+ try {
573
+ // Use the author's key delivery public key when delivering
574
+ // to the external author; for other participants (e.g.
575
+ // recipient, role holders) fall back to owner-key encryption.
576
+ const recipientKey = (participantDid === authorDid && authorKeyDeliveryPubKey)
577
+ ? authorKeyDeliveryPubKey
578
+ : undefined;
579
+ yield this.writeContextKeyRecord({
580
+ tenantDid: request.target,
581
+ recipientDid: participantDid,
582
+ contextKeyData: contextKeyPayload,
583
+ sourceProtocol: writeParams.protocol,
584
+ sourceContextId: rootContextId,
585
+ recipientKeyDeliveryPublicKey: recipientKey,
586
+ });
587
+ }
588
+ catch (keyDeliveryError) {
589
+ console.warn(`AgentDwnApi: Key delivery to '${participantDid}' for context ` +
590
+ `'${rootContextId}' failed: ${keyDeliveryError.message}. ` +
591
+ `The participant may not be able to decrypt records in this context.`);
592
+ }
538
593
  }
539
594
  }
595
+ // --- Post-connect delegate context key delivery (#824) ---
596
+ // Same-process: direct cache injection (fast, no network).
597
+ if (needsDelegateDelivery) {
598
+ this.deliverContextKeyToDelegates(writeParams.protocol, rootContextId, contextKeyPayload);
599
+ }
600
+ }
601
+ // --- Cross-device delegate context key delivery (#826) ---
602
+ // Query grants to find ALL eligible delegates (including those on
603
+ // other agents) and write contextKey records to the DWN.
604
+ // This is separate from same-process delivery: it discovers delegates
605
+ // from the owner's DWN grants, not just in-memory caches. It must
606
+ // run even when no same-process delegates or participants exist.
607
+ if (isMultiParty && isRootRecord && !isExternallyAuthored) {
608
+ // Derive the context key if not already done above.
609
+ const { keyId: xdKeyId, keyUri: xdKeyUri } = yield getEncryptionKeyInfoFn(this.agent, request.target);
610
+ const xdContextDerivationPath = [
611
+ KeyDerivationScheme.ProtocolContext,
612
+ rootContextId,
613
+ ];
614
+ const xdContextDerivedPrivateKeyBytes = yield this.agent.keyManager.derivePrivateKeyBytes({
615
+ keyUri: xdKeyUri,
616
+ derivationPath: xdContextDerivationPath,
617
+ });
618
+ const xdContextDerivedPrivateJwk = yield X25519.bytesToPrivateKey({ privateKeyBytes: xdContextDerivedPrivateKeyBytes });
619
+ const xdContextKeyPayload = {
620
+ rootKeyId: xdKeyId,
621
+ derivationScheme: KeyDerivationScheme.ProtocolContext,
622
+ derivationPath: xdContextDerivationPath,
623
+ derivedPrivateKey: xdContextDerivedPrivateJwk,
624
+ };
625
+ yield this.deliverContextKeyToDelegatesViaDwn(request.target, writeParams.protocol, rootContextId, xdContextKeyPayload);
540
626
  }
541
627
  }
542
628
  catch (detectionError) {
@@ -596,6 +682,7 @@ export class AgentDwnApi {
596
682
  }
597
683
  constructDwnMessage(_a) {
598
684
  return __awaiter(this, arguments, void 0, function* ({ request }) {
685
+ var _b, _c, _d;
599
686
  // if the request has a granteeDid, ensure the messageParams include the proper grant parameters
600
687
  if (request.granteeDid && !this.hasGrantParams(request.messageParams)) {
601
688
  throw new Error('AgentDwnApi: Requested to sign with a permission but no grant messageParams were provided in the request');
@@ -642,6 +729,19 @@ export class AgentDwnApi {
642
729
  // Invalidate cache for this protocol
643
730
  this._protocolDefinitionCache.delete(`${request.target}~${messageParams.definition.protocol}`);
644
731
  }
732
+ // When a ProtocolsConfigure is processed WITHOUT the encryption flag
733
+ // (e.g. a delegate installing the owner's protocol definition that
734
+ // already contains `$encryption` keys from the remote DWN), cache the
735
+ // definition so that subsequent RecordsWrite encryption can find it
736
+ // without re-querying the local DWN (which would fail for delegates
737
+ // because the query author doesn't match the unpublished protocol's
738
+ // tenant).
739
+ if (isDwnRequest(request, DwnInterface.ProtocolsConfigure) && !request.encryption && !rawMessage) {
740
+ const def = (_b = request.messageParams) === null || _b === void 0 ? void 0 : _b.definition;
741
+ if (def === null || def === void 0 ? void 0 : def.protocol) {
742
+ this._protocolDefinitionCache.set(`${request.target}~${def.protocol}`, def);
743
+ }
744
+ }
645
745
  // Auto-encrypt data on RecordsWrite.
646
746
  //
647
747
  // Encryption scheme decision (unified key delivery):
@@ -680,7 +780,7 @@ export class AgentDwnApi {
680
780
  protocolDefinition = yield this.fetchRemoteProtocolDefinition(request.target, messageParams.protocol);
681
781
  }
682
782
  else {
683
- protocolDefinition = yield this.getProtocolDefinition(request.target, messageParams.protocol);
783
+ protocolDefinition = yield this.getProtocolDefinition(request.target, messageParams.protocol, request.granteeDid);
684
784
  }
685
785
  if (!protocolDefinition) {
686
786
  throw new Error(`AgentDwnApi: Protocol '${messageParams.protocol}' is not installed ` +
@@ -832,7 +932,29 @@ export class AgentDwnApi {
832
932
  const signer = request.granteeDid ?
833
933
  yield this.getSigner(request.granteeDid) :
834
934
  yield this.getSigner(request.author);
835
- dwnMessage = yield dwnMessageConstructor.create(Object.assign(Object.assign({}, request.messageParams), { signer }));
935
+ // When signing as a delegate with a permissionGrantId, fetch the full
936
+ // grant message and pass it as `delegatedGrant` so the DWN SDK correctly
937
+ // sets `authorization.authorDelegatedGrant` and resolves the logical
938
+ // author to the grantor (owner) rather than the signer (delegate).
939
+ const params = Object.assign({}, request.messageParams);
940
+ if (request.granteeDid && params.permissionGrantId && !params.delegatedGrant
941
+ && isDwnRequest(request, DwnInterface.RecordsWrite)) {
942
+ // Read as the grantee (delegate), not the owner. The delegate is
943
+ // the grant's recipient so the permissions protocol authorizes the
944
+ // read. The owner's signing key may not be available on the
945
+ // delegate agent in real wallet-connect flows.
946
+ const { reply: grantReply } = yield this.processRequest({
947
+ author: request.granteeDid,
948
+ target: request.author,
949
+ messageType: DwnInterface.RecordsRead,
950
+ messageParams: { filter: { recordId: params.permissionGrantId } },
951
+ });
952
+ if (grantReply.status.code === 200 && ((_c = grantReply.entry) === null || _c === void 0 ? void 0 : _c.recordsWrite) && ((_d = grantReply.entry) === null || _d === void 0 ? void 0 : _d.data)) {
953
+ const grantDataBytes = yield DataStream.toBytes(grantReply.entry.data);
954
+ params.delegatedGrant = Object.assign(Object.assign({}, grantReply.entry.recordsWrite), { encodedData: Convert.uint8Array(grantDataBytes).toBase64Url() });
955
+ }
956
+ }
957
+ dwnMessage = yield dwnMessageConstructor.create(Object.assign(Object.assign({}, params), { signer }));
836
958
  // Deferred context encryption for root multi-party records (Component 9).
837
959
  // Now that the message exists, we know recordId = contextId.
838
960
  // Following the SDK two-pass pattern: encryptSymmetricEncryptionKey -> sign.
@@ -977,7 +1099,7 @@ export class AgentDwnApi {
977
1099
  * @param protocolUri - The protocol URI to fetch
978
1100
  * @returns The protocol definition, or undefined if not found
979
1101
  */
980
- getProtocolDefinition(tenantDid, protocolUri) {
1102
+ getProtocolDefinition(tenantDid, protocolUri, granteeDid) {
981
1103
  return __awaiter(this, void 0, void 0, function* () {
982
1104
  if (!this._dwn) {
983
1105
  // Remote mode: query via RPC (same as fetchRemoteProtocolDefinition,
@@ -998,7 +1120,26 @@ export class AgentDwnApi {
998
1120
  throw error;
999
1121
  }
1000
1122
  }
1001
- return getProtocolDefinitionFn(tenantDid, protocolUri, this._dwn, this.getSigner.bind(this), this._protocolDefinitionCache);
1123
+ // When operating as a delegate, resolve the ProtocolsQuery grant so
1124
+ // the local DWN authorises the query for unpublished protocols.
1125
+ let permissionGrantId;
1126
+ if (granteeDid) {
1127
+ try {
1128
+ const permissionsApi = new AgentPermissionsApi({ agent: this.agent });
1129
+ const { grant } = yield permissionsApi.getPermissionForRequest({
1130
+ connectedDid: tenantDid,
1131
+ delegateDid: granteeDid,
1132
+ protocol: protocolUri,
1133
+ cached: true,
1134
+ messageType: DwnInterface.ProtocolsQuery,
1135
+ });
1136
+ permissionGrantId = grant.id;
1137
+ }
1138
+ catch (_a) {
1139
+ // No grant found — try without (works for published protocols).
1140
+ }
1141
+ }
1142
+ return getProtocolDefinitionFn(tenantDid, protocolUri, this._dwn, this.getSigner.bind(this), this._protocolDefinitionCache, granteeDid, permissionGrantId);
1002
1143
  });
1003
1144
  }
1004
1145
  /**
@@ -1033,7 +1174,7 @@ export class AgentDwnApi {
1033
1174
  */
1034
1175
  maybeDecryptReply(request, reply) {
1035
1176
  return __awaiter(this, void 0, void 0, function* () {
1036
- return maybeDecryptReplyFn(request, reply, this.agent, this._contextDerivedKeyCache, this.fetchContextKeyRecord.bind(this));
1177
+ return maybeDecryptReplyFn(request, reply, this.agent, this._contextDerivedKeyCache, this.fetchContextKeyRecord.bind(this), this._delegateDecryptionKeyCache, this._delegateContextKeyCache);
1037
1178
  });
1038
1179
  }
1039
1180
  getDwnMessage(_a) {
@@ -1088,6 +1229,297 @@ export class AgentDwnApi {
1088
1229
  return ensureKeyDeliveryProtocolFn(this.agent, tenantDid, this.processRequest.bind(this), this.getProtocolDefinition.bind(this), this._keyDeliveryProtocolInstalledCache, this._protocolDefinitionCache);
1089
1230
  });
1090
1231
  }
1232
+ /**
1233
+ * Imports scope-aware decryption keys for delegate sessions.
1234
+ *
1235
+ * Called during the connect flow when the wallet delivers decryption keys
1236
+ * for encrypted protocols. Keys are derived only for read-like scopes
1237
+ * (Read/Query/Subscribe) — write-only delegates receive no keys.
1238
+ *
1239
+ * The keys are cached and used by `resolveKeyDecrypter()` to decrypt
1240
+ * records when the delegate does not possess the owner's root X25519
1241
+ * private key.
1242
+ *
1243
+ * @param delegateDid - The delegate DID for this session (unique per connect)
1244
+ * @param keys - Array of scope-aware decryption key entries
1245
+ */
1246
+ /**
1247
+ * Sets a callback invoked whenever post-connect context keys are
1248
+ * delivered to a delegate. The auth layer uses this to persist
1249
+ * updated context keys so they survive restart.
1250
+ *
1251
+ * @param callback - Called with the delegateDid that received new keys.
1252
+ * Set to `undefined` to unregister.
1253
+ */
1254
+ set onDelegateContextKeysChanged(callback) {
1255
+ this._onDelegateContextKeysChanged = callback;
1256
+ }
1257
+ importDelegateDecryptionKeys(delegateDid, keys) {
1258
+ const cacheKey = `ddk~${delegateDid}`;
1259
+ this._delegateDecryptionKeyCache.set(cacheKey, keys);
1260
+ }
1261
+ /**
1262
+ * Imports ProtocolContext decryption keys for multi-party encrypted protocols.
1263
+ * Each key is scoped to one rootContextId within one protocol.
1264
+ *
1265
+ * @param delegateDid - The delegate DID for this session
1266
+ * @param keys - Array of `{ protocol, contextId, derivedPrivateKey }` entries
1267
+ */
1268
+ importDelegateContextKeys(delegateDid, keys, multiPartyProtocols) {
1269
+ // Clear any previously indexed entries for this delegate first,
1270
+ // so a re-import (e.g. session restore) doesn't leave stale entries.
1271
+ const previousKeys = this._delegateContextKeyCacheIndex.get(delegateDid);
1272
+ if (previousKeys) {
1273
+ for (const ck of previousKeys) {
1274
+ this._delegateContextKeyCache.delete(ck);
1275
+ }
1276
+ }
1277
+ const cacheKeys = [];
1278
+ for (const key of keys) {
1279
+ const ck = `dctx~${delegateDid}~${key.protocol}~${key.contextId}`;
1280
+ this._delegateContextKeyCache.set(ck, key.derivedPrivateKey);
1281
+ cacheKeys.push(ck);
1282
+ }
1283
+ this._delegateContextKeyCacheIndex.set(delegateDid, cacheKeys);
1284
+ // Populate the explicit multi-party protocol registry.
1285
+ // Sources: explicit parameter (always wins) + protocols from delivered keys.
1286
+ const protocols = new Set(multiPartyProtocols !== null && multiPartyProtocols !== void 0 ? multiPartyProtocols : []);
1287
+ for (const key of keys) {
1288
+ protocols.add(key.protocol);
1289
+ }
1290
+ if (protocols.size > 0) {
1291
+ this._delegateMultiPartyProtocols.set(delegateDid, protocols);
1292
+ }
1293
+ else {
1294
+ this._delegateMultiPartyProtocols.delete(delegateDid);
1295
+ }
1296
+ }
1297
+ /**
1298
+ * Clears all delegate decryption keys (both ProtocolPath and ProtocolContext)
1299
+ * from the in-memory cache. Called on disconnect to prevent stale keys from
1300
+ * persisting across sessions.
1301
+ *
1302
+ * @param delegateDid - If provided, clears keys for that delegate session only.
1303
+ * If omitted, clears all delegate keys.
1304
+ */
1305
+ clearDelegateDecryptionKeys(delegateDid) {
1306
+ if (delegateDid) {
1307
+ this._delegateDecryptionKeyCache.delete(`ddk~${delegateDid}`);
1308
+ // Delete only context keys belonging to this delegate.
1309
+ const cacheKeys = this._delegateContextKeyCacheIndex.get(delegateDid);
1310
+ if (cacheKeys) {
1311
+ for (const ck of cacheKeys) {
1312
+ this._delegateContextKeyCache.delete(ck);
1313
+ }
1314
+ this._delegateContextKeyCacheIndex.delete(delegateDid);
1315
+ }
1316
+ this._delegateMultiPartyProtocols.delete(delegateDid);
1317
+ }
1318
+ else {
1319
+ this._delegateDecryptionKeyCache.clear();
1320
+ this._delegateContextKeyCache.clear();
1321
+ this._delegateContextKeyCacheIndex.clear();
1322
+ this._delegateMultiPartyProtocols.clear();
1323
+ }
1324
+ }
1325
+ /**
1326
+ * Exports the current set of delegate context keys for a specific delegate.
1327
+ * Returns an array of `{ protocol, contextId, derivedPrivateKey }` entries
1328
+ * suitable for serialization and persistence.
1329
+ *
1330
+ * Called by the auth layer to persist context keys (including keys delivered
1331
+ * post-connect) so they survive agent restarts.
1332
+ *
1333
+ * @param delegateDid - The delegate DID whose context keys to export
1334
+ * @returns Array of context key entries (may be empty)
1335
+ */
1336
+ exportDelegateContextKeys(delegateDid) {
1337
+ const cacheKeys = this._delegateContextKeyCacheIndex.get(delegateDid);
1338
+ if (!cacheKeys) {
1339
+ return [];
1340
+ }
1341
+ const result = [];
1342
+ const prefix = `dctx~${delegateDid}~`;
1343
+ for (const ck of cacheKeys) {
1344
+ const key = this._delegateContextKeyCache.get(ck);
1345
+ if (!key || !ck.startsWith(prefix)) {
1346
+ continue;
1347
+ }
1348
+ // Parse protocol and contextId from cache key:
1349
+ // format is `dctx~<delegateDid>~<protocol>~<contextId>`
1350
+ // contextId is always the last segment; protocol is everything between.
1351
+ const rest = ck.slice(prefix.length);
1352
+ const lastTilde = rest.lastIndexOf('~');
1353
+ if (lastTilde === -1) {
1354
+ continue;
1355
+ }
1356
+ result.push({
1357
+ protocol: rest.slice(0, lastTilde),
1358
+ contextId: rest.slice(lastTilde + 1),
1359
+ derivedPrivateKey: key,
1360
+ });
1361
+ }
1362
+ return result;
1363
+ }
1364
+ /**
1365
+ * Exports the registered multi-party protocol URIs for a delegate.
1366
+ * Used by the auth layer for persistence.
1367
+ */
1368
+ exportDelegateMultiPartyProtocols(delegateDid) {
1369
+ const protocols = this._delegateMultiPartyProtocols.get(delegateDid);
1370
+ return protocols ? [...protocols] : [];
1371
+ }
1372
+ /**
1373
+ * Checks whether any active delegate session has context keys for the
1374
+ * given protocol. Used by `postWriteKeyDelivery()` to determine if
1375
+ * delegate delivery is needed.
1376
+ */
1377
+ hasEligibleDelegatesForProtocol(protocol) {
1378
+ for (const [, protocols] of this._delegateMultiPartyProtocols) {
1379
+ if (protocols.has(protocol)) {
1380
+ return true;
1381
+ }
1382
+ }
1383
+ return false;
1384
+ }
1385
+ /**
1386
+ * Delivers a newly created multi-party context key to all active delegate
1387
+ * sessions that have existing context keys for the given protocol.
1388
+ *
1389
+ * This is same-process delivery: the context key is injected directly
1390
+ * into `_delegateContextKeyCache`. It works when the delegate cache is
1391
+ * on the same agent instance that creates the root record.
1392
+ *
1393
+ * Cross-device DWN-based delivery (where the owner's agent and the
1394
+ * delegate's agent are separate processes) is a documented follow-up.
1395
+ *
1396
+ * @param protocol - The protocol URI
1397
+ * @param rootContextId - The root context ID of the new context
1398
+ * @param contextKey - The derived context key (`DerivedPrivateJwk`)
1399
+ */
1400
+ deliverContextKeyToDelegates(protocol, rootContextId, contextKey) {
1401
+ var _a, _b;
1402
+ for (const [delegateDid, protocols] of this._delegateMultiPartyProtocols) {
1403
+ if (!protocols.has(protocol)) {
1404
+ continue;
1405
+ }
1406
+ // Skip if this delegate already has a key for this context (idempotent).
1407
+ const newCacheKey = `dctx~${delegateDid}~${protocol}~${rootContextId}`;
1408
+ if (this._delegateContextKeyCache.get(newCacheKey)) {
1409
+ continue;
1410
+ }
1411
+ this._delegateContextKeyCache.set(newCacheKey, contextKey);
1412
+ const indexKeys = (_a = this._delegateContextKeyCacheIndex.get(delegateDid)) !== null && _a !== void 0 ? _a : [];
1413
+ indexKeys.push(newCacheKey);
1414
+ this._delegateContextKeyCacheIndex.set(delegateDid, indexKeys);
1415
+ // Notify the auth layer so it can persist the updated keys.
1416
+ (_b = this._onDelegateContextKeysChanged) === null || _b === void 0 ? void 0 : _b.call(this, delegateDid);
1417
+ }
1418
+ }
1419
+ /**
1420
+ * Delivers a newly created multi-party context key to eligible delegates
1421
+ * by writing `contextKey` records to the owner's DWN via the key-delivery
1422
+ * protocol. This enables cross-device delivery: the delegate can later
1423
+ * fetch the record from the owner's DWN and decrypt it.
1424
+ *
1425
+ * Eligible delegates are discovered by querying the owner's DWN for
1426
+ * active permission grants with read-like scopes on the given protocol.
1427
+ *
1428
+ * The contextKey record is encrypted using the delegate's pre-derived
1429
+ * key-delivery leaf public key, which is stored in the grant's tags
1430
+ * during `submitConnectResponse()`.
1431
+ *
1432
+ * @param tenantDid - The owner's DID
1433
+ * @param protocol - The protocol URI
1434
+ * @param rootContextId - The root context ID of the new context
1435
+ * @param contextKey - The derived context key (`DerivedPrivateJwk`)
1436
+ */
1437
+ deliverContextKeyToDelegatesViaDwn(tenantDid, protocol, rootContextId, contextKey) {
1438
+ return __awaiter(this, void 0, void 0, function* () {
1439
+ try {
1440
+ const permissionsApi = new AgentPermissionsApi({ agent: this.agent });
1441
+ const grants = yield permissionsApi.fetchGrants({
1442
+ author: tenantDid,
1443
+ target: tenantDid,
1444
+ grantor: tenantDid,
1445
+ protocol,
1446
+ checkRevoked: true,
1447
+ });
1448
+ const readMethods = new Set(['Read', 'Query', 'Subscribe']);
1449
+ const nowMs = Date.now();
1450
+ // Deduplicate: one contextKey per delegate, not per grant.
1451
+ // Multiple grants (Read, Query, Subscribe) for the same delegate
1452
+ // should produce exactly one contextKey record.
1453
+ // IMPORTANT: dedup happens AFTER tag validation so that an older
1454
+ // untagged grant doesn't shadow a valid tagged grant for the same delegate.
1455
+ const deliveredDelegates = new Set();
1456
+ for (const grant of grants) {
1457
+ // Only delegated grants are eligible. Non-delegated grants (direct
1458
+ // access without a delegate session) should not trigger cross-device
1459
+ // context key delivery.
1460
+ if (!grant.grant.delegated) {
1461
+ continue;
1462
+ }
1463
+ // Filter expired grants. fetchGrants checks revocation but does
1464
+ // NOT filter by dateExpires. Use numeric comparison for safety.
1465
+ if (new Date(grant.grant.dateExpires).getTime() <= nowMs) {
1466
+ continue;
1467
+ }
1468
+ const scope = grant.grant.scope;
1469
+ if (scope.interface !== 'Records' || !readMethods.has(scope.method)) {
1470
+ continue;
1471
+ }
1472
+ // Narrow scopes (protocolPath, contextId) are not supported for
1473
+ // multi-party delegate delivery — skip them silently.
1474
+ if (scope.protocolPath || scope.contextId) {
1475
+ continue;
1476
+ }
1477
+ const delegateDid = grant.grant.grantee;
1478
+ // Read the pre-derived key-delivery leaf public key from the
1479
+ // grant data payload. This was computed during submitConnectResponse()
1480
+ // and stored alongside the grant's standard fields.
1481
+ const keyDelivery = grant.grant.delegateKeyDelivery;
1482
+ if (!(keyDelivery === null || keyDelivery === void 0 ? void 0 : keyDelivery.rootKeyId) || !(keyDelivery === null || keyDelivery === void 0 ? void 0 : keyDelivery.publicKeyJwk)) {
1483
+ // Grant was created before key-delivery was supported, or
1484
+ // is not a read-like grant. Skip — do NOT dedup yet.
1485
+ continue;
1486
+ }
1487
+ // Dedup check — skip if already delivered via an earlier grant.
1488
+ if (deliveredDelegates.has(delegateDid)) {
1489
+ continue;
1490
+ }
1491
+ const leafPublicKeyJwk = keyDelivery.publicKeyJwk;
1492
+ try {
1493
+ yield this.writeContextKeyRecord({
1494
+ tenantDid,
1495
+ recipientDid: delegateDid,
1496
+ contextKeyData: contextKey,
1497
+ sourceProtocol: protocol,
1498
+ sourceContextId: rootContextId,
1499
+ recipientKeyDeliveryPublicKey: {
1500
+ rootKeyId: keyDelivery.rootKeyId,
1501
+ publicKeyJwk: leafPublicKeyJwk,
1502
+ },
1503
+ });
1504
+ // Mark as delivered ONLY after the write succeeds. If the write
1505
+ // fails, a later valid grant for the same delegate can still try.
1506
+ deliveredDelegates.add(delegateDid);
1507
+ }
1508
+ catch (delegateError) {
1509
+ console.warn(`AgentDwnApi: Cross-device key delivery to delegate ` +
1510
+ `'${delegateDid}' for context '${rootContextId}' failed: ` +
1511
+ `${delegateError.message}. The delegate may need to reconnect.`);
1512
+ }
1513
+ }
1514
+ }
1515
+ catch (discoveryError) {
1516
+ // Grant discovery failure is non-fatal — same-process delivery may
1517
+ // still have succeeded, and sync will eventually deliver the record.
1518
+ console.warn(`AgentDwnApi: Delegate grant discovery for protocol ` +
1519
+ `'${protocol}' failed: ${discoveryError.message}`);
1520
+ }
1521
+ });
1522
+ }
1091
1523
  /**
1092
1524
  * Writes a `contextKey` record to the owner's DWN, delivering an encrypted
1093
1525
  * context key to a participant.
@@ -1118,7 +1550,7 @@ export class AgentDwnApi {
1118
1550
  */
1119
1551
  fetchContextKeyRecord(params) {
1120
1552
  return __awaiter(this, void 0, void 0, function* () {
1121
- return fetchContextKeyRecordFn(this.agent, params, this.processRequest.bind(this), this.getSigner.bind(this), this.sendDwnRpcRequest.bind(this), this.getDwnEndpointUrlsForTarget.bind(this));
1553
+ return fetchContextKeyRecordFn(this.agent, params, this.processRequest.bind(this));
1122
1554
  });
1123
1555
  }
1124
1556
  }