@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.
- 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 +1 -1
- 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/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(
|