@enbox/auth 0.3.1 → 0.5.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.
- package/dist/esm/auth-manager.js +245 -5
- package/dist/esm/auth-manager.js.map +1 -1
- package/dist/esm/flows/dwn-discovery.js +96 -81
- package/dist/esm/flows/dwn-discovery.js.map +1 -1
- package/dist/esm/flows/dwn-registration.js +49 -3
- package/dist/esm/flows/dwn-registration.js.map +1 -1
- package/dist/esm/flows/import-identity.js +2 -0
- package/dist/esm/flows/import-identity.js.map +1 -1
- package/dist/esm/flows/local-connect.js +25 -8
- package/dist/esm/flows/local-connect.js.map +1 -1
- package/dist/esm/flows/session-restore.js +20 -4
- package/dist/esm/flows/session-restore.js.map +1 -1
- package/dist/esm/flows/wallet-connect.js +5 -4
- package/dist/esm/flows/wallet-connect.js.map +1 -1
- package/dist/esm/index.js +5 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/password-provider.js +319 -0
- package/dist/esm/password-provider.js.map +1 -0
- package/dist/esm/types.js +9 -1
- package/dist/esm/types.js.map +1 -1
- package/dist/types/auth-manager.d.ts +83 -2
- package/dist/types/auth-manager.d.ts.map +1 -1
- package/dist/types/flows/dwn-discovery.d.ts +40 -53
- package/dist/types/flows/dwn-discovery.d.ts.map +1 -1
- package/dist/types/flows/dwn-registration.d.ts +20 -1
- package/dist/types/flows/dwn-registration.d.ts.map +1 -1
- package/dist/types/flows/import-identity.d.ts.map +1 -1
- package/dist/types/flows/local-connect.d.ts +2 -0
- package/dist/types/flows/local-connect.d.ts.map +1 -1
- package/dist/types/flows/session-restore.d.ts +2 -0
- package/dist/types/flows/session-restore.d.ts.map +1 -1
- package/dist/types/flows/wallet-connect.d.ts +2 -2
- package/dist/types/flows/wallet-connect.d.ts.map +1 -1
- package/dist/types/index.d.ts +5 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/password-provider.d.ts +194 -0
- package/dist/types/password-provider.d.ts.map +1 -0
- package/dist/types/types.d.ts +106 -1
- package/dist/types/types.d.ts.map +1 -1
- package/package.json +8 -9
- package/src/auth-manager.ts +284 -9
- package/src/flows/dwn-discovery.ts +99 -79
- package/src/flows/dwn-registration.ts +60 -5
- package/src/flows/import-identity.ts +2 -0
- package/src/flows/local-connect.ts +24 -3
- package/src/flows/session-restore.ts +22 -2
- package/src/flows/wallet-connect.ts +5 -4
- package/src/index.ts +10 -1
- package/src/password-provider.ts +383 -0
- package/src/types.ts +114 -1
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PasswordProvider — composable password acquisition strategies.
|
|
3
|
+
*
|
|
4
|
+
* Replaces ad-hoc password prompting scattered across CLI consumers
|
|
5
|
+
* (env vars, raw-mode TTY, `/dev/tty` + `stty`, `@clack/prompts`, etc.)
|
|
6
|
+
* with a single, composable abstraction.
|
|
7
|
+
*
|
|
8
|
+
* @example Chained provider (env first, fall back to TTY)
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { PasswordProvider } from '@enbox/auth';
|
|
11
|
+
*
|
|
12
|
+
* const provider = PasswordProvider.chain([
|
|
13
|
+
* PasswordProvider.fromEnv('ENBOX_PASSWORD'),
|
|
14
|
+
* PasswordProvider.fromTty({ prompt: 'Vault password: ' }),
|
|
15
|
+
* ]);
|
|
16
|
+
*
|
|
17
|
+
* const auth = await AuthManager.create({ passwordProvider: provider });
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
// ─── Types ───────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/** Context passed to a password provider explaining why a password is needed. */
|
|
26
|
+
export interface PasswordContext {
|
|
27
|
+
/**
|
|
28
|
+
* Why the password is being requested.
|
|
29
|
+
*
|
|
30
|
+
* - `'create'` — first launch, creating a new vault (prompt may ask
|
|
31
|
+
* for confirmation).
|
|
32
|
+
* - `'unlock'` — unlocking an existing vault.
|
|
33
|
+
*/
|
|
34
|
+
reason: 'create' | 'unlock';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A strategy for obtaining a vault password.
|
|
39
|
+
*
|
|
40
|
+
* Implementations may be interactive (TTY prompts) or non-interactive
|
|
41
|
+
* (environment variables, cached values). Use {@link PasswordProvider.chain}
|
|
42
|
+
* to compose multiple strategies with automatic fallback.
|
|
43
|
+
*/
|
|
44
|
+
export interface PasswordProvider {
|
|
45
|
+
/**
|
|
46
|
+
* Obtain a password.
|
|
47
|
+
*
|
|
48
|
+
* @param context - Why the password is needed.
|
|
49
|
+
* @returns The password string.
|
|
50
|
+
* @throws If the provider cannot obtain a password (e.g. env var
|
|
51
|
+
* not set, no TTY available). The error is caught by `chain()`
|
|
52
|
+
* which falls through to the next provider.
|
|
53
|
+
*/
|
|
54
|
+
getPassword(context: PasswordContext): Promise<string>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── Internal I/O interfaces (for testing) ───────────────────────
|
|
58
|
+
|
|
59
|
+
/** @internal Minimal interface for an stdin-like readable stream. */
|
|
60
|
+
export interface TtyReadable {
|
|
61
|
+
isTTY?: boolean;
|
|
62
|
+
setRawMode(mode: boolean): void;
|
|
63
|
+
setEncoding(encoding: string): void;
|
|
64
|
+
resume(): void;
|
|
65
|
+
pause(): void;
|
|
66
|
+
on(event: 'data', listener: (chunk: string) => void): void;
|
|
67
|
+
removeListener(event: 'data', listener: (chunk: string) => void): void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @internal Minimal interface for an stdout-like writable stream. */
|
|
71
|
+
export interface TtyWritable {
|
|
72
|
+
write(data: string): boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── Internal helpers ────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Read a password from a raw-mode TTY stream.
|
|
79
|
+
*
|
|
80
|
+
* Reads character-by-character with no echo. Handles Enter (resolve),
|
|
81
|
+
* Ctrl-C (reject), backspace, and printable characters.
|
|
82
|
+
*
|
|
83
|
+
* @internal Exported for testing only.
|
|
84
|
+
*/
|
|
85
|
+
export function readPasswordRawMode(
|
|
86
|
+
stdin: TtyReadable,
|
|
87
|
+
stdout: TtyWritable,
|
|
88
|
+
prompt: string,
|
|
89
|
+
): Promise<string> {
|
|
90
|
+
stdout.write(prompt);
|
|
91
|
+
|
|
92
|
+
return new Promise<string>((resolve, reject) => {
|
|
93
|
+
let buf = '';
|
|
94
|
+
stdin.setRawMode(true);
|
|
95
|
+
stdin.setEncoding('utf8');
|
|
96
|
+
stdin.resume();
|
|
97
|
+
|
|
98
|
+
const onData = (ch: string): void => {
|
|
99
|
+
const code = ch.charCodeAt(0);
|
|
100
|
+
|
|
101
|
+
if (ch === '\r' || ch === '\n') {
|
|
102
|
+
// Enter — done.
|
|
103
|
+
stdin.setRawMode(false);
|
|
104
|
+
stdin.pause();
|
|
105
|
+
stdin.removeListener('data', onData);
|
|
106
|
+
stdout.write('\n');
|
|
107
|
+
resolve(buf);
|
|
108
|
+
} else if (code === 3) {
|
|
109
|
+
// Ctrl-C — abort.
|
|
110
|
+
stdin.setRawMode(false);
|
|
111
|
+
stdin.pause();
|
|
112
|
+
stdin.removeListener('data', onData);
|
|
113
|
+
stdout.write('\n');
|
|
114
|
+
reject(new Error('[@enbox/auth] PasswordProvider.fromTty: cancelled by user.'));
|
|
115
|
+
} else if (code === 127 || code === 8) {
|
|
116
|
+
// Backspace / Delete.
|
|
117
|
+
if (buf.length > 0) {
|
|
118
|
+
buf = buf.slice(0, -1);
|
|
119
|
+
}
|
|
120
|
+
} else if (code >= 32) {
|
|
121
|
+
// Printable character.
|
|
122
|
+
buf += ch;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
stdin.on('data', onData);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** @internal Injectable I/O for testing `readPasswordDevTty`. */
|
|
131
|
+
export interface DevTtyIo {
|
|
132
|
+
openSync(path: string, flags: string): number;
|
|
133
|
+
readSync(fd: number, buf: Uint8Array, offset: number, length: number, position: null): number;
|
|
134
|
+
writeSync(fd: number, data: string): number;
|
|
135
|
+
closeSync(fd: number): void;
|
|
136
|
+
execSync(cmd: string, opts: { stdio: string }): void;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Read a password from `/dev/tty` using synchronous I/O.
|
|
141
|
+
*
|
|
142
|
+
* Opens `/dev/tty` directly, uses `stty -echo` to suppress input,
|
|
143
|
+
* reads until newline, then restores echo and closes file descriptors.
|
|
144
|
+
*
|
|
145
|
+
* @param prompt - The prompt string to display.
|
|
146
|
+
* @param io - Injectable I/O functions (defaults to `node:fs` + `node:child_process`).
|
|
147
|
+
* @internal Exported for testing only.
|
|
148
|
+
*/
|
|
149
|
+
export async function readPasswordDevTty(
|
|
150
|
+
prompt: string,
|
|
151
|
+
io?: DevTtyIo,
|
|
152
|
+
): Promise<string> {
|
|
153
|
+
// Use injected I/O or import real modules.
|
|
154
|
+
let fsIo: DevTtyIo;
|
|
155
|
+
if (io) {
|
|
156
|
+
fsIo = io;
|
|
157
|
+
} else {
|
|
158
|
+
const { openSync, readSync, writeSync, closeSync } = await import('node:fs');
|
|
159
|
+
const { execSync } = await import('node:child_process');
|
|
160
|
+
fsIo = {
|
|
161
|
+
openSync,
|
|
162
|
+
readSync,
|
|
163
|
+
writeSync,
|
|
164
|
+
closeSync,
|
|
165
|
+
execSync: (cmd: string, opts: { stdio: string }): void => { execSync(cmd, opts as any); },
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let readFd: number;
|
|
170
|
+
let writeFd: number;
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
readFd = fsIo.openSync('/dev/tty', 'r');
|
|
174
|
+
writeFd = fsIo.openSync('/dev/tty', 'w');
|
|
175
|
+
} catch {
|
|
176
|
+
throw new Error(
|
|
177
|
+
'[@enbox/auth] PasswordProvider.fromDevTty: cannot open /dev/tty. ' +
|
|
178
|
+
'No controlling terminal available.'
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
// Suppress echo.
|
|
184
|
+
try {
|
|
185
|
+
fsIo.execSync('stty -echo < /dev/tty', { stdio: 'ignore' });
|
|
186
|
+
} catch {
|
|
187
|
+
// Continue — the user sees their password but the flow works.
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
fsIo.writeSync(writeFd, prompt);
|
|
191
|
+
|
|
192
|
+
// Cooked-mode read (line-buffered; terminal handles backspace).
|
|
193
|
+
const readBuf = new Uint8Array(256);
|
|
194
|
+
const decoder = new TextDecoder('utf-8');
|
|
195
|
+
let password = '';
|
|
196
|
+
|
|
197
|
+
while (true) {
|
|
198
|
+
const bytesRead = fsIo.readSync(readFd, readBuf, 0, readBuf.length, null);
|
|
199
|
+
if (bytesRead === 0) { break; }
|
|
200
|
+
|
|
201
|
+
password += decoder.decode(readBuf.subarray(0, bytesRead), { stream: true });
|
|
202
|
+
|
|
203
|
+
const nlIdx = password.indexOf('\n');
|
|
204
|
+
if (nlIdx !== -1) { password = password.slice(0, nlIdx); break; }
|
|
205
|
+
|
|
206
|
+
const crIdx = password.indexOf('\r');
|
|
207
|
+
if (crIdx !== -1) { password = password.slice(0, crIdx); break; }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
fsIo.writeSync(writeFd, '\n');
|
|
211
|
+
return password;
|
|
212
|
+
} finally {
|
|
213
|
+
// Restore echo.
|
|
214
|
+
try { fsIo.execSync('stty echo < /dev/tty', { stdio: 'ignore' }); } catch { /* best-effort */ }
|
|
215
|
+
fsIo.closeSync(readFd);
|
|
216
|
+
fsIo.closeSync(writeFd);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ─── Factory functions ───────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
export namespace PasswordProvider {
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Read the password from an environment variable.
|
|
227
|
+
*
|
|
228
|
+
* Throws if the variable is not set or is empty, allowing `chain()`
|
|
229
|
+
* to fall through to the next provider.
|
|
230
|
+
*
|
|
231
|
+
* @param envVar - Name of the environment variable. Default: `'ENBOX_PASSWORD'`.
|
|
232
|
+
*
|
|
233
|
+
* @example
|
|
234
|
+
* ```ts
|
|
235
|
+
* const provider = PasswordProvider.fromEnv('MY_APP_PASSWORD');
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
export function fromEnv(envVar = 'ENBOX_PASSWORD'): PasswordProvider {
|
|
239
|
+
return {
|
|
240
|
+
async getPassword(): Promise<string> {
|
|
241
|
+
const value = process.env[envVar];
|
|
242
|
+
if (!value) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`[@enbox/auth] PasswordProvider.fromEnv: environment variable '${envVar}' is not set.`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
return value;
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Wrap an async callback as a password provider.
|
|
254
|
+
*
|
|
255
|
+
* This is the escape hatch for custom UI (e.g. `@clack/prompts`,
|
|
256
|
+
* Electron dialog, browser modal).
|
|
257
|
+
*
|
|
258
|
+
* @param callback - Called with the password context; must return a password string.
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* ```ts
|
|
262
|
+
* const provider = PasswordProvider.fromCallback(async ({ reason }) => {
|
|
263
|
+
* if (reason === 'create') {
|
|
264
|
+
* return await showCreatePasswordDialog();
|
|
265
|
+
* }
|
|
266
|
+
* return await showUnlockDialog();
|
|
267
|
+
* });
|
|
268
|
+
* ```
|
|
269
|
+
*/
|
|
270
|
+
export function fromCallback(
|
|
271
|
+
callback: (context: PasswordContext) => Promise<string>,
|
|
272
|
+
): PasswordProvider {
|
|
273
|
+
return { getPassword: callback };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Prompt for a password via `process.stdin` in raw mode.
|
|
278
|
+
*
|
|
279
|
+
* Input is read character-by-character with no echo. Handles
|
|
280
|
+
* backspace and Ctrl-C (rejects with an error). Only works when
|
|
281
|
+
* `process.stdin.isTTY` is `true`; throws otherwise so `chain()`
|
|
282
|
+
* can fall through to the next provider.
|
|
283
|
+
*
|
|
284
|
+
* Suitable for main CLI processes that own stdin/stdout.
|
|
285
|
+
*
|
|
286
|
+
* @param options - Optional configuration.
|
|
287
|
+
* @param options.prompt - Text to display before reading. Default: `'Vault password: '`.
|
|
288
|
+
*
|
|
289
|
+
* @example
|
|
290
|
+
* ```ts
|
|
291
|
+
* const provider = PasswordProvider.fromTty({ prompt: 'Password: ' });
|
|
292
|
+
* ```
|
|
293
|
+
*/
|
|
294
|
+
export function fromTty(options: { prompt?: string } = {}): PasswordProvider {
|
|
295
|
+
const prompt = options.prompt ?? 'Vault password: ';
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
async getPassword(): Promise<string> {
|
|
299
|
+
if (!process.stdin.isTTY) {
|
|
300
|
+
throw new Error(
|
|
301
|
+
'[@enbox/auth] PasswordProvider.fromTty: stdin is not a TTY.'
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return readPasswordRawMode(
|
|
306
|
+
process.stdin as unknown as TtyReadable,
|
|
307
|
+
process.stdout,
|
|
308
|
+
prompt,
|
|
309
|
+
);
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Prompt for a password via `/dev/tty` (Unix only).
|
|
316
|
+
*
|
|
317
|
+
* Opens `/dev/tty` directly, bypassing `process.stdin`. This is
|
|
318
|
+
* essential for subprocesses where stdin is owned by the parent
|
|
319
|
+
* (e.g. Git credential helpers, SSH, GPG). Uses `stty -echo` to
|
|
320
|
+
* suppress input echo.
|
|
321
|
+
*
|
|
322
|
+
* Throws if `/dev/tty` cannot be opened (e.g. non-Unix platform,
|
|
323
|
+
* no controlling terminal), allowing `chain()` to fall through.
|
|
324
|
+
*
|
|
325
|
+
* @param options - Optional configuration.
|
|
326
|
+
* @param options.prompt - Text to display before reading. Default: `'Vault password: '`.
|
|
327
|
+
*
|
|
328
|
+
* @example
|
|
329
|
+
* ```ts
|
|
330
|
+
* // For git credential helpers:
|
|
331
|
+
* const provider = PasswordProvider.fromDevTty();
|
|
332
|
+
* ```
|
|
333
|
+
*/
|
|
334
|
+
export function fromDevTty(options: { prompt?: string } = {}): PasswordProvider {
|
|
335
|
+
const prompt = options.prompt ?? 'Vault password: ';
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
async getPassword(): Promise<string> {
|
|
339
|
+
return readPasswordDevTty(prompt);
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Compose multiple providers with automatic fallback.
|
|
346
|
+
*
|
|
347
|
+
* Tries each provider in order. If a provider throws, the next one
|
|
348
|
+
* is tried. If all providers fail, the last error is rethrown.
|
|
349
|
+
*
|
|
350
|
+
* @param providers - Ordered list of providers to try.
|
|
351
|
+
*
|
|
352
|
+
* @example
|
|
353
|
+
* ```ts
|
|
354
|
+
* // Try env var first, then interactive TTY, then /dev/tty for subprocesses.
|
|
355
|
+
* const provider = PasswordProvider.chain([
|
|
356
|
+
* PasswordProvider.fromEnv('ENBOX_PASSWORD'),
|
|
357
|
+
* PasswordProvider.fromTty(),
|
|
358
|
+
* PasswordProvider.fromDevTty(),
|
|
359
|
+
* ]);
|
|
360
|
+
* ```
|
|
361
|
+
*/
|
|
362
|
+
export function chain(providers: PasswordProvider[]): PasswordProvider {
|
|
363
|
+
if (providers.length === 0) {
|
|
364
|
+
throw new Error('[@enbox/auth] PasswordProvider.chain: at least one provider is required.');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
async getPassword(context: PasswordContext): Promise<string> {
|
|
369
|
+
let lastError: Error | undefined;
|
|
370
|
+
|
|
371
|
+
for (const provider of providers) {
|
|
372
|
+
try {
|
|
373
|
+
return await provider.getPassword(context);
|
|
374
|
+
} catch (err) {
|
|
375
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
throw lastError ?? new Error('[@enbox/auth] PasswordProvider.chain: all providers failed.');
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
import type { ConnectPermissionRequest, EnboxUserAgent, HdIdentityVault, LocalDwnStrategy, PortableIdentity } from '@enbox/agent';
|
|
7
7
|
|
|
8
|
+
import type { PasswordProvider } from './password-provider.js';
|
|
9
|
+
|
|
8
10
|
// Re-export types that consumers will need
|
|
9
11
|
export type { ConnectPermissionRequest, HdIdentityVault, IdentityVaultBackup, LocalDwnStrategy, PortableIdentity } from '@enbox/agent';
|
|
10
12
|
|
|
@@ -166,14 +168,46 @@ export interface RegistrationOptions {
|
|
|
166
168
|
* Pre-existing registration tokens from a previous session, keyed by
|
|
167
169
|
* DWN endpoint URL. If a valid (non-expired) token exists for an
|
|
168
170
|
* endpoint, it is used directly without re-running the auth flow.
|
|
171
|
+
*
|
|
172
|
+
* When {@link persistTokens} is `true`, this field is ignored —
|
|
173
|
+
* tokens are loaded automatically from the `StorageAdapter`.
|
|
169
174
|
*/
|
|
170
175
|
registrationTokens?: Record<string, RegistrationTokenData>;
|
|
171
176
|
|
|
172
177
|
/**
|
|
173
178
|
* Called when new or refreshed registration tokens are obtained.
|
|
174
179
|
* The app should persist these for future sessions.
|
|
180
|
+
*
|
|
181
|
+
* When {@link persistTokens} is `true`, tokens are saved automatically
|
|
182
|
+
* to the `StorageAdapter`. This callback is still invoked (if provided)
|
|
183
|
+
* **after** the automatic save, so consumers can observe token changes
|
|
184
|
+
* without handling persistence themselves.
|
|
175
185
|
*/
|
|
176
186
|
onRegistrationTokens?: (tokens: Record<string, RegistrationTokenData>) => void;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Automatically persist and restore registration tokens using the
|
|
190
|
+
* auth manager's `StorageAdapter`.
|
|
191
|
+
*
|
|
192
|
+
* When `true`, tokens are loaded from storage before registration and
|
|
193
|
+
* saved back after new or refreshed tokens are obtained. This removes
|
|
194
|
+
* the need for consumers to implement their own token I/O via
|
|
195
|
+
* {@link registrationTokens} and {@link onRegistrationTokens}.
|
|
196
|
+
*
|
|
197
|
+
* Defaults to `false` for backward compatibility.
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* ```ts
|
|
201
|
+
* const auth = await AuthManager.create({
|
|
202
|
+
* registration: {
|
|
203
|
+
* onSuccess: () => {},
|
|
204
|
+
* onFailure: (err) => console.error(err),
|
|
205
|
+
* persistTokens: true,
|
|
206
|
+
* },
|
|
207
|
+
* });
|
|
208
|
+
* ```
|
|
209
|
+
*/
|
|
210
|
+
persistTokens?: boolean;
|
|
177
211
|
}
|
|
178
212
|
|
|
179
213
|
/** Options for {@link AuthManager.create}. */
|
|
@@ -223,9 +257,34 @@ export interface AuthManagerOptions {
|
|
|
223
257
|
/**
|
|
224
258
|
* Default password for vault operations.
|
|
225
259
|
* If not provided, an insecure default is used (with a console warning).
|
|
260
|
+
*
|
|
261
|
+
* For more flexible password acquisition (env vars, TTY prompts,
|
|
262
|
+
* chained fallbacks), use {@link passwordProvider} instead.
|
|
226
263
|
*/
|
|
227
264
|
password?: string;
|
|
228
265
|
|
|
266
|
+
/**
|
|
267
|
+
* A composable password provider for obtaining the vault password.
|
|
268
|
+
*
|
|
269
|
+
* When set, this provider is consulted by `connect()`,
|
|
270
|
+
* `restoreSession()`, and `connectHeadless()` whenever a password
|
|
271
|
+
* is needed and none was given explicitly. It takes precedence over
|
|
272
|
+
* the static {@link password} option.
|
|
273
|
+
*
|
|
274
|
+
* @example
|
|
275
|
+
* ```ts
|
|
276
|
+
* import { AuthManager, PasswordProvider } from '@enbox/auth';
|
|
277
|
+
*
|
|
278
|
+
* const auth = await AuthManager.create({
|
|
279
|
+
* passwordProvider: PasswordProvider.chain([
|
|
280
|
+
* PasswordProvider.fromEnv('ENBOX_PASSWORD'),
|
|
281
|
+
* PasswordProvider.fromTty(),
|
|
282
|
+
* ]),
|
|
283
|
+
* });
|
|
284
|
+
* ```
|
|
285
|
+
*/
|
|
286
|
+
passwordProvider?: PasswordProvider;
|
|
287
|
+
|
|
229
288
|
/**
|
|
230
289
|
* Sync interval for DWN synchronization.
|
|
231
290
|
* - `'off'` — disable sync
|
|
@@ -317,6 +376,42 @@ export interface ImportFromPortableOptions {
|
|
|
317
376
|
export interface RestoreSessionOptions {
|
|
318
377
|
/** Password to unlock the vault (needed if vault is locked). */
|
|
319
378
|
password?: string;
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Called when the vault is locked and a password is required to proceed.
|
|
382
|
+
*
|
|
383
|
+
* If provided, this callback is invoked instead of falling back to the
|
|
384
|
+
* default password or the insecure static phrase. This is the recommended
|
|
385
|
+
* way to implement interactive password prompts (e.g., a PIN entry dialog
|
|
386
|
+
* or CLI prompt).
|
|
387
|
+
*
|
|
388
|
+
* @returns The password entered by the user.
|
|
389
|
+
*
|
|
390
|
+
* @example Browser PIN dialog
|
|
391
|
+
* ```ts
|
|
392
|
+
* const session = await auth.restoreSession({
|
|
393
|
+
* onPasswordRequired: async () => {
|
|
394
|
+
* return await showPinDialog();
|
|
395
|
+
* },
|
|
396
|
+
* });
|
|
397
|
+
* ```
|
|
398
|
+
*/
|
|
399
|
+
onPasswordRequired?: () => Promise<string>;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** Options for {@link AuthManager.connectHeadless}. */
|
|
403
|
+
export interface HeadlessConnectOptions {
|
|
404
|
+
/** Vault password (overrides manager default). */
|
|
405
|
+
password?: string;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Options for {@link AuthManager.shutdown}. */
|
|
409
|
+
export interface ShutdownOptions {
|
|
410
|
+
/**
|
|
411
|
+
* Milliseconds to wait for pending sync operations before shutting down.
|
|
412
|
+
* Default: `2000`.
|
|
413
|
+
*/
|
|
414
|
+
timeout?: number;
|
|
320
415
|
}
|
|
321
416
|
|
|
322
417
|
/** Options for {@link AuthManager.disconnect}. */
|
|
@@ -353,6 +448,15 @@ export interface StorageAdapter {
|
|
|
353
448
|
|
|
354
449
|
/** Clear all stored data. */
|
|
355
450
|
clear(): Promise<void>;
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Close the underlying storage resources (e.g. LevelDB handles).
|
|
454
|
+
*
|
|
455
|
+
* Optional — not all adapters need cleanup. Called by
|
|
456
|
+
* {@link AuthManager.shutdown} to release resources so the process
|
|
457
|
+
* can exit cleanly.
|
|
458
|
+
*/
|
|
459
|
+
close?(): Promise<void>;
|
|
356
460
|
}
|
|
357
461
|
|
|
358
462
|
// ─── Internal helpers ────────────────────────────────────────────
|
|
@@ -378,11 +482,20 @@ export const STORAGE_KEYS = {
|
|
|
378
482
|
CONNECTED_DID: 'enbox:auth:connectedDid',
|
|
379
483
|
|
|
380
484
|
/**
|
|
381
|
-
* The base URL of the local DWN server discovered via the `dwn://
|
|
485
|
+
* The base URL of the local DWN server discovered via the `dwn://connect`
|
|
382
486
|
* browser redirect flow. Persisted so subsequent page loads can skip the
|
|
383
487
|
* redirect and inject the endpoint directly.
|
|
384
488
|
*
|
|
385
489
|
* @see https://github.com/enboxorg/enbox/issues/589
|
|
386
490
|
*/
|
|
387
491
|
LOCAL_DWN_ENDPOINT: 'enbox:auth:localDwnEndpoint',
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* JSON-serialised `Record<string, RegistrationTokenData>` for DWN endpoint
|
|
495
|
+
* registration tokens. Automatically loaded before registration and saved
|
|
496
|
+
* after new/refreshed tokens are obtained when `persistTokens` is enabled.
|
|
497
|
+
*
|
|
498
|
+
* @see https://github.com/enboxorg/enbox/issues/690
|
|
499
|
+
*/
|
|
500
|
+
REGISTRATION_TOKENS: 'enbox:auth:registrationTokens',
|
|
388
501
|
} as const;
|