@flow-industries/id 0.24.5 → 0.26.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/README.md +3 -1
- package/contracts/v1/sdk-exports.json +15 -0
- package/dist/sdk/browser-session-route.d.ts +3 -1
- package/dist/sdk/browser-session-route.js +88 -54
- package/dist/sdk/client/create-flow.js +99 -21
- package/dist/sdk/client/index.d.ts +1 -1
- package/dist/sdk/client/profile-button.d.ts +1 -1
- package/dist/sdk/client/profile-button.js +29 -174
- package/dist/sdk/client/session.js +1 -1
- package/dist/sdk/client/static-flow.js +3 -0
- package/dist/sdk/db/schema.d.ts +167 -0
- package/dist/sdk/db/schema.js +19 -0
- package/dist/sdk/profile.css +2 -0
- package/dist/sdk/react/account-menu.d.ts +4 -0
- package/dist/sdk/react/account-menu.js +82 -0
- package/dist/sdk/react/profile-button.d.ts +5 -3
- package/dist/sdk/react/profile-button.js +51 -25
- package/dist/sdk/react/profile-renderer.d.ts +2 -0
- package/dist/sdk/react/profile-renderer.js +19 -0
- package/dist/sdk/react/profile-theme.d.ts +3 -0
- package/dist/sdk/react/profile-theme.js +19 -0
- package/dist/sdk/session-core.d.ts +1 -1
- package/dist/sdk/session-core.js +2 -1
- package/dist/sdk/session-route.js +50 -3
- package/dist/sdk/types/auth.d.ts +2 -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 +2 -1
- package/dist/sdk/types/sdk.d.ts +25 -1
- package/package.json +15 -6
package/README.md
CHANGED
|
@@ -127,7 +127,9 @@ and navigates to Flow ID's `/authorize` page. Completion returns to the saved
|
|
|
127
127
|
app path; read the new session after navigation instead of continuing after
|
|
128
128
|
`await flow.login()`.
|
|
129
129
|
|
|
130
|
-
`ProfileButton`
|
|
130
|
+
`ProfileButton` opens the shared account dropdown, with saved accounts, account
|
|
131
|
+
switching, add-account, management, and sign-out actions. `flow.openAccount()`
|
|
132
|
+
opens `/account` in a normal new tab.
|
|
131
133
|
The source app stays loaded and refreshes its session on refocus. Sign-in and
|
|
132
134
|
own-account management no longer use an embedded dialog. The remaining dialog supports signing, public profiles, and widgets. SDK `ConnectResponse` remains a deprecated type alias for source compatibility; sign-in itself returns through the first-party callback. See the
|
|
133
135
|
[integration guide](docs/integration-guide.md) for server wiring and migration.
|
|
@@ -92,6 +92,16 @@
|
|
|
92
92
|
"type": false,
|
|
93
93
|
"value": true
|
|
94
94
|
},
|
|
95
|
+
{
|
|
96
|
+
"name": "BrowserAccount",
|
|
97
|
+
"type": true,
|
|
98
|
+
"value": false
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"name": "BrowserAccounts",
|
|
102
|
+
"type": true,
|
|
103
|
+
"value": false
|
|
104
|
+
},
|
|
95
105
|
{
|
|
96
106
|
"name": "catalogFieldIndex",
|
|
97
107
|
"type": false,
|
|
@@ -607,6 +617,11 @@
|
|
|
607
617
|
"type": true,
|
|
608
618
|
"value": false
|
|
609
619
|
},
|
|
620
|
+
{
|
|
621
|
+
"name": "SwitchAccountOptions",
|
|
622
|
+
"type": true,
|
|
623
|
+
"value": false
|
|
624
|
+
},
|
|
610
625
|
{
|
|
611
626
|
"name": "UpdateStudySubjectInput",
|
|
612
627
|
"type": true,
|
|
@@ -1,8 +1,10 @@
|
|
|
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;
|
|
7
|
+
userId?: string;
|
|
6
8
|
signUp?: boolean;
|
|
7
9
|
accessKeyHash?: string;
|
|
8
10
|
}, apiUrl?: string): Promise<Response>;
|
|
@@ -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,10 +10,12 @@ 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(),
|
|
16
17
|
refreshToken: z.string(),
|
|
18
|
+
accountsGrant: z.string().regex(opaquePattern).optional(),
|
|
17
19
|
webauthn: browserSigningSchema.optional(),
|
|
18
20
|
});
|
|
19
21
|
function base64Url(bytes) {
|
|
@@ -62,8 +64,13 @@ export function safeReturnTo(value, audience, sessionPath = "/flow/session") {
|
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
export async function startBrowserSession(request, audience, issuerUrl, body, apiUrl = resolveIssuerApiUrl(issuerUrl)) {
|
|
67
|
+
const sessionPath = new URL(request.url).pathname;
|
|
68
|
+
const returnTo = safeReturnTo(body.returnTo ?? "/", audience, sessionPath);
|
|
69
|
+
if (!returnTo)
|
|
70
|
+
return reply({ error: "invalid_return_destination" }, 400);
|
|
65
71
|
const cookies = parseCookieHeader(request.headers.get("Cookie"));
|
|
66
|
-
const
|
|
72
|
+
const refreshCookieName = cookieNamesFor(audience).refresh;
|
|
73
|
+
const refresh = cookies[refreshCookieName];
|
|
67
74
|
const headers = new Headers({ "Content-Type": "application/json" });
|
|
68
75
|
if (refresh)
|
|
69
76
|
headers.set("Authorization", `Bearer ${refresh}`);
|
|
@@ -72,65 +79,81 @@ export async function startBrowserSession(request, audience, issuerUrl, body, ap
|
|
|
72
79
|
if (value)
|
|
73
80
|
headers.set(name, value);
|
|
74
81
|
}
|
|
82
|
+
let manageAccount = false;
|
|
83
|
+
let sessionUpdates = [];
|
|
75
84
|
if (body.action === "account") {
|
|
76
85
|
if (!refresh)
|
|
77
86
|
return reply({ error: "signed_out" }, 401);
|
|
78
|
-
const
|
|
87
|
+
const resolved = await resolveSessionCore(request.headers.get("Cookie"), audience, issuerUrl, apiUrl);
|
|
88
|
+
sessionUpdates = resolved.setCookies;
|
|
89
|
+
if (!resolved.session)
|
|
90
|
+
return reply({ error: "account_unavailable" }, 401, sessionUpdates);
|
|
91
|
+
manageAccount = !resolved.session.user.isGuest;
|
|
92
|
+
const refreshedCookie = sessionUpdates.find((cookie) => cookie.startsWith(`${refreshCookieName}=`));
|
|
93
|
+
if (refreshedCookie) {
|
|
94
|
+
const token = parseCookieHeader(refreshedCookie)[refreshCookieName];
|
|
95
|
+
if (token)
|
|
96
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
let intent = body.intent ?? "login";
|
|
101
|
+
if (body.action === "account")
|
|
102
|
+
intent = manageAccount ? "manage" : "login";
|
|
103
|
+
if (body.action === "switch")
|
|
104
|
+
intent = "switch";
|
|
105
|
+
const state = secret();
|
|
106
|
+
const verifier = secret();
|
|
107
|
+
const callback = `${audience}${sessionPath}`;
|
|
108
|
+
const response = await fetchIssuerApi(`${apiUrl}/api/auth/browser/transaction`, {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers,
|
|
111
|
+
body: JSON.stringify({
|
|
112
|
+
audience,
|
|
113
|
+
callback,
|
|
114
|
+
state,
|
|
115
|
+
challenge: await pkce(verifier),
|
|
116
|
+
signUp: body.signUp ?? false,
|
|
117
|
+
targetUserId: body.action === "switch" ? body.userId : undefined,
|
|
118
|
+
intent,
|
|
119
|
+
accessKeyHash: body.accessKeyHash,
|
|
120
|
+
}),
|
|
121
|
+
});
|
|
79
122
|
if (!response.ok)
|
|
80
|
-
return reply({ error: "
|
|
81
|
-
const {
|
|
82
|
-
.object({
|
|
123
|
+
return reply({ error: "login_unavailable" }, response.status, sessionUpdates);
|
|
124
|
+
const { transaction } = z
|
|
125
|
+
.object({ transaction: z.string().regex(opaquePattern) })
|
|
83
126
|
.parse(await response.json());
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
127
|
+
const pending = btoa(encodeURIComponent(JSON.stringify({
|
|
128
|
+
state,
|
|
129
|
+
verifier,
|
|
130
|
+
returnTo,
|
|
131
|
+
manageAccount,
|
|
132
|
+
expires: Date.now() + BROWSER_FLOW_TTL_MS,
|
|
133
|
+
})));
|
|
134
|
+
const authorizeUrl = `${issuerUrl}/authorize?transaction=${transaction}`;
|
|
135
|
+
const pendingCookies = [
|
|
136
|
+
...sessionUpdates,
|
|
137
|
+
serializeCookie(cookieName(audience, state), pending, {
|
|
138
|
+
maxAge: BROWSER_FLOW_TTL_MS / 1000,
|
|
139
|
+
secure: audience.startsWith("https:"),
|
|
140
|
+
}),
|
|
141
|
+
];
|
|
142
|
+
if (body.redirect) {
|
|
143
|
+
const headers = new Headers({
|
|
144
|
+
Location: authorizeUrl,
|
|
145
|
+
"Cache-Control": "no-store",
|
|
146
|
+
"Referrer-Policy": "no-referrer",
|
|
93
147
|
});
|
|
94
|
-
|
|
148
|
+
for (const cookie of pendingCookies)
|
|
149
|
+
headers.append("Set-Cookie", cookie);
|
|
150
|
+
return new Response(null, { status: 303, headers });
|
|
151
|
+
}
|
|
152
|
+
return reply({ loginId: state, authorizeUrl, accountUrl: authorizeUrl }, 200, pendingCookies);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return reply({ error: "login_unavailable" }, 502, sessionUpdates);
|
|
95
156
|
}
|
|
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
157
|
}
|
|
135
158
|
export async function finishBrowserSession(request, audience, issuerUrl, apiUrl = resolveIssuerApiUrl(issuerUrl)) {
|
|
136
159
|
const url = new URL(request.url);
|
|
@@ -190,6 +213,15 @@ export async function finishBrowserSession(request, audience, issuerUrl, apiUrl
|
|
|
190
213
|
throw new Error("Invalid session");
|
|
191
214
|
for (const value of sessionCookies(audience, session.jwt, session.refreshToken))
|
|
192
215
|
headers.append("Set-Cookie", value);
|
|
216
|
+
const accountsCookieName = `${cookieNamesFor(audience).refresh}.accounts`;
|
|
217
|
+
headers.append("Set-Cookie", session.accountsGrant
|
|
218
|
+
? serializeCookie(accountsCookieName, session.accountsGrant, {
|
|
219
|
+
maxAge: 60 * 60 * 24 * 60,
|
|
220
|
+
secure: audience.startsWith("https:"),
|
|
221
|
+
})
|
|
222
|
+
: clearCookieString(accountsCookieName, {
|
|
223
|
+
secure: audience.startsWith("https:"),
|
|
224
|
+
}));
|
|
193
225
|
if (session.webauthn) {
|
|
194
226
|
const completion = btoa(encodeURIComponent(JSON.stringify({
|
|
195
227
|
loginId: state,
|
|
@@ -200,6 +232,8 @@ export async function finishBrowserSession(request, audience, issuerUrl, apiUrl
|
|
|
200
232
|
headers.append("Set-Cookie", serializeCookie(`${cookieNamesFor(audience).refresh}.completion`, completion, { maxAge: 60, secure: audience.startsWith("https:") }));
|
|
201
233
|
}
|
|
202
234
|
headers.append("Set-Cookie", clear);
|
|
235
|
+
if (pending.manageAccount)
|
|
236
|
+
headers.set("Location", `${issuerUrl}/account`);
|
|
203
237
|
return new Response(null, { status: 303, headers });
|
|
204
238
|
}
|
|
205
239
|
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,30 @@ 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 addAccount(loginOpts = {}) {
|
|
399
|
+
return startLogin(loginOpts, "add");
|
|
400
|
+
}
|
|
401
|
+
async function listAccounts() {
|
|
402
|
+
const response = await fetch(sessionPath, {
|
|
403
|
+
method: "POST",
|
|
404
|
+
headers: { "Content-Type": "application/json" },
|
|
405
|
+
body: JSON.stringify({ action: "accounts" }),
|
|
406
|
+
});
|
|
407
|
+
if (!response.ok)
|
|
408
|
+
throw new Error("Could not load saved accounts. Try again.");
|
|
409
|
+
return response.json();
|
|
410
|
+
}
|
|
411
|
+
async function startLogin(loginOpts, intent) {
|
|
360
412
|
if (window.top !== window) {
|
|
361
413
|
throw new Error("Open sign-in from the containing app.");
|
|
362
414
|
}
|
|
363
415
|
let preparation;
|
|
364
|
-
if (accessKeyOptions) {
|
|
416
|
+
if (accessKeyOptions && intent === "login") {
|
|
365
417
|
const chain = getChain();
|
|
366
418
|
if (!chain)
|
|
367
419
|
throw new Error("accessKey requires a configured chain");
|
|
@@ -372,7 +424,9 @@ export function createFlow(options = {}) {
|
|
|
372
424
|
method: "POST",
|
|
373
425
|
headers: { "Content-Type": "application/json" },
|
|
374
426
|
body: JSON.stringify({
|
|
375
|
-
action: "login",
|
|
427
|
+
action: intent === "add" ? "login" : intent,
|
|
428
|
+
intent,
|
|
429
|
+
userId: loginOpts.userId,
|
|
376
430
|
accessKeyHash: preparation?.accessKeyHash,
|
|
377
431
|
signUp: loginOpts.signUp,
|
|
378
432
|
returnTo: loginOpts.returnTo ??
|
|
@@ -396,6 +450,7 @@ export function createFlow(options = {}) {
|
|
|
396
450
|
function openAccount() {
|
|
397
451
|
const url = new URL(sessionPath, window.location.origin);
|
|
398
452
|
url.searchParams.set("action", "account");
|
|
453
|
+
url.searchParams.set("returnTo", `${window.location.pathname}${window.location.search}${window.location.hash}`);
|
|
399
454
|
const tab = window.open(url.href, "_blank");
|
|
400
455
|
if (!tab)
|
|
401
456
|
return false;
|
|
@@ -403,19 +458,37 @@ export function createFlow(options = {}) {
|
|
|
403
458
|
return true;
|
|
404
459
|
}
|
|
405
460
|
async function logout() {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
461
|
+
if (logoutInFlight)
|
|
462
|
+
return logoutInFlight;
|
|
463
|
+
++logoutGeneration;
|
|
464
|
+
++sessionRequest;
|
|
465
|
+
++sessionRevision;
|
|
466
|
+
logoutInFlight = (async () => {
|
|
467
|
+
const response = await fetch(sessionPath, { method: "DELETE" });
|
|
468
|
+
if (!response.ok)
|
|
469
|
+
throw new Error("Could not sign out. Try again.");
|
|
470
|
+
++sessionRequest;
|
|
471
|
+
++sessionRevision;
|
|
472
|
+
await runLogout(store);
|
|
473
|
+
additionalSubject = null;
|
|
474
|
+
})();
|
|
475
|
+
try {
|
|
476
|
+
await logoutInFlight;
|
|
477
|
+
}
|
|
478
|
+
finally {
|
|
479
|
+
logoutInFlight = null;
|
|
480
|
+
}
|
|
411
481
|
if (options.autoGuest)
|
|
412
482
|
await ensureGuest();
|
|
413
483
|
}
|
|
414
484
|
let focusRefresh = null;
|
|
415
485
|
const refreshOnFocus = () => {
|
|
416
|
-
if (document.visibilityState === "hidden" || focusRefresh)
|
|
486
|
+
if (document.visibilityState === "hidden" || focusRefresh || logoutInFlight)
|
|
417
487
|
return;
|
|
418
488
|
focusRefresh = withOriginLock("flow.id.refresh", async () => {
|
|
489
|
+
if (logoutInFlight)
|
|
490
|
+
return;
|
|
491
|
+
const request = ++sessionRequest;
|
|
419
492
|
const response = await fetch(`${sessionPath}?refresh=1`, {
|
|
420
493
|
headers: { Accept: "application/json" },
|
|
421
494
|
});
|
|
@@ -423,8 +496,10 @@ export function createFlow(options = {}) {
|
|
|
423
496
|
return;
|
|
424
497
|
// SAFETY: the app session handler returns the SDK protocol response.
|
|
425
498
|
const result = (await response.json());
|
|
499
|
+
if (request !== sessionRequest)
|
|
500
|
+
return;
|
|
426
501
|
if (result.state)
|
|
427
|
-
commitSession(result.state);
|
|
502
|
+
await commitSession(result.state);
|
|
428
503
|
else
|
|
429
504
|
await runLogout(store);
|
|
430
505
|
})
|
|
@@ -456,6 +531,9 @@ export function createFlow(options = {}) {
|
|
|
456
531
|
},
|
|
457
532
|
login,
|
|
458
533
|
openAccount,
|
|
534
|
+
switchAccounts,
|
|
535
|
+
addAccount,
|
|
536
|
+
listAccounts,
|
|
459
537
|
logout,
|
|
460
538
|
restore: refreshViaSession,
|
|
461
539
|
ensureGuest,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { defaultIdHost, isLocalHostname } from "../id-host";
|
|
2
|
-
export type { AccessKeyOptions, ActionCatalogEntry, ActionCatalogResponse, ActionCurve, ActionEventRequest, ActionEventResponse, ActionSubjectRef, ActionSurface, ActionsApi, ActionTransitionKind, ActiveAction, ActiveActionResponse, ActiveSessionConflict, AdditionalSession, Address, CatalogFieldRef, ClockAnchor, ConnectCapabilities, ConnectResponse, CreateFlowOptions, CreateStudySubjectInput, CurrentAction, DialogHost, FinishActionRequest, FinishActionResponse, FinishedSummary, Flow, FlowCredential, FlowSessionState, FlowState, FlowUser, FlowWidgetHandle, FlowWidgetName, FocusApi, FocusPhase, FocusSessionEvent, FocusSessionState, ListStudySessionsOptions, ListStudySubjectsOptions, LoginOptions, MarkNotificationsReadInput, MethodName, MountFlowWidgetOptions, MountProfileOptions, NotificationCounts, NotificationEntry, NotificationPage, NotificationReadResult, NotificationRefusal, OpenProfileOptions, ProfileButtonHandle, ProfilePosition, RoomMemberEntry, Session, StartActionInput, StartActionResponse, StartActionResult, StartFocusRequest, StudyApi, StudyCatalog, StudyCategory, StudyCategoryTotals, StudyField, StudyRange, StudySearchHit, StudySessionEntry, StudySessionPage, StudySubject, StudySubjectList, StudySubjectRefusal, StudySubjectRemovalResponse, StudySubjectResponse, StudySubjectTotals, StudySummary, UpdateStudySubjectInput, UserActionSurface, XpEstimateContext, XpMode, } from "../types";
|
|
2
|
+
export type { AccessKeyOptions, ActionCatalogEntry, ActionCatalogResponse, ActionCurve, ActionEventRequest, ActionEventResponse, ActionSubjectRef, ActionSurface, ActionsApi, ActionTransitionKind, ActiveAction, ActiveActionResponse, ActiveSessionConflict, AdditionalSession, Address, BrowserAccount, BrowserAccounts, CatalogFieldRef, ClockAnchor, ConnectCapabilities, ConnectResponse, CreateFlowOptions, CreateStudySubjectInput, CurrentAction, DialogHost, FinishActionRequest, FinishActionResponse, FinishedSummary, Flow, FlowCredential, FlowSessionState, FlowState, FlowUser, FlowWidgetHandle, FlowWidgetName, FocusApi, FocusPhase, FocusSessionEvent, FocusSessionState, ListStudySessionsOptions, ListStudySubjectsOptions, LoginOptions, MarkNotificationsReadInput, MethodName, MountFlowWidgetOptions, MountProfileOptions, NotificationCounts, NotificationEntry, NotificationPage, NotificationReadResult, NotificationRefusal, OpenProfileOptions, ProfileButtonHandle, ProfilePosition, RoomMemberEntry, Session, StartActionInput, StartActionResponse, StartActionResult, StartFocusRequest, StudyApi, StudyCatalog, StudyCategory, StudyCategoryTotals, StudyField, StudyRange, StudySearchHit, StudySessionEntry, StudySessionPage, StudySubject, StudySubjectList, StudySubjectRefusal, StudySubjectRemovalResponse, StudySubjectResponse, StudySubjectTotals, StudySummary, SwitchAccountOptions, UpdateStudySubjectInput, UserActionSurface, XpEstimateContext, XpMode, } from "../types";
|
|
3
3
|
export { consistencyCurve, estimateActionXp, FLOW_SCORE_MAX, flowMultiplier, } from "../xp/curve";
|
|
4
4
|
export { ActionsRequestError, createActionsApi, isActiveSessionConflict, } from "./actions";
|
|
5
5
|
export { createFlow, getFlow, requireFlow, resetFlow } from "./create-flow";
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { MountProfileOptions, ProfileButtonHandle } from "../types";
|
|
2
|
-
/** Mount
|
|
2
|
+
/** Mount the shared profile menu. Import @flow-industries/id/profile.css in non-Tailwind apps. */
|
|
3
3
|
export declare function createProfileButton(options: MountProfileOptions): ProfileButtonHandle;
|