@enbox/api 0.6.24 → 0.6.25
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 +19 -11
- package/dist/browser.mjs.map +4 -4
- package/dist/esm/enbox-types.js +11 -0
- package/dist/esm/enbox-types.js.map +1 -0
- package/dist/esm/enbox.js +300 -40
- package/dist/esm/enbox.js.map +1 -1
- package/dist/esm/index.js +7 -5
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/read-only-record.js.map +1 -1
- package/dist/esm/repository.js +29 -3
- package/dist/esm/repository.js.map +1 -1
- package/dist/types/enbox-types.d.ts +94 -0
- package/dist/types/enbox-types.d.ts.map +1 -0
- package/dist/types/enbox.d.ts +141 -74
- package/dist/types/enbox.d.ts.map +1 -1
- package/dist/types/index.d.ts +8 -5
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/repository.d.ts +19 -0
- package/dist/types/repository.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/enbox-types.ts +111 -0
- package/src/enbox.ts +340 -83
- package/src/index.ts +8 -5
- package/src/read-only-record.ts +1 -1
- package/src/repository.ts +30 -4
package/src/enbox.ts
CHANGED
|
@@ -4,15 +4,24 @@
|
|
|
4
4
|
*/
|
|
5
5
|
/// <reference types="@enbox/dwn-sdk-js" />
|
|
6
6
|
|
|
7
|
-
import type {
|
|
8
|
-
import type { DidMethodResolver } from '@enbox/dids';
|
|
9
|
-
import type { EnboxAgent } from '@enbox/agent';
|
|
7
|
+
import type { EnboxPlatformAgent } from '@enbox/agent';
|
|
10
8
|
import type { ProtocolDefinition } from '@enbox/dwn-sdk-js';
|
|
9
|
+
import type { AuthManagerOptions, ConnectOptions, HandlerConnectOptions, LocalConnectOptions } from '@enbox/auth';
|
|
11
10
|
|
|
11
|
+
import type {
|
|
12
|
+
EnboxAnonymousApi,
|
|
13
|
+
EnboxAnonymousOptions,
|
|
14
|
+
EnboxConnectOptions,
|
|
15
|
+
EnboxConnectResult,
|
|
16
|
+
EnboxParams,
|
|
17
|
+
EnboxSessionParams,
|
|
18
|
+
} from './enbox-types.js';
|
|
12
19
|
import type { SchemaMap, TypedProtocol } from './protocol-types.js';
|
|
13
20
|
|
|
14
21
|
import { AnonymousDwnApi } from '@enbox/agent';
|
|
22
|
+
import { AuthManager } from '@enbox/auth/auth-manager';
|
|
15
23
|
import { EnboxRpcClient } from '@enbox/dwn-clients';
|
|
24
|
+
import { omitUndefined } from '@enbox/common';
|
|
16
25
|
import { DidDht, DidJwk, DidKey, DidResolverCacheMemory, DidWeb, UniversalResolver } from '@enbox/dids';
|
|
17
26
|
|
|
18
27
|
import { DidApi } from './did-api.js';
|
|
@@ -22,74 +31,51 @@ import { TypedEnbox } from './typed-enbox.js';
|
|
|
22
31
|
import { VcApi } from './vc-api.js';
|
|
23
32
|
|
|
24
33
|
/**
|
|
25
|
-
*
|
|
34
|
+
* Module-level registry of in-flight {@link Enbox.connect} calls, keyed by
|
|
35
|
+
* the resolved data path (or a sentinel for "default path").
|
|
26
36
|
*
|
|
27
|
-
*
|
|
37
|
+
* `AuthManager.create()` opens LevelDB handles at the agent's `dataPath`;
|
|
38
|
+
* LevelDB enforces an exclusive lock per directory, so two parallel
|
|
39
|
+
* `Enbox.connect()` invocations on the same path would race on the lock
|
|
40
|
+
* and surface a cryptic `LEVEL_LOCKED` error to the caller. The registry
|
|
41
|
+
* detects the race at the API boundary and throws a clear, domain-level
|
|
42
|
+
* error instead. Custom `storage` adapters that don't share a path with
|
|
43
|
+
* the agent's vault can still race below this guard — but for the default
|
|
44
|
+
* path that every dapp uses, this catches the common case.
|
|
28
45
|
*/
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
didResolvers?: DidMethodResolver[];
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* The result of calling {@link Enbox.anonymous}.
|
|
36
|
-
*
|
|
37
|
-
* Contains only a read-only `dwn` property — no `did`, `vc`, or `agent`.
|
|
38
|
-
*
|
|
39
|
-
* @beta
|
|
40
|
-
*/
|
|
41
|
-
export type EnboxAnonymousApi = {
|
|
42
|
-
/** A read-only DWN API for querying public data on remote DWNs. */
|
|
43
|
-
dwn: DwnReaderApi;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Parameters for constructing an {@link Enbox} instance.
|
|
48
|
-
*
|
|
49
|
-
* These are the minimal primitives needed to interact with the DWN network.
|
|
50
|
-
* Typically obtained from an {@link AuthSession} via `@enbox/auth`.
|
|
51
|
-
*/
|
|
52
|
-
export type EnboxParams = {
|
|
53
|
-
/**
|
|
54
|
-
* A {@link EnboxAgent} instance that handles DIDs, DWNs and VCs requests. The agent manages the
|
|
55
|
-
* user keys and identities, and is responsible to sign and verify messages.
|
|
56
|
-
*/
|
|
57
|
-
agent: EnboxAgent;
|
|
58
|
-
|
|
59
|
-
/** The DID of the tenant under which all DID, DWN, and VC requests are being performed. */
|
|
60
|
-
connectedDid: string;
|
|
61
|
-
|
|
62
|
-
/** The DID that will be signing messages using grants from the connectedDid. */
|
|
63
|
-
delegateDid?: string;
|
|
64
|
-
};
|
|
46
|
+
const DEFAULT_DATA_PATH_KEY = '\x00default\x00';
|
|
47
|
+
const inflightConnects = new Map<string, Promise<unknown>>();
|
|
65
48
|
|
|
66
49
|
/**
|
|
67
50
|
* The main Enbox API interface. It provides protocol-scoped access to
|
|
68
51
|
* Decentralized Web Nodes (DWNs), Decentralized Identifiers (DIDs),
|
|
69
52
|
* and Verifiable Credentials (VCs).
|
|
70
53
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
54
|
+
* For common app flows, use the asynchronous {@link Enbox.connect} helper.
|
|
55
|
+
* For custom auth/session flows, use {@link Enbox.fromSession} with an
|
|
56
|
+
* existing session, or the public constructor with raw `{ agent, connectedDid }`.
|
|
74
57
|
*
|
|
75
58
|
* @example
|
|
76
59
|
* ```ts
|
|
77
|
-
* import { AuthManager } from '@enbox/auth';
|
|
78
60
|
* import { Enbox } from '@enbox/api';
|
|
79
61
|
*
|
|
80
|
-
* const
|
|
81
|
-
*
|
|
62
|
+
* const { enbox } = await Enbox.connect({
|
|
63
|
+
* createIdentity: true,
|
|
64
|
+
* sync: '15s',
|
|
65
|
+
* });
|
|
82
66
|
*
|
|
83
|
-
* const enbox = Enbox.connect({ session });
|
|
84
67
|
* const social = enbox.using(SocialProtocol);
|
|
85
68
|
* ```
|
|
86
69
|
*/
|
|
87
70
|
export class Enbox {
|
|
88
71
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
72
|
+
* The {@link EnboxPlatformAgent} this instance is bound to. The platform
|
|
73
|
+
* agent handles DIDs, DWN access, signing keys, and DWN sync — every
|
|
74
|
+
* Enbox session needs all of those, so the type is narrower than the
|
|
75
|
+
* minimal {@link EnboxAgent} interface and the constructor refuses
|
|
76
|
+
* non-platform agents at compile time.
|
|
91
77
|
*/
|
|
92
|
-
public agent:
|
|
78
|
+
public agent: EnboxPlatformAgent;
|
|
93
79
|
|
|
94
80
|
/** Exposed instance to the DID APIs, allow users to create and resolve DIDs. */
|
|
95
81
|
public did: DidApi;
|
|
@@ -109,6 +95,22 @@ export class Enbox {
|
|
|
109
95
|
/** Exposed instance to the VC APIs, allow users to issue, present and verify VCs. */
|
|
110
96
|
public vc: VcApi;
|
|
111
97
|
|
|
98
|
+
/**
|
|
99
|
+
* The `AuthManager` this instance owns and is responsible for tearing down
|
|
100
|
+
* during {@link Enbox.disconnect}. Set only by the async
|
|
101
|
+
* {@link Enbox.connect} factory; never populated by the public constructor
|
|
102
|
+
* or {@link Enbox.fromSession}.
|
|
103
|
+
*/
|
|
104
|
+
private _ownedAuth?: AuthManager;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Memoized teardown promise. Two parallel `enbox.disconnect()` calls
|
|
108
|
+
* share the same promise so `agent.sync.stopSync()` (and the optional
|
|
109
|
+
* `auth.shutdown()`) run exactly once even when callers fire the
|
|
110
|
+
* teardown from independent code paths.
|
|
111
|
+
*/
|
|
112
|
+
private _disconnecting?: Promise<void>;
|
|
113
|
+
|
|
112
114
|
constructor({ agent, connectedDid, delegateDid }: EnboxParams) {
|
|
113
115
|
this.agent = agent;
|
|
114
116
|
this.did = new DidApi({ agent, connectedDid });
|
|
@@ -162,30 +164,105 @@ export class Enbox {
|
|
|
162
164
|
}
|
|
163
165
|
|
|
164
166
|
/**
|
|
165
|
-
*
|
|
167
|
+
* Signs the user out and releases resources held by this Enbox instance.
|
|
168
|
+
*
|
|
169
|
+
* When this instance owns the underlying `AuthManager` (i.e. it was
|
|
170
|
+
* created via `Enbox.connect()` with no caller-supplied `agent`), the
|
|
171
|
+
* disconnect proceeds in three phases:
|
|
166
172
|
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
* `disconnect()
|
|
173
|
+
* 1. **Stop sync** — `agent.sync.stopSync(timeout)` halts the DWN
|
|
174
|
+
* sync engine. Always runs, regardless of ownership.
|
|
175
|
+
* 2. **Sign out** — `AuthManager.disconnect()` sends delegate-grant
|
|
176
|
+
* revocations to the connected DWN endpoints, clears session
|
|
177
|
+
* restore markers (PREVIOUSLY_CONNECTED, ACTIVE_IDENTITY,
|
|
178
|
+
* delegate keys, etc.) from the StorageAdapter, and clears the
|
|
179
|
+
* in-memory delegate decryption key cache. **Without this step
|
|
180
|
+
* a later `Enbox.connect()` would silently restore the
|
|
181
|
+
* supposedly signed-out session from the persisted markers.**
|
|
182
|
+
* 3. **Release resources** — `AuthManager.shutdown()` locks the
|
|
183
|
+
* vault, closes the sync engine, and closes the StorageAdapter.
|
|
184
|
+
*
|
|
185
|
+
* When the instance was created from a caller-owned session
|
|
186
|
+
* ({@link Enbox.fromSession}), a raw agent (the public constructor),
|
|
187
|
+
* or `Enbox.connect({ agent })`, only step 1 (stop sync) runs — the
|
|
188
|
+
* caller's AuthManager / agent keep their lifecycle. The caller is
|
|
189
|
+
* responsible for calling `auth.disconnect()` and `auth.shutdown()`
|
|
190
|
+
* on their own handle when they're done.
|
|
191
|
+
*
|
|
192
|
+
* Idempotent: parallel calls share the same teardown promise. After
|
|
193
|
+
* calling `disconnect()`, the `Enbox` instance should not be reused.
|
|
170
194
|
*
|
|
171
195
|
* @param timeout - Maximum milliseconds to wait for an in-progress sync
|
|
172
196
|
* cycle to finish before force-stopping. Defaults to `2000`.
|
|
173
197
|
*
|
|
174
198
|
* @example
|
|
175
199
|
* ```ts
|
|
176
|
-
*
|
|
200
|
+
* // High-level flow: a single disconnect() does sign-out + teardown.
|
|
201
|
+
* const { enbox } = await Enbox.connect({ createIdentity: true });
|
|
202
|
+
* // ... user uses the app ...
|
|
203
|
+
* await enbox.disconnect(); // revoke grants, clear markers, close vault
|
|
204
|
+
*
|
|
205
|
+
* // Caller-owned auth: enbox.disconnect() only stops Enbox-side state.
|
|
206
|
+
* const auth = await AuthManager.create({...});
|
|
207
|
+
* const session = await auth.connect();
|
|
208
|
+
* const enbox = Enbox.fromSession(session);
|
|
209
|
+
* await enbox.disconnect(); // stops sync + clears typed-enbox cache
|
|
210
|
+
* await auth.disconnect(); // sign-out: caller's responsibility
|
|
211
|
+
* await auth.shutdown(); // close resources: caller's responsibility
|
|
177
212
|
* ```
|
|
178
213
|
*
|
|
179
214
|
* @beta
|
|
180
215
|
*/
|
|
181
216
|
public async disconnect(timeout?: number): Promise<void> {
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
217
|
+
// Memoize so parallel calls share the same teardown promise. Without
|
|
218
|
+
// this, two concurrent disconnect()s would each invoke
|
|
219
|
+
// agent.sync.stopSync (idempotent, but redundant) and could race on
|
|
220
|
+
// _ownedAuth ownership transfer.
|
|
221
|
+
if (this._disconnecting !== undefined) {
|
|
222
|
+
return this._disconnecting;
|
|
185
223
|
}
|
|
224
|
+
this._disconnecting = this._doDisconnect(timeout);
|
|
225
|
+
return this._disconnecting;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private async _doDisconnect(timeout?: number): Promise<void> {
|
|
229
|
+
await this.agent.sync.stopSync(timeout);
|
|
186
230
|
|
|
187
231
|
// Clear cached TypedEnbox instances so they are not accidentally reused.
|
|
188
232
|
this._typedInstances.clear();
|
|
233
|
+
|
|
234
|
+
// If this Enbox owns the AuthManager (created via Enbox.connect), the
|
|
235
|
+
// sign-out + resource-teardown both fall on us:
|
|
236
|
+
//
|
|
237
|
+
// 1. `auth.disconnect()` — sign-out semantics: revoke delegate
|
|
238
|
+
// grants on the remote DWN, clear session restore markers
|
|
239
|
+
// (PREVIOUSLY_CONNECTED, ACTIVE_IDENTITY, delegate keys, etc.)
|
|
240
|
+
// from the StorageAdapter, and clear the in-memory delegate
|
|
241
|
+
// decryption key cache. MUST run while the agent's resources
|
|
242
|
+
// are still open (revocations need DWN access). Without this,
|
|
243
|
+
// a later `Enbox.connect()` would restore the supposedly
|
|
244
|
+
// signed-out session from the persisted markers.
|
|
245
|
+
// 2. `auth.shutdown()` — resource teardown: lock the vault,
|
|
246
|
+
// close the sync engine, close the storage adapter. Runs
|
|
247
|
+
// after disconnect so revocation traffic completes first.
|
|
248
|
+
//
|
|
249
|
+
// Both are idempotent and best-effort; failures are logged but do
|
|
250
|
+
// not propagate. We always attempt shutdown() even if disconnect()
|
|
251
|
+
// throws, so resources still close.
|
|
252
|
+
if (this._ownedAuth !== undefined) {
|
|
253
|
+
const owned = this._ownedAuth;
|
|
254
|
+
this._ownedAuth = undefined;
|
|
255
|
+
try {
|
|
256
|
+
await owned.disconnect({ timeout });
|
|
257
|
+
} catch (error: unknown) {
|
|
258
|
+
console.warn('[@enbox/api] Enbox.disconnect: AuthManager.disconnect() failed', error);
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
await owned.shutdown({ timeout });
|
|
262
|
+
} catch (error: unknown) {
|
|
263
|
+
console.warn('[@enbox/api] Enbox.disconnect: AuthManager.shutdown() failed', error);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
189
266
|
}
|
|
190
267
|
|
|
191
268
|
/**
|
|
@@ -229,37 +306,217 @@ export class Enbox {
|
|
|
229
306
|
}
|
|
230
307
|
|
|
231
308
|
/**
|
|
232
|
-
* Creates an {@link Enbox} instance from
|
|
309
|
+
* Creates an {@link Enbox} instance from a session-shaped object.
|
|
233
310
|
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
311
|
+
* Accepts `AuthSession`, `AgentSession`, or any compatible custom session
|
|
312
|
+
* with `{ agent, did, delegateDid? }`. This is the right entry point
|
|
313
|
+
* whenever you already hold an active session — including ones produced by
|
|
314
|
+
* a caller-managed `AuthManager`.
|
|
237
315
|
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
|
|
316
|
+
* For raw `{ agent, connectedDid }` access (no session shape), use the
|
|
317
|
+
* public constructor directly: `new Enbox({ agent, connectedDid })`.
|
|
318
|
+
*/
|
|
319
|
+
public static fromSession(session: EnboxSessionParams): Enbox {
|
|
320
|
+
return new Enbox({
|
|
321
|
+
agent : session.agent,
|
|
322
|
+
connectedDid : session.did,
|
|
323
|
+
delegateDid : session.delegateDid,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* High-level entry point that creates an {@link AuthManager}, runs
|
|
329
|
+
* `auth.connect()`, and returns the resulting `{ auth, session, enbox }`.
|
|
330
|
+
*
|
|
331
|
+
* For callers that already own an agent and DID, use the dedicated
|
|
332
|
+
* synchronous entry points instead:
|
|
333
|
+
* - `new Enbox({ agent, connectedDid })` for raw parameters
|
|
334
|
+
* - {@link Enbox.fromSession} for session-shaped objects
|
|
335
|
+
*
|
|
336
|
+
* Routing happens at runtime inside `AuthManager._isLocalConnect`:
|
|
337
|
+
* presence of a non-empty `protocols` array or a `connectHandler` selects
|
|
338
|
+
* the handler flow; everything else routes to local. Local-style fields
|
|
339
|
+
* (`password`, `dwnEndpoints`, etc.) are forwarded to both the manager
|
|
340
|
+
* (as defaults) and the per-call payload, so behavior is consistent with
|
|
341
|
+
* restored sessions.
|
|
342
|
+
*
|
|
343
|
+
* If you need exact control of the `AuthManager.connect()` payload,
|
|
344
|
+
* drop down one layer: create the `AuthManager` yourself with
|
|
345
|
+
* `AuthManager.create()`, call `auth.connect(...)` with your exact
|
|
346
|
+
* options, and pass the resulting session to `Enbox.fromSession`.
|
|
241
347
|
*
|
|
242
348
|
* @example
|
|
243
349
|
* ```ts
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
* const enbox = Enbox.connect({ session });
|
|
249
|
-
*
|
|
250
|
-
* // Using raw parameters
|
|
251
|
-
* const enbox = Enbox.connect({ agent, connectedDid: did });
|
|
350
|
+
* const { enbox, session, auth } = await Enbox.connect({ createIdentity: true });
|
|
351
|
+
* // ...
|
|
352
|
+
* await enbox.disconnect();
|
|
353
|
+
* await auth.shutdown(); // release vault + storage handles
|
|
252
354
|
* ```
|
|
253
355
|
*/
|
|
254
|
-
public static connect(
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
356
|
+
public static async connect(options: EnboxConnectOptions = {}): Promise<EnboxConnectResult> {
|
|
357
|
+
// Cross-instance concurrency guard. `AuthManager.create()` opens the
|
|
358
|
+
// agent's LevelDB at `options.dataPath` (or the platform default), and
|
|
359
|
+
// LevelDB enforces an exclusive `LOCK` file per directory — two
|
|
360
|
+
// parallel calls on the same path would otherwise race and surface
|
|
361
|
+
// `LEVEL_LOCKED` as a low-level error. The guard / ownership flag
|
|
362
|
+
// tracks one question only: did we build the underlying agent? If
|
|
363
|
+
// `options.agent` is supplied the caller owns the agent's lifecycle
|
|
364
|
+
// and on-disk handles, so we skip both the guard (no new dataPath
|
|
365
|
+
// race) and ownership cleanup (`disconnect()` must not tear down
|
|
366
|
+
// the caller's agent). A custom `storage` adapter alone is NOT a
|
|
367
|
+
// basis for skipping — Enbox still builds an agent that opens
|
|
368
|
+
// LevelDB handles at `dataPath`, and the storage adapter governs
|
|
369
|
+
// session-persistence keys, not the agent vault.
|
|
370
|
+
//
|
|
371
|
+
// Limitation: the key is the raw `options.dataPath` string, not its
|
|
372
|
+
// resolved on-disk location. Two callers — one with `dataPath`
|
|
373
|
+
// omitted and another passing the explicit platform default
|
|
374
|
+
// ('DATA/AGENT' in browser, '~/.enbox' in CLI) — refer to the same
|
|
375
|
+
// directory but produce different keys, so the guard won't catch
|
|
376
|
+
// that race. Resolving the path here would require pulling in the
|
|
377
|
+
// platform-default logic from `@enbox/agent`, which we deliberately
|
|
378
|
+
// avoid to keep the helper layered above the agent. Pick one
|
|
379
|
+
// convention per app: always omit `dataPath`, or always set it
|
|
380
|
+
// explicitly.
|
|
381
|
+
const shouldGuard = options.agent === undefined;
|
|
382
|
+
const key = options.dataPath ?? DEFAULT_DATA_PATH_KEY;
|
|
383
|
+
|
|
384
|
+
if (shouldGuard && inflightConnects.has(key)) {
|
|
385
|
+
throw new Error(
|
|
386
|
+
`[@enbox/api] Enbox.connect() is already in progress for dataPath '${
|
|
387
|
+
options.dataPath ?? '<default>'
|
|
388
|
+
}'. Await the in-flight call before starting another, or pass a custom dataPath.`
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const run = async (): Promise<EnboxConnectResult> => {
|
|
393
|
+
const auth = await AuthManager.create(Enbox.toAuthManagerOptions(options));
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
const session = await auth.connect(Enbox.toAuthConnectOptions(options));
|
|
397
|
+
const enbox = Enbox.fromSession(session);
|
|
398
|
+
// Take AuthManager ownership ONLY when we built the agent
|
|
399
|
+
// ourselves — `auth.shutdown()` will lock that agent's vault
|
|
400
|
+
// and close its sync engine. If the caller supplied
|
|
401
|
+
// `options.agent`, they retain agent ownership and must not
|
|
402
|
+
// have its lifecycle resources torn down by
|
|
403
|
+
// `enbox.disconnect()`. Callers that want explicit control
|
|
404
|
+
// (e.g. their own storage adapter without their own agent)
|
|
405
|
+
// can still grab the `auth` handle from the result and call
|
|
406
|
+
// `auth.shutdown()` themselves; we always include it in the
|
|
407
|
+
// returned `EnboxConnectResult`.
|
|
408
|
+
if (shouldGuard) {
|
|
409
|
+
enbox._ownedAuth = auth;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return { auth, enbox, session };
|
|
413
|
+
} catch (error: unknown) {
|
|
414
|
+
try {
|
|
415
|
+
await auth.shutdown();
|
|
416
|
+
} catch (shutdownError: unknown) {
|
|
417
|
+
// Surface the recovery failure for diagnosis but preserve the
|
|
418
|
+
// original connect rejection on the rethrow path below.
|
|
419
|
+
console.warn(
|
|
420
|
+
'[@enbox/api] Enbox.connect: auth.shutdown() failed during error recovery',
|
|
421
|
+
shutdownError,
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
throw error;
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
if (!shouldGuard) {
|
|
429
|
+
return run();
|
|
262
430
|
}
|
|
263
|
-
|
|
431
|
+
|
|
432
|
+
const promise = run();
|
|
433
|
+
inflightConnects.set(key, promise);
|
|
434
|
+
try {
|
|
435
|
+
return await promise;
|
|
436
|
+
} finally {
|
|
437
|
+
inflightConnects.delete(key);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Split `EnboxConnectOptions` into the manager-wide defaults that
|
|
443
|
+
* `AuthManager.create()` consumes.
|
|
444
|
+
*
|
|
445
|
+
* Implemented as an **explicit allowlist** for the same reason as
|
|
446
|
+
* `toAuthConnectOptions`: a denylist would silently leak future
|
|
447
|
+
* `HandlerConnectOptions` / `LocalConnectOptions` fields into the
|
|
448
|
+
* manager-default record. The `satisfies Partial<AuthManagerOptions>`
|
|
449
|
+
* annotation makes the compiler verify every key in the allowlist
|
|
450
|
+
* exists on `AuthManagerOptions`, so a misspelled or stale field name
|
|
451
|
+
* is a type error.
|
|
452
|
+
*/
|
|
453
|
+
private static toAuthManagerOptions(options: EnboxConnectOptions): AuthManagerOptions {
|
|
454
|
+
// Explicit allowlist — every key must exist on `AuthManagerOptions`
|
|
455
|
+
// (verified by `satisfies` below).
|
|
456
|
+
const allowlisted = {
|
|
457
|
+
agent : options.agent,
|
|
458
|
+
agentVault : options.agentVault,
|
|
459
|
+
localDwnStrategy : options.localDwnStrategy,
|
|
460
|
+
dataPath : options.dataPath,
|
|
461
|
+
storage : options.storage,
|
|
462
|
+
password : options.password,
|
|
463
|
+
passwordProvider : options.passwordProvider,
|
|
464
|
+
sync : options.sync,
|
|
465
|
+
dwnEndpoints : options.dwnEndpoints,
|
|
466
|
+
registration : options.registration,
|
|
467
|
+
connectHandler : options.connectHandler,
|
|
468
|
+
} satisfies Partial<AuthManagerOptions>;
|
|
469
|
+
return omitUndefined(allowlisted);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Split `EnboxConnectOptions` into the per-call payload that
|
|
474
|
+
* `AuthManager.connect()` consumes.
|
|
475
|
+
*
|
|
476
|
+
* Implemented as an **explicit allowlist** of the fields declared on
|
|
477
|
+
* {@link HandlerConnectOptions} ∪ {@link LocalConnectOptions}. A
|
|
478
|
+
* denylist (strip manager-only fields, forward the rest) would
|
|
479
|
+
* silently leak any future `AuthManagerOptions` field into
|
|
480
|
+
* `AuthManager.connect()`; the allowlist makes the type boundary
|
|
481
|
+
* explicit and the leak impossible. The trade-off is symmetric:
|
|
482
|
+
* any new connect-options field requires an edit here. That's
|
|
483
|
+
* acceptable — it's the same edit the public `ConnectOptions` type
|
|
484
|
+
* already needs, just on one extra line.
|
|
485
|
+
*
|
|
486
|
+
* The `satisfies Partial<ConnectOptions>` annotation makes the
|
|
487
|
+
* compiler verify every key in the allowlist exists on `ConnectOptions`,
|
|
488
|
+
* so misspelling a field name is a type error rather than a silent
|
|
489
|
+
* drop. Routing between local and handler flow happens inside
|
|
490
|
+
* `AuthManager._isLocalConnect` based on the presence of `protocols`
|
|
491
|
+
* / `connectHandler`.
|
|
492
|
+
*
|
|
493
|
+
* `protocols: []` is normalized away — an empty array carries no
|
|
494
|
+
* permission intent and would otherwise produce a zero-grant
|
|
495
|
+
* "connected" handler session indistinguishable from a denied connect.
|
|
496
|
+
*/
|
|
497
|
+
private static toAuthConnectOptions(options: EnboxConnectOptions): ConnectOptions | undefined {
|
|
498
|
+
// Explicit allowlist — every key must exist on the
|
|
499
|
+
// `ConnectOptions` union (verified by `satisfies` below).
|
|
500
|
+
const allowlisted = {
|
|
501
|
+
// Shared across handler + local
|
|
502
|
+
password : options.password,
|
|
503
|
+
sync : options.sync,
|
|
504
|
+
dwnEndpoints : options.dwnEndpoints,
|
|
505
|
+
// Handler-only
|
|
506
|
+
protocols : options.protocols,
|
|
507
|
+
connectHandler : options.connectHandler,
|
|
508
|
+
// Local-only
|
|
509
|
+
recoveryPhrase : options.recoveryPhrase,
|
|
510
|
+
createIdentity : options.createIdentity,
|
|
511
|
+
metadata : options.metadata,
|
|
512
|
+
} satisfies Partial<HandlerConnectOptions & LocalConnectOptions>;
|
|
513
|
+
|
|
514
|
+
// Normalize `protocols: []` to undefined so omitUndefined strips it.
|
|
515
|
+
const normalized = (Array.isArray(allowlisted.protocols) && allowlisted.protocols.length === 0)
|
|
516
|
+
? { ...allowlisted, protocols: undefined }
|
|
517
|
+
: allowlisted;
|
|
518
|
+
|
|
519
|
+
const cleaned = omitUndefined(normalized);
|
|
520
|
+
return Object.keys(cleaned).length === 0 ? undefined : cleaned;
|
|
264
521
|
}
|
|
265
522
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,16 +4,18 @@
|
|
|
4
4
|
* The SDK provides protocol-scoped access to DWN records with compile-time
|
|
5
5
|
* type safety, DID management, and Verifiable Credential operations.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* Common authentication and identity setup is available through
|
|
8
|
+
* `Enbox.connect()`. Advanced session management can use `@enbox/auth` or
|
|
9
|
+
* `@enbox/agent` directly.
|
|
8
10
|
*
|
|
9
11
|
* @example
|
|
10
12
|
* ```ts
|
|
11
|
-
* import { AuthManager } from '@enbox/auth';
|
|
12
13
|
* import { Enbox } from '@enbox/api';
|
|
13
14
|
*
|
|
14
|
-
* const
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* const { enbox } = await Enbox.connect({
|
|
16
|
+
* createIdentity: true,
|
|
17
|
+
* sync: '15s',
|
|
18
|
+
* });
|
|
17
19
|
* ```
|
|
18
20
|
*
|
|
19
21
|
* [Link to GitHub Repo](https://github.com/enboxorg/enbox)
|
|
@@ -25,6 +27,7 @@ export * from './define-protocol.js';
|
|
|
25
27
|
export * from './did-api.js';
|
|
26
28
|
export * from './dwn-reader-api.js';
|
|
27
29
|
export * from './enbox.js';
|
|
30
|
+
export type * from './enbox-types.js';
|
|
28
31
|
export * from './grant-revocation.js';
|
|
29
32
|
export * from './live-query.js';
|
|
30
33
|
export * from './permission-grant.js';
|
package/src/read-only-record.ts
CHANGED
|
@@ -217,7 +217,7 @@ export class ReadOnlyRecord {
|
|
|
217
217
|
},
|
|
218
218
|
|
|
219
219
|
async json<T = unknown>(): Promise<T> {
|
|
220
|
-
return await Stream.consumeToJson({ readableStream: await this.stream() })
|
|
220
|
+
return await Stream.consumeToJson<T>({ readableStream: await this.stream() });
|
|
221
221
|
},
|
|
222
222
|
|
|
223
223
|
async text(): Promise<string> {
|
package/src/repository.ts
CHANGED
|
@@ -24,6 +24,25 @@
|
|
|
24
24
|
* const members = await social.group.member.query(groupContextId);
|
|
25
25
|
* ```
|
|
26
26
|
*
|
|
27
|
+
* ## Deliberate `as never` pattern
|
|
28
|
+
*
|
|
29
|
+
* The CRUD methods below pass their `options: Record<string, unknown>`
|
|
30
|
+
* parameter to `typed.records.{create, query, subscribe, update}` with
|
|
31
|
+
* `as never`. This is the type-system bridge between the type-erased
|
|
32
|
+
* Proxy (which doesn't carry the per-protocol generic) and the strictly-
|
|
33
|
+
* typed record methods (which take `TypedRecordCreateParams<...>` etc.).
|
|
34
|
+
*
|
|
35
|
+
* `never` is the universal bottom subtype, so any strict generic is
|
|
36
|
+
* satisfied. The structural runtime shape of `options` is validated by
|
|
37
|
+
* the DWN SDK's grant-processing layer downstream — the cast is a
|
|
38
|
+
* narrow, intentional escape hatch at the proxy boundary.
|
|
39
|
+
*
|
|
40
|
+
* SonarCloud's `typescript:S4325` flags these casts as redundant
|
|
41
|
+
* because its rule doesn't model generic-parameter constraints. We
|
|
42
|
+
* suppress S4325 at the file level (see `sonar-project.properties`)
|
|
43
|
+
* because every CRUD method uses the same pattern and per-line
|
|
44
|
+
* `// NOSONAR` would dwarf the actual code.
|
|
45
|
+
*
|
|
27
46
|
* @module
|
|
28
47
|
*/
|
|
29
48
|
|
|
@@ -51,10 +70,12 @@ function isRecordLimitExceeded(status: { code: number; detail: string }): boolea
|
|
|
51
70
|
* (has `$recordLimit: { max: 1 }`).
|
|
52
71
|
*/
|
|
53
72
|
function isSingletonPath(definition: ProtocolDefinition, path: string): boolean {
|
|
54
|
-
const
|
|
55
|
-
|
|
73
|
+
const [first, ...rest] = path.split('/');
|
|
74
|
+
// Top-level lookup uses the declared `{ [key: string]: ProtocolRuleSet }`
|
|
75
|
+
// index signature directly — no top-level cast needed.
|
|
76
|
+
let node: ProtocolRuleSet | undefined = definition.structure[first];
|
|
56
77
|
|
|
57
|
-
for (const seg of
|
|
78
|
+
for (const seg of rest) {
|
|
58
79
|
if (!node || typeof node !== 'object') { return false; }
|
|
59
80
|
node = (node as Record<string, ProtocolRuleSet>)[seg];
|
|
60
81
|
}
|
|
@@ -72,8 +93,13 @@ function isSingletonPath(definition: ProtocolDefinition, path: string): boolean
|
|
|
72
93
|
* reached by the given path.
|
|
73
94
|
*/
|
|
74
95
|
function getChildKeys(definition: ProtocolDefinition, path: string): string[] {
|
|
96
|
+
// Top-level: walk into the structure's declared index-signature value.
|
|
97
|
+
// The structure is `{ [key: string]: ProtocolRuleSet }`; navigating
|
|
98
|
+
// beneath the first segment uses `ProtocolRuleSet`'s own index
|
|
99
|
+
// signature, which contains both rule-set children and `$`-prefixed
|
|
100
|
+
// metadata — we filter out the metadata at the leaf below.
|
|
75
101
|
const segments = path.split('/');
|
|
76
|
-
let node: Record<string, unknown> = definition.structure
|
|
102
|
+
let node: Record<string, unknown> = definition.structure;
|
|
77
103
|
|
|
78
104
|
for (const seg of segments) {
|
|
79
105
|
if (!node || typeof node !== 'object') { return []; }
|