@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,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local DWN discovery integration for browser and CLI environments.
|
|
3
|
+
*
|
|
4
|
+
* This module bridges the local DWN discovery mechanisms (implemented in
|
|
5
|
+
* `@enbox/agent`) with the `@enbox/auth` storage, session lifecycle, and
|
|
6
|
+
* event system.
|
|
7
|
+
*
|
|
8
|
+
* ## Discovery channels (browser, highest to lowest priority)
|
|
9
|
+
*
|
|
10
|
+
* 1. **URL fragment payload** — A `dwn://register` redirect just landed
|
|
11
|
+
* on the page with the endpoint in `#`. Highest priority because it's
|
|
12
|
+
* fresh and explicit.
|
|
13
|
+
* 2. **Persisted endpoint** (localStorage) — A previously discovered
|
|
14
|
+
* endpoint restored and re-validated via `GET /info`.
|
|
15
|
+
* 3. **Agent-level discovery** (transparent, runs on every `sendRequest`)
|
|
16
|
+
* — `~/.enbox/dwn.json` discovery file (Node/Bun only; skipped in
|
|
17
|
+
* browsers) and sequential port probing on `127.0.0.1:{3000,55500–55509}`.
|
|
18
|
+
* This channel works even if the browser-specific functions here
|
|
19
|
+
* return `false`.
|
|
20
|
+
*
|
|
21
|
+
* ## Discovery channels (CLI / native, all transparent)
|
|
22
|
+
*
|
|
23
|
+
* In Node/Bun environments, all discovery happens automatically inside
|
|
24
|
+
* `AgentDwnApi.getLocalDwnEndpoint()`. The browser-specific functions
|
|
25
|
+
* in this module (`checkUrlForDwnDiscoveryPayload`, `requestLocalDwnDiscovery`)
|
|
26
|
+
* are not needed — the agent reads `~/.enbox/dwn.json` and probes ports
|
|
27
|
+
* on its own.
|
|
28
|
+
*
|
|
29
|
+
* @see https://github.com/enboxorg/enbox/issues/589
|
|
30
|
+
* @module
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import type { EnboxUserAgent } from '@enbox/agent';
|
|
34
|
+
|
|
35
|
+
import { buildDwnRegisterUrl, readDwnDiscoveryPayloadFromUrl } from '@enbox/agent';
|
|
36
|
+
|
|
37
|
+
import type { AuthEventEmitter } from '../events.js';
|
|
38
|
+
import { STORAGE_KEYS } from '../types.js';
|
|
39
|
+
import type { StorageAdapter } from '../types.js';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Check the current page URL for a `DwnDiscoveryPayload` in the fragment.
|
|
43
|
+
*
|
|
44
|
+
* This is called once at the start of a connection flow to detect whether
|
|
45
|
+
* the user was just redirected back from a `dwn://register` handler. If a
|
|
46
|
+
* valid payload is found, the endpoint is persisted and the fragment is
|
|
47
|
+
* cleared to prevent double-reads.
|
|
48
|
+
*
|
|
49
|
+
* @returns The discovered endpoint string, or `undefined` if no payload
|
|
50
|
+
* was found in the URL.
|
|
51
|
+
*/
|
|
52
|
+
export function checkUrlForDwnDiscoveryPayload(): string | undefined {
|
|
53
|
+
if (typeof globalThis.location === 'undefined') {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const payload = readDwnDiscoveryPayloadFromUrl(globalThis.location.href);
|
|
58
|
+
if (!payload) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Clear the fragment to prevent re-reading on subsequent calls or
|
|
63
|
+
// if the user refreshes the page after the redirect.
|
|
64
|
+
if (typeof globalThis.history !== 'undefined' && globalThis.history.replaceState) {
|
|
65
|
+
const cleanUrl = globalThis.location.href.split('#')[0];
|
|
66
|
+
globalThis.history.replaceState(null, '', cleanUrl);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return payload.endpoint;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Persist a discovered local DWN endpoint in auth storage.
|
|
74
|
+
*
|
|
75
|
+
* @param storage - The auth storage adapter.
|
|
76
|
+
* @param endpoint - The local DWN server base URL.
|
|
77
|
+
*/
|
|
78
|
+
export async function persistLocalDwnEndpoint(
|
|
79
|
+
storage: StorageAdapter,
|
|
80
|
+
endpoint: string,
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
await storage.set(STORAGE_KEYS.LOCAL_DWN_ENDPOINT, endpoint);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Clear the persisted local DWN endpoint from auth storage.
|
|
87
|
+
*
|
|
88
|
+
* Call this when the cached endpoint is found to be stale (server no
|
|
89
|
+
* longer running).
|
|
90
|
+
*
|
|
91
|
+
* @param storage - The auth storage adapter.
|
|
92
|
+
*/
|
|
93
|
+
export async function clearLocalDwnEndpoint(
|
|
94
|
+
storage: StorageAdapter,
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
await storage.remove(STORAGE_KEYS.LOCAL_DWN_ENDPOINT);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Restore a previously persisted local DWN endpoint and inject it into the
|
|
101
|
+
* agent's discovery cache.
|
|
102
|
+
*
|
|
103
|
+
* The endpoint is validated by the agent (via `GET /info`) before being
|
|
104
|
+
* accepted. If validation fails, the stale entry is removed from storage.
|
|
105
|
+
*
|
|
106
|
+
* @param agent - The running EnboxUserAgent.
|
|
107
|
+
* @param storage - The auth storage adapter.
|
|
108
|
+
* @returns `true` if an endpoint was restored and validated, `false` otherwise.
|
|
109
|
+
*/
|
|
110
|
+
export async function restoreLocalDwnEndpoint(
|
|
111
|
+
agent: EnboxUserAgent,
|
|
112
|
+
storage: StorageAdapter,
|
|
113
|
+
): Promise<boolean> {
|
|
114
|
+
const endpoint = await storage.get(STORAGE_KEYS.LOCAL_DWN_ENDPOINT);
|
|
115
|
+
if (!endpoint) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const accepted = await agent.dwn.setCachedLocalDwnEndpoint(endpoint);
|
|
120
|
+
if (!accepted) {
|
|
121
|
+
// The server is no longer running — remove the stale entry.
|
|
122
|
+
await clearLocalDwnEndpoint(storage);
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Run the full local DWN discovery sequence for a browser connection flow.
|
|
131
|
+
*
|
|
132
|
+
* This function handles the **receiving** side of local DWN discovery in
|
|
133
|
+
* the browser. It does NOT trigger the `dwn://register` redirect — use
|
|
134
|
+
* {@link requestLocalDwnDiscovery} for that.
|
|
135
|
+
*
|
|
136
|
+
* The discovery channels, from highest to lowest priority:
|
|
137
|
+
*
|
|
138
|
+
* 1. **URL fragment payload** — A `dwn://register` redirect just landed on
|
|
139
|
+
* this page with the DWN endpoint in `#`. This is the highest-priority
|
|
140
|
+
* signal because it's fresh and explicit.
|
|
141
|
+
*
|
|
142
|
+
* 2. **Persisted endpoint** (localStorage) — A previously discovered
|
|
143
|
+
* endpoint is restored and re-validated via `GET /info`.
|
|
144
|
+
*
|
|
145
|
+
* 3. **Agent-level discovery** (transparent) — Even if this function
|
|
146
|
+
* returns `false`, the agent's `LocalDwnDiscovery` will independently
|
|
147
|
+
* try the discovery file (`~/.enbox/dwn.json`) and port probing on
|
|
148
|
+
* every `sendRequest()` call. Those channels are not available in
|
|
149
|
+
* browsers (no filesystem access, CORS may block probes), but they
|
|
150
|
+
* work transparently in Node/Bun CLI environments.
|
|
151
|
+
*
|
|
152
|
+
* When an `emitter` is provided, this function emits:
|
|
153
|
+
* - `'local-dwn-available'` with the endpoint when discovery succeeds.
|
|
154
|
+
* - `'local-dwn-unavailable'` when no local DWN could be reached.
|
|
155
|
+
*
|
|
156
|
+
* @param agent - The running EnboxUserAgent.
|
|
157
|
+
* @param storage - The auth storage adapter.
|
|
158
|
+
* @param emitter - Optional event emitter for local DWN status notifications.
|
|
159
|
+
* @returns `true` if a local DWN endpoint was discovered and injected.
|
|
160
|
+
*/
|
|
161
|
+
export async function applyLocalDwnDiscovery(
|
|
162
|
+
agent: EnboxUserAgent,
|
|
163
|
+
storage: StorageAdapter,
|
|
164
|
+
emitter?: AuthEventEmitter,
|
|
165
|
+
): Promise<boolean> {
|
|
166
|
+
// Step 1: Check for a fresh payload in the URL fragment (redirect just happened).
|
|
167
|
+
const freshEndpoint = checkUrlForDwnDiscoveryPayload();
|
|
168
|
+
|
|
169
|
+
if (freshEndpoint) {
|
|
170
|
+
const accepted = await agent.dwn.setCachedLocalDwnEndpoint(freshEndpoint);
|
|
171
|
+
if (accepted) {
|
|
172
|
+
await persistLocalDwnEndpoint(storage, freshEndpoint);
|
|
173
|
+
emitter?.emit('local-dwn-available', { endpoint: freshEndpoint });
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
// Payload was in the URL but the server is not reachable — fall through.
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Step 2: Try restoring from storage.
|
|
180
|
+
const restored = await restoreLocalDwnEndpoint(agent, storage);
|
|
181
|
+
|
|
182
|
+
if (restored) {
|
|
183
|
+
const endpoint = await storage.get(STORAGE_KEYS.LOCAL_DWN_ENDPOINT);
|
|
184
|
+
if (endpoint) {
|
|
185
|
+
emitter?.emit('local-dwn-available', { endpoint });
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
emitter?.emit('local-dwn-unavailable', {});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return restored;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ─── dwn://register trigger ─────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Initiate the `dwn://register` flow by opening the register URL.
|
|
198
|
+
*
|
|
199
|
+
* This asks the operating system to route `dwn://register?callback=<url>`
|
|
200
|
+
* to the registered handler (electrobun-dwn), which will redirect the
|
|
201
|
+
* user's browser back to `callbackUrl` with the local DWN endpoint
|
|
202
|
+
* encoded in the URL fragment.
|
|
203
|
+
*
|
|
204
|
+
* **Important:** There is no reliable cross-browser API to detect whether
|
|
205
|
+
* a `dwn://` handler is installed. If no handler is registered, this call
|
|
206
|
+
* will silently fail or show an OS-level error dialog. Use
|
|
207
|
+
* {@link probeLocalDwn} first to check if a local DWN is already
|
|
208
|
+
* reachable via port probing — if it is, you can skip the register flow
|
|
209
|
+
* entirely and call {@link applyLocalDwnDiscovery} instead.
|
|
210
|
+
*
|
|
211
|
+
* @param callbackUrl - The URL to redirect back to. Defaults to the
|
|
212
|
+
* current page URL (without its fragment) if running in a browser.
|
|
213
|
+
* @returns `true` if the register URL was opened, `false` if no
|
|
214
|
+
* callback URL could be determined (e.g. no `globalThis.location`).
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* ```ts
|
|
218
|
+
* // Check if local DWN is already available via direct probe.
|
|
219
|
+
* const alreadyAvailable = await probeLocalDwn();
|
|
220
|
+
* if (!alreadyAvailable) {
|
|
221
|
+
* // No local DWN found — trigger the dwn://register flow.
|
|
222
|
+
* requestLocalDwnDiscovery();
|
|
223
|
+
* // The page will reload with the endpoint in the URL fragment.
|
|
224
|
+
* }
|
|
225
|
+
* ```
|
|
226
|
+
*/
|
|
227
|
+
export function requestLocalDwnDiscovery(callbackUrl?: string): boolean {
|
|
228
|
+
const resolvedCallback = callbackUrl ?? currentPageUrl();
|
|
229
|
+
if (!resolvedCallback) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const registerUrl = buildDwnRegisterUrl(resolvedCallback);
|
|
234
|
+
|
|
235
|
+
// Open the dwn:// URL. Use window.open() rather than location.href
|
|
236
|
+
// assignment to avoid navigating away from the current page if the
|
|
237
|
+
// OS handler isn't installed.
|
|
238
|
+
if (typeof globalThis.open === 'function') {
|
|
239
|
+
globalThis.open(registerUrl);
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Fallback for environments with location but no window.open.
|
|
244
|
+
if (typeof globalThis.location !== 'undefined') {
|
|
245
|
+
globalThis.location.href = registerUrl;
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Probe whether a local DWN server is reachable via direct HTTP fetch.
|
|
254
|
+
*
|
|
255
|
+
* Attempts `GET http://127.0.0.1:{port}/info` on the well-known port
|
|
256
|
+
* candidates and returns the endpoint URL of the first server that
|
|
257
|
+
* responds with a valid `@enbox/dwn-server` identity.
|
|
258
|
+
*
|
|
259
|
+
* This is useful in browsers to check if a local DWN is available
|
|
260
|
+
* *before* triggering the `dwn://register` redirect flow — if the
|
|
261
|
+
* server is already reachable (CORS permitting), the redirect is
|
|
262
|
+
* unnecessary.
|
|
263
|
+
*
|
|
264
|
+
* @returns The local DWN endpoint URL, or `undefined` if no server
|
|
265
|
+
* was found. Returns `undefined` (rather than throwing) on CORS
|
|
266
|
+
* errors or network failures.
|
|
267
|
+
*/
|
|
268
|
+
export async function probeLocalDwn(): Promise<string | undefined> {
|
|
269
|
+
// Import port candidates from @enbox/agent. Using a dynamic import
|
|
270
|
+
// here keeps the function self-contained and avoids circular deps.
|
|
271
|
+
const { localDwnPortCandidates, localDwnHostCandidates } = await import('@enbox/agent');
|
|
272
|
+
|
|
273
|
+
for (const port of localDwnPortCandidates) {
|
|
274
|
+
for (const host of localDwnHostCandidates) {
|
|
275
|
+
const endpoint = `http://${host}:${port}`;
|
|
276
|
+
try {
|
|
277
|
+
const response = await fetch(`${endpoint}/info`, { signal: AbortSignal.timeout(2_000) });
|
|
278
|
+
if (!response.ok) { continue; }
|
|
279
|
+
|
|
280
|
+
const serverInfo = await response.json() as { server?: string };
|
|
281
|
+
if (serverInfo?.server === '@enbox/dwn-server') {
|
|
282
|
+
return endpoint;
|
|
283
|
+
}
|
|
284
|
+
} catch {
|
|
285
|
+
// Network error, CORS block, or timeout — try next candidate.
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ─── Internal helpers ───────────────────────────────────────────
|
|
293
|
+
|
|
294
|
+
/** Return the current page URL without the fragment, or `undefined`. */
|
|
295
|
+
function currentPageUrl(): string | undefined {
|
|
296
|
+
if (typeof globalThis.location === 'undefined') {
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
return globalThis.location.href.split('#')[0];
|
|
300
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DWN registration flow.
|
|
3
|
+
*
|
|
4
|
+
* Registers the agent DID and connected DID with DWN endpoints.
|
|
5
|
+
* Supports two registration paths:
|
|
6
|
+
* 1. Provider auth (`provider-auth-v0`) — OAuth-style with tokens
|
|
7
|
+
* 2. Proof of Work (default) — PoW challenge-response
|
|
8
|
+
*
|
|
9
|
+
* This matches the registration logic from `Enbox.connect()` but as a
|
|
10
|
+
* standalone, reusable function.
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { EnboxUserAgent } from '@enbox/agent';
|
|
15
|
+
|
|
16
|
+
import { DwnRegistrar } from '@enbox/dwn-clients';
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
RegistrationOptions,
|
|
20
|
+
RegistrationTokenData,
|
|
21
|
+
} from '../types.js';
|
|
22
|
+
|
|
23
|
+
/** @internal */
|
|
24
|
+
export interface RegistrationContext {
|
|
25
|
+
/** The user agent with RPC access for getServerInfo(). */
|
|
26
|
+
userAgent: EnboxUserAgent;
|
|
27
|
+
|
|
28
|
+
/** DWN endpoints to register with. */
|
|
29
|
+
dwnEndpoints: string[];
|
|
30
|
+
|
|
31
|
+
/** The agent DID URI. */
|
|
32
|
+
agentDid: string;
|
|
33
|
+
|
|
34
|
+
/** The connected DID URI (the identity's DID). */
|
|
35
|
+
connectedDid: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Register the agent and connected DIDs with the configured DWN endpoints.
|
|
40
|
+
*
|
|
41
|
+
* For each endpoint:
|
|
42
|
+
* 1. Fetches server info to check registration requirements.
|
|
43
|
+
* 2. If the server requires `provider-auth-v0` and the app provides
|
|
44
|
+
* `onProviderAuthRequired`, runs the OAuth flow (with token refresh).
|
|
45
|
+
* 3. Otherwise falls back to PoW registration.
|
|
46
|
+
* 4. Calls `onSuccess` when all endpoints succeed, `onFailure` on error.
|
|
47
|
+
*
|
|
48
|
+
* @internal
|
|
49
|
+
*/
|
|
50
|
+
export async function registerWithDwnEndpoints(
|
|
51
|
+
ctx: RegistrationContext,
|
|
52
|
+
registration: RegistrationOptions,
|
|
53
|
+
): Promise<void> {
|
|
54
|
+
const { userAgent, dwnEndpoints, agentDid, connectedDid } = ctx;
|
|
55
|
+
|
|
56
|
+
const updatedTokens: Record<string, RegistrationTokenData> = {
|
|
57
|
+
...(registration.registrationTokens ?? {}),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
for (const dwnEndpoint of dwnEndpoints) {
|
|
62
|
+
const serverInfo = await userAgent.rpc.getServerInfo(dwnEndpoint);
|
|
63
|
+
|
|
64
|
+
if (serverInfo.registrationRequirements.length === 0) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Deduplicate DIDs to register.
|
|
69
|
+
const didsToRegister = [agentDid, connectedDid]
|
|
70
|
+
.filter((did, i, arr): did is string => arr.indexOf(did) === i);
|
|
71
|
+
|
|
72
|
+
const hasProviderAuth =
|
|
73
|
+
serverInfo.registrationRequirements.includes('provider-auth-v0')
|
|
74
|
+
&& serverInfo.providerAuth !== undefined;
|
|
75
|
+
|
|
76
|
+
if (hasProviderAuth && registration.onProviderAuthRequired) {
|
|
77
|
+
// --- Provider Auth Path ---
|
|
78
|
+
let tokenData = updatedTokens[dwnEndpoint] as RegistrationTokenData | undefined;
|
|
79
|
+
|
|
80
|
+
// Refresh expired tokens.
|
|
81
|
+
if (tokenData?.expiresAt !== undefined && tokenData.expiresAt < Date.now()) {
|
|
82
|
+
if (tokenData.refreshUrl && tokenData.refreshToken) {
|
|
83
|
+
const refreshed = await DwnRegistrar.refreshRegistrationToken(
|
|
84
|
+
tokenData.refreshUrl, tokenData.refreshToken,
|
|
85
|
+
);
|
|
86
|
+
tokenData = {
|
|
87
|
+
registrationToken : refreshed.registrationToken,
|
|
88
|
+
refreshToken : refreshed.refreshToken,
|
|
89
|
+
expiresAt : refreshed.expiresIn !== undefined
|
|
90
|
+
? Date.now() + (refreshed.expiresIn * 1000) : undefined,
|
|
91
|
+
tokenUrl : tokenData.tokenUrl,
|
|
92
|
+
refreshUrl : tokenData.refreshUrl,
|
|
93
|
+
};
|
|
94
|
+
updatedTokens[dwnEndpoint] = tokenData;
|
|
95
|
+
} else {
|
|
96
|
+
tokenData = undefined;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Run the auth flow if no valid token exists.
|
|
101
|
+
if (tokenData === undefined) {
|
|
102
|
+
const state = crypto.randomUUID();
|
|
103
|
+
const providerAuth = serverInfo.providerAuth!;
|
|
104
|
+
const separator = providerAuth.authorizeUrl.includes('?') ? '&' : '?';
|
|
105
|
+
const authorizeUrl = `${providerAuth.authorizeUrl}${separator}`
|
|
106
|
+
+ `redirect_uri=${encodeURIComponent(dwnEndpoint)}`
|
|
107
|
+
+ `&state=${encodeURIComponent(state)}`;
|
|
108
|
+
|
|
109
|
+
const authResult = await registration.onProviderAuthRequired({
|
|
110
|
+
authorizeUrl,
|
|
111
|
+
dwnEndpoint,
|
|
112
|
+
state,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (authResult.state !== state) {
|
|
116
|
+
throw new Error('Provider auth state mismatch \u2014 possible CSRF attack.');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const tokenResponse = await DwnRegistrar.exchangeAuthCode(
|
|
120
|
+
providerAuth.tokenUrl, authResult.code, dwnEndpoint,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
tokenData = {
|
|
124
|
+
registrationToken : tokenResponse.registrationToken,
|
|
125
|
+
refreshToken : tokenResponse.refreshToken,
|
|
126
|
+
expiresAt : tokenResponse.expiresIn !== undefined
|
|
127
|
+
? Date.now() + (tokenResponse.expiresIn * 1000) : undefined,
|
|
128
|
+
tokenUrl : providerAuth.tokenUrl,
|
|
129
|
+
refreshUrl : providerAuth.refreshUrl,
|
|
130
|
+
};
|
|
131
|
+
updatedTokens[dwnEndpoint] = tokenData;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Register each DID using the provider auth token.
|
|
135
|
+
for (const did of didsToRegister) {
|
|
136
|
+
await DwnRegistrar.registerTenantWithToken(
|
|
137
|
+
dwnEndpoint, did, tokenData.registrationToken,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
// --- Default Path (PoW / general registration) ---
|
|
142
|
+
for (const did of didsToRegister) {
|
|
143
|
+
await DwnRegistrar.registerTenant(dwnEndpoint, did);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Notify app of updated tokens for persistence.
|
|
149
|
+
if (registration.onRegistrationTokens) {
|
|
150
|
+
registration.onRegistrationTokens(updatedTokens);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
registration.onSuccess();
|
|
154
|
+
} catch (error: unknown) {
|
|
155
|
+
registration.onFailure(error);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
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 { EnboxUserAgent } from '@enbox/agent';
|
|
10
|
+
|
|
11
|
+
import type { AuthEventEmitter } from '../events.js';
|
|
12
|
+
import { AuthSession } from '../identity-session.js';
|
|
13
|
+
import { registerWithDwnEndpoints } from './dwn-registration.js';
|
|
14
|
+
import { STORAGE_KEYS } from '../types.js';
|
|
15
|
+
import type {
|
|
16
|
+
ImportFromPhraseOptions,
|
|
17
|
+
ImportFromPortableOptions,
|
|
18
|
+
RegistrationOptions,
|
|
19
|
+
StorageAdapter,
|
|
20
|
+
SyncOption,
|
|
21
|
+
} from '../types.js';
|
|
22
|
+
|
|
23
|
+
/** @internal */
|
|
24
|
+
export interface ImportContext {
|
|
25
|
+
userAgent: EnboxUserAgent;
|
|
26
|
+
emitter: AuthEventEmitter;
|
|
27
|
+
storage: StorageAdapter;
|
|
28
|
+
defaultSync?: SyncOption;
|
|
29
|
+
defaultDwnEndpoints?: string[];
|
|
30
|
+
registration?: RegistrationOptions;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Import (or recover) an identity from a BIP-39 recovery phrase.
|
|
35
|
+
*
|
|
36
|
+
* This re-initializes the vault with the given phrase and password,
|
|
37
|
+
* recovering the agent DID and all derived keys.
|
|
38
|
+
*/
|
|
39
|
+
export async function importFromPhrase(
|
|
40
|
+
ctx: ImportContext,
|
|
41
|
+
options: ImportFromPhraseOptions,
|
|
42
|
+
): Promise<AuthSession> {
|
|
43
|
+
const { userAgent, emitter, storage } = ctx;
|
|
44
|
+
const { recoveryPhrase, password } = options;
|
|
45
|
+
const sync = options.sync ?? ctx.defaultSync;
|
|
46
|
+
const dwnEndpoints = options.dwnEndpoints ?? ctx.defaultDwnEndpoints ?? ['https://enbox-dwn.fly.dev'];
|
|
47
|
+
|
|
48
|
+
// Initialize the vault with the recovery phrase.
|
|
49
|
+
// This re-derives the same agent DID and CEK from the mnemonic.
|
|
50
|
+
if (await userAgent.firstLaunch()) {
|
|
51
|
+
await userAgent.initialize({
|
|
52
|
+
password,
|
|
53
|
+
recoveryPhrase,
|
|
54
|
+
dwnEndpoints,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
await userAgent.start({ password });
|
|
59
|
+
emitter.emit('vault-unlocked', {});
|
|
60
|
+
|
|
61
|
+
// The recovery phrase re-derives the same agent DID,
|
|
62
|
+
// but the user identity might not exist yet — create one if needed.
|
|
63
|
+
const identities = await userAgent.identity.list();
|
|
64
|
+
let identity = identities[0];
|
|
65
|
+
let isNewIdentity = false;
|
|
66
|
+
|
|
67
|
+
if (!identity) {
|
|
68
|
+
isNewIdentity = true;
|
|
69
|
+
identity = await userAgent.identity.create({
|
|
70
|
+
didMethod : 'dht',
|
|
71
|
+
metadata : { name: 'Default' },
|
|
72
|
+
didOptions : {
|
|
73
|
+
services: [
|
|
74
|
+
{
|
|
75
|
+
id : 'dwn',
|
|
76
|
+
type : 'DecentralizedWebNode',
|
|
77
|
+
serviceEndpoint : dwnEndpoints,
|
|
78
|
+
enc : '#enc',
|
|
79
|
+
sig : '#sig',
|
|
80
|
+
}
|
|
81
|
+
],
|
|
82
|
+
verificationMethods: [
|
|
83
|
+
{
|
|
84
|
+
algorithm : 'Ed25519',
|
|
85
|
+
id : 'sig',
|
|
86
|
+
purposes : ['assertionMethod', 'authentication'],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
algorithm : 'X25519',
|
|
90
|
+
id : 'enc',
|
|
91
|
+
purposes : ['keyAgreement'],
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const connectedDid = identity.did.uri;
|
|
99
|
+
|
|
100
|
+
// Register with DWN endpoints (if registration options are provided).
|
|
101
|
+
if (ctx.registration) {
|
|
102
|
+
await registerWithDwnEndpoints(
|
|
103
|
+
{
|
|
104
|
+
userAgent : userAgent,
|
|
105
|
+
dwnEndpoints,
|
|
106
|
+
agentDid : userAgent.agentDid.uri,
|
|
107
|
+
connectedDid,
|
|
108
|
+
},
|
|
109
|
+
ctx.registration,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Register and start sync.
|
|
114
|
+
if (isNewIdentity && sync !== 'off') {
|
|
115
|
+
await userAgent.sync.registerIdentity({ did: connectedDid, options: { protocols: [] } });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (sync !== 'off') {
|
|
119
|
+
const syncMode = sync === undefined ? 'live' : 'poll';
|
|
120
|
+
const syncInterval = sync ?? (syncMode === 'live' ? '5m' : '2m');
|
|
121
|
+
userAgent.sync.startSync({ mode: syncMode, interval: syncInterval })
|
|
122
|
+
.catch((err: unknown) => console.error('[@enbox/auth] Sync failed:', err));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
await storage.set(STORAGE_KEYS.PREVIOUSLY_CONNECTED, 'true');
|
|
126
|
+
await storage.set(STORAGE_KEYS.ACTIVE_IDENTITY, connectedDid);
|
|
127
|
+
|
|
128
|
+
const identityInfo = {
|
|
129
|
+
didUri : connectedDid,
|
|
130
|
+
name : identity.metadata.name,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const session = new AuthSession({
|
|
134
|
+
agent : userAgent,
|
|
135
|
+
did : connectedDid,
|
|
136
|
+
identity : identityInfo,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
emitter.emit('identity-added', { identity: identityInfo });
|
|
140
|
+
emitter.emit('session-start', {
|
|
141
|
+
session: { did: connectedDid, identity: identityInfo },
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return session;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Import an identity from a PortableIdentity JSON object.
|
|
149
|
+
*
|
|
150
|
+
* The portable identity contains the DID's private keys and metadata,
|
|
151
|
+
* allowing it to be used on this device.
|
|
152
|
+
*/
|
|
153
|
+
export async function importFromPortable(
|
|
154
|
+
ctx: ImportContext,
|
|
155
|
+
options: ImportFromPortableOptions,
|
|
156
|
+
): Promise<AuthSession> {
|
|
157
|
+
const { userAgent, emitter, storage } = ctx;
|
|
158
|
+
const sync = options.sync ?? ctx.defaultSync;
|
|
159
|
+
|
|
160
|
+
const identity = await userAgent.identity.import({
|
|
161
|
+
portableIdentity: options.portableIdentity,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const connectedDid = identity.metadata.connectedDid ?? identity.did.uri;
|
|
165
|
+
const delegateDid = identity.metadata.connectedDid ? identity.did.uri : undefined;
|
|
166
|
+
|
|
167
|
+
// Register with DWN endpoints (if registration options are provided).
|
|
168
|
+
// For portable imports, extract endpoints from the DID document's DWN service.
|
|
169
|
+
if (ctx.registration) {
|
|
170
|
+
const dwnEndpoints = ctx.defaultDwnEndpoints ?? ['https://enbox-dwn.fly.dev'];
|
|
171
|
+
await registerWithDwnEndpoints(
|
|
172
|
+
{
|
|
173
|
+
userAgent : userAgent,
|
|
174
|
+
dwnEndpoints,
|
|
175
|
+
agentDid : userAgent.agentDid.uri,
|
|
176
|
+
connectedDid,
|
|
177
|
+
},
|
|
178
|
+
ctx.registration,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Register and start sync.
|
|
183
|
+
if (sync !== 'off') {
|
|
184
|
+
await userAgent.sync.registerIdentity({
|
|
185
|
+
did : connectedDid,
|
|
186
|
+
options : { delegateDid, protocols: [] },
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const syncMode = sync === undefined ? 'live' : 'poll';
|
|
190
|
+
const syncInterval = sync ?? (syncMode === 'live' ? '5m' : '2m');
|
|
191
|
+
userAgent.sync.startSync({ mode: syncMode, interval: syncInterval })
|
|
192
|
+
.catch((err: unknown) => console.error('[@enbox/auth] Sync failed:', err));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
await storage.set(STORAGE_KEYS.PREVIOUSLY_CONNECTED, 'true');
|
|
196
|
+
await storage.set(STORAGE_KEYS.ACTIVE_IDENTITY, connectedDid);
|
|
197
|
+
|
|
198
|
+
const identityInfo = {
|
|
199
|
+
didUri : connectedDid,
|
|
200
|
+
name : identity.metadata.name,
|
|
201
|
+
connectedDid : identity.metadata.connectedDid,
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const session = new AuthSession({
|
|
205
|
+
agent : userAgent,
|
|
206
|
+
did : connectedDid,
|
|
207
|
+
delegateDid,
|
|
208
|
+
identity : identityInfo,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
emitter.emit('identity-added', { identity: identityInfo });
|
|
212
|
+
emitter.emit('session-start', {
|
|
213
|
+
session: { did: connectedDid, delegateDid, identity: identityInfo },
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
return session;
|
|
217
|
+
}
|