@flow-industries/id 0.15.0 → 0.17.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.
@@ -0,0 +1,20 @@
1
+ import type { FlowWidgetHandle, MountFlowWidgetOptions } from "../types";
2
+ /**
3
+ * Mounts one of the inline Flow ID widgets — the XP strip, the action timer —
4
+ * as a transparent iframe the embedder positions and sizes itself.
5
+ *
6
+ * The reason this exists rather than embedders hand-rolling an `<iframe>`: the
7
+ * widget reads the Flow ID API from a cross-site frame, where its own origin's
8
+ * cookie is unavailable or bound to a different audience, so it needs the
9
+ * host's access token. It gets one by asking over the postMessage bridge, and
10
+ * only a frame that received the `init` handshake knows which origin to ask.
11
+ * A hand-rolled iframe never sends `init` and never gets an answer, so it is
12
+ * silently limited to whatever the cookie happens to allow (AUTH-171).
13
+ *
14
+ * The handle exposes the frame instead of styling it, because each embedder
15
+ * wants different geometry (the in-game overlay is fixed and click-through,
16
+ * the menu's is a laid-out block) and none of that belongs in the SDK.
17
+ *
18
+ * No-op under SSR.
19
+ */
20
+ export declare function createFlowWidget(options: MountFlowWidgetOptions): FlowWidgetHandle;
@@ -0,0 +1,78 @@
1
+ import { resolveIdHost } from "../id-host";
2
+ import { getFlow, requireFlow } from "./create-flow";
3
+ import { answerTokenRequest, bridgeToWindow, makeIframe } from "./iframe-host";
4
+ /**
5
+ * The inline widgets an embedder may mount, mapped to the dialog routes that
6
+ * serve them. Callers name a widget rather than passing a route so the dialog's
7
+ * URL space stays an internal detail and an embedder can't point this at an
8
+ * auth route, which would render a sign-in ceremony inside a decorative frame.
9
+ */
10
+ const WIDGET_ROUTE = {
11
+ xp: "/dialog/xp-widget",
12
+ "action-timer": "/dialog/action-timer",
13
+ };
14
+ /**
15
+ * Mounts one of the inline Flow ID widgets — the XP strip, the action timer —
16
+ * as a transparent iframe the embedder positions and sizes itself.
17
+ *
18
+ * The reason this exists rather than embedders hand-rolling an `<iframe>`: the
19
+ * widget reads the Flow ID API from a cross-site frame, where its own origin's
20
+ * cookie is unavailable or bound to a different audience, so it needs the
21
+ * host's access token. It gets one by asking over the postMessage bridge, and
22
+ * only a frame that received the `init` handshake knows which origin to ask.
23
+ * A hand-rolled iframe never sends `init` and never gets an answer, so it is
24
+ * silently limited to whatever the cookie happens to allow (AUTH-171).
25
+ *
26
+ * The handle exposes the frame instead of styling it, because each embedder
27
+ * wants different geometry (the in-game overlay is fixed and click-through,
28
+ * the menu's is a laid-out block) and none of that belongs in the SDK.
29
+ *
30
+ * No-op under SSR.
31
+ */
32
+ export function createFlowWidget(options) {
33
+ if (typeof document === "undefined")
34
+ return {
35
+ frame: null,
36
+ post() { },
37
+ destroy() { },
38
+ };
39
+ const flow = options.flow ?? getFlow() ?? requireFlow();
40
+ const host = resolveIdHost(options.host ?? flow.host);
41
+ const hostOrigin = new URL(host).origin;
42
+ const theme = options.theme ?? "light dark";
43
+ const route = WIDGET_ROUTE[options.widget];
44
+ const frame = makeIframe(`${host}${route}`);
45
+ frame.style.background = "transparent";
46
+ const container = options.container ?? document.body;
47
+ // Appended before bridging: `contentWindow` is null until the frame is in the
48
+ // document, and the bridge is bound to that window.
49
+ container.appendChild(frame);
50
+ const bridge = bridgeToWindow(frame.contentWindow, {
51
+ targetOrigin: hostOrigin,
52
+ });
53
+ bridge.on("__internal", (payload) => {
54
+ if (payload.type === "profile-token-request")
55
+ void answerTokenRequest(bridge, flow, payload.id);
56
+ });
57
+ // The widget already loads its own route directly, so `route` here only
58
+ // confirms where the frame is; what the handshake is really for is telling
59
+ // the dialog which origin it is embedded by, which is what it later asks for
60
+ // a token.
61
+ void bridge.send("__internal", {
62
+ type: "init",
63
+ mode: "iframe",
64
+ referrer: { title: document.title },
65
+ theme: { colorScheme: theme },
66
+ route,
67
+ });
68
+ return {
69
+ frame,
70
+ post(message) {
71
+ frame.contentWindow?.postMessage(message, hostOrigin);
72
+ },
73
+ destroy() {
74
+ bridge.destroy();
75
+ frame.remove();
76
+ },
77
+ };
78
+ }
@@ -1,4 +1,5 @@
1
1
  import * as Messenger from "../dialog/remote/Messenger";
2
+ import type { Flow } from "../types";
2
3
  /**
3
4
  * Permissions the dialog iframe needs to run WebAuthn passkey ceremonies and
4
5
  * copy recovery values. Shared by every Flow iframe host (the dialog host and
@@ -21,3 +22,14 @@ export declare function makeIframe(src: string): HTMLIFrameElement;
21
22
  export declare function bridgeToWindow(target: Window, options?: {
22
23
  targetOrigin?: string;
23
24
  }): Messenger.Bridge;
25
+ /**
26
+ * Answers a widget's request for the host's credential. Every Flow iframe that
27
+ * reads the Flow ID API from a cross-site frame asks for this, because its own
28
+ * origin's cookie is unavailable or bound to another audience there.
29
+ *
30
+ * `getToken` refreshes a token that is missing or near expiry, so a widget left
31
+ * open past the 1h access-token lifetime still gets a live one rather than a
32
+ * 401. A failure answers `null` — "no session", which widgets render as signed
33
+ * out — instead of leaving the request to time out.
34
+ */
35
+ export declare function answerTokenRequest(bridge: Messenger.Bridge, flow: Flow, id: string): Promise<void>;
@@ -32,3 +32,17 @@ export function bridgeToWindow(target, options = {}) {
32
32
  waitForReady: true,
33
33
  });
34
34
  }
35
+ /**
36
+ * Answers a widget's request for the host's credential. Every Flow iframe that
37
+ * reads the Flow ID API from a cross-site frame asks for this, because its own
38
+ * origin's cookie is unavailable or bound to another audience there.
39
+ *
40
+ * `getToken` refreshes a token that is missing or near expiry, so a widget left
41
+ * open past the 1h access-token lifetime still gets a live one rather than a
42
+ * 401. A failure answers `null` — "no session", which widgets render as signed
43
+ * out — instead of leaving the request to time out.
44
+ */
45
+ export async function answerTokenRequest(bridge, flow, id) {
46
+ const token = await flow.getToken().catch(() => null);
47
+ void bridge.send("__internal", { type: "profile-token", id, token });
48
+ }
@@ -1,7 +1,8 @@
1
1
  export { defaultIdHost, isLocalHostname } from "../id-host";
2
- export type { AccessKeyOptions, AdditionalSession, Address, ConnectCapabilities, ConnectResponse, CreateFlowOptions, DialogHost, Flow, FlowCredential, FlowSessionState, FlowState, FlowUser, LoginOptions, MethodName, MountProfileOptions, ProfileButtonHandle, ProfilePosition, Session, } from "../types";
2
+ export type { AccessKeyOptions, AdditionalSession, Address, ConnectCapabilities, ConnectResponse, CreateFlowOptions, DialogHost, Flow, FlowCredential, FlowSessionState, FlowState, FlowUser, FlowWidgetHandle, FlowWidgetName, LoginOptions, MethodName, MountFlowWidgetOptions, MountProfileOptions, ProfileButtonHandle, ProfilePosition, Session, } from "../types";
3
3
  export { createFlow, getFlow, requireFlow, resetFlow } from "./create-flow";
4
4
  export { createDialogHost } from "./dialog-host";
5
+ export { createFlowWidget } from "./flow-widget";
5
6
  export { METHODS } from "./methods";
6
7
  export { createProfileButton } from "./profile-button";
7
8
  export { createRoomsApi, RoomsRequestError } from "./rooms";
@@ -1,6 +1,7 @@
1
1
  export { defaultIdHost, isLocalHostname } from "../id-host";
2
2
  export { createFlow, getFlow, requireFlow, resetFlow } from "./create-flow";
3
3
  export { createDialogHost } from "./dialog-host";
4
+ export { createFlowWidget } from "./flow-widget";
4
5
  export { METHODS } from "./methods";
5
6
  export { createProfileButton } from "./profile-button";
6
7
  export { createRoomsApi, RoomsRequestError } from "./rooms";
@@ -1,6 +1,6 @@
1
1
  import { resolveIdHost } from "../id-host";
2
2
  import { getFlow, requireFlow } from "./create-flow";
3
- import { bridgeToWindow, makeIframe } from "./iframe-host";
3
+ import { answerTokenRequest, bridgeToWindow, makeIframe } from "./iframe-host";
4
4
  const POSITION_STYLE = {
5
5
  "top-right": { top: "0", right: "0" },
6
6
  "top-left": { top: "0", left: "0" },
@@ -105,6 +105,9 @@ export function createProfileButton(options) {
105
105
  else if (payload.type === "profile-expand") {
106
106
  showDialog();
107
107
  }
108
+ else if (payload.type === "profile-token-request") {
109
+ void answerTokenRequest(pillBridge, flow, payload.id);
110
+ }
108
111
  });
109
112
  // ----- the dialog overlay (lazy; a separate fullscreen iframe layered ABOVE
110
113
  // the pill, so the pill shows through its dimmed backdrop) -----
@@ -131,6 +134,10 @@ export function createProfileButton(options) {
131
134
  hideDialog();
132
135
  void flow.logout().catch(() => { });
133
136
  }
137
+ else if (payload.type === "profile-token-request") {
138
+ if (dialogBridge)
139
+ void answerTokenRequest(dialogBridge, flow, payload.id);
140
+ }
134
141
  });
135
142
  void dialogBridge.send("__internal", {
136
143
  type: "profile-identity",
@@ -0,0 +1,12 @@
1
+ import type { FlowWidgetProps } from "../types";
2
+ /**
3
+ * Drops an inline Flow ID widget — the XP strip, the action timer — into a
4
+ * React tree. Renders a host element (style it via `className` for placement
5
+ * and size) that the widget iframe fills. Resolves the Flow instance from a
6
+ * `<FlowIdProvider>` or the createFlow() singleton.
7
+ *
8
+ * Prefer this over embedding the widget's URL in your own `<iframe>`: only a
9
+ * frame mounted this way completes the handshake that lets it authenticate as
10
+ * the signed-in user from a cross-site page (AUTH-171).
11
+ */
12
+ export declare function FlowWidget({ widget, className, host, theme, }: FlowWidgetProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from "react";
3
+ import { createFlowWidget } from "../client/flow-widget";
4
+ import { useFlow } from "./hooks";
5
+ /**
6
+ * Drops an inline Flow ID widget — the XP strip, the action timer — into a
7
+ * React tree. Renders a host element (style it via `className` for placement
8
+ * and size) that the widget iframe fills. Resolves the Flow instance from a
9
+ * `<FlowIdProvider>` or the createFlow() singleton.
10
+ *
11
+ * Prefer this over embedding the widget's URL in your own `<iframe>`: only a
12
+ * frame mounted this way completes the handshake that lets it authenticate as
13
+ * the signed-in user from a cross-site page (AUTH-171).
14
+ */
15
+ export function FlowWidget({ widget, className, host, theme, }) {
16
+ const flow = useFlow();
17
+ const ref = useRef(null);
18
+ useEffect(() => {
19
+ const container = ref.current;
20
+ if (!container)
21
+ return;
22
+ const handle = createFlowWidget({
23
+ widget,
24
+ container,
25
+ flow,
26
+ ...(host ? { host } : {}),
27
+ ...(theme ? { theme } : {}),
28
+ });
29
+ if (handle.frame) {
30
+ handle.frame.style.width = "100%";
31
+ handle.frame.style.height = "100%";
32
+ handle.frame.style.display = "block";
33
+ }
34
+ return () => handle.destroy();
35
+ }, [widget, flow, host, theme]);
36
+ return _jsx("div", { ref: ref, className: className });
37
+ }
@@ -1,4 +1,5 @@
1
- export type { FlowIdProviderProps, ProfileButtonProps } from "../types";
1
+ export type { FlowIdProviderProps, FlowWidgetProps, ProfileButtonProps, } from "../types";
2
+ export { FlowWidget } from "./flow-widget";
2
3
  export { useFlow, useFlowId, useFlowState, useRoom, useRooms } from "./hooks";
3
4
  export { ProfileButton } from "./profile-button";
4
5
  export { FlowIdProvider } from "./provider";
@@ -1,3 +1,4 @@
1
+ export { FlowWidget } from "./flow-widget";
1
2
  export { useFlow, useFlowId, useFlowState, useRoom, useRooms } from "./hooks";
2
3
  export { ProfileButton } from "./profile-button";
3
4
  export { FlowIdProvider } from "./provider";
@@ -1,4 +1,5 @@
1
1
  import type { JWTPayload } from "jose";
2
+ import type { AuthEventName } from "./events";
2
3
  export type FlowUser = {
3
4
  id: string;
4
5
  username: string;
@@ -14,6 +15,17 @@ export type FlowCredential = {
14
15
  id: string;
15
16
  publicKey: string;
16
17
  };
18
+ /** The better-auth session user with Flow's additional fields — the one
19
+ * shape routes cast `session.user` to instead of ad-hoc inline casts. */
20
+ export interface SessionUser {
21
+ id: string;
22
+ email?: string | null;
23
+ emailVerified?: boolean;
24
+ username?: string | null;
25
+ isGuest?: boolean;
26
+ role?: string | null;
27
+ image?: string | null;
28
+ }
17
29
  /**
18
30
  * A session minted for one of the ADDITIONAL audiences a login requested
19
31
  * (AUTH-39): its own refresh-token lineage and access JWT, both bound to
@@ -61,6 +73,56 @@ export type VerifyOptions = {
61
73
  audience: string;
62
74
  issuerUrl?: string;
63
75
  };
76
+ /**
77
+ * One entry in the signed-in user's session list (`GET /api/account/sessions`).
78
+ * An entry is either a cookie session (with the activity of its linked app
79
+ * refresh lineages folded in) or an app-only refresh lineage whose cookie
80
+ * session no longer exists. `id` is an opaque revocation handle — never a
81
+ * token, and never another user's row.
82
+ */
83
+ export type AccountSession = {
84
+ id: string;
85
+ /** The app origin the session was opened for; null on a pre-audience row. */
86
+ audience: string | null;
87
+ /** Coarse "Browser · OS" from the sign-in user agent; null when unknown. */
88
+ device: string | null;
89
+ createdAt: string;
90
+ /** Latest activity across the cookie session and its app refresh lineages. */
91
+ lastSeenAt: string;
92
+ expiresAt: string;
93
+ /** True for the session serving this request. */
94
+ isCurrent: boolean;
95
+ };
96
+ export type AccountSessionsResponse = {
97
+ sessions: AccountSession[];
98
+ };
99
+ /** Result of `DELETE /api/account/sessions/:id`. */
100
+ export type RevokeSessionResponse = {
101
+ revoked: true;
102
+ /** True when the caller revoked the session serving the request itself. */
103
+ wasCurrent: boolean;
104
+ };
105
+ /** Result of `POST /api/account/sessions/revoke-all`. */
106
+ export type RevokeAllSessionsResponse = {
107
+ revokedSessions: number;
108
+ revokedLineages: number;
109
+ };
110
+ /**
111
+ * One entry of the user-facing security feed
112
+ * (`GET /api/account/security-events`). Country and coarse device only —
113
+ * the stored ip and raw user agent never leave the server.
114
+ */
115
+ export type SecurityActivityEntry = {
116
+ id: string;
117
+ event: AuthEventName;
118
+ audience: string | null;
119
+ country: string | null;
120
+ device: string | null;
121
+ createdAt: string;
122
+ };
123
+ export type SecurityActivityResponse = {
124
+ events: SecurityActivityEntry[];
125
+ };
64
126
  export type VerifiedFlowJWT = JWTPayload & {
65
127
  sub: string;
66
128
  username?: string;
@@ -1,3 +1,16 @@
1
+ /**
2
+ * Async view state for a dialog panel — the three states every fetched
3
+ * section renders (spinner / retry row / content), so "empty" stays
4
+ * distinguishable from "not loaded yet".
5
+ */
6
+ export type Loadable<T> = {
7
+ status: "loading";
8
+ } | {
9
+ status: "error";
10
+ } | {
11
+ status: "ready";
12
+ data: T;
13
+ };
1
14
  export type DialogReferrer = {
2
15
  icon?: string | {
3
16
  light: string;
@@ -4,12 +4,13 @@
4
4
  * here are the contract every dashboard/alert query depends on — changing one
5
5
  * is a breaking change to the dashboard.
6
6
  */
7
+ import type { GlobalRole } from "./rooms";
7
8
  export type AuthOutcome = "success" | "failure" | "info";
8
9
  export type AuthMode = "sign-up" | "sign-in";
9
10
  /** Client-only funnel steps reported via the `/api/events` beacon. */
10
11
  export type FunnelStep = "mode_selected" | "email_entered" | "ceremony_started" | "ceremony_failed" | "done_shown";
11
- 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.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.audience.rejected" | "auth.audience.minted" | "auth.guest.created" | "auth.guest.restored" | "auth.guest.upgraded" | "auth.guest.failed" | "auth.funnel.mode_selected" | "auth.funnel.email_entered" | "auth.funnel.ceremony_started" | "auth.funnel.ceremony_failed" | "auth.funnel.done_shown";
12
- 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" | "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_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";
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.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.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
+ 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" | "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";
13
14
  /** One flat record per event = one row in the `auth_events` stream. */
14
15
  export interface AuthEventRecord {
15
16
  service: "auth";
@@ -26,11 +27,23 @@ export interface AuthEventRecord {
26
27
  mode: AuthMode | null;
27
28
  step: FunnelStep | null;
28
29
  errorCode: AuthErrorCode | null;
30
+ /** Role-audit fields: who changed whose global role, from what, to what.
31
+ * `userId`/`username` carry the target. */
32
+ actorId: string | null;
33
+ roleFrom: GlobalRole | null;
34
+ roleTo: GlobalRole | null;
29
35
  ip: string | null;
30
36
  country: string | null;
31
37
  userAgent: string | null;
32
38
  durationMs: number | null;
33
39
  traceId: string | null;
40
+ /** Room-event scheduler fields: which series and dated instance a transition
41
+ * or dispatch was about, and for a dispatch, which reminder and how many
42
+ * subscribers it reached. */
43
+ roomEventId: string | null;
44
+ occurrenceId: string | null;
45
+ dispatchKind: string | null;
46
+ recipients: number | null;
34
47
  }
35
48
  /** Body accepted by `POST /api/events` from the dialog. */
36
49
  export interface BeaconBody {
@@ -1,11 +1,12 @@
1
- export type { AdditionalSession, AuthConfig, AuthResponse, AuthResponseWithWebAuthn, FlowCredential, FlowUser, PasskeyPluginOptions, VerifiedFlowJWT, VerifyOptions, WebAuthnSignature, } from "./auth";
2
- export type { BoundaryError, DialogCustomFeatures, DialogCustomLabels, DialogError, DialogReferrer, DialogState, ProfileData, ProfileIdentity, } from "./dialog";
1
+ export type { AccountSession, AccountSessionsResponse, AdditionalSession, AuthConfig, AuthResponse, AuthResponseWithWebAuthn, FlowCredential, FlowUser, PasskeyPluginOptions, RevokeAllSessionsResponse, RevokeSessionResponse, SecurityActivityEntry, SecurityActivityResponse, SessionUser, VerifiedFlowJWT, VerifyOptions, WebAuthnSignature, } from "./auth";
2
+ export type { BoundaryError, DialogCustomFeatures, DialogCustomLabels, DialogError, DialogReferrer, DialogState, Loadable, ProfileData, ProfileIdentity, } from "./dialog";
3
3
  export type { AuthErrorCode, AuthEventName, AuthEventRecord, AuthMode, AuthOutcome, BeaconBody, FunnelStep, } from "./events";
4
4
  export type { Bridge, BridgeParameters, FlowAccount, FlowRemote, FlowRemoteConfig, FromWindowOptions, MessageResponse, Messenger, OneOf, Payload, QueuedRequest, ReadyOptions, RemoteFlowState, RemoteState, Schema, Storage, Topic, WithReady, } from "./messenger";
5
5
  export type { Call, ConnectCapabilities, ConnectRequest, ConnectResponse, GuestRequest, GuestResponse, MethodName, MethodParams, MethodResult, RestoreResponse, RpcRequest, SendCallsParams, SendCallsRequest, SendCallsResponse, SendTransactionParams, SendTransactionRequest, SendTransactionResponse, SignMessageRequest, SignMessageResponse, SignOutRequest, SignOutResponse, SignTypedDataRequest, SignTypedDataResponse, TransactionArgs, TypedData, TypedDataDomain, TypedDataField, } from "./protocol";
6
6
  export { getConnectCapabilities, isPersonalSignParams, isSendCallsParams, isSendTransactionParams, } from "./protocol";
7
- export type { CreateRoomInput, MyRooms, RoomChannel, RoomDetail, RoomList, RoomMemberEntry, RoomMembers, RoomOccupancy, RoomOwner, RoomPresenceEntry, RoomPresenceSnapshot, RoomRestrictionKind, RoomRole, RoomSummary, RoomSurfaceSettings, RoomsApi, RoomVisibility, UpdateRoomInput, VerifyRoomContext, } from "./rooms";
8
- export type { AccessKeyOptions, AccessKeyPreparation, Address, CreateDialogHostOptions, CreateFlowOptions, DialogHost, DialogOpenOptions, FinalizeAccessKeyParams, Flow, FlowConnectorParameters, FlowCookieNames, FlowIdProviderProps, FlowSessionState, FlowState, Listener, LoginOptions, MountProfileOptions, PrepareArgsWithAuth, PrepareTransactionRequestPhase, ProfileButtonHandle, ProfileButtonProps, ProfilePosition, ResolveAccountParams, ResolvedAccessKeyOptions, RootCredential, RunLoginParams, RunLoginResult, SendCallsArgs, SendTransactionArgs, Session, SigningContext, SignMessageArgs, SignTypedDataArgs, Store, StoredAccessKey, WagmiConnectCapabilities, WagmiConnectParams, } from "./sdk";
7
+ export type { CreateRoomEventInput, RoomEventAttendance, RoomEventFrequency, RoomEventInstance, RoomEventInterest, RoomEventInterestState, RoomEventList, RoomEventOccurrence, RoomEventRecurrence, RoomEventRoomRef, RoomEventSeries, RoomEventStatus, SetInterestInput, UpdateRoomEventInput, } from "./room-events";
8
+ export type { CreateRoomInput, GlobalRole, ModerationSubject, MyRooms, RoleChangeVerdict, RoomChannel, RoomDetail, RoomList, RoomMemberEntry, RoomMembers, RoomOccupancy, RoomOwner, RoomPresenceEntry, RoomPresenceSnapshot, RoomRestrictionKind, RoomRole, RoomSummary, RoomSurfaceSettings, RoomsApi, RoomVisibility, StaffEntry, UpdateRoomInput, VerifyRoomContext, Viewer, } from "./rooms";
9
+ export type { AccessKeyOptions, AccessKeyPreparation, Address, CreateDialogHostOptions, CreateFlowOptions, DialogHost, DialogOpenOptions, FinalizeAccessKeyParams, Flow, FlowConnectorParameters, FlowCookieNames, FlowIdProviderProps, FlowSessionState, FlowState, FlowWidgetHandle, FlowWidgetName, FlowWidgetProps, Listener, LoginOptions, MountFlowWidgetOptions, MountProfileOptions, PrepareArgsWithAuth, PrepareTransactionRequestPhase, ProfileButtonHandle, ProfileButtonProps, ProfilePosition, ResolveAccountParams, ResolvedAccessKeyOptions, RootCredential, RunLoginParams, RunLoginResult, SendCallsArgs, SendTransactionArgs, Session, SigningContext, SignMessageArgs, SignTypedDataArgs, Store, StoredAccessKey, WagmiConnectCapabilities, WagmiConnectParams, } from "./sdk";
9
10
  export type { ResolvedFlowSession, ResolveSessionOptions, SessionRouteOptions, SessionRouteResponse, SessionRouteSession, } from "./server";
10
11
  export type { CoinAsset, IdentifiedTx, TxApprove, TxConvert, TxSend, TxSwap, } from "./tx";
11
12
  export type { ActionDayContext, ActionEventKind, ActionSessionState, ActiveActionResponse, LevelProgress, PublicProfile, XpGrantResult, XpRecentGrant, XpSummary, } from "./xp";
@@ -90,6 +90,13 @@ export type Schema = [
90
90
  username: string;
91
91
  isGuest: boolean;
92
92
  } | null;
93
+ } | {
94
+ type: "profile-token-request";
95
+ id: string;
96
+ } | {
97
+ type: "profile-token";
98
+ id: string;
99
+ token: string | null;
93
100
  } | {
94
101
  type: "profile-login";
95
102
  mode: "signin" | "signup";
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Room events — scheduled happenings inside a room, shared by the server
3
+ * routes and the client SDK.
4
+ *
5
+ * Deliberately named `room-events` throughout: `types/events.ts` and
6
+ * `routes/events.ts` already mean structured product/telemetry events, and the
7
+ * two must never blur.
8
+ *
9
+ * Two shapes, and which one an endpoint speaks is load-bearing. A
10
+ * {@link RoomEventSeries} is what an owner authors and edits; an instance is
11
+ * one dated occurrence of it. Players only ever address occurrences — interest,
12
+ * reminders and attendance all attach to an instance, so a weekly race's third
13
+ * week is a thing in its own right.
14
+ */
15
+ /** Discord's Guild Scheduled Event lifecycle, mirrored exactly. */
16
+ export type RoomEventStatus = "scheduled" | "active" | "completed" | "cancelled";
17
+ export type RoomEventFrequency = "daily" | "weekly" | "monthly";
18
+ /**
19
+ * The RRULE subset a series may repeat on. Expanded in the event's IANA
20
+ * timezone by the (db-free) expander, never in UTC. `until` is an ISO instant;
21
+ * `until` and `count` are mutually exclusive and both may be absent for an
22
+ * open-ended series.
23
+ */
24
+ export interface RoomEventRecurrence {
25
+ freq: RoomEventFrequency;
26
+ interval?: number;
27
+ /** 0 = Sunday … 6 = Saturday, matching `Date.prototype.getDay`. */
28
+ byweekday?: number[];
29
+ bymonthday?: number[];
30
+ until?: string;
31
+ count?: number;
32
+ }
33
+ /** The room an event belongs to, denormalized so a listing needs no second read. */
34
+ export interface RoomEventRoomRef {
35
+ id: string;
36
+ slug: string;
37
+ displayName: string;
38
+ }
39
+ /**
40
+ * Interest in one dated instance. `interestedCount` is public; the two viewer
41
+ * flags are meaningful only when the request carried a bearer and read false
42
+ * otherwise — the same shape the room payload uses for the caller's role.
43
+ *
44
+ * A player counts as interested in an instance either by marking that instance
45
+ * or by following its whole series, so `viewerInterested` can be true while
46
+ * `viewerFollowing` is what actually produced it.
47
+ */
48
+ export interface RoomEventInterest {
49
+ interestedCount: number;
50
+ viewerInterested: boolean;
51
+ viewerFollowing: boolean;
52
+ }
53
+ /**
54
+ * One dated instance, as it appears under the series that owns it: only what
55
+ * differs per instance, since the series already carries the title, room and
56
+ * timezone. `startsAt`/`endsAt` are ISO instants in UTC — render them in the
57
+ * series' `timezone` to show the local time the owner scheduled.
58
+ */
59
+ export interface RoomEventInstance extends RoomEventInterest {
60
+ id: string;
61
+ startsAt: string;
62
+ endsAt: string | null;
63
+ status: RoomEventStatus;
64
+ /** Moved or cancelled on its own, so a series-wide edit leaves it alone. */
65
+ overridden: boolean;
66
+ /** Written once the instance completes — the history view reads these rather
67
+ * than scanning presence. Null while it has not finished. */
68
+ attendance: RoomEventAttendance | null;
69
+ }
70
+ /** What actually happened, as observed from room presence while it ran. */
71
+ export interface RoomEventAttendance {
72
+ attendeeCount: number;
73
+ peakConcurrent: number;
74
+ startedAt: string | null;
75
+ endedAt: string | null;
76
+ }
77
+ /** An instance standing on its own in a listing, carrying enough of its series
78
+ * and room to render a card. */
79
+ export interface RoomEventOccurrence extends RoomEventInstance {
80
+ eventId: string;
81
+ room: RoomEventRoomRef;
82
+ title: string;
83
+ description: string | null;
84
+ coverUrl: string | null;
85
+ timezone: string;
86
+ /** True when the parent series repeats, so a card can badge it as recurring. */
87
+ recurring: boolean;
88
+ createdBy: string | null;
89
+ }
90
+ /** The authored series, with the instances it has materialized so far. */
91
+ export interface RoomEventSeries {
92
+ id: string;
93
+ room: RoomEventRoomRef;
94
+ title: string;
95
+ description: string | null;
96
+ coverUrl: string | null;
97
+ startsAt: string;
98
+ endsAt: string | null;
99
+ timezone: string;
100
+ recurrence: RoomEventRecurrence | null;
101
+ /** Minutes before the start at which subscribers are reminded; null is the
102
+ * default schedule, an empty list is silence. */
103
+ reminderOffsets: number[] | null;
104
+ status: RoomEventStatus;
105
+ createdBy: string | null;
106
+ createdAt: string;
107
+ updatedAt: string;
108
+ occurrences: RoomEventInstance[];
109
+ }
110
+ export interface RoomEventList {
111
+ events: RoomEventOccurrence[];
112
+ }
113
+ export interface CreateRoomEventInput {
114
+ title: string;
115
+ description?: string | null;
116
+ coverUrl?: string | null;
117
+ startsAt: string;
118
+ endsAt?: string | null;
119
+ timezone?: string;
120
+ recurrence?: RoomEventRecurrence | null;
121
+ reminderOffsets?: number[] | null;
122
+ }
123
+ /** Every create field, all optional — a patch is merged over the stored row and
124
+ * validated by the same rules, so the two shapes can never drift. */
125
+ export type UpdateRoomEventInput = Partial<CreateRoomEventInput>;
126
+ export interface SetInterestInput {
127
+ /** Also follow (or unfollow) the whole series, not just this instance. */
128
+ series?: boolean;
129
+ }
130
+ /** What the interest endpoints answer with, so a card can update in place. */
131
+ export interface RoomEventInterestState extends RoomEventInterest {
132
+ occurrenceId: string;
133
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Room events — scheduled happenings inside a room, shared by the server
3
+ * routes and the client SDK.
4
+ *
5
+ * Deliberately named `room-events` throughout: `types/events.ts` and
6
+ * `routes/events.ts` already mean structured product/telemetry events, and the
7
+ * two must never blur.
8
+ *
9
+ * Two shapes, and which one an endpoint speaks is load-bearing. A
10
+ * {@link RoomEventSeries} is what an owner authors and edits; an instance is
11
+ * one dated occurrence of it. Players only ever address occurrences — interest,
12
+ * reminders and attendance all attach to an instance, so a weekly race's third
13
+ * week is a thing in its own right.
14
+ */
@@ -5,6 +5,42 @@ export type RoomVisibility = "public" | "unlisted" | "private";
5
5
  * Member rows carry moderator|member.
6
6
  */
7
7
  export type RoomRole = "owner" | "moderator" | "member";
8
+ /**
9
+ * Global permission tier held on the user row, composing over room roles: an
10
+ * admin holds owner-power in any room, a platform moderator holds mod-power
11
+ * anywhere. Read from the DB at enforcement time — never a JWT claim — so a
12
+ * demotion is instant.
13
+ */
14
+ export type GlobalRole = "user" | "moderator" | "admin";
15
+ /** What the moderation lattice ranks: a subject's global tier plus their
16
+ * room-axis role (`owner` from `room.ownerId`, else the member row). */
17
+ export interface ModerationSubject {
18
+ globalRole: GlobalRole;
19
+ roomRole: RoomRole | null;
20
+ }
21
+ /** A verified caller resolved against one room: the raw composite the rank
22
+ * lattice needs, plus the wire projection every route-side gate compares. */
23
+ export interface Viewer {
24
+ sub: string;
25
+ guest: boolean;
26
+ subject: ModerationSubject;
27
+ role: RoomRole | null;
28
+ }
29
+ /** One row of `GET /api/admin/staff` — a user holding a non-`user` tier. */
30
+ export interface StaffEntry {
31
+ userId: string;
32
+ username: string;
33
+ role: GlobalRole;
34
+ }
35
+ /** Outcome of a global-role change request: refused with a reason, or
36
+ * allowed with the audit event it is (`null` for a no-op). */
37
+ export type RoleChangeVerdict = {
38
+ allowed: true;
39
+ event: "auth.role.granted" | "auth.role.revoked" | null;
40
+ } | {
41
+ allowed: false;
42
+ reason: "self_target" | "guest_target";
43
+ };
8
44
  export type RoomChannel = "game" | "voice" | "text";
9
45
  export type RoomRestrictionKind = "ban" | "mute";
10
46
  export interface RoomOwner {
@@ -217,6 +217,54 @@ export type ProfileButtonHandle = {
217
217
  /** Tears down the iframe, listeners, and any auto-created container. */
218
218
  destroy: () => void;
219
219
  };
220
+ /** The inline Flow ID widgets an embedder can mount alongside its own UI. */
221
+ export type FlowWidgetName = "xp" | "action-timer";
222
+ export type MountFlowWidgetOptions = {
223
+ /** Which inline widget to mount. */
224
+ widget: FlowWidgetName;
225
+ /**
226
+ * Element to append the widget iframe to. Defaults to `document.body`, which
227
+ * is what a fixed-position overlay wants; pass a container to lay the widget
228
+ * out inline instead.
229
+ */
230
+ container?: HTMLElement;
231
+ /**
232
+ * The Flow ID origin serving the widget (e.g. https://id.flow.industries).
233
+ * Defaults to the resolved Flow instance's `host`, then the SDK default.
234
+ */
235
+ host?: string;
236
+ /**
237
+ * Color scheme for the widget chrome. Set this to match the host (the dark
238
+ * game UI passes "dark"). Defaults to "light dark" (follows the OS).
239
+ */
240
+ theme?: "light" | "dark" | "light dark";
241
+ /** Explicit Flow instance. Defaults to the createFlow() singleton. */
242
+ flow?: Flow;
243
+ };
244
+ export type FlowWidgetHandle = {
245
+ /**
246
+ * The mounted iframe, so the embedder owns geometry and visibility. Null
247
+ * under SSR, where nothing was mounted.
248
+ */
249
+ readonly frame: HTMLIFrameElement | null;
250
+ /**
251
+ * Sends a widget-specific message (e.g. `{ type: "xp-refresh" }`), pinned to
252
+ * the Flow ID origin.
253
+ */
254
+ post: (message: unknown) => void;
255
+ /** Tears down the iframe and its bridge. */
256
+ destroy: () => void;
257
+ };
258
+ export type FlowWidgetProps = {
259
+ /** Which inline widget to mount. */
260
+ widget: FlowWidgetName;
261
+ /** Class applied to the host element the widget iframe fills. */
262
+ className?: string;
263
+ /** Override the Flow ID origin; defaults to the resolved Flow's `host`. */
264
+ host?: string;
265
+ /** Color scheme for the widget chrome; defaults to "light dark" (the OS). */
266
+ theme?: "light" | "dark" | "light dark";
267
+ };
220
268
  export type DialogHost = {
221
269
  open: (options?: DialogOpenOptions) => void;
222
270
  close: () => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flow-industries/id",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "main": "./dist/sdk/client/index.js",
5
5
  "module": "./dist/sdk/client/index.js",
6
6
  "types": "./dist/sdk/client/index.d.ts",
@@ -58,6 +58,7 @@
58
58
  "build:sdk": "tsc --project tsconfig.sdk.json",
59
59
  "prepublishOnly": "bun run build:sdk",
60
60
  "start": "bun run src/index.ts",
61
+ "worker": "bun run src/worker.ts",
61
62
  "email:dev": "email dev --dir src/emails --port 3010",
62
63
  "db:generate": "bunx @better-auth/cli generate",
63
64
  "db:migrate:gen": "bunx drizzle-kit generate",
@@ -91,6 +92,7 @@
91
92
  "devDependencies": {
92
93
  "@better-auth/cli": "^1.4.17",
93
94
  "@biomejs/biome": "2.4.12",
95
+ "@electric-sql/pglite": "^0.5.4",
94
96
  "@flow-industries/lint": "0.1.0",
95
97
  "@react-email/ui": "^6.6.0",
96
98
  "@tailwindcss/vite": "^4.1.18",
@@ -112,7 +114,7 @@
112
114
  },
113
115
  "dependencies": {
114
116
  "@aws-sdk/client-s3": "^3.1073.0",
115
- "@flow-industries/ui": "^0.15.3",
117
+ "@flow-industries/ui": "^0.19.0",
116
118
  "@hono/otel": "^1.1.2",
117
119
  "@openobserve/browser-logs": "^0.3.1",
118
120
  "@openobserve/browser-rum": "^0.3.1",