@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/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
|
|
539
|
-
await
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
|
633
|
-
?
|
|
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
|
-
|
|
760
|
-
|
|
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
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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
|
|
1230
|
-
if (
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
/**
|
package/src/dwn-key-delivery.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
439
|
-
|
|
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
|
-
|
|
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
|
-
|
|
450
|
+
const pubJwk = { ...publicKeyJwk, alg: 'EdDSA' };
|
|
448
451
|
|
|
449
|
-
const publicX25519 = await Ed25519.convertPublicKeyToX25519({ publicKey:
|
|
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
|
-
|
|
490
|
+
delegatePublicKeyJwk,
|
|
479
491
|
pin,
|
|
480
492
|
}: {
|
|
481
493
|
jwt: string;
|
|
482
494
|
encryptionKey: Uint8Array;
|
|
483
|
-
|
|
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
|
-
|
|
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
|
|
537
|
-
if (!header.
|
|
538
|
-
throw new Error('Connect: JWE protected header is missing required "
|
|
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
|
|
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
|
|
1301
|
-
encryptionKey
|
|
1302
|
-
|
|
1317
|
+
jwt : responseObjectJwt,
|
|
1318
|
+
encryptionKey : sharedKey,
|
|
1319
|
+
delegatePublicKeyJwk : delegateBearerDid.document.verificationMethod![0].publicKeyJwk!,
|
|
1303
1320
|
pin,
|
|
1304
1321
|
});
|
|
1305
1322
|
|
package/src/enbox-user-agent.ts
CHANGED
|
@@ -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
|
-
|
|
229
|
+
secretsApi,
|
|
230
|
+
syncApi,
|
|
219
231
|
});
|
|
220
232
|
}
|
|
221
233
|
|
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();
|