@enbox/agent 0.2.2 → 0.3.1

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 (133) hide show
  1. package/dist/browser.mjs +9 -9
  2. package/dist/browser.mjs.map +4 -4
  3. package/dist/esm/agent-did-resolver-cache.js.map +1 -1
  4. package/dist/esm/anonymous-dwn-api.js +1 -1
  5. package/dist/esm/bearer-identity.js +1 -1
  6. package/dist/esm/connect.js +5 -9
  7. package/dist/esm/connect.js.map +1 -1
  8. package/dist/esm/did-api.js +3 -3
  9. package/dist/esm/did-api.js.map +1 -1
  10. package/dist/esm/dwn-api.js +39 -8
  11. package/dist/esm/dwn-api.js.map +1 -1
  12. package/dist/esm/dwn-discovery-file.js +244 -0
  13. package/dist/esm/dwn-discovery-file.js.map +1 -0
  14. package/dist/esm/dwn-discovery-payload.js +253 -0
  15. package/dist/esm/dwn-discovery-payload.js.map +1 -0
  16. package/dist/esm/dwn-encryption.js.map +1 -1
  17. package/dist/esm/dwn-key-delivery.js.map +1 -1
  18. package/dist/esm/dwn-record-upgrade.js.map +1 -1
  19. package/dist/esm/{web5-user-agent.js → enbox-user-agent.js} +12 -7
  20. package/dist/esm/enbox-user-agent.js.map +1 -0
  21. package/dist/esm/identity-api.js +3 -3
  22. package/dist/esm/identity-api.js.map +1 -1
  23. package/dist/esm/index.js +3 -1
  24. package/dist/esm/index.js.map +1 -1
  25. package/dist/esm/local-dwn.js +150 -26
  26. package/dist/esm/local-dwn.js.map +1 -1
  27. package/dist/esm/local-key-manager.js +2 -2
  28. package/dist/esm/local-key-manager.js.map +1 -1
  29. package/dist/esm/oidc.js +11 -11
  30. package/dist/esm/oidc.js.map +1 -1
  31. package/dist/esm/permissions-api.js.map +1 -1
  32. package/dist/esm/store-data.js.map +1 -1
  33. package/dist/esm/sync-api.js +2 -2
  34. package/dist/esm/sync-api.js.map +1 -1
  35. package/dist/esm/sync-engine-level.js +2 -2
  36. package/dist/esm/sync-engine-level.js.map +1 -1
  37. package/dist/esm/test-harness.js +3 -3
  38. package/dist/esm/test-harness.js.map +1 -1
  39. package/dist/esm/utils-internal.js +2 -2
  40. package/dist/types/agent-did-resolver-cache.d.ts +7 -7
  41. package/dist/types/agent-did-resolver-cache.d.ts.map +1 -1
  42. package/dist/types/anonymous-dwn-api.d.ts +3 -3
  43. package/dist/types/anonymous-dwn-api.d.ts.map +1 -1
  44. package/dist/types/bearer-identity.d.ts +1 -1
  45. package/dist/types/connect.d.ts +8 -8
  46. package/dist/types/connect.d.ts.map +1 -1
  47. package/dist/types/did-api.d.ts +12 -11
  48. package/dist/types/did-api.d.ts.map +1 -1
  49. package/dist/types/dwn-api.d.ts +27 -11
  50. package/dist/types/dwn-api.d.ts.map +1 -1
  51. package/dist/types/dwn-discovery-file.d.ts +122 -0
  52. package/dist/types/dwn-discovery-file.d.ts.map +1 -0
  53. package/dist/types/dwn-discovery-payload.d.ts +105 -0
  54. package/dist/types/dwn-discovery-payload.d.ts.map +1 -0
  55. package/dist/types/dwn-encryption.d.ts +8 -8
  56. package/dist/types/dwn-encryption.d.ts.map +1 -1
  57. package/dist/types/dwn-key-delivery.d.ts +5 -5
  58. package/dist/types/dwn-key-delivery.d.ts.map +1 -1
  59. package/dist/types/dwn-record-upgrade.d.ts +2 -2
  60. package/dist/types/dwn-record-upgrade.d.ts.map +1 -1
  61. package/dist/types/{web5-user-agent.d.ts → enbox-user-agent.d.ts} +17 -13
  62. package/dist/types/enbox-user-agent.d.ts.map +1 -0
  63. package/dist/types/identity-api.d.ts +10 -10
  64. package/dist/types/identity-api.d.ts.map +1 -1
  65. package/dist/types/index.d.ts +3 -1
  66. package/dist/types/index.d.ts.map +1 -1
  67. package/dist/types/local-dwn.d.ts +93 -15
  68. package/dist/types/local-dwn.d.ts.map +1 -1
  69. package/dist/types/local-key-manager.d.ts +9 -9
  70. package/dist/types/local-key-manager.d.ts.map +1 -1
  71. package/dist/types/oidc.d.ts +23 -19
  72. package/dist/types/oidc.d.ts.map +1 -1
  73. package/dist/types/permissions-api.d.ts +4 -4
  74. package/dist/types/permissions-api.d.ts.map +1 -1
  75. package/dist/types/store-data.d.ts +3 -3
  76. package/dist/types/store-data.d.ts.map +1 -1
  77. package/dist/types/store-did.d.ts +2 -2
  78. package/dist/types/store-did.d.ts.map +1 -1
  79. package/dist/types/store-identity.d.ts +2 -2
  80. package/dist/types/store-identity.d.ts.map +1 -1
  81. package/dist/types/store-key.d.ts +2 -2
  82. package/dist/types/store-key.d.ts.map +1 -1
  83. package/dist/types/sync-api.d.ts +9 -9
  84. package/dist/types/sync-api.d.ts.map +1 -1
  85. package/dist/types/sync-engine-level.d.ts +9 -9
  86. package/dist/types/sync-engine-level.d.ts.map +1 -1
  87. package/dist/types/sync-messages.d.ts +5 -5
  88. package/dist/types/sync-messages.d.ts.map +1 -1
  89. package/dist/types/test-harness.d.ts +4 -4
  90. package/dist/types/test-harness.d.ts.map +1 -1
  91. package/dist/types/types/agent.d.ts +24 -19
  92. package/dist/types/types/agent.d.ts.map +1 -1
  93. package/dist/types/types/identity.d.ts +1 -1
  94. package/dist/types/types/key-manager.d.ts +2 -2
  95. package/dist/types/types/key-manager.d.ts.map +1 -1
  96. package/dist/types/types/sync.d.ts +2 -2
  97. package/dist/types/types/sync.d.ts.map +1 -1
  98. package/dist/types/utils-internal.d.ts +4 -4
  99. package/dist/types/utils-internal.d.ts.map +1 -1
  100. package/package.json +6 -6
  101. package/src/agent-did-resolver-cache.ts +8 -8
  102. package/src/anonymous-dwn-api.ts +4 -4
  103. package/src/bearer-identity.ts +1 -1
  104. package/src/connect.ts +14 -19
  105. package/src/did-api.ts +13 -11
  106. package/src/dwn-api.ts +61 -16
  107. package/src/dwn-discovery-file.ts +305 -0
  108. package/src/dwn-discovery-payload.ts +308 -0
  109. package/src/dwn-encryption.ts +8 -8
  110. package/src/dwn-key-delivery.ts +5 -5
  111. package/src/dwn-record-upgrade.ts +2 -2
  112. package/src/{web5-user-agent.ts → enbox-user-agent.ts} +26 -16
  113. package/src/identity-api.ts +11 -11
  114. package/src/index.ts +3 -1
  115. package/src/local-dwn.ts +154 -28
  116. package/src/local-key-manager.ts +10 -10
  117. package/src/oidc.ts +40 -30
  118. package/src/permissions-api.ts +5 -5
  119. package/src/store-data.ts +7 -7
  120. package/src/store-did.ts +2 -2
  121. package/src/store-identity.ts +2 -2
  122. package/src/store-key.ts +2 -2
  123. package/src/sync-api.ts +10 -10
  124. package/src/sync-engine-level.ts +12 -12
  125. package/src/sync-messages.ts +5 -5
  126. package/src/test-harness.ts +9 -9
  127. package/src/types/agent.ts +31 -20
  128. package/src/types/identity.ts +1 -1
  129. package/src/types/key-manager.ts +2 -2
  130. package/src/types/sync.ts +2 -2
  131. package/src/utils-internal.ts +4 -4
  132. package/dist/esm/web5-user-agent.js.map +0 -1
  133. package/dist/types/web5-user-agent.d.ts.map +0 -1
package/src/connect.ts CHANGED
@@ -1,6 +1,6 @@
1
1
 
2
2
  import type { PushedAuthResponse } from './oidc.js';
3
- import type { DwnPermissionScope, DwnProtocolDefinition, Web5ConnectAuthResponse } from './index.js';
3
+ import type { DwnPermissionScope, DwnProtocolDefinition, EnboxConnectAuthResponse } from './index.js';
4
4
 
5
5
  import { CryptoUtils } from '@enbox/crypto';
6
6
  import { DidJwk } from '@enbox/dids';
@@ -21,8 +21,8 @@ async function initClient({
21
21
  onWalletUriReady,
22
22
  validatePin,
23
23
  }: WalletConnectOptions): Promise<{
24
- delegateGrants: Web5ConnectAuthResponse['delegateGrants'];
25
- delegatePortableDid: Web5ConnectAuthResponse['delegatePortableDid'];
24
+ delegateGrants: EnboxConnectAuthResponse['delegateGrants'];
25
+ delegatePortableDid: EnboxConnectAuthResponse['delegatePortableDid'];
26
26
  connectedDid: string;
27
27
  } | undefined> {
28
28
  // ephemeral client did for ECDH, signing, verification
@@ -68,21 +68,16 @@ async function initClient({
68
68
  encryptionKey,
69
69
  });
70
70
 
71
- // Convert the encrypted Request Object to URLSearchParams for form encoding.
72
- const formEncodedRequest = new URLSearchParams({
73
- request: requestObjectJwe,
74
- });
75
-
76
71
  const pushedAuthorizationRequestEndpoint = Oidc.buildOidcUrl({
77
72
  baseURL : connectServerUrl,
78
73
  endpoint : 'pushedAuthorizationRequest',
79
74
  });
80
75
 
81
76
  const parResponse = await fetch(pushedAuthorizationRequestEndpoint, {
82
- body : formEncodedRequest,
77
+ body : JSON.stringify({ request: requestObjectJwe }),
83
78
  method : 'POST',
84
79
  headers : {
85
- 'Content-Type': 'application/x-www-form-urlencoded',
80
+ 'Content-Type': 'application/json',
86
81
  },
87
82
  signal: AbortSignal.timeout(30_000),
88
83
  });
@@ -93,8 +88,8 @@ async function initClient({
93
88
 
94
89
  const parData: PushedAuthResponse = await parResponse.json();
95
90
 
96
- // a deeplink to a web5 compatible wallet. if the wallet scans this link it should receive
97
- // a route to its web5 connect provider flow and the params of where to fetch the auth request.
91
+ // a deeplink to a compatible wallet. if the wallet scans this link it should receive
92
+ // a route to its Connect provider flow and the params of where to fetch the auth request.
98
93
  logger.log(`Wallet URI: ${walletUri}`);
99
94
  const generatedWalletUri = new URL(walletUri);
100
95
  generatedWalletUri.searchParams.set('request_uri', parData.request_uri);
@@ -112,7 +107,7 @@ async function initClient({
112
107
  tokenParam : request.state,
113
108
  });
114
109
 
115
- // subscribe to receiving a response from the wallet with default TTL. receive ciphertext of {@link Web5ConnectAuthResponse}
110
+ // subscribe to receiving a response from the wallet with default TTL. receive ciphertext of {@link EnboxConnectAuthResponse}
116
111
  const authResponse = await pollWithTtl(() => fetch(tokenUrl, { signal: AbortSignal.timeout(30_000) }));
117
112
 
118
113
  if (authResponse) {
@@ -123,7 +118,7 @@ async function initClient({
123
118
  const jwt = await Oidc.decryptAuthResponse(clientDid, jwe, pin);
124
119
  const verifiedAuthResponse = (await Oidc.verifyJwt({
125
120
  jwt,
126
- })) as Web5ConnectAuthResponse;
121
+ })) as EnboxConnectAuthResponse;
127
122
 
128
123
  return {
129
124
  delegateGrants : verifiedAuthResponse.delegateGrants,
@@ -159,20 +154,20 @@ export type WalletConnectOptions = {
159
154
  permissionRequests: ConnectPermissionRequest[];
160
155
 
161
156
  /**
162
- * The Web5 API provides a URI to the wallet based on the `walletUri` plus a query params payload valid for 5 minutes.
157
+ * The Connect API provides a URI to the wallet based on the `walletUri` plus a query params payload valid for 5 minutes.
163
158
  * The link can either be used as a deep link on the same device or a QR code for cross device or both.
164
159
  * The query params are `{ request_uri: string; encryption_key: string; }`
165
160
  * The wallet will use the `request_uri to contact the intermediary server's `authorize` endpoint
166
- * and pull down the {@link Web5ConnectAuthRequest} and use the `encryption_key` to decrypt it.
161
+ * and pull down the {@link EnboxConnectAuthRequest} and use the `encryption_key` to decrypt it.
167
162
  *
168
- * @param uri - The URI returned by the web5 connect API to be passed to a provider.
163
+ * @param uri - The URI returned by the Connect API to be passed to a provider.
169
164
  */
170
165
  onWalletUriReady: (uri: string) => void;
171
166
 
172
167
  /**
173
168
  * Function that must be provided to submit the pin entered by the user on the client.
174
- * The pin is used to decrypt the {@link Web5ConnectAuthResponse} that was retrieved from the
175
- * token endpoint by the client inside of web5 connect.
169
+ * The pin is used to decrypt the {@link EnboxConnectAuthResponse} that was retrieved from the
170
+ * token endpoint by the client inside of Connect.
176
171
  *
177
172
  * @returns A promise that resolves to the PIN as a string.
178
173
  */
package/src/did-api.ts CHANGED
@@ -8,6 +8,7 @@ import type {
8
8
  DidResolutionResult,
9
9
  DidResolverCache,
10
10
  DidVerificationMethod,
11
+ DidWebCreateOptions,
11
12
  PortableDid,
12
13
  } from '@enbox/dids';
13
14
 
@@ -15,7 +16,7 @@ import { BearerDid, Did, DidDht, UniversalResolver } from '@enbox/dids';
15
16
 
16
17
  import type { AgentDataStore } from './store-data.js';
17
18
  import type { AgentKeyManager } from './types/key-manager.js';
18
- import type { ResponseStatus, Web5PlatformAgent } from './types/agent.js';
19
+ import type { EnboxPlatformAgent, ResponseStatus } from './types/agent.js';
19
20
 
20
21
  import { AgentDidResolverCache } from './agent-did-resolver-cache.js';
21
22
  import { canonicalize } from '@enbox/crypto';
@@ -77,12 +78,13 @@ export interface DidCreateParams<
77
78
  export interface DidMethodCreateOptions<TKeyManager> {
78
79
  dht: DidDhtCreateOptions<TKeyManager>;
79
80
  jwk: DidJwkCreateOptions<TKeyManager>;
81
+ web: DidWebCreateOptions<TKeyManager>;
80
82
  }
81
83
 
82
84
  export interface DidApiParams {
83
85
  didMethods: DidMethodApi[];
84
86
 
85
- agent?: Web5PlatformAgent;
87
+ agent?: EnboxPlatformAgent;
86
88
 
87
89
  /**
88
90
  * An optional `DidResolverCache` instance used for caching resolved DID documents.
@@ -105,19 +107,19 @@ export function isDidRequest<T extends DidInterface>(
105
107
  }
106
108
 
107
109
  /**
108
- * This API is used to manage and interact with DIDs within the Web5 Agent framework.
110
+ * This API is used to manage and interact with DIDs within the Enbox Agent framework.
109
111
  *
110
112
  * If a DWN Data Store is used, the DID information is stored under DID's own tenant by default.
111
113
  * If a tenant property is passed, that tenant will be used to store the DID information.
112
114
  */
113
115
  export class AgentDidApi<TKeyManager extends AgentKeyManager = AgentKeyManager> extends UniversalResolver {
114
116
  /**
115
- * Holds the instance of a `Web5PlatformAgent` that represents the current execution context for
116
- * the `AgentDidApi`. This agent is used to interact with other Web5 agent components. It's vital
117
- * to ensure this instance is set to correctly contextualize operations within the broader Web5
117
+ * Holds the instance of a `EnboxPlatformAgent` that represents the current execution context for
118
+ * the `AgentDidApi`. This agent is used to interact with other Enbox agent components. It's vital
119
+ * to ensure this instance is set to correctly contextualize operations within the broader Enbox
118
120
  * Agent framework.
119
121
  */
120
- private _agent?: Web5PlatformAgent;
122
+ private _agent?: EnboxPlatformAgent;
121
123
 
122
124
  private _didMethods: Map<string, DidMethodApi> = new Map();
123
125
 
@@ -146,12 +148,12 @@ export class AgentDidApi<TKeyManager extends AgentKeyManager = AgentKeyManager>
146
148
  }
147
149
 
148
150
  /**
149
- * Retrieves the `Web5PlatformAgent` execution context.
151
+ * Retrieves the `EnboxPlatformAgent` execution context.
150
152
  *
151
- * @returns The `Web5PlatformAgent` instance that represents the current execution context.
153
+ * @returns The `EnboxPlatformAgent` instance that represents the current execution context.
152
154
  * @throws Will throw an error if the `agent` instance property is undefined.
153
155
  */
154
- get agent(): Web5PlatformAgent {
156
+ get agent(): EnboxPlatformAgent {
155
157
  if (this._agent === undefined) {
156
158
  throw new Error('AgentDidApi: Unable to determine agent execution context.');
157
159
  }
@@ -159,7 +161,7 @@ export class AgentDidApi<TKeyManager extends AgentKeyManager = AgentKeyManager>
159
161
  return this._agent;
160
162
  }
161
163
 
162
- set agent(agent: Web5PlatformAgent) {
164
+ set agent(agent: EnboxPlatformAgent) {
163
165
  this._agent = agent;
164
166
 
165
167
  // AgentDidResolverCache should set the agent if it is the type of cache being used
package/src/dwn-api.ts CHANGED
@@ -33,8 +33,8 @@ import {
33
33
  import { CryptoUtils, X25519 } from '@enbox/crypto';
34
34
  import { DidDht, DidJwk, DidResolverCacheLevel, UniversalResolver } from '@enbox/dids';
35
35
 
36
+ import type { EnboxPlatformAgent } from './types/agent.js';
36
37
  import type { LocalDwnStrategy } from './local-dwn.js';
37
- import type { Web5PlatformAgent } from './types/agent.js';
38
38
  import type {
39
39
  DwnMessage,
40
40
  DwnMessageInstance,
@@ -48,6 +48,7 @@ import type {
48
48
  SendDwnRequest,
49
49
  } from './types/dwn.js';
50
50
 
51
+ import { DwnDiscoveryFile } from './dwn-discovery-file.js';
51
52
  import { KeyDeliveryProtocolDefinition } from './store-data-protocols.js';
52
53
  import { LocalDwnDiscovery } from './local-dwn.js';
53
54
  import { DwnInterface, dwnMessageConstructors } from './types/dwn.js';
@@ -101,7 +102,7 @@ type DwnMessageWithBlob<T extends DwnInterface> = {
101
102
  };
102
103
 
103
104
  type DwnApiParams = {
104
- agent?: Web5PlatformAgent;
105
+ agent?: EnboxPlatformAgent;
105
106
  dwn: Dwn;
106
107
  localDwnStrategy?: LocalDwnStrategy;
107
108
  };
@@ -112,12 +113,12 @@ interface DwnApiCreateDwnParams extends Partial<DwnConfig> {
112
113
 
113
114
  export class AgentDwnApi {
114
115
  /**
115
- * Holds the instance of a `Web5PlatformAgent` that represents the current execution context for
116
- * the `AgentDwnApi`. This agent is used to interact with other Web5 agent components. It's vital
117
- * to ensure this instance is set to correctly contextualize operations within the broader Web5
116
+ * Holds the instance of a `EnboxPlatformAgent` that represents the current execution context for
117
+ * the `AgentDwnApi`. This agent is used to interact with other Enbox agent components. It's vital
118
+ * to ensure this instance is set to correctly contextualize operations within the broader Enbox
118
119
  * Agent framework.
119
120
  */
120
- private _agent?: Web5PlatformAgent;
121
+ private _agent?: EnboxPlatformAgent;
121
122
 
122
123
  /**
123
124
  * The DWN instance to use for this API.
@@ -177,17 +178,21 @@ export class AgentDwnApi {
177
178
 
178
179
  // If agent is already available, eagerly initialize the discovery instance.
179
180
  if (agent) {
180
- this._localDwnDiscovery = new LocalDwnDiscovery(agent.rpc);
181
+ this._localDwnDiscovery = new LocalDwnDiscovery(
182
+ agent.rpc,
183
+ 10_000,
184
+ AgentDwnApi._tryCreateDiscoveryFile(),
185
+ );
181
186
  }
182
187
  }
183
188
 
184
189
  /**
185
- * Retrieves the `Web5PlatformAgent` execution context.
190
+ * Retrieves the `EnboxPlatformAgent` execution context.
186
191
  *
187
- * @returns The `Web5PlatformAgent` instance that represents the current execution context.
192
+ * @returns The `EnboxPlatformAgent` instance that represents the current execution context.
188
193
  * @throws Will throw an error if the `agent` instance property is undefined.
189
194
  */
190
- get agent(): Web5PlatformAgent {
195
+ get agent(): EnboxPlatformAgent {
191
196
  if (this._agent === undefined) {
192
197
  throw new Error('AgentDwnApi: Unable to determine agent execution context.');
193
198
  }
@@ -195,10 +200,14 @@ export class AgentDwnApi {
195
200
  return this._agent;
196
201
  }
197
202
 
198
- set agent(agent: Web5PlatformAgent) {
203
+ set agent(agent: EnboxPlatformAgent) {
199
204
  this._agent = agent;
200
205
  // Re-initialize local DWN discovery with the new agent's RPC client.
201
- this._localDwnDiscovery = new LocalDwnDiscovery(agent.rpc);
206
+ this._localDwnDiscovery = new LocalDwnDiscovery(
207
+ agent.rpc,
208
+ 10_000,
209
+ AgentDwnApi._tryCreateDiscoveryFile(),
210
+ );
202
211
  this._localManagedDidCache.clear();
203
212
  }
204
213
 
@@ -210,6 +219,24 @@ export class AgentDwnApi {
210
219
  this._localDwnStrategy = strategy;
211
220
  }
212
221
 
222
+ /**
223
+ * Inject a cached local DWN endpoint (e.g. from a `dwn://register`
224
+ * browser redirect or from persisted storage). The endpoint is validated
225
+ * via `GET /info` before being accepted.
226
+ *
227
+ * @param endpoint - The local DWN server base URL.
228
+ * @returns `true` if the endpoint was validated and cached, `false` otherwise.
229
+ * @see https://github.com/enboxorg/enbox/issues/589
230
+ */
231
+ public async setCachedLocalDwnEndpoint(endpoint: string): Promise<boolean> {
232
+ this._localDwnDiscovery ??= new LocalDwnDiscovery(
233
+ this.agent.rpc,
234
+ 10_000,
235
+ AgentDwnApi._tryCreateDiscoveryFile(),
236
+ );
237
+ return this._localDwnDiscovery.setCachedEndpoint(endpoint);
238
+ }
239
+
213
240
  /**
214
241
  * Resolves the DWN service endpoint URLs for the given target DID, optionally
215
242
  * prepending a local DWN server endpoint when local discovery is enabled and
@@ -231,7 +258,7 @@ export class AgentDwnApi {
231
258
  if (!localDwnEndpoint) {
232
259
  throw new Error(
233
260
  `AgentDwnApi: Local DWN strategy is 'only' but no local server is available ` +
234
- `on localhost/127.0.0.1:{3000,55555-55559}`
261
+ `on 127.0.0.1:{3000,55500-55509}`
235
262
  );
236
263
  }
237
264
 
@@ -259,12 +286,30 @@ export class AgentDwnApi {
259
286
  return [...uniqueEndpoints];
260
287
  }
261
288
 
262
- /** Lazily retrieves the local DWN server endpoint via discovery probing. */
289
+ /** Lazily retrieves the local DWN server endpoint via discovery. */
263
290
  private async getLocalDwnEndpoint(): Promise<string | undefined> {
264
- this._localDwnDiscovery ??= new LocalDwnDiscovery(this.agent.rpc);
291
+ this._localDwnDiscovery ??= new LocalDwnDiscovery(
292
+ this.agent.rpc,
293
+ 10_000,
294
+ AgentDwnApi._tryCreateDiscoveryFile(),
295
+ );
265
296
  return this._localDwnDiscovery.getEndpoint();
266
297
  }
267
298
 
299
+ /**
300
+ * Attempt to create a {@link DwnDiscoveryFile} for file-based local DWN
301
+ * discovery. Returns `undefined` in environments where the filesystem
302
+ * is not available (e.g. browsers).
303
+ */
304
+ private static _tryCreateDiscoveryFile(): DwnDiscoveryFile | undefined {
305
+ try {
306
+ return new DwnDiscoveryFile();
307
+ } catch {
308
+ // Browser environment — node:fs/promises not available.
309
+ return undefined;
310
+ }
311
+ }
312
+
268
313
  /**
269
314
  * Determines whether the given target DID should be routed through the
270
315
  * local DWN server. Returns `true` if the DID is the agent DID or one
@@ -315,7 +360,7 @@ export class AgentDwnApi {
315
360
  * However, it is recommended to use the `processRequest` method to interact with the DWN
316
361
  * instance to ensure that the DWN message is constructed correctly.
317
362
  * - The getter is named `node` to avoid confusion with the `dwn` property of the
318
- * `Web5PlatformAgent`. In other words, so that a developer can call `agent.dwn.node` to access
363
+ * `EnboxPlatformAgent`. In other words, so that a developer can call `agent.dwn.node` to access
319
364
  * the DWN instance and not `agent.dwn.dwn`.
320
365
  */
321
366
  get node(): Dwn {
@@ -0,0 +1,305 @@
1
+ /**
2
+ * File-based local DWN discovery for CLI and native apps.
3
+ *
4
+ * When `electrobun-dwn` (or any local DWN server) starts, it writes a
5
+ * well-known file (`~/.enbox/dwn.json`) containing the DWN endpoint URL
6
+ * and the server PID. CLI tools and native apps read this file to discover
7
+ * the local DWN without port probing.
8
+ *
9
+ * The filesystem operations are abstracted behind {@link DiscoveryFileFs}
10
+ * so the module can be tested without touching the real filesystem, and
11
+ * adapted to runtimes that provide different I/O primitives.
12
+ *
13
+ * @see https://github.com/enboxorg/enbox/issues/587
14
+ * @module
15
+ */
16
+
17
+ import { normalizeBaseUrl } from './local-dwn.js';
18
+
19
+ // ─── Types ────────────────────────────────────────────────────────
20
+
21
+ /**
22
+ * The JSON shape persisted in the discovery file.
23
+ *
24
+ * @see https://identity.foundation/dwn-transport/#discovery-file
25
+ */
26
+ export interface DwnDiscoveryRecord {
27
+ /** Base URL of the running DWN server (e.g. `"http://127.0.0.1:55500"`). */
28
+ endpoint: string;
29
+ /** OS process ID of the DWN server. Used for liveness checking. */
30
+ pid: number;
31
+ /**
32
+ * Transport capabilities advertised by the server (e.g. `["http", "ws"]`).
33
+ * Optional per the DWN Transport Spec.
34
+ */
35
+ capabilities?: string[];
36
+ }
37
+
38
+ /**
39
+ * Minimal filesystem interface required by {@link DwnDiscoveryFile}.
40
+ *
41
+ * Consumers can provide a custom implementation for testing or for
42
+ * runtimes that do not expose Node-compatible `fs` and `os` modules.
43
+ */
44
+ export interface DiscoveryFileFs {
45
+ /** Read the file at `path` and return its UTF-8 contents, or `null` if not found. */
46
+ readFile(path: string): Promise<string | null>;
47
+ /** Write `contents` to the file at `path`, creating parent directories as needed. */
48
+ writeFile(path: string, contents: string): Promise<void>;
49
+ /** Delete the file at `path`. Must not throw if the file does not exist. */
50
+ removeFile(path: string): Promise<void>;
51
+ /** Return `true` if the process with the given PID is alive. */
52
+ isProcessAlive(pid: number): boolean;
53
+ /** Return the user's home directory (e.g. `/home/alice`). */
54
+ homedir(): string;
55
+ }
56
+
57
+ // ─── Default Node/Bun filesystem implementation ──────────────────
58
+
59
+ /**
60
+ * Creates a {@link DiscoveryFileFs} backed by Node.js / Bun built-in
61
+ * modules. Returns `undefined` in environments where `node:fs/promises`
62
+ * or `node:os` are not available (e.g. browsers).
63
+ */
64
+ export function createNodeDiscoveryFileFs(): DiscoveryFileFs | undefined {
65
+ try {
66
+ // Dynamic require avoids hard dependency on Node built-ins so bundlers
67
+ // can tree-shake this path away in browser builds.
68
+ const nodeRequire = require;
69
+ const fs = nodeRequire('node:fs/promises') as {
70
+ readFile(path: string, encoding: string): Promise<string>;
71
+ writeFile(path: string, data: string, options: { encoding: string; mode?: number }): Promise<void>;
72
+ mkdir(path: string, options: { recursive: boolean }): Promise<string | undefined>;
73
+ unlink(path: string): Promise<void>;
74
+ };
75
+ const path = nodeRequire('node:path') as {
76
+ join(...segments: string[]): string;
77
+ dirname(path: string): string;
78
+ };
79
+ const os = nodeRequire('node:os') as {
80
+ homedir(): string;
81
+ };
82
+
83
+ return {
84
+ async readFile(filePath: string): Promise<string | null> {
85
+ try {
86
+ return await fs.readFile(filePath, 'utf-8');
87
+ } catch {
88
+ return null;
89
+ }
90
+ },
91
+
92
+ async writeFile(filePath: string, contents: string): Promise<void> {
93
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
94
+ // mode 0o600: owner read/write only — the file contains the PID
95
+ // and endpoint of a local DWN server.
96
+ await fs.writeFile(filePath, contents, { encoding: 'utf-8', mode: 0o600 });
97
+ },
98
+
99
+ async removeFile(filePath: string): Promise<void> {
100
+ try {
101
+ await fs.unlink(filePath);
102
+ } catch {
103
+ // Ignore ENOENT — the file was already gone.
104
+ }
105
+ },
106
+
107
+ isProcessAlive(pid: number): boolean {
108
+ try {
109
+ process.kill(pid, 0);
110
+ return true;
111
+ } catch {
112
+ return false;
113
+ }
114
+ },
115
+
116
+ homedir(): string {
117
+ return os.homedir();
118
+ },
119
+ };
120
+ } catch {
121
+ return undefined;
122
+ }
123
+ }
124
+
125
+ // ─── Constants ────────────────────────────────────────────────────
126
+
127
+ /**
128
+ * Directory under the user's home where the discovery file lives.
129
+ * Shared with the `electrobun-dwn` app and other Enbox tooling.
130
+ */
131
+ export const DISCOVERY_DIR = '.enbox';
132
+
133
+ /** Filename of the discovery file. */
134
+ export const DISCOVERY_FILENAME = 'dwn.json';
135
+
136
+ // ─── DwnDiscoveryFile ────────────────────────────────────────────
137
+
138
+ /**
139
+ * Reads, writes, and validates the `~/.enbox/dwn.json` discovery file.
140
+ *
141
+ * This is the **file-based discovery channel** for CLI and native apps.
142
+ * It is complementary to the `dwn://register` browser redirect flow.
143
+ *
144
+ * @example Reading the discovery file
145
+ * ```ts
146
+ * const discoveryFile = new DwnDiscoveryFile();
147
+ * const record = await discoveryFile.read();
148
+ *
149
+ * if (record) {
150
+ * console.log(`Local DWN at ${record.endpoint}`);
151
+ * }
152
+ * ```
153
+ *
154
+ * @example Writing the discovery file (from electrobun-dwn)
155
+ * ```ts
156
+ * const discoveryFile = new DwnDiscoveryFile();
157
+ * await discoveryFile.write({
158
+ * endpoint : 'http://127.0.0.1:55557',
159
+ * pid : process.pid,
160
+ * });
161
+ * ```
162
+ */
163
+ export class DwnDiscoveryFile {
164
+ private readonly _fs: DiscoveryFileFs;
165
+ private readonly _filePath: string;
166
+
167
+ /**
168
+ * @param fs - Filesystem adapter. Defaults to Node/Bun built-ins.
169
+ * @param filePath - Override the discovery file path (mainly for testing).
170
+ * @throws If no filesystem adapter is available (e.g. in a browser).
171
+ */
172
+ constructor(fs?: DiscoveryFileFs, filePath?: string) {
173
+ const resolvedFs = fs ?? createNodeDiscoveryFileFs();
174
+ if (!resolvedFs) {
175
+ throw new Error(
176
+ 'DwnDiscoveryFile: No filesystem adapter available. ' +
177
+ 'Provide a DiscoveryFileFs implementation or run in Node.js / Bun.'
178
+ );
179
+ }
180
+ this._fs = resolvedFs;
181
+
182
+ if (filePath) {
183
+ this._filePath = filePath;
184
+ } else {
185
+ // Dynamic require so we can resolve the path at construction time.
186
+ const nodeRequire = require;
187
+ const path = nodeRequire('node:path') as { join(...segments: string[]): string };
188
+ this._filePath = path.join(resolvedFs.homedir(), DISCOVERY_DIR, DISCOVERY_FILENAME);
189
+ }
190
+ }
191
+
192
+ /** The absolute path of the discovery file. */
193
+ public get path(): string {
194
+ return this._filePath;
195
+ }
196
+
197
+ /**
198
+ * Read and validate the discovery file.
199
+ *
200
+ * Returns the parsed {@link DwnDiscoveryRecord} if:
201
+ * 1. The file exists and contains valid JSON.
202
+ * 2. The `endpoint` is a non-empty string.
203
+ * 3. The `pid` is a positive integer whose process is still alive.
204
+ *
205
+ * Returns `undefined` in all other cases (missing file, parse error,
206
+ * stale PID). Stale files are automatically removed.
207
+ */
208
+ public async read(): Promise<DwnDiscoveryRecord | undefined> {
209
+ const raw = await this._fs.readFile(this._filePath);
210
+ if (raw === null) {
211
+ return undefined;
212
+ }
213
+
214
+ let parsed: unknown;
215
+ try {
216
+ parsed = JSON.parse(raw);
217
+ } catch {
218
+ // Corrupted file — remove it.
219
+ await this._fs.removeFile(this._filePath);
220
+ return undefined;
221
+ }
222
+
223
+ if (!isValidRecord(parsed)) {
224
+ await this._fs.removeFile(this._filePath);
225
+ return undefined;
226
+ }
227
+
228
+ // Check that the server process is still alive.
229
+ if (!this._fs.isProcessAlive(parsed.pid)) {
230
+ await this._fs.removeFile(this._filePath);
231
+ return undefined;
232
+ }
233
+
234
+ const result: DwnDiscoveryRecord = {
235
+ endpoint : normalizeBaseUrl(parsed.endpoint),
236
+ pid : parsed.pid,
237
+ };
238
+
239
+ if (parsed.capabilities !== undefined) {
240
+ result.capabilities = parsed.capabilities;
241
+ }
242
+
243
+ return result;
244
+ }
245
+
246
+ /**
247
+ * Write the discovery file. Creates the `~/.enbox/` directory if needed.
248
+ *
249
+ * @param record - The endpoint and PID to persist.
250
+ */
251
+ public async write(record: DwnDiscoveryRecord): Promise<void> {
252
+ const serialized: Record<string, unknown> = {
253
+ endpoint : normalizeBaseUrl(record.endpoint),
254
+ pid : record.pid,
255
+ };
256
+
257
+ if (record.capabilities !== undefined && record.capabilities.length > 0) {
258
+ serialized.capabilities = record.capabilities;
259
+ }
260
+
261
+ const json = JSON.stringify(serialized, null, 2);
262
+ await this._fs.writeFile(this._filePath, json);
263
+ }
264
+
265
+ /**
266
+ * Remove the discovery file. Does not throw if the file is already gone.
267
+ */
268
+ public async remove(): Promise<void> {
269
+ await this._fs.removeFile(this._filePath);
270
+ }
271
+ }
272
+
273
+ // ─── Internal helpers ─────────────────────────────────────────────
274
+
275
+ /** Type guard for a valid {@link DwnDiscoveryRecord}. */
276
+ function isValidRecord(value: unknown): value is DwnDiscoveryRecord {
277
+ if (typeof value !== 'object' || value === null) {
278
+ return false;
279
+ }
280
+
281
+ const record = value as Record<string, unknown>;
282
+
283
+ if (typeof record.endpoint !== 'string' || record.endpoint.length === 0) {
284
+ return false;
285
+ }
286
+
287
+ if (typeof record.pid !== 'number' || !Number.isInteger(record.pid) || record.pid <= 0) {
288
+ return false;
289
+ }
290
+
291
+ // `capabilities` is optional, but when present must be a string array.
292
+ if (record.capabilities !== undefined) {
293
+ if (!Array.isArray(record.capabilities)) {
294
+ return false;
295
+ }
296
+
297
+ if (!record.capabilities.every((item: unknown) => typeof item === 'string')) {
298
+ return false;
299
+ }
300
+ }
301
+
302
+ return true;
303
+ }
304
+
305
+