@flow-industries/id 0.21.1 → 0.21.2

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.
@@ -362,6 +362,7 @@ export function createFlow(options = {}) {
362
362
  */
363
363
  async function login(loginOpts = {}) {
364
364
  const dialogHost = getDialog();
365
+ dialogHost.captureFocus();
365
366
  let accessKeyModule;
366
367
  let accessKeyPrep;
367
368
  let extraCapabilities;
@@ -1,4 +1,5 @@
1
1
  import { bridgeToWindow, makeIframe } from "./iframe-host";
2
+ import { setOverlayFocus } from "./overlay-focus";
2
3
  const HIDDEN_STYLE = {
3
4
  position: "fixed",
4
5
  inset: "0",
@@ -35,6 +36,12 @@ export function createDialogHost(options) {
35
36
  const { host, container = document.body } = options;
36
37
  let iframe = null;
37
38
  let messenger = null;
39
+ let opener = null;
40
+ function captureFocus() {
41
+ const active = document.activeElement;
42
+ if (active instanceof HTMLElement && active !== iframe)
43
+ opener = active;
44
+ }
38
45
  const pending = new Map();
39
46
  /**
40
47
  * Mounts the iframe and wires up the postMessage bridge. Idempotent — safe
@@ -43,8 +50,9 @@ export function createDialogHost(options) {
43
50
  function ensureFrame() {
44
51
  if (iframe)
45
52
  return;
46
- iframe = makeIframe(`${host}`);
53
+ iframe = makeIframe(`${host}`, "Flow ID");
47
54
  iframe.dataset.flowId = "";
55
+ iframe.inert = true;
48
56
  Object.assign(iframe.style, HIDDEN_STYLE);
49
57
  container.appendChild(iframe);
50
58
  // Bind the bridge to this iframe's own window — a page may host more than
@@ -109,6 +117,8 @@ export function createDialogHost(options) {
109
117
  if (!iframe)
110
118
  return;
111
119
  Object.assign(iframe.style, VISIBLE_STYLE);
120
+ setOverlayFocus(iframe, true, opener);
121
+ opener = null;
112
122
  // The dialog owns its open/close animation and mounts the overlay (playing
113
123
  // the enter animation) on this signal — same mechanism as the profile
114
124
  // widget, so every dialog appears and disappears identically.
@@ -124,6 +134,7 @@ export function createDialogHost(options) {
124
134
  // requestAnimationFrame, making the close (and the next open) skip straight
125
135
  // to the end. While hidden the overlay is transparent and click-through.
126
136
  iframe.style.pointerEvents = "none";
137
+ setOverlayFocus(iframe, false);
127
138
  void (
128
139
  // SAFETY: the payload map types this topic; the dialog reads exactly these fields.
129
140
  messenger?.send("__internal", { type: "dialog-hidden" }));
@@ -190,6 +201,7 @@ export function createDialogHost(options) {
190
201
  return dispatchRequest(method, params);
191
202
  }
192
203
  return {
204
+ captureFocus,
193
205
  open,
194
206
  close,
195
207
  destroy,
@@ -11,6 +11,11 @@ const WIDGET_ROUTE = {
11
11
  xp: "/dialog/xp-widget",
12
12
  "action-timer": "/dialog/action-timer",
13
13
  };
14
+ // `satisfies` the full widget union so a new widget cannot ship untitled.
15
+ const WIDGET_TITLE = {
16
+ xp: "Flow ID XP",
17
+ "action-timer": "Flow ID action timer",
18
+ };
14
19
  /**
15
20
  * Mounts one of the inline Flow ID widgets — the XP strip, the action timer —
16
21
  * as a transparent iframe the embedder positions and sizes itself.
@@ -42,7 +47,7 @@ export function createFlowWidget(options) {
42
47
  const hostOrigin = new URL(host).origin;
43
48
  let theme = options.theme ?? "light dark";
44
49
  const route = WIDGET_ROUTE[options.widget];
45
- const frame = makeIframe(`${host}${route}`);
50
+ const frame = makeIframe(`${host}${route}`, WIDGET_TITLE[options.widget]);
46
51
  frame.style.background = "transparent";
47
52
  const container = options.container ?? document.body;
48
53
  // Appended before bridging: `contentWindow` is null until the frame is in the
@@ -16,10 +16,12 @@ export declare const OVERLAY_STYLE: Partial<CSSStyleDeclaration>;
16
16
  /**
17
17
  * Creates a Flow dialog iframe element: WebAuthn-permitted, borderless, and
18
18
  * `color-scheme: normal` so the iframe itself stays transparent (the rendered
19
- * theme is applied to nested card wrappers, not the iframe). The caller owns
20
- * positioning/visibility and any `data-*` marker.
19
+ * theme is applied to nested card wrappers, not the iframe). `title` is the
20
+ * accessible name screen readers announce for the frame (WCAG 4.1.2), so every
21
+ * Flow surface must say what it is. The caller owns positioning/visibility and
22
+ * any `data-*` marker.
21
23
  */
22
- export declare function makeIframe(src: string): HTMLIFrameElement;
24
+ export declare function makeIframe(src: string, title: string): HTMLIFrameElement;
23
25
  /**
24
26
  * Builds a postMessage bridge to a child window (iframe `contentWindow` or
25
27
  * popup). Inbound is filtered by `source` so multiple same-origin Flow frames
@@ -21,12 +21,15 @@ export const OVERLAY_STYLE = {
21
21
  /**
22
22
  * Creates a Flow dialog iframe element: WebAuthn-permitted, borderless, and
23
23
  * `color-scheme: normal` so the iframe itself stays transparent (the rendered
24
- * theme is applied to nested card wrappers, not the iframe). The caller owns
25
- * positioning/visibility and any `data-*` marker.
24
+ * theme is applied to nested card wrappers, not the iframe). `title` is the
25
+ * accessible name screen readers announce for the frame (WCAG 4.1.2), so every
26
+ * Flow surface must say what it is. The caller owns positioning/visibility and
27
+ * any `data-*` marker.
26
28
  */
27
- export function makeIframe(src) {
29
+ export function makeIframe(src, title) {
28
30
  const frame = document.createElement("iframe");
29
31
  frame.src = src;
32
+ frame.title = title;
30
33
  frame.allow = IFRAME_ALLOW;
31
34
  frame.style.border = "none";
32
35
  frame.style.colorScheme = "normal";
@@ -2,6 +2,7 @@ import { resolveIdHost } from "../id-host";
2
2
  import { isValidUsername } from "../usernames";
3
3
  import { getFlow, requireFlow } from "./create-flow";
4
4
  import { answerTokenRequest, bridgeToWindow, hostIdentity, makeIframe, OVERLAY_STYLE, } from "./iframe-host";
5
+ import { setOverlayFocus } from "./overlay-focus";
5
6
  // One overlay per page, kept for the page's lifetime. The alternative — a
6
7
  // handle per call site — would put a second fullscreen iframe over the host the
7
8
  // moment two parts of an app (a scoreboard and a chat line) both showed a
@@ -23,12 +24,15 @@ function createViewer(options) {
23
24
  let visible = false;
24
25
  let onClose;
25
26
  function applyHitTesting() {
26
- if (frame)
27
- frame.style.pointerEvents = ready && visible ? "auto" : "none";
27
+ if (!frame)
28
+ return;
29
+ frame.style.pointerEvents = ready && visible ? "auto" : "none";
30
+ setOverlayFocus(frame, ready && visible);
28
31
  }
29
32
  function mount(username) {
30
- const el = makeIframe(`${host}/dialog/`);
33
+ const el = makeIframe(`${host}/dialog/`, "Flow ID profile");
31
34
  el.dataset.flowProfileView = "";
35
+ el.inert = true;
32
36
  Object.assign(el.style, OVERLAY_STYLE, { pointerEvents: "none" });
33
37
  // Appended before bridging: `contentWindow` is null until the frame is in
34
38
  // the document, and the bridge is bound to that window.
@@ -0,0 +1 @@
1
+ export declare function setOverlayFocus(frame: HTMLIFrameElement, visible: boolean, opener?: HTMLElement | null): void;
@@ -0,0 +1,24 @@
1
+ const triggers = new WeakMap();
2
+ export function setOverlayFocus(frame, visible, opener) {
3
+ if (visible) {
4
+ if (triggers.has(frame))
5
+ return;
6
+ const active = opener ?? document.activeElement;
7
+ triggers.set(frame, active instanceof HTMLElement && active !== frame ? active : null);
8
+ frame.inert = false;
9
+ frame.focus();
10
+ return;
11
+ }
12
+ const restore = document.activeElement === frame;
13
+ frame.inert = true;
14
+ const trigger = triggers.get(frame);
15
+ triggers.delete(frame);
16
+ if (restore && trigger?.isConnected) {
17
+ requestAnimationFrame(() => {
18
+ if (trigger.isConnected &&
19
+ (document.activeElement === frame ||
20
+ document.activeElement === document.body))
21
+ trigger.focus();
22
+ });
23
+ }
24
+ }
@@ -1,6 +1,7 @@
1
1
  import { resolveIdHost } from "../id-host";
2
2
  import { getFlow, requireFlow } from "./create-flow";
3
3
  import { answerTokenRequest, bridgeToWindow, hostIdentity, makeIframe, OVERLAY_STYLE, } from "./iframe-host";
4
+ import { setOverlayFocus } from "./overlay-focus";
4
5
  const POSITION_STYLE = {
5
6
  "top-right": { top: "0", right: "0" },
6
7
  "top-left": { top: "0", left: "0" },
@@ -46,8 +47,8 @@ export function createProfileButton(options) {
46
47
  createdContainer = el;
47
48
  container = el;
48
49
  }
49
- function makeFrame() {
50
- const frame = makeIframe(`${host}/dialog/`);
50
+ function makeFrame(title) {
51
+ const frame = makeIframe(`${host}/dialog/`, title);
51
52
  frame.dataset.flowProfile = "";
52
53
  frame.style.display = "block";
53
54
  return frame;
@@ -71,7 +72,7 @@ export function createProfileButton(options) {
71
72
  return bridge;
72
73
  }
73
74
  // ----- the persistent pill (stays mounted; sizes to its own content) -----
74
- const pill = makeFrame();
75
+ const pill = makeFrame("Flow ID profile button");
75
76
  let pillWidth = 0;
76
77
  let pillHeight = 0;
77
78
  function applyPillSize() {
@@ -105,7 +106,8 @@ export function createProfileButton(options) {
105
106
  function ensureDialog() {
106
107
  if (dialog)
107
108
  return;
108
- dialog = makeFrame();
109
+ dialog = makeFrame("Flow ID profile");
110
+ dialog.inert = true;
109
111
  Object.assign(dialog.style, OVERLAY_STYLE, {
110
112
  pointerEvents: "none",
111
113
  });
@@ -144,12 +146,14 @@ export function createProfileButton(options) {
144
146
  if (!dialog)
145
147
  return;
146
148
  dialog.style.pointerEvents = "auto";
149
+ setOverlayFocus(dialog, true);
147
150
  void dialogBridge?.send("__internal", { type: "dialog-shown" });
148
151
  }
149
152
  function hideDialog() {
150
153
  if (!dialog)
151
154
  return;
152
155
  dialog.style.pointerEvents = "none";
156
+ setOverlayFocus(dialog, false);
153
157
  void dialogBridge?.send("__internal", { type: "dialog-hidden" });
154
158
  }
155
159
  // ----- keep both iframes' identity in sync with the Flow session -----
@@ -186,6 +190,7 @@ export function createProfileButton(options) {
186
190
  });
187
191
  },
188
192
  destroy() {
193
+ hideDialog();
189
194
  unsubscribe();
190
195
  pillBridge.destroy();
191
196
  dialogBridge?.destroy();
@@ -0,0 +1,269 @@
1
+ import type { RawRecord, RawValue } from "../json";
2
+ import type { AccountSession, SecurityActivityEntry } from "./auth";
3
+ import type { RoomEventRecurrence } from "./room-events";
4
+ /**
5
+ * Self-serve account deletion (AUTH-246), the personal-data export (AUTH-247)
6
+ * and the erasure ledger downstream services replay (AUTH-119).
7
+ */
8
+ /** Body of `POST /api/account/delete`: the caller's current username, typed
9
+ * back as the confirmation. */
10
+ export type AccountDeleteRequest = {
11
+ confirm: string;
12
+ };
13
+ export type AccountDeleteResponse = {
14
+ ok: true;
15
+ };
16
+ /** What one account deletion removed, beyond the user row's own cascade. */
17
+ export type AccountErasureCounts = {
18
+ /** Non-system rooms the account owned, deleted with everything inside them. */
19
+ ownedRoomsDeleted: number;
20
+ /** Cookie sessions the cascade took down. */
21
+ sessionsDeleted: number;
22
+ /** Passkeys the cascade took down. */
23
+ passkeysDeleted: number;
24
+ /** OTP / quota rows keyed on the account's email or id. */
25
+ verificationsDeleted: number;
26
+ /** Per-user rate-limit rows (never the ip-keyed ones). */
27
+ rateLimitsDeleted: number;
28
+ };
29
+ /** Result of `deleteAccount`: `missing` is the idempotent second call. */
30
+ export type AccountErasureResult = {
31
+ outcome: "missing";
32
+ } | {
33
+ outcome: "deleted";
34
+ userId: string;
35
+ isGuest: boolean;
36
+ /** The stored avatar URL, for the post-commit object delete. */
37
+ image: string | null;
38
+ counts: AccountErasureCounts;
39
+ };
40
+ /** One row of the erasure ledger as `GET /api/account/erasures` serves it. */
41
+ export type AccountErasureEntry = {
42
+ seq: number;
43
+ /** The deleted account's Flow `user.id` — the JWT `sub` consumers keyed on. */
44
+ sub: string;
45
+ deletedRoomIds: string[];
46
+ erasedAt: string;
47
+ };
48
+ export type AccountErasureFeed = {
49
+ /** Ascending by `seq`. */
50
+ erasures: AccountErasureEntry[];
51
+ /** Feed this back as `after` for the next page; equals the request's
52
+ * `after` when the page was empty. */
53
+ nextAfter: number;
54
+ };
55
+ export type AccountExportAccount = {
56
+ id: string;
57
+ username: string;
58
+ name: string | null;
59
+ email: string | null;
60
+ emailVerified: boolean;
61
+ isGuest: boolean;
62
+ role: string;
63
+ image: string | null;
64
+ bio: string | null;
65
+ walletAddress: string | null;
66
+ subscribedToNews: boolean;
67
+ createdAt: string;
68
+ };
69
+ /** A passkey as the owner sees it: metadata only, never the public key,
70
+ * credential id or signature counter. */
71
+ export type AccountExportPasskey = {
72
+ id: string;
73
+ name: string | null;
74
+ deviceType: string | null;
75
+ backedUp: boolean | null;
76
+ transports: string | null;
77
+ aaguid: string | null;
78
+ createdAt: string;
79
+ };
80
+ export type AccountExportSettingsSurface = {
81
+ version: number;
82
+ settings: Record<string, RawValue>;
83
+ };
84
+ export type AccountExportXpSummary = {
85
+ totalXp: number;
86
+ flowScore: number;
87
+ lastVisitDate: string | null;
88
+ updatedAt: string;
89
+ };
90
+ export type AccountExportXpGrant = {
91
+ id: string;
92
+ source: string;
93
+ baseAmount: number;
94
+ flowScore: number;
95
+ amount: number;
96
+ metadata: RawRecord | null;
97
+ createdAt: string;
98
+ };
99
+ export type AccountExportActionEvent = {
100
+ id: string;
101
+ sessionId: string;
102
+ kind: string;
103
+ accruedSeconds: number;
104
+ at: string;
105
+ };
106
+ export type AccountExportRoomSettings = {
107
+ roomId: string;
108
+ surface: string;
109
+ settings: RawRecord;
110
+ version: number;
111
+ };
112
+ export type AccountExportActionSession = {
113
+ id: string;
114
+ source: string;
115
+ state: string;
116
+ subjectId: string | null;
117
+ accruedSeconds: number;
118
+ focusedSeconds: number;
119
+ xpGranted: number | null;
120
+ metadata: RawRecord | null;
121
+ startedAt: string;
122
+ updatedAt: string;
123
+ finishedAt: string | null;
124
+ };
125
+ export type AccountExportStudySubject = {
126
+ id: string;
127
+ field: string | null;
128
+ name: string;
129
+ archivedAt: string | null;
130
+ createdAt: string;
131
+ lastUsedAt: string;
132
+ };
133
+ export type AccountExportCharacterStateTime = {
134
+ day: string;
135
+ state: string;
136
+ seconds: number;
137
+ updatedAt: string;
138
+ };
139
+ export type AccountExportOwnedRoom = {
140
+ id: string;
141
+ slug: string;
142
+ displayName: string;
143
+ description: string | null;
144
+ visibility: string;
145
+ isSystem: boolean;
146
+ createdAt: string;
147
+ updatedAt: string;
148
+ };
149
+ export type AccountExportRoomMembership = {
150
+ roomId: string;
151
+ roomSlug: string;
152
+ role: string;
153
+ joinedAt: string;
154
+ };
155
+ /** A restriction placed on the caller. The issuing moderator is deliberately
156
+ * absent — the export carries only the caller's own identity. */
157
+ export type AccountExportRoomRestriction = {
158
+ id: string;
159
+ roomId: string;
160
+ kind: string;
161
+ reason: string | null;
162
+ expiresAt: string | null;
163
+ createdAt: string;
164
+ };
165
+ export type AccountExportRoomPresence = {
166
+ roomId: string;
167
+ channel: string;
168
+ joinedAt: string;
169
+ updatedAt: string;
170
+ };
171
+ export type AccountExportRoomPlayerState = {
172
+ roomId: string;
173
+ x: number;
174
+ y: number;
175
+ z: number;
176
+ yaw: number;
177
+ worldId: string;
178
+ updatedAt: string;
179
+ };
180
+ export type AccountExportRooms = {
181
+ settings: AccountExportRoomSettings[];
182
+ owned: AccountExportOwnedRoom[];
183
+ memberships: AccountExportRoomMembership[];
184
+ restrictions: AccountExportRoomRestriction[];
185
+ presence: AccountExportRoomPresence[];
186
+ playerState: AccountExportRoomPlayerState[];
187
+ };
188
+ export type AccountExportAuthoredEvent = {
189
+ id: string;
190
+ roomId: string;
191
+ title: string;
192
+ description: string | null;
193
+ coverUrl: string | null;
194
+ startsAt: string;
195
+ endsAt: string | null;
196
+ timezone: string;
197
+ recurrence: RoomEventRecurrence | null;
198
+ reminderOffsets: number[] | null;
199
+ status: string;
200
+ createdAt: string;
201
+ updatedAt: string;
202
+ };
203
+ export type AccountExportRsvp = {
204
+ occurrenceId: string;
205
+ createdAt: string;
206
+ };
207
+ export type AccountExportFollow = {
208
+ eventId: string;
209
+ createdAt: string;
210
+ };
211
+ export type AccountExportAttendance = {
212
+ occurrenceId: string;
213
+ firstSeenAt: string;
214
+ lastSeenAt: string;
215
+ };
216
+ export type AccountExportRoomEvents = {
217
+ authored: AccountExportAuthoredEvent[];
218
+ rsvps: AccountExportRsvp[];
219
+ follows: AccountExportFollow[];
220
+ attendance: AccountExportAttendance[];
221
+ };
222
+ export type AccountExportGameEvent = {
223
+ id: string;
224
+ event: string;
225
+ occurredAt: string;
226
+ recordedAt: string;
227
+ serverId: string;
228
+ sessionId: string | null;
229
+ roomId: string | null;
230
+ roomSlug: string | null;
231
+ reason: string | null;
232
+ detail: RawRecord | null;
233
+ };
234
+ /** Where the rest of a person's Flow data lives, so one export names every
235
+ * service that has to be asked. */
236
+ export type AccountExportOtherService = {
237
+ service: string;
238
+ export: string;
239
+ holds: string;
240
+ };
241
+ /** The whole personal-data export (`GET /api/account/export`), version 1. */
242
+ export type AccountExport = {
243
+ format: "flow-id-export";
244
+ version: 1;
245
+ exportedAt: string;
246
+ account: AccountExportAccount;
247
+ passkeys: AccountExportPasskey[];
248
+ sessions: AccountSession[];
249
+ settings: Record<string, AccountExportSettingsSurface>;
250
+ xp: {
251
+ summary: AccountExportXpSummary | null;
252
+ ledger: AccountExportXpGrant[];
253
+ };
254
+ actions: {
255
+ events: AccountExportActionEvent[];
256
+ sessions: AccountExportActionSession[];
257
+ };
258
+ study: {
259
+ subjects: AccountExportStudySubject[];
260
+ };
261
+ characterStateTime: AccountExportCharacterStateTime[];
262
+ rooms: AccountExportRooms;
263
+ roomEvents: AccountExportRoomEvents;
264
+ gameEvents: AccountExportGameEvent[];
265
+ securityActivity: SecurityActivityEntry[];
266
+ otherServices: AccountExportOtherService[];
267
+ /** Dotted names of every collection that hit the row cap (newest rows kept). */
268
+ truncated: string[];
269
+ };
File without changes
@@ -9,7 +9,7 @@ export type AuthOutcome = "success" | "failure" | "info";
9
9
  export type AuthMode = "sign-up" | "sign-in";
10
10
  /** Client-only funnel steps reported via the `/api/events` beacon. */
11
11
  export type FunnelStep = "mode_selected" | "email_entered" | "ceremony_started" | "ceremony_failed" | "done_shown";
12
- export type AuthEventName = "auth.username.checked" | "auth.signup.succeeded" | "auth.signup.failed" | "auth.otp.sent" | "auth.otp.verified" | "auth.otp.failed" | "auth.email.verify.sent" | "auth.email.verify.succeeded" | "auth.email.verify.failed" | "auth.email.changed" | "auth.avatar.upload.succeeded" | "auth.avatar.upload.failed" | "auth.bio.update.succeeded" | "auth.bio.update.failed" | "auth.profile.view.failed" | "auth.profile.view.uncapped" | "auth.challenge.issued" | "auth.signin.succeeded" | "auth.signin.failed" | "auth.passkey.added" | "auth.passkey.add_failed" | "auth.restore.succeeded" | "auth.restore.failed" | "auth.restore.no_session" | "auth.refresh.succeeded" | "auth.refresh.failed" | "auth.refresh.reuse" | "auth.refresh.grace" | "auth.jwt.verified" | "auth.jwt.rejected" | "auth.signout" | "auth.session.revoked" | "auth.session.revoked_all" | "auth.audience.rejected" | "auth.audience.minted" | "auth.guest.created" | "auth.guest.restored" | "auth.guest.upgraded" | "auth.guest.failed" | "auth.role.granted" | "auth.role.revoked" | "room.restriction.issued" | "room.restriction.lifted" | "auth.funnel.mode_selected" | "auth.funnel.email_entered" | "auth.funnel.ceremony_started" | "auth.funnel.ceremony_failed" | "auth.funnel.done_shown" | "room_event.activated" | "room_event.completed" | "room_event.reminder_dispatched" | "room_event.reminder_failed";
12
+ export type AuthEventName = "auth.username.checked" | "auth.signup.succeeded" | "auth.signup.failed" | "auth.otp.sent" | "auth.otp.verified" | "auth.otp.failed" | "auth.email.verify.sent" | "auth.email.verify.succeeded" | "auth.email.verify.failed" | "auth.email.changed" | "auth.avatar.upload.succeeded" | "auth.avatar.upload.failed" | "auth.bio.update.succeeded" | "auth.bio.update.failed" | "auth.profile.view.failed" | "auth.profile.view.uncapped" | "auth.challenge.issued" | "auth.signin.succeeded" | "auth.signin.failed" | "auth.passkey.added" | "auth.passkey.add_failed" | "auth.restore.succeeded" | "auth.restore.failed" | "auth.restore.no_session" | "auth.refresh.succeeded" | "auth.refresh.failed" | "auth.refresh.reuse" | "auth.refresh.grace" | "auth.jwt.verified" | "auth.jwt.rejected" | "auth.signout" | "auth.session.revoked" | "auth.session.revoked_all" | "auth.audience.rejected" | "auth.audience.minted" | "auth.guest.created" | "auth.guest.restored" | "auth.guest.upgraded" | "auth.guest.failed" | "auth.role.granted" | "auth.role.revoked" | "auth.account.deleted" | "auth.account.exported" | "room.restriction.issued" | "room.restriction.lifted" | "auth.funnel.mode_selected" | "auth.funnel.email_entered" | "auth.funnel.ceremony_started" | "auth.funnel.ceremony_failed" | "auth.funnel.done_shown" | "room_event.activated" | "room_event.completed" | "room_event.reminder_dispatched" | "room_event.reminder_failed";
13
13
  export type AuthErrorCode = "username_taken" | "credential_taken" | "email_taken" | "email_not_verified" | "no_email" | "image_upload_forbidden" | "image_upload_rate_limited" | "invalid_image_type" | "image_too_large" | "image_upload_failed" | "bio_forbidden" | "bio_invalid" | "bio_too_long" | "profile_rate_limited" | "profile_ip_missing" | "otp_invalid" | "otp_expired" | "otp_attempts_exceeded" | "otp_resend_cooldown" | "otp_resend_limit" | "otp_global_limit" | "otp_send_failed" | "challenge_expired" | "unknown_credential" | "invalid_assertion_type" | "invalid_assertion_origin" | "user_verification_required" | "invalid_signature" | "user_not_found" | "missing_audience" | "audience_not_allowed" | "audience_mismatch" | "no_session" | "no_passkey" | "refresh_token_invalid" | "refresh_token_expired" | "refresh_epoch_stale" | "refresh_user_epoch_stale" | "refresh_reuse_detected" | "guest_rate_limited" | "guest_global_limit" | "guest_username_exhausted" | "guest_session" | "passkey_exists" | "already_upgraded" | "malformed_token" | "unknown_key" | "verification_failed" | "webauthn_not_allowed" | "webauthn_security" | "webauthn_invalid_state" | "webauthn_not_supported" | "webauthn_constraint" | "webauthn_aborted" | "webauthn_unknown" | "internal_error";
14
14
  /** One flat record per event = one row in the `auth_events` stream. */
15
15
  export interface AuthEventRecord {
@@ -47,6 +47,10 @@ export interface AuthEventRecord {
47
47
  occurrenceId: string | null;
48
48
  dispatchKind: string | null;
49
49
  recipients: number | null;
50
+ /** Account-lifecycle fields: whether the account was a guest, and what an
51
+ * erasure or export touched, as per-section row counts. */
52
+ guest: boolean | null;
53
+ counts: Record<string, number> | null;
50
54
  }
51
55
  /** Body accepted by `POST /api/events` from the dialog. */
52
56
  export interface BeaconBody {
@@ -1,3 +1,4 @@
1
+ export type { AccountDeleteRequest, AccountDeleteResponse, AccountErasureCounts, AccountErasureEntry, AccountErasureFeed, AccountErasureResult, AccountExport, AccountExportAccount, AccountExportActionEvent, AccountExportActionSession, AccountExportAttendance, AccountExportAuthoredEvent, AccountExportCharacterStateTime, AccountExportFollow, AccountExportGameEvent, AccountExportOtherService, AccountExportOwnedRoom, AccountExportPasskey, AccountExportRoomEvents, AccountExportRoomMembership, AccountExportRoomPlayerState, AccountExportRoomPresence, AccountExportRoomRestriction, AccountExportRoomSettings, AccountExportRooms, AccountExportRsvp, AccountExportSettingsSurface, AccountExportStudySubject, AccountExportXpGrant, AccountExportXpSummary, } from "./account-data";
1
2
  export type { ActionCurve, ActionEventRequest, ActionEventResponse, ActionsApi, ActiveActionState, FinishActionRequest, ListStudySessionsOptions, ListStudySubjectsOptions, StartActionInput, StartActionResult, StudyApi, StudySubjectResponse, UseActiveActionOptions, XpEstimateContext, } from "./actions";
2
3
  export type { AccountSession, AccountSessionsResponse, AdditionalSession, AuthConfig, AuthResponse, AuthResponseWithWebAuthn, BioNormalization, BioRejection, BioWriteRefusal, BioWriteResult, FlowCredential, FlowUser, PasskeyPluginOptions, RevokeAllSessionsResponse, RevokeSessionResponse, SecurityActivityEntry, SecurityActivityResponse, SessionUser, VerifiedFlowJWT, VerifyOptions, WebAuthnSignature, } from "./auth";
3
4
  export type { BodyRegion, CosmeticItem, CosmeticMaterial, CosmeticPaint, CosmeticSlot, EquipConflict, EquippedCosmetic, EquippedCosmetics, EquipRegion, PaintSlot, SettingsConflict, } from "./cosmetics";
@@ -312,6 +312,7 @@ export type FlowWidgetProps = {
312
312
  theme?: ColorScheme;
313
313
  };
314
314
  export type DialogHost = {
315
+ captureFocus: () => void;
315
316
  open: (options?: DialogOpenOptions) => void;
316
317
  close: () => void;
317
318
  destroy: () => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flow-industries/id",
3
- "version": "0.21.1",
3
+ "version": "0.21.2",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -126,7 +126,7 @@
126
126
  ],
127
127
  "dependencies": {
128
128
  "@aws-sdk/client-s3": "^3.1073.0",
129
- "@flow-industries/ui": "^0.19.0",
129
+ "@flow-industries/ui": "^0.21.0",
130
130
  "@hono/otel": "^1.1.2",
131
131
  "@openobserve/browser-logs": "^0.3.1",
132
132
  "@openobserve/browser-rum": "^0.3.1",