@enbox/auth 0.5.0 → 0.6.1

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 (90) hide show
  1. package/dist/esm/auth-manager.js +240 -171
  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 +378 -0
  6. package/dist/esm/connect/lifecycle.js.map +1 -0
  7. package/dist/esm/connect/local.js +105 -0
  8. package/dist/esm/connect/local.js.map +1 -0
  9. package/dist/esm/connect/restore.js +117 -0
  10. package/dist/esm/connect/restore.js.map +1 -0
  11. package/dist/esm/connect/wallet.js +80 -0
  12. package/dist/esm/connect/wallet.js.map +1 -0
  13. package/dist/esm/{flows/dwn-discovery.js → discovery.js} +2 -2
  14. package/dist/esm/discovery.js.map +1 -0
  15. package/dist/esm/index.js +13 -19
  16. package/dist/esm/index.js.map +1 -1
  17. package/dist/esm/permissions.js +41 -0
  18. package/dist/esm/permissions.js.map +1 -0
  19. package/dist/esm/{flows/dwn-registration.js → registration.js} +2 -2
  20. package/dist/esm/registration.js.map +1 -0
  21. package/dist/esm/types.js +4 -0
  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 +89 -11
  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 +199 -0
  30. package/dist/types/connect/lifecycle.d.ts.map +1 -0
  31. package/dist/types/connect/local.d.ts +23 -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/connect/wallet.d.ts +21 -0
  36. package/dist/types/connect/wallet.d.ts.map +1 -0
  37. package/dist/types/{flows/dwn-discovery.d.ts → discovery.d.ts} +3 -3
  38. package/dist/types/discovery.d.ts.map +1 -0
  39. package/dist/types/index.d.ts +14 -19
  40. package/dist/types/index.d.ts.map +1 -1
  41. package/dist/types/permissions.d.ts +18 -0
  42. package/dist/types/permissions.d.ts.map +1 -0
  43. package/dist/types/{flows/dwn-registration.d.ts → registration.d.ts} +2 -2
  44. package/dist/types/registration.d.ts.map +1 -0
  45. package/dist/types/types.d.ts +154 -4
  46. package/dist/types/types.d.ts.map +1 -1
  47. package/dist/types/wallet-connect-client.d.ts +86 -0
  48. package/dist/types/wallet-connect-client.d.ts.map +1 -0
  49. package/package.json +9 -5
  50. package/src/auth-manager.ts +258 -191
  51. package/src/connect/import.ts +148 -0
  52. package/src/connect/lifecycle.ts +487 -0
  53. package/src/connect/local.ts +116 -0
  54. package/src/connect/restore.ts +133 -0
  55. package/src/connect/wallet.ts +89 -0
  56. package/src/{flows/dwn-discovery.ts → discovery.ts} +4 -3
  57. package/src/index.ts +20 -19
  58. package/src/permissions.ts +48 -0
  59. package/src/{flows/dwn-registration.ts → registration.ts} +2 -2
  60. package/src/types.ts +171 -4
  61. package/src/wallet-connect-client.ts +275 -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 -177
  65. package/dist/esm/flows/import-identity.js.map +0 -1
  66. package/dist/esm/flows/local-connect.js +0 -158
  67. package/dist/esm/flows/local-connect.js.map +0 -1
  68. package/dist/esm/flows/session-restore.js +0 -125
  69. package/dist/esm/flows/session-restore.js.map +0 -1
  70. package/dist/esm/flows/wallet-connect.js +0 -200
  71. package/dist/esm/flows/wallet-connect.js.map +0 -1
  72. package/dist/esm/vault/vault-manager.js +0 -95
  73. package/dist/esm/vault/vault-manager.js.map +0 -1
  74. package/dist/types/flows/dwn-discovery.d.ts.map +0 -1
  75. package/dist/types/flows/dwn-registration.d.ts.map +0 -1
  76. package/dist/types/flows/import-identity.d.ts +0 -35
  77. package/dist/types/flows/import-identity.d.ts.map +0 -1
  78. package/dist/types/flows/local-connect.d.ts +0 -31
  79. package/dist/types/flows/local-connect.d.ts.map +0 -1
  80. package/dist/types/flows/session-restore.d.ts +0 -29
  81. package/dist/types/flows/session-restore.d.ts.map +0 -1
  82. package/dist/types/flows/wallet-connect.d.ts +0 -44
  83. package/dist/types/flows/wallet-connect.d.ts.map +0 -1
  84. package/dist/types/vault/vault-manager.d.ts +0 -57
  85. package/dist/types/vault/vault-manager.d.ts.map +0 -1
  86. package/src/flows/import-identity.ts +0 -219
  87. package/src/flows/local-connect.ts +0 -192
  88. package/src/flows/session-restore.ts +0 -155
  89. package/src/flows/wallet-connect.ts +0 -226
  90. 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,487 @@
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 { PortableDid } from '@enbox/dids';
18
+ import type { BearerIdentity, DwnDataEncodedRecordsWriteMessage, DwnMessagesPermissionScope, DwnRecordsPermissionScope, EnboxUserAgent } from '@enbox/agent';
19
+
20
+ import type { AuthEventEmitter } from '../events.js';
21
+ import type { PasswordProvider } from '../password-provider.js';
22
+ import type { IdentityInfo, RegistrationOptions, StorageAdapter, SyncOption } from '../types.js';
23
+
24
+ import { Convert } from '@enbox/common';
25
+ import { DwnInterface, DwnPermissionGrant } from '@enbox/agent';
26
+
27
+ import { AuthSession } from '../identity-session.js';
28
+ import { DEFAULT_DWN_ENDPOINTS, INSECURE_DEFAULT_PASSWORD, STORAGE_KEYS } from '../types.js';
29
+
30
+ // ─── FlowContext ─────────────────────────────────────────────────
31
+
32
+ /**
33
+ * Unified context passed from `AuthManager` to every connect flow.
34
+ *
35
+ * Replaces the per-flow `LocalConnectContext`, `SessionRestoreContext`,
36
+ * `WalletConnectContext`, and `ImportContext` interfaces. All fields are
37
+ * optional beyond the core triple (`userAgent`, `emitter`, `storage`) so
38
+ * flows only consume what they need.
39
+ *
40
+ * @internal
41
+ */
42
+ export interface FlowContext {
43
+ userAgent: EnboxUserAgent;
44
+ emitter: AuthEventEmitter;
45
+ storage: StorageAdapter;
46
+ defaultPassword?: string;
47
+ passwordProvider?: PasswordProvider;
48
+ defaultSync?: SyncOption;
49
+ defaultDwnEndpoints?: string[];
50
+ registration?: RegistrationOptions;
51
+ }
52
+
53
+ // ─── resolvePassword ─────────────────────────────────────────────
54
+
55
+ /**
56
+ * Resolve a password through the standard chain:
57
+ * explicit option → manager default → provider → insecure fallback.
58
+ *
59
+ * Emits a console warning when the insecure default is used.
60
+ *
61
+ * @param ctx - The flow context (provides `defaultPassword` and `passwordProvider`).
62
+ * @param explicit - An explicit password from the caller (highest priority).
63
+ * @param isFirstLaunch - Whether the vault has never been initialized.
64
+ * @returns The resolved password string.
65
+ *
66
+ * @internal
67
+ */
68
+ export async function resolvePassword(
69
+ ctx: Pick<FlowContext, 'defaultPassword' | 'passwordProvider'>,
70
+ explicit: string | undefined,
71
+ isFirstLaunch: boolean,
72
+ ): Promise<string> {
73
+ let password = explicit ?? ctx.defaultPassword;
74
+
75
+ if (!password && ctx.passwordProvider) {
76
+ try {
77
+ password = await ctx.passwordProvider.getPassword({
78
+ reason: isFirstLaunch ? 'create' : 'unlock',
79
+ });
80
+ } catch {
81
+ // Provider failed — fall through to insecure default.
82
+ }
83
+ }
84
+
85
+ password ??= INSECURE_DEFAULT_PASSWORD;
86
+
87
+ if (password === INSECURE_DEFAULT_PASSWORD) {
88
+ console.warn(
89
+ '[@enbox/auth] SECURITY WARNING: No password set. Using insecure default. ' +
90
+ 'Set a password via AuthManager.create({ password }) or connect({ password }) ' +
91
+ 'to protect your identity vault.'
92
+ );
93
+ }
94
+
95
+ return password;
96
+ }
97
+
98
+ // ─── ensureVaultReady ────────────────────────────────────────────
99
+
100
+ /**
101
+ * Initialize (on first launch) and start the agent, then emit `vault-unlocked`.
102
+ *
103
+ * This consolidates the 5 copies of:
104
+ * ```ts
105
+ * if (isFirstLaunch) { await userAgent.initialize({ password, ... }); }
106
+ * await userAgent.start({ password });
107
+ * emitter.emit('vault-unlocked', {});
108
+ * ```
109
+ *
110
+ * @returns The recovery phrase if the vault was just initialized, otherwise `undefined`.
111
+ *
112
+ * @internal
113
+ */
114
+ export async function ensureVaultReady(params: {
115
+ userAgent: EnboxUserAgent;
116
+ emitter: AuthEventEmitter;
117
+ password: string;
118
+ isFirstLaunch: boolean;
119
+ recoveryPhrase?: string;
120
+ dwnEndpoints?: string[];
121
+ }): Promise<string | undefined> {
122
+ const { userAgent, emitter, password, isFirstLaunch } = params;
123
+ let recoveryPhrase: string | undefined;
124
+
125
+ if (isFirstLaunch) {
126
+ recoveryPhrase = await userAgent.initialize({
127
+ password,
128
+ recoveryPhrase : params.recoveryPhrase,
129
+ dwnEndpoints : params.dwnEndpoints,
130
+ });
131
+ }
132
+
133
+ await userAgent.start({ password });
134
+ emitter.emit('vault-unlocked', {});
135
+
136
+ return recoveryPhrase;
137
+ }
138
+
139
+ // ─── startSyncIfEnabled ─────────────────────────────────────────
140
+
141
+ /**
142
+ * Start DWN synchronisation if `sync` is not `'off'`.
143
+ *
144
+ * Consolidates 6 copies of:
145
+ * ```ts
146
+ * const syncMode = sync === undefined ? 'live' : 'poll';
147
+ * const syncInterval = sync ?? (syncMode === 'live' ? '5m' : '2m');
148
+ * userAgent.sync.startSync({ mode: syncMode, interval: syncInterval })
149
+ * .catch((err) => console.error('[@enbox/auth] Sync failed:', err));
150
+ * ```
151
+ *
152
+ * @internal
153
+ */
154
+ export function startSyncIfEnabled(
155
+ userAgent: EnboxUserAgent,
156
+ sync: SyncOption | undefined,
157
+ ): void {
158
+ if (sync === 'off') {
159
+ return;
160
+ }
161
+
162
+ const syncMode = sync === undefined ? 'live' : 'poll';
163
+ const syncInterval = sync ?? (syncMode === 'live' ? '5m' : '2m');
164
+
165
+ userAgent.sync.startSync({ mode: syncMode, interval: syncInterval })
166
+ .catch((err: unknown) => {
167
+ console.error('[@enbox/auth] Sync failed:', err);
168
+ });
169
+ }
170
+
171
+ // ─── createDefaultIdentity ──────────────────────────────────────
172
+
173
+ /**
174
+ * Create a new `did:dht` identity with Ed25519 signing and X25519
175
+ * encryption keys, and a DWN service endpoint.
176
+ *
177
+ * This consolidates the identical identity creation block that was
178
+ * duplicated in `localConnect` and `importFromPhrase`.
179
+ *
180
+ * @internal
181
+ */
182
+ export async function createDefaultIdentity(
183
+ userAgent: EnboxUserAgent,
184
+ dwnEndpoints: string[] = DEFAULT_DWN_ENDPOINTS,
185
+ name = 'Default',
186
+ ): Promise<BearerIdentity> {
187
+ return userAgent.identity.create({
188
+ didMethod : 'dht',
189
+ metadata : { name },
190
+ didOptions : {
191
+ services: [
192
+ {
193
+ id : 'dwn',
194
+ type : 'DecentralizedWebNode',
195
+ serviceEndpoint : dwnEndpoints,
196
+ }
197
+ ],
198
+ verificationMethods: [
199
+ {
200
+ algorithm : 'Ed25519',
201
+ id : 'sig',
202
+ purposes : ['assertionMethod', 'authentication'],
203
+ },
204
+ {
205
+ algorithm : 'X25519',
206
+ id : 'enc',
207
+ purposes : ['keyAgreement'],
208
+ },
209
+ ],
210
+ },
211
+ });
212
+ }
213
+
214
+ // ─── resolveIdentityDids ────────────────────────────────────────
215
+
216
+ /**
217
+ * Derive `connectedDid` and `delegateDid` from identity metadata.
218
+ *
219
+ * For a **local** identity: `connectedDid` is the identity's own DID URI
220
+ * and `delegateDid` is `undefined`.
221
+ *
222
+ * For a **wallet-connected** identity: `connectedDid` is the external wallet
223
+ * DID, and `delegateDid` is the local identity's DID URI.
224
+ *
225
+ * @param identity - The bearer identity to extract DIDs from.
226
+ * @param storedDelegateDid - Optional fallback delegate DID from storage,
227
+ * used by session-restore when the identity metadata doesn't include a
228
+ * `connectedDid` but a delegate DID was persisted in a prior session.
229
+ *
230
+ * @internal
231
+ */
232
+ export function resolveIdentityDids(
233
+ identity: BearerIdentity,
234
+ storedDelegateDid?: string,
235
+ ): {
236
+ connectedDid: string;
237
+ delegateDid: string | undefined;
238
+ } {
239
+ const connectedDid = identity.metadata.connectedDid ?? identity.did.uri;
240
+ const delegateDid = identity.metadata.connectedDid
241
+ ? identity.did.uri
242
+ : (storedDelegateDid ?? undefined);
243
+ return { connectedDid, delegateDid };
244
+ }
245
+
246
+ // ─── processConnectedGrants ─────────────────────────────────────
247
+
248
+ /**
249
+ * Process connected grants by storing them in the local DWN as the owner.
250
+ *
251
+ * This is the agent-level equivalent of `Enbox.processConnectedGrants()`.
252
+ * It stores each grant, signed as owner, and returns the deduplicated
253
+ * list of protocol URIs represented by the grants.
254
+ *
255
+ * @internal
256
+ */
257
+ export async function processConnectedGrants(params: {
258
+ agent: EnboxUserAgent;
259
+ delegateDid: string;
260
+ grants: DwnDataEncodedRecordsWriteMessage[];
261
+ }): Promise<string[]> {
262
+ const { agent, delegateDid, grants } = params;
263
+ const connectedProtocols = new Set<string>();
264
+
265
+ for (const grantMessage of grants) {
266
+ const grant = DwnPermissionGrant.parse(grantMessage);
267
+
268
+ // Store the grant as the owner of the DWN so the delegateDid
269
+ // can use it when impersonating the connectedDid.
270
+ const { encodedData, ...rawMessage } = grantMessage;
271
+ const dataStream = new Blob([Convert.base64Url(encodedData).toUint8Array() as BlobPart]);
272
+
273
+ const { reply } = await agent.processDwnRequest({
274
+ store : true,
275
+ author : delegateDid,
276
+ target : delegateDid,
277
+ messageType : DwnInterface.RecordsWrite,
278
+ signAsOwner : true,
279
+ rawMessage,
280
+ dataStream,
281
+ });
282
+
283
+ if (reply.status.code !== 202) {
284
+ throw new Error(
285
+ `[@enbox/auth] Failed to process connected grant: ${reply.status.detail}`
286
+ );
287
+ }
288
+
289
+ const protocol = (grant.scope as DwnMessagesPermissionScope | DwnRecordsPermissionScope).protocol;
290
+ if (protocol) {
291
+ connectedProtocols.add(protocol);
292
+ }
293
+ }
294
+
295
+ return [...connectedProtocols];
296
+ }
297
+
298
+ // ─── importDelegateAndSetupSync ─────────────────────────────────
299
+
300
+ /**
301
+ * Import a delegated DID, process its grants, register sync, and pull.
302
+ *
303
+ * This is the shared post-connect lifecycle used by both the DWeb Connect
304
+ * and relay WalletConnect flows. On failure, the imported identity is
305
+ * cleaned up before re-throwing.
306
+ *
307
+ * @internal
308
+ */
309
+ export async function importDelegateAndSetupSync(params: {
310
+ userAgent: EnboxUserAgent;
311
+ delegatePortableDid: PortableDid;
312
+ connectedDid: string;
313
+ delegateGrants: DwnDataEncodedRecordsWriteMessage[];
314
+ flowName: string;
315
+ }): Promise<BearerIdentity> {
316
+ const { userAgent, delegatePortableDid, connectedDid, delegateGrants, flowName } = params;
317
+
318
+ let identity: BearerIdentity | undefined;
319
+ try {
320
+ identity = await userAgent.identity.import({
321
+ portableIdentity: {
322
+ portableDid : delegatePortableDid,
323
+ metadata : {
324
+ connectedDid,
325
+ name : 'Default',
326
+ uri : delegatePortableDid.uri,
327
+ tenant : userAgent.agentDid.uri,
328
+ },
329
+ },
330
+ });
331
+
332
+ const connectedProtocols = await processConnectedGrants({
333
+ agent : userAgent,
334
+ delegateDid : delegatePortableDid.uri,
335
+ grants : delegateGrants,
336
+ });
337
+
338
+ await userAgent.sync.registerIdentity({
339
+ did : connectedDid,
340
+ options : {
341
+ delegateDid : delegatePortableDid.uri,
342
+ protocols : connectedProtocols,
343
+ },
344
+ });
345
+
346
+ await userAgent.sync.sync('pull');
347
+
348
+ return identity;
349
+ } catch (error: unknown) {
350
+ if (identity) {
351
+ try {
352
+ await userAgent.did.delete({
353
+ didUri : identity.did.uri,
354
+ tenant : identity.metadata.tenant,
355
+ deleteKey : true,
356
+ });
357
+ } catch { /* best effort */ }
358
+
359
+ try {
360
+ await userAgent.identity.delete({ didUri: identity.did.uri });
361
+ } catch { /* best effort */ }
362
+ }
363
+
364
+ const message = error instanceof Error ? error.message : String(error);
365
+ throw new Error(`[@enbox/auth] ${flowName} failed: ${message}`);
366
+ }
367
+ }
368
+
369
+ // ─── finalizeDelegateSession ────────────────────────────────────
370
+
371
+ /**
372
+ * Build an `AuthSession` for a delegated connect flow (DWeb Connect or
373
+ * relay WalletConnect). Starts sync and persists delegate/connected DID
374
+ * markers.
375
+ *
376
+ * @internal
377
+ */
378
+ export async function finalizeDelegateSession(params: {
379
+ userAgent: EnboxUserAgent;
380
+ emitter: AuthEventEmitter;
381
+ storage: StorageAdapter;
382
+ identity: BearerIdentity;
383
+ connectedDid: string;
384
+ delegateDid: string;
385
+ sync: SyncOption | undefined;
386
+ }): Promise<AuthSession> {
387
+ const { userAgent, emitter, storage, identity, connectedDid, delegateDid, sync } = params;
388
+
389
+ startSyncIfEnabled(userAgent, sync);
390
+
391
+ return finalizeSession({
392
+ userAgent,
393
+ emitter,
394
+ storage,
395
+ connectedDid,
396
+ delegateDid,
397
+ identityName : identity.metadata.name,
398
+ identityConnectedDid : identity.metadata.connectedDid,
399
+ extraStorageKeys : {
400
+ [STORAGE_KEYS.DELEGATE_DID] : delegateDid,
401
+ [STORAGE_KEYS.CONNECTED_DID] : connectedDid,
402
+ },
403
+ });
404
+ }
405
+
406
+ // ─── finalizeSession ────────────────────────────────────────────
407
+
408
+ /**
409
+ * Persist session markers, build an `AuthSession`, and emit lifecycle events.
410
+ *
411
+ * Consolidates 5 copies of:
412
+ * ```ts
413
+ * await storage.set(STORAGE_KEYS.PREVIOUSLY_CONNECTED, 'true');
414
+ * await storage.set(STORAGE_KEYS.ACTIVE_IDENTITY, connectedDid);
415
+ * const session = new AuthSession({ ... });
416
+ * emitter.emit('identity-added', { identity: identityInfo });
417
+ * emitter.emit('session-start', { session: { ... } });
418
+ * ```
419
+ *
420
+ * @param params.emitIdentityAdded - Whether to emit `identity-added`. Defaults to `true`.
421
+ * Set to `false` for session-restore (identity was already added in the original flow).
422
+ * @param params.extraStorageKeys - Additional key-value pairs to persist (e.g. delegate/connected DIDs
423
+ * for wallet-connect flows).
424
+ *
425
+ * @internal
426
+ */
427
+ export async function finalizeSession(params: {
428
+ userAgent: EnboxUserAgent;
429
+ emitter: AuthEventEmitter;
430
+ storage: StorageAdapter;
431
+ connectedDid: string;
432
+ delegateDid?: string;
433
+ recoveryPhrase?: string;
434
+ identityName?: string;
435
+ identityConnectedDid?: string;
436
+ emitIdentityAdded?: boolean;
437
+ extraStorageKeys?: Record<string, string>;
438
+ }): Promise<AuthSession> {
439
+ const {
440
+ userAgent,
441
+ emitter,
442
+ storage,
443
+ connectedDid,
444
+ delegateDid,
445
+ recoveryPhrase,
446
+ identityName,
447
+ identityConnectedDid,
448
+ emitIdentityAdded = true,
449
+ extraStorageKeys,
450
+ } = params;
451
+
452
+ // Persist session markers.
453
+ await storage.set(STORAGE_KEYS.PREVIOUSLY_CONNECTED, 'true');
454
+ await storage.set(STORAGE_KEYS.ACTIVE_IDENTITY, connectedDid);
455
+
456
+ if (extraStorageKeys) {
457
+ for (const [key, value] of Object.entries(extraStorageKeys)) {
458
+ await storage.set(key, value);
459
+ }
460
+ }
461
+
462
+ // When identityName is undefined, no user identity exists (agent-only session).
463
+ // Build an IdentityInfo with the agent DID as a fallback.
464
+ const identityInfo: IdentityInfo = {
465
+ didUri : connectedDid,
466
+ name : identityName ?? 'Agent',
467
+ connectedDid : identityConnectedDid,
468
+ };
469
+
470
+ const session = new AuthSession({
471
+ agent : userAgent,
472
+ did : connectedDid,
473
+ delegateDid,
474
+ recoveryPhrase,
475
+ identity : identityInfo,
476
+ });
477
+
478
+ if (emitIdentityAdded && identityName !== undefined) {
479
+ emitter.emit('identity-added', { identity: identityInfo });
480
+ }
481
+
482
+ emitter.emit('session-start', {
483
+ session: { did: connectedDid, delegateDid, identity: identityInfo },
484
+ });
485
+
486
+ return session;
487
+ }