@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
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Identity import flows.
3
+ *
4
+ * - Import from BIP-39 recovery phrase (re-derive vault + identity).
5
+ * - Import from PortableIdentity JSON.
6
+ * @module
7
+ */
8
+
9
+ import type { AuthSession } from '../identity-session.js';
10
+ import type { FlowContext } from './lifecycle.js';
11
+ import type { ImportFromPhraseOptions, ImportFromPortableOptions } from '../types.js';
12
+
13
+ import { DEFAULT_DWN_ENDPOINTS } from '../types.js';
14
+ import { registerWithDwnEndpoints } from '../registration.js';
15
+ import { createDefaultIdentity, ensureVaultReady, finalizeSession, resolveIdentityDids, startSyncIfEnabled } from './lifecycle.js';
16
+
17
+ /**
18
+ * Import (or recover) an identity from a BIP-39 recovery phrase.
19
+ *
20
+ * This re-initializes the vault with the given phrase and password,
21
+ * recovering the agent DID and all derived keys.
22
+ */
23
+ export async function importFromPhrase(
24
+ ctx: FlowContext,
25
+ options: ImportFromPhraseOptions,
26
+ ): Promise<AuthSession> {
27
+ const { userAgent, emitter, storage } = ctx;
28
+ const { recoveryPhrase, password } = options;
29
+ const sync = options.sync ?? ctx.defaultSync;
30
+ const dwnEndpoints = options.dwnEndpoints ?? ctx.defaultDwnEndpoints ?? DEFAULT_DWN_ENDPOINTS;
31
+
32
+ // Initialize the vault with the recovery phrase and start the agent.
33
+ const isFirstLaunch = await userAgent.firstLaunch();
34
+ await ensureVaultReady({
35
+ userAgent,
36
+ emitter,
37
+ password,
38
+ isFirstLaunch,
39
+ recoveryPhrase,
40
+ dwnEndpoints,
41
+ });
42
+
43
+ // The recovery phrase re-derives the same agent DID,
44
+ // but the user identity might not exist yet — create one if needed.
45
+ const identities = await userAgent.identity.list();
46
+ let identity = identities[0];
47
+ let isNewIdentity = false;
48
+
49
+ if (!identity) {
50
+ isNewIdentity = true;
51
+ identity = await createDefaultIdentity(userAgent, dwnEndpoints);
52
+ }
53
+
54
+ const { connectedDid, delegateDid } = resolveIdentityDids(identity);
55
+
56
+ // Register with DWN endpoints (if registration options are provided).
57
+ if (ctx.registration) {
58
+ await registerWithDwnEndpoints(
59
+ {
60
+ userAgent : userAgent,
61
+ dwnEndpoints,
62
+ agentDid : userAgent.agentDid.uri,
63
+ connectedDid,
64
+ storage : storage,
65
+ },
66
+ ctx.registration,
67
+ );
68
+ }
69
+
70
+ // Register sync for new identities.
71
+ if (isNewIdentity && sync !== 'off') {
72
+ await userAgent.sync.registerIdentity({
73
+ did : connectedDid,
74
+ options : { delegateDid, protocols: [] },
75
+ });
76
+ }
77
+
78
+ // Start sync.
79
+ startSyncIfEnabled(userAgent, sync);
80
+
81
+ // Persist session info, build AuthSession, and emit lifecycle events.
82
+ return finalizeSession({
83
+ userAgent,
84
+ emitter,
85
+ storage,
86
+ connectedDid,
87
+ delegateDid,
88
+ identityName : identity.metadata.name,
89
+ identityConnectedDid : identity.metadata.connectedDid,
90
+ });
91
+ }
92
+
93
+ /**
94
+ * Import an identity from a PortableIdentity JSON object.
95
+ *
96
+ * The portable identity contains the DID's private keys and metadata,
97
+ * allowing it to be used on this device.
98
+ */
99
+ export async function importFromPortable(
100
+ ctx: FlowContext,
101
+ options: ImportFromPortableOptions,
102
+ ): Promise<AuthSession> {
103
+ const { userAgent, emitter, storage } = ctx;
104
+ const sync = options.sync ?? ctx.defaultSync;
105
+
106
+ const identity = await userAgent.identity.import({
107
+ portableIdentity: options.portableIdentity,
108
+ });
109
+
110
+ const { connectedDid, delegateDid } = resolveIdentityDids(identity);
111
+
112
+ // Register with DWN endpoints (if registration options are provided).
113
+ // For portable imports, extract endpoints from the DID document's DWN service.
114
+ if (ctx.registration) {
115
+ const dwnEndpoints = ctx.defaultDwnEndpoints ?? DEFAULT_DWN_ENDPOINTS;
116
+ await registerWithDwnEndpoints(
117
+ {
118
+ userAgent : userAgent,
119
+ dwnEndpoints,
120
+ agentDid : userAgent.agentDid.uri,
121
+ connectedDid,
122
+ storage : storage,
123
+ },
124
+ ctx.registration,
125
+ );
126
+ }
127
+
128
+ // Register and start sync.
129
+ if (sync !== 'off') {
130
+ await userAgent.sync.registerIdentity({
131
+ did : connectedDid,
132
+ options : { delegateDid, protocols: [] },
133
+ });
134
+ }
135
+
136
+ startSyncIfEnabled(userAgent, sync);
137
+
138
+ // Persist session info, build AuthSession, and emit lifecycle events.
139
+ return finalizeSession({
140
+ userAgent,
141
+ emitter,
142
+ storage,
143
+ connectedDid,
144
+ delegateDid,
145
+ identityName : identity.metadata.name,
146
+ identityConnectedDid : identity.metadata.connectedDid,
147
+ });
148
+ }
@@ -0,0 +1,321 @@
1
+ /**
2
+ * Shared helpers for connect flows.
3
+ *
4
+ * Consolidates duplicated logic across `local-connect`, `session-restore`,
5
+ * `wallet-connect`, and `import-identity` flows:
6
+ *
7
+ * - Password resolution chain
8
+ * - Vault init/start lifecycle
9
+ * - Sync mode/interval calculation and startup
10
+ * - `connectedDid` / `delegateDid` derivation from identity metadata
11
+ * - Session finalization (storage persistence + AuthSession construction + events)
12
+ *
13
+ * @module
14
+ * @internal
15
+ */
16
+
17
+ import type { BearerIdentity, EnboxUserAgent } from '@enbox/agent';
18
+
19
+ import type { AuthEventEmitter } from '../events.js';
20
+ import type { PasswordProvider } from '../password-provider.js';
21
+ import type { IdentityInfo, RegistrationOptions, StorageAdapter, SyncOption } from '../types.js';
22
+
23
+ import { AuthSession } from '../identity-session.js';
24
+ import { DEFAULT_DWN_ENDPOINTS, INSECURE_DEFAULT_PASSWORD, STORAGE_KEYS } from '../types.js';
25
+
26
+ // ─── FlowContext ─────────────────────────────────────────────────
27
+
28
+ /**
29
+ * Unified context passed from `AuthManager` to every connect flow.
30
+ *
31
+ * Replaces the per-flow `LocalConnectContext`, `SessionRestoreContext`,
32
+ * `WalletConnectContext`, and `ImportContext` interfaces. All fields are
33
+ * optional beyond the core triple (`userAgent`, `emitter`, `storage`) so
34
+ * flows only consume what they need.
35
+ *
36
+ * @internal
37
+ */
38
+ export interface FlowContext {
39
+ userAgent: EnboxUserAgent;
40
+ emitter: AuthEventEmitter;
41
+ storage: StorageAdapter;
42
+ defaultPassword?: string;
43
+ passwordProvider?: PasswordProvider;
44
+ defaultSync?: SyncOption;
45
+ defaultDwnEndpoints?: string[];
46
+ registration?: RegistrationOptions;
47
+ }
48
+
49
+ // ─── resolvePassword ─────────────────────────────────────────────
50
+
51
+ /**
52
+ * Resolve a password through the standard chain:
53
+ * explicit option → manager default → provider → insecure fallback.
54
+ *
55
+ * Emits a console warning when the insecure default is used.
56
+ *
57
+ * @param ctx - The flow context (provides `defaultPassword` and `passwordProvider`).
58
+ * @param explicit - An explicit password from the caller (highest priority).
59
+ * @param isFirstLaunch - Whether the vault has never been initialized.
60
+ * @returns The resolved password string.
61
+ *
62
+ * @internal
63
+ */
64
+ export async function resolvePassword(
65
+ ctx: Pick<FlowContext, 'defaultPassword' | 'passwordProvider'>,
66
+ explicit: string | undefined,
67
+ isFirstLaunch: boolean,
68
+ ): Promise<string> {
69
+ let password = explicit ?? ctx.defaultPassword;
70
+
71
+ if (!password && ctx.passwordProvider) {
72
+ try {
73
+ password = await ctx.passwordProvider.getPassword({
74
+ reason: isFirstLaunch ? 'create' : 'unlock',
75
+ });
76
+ } catch {
77
+ // Provider failed — fall through to insecure default.
78
+ }
79
+ }
80
+
81
+ password ??= INSECURE_DEFAULT_PASSWORD;
82
+
83
+ if (password === INSECURE_DEFAULT_PASSWORD) {
84
+ console.warn(
85
+ '[@enbox/auth] SECURITY WARNING: No password set. Using insecure default. ' +
86
+ 'Set a password via AuthManager.create({ password }) or connect({ password }) ' +
87
+ 'to protect your identity vault.'
88
+ );
89
+ }
90
+
91
+ return password;
92
+ }
93
+
94
+ // ─── ensureVaultReady ────────────────────────────────────────────
95
+
96
+ /**
97
+ * Initialize (on first launch) and start the agent, then emit `vault-unlocked`.
98
+ *
99
+ * This consolidates the 5 copies of:
100
+ * ```ts
101
+ * if (isFirstLaunch) { await userAgent.initialize({ password, ... }); }
102
+ * await userAgent.start({ password });
103
+ * emitter.emit('vault-unlocked', {});
104
+ * ```
105
+ *
106
+ * @returns The recovery phrase if the vault was just initialized, otherwise `undefined`.
107
+ *
108
+ * @internal
109
+ */
110
+ export async function ensureVaultReady(params: {
111
+ userAgent: EnboxUserAgent;
112
+ emitter: AuthEventEmitter;
113
+ password: string;
114
+ isFirstLaunch: boolean;
115
+ recoveryPhrase?: string;
116
+ dwnEndpoints?: string[];
117
+ }): Promise<string | undefined> {
118
+ const { userAgent, emitter, password, isFirstLaunch } = params;
119
+ let recoveryPhrase: string | undefined;
120
+
121
+ if (isFirstLaunch) {
122
+ recoveryPhrase = await userAgent.initialize({
123
+ password,
124
+ recoveryPhrase : params.recoveryPhrase,
125
+ dwnEndpoints : params.dwnEndpoints,
126
+ });
127
+ }
128
+
129
+ await userAgent.start({ password });
130
+ emitter.emit('vault-unlocked', {});
131
+
132
+ return recoveryPhrase;
133
+ }
134
+
135
+ // ─── startSyncIfEnabled ─────────────────────────────────────────
136
+
137
+ /**
138
+ * Start DWN synchronisation if `sync` is not `'off'`.
139
+ *
140
+ * Consolidates 6 copies of:
141
+ * ```ts
142
+ * const syncMode = sync === undefined ? 'live' : 'poll';
143
+ * const syncInterval = sync ?? (syncMode === 'live' ? '5m' : '2m');
144
+ * userAgent.sync.startSync({ mode: syncMode, interval: syncInterval })
145
+ * .catch((err) => console.error('[@enbox/auth] Sync failed:', err));
146
+ * ```
147
+ *
148
+ * @internal
149
+ */
150
+ export function startSyncIfEnabled(
151
+ userAgent: EnboxUserAgent,
152
+ sync: SyncOption | undefined,
153
+ ): void {
154
+ if (sync === 'off') {
155
+ return;
156
+ }
157
+
158
+ const syncMode = sync === undefined ? 'live' : 'poll';
159
+ const syncInterval = sync ?? (syncMode === 'live' ? '5m' : '2m');
160
+
161
+ userAgent.sync.startSync({ mode: syncMode, interval: syncInterval })
162
+ .catch((err: unknown) => {
163
+ console.error('[@enbox/auth] Sync failed:', err);
164
+ });
165
+ }
166
+
167
+ // ─── createDefaultIdentity ──────────────────────────────────────
168
+
169
+ /**
170
+ * Create a new `did:dht` identity with Ed25519 signing and X25519
171
+ * encryption keys, and a DWN service endpoint.
172
+ *
173
+ * This consolidates the identical identity creation block that was
174
+ * duplicated in `localConnect` and `importFromPhrase`.
175
+ *
176
+ * @internal
177
+ */
178
+ export async function createDefaultIdentity(
179
+ userAgent: EnboxUserAgent,
180
+ dwnEndpoints: string[] = DEFAULT_DWN_ENDPOINTS,
181
+ name = 'Default',
182
+ ): Promise<BearerIdentity> {
183
+ return userAgent.identity.create({
184
+ didMethod : 'dht',
185
+ metadata : { name },
186
+ didOptions : {
187
+ services: [
188
+ {
189
+ id : 'dwn',
190
+ type : 'DecentralizedWebNode',
191
+ serviceEndpoint : dwnEndpoints,
192
+ }
193
+ ],
194
+ verificationMethods: [
195
+ {
196
+ algorithm : 'Ed25519',
197
+ id : 'sig',
198
+ purposes : ['assertionMethod', 'authentication'],
199
+ },
200
+ {
201
+ algorithm : 'X25519',
202
+ id : 'enc',
203
+ purposes : ['keyAgreement'],
204
+ },
205
+ ],
206
+ },
207
+ });
208
+ }
209
+
210
+ // ─── resolveIdentityDids ────────────────────────────────────────
211
+
212
+ /**
213
+ * Derive `connectedDid` and `delegateDid` from identity metadata.
214
+ *
215
+ * For a **local** identity: `connectedDid` is the identity's own DID URI
216
+ * and `delegateDid` is `undefined`.
217
+ *
218
+ * For a **wallet-connected** identity: `connectedDid` is the external wallet
219
+ * DID, and `delegateDid` is the local identity's DID URI.
220
+ *
221
+ * @param identity - The bearer identity to extract DIDs from.
222
+ * @param storedDelegateDid - Optional fallback delegate DID from storage,
223
+ * used by session-restore when the identity metadata doesn't include a
224
+ * `connectedDid` but a delegate DID was persisted in a prior session.
225
+ *
226
+ * @internal
227
+ */
228
+ export function resolveIdentityDids(
229
+ identity: BearerIdentity,
230
+ storedDelegateDid?: string,
231
+ ): {
232
+ connectedDid: string;
233
+ delegateDid: string | undefined;
234
+ } {
235
+ const connectedDid = identity.metadata.connectedDid ?? identity.did.uri;
236
+ const delegateDid = identity.metadata.connectedDid
237
+ ? identity.did.uri
238
+ : (storedDelegateDid ?? undefined);
239
+ return { connectedDid, delegateDid };
240
+ }
241
+
242
+ // ─── finalizeSession ────────────────────────────────────────────
243
+
244
+ /**
245
+ * Persist session markers, build an `AuthSession`, and emit lifecycle events.
246
+ *
247
+ * Consolidates 5 copies of:
248
+ * ```ts
249
+ * await storage.set(STORAGE_KEYS.PREVIOUSLY_CONNECTED, 'true');
250
+ * await storage.set(STORAGE_KEYS.ACTIVE_IDENTITY, connectedDid);
251
+ * const session = new AuthSession({ ... });
252
+ * emitter.emit('identity-added', { identity: identityInfo });
253
+ * emitter.emit('session-start', { session: { ... } });
254
+ * ```
255
+ *
256
+ * @param params.emitIdentityAdded - Whether to emit `identity-added`. Defaults to `true`.
257
+ * Set to `false` for session-restore (identity was already added in the original flow).
258
+ * @param params.extraStorageKeys - Additional key-value pairs to persist (e.g. delegate/connected DIDs
259
+ * for wallet-connect flows).
260
+ *
261
+ * @internal
262
+ */
263
+ export async function finalizeSession(params: {
264
+ userAgent: EnboxUserAgent;
265
+ emitter: AuthEventEmitter;
266
+ storage: StorageAdapter;
267
+ connectedDid: string;
268
+ delegateDid?: string;
269
+ recoveryPhrase?: string;
270
+ identityName: string;
271
+ identityConnectedDid?: string;
272
+ emitIdentityAdded?: boolean;
273
+ extraStorageKeys?: Record<string, string>;
274
+ }): Promise<AuthSession> {
275
+ const {
276
+ userAgent,
277
+ emitter,
278
+ storage,
279
+ connectedDid,
280
+ delegateDid,
281
+ recoveryPhrase,
282
+ identityName,
283
+ identityConnectedDid,
284
+ emitIdentityAdded = true,
285
+ extraStorageKeys,
286
+ } = params;
287
+
288
+ // Persist session markers.
289
+ await storage.set(STORAGE_KEYS.PREVIOUSLY_CONNECTED, 'true');
290
+ await storage.set(STORAGE_KEYS.ACTIVE_IDENTITY, connectedDid);
291
+
292
+ if (extraStorageKeys) {
293
+ for (const [key, value] of Object.entries(extraStorageKeys)) {
294
+ await storage.set(key, value);
295
+ }
296
+ }
297
+
298
+ const identityInfo: IdentityInfo = {
299
+ didUri : connectedDid,
300
+ name : identityName,
301
+ connectedDid : identityConnectedDid,
302
+ };
303
+
304
+ const session = new AuthSession({
305
+ agent : userAgent,
306
+ did : connectedDid,
307
+ delegateDid,
308
+ recoveryPhrase,
309
+ identity : identityInfo,
310
+ });
311
+
312
+ if (emitIdentityAdded) {
313
+ emitter.emit('identity-added', { identity: identityInfo });
314
+ }
315
+
316
+ emitter.emit('session-start', {
317
+ session: { did: connectedDid, delegateDid, identity: identityInfo },
318
+ });
319
+
320
+ return session;
321
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Local DID connect flow.
3
+ *
4
+ * Creates or reconnects a local identity with vault-protected keys.
5
+ * This replaces the "Mode D/E" paths in Enbox.connect().
6
+ * @module
7
+ */
8
+
9
+ import type { AuthSession } from '../identity-session.js';
10
+ import type { FlowContext } from './lifecycle.js';
11
+ import type { LocalConnectOptions } from '../types.js';
12
+
13
+ import { applyLocalDwnDiscovery } from '../discovery.js';
14
+ import { DEFAULT_DWN_ENDPOINTS } from '../types.js';
15
+ import { registerWithDwnEndpoints } from '../registration.js';
16
+ import { createDefaultIdentity, ensureVaultReady, finalizeSession, resolveIdentityDids, resolvePassword, startSyncIfEnabled } from './lifecycle.js';
17
+
18
+ /**
19
+ * Execute the local connect flow.
20
+ *
21
+ * - On first launch: initializes the vault, creates a new DID, returns recovery phrase.
22
+ * - On subsequent launches: unlocks the vault and reconnects to the existing identity.
23
+ */
24
+ export async function localConnect(
25
+ ctx: FlowContext,
26
+ options: LocalConnectOptions = {},
27
+ ): Promise<AuthSession> {
28
+ const { userAgent, emitter, storage } = ctx;
29
+
30
+ // Resolve password through the standard chain.
31
+ const isFirstLaunch = await userAgent.firstLaunch();
32
+ const password = await resolvePassword(ctx, options.password, isFirstLaunch);
33
+
34
+ const sync = options.sync ?? ctx.defaultSync;
35
+ const dwnEndpoints = options.dwnEndpoints ?? ctx.defaultDwnEndpoints ?? DEFAULT_DWN_ENDPOINTS;
36
+
37
+ // Initialize vault on first launch and start the agent.
38
+ const recoveryPhrase = await ensureVaultReady({
39
+ userAgent,
40
+ emitter,
41
+ password,
42
+ isFirstLaunch,
43
+ recoveryPhrase: options.recoveryPhrase,
44
+ dwnEndpoints,
45
+ });
46
+
47
+ // Apply local DWN discovery (browser redirect payload or persisted endpoint).
48
+ // In remote mode, discovery already ran before agent creation — skip.
49
+ if (!userAgent.dwn.isRemoteMode) {
50
+ await applyLocalDwnDiscovery(userAgent, storage, emitter);
51
+ }
52
+
53
+ // Find or create the user identity.
54
+ const identities = await userAgent.identity.list();
55
+ let identity = identities[0];
56
+ let isNewIdentity = false;
57
+
58
+ if (!identity) {
59
+ isNewIdentity = true;
60
+ identity = await createDefaultIdentity(userAgent, dwnEndpoints, options.metadata?.name ?? 'Default');
61
+ }
62
+
63
+ const { connectedDid, delegateDid } = resolveIdentityDids(identity);
64
+
65
+ // Register with DWN endpoints (if registration options are provided).
66
+ if (ctx.registration) {
67
+ await registerWithDwnEndpoints(
68
+ {
69
+ userAgent : userAgent,
70
+ dwnEndpoints,
71
+ agentDid : userAgent.agentDid.uri,
72
+ connectedDid,
73
+ storage : storage,
74
+ },
75
+ ctx.registration,
76
+ );
77
+ }
78
+
79
+ // Register sync for new identities.
80
+ if (isNewIdentity && sync !== 'off') {
81
+ await userAgent.sync.registerIdentity({
82
+ did : connectedDid,
83
+ options : { delegateDid, protocols: [] },
84
+ });
85
+ }
86
+
87
+ // Start sync.
88
+ startSyncIfEnabled(userAgent, sync);
89
+
90
+ // Persist session info, build AuthSession, and emit lifecycle events.
91
+ return finalizeSession({
92
+ userAgent,
93
+ emitter,
94
+ storage,
95
+ connectedDid,
96
+ delegateDid,
97
+ recoveryPhrase,
98
+ identityName : identity.metadata.name,
99
+ identityConnectedDid : identity.metadata.connectedDid,
100
+ });
101
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Session restore flow.
3
+ *
4
+ * Restores a previously established session from persisted storage,
5
+ * replacing the "previouslyConnected" pattern in apps.
6
+ * @module
7
+ */
8
+
9
+ import type { AuthSession } from '../identity-session.js';
10
+ import type { FlowContext } from './lifecycle.js';
11
+ import type { RestoreSessionOptions } from '../types.js';
12
+
13
+ import { applyLocalDwnDiscovery } from '../discovery.js';
14
+ import { STORAGE_KEYS } from '../types.js';
15
+ import { ensureVaultReady, finalizeSession, resolveIdentityDids, resolvePassword, startSyncIfEnabled } from './lifecycle.js';
16
+
17
+ /**
18
+ * Attempt to restore a previous session.
19
+ *
20
+ * Returns `undefined` if no previous session exists.
21
+ * Returns an `AuthSession` if the session was successfully restored.
22
+ */
23
+ export async function restoreSession(
24
+ ctx: FlowContext,
25
+ options: RestoreSessionOptions = {},
26
+ ): Promise<AuthSession | undefined> {
27
+ const { userAgent, emitter, storage } = ctx;
28
+
29
+ // Check if there was a previous session.
30
+ const previouslyConnected = await storage.get(STORAGE_KEYS.PREVIOUSLY_CONNECTED);
31
+ if (previouslyConnected !== 'true') {
32
+ return undefined;
33
+ }
34
+
35
+ // Resolve password: explicit option → callback → provider → manager default → insecure fallback.
36
+ // Note: restoreSession has an extra `onPasswordRequired` callback that sits between
37
+ // the explicit password and the provider. We handle that here, then delegate the
38
+ // remainder of the chain to `resolvePassword()`.
39
+ let explicitPassword = options.password;
40
+
41
+ if (!explicitPassword && !ctx.defaultPassword && options.onPasswordRequired) {
42
+ explicitPassword = await options.onPasswordRequired();
43
+ }
44
+
45
+ // Check for stale session marker: if the vault was never initialized,
46
+ // previouslyConnected is a leftover — clean up and bail.
47
+ const isFirstLaunch = await userAgent.firstLaunch();
48
+ if (isFirstLaunch) {
49
+ await storage.remove(STORAGE_KEYS.PREVIOUSLY_CONNECTED);
50
+ return undefined;
51
+ }
52
+
53
+ const password = await resolvePassword(ctx, explicitPassword, false);
54
+
55
+ // Start the agent (vault is known to exist).
56
+ await ensureVaultReady({
57
+ userAgent,
58
+ emitter,
59
+ password,
60
+ isFirstLaunch: false,
61
+ });
62
+
63
+ // Apply local DWN discovery (browser redirect payload or persisted endpoint).
64
+ // In remote mode, discovery already ran before agent creation — skip.
65
+ if (!userAgent.dwn.isRemoteMode) {
66
+ await applyLocalDwnDiscovery(userAgent, storage, emitter);
67
+ }
68
+
69
+ // Determine which identity to reconnect.
70
+ const activeIdentityDid = await storage.get(STORAGE_KEYS.ACTIVE_IDENTITY);
71
+ const storedDelegateDid = await storage.get(STORAGE_KEYS.DELEGATE_DID);
72
+
73
+ // First try the connected identity (wallet-connected sessions).
74
+ let identity = await userAgent.identity.connectedIdentity();
75
+
76
+ if (!identity) {
77
+ // Try to find the specific active identity.
78
+ if (activeIdentityDid) {
79
+ identity = await userAgent.identity.get({ didUri: activeIdentityDid });
80
+ }
81
+
82
+ // Fall back to the first available identity.
83
+ if (!identity) {
84
+ const identities = await userAgent.identity.list();
85
+ identity = identities[0];
86
+ }
87
+ }
88
+
89
+ if (!identity) {
90
+ // No identity found — clean up stale session data.
91
+ await storage.remove(STORAGE_KEYS.PREVIOUSLY_CONNECTED);
92
+ await storage.remove(STORAGE_KEYS.ACTIVE_IDENTITY);
93
+ await storage.remove(STORAGE_KEYS.DELEGATE_DID);
94
+ await storage.remove(STORAGE_KEYS.CONNECTED_DID);
95
+ return undefined;
96
+ }
97
+
98
+ const { connectedDid, delegateDid } = resolveIdentityDids(
99
+ identity, storedDelegateDid ?? undefined,
100
+ );
101
+
102
+ // Start sync.
103
+ startSyncIfEnabled(userAgent, ctx.defaultSync);
104
+
105
+ // Persist session info, build AuthSession, and emit lifecycle events.
106
+ // Session restore does not emit `identity-added` (identity was already added in the original flow).
107
+ return finalizeSession({
108
+ userAgent,
109
+ emitter,
110
+ storage,
111
+ connectedDid,
112
+ delegateDid,
113
+ identityName : identity.metadata.name,
114
+ identityConnectedDid : identity.metadata.connectedDid,
115
+ emitIdentityAdded : false,
116
+ });
117
+ }