@enbox/agent 0.6.5 → 0.6.7

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 (113) hide show
  1. package/README.md +18 -5
  2. package/dist/browser.mjs +11 -11
  3. package/dist/browser.mjs.map +4 -4
  4. package/dist/esm/agent-did-resolver-cache.js +5 -5
  5. package/dist/esm/agent-did-resolver-cache.js.map +1 -1
  6. package/dist/esm/crypto-api.js.map +1 -1
  7. package/dist/esm/did-api.js +1 -1
  8. package/dist/esm/did-api.js.map +1 -1
  9. package/dist/esm/dwn-api.js +93 -53
  10. package/dist/esm/dwn-api.js.map +1 -1
  11. package/dist/esm/dwn-discovery-payload.js +7 -4
  12. package/dist/esm/dwn-discovery-payload.js.map +1 -1
  13. package/dist/esm/dwn-key-delivery.js +8 -3
  14. package/dist/esm/dwn-key-delivery.js.map +1 -1
  15. package/dist/esm/enbox-connect-protocol.js +34 -14
  16. package/dist/esm/enbox-connect-protocol.js.map +1 -1
  17. package/dist/esm/enbox-user-agent.js +11 -3
  18. package/dist/esm/enbox-user-agent.js.map +1 -1
  19. package/dist/esm/hd-identity-vault.js +33 -18
  20. package/dist/esm/hd-identity-vault.js.map +1 -1
  21. package/dist/esm/identity-api.js +5 -4
  22. package/dist/esm/identity-api.js.map +1 -1
  23. package/dist/esm/index.js +1 -0
  24. package/dist/esm/index.js.map +1 -1
  25. package/dist/esm/local-dwn.js.map +1 -1
  26. package/dist/esm/local-key-manager.js.map +1 -1
  27. package/dist/esm/permissions-api.js +9 -5
  28. package/dist/esm/permissions-api.js.map +1 -1
  29. package/dist/esm/prototyping/crypto/jose/jwe-flattened.js +9 -9
  30. package/dist/esm/prototyping/crypto/jose/jwe-flattened.js.map +1 -1
  31. package/dist/esm/secret-store.js +106 -0
  32. package/dist/esm/secret-store.js.map +1 -0
  33. package/dist/esm/store-data.js +32 -11
  34. package/dist/esm/store-data.js.map +1 -1
  35. package/dist/esm/sync-closure-resolver.js +1 -1
  36. package/dist/esm/sync-closure-resolver.js.map +1 -1
  37. package/dist/esm/sync-engine-level.js +418 -141
  38. package/dist/esm/sync-engine-level.js.map +1 -1
  39. package/dist/esm/sync-replication-ledger.js +25 -0
  40. package/dist/esm/sync-replication-ledger.js.map +1 -1
  41. package/dist/esm/test-harness.js +32 -5
  42. package/dist/esm/test-harness.js.map +1 -1
  43. package/dist/esm/types/sync.js +9 -3
  44. package/dist/esm/types/sync.js.map +1 -1
  45. package/dist/esm/utils.js.map +1 -1
  46. package/dist/types/agent-did-resolver-cache.d.ts +1 -1
  47. package/dist/types/agent-did-resolver-cache.d.ts.map +1 -1
  48. package/dist/types/anonymous-dwn-api.d.ts +2 -2
  49. package/dist/types/anonymous-dwn-api.d.ts.map +1 -1
  50. package/dist/types/crypto-api.d.ts +1 -1
  51. package/dist/types/crypto-api.d.ts.map +1 -1
  52. package/dist/types/did-api.d.ts +2 -2
  53. package/dist/types/did-api.d.ts.map +1 -1
  54. package/dist/types/dwn-api.d.ts +51 -11
  55. package/dist/types/dwn-api.d.ts.map +1 -1
  56. package/dist/types/dwn-key-delivery.d.ts +4 -1
  57. package/dist/types/dwn-key-delivery.d.ts.map +1 -1
  58. package/dist/types/enbox-connect-protocol.d.ts +3 -2
  59. package/dist/types/enbox-connect-protocol.d.ts.map +1 -1
  60. package/dist/types/enbox-user-agent.d.ts +5 -1
  61. package/dist/types/enbox-user-agent.d.ts.map +1 -1
  62. package/dist/types/hd-identity-vault.d.ts +9 -2
  63. package/dist/types/hd-identity-vault.d.ts.map +1 -1
  64. package/dist/types/identity-api.d.ts +1 -1
  65. package/dist/types/identity-api.d.ts.map +1 -1
  66. package/dist/types/index.d.ts +1 -0
  67. package/dist/types/index.d.ts.map +1 -1
  68. package/dist/types/local-dwn.d.ts +3 -3
  69. package/dist/types/local-dwn.d.ts.map +1 -1
  70. package/dist/types/local-key-manager.d.ts +2 -2
  71. package/dist/types/local-key-manager.d.ts.map +1 -1
  72. package/dist/types/permissions-api.d.ts +1 -1
  73. package/dist/types/permissions-api.d.ts.map +1 -1
  74. package/dist/types/secret-store.d.ts +81 -0
  75. package/dist/types/secret-store.d.ts.map +1 -0
  76. package/dist/types/store-data.d.ts +15 -3
  77. package/dist/types/store-data.d.ts.map +1 -1
  78. package/dist/types/sync-engine-level.d.ts +52 -16
  79. package/dist/types/sync-engine-level.d.ts.map +1 -1
  80. package/dist/types/sync-replication-ledger.d.ts +10 -1
  81. package/dist/types/sync-replication-ledger.d.ts.map +1 -1
  82. package/dist/types/test-harness.d.ts +3 -0
  83. package/dist/types/test-harness.d.ts.map +1 -1
  84. package/dist/types/types/agent.d.ts +3 -0
  85. package/dist/types/types/agent.d.ts.map +1 -1
  86. package/dist/types/types/sync.d.ts +27 -4
  87. package/dist/types/types/sync.d.ts.map +1 -1
  88. package/package.json +3 -3
  89. package/src/agent-did-resolver-cache.ts +5 -5
  90. package/src/anonymous-dwn-api.ts +2 -2
  91. package/src/crypto-api.ts +1 -1
  92. package/src/did-api.ts +3 -3
  93. package/src/dwn-api.ts +107 -69
  94. package/src/dwn-discovery-payload.ts +5 -4
  95. package/src/dwn-key-delivery.ts +8 -2
  96. package/src/enbox-connect-protocol.ts +38 -21
  97. package/src/enbox-user-agent.ts +15 -3
  98. package/src/hd-identity-vault.ts +47 -21
  99. package/src/identity-api.ts +6 -5
  100. package/src/index.ts +1 -0
  101. package/src/local-dwn.ts +3 -3
  102. package/src/local-key-manager.ts +2 -2
  103. package/src/permissions-api.ts +12 -8
  104. package/src/prototyping/crypto/jose/jwe-flattened.ts +8 -8
  105. package/src/secret-store.ts +173 -0
  106. package/src/store-data.ts +40 -14
  107. package/src/sync-closure-resolver.ts +2 -2
  108. package/src/sync-engine-level.ts +423 -162
  109. package/src/sync-replication-ledger.ts +26 -1
  110. package/src/test-harness.ts +40 -5
  111. package/src/types/agent.ts +3 -0
  112. package/src/types/sync.ts +35 -7
  113. package/src/utils.ts +1 -1
package/src/did-api.ts CHANGED
@@ -121,9 +121,9 @@ export class AgentDidApi<TKeyManager extends AgentKeyManager = AgentKeyManager>
121
121
  */
122
122
  private _agent?: EnboxPlatformAgent;
123
123
 
124
- private _didMethods: Map<string, DidMethodApi> = new Map();
124
+ private readonly _didMethods: Map<string, DidMethodApi> = new Map();
125
125
 
126
- private _store: AgentDataStore<PortableDid>;
126
+ private readonly _store: AgentDataStore<PortableDid>;
127
127
 
128
128
  constructor({ agent, didMethods, resolverCache, store }: DidApiParams) {
129
129
  if (!didMethods) {
@@ -303,7 +303,7 @@ export class AgentDidApi<TKeyManager extends AgentKeyManager = AgentKeyManager>
303
303
  const parsedDid = Did.parse(uri);
304
304
  // currently only supporting DHT as a publishable method.
305
305
  // TODO: abstract this into the didMethod class so that other publishable methods can be supported.
306
- if (parsedDid && parsedDid.method === 'dht') {
306
+ if (parsedDid?.method === 'dht') {
307
307
  await DidDht.publish({ did: bearerDid });
308
308
  }
309
309
  }
package/src/dwn-api.ts CHANGED
@@ -130,20 +130,20 @@ export class AgentDwnApi {
130
130
  * `undefined` in remote mode — all operations route through RPC to
131
131
  * the local DWN server endpoint.
132
132
  */
133
- private _dwn?: Dwn;
133
+ private readonly _dwn?: Dwn;
134
134
 
135
135
  /**
136
136
  * The local DWN server endpoint for remote mode.
137
137
  * When set, `_dwn` is `undefined` and `processRequest()` routes
138
138
  * through `sendDwnRpcRequest()`.
139
139
  */
140
- private _localDwnEndpoint?: string;
140
+ private readonly _localDwnEndpoint?: string;
141
141
 
142
142
  /**
143
143
  * Protocol definition cache — TTL 30 minutes. Protocols rarely change.
144
144
  * Keyed by `${tenantDid}~${protocolUri}`.
145
145
  */
146
- private _protocolDefinitionCache = new TtlCache<string, ProtocolDefinition>({
146
+ private readonly _protocolDefinitionCache = new TtlCache<string, ProtocolDefinition>({
147
147
  ttl: 30 * 60 * 1000
148
148
  });
149
149
 
@@ -151,7 +151,7 @@ export class AgentDwnApi {
151
151
  * Context key cache — stores resolved context encryption key info for
152
152
  * multi-party protocols. Keyed by rootContextId. TTL 30 minutes.
153
153
  */
154
- private _contextKeyCache = new TtlCache<string, {
154
+ private readonly _contextKeyCache = new TtlCache<string, {
155
155
  keyId: string;
156
156
  keyUri: KeyIdentifier;
157
157
  contextDerivationPath: string[];
@@ -162,7 +162,7 @@ export class AgentDwnApi {
162
162
  * where the current user is a participant (not the creator).
163
163
  * Keyed by `ctx~${authorDid}~${rootContextId}`. TTL 30 minutes.
164
164
  */
165
- private _contextDerivedKeyCache = new TtlCache<string, DerivedPrivateJwk>({
165
+ private readonly _contextDerivedKeyCache = new TtlCache<string, DerivedPrivateJwk>({
166
166
  ttl: 30 * 60 * 1000
167
167
  });
168
168
 
@@ -176,7 +176,7 @@ export class AgentDwnApi {
176
176
  * granted read scopes for that delegate session.
177
177
  * TTL 24 hours (keys are re-populated on session restore).
178
178
  */
179
- private _delegateDecryptionKeyCache = new TtlCache<string, {
179
+ private readonly _delegateDecryptionKeyCache = new TtlCache<string, {
180
180
  protocol: string;
181
181
  scope: { kind: 'protocol' } | { kind: 'protocolPath'; protocolPath: string; match: 'exact' };
182
182
  derivedPrivateKey: DerivedPrivateJwk;
@@ -190,12 +190,12 @@ export class AgentDwnApi {
190
190
  * Keyed by `dctx~${delegateDid}~${protocol}~${rootContextId}`.
191
191
  * TTL 24 hours (re-populated on session restore).
192
192
  */
193
- private _delegateContextKeyCache = new TtlCache<string, DerivedPrivateJwk>({
193
+ private readonly _delegateContextKeyCache = new TtlCache<string, DerivedPrivateJwk>({
194
194
  ttl: 24 * 60 * 60 * 1000
195
195
  });
196
196
 
197
197
  /** Tracks which context key cache entries belong to which delegate DID. */
198
- private _delegateContextKeyCacheIndex = new Map<string, string[]>();
198
+ private readonly _delegateContextKeyCacheIndex = new Map<string, string[]>();
199
199
 
200
200
  /**
201
201
  * Explicit registry of which multi-party protocols each delegate has
@@ -207,7 +207,7 @@ export class AgentDwnApi {
207
207
  * Unlike the context key cache, this registry is NOT time-limited —
208
208
  * it persists for the lifetime of the session.
209
209
  */
210
- private _delegateMultiPartyProtocols = new Map<string, Set<string>>();
210
+ private readonly _delegateMultiPartyProtocols = new Map<string, Set<string>>();
211
211
 
212
212
  /**
213
213
  * Optional callback invoked when post-connect context keys are delivered
@@ -219,7 +219,7 @@ export class AgentDwnApi {
219
219
  * Cache of locally-managed DIDs (agent DID + identities). Used to decide
220
220
  * whether a target DID should be routed through the local DWN server.
221
221
  */
222
- private _localManagedDidCache = new TtlCache<string, boolean>({
222
+ private readonly _localManagedDidCache = new TtlCache<string, boolean>({
223
223
  ttl: 30 * 60 * 1000
224
224
  });
225
225
 
@@ -229,6 +229,17 @@ export class AgentDwnApi {
229
229
  /** Lazy-initialized local DWN discovery instance. */
230
230
  private _localDwnDiscovery?: LocalDwnDiscovery;
231
231
 
232
+ /**
233
+ * Tracks fire-and-forget eager-send promises dispatched by
234
+ * `writeContextKeyRecord`. The set is consumed by `drainPendingEagerSends()`
235
+ * during test-harness teardown so orphan promises cannot outlive the agent
236
+ * and touch closed LevelDB handles or nulled state.
237
+ *
238
+ * Entries auto-remove on settlement via the `.finally` attached in
239
+ * `trackEagerSend`.
240
+ */
241
+ private readonly _pendingEagerSends: Set<Promise<void>> = new Set();
242
+
232
243
  constructor(params: DwnApiParams) {
233
244
  const { agent, localDwnStrategy = 'prefer' } = params;
234
245
 
@@ -535,11 +546,11 @@ export class AgentDwnApi {
535
546
  });
536
547
  }
537
548
 
538
- // Post-write key delivery: detect new participants and write contextKey records.
539
- await this.postWriteKeyDelivery(request, message, reply);
540
-
541
- // Auto-decrypt reply data if encryption is enabled (Component 7)
542
- await this.maybeDecryptReply(request, reply);
549
+ // Post-write key delivery and reply decryption are independent run in parallel.
550
+ await Promise.all([
551
+ this.postWriteKeyDelivery(request, message, reply),
552
+ this.maybeDecryptReply(request, reply),
553
+ ]);
543
554
 
544
555
  // Returns an object containing the reply from processing the message, the original message,
545
556
  // and the content identifier (CID) of the message.
@@ -629,9 +640,9 @@ export class AgentDwnApi {
629
640
  let resubscribeFactory: ResubscribeFactory | undefined;
630
641
  if (subscriptionHandler !== undefined && !('messageCid' in request)) {
631
642
  resubscribeFactory = async (cursor?: ProgressToken): Promise<GenericMessage> => {
632
- const resumeParams = cursor !== undefined
633
- ? { ...request.messageParams, cursor } as DwnMessageParams[T]
634
- : request.messageParams;
643
+ const resumeParams = cursor === undefined
644
+ ? request.messageParams
645
+ : { ...request.messageParams, cursor } as DwnMessageParams[T];
635
646
 
636
647
  const resumeRequest: ProcessDwnRequest<T> = { ...request, messageParams: resumeParams };
637
648
  const { message: resumeMessage } = await this.constructDwnMessage({ request: resumeRequest });
@@ -756,8 +767,13 @@ export class AgentDwnApi {
756
767
  && !isExternallyAuthored
757
768
  && this.hasEligibleDelegatesForProtocol(writeParams.protocol);
758
769
 
759
- if (newParticipants.size > 0 || needsDelegateDelivery) {
760
- // Derive the context key once (shared for participant + delegate delivery).
770
+ // Cross-device delegate delivery (via DWN) runs whenever the owner
771
+ // creates a root record in a multi-party context, regardless of
772
+ // whether same-process delegates exist.
773
+ const needsCrossDeviceDelivery = isMultiParty && isRootRecord && !isExternallyAuthored;
774
+
775
+ // Derive the context key once and share it across all delivery paths.
776
+ if (newParticipants.size > 0 || needsDelegateDelivery || needsCrossDeviceDelivery) {
761
777
  const { keyId, keyUri } = await getEncryptionKeyInfoFn(this.agent, request.target);
762
778
  const contextDerivationPath = [
763
779
  KeyDerivationScheme.ProtocolContext,
@@ -818,38 +834,18 @@ export class AgentDwnApi {
818
834
  writeParams.protocol, rootContextId, contextKeyPayload,
819
835
  );
820
836
  }
821
- }
822
837
 
823
- // --- Cross-device delegate context key delivery (#826) ---
824
- // Query grants to find ALL eligible delegates (including those on
825
- // other agents) and write contextKey records to the DWN.
826
- // This is separate from same-process delivery: it discovers delegates
827
- // from the owner's DWN grants, not just in-memory caches. It must
828
- // run even when no same-process delegates or participants exist.
829
- if (isMultiParty && isRootRecord && !isExternallyAuthored) {
830
- // Derive the context key if not already done above.
831
- const { keyId: xdKeyId, keyUri: xdKeyUri } = await getEncryptionKeyInfoFn(this.agent, request.target);
832
- const xdContextDerivationPath = [
833
- KeyDerivationScheme.ProtocolContext,
834
- rootContextId,
835
- ];
836
- const xdContextDerivedPrivateKeyBytes =
837
- await this.agent.keyManager.derivePrivateKeyBytes({
838
- keyUri : xdKeyUri,
839
- derivationPath : xdContextDerivationPath,
840
- });
841
- const xdContextDerivedPrivateJwk =
842
- await X25519.bytesToPrivateKey({ privateKeyBytes: xdContextDerivedPrivateKeyBytes });
843
- const xdContextKeyPayload: DerivedPrivateJwk = {
844
- rootKeyId : xdKeyId,
845
- derivationScheme : KeyDerivationScheme.ProtocolContext,
846
- derivationPath : xdContextDerivationPath,
847
- derivedPrivateKey : xdContextDerivedPrivateJwk as PrivateKeyJwk,
848
- };
849
-
850
- await this.deliverContextKeyToDelegatesViaDwn(
851
- request.target, writeParams.protocol, rootContextId, xdContextKeyPayload,
852
- );
838
+ // --- Cross-device delegate context key delivery (#826) ---
839
+ // Query grants to find ALL eligible delegates (including those on
840
+ // other agents) and write contextKey records to the DWN.
841
+ // This is separate from same-process delivery: it discovers delegates
842
+ // from the owner's DWN grants, not just in-memory caches. It must
843
+ // run even when no same-process delegates or participants exist.
844
+ if (needsCrossDeviceDelivery) {
845
+ await this.deliverContextKeyToDelegatesViaDwn(
846
+ request.target, writeParams.protocol, rootContextId, contextKeyPayload,
847
+ );
848
+ }
853
849
  }
854
850
  } catch (detectionError: any) {
855
851
  // Participant detection failure is non-fatal — the record is still stored.
@@ -1226,8 +1222,22 @@ export class AgentDwnApi {
1226
1222
  let dwnMessage: DwnMessageInstance[T];
1227
1223
  const dwnMessageConstructor = dwnMessageConstructors[request.messageType];
1228
1224
 
1229
- // if there is no raw message provided, we need to create the dwn message
1230
- if (!rawMessage) {
1225
+ // if a raw message is provided, parse it; otherwise create a new dwn message
1226
+ if (rawMessage) {
1227
+ dwnMessage = await dwnMessageConstructor.parse(rawMessage);
1228
+ if (isRecordsWrite(dwnMessage) && request.signAsOwner) {
1229
+ // if we are signing as owner, we use the author's signer
1230
+ const signer = await this.getSigner(request.author);
1231
+ await dwnMessage.signAsOwner(signer);
1232
+ } else if (request.granteeDid && isRecordsWrite(dwnMessage) && request.signAsOwnerDelegate) {
1233
+ // if we are signing as owner delegate, we use the grantee's signer and the provided delegated grant
1234
+ const signer = await this.getSigner(request.granteeDid);
1235
+
1236
+ //if we have reached here, the presence of the grant params has already been checked
1237
+ const messageParams = request.messageParams as DwnMessageParams[DwnInterface.RecordsWrite];
1238
+ await dwnMessage.signAsOwnerDelegate(signer, messageParams.delegatedGrant!);
1239
+ }
1240
+ } else {
1231
1241
  if (request.messageParams === undefined) {
1232
1242
  throw new Error('AgentDwnApi: messageParams must be provided when rawMessage is not given.');
1233
1243
  }
@@ -1290,21 +1300,6 @@ export class AgentDwnApi {
1290
1300
  // Cache context key info for subsequent writes in this context
1291
1301
  this._contextKeyCache.set(contextId, { keyId, keyUri, contextDerivationPath });
1292
1302
  }
1293
-
1294
- } else {
1295
- dwnMessage = await dwnMessageConstructor.parse(rawMessage);
1296
- if (isRecordsWrite(dwnMessage) && request.signAsOwner) {
1297
- // if we are signing as owner, we use the author's signer
1298
- const signer = await this.getSigner(request.author);
1299
- await dwnMessage.signAsOwner(signer);
1300
- } else if (request.granteeDid && isRecordsWrite(dwnMessage) && request.signAsOwnerDelegate) {
1301
- // if we are signing as owner delegate, we use the grantee's signer and the provided delegated grant
1302
- const signer = await this.getSigner(request.granteeDid);
1303
-
1304
- //if we have reached here, the presence of the grant params has already been checked
1305
- const messageParams = request.messageParams as DwnMessageParams[DwnInterface.RecordsWrite];
1306
- await dwnMessage.signAsOwnerDelegate(signer, messageParams.delegatedGrant!);
1307
- }
1308
1303
  }
1309
1304
 
1310
1305
  return {
@@ -1603,7 +1598,7 @@ export class AgentDwnApi {
1603
1598
  * Cache for key delivery protocol installation status per tenant.
1604
1599
  * Once confirmed installed, we skip re-checking for 21 days.
1605
1600
  */
1606
- private _keyDeliveryProtocolInstalledCache = new TtlCache<string, boolean>({
1601
+ private readonly _keyDeliveryProtocolInstalledCache = new TtlCache<string, boolean>({
1607
1602
  ttl : 21 * 24 * 60 * 60 * 1000,
1608
1603
  max : 1000,
1609
1604
  });
@@ -1946,6 +1941,48 @@ export class AgentDwnApi {
1946
1941
  }
1947
1942
  }
1948
1943
 
1944
+ /**
1945
+ * Registers a fire-and-forget eager-send promise in the in-flight tracker so
1946
+ * it can be awaited by `drainPendingEagerSends()` during teardown. The entry
1947
+ * auto-removes on settlement via `.finally`.
1948
+ *
1949
+ * The promise is returned **unchanged** so callers can still chain `.catch`
1950
+ * on it for observability (e.g. the existing `console.warn` on failure).
1951
+ *
1952
+ * @param p - The eager-send promise to track. Must always resolve (callers
1953
+ * are expected to attach a `.catch` handler before passing).
1954
+ * @returns The same `p`, unchanged.
1955
+ */
1956
+ private trackEagerSend(p: Promise<void>): Promise<void> {
1957
+ this._pendingEagerSends.add(p);
1958
+ p.finally((): void => { this._pendingEagerSends.delete(p); });
1959
+ return p;
1960
+ }
1961
+
1962
+ /**
1963
+ * Waits for all currently-tracked eager-send promises to settle. Used by
1964
+ * the test harness during teardown (`clearStorage()`/`closeStorage()`) to
1965
+ * prevent orphan promises from touching closed stores or nulled state.
1966
+ *
1967
+ * Snapshot semantics: awaits only the sends tracked at the moment of
1968
+ * invocation. Sends registered **after** this call begins are not joined
1969
+ * into its drain — a subsequent `drainPendingEagerSends()` is needed to
1970
+ * await them.
1971
+ *
1972
+ * Fast path: when no sends are tracked, resolves immediately without
1973
+ * invoking `Promise.allSettled([])`.
1974
+ *
1975
+ * @returns A promise that resolves to `undefined` once all snapshotted
1976
+ * sends have settled. Never rejects (uses `allSettled` semantics).
1977
+ */
1978
+ public async drainPendingEagerSends(): Promise<void> {
1979
+ if (this._pendingEagerSends.size === 0) {
1980
+ return;
1981
+ }
1982
+ const snapshot = [...this._pendingEagerSends];
1983
+ await Promise.allSettled(snapshot);
1984
+ }
1985
+
1949
1986
  /**
1950
1987
  * Writes a `contextKey` record to the owner's DWN, delivering an encrypted
1951
1988
  * context key to a participant.
@@ -1966,6 +2003,7 @@ export class AgentDwnApi {
1966
2003
  this.processRequest.bind(this),
1967
2004
  this.ensureKeyDeliveryProtocol.bind(this),
1968
2005
  this.eagerSendContextKeyRecord.bind(this),
2006
+ this.trackEagerSend.bind(this),
1969
2007
  );
1970
2008
  }
1971
2009
 
@@ -276,10 +276,11 @@ function stringToBase64Url(input: string): string {
276
276
  for (let i = 0; i < bytes.length; i++) {
277
277
  binary += String.fromCharCode(bytes[i]);
278
278
  }
279
- return btoa(binary)
280
- .replace(/\+/g, '-')
281
- .replace(/\//g, '_')
282
- .replace(/=+$/, '');
279
+ const result = btoa(binary).replaceAll('+', '-').replaceAll('/', '_');
280
+ // Strip trailing '=' padding without regex quantifiers (avoids ReDoS scanners).
281
+ let end = result.length;
282
+ while (end > 0 && result.codePointAt(end - 1) === 61) { end--; } // 61 === '='
283
+ return end === result.length ? result : result.slice(0, end);
283
284
  }
284
285
 
285
286
  /**
@@ -116,6 +116,9 @@ export async function ensureKeyDeliveryProtocol(
116
116
  * @param processRequest - The agent's processRequest method (bound)
117
117
  * @param ensureProtocol - Function to ensure key delivery protocol is installed
118
118
  * @param eagerSend - Function to eagerly send the record to the remote DWN
119
+ * @param trackEagerSend - Tracker callback that registers the fire-and-forget
120
+ * eager-send promise so it can be awaited during teardown via
121
+ * `AgentDwnApi.drainPendingEagerSends()`. Returns the same promise unchanged.
119
122
  * @returns The recordId of the written contextKey record
120
123
  */
121
124
  export async function writeContextKeyRecord(
@@ -124,6 +127,7 @@ export async function writeContextKeyRecord(
124
127
  processRequest: ProcessRequestFn,
125
128
  ensureProtocol: (tenantDid: string) => Promise<void>,
126
129
  eagerSend: (tenantDid: string, message: DwnMessage[DwnInterface.RecordsWrite]) => Promise<void>,
130
+ trackEagerSend: (p: Promise<void>) => Promise<void>,
127
131
  ): Promise<string> {
128
132
  const { tenantDid, recipientDid, contextKeyData, sourceProtocol, sourceContextId, recipientKeyDeliveryPublicKey } = params;
129
133
 
@@ -198,12 +202,14 @@ export async function writeContextKeyRecord(
198
202
  // Eagerly send the contextKey record to the tenant's remote DWN so that
199
203
  // participants can fetch it immediately without waiting for sync.
200
204
  // This is fire-and-forget — sync will guarantee eventual consistency.
201
- eagerSend(tenantDid, message).catch((err: Error) => {
205
+ // `trackEagerSend` registers the promise with `AgentDwnApi` so it can be
206
+ // drained during teardown (prevents orphan promises leaking past close).
207
+ trackEagerSend(eagerSend(tenantDid, message).catch((err: Error) => {
202
208
  console.warn(
203
209
  `AgentDwnApi: Eager send of contextKey record '${message.recordId}' ` +
204
210
  `to remote DWN failed: ${err.message}. Sync will deliver it later.`
205
211
  );
206
- });
212
+ }));
207
213
 
208
214
  return message.recordId;
209
215
  }
@@ -435,18 +435,21 @@ async function decryptRequest({
435
435
  // Encryption: response (ECDH shared key + optional PIN)
436
436
  // ---------------------------------------------------------------------------
437
437
 
438
- /** Derives a shared ECDH key for encrypting/decrypting the connect response. */
439
- async function deriveSharedKey(
438
+ /**
439
+ * Core ECDH key derivation from a raw public key JWK.
440
+ *
441
+ * Converts both keys to X25519, performs ECDH, and derives the final
442
+ * symmetric key via HKDF-SHA-256.
443
+ */
444
+ async function deriveSharedKeyFromJwk(
440
445
  privateKeyDid: BearerDid,
441
- publicKeyDid: DidDocument
446
+ publicKeyJwk: Jwk
442
447
  ): Promise<Uint8Array> {
443
448
  const privatePortableDid = await privateKeyDid.export();
444
-
445
- const publicJwk = publicKeyDid.verificationMethod?.[0].publicKeyJwk!;
446
449
  const privateJwk = privatePortableDid.privateKeys?.[0]!;
447
- publicJwk.alg = 'EdDSA';
450
+ const pubJwk = { ...publicKeyJwk, alg: 'EdDSA' };
448
451
 
449
- const publicX25519 = await Ed25519.convertPublicKeyToX25519({ publicKey: publicJwk });
452
+ const publicX25519 = await Ed25519.convertPublicKeyToX25519({ publicKey: pubJwk });
450
453
  const privateX25519 = await Ed25519.convertPrivateKeyToX25519({ privateKey: privateJwk });
451
454
 
452
455
  const sharedKey = await X25519.sharedSecret({
@@ -463,6 +466,15 @@ async function deriveSharedKey(
463
466
  });
464
467
  }
465
468
 
469
+ /** Derives a shared ECDH key for encrypting/decrypting the connect response. */
470
+ async function deriveSharedKey(
471
+ privateKeyDid: BearerDid,
472
+ publicKeyDid: DidDocument
473
+ ): Promise<Uint8Array> {
474
+ const publicJwk = publicKeyDid.verificationMethod?.[0].publicKeyJwk!;
475
+ return deriveSharedKeyFromJwk(privateKeyDid, publicJwk);
476
+ }
477
+
466
478
  /**
467
479
  * Encrypts the connect response JWT.
468
480
  *
@@ -475,20 +487,29 @@ async function deriveSharedKey(
475
487
  async function encryptResponse({
476
488
  jwt,
477
489
  encryptionKey,
478
- delegateDidKeyId,
490
+ delegatePublicKeyJwk,
479
491
  pin,
480
492
  }: {
481
493
  jwt: string;
482
494
  encryptionKey: Uint8Array;
483
- delegateDidKeyId: string;
495
+ delegatePublicKeyJwk: Jwk;
484
496
  pin?: string;
485
497
  }): Promise<string> {
498
+ // Include only the minimum key material (kty, crv, x) in the ephemeral
499
+ // public key header. This avoids leaking DID-level identifiers (kid,
500
+ // alg, etc.) that would let the relay correlate sessions or resolve
501
+ // the delegate DID. See https://github.com/enboxorg/enbox/issues/890
502
+ const epk: Jwk = {
503
+ kty : delegatePublicKeyJwk.kty,
504
+ crv : delegatePublicKeyJwk.crv,
505
+ x : delegatePublicKeyJwk.x,
506
+ };
486
507
  const protectedHeader = {
487
508
  alg : 'dir',
488
509
  cty : 'JWT',
489
510
  enc : 'XC20P',
490
511
  typ : 'JWT',
491
- kid : delegateDidKeyId,
512
+ epk,
492
513
  };
493
514
  const nonce = CryptoUtils.randomBytes(24);
494
515
 
@@ -533,16 +554,12 @@ async function decryptResponse(
533
554
  authenticationTagB64U,
534
555
  ] = jwe.split('.');
535
556
 
536
- const header = Convert.base64Url(protectedHeaderB64U).toObject() as Jwk;
537
- if (!header.kid) {
538
- throw new Error('Connect: JWE protected header is missing required "kid" property.');
557
+ const header = Convert.base64Url(protectedHeaderB64U).toObject() as Record<string, unknown>;
558
+ if (!header.epk || typeof header.epk !== 'object') {
559
+ throw new Error('Connect: JWE protected header is missing required "epk" property.');
539
560
  }
540
- const delegateResolvedDid = await DidJwk.resolve(header.kid.split('#')[0]);
541
561
 
542
- const sharedKey = await EnboxConnectProtocol.deriveSharedKey(
543
- clientDid,
544
- delegateResolvedDid.didDocument!
545
- );
562
+ const sharedKey = await deriveSharedKeyFromJwk(clientDid, header.epk as Jwk);
546
563
 
547
564
  // Build AAD — include PIN if provided (must match what was used during encryption).
548
565
  const aadObject = pin
@@ -1297,9 +1314,9 @@ async function submitConnectResponse(
1297
1314
 
1298
1315
  logger.log('Encrypting connect response...');
1299
1316
  const encryptedResponse = await EnboxConnectProtocol.encryptResponse({
1300
- jwt : responseObjectJwt!,
1301
- encryptionKey : sharedKey,
1302
- delegateDidKeyId : delegateBearerDid.document.verificationMethod![0].id,
1317
+ jwt : responseObjectJwt,
1318
+ encryptionKey : sharedKey,
1319
+ delegatePublicKeyJwk : delegateBearerDid.document.verificationMethod![0].publicKeyJwk!,
1303
1320
  pin,
1304
1321
  });
1305
1322
 
@@ -3,6 +3,7 @@ import type { BearerDid } from '@enbox/dids';
3
3
  import type { EnboxPlatformAgent } from './types/agent.js';
4
4
  import type { EnboxRpc } from '@enbox/dwn-clients';
5
5
  import type { LocalDwnStrategy } from './local-dwn.js';
6
+ import type { SecretStore } from './secret-store.js';
6
7
  import type { SyncEngine } from './types/sync.js';
7
8
  import type { DidInterface, DidRequest, DidResponse } from './did-api.js';
8
9
  import type { DwnInterface, DwnResponse, ProcessDwnRequest, SendDwnRequest } from './types/dwn.js';
@@ -23,6 +24,7 @@ import { LevelStore } from '@enbox/common';
23
24
  import { LocalKeyManager } from './local-key-manager.js';
24
25
  import { SyncEngineLevel } from './sync-engine-level.js';
25
26
  import { DidDht, DidJwk } from '@enbox/dids';
27
+ import { InMemorySecretStore, VaultBackedSecretStore } from './secret-store.js';
26
28
 
27
29
  /**
28
30
  * Initialization parameters for {@link EnboxUserAgent}, including an optional recovery phrase that
@@ -84,6 +86,8 @@ export type AgentParams<TKeyManager extends AgentKeyManager = LocalKeyManager> =
84
86
  permissionsApi: AgentPermissionsApi;
85
87
  /** Remote procedure call (RPC) client used to communicate with other Enbox services. */
86
88
  rpcClient: EnboxRpc;
89
+ /** Vault-backed secret store for classified credentials. */
90
+ secretsApi?: SecretStore;
87
91
  /** Facilitates data synchronization of DWN records between nodes. */
88
92
  syncApi: SyncEngine;
89
93
  };
@@ -110,6 +114,7 @@ export class EnboxUserAgent<TKeyManager extends AgentKeyManager = LocalKeyManage
110
114
  public keyManager: TKeyManager;
111
115
  public permissions: AgentPermissionsApi;
112
116
  public rpc: EnboxRpc;
117
+ public secrets: SecretStore;
113
118
  public sync: SyncEngine;
114
119
  public vault: HdIdentityVault;
115
120
 
@@ -124,6 +129,7 @@ export class EnboxUserAgent<TKeyManager extends AgentKeyManager = LocalKeyManage
124
129
  this.keyManager = params.keyManager;
125
130
  this.permissions = params.permissionsApi;
126
131
  this.rpc = params.rpcClient;
132
+ this.secrets = params.secretsApi ?? new InMemorySecretStore();
127
133
  this.sync = params.syncApi;
128
134
  this.vault = params.agentVault;
129
135
 
@@ -157,7 +163,7 @@ export class EnboxUserAgent<TKeyManager extends AgentKeyManager = LocalKeyManage
157
163
  dataPath = 'DATA/AGENT',
158
164
  localDwnStrategy,
159
165
  localDwnEndpoint,
160
- agentDid, agentVault, cryptoApi, didApi, dwnApi, identityApi, keyManager, permissionsApi, rpcClient, syncApi
166
+ agentDid, agentVault, cryptoApi, didApi, dwnApi, identityApi, keyManager, permissionsApi, rpcClient, secretsApi, syncApi
161
167
  }: CreateUserAgentParams = {}
162
168
  ): Promise<EnboxUserAgent> {
163
169
 
@@ -166,6 +172,11 @@ export class EnboxUserAgent<TKeyManager extends AgentKeyManager = LocalKeyManage
166
172
  store : new LevelStore<string, string>({ location: `${dataPath}/VAULT_STORE` })
167
173
  });
168
174
 
175
+ secretsApi ??= new VaultBackedSecretStore({
176
+ vault : agentVault,
177
+ store : new LevelStore<string, string>({ location: `${dataPath}/SECRET_STORE` }),
178
+ });
179
+
169
180
  cryptoApi ??= new AgentCryptoApi();
170
181
 
171
182
  didApi ??= new AgentDidApi({
@@ -211,11 +222,12 @@ export class EnboxUserAgent<TKeyManager extends AgentKeyManager = LocalKeyManage
211
222
  cryptoApi,
212
223
  didApi,
213
224
  dwnApi,
225
+ identityApi,
214
226
  keyManager,
215
227
  permissionsApi,
216
- identityApi,
217
228
  rpcClient,
218
- syncApi
229
+ secretsApi,
230
+ syncApi,
219
231
  });
220
232
  }
221
233