@enbox/agent 0.6.4 → 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.
- package/README.md +18 -5
- package/dist/browser.mjs +11 -11
- package/dist/browser.mjs.map +4 -4
- package/dist/esm/agent-did-resolver-cache.js +5 -5
- package/dist/esm/agent-did-resolver-cache.js.map +1 -1
- package/dist/esm/crypto-api.js.map +1 -1
- package/dist/esm/did-api.js +1 -1
- package/dist/esm/did-api.js.map +1 -1
- package/dist/esm/dwn-api.js +93 -53
- package/dist/esm/dwn-api.js.map +1 -1
- package/dist/esm/dwn-discovery-payload.js +7 -4
- package/dist/esm/dwn-discovery-payload.js.map +1 -1
- package/dist/esm/dwn-key-delivery.js +8 -3
- package/dist/esm/dwn-key-delivery.js.map +1 -1
- package/dist/esm/enbox-connect-protocol.js +34 -14
- package/dist/esm/enbox-connect-protocol.js.map +1 -1
- package/dist/esm/enbox-user-agent.js +11 -3
- package/dist/esm/enbox-user-agent.js.map +1 -1
- package/dist/esm/hd-identity-vault.js +33 -18
- package/dist/esm/hd-identity-vault.js.map +1 -1
- package/dist/esm/identity-api.js +5 -4
- package/dist/esm/identity-api.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/local-dwn.js.map +1 -1
- package/dist/esm/local-key-manager.js.map +1 -1
- package/dist/esm/permissions-api.js +9 -5
- package/dist/esm/permissions-api.js.map +1 -1
- package/dist/esm/prototyping/crypto/jose/jwe-flattened.js +9 -9
- package/dist/esm/prototyping/crypto/jose/jwe-flattened.js.map +1 -1
- package/dist/esm/secret-store.js +106 -0
- package/dist/esm/secret-store.js.map +1 -0
- package/dist/esm/store-data.js +32 -11
- package/dist/esm/store-data.js.map +1 -1
- package/dist/esm/sync-closure-resolver.js +1 -1
- package/dist/esm/sync-closure-resolver.js.map +1 -1
- package/dist/esm/sync-engine-level.js +418 -141
- package/dist/esm/sync-engine-level.js.map +1 -1
- package/dist/esm/sync-replication-ledger.js +25 -0
- package/dist/esm/sync-replication-ledger.js.map +1 -1
- package/dist/esm/test-harness.js +32 -5
- package/dist/esm/test-harness.js.map +1 -1
- package/dist/esm/types/sync.js +9 -3
- package/dist/esm/types/sync.js.map +1 -1
- package/dist/esm/utils.js.map +1 -1
- package/dist/types/agent-did-resolver-cache.d.ts +1 -1
- package/dist/types/agent-did-resolver-cache.d.ts.map +1 -1
- package/dist/types/anonymous-dwn-api.d.ts +2 -2
- package/dist/types/anonymous-dwn-api.d.ts.map +1 -1
- package/dist/types/crypto-api.d.ts +1 -1
- package/dist/types/crypto-api.d.ts.map +1 -1
- package/dist/types/did-api.d.ts +2 -2
- package/dist/types/did-api.d.ts.map +1 -1
- package/dist/types/dwn-api.d.ts +51 -11
- package/dist/types/dwn-api.d.ts.map +1 -1
- package/dist/types/dwn-key-delivery.d.ts +4 -1
- package/dist/types/dwn-key-delivery.d.ts.map +1 -1
- package/dist/types/enbox-connect-protocol.d.ts +3 -2
- package/dist/types/enbox-connect-protocol.d.ts.map +1 -1
- package/dist/types/enbox-user-agent.d.ts +5 -1
- package/dist/types/enbox-user-agent.d.ts.map +1 -1
- package/dist/types/hd-identity-vault.d.ts +9 -2
- package/dist/types/hd-identity-vault.d.ts.map +1 -1
- package/dist/types/identity-api.d.ts +1 -1
- package/dist/types/identity-api.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/local-dwn.d.ts +3 -3
- package/dist/types/local-dwn.d.ts.map +1 -1
- package/dist/types/local-key-manager.d.ts +2 -2
- package/dist/types/local-key-manager.d.ts.map +1 -1
- package/dist/types/permissions-api.d.ts +1 -1
- package/dist/types/permissions-api.d.ts.map +1 -1
- package/dist/types/secret-store.d.ts +81 -0
- package/dist/types/secret-store.d.ts.map +1 -0
- package/dist/types/store-data.d.ts +15 -3
- package/dist/types/store-data.d.ts.map +1 -1
- package/dist/types/sync-engine-level.d.ts +52 -16
- package/dist/types/sync-engine-level.d.ts.map +1 -1
- package/dist/types/sync-replication-ledger.d.ts +10 -1
- package/dist/types/sync-replication-ledger.d.ts.map +1 -1
- package/dist/types/test-harness.d.ts +3 -0
- package/dist/types/test-harness.d.ts.map +1 -1
- package/dist/types/types/agent.d.ts +3 -0
- package/dist/types/types/agent.d.ts.map +1 -1
- package/dist/types/types/sync.d.ts +27 -4
- package/dist/types/types/sync.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agent-did-resolver-cache.ts +5 -5
- package/src/anonymous-dwn-api.ts +2 -2
- package/src/crypto-api.ts +1 -1
- package/src/did-api.ts +3 -3
- package/src/dwn-api.ts +107 -69
- package/src/dwn-discovery-payload.ts +5 -4
- package/src/dwn-key-delivery.ts +8 -2
- package/src/enbox-connect-protocol.ts +38 -21
- package/src/enbox-user-agent.ts +15 -3
- package/src/hd-identity-vault.ts +47 -21
- package/src/identity-api.ts +6 -5
- package/src/index.ts +1 -0
- package/src/local-dwn.ts +3 -3
- package/src/local-key-manager.ts +2 -2
- package/src/permissions-api.ts +12 -8
- package/src/prototyping/crypto/jose/jwe-flattened.ts +8 -8
- package/src/secret-store.ts +173 -0
- package/src/store-data.ts +40 -14
- package/src/sync-closure-resolver.ts +2 -2
- package/src/sync-engine-level.ts +423 -162
- package/src/sync-replication-ledger.ts +26 -1
- package/src/test-harness.ts +40 -5
- package/src/types/agent.ts +3 -0
- package/src/types/sync.ts +35 -7
- package/src/utils.ts +1 -1
package/src/hd-identity-vault.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { DidDhtCreateOptions } from '@enbox/dids';
|
|
2
1
|
import type { Jwk } from '@enbox/crypto';
|
|
3
2
|
import type { KeyValueStore } from '@enbox/common';
|
|
4
3
|
|
|
4
|
+
import type { DidDhtCreateOptions, PortableDid } from '@enbox/dids';
|
|
5
|
+
|
|
5
6
|
import { HDKey } from 'ed25519-keygen/hdkey';
|
|
6
7
|
import { wordlist } from '@scure/bip39/wordlists/english';
|
|
7
8
|
import { BearerDid, DidDht, isPortableDid } from '@enbox/dids';
|
|
@@ -142,10 +143,10 @@ export class HdIdentityVault implements IdentityVault<{ InitializeResult: string
|
|
|
142
143
|
public crypto = new AgentCryptoApi();
|
|
143
144
|
|
|
144
145
|
/** Determines the computational intensity of the key derivation process. */
|
|
145
|
-
private _keyDerivationWorkFactor: number;
|
|
146
|
+
private readonly _keyDerivationWorkFactor: number;
|
|
146
147
|
|
|
147
148
|
/** The underlying key-value store for the vault's encrypted content. */
|
|
148
|
-
private _store: KeyValueStore<string, string>;
|
|
149
|
+
private readonly _store: KeyValueStore<string, string>;
|
|
149
150
|
|
|
150
151
|
/** The cryptographic key used to encrypt and decrypt the vault's content securely. */
|
|
151
152
|
private _contentEncryptionKey: Jwk | undefined;
|
|
@@ -158,6 +159,14 @@ export class HdIdentityVault implements IdentityVault<{ InitializeResult: string
|
|
|
158
159
|
*/
|
|
159
160
|
private _cachedInitialized: boolean | undefined;
|
|
160
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Cached decrypted PortableDid from the last successful {@link getDid} call.
|
|
164
|
+
* Caching the PortableDid (not the BearerDid) avoids the expensive JWE
|
|
165
|
+
* decrypt + LevelDB read on every call, while still returning a fresh
|
|
166
|
+
* BearerDid instance each time so callers cannot mutate shared state.
|
|
167
|
+
*/
|
|
168
|
+
private _cachedPortableDid: PortableDid | undefined;
|
|
169
|
+
|
|
161
170
|
/**
|
|
162
171
|
* Constructs an instance of `HdIdentityVault`, initializing the key derivation factor and data
|
|
163
172
|
* store. It sets the default key derivation work factor and initializes the internal data store,
|
|
@@ -298,26 +307,34 @@ export class HdIdentityVault implements IdentityVault<{ InitializeResult: string
|
|
|
298
307
|
throw new Error(`HdIdentityVault: Vault has not been initialized and unlocked.`);
|
|
299
308
|
}
|
|
300
309
|
|
|
301
|
-
//
|
|
302
|
-
|
|
310
|
+
// If the decrypted PortableDid is not yet cached, decrypt it from the
|
|
311
|
+
// vault store. This avoids the expensive LevelDB read + JWE decrypt on
|
|
312
|
+
// every call while the vault is unlocked.
|
|
313
|
+
if (!this._cachedPortableDid) {
|
|
314
|
+
const didJwe = await this.getStoredDid();
|
|
303
315
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
316
|
+
const { plaintext: portableDidBytes } = await CompactJwe.decrypt({
|
|
317
|
+
jwe : didJwe,
|
|
318
|
+
key : this._contentEncryptionKey!,
|
|
319
|
+
crypto : this.crypto,
|
|
320
|
+
keyManager : new LocalKeyManager(),
|
|
321
|
+
options : { minP2cCount: 1 }, // Vault decrypts its own JWEs; no external-input floor needed.
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const portableDid = Convert.uint8Array(portableDidBytes).toObject();
|
|
325
|
+
if (!isPortableDid(portableDid)) {
|
|
326
|
+
throw new Error('HdIdentityVault: Unable to decode malformed DID in identity vault');
|
|
327
|
+
}
|
|
312
328
|
|
|
313
|
-
|
|
314
|
-
const portableDid = Convert.uint8Array(portableDidBytes).toObject();
|
|
315
|
-
if (!isPortableDid(portableDid)) {
|
|
316
|
-
throw new Error('HdIdentityVault: Unable to decode malformed DID in identity vault');
|
|
329
|
+
this._cachedPortableDid = portableDid;
|
|
317
330
|
}
|
|
318
331
|
|
|
319
|
-
//
|
|
320
|
-
|
|
332
|
+
// Always return a fresh BearerDid from a deep copy of the cached
|
|
333
|
+
// PortableDid. BearerDid is mutable (document, metadata, keyManager
|
|
334
|
+
// are public) and BearerDid.import() takes document/metadata by
|
|
335
|
+
// reference, so without the clone a caller mutating a returned DID
|
|
336
|
+
// would corrupt the cache for all subsequent getDid() calls.
|
|
337
|
+
return await BearerDid.import({ portableDid: structuredClone(this._cachedPortableDid) });
|
|
321
338
|
}
|
|
322
339
|
|
|
323
340
|
/**
|
|
@@ -661,6 +678,7 @@ export class HdIdentityVault implements IdentityVault<{ InitializeResult: string
|
|
|
661
678
|
// Clear the vault content encryption key (CEK), effectively locking the vault.
|
|
662
679
|
if (this._contentEncryptionKey) {this._contentEncryptionKey.k = '';}
|
|
663
680
|
this._contentEncryptionKey = undefined;
|
|
681
|
+
this._cachedPortableDid = undefined;
|
|
664
682
|
}
|
|
665
683
|
|
|
666
684
|
/**
|
|
@@ -767,8 +785,16 @@ export class HdIdentityVault implements IdentityVault<{ InitializeResult: string
|
|
|
767
785
|
* incorrect.
|
|
768
786
|
*/
|
|
769
787
|
public async unlock({ password }: { password: string }): Promise<void> {
|
|
770
|
-
//
|
|
771
|
-
await this.
|
|
788
|
+
// Verify the vault has been initialized before attempting to unlock.
|
|
789
|
+
if (await this.isInitialized() === false) {
|
|
790
|
+
throw new Error(`HdIdentityVault: Unable to unlock the vault. Vault has not been initialized.`);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Lock the vault if not already locked — avoids an unnecessary
|
|
794
|
+
// redundant isInitialized() store read inside lock().
|
|
795
|
+
if (!this.isLocked()) {
|
|
796
|
+
await this.lock();
|
|
797
|
+
}
|
|
772
798
|
|
|
773
799
|
// Retrieve the content encryption key (CEK) record as a compact JWE from the data store.
|
|
774
800
|
const cekJwe = await this.getStoredContentEncryptionKey();
|
package/src/identity-api.ts
CHANGED
|
@@ -54,7 +54,7 @@ export class AgentIdentityApi<TKeyManager extends AgentKeyManager = AgentKeyMana
|
|
|
54
54
|
*/
|
|
55
55
|
private _agent?: EnboxPlatformAgent<TKeyManager>;
|
|
56
56
|
|
|
57
|
-
private _store: AgentDataStore<IdentityMetadata>;
|
|
57
|
+
private readonly _store: AgentDataStore<IdentityMetadata>;
|
|
58
58
|
|
|
59
59
|
constructor({ agent, store }: IdentityApiParams<TKeyManager> = {}) {
|
|
60
60
|
this._agent = agent;
|
|
@@ -256,11 +256,12 @@ export class AgentIdentityApi<TKeyManager extends AgentKeyManager = AgentKeyMana
|
|
|
256
256
|
};
|
|
257
257
|
|
|
258
258
|
// if no other services exist, create a new array with the DWN service
|
|
259
|
-
if (
|
|
260
|
-
|
|
261
|
-
} else {
|
|
262
|
-
// otherwise, push the new DWN service to the existing services
|
|
259
|
+
if (portableDid.document.service) {
|
|
260
|
+
// push the new DWN service to the existing services
|
|
263
261
|
portableDid.document.service.push(newDwnService);
|
|
262
|
+
} else {
|
|
263
|
+
// no other services exist, create a new array with the DWN service
|
|
264
|
+
portableDid.document.service = [newDwnService];
|
|
264
265
|
}
|
|
265
266
|
}
|
|
266
267
|
|
package/src/index.ts
CHANGED
|
@@ -32,6 +32,7 @@ export * from './identity-api.js';
|
|
|
32
32
|
export * from './local-dwn.js';
|
|
33
33
|
export * from './local-key-manager.js';
|
|
34
34
|
export * from './permissions-api.js';
|
|
35
|
+
export * from './secret-store.js';
|
|
35
36
|
export * from './store-data.js';
|
|
36
37
|
export * from './store-did.js';
|
|
37
38
|
export * from './store-identity.js';
|
package/src/local-dwn.ts
CHANGED
|
@@ -62,9 +62,9 @@ export class LocalDwnDiscovery {
|
|
|
62
62
|
private _cacheExpiry = 0;
|
|
63
63
|
|
|
64
64
|
constructor(
|
|
65
|
-
private _rpcClient: EnboxRpc,
|
|
66
|
-
private _cacheTtlMs = 10_000,
|
|
67
|
-
private _discoveryFile?: DwnDiscoveryFile,
|
|
65
|
+
private readonly _rpcClient: EnboxRpc,
|
|
66
|
+
private readonly _cacheTtlMs = 10_000,
|
|
67
|
+
private readonly _discoveryFile?: DwnDiscoveryFile,
|
|
68
68
|
) {}
|
|
69
69
|
|
|
70
70
|
/**
|
package/src/local-key-manager.ts
CHANGED
|
@@ -177,7 +177,7 @@ export class LocalKeyManager implements AgentKeyManager {
|
|
|
177
177
|
* that implements a specific cryptographic algorithm. This map is used to cache and reuse
|
|
178
178
|
* instances for performance optimization, ensuring that each algorithm is instantiated only once.
|
|
179
179
|
*/
|
|
180
|
-
private _algorithmInstances: Map<AlgorithmConstructor, InstanceType<typeof CryptoAlgorithm>> = new Map();
|
|
180
|
+
private readonly _algorithmInstances: Map<AlgorithmConstructor, InstanceType<typeof CryptoAlgorithm>> = new Map();
|
|
181
181
|
|
|
182
182
|
/**
|
|
183
183
|
* The `_keyStore` private variable in `LocalKeyManager` is a {@link AgentDataStore} instance used
|
|
@@ -187,7 +187,7 @@ export class LocalKeyManager implements AgentKeyManager {
|
|
|
187
187
|
* persistent storage, providing flexibility in key management according to the application's
|
|
188
188
|
* requirements.
|
|
189
189
|
*/
|
|
190
|
-
private _keyStore: AgentDataStore<Jwk>;
|
|
190
|
+
private readonly _keyStore: AgentDataStore<Jwk>;
|
|
191
191
|
|
|
192
192
|
constructor({ agent, keyStore }: LocalKmsParams = {}) {
|
|
193
193
|
this._agent = agent;
|
package/src/permissions-api.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EnboxAgent } from './types/agent.js';
|
|
2
2
|
import type { CreateGrantParams, CreateRequestParams, CreateRevocationParams, FetchPermissionRequestParams, FetchPermissionsParams, GetPermissionParams, IsGrantRevokedParams, PermissionGrantEntry, PermissionRequestEntry, PermissionRevocationEntry, PermissionsApi } from './types/permissions.js';
|
|
3
|
-
import type { DwnDataEncodedRecordsWriteMessage, DwnMessageParams,
|
|
3
|
+
import type { DwnDataEncodedRecordsWriteMessage, DwnMessageParams, DwnPermissionScope, DwnRecordsPermissionScope, ProcessDwnRequest } from './types/dwn.js';
|
|
4
4
|
import type { PermissionGrant, PermissionGrantData, PermissionRequestData, PermissionRevocationData } from '@enbox/dwn-sdk-js';
|
|
5
5
|
|
|
6
6
|
import { isRecordsType } from './dwn-api.js';
|
|
@@ -11,7 +11,7 @@ import { DwnInterface, DwnPermissionGrant, DwnPermissionRequest } from './types/
|
|
|
11
11
|
export class AgentPermissionsApi implements PermissionsApi {
|
|
12
12
|
|
|
13
13
|
/** cache for fetching a permission {@link PermissionGrant}, keyed by a specific MessageType and protocol */
|
|
14
|
-
private _cachedPermissions: TtlCache<string, PermissionGrantEntry> = new TtlCache({ ttl: 60 * 1000 });
|
|
14
|
+
private readonly _cachedPermissions: TtlCache<string, PermissionGrantEntry> = new TtlCache({ ttl: 60 * 1000 });
|
|
15
15
|
|
|
16
16
|
private _agent?: EnboxAgent;
|
|
17
17
|
|
|
@@ -445,11 +445,15 @@ export class AgentPermissionsApi implements PermissionsApi {
|
|
|
445
445
|
const scope = grant.scope;
|
|
446
446
|
const scopeMessageType = scope.interface + scope.method;
|
|
447
447
|
|
|
448
|
-
// Messages.Read is
|
|
449
|
-
//
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
448
|
+
// Messages.Read is the only valid Messages scope and covers Read, Sync, and Subscribe operations.
|
|
449
|
+
// Defensively require method === Read so malformed/legacy grants with method Sync/Subscribe
|
|
450
|
+
// are rejected rather than treated as valid scopes.
|
|
451
|
+
const isMessagesScopeMatch = scope.interface === 'Messages'
|
|
452
|
+
? scope.method === 'Read'
|
|
453
|
+
&& (messageType === DwnInterface.MessagesRead
|
|
454
|
+
|| messageType === DwnInterface.MessagesSync
|
|
455
|
+
|| messageType === DwnInterface.MessagesSubscribe)
|
|
456
|
+
: scopeMessageType === messageType;
|
|
453
457
|
|
|
454
458
|
if (isMessagesScopeMatch) {
|
|
455
459
|
if (isRecordsType(messageType)) {
|
|
@@ -474,7 +478,7 @@ export class AgentPermissionsApi implements PermissionsApi {
|
|
|
474
478
|
return true;
|
|
475
479
|
}
|
|
476
480
|
} else {
|
|
477
|
-
const messagesScope = scope
|
|
481
|
+
const messagesScope = scope;
|
|
478
482
|
// Checks for unrestricted protocol scope, if no protocol is defined in the scope it is unrestricted
|
|
479
483
|
if (messagesScope.protocol === undefined) {
|
|
480
484
|
return true;
|
|
@@ -301,24 +301,24 @@ export class FlattenedJwe {
|
|
|
301
301
|
const tag = decodeHeaderParam('tag', jwe.tag);
|
|
302
302
|
|
|
303
303
|
// Decode the JWE Ciphertext to a byte array, and if present, append the Authentication Tag.
|
|
304
|
-
const ciphertext = tag
|
|
305
|
-
?
|
|
304
|
+
const ciphertext = tag === undefined
|
|
305
|
+
? Convert.base64Url(jwe.ciphertext).toUint8Array()
|
|
306
|
+
: new Uint8Array([
|
|
306
307
|
...Convert.base64Url(jwe.ciphertext).toUint8Array(),
|
|
307
308
|
...(tag ?? [])
|
|
308
|
-
])
|
|
309
|
-
: Convert.base64Url(jwe.ciphertext).toUint8Array();
|
|
309
|
+
]);
|
|
310
310
|
|
|
311
311
|
// If the JWE Additional Authenticated Data (AAD) is present, the Additional Authenticated Data
|
|
312
312
|
// input to the Content Encryption Algorithm is
|
|
313
313
|
// ASCII(Encoded Protected Header || '.' || BASE64URL(JWE AAD)). If the JWE AAD is absent, the
|
|
314
314
|
// Additional Authenticated Data is ASCII(BASE64URL(UTF8(JWE Protected Header))).
|
|
315
|
-
const additionalData = jwe.aad
|
|
316
|
-
?
|
|
315
|
+
const additionalData = jwe.aad === undefined
|
|
316
|
+
? Convert.string(jwe.protected ?? '').toUint8Array()
|
|
317
|
+
: new Uint8Array([
|
|
317
318
|
...Convert.string(jwe.protected ?? '').toUint8Array(),
|
|
318
319
|
...Convert.string('.').toUint8Array(),
|
|
319
320
|
...Convert.string(jwe.aad).toUint8Array()
|
|
320
|
-
])
|
|
321
|
-
: Convert.string(jwe.protected ?? '').toUint8Array();
|
|
321
|
+
]);
|
|
322
322
|
|
|
323
323
|
// Decrypt the JWE using the Content Encryption Key (CEK) with:
|
|
324
324
|
// - Key Manager: If the CEK is a Key Identifier.
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { KeyValueStore } from '@enbox/common';
|
|
2
|
+
|
|
3
|
+
import type { IdentityVault } from './types/identity-vault.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Options for storing a secret.
|
|
7
|
+
*/
|
|
8
|
+
export type SecretStorePutOptions = {
|
|
9
|
+
/**
|
|
10
|
+
* ISO-8601 timestamp after which the secret is considered expired.
|
|
11
|
+
*
|
|
12
|
+
* Expired secrets are silently deleted on the next {@link SecretStore.get}
|
|
13
|
+
* call and `undefined` is returned.
|
|
14
|
+
*/
|
|
15
|
+
expiresAt?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Platform-agnostic interface for storing classified secrets encrypted at rest
|
|
20
|
+
* by the agent vault.
|
|
21
|
+
*
|
|
22
|
+
* Implementations must ensure that plaintext secret material is never persisted
|
|
23
|
+
* to browser-accessible storage such as `localStorage` or `sessionStorage`.
|
|
24
|
+
*/
|
|
25
|
+
export interface SecretStore {
|
|
26
|
+
/**
|
|
27
|
+
* Store a secret, encrypted at rest with the vault key.
|
|
28
|
+
*
|
|
29
|
+
* @param key - Unique identifier for the secret.
|
|
30
|
+
* @param value - The secret bytes to store.
|
|
31
|
+
* @param options - Optional metadata (e.g. TTL).
|
|
32
|
+
* @throws If the vault is locked.
|
|
33
|
+
*/
|
|
34
|
+
put(key: string, value: Uint8Array, options?: SecretStorePutOptions): Promise<void>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Retrieve and decrypt a secret.
|
|
38
|
+
*
|
|
39
|
+
* Returns `undefined` if the key does not exist or the secret has expired.
|
|
40
|
+
*
|
|
41
|
+
* @param key - The identifier used when the secret was stored.
|
|
42
|
+
* @throws If the vault is locked.
|
|
43
|
+
*/
|
|
44
|
+
get(key: string): Promise<Uint8Array | undefined>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Delete a secret.
|
|
48
|
+
*
|
|
49
|
+
* @param key - The identifier used when the secret was stored.
|
|
50
|
+
* @returns `true` if the key existed and was removed, `false` otherwise.
|
|
51
|
+
*/
|
|
52
|
+
delete(key: string): Promise<boolean>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Internal envelope persisted in the backing KeyValueStore.
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
/** @internal */
|
|
60
|
+
type SecretEnvelope = {
|
|
61
|
+
/** Compact JWE encrypted by the vault's content encryption key. */
|
|
62
|
+
jwe: string;
|
|
63
|
+
|
|
64
|
+
/** Optional ISO-8601 expiry timestamp. */
|
|
65
|
+
expiresAt?: string;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// VaultBackedSecretStore
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Vault-backed implementation of {@link SecretStore}.
|
|
74
|
+
*
|
|
75
|
+
* Secrets are encrypted with the vault's content encryption key (AES-256-GCM
|
|
76
|
+
* via `dir` algorithm) before being persisted to the backing
|
|
77
|
+
* {@link KeyValueStore}. The vault must be unlocked for {@link put} and
|
|
78
|
+
* {@link get} operations; {@link delete} does not require the vault to be
|
|
79
|
+
* unlocked since it only removes the encrypted envelope.
|
|
80
|
+
*/
|
|
81
|
+
export class VaultBackedSecretStore implements SecretStore {
|
|
82
|
+
private readonly _vault: IdentityVault;
|
|
83
|
+
private readonly _store: KeyValueStore<string, string>;
|
|
84
|
+
|
|
85
|
+
constructor({ vault, store }: {
|
|
86
|
+
vault : IdentityVault;
|
|
87
|
+
store : KeyValueStore<string, string>;
|
|
88
|
+
}) {
|
|
89
|
+
this._vault = vault;
|
|
90
|
+
this._store = store;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public async put(
|
|
94
|
+
key: string,
|
|
95
|
+
value: Uint8Array,
|
|
96
|
+
options?: SecretStorePutOptions,
|
|
97
|
+
): Promise<void> {
|
|
98
|
+
// vault.encryptData() throws if the vault is locked.
|
|
99
|
+
const jwe = await this._vault.encryptData({ plaintext: value });
|
|
100
|
+
|
|
101
|
+
const envelope: SecretEnvelope = { jwe };
|
|
102
|
+
if (options?.expiresAt !== undefined) {
|
|
103
|
+
envelope.expiresAt = options.expiresAt;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
await this._store.set(key, JSON.stringify(envelope));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public async get(key: string): Promise<Uint8Array | undefined> {
|
|
110
|
+
const raw = await this._store.get(key);
|
|
111
|
+
if (raw === undefined) { return undefined; }
|
|
112
|
+
|
|
113
|
+
const envelope = JSON.parse(raw) as SecretEnvelope;
|
|
114
|
+
|
|
115
|
+
// Purge expired secrets.
|
|
116
|
+
if (envelope.expiresAt !== undefined && new Date(envelope.expiresAt).getTime() <= Date.now()) {
|
|
117
|
+
await this._store.delete(key);
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// vault.decryptData() throws if the vault is locked.
|
|
122
|
+
return this._vault.decryptData({ jwe: envelope.jwe });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
public async delete(key: string): Promise<boolean> {
|
|
126
|
+
const existing = await this._store.get(key);
|
|
127
|
+
if (existing === undefined) { return false; }
|
|
128
|
+
await this._store.delete(key);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// InMemorySecretStore
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* In-memory implementation of {@link SecretStore} for testing.
|
|
139
|
+
*
|
|
140
|
+
* Secrets are stored as plain `Uint8Array` values in a `Map` — no encryption,
|
|
141
|
+
* no persistence. This is suitable only for unit tests.
|
|
142
|
+
*/
|
|
143
|
+
export class InMemorySecretStore implements SecretStore {
|
|
144
|
+
private readonly _store = new Map<string, { value: Uint8Array; expiresAt?: string }>();
|
|
145
|
+
|
|
146
|
+
public async put(
|
|
147
|
+
key: string,
|
|
148
|
+
value: Uint8Array,
|
|
149
|
+
options?: SecretStorePutOptions,
|
|
150
|
+
): Promise<void> {
|
|
151
|
+
this._store.set(key, {
|
|
152
|
+
value : new Uint8Array(value),
|
|
153
|
+
expiresAt : options?.expiresAt,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
public async get(key: string): Promise<Uint8Array | undefined> {
|
|
158
|
+
const entry = this._store.get(key);
|
|
159
|
+
if (entry === undefined) { return undefined; }
|
|
160
|
+
|
|
161
|
+
// Purge expired secrets.
|
|
162
|
+
if (entry.expiresAt !== undefined && new Date(entry.expiresAt).getTime() <= Date.now()) {
|
|
163
|
+
this._store.delete(key);
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return new Uint8Array(entry.value);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
public async delete(key: string): Promise<boolean> {
|
|
171
|
+
return this._store.delete(key);
|
|
172
|
+
}
|
|
173
|
+
}
|
package/src/store-data.ts
CHANGED
|
@@ -69,9 +69,14 @@ export class DwnDataStore<TStoreObject extends Record<string, any> = Jwk> implem
|
|
|
69
69
|
* Cache of tenant DIDs that have been initialized with the protocol.
|
|
70
70
|
* This is used to avoid redundant protocol initialization requests.
|
|
71
71
|
*
|
|
72
|
-
*
|
|
72
|
+
* Uses TtlCache with a 1-hour TTL rather than a permanent Set so that
|
|
73
|
+
* protocol state changes by another agent/process (protocol upgrade,
|
|
74
|
+
* reinstall, or tenant clear) are eventually re-detected. Both this
|
|
75
|
+
* and `_tenantEncryptionActive` must share the same TTL so they expire
|
|
76
|
+
* together — otherwise `initialize()` could return early while the
|
|
77
|
+
* encryption state is stale.
|
|
73
78
|
*/
|
|
74
|
-
protected _protocolInitializedCache: TtlCache<string, boolean> = new TtlCache({ ttl: ms('
|
|
79
|
+
protected _protocolInitializedCache: TtlCache<string, boolean> = new TtlCache({ ttl: ms('1 hour'), max: 1000 });
|
|
75
80
|
|
|
76
81
|
/**
|
|
77
82
|
* The protocol assigned to this storage instance.
|
|
@@ -84,8 +89,15 @@ export class DwnDataStore<TStoreObject extends Record<string, any> = Jwk> implem
|
|
|
84
89
|
* When any type in `_recordProtocolDefinition` has `encryptionRequired: true`,
|
|
85
90
|
* this will always be `true` after successful initialization (since
|
|
86
91
|
* installation fails if encryption is not possible).
|
|
92
|
+
*
|
|
93
|
+
* Uses the same 1-hour TTL as `_protocolInitializedCache` so both
|
|
94
|
+
* caches expire and re-derive together. See comment above.
|
|
87
95
|
*/
|
|
88
|
-
private _tenantEncryptionActive: TtlCache<string, boolean> = new TtlCache({ ttl: ms('
|
|
96
|
+
private readonly _tenantEncryptionActive: TtlCache<string, boolean> = new TtlCache({ ttl: ms('1 hour'), max: 1000 });
|
|
97
|
+
|
|
98
|
+
/** Cached result of the `encryptionRequired` check on the protocol definition.
|
|
99
|
+
* Computed lazily on first access — the definition is immutable after assignment. */
|
|
100
|
+
private _encryptionRequired: boolean | undefined;
|
|
89
101
|
|
|
90
102
|
/**
|
|
91
103
|
* Properties to use when writing and querying records with the DWN store.
|
|
@@ -218,7 +230,11 @@ export class DwnDataStore<TStoreObject extends Record<string, any> = Jwk> implem
|
|
|
218
230
|
*/
|
|
219
231
|
public async initialize({ tenant, agent }: DataStoreTenantParams): Promise<void> {
|
|
220
232
|
const tenantDid = await getDataStoreTenant({ agent, tenant });
|
|
221
|
-
|
|
233
|
+
// Short-circuit only when both caches are present. If the encryption
|
|
234
|
+
// cache has expired or was cleared while the init cache survived,
|
|
235
|
+
// re-derive instead of silently dropping encryption.
|
|
236
|
+
if (this._protocolInitializedCache.has(tenantDid)
|
|
237
|
+
&& (!this.encryptionRequired || this._tenantEncryptionActive.has(tenantDid))) {
|
|
222
238
|
return;
|
|
223
239
|
}
|
|
224
240
|
|
|
@@ -237,27 +253,37 @@ export class DwnDataStore<TStoreObject extends Record<string, any> = Jwk> implem
|
|
|
237
253
|
throw new Error(`Failed to query for protocols: ${status.code} - ${status.detail}`);
|
|
238
254
|
}
|
|
239
255
|
|
|
256
|
+
let encryptionActive = false;
|
|
257
|
+
|
|
240
258
|
if (entries?.length === 0) {
|
|
241
259
|
// protocol is not installed, install it
|
|
242
|
-
await this.installProtocol(tenantDid, agent);
|
|
243
|
-
|
|
260
|
+
const installed = await this.installProtocol(tenantDid, agent);
|
|
261
|
+
encryptionActive = installed.encryptionActive;
|
|
262
|
+
} else if (this.encryptionRequired) {
|
|
244
263
|
// Protocol already installed — determine if it has $encryption keys
|
|
245
264
|
// by inspecting the installed definition.
|
|
246
265
|
const definition = entries![0].descriptor?.definition;
|
|
247
266
|
const firstType = Object.keys(definition?.structure ?? {})[0];
|
|
248
|
-
|
|
249
|
-
this._tenantEncryptionActive.set(tenantDid, hasEncryption);
|
|
267
|
+
encryptionActive = !!(firstType && definition?.structure[firstType]?.$encryption);
|
|
250
268
|
}
|
|
251
269
|
|
|
270
|
+
// Set both caches atomically so they always expire together. This
|
|
271
|
+
// prevents a stale-encryption window where one cache expires before
|
|
272
|
+
// the other and initialize() short-circuits on the surviving entry.
|
|
273
|
+
this._tenantEncryptionActive.set(tenantDid, encryptionActive);
|
|
252
274
|
this._protocolInitializedCache.set(tenantDid, true);
|
|
253
275
|
}
|
|
254
276
|
|
|
255
277
|
/**
|
|
256
278
|
* Returns `true` if any type in the protocol definition has `encryptionRequired: true`.
|
|
279
|
+
* Cached after first computation since the protocol definition is immutable.
|
|
257
280
|
*/
|
|
258
281
|
private get encryptionRequired(): boolean {
|
|
259
|
-
|
|
260
|
-
.
|
|
282
|
+
if (this._encryptionRequired === undefined) {
|
|
283
|
+
this._encryptionRequired = Object.values(this._recordProtocolDefinition.types)
|
|
284
|
+
.some((type): boolean => type.encryptionRequired === true);
|
|
285
|
+
}
|
|
286
|
+
return this._encryptionRequired;
|
|
261
287
|
}
|
|
262
288
|
|
|
263
289
|
/**
|
|
@@ -328,7 +354,7 @@ export class DwnDataStore<TStoreObject extends Record<string, any> = Jwk> implem
|
|
|
328
354
|
* If the tenant DID lacks an X25519 keyAgreement key, the error propagates
|
|
329
355
|
* — plaintext fallback is not allowed.
|
|
330
356
|
*/
|
|
331
|
-
private async installProtocol(tenant: string, agent: EnboxPlatformAgent): Promise<
|
|
357
|
+
private async installProtocol(tenant: string, agent: EnboxPlatformAgent): Promise<{ encryptionActive: boolean }> {
|
|
332
358
|
let definition = this._recordProtocolDefinition;
|
|
333
359
|
let encryptionActive = false;
|
|
334
360
|
|
|
@@ -342,8 +368,6 @@ export class DwnDataStore<TStoreObject extends Record<string, any> = Jwk> implem
|
|
|
342
368
|
encryptionActive = true;
|
|
343
369
|
}
|
|
344
370
|
|
|
345
|
-
this._tenantEncryptionActive.set(tenant, encryptionActive);
|
|
346
|
-
|
|
347
371
|
const { reply : { status } } = await agent.dwn.processRequest({
|
|
348
372
|
author : tenant,
|
|
349
373
|
target : tenant,
|
|
@@ -354,6 +378,8 @@ export class DwnDataStore<TStoreObject extends Record<string, any> = Jwk> implem
|
|
|
354
378
|
if (status.code !== 202) {
|
|
355
379
|
throw new Error(`Failed to install protocol: ${status.code} - ${status.detail}`);
|
|
356
380
|
}
|
|
381
|
+
|
|
382
|
+
return { encryptionActive };
|
|
357
383
|
}
|
|
358
384
|
|
|
359
385
|
private async lookupRecordId({ id, tenantDid, agent }: {
|
|
@@ -403,7 +429,7 @@ export class InMemoryDataStore<TStoreObject extends Record<string, any> = Jwk> i
|
|
|
403
429
|
/**
|
|
404
430
|
* A private field that contains the Map used as the in-memory data store.
|
|
405
431
|
*/
|
|
406
|
-
private store: Map<string, TStoreObject> = new Map();
|
|
432
|
+
private readonly store: Map<string, TStoreObject> = new Map();
|
|
407
433
|
|
|
408
434
|
public async delete({ id, agent, tenant }: DataStoreDeleteParams): Promise<boolean> {
|
|
409
435
|
// Determine the tenant identifier (DID) for the delete operation.
|
|
@@ -366,7 +366,7 @@ function extractProtocolAwareDeps(
|
|
|
366
366
|
Buffer.from(payload, 'base64url').toString('utf-8')
|
|
367
367
|
);
|
|
368
368
|
const protocolRole = decoded.protocolRole as string | undefined;
|
|
369
|
-
if (protocolRole
|
|
369
|
+
if (protocolRole?.includes(':')) {
|
|
370
370
|
// Cross-protocol role: "alias:protocolPath"
|
|
371
371
|
const roleColonIdx = protocolRole.indexOf(':');
|
|
372
372
|
const roleAlias = protocolRole.substring(0, roleColonIdx);
|
|
@@ -518,7 +518,7 @@ export async function evaluateClosure(
|
|
|
518
518
|
const currentProtocol = currentDesc.protocol as string | undefined;
|
|
519
519
|
if (currentProtocol) {
|
|
520
520
|
const cachedProtocolMsg = context.protocolCache.get(currentProtocol);
|
|
521
|
-
const protocolDef =
|
|
521
|
+
const protocolDef = cachedProtocolMsg?.descriptor?.definition;
|
|
522
522
|
if (protocolDef) {
|
|
523
523
|
const protoAwareEdges = extractProtocolAwareDeps(current, protocolDef, context.isDelegateSession);
|
|
524
524
|
const protoResult = await resolveEdges(
|