@flow-industries/id 0.24.5 → 0.25.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/sdk/browser-session-route.d.ts +2 -1
- package/dist/sdk/browser-session-route.js +77 -54
- package/dist/sdk/client/create-flow.js +82 -21
- package/dist/sdk/client/session.js +1 -1
- package/dist/sdk/client/static-flow.js +1 -0
- package/dist/sdk/db/schema.d.ts +167 -0
- package/dist/sdk/db/schema.js +19 -0
- package/dist/sdk/session-route.js +10 -3
- package/dist/sdk/types/auth.d.ts +1 -0
- package/dist/sdk/types/browser-accounts.d.ts +9 -0
- package/dist/sdk/types/browser-accounts.js +0 -0
- package/dist/sdk/types/index.d.ts +1 -0
- package/dist/sdk/types/sdk.d.ts +1 -0
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export declare function safeReturnTo(value: string, audience: string, sessionPath?: string): string | null;
|
|
2
2
|
export declare function startBrowserSession(request: Request, audience: string, issuerUrl: string, body: {
|
|
3
|
-
action: "login" | "account";
|
|
3
|
+
action: "login" | "account" | "switch";
|
|
4
|
+
intent?: "login" | "manage" | "switch" | "add";
|
|
4
5
|
redirect?: boolean;
|
|
5
6
|
returnTo?: string;
|
|
6
7
|
signUp?: boolean;
|
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { BROWSER_FLOW_TTL_MS, browserSigningSchema } from "./browser-contract";
|
|
3
3
|
import { clearCookieString, cookieNamesFor, parseCookieHeader, serializeCookie, } from "./cookies";
|
|
4
4
|
import { fetchIssuerApi, resolveIssuerApiUrl } from "./issuer-api";
|
|
5
|
-
import { claimsToUser, sessionCookies } from "./session-core";
|
|
5
|
+
import { claimsToUser, resolveSessionCore, sessionCookies, } from "./session-core";
|
|
6
6
|
import { verifyFlowJWT } from "./verify";
|
|
7
7
|
const opaquePattern = /^[A-Za-z0-9_-]{43}$/;
|
|
8
8
|
const pendingSchema = z.object({
|
|
@@ -10,6 +10,7 @@ const pendingSchema = z.object({
|
|
|
10
10
|
verifier: z.string().regex(opaquePattern),
|
|
11
11
|
returnTo: z.string(),
|
|
12
12
|
expires: z.number(),
|
|
13
|
+
manageAccount: z.boolean().optional(),
|
|
13
14
|
});
|
|
14
15
|
const exchangeSchema = z.object({
|
|
15
16
|
jwt: z.string(),
|
|
@@ -62,8 +63,13 @@ export function safeReturnTo(value, audience, sessionPath = "/flow/session") {
|
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
65
|
export async function startBrowserSession(request, audience, issuerUrl, body, apiUrl = resolveIssuerApiUrl(issuerUrl)) {
|
|
66
|
+
const sessionPath = new URL(request.url).pathname;
|
|
67
|
+
const returnTo = safeReturnTo(body.returnTo ?? "/", audience, sessionPath);
|
|
68
|
+
if (!returnTo)
|
|
69
|
+
return reply({ error: "invalid_return_destination" }, 400);
|
|
65
70
|
const cookies = parseCookieHeader(request.headers.get("Cookie"));
|
|
66
|
-
const
|
|
71
|
+
const refreshCookieName = cookieNamesFor(audience).refresh;
|
|
72
|
+
const refresh = cookies[refreshCookieName];
|
|
67
73
|
const headers = new Headers({ "Content-Type": "application/json" });
|
|
68
74
|
if (refresh)
|
|
69
75
|
headers.set("Authorization", `Bearer ${refresh}`);
|
|
@@ -72,65 +78,80 @@ export async function startBrowserSession(request, audience, issuerUrl, body, ap
|
|
|
72
78
|
if (value)
|
|
73
79
|
headers.set(name, value);
|
|
74
80
|
}
|
|
81
|
+
let manageAccount = false;
|
|
82
|
+
let sessionUpdates = [];
|
|
75
83
|
if (body.action === "account") {
|
|
76
84
|
if (!refresh)
|
|
77
85
|
return reply({ error: "signed_out" }, 401);
|
|
78
|
-
const
|
|
86
|
+
const resolved = await resolveSessionCore(request.headers.get("Cookie"), audience, issuerUrl, apiUrl);
|
|
87
|
+
sessionUpdates = resolved.setCookies;
|
|
88
|
+
if (!resolved.session)
|
|
89
|
+
return reply({ error: "account_unavailable" }, 401, sessionUpdates);
|
|
90
|
+
manageAccount = !resolved.session.user.isGuest;
|
|
91
|
+
const refreshedCookie = sessionUpdates.find((cookie) => cookie.startsWith(`${refreshCookieName}=`));
|
|
92
|
+
if (refreshedCookie) {
|
|
93
|
+
const token = parseCookieHeader(refreshedCookie)[refreshCookieName];
|
|
94
|
+
if (token)
|
|
95
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
let intent = body.intent ?? "login";
|
|
100
|
+
if (body.action === "account")
|
|
101
|
+
intent = manageAccount ? "manage" : "login";
|
|
102
|
+
if (body.action === "switch")
|
|
103
|
+
intent = "switch";
|
|
104
|
+
const state = secret();
|
|
105
|
+
const verifier = secret();
|
|
106
|
+
const callback = `${audience}${sessionPath}`;
|
|
107
|
+
const response = await fetchIssuerApi(`${apiUrl}/api/auth/browser/transaction`, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers,
|
|
110
|
+
body: JSON.stringify({
|
|
111
|
+
audience,
|
|
112
|
+
callback,
|
|
113
|
+
state,
|
|
114
|
+
challenge: await pkce(verifier),
|
|
115
|
+
signUp: body.signUp ?? false,
|
|
116
|
+
intent,
|
|
117
|
+
accessKeyHash: body.accessKeyHash,
|
|
118
|
+
}),
|
|
119
|
+
});
|
|
79
120
|
if (!response.ok)
|
|
80
|
-
return reply({ error: "
|
|
81
|
-
const {
|
|
82
|
-
.object({
|
|
121
|
+
return reply({ error: "login_unavailable" }, response.status, sessionUpdates);
|
|
122
|
+
const { transaction } = z
|
|
123
|
+
.object({ transaction: z.string().regex(opaquePattern) })
|
|
83
124
|
.parse(await response.json());
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
125
|
+
const pending = btoa(encodeURIComponent(JSON.stringify({
|
|
126
|
+
state,
|
|
127
|
+
verifier,
|
|
128
|
+
returnTo,
|
|
129
|
+
manageAccount,
|
|
130
|
+
expires: Date.now() + BROWSER_FLOW_TTL_MS,
|
|
131
|
+
})));
|
|
132
|
+
const authorizeUrl = `${issuerUrl}/authorize?transaction=${transaction}`;
|
|
133
|
+
const pendingCookies = [
|
|
134
|
+
...sessionUpdates,
|
|
135
|
+
serializeCookie(cookieName(audience, state), pending, {
|
|
136
|
+
maxAge: BROWSER_FLOW_TTL_MS / 1000,
|
|
137
|
+
secure: audience.startsWith("https:"),
|
|
138
|
+
}),
|
|
139
|
+
];
|
|
140
|
+
if (body.redirect) {
|
|
141
|
+
const headers = new Headers({
|
|
142
|
+
Location: authorizeUrl,
|
|
143
|
+
"Cache-Control": "no-store",
|
|
144
|
+
"Referrer-Policy": "no-referrer",
|
|
93
145
|
});
|
|
94
|
-
|
|
146
|
+
for (const cookie of pendingCookies)
|
|
147
|
+
headers.append("Set-Cookie", cookie);
|
|
148
|
+
return new Response(null, { status: 303, headers });
|
|
149
|
+
}
|
|
150
|
+
return reply({ loginId: state, authorizeUrl, accountUrl: authorizeUrl }, 200, pendingCookies);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return reply({ error: "login_unavailable" }, 502, sessionUpdates);
|
|
95
154
|
}
|
|
96
|
-
const returnTo = safeReturnTo(body.returnTo ?? "/", audience, new URL(request.url).pathname);
|
|
97
|
-
if (!returnTo)
|
|
98
|
-
return reply({ error: "invalid_return_destination" }, 400);
|
|
99
|
-
const state = secret();
|
|
100
|
-
const verifier = secret();
|
|
101
|
-
const callback = `${audience}${new URL(request.url).pathname}`;
|
|
102
|
-
const response = await fetchIssuerApi(`${apiUrl}/api/auth/browser/transaction`, {
|
|
103
|
-
method: "POST",
|
|
104
|
-
headers,
|
|
105
|
-
body: JSON.stringify({
|
|
106
|
-
audience,
|
|
107
|
-
callback,
|
|
108
|
-
state,
|
|
109
|
-
challenge: await pkce(verifier),
|
|
110
|
-
signUp: body.signUp ?? false,
|
|
111
|
-
accessKeyHash: body.accessKeyHash,
|
|
112
|
-
}),
|
|
113
|
-
});
|
|
114
|
-
if (!response.ok)
|
|
115
|
-
return reply({ error: "login_unavailable" }, response.status);
|
|
116
|
-
const { transaction } = z
|
|
117
|
-
.object({ transaction: z.string().regex(opaquePattern) })
|
|
118
|
-
.parse(await response.json());
|
|
119
|
-
const pending = btoa(encodeURIComponent(JSON.stringify({
|
|
120
|
-
state,
|
|
121
|
-
verifier,
|
|
122
|
-
returnTo,
|
|
123
|
-
expires: Date.now() + BROWSER_FLOW_TTL_MS,
|
|
124
|
-
})));
|
|
125
|
-
return reply({
|
|
126
|
-
loginId: state,
|
|
127
|
-
authorizeUrl: `${issuerUrl}/authorize?transaction=${transaction}`,
|
|
128
|
-
}, 200, [
|
|
129
|
-
serializeCookie(cookieName(audience, state), pending, {
|
|
130
|
-
maxAge: BROWSER_FLOW_TTL_MS / 1000,
|
|
131
|
-
secure: audience.startsWith("https:"),
|
|
132
|
-
}),
|
|
133
|
-
]);
|
|
134
155
|
}
|
|
135
156
|
export async function finishBrowserSession(request, audience, issuerUrl, apiUrl = resolveIssuerApiUrl(issuerUrl)) {
|
|
136
157
|
const url = new URL(request.url);
|
|
@@ -200,6 +221,8 @@ export async function finishBrowserSession(request, audience, issuerUrl, apiUrl
|
|
|
200
221
|
headers.append("Set-Cookie", serializeCookie(`${cookieNamesFor(audience).refresh}.completion`, completion, { maxAge: 60, secure: audience.startsWith("https:") }));
|
|
201
222
|
}
|
|
202
223
|
headers.append("Set-Cookie", clear);
|
|
224
|
+
if (pending.manageAccount)
|
|
225
|
+
headers.set("Location", `${issuerUrl}/account`);
|
|
203
226
|
return new Response(null, { status: 303, headers });
|
|
204
227
|
}
|
|
205
228
|
catch {
|
|
@@ -112,11 +112,20 @@ export function createFlow(options = {}) {
|
|
|
112
112
|
* knows neither, and overwriting would erase state restored from IDB.
|
|
113
113
|
*/
|
|
114
114
|
let additionalSubject = null;
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
let sessionRevision = 0;
|
|
116
|
+
async function commitSession(session) {
|
|
117
|
+
const revision = ++sessionRevision;
|
|
118
|
+
const previousState = store.getSnapshot();
|
|
119
|
+
const previous = previousState.user?.id;
|
|
117
120
|
if (previous && previous !== session.user.id) {
|
|
118
121
|
store.setState({ credential: null, address: null });
|
|
119
|
-
|
|
122
|
+
await idb.delete("flow.activeCredential");
|
|
123
|
+
if (previousState.address) {
|
|
124
|
+
const { clearAccessKey } = await import("./access-key");
|
|
125
|
+
await clearAccessKey(previousState.address);
|
|
126
|
+
}
|
|
127
|
+
if (revision !== sessionRevision)
|
|
128
|
+
return;
|
|
120
129
|
}
|
|
121
130
|
const next = {
|
|
122
131
|
user: session.user,
|
|
@@ -147,6 +156,7 @@ export function createFlow(options = {}) {
|
|
|
147
156
|
}
|
|
148
157
|
}
|
|
149
158
|
async function recoverAccessKey(state, loginCompletion) {
|
|
159
|
+
const revision = sessionRevision;
|
|
150
160
|
const completionKey = "flow.pendingAccessKey.completion";
|
|
151
161
|
if (loginCompletion)
|
|
152
162
|
await idb.set(completionKey, {
|
|
@@ -164,10 +174,11 @@ export function createFlow(options = {}) {
|
|
|
164
174
|
throw new Error("Could not restore signing state");
|
|
165
175
|
// SAFETY: the app session handler returns the SDK protocol response.
|
|
166
176
|
const resolved = (await fresh.json());
|
|
167
|
-
if (
|
|
177
|
+
if (revision === sessionRevision &&
|
|
178
|
+
resolved.state?.user.id === completion.userId &&
|
|
168
179
|
resolved.state.credential &&
|
|
169
180
|
resolved.state.address) {
|
|
170
|
-
commitSession(resolved.state);
|
|
181
|
+
await commitSession(resolved.state);
|
|
171
182
|
const { finalizeAccessKey } = await import("./access-key");
|
|
172
183
|
await finalizeAccessKey({
|
|
173
184
|
address: resolved.state.address,
|
|
@@ -189,7 +200,13 @@ export function createFlow(options = {}) {
|
|
|
189
200
|
* false (never throws) when the visitor is signed out or the route is
|
|
190
201
|
* unreachable — the caller then falls back to ensureGuest()/login().
|
|
191
202
|
*/
|
|
203
|
+
let sessionRequest = 0;
|
|
204
|
+
let logoutInFlight = null;
|
|
205
|
+
let logoutGeneration = 0;
|
|
192
206
|
async function refreshViaSession() {
|
|
207
|
+
if (logoutInFlight)
|
|
208
|
+
return false;
|
|
209
|
+
const request = ++sessionRequest;
|
|
193
210
|
try {
|
|
194
211
|
const res = await fetch(sessionPath, {
|
|
195
212
|
headers: { Accept: "application/json" },
|
|
@@ -198,9 +215,9 @@ export function createFlow(options = {}) {
|
|
|
198
215
|
return false;
|
|
199
216
|
// SAFETY: the app's own /flow/session route answers this shape.
|
|
200
217
|
const { state, loginCompletion } = (await res.json());
|
|
201
|
-
if (!state)
|
|
218
|
+
if (!state || request !== sessionRequest)
|
|
202
219
|
return false;
|
|
203
|
-
commitSession(state);
|
|
220
|
+
await commitSession(state);
|
|
204
221
|
await recoverAccessKey(state, loginCompletion).catch(() => { });
|
|
205
222
|
return true;
|
|
206
223
|
}
|
|
@@ -240,7 +257,11 @@ export function createFlow(options = {}) {
|
|
|
240
257
|
// and the guard short-circuits a caller that arrives after a concurrent
|
|
241
258
|
// holder already minted a fresh JWT.
|
|
242
259
|
async function doRefresh() {
|
|
260
|
+
if (logoutInFlight)
|
|
261
|
+
return false;
|
|
243
262
|
return withOriginLock("flow.id.refresh", async () => {
|
|
263
|
+
if (logoutInFlight)
|
|
264
|
+
return false;
|
|
244
265
|
const current = store.getSnapshot().jwt;
|
|
245
266
|
if (current && !isExpiring(current))
|
|
246
267
|
return true;
|
|
@@ -290,18 +311,27 @@ export function createFlow(options = {}) {
|
|
|
290
311
|
* in-memory store (never IDB) — IDB is reserved for a real passkey credential.
|
|
291
312
|
*/
|
|
292
313
|
async function ensureGuest() {
|
|
314
|
+
if (logoutInFlight)
|
|
315
|
+
return false;
|
|
293
316
|
if (store.getSnapshot().user)
|
|
294
317
|
return true;
|
|
295
318
|
if (!guestInFlight) {
|
|
319
|
+
const generation = logoutGeneration;
|
|
296
320
|
guestInFlight = withOriginLock("flow.id.guest", async () => {
|
|
297
321
|
// Re-check under the lock: another tab may have minted the guest while
|
|
298
322
|
// we waited. Refresh-first (from this origin's session cookie)
|
|
299
323
|
// recovers an existing session instead of minting a duplicate guest.
|
|
324
|
+
if (logoutInFlight || generation !== logoutGeneration)
|
|
325
|
+
return false;
|
|
300
326
|
if (store.getSnapshot().user)
|
|
301
327
|
return true;
|
|
302
|
-
|
|
328
|
+
const restored = await doRefresh();
|
|
329
|
+
if (logoutInFlight || generation !== logoutGeneration)
|
|
330
|
+
return false;
|
|
331
|
+
if (restored)
|
|
303
332
|
return true;
|
|
304
333
|
try {
|
|
334
|
+
const request = ++sessionRequest;
|
|
305
335
|
const response = await fetch(sessionPath, {
|
|
306
336
|
method: "POST",
|
|
307
337
|
headers: { "Content-Type": "application/json" },
|
|
@@ -311,9 +341,9 @@ export function createFlow(options = {}) {
|
|
|
311
341
|
return false;
|
|
312
342
|
// SAFETY: the app session handler returns the SDK protocol response.
|
|
313
343
|
const result = (await response.json());
|
|
314
|
-
if (!result.state)
|
|
344
|
+
if (!result.state || request !== sessionRequest)
|
|
315
345
|
return false;
|
|
316
|
-
commitSession(result.state);
|
|
346
|
+
await commitSession(result.state);
|
|
317
347
|
return true;
|
|
318
348
|
}
|
|
319
349
|
catch {
|
|
@@ -329,13 +359,16 @@ export function createFlow(options = {}) {
|
|
|
329
359
|
void idb.pruneExpired("flow.pendingAccessKey.").catch(() => { });
|
|
330
360
|
if (options.autoRestore !== false) {
|
|
331
361
|
const restored = await refreshViaSession();
|
|
332
|
-
if (restored) {
|
|
362
|
+
if (restored && !logoutInFlight) {
|
|
363
|
+
const request = ++sessionRequest;
|
|
333
364
|
const response = await fetch(`${sessionPath}?refresh=1`);
|
|
334
365
|
if (response.ok) {
|
|
335
366
|
// SAFETY: the app session handler returns the SDK protocol response.
|
|
336
367
|
const result = (await response.json());
|
|
368
|
+
if (request !== sessionRequest)
|
|
369
|
+
return;
|
|
337
370
|
if (result.state)
|
|
338
|
-
commitSession(result.state);
|
|
371
|
+
await commitSession(result.state);
|
|
339
372
|
else
|
|
340
373
|
await runLogout(store);
|
|
341
374
|
}
|
|
@@ -357,11 +390,17 @@ export function createFlow(options = {}) {
|
|
|
357
390
|
}
|
|
358
391
|
/** Starts top-level authentication; successful navigation never resolves in this document. */
|
|
359
392
|
async function login(loginOpts = {}) {
|
|
393
|
+
return startLogin(loginOpts, "login");
|
|
394
|
+
}
|
|
395
|
+
async function switchAccounts(loginOpts = {}) {
|
|
396
|
+
return startLogin(loginOpts, "switch");
|
|
397
|
+
}
|
|
398
|
+
async function startLogin(loginOpts, intent) {
|
|
360
399
|
if (window.top !== window) {
|
|
361
400
|
throw new Error("Open sign-in from the containing app.");
|
|
362
401
|
}
|
|
363
402
|
let preparation;
|
|
364
|
-
if (accessKeyOptions) {
|
|
403
|
+
if (accessKeyOptions && intent === "login") {
|
|
365
404
|
const chain = getChain();
|
|
366
405
|
if (!chain)
|
|
367
406
|
throw new Error("accessKey requires a configured chain");
|
|
@@ -372,7 +411,7 @@ export function createFlow(options = {}) {
|
|
|
372
411
|
method: "POST",
|
|
373
412
|
headers: { "Content-Type": "application/json" },
|
|
374
413
|
body: JSON.stringify({
|
|
375
|
-
action:
|
|
414
|
+
action: intent,
|
|
376
415
|
accessKeyHash: preparation?.accessKeyHash,
|
|
377
416
|
signUp: loginOpts.signUp,
|
|
378
417
|
returnTo: loginOpts.returnTo ??
|
|
@@ -396,6 +435,7 @@ export function createFlow(options = {}) {
|
|
|
396
435
|
function openAccount() {
|
|
397
436
|
const url = new URL(sessionPath, window.location.origin);
|
|
398
437
|
url.searchParams.set("action", "account");
|
|
438
|
+
url.searchParams.set("returnTo", `${window.location.pathname}${window.location.search}${window.location.hash}`);
|
|
399
439
|
const tab = window.open(url.href, "_blank");
|
|
400
440
|
if (!tab)
|
|
401
441
|
return false;
|
|
@@ -403,19 +443,37 @@ export function createFlow(options = {}) {
|
|
|
403
443
|
return true;
|
|
404
444
|
}
|
|
405
445
|
async function logout() {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
446
|
+
if (logoutInFlight)
|
|
447
|
+
return logoutInFlight;
|
|
448
|
+
++logoutGeneration;
|
|
449
|
+
++sessionRequest;
|
|
450
|
+
++sessionRevision;
|
|
451
|
+
logoutInFlight = (async () => {
|
|
452
|
+
const response = await fetch(sessionPath, { method: "DELETE" });
|
|
453
|
+
if (!response.ok)
|
|
454
|
+
throw new Error("Could not sign out. Try again.");
|
|
455
|
+
++sessionRequest;
|
|
456
|
+
++sessionRevision;
|
|
457
|
+
await runLogout(store);
|
|
458
|
+
additionalSubject = null;
|
|
459
|
+
})();
|
|
460
|
+
try {
|
|
461
|
+
await logoutInFlight;
|
|
462
|
+
}
|
|
463
|
+
finally {
|
|
464
|
+
logoutInFlight = null;
|
|
465
|
+
}
|
|
411
466
|
if (options.autoGuest)
|
|
412
467
|
await ensureGuest();
|
|
413
468
|
}
|
|
414
469
|
let focusRefresh = null;
|
|
415
470
|
const refreshOnFocus = () => {
|
|
416
|
-
if (document.visibilityState === "hidden" || focusRefresh)
|
|
471
|
+
if (document.visibilityState === "hidden" || focusRefresh || logoutInFlight)
|
|
417
472
|
return;
|
|
418
473
|
focusRefresh = withOriginLock("flow.id.refresh", async () => {
|
|
474
|
+
if (logoutInFlight)
|
|
475
|
+
return;
|
|
476
|
+
const request = ++sessionRequest;
|
|
419
477
|
const response = await fetch(`${sessionPath}?refresh=1`, {
|
|
420
478
|
headers: { Accept: "application/json" },
|
|
421
479
|
});
|
|
@@ -423,8 +481,10 @@ export function createFlow(options = {}) {
|
|
|
423
481
|
return;
|
|
424
482
|
// SAFETY: the app session handler returns the SDK protocol response.
|
|
425
483
|
const result = (await response.json());
|
|
484
|
+
if (request !== sessionRequest)
|
|
485
|
+
return;
|
|
426
486
|
if (result.state)
|
|
427
|
-
commitSession(result.state);
|
|
487
|
+
await commitSession(result.state);
|
|
428
488
|
else
|
|
429
489
|
await runLogout(store);
|
|
430
490
|
})
|
|
@@ -456,6 +516,7 @@ export function createFlow(options = {}) {
|
|
|
456
516
|
},
|
|
457
517
|
login,
|
|
458
518
|
openAccount,
|
|
519
|
+
switchAccounts,
|
|
459
520
|
logout,
|
|
460
521
|
restore: refreshViaSession,
|
|
461
522
|
ensureGuest,
|
|
@@ -28,12 +28,12 @@ export async function restoreCredential(store) {
|
|
|
28
28
|
*/
|
|
29
29
|
export async function runLogout(store) {
|
|
30
30
|
const state = store.getSnapshot();
|
|
31
|
+
store.setState(initialFlowState);
|
|
31
32
|
if (state.address) {
|
|
32
33
|
const { clearAccessKey } = await import("./access-key");
|
|
33
34
|
await clearAccessKey(state.address);
|
|
34
35
|
}
|
|
35
36
|
await idb.delete(CREDENTIAL_KEY);
|
|
36
|
-
store.setState(initialFlowState);
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
39
|
* Derives the user's wallet address from their passkey public key by
|
|
@@ -45,6 +45,7 @@ export function createStaticFlow(state, options = {}) {
|
|
|
45
45
|
},
|
|
46
46
|
login: () => unavailable("login"),
|
|
47
47
|
openAccount: () => unavailable("openAccount"),
|
|
48
|
+
switchAccounts: () => unavailable("switchAccounts"),
|
|
48
49
|
logout: () => unavailable("logout"),
|
|
49
50
|
restore: async () => false,
|
|
50
51
|
ensureGuest: () => unavailable("ensureGuest"),
|
package/dist/sdk/db/schema.d.ts
CHANGED
|
@@ -651,6 +651,173 @@ export declare const session: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
|
651
651
|
};
|
|
652
652
|
dialect: "pg";
|
|
653
653
|
}>;
|
|
654
|
+
export declare const browserAccountSet: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
655
|
+
name: "browser_account_set";
|
|
656
|
+
schema: undefined;
|
|
657
|
+
columns: {
|
|
658
|
+
initialized: import("drizzle-orm/pg-core").PgColumn<{
|
|
659
|
+
name: "initialized";
|
|
660
|
+
tableName: "browser_account_set";
|
|
661
|
+
dataType: "boolean";
|
|
662
|
+
columnType: "PgBoolean";
|
|
663
|
+
data: boolean;
|
|
664
|
+
driverParam: boolean;
|
|
665
|
+
notNull: true;
|
|
666
|
+
hasDefault: true;
|
|
667
|
+
isPrimaryKey: false;
|
|
668
|
+
isAutoincrement: false;
|
|
669
|
+
hasRuntimeDefault: false;
|
|
670
|
+
enumValues: undefined;
|
|
671
|
+
baseColumn: never;
|
|
672
|
+
identity: undefined;
|
|
673
|
+
generated: undefined;
|
|
674
|
+
}, {}, {}>;
|
|
675
|
+
id: import("drizzle-orm/pg-core").PgColumn<{
|
|
676
|
+
name: "id";
|
|
677
|
+
tableName: "browser_account_set";
|
|
678
|
+
dataType: "string";
|
|
679
|
+
columnType: "PgText";
|
|
680
|
+
data: string;
|
|
681
|
+
driverParam: string;
|
|
682
|
+
notNull: true;
|
|
683
|
+
hasDefault: false;
|
|
684
|
+
isPrimaryKey: true;
|
|
685
|
+
isAutoincrement: false;
|
|
686
|
+
hasRuntimeDefault: false;
|
|
687
|
+
enumValues: [string, ...string[]];
|
|
688
|
+
baseColumn: never;
|
|
689
|
+
identity: undefined;
|
|
690
|
+
generated: undefined;
|
|
691
|
+
}, {}, {}>;
|
|
692
|
+
activeUserId: import("drizzle-orm/pg-core").PgColumn<{
|
|
693
|
+
name: "active_user_id";
|
|
694
|
+
tableName: "browser_account_set";
|
|
695
|
+
dataType: "string";
|
|
696
|
+
columnType: "PgText";
|
|
697
|
+
data: string;
|
|
698
|
+
driverParam: string;
|
|
699
|
+
notNull: false;
|
|
700
|
+
hasDefault: false;
|
|
701
|
+
isPrimaryKey: false;
|
|
702
|
+
isAutoincrement: false;
|
|
703
|
+
hasRuntimeDefault: false;
|
|
704
|
+
enumValues: [string, ...string[]];
|
|
705
|
+
baseColumn: never;
|
|
706
|
+
identity: undefined;
|
|
707
|
+
generated: undefined;
|
|
708
|
+
}, {}, {}>;
|
|
709
|
+
updatedAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
710
|
+
name: "updated_at";
|
|
711
|
+
tableName: "browser_account_set";
|
|
712
|
+
dataType: "date";
|
|
713
|
+
columnType: "PgTimestamp";
|
|
714
|
+
data: Date;
|
|
715
|
+
driverParam: string;
|
|
716
|
+
notNull: true;
|
|
717
|
+
hasDefault: true;
|
|
718
|
+
isPrimaryKey: false;
|
|
719
|
+
isAutoincrement: false;
|
|
720
|
+
hasRuntimeDefault: false;
|
|
721
|
+
enumValues: undefined;
|
|
722
|
+
baseColumn: never;
|
|
723
|
+
identity: undefined;
|
|
724
|
+
generated: undefined;
|
|
725
|
+
}, {}, {}>;
|
|
726
|
+
};
|
|
727
|
+
dialect: "pg";
|
|
728
|
+
}>;
|
|
729
|
+
export declare const browserAccountMember: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
730
|
+
name: "browser_account_member";
|
|
731
|
+
schema: undefined;
|
|
732
|
+
columns: {
|
|
733
|
+
setId: import("drizzle-orm/pg-core").PgColumn<{
|
|
734
|
+
name: "set_id";
|
|
735
|
+
tableName: "browser_account_member";
|
|
736
|
+
dataType: "string";
|
|
737
|
+
columnType: "PgText";
|
|
738
|
+
data: string;
|
|
739
|
+
driverParam: string;
|
|
740
|
+
notNull: true;
|
|
741
|
+
hasDefault: false;
|
|
742
|
+
isPrimaryKey: false;
|
|
743
|
+
isAutoincrement: false;
|
|
744
|
+
hasRuntimeDefault: false;
|
|
745
|
+
enumValues: [string, ...string[]];
|
|
746
|
+
baseColumn: never;
|
|
747
|
+
identity: undefined;
|
|
748
|
+
generated: undefined;
|
|
749
|
+
}, {}, {}>;
|
|
750
|
+
userId: import("drizzle-orm/pg-core").PgColumn<{
|
|
751
|
+
name: "user_id";
|
|
752
|
+
tableName: "browser_account_member";
|
|
753
|
+
dataType: "string";
|
|
754
|
+
columnType: "PgText";
|
|
755
|
+
data: string;
|
|
756
|
+
driverParam: string;
|
|
757
|
+
notNull: true;
|
|
758
|
+
hasDefault: false;
|
|
759
|
+
isPrimaryKey: false;
|
|
760
|
+
isAutoincrement: false;
|
|
761
|
+
hasRuntimeDefault: false;
|
|
762
|
+
enumValues: [string, ...string[]];
|
|
763
|
+
baseColumn: never;
|
|
764
|
+
identity: undefined;
|
|
765
|
+
generated: undefined;
|
|
766
|
+
}, {}, {}>;
|
|
767
|
+
sessionId: import("drizzle-orm/pg-core").PgColumn<{
|
|
768
|
+
name: "session_id";
|
|
769
|
+
tableName: "browser_account_member";
|
|
770
|
+
dataType: "string";
|
|
771
|
+
columnType: "PgText";
|
|
772
|
+
data: string;
|
|
773
|
+
driverParam: string;
|
|
774
|
+
notNull: true;
|
|
775
|
+
hasDefault: false;
|
|
776
|
+
isPrimaryKey: false;
|
|
777
|
+
isAutoincrement: false;
|
|
778
|
+
hasRuntimeDefault: false;
|
|
779
|
+
enumValues: [string, ...string[]];
|
|
780
|
+
baseColumn: never;
|
|
781
|
+
identity: undefined;
|
|
782
|
+
generated: undefined;
|
|
783
|
+
}, {}, {}>;
|
|
784
|
+
userEpoch: import("drizzle-orm/pg-core").PgColumn<{
|
|
785
|
+
name: "user_epoch";
|
|
786
|
+
tableName: "browser_account_member";
|
|
787
|
+
dataType: "number";
|
|
788
|
+
columnType: "PgInteger";
|
|
789
|
+
data: number;
|
|
790
|
+
driverParam: string | number;
|
|
791
|
+
notNull: true;
|
|
792
|
+
hasDefault: false;
|
|
793
|
+
isPrimaryKey: false;
|
|
794
|
+
isAutoincrement: false;
|
|
795
|
+
hasRuntimeDefault: false;
|
|
796
|
+
enumValues: undefined;
|
|
797
|
+
baseColumn: never;
|
|
798
|
+
identity: undefined;
|
|
799
|
+
generated: undefined;
|
|
800
|
+
}, {}, {}>;
|
|
801
|
+
lastUsedAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
802
|
+
name: "last_used_at";
|
|
803
|
+
tableName: "browser_account_member";
|
|
804
|
+
dataType: "date";
|
|
805
|
+
columnType: "PgTimestamp";
|
|
806
|
+
data: Date;
|
|
807
|
+
driverParam: string;
|
|
808
|
+
notNull: true;
|
|
809
|
+
hasDefault: true;
|
|
810
|
+
isPrimaryKey: false;
|
|
811
|
+
isAutoincrement: false;
|
|
812
|
+
hasRuntimeDefault: false;
|
|
813
|
+
enumValues: undefined;
|
|
814
|
+
baseColumn: never;
|
|
815
|
+
identity: undefined;
|
|
816
|
+
generated: undefined;
|
|
817
|
+
}, {}, {}>;
|
|
818
|
+
};
|
|
819
|
+
dialect: "pg";
|
|
820
|
+
}>;
|
|
654
821
|
export declare const refreshToken: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
655
822
|
name: "refresh_token";
|
|
656
823
|
schema: undefined;
|
package/dist/sdk/db/schema.js
CHANGED
|
@@ -79,6 +79,25 @@ export const session = pgTable("session", {
|
|
|
79
79
|
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
80
80
|
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
|
81
81
|
}, (table) => [index("session_userId_idx").on(table.userId)]);
|
|
82
|
+
export const browserAccountSet = pgTable("browser_account_set", {
|
|
83
|
+
initialized: boolean("initialized").notNull().default(false),
|
|
84
|
+
id: text("id").primaryKey(),
|
|
85
|
+
activeUserId: text("active_user_id"),
|
|
86
|
+
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
|
87
|
+
});
|
|
88
|
+
export const browserAccountMember = pgTable("browser_account_member", {
|
|
89
|
+
setId: text("set_id")
|
|
90
|
+
.notNull()
|
|
91
|
+
.references(() => browserAccountSet.id, { onDelete: "cascade" }),
|
|
92
|
+
userId: text("user_id")
|
|
93
|
+
.notNull()
|
|
94
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
95
|
+
sessionId: text("session_id")
|
|
96
|
+
.notNull()
|
|
97
|
+
.references(() => session.id, { onDelete: "cascade" }),
|
|
98
|
+
userEpoch: integer("user_epoch").notNull(),
|
|
99
|
+
lastUsedAt: timestamp("last_used_at").notNull().defaultNow(),
|
|
100
|
+
}, (table) => [primaryKey({ columns: [table.setId, table.userId] })]);
|
|
82
101
|
// First-party session persistence: consumer apps store the opaque token in their
|
|
83
102
|
// OWN origin and mint access JWTs via POST /api/session/refresh — no third-party
|
|
84
103
|
// id.flow.industries cookie, so it survives iOS ITP. Rotated on every use.
|
|
@@ -32,7 +32,8 @@ const installBodySchema = z.object({
|
|
|
32
32
|
refreshToken: z.string().min(1).optional(),
|
|
33
33
|
jwt: z.string().min(1).optional(),
|
|
34
34
|
mint: z.array(z.string()).optional(),
|
|
35
|
-
action: z.enum(["login", "account", "guest"]).optional(),
|
|
35
|
+
action: z.enum(["login", "account", "switch", "guest"]).optional(),
|
|
36
|
+
intent: z.enum(["login", "manage", "switch", "add"]).optional(),
|
|
36
37
|
returnTo: z.string().optional(),
|
|
37
38
|
signUp: z.boolean().optional(),
|
|
38
39
|
accessKeyHash: z
|
|
@@ -95,7 +96,11 @@ export async function handleSessionRequest(request, opts = {}) {
|
|
|
95
96
|
case "GET": {
|
|
96
97
|
if (callback.searchParams.get("action") === "account") {
|
|
97
98
|
try {
|
|
98
|
-
return await startBrowserSession(request, audience, issuerUrl, {
|
|
99
|
+
return await startBrowserSession(request, audience, issuerUrl, {
|
|
100
|
+
action: "account",
|
|
101
|
+
redirect: true,
|
|
102
|
+
returnTo: callback.searchParams.get("returnTo") ?? "/",
|
|
103
|
+
}, apiUrl);
|
|
99
104
|
}
|
|
100
105
|
catch {
|
|
101
106
|
return json({ state: null }, 502);
|
|
@@ -134,7 +139,9 @@ export async function handleSessionRequest(request, opts = {}) {
|
|
|
134
139
|
case "POST": {
|
|
135
140
|
const parsed = installBodySchema.safeParse(await request.json().catch(() => null));
|
|
136
141
|
const body = parsed.success ? parsed.data : null;
|
|
137
|
-
if (body?.action === "login" ||
|
|
142
|
+
if (body?.action === "login" ||
|
|
143
|
+
body?.action === "account" ||
|
|
144
|
+
body?.action === "switch") {
|
|
138
145
|
try {
|
|
139
146
|
return await startBrowserSession(request, audience, issuerUrl, {
|
|
140
147
|
...body,
|
package/dist/sdk/types/auth.d.ts
CHANGED
|
@@ -176,6 +176,7 @@ export type AccountRegistration = {
|
|
|
176
176
|
credential: FlowCredential | null;
|
|
177
177
|
};
|
|
178
178
|
export type AuthorizationTransaction = {
|
|
179
|
+
intent: "login" | "manage" | "switch" | "add";
|
|
179
180
|
expiresAt: number;
|
|
180
181
|
accessKeyHash?: string;
|
|
181
182
|
credentialId?: string;
|
|
File without changes
|
|
@@ -2,6 +2,7 @@ export type { AccountDeleteRequest, AccountDeleteResponse, AccountErasureCounts,
|
|
|
2
2
|
export type { ActionCurve, ActionEventRequest, ActionEventResponse, ActionsApi, ActiveActionState, FinishActionRequest, ListStudySessionsOptions, ListStudySubjectsOptions, StartActionInput, StartActionResult, StudyApi, StudySubjectResponse, UseActiveActionOptions, XpEstimateContext, } from "./actions";
|
|
3
3
|
export type { AtprotoCompletion, AtprotoStartRequest, AtprotoStatus, } from "./atproto";
|
|
4
4
|
export type { AccountRegistration, AccountSession, AccountSessionsResponse, AdditionalSession, AuthConfig, AuthorizationTransaction, AuthPageBootstrap, AuthResponse, AuthResponseWithWebAuthn, BioNormalization, BioRejection, BioWriteRefusal, BioWriteResult, FlowCredential, FlowUser, PasskeyEnrollment, PasskeyPluginOptions, RevokeAllSessionsResponse, RevokeSessionResponse, SecurityActivityEntry, SecurityActivityResponse, SessionUser, VerifiedFlowJWT, VerifyOptions, WebAuthnSignature, } from "./auth";
|
|
5
|
+
export type { BrowserAccount, BrowserAccounts } from "./browser-accounts";
|
|
5
6
|
export type { BodyRegion, CosmeticItem, CosmeticMaterial, CosmeticPaint, CosmeticSlot, EquipConflict, EquippedCosmetic, EquippedCosmetics, EquipRegion, PaintSlot, SettingsConflict, } from "./cosmetics";
|
|
6
7
|
export { BODY_REGIONS, COSMETIC_MATERIALS } from "./cosmetics";
|
|
7
8
|
export type { AccountDataDownload, AccountDataIdentity, BoundaryError, DialogCustomFeatures, DialogCustomLabels, DialogError, DialogReferrer, DialogState, Loadable, ProfileData, ProfileIdentity, SignInWithEmailProps, SignUpProps, } from "./dialog";
|
package/dist/sdk/types/sdk.d.ts
CHANGED
|
@@ -332,6 +332,7 @@ export type Flow = {
|
|
|
332
332
|
readonly isGuest: boolean;
|
|
333
333
|
login(options?: LoginOptions): Promise<never>;
|
|
334
334
|
openAccount(): boolean;
|
|
335
|
+
switchAccounts(options?: Pick<LoginOptions, "returnTo">): Promise<never>;
|
|
335
336
|
logout(): Promise<void>;
|
|
336
337
|
restore(): Promise<boolean>;
|
|
337
338
|
/**
|