@enbox/agent 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/browser.mjs +8 -8
  2. package/dist/browser.mjs.map +4 -4
  3. package/dist/esm/dwn-api.js +116 -7
  4. package/dist/esm/dwn-api.js.map +1 -1
  5. package/dist/esm/dwn-key-delivery.js +6 -5
  6. package/dist/esm/dwn-key-delivery.js.map +1 -1
  7. package/dist/esm/dwn-protocol-cache.js +6 -7
  8. package/dist/esm/dwn-protocol-cache.js.map +1 -1
  9. package/dist/esm/identity-api.js +1 -2
  10. package/dist/esm/identity-api.js.map +1 -1
  11. package/dist/esm/index.js +1 -0
  12. package/dist/esm/index.js.map +1 -1
  13. package/dist/esm/local-dwn.js +73 -0
  14. package/dist/esm/local-dwn.js.map +1 -0
  15. package/dist/esm/sync-engine-level.js +1 -2
  16. package/dist/esm/sync-engine-level.js.map +1 -1
  17. package/dist/esm/test-harness.js +2 -1
  18. package/dist/esm/test-harness.js.map +1 -1
  19. package/dist/esm/web5-user-agent.js +6 -2
  20. package/dist/esm/web5-user-agent.js.map +1 -1
  21. package/dist/types/dwn-api.d.ts +32 -1
  22. package/dist/types/dwn-api.d.ts.map +1 -1
  23. package/dist/types/dwn-key-delivery.d.ts +4 -2
  24. package/dist/types/dwn-key-delivery.d.ts.map +1 -1
  25. package/dist/types/dwn-protocol-cache.d.ts +6 -5
  26. package/dist/types/dwn-protocol-cache.d.ts.map +1 -1
  27. package/dist/types/identity-api.d.ts.map +1 -1
  28. package/dist/types/index.d.ts +1 -0
  29. package/dist/types/index.d.ts.map +1 -1
  30. package/dist/types/local-dwn.d.ts +43 -0
  31. package/dist/types/local-dwn.d.ts.map +1 -0
  32. package/dist/types/sync-engine-level.d.ts.map +1 -1
  33. package/dist/types/test-harness.d.ts.map +1 -1
  34. package/dist/types/web5-user-agent.d.ts +5 -1
  35. package/dist/types/web5-user-agent.d.ts.map +1 -1
  36. package/package.json +2 -2
  37. package/src/dwn-api.ts +140 -5
  38. package/src/dwn-key-delivery.ts +6 -3
  39. package/src/dwn-protocol-cache.ts +9 -8
  40. package/src/identity-api.ts +1 -2
  41. package/src/index.ts +1 -0
  42. package/src/local-dwn.ts +81 -0
  43. package/src/sync-engine-level.ts +1 -2
  44. package/src/test-harness.ts +2 -1
  45. package/src/web5-user-agent.ts +13 -3
package/src/dwn-api.ts CHANGED
@@ -33,6 +33,7 @@ import {
33
33
  import { CryptoUtils, X25519 } from '@enbox/crypto';
34
34
  import { DidDht, DidJwk, DidResolverCacheLevel, UniversalResolver } from '@enbox/dids';
35
35
 
36
+ import type { LocalDwnStrategy } from './local-dwn.js';
36
37
  import type { Web5PlatformAgent } from './types/agent.js';
37
38
  import type {
38
39
  DwnMessage,
@@ -48,6 +49,7 @@ import type {
48
49
  } from './types/dwn.js';
49
50
 
50
51
  import { KeyDeliveryProtocolDefinition } from './store-data-protocols.js';
52
+ import { LocalDwnDiscovery } from './local-dwn.js';
51
53
  import { DwnInterface, dwnMessageConstructors } from './types/dwn.js';
52
54
  import { getDwnServiceEndpointUrls, isRecordsWrite } from './utils.js';
53
55
 
@@ -101,6 +103,7 @@ type DwnMessageWithBlob<T extends DwnInterface> = {
101
103
  type DwnApiParams = {
102
104
  agent?: Web5PlatformAgent;
103
105
  dwn: Dwn;
106
+ localDwnStrategy?: LocalDwnStrategy;
104
107
  };
105
108
 
106
109
  interface DwnApiCreateDwnParams extends Partial<DwnConfig> {
@@ -148,12 +151,34 @@ export class AgentDwnApi {
148
151
  ttl: 30 * 60 * 1000
149
152
  });
150
153
 
151
- constructor({ agent, dwn }: DwnApiParams) {
154
+ /**
155
+ * Cache of locally-managed DIDs (agent DID + identities). Used to decide
156
+ * whether a target DID should be routed through the local DWN server.
157
+ */
158
+ private _localManagedDidCache = new TtlCache<string, boolean>({
159
+ ttl: 30 * 60 * 1000
160
+ });
161
+
162
+ /** Controls local DWN discovery behavior ('prefer' | 'only' | 'off'). */
163
+ private _localDwnStrategy: LocalDwnStrategy;
164
+
165
+ /** Lazy-initialized local DWN discovery instance. */
166
+ private _localDwnDiscovery?: LocalDwnDiscovery;
167
+
168
+ constructor({ agent, dwn, localDwnStrategy = 'prefer' }: DwnApiParams) {
152
169
  // If an agent is provided, set it as the execution context for this API.
153
170
  this._agent = agent;
154
171
 
155
172
  // Set the DWN instance for this API.
156
173
  this._dwn = dwn;
174
+
175
+ // Set the local DWN discovery strategy.
176
+ this._localDwnStrategy = localDwnStrategy;
177
+
178
+ // If agent is already available, eagerly initialize the discovery instance.
179
+ if (agent) {
180
+ this._localDwnDiscovery = new LocalDwnDiscovery(agent.rpc);
181
+ }
157
182
  }
158
183
 
159
184
  /**
@@ -172,6 +197,114 @@ export class AgentDwnApi {
172
197
 
173
198
  set agent(agent: Web5PlatformAgent) {
174
199
  this._agent = agent;
200
+ // Re-initialize local DWN discovery with the new agent's RPC client.
201
+ this._localDwnDiscovery = new LocalDwnDiscovery(agent.rpc);
202
+ this._localManagedDidCache.clear();
203
+ }
204
+
205
+ get localDwnStrategy(): LocalDwnStrategy {
206
+ return this._localDwnStrategy;
207
+ }
208
+
209
+ public setLocalDwnStrategy(strategy: LocalDwnStrategy): void {
210
+ this._localDwnStrategy = strategy;
211
+ }
212
+
213
+ /**
214
+ * Resolves the DWN service endpoint URLs for the given target DID, optionally
215
+ * prepending a local DWN server endpoint when local discovery is enabled and
216
+ * the target is a locally-managed DID.
217
+ *
218
+ * @param targetDid - The DID whose DWN endpoints should be resolved.
219
+ * @returns An array of endpoint URLs.
220
+ * @throws When strategy is `'only'` and no local server is available.
221
+ */
222
+ public async getDwnEndpointUrlsForTarget(targetDid: string): Promise<string[]> {
223
+ const shouldUseLocalDwn = await this.shouldUseLocalDwnForTarget(targetDid);
224
+
225
+ if (!shouldUseLocalDwn) {
226
+ return getDwnServiceEndpointUrls(targetDid, this.agent.did);
227
+ }
228
+
229
+ const localDwnEndpoint = await this.getLocalDwnEndpoint();
230
+ if (this._localDwnStrategy === 'only') {
231
+ if (!localDwnEndpoint) {
232
+ throw new Error(
233
+ `AgentDwnApi: Local DWN strategy is 'only' but no local server is available ` +
234
+ `on localhost/127.0.0.1:{3000,55555-55559}`
235
+ );
236
+ }
237
+
238
+ return [localDwnEndpoint];
239
+ }
240
+
241
+ let dwnEndpointUrls: string[] = [];
242
+ try {
243
+ dwnEndpointUrls = await getDwnServiceEndpointUrls(targetDid, this.agent.did);
244
+ } catch (error) {
245
+ if (!localDwnEndpoint) {
246
+ throw error;
247
+ }
248
+ }
249
+
250
+ if (!localDwnEndpoint) {
251
+ return dwnEndpointUrls;
252
+ }
253
+
254
+ const uniqueEndpoints = new Set<string>([
255
+ localDwnEndpoint,
256
+ ...dwnEndpointUrls,
257
+ ]);
258
+
259
+ return [...uniqueEndpoints];
260
+ }
261
+
262
+ /** Lazily retrieves the local DWN server endpoint via discovery probing. */
263
+ private async getLocalDwnEndpoint(): Promise<string | undefined> {
264
+ this._localDwnDiscovery ??= new LocalDwnDiscovery(this.agent.rpc);
265
+ return this._localDwnDiscovery.getEndpoint();
266
+ }
267
+
268
+ /**
269
+ * Determines whether the given target DID should be routed through the
270
+ * local DWN server. Returns `true` if the DID is the agent DID or one
271
+ * of the locally-managed identity DIDs.
272
+ */
273
+ private async shouldUseLocalDwnForTarget(targetDid: string): Promise<boolean> {
274
+ if (this._localDwnStrategy === 'off') {
275
+ return false;
276
+ }
277
+
278
+ const cached = this._localManagedDidCache.get(targetDid);
279
+ if (cached !== undefined) {
280
+ return cached;
281
+ }
282
+
283
+ if (targetDid === this.agent.agentDid.uri) {
284
+ this._localManagedDidCache.set(targetDid, true);
285
+ return true;
286
+ }
287
+
288
+ const identities = await this.agent.identity.list();
289
+ const localManagedDids = new Set<string>();
290
+
291
+ for (const identity of identities) {
292
+ localManagedDids.add(identity.did.uri);
293
+ if (identity.metadata.connectedDid) {
294
+ localManagedDids.add(identity.metadata.connectedDid);
295
+ }
296
+ }
297
+
298
+ for (const localDid of localManagedDids) {
299
+ this._localManagedDidCache.set(localDid, true);
300
+ }
301
+
302
+ const isLocalManaged = localManagedDids.has(targetDid);
303
+ if (!isLocalManaged) {
304
+ this._localManagedDidCache.set(targetDid, false);
305
+ }
306
+
307
+ return isLocalManaged;
175
308
  }
176
309
 
177
310
  /**
@@ -252,8 +385,8 @@ export class AgentDwnApi {
252
385
  public async sendRequest<T extends DwnInterface>(
253
386
  request: SendDwnRequest<T>
254
387
  ): Promise<DwnResponse<T>> {
255
- // First, confirm the target DID can be dereferenced and extract the DWN service endpoint URLs.
256
- const dwnEndpointUrls = await getDwnServiceEndpointUrls(request.target, this.agent.did);
388
+ // Resolve DWN service endpoint URLs, with local DWN discovery if enabled.
389
+ const dwnEndpointUrls = await this.getDwnEndpointUrlsForTarget(request.target);
257
390
  if (dwnEndpointUrls.length === 0) {
258
391
  throw new Error(`AgentDwnApi: DID Service is missing or malformed: ${request.target}#dwn`);
259
392
  }
@@ -997,7 +1130,7 @@ export class AgentDwnApi {
997
1130
  protocolUri: string,
998
1131
  ): Promise<ProtocolDefinition> {
999
1132
  return fetchRemoteProtocolDefinitionFn(
1000
- targetDid, protocolUri, this.agent.did,
1133
+ targetDid, protocolUri, this.getDwnEndpointUrlsForTarget.bind(this),
1001
1134
  this.sendDwnRpcRequest.bind(this), this._protocolDefinitionCache,
1002
1135
  );
1003
1136
  }
@@ -1022,7 +1155,7 @@ export class AgentDwnApi {
1022
1155
  ): Promise<{ rootKeyId: string; derivedPublicKey: PublicKeyJwk } | undefined> {
1023
1156
  return extractDerivedPublicKeyFn(
1024
1157
  targetDid, protocolUri, rootContextId, requesterDid,
1025
- this.agent.did, this.getSigner.bind(this),
1158
+ this.getDwnEndpointUrlsForTarget.bind(this), this.getSigner.bind(this),
1026
1159
  this.sendDwnRpcRequest.bind(this),
1027
1160
  );
1028
1161
  }
@@ -1139,6 +1272,7 @@ export class AgentDwnApi {
1139
1272
  this.agent, tenantDid, contextKeyMessage,
1140
1273
  this.getDwnMessage.bind(this),
1141
1274
  this.sendDwnRpcRequest.bind(this),
1275
+ this.getDwnEndpointUrlsForTarget.bind(this),
1142
1276
  );
1143
1277
  }
1144
1278
 
@@ -1160,6 +1294,7 @@ export class AgentDwnApi {
1160
1294
  this.processRequest.bind(this),
1161
1295
  this.getSigner.bind(this),
1162
1296
  this.sendDwnRpcRequest.bind(this),
1297
+ this.getDwnEndpointUrlsForTarget.bind(this),
1163
1298
  );
1164
1299
  }
1165
1300
  }
@@ -22,7 +22,6 @@ import {
22
22
  Records,
23
23
  } from '@enbox/dwn-sdk-js';
24
24
 
25
- import { getDwnServiceEndpointUrls } from './utils.js';
26
25
  import { KeyDeliveryProtocolDefinition } from './store-data-protocols.js';
27
26
  import { buildEncryptionInput, encryptAndComputeCid, getEncryptionKeyDeriver, getKeyDecrypter, ivLength } from './dwn-encryption.js';
28
27
  import { DwnInterface, dwnMessageConstructors } from './types/dwn.js';
@@ -219,6 +218,7 @@ export async function writeContextKeyRecord(
219
218
  * @param contextKeyMessage - The context key message to send
220
219
  * @param getDwnMessage - Function to read a full message from local DWN
221
220
  * @param sendDwnRpcRequest - Function to send a DWN RPC request
221
+ * @param getDwnEndpointUrlsForTarget - Function to resolve DWN endpoint URLs (with local discovery)
222
222
  */
223
223
  export async function eagerSendContextKeyRecord(
224
224
  agent: Web5PlatformAgent,
@@ -226,10 +226,11 @@ export async function eagerSendContextKeyRecord(
226
226
  contextKeyMessage: DwnMessage[DwnInterface.RecordsWrite],
227
227
  getDwnMessage: (params: { author: string; messageType: DwnInterface; messageCid: string }) => Promise<{ message: any; data?: Blob }>,
228
228
  sendDwnRpcRequest: (params: { targetDid: string; dwnEndpointUrls: string[]; message: any; data?: Blob }) => Promise<any>,
229
+ getDwnEndpointUrlsForTarget: (targetDid: string) => Promise<string[]>,
229
230
  ): Promise<void> {
230
231
  let dwnEndpointUrls: string[];
231
232
  try {
232
- dwnEndpointUrls = await getDwnServiceEndpointUrls(tenantDid, agent.did);
233
+ dwnEndpointUrls = await getDwnEndpointUrlsForTarget(tenantDid);
233
234
  } catch {
234
235
  // DID resolution or endpoint lookup failed — not fatal, sync will handle it.
235
236
  return;
@@ -266,6 +267,7 @@ export async function eagerSendContextKeyRecord(
266
267
  * @param processRequest - The agent's processRequest method (bound)
267
268
  * @param getSigner - Function to get a signer for a DID
268
269
  * @param sendDwnRpcRequest - Function to send a DWN RPC request
270
+ * @param getDwnEndpointUrlsForTarget - Function to resolve DWN endpoint URLs (with local discovery)
269
271
  * @returns The decrypted `DerivedPrivateJwk`, or `undefined` if no matching record found
270
272
  */
271
273
  export async function fetchContextKeyRecord(
@@ -274,6 +276,7 @@ export async function fetchContextKeyRecord(
274
276
  processRequest: ProcessRequestFn,
275
277
  getSigner: (author: string) => Promise<any>,
276
278
  sendDwnRpcRequest: (params: { targetDid: string; dwnEndpointUrls: string[]; message: any; data?: Blob }) => Promise<any>,
279
+ getDwnEndpointUrlsForTarget: (targetDid: string) => Promise<string[]>,
277
280
  ): Promise<DerivedPrivateJwk | undefined> {
278
281
  const { ownerDid, requesterDid, sourceProtocol, sourceContextId } = params;
279
282
  const protocolUri = KeyDeliveryProtocolDefinition.protocol;
@@ -323,7 +326,7 @@ export async function fetchContextKeyRecord(
323
326
  } else {
324
327
  // Remote query: participant queries the context owner's DWN
325
328
  const signer = await getSigner(requesterDid);
326
- const dwnEndpointUrls = await getDwnServiceEndpointUrls(ownerDid, agent.did);
329
+ const dwnEndpointUrls = await getDwnEndpointUrlsForTarget(ownerDid);
327
330
 
328
331
  const recordsQuery = await dwnMessageConstructors[DwnInterface.RecordsQuery].create({
329
332
  signer,
@@ -7,7 +7,6 @@
7
7
  * @module
8
8
  */
9
9
 
10
- import type { DidUrlDereferencer } from '@enbox/dids';
11
10
  import type { PublicKeyJwk } from '@enbox/crypto';
12
11
  import type { TtlCache } from '@enbox/common';
13
12
  import type {
@@ -26,13 +25,15 @@ import type {
26
25
 
27
26
  import { KeyDerivationScheme } from '@enbox/dwn-sdk-js';
28
27
 
29
- import { getDwnServiceEndpointUrls } from './utils.js';
30
28
  import { DwnInterface as DwnInterfaceEnum, dwnMessageConstructors } from './types/dwn.js';
31
29
 
32
30
  // ---------------------------------------------------------------------------
33
31
  // Dependency signatures — keep the extracted code free of `this` references.
34
32
  // ---------------------------------------------------------------------------
35
33
 
34
+ /** Callback to resolve DWN endpoint URLs for a target DID (with local discovery). */
35
+ type GetDwnEndpointUrlsFn = (targetDid: string) => Promise<string[]>;
36
+
36
37
  /** Callback to obtain a DWN signer for a given DID. */
37
38
  type GetSignerFn = (author: string) => Promise<DwnSigner>;
38
39
 
@@ -106,7 +107,7 @@ export async function getProtocolDefinition(
106
107
  *
107
108
  * @param targetDid - The remote DWN owner
108
109
  * @param protocolUri - The protocol URI to look up
109
- * @param didDereferencer - A DID URL dereferencer for resolving service endpoints
110
+ * @param getDwnEndpointUrls - Callback to resolve DWN endpoint URLs (with local discovery)
110
111
  * @param sendDwnRpcRequest - Callback to send the RPC query
111
112
  * @param cache - The shared protocol definition cache
112
113
  * @returns The protocol definition
@@ -115,7 +116,7 @@ export async function getProtocolDefinition(
115
116
  export async function fetchRemoteProtocolDefinition(
116
117
  targetDid: string,
117
118
  protocolUri: string,
118
- didDereferencer: DidUrlDereferencer,
119
+ getDwnEndpointUrls: GetDwnEndpointUrlsFn,
119
120
  sendDwnRpcRequest: SendDwnRpcRequestFn,
120
121
  cache: TtlCache<string, ProtocolDefinition>,
121
122
  ): Promise<ProtocolDefinition> {
@@ -131,7 +132,7 @@ export async function fetchRemoteProtocolDefinition(
131
132
 
132
133
  const reply = await sendDwnRpcRequest({
133
134
  targetDid,
134
- dwnEndpointUrls : await getDwnServiceEndpointUrls(targetDid, didDereferencer),
135
+ dwnEndpointUrls : await getDwnEndpointUrls(targetDid),
135
136
  message : protocolsQuery.message,
136
137
  }) as ProtocolsQueryReply;
137
138
 
@@ -158,7 +159,7 @@ export async function fetchRemoteProtocolDefinition(
158
159
  * @param protocolUri - The protocol URI to search
159
160
  * @param rootContextId - The root context ID
160
161
  * @param requesterDid - The DID of the requester (used for signing the query)
161
- * @param didDereferencer - A DID URL dereferencer for resolving service endpoints
162
+ * @param getDwnEndpointUrls - Callback to resolve DWN endpoint URLs (with local discovery)
162
163
  * @param getSigner - Callback to obtain the signer for `requesterDid`
163
164
  * @param sendDwnRpcRequest - Callback to send the RPC query
164
165
  * @returns The rootKeyId and derivedPublicKey, or `undefined` if no
@@ -169,7 +170,7 @@ export async function extractDerivedPublicKey(
169
170
  protocolUri: string,
170
171
  rootContextId: string,
171
172
  requesterDid: string,
172
- didDereferencer: DidUrlDereferencer,
173
+ getDwnEndpointUrls: GetDwnEndpointUrlsFn,
173
174
  getSigner: GetSignerFn,
174
175
  sendDwnRpcRequest: SendDwnRpcRequestFn,
175
176
  ): Promise<{ rootKeyId: string; derivedPublicKey: PublicKeyJwk } | undefined> {
@@ -184,7 +185,7 @@ export async function extractDerivedPublicKey(
184
185
  },
185
186
  });
186
187
 
187
- const dwnEndpointUrls = await getDwnServiceEndpointUrls(targetDid, didDereferencer);
188
+ const dwnEndpointUrls = await getDwnEndpointUrls(targetDid);
188
189
  const queryReply = await sendDwnRpcRequest<DwnInterfaceEnum.RecordsQuery>({
189
190
  targetDid,
190
191
  dwnEndpointUrls,
@@ -9,7 +9,6 @@ import type { IdentityMetadata, PortableIdentity } from './types/identity.js';
9
9
  import { isPortableDid } from '@enbox/dids';
10
10
 
11
11
  import { BearerIdentity } from './bearer-identity.js';
12
- import { getDwnServiceEndpointUrls } from './utils.js';
13
12
  import { InMemoryIdentityStore } from './store-identity.js';
14
13
 
15
14
  export interface IdentityApiParams<TKeyManager extends AgentKeyManager> {
@@ -226,7 +225,7 @@ export class AgentIdentityApi<TKeyManager extends AgentKeyManager = AgentKeyMana
226
225
  * @throws An error if the DID is not found, or no DWN service exists.
227
226
  */
228
227
  public getDwnEndpoints({ didUri }: { didUri: string; }): Promise<string[]> {
229
- return getDwnServiceEndpointUrls(didUri, this.agent.did);
228
+ return this.agent.dwn.getDwnEndpointUrlsForTarget(didUri);
230
229
  }
231
230
 
232
231
  /**
package/src/index.ts CHANGED
@@ -20,6 +20,7 @@ export * from './dwn-type-guards.js';
20
20
  export * from './protocol-utils.js';
21
21
  export * from './hd-identity-vault.js';
22
22
  export * from './identity-api.js';
23
+ export * from './local-dwn.js';
23
24
  export * from './local-key-manager.js';
24
25
  export * from './permissions-api.js';
25
26
  export * from './store-data.js';
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Local DWN discovery — probes well-known localhost ports for a running
3
+ * `@enbox/dwn-server` instance so the agent can route traffic to it.
4
+ *
5
+ * @module
6
+ */
7
+
8
+ import type { Web5Rpc } from '@enbox/dwn-clients';
9
+
10
+ /** Well-known ports the local DWN desktop app may bind to. */
11
+ export const localDwnPortCandidates = [3000, 55555, 55556, 55557, 55558, 55559] as const;
12
+
13
+ /** Hosts probed when discovering a local DWN server. */
14
+ export const localDwnHostCandidates = ['127.0.0.1', 'localhost'] as const;
15
+
16
+ /**
17
+ * Controls how the agent discovers and routes to a local DWN server.
18
+ *
19
+ * - `'prefer'` — probe localhost first; fall back to DID-document endpoints.
20
+ * - `'only'` — require a local server; throw if none is found.
21
+ * - `'off'` — skip local discovery entirely.
22
+ */
23
+ export type LocalDwnStrategy = 'prefer' | 'only' | 'off';
24
+
25
+ /** The `server` field returned by `GET /info` on `@enbox/dwn-server`. */
26
+ export const localDwnServerName = '@enbox/dwn-server';
27
+
28
+ /** Strips a trailing slash from a URL so endpoint comparisons are consistent. */
29
+ function normalizeBaseUrl(url: string): string {
30
+ return url.endsWith('/') ? url.slice(0, -1) : url;
31
+ }
32
+
33
+ /**
34
+ * Probes well-known localhost ports for a running `@enbox/dwn-server` instance.
35
+ *
36
+ * Results are cached for {@link _cacheTtlMs} milliseconds (default 10 s) to
37
+ * avoid repeated HTTP round-trips on hot paths such as sync.
38
+ *
39
+ * TODO: Replace sequential port probing with an `enbox://` protocol-handler
40
+ * handshake (https://github.com/enboxorg/enbox/issues/287).
41
+ */
42
+ export class LocalDwnDiscovery {
43
+ private _cachedEndpoint?: string;
44
+ private _cacheExpiry = 0;
45
+
46
+ constructor(
47
+ private _rpcClient: Web5Rpc,
48
+ private _cacheTtlMs = 10_000
49
+ ) {}
50
+
51
+ /**
52
+ * Returns the base URL of a local DWN server, or `undefined` if none is
53
+ * reachable on the well-known port candidates.
54
+ */
55
+ public async getEndpoint(): Promise<string | undefined> {
56
+ const now = Date.now();
57
+ if (now < this._cacheExpiry) {
58
+ return this._cachedEndpoint;
59
+ }
60
+
61
+ for (const port of localDwnPortCandidates) {
62
+ for (const host of localDwnHostCandidates) {
63
+ const endpoint = `http://${host}:${port}`;
64
+ try {
65
+ const serverInfo = await this._rpcClient.getServerInfo(endpoint);
66
+ if (serverInfo.server === localDwnServerName) {
67
+ this._cachedEndpoint = normalizeBaseUrl(endpoint);
68
+ this._cacheExpiry = now + this._cacheTtlMs;
69
+ return this._cachedEndpoint;
70
+ }
71
+ } catch {
72
+ // keep probing candidate endpoints
73
+ }
74
+ }
75
+ }
76
+
77
+ this._cachedEndpoint = undefined;
78
+ this._cacheExpiry = now + this._cacheTtlMs;
79
+ return undefined;
80
+ }
81
+ }
@@ -12,7 +12,6 @@ import type { Web5Agent, Web5PlatformAgent } from './types/agent.js';
12
12
 
13
13
  import { AgentPermissionsApi } from './permissions-api.js';
14
14
  import { DwnInterface } from './types/dwn.js';
15
- import { getDwnServiceEndpointUrls } from './utils.js';
16
15
  import { isRecordsWrite } from './utils.js';
17
16
  import { topologicalSort } from './sync-topological-sort.js';
18
17
  import { pullMessages, pushMessages } from './sync-messages.js';
@@ -1063,7 +1062,7 @@ export class SyncEngineLevel implements SyncEngine {
1063
1062
  }
1064
1063
  const { protocols, delegateDid } = parsed;
1065
1064
 
1066
- const dwnEndpointUrls = await getDwnServiceEndpointUrls(did, this.agent.did);
1065
+ const dwnEndpointUrls = await this.agent.dwn.getDwnEndpointUrlsForTarget(did);
1067
1066
  if (dwnEndpointUrls.length === 0) {
1068
1067
  continue;
1069
1068
  }
@@ -277,7 +277,8 @@ export class PlatformAgentTestHarness {
277
277
  });
278
278
 
279
279
  // Instantiate Agent's DWN API using the custom DWN instance.
280
- const dwnApi = new AgentDwnApi({ dwn });
280
+ // Disable local DWN discovery so tests don't accidentally probe localhost.
281
+ const dwnApi = new AgentDwnApi({ dwn, localDwnStrategy: 'off' });
281
282
 
282
283
  // Instantiate Agent's Sync API using a custom LevelDB-backed store.
283
284
  const syncStore = new Level(testDataPath('SYNC_STORE'));
@@ -1,5 +1,6 @@
1
1
  import type { AgentKeyManager } from './types/key-manager.js';
2
2
  import type { BearerDid } from '@enbox/dids';
3
+ import type { LocalDwnStrategy } from './local-dwn.js';
3
4
  import type { Web5PlatformAgent } from './types/agent.js';
4
5
  import type { Web5Rpc } from '@enbox/dwn-clients';
5
6
  import type { DidInterface, DidRequest, DidResponse } from './did-api.js';
@@ -87,6 +88,10 @@ export type AgentParams<TKeyManager extends AgentKeyManager = LocalKeyManager> =
87
88
  syncApi: AgentSyncApi;
88
89
  };
89
90
 
91
+ export type CreateUserAgentParams = Partial<AgentParams> & {
92
+ localDwnStrategy?: LocalDwnStrategy;
93
+ };
94
+
90
95
  export class Web5UserAgent<TKeyManager extends AgentKeyManager = LocalKeyManager> implements Web5PlatformAgent<TKeyManager> {
91
96
  public crypto: AgentCryptoApi;
92
97
  public did: AgentDidApi<TKeyManager>;
@@ -140,8 +145,9 @@ export class Web5UserAgent<TKeyManager extends AgentKeyManager = LocalKeyManager
140
145
  */
141
146
  public static async create({
142
147
  dataPath = 'DATA/AGENT',
148
+ localDwnStrategy,
143
149
  agentDid, agentVault, cryptoApi, didApi, dwnApi, identityApi, keyManager, permissionsApi, rpcClient, syncApi
144
- }: Partial<AgentParams> = {}
150
+ }: CreateUserAgentParams = {}
145
151
  ): Promise<Web5UserAgent> {
146
152
 
147
153
  agentVault ??= new HdIdentityVault({
@@ -158,8 +164,12 @@ export class Web5UserAgent<TKeyManager extends AgentKeyManager = LocalKeyManager
158
164
  });
159
165
 
160
166
  dwnApi ??= new AgentDwnApi({
161
- dwn: await AgentDwnApi.createDwn({ dataPath, didResolver: didApi })
167
+ dwn : await AgentDwnApi.createDwn({ dataPath, didResolver: didApi }),
168
+ localDwnStrategy : localDwnStrategy ?? 'prefer',
162
169
  });
170
+ if (localDwnStrategy) {
171
+ dwnApi.setLocalDwnStrategy(localDwnStrategy);
172
+ }
163
173
 
164
174
  identityApi ??= new AgentIdentityApi({ store: new DwnIdentityStore() });
165
175
 
@@ -250,4 +260,4 @@ export class Web5UserAgent<TKeyManager extends AgentKeyManager = LocalKeyManager
250
260
  // Set the Agent's DID.
251
261
  this.agentDid = await this.vault.getDid();
252
262
  }
253
- }
263
+ }