@enbox/auth 0.4.0 → 0.6.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.
Files changed (86) hide show
  1. package/dist/esm/auth-manager.js +244 -121
  2. package/dist/esm/auth-manager.js.map +1 -1
  3. package/dist/esm/connect/import.js +131 -0
  4. package/dist/esm/connect/import.js.map +1 -0
  5. package/dist/esm/connect/lifecycle.js +235 -0
  6. package/dist/esm/connect/lifecycle.js.map +1 -0
  7. package/dist/esm/connect/local.js +91 -0
  8. package/dist/esm/connect/local.js.map +1 -0
  9. package/dist/esm/{flows/session-restore.js → connect/restore.js} +39 -50
  10. package/dist/esm/connect/restore.js.map +1 -0
  11. package/dist/esm/{flows/wallet-connect.js → connect/wallet.js} +33 -39
  12. package/dist/esm/connect/wallet.js.map +1 -0
  13. package/dist/esm/{flows/dwn-discovery.js → discovery.js} +98 -83
  14. package/dist/esm/discovery.js.map +1 -0
  15. package/dist/esm/index.js +7 -3
  16. package/dist/esm/index.js.map +1 -1
  17. package/dist/esm/password-provider.js +319 -0
  18. package/dist/esm/password-provider.js.map +1 -0
  19. package/dist/esm/{flows/dwn-registration.js → registration.js} +50 -4
  20. package/dist/esm/registration.js.map +1 -0
  21. package/dist/esm/types.js +11 -1
  22. package/dist/esm/types.js.map +1 -1
  23. package/dist/esm/wallet-connect-client.js +188 -0
  24. package/dist/esm/wallet-connect-client.js.map +1 -0
  25. package/dist/types/auth-manager.d.ts +86 -7
  26. package/dist/types/auth-manager.d.ts.map +1 -1
  27. package/dist/types/connect/import.d.ts +25 -0
  28. package/dist/types/connect/import.d.ts.map +1 -0
  29. package/dist/types/connect/lifecycle.d.ts +152 -0
  30. package/dist/types/connect/lifecycle.d.ts.map +1 -0
  31. package/dist/types/connect/local.d.ts +18 -0
  32. package/dist/types/connect/local.d.ts.map +1 -0
  33. package/dist/types/connect/restore.d.ts +18 -0
  34. package/dist/types/connect/restore.d.ts.map +1 -0
  35. package/dist/types/{flows/wallet-connect.d.ts → connect/wallet.d.ts} +7 -16
  36. package/dist/types/connect/wallet.d.ts.map +1 -0
  37. package/dist/types/{flows/dwn-discovery.d.ts → discovery.d.ts} +43 -56
  38. package/dist/types/discovery.d.ts.map +1 -0
  39. package/dist/types/index.d.ts +8 -4
  40. package/dist/types/index.d.ts.map +1 -1
  41. package/dist/types/password-provider.d.ts +194 -0
  42. package/dist/types/password-provider.d.ts.map +1 -0
  43. package/dist/types/{flows/dwn-registration.d.ts → registration.d.ts} +21 -2
  44. package/dist/types/registration.d.ts.map +1 -0
  45. package/dist/types/types.d.ts +92 -4
  46. package/dist/types/types.d.ts.map +1 -1
  47. package/dist/types/wallet-connect-client.d.ts +89 -0
  48. package/dist/types/wallet-connect-client.d.ts.map +1 -0
  49. package/package.json +15 -12
  50. package/src/auth-manager.ts +279 -145
  51. package/src/connect/import.ts +148 -0
  52. package/src/connect/lifecycle.ts +321 -0
  53. package/src/connect/local.ts +101 -0
  54. package/src/connect/restore.ts +117 -0
  55. package/src/{flows/wallet-connect.ts → connect/wallet.ts} +42 -58
  56. package/src/{flows/dwn-discovery.ts → discovery.ts} +103 -82
  57. package/src/index.ts +14 -4
  58. package/src/password-provider.ts +383 -0
  59. package/src/{flows/dwn-registration.ts → registration.ts} +61 -6
  60. package/src/types.ts +100 -4
  61. package/src/wallet-connect-client.ts +278 -0
  62. package/dist/esm/flows/dwn-discovery.js.map +0 -1
  63. package/dist/esm/flows/dwn-registration.js.map +0 -1
  64. package/dist/esm/flows/import-identity.js +0 -175
  65. package/dist/esm/flows/import-identity.js.map +0 -1
  66. package/dist/esm/flows/local-connect.js +0 -141
  67. package/dist/esm/flows/local-connect.js.map +0 -1
  68. package/dist/esm/flows/session-restore.js.map +0 -1
  69. package/dist/esm/flows/wallet-connect.js.map +0 -1
  70. package/dist/esm/vault/vault-manager.js +0 -95
  71. package/dist/esm/vault/vault-manager.js.map +0 -1
  72. package/dist/types/flows/dwn-discovery.d.ts.map +0 -1
  73. package/dist/types/flows/dwn-registration.d.ts.map +0 -1
  74. package/dist/types/flows/import-identity.d.ts +0 -35
  75. package/dist/types/flows/import-identity.d.ts.map +0 -1
  76. package/dist/types/flows/local-connect.d.ts +0 -29
  77. package/dist/types/flows/local-connect.d.ts.map +0 -1
  78. package/dist/types/flows/session-restore.d.ts +0 -27
  79. package/dist/types/flows/session-restore.d.ts.map +0 -1
  80. package/dist/types/flows/wallet-connect.d.ts.map +0 -1
  81. package/dist/types/vault/vault-manager.d.ts +0 -57
  82. package/dist/types/vault/vault-manager.d.ts.map +0 -1
  83. package/src/flows/import-identity.ts +0 -217
  84. package/src/flows/local-connect.ts +0 -171
  85. package/src/flows/session-restore.ts +0 -142
  86. package/src/vault/vault-manager.ts +0 -89
@@ -6,34 +6,41 @@
6
6
  * @module
7
7
  */
8
8
 
9
- import { EnboxUserAgent } from '@enbox/agent';
10
- import type { BearerIdentity, PortableIdentity } from '@enbox/agent';
9
+ import type { BearerIdentity, HdIdentityVault, PortableIdentity } from '@enbox/agent';
11
10
 
12
- import { AuthEventEmitter } from './events.js';
13
- import { AuthSession } from './identity-session.js';
14
- import { createDefaultStorage } from './storage/storage.js';
15
- import { localConnect } from './flows/local-connect.js';
16
- import { restoreSession } from './flows/session-restore.js';
17
- import { STORAGE_KEYS } from './types.js';
18
- import { VaultManager } from './vault/vault-manager.js';
19
- import { walletConnect } from './flows/wallet-connect.js';
11
+ import type { FlowContext } from './connect/lifecycle.js';
12
+ import type { PasswordProvider } from './password-provider.js';
20
13
  import type {
21
14
  AuthEvent,
22
15
  AuthEventHandler,
23
16
  AuthManagerOptions,
24
17
  AuthState,
25
18
  DisconnectOptions,
19
+ HeadlessConnectOptions,
26
20
  IdentityInfo,
27
21
  ImportFromPhraseOptions,
28
22
  ImportFromPortableOptions,
29
23
  LocalConnectOptions,
30
24
  RegistrationOptions,
31
25
  RestoreSessionOptions,
26
+ ShutdownOptions,
32
27
  StorageAdapter,
33
28
  SyncOption,
34
29
  WalletConnectOptions,
35
30
  } from './types.js';
36
- import { importFromPhrase, importFromPortable } from './flows/import-identity.js';
31
+
32
+ import { EnboxUserAgent } from '@enbox/agent';
33
+
34
+ import { AuthEventEmitter } from './events.js';
35
+ import { AuthSession } from './identity-session.js';
36
+ import { createDefaultStorage } from './storage/storage.js';
37
+ import { discoverLocalDwn } from './discovery.js';
38
+ import { localConnect } from './connect/local.js';
39
+ import { restoreSession } from './connect/restore.js';
40
+ import { STORAGE_KEYS } from './types.js';
41
+ import { walletConnect } from './connect/wallet.js';
42
+ import { ensureVaultReady, resolveIdentityDids, startSyncIfEnabled } from './connect/lifecycle.js';
43
+ import { importFromPhrase, importFromPortable } from './connect/import.js';
37
44
 
38
45
  /**
39
46
  * The primary entry point for authentication and identity management.
@@ -72,35 +79,46 @@ export class AuthManager {
72
79
  private _userAgent: EnboxUserAgent;
73
80
  private _emitter: AuthEventEmitter;
74
81
  private _storage: StorageAdapter;
75
- private _vault: VaultManager;
76
82
  private _session: AuthSession | undefined;
77
83
  private _state: AuthState = 'uninitialized';
78
84
  private _isConnecting = false;
85
+ private _isShutDown = false;
79
86
 
80
87
  // Default options from create()
81
88
  private _defaultPassword?: string;
89
+ private _passwordProvider?: PasswordProvider;
82
90
  private _defaultSync?: SyncOption;
83
91
  private _defaultDwnEndpoints?: string[];
84
92
  private _registration?: RegistrationOptions;
85
93
 
94
+ /**
95
+ * The local DWN server endpoint discovered during `create()`, if any.
96
+ * `undefined` means no local server was found. This is set before any
97
+ * event listeners are attached, so consumers should check this property
98
+ * after `create()` returns rather than relying solely on events.
99
+ */
100
+ private _localDwnEndpoint?: string;
101
+
86
102
  private constructor(params: {
87
103
  userAgent: EnboxUserAgent;
88
104
  emitter: AuthEventEmitter;
89
105
  storage: StorageAdapter;
90
- vault: VaultManager;
91
106
  defaultPassword?: string;
107
+ passwordProvider?: PasswordProvider;
92
108
  defaultSync?: SyncOption;
93
109
  defaultDwnEndpoints?: string[];
94
110
  registration?: RegistrationOptions;
111
+ localDwnEndpoint?: string;
95
112
  }) {
96
113
  this._userAgent = params.userAgent;
97
114
  this._emitter = params.emitter;
98
115
  this._storage = params.storage;
99
- this._vault = params.vault;
100
116
  this._defaultPassword = params.defaultPassword;
117
+ this._passwordProvider = params.passwordProvider;
101
118
  this._defaultSync = params.defaultSync;
102
119
  this._defaultDwnEndpoints = params.defaultDwnEndpoints;
103
120
  this._registration = params.registration;
121
+ this._localDwnEndpoint = params.localDwnEndpoint;
104
122
  }
105
123
 
106
124
  /**
@@ -117,29 +135,44 @@ export class AuthManager {
117
135
  const emitter = new AuthEventEmitter();
118
136
  const storage = options.storage ?? createDefaultStorage();
119
137
 
138
+ // Run local DWN discovery BEFORE creating the agent. Discovery has
139
+ // zero vault/DWN dependencies — it only checks the URL fragment,
140
+ // reads localStorage, and validates via GET /info.
141
+ //
142
+ // When a local DWN server is available, the agent is created in
143
+ // "remote mode": it skips creating an in-process DWN and routes all
144
+ // DWN operations through RPC to the local server.
145
+ let localDwnEndpoint: string | undefined;
146
+ if (!options.agent && options.localDwnStrategy !== 'off') {
147
+ localDwnEndpoint = await discoverLocalDwn(storage);
148
+ // NOTE: We intentionally do NOT emit 'local-dwn-available' here
149
+ // because event listeners aren't attached yet. Consumers should
150
+ // check `authManager.localDwnEndpoint` after create() returns.
151
+ }
152
+
120
153
  // Use a pre-built agent or create one with the given options.
121
154
  const userAgent = options.agent ?? await EnboxUserAgent.create({
122
155
  dataPath : options.dataPath,
123
156
  agentVault : options.agentVault,
124
157
  localDwnStrategy : options.localDwnStrategy,
158
+ localDwnEndpoint,
125
159
  });
126
160
 
127
- const vault = new VaultManager(userAgent.vault, emitter);
128
-
129
161
  const manager = new AuthManager({
130
162
  userAgent,
131
163
  emitter,
132
164
  storage,
133
- vault,
134
165
  defaultPassword : options.password,
166
+ passwordProvider : options.passwordProvider,
135
167
  defaultSync : options.sync,
136
168
  defaultDwnEndpoints : options.dwnEndpoints,
137
169
  registration : options.registration,
170
+ localDwnEndpoint,
138
171
  });
139
172
 
140
173
  // Determine initial state.
141
- if (await vault.isInitialized()) {
142
- manager._setState(vault.isLocked ? 'locked' : 'unlocked');
174
+ if (await userAgent.vault.isInitialized()) {
175
+ manager._setState(userAgent.vault.isLocked() ? 'locked' : 'unlocked');
143
176
  } else {
144
177
  manager._setState('uninitialized');
145
178
  }
@@ -160,33 +193,11 @@ export class AuthManager {
160
193
  * @throws If a connection attempt is already in progress.
161
194
  */
162
195
  async connect(options?: LocalConnectOptions): Promise<AuthSession> {
163
- this._guardConcurrency();
164
- this._isConnecting = true;
165
-
166
- try {
167
- const session = await localConnect(
168
- {
169
- userAgent : this._userAgent,
170
- emitter : this._emitter,
171
- storage : this._storage,
172
- defaultPassword : this._defaultPassword,
173
- defaultSync : this._defaultSync,
174
- defaultDwnEndpoints : this._defaultDwnEndpoints,
175
- registration : this._registration,
176
- },
177
- options,
178
- );
179
-
180
- this._session = session;
181
- this._setState('connected');
182
- return session;
183
- } finally {
184
- this._isConnecting = false;
185
- }
196
+ return this._withConnect(() => localConnect(this._flowContext(), options));
186
197
  }
187
198
 
188
199
  /**
189
- * Connect to an external wallet via the OIDC/QR relay protocol.
200
+ * Connect to an external wallet via the Enbox Connect relay protocol.
190
201
  *
191
202
  * This runs the full WalletConnect flow: generates a URI for QR display,
192
203
  * validates the PIN, imports the delegated DID, and processes grants.
@@ -197,36 +208,7 @@ export class AuthManager {
197
208
  * @throws If a connection attempt is already in progress.
198
209
  */
199
210
  async walletConnect(options: WalletConnectOptions): Promise<AuthSession> {
200
- this._guardConcurrency();
201
- this._isConnecting = true;
202
-
203
- try {
204
- // Ensure the agent is initialized and started before wallet connect.
205
- const password = this._defaultPassword ?? 'insecure-static-phrase';
206
- if (await this._userAgent.firstLaunch()) {
207
- await this._userAgent.initialize({ password });
208
- }
209
- await this._userAgent.start({ password });
210
- this._emitter.emit('vault-unlocked', {});
211
-
212
- const session = await walletConnect(
213
- {
214
- userAgent : this._userAgent,
215
- emitter : this._emitter,
216
- storage : this._storage,
217
- defaultSync : this._defaultSync,
218
- defaultDwnEndpoints : this._defaultDwnEndpoints,
219
- registration : this._registration,
220
- },
221
- options,
222
- );
223
-
224
- this._session = session;
225
- this._setState('connected');
226
- return session;
227
- } finally {
228
- this._isConnecting = false;
229
- }
211
+ return this._withConnect(() => walletConnect(this._flowContext(), options));
230
212
  }
231
213
 
232
214
  /**
@@ -236,28 +218,7 @@ export class AuthManager {
236
218
  * recovering the identity on this device.
237
219
  */
238
220
  async importFromPhrase(options: ImportFromPhraseOptions): Promise<AuthSession> {
239
- this._guardConcurrency();
240
- this._isConnecting = true;
241
-
242
- try {
243
- const session = await importFromPhrase(
244
- {
245
- userAgent : this._userAgent,
246
- emitter : this._emitter,
247
- storage : this._storage,
248
- defaultSync : this._defaultSync,
249
- defaultDwnEndpoints : this._defaultDwnEndpoints,
250
- registration : this._registration,
251
- },
252
- options,
253
- );
254
-
255
- this._session = session;
256
- this._setState('connected');
257
- return session;
258
- } finally {
259
- this._isConnecting = false;
260
- }
221
+ return this._withConnect(() => importFromPhrase(this._flowContext(), options));
261
222
  }
262
223
 
263
224
  /**
@@ -266,28 +227,7 @@ export class AuthManager {
266
227
  * The portable identity contains the DID's private keys and metadata.
267
228
  */
268
229
  async importFromPortable(options: ImportFromPortableOptions): Promise<AuthSession> {
269
- this._guardConcurrency();
270
- this._isConnecting = true;
271
-
272
- try {
273
- const session = await importFromPortable(
274
- {
275
- userAgent : this._userAgent,
276
- emitter : this._emitter,
277
- storage : this._storage,
278
- defaultSync : this._defaultSync,
279
- defaultDwnEndpoints : this._defaultDwnEndpoints,
280
- registration : this._registration,
281
- },
282
- options,
283
- );
284
-
285
- this._session = session;
286
- this._setState('connected');
287
- return session;
288
- } finally {
289
- this._isConnecting = false;
290
- }
230
+ return this._withConnect(() => importFromPortable(this._flowContext(), options));
291
231
  }
292
232
 
293
233
  /**
@@ -301,16 +241,7 @@ export class AuthManager {
301
241
  this._isConnecting = true;
302
242
 
303
243
  try {
304
- const session = await restoreSession(
305
- {
306
- userAgent : this._userAgent,
307
- emitter : this._emitter,
308
- storage : this._storage,
309
- defaultPassword : this._defaultPassword,
310
- defaultSync : this._defaultSync,
311
- },
312
- options,
313
- );
244
+ const session = await restoreSession(this._flowContext(), options);
314
245
 
315
246
  if (session) {
316
247
  this._session = session;
@@ -322,6 +253,89 @@ export class AuthManager {
322
253
  }
323
254
  }
324
255
 
256
+ /**
257
+ * Lightweight vault unlock for one-shot utilities and subprocesses.
258
+ *
259
+ * Unlocks the vault and retrieves the active (or first available)
260
+ * identity **without** starting sync, DWN registration, or persisting
261
+ * session markers. This is the recommended replacement for calling
262
+ * `agent.start({ password })` directly.
263
+ *
264
+ * Typical use cases:
265
+ * - Git credential helpers that need to sign a token and exit
266
+ * - CLI utilities that perform a single operation
267
+ * - Any subprocess that shares a data directory with a long-running daemon
268
+ *
269
+ * @param options - Optional password override.
270
+ * @returns An active AuthSession (with sync disabled).
271
+ *
272
+ * @example
273
+ * ```ts
274
+ * const session = await auth.connectHeadless({ password });
275
+ * const did = session.did; // ready to use
276
+ * await auth.shutdown(); // clean exit
277
+ * ```
278
+ */
279
+ async connectHeadless(options?: HeadlessConnectOptions): Promise<AuthSession> {
280
+ let password = options?.password ?? this._defaultPassword;
281
+ const isFirstLaunch = await this._userAgent.firstLaunch();
282
+
283
+ // Try the password provider if no explicit password.
284
+ if (!password && this._passwordProvider) {
285
+ password = await this._passwordProvider.getPassword({
286
+ reason: isFirstLaunch ? 'create' : 'unlock',
287
+ });
288
+ }
289
+
290
+ if (!password) {
291
+ throw new Error(
292
+ '[@enbox/auth] connectHeadless() requires a password. ' +
293
+ 'Provide one via options.password, a passwordProvider, or the AuthManager default.'
294
+ );
295
+ }
296
+
297
+ // Unlock the vault (initialise on first launch, always start).
298
+ await ensureVaultReady({
299
+ userAgent : this._userAgent,
300
+ emitter : this._emitter,
301
+ password,
302
+ isFirstLaunch,
303
+ });
304
+
305
+ // Find the active identity.
306
+ const identities = await this._userAgent.identity.list();
307
+ if (identities.length === 0) {
308
+ throw new Error('[@enbox/auth] No identities found in vault.');
309
+ }
310
+
311
+ // Prefer the previously-active identity, fall back to first.
312
+ const savedDid = await this._storage.get(STORAGE_KEYS.ACTIVE_IDENTITY);
313
+ const identity = (savedDid
314
+ ? identities.find(id => id.did.uri === savedDid || id.metadata.connectedDid === savedDid)
315
+ : undefined
316
+ ) ?? identities[0];
317
+
318
+ const { connectedDid, delegateDid } = resolveIdentityDids(identity);
319
+
320
+ const identityInfo: IdentityInfo = {
321
+ didUri : connectedDid,
322
+ name : identity.metadata.name,
323
+ connectedDid : identity.metadata.connectedDid,
324
+ };
325
+
326
+ // No sync, no registration, no session persistence markers.
327
+ this._session = new AuthSession({
328
+ agent : this._userAgent,
329
+ did : connectedDid,
330
+ delegateDid,
331
+ identity : identityInfo,
332
+ });
333
+
334
+ this._setState('connected');
335
+
336
+ return this._session;
337
+ }
338
+
325
339
  // ─── Session management ────────────────────────────────────────
326
340
 
327
341
  /** The current active session, or `undefined` if not connected. */
@@ -347,15 +361,14 @@ export class AuthManager {
347
361
  const did = this._session?.did;
348
362
 
349
363
  // 1. Stop sync.
350
- if ('sync' in this._userAgent && typeof (this._userAgent as any).sync?.stopSync === 'function') {
351
- await (this._userAgent as any).sync.stopSync(timeout);
352
- }
364
+ await this._userAgent.sync.stopSync(timeout);
353
365
 
354
366
  // 2. Clear the session (but keep storage markers for restore).
355
367
  this._session = undefined;
356
368
 
357
- // 3. Lock the vault (also emits 'vault-locked').
358
- await this._vault.lock();
369
+ // 3. Lock the vault.
370
+ await this._userAgent.vault.lock();
371
+ this._emitter.emit('vault-locked', {});
359
372
 
360
373
  // 4. Transition state.
361
374
  this._setState('locked');
@@ -380,9 +393,7 @@ export class AuthManager {
380
393
 
381
394
  // Stop sync.
382
395
  if (this._session) {
383
- if ('sync' in this._userAgent && typeof (this._userAgent as any).sync?.stopSync === 'function') {
384
- await (this._userAgent as any).sync.stopSync(timeout);
385
- }
396
+ await this._userAgent.sync.stopSync(timeout);
386
397
  }
387
398
 
388
399
  this._session = undefined;
@@ -422,6 +433,81 @@ export class AuthManager {
422
433
  }
423
434
  }
424
435
 
436
+ /**
437
+ * Gracefully shut down the auth manager, releasing all resources.
438
+ *
439
+ * This goes beyond {@link disconnect} or {@link lock}: it stops sync,
440
+ * clears the active session, locks the vault, and **closes** the
441
+ * underlying storage handles (e.g. LevelDB) so the process can exit
442
+ * without dangling timers or open file descriptors.
443
+ *
444
+ * After calling `shutdown()`, the `AuthManager` instance should not be
445
+ * reused — create a new one via {@link AuthManager.create} if needed.
446
+ *
447
+ * Idempotent: calling `shutdown()` more than once is safe.
448
+ *
449
+ * @param options - Optional shutdown configuration.
450
+ * @param options.timeout - Milliseconds to wait for sync to stop. Default: `2000`.
451
+ *
452
+ * @example
453
+ * ```ts
454
+ * const session = await auth.connectHeadless({ password });
455
+ * // ... perform work ...
456
+ * await auth.shutdown(); // clean exit, no process.exit() needed
457
+ * ```
458
+ */
459
+ async shutdown(options: ShutdownOptions = {}): Promise<void> {
460
+ if (this._isShutDown) {
461
+ return;
462
+ }
463
+
464
+ const { timeout = 2000 } = options;
465
+ const did = this._session?.did;
466
+
467
+ // 1. Stop sync.
468
+ try {
469
+ await this._userAgent.sync.stopSync(timeout);
470
+ } catch {
471
+ // Best-effort — don't block shutdown on sync errors.
472
+ }
473
+
474
+ // 2. Clear the active session.
475
+ this._session = undefined;
476
+
477
+ // 3. Lock the vault.
478
+ try {
479
+ await this._userAgent.vault.lock();
480
+ this._emitter.emit('vault-locked', {});
481
+ } catch {
482
+ // Vault may already be locked or uninitialised — safe to ignore.
483
+ }
484
+
485
+ // 4. Close the sync engine (releases LevelDB handles, timers).
486
+ try {
487
+ await this._userAgent.sync.close();
488
+ } catch {
489
+ // Best-effort.
490
+ }
491
+
492
+ // 5. Close the storage adapter (e.g. LevelDB session store).
493
+ if (typeof this._storage.close === 'function') {
494
+ try {
495
+ await this._storage.close();
496
+ } catch {
497
+ // Best-effort.
498
+ }
499
+ }
500
+
501
+ // 6. Mark as shut down and transition state.
502
+ this._isShutDown = true;
503
+ this._setState('locked');
504
+
505
+ // 7. Emit session-end if there was an active session.
506
+ if (did) {
507
+ this._emitter.emit('session-end', { did });
508
+ }
509
+ }
510
+
425
511
  // ─── Multi-identity ────────────────────────────────────────────
426
512
 
427
513
  /**
@@ -456,8 +542,7 @@ export class AuthManager {
456
542
  throw new Error(`[@enbox/auth] Identity not found: ${didUri}`);
457
543
  }
458
544
 
459
- const connectedDid = identity.metadata.connectedDid ?? identity.did.uri;
460
- const delegateDid = identity.metadata.connectedDid ? identity.did.uri : undefined;
545
+ const { connectedDid, delegateDid } = resolveIdentityDids(identity);
461
546
 
462
547
  // Persist the switch.
463
548
  await this._storage.set(STORAGE_KEYS.PREVIOUSLY_CONNECTED, 'true');
@@ -482,10 +567,7 @@ export class AuthManager {
482
567
  // Already registered — safe to ignore.
483
568
  }
484
569
 
485
- const syncMode = sync === undefined ? 'live' : 'poll';
486
- const syncInterval = sync ?? (syncMode === 'live' ? '5m' : '2m');
487
- this._userAgent.sync.startSync({ mode: syncMode, interval: syncInterval })
488
- .catch((err: unknown) => console.error('[@enbox/auth] Sync failed:', err));
570
+ startSyncIfEnabled(this._userAgent, sync);
489
571
  }
490
572
 
491
573
  this._session = new AuthSession({
@@ -551,9 +633,9 @@ export class AuthManager {
551
633
 
552
634
  // ─── Vault ─────────────────────────────────────────────────────
553
635
 
554
- /** Access the vault manager for lock/unlock/backup operations. */
555
- get vault(): VaultManager {
556
- return this._vault;
636
+ /** Access the underlying identity vault for lock/unlock/backup operations. */
637
+ get vault(): HdIdentityVault {
638
+ return this._userAgent.vault;
557
639
  }
558
640
 
559
641
  // ─── Events ────────────────────────────────────────────────────
@@ -583,7 +665,7 @@ export class AuthManager {
583
665
 
584
666
  /** Whether the vault is currently locked. */
585
667
  get isLocked(): boolean {
586
- return this._vault.isLocked;
668
+ return this._userAgent.vault.isLocked();
587
669
  }
588
670
 
589
671
  /** Whether a connection attempt is in progress. */
@@ -596,8 +678,60 @@ export class AuthManager {
596
678
  return this._userAgent;
597
679
  }
598
680
 
681
+ /**
682
+ * The local DWN server endpoint discovered during `create()`, if any.
683
+ *
684
+ * When set, the agent is operating in remote mode (no in-process DWN).
685
+ * This property is available immediately after `create()` returns,
686
+ * before any event listeners are attached.
687
+ */
688
+ get localDwnEndpoint(): string | undefined {
689
+ return this._localDwnEndpoint;
690
+ }
691
+
599
692
  // ─── Private helpers ───────────────────────────────────────────
600
693
 
694
+ /**
695
+ * Build a `FlowContext` from the manager's current state.
696
+ *
697
+ * Replaces the 5 manual inline context constructions that were
698
+ * previously duplicated across `connect()`, `walletConnect()`,
699
+ * `importFromPhrase()`, `importFromPortable()`, and `restoreSession()`.
700
+ */
701
+ private _flowContext(): FlowContext {
702
+ return {
703
+ userAgent : this._userAgent,
704
+ emitter : this._emitter,
705
+ storage : this._storage,
706
+ defaultPassword : this._defaultPassword,
707
+ passwordProvider : this._passwordProvider,
708
+ defaultSync : this._defaultSync,
709
+ defaultDwnEndpoints : this._defaultDwnEndpoints,
710
+ registration : this._registration,
711
+ };
712
+ }
713
+
714
+ /**
715
+ * Template for connection flows that follow the guard → try/finally → setState pattern.
716
+ *
717
+ * Consolidates the duplicated concurrency guard, `_isConnecting` flag management,
718
+ * session assignment, and state transition across `connect()`, `walletConnect()`,
719
+ * `importFromPhrase()`, and `importFromPortable()`.
720
+ */
721
+ private async _withConnect(fn: () => Promise<AuthSession>): Promise<AuthSession> {
722
+ this._guardConcurrency();
723
+ this._isConnecting = true;
724
+
725
+ try {
726
+ const session = await fn();
727
+ this._session = session;
728
+ this._setState('connected');
729
+ return session;
730
+ } finally {
731
+ this._isConnecting = false;
732
+ }
733
+ }
734
+
601
735
  private _setState(state: AuthState): void {
602
736
  if (state === this._state) {return;}
603
737
  const previous = this._state;