@enbox/agent 0.2.2 → 0.3.0
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/dist/browser.mjs +9 -9
- package/dist/browser.mjs.map +4 -4
- package/dist/esm/agent-did-resolver-cache.js.map +1 -1
- package/dist/esm/anonymous-dwn-api.js +1 -1
- package/dist/esm/bearer-identity.js +1 -1
- package/dist/esm/connect.js +3 -3
- package/dist/esm/connect.js.map +1 -1
- package/dist/esm/did-api.js +3 -3
- package/dist/esm/did-api.js.map +1 -1
- package/dist/esm/dwn-api.js +39 -8
- package/dist/esm/dwn-api.js.map +1 -1
- package/dist/esm/dwn-discovery-file.js +244 -0
- package/dist/esm/dwn-discovery-file.js.map +1 -0
- package/dist/esm/dwn-discovery-payload.js +253 -0
- package/dist/esm/dwn-discovery-payload.js.map +1 -0
- package/dist/esm/dwn-encryption.js.map +1 -1
- package/dist/esm/dwn-key-delivery.js.map +1 -1
- package/dist/esm/dwn-record-upgrade.js.map +1 -1
- package/dist/esm/{web5-user-agent.js → enbox-user-agent.js} +12 -7
- package/dist/esm/enbox-user-agent.js.map +1 -0
- package/dist/esm/identity-api.js +3 -3
- package/dist/esm/identity-api.js.map +1 -1
- package/dist/esm/index.js +3 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/local-dwn.js +150 -26
- package/dist/esm/local-dwn.js.map +1 -1
- package/dist/esm/local-key-manager.js +2 -2
- package/dist/esm/local-key-manager.js.map +1 -1
- package/dist/esm/oidc.js +11 -11
- package/dist/esm/oidc.js.map +1 -1
- package/dist/esm/permissions-api.js.map +1 -1
- package/dist/esm/store-data.js.map +1 -1
- package/dist/esm/sync-api.js +2 -2
- package/dist/esm/sync-api.js.map +1 -1
- package/dist/esm/sync-engine-level.js +2 -2
- package/dist/esm/sync-engine-level.js.map +1 -1
- package/dist/esm/test-harness.js +3 -3
- package/dist/esm/test-harness.js.map +1 -1
- package/dist/esm/utils-internal.js +2 -2
- package/dist/types/agent-did-resolver-cache.d.ts +7 -7
- package/dist/types/agent-did-resolver-cache.d.ts.map +1 -1
- package/dist/types/anonymous-dwn-api.d.ts +3 -3
- package/dist/types/anonymous-dwn-api.d.ts.map +1 -1
- package/dist/types/bearer-identity.d.ts +1 -1
- package/dist/types/connect.d.ts +8 -8
- package/dist/types/connect.d.ts.map +1 -1
- package/dist/types/did-api.d.ts +12 -11
- package/dist/types/did-api.d.ts.map +1 -1
- package/dist/types/dwn-api.d.ts +27 -11
- package/dist/types/dwn-api.d.ts.map +1 -1
- package/dist/types/dwn-discovery-file.d.ts +122 -0
- package/dist/types/dwn-discovery-file.d.ts.map +1 -0
- package/dist/types/dwn-discovery-payload.d.ts +105 -0
- package/dist/types/dwn-discovery-payload.d.ts.map +1 -0
- package/dist/types/dwn-encryption.d.ts +8 -8
- package/dist/types/dwn-encryption.d.ts.map +1 -1
- package/dist/types/dwn-key-delivery.d.ts +5 -5
- package/dist/types/dwn-key-delivery.d.ts.map +1 -1
- package/dist/types/dwn-record-upgrade.d.ts +2 -2
- package/dist/types/dwn-record-upgrade.d.ts.map +1 -1
- package/dist/types/{web5-user-agent.d.ts → enbox-user-agent.d.ts} +17 -13
- package/dist/types/enbox-user-agent.d.ts.map +1 -0
- package/dist/types/identity-api.d.ts +10 -10
- package/dist/types/identity-api.d.ts.map +1 -1
- package/dist/types/index.d.ts +3 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/local-dwn.d.ts +93 -15
- package/dist/types/local-dwn.d.ts.map +1 -1
- package/dist/types/local-key-manager.d.ts +9 -9
- package/dist/types/local-key-manager.d.ts.map +1 -1
- package/dist/types/oidc.d.ts +23 -19
- package/dist/types/oidc.d.ts.map +1 -1
- package/dist/types/permissions-api.d.ts +4 -4
- package/dist/types/permissions-api.d.ts.map +1 -1
- package/dist/types/store-data.d.ts +3 -3
- package/dist/types/store-data.d.ts.map +1 -1
- package/dist/types/store-did.d.ts +2 -2
- package/dist/types/store-did.d.ts.map +1 -1
- package/dist/types/store-identity.d.ts +2 -2
- package/dist/types/store-identity.d.ts.map +1 -1
- package/dist/types/store-key.d.ts +2 -2
- package/dist/types/store-key.d.ts.map +1 -1
- package/dist/types/sync-api.d.ts +9 -9
- package/dist/types/sync-api.d.ts.map +1 -1
- package/dist/types/sync-engine-level.d.ts +9 -9
- package/dist/types/sync-engine-level.d.ts.map +1 -1
- package/dist/types/sync-messages.d.ts +5 -5
- package/dist/types/sync-messages.d.ts.map +1 -1
- package/dist/types/test-harness.d.ts +4 -4
- package/dist/types/test-harness.d.ts.map +1 -1
- package/dist/types/types/agent.d.ts +24 -19
- package/dist/types/types/agent.d.ts.map +1 -1
- package/dist/types/types/identity.d.ts +1 -1
- package/dist/types/types/key-manager.d.ts +2 -2
- package/dist/types/types/key-manager.d.ts.map +1 -1
- package/dist/types/types/sync.d.ts +2 -2
- package/dist/types/types/sync.d.ts.map +1 -1
- package/dist/types/utils-internal.d.ts +4 -4
- package/dist/types/utils-internal.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/agent-did-resolver-cache.ts +8 -8
- package/src/anonymous-dwn-api.ts +4 -4
- package/src/bearer-identity.ts +1 -1
- package/src/connect.ts +12 -12
- package/src/did-api.ts +13 -11
- package/src/dwn-api.ts +61 -16
- package/src/dwn-discovery-file.ts +305 -0
- package/src/dwn-discovery-payload.ts +308 -0
- package/src/dwn-encryption.ts +8 -8
- package/src/dwn-key-delivery.ts +5 -5
- package/src/dwn-record-upgrade.ts +2 -2
- package/src/{web5-user-agent.ts → enbox-user-agent.ts} +26 -16
- package/src/identity-api.ts +11 -11
- package/src/index.ts +3 -1
- package/src/local-dwn.ts +154 -28
- package/src/local-key-manager.ts +10 -10
- package/src/oidc.ts +40 -30
- package/src/permissions-api.ts +5 -5
- package/src/store-data.ts +7 -7
- package/src/store-did.ts +2 -2
- package/src/store-identity.ts +2 -2
- package/src/store-key.ts +2 -2
- package/src/sync-api.ts +10 -10
- package/src/sync-engine-level.ts +12 -12
- package/src/sync-messages.ts +5 -5
- package/src/test-harness.ts +9 -9
- package/src/types/agent.ts +31 -20
- package/src/types/identity.ts +1 -1
- package/src/types/key-manager.ts +2 -2
- package/src/types/sync.ts +2 -2
- package/src/utils-internal.ts +4 -4
- package/dist/esm/web5-user-agent.js.map +0 -1
- package/dist/types/web5-user-agent.d.ts.map +0 -1
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?:
|
|
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 `
|
|
116
|
-
* the `AgentDwnApi`. This agent is used to interact with other
|
|
117
|
-
* to ensure this instance is set to correctly contextualize operations within the broader
|
|
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?:
|
|
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(
|
|
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 `
|
|
190
|
+
* Retrieves the `EnboxPlatformAgent` execution context.
|
|
186
191
|
*
|
|
187
|
-
* @returns The `
|
|
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():
|
|
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:
|
|
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(
|
|
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
|
|
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
|
|
289
|
+
/** Lazily retrieves the local DWN server endpoint via discovery. */
|
|
263
290
|
private async getLocalDwnEndpoint(): Promise<string | undefined> {
|
|
264
|
-
this._localDwnDiscovery ??= new LocalDwnDiscovery(
|
|
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
|
-
* `
|
|
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
|
+
|