@flow-industries/id 0.3.1 → 0.6.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.
Files changed (42) hide show
  1. package/dist/sdk/client/access-key.d.ts +5 -3
  2. package/dist/sdk/client/access-key.js +6 -4
  3. package/dist/sdk/client/create-flow.js +145 -3
  4. package/dist/sdk/client/dialog-host.js +24 -18
  5. package/dist/sdk/client/iframe-host.d.ts +23 -0
  6. package/dist/sdk/client/iframe-host.js +34 -0
  7. package/dist/sdk/client/index.d.ts +2 -1
  8. package/dist/sdk/client/index.js +1 -0
  9. package/dist/sdk/client/methods.d.ts +1 -0
  10. package/dist/sdk/client/methods.js +1 -0
  11. package/dist/sdk/client/profile-button.d.ts +15 -0
  12. package/dist/sdk/client/profile-button.js +189 -0
  13. package/dist/sdk/client/signing.d.ts +1 -1
  14. package/dist/sdk/client/signing.js +3 -1
  15. package/dist/sdk/client/store.js +4 -1
  16. package/dist/sdk/dialog/remote/Messenger.d.ts +1 -2
  17. package/dist/sdk/dialog/remote/Messenger.js +3 -1
  18. package/dist/sdk/react/hooks.d.ts +6 -2
  19. package/dist/sdk/react/hooks.js +6 -2
  20. package/dist/sdk/react/index.d.ts +3 -2
  21. package/dist/sdk/react/index.js +2 -1
  22. package/dist/sdk/react/profile-button.d.ts +9 -0
  23. package/dist/sdk/react/profile-button.js +28 -0
  24. package/dist/sdk/react/provider.js +1 -1
  25. package/dist/sdk/types/auth.d.ts +13 -0
  26. package/dist/sdk/types/dialog.d.ts +22 -0
  27. package/dist/sdk/types/events.d.ts +40 -0
  28. package/dist/sdk/types/events.js +6 -0
  29. package/dist/sdk/types/index.d.ts +5 -4
  30. package/dist/sdk/types/messenger.d.ts +30 -0
  31. package/dist/sdk/types/protocol.d.ts +15 -1
  32. package/dist/sdk/types/sdk.d.ts +72 -1
  33. package/dist/sdk/verify.d.ts +7 -0
  34. package/dist/sdk/verify.js +7 -0
  35. package/dist/sdk/wagmi/index.js +8 -3
  36. package/package.json +30 -4
  37. package/dist/sdk/client/protocol.d.ts +0 -97
  38. package/dist/sdk/client/protocol.js +0 -9
  39. package/dist/sdk/client/types.d.ts +0 -35
  40. package/dist/sdk/client/types.js +0 -0
  41. package/dist/sdk/types.d.ts +0 -32
  42. package/dist/sdk/types.js +0 -0
@@ -1,6 +1,5 @@
1
1
  import type { Bridge, BridgeParameters, FromWindowOptions, Messenger } from "../../types";
2
- export type { Bridge, BridgeParameters, FromWindowOptions, Messenger, Payload, QueuedRequest, ReadyOptions, Schema, Topic, WithReady, } from "../../types";
3
- export type { MessageResponse as Response } from "../../types";
2
+ export type { Bridge, BridgeParameters, FromWindowOptions, MessageResponse as Response, Messenger, Payload, QueuedRequest, ReadyOptions, Schema, Topic, WithReady, } from "../../types";
4
3
  export declare function from(messenger: Messenger): Messenger;
5
4
  /**
6
5
  * Wraps a Window in the Messenger interface. Reads come from
@@ -38,7 +38,7 @@ export function from(messenger) {
38
38
  * isolation that prevents arbitrary pages from injecting messages.
39
39
  */
40
40
  export function fromWindow(w, options = {}) {
41
- const { targetOrigin } = options;
41
+ const { targetOrigin, source } = options;
42
42
  const listeners = new Map();
43
43
  return from({
44
44
  destroy() {
@@ -54,6 +54,8 @@ export function fromWindow(w, options = {}) {
54
54
  return;
55
55
  if (targetOrigin && event.origin !== targetOrigin)
56
56
  return;
57
+ if (source && event.source !== source)
58
+ return;
57
59
  listener(event.data.payload, event);
58
60
  }
59
61
  w.addEventListener("message", handler);
@@ -18,16 +18,20 @@ export declare function useFlowState(): FlowState;
18
18
  /**
19
19
  * Convenience hook that returns the most commonly needed identity data and
20
20
  * actions in one go: user/jwt/address plus `login`, `logout`, `restore`,
21
- * and `refreshJwt`. The `isAuthenticated` boolean is derived from `user`
22
- * being set so it tracks reactive state.
21
+ * `refreshJwt`, and `getToken`. The `isAuthenticated` boolean is derived from
22
+ * `user` being set so it tracks reactive state; `isGuest` is true while the
23
+ * user is a guest — gate sensitive actions on `!isGuest`, not `isAuthenticated`.
23
24
  */
24
25
  export declare function useFlowId(): {
25
26
  user: import("../types").FlowUser | null;
26
27
  jwt: string | null;
27
28
  address: `0x${string}` | null;
28
29
  isAuthenticated: boolean;
30
+ isGuest: boolean;
29
31
  login: (options?: import("../types").LoginOptions) => Promise<import("../types").Session>;
30
32
  logout: () => Promise<void>;
31
33
  restore: () => Promise<boolean>;
34
+ ensureGuest: () => Promise<boolean>;
32
35
  refreshJwt: () => Promise<boolean>;
36
+ getToken: () => Promise<string | null>;
33
37
  };
@@ -32,8 +32,9 @@ export function useFlowState() {
32
32
  /**
33
33
  * Convenience hook that returns the most commonly needed identity data and
34
34
  * actions in one go: user/jwt/address plus `login`, `logout`, `restore`,
35
- * and `refreshJwt`. The `isAuthenticated` boolean is derived from `user`
36
- * being set so it tracks reactive state.
35
+ * `refreshJwt`, and `getToken`. The `isAuthenticated` boolean is derived from
36
+ * `user` being set so it tracks reactive state; `isGuest` is true while the
37
+ * user is a guest — gate sensitive actions on `!isGuest`, not `isAuthenticated`.
37
38
  */
38
39
  export function useFlowId() {
39
40
  const flow = useFlow();
@@ -43,9 +44,12 @@ export function useFlowId() {
43
44
  jwt: state.jwt,
44
45
  address: state.address,
45
46
  isAuthenticated: state.user !== null,
47
+ isGuest: state.user?.isGuest ?? false,
46
48
  login: flow.login,
47
49
  logout: flow.logout,
48
50
  restore: flow.restore,
51
+ ensureGuest: flow.ensureGuest,
49
52
  refreshJwt: flow.refreshJwt,
53
+ getToken: flow.getToken,
50
54
  };
51
55
  }
@@ -1,3 +1,4 @@
1
- export { FlowIdProvider } from "./provider";
1
+ export type { FlowIdProviderProps, ProfileButtonProps } from "../types";
2
2
  export { useFlow, useFlowId, useFlowState } from "./hooks";
3
- export type { FlowIdProviderProps } from "../types";
3
+ export { ProfileButton } from "./profile-button";
4
+ export { FlowIdProvider } from "./provider";
@@ -1,2 +1,3 @@
1
- export { FlowIdProvider } from "./provider";
2
1
  export { useFlow, useFlowId, useFlowState } from "./hooks";
2
+ export { ProfileButton } from "./profile-button";
3
+ export { FlowIdProvider } from "./provider";
@@ -0,0 +1,9 @@
1
+ import type { ProfileButtonProps } from "../types";
2
+ /**
3
+ * Drops the universal Flow ID profile button into a React tree. Renders a host
4
+ * element (style it via `className` for placement/padding — e.g. top-right) that
5
+ * the pill iframe mounts into; the iframe itself is sized to its content and
6
+ * expands to the profile dialog on click. Resolves the Flow instance from a
7
+ * `<FlowIdProvider>` or the createFlow() singleton.
8
+ */
9
+ export declare function ProfileButton({ className, host, theme }: ProfileButtonProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from "react";
3
+ import { createProfileButton } from "../client/profile-button";
4
+ import { useFlow } from "./hooks";
5
+ /**
6
+ * Drops the universal Flow ID profile button into a React tree. Renders a host
7
+ * element (style it via `className` for placement/padding — e.g. top-right) that
8
+ * the pill iframe mounts into; the iframe itself is sized to its content and
9
+ * expands to the profile dialog on click. Resolves the Flow instance from a
10
+ * `<FlowIdProvider>` or the createFlow() singleton.
11
+ */
12
+ export function ProfileButton({ className, host, theme }) {
13
+ const flow = useFlow();
14
+ const ref = useRef(null);
15
+ useEffect(() => {
16
+ const container = ref.current;
17
+ if (!container)
18
+ return;
19
+ const handle = createProfileButton({
20
+ container,
21
+ flow,
22
+ ...(host ? { host } : {}),
23
+ ...(theme ? { theme } : {}),
24
+ });
25
+ return () => handle.destroy();
26
+ }, [flow, host, theme]);
27
+ return _jsx("div", { ref: ref, className: className });
28
+ }
@@ -11,5 +11,5 @@ export const FlowContext = createContext(null);
11
11
  * the dependency explicit at a particular boundary.
12
12
  */
13
13
  export function FlowIdProvider({ flow, children }) {
14
- return _jsx(FlowContext.Provider, { value: flow ?? null, children: children });
14
+ return (_jsx(FlowContext.Provider, { value: flow ?? null, children: children }));
15
15
  }
@@ -2,6 +2,13 @@ import type { JWTPayload } from "jose";
2
2
  export type FlowUser = {
3
3
  id: string;
4
4
  username: string;
5
+ /**
6
+ * True only for guest accounts (no passkey/email yet). Optional so the many
7
+ * `{ id, username }` producers stay valid; readers treat `undefined`/`false`
8
+ * as "not a guest". Sensitive actions should gate on `!isGuest` (or the JWT
9
+ * `guest` claim), never on session presence alone.
10
+ */
11
+ isGuest?: boolean;
5
12
  };
6
13
  export type FlowCredential = {
7
14
  id: string;
@@ -42,4 +49,10 @@ export type VerifyOptions = {
42
49
  export type VerifiedFlowJWT = JWTPayload & {
43
50
  sub: string;
44
51
  username?: string;
52
+ /**
53
+ * Present and `true` only on guest tokens. A relying party gating sensitive
54
+ * actions MUST treat a MISSING claim as `guest === true` (fail-safe), so a
55
+ * future token-format change can never silently grant full privileges.
56
+ */
57
+ guest?: boolean;
45
58
  };
@@ -37,6 +37,27 @@ export type DialogCustomLabels = {
37
37
  switchAccount?: string;
38
38
  signUpLink?: string;
39
39
  };
40
+ /**
41
+ * The profile fields the widget renders, read from `/api/me` (the cookie
42
+ * session's `session.user`). `image`/`createdAt` are not on the SDK's
43
+ * `FlowUser`, so they only come from this authenticated fetch.
44
+ */
45
+ export type ProfileData = {
46
+ username: string;
47
+ image?: string | null;
48
+ createdAt?: string;
49
+ isGuest?: boolean;
50
+ };
51
+ /**
52
+ * Identity the embedding host pushes into the widget over postMessage. Lets the
53
+ * pill render the username immediately (and reactively on login/logout) without
54
+ * waiting on `/api/me` — the fast path that also works where third-party cookies
55
+ * are blocked (Safari/Firefox).
56
+ */
57
+ export type ProfileIdentity = {
58
+ username: string;
59
+ isGuest: boolean;
60
+ };
40
61
  export type DialogState = {
41
62
  mode: string;
42
63
  display: "floating" | "drawer" | "full";
@@ -50,6 +71,7 @@ export type DialogState = {
50
71
  username?: string;
51
72
  email?: string;
52
73
  }>;
74
+ shown: boolean;
53
75
  customFeatures?: DialogCustomFeatures;
54
76
  customLabels?: DialogCustomLabels;
55
77
  };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Structured auth-observability events. Emitted server-side to the dedicated
3
+ * OpenObserve `auth_events` stream (and mirrored to stdout). The field names
4
+ * here are the contract every dashboard/alert query depends on — changing one
5
+ * is a breaking change to the dashboard.
6
+ */
7
+ export type AuthOutcome = "success" | "failure" | "info";
8
+ export type AuthMode = "sign-up" | "sign-in";
9
+ /** Client-only funnel steps reported via the `/api/events` beacon. */
10
+ export type FunnelStep = "mode_selected" | "email_entered" | "ceremony_started" | "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.restore.succeeded" | "auth.restore.failed" | "auth.restore.no_session" | "auth.jwt.verified" | "auth.jwt.rejected" | "auth.signout" | "auth.audience.rejected" | "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.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" | "guest_rate_limited" | "guest_global_limit" | "guest_username_exhausted" | "already_upgraded" | "malformed_token" | "unknown_key" | "verification_failed" | "internal_error";
13
+ /** One flat record per event = one row in the `auth_events` stream. */
14
+ export interface AuthEventRecord {
15
+ service: "auth";
16
+ env: string;
17
+ event: AuthEventName;
18
+ outcome: AuthOutcome;
19
+ app: string | null;
20
+ audience: string | null;
21
+ userId: string | null;
22
+ username: string | null;
23
+ address: string | null;
24
+ credentialId: string | null;
25
+ funnelId: string | null;
26
+ mode: AuthMode | null;
27
+ step: FunnelStep | null;
28
+ errorCode: AuthErrorCode | null;
29
+ ip: string | null;
30
+ country: string | null;
31
+ userAgent: string | null;
32
+ durationMs: number | null;
33
+ traceId: string | null;
34
+ }
35
+ /** Body accepted by `POST /api/events` from the dialog. */
36
+ export interface BeaconBody {
37
+ step: FunnelStep;
38
+ funnelId: string;
39
+ mode?: AuthMode | null;
40
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Structured auth-observability events. Emitted server-side to the dedicated
3
+ * OpenObserve `auth_events` stream (and mirrored to stdout). The field names
4
+ * here are the contract every dashboard/alert query depends on — changing one
5
+ * is a breaking change to the dashboard.
6
+ */
@@ -1,7 +1,8 @@
1
1
  export type { AuthConfig, AuthResponse, AuthResponseWithWebAuthn, FlowCredential, FlowUser, PasskeyPluginOptions, VerifiedFlowJWT, VerifyOptions, WebAuthnSignature, } from "./auth";
2
- export type { AccessKeyOptions, AccessKeyPreparation, Address, CreateDialogHostOptions, CreateFlowOptions, DialogHost, DialogOpenOptions, FinalizeAccessKeyParams, Flow, FlowIdProviderProps, FlowConnectorParameters, FlowState, Listener, LoginOptions, PrepareArgsWithAuth, PrepareTransactionRequestPhase, ResolveAccountParams, ResolvedAccessKeyOptions, RootCredential, RunLoginParams, RunLoginResult, SendCallsArgs, SendTransactionArgs, Session, SignMessageArgs, SignTypedDataArgs, SigningContext, StoredAccessKey, Store, WagmiConnectCapabilities, WagmiConnectParams, } from "./sdk";
3
- export { isPersonalSignParams, isSendCallsParams, isSendTransactionParams, } from "./protocol";
4
- export type { Call, ConnectCapabilities, ConnectRequest, ConnectResponse, MethodName, MethodParams, MethodResult, RestoreRequest, RestoreResponse, RpcRequest, SendCallsParams, SendCallsRequest, SendCallsResponse, SendTransactionParams, SendTransactionRequest, SendTransactionResponse, SignMessageRequest, SignMessageResponse, SignOutRequest, SignOutResponse, SignTypedDataRequest, SignTypedDataResponse, TransactionArgs, TypedData, TypedDataDomain, TypedDataField, } from "./protocol";
2
+ export type { BoundaryError, DialogCustomFeatures, DialogCustomLabels, DialogError, DialogReferrer, DialogState, ProfileData, ProfileIdentity, } from "./dialog";
3
+ export type { AuthErrorCode, AuthEventName, AuthEventRecord, AuthMode, AuthOutcome, BeaconBody, FunnelStep, } from "./events";
5
4
  export type { Bridge, BridgeParameters, FlowAccount, FlowRemote, FlowRemoteConfig, FromWindowOptions, MessageResponse, Messenger, OneOf, Payload, QueuedRequest, ReadyOptions, RemoteFlowState, RemoteState, Schema, Storage, Topic, WithReady, } from "./messenger";
6
- export type { BoundaryError, DialogCustomFeatures, DialogCustomLabels, DialogError, DialogReferrer, DialogState, } from "./dialog";
5
+ export type { Call, ConnectCapabilities, ConnectRequest, ConnectResponse, GuestRequest, GuestResponse, MethodName, MethodParams, MethodResult, RestoreRequest, RestoreResponse, RpcRequest, SendCallsParams, SendCallsRequest, SendCallsResponse, SendTransactionParams, SendTransactionRequest, SendTransactionResponse, SignMessageRequest, SignMessageResponse, SignOutRequest, SignOutResponse, SignTypedDataRequest, SignTypedDataResponse, TransactionArgs, TypedData, TypedDataDomain, TypedDataField, } from "./protocol";
6
+ export { isPersonalSignParams, isSendCallsParams, isSendTransactionParams, } from "./protocol";
7
+ export type { AccessKeyOptions, AccessKeyPreparation, Address, CreateDialogHostOptions, CreateFlowOptions, DialogHost, DialogOpenOptions, FinalizeAccessKeyParams, Flow, FlowConnectorParameters, FlowIdProviderProps, 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
8
  export type { CoinAsset, IdentifiedTx, TxApprove, TxConvert, TxSend, TxSwap, } from "./tx";
@@ -65,6 +65,7 @@ export type Schema = [
65
65
  };
66
66
  features?: Record<string, boolean>;
67
67
  labels?: Record<string, string>;
68
+ route?: string;
68
69
  } | {
69
70
  type: "switch";
70
71
  mode: "iframe" | "popup" | "popup-standalone" | "page";
@@ -77,6 +78,27 @@ export type Schema = [
77
78
  theme: {
78
79
  colorScheme?: string;
79
80
  };
81
+ } | {
82
+ type: "profile-resize";
83
+ width: number;
84
+ height: number;
85
+ } | {
86
+ type: "profile-expand";
87
+ } | {
88
+ type: "profile-identity";
89
+ identity: {
90
+ username: string;
91
+ isGuest: boolean;
92
+ } | null;
93
+ } | {
94
+ type: "profile-login";
95
+ mode: "signin" | "signup";
96
+ } | {
97
+ type: "profile-logout";
98
+ } | {
99
+ type: "dialog-shown";
100
+ } | {
101
+ type: "dialog-hidden";
80
102
  };
81
103
  response: undefined;
82
104
  }
@@ -106,6 +128,14 @@ export type Bridge = WithReady & {
106
128
  };
107
129
  export type FromWindowOptions = {
108
130
  targetOrigin?: string;
131
+ /**
132
+ * When set, inbound messages are only accepted from this exact window
133
+ * (`event.source`). Required when more than one same-origin iframe posts to
134
+ * the same host window (e.g. the auth dialog and the profile widget) —
135
+ * origin filtering alone can't tell same-origin frames apart, so without this
136
+ * one frame's `ready` handshake would resolve the other bridge's gate.
137
+ */
138
+ source?: MessageEventSource | null;
109
139
  };
110
140
  export type BridgeParameters = {
111
141
  from: Messenger;
@@ -1,6 +1,6 @@
1
1
  import type { FlowCredential, FlowUser, WebAuthnSignature } from "./auth";
2
2
  import type { Address } from "./sdk";
3
- export type MethodName = "wallet_connect" | "personal_sign" | "eth_signTypedData" | "eth_sendTransaction" | "wallet_sendCalls" | "wallet_restore" | "wallet_signout";
3
+ export type MethodName = "wallet_connect" | "personal_sign" | "eth_signTypedData" | "eth_sendTransaction" | "wallet_sendCalls" | "wallet_restore" | "wallet_guest" | "wallet_signout";
4
4
  export type ConnectCapabilities = {
5
5
  createAccount?: boolean;
6
6
  signIn?: boolean;
@@ -65,6 +65,18 @@ export type RestoreResponse = {
65
65
  credential: FlowCredential;
66
66
  address: Address;
67
67
  };
68
+ export type GuestRequest = [];
69
+ /**
70
+ * A guest has no passkey and no derivable wallet address yet, so `credential`
71
+ * and `address` are always null — the shape mirrors RestoreResponse so the SDK
72
+ * can commit either to the same store fields.
73
+ */
74
+ export type GuestResponse = {
75
+ jwt: string;
76
+ user: FlowUser;
77
+ credential: null;
78
+ address: null;
79
+ };
68
80
  export type SignOutRequest = [];
69
81
  export type SignOutResponse = {
70
82
  ok: true;
@@ -76,6 +88,7 @@ export type MethodParams = {
76
88
  eth_sendTransaction: SendTransactionRequest;
77
89
  wallet_sendCalls: SendCallsRequest;
78
90
  wallet_restore: RestoreRequest;
91
+ wallet_guest: GuestRequest;
79
92
  wallet_signout: SignOutRequest;
80
93
  };
81
94
  export type MethodResult = {
@@ -85,6 +98,7 @@ export type MethodResult = {
85
98
  eth_sendTransaction: SendTransactionResponse;
86
99
  wallet_sendCalls: SendCallsResponse;
87
100
  wallet_restore: RestoreResponse;
101
+ wallet_guest: GuestResponse;
88
102
  wallet_signout: SignOutResponse;
89
103
  };
90
104
  export type RpcRequest = {
@@ -1,6 +1,6 @@
1
+ import type { ReactNode } from "react";
1
2
  import type { Chain, Hex, PrepareTransactionRequestParameters, SendTransactionParameters, SignTypedDataParameters, Transport, WalletClient } from "viem";
2
3
  import type { SendCallsParameters } from "viem/actions";
3
- import type { ReactNode } from "react";
4
4
  import type { FlowCredential, FlowUser, WebAuthnSignature } from "./auth";
5
5
  import type { ConnectCapabilities, ConnectResponse } from "./protocol";
6
6
  export type Address = `0x${string}`;
@@ -63,6 +63,13 @@ export type CreateFlowOptions = {
63
63
  * if the user has a valid Flow cookie session, state populates without UI.
64
64
  */
65
65
  autoRestore?: boolean;
66
+ /**
67
+ * If true, every visitor without an existing session is silently given a
68
+ * persistent guest account on startup (no UI, no passkey). Off by default —
69
+ * the consumer app opts in. Signing up later upgrades the guest in place, so
70
+ * data keyed on the Flow user id survives. See `ensureGuest`.
71
+ */
72
+ autoGuest?: boolean;
66
73
  };
67
74
  export type FlowState = {
68
75
  user: FlowUser | null;
@@ -122,6 +129,36 @@ export type DialogOpenOptions = {
122
129
  icon?: string;
123
130
  };
124
131
  };
132
+ /** Anchor for an auto-created (host-less) profile widget container. */
133
+ export type ProfilePosition = "top-right" | "top-left" | "bottom-right" | "bottom-left";
134
+ export type MountProfileOptions = {
135
+ /**
136
+ * Element to mount the pill iframe into. When omitted, a fixed-position
137
+ * container is created and anchored via `position` — the path of least
138
+ * resistance for non-DOM hosts (the Godot canvas overlay).
139
+ */
140
+ container?: HTMLElement;
141
+ /** Anchor used only when `container` is omitted. Defaults to "top-right". */
142
+ position?: ProfilePosition;
143
+ /** CSS padding for the auto-created container. Defaults to a responsive inset. */
144
+ padding?: string;
145
+ /**
146
+ * The Flow ID origin serving the widget (e.g. https://id.flow.industries).
147
+ * Defaults to the resolved Flow instance's `host`, then the SDK default.
148
+ */
149
+ host?: string;
150
+ /**
151
+ * Color scheme for the widget chrome. Set this to match the host (the dark
152
+ * game UI passes "dark"). Defaults to "light dark" (follows the OS).
153
+ */
154
+ theme?: "light" | "dark" | "light dark";
155
+ /** Explicit Flow instance. Defaults to the createFlow() singleton. */
156
+ flow?: Flow;
157
+ };
158
+ export type ProfileButtonHandle = {
159
+ /** Tears down the iframe, listeners, and any auto-created container. */
160
+ destroy: () => void;
161
+ };
125
162
  export type DialogHost = {
126
163
  open: (options?: DialogOpenOptions) => void;
127
164
  close: () => void;
@@ -136,10 +173,29 @@ export type Flow = {
136
173
  readonly credential: FlowState["credential"];
137
174
  readonly address: Address | null;
138
175
  readonly isAuthenticated: boolean;
176
+ /**
177
+ * True when the current session is a guest (authenticated but not yet a full
178
+ * account). `isAuthenticated` is also true for guests — gate sensitive
179
+ * actions on `!isGuest`, not on `isAuthenticated`.
180
+ */
181
+ readonly isGuest: boolean;
139
182
  login(options?: LoginOptions): Promise<Session>;
140
183
  logout(): Promise<void>;
141
184
  restore(): Promise<boolean>;
185
+ /**
186
+ * Ensures a session exists: restores an existing one, otherwise silently
187
+ * creates a persistent guest account. Idempotent and single-flight; resolves
188
+ * true if a session (guest or full) is now active, false if creation failed.
189
+ * Called automatically on startup when `autoGuest` is set.
190
+ */
191
+ ensureGuest(): Promise<boolean>;
142
192
  refreshJwt(): Promise<boolean>;
193
+ /**
194
+ * Returns a currently-valid JWT, silently refreshing from the cookie session
195
+ * if the cached token is expired or near expiry; null if there is no session.
196
+ * Prefer this over reading `jwt` directly when calling a backend.
197
+ */
198
+ getToken(): Promise<string | null>;
143
199
  signMessage(args: SignMessageArgs): Promise<Hex>;
144
200
  signTypedData(args: SignTypedDataArgs): Promise<Hex>;
145
201
  sendTransaction(args: SendTransactionArgs): Promise<Hex>;
@@ -152,6 +208,13 @@ export type Flow = {
152
208
  subscribe(listener: Listener<FlowState>): () => void;
153
209
  getState(): FlowState;
154
210
  dialog: DialogHost;
211
+ /**
212
+ * The resolved Flow ID origin this instance talks to (no trailing slash),
213
+ * e.g. `https://id.flow.industries`. Exposed so widgets like the profile
214
+ * button build their iframe URL from the single host configured on createFlow
215
+ * instead of re-resolving it.
216
+ */
217
+ readonly host: string;
155
218
  };
156
219
  export type FlowConnectorParameters = Omit<CreateFlowOptions, "chains" | "transports"> & {
157
220
  /**
@@ -176,6 +239,14 @@ export type FlowIdProviderProps = {
176
239
  flow?: Flow;
177
240
  children: ReactNode;
178
241
  };
242
+ export type ProfileButtonProps = {
243
+ /** Class applied to the host element the pill iframe mounts into. */
244
+ className?: string;
245
+ /** Override the Flow ID origin; defaults to the resolved Flow's `host`. */
246
+ host?: string;
247
+ /** Color scheme for the widget chrome; defaults to "light dark" (the OS). */
248
+ theme?: "light" | "dark" | "light dark";
249
+ };
179
250
  export type RunLoginParams = {
180
251
  dialog: DialogHost;
181
252
  store: Store<FlowState>;
@@ -11,5 +11,12 @@ export type { VerifiedFlowJWT, VerifyOptions } from "./types";
11
11
  *
12
12
  * `audience` MUST match the calling app's origin — this is what prevents
13
13
  * a token issued for `flow.talk` from being replayed against `flow.game`.
14
+ *
15
+ * Every Flow token carries an explicit boolean `guest` claim (`true` for guest
16
+ * sessions with no passkey yet, `false` for full accounts). When gating
17
+ * sensitive actions, require `payload.guest === false`; treat anything else —
18
+ * `true`, or a MISSING claim (not a Flow token, or a future format change) — as
19
+ * guest and deny. This fail-safe means a dropped or renamed claim can never
20
+ * silently grant a guest full privileges.
14
21
  */
15
22
  export declare function verifyFlowJWT(token: string, opts: VerifyOptions): Promise<VerifiedFlowJWT>;
@@ -12,6 +12,13 @@ const DEFAULT_ISSUER_URL = "https://id.flow.industries";
12
12
  *
13
13
  * `audience` MUST match the calling app's origin — this is what prevents
14
14
  * a token issued for `flow.talk` from being replayed against `flow.game`.
15
+ *
16
+ * Every Flow token carries an explicit boolean `guest` claim (`true` for guest
17
+ * sessions with no passkey yet, `false` for full accounts). When gating
18
+ * sensitive actions, require `payload.guest === false`; treat anything else —
19
+ * `true`, or a MISSING claim (not a Flow token, or a future format change) — as
20
+ * guest and deny. This fail-safe means a dropped or renamed claim can never
21
+ * silently grant a guest full privileges.
15
22
  */
16
23
  export async function verifyFlowJWT(token, opts) {
17
24
  const issuerUrl = opts.issuerUrl ?? DEFAULT_ISSUER_URL;
@@ -8,7 +8,8 @@ function loginOptionsFromCapabilities(capabilities) {
8
8
  return {
9
9
  ...(signUp ? { signUp: true } : {}),
10
10
  ...(!signUp && signIn ? { signIn: true } : {}),
11
- ...(!signUp && (capabilities?.signInHeadless || capabilities?.type === "sign-in")
11
+ ...(!signUp &&
12
+ (capabilities?.signInHeadless || capabilities?.type === "sign-in")
12
13
  ? { signInHeadless: true }
13
14
  : {}),
14
15
  };
@@ -137,8 +138,12 @@ export function flowConnector(parameters = {}) {
137
138
  return f.walletClient({ ...(chainId ? { chainId } : {}) });
138
139
  },
139
140
  async getProvider({ chainId } = {}) {
140
- const client = await this.getClient({ ...(chainId ? { chainId } : {}) });
141
- return { request: client.request };
141
+ const client = await this.getClient({
142
+ ...(chainId ? { chainId } : {}),
143
+ });
144
+ return {
145
+ request: client.request,
146
+ };
142
147
  },
143
148
  onAccountsChanged() { },
144
149
  onChainChanged(chain) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flow-industries/id",
3
- "version": "0.3.1",
3
+ "version": "0.6.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",
@@ -46,10 +46,16 @@
46
46
  "build:sdk": "tsc --project tsconfig.sdk.json",
47
47
  "prepublishOnly": "bun run build:sdk",
48
48
  "start": "bun run src/index.ts",
49
+ "email:dev": "email dev --dir src/emails --port 3010",
49
50
  "db:generate": "bunx @better-auth/cli generate",
50
- "db:migrate": "bunx drizzle-kit generate && bunx drizzle-kit migrate",
51
+ "db:migrate:gen": "bunx drizzle-kit generate",
52
+ "db:migrate": "bunx drizzle-kit migrate",
51
53
  "db:push": "bunx drizzle-kit push",
52
- "db:studio": "bunx drizzle-kit studio"
54
+ "db:studio": "bunx drizzle-kit studio",
55
+ "lint": "biome check",
56
+ "format": "biome format --write",
57
+ "check": "biome check --write",
58
+ "typecheck": "tsr generate && tsc --noEmit"
53
59
  },
54
60
  "peerDependencies": {
55
61
  "viem": ">=2.47.10",
@@ -67,21 +73,39 @@
67
73
  },
68
74
  "devDependencies": {
69
75
  "@better-auth/cli": "^1.4.17",
76
+ "@biomejs/biome": "2.4.12",
77
+ "@flow-industries/lint": "0.1.0",
78
+ "@react-email/ui": "^6.6.0",
70
79
  "@tailwindcss/vite": "^4.1.18",
71
80
  "@tanstack/react-router-devtools": "^1.160.0",
81
+ "@tanstack/router-cli": "^1.167.17",
72
82
  "@tanstack/router-plugin": "^1.160.0",
73
83
  "@types/bun": "latest",
74
84
  "@types/react": "^19.2.13",
75
85
  "@types/react-dom": "^19.2.3",
76
86
  "@vitejs/plugin-react": "^5.1.3",
77
87
  "drizzle-kit": "^0.31.8",
88
+ "pino-pretty": "^13.1.3",
89
+ "react-email": "^6.6.0",
78
90
  "tailwindcss": "^4.1.18",
79
91
  "tw-animate-css": "^1.4.0",
80
92
  "typescript": "^5",
81
93
  "vite": "^7.3.1"
82
94
  },
83
95
  "dependencies": {
84
- "@flow-industries/ui": "^0.15.2",
96
+ "@aws-sdk/client-s3": "^3.1073.0",
97
+ "@flow-industries/ui": "^0.15.3",
98
+ "@hono/otel": "^1.1.2",
99
+ "@openobserve/browser-logs": "^0.3.1",
100
+ "@openobserve/browser-rum": "^0.3.1",
101
+ "@opentelemetry/api": "^1.9.1",
102
+ "@opentelemetry/exporter-trace-otlp-http": "^0.218.0",
103
+ "@opentelemetry/resources": "^2.7.1",
104
+ "@opentelemetry/sdk-trace-base": "^2.7.1",
105
+ "@opentelemetry/sdk-trace-node": "^2.7.1",
106
+ "@opentelemetry/semantic-conventions": "^1.41.1",
107
+ "@react-email/components": "^1.0.12",
108
+ "@react-email/render": "^2.0.8",
85
109
  "@tanstack/query-sync-storage-persister": "^5.90.22",
86
110
  "@tanstack/react-query": "^5.90.20",
87
111
  "@tanstack/react-query-persist-client": "^5.90.22",
@@ -93,10 +117,12 @@
93
117
  "jose": "^6.1.3",
94
118
  "lucide-react": "^0.563.0",
95
119
  "motion": "^12.33.0",
120
+ "pino": "^10.3.1",
96
121
  "postgres": "^3.4.8",
97
122
  "react": "^19.2.4",
98
123
  "react-dom": "^19.2.4",
99
124
  "react-intersection-observer": "^10.0.2",
125
+ "sharp": "^0.35.2",
100
126
  "tempo.ts": "^0.14.2",
101
127
  "viem": "2.47.10",
102
128
  "wagmi": "^3.4.2",