@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,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage adapter implementations for session persistence.
|
|
3
|
+
* @module
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Level } from 'level';
|
|
7
|
+
|
|
8
|
+
import type { StorageAdapter } from '../types.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Browser storage adapter backed by `localStorage`.
|
|
12
|
+
*
|
|
13
|
+
* All keys are prefixed to avoid collisions with other localStorage users.
|
|
14
|
+
*/
|
|
15
|
+
export class BrowserStorage implements StorageAdapter {
|
|
16
|
+
private readonly _prefix: string;
|
|
17
|
+
|
|
18
|
+
constructor(prefix = 'enbox:') {
|
|
19
|
+
this._prefix = prefix;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async get(key: string): Promise<string | null> {
|
|
23
|
+
return globalThis.localStorage.getItem(this._prefix + key);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async set(key: string, value: string): Promise<void> {
|
|
27
|
+
globalThis.localStorage.setItem(this._prefix + key, value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async remove(key: string): Promise<void> {
|
|
31
|
+
globalThis.localStorage.removeItem(this._prefix + key);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async clear(): Promise<void> {
|
|
35
|
+
const keysToRemove: string[] = [];
|
|
36
|
+
for (let i = 0; i < globalThis.localStorage.length; i++) {
|
|
37
|
+
const key = globalThis.localStorage.key(i);
|
|
38
|
+
if (key?.startsWith(this._prefix)) {
|
|
39
|
+
keysToRemove.push(key);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
for (const key of keysToRemove) {
|
|
43
|
+
globalThis.localStorage.removeItem(key);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* In-memory storage adapter for testing or environments without persistence.
|
|
50
|
+
*/
|
|
51
|
+
export class MemoryStorage implements StorageAdapter {
|
|
52
|
+
private readonly _store = new Map<string, string>();
|
|
53
|
+
|
|
54
|
+
async get(key: string): Promise<string | null> {
|
|
55
|
+
return this._store.get(key) ?? null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async set(key: string, value: string): Promise<void> {
|
|
59
|
+
this._store.set(key, value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async remove(key: string): Promise<void> {
|
|
63
|
+
this._store.delete(key);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async clear(): Promise<void> {
|
|
67
|
+
this._store.clear();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* LevelDB-backed storage adapter for Node / CLI environments.
|
|
73
|
+
*
|
|
74
|
+
* Uses the `level` package (v8), which selects `classic-level` (LevelDB)
|
|
75
|
+
* in Node and `browser-level` (IndexedDB) in browsers. Level auto-opens
|
|
76
|
+
* on construction and queues operations until ready, so no explicit
|
|
77
|
+
* `open()` call is required.
|
|
78
|
+
*/
|
|
79
|
+
export class LevelStorage implements StorageAdapter {
|
|
80
|
+
private readonly _db: Level<string, string>;
|
|
81
|
+
|
|
82
|
+
constructor(dataPath = 'DATA/AGENT/AUTH_STORE') {
|
|
83
|
+
this._db = new Level<string, string>(dataPath);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async get(key: string): Promise<string | null> {
|
|
87
|
+
try {
|
|
88
|
+
return await this._db.get(key);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
const e = error as { code?: string };
|
|
91
|
+
if (e.code === 'LEVEL_NOT_FOUND') {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async set(key: string, value: string): Promise<void> {
|
|
99
|
+
await this._db.put(key, value);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async remove(key: string): Promise<void> {
|
|
103
|
+
try {
|
|
104
|
+
await this._db.del(key);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
const e = error as { code?: string };
|
|
107
|
+
// Silently ignore deleting a key that doesn't exist.
|
|
108
|
+
if (e.code !== 'LEVEL_NOT_FOUND') {
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async clear(): Promise<void> {
|
|
115
|
+
await this._db.clear();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Close the underlying LevelDB database. */
|
|
119
|
+
async close(): Promise<void> {
|
|
120
|
+
await this._db.close();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Detect the runtime environment and return an appropriate default storage adapter.
|
|
126
|
+
*
|
|
127
|
+
* - If `localStorage` is available → `BrowserStorage`
|
|
128
|
+
* - Otherwise → `LevelStorage` (persistent on disk via LevelDB)
|
|
129
|
+
*/
|
|
130
|
+
export function createDefaultStorage(): StorageAdapter {
|
|
131
|
+
if (typeof globalThis.localStorage !== 'undefined') {
|
|
132
|
+
return new BrowserStorage();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return new LevelStorage();
|
|
136
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @enbox/auth
|
|
3
|
+
* Public types for the authentication and identity management SDK.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ConnectPermissionRequest, EnboxUserAgent, HdIdentityVault, LocalDwnStrategy, PortableIdentity } from '@enbox/agent';
|
|
7
|
+
|
|
8
|
+
// Re-export types that consumers will need
|
|
9
|
+
export type { ConnectPermissionRequest, HdIdentityVault, IdentityVaultBackup, LocalDwnStrategy, PortableIdentity } from '@enbox/agent';
|
|
10
|
+
|
|
11
|
+
// Re-export EnboxUserAgent so consumers don't need a direct @enbox/agent dep
|
|
12
|
+
export type { EnboxUserAgent } from '@enbox/agent';
|
|
13
|
+
|
|
14
|
+
// ─── Sync ────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Controls DWN synchronisation behaviour.
|
|
18
|
+
*
|
|
19
|
+
* - `'off'` — Sync disabled entirely.
|
|
20
|
+
* - An interval string such as `'30s'`, `'2m'`, `'1h'` — Poll mode at the
|
|
21
|
+
* specified interval.
|
|
22
|
+
* - `undefined` (omitted) — Live WebSocket sync (default).
|
|
23
|
+
*/
|
|
24
|
+
export type SyncOption = 'off' | `${number}${'s' | 'm' | 'h'}`;
|
|
25
|
+
|
|
26
|
+
// ─── Auth State Machine ─────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The possible states of the auth manager.
|
|
30
|
+
*
|
|
31
|
+
* State transitions:
|
|
32
|
+
* ```
|
|
33
|
+
* uninitialized → locked → unlocked → connected
|
|
34
|
+
* ↑ ↑ │
|
|
35
|
+
* └──────────┴──────────┘ (disconnect / lock)
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export type AuthState =
|
|
39
|
+
| 'uninitialized' // No vault exists, no identities
|
|
40
|
+
| 'locked' // Vault exists but is locked (password required)
|
|
41
|
+
| 'unlocked' // Vault unlocked, no active session
|
|
42
|
+
| 'connected'; // Active session with an identity
|
|
43
|
+
|
|
44
|
+
// ─── Events ──────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/** All event names emitted by the auth manager. */
|
|
47
|
+
export type AuthEvent =
|
|
48
|
+
| 'state-change'
|
|
49
|
+
| 'session-start'
|
|
50
|
+
| 'session-end'
|
|
51
|
+
| 'identity-added'
|
|
52
|
+
| 'identity-removed'
|
|
53
|
+
| 'vault-locked'
|
|
54
|
+
| 'vault-unlocked'
|
|
55
|
+
| 'local-dwn-available'
|
|
56
|
+
| 'local-dwn-unavailable';
|
|
57
|
+
|
|
58
|
+
/** Payload type for each event, keyed by event name. */
|
|
59
|
+
export interface AuthEventMap {
|
|
60
|
+
'state-change': { previous: AuthState; current: AuthState };
|
|
61
|
+
'session-start': { session: AuthSessionInfo };
|
|
62
|
+
'session-end': { did: string };
|
|
63
|
+
'identity-added': { identity: IdentityInfo };
|
|
64
|
+
'identity-removed': { didUri: string };
|
|
65
|
+
'vault-locked': Record<string, never>;
|
|
66
|
+
'vault-unlocked': Record<string, never>;
|
|
67
|
+
/** Emitted when a local DWN server is discovered and validated. */
|
|
68
|
+
'local-dwn-available': { endpoint: string };
|
|
69
|
+
/** Emitted when no local DWN server could be discovered or a previously known one is no longer reachable. */
|
|
70
|
+
'local-dwn-unavailable': Record<string, never>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** A type-safe event handler for a specific event. */
|
|
74
|
+
export type AuthEventHandler<E extends AuthEvent = AuthEvent> =
|
|
75
|
+
(payload: AuthEventMap[E]) => void;
|
|
76
|
+
|
|
77
|
+
// ─── Identity ────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
/** Lightweight metadata about a stored identity. */
|
|
80
|
+
export interface IdentityInfo {
|
|
81
|
+
/** The DID URI for this identity. */
|
|
82
|
+
didUri: string;
|
|
83
|
+
|
|
84
|
+
/** Human-readable name. */
|
|
85
|
+
name: string;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Present when this identity is a delegate of another DID
|
|
89
|
+
* (i.e. connected via wallet connect).
|
|
90
|
+
*/
|
|
91
|
+
connectedDid?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Serializable session info for the `session-start` event. */
|
|
95
|
+
export interface AuthSessionInfo {
|
|
96
|
+
did: string;
|
|
97
|
+
delegateDid?: string;
|
|
98
|
+
identity: IdentityInfo;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── Registration ────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
/** Parameters passed to the onProviderAuthRequired callback. */
|
|
104
|
+
export interface ProviderAuthParams {
|
|
105
|
+
/** Full authorize URL to open in a browser (query params already appended). */
|
|
106
|
+
authorizeUrl: string;
|
|
107
|
+
/** The DWN endpoint URL this auth is for (informational). */
|
|
108
|
+
dwnEndpoint: string;
|
|
109
|
+
/** CSRF nonce — the provider will return this unchanged in the redirect. */
|
|
110
|
+
state: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Result returned by the app after the user completes provider auth. */
|
|
114
|
+
export interface ProviderAuthResult {
|
|
115
|
+
/** Authorization code from the provider's redirect. */
|
|
116
|
+
code: string;
|
|
117
|
+
/** Must match the state from ProviderAuthParams (CSRF validation). */
|
|
118
|
+
state: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Persisted registration token data for a DWN endpoint. */
|
|
122
|
+
export interface RegistrationTokenData {
|
|
123
|
+
/** Opaque registration token for POST /registration. */
|
|
124
|
+
registrationToken: string;
|
|
125
|
+
/** Refresh token for obtaining new registration tokens. */
|
|
126
|
+
refreshToken?: string;
|
|
127
|
+
/** Unix timestamp (ms) when the token expires. Undefined = never expires. */
|
|
128
|
+
expiresAt?: number;
|
|
129
|
+
/** Provider's token exchange URL (needed for code exchange). */
|
|
130
|
+
tokenUrl: string;
|
|
131
|
+
/** Provider's refresh URL (needed for token refresh). */
|
|
132
|
+
refreshUrl?: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─── Options ─────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* DWN registration configuration.
|
|
139
|
+
*
|
|
140
|
+
* When provided, the agent DID and connected DID will be registered with
|
|
141
|
+
* DWN endpoints after identity creation. Supports two paths:
|
|
142
|
+
*
|
|
143
|
+
* 1. **Provider auth** (`provider-auth-v0`) — the DWN endpoint requires
|
|
144
|
+
* OAuth-style auth. If {@link onProviderAuthRequired} is provided and
|
|
145
|
+
* the server advertises provider auth, the app handles the auth flow.
|
|
146
|
+
* 2. **Proof of Work** (default fallback) — the DWN endpoint requires
|
|
147
|
+
* solving a PoW challenge to register.
|
|
148
|
+
*/
|
|
149
|
+
export interface RegistrationOptions {
|
|
150
|
+
/** Called when all DWN registrations complete successfully. */
|
|
151
|
+
onSuccess: () => void;
|
|
152
|
+
|
|
153
|
+
/** Called when any DWN registration fails. */
|
|
154
|
+
onFailure: (error: unknown) => void;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Called when a DWN endpoint requires provider auth (`'provider-auth-v0'`).
|
|
158
|
+
*
|
|
159
|
+
* The app should open the `authorizeUrl` in a browser, capture the
|
|
160
|
+
* redirect with the auth code, and return the result. If not provided,
|
|
161
|
+
* endpoints requiring provider auth fall back to PoW registration.
|
|
162
|
+
*/
|
|
163
|
+
onProviderAuthRequired?: (params: ProviderAuthParams) => Promise<ProviderAuthResult>;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Pre-existing registration tokens from a previous session, keyed by
|
|
167
|
+
* DWN endpoint URL. If a valid (non-expired) token exists for an
|
|
168
|
+
* endpoint, it is used directly without re-running the auth flow.
|
|
169
|
+
*/
|
|
170
|
+
registrationTokens?: Record<string, RegistrationTokenData>;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Called when new or refreshed registration tokens are obtained.
|
|
174
|
+
* The app should persist these for future sessions.
|
|
175
|
+
*/
|
|
176
|
+
onRegistrationTokens?: (tokens: Record<string, RegistrationTokenData>) => void;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Options for {@link AuthManager.create}. */
|
|
180
|
+
export interface AuthManagerOptions {
|
|
181
|
+
/**
|
|
182
|
+
* Provide a pre-built {@link EnboxUserAgent} instance.
|
|
183
|
+
*
|
|
184
|
+
* When provided, `dataPath`, `agentVault`, and `localDwnStrategy` are
|
|
185
|
+
* ignored — the agent is used as-is. This is the escape hatch for
|
|
186
|
+
* advanced scenarios like custom DWN stores (e.g., SQLite-backed DWN).
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```ts
|
|
190
|
+
* const agent = await EnboxUserAgent.create({ dwnApi: myCustomDwnApi });
|
|
191
|
+
* const auth = await AuthManager.create({ agent });
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
agent?: EnboxUserAgent;
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Provide a custom {@link HdIdentityVault} implementation.
|
|
198
|
+
* Defaults to a LevelDB-backed vault with PBES2-HS512+A256KW encryption.
|
|
199
|
+
* Ignored when `agent` is provided.
|
|
200
|
+
*/
|
|
201
|
+
agentVault?: HdIdentityVault;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Controls local DWN discovery behavior for remote-target DWN sends/sync.
|
|
205
|
+
* `'off'` (default) disables local probing, `'prefer'` tries local first
|
|
206
|
+
* then falls back to DID-document endpoints, `'only'` requires a local server.
|
|
207
|
+
* Ignored when `agent` is provided.
|
|
208
|
+
*/
|
|
209
|
+
localDwnStrategy?: LocalDwnStrategy;
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Data path for agent storage.
|
|
213
|
+
* - Browser default: `'DATA/AGENT'`
|
|
214
|
+
* - CLI default: `'~/.enbox'`
|
|
215
|
+
*
|
|
216
|
+
* Ignored when `agent` is provided.
|
|
217
|
+
*/
|
|
218
|
+
dataPath?: string;
|
|
219
|
+
|
|
220
|
+
/** Storage adapter for session persistence. Auto-detected if not provided. */
|
|
221
|
+
storage?: StorageAdapter;
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Default password for vault operations.
|
|
225
|
+
* If not provided, an insecure default is used (with a console warning).
|
|
226
|
+
*/
|
|
227
|
+
password?: string;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Sync interval for DWN synchronization.
|
|
231
|
+
* - `'off'` — disable sync
|
|
232
|
+
* - `'15s'`, `'1m'`, etc. — poll at interval
|
|
233
|
+
* - `undefined` — live WebSocket sync
|
|
234
|
+
*/
|
|
235
|
+
sync?: SyncOption;
|
|
236
|
+
|
|
237
|
+
/** Default DWN endpoints for new identities. */
|
|
238
|
+
dwnEndpoints?: string[];
|
|
239
|
+
|
|
240
|
+
/** DWN registration configuration. */
|
|
241
|
+
registration?: RegistrationOptions;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Options for {@link AuthManager.connect}. */
|
|
245
|
+
export interface LocalConnectOptions {
|
|
246
|
+
/** Vault password (overrides manager default). */
|
|
247
|
+
password?: string;
|
|
248
|
+
|
|
249
|
+
/** Re-derive identity from an existing BIP-39 recovery phrase. */
|
|
250
|
+
recoveryPhrase?: string;
|
|
251
|
+
|
|
252
|
+
/** Override manager default sync interval. */
|
|
253
|
+
sync?: SyncOption;
|
|
254
|
+
|
|
255
|
+
/** Override manager default DWN endpoints. */
|
|
256
|
+
dwnEndpoints?: string[];
|
|
257
|
+
|
|
258
|
+
/** Identity metadata. */
|
|
259
|
+
metadata?: { name?: string };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Options for {@link AuthManager.walletConnect}. */
|
|
263
|
+
export interface WalletConnectOptions {
|
|
264
|
+
/** Display name shown in the wallet during the connect flow. */
|
|
265
|
+
displayName: string;
|
|
266
|
+
|
|
267
|
+
/** URL of the connect relay server. */
|
|
268
|
+
connectServerUrl: string;
|
|
269
|
+
|
|
270
|
+
/** Wallet URI scheme. Defaults to `'web5://connect'`. */
|
|
271
|
+
walletUri?: string;
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Protocol permission requests for the wallet connect flow.
|
|
275
|
+
*
|
|
276
|
+
* Each entry is a `ConnectPermissionRequest` from `@enbox/agent` containing
|
|
277
|
+
* a `protocolDefinition` and `permissionScopes`. Use
|
|
278
|
+
* `WalletConnect.createPermissionRequestForProtocol()` to build these.
|
|
279
|
+
*/
|
|
280
|
+
permissionRequests: ConnectPermissionRequest[];
|
|
281
|
+
|
|
282
|
+
/** Called when the wallet URI is ready (render as QR code). */
|
|
283
|
+
onWalletUriReady: (uri: string) => void;
|
|
284
|
+
|
|
285
|
+
/** Called to collect the PIN from the user. */
|
|
286
|
+
validatePin: () => Promise<string>;
|
|
287
|
+
|
|
288
|
+
/** Override manager default sync interval. */
|
|
289
|
+
sync?: SyncOption;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Options for {@link AuthManager.importFromPhrase}. */
|
|
293
|
+
export interface ImportFromPhraseOptions {
|
|
294
|
+
/** The BIP-39 recovery phrase. */
|
|
295
|
+
recoveryPhrase: string;
|
|
296
|
+
|
|
297
|
+
/** Password to protect the vault. */
|
|
298
|
+
password: string;
|
|
299
|
+
|
|
300
|
+
/** Override manager default sync interval. */
|
|
301
|
+
sync?: SyncOption;
|
|
302
|
+
|
|
303
|
+
/** Override manager default DWN endpoints. */
|
|
304
|
+
dwnEndpoints?: string[];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Options for {@link AuthManager.importFromPortable}. */
|
|
308
|
+
export interface ImportFromPortableOptions {
|
|
309
|
+
/** The portable identity JSON to import. */
|
|
310
|
+
portableIdentity: PortableIdentity;
|
|
311
|
+
|
|
312
|
+
/** Override manager default sync interval. */
|
|
313
|
+
sync?: SyncOption;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Options for {@link AuthManager.restoreSession}. */
|
|
317
|
+
export interface RestoreSessionOptions {
|
|
318
|
+
/** Password to unlock the vault (needed if vault is locked). */
|
|
319
|
+
password?: string;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Options for {@link AuthManager.disconnect}. */
|
|
323
|
+
export interface DisconnectOptions {
|
|
324
|
+
/**
|
|
325
|
+
* If `true`, performs a nuclear wipe: clears all localStorage keys,
|
|
326
|
+
* deletes all IndexedDB databases, and removes persisted session data.
|
|
327
|
+
* Default: `false` (clean disconnect — keeps vault and identities).
|
|
328
|
+
*/
|
|
329
|
+
clearStorage?: boolean;
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Milliseconds to wait for pending sync operations before disconnecting.
|
|
333
|
+
* Default: `2000`.
|
|
334
|
+
*/
|
|
335
|
+
timeout?: number;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ─── Storage ─────────────────────────────────────────────────────
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Platform-agnostic key-value storage adapter for session persistence.
|
|
342
|
+
* Implementations are provided for browser (localStorage) and CLI (file system).
|
|
343
|
+
*/
|
|
344
|
+
export interface StorageAdapter {
|
|
345
|
+
/** Get a value by key. Returns `null` if not found. */
|
|
346
|
+
get(key: string): Promise<string | null>;
|
|
347
|
+
|
|
348
|
+
/** Set a key-value pair. */
|
|
349
|
+
set(key: string, value: string): Promise<void>;
|
|
350
|
+
|
|
351
|
+
/** Remove a key. */
|
|
352
|
+
remove(key: string): Promise<void>;
|
|
353
|
+
|
|
354
|
+
/** Clear all stored data. */
|
|
355
|
+
clear(): Promise<void>;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ─── Internal helpers ────────────────────────────────────────────
|
|
359
|
+
|
|
360
|
+
/** The insecure default password used when none is provided. */
|
|
361
|
+
export const INSECURE_DEFAULT_PASSWORD = 'insecure-static-phrase';
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Storage keys used by the auth manager for session persistence.
|
|
365
|
+
* @internal
|
|
366
|
+
*/
|
|
367
|
+
export const STORAGE_KEYS = {
|
|
368
|
+
/** Whether a session was previously established. */
|
|
369
|
+
PREVIOUSLY_CONNECTED: 'enbox:auth:previouslyConnected',
|
|
370
|
+
|
|
371
|
+
/** The DID URI of the last active identity. */
|
|
372
|
+
ACTIVE_IDENTITY: 'enbox:auth:activeIdentity',
|
|
373
|
+
|
|
374
|
+
/** The delegate DID URI (for wallet-connected sessions). */
|
|
375
|
+
DELEGATE_DID: 'enbox:auth:delegateDid',
|
|
376
|
+
|
|
377
|
+
/** The connected DID (for wallet-connected sessions). */
|
|
378
|
+
CONNECTED_DID: 'enbox:auth:connectedDid',
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* The base URL of the local DWN server discovered via the `dwn://register`
|
|
382
|
+
* browser redirect flow. Persisted so subsequent page loads can skip the
|
|
383
|
+
* redirect and inject the endpoint directly.
|
|
384
|
+
*
|
|
385
|
+
* @see https://github.com/enboxorg/enbox/issues/589
|
|
386
|
+
*/
|
|
387
|
+
LOCAL_DWN_ENDPOINT: 'enbox:auth:localDwnEndpoint',
|
|
388
|
+
} as const;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VaultManager wraps {@link HdIdentityVault} with a high-level API
|
|
3
|
+
* and emits events on lock/unlock.
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { HdIdentityVault, IdentityVaultBackup } from '@enbox/agent';
|
|
8
|
+
|
|
9
|
+
import type { AuthEventEmitter } from '../events.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Manages the encrypted identity vault lifecycle.
|
|
13
|
+
*
|
|
14
|
+
* The vault stores the agent's DID and content encryption key (CEK),
|
|
15
|
+
* protected by a user password using PBES2-HS512+A256KW with a 210K
|
|
16
|
+
* iteration work factor. The vault supports HD key derivation from
|
|
17
|
+
* a BIP-39 mnemonic for recovery.
|
|
18
|
+
*/
|
|
19
|
+
export class VaultManager {
|
|
20
|
+
private readonly _vault: HdIdentityVault;
|
|
21
|
+
private readonly _emitter: AuthEventEmitter;
|
|
22
|
+
|
|
23
|
+
constructor(vault: HdIdentityVault, emitter: AuthEventEmitter) {
|
|
24
|
+
this._vault = vault;
|
|
25
|
+
this._emitter = emitter;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The underlying vault instance (for advanced usage). */
|
|
29
|
+
get raw(): HdIdentityVault {
|
|
30
|
+
return this._vault;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Whether the vault has been initialized (has encrypted data). */
|
|
34
|
+
async isInitialized(): Promise<boolean> {
|
|
35
|
+
return this._vault.isInitialized();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Whether the vault is currently locked (synchronous check). */
|
|
39
|
+
get isLocked(): boolean {
|
|
40
|
+
return this._vault.isLocked();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Unlock the vault with the given password.
|
|
45
|
+
* Decrypts the CEK into memory so the agent DID can be retrieved.
|
|
46
|
+
*
|
|
47
|
+
* @throws If the password is incorrect or vault is not initialized.
|
|
48
|
+
*/
|
|
49
|
+
async unlock(password: string): Promise<void> {
|
|
50
|
+
await this._vault.unlock({ password });
|
|
51
|
+
this._emitter.emit('vault-unlocked', {});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Lock the vault, clearing the CEK from memory.
|
|
56
|
+
* After locking, the password must be provided again to unlock.
|
|
57
|
+
*/
|
|
58
|
+
async lock(): Promise<void> {
|
|
59
|
+
await this._vault.lock();
|
|
60
|
+
this._emitter.emit('vault-locked', {});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Change the vault password. Re-encrypts the CEK with the new password.
|
|
65
|
+
*
|
|
66
|
+
* @throws If the old password is incorrect or vault is locked.
|
|
67
|
+
*/
|
|
68
|
+
async changePassword(oldPassword: string, newPassword: string): Promise<void> {
|
|
69
|
+
await this._vault.changePassword({ oldPassword, newPassword });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Create a backup of the vault.
|
|
74
|
+
*
|
|
75
|
+
* @throws If the vault is not initialized or is locked.
|
|
76
|
+
*/
|
|
77
|
+
async backup(): Promise<IdentityVaultBackup> {
|
|
78
|
+
return this._vault.backup();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Restore the vault from a backup.
|
|
83
|
+
*
|
|
84
|
+
* @throws If the password doesn't match the backup's encryption.
|
|
85
|
+
*/
|
|
86
|
+
async restore(backup: IdentityVaultBackup, password: string): Promise<void> {
|
|
87
|
+
await this._vault.restore({ backup, password });
|
|
88
|
+
}
|
|
89
|
+
}
|