@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 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` and `flow.openAccount()` open `/account` in a normal new tab.
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 refresh = cookies[cookieNamesFor(audience).refresh];
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 response = await fetchIssuerApi(`${apiUrl}/api/auth/browser/account-handoff`, { method: "POST", headers, body: JSON.stringify({ audience }) });
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: "account_unavailable" }, response.status);
81
- const { handoff } = z
82
- .object({ handoff: z.string().regex(opaquePattern) })
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 accountUrl = `${issuerUrl}/account#handoff=${handoff}`;
85
- if (body.redirect)
86
- return new Response(null, {
87
- status: 303,
88
- headers: {
89
- Location: accountUrl,
90
- "Cache-Control": "no-store",
91
- "Referrer-Policy": "no-referrer",
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
- return reply({ accountUrl });
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
- function commitSession(session) {
116
- const previous = store.getSnapshot().user?.id;
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
- void idb.delete("flow.activeCredential");
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 (resolved.state?.user.id === completion.userId &&
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
- if (await doRefresh())
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
- const response = await fetch(sessionPath, { method: "DELETE" });
407
- if (!response.ok)
408
- throw new Error("Could not sign out. Try again.");
409
- await runLogout(store);
410
- additionalSubject = null;
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 an account button that signs guests in or opens full-account settings. */
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;