@enbox/auth 0.3.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.
- package/dist/esm/auth-manager.js +496 -0
- package/dist/esm/auth-manager.js.map +1 -0
- package/dist/esm/events.js +65 -0
- package/dist/esm/events.js.map +1 -0
- package/dist/esm/flows/dwn-discovery.js +281 -0
- package/dist/esm/flows/dwn-discovery.js.map +1 -0
- package/dist/esm/flows/dwn-registration.js +122 -0
- package/dist/esm/flows/dwn-registration.js.map +1 -0
- package/dist/esm/flows/import-identity.js +175 -0
- package/dist/esm/flows/import-identity.js.map +1 -0
- package/dist/esm/flows/local-connect.js +141 -0
- package/dist/esm/flows/local-connect.js.map +1 -0
- package/dist/esm/flows/session-restore.js +109 -0
- package/dist/esm/flows/session-restore.js.map +1 -0
- package/dist/esm/flows/wallet-connect.js +199 -0
- package/dist/esm/flows/wallet-connect.js.map +1 -0
- package/dist/esm/identity-session.js +33 -0
- package/dist/esm/identity-session.js.map +1 -0
- package/dist/esm/index.js +50 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/storage/storage.js +152 -0
- package/dist/esm/storage/storage.js.map +1 -0
- package/dist/esm/types.js +30 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/vault/vault-manager.js +95 -0
- package/dist/esm/vault/vault-manager.js.map +1 -0
- package/dist/types/auth-manager.d.ts +176 -0
- package/dist/types/auth-manager.d.ts.map +1 -0
- package/dist/types/events.d.ts +36 -0
- package/dist/types/events.d.ts.map +1 -0
- package/dist/types/flows/dwn-discovery.d.ts +157 -0
- package/dist/types/flows/dwn-discovery.d.ts.map +1 -0
- package/dist/types/flows/dwn-registration.d.ts +39 -0
- package/dist/types/flows/dwn-registration.d.ts.map +1 -0
- package/dist/types/flows/import-identity.d.ts +35 -0
- package/dist/types/flows/import-identity.d.ts.map +1 -0
- package/dist/types/flows/local-connect.d.ts +29 -0
- package/dist/types/flows/local-connect.d.ts.map +1 -0
- package/dist/types/flows/session-restore.d.ts +27 -0
- package/dist/types/flows/session-restore.d.ts.map +1 -0
- package/dist/types/flows/wallet-connect.d.ts +44 -0
- package/dist/types/flows/wallet-connect.d.ts.map +1 -0
- package/dist/types/identity-session.d.ts +52 -0
- package/dist/types/identity-session.d.ts.map +1 -0
- package/dist/types/index.d.ts +45 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/storage/storage.d.ts +54 -0
- package/dist/types/storage/storage.d.ts.map +1 -0
- package/dist/types/types.d.ts +312 -0
- package/dist/types/types.d.ts.map +1 -0
- package/dist/types/vault/vault-manager.d.ts +57 -0
- package/dist/types/vault/vault-manager.d.ts.map +1 -0
- package/package.json +71 -0
- package/src/auth-manager.ts +569 -0
- package/src/events.ts +66 -0
- package/src/flows/dwn-discovery.ts +300 -0
- package/src/flows/dwn-registration.ts +157 -0
- package/src/flows/import-identity.ts +217 -0
- package/src/flows/local-connect.ts +171 -0
- package/src/flows/session-restore.ts +135 -0
- package/src/flows/wallet-connect.ts +225 -0
- package/src/identity-session.ts +65 -0
- package/src/index.ts +89 -0
- package/src/storage/storage.ts +136 -0
- package/src/types.ts +388 -0
- package/src/vault/vault-manager.ts +89 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AuthManager — the primary entry point for `@enbox/auth`.
|
|
3
|
+
*
|
|
4
|
+
* Replaces `Enbox.connect()` (formerly `Web5.connect()`) with a composable,
|
|
5
|
+
* multi-identity-aware auth system that works in both browser and CLI environments.
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { EnboxUserAgent } from '@enbox/agent';
|
|
10
|
+
import type { BearerIdentity, PortableIdentity } from '@enbox/agent';
|
|
11
|
+
|
|
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';
|
|
20
|
+
import type {
|
|
21
|
+
AuthEvent,
|
|
22
|
+
AuthEventHandler,
|
|
23
|
+
AuthManagerOptions,
|
|
24
|
+
AuthState,
|
|
25
|
+
DisconnectOptions,
|
|
26
|
+
IdentityInfo,
|
|
27
|
+
ImportFromPhraseOptions,
|
|
28
|
+
ImportFromPortableOptions,
|
|
29
|
+
LocalConnectOptions,
|
|
30
|
+
RegistrationOptions,
|
|
31
|
+
RestoreSessionOptions,
|
|
32
|
+
StorageAdapter,
|
|
33
|
+
SyncOption,
|
|
34
|
+
WalletConnectOptions,
|
|
35
|
+
} from './types.js';
|
|
36
|
+
import { importFromPhrase, importFromPortable } from './flows/import-identity.js';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The primary entry point for authentication and identity management.
|
|
40
|
+
*
|
|
41
|
+
* `AuthManager` replaces `Enbox.connect()` (formerly `Web5.connect()`) with a composable, multi-identity-aware
|
|
42
|
+
* system. It manages vault lifecycle, identity CRUD, session persistence,
|
|
43
|
+
* and all connection flows (local DID, wallet connect, import, restore).
|
|
44
|
+
*
|
|
45
|
+
* @example Basic usage
|
|
46
|
+
* ```ts
|
|
47
|
+
* import { AuthManager } from '@enbox/auth';
|
|
48
|
+
*
|
|
49
|
+
* const auth = await AuthManager.create({ sync: '15s' });
|
|
50
|
+
*
|
|
51
|
+
* // First time: creates a new identity
|
|
52
|
+
* // Subsequent times: restores the previous session
|
|
53
|
+
* const session = await auth.restoreSession() ?? await auth.connect();
|
|
54
|
+
*
|
|
55
|
+
* // session.agent — the authenticated Enbox agent
|
|
56
|
+
* // session.did — the connected DID URI
|
|
57
|
+
* // session.identity — metadata about the connected identity
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* @example Wallet connect
|
|
61
|
+
* ```ts
|
|
62
|
+
* const session = await auth.walletConnect({
|
|
63
|
+
* displayName: 'My App',
|
|
64
|
+
* connectServerUrl: 'https://enbox-dwn.fly.dev/connect',
|
|
65
|
+
* permissionRequests: [{ protocolDefinition: MyProtocol.definition }],
|
|
66
|
+
* onWalletUriReady: (uri) => showQRCode(uri),
|
|
67
|
+
* validatePin: () => promptUserForPin(),
|
|
68
|
+
* });
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export class AuthManager {
|
|
72
|
+
private _userAgent: EnboxUserAgent;
|
|
73
|
+
private _emitter: AuthEventEmitter;
|
|
74
|
+
private _storage: StorageAdapter;
|
|
75
|
+
private _vault: VaultManager;
|
|
76
|
+
private _session: AuthSession | undefined;
|
|
77
|
+
private _state: AuthState = 'uninitialized';
|
|
78
|
+
private _isConnecting = false;
|
|
79
|
+
|
|
80
|
+
// Default options from create()
|
|
81
|
+
private _defaultPassword?: string;
|
|
82
|
+
private _defaultSync?: SyncOption;
|
|
83
|
+
private _defaultDwnEndpoints?: string[];
|
|
84
|
+
private _registration?: RegistrationOptions;
|
|
85
|
+
|
|
86
|
+
private constructor(params: {
|
|
87
|
+
userAgent: EnboxUserAgent;
|
|
88
|
+
emitter: AuthEventEmitter;
|
|
89
|
+
storage: StorageAdapter;
|
|
90
|
+
vault: VaultManager;
|
|
91
|
+
defaultPassword?: string;
|
|
92
|
+
defaultSync?: SyncOption;
|
|
93
|
+
defaultDwnEndpoints?: string[];
|
|
94
|
+
registration?: RegistrationOptions;
|
|
95
|
+
}) {
|
|
96
|
+
this._userAgent = params.userAgent;
|
|
97
|
+
this._emitter = params.emitter;
|
|
98
|
+
this._storage = params.storage;
|
|
99
|
+
this._vault = params.vault;
|
|
100
|
+
this._defaultPassword = params.defaultPassword;
|
|
101
|
+
this._defaultSync = params.defaultSync;
|
|
102
|
+
this._defaultDwnEndpoints = params.defaultDwnEndpoints;
|
|
103
|
+
this._registration = params.registration;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Create a new AuthManager instance.
|
|
108
|
+
*
|
|
109
|
+
* When a pre-built `agent` is provided, it is used as-is and
|
|
110
|
+
* `dataPath`, `agentVault`, and `localDwnStrategy` are ignored.
|
|
111
|
+
* Otherwise, a new `EnboxUserAgent` is created with the given options.
|
|
112
|
+
*
|
|
113
|
+
* @param options - Configuration options for the auth manager.
|
|
114
|
+
* @returns A ready-to-use AuthManager instance.
|
|
115
|
+
*/
|
|
116
|
+
static async create(options: AuthManagerOptions = {}): Promise<AuthManager> {
|
|
117
|
+
const emitter = new AuthEventEmitter();
|
|
118
|
+
const storage = options.storage ?? createDefaultStorage();
|
|
119
|
+
|
|
120
|
+
// Use a pre-built agent or create one with the given options.
|
|
121
|
+
const userAgent = options.agent ?? await EnboxUserAgent.create({
|
|
122
|
+
dataPath : options.dataPath,
|
|
123
|
+
agentVault : options.agentVault,
|
|
124
|
+
localDwnStrategy : options.localDwnStrategy,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const vault = new VaultManager(userAgent.vault, emitter);
|
|
128
|
+
|
|
129
|
+
const manager = new AuthManager({
|
|
130
|
+
userAgent,
|
|
131
|
+
emitter,
|
|
132
|
+
storage,
|
|
133
|
+
vault,
|
|
134
|
+
defaultPassword : options.password,
|
|
135
|
+
defaultSync : options.sync,
|
|
136
|
+
defaultDwnEndpoints : options.dwnEndpoints,
|
|
137
|
+
registration : options.registration,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Determine initial state.
|
|
141
|
+
if (await vault.isInitialized()) {
|
|
142
|
+
manager._setState(vault.isLocked ? 'locked' : 'unlocked');
|
|
143
|
+
} else {
|
|
144
|
+
manager._setState('uninitialized');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return manager;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ─── Connection flows ──────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Create or reconnect a local identity.
|
|
154
|
+
*
|
|
155
|
+
* On first use, this creates a new vault, agent DID, and user identity.
|
|
156
|
+
* On subsequent uses, it unlocks the vault and reconnects.
|
|
157
|
+
*
|
|
158
|
+
* @param options - Optional overrides for password, sync, DWN endpoints.
|
|
159
|
+
* @returns An active AuthSession.
|
|
160
|
+
* @throws If a connection attempt is already in progress.
|
|
161
|
+
*/
|
|
162
|
+
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
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Connect to an external wallet via the OIDC/QR relay protocol.
|
|
190
|
+
*
|
|
191
|
+
* This runs the full WalletConnect flow: generates a URI for QR display,
|
|
192
|
+
* validates the PIN, imports the delegated DID, and processes grants.
|
|
193
|
+
*
|
|
194
|
+
* @param options - Wallet connect configuration.
|
|
195
|
+
* @returns An active AuthSession with delegated permissions.
|
|
196
|
+
* @throws If sync is disabled (required for wallet connect).
|
|
197
|
+
* @throws If a connection attempt is already in progress.
|
|
198
|
+
*/
|
|
199
|
+
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
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Import an identity from a BIP-39 recovery phrase.
|
|
234
|
+
*
|
|
235
|
+
* This re-derives the vault and agent DID from the mnemonic,
|
|
236
|
+
* recovering the identity on this device.
|
|
237
|
+
*/
|
|
238
|
+
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
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Import an identity from a PortableIdentity JSON object.
|
|
265
|
+
*
|
|
266
|
+
* The portable identity contains the DID's private keys and metadata.
|
|
267
|
+
*/
|
|
268
|
+
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
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Restore a previous session from persisted storage.
|
|
295
|
+
*
|
|
296
|
+
* Returns `undefined` if no previous session exists.
|
|
297
|
+
* This replaces the manual `previouslyConnected` localStorage pattern.
|
|
298
|
+
*/
|
|
299
|
+
async restoreSession(options?: RestoreSessionOptions): Promise<AuthSession | undefined> {
|
|
300
|
+
this._guardConcurrency();
|
|
301
|
+
this._isConnecting = true;
|
|
302
|
+
|
|
303
|
+
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
|
+
);
|
|
314
|
+
|
|
315
|
+
if (session) {
|
|
316
|
+
this._session = session;
|
|
317
|
+
this._setState('connected');
|
|
318
|
+
}
|
|
319
|
+
return session;
|
|
320
|
+
} finally {
|
|
321
|
+
this._isConnecting = false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ─── Session management ────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
/** The current active session, or `undefined` if not connected. */
|
|
328
|
+
get session(): AuthSession | undefined {
|
|
329
|
+
return this._session;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Disconnect the current session.
|
|
334
|
+
*
|
|
335
|
+
* @param options - Disconnect options.
|
|
336
|
+
* @param options.clearStorage - If `true`, performs a nuclear wipe:
|
|
337
|
+
* clears all persisted data (localStorage + IndexedDB). Default: `false`.
|
|
338
|
+
* @param options.timeout - Milliseconds to wait for sync to complete.
|
|
339
|
+
*/
|
|
340
|
+
async disconnect(options: DisconnectOptions = {}): Promise<void> {
|
|
341
|
+
const { clearStorage = false, timeout = 2000 } = options;
|
|
342
|
+
const did = this._session?.did;
|
|
343
|
+
|
|
344
|
+
// Stop sync.
|
|
345
|
+
if (this._session) {
|
|
346
|
+
if ('sync' in this._userAgent && typeof (this._userAgent as any).sync?.stopSync === 'function') {
|
|
347
|
+
await (this._userAgent as any).sync.stopSync(timeout);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
this._session = undefined;
|
|
352
|
+
|
|
353
|
+
if (clearStorage) {
|
|
354
|
+
// Nuclear wipe: clear all persisted auth data.
|
|
355
|
+
await this._storage.clear();
|
|
356
|
+
|
|
357
|
+
// Also clear non-prefixed localStorage and IndexedDB (browser).
|
|
358
|
+
if (typeof globalThis.localStorage !== 'undefined') {
|
|
359
|
+
globalThis.localStorage.clear();
|
|
360
|
+
}
|
|
361
|
+
if (typeof globalThis.indexedDB !== 'undefined') {
|
|
362
|
+
try {
|
|
363
|
+
const databases = await globalThis.indexedDB.databases();
|
|
364
|
+
for (const db of databases) {
|
|
365
|
+
if (db.name) {
|
|
366
|
+
globalThis.indexedDB.deleteDatabase(db.name);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
} catch {
|
|
370
|
+
// indexedDB.databases() not available in all browsers.
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
} else {
|
|
374
|
+
// Clean disconnect: remove session markers but keep vault/identities.
|
|
375
|
+
await this._storage.remove(STORAGE_KEYS.PREVIOUSLY_CONNECTED);
|
|
376
|
+
await this._storage.remove(STORAGE_KEYS.ACTIVE_IDENTITY);
|
|
377
|
+
await this._storage.remove(STORAGE_KEYS.DELEGATE_DID);
|
|
378
|
+
await this._storage.remove(STORAGE_KEYS.CONNECTED_DID);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
this._setState('unlocked');
|
|
382
|
+
|
|
383
|
+
if (did) {
|
|
384
|
+
this._emitter.emit('session-end', { did });
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ─── Multi-identity ────────────────────────────────────────────
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* List all stored identities.
|
|
392
|
+
*
|
|
393
|
+
* Each identity has a DID URI, name, and optional connected DID
|
|
394
|
+
* (for wallet-connected/delegated identities).
|
|
395
|
+
*/
|
|
396
|
+
async listIdentities(): Promise<IdentityInfo[]> {
|
|
397
|
+
const identities = await this._userAgent.identity.list();
|
|
398
|
+
return identities.map((identity: BearerIdentity) => ({
|
|
399
|
+
didUri : identity.did.uri,
|
|
400
|
+
name : identity.metadata.name,
|
|
401
|
+
connectedDid : identity.metadata.connectedDid,
|
|
402
|
+
}));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Switch the active identity.
|
|
407
|
+
*
|
|
408
|
+
* Disconnects the current session (if any) and creates a new session
|
|
409
|
+
* for the specified identity.
|
|
410
|
+
*/
|
|
411
|
+
async switchIdentity(didUri: string): Promise<AuthSession> {
|
|
412
|
+
// Disconnect current session cleanly (keep data).
|
|
413
|
+
if (this._session) {
|
|
414
|
+
await this.disconnect();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const identity = await this._userAgent.identity.get({ didUri });
|
|
418
|
+
if (!identity) {
|
|
419
|
+
throw new Error(`[@enbox/auth] Identity not found: ${didUri}`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const connectedDid = identity.metadata.connectedDid ?? identity.did.uri;
|
|
423
|
+
const delegateDid = identity.metadata.connectedDid ? identity.did.uri : undefined;
|
|
424
|
+
|
|
425
|
+
// Persist the switch.
|
|
426
|
+
await this._storage.set(STORAGE_KEYS.PREVIOUSLY_CONNECTED, 'true');
|
|
427
|
+
await this._storage.set(STORAGE_KEYS.ACTIVE_IDENTITY, connectedDid);
|
|
428
|
+
|
|
429
|
+
const identityInfo: IdentityInfo = {
|
|
430
|
+
didUri : connectedDid,
|
|
431
|
+
name : identity.metadata.name,
|
|
432
|
+
connectedDid : identity.metadata.connectedDid,
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
// Restart sync.
|
|
436
|
+
const sync = this._defaultSync;
|
|
437
|
+
if (sync !== 'off') {
|
|
438
|
+
const syncMode = sync === undefined ? 'live' : 'poll';
|
|
439
|
+
const syncInterval = sync ?? (syncMode === 'live' ? '5m' : '2m');
|
|
440
|
+
this._userAgent.sync.startSync({ mode: syncMode, interval: syncInterval })
|
|
441
|
+
.catch((err: unknown) => console.error('[@enbox/auth] Sync failed:', err));
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
this._session = new AuthSession({
|
|
445
|
+
agent : this._userAgent,
|
|
446
|
+
did : connectedDid,
|
|
447
|
+
delegateDid,
|
|
448
|
+
identity : identityInfo,
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
this._setState('connected');
|
|
452
|
+
|
|
453
|
+
this._emitter.emit('session-start', {
|
|
454
|
+
session: { did: connectedDid, delegateDid, identity: identityInfo },
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
return this._session;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Delete a stored identity.
|
|
462
|
+
*
|
|
463
|
+
* If the identity is currently active, it will be disconnected first.
|
|
464
|
+
*
|
|
465
|
+
* @throws If the identity is not found.
|
|
466
|
+
*/
|
|
467
|
+
async deleteIdentity(didUri: string): Promise<void> {
|
|
468
|
+
// Disconnect if this is the active identity.
|
|
469
|
+
if (this._session?.did === didUri) {
|
|
470
|
+
await this.disconnect();
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const identity = await this._userAgent.identity.get({ didUri });
|
|
474
|
+
if (!identity) {
|
|
475
|
+
throw new Error(`[@enbox/auth] Identity not found: ${didUri}`);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Delete the DID and keys.
|
|
479
|
+
try {
|
|
480
|
+
await this._userAgent.did.delete({
|
|
481
|
+
didUri : identity.did.uri,
|
|
482
|
+
tenant : identity.metadata.tenant,
|
|
483
|
+
deleteKey : true,
|
|
484
|
+
});
|
|
485
|
+
} catch (err: unknown) {
|
|
486
|
+
console.error(`[@enbox/auth] Failed to delete DID ${didUri}:`, err);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Delete the identity record.
|
|
490
|
+
await this._userAgent.identity.delete({ didUri });
|
|
491
|
+
|
|
492
|
+
this._emitter.emit('identity-removed', { didUri });
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Export an identity as a PortableIdentity JSON object.
|
|
497
|
+
*
|
|
498
|
+
* This can be used for backup or transferring the identity
|
|
499
|
+
* to another device.
|
|
500
|
+
*/
|
|
501
|
+
async exportIdentity(didUri: string): Promise<PortableIdentity> {
|
|
502
|
+
return this._userAgent.identity.export({ didUri });
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// ─── Vault ─────────────────────────────────────────────────────
|
|
506
|
+
|
|
507
|
+
/** Access the vault manager for lock/unlock/backup operations. */
|
|
508
|
+
get vault(): VaultManager {
|
|
509
|
+
return this._vault;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// ─── Events ────────────────────────────────────────────────────
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Subscribe to an auth lifecycle event.
|
|
516
|
+
*
|
|
517
|
+
* @param event - The event name.
|
|
518
|
+
* @param handler - The event handler.
|
|
519
|
+
* @returns An unsubscribe function.
|
|
520
|
+
*/
|
|
521
|
+
on<E extends AuthEvent>(event: E, handler: AuthEventHandler<E>): () => void {
|
|
522
|
+
return this._emitter.on(event, handler);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// ─── State ─────────────────────────────────────────────────────
|
|
526
|
+
|
|
527
|
+
/** The current auth state. */
|
|
528
|
+
get state(): AuthState {
|
|
529
|
+
return this._state;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Whether an active session exists. */
|
|
533
|
+
get isConnected(): boolean {
|
|
534
|
+
return this._state === 'connected';
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** Whether the vault is currently locked. */
|
|
538
|
+
get isLocked(): boolean {
|
|
539
|
+
return this._vault.isLocked;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/** Whether a connection attempt is in progress. */
|
|
543
|
+
get isConnecting(): boolean {
|
|
544
|
+
return this._isConnecting;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** The underlying EnboxUserAgent (for advanced usage). */
|
|
548
|
+
get agent(): EnboxUserAgent {
|
|
549
|
+
return this._userAgent;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ─── Private helpers ───────────────────────────────────────────
|
|
553
|
+
|
|
554
|
+
private _setState(state: AuthState): void {
|
|
555
|
+
if (state === this._state) {return;}
|
|
556
|
+
const previous = this._state;
|
|
557
|
+
this._state = state;
|
|
558
|
+
this._emitter.emit('state-change', { previous, current: state });
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private _guardConcurrency(): void {
|
|
562
|
+
if (this._isConnecting) {
|
|
563
|
+
throw new Error(
|
|
564
|
+
'[@enbox/auth] A connection attempt is already in progress. ' +
|
|
565
|
+
'Wait for it to complete before starting another.'
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed event emitter for auth state changes.
|
|
3
|
+
* @module
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AuthEvent, AuthEventHandler, AuthEventMap } from './types.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A minimal, type-safe event emitter for auth lifecycle events.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* ```ts
|
|
13
|
+
* const emitter = new AuthEventEmitter();
|
|
14
|
+
* const unsub = emitter.on('state-change', ({ previous, current }) => {
|
|
15
|
+
* console.log(`State: ${previous} → ${current}`);
|
|
16
|
+
* });
|
|
17
|
+
* emitter.emit('state-change', { previous: 'locked', current: 'unlocked' });
|
|
18
|
+
* unsub(); // stop listening
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export class AuthEventEmitter {
|
|
22
|
+
private _listeners = new Map<AuthEvent, Set<AuthEventHandler<AuthEvent>>>();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Subscribe to an event. Returns an unsubscribe function.
|
|
26
|
+
*/
|
|
27
|
+
on<E extends AuthEvent>(event: E, handler: AuthEventHandler<E>): () => void {
|
|
28
|
+
let set = this._listeners.get(event);
|
|
29
|
+
if (!set) {
|
|
30
|
+
set = new Set();
|
|
31
|
+
this._listeners.set(event, set);
|
|
32
|
+
}
|
|
33
|
+
set.add(handler as AuthEventHandler<AuthEvent>);
|
|
34
|
+
|
|
35
|
+
return () => {
|
|
36
|
+
set!.delete(handler as AuthEventHandler<AuthEvent>);
|
|
37
|
+
if (set!.size === 0) {
|
|
38
|
+
this._listeners.delete(event);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Emit an event to all registered listeners.
|
|
45
|
+
* @internal
|
|
46
|
+
*/
|
|
47
|
+
emit<E extends AuthEvent>(event: E, payload: AuthEventMap[E]): void {
|
|
48
|
+
const set = this._listeners.get(event);
|
|
49
|
+
if (!set) { return; }
|
|
50
|
+
for (const handler of set) {
|
|
51
|
+
try {
|
|
52
|
+
handler(payload);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
console.error(`[AuthEventEmitter] Error in '${event}' handler:`, err);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Remove all listeners for all events.
|
|
61
|
+
* @internal
|
|
62
|
+
*/
|
|
63
|
+
removeAllListeners(): void {
|
|
64
|
+
this._listeners.clear();
|
|
65
|
+
}
|
|
66
|
+
}
|