@enbox/agent 0.6.5 → 0.6.6

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 +1 -1
  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
@@ -21,7 +21,7 @@ const KEY_SEP = '^';
21
21
  */
22
22
  export class ReplicationLedger {
23
23
  private readonly db: AbstractLevel<string | Buffer | Uint8Array>;
24
- private sublevel;
24
+ private readonly sublevel;
25
25
 
26
26
  constructor(db: AbstractLevel<string | Buffer | Uint8Array>) {
27
27
  this.db = db;
@@ -126,6 +126,31 @@ export class ReplicationLedger {
126
126
  return links;
127
127
  }
128
128
 
129
+ // ---------------------------------------------------------------------------
130
+ // Delegate updates
131
+ // ---------------------------------------------------------------------------
132
+
133
+ /**
134
+ * Update the `delegateDid` on all persisted links for a tenant and persist.
135
+ * This ensures that repair and reconcile paths — which read `delegateDid`
136
+ * from the durable {@link ReplicationLinkState} — use the current delegate
137
+ * after a hot-swap via `updateIdentityOptions()`.
138
+ *
139
+ * @returns the links that were updated.
140
+ */
141
+ public async updateDelegateDid(tenantDid: string, delegateDid: string | undefined): Promise<ReplicationLinkState[]> {
142
+ const links = await this.getLinksForTenant(tenantDid);
143
+ const updated: ReplicationLinkState[] = [];
144
+ for (const link of links) {
145
+ if (link.delegateDid !== delegateDid) {
146
+ link.delegateDid = delegateDid;
147
+ await this.saveLink(link);
148
+ updated.push(link);
149
+ }
150
+ }
151
+ return updated;
152
+ }
153
+
129
154
  // ---------------------------------------------------------------------------
130
155
  // Status transitions
131
156
  // ---------------------------------------------------------------------------
@@ -23,6 +23,7 @@ import { SyncEngineLevel } from './sync-engine-level.js';
23
23
  import { DwnDidStore, InMemoryDidStore } from './store-did.js';
24
24
  import { DwnIdentityStore, InMemoryIdentityStore } from './store-identity.js';
25
25
  import { DwnKeyStore, InMemoryKeyStore } from './store-key.js';
26
+ import { InMemorySecretStore, VaultBackedSecretStore } from './secret-store.js';
26
27
 
27
28
  type StoreSetupResult = {
28
29
  agentVault: HdIdentityVault;
@@ -31,6 +32,9 @@ type StoreSetupResult = {
31
32
  identityApi: AgentIdentityApi<LocalKeyManager>;
32
33
  keyManager: LocalKeyManager;
33
34
  permissionsApi: AgentPermissionsApi;
35
+ secretsApi: InMemorySecretStore | VaultBackedSecretStore;
36
+ /** Backing KeyValueStore for VaultBackedSecretStore (for clear/close lifecycle). */
37
+ secretStore?: KeyValueStore<string, string>;
34
38
  vaultStore: KeyValueStore<string, string>;
35
39
  };
36
40
 
@@ -44,6 +48,8 @@ type PlatformAgentTestHarnessParams = {
44
48
  dwnStateIndex: StateIndexLevel;
45
49
  dwnMessageStore: MessageStoreLevel;
46
50
  dwnResumableTaskStore: ResumableTaskStoreLevel;
51
+ /** Backing KeyValueStore for VaultBackedSecretStore (disk mode only). */
52
+ secretStore?: KeyValueStore<string, string>;
47
53
  syncStore: AbstractLevel<string | Buffer | Uint8Array>;
48
54
  vaultStore: KeyValueStore<string, string>;
49
55
  dwnStores: {
@@ -64,6 +70,7 @@ export class PlatformAgentTestHarness {
64
70
  public dwnStateIndex: StateIndexLevel;
65
71
  public dwnMessageStore: MessageStoreLevel;
66
72
  public dwnResumableTaskStore: ResumableTaskStoreLevel;
73
+ public secretStore?: KeyValueStore<string, string>;
67
74
  public syncStore: AbstractLevel<string | Buffer | Uint8Array>;
68
75
  public vaultStore: KeyValueStore<string, string>;
69
76
 
@@ -87,6 +94,7 @@ export class PlatformAgentTestHarness {
87
94
  this.dwnDataStore = params.dwnDataStore;
88
95
  this.dwnStateIndex = params.dwnStateIndex;
89
96
  this.dwnMessageStore = params.dwnMessageStore;
97
+ this.secretStore = params.secretStore;
90
98
  this.syncStore = params.syncStore;
91
99
  this.vaultStore = params.vaultStore;
92
100
  this.dwnResumableTaskStore = params.dwnResumableTaskStore;
@@ -97,6 +105,12 @@ export class PlatformAgentTestHarness {
97
105
  // first stop any ongoing sync operations
98
106
  await this.agent.sync.stopSync();
99
107
 
108
+ // Drain any in-flight fire-and-forget eager-send promises dispatched by
109
+ // `AgentDwnApi.writeContextKeyRecord` so they cannot outlive the agent
110
+ // and touch a nulled `agentDid` or a cleared LevelDB store. Fast path
111
+ // when the tracker is empty (no pending sends).
112
+ await this.agent.dwn.drainPendingEagerSends();
113
+
100
114
  // @ts-expect-error since normally this property shouldn't be set to undefined.
101
115
  this.agent.agentDid = undefined;
102
116
  await this.didResolverCache.clear();
@@ -106,6 +120,7 @@ export class PlatformAgentTestHarness {
106
120
  await this.dwnResumableTaskStore.clear();
107
121
  await this.syncStore.clear();
108
122
  await this.vaultStore.clear();
123
+ if (this.secretStore) { await this.secretStore.clear(); }
109
124
  (this.agent.vault as any)['_cachedInitialized'] = undefined;
110
125
  await this.agent.permissions.clear();
111
126
  this.dwnStores.clear();
@@ -120,11 +135,12 @@ export class PlatformAgentTestHarness {
120
135
 
121
136
  // Easiest way to start with fresh in-memory stores is to re-instantiate Agent components.
122
137
  if (this.agentStores === 'memory') {
123
- const { didApi, identityApi, permissionsApi, keyManager } = PlatformAgentTestHarness.useMemoryStores({ agent: this.agent });
138
+ const { didApi, identityApi, permissionsApi, keyManager, secretsApi } = PlatformAgentTestHarness.useMemoryStores({ agent: this.agent });
124
139
  this.agent.did = didApi;
125
140
  this.agent.identity = identityApi;
126
141
  this.agent.keyManager = keyManager;
127
142
  this.agent.permissions = permissionsApi;
143
+ this.agent.secrets = secretsApi;
128
144
  }
129
145
  }
130
146
 
@@ -148,11 +164,18 @@ export class PlatformAgentTestHarness {
148
164
  }
149
165
 
150
166
  public async closeStorage(): Promise<void> {
167
+ // Drain any in-flight fire-and-forget eager-send promises dispatched by
168
+ // `AgentDwnApi.writeContextKeyRecord` before closing the LevelDB-backed
169
+ // stores. Prevents orphan promises from hitting closed handles with
170
+ // `LEVEL_DATABASE_NOT_OPEN` after teardown. Fast path when empty.
171
+ await this.agent.dwn.drainPendingEagerSends();
172
+
151
173
  await this.didResolverCache.close();
152
174
  await this.dwnDataStore.close();
153
175
  await this.dwnStateIndex.close();
154
176
  await this.dwnMessageStore.close();
155
177
  await this.dwnResumableTaskStore.close();
178
+ if (this.secretStore) { await this.secretStore.close(); }
156
179
  await this.syncStore.close();
157
180
  await this.vaultStore.close();
158
181
  }
@@ -248,7 +271,9 @@ export class PlatformAgentTestHarness {
248
271
  keyManager,
249
272
  didResolverCache,
250
273
  vaultStore,
251
- permissionsApi
274
+ permissionsApi,
275
+ secretsApi,
276
+ secretStore,
252
277
  } = (agentStores === 'memory')
253
278
  ? PlatformAgentTestHarness.useMemoryStores()
254
279
  : PlatformAgentTestHarness.useDiskStores({ testDataLocation, stores: dwnStores });
@@ -294,6 +319,7 @@ export class PlatformAgentTestHarness {
294
319
  keyManager,
295
320
  permissionsApi,
296
321
  rpcClient,
322
+ secretsApi,
297
323
  syncApi,
298
324
  });
299
325
 
@@ -307,8 +333,9 @@ export class PlatformAgentTestHarness {
307
333
  dwnMessageStore,
308
334
  dwnResumableTaskStore,
309
335
  dwnStores,
336
+ secretStore,
310
337
  syncStore,
311
- vaultStore
338
+ vaultStore,
312
339
  });
313
340
  }
314
341
 
@@ -346,7 +373,13 @@ export class PlatformAgentTestHarness {
346
373
 
347
374
  const permissionsApi = new AgentPermissionsApi({ agent });
348
375
 
349
- return { agentVault, didApi, didResolverCache, identityApi, keyManager, permissionsApi, vaultStore };
376
+ const secretStore = new LevelStore<string, string>({ location: testDataPath('SECRET_STORE') });
377
+ const secretsApi = new VaultBackedSecretStore({
378
+ vault : agentVault,
379
+ store : secretStore,
380
+ });
381
+
382
+ return { agentVault, didApi, didResolverCache, identityApi, keyManager, permissionsApi, secretsApi, secretStore, vaultStore };
350
383
  }
351
384
 
352
385
  private static useMemoryStores({ agent }: { agent?: EnboxPlatformAgent<LocalKeyManager> } = {}): StoreSetupResult {
@@ -369,6 +402,8 @@ export class PlatformAgentTestHarness {
369
402
 
370
403
  const permissionsApi = new AgentPermissionsApi({ agent });
371
404
 
372
- return { agentVault, didApi, didResolverCache, identityApi, keyManager, permissionsApi, vaultStore };
405
+ const secretsApi = new InMemorySecretStore();
406
+
407
+ return { agentVault, didApi, didResolverCache, identityApi, keyManager, permissionsApi, secretsApi, vaultStore };
373
408
  }
374
409
  }
@@ -7,6 +7,7 @@ import type { AgentKeyManager } from './key-manager.js';
7
7
  import type { AgentPermissionsApi } from '../permissions-api.js';
8
8
  import type { EnboxRpc } from '@enbox/dwn-clients';
9
9
  import type { IdentityVault } from './identity-vault.js';
10
+ import type { SecretStore } from '../secret-store.js';
10
11
  import type { SyncEngine } from './sync.js';
11
12
  import type { AgentDidApi, DidInterface, DidRequest, DidResponse } from '../did-api.js';
12
13
  import type { DwnInterface, DwnResponse, ProcessDwnRequest, SendDwnRequest } from './dwn.js';
@@ -169,6 +170,8 @@ export interface EnboxPlatformAgent<TKeyManager extends AgentKeyManager = AgentK
169
170
  */
170
171
  sync: SyncEngine;
171
172
 
173
+ /** Vault-backed secret store for classified credentials, encrypted at rest with the vault key. */
174
+ secrets: SecretStore;
172
175
  /**
173
176
  * An instance of {@link IdentityVault}, providing secure storage and management of an Enbox Agent's
174
177
  * DID and cryptographic keys.
package/src/types/sync.ts CHANGED
@@ -11,9 +11,11 @@ export type SyncIdentityOptions = {
11
11
  */
12
12
  delegateDid?: string;
13
13
  /**
14
- * The protocols that should be synced for this identity, if an empty array is provided, all messages for all protocols will be synced.
14
+ * The protocols that should be synced for this identity.
15
+ * - `'all'` — sync all protocols (full replica).
16
+ * - `string[]` — sync only the listed protocol URIs.
15
17
  */
16
- protocols: string[];
18
+ protocols: 'all' | [string, ...string[]];
17
19
  };
18
20
 
19
21
  /**
@@ -65,10 +67,10 @@ export async function computeScopeId(scope: SyncScope): Promise<string> {
65
67
  if (scope.kind === 'protocol') {
66
68
  canonical.protocol = scope.protocol;
67
69
  if (scope.protocolPathPrefixes !== undefined) {
68
- canonical.protocolPathPrefixes = [...new Set(scope.protocolPathPrefixes)].sort();
70
+ canonical.protocolPathPrefixes = [...new Set(scope.protocolPathPrefixes)].sort((a, b) => a.localeCompare(b));
69
71
  }
70
72
  if (scope.contextIdPrefixes !== undefined) {
71
- canonical.contextIdPrefixes = [...new Set(scope.contextIdPrefixes)].sort();
73
+ canonical.contextIdPrefixes = [...new Set(scope.contextIdPrefixes)].sort((a, b) => a.localeCompare(b));
72
74
  }
73
75
  }
74
76
 
@@ -83,7 +85,11 @@ export async function computeScopeId(scope: SyncScope): Promise<string> {
83
85
  for (const b of hashArray) {
84
86
  base64 += String.fromCharCode(b);
85
87
  }
86
- return btoa(base64).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
88
+ const result = btoa(base64).replaceAll('+', '-').replaceAll('/', '_');
89
+ // Strip trailing '=' padding without regex quantifiers (avoids ReDoS scanners).
90
+ let end = result.length;
91
+ while (end > 0 && result.codePointAt(end - 1) === 61) { end--; } // 61 === '='
92
+ return end === result.length ? result : result.slice(0, end);
87
93
  }
88
94
 
89
95
  // ---------------------------------------------------------------------------
@@ -323,13 +329,35 @@ export interface SyncEngine {
323
329
  */
324
330
  readonly connectivityState: SyncConnectivityState;
325
331
 
332
+ /**
333
+ * Whether at least one live pull or push subscription is open.
334
+ *
335
+ * This is specifically about live-mode subscriptions — it is `false` in
336
+ * poll mode and `false` when only the integrity timer remains (e.g. after
337
+ * the last identity was removed). Callers use this to avoid calling
338
+ * `startSync()` when live subscriptions are active, which would tear
339
+ * them all down and rebuild from scratch.
340
+ */
341
+ readonly hasActiveSubscriptions: boolean;
342
+
326
343
  /**
327
344
  * Register an identity to be managed by the SyncEngine for syncing.
328
- * The options can define specific protocols that should only be synced, or a delegate DID that should be used to sign the sync messages.
345
+ * Callers must explicitly specify which protocols to sync (`'all'` for a
346
+ * full replica, or a list of protocol URIs) so that sync scope is always
347
+ * a deliberate choice rather than an invisible default.
348
+ *
349
+ * When live sync is active, the new identity is hot-added: its replication
350
+ * links are created and subscriptions opened immediately, without tearing
351
+ * down existing subscriptions for other identities. This enables
352
+ * multi-identity agents (e.g. ElectroBun desktop DWN, multi-persona dApps)
353
+ * to add identities at runtime without disrupting sync for others.
329
354
  */
330
- registerIdentity(params: { did: string, options?: SyncIdentityOptions }): Promise<void>;
355
+ registerIdentity(params: { did: string, options: SyncIdentityOptions }): Promise<void>;
331
356
  /**
332
357
  * Unregister an identity from the SyncEngine, this will stop syncing messages for this identity.
358
+ *
359
+ * When live sync is active, the identity is hot-removed: its subscriptions
360
+ * are closed and runtime state cleaned up without affecting other identities.
333
361
  */
334
362
  unregisterIdentity(did: string): Promise<void>;
335
363
  /**
package/src/utils.ts CHANGED
@@ -19,7 +19,7 @@ export async function getDwnServiceEndpointUrls(didUri: string, dereferencer: Di
19
19
  ? [serviceEndpoint]
20
20
  : Array.isArray(serviceEndpoint) && serviceEndpoint.every(endpoint => typeof endpoint === 'string')
21
21
  // If the service endpoint is an array of strings, use it as is.
22
- ? serviceEndpoint as string[]
22
+ ? serviceEndpoint
23
23
  // If the service endpoint is neither a string nor an array of strings, return an empty array.
24
24
  : [];
25
25