@enbox/api 0.6.24 → 0.6.26

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/src/enbox.ts CHANGED
@@ -4,15 +4,24 @@
4
4
  */
5
5
  /// <reference types="@enbox/dwn-sdk-js" />
6
6
 
7
- import type { AuthSession } from '@enbox/auth';
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, VaultConnectOptions } 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
- * Options for creating an anonymous (read-only) Enbox instance via {@link Enbox.anonymous}.
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
- * @beta
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
- export type EnboxAnonymousOptions = {
30
- /** Override the default DID method resolvers. Defaults to `[DidDht, DidJwk, DidKey, DidWeb]`. */
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
- * Authentication and identity management are handled externally by
72
- * `@enbox/auth`. Use {@link Enbox.connect} to create an instance from
73
- * an {@link AuthSession} or raw parameters.
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 auth = await AuthManager.create({ sync: '15s' });
81
- * const session = await auth.connect();
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
- * A {@link EnboxAgent} instance that handles DIDs, DWNs and VCs requests. The agent manages the
90
- * user keys and identities, and is responsible to sign and verify messages.
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: EnboxAgent;
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
- * Stops DWN sync and clears the cached {@link TypedEnbox} instances.
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
- * Call this when the application is shutting down or the user is
168
- * disconnecting to cleanly release background resources. After calling
169
- * `disconnect()`, the `Enbox` instance should not be reused.
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
- * await enbox.disconnect();
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
- // Stop any active sync.
183
- if ('sync' in this.agent && typeof (this.agent as any).sync?.stopSync === 'function') {
184
- await (this.agent as any).sync.stopSync(timeout);
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 an {@link AuthSession} or raw parameters.
309
+ * Creates an {@link Enbox} instance from a session-shaped object.
233
310
  *
234
- * This is a thin factory all authentication, identity management, vault
235
- * initialization, DWN registration, and sync setup are handled externally
236
- * by `@enbox/auth` via {@link AuthSession}.
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
- * @param params - Either `{ session }` with an {@link AuthSession} from
239
- * `@enbox/auth`, or raw `{ agent, connectedDid, delegateDid? }` parameters.
240
- * @returns A new {@link Enbox} instance ready for use.
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._isVaultConnect`:
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
- * // Using an AuthSession from @enbox/auth
245
- * import { AuthManager } from '@enbox/auth';
246
- * const auth = await AuthManager.create({ sync: '15s' });
247
- * const session = await auth.connect();
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(params: { session: AuthSession } | EnboxParams): Enbox {
255
- if ('session' in params) {
256
- const { session } = params;
257
- return new Enbox({
258
- agent : session.agent,
259
- connectedDid : session.did,
260
- delegateDid : session.delegateDid,
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
- return new Enbox(params);
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` / `VaultConnectOptions` 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 VaultConnectOptions}. 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._isVaultConnect` 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 & VaultConnectOptions>;
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
- * Authentication and identity management are handled by `@enbox/auth`.
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 auth = await AuthManager.create({ sync: '15s' });
15
- * const session = await auth.connect();
16
- * const enbox = Enbox.connect({ session });
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';
@@ -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() }) as T;
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 segments = path.split('/');
55
- let node: ProtocolRuleSet | undefined = definition.structure as unknown as ProtocolRuleSet;
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 segments) {
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 as unknown as Record<string, unknown>;
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 []; }