@enbox/auth 0.6.32 → 0.6.34

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 (51) hide show
  1. package/dist/esm/auth-manager.js +105 -52
  2. package/dist/esm/auth-manager.js.map +1 -1
  3. package/dist/esm/connect/import.js +17 -0
  4. package/dist/esm/connect/import.js.map +1 -1
  5. package/dist/esm/connect/lifecycle.js +31 -33
  6. package/dist/esm/connect/lifecycle.js.map +1 -1
  7. package/dist/esm/connect/restore.js +3 -2
  8. package/dist/esm/connect/restore.js.map +1 -1
  9. package/dist/esm/connect/{local.js → vault.js} +32 -5
  10. package/dist/esm/connect/vault.js.map +1 -0
  11. package/dist/esm/connect/wallet.js +10 -2
  12. package/dist/esm/connect/wallet.js.map +1 -1
  13. package/dist/esm/identity-session.js +11 -23
  14. package/dist/esm/identity-session.js.map +1 -1
  15. package/dist/esm/index.js +1 -1
  16. package/dist/esm/password-provider.js.map +1 -1
  17. package/dist/esm/types.js.map +1 -1
  18. package/dist/esm/wallet-connect-client.js +12 -10
  19. package/dist/esm/wallet-connect-client.js.map +1 -1
  20. package/dist/types/auth-manager.d.ts +57 -19
  21. package/dist/types/auth-manager.d.ts.map +1 -1
  22. package/dist/types/connect/import.d.ts.map +1 -1
  23. package/dist/types/connect/lifecycle.d.ts +19 -5
  24. package/dist/types/connect/lifecycle.d.ts.map +1 -1
  25. package/dist/types/connect/restore.d.ts.map +1 -1
  26. package/dist/types/connect/{local.d.ts → vault.d.ts} +9 -6
  27. package/dist/types/connect/vault.d.ts.map +1 -0
  28. package/dist/types/connect/wallet.d.ts.map +1 -1
  29. package/dist/types/identity-session.d.ts +11 -42
  30. package/dist/types/identity-session.d.ts.map +1 -1
  31. package/dist/types/index.d.ts +2 -2
  32. package/dist/types/index.d.ts.map +1 -1
  33. package/dist/types/password-provider.d.ts +25 -9
  34. package/dist/types/password-provider.d.ts.map +1 -1
  35. package/dist/types/types.d.ts +25 -18
  36. package/dist/types/types.d.ts.map +1 -1
  37. package/dist/types/wallet-connect-client.d.ts.map +1 -1
  38. package/package.json +11 -7
  39. package/src/auth-manager.ts +124 -66
  40. package/src/connect/import.ts +19 -0
  41. package/src/connect/lifecycle.ts +61 -39
  42. package/src/connect/restore.ts +15 -5
  43. package/src/connect/{local.ts → vault.ts} +35 -6
  44. package/src/connect/wallet.ts +11 -3
  45. package/src/identity-session.ts +11 -55
  46. package/src/index.ts +2 -1
  47. package/src/password-provider.ts +27 -11
  48. package/src/types.ts +27 -20
  49. package/src/wallet-connect-client.ts +13 -11
  50. package/dist/esm/connect/local.js.map +0 -1
  51. package/dist/types/connect/local.d.ts.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enbox/auth",
3
- "version": "0.6.32",
3
+ "version": "0.6.34",
4
4
  "description": "Headless authentication and identity management SDK for Enbox",
5
5
  "type": "module",
6
6
  "main": "./dist/esm/index.js",
@@ -37,6 +37,10 @@
37
37
  ".": {
38
38
  "types": "./dist/types/index.d.ts",
39
39
  "import": "./dist/esm/index.js"
40
+ },
41
+ "./auth-manager": {
42
+ "types": "./dist/types/auth-manager.d.ts",
43
+ "import": "./dist/esm/auth-manager.js"
40
44
  }
41
45
  },
42
46
  "keywords": [
@@ -56,12 +60,12 @@
56
60
  "bun": ">=1.0.0"
57
61
  },
58
62
  "dependencies": {
59
- "@enbox/agent": "0.7.0",
60
- "@enbox/common": "0.1.0",
61
- "@enbox/crypto": "0.1.0",
62
- "@enbox/dids": "0.1.0",
63
- "@enbox/dwn-clients": "0.4.0",
64
- "@enbox/dwn-sdk-js": "0.3.5",
63
+ "@enbox/agent": "0.7.2",
64
+ "@enbox/common": "0.1.1",
65
+ "@enbox/crypto": "0.1.1",
66
+ "@enbox/dids": "0.1.1",
67
+ "@enbox/dwn-clients": "0.4.1",
68
+ "@enbox/dwn-sdk-js": "0.3.6",
65
69
  "level": "8.0.1"
66
70
  },
67
71
  "devDependencies": {
@@ -3,10 +3,19 @@
3
3
  *
4
4
  * Replaces `Enbox.connect()` (formerly `Web5.connect()`) with a composable,
5
5
  * multi-identity-aware auth system that works in both browser and CLI environments.
6
+ *
7
+ * NOTE: this file is also exposed as the `@enbox/auth/auth-manager` subpath
8
+ * export so `@enbox/api` can import the AuthManager class without pulling in
9
+ * the full barrel. The subpath is **internal to the monorepo** — external
10
+ * consumers should import from `@enbox/auth` (or `@enbox/browser`) instead;
11
+ * the subpath's file layout is not part of any stability guarantee.
12
+ *
13
+ * @internal
6
14
  * @module
7
15
  */
8
16
 
9
- import type { BearerIdentity, HdIdentityVault, PortableIdentity } from '@enbox/agent';
17
+ import type { RecordsWriteMessage } from '@enbox/dwn-sdk-js';
18
+ import type { AgentSessionIdentity, BearerIdentity, DwnDataEncodedRecordsWriteMessage, HdIdentityVault, PortableIdentity } from '@enbox/agent';
10
19
 
11
20
  import type { FlowContext } from './connect/lifecycle.js';
12
21
  import type { PasswordProvider } from './password-provider.js';
@@ -20,15 +29,14 @@ import type {
20
29
  DisconnectOptions,
21
30
  HandlerConnectOptions,
22
31
  HeadlessConnectOptions,
23
- IdentityInfo,
24
32
  ImportFromPhraseOptions,
25
33
  ImportFromPortableOptions,
26
- LocalConnectOptions,
27
34
  RegistrationOptions,
28
35
  RestoreSessionOptions,
29
36
  ShutdownOptions,
30
37
  StorageAdapter,
31
38
  SyncOption,
39
+ VaultConnectOptions,
32
40
  WalletConnectOptions,
33
41
  } from './types.js';
34
42
 
@@ -40,10 +48,10 @@ import { AuthEventEmitter } from './events.js';
40
48
  import { AuthSession } from './identity-session.js';
41
49
  import { createDefaultStorage } from './storage/storage.js';
42
50
  import { discoverLocalDwn } from './discovery.js';
43
- import { localConnect } from './connect/local.js';
44
51
  import { normalizeProtocolRequests } from './permissions.js';
45
52
  import { restoreSession } from './connect/restore.js';
46
53
  import { STORAGE_KEYS } from './types.js';
54
+ import { vaultConnect } from './connect/vault.js';
47
55
  import { walletConnect } from './connect/wallet.js';
48
56
  import { deriveActiveSyncScope, ensureVaultReady, finalizeDelegateSession, importDelegateAndSetupSync, resolveIdentityDids, resolvePassword, startSyncIfEnabled, toSyncIdentityProtocols } from './connect/lifecycle.js';
49
57
  import { importFromPhrase, importFromPortable } from './connect/import.js';
@@ -241,27 +249,45 @@ export class AuthManager {
241
249
  const restored = await restoreSession(this._flowContext());
242
250
  if (restored) { return restored; }
243
251
 
244
- // 2. Route to the appropriate flow.
245
- if (this._isLocalConnect(options)) {
246
- return localConnect(this._flowContext(), options as LocalConnectOptions);
252
+ // 2. Route to the appropriate flow. `_isVaultConnect` is a type guard
253
+ // so the two branches receive correctly narrowed `options` types
254
+ // without casts.
255
+ if (this._isVaultConnect(options)) {
256
+ return vaultConnect(this._flowContext(), options);
247
257
  }
248
258
 
249
- return this._handlerConnect(options as HandlerConnectOptions | undefined);
259
+ return this._handlerConnect(options);
250
260
  });
251
261
  }
252
262
 
253
263
  /**
254
- * Create or reconnect a local identity (explicit local connect).
264
+ * Create or reconnect a local identity (explicit vault connect).
255
265
  *
256
- * Use this when you explicitly want the local vault flow, bypassing
266
+ * Use this when you explicitly want the vault flow, bypassing
257
267
  * auto-detection. This is the preferred method for wallet apps.
258
268
  *
259
- * @param options - Local connect options.
269
+ * @param options - Vault connect options.
260
270
  * @returns An active AuthSession.
261
271
  * @throws If a connection attempt is already in progress.
272
+ * @deprecated Use {@link connectVault} instead. Will be removed in 1.0.
262
273
  */
263
- async connectLocal(options?: LocalConnectOptions): Promise<AuthSession> {
264
- return this._withConnect(() => localConnect(this._flowContext(), options));
274
+ async connectLocal(options?: VaultConnectOptions): Promise<AuthSession> {
275
+ return this.connectVault(options);
276
+ }
277
+
278
+ /**
279
+ * Create or reconnect an identity via the local HD vault.
280
+ *
281
+ * Use this when you explicitly want the vault flow, bypassing
282
+ * auto-detection. This is the preferred method for wallet apps
283
+ * and CLI tools that own the identity vault directly.
284
+ *
285
+ * @param options - Vault connect options.
286
+ * @returns An active AuthSession.
287
+ * @throws If a connection attempt is already in progress.
288
+ */
289
+ async connectVault(options?: VaultConnectOptions): Promise<AuthSession> {
290
+ return this._withConnect(() => vaultConnect(this._flowContext(), options));
265
291
  }
266
292
 
267
293
  /**
@@ -305,6 +331,7 @@ export class AuthManager {
305
331
  * This replaces the manual `previouslyConnected` localStorage pattern.
306
332
  */
307
333
  async restoreSession(options?: RestoreSessionOptions): Promise<AuthSession | undefined> {
334
+ this._guardShutdown();
308
335
  this._guardConcurrency();
309
336
  this._isConnecting = true;
310
337
 
@@ -345,6 +372,7 @@ export class AuthManager {
345
372
  * ```
346
373
  */
347
374
  async connectHeadless(options?: HeadlessConnectOptions): Promise<AuthSession> {
375
+ this._guardShutdown();
348
376
  let password = options?.password ?? this._defaultPassword;
349
377
  const isFirstLaunch = await this._userAgent.firstLaunch();
350
378
 
@@ -385,7 +413,7 @@ export class AuthManager {
385
413
 
386
414
  const { connectedDid, delegateDid } = resolveIdentityDids(identity);
387
415
 
388
- const identityInfo: IdentityInfo = {
416
+ const identityInfo: AgentSessionIdentity = {
389
417
  didUri : connectedDid,
390
418
  name : identity.metadata.name,
391
419
  connectedDid : identity.metadata.connectedDid,
@@ -496,17 +524,17 @@ export class AuthManager {
496
524
  messageType : DwnInterface.RecordsRead,
497
525
  messageParams : { filter: { recordId: grantId } },
498
526
  });
499
- if (readReply.status.code !== 200 || !readReply.entry) { continue; }
527
+ if (readReply.status.code !== 200 || !readReply.entry?.recordsWrite) { continue; }
500
528
  // Reconstruct DwnDataEncodedRecordsWriteMessage: RecordsRead returns
501
529
  // data as a stream, but PermissionGrant.parse needs encodedData.
502
530
  const grantDataBytes = readReply.entry.data
503
531
  ? await DataStream.toBytes(readReply.entry.data)
504
532
  : new Uint8Array(0);
505
- const grantMsgWithData = {
533
+ const grantMsgWithData: DwnDataEncodedRecordsWriteMessage = {
506
534
  ...readReply.entry.recordsWrite,
507
535
  encodedData: Convert.uint8Array(grantDataBytes).toBase64Url(),
508
536
  };
509
- const grant = DwnPermissionGrant.parse(grantMsgWithData as any);
537
+ const grant = DwnPermissionGrant.parse(grantMsgWithData);
510
538
 
511
539
  // Self-healing: ensure the revocation grant is on the remote
512
540
  // DWN. The best-effort fanout at connect time may have failed.
@@ -519,7 +547,19 @@ export class AuthManager {
519
547
  messageParams : { filter: { recordId: revocationGrantId } },
520
548
  });
521
549
  if (revGrantReply.status.code === 200 && revGrantReply.entry?.recordsWrite) {
522
- const { encodedData: revGrantEncoded, ...revGrantRaw } = revGrantReply.entry.recordsWrite as any;
550
+ // Strip `encodedData` from the wire payload the
551
+ // bytes are sent via `data` (Blob) on the next line.
552
+ // `RecordsWriteMessage` doesn't declare `encodedData`,
553
+ // but the wire-format reply may include it; widen the
554
+ // local type to acknowledge that without `any`.
555
+ // NOSONAR S4325 false positive: the cast is required to
556
+ // typecheck the destructuring of the undeclared
557
+ // optional `encodedData` property; removing it fails
558
+ // TS2339. Sonar reads the intersection-with-optional-
559
+ // field as a no-op widening, which it isn't here.
560
+ type RecordsWriteWireMessage = RecordsWriteMessage & { encodedData?: string };
561
+ const { encodedData: _encoded, ...revGrantRaw } =
562
+ revGrantReply.entry.recordsWrite as RecordsWriteWireMessage; // NOSONAR
523
563
  const revGrantData = revGrantReply.entry.data
524
564
  ? new Blob([await DataStream.toBytes(revGrantReply.entry.data) as BlobPart])
525
565
  : undefined;
@@ -552,7 +592,7 @@ export class AuthManager {
552
592
  // delivery, the owner-side authority source won't see it.
553
593
  let remoteDelivered = false;
554
594
  if (revocationMessage && remoteDwnUrls.length > 0) {
555
- const { encodedData, ...rawMessage } = revocationMessage as any;
595
+ const { encodedData, ...rawMessage } = revocationMessage;
556
596
  const data = encodedData
557
597
  ? new Blob([Convert.base64Url(encodedData).toUint8Array() as BlobPart])
558
598
  : undefined;
@@ -783,7 +823,7 @@ export class AuthManager {
783
823
  * Each identity has a DID URI, name, and optional connected DID
784
824
  * (for wallet-connected/delegated identities).
785
825
  */
786
- async listIdentities(): Promise<IdentityInfo[]> {
826
+ async listIdentities(): Promise<AgentSessionIdentity[]> {
787
827
  const identities = await this._userAgent.identity.list();
788
828
  return identities.map((identity: BearerIdentity) => ({
789
829
  didUri : identity.did.uri,
@@ -815,7 +855,7 @@ export class AuthManager {
815
855
  await this._storage.set(STORAGE_KEYS.PREVIOUSLY_CONNECTED, 'true');
816
856
  await this._storage.set(STORAGE_KEYS.ACTIVE_IDENTITY, connectedDid);
817
857
 
818
- const identityInfo: IdentityInfo = {
858
+ const identityInfo: AgentSessionIdentity = {
819
859
  didUri : connectedDid,
820
860
  name : identity.metadata.name,
821
861
  connectedDid : identity.metadata.connectedDid,
@@ -952,47 +992,40 @@ export class AuthManager {
952
992
  // ─── Private helpers ───────────────────────────────────────────
953
993
 
954
994
  /**
955
- * Determine whether the given options indicate a local connect flow.
995
+ * Determine whether the given options indicate a vault connect flow.
956
996
  *
957
- * Local connect is indicated by the presence of `password`,
958
- * `createIdentity`, or `recoveryPhrase` signals that the caller
959
- * is managing its own vault/identity lifecycle. In non-browser
960
- * environments, local connect is the fallback.
997
+ * Handler intent is asserted by the presence of a non-empty `protocols`
998
+ * array OR a non-null `connectHandler`; everything else (including the
999
+ * no-options case, an empty `protocols: []`, and `null` values from JS
1000
+ * callers) routes to vault connect. An empty `protocols` array is
1001
+ * intentionally NOT a handler signal — it carries no permission scopes
1002
+ * for the handler to authorize, so treating it as handler-flow would
1003
+ * produce a zero-grant "connected" session indistinguishable from a
1004
+ * denied connect.
1005
+ *
1006
+ * Acts as a TypeScript type guard: a `true` return narrows `options` to
1007
+ * `VaultConnectOptions | undefined` at call sites, so the routing in
1008
+ * {@link AuthManager.connect} can dispatch without unsafe casts.
961
1009
  */
962
- private _isLocalConnect(options?: ConnectOptions): boolean {
963
- const o = (options ?? {}) as Record<string, unknown>;
964
-
965
- // If any local-connect-specific keys are present, it's definitely local.
966
- const hasLocalSignals = (
967
- o.password !== undefined ||
968
- o.createIdentity !== undefined ||
969
- o.recoveryPhrase !== undefined ||
970
- o.dwnEndpoints !== undefined ||
971
- o.metadata !== undefined
972
- );
973
- if (hasLocalSignals) { return true; }
974
-
975
- // If any handler-connect signals are present, use the handler flow.
976
- const hasHandlerSignals = (
977
- o.protocols !== undefined ||
978
- o.connectHandler !== undefined
979
- );
980
- if (hasHandlerSignals) { return false; }
981
-
982
- // No explicit signals → default to local connect.
983
- // Callers that want handler-based connect must provide protocols
984
- // or a connectHandler.
1010
+ private _isVaultConnect(options?: ConnectOptions): options is VaultConnectOptions | undefined {
1011
+ if (options === undefined || options === null) { return true; }
1012
+ if ('protocols' in options && Array.isArray(options.protocols) && options.protocols.length > 0) { return false; }
1013
+ if ('connectHandler' in options && options.connectHandler !== undefined && options.connectHandler !== null) { return false; }
985
1014
  return true;
986
1015
  }
987
1016
 
988
1017
  /**
989
1018
  * Run a handler-based (delegated) connect flow.
990
1019
  *
991
- * 1. Initialize the vault (agent-only, no identity).
992
- * 2. Normalize protocol permission requests.
993
- * 3. Delegate to the connect handler for credential acquisition.
994
- * 4. Import the delegate DID, process grants, set up sync.
995
- * 5. Finalize and return the AuthSession.
1020
+ * Handler resolution happens BEFORE any vault I/O so a misconfigured call
1021
+ * (no handler reachable) fails fast without mutating on-disk state.
1022
+ *
1023
+ * 1. Resolve the connect handler (fast-fail when none is reachable).
1024
+ * 2. Initialize the vault (agent-only, no identity).
1025
+ * 3. Normalize protocol permission requests.
1026
+ * 4. Delegate to the connect handler for credential acquisition.
1027
+ * 5. Import the delegate DID, process grants, set up sync.
1028
+ * 6. Finalize and return the AuthSession.
996
1029
  */
997
1030
  private async _handlerConnect(
998
1031
  options?: HandlerConnectOptions,
@@ -1001,15 +1034,8 @@ export class AuthManager {
1001
1034
  const { userAgent, emitter, storage } = ctx;
1002
1035
  const sync = options?.sync ?? ctx.defaultSync;
1003
1036
 
1004
- // 1. Initialize vault (agent-only, no identity).
1005
- const isFirstLaunch = await userAgent.firstLaunch();
1006
- const password = await resolvePassword(ctx, undefined, isFirstLaunch);
1007
- await ensureVaultReady({ userAgent, emitter, password, isFirstLaunch });
1008
-
1009
- // 2. Normalize protocol requests.
1010
- const permissionRequests = normalizeProtocolRequests(options?.protocols);
1011
-
1012
- // 3. Resolve the handler.
1037
+ // 1. Resolve the handler FIRST. Anything past this point may mutate the
1038
+ // vault on disk, so misconfiguration must fail before that happens.
1013
1039
  const handler = options?.connectHandler ?? this._connectHandler;
1014
1040
  if (!handler) {
1015
1041
  throw new Error(
@@ -1019,6 +1045,16 @@ export class AuthManager {
1019
1045
  );
1020
1046
  }
1021
1047
 
1048
+ // 2. Initialize vault (agent-only, no identity). The per-call password
1049
+ // (when supplied via `HandlerConnectOptions.password`) wins over the
1050
+ // manager default, matching the behavior of the vault-connect flow.
1051
+ const isFirstLaunch = await userAgent.firstLaunch();
1052
+ const password = await resolvePassword(ctx, options?.password, isFirstLaunch);
1053
+ await ensureVaultReady({ userAgent, emitter, password, isFirstLaunch });
1054
+
1055
+ // 3. Normalize protocol requests.
1056
+ const permissionRequests = normalizeProtocolRequests(options?.protocols);
1057
+
1022
1058
  // 4. Delegate to the handler.
1023
1059
  const result = await handler.requestAccess({ permissionRequests });
1024
1060
  if (!result) {
@@ -1033,14 +1069,22 @@ export class AuthManager {
1033
1069
  const identity = await importDelegateAndSetupSync({
1034
1070
  userAgent, delegatePortableDid, connectedDid, delegateGrants,
1035
1071
  delegateDecryptionKeys, delegateContextKeys, delegateMultiPartyProtocols,
1036
- sessionRevocations,
1037
1072
  flowName: 'Connect',
1038
1073
  });
1039
1074
 
1040
- // 6. Finalize session.
1075
+ // 6. Finalize session. Pass the transient delegate state explicitly
1076
+ // so `persistOrClearDelegateSecrets` doesn't need to read it back
1077
+ // off the identity (which was the old `(identity as any)._foo`
1078
+ // smuggling pattern).
1041
1079
  return finalizeDelegateSession({
1042
1080
  userAgent, emitter, storage, identity,
1043
- connectedDid, delegateDid: delegatePortableDid.uri, sync,
1081
+ connectedDid, delegateDid : delegatePortableDid.uri, sync,
1082
+ delegateState : {
1083
+ delegateDecryptionKeys,
1084
+ delegateContextKeys,
1085
+ delegateMultiPartyProtocols,
1086
+ sessionRevocations,
1087
+ },
1044
1088
  });
1045
1089
  }
1046
1090
 
@@ -1070,8 +1114,13 @@ export class AuthManager {
1070
1114
  * Consolidates the duplicated concurrency guard, `_isConnecting` flag management,
1071
1115
  * session assignment, and state transition across `connect()`, `walletConnect()`,
1072
1116
  * `importFromPhrase()`, and `importFromPortable()`.
1117
+ *
1118
+ * Also short-circuits if the manager has already been shut down — using
1119
+ * a closed manager would otherwise fail deep inside sync/storage with an
1120
+ * unhelpful error.
1073
1121
  */
1074
1122
  private async _withConnect(fn: () => Promise<AuthSession>): Promise<AuthSession> {
1123
+ this._guardShutdown();
1075
1124
  this._guardConcurrency();
1076
1125
  this._isConnecting = true;
1077
1126
 
@@ -1186,4 +1235,13 @@ export class AuthManager {
1186
1235
  );
1187
1236
  }
1188
1237
  }
1238
+
1239
+ private _guardShutdown(): void {
1240
+ if (this._isShutDown) {
1241
+ throw new Error(
1242
+ '[@enbox/auth] AuthManager has been shut down and cannot be reused. ' +
1243
+ 'Create a new instance via AuthManager.create().'
1244
+ );
1245
+ }
1246
+ }
1189
1247
  }
@@ -10,10 +10,17 @@ import type { AuthSession } from '../identity-session.js';
10
10
  import type { FlowContext } from './lifecycle.js';
11
11
  import type { ImportFromPhraseOptions, ImportFromPortableOptions } from '../types.js';
12
12
 
13
+ import { IdentityProtocolDefinition, JwkProtocolDefinition } from '@enbox/agent';
14
+
13
15
  import { DEFAULT_DWN_ENDPOINTS } from '../types.js';
14
16
  import { registerWithDwnEndpoints } from '../registration.js';
15
17
  import { createDefaultIdentity, ensureVaultReady, finalizeSession, registerSyncScopeForIdentity, resolveIdentityDids, startSyncIfEnabled } from './lifecycle.js';
16
18
 
19
+ const AGENT_DID_SYNC_PROTOCOLS: [string, ...string[]] = [
20
+ IdentityProtocolDefinition.protocol,
21
+ JwkProtocolDefinition.protocol,
22
+ ];
23
+
17
24
  /**
18
25
  * Import (or recover) an identity from a BIP-39 recovery phrase.
19
26
  *
@@ -80,6 +87,18 @@ export async function importFromPhrase(
80
87
  await registerSyncScopeForIdentity({ userAgent, connectedDid });
81
88
  }
82
89
 
90
+ // Register the agent DID for sync (same rationale as vaultConnect).
91
+ if (sync !== 'off') {
92
+ try {
93
+ await userAgent.sync.registerIdentity({
94
+ did : userAgent.agentDid.uri,
95
+ options : { protocols: AGENT_DID_SYNC_PROTOCOLS },
96
+ });
97
+ } catch {
98
+ // Already registered from a previous session.
99
+ }
100
+ }
101
+
83
102
  // Start sync.
84
103
  await startSyncIfEnabled(userAgent, sync);
85
104
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Shared helpers for connect flows.
3
3
  *
4
- * Consolidates duplicated logic across `local-connect`, `session-restore`,
4
+ * Consolidates duplicated logic across `vault-connect`, `session-restore`,
5
5
  * `wallet-connect`, and `import-identity` flows:
6
6
  *
7
7
  * - Password resolution chain
@@ -15,11 +15,11 @@
15
15
  */
16
16
 
17
17
  import type { PortableDid } from '@enbox/dids';
18
- import type { BearerIdentity, DelegateContextKey, DelegateDecryptionKey, DwnDataEncodedRecordsWriteMessage, EnboxUserAgent } from '@enbox/agent';
18
+ import type { AgentSessionIdentity, BearerIdentity, DelegateContextKey, DelegateDecryptionKey, DwnDataEncodedRecordsWriteMessage, EnboxUserAgent } from '@enbox/agent';
19
19
 
20
20
  import type { AuthEventEmitter } from '../events.js';
21
21
  import type { PasswordProvider } from '../password-provider.js';
22
- import type { IdentityInfo, RegistrationOptions, StorageAdapter, SyncOption } from '../types.js';
22
+ import type { RegistrationOptions, StorageAdapter, SyncOption } from '../types.js';
23
23
 
24
24
  import { Convert } from '@enbox/common';
25
25
  import type { GenericMessage } from '@enbox/dwn-sdk-js';
@@ -35,7 +35,7 @@ import { DEFAULT_DWN_ENDPOINTS, INSECURE_DEFAULT_PASSWORD, STORAGE_KEYS } from '
35
35
  /**
36
36
  * Unified context passed from `AuthManager` to every connect flow.
37
37
  *
38
- * Replaces the per-flow `LocalConnectContext`, `SessionRestoreContext`,
38
+ * Replaces the per-flow `VaultConnectContext`, `SessionRestoreContext`,
39
39
  * `WalletConnectContext`, and `ImportContext` interfaces. All fields are
40
40
  * optional beyond the core triple (`userAgent`, `emitter`, `storage`) so
41
41
  * flows only consume what they need.
@@ -91,11 +91,15 @@ export async function resolvePassword(
91
91
 
92
92
  password ??= INSECURE_DEFAULT_PASSWORD;
93
93
 
94
- if (password === INSECURE_DEFAULT_PASSWORD) {
94
+ // Two cases reach this branch: no password was supplied at any level
95
+ // (and we fell through to the insecure default), or the caller explicitly
96
+ // supplied an empty string. Both produce a vault with effectively zero
97
+ // password protection — surface a single warning in both cases.
98
+ if (password === INSECURE_DEFAULT_PASSWORD || password.length === 0) {
95
99
  console.warn(
96
- '[@enbox/auth] SECURITY WARNING: No password set. Using insecure default. ' +
97
- 'Set a password via AuthManager.create({ password }) or connect({ password }) ' +
98
- 'to protect your identity vault.'
100
+ '[@enbox/auth] SECURITY WARNING: No password set (or an empty string was supplied). ' +
101
+ 'Using an insecure default; this leaves the identity vault unprotected. ' +
102
+ 'Set a non-empty password via AuthManager.create({ password }) or connect({ password }).'
99
103
  );
100
104
  }
101
105
 
@@ -180,7 +184,7 @@ export async function startSyncIfEnabled(
180
184
  * encryption keys, and a DWN service endpoint.
181
185
  *
182
186
  * This consolidates the identical identity creation block that was
183
- * duplicated in `localConnect` and `importFromPhrase`.
187
+ * duplicated in `vaultConnect` and `importFromPhrase`.
184
188
  *
185
189
  * @internal
186
190
  */
@@ -270,9 +274,11 @@ export function deriveSyncScopeFromGrants(grants: DwnPermissionGrant[]): 'all' |
270
274
  const protocols = new Set<string>();
271
275
 
272
276
  for (const grant of grants) {
273
- const scope = grant.scope as any;
274
-
275
- // Only Messages.Read grants authorize sync.
277
+ // Only Messages.Read grants authorize sync. The discriminated union
278
+ // (`PermissionScope = Messages | Protocols | Records`) narrows
279
+ // `scope.protocol` to `string | undefined` after the interface +
280
+ // method checks — no cast needed.
281
+ const { scope } = grant;
276
282
  if (scope.interface !== 'Messages' || scope.method !== 'Read') {
277
283
  continue;
278
284
  }
@@ -282,7 +288,7 @@ export function deriveSyncScopeFromGrants(grants: DwnPermissionGrant[]): 'all' |
282
288
  continue;
283
289
  }
284
290
 
285
- const protocol = scope.protocol as string | undefined;
291
+ const protocol = scope.protocol;
286
292
  if (protocol === undefined) {
287
293
  // Unrestricted Messages.Read — delegate can sync all protocols.
288
294
  return 'all';
@@ -331,10 +337,15 @@ export async function deriveActiveSyncScope(
331
337
  if (revocationResponse.reply.status.code !== 200) { return []; }
332
338
 
333
339
  // Build the set of revoked grant IDs from revocation parent context.
340
+ // `descriptor.parentId` is the canonical location; the top-level
341
+ // `parentId` is a legacy/alternate-format fallback. Typed narrowly
342
+ // (no `any`) so additions to the shape land in the type system.
334
343
  const revokedGrantIds = new Set<string>();
335
344
  if (revocationResponse.reply.entries) {
336
345
  for (const entry of revocationResponse.reply.entries as DwnDataEncodedRecordsWriteMessage[]) {
337
- const parentId = (entry as any).descriptor?.parentId ?? (entry as any).parentId;
346
+ const parentId =
347
+ entry.descriptor.parentId
348
+ ?? (entry as { parentId?: string }).parentId;
338
349
  if (parentId) { revokedGrantIds.add(parentId); }
339
350
  }
340
351
  }
@@ -586,13 +597,12 @@ export async function importDelegateAndSetupSync(params: {
586
597
  delegateDecryptionKeys?: DelegateDecryptionKey[];
587
598
  delegateContextKeys?: DelegateContextKey[];
588
599
  delegateMultiPartyProtocols?: string[];
589
- sessionRevocations?: { grantId: string; revocationGrantId: string }[];
590
600
  flowName: string;
591
601
  }): Promise<BearerIdentity> {
592
602
  const {
593
603
  userAgent, delegatePortableDid, connectedDid, delegateGrants,
594
604
  delegateDecryptionKeys, delegateContextKeys, delegateMultiPartyProtocols,
595
- sessionRevocations, flowName,
605
+ flowName,
596
606
  } = params;
597
607
 
598
608
  let identity: BearerIdentity | undefined;
@@ -683,20 +693,11 @@ export async function importDelegateAndSetupSync(params: {
683
693
  // Doing a manual pull first would double the startup burst and can
684
694
  // trigger rate limits on the remote DWN.
685
695
 
686
- // Store protocol keys on the identity for finalize to persist.
687
- if (delegateDecryptionKeys && delegateDecryptionKeys.length > 0) {
688
- (identity as any)._delegateDecryptionKeys = delegateDecryptionKeys;
689
- }
690
- if (delegateContextKeys && delegateContextKeys.length > 0) {
691
- (identity as any)._delegateContextKeys = delegateContextKeys;
692
- }
693
- if (delegateMultiPartyProtocols && delegateMultiPartyProtocols.length > 0) {
694
- (identity as any)._delegateMultiPartyProtocols = delegateMultiPartyProtocols;
695
- }
696
- if (sessionRevocations && sessionRevocations.length > 0) {
697
- (identity as any)._sessionRevocations = sessionRevocations;
698
- }
699
-
696
+ // Delegate protocol keys / multi-party state / revocation map are
697
+ // passed back to the caller explicitly via the return value so
698
+ // `finalizeDelegateSession` can persist them. (The previous flow
699
+ // smuggled them through `(identity as any)._foo`, which traded
700
+ // type safety for one fewer return value.)
700
701
  return identity;
701
702
  } catch (error: unknown) {
702
703
  if (identity) {
@@ -720,6 +721,21 @@ export async function importDelegateAndSetupSync(params: {
720
721
 
721
722
  // ─── finalizeDelegateSession ────────────────────────────────────
722
723
 
724
+ /**
725
+ * Transient state produced by a successful delegated connect flow that
726
+ * `finalizeDelegateSession` persists into the SecretStore + storage
727
+ * adapter. Passed explicitly through the call chain rather than
728
+ * smuggled via private fields on `BearerIdentity`.
729
+ *
730
+ * @internal
731
+ */
732
+ export type DelegateSessionState = {
733
+ delegateDecryptionKeys?: DelegateDecryptionKey[];
734
+ delegateContextKeys?: DelegateContextKey[];
735
+ delegateMultiPartyProtocols?: string[];
736
+ sessionRevocations?: { grantId: string; revocationGrantId: string }[];
737
+ };
738
+
723
739
  /**
724
740
  * Build an `AuthSession` for a delegated connect flow (DWeb Connect or
725
741
  * relay WalletConnect). Starts sync and persists delegate/connected DID
@@ -735,8 +751,12 @@ export async function finalizeDelegateSession(params: {
735
751
  connectedDid: string;
736
752
  delegateDid: string;
737
753
  sync: SyncOption | undefined;
754
+ delegateState?: DelegateSessionState;
738
755
  }): Promise<AuthSession> {
739
- const { userAgent, emitter, storage, identity, connectedDid, delegateDid, sync } = params;
756
+ const {
757
+ userAgent, emitter, storage, identity, connectedDid, delegateDid, sync,
758
+ delegateState = {},
759
+ } = params;
740
760
 
741
761
  await startSyncIfEnabled(userAgent, sync);
742
762
 
@@ -752,7 +772,7 @@ export async function finalizeDelegateSession(params: {
752
772
  // Persist or clear delegate keys/revocations. Clearing stale values
753
773
  // from prior sessions prevents a reconnect with fewer capabilities
754
774
  // from retaining old decryption material.
755
- await persistOrClearDelegateSecrets(userAgent, storage, identity, extraStorageKeys);
775
+ await persistOrClearDelegateSecrets(userAgent, storage, extraStorageKeys, delegateState);
756
776
 
757
777
  // Wire post-connect context key persistence: when the owner creates a
758
778
  // new multi-party context, the agent injects the key into the delegate
@@ -837,8 +857,8 @@ export async function finalizeSession(params: {
837
857
  await Promise.all(storageWrites);
838
858
 
839
859
  // When identityName is undefined, no user identity exists (agent-only session).
840
- // Build an IdentityInfo with the agent DID as a fallback.
841
- const identityInfo: IdentityInfo = {
860
+ // Build an AgentSessionIdentity with the agent DID as a fallback.
861
+ const identityInfo: AgentSessionIdentity = {
842
862
  didUri : connectedDid,
843
863
  name : identityName ?? 'Agent',
844
864
  connectedDid : identityConnectedDid,
@@ -869,11 +889,15 @@ export async function finalizeSession(params: {
869
889
  async function persistOrClearDelegateSecrets(
870
890
  userAgent: EnboxUserAgent,
871
891
  storage: StorageAdapter,
872
- identity: BearerIdentity,
873
892
  extraStorageKeys: Record<string, string>,
893
+ delegateState: DelegateSessionState,
874
894
  ): Promise<void> {
875
- const delegateDecryptionKeys = (identity as any)._delegateDecryptionKeys as DelegateDecryptionKey[] | undefined;
876
- const delegateContextKeys = (identity as any)._delegateContextKeys as DelegateContextKey[] | undefined;
895
+ const {
896
+ delegateDecryptionKeys,
897
+ delegateContextKeys,
898
+ delegateMultiPartyProtocols,
899
+ sessionRevocations,
900
+ } = delegateState;
877
901
 
878
902
  // Persist or clear keys in the SecretStore + legacy StorageAdapter.
879
903
  const secretWrites: Promise<void>[] = [];
@@ -895,14 +919,12 @@ async function persistOrClearDelegateSecrets(
895
919
  try { await storage.remove(STORAGE_KEYS.DELEGATE_CONTEXT_KEYS); } catch { /* best-effort */ }
896
920
  }
897
921
 
898
- const delegateMultiPartyProtocols = (identity as any)._delegateMultiPartyProtocols as string[] | undefined;
899
922
  if (delegateMultiPartyProtocols?.length) {
900
923
  extraStorageKeys[STORAGE_KEYS.DELEGATE_MULTI_PARTY_PROTOCOLS] = JSON.stringify(delegateMultiPartyProtocols);
901
924
  } else {
902
925
  try { await storage.remove(STORAGE_KEYS.DELEGATE_MULTI_PARTY_PROTOCOLS); } catch { /* best-effort */ }
903
926
  }
904
927
 
905
- const sessionRevocations = (identity as any)._sessionRevocations as { grantId: string; revocationGrantId: string }[] | undefined;
906
928
  if (sessionRevocations?.length) {
907
929
  extraStorageKeys[STORAGE_KEYS.SESSION_REVOCATIONS] = JSON.stringify(sessionRevocations);
908
930
  } else {