@flow-industries/id 0.11.0 → 0.13.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,127 @@
1
+ /**
2
+ * The first-party session route every consumer app serves from its own
3
+ * origin — the only path between a browser and the refresh token. The
4
+ * rotating refresh token lives in an HttpOnly cookie owned by the app's
5
+ * server; the browser client calls this route instead of ever holding the
6
+ * token itself:
7
+ *
8
+ * - `GET` resolve: fresh JWT from the cookies, rotating server-side when
9
+ * the access token is expiring.
10
+ * - `POST` install: one-time handoff of a freshly minted session (refresh
11
+ * token + access JWT from the dialog) into the HttpOnly cookies.
12
+ * The JWT verifies locally against the issuer's cached JWKS — no
13
+ * rotation of a seconds-old token; a bogus refresh token simply
14
+ * dies at the first GET, like any planted cookie.
15
+ * - `DELETE` sign-out: revoke the token's lineage at the issuer and clear
16
+ * the cookies.
17
+ *
18
+ * Framework-agnostic (`Request` → `Response`); `flowSessionHandlers` from
19
+ * `@flow-industries/id/start` adapts it to a TanStack Start route file.
20
+ */
21
+ import { cookieNamesFor, parseCookieHeader } from "./cookies";
22
+ import { DEFAULT_SESSION_PATH, defaultAudience, resolveIdHost, } from "./id-host";
23
+ import { claimsToUser, clearingCookies, isJwtVerificationFailure, resolveSessionCore, sessionCookies, } from "./session-core";
24
+ import { verifyFlowJWT } from "./verify";
25
+ function json(body, status, setCookies = []) {
26
+ const headers = new Headers({
27
+ "Content-Type": "application/json",
28
+ "Cache-Control": "no-store",
29
+ });
30
+ for (const cookie of setCookies)
31
+ headers.append("Set-Cookie", cookie);
32
+ return new Response(JSON.stringify(body), { status, headers });
33
+ }
34
+ /**
35
+ * Cross-site requests must never reach the session route: the cookies are
36
+ * SameSite=Lax already, so this is defense in depth. `Sec-Fetch-Site` is
37
+ * authoritative where present ("none" = direct navigation, harmless); the
38
+ * Origin header — sent by browsers on every non-GET — must match the app.
39
+ * Requests carrying neither header (server-to-server tooling) pass.
40
+ */
41
+ function isCrossSite(request, audience) {
42
+ const site = request.headers.get("Sec-Fetch-Site");
43
+ if (site && site !== "same-origin" && site !== "none")
44
+ return true;
45
+ const origin = request.headers.get("Origin");
46
+ if (origin && origin !== audience && origin !== new URL(request.url).origin) {
47
+ return true;
48
+ }
49
+ return false;
50
+ }
51
+ /**
52
+ * Handles one session-route request (no path check — the caller routed it).
53
+ * `audience` defaults per `defaultAudience` (`APP_ORIGIN` env, then the
54
+ * request's own origin); `issuerUrl` per `defaultIdHost`.
55
+ */
56
+ export async function handleSessionRequest(request, opts = {}) {
57
+ const audience = opts.audience ?? defaultAudience(new URL(request.url).origin);
58
+ const issuerUrl = resolveIdHost(opts.issuerUrl, audience);
59
+ if (isCrossSite(request, audience)) {
60
+ return json({ state: null }, 403);
61
+ }
62
+ switch (request.method) {
63
+ case "GET": {
64
+ const { session, setCookies } = await resolveSessionCore(request.headers.get("Cookie"), audience, issuerUrl);
65
+ return json({ state: session }, 200, setCookies);
66
+ }
67
+ case "POST": {
68
+ const body = (await request.json().catch(() => null));
69
+ const refreshToken = body?.refreshToken;
70
+ const jwt = body?.jwt;
71
+ if (typeof refreshToken !== "string" ||
72
+ refreshToken.length === 0 ||
73
+ typeof jwt !== "string" ||
74
+ jwt.length === 0) {
75
+ return json({ state: null }, 400);
76
+ }
77
+ // The audience check on the verified JWT is what stops a token minted
78
+ // for another app from being installed here; a failed install must not
79
+ // clear cookies that may hold a live session.
80
+ try {
81
+ const claims = await verifyFlowJWT(jwt, { audience, issuerUrl });
82
+ return json({ state: { user: claimsToUser(claims), jwt } }, 200, sessionCookies(audience, jwt, refreshToken));
83
+ }
84
+ catch (err) {
85
+ return json({ state: null }, isJwtVerificationFailure(err) ? 401 : 502);
86
+ }
87
+ }
88
+ case "DELETE": {
89
+ const names = cookieNamesFor(audience);
90
+ const token = parseCookieHeader(request.headers.get("Cookie"))[names.refresh];
91
+ // Best-effort: clearing the cookies signs this browser out either way;
92
+ // an unreachable issuer just leaves the lineage to expire on its own.
93
+ if (token) {
94
+ try {
95
+ await fetch(`${issuerUrl}/api/session/revoke`, {
96
+ method: "POST",
97
+ headers: { Authorization: `Bearer ${token}` },
98
+ });
99
+ }
100
+ catch { }
101
+ }
102
+ const headers = new Headers({ "Cache-Control": "no-store" });
103
+ for (const cookie of clearingCookies(audience)) {
104
+ headers.append("Set-Cookie", cookie);
105
+ }
106
+ return new Response(null, { status: 204, headers });
107
+ }
108
+ default:
109
+ return new Response(null, {
110
+ status: 405,
111
+ headers: { Allow: "GET, POST, DELETE" },
112
+ });
113
+ }
114
+ }
115
+ /**
116
+ * Wraps `handleSessionRequest` with a path check for servers that route by
117
+ * hand (a Bun/Hono server): returns null for requests that aren't for the
118
+ * session path so the caller can fall through.
119
+ */
120
+ export function createSessionHandler(opts = {}) {
121
+ const path = opts.path ?? DEFAULT_SESSION_PATH;
122
+ return async (request) => {
123
+ if (new URL(request.url).pathname !== path)
124
+ return null;
125
+ return handleSessionRequest(request, opts);
126
+ };
127
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * TanStack Start integration — the whole Flow ID wiring a Start app needs,
3
+ * so integrating a new domain is one route file plus one root wrapper:
4
+ *
5
+ * ```tsx
6
+ * // src/routes/flow.session.ts — the first-party session route + SSR fn
7
+ * import { createServerFn } from "@tanstack/react-start";
8
+ * import { createFileRoute } from "@tanstack/react-router";
9
+ * import { flowSessionHandlers, resolveFlowSessionRequest } from "@flow-industries/id/start/server";
10
+ *
11
+ * export const getFlowSession = createServerFn({ method: "GET" }).handler(resolveFlowSessionRequest);
12
+ * export const Route = createFileRoute("/flow/session")({ server: { handlers: flowSessionHandlers() } });
13
+ *
14
+ * // src/routes/__root.tsx
15
+ * beforeLoad: () => flowSessionContext(getFlowSession),
16
+ * // and in the root component:
17
+ * <FlowRoot session={session}>{children}</FlowRoot>
18
+ * ```
19
+ *
20
+ * The server fn stays in app source because TanStack's compiler splits it
21
+ * into the server bundle there; everything it does lives here.
22
+ */
23
+ import { type ReactNode } from "react";
24
+ import type { AdditionalSession, FlowSessionState } from "../types";
25
+ /**
26
+ * The `beforeLoad` body: during SSR resolve the session server-side (via the
27
+ * app's `getFlowSession` server fn, which appends rotated Set-Cookie values);
28
+ * in the browser snapshot the live singleton instead — no network.
29
+ */
30
+ export declare function flowSessionContext(getSession: () => Promise<FlowSessionState | null>): Promise<{
31
+ session: FlowSessionState | null;
32
+ }>;
33
+ export type FlowRootProps = {
34
+ /** The session resolved by `flowSessionContext` in `beforeLoad`. */
35
+ session: FlowSessionState | null;
36
+ /** Mint a silent guest for first-time visitors (see `CreateFlowOptions`). */
37
+ autoGuest?: boolean;
38
+ /** Override the Flow ID origin; defaults per `defaultIdHost`. */
39
+ host?: string;
40
+ /** Extra audiences every mint requests (see `CreateFlowOptions`). */
41
+ additionalAudiences?: readonly string[];
42
+ /** One-time delivery of the extra sessions (see `CreateFlowOptions`). */
43
+ onAdditionalSessions?: (sessions: AdditionalSession[]) => void;
44
+ children: ReactNode;
45
+ };
46
+ /**
47
+ * Renders the subtree under a request-scoped static Flow on the server and
48
+ * the seeded browser singleton on the client, so the first client render
49
+ * matches the SSR HTML and no bootstrap refresh runs while the hydrated JWT
50
+ * is fresh.
51
+ */
52
+ export declare function FlowRoot({ session, autoGuest, host, additionalAudiences, onAdditionalSessions, children, }: FlowRootProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,58 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * TanStack Start integration — the whole Flow ID wiring a Start app needs,
4
+ * so integrating a new domain is one route file plus one root wrapper:
5
+ *
6
+ * ```tsx
7
+ * // src/routes/flow.session.ts — the first-party session route + SSR fn
8
+ * import { createServerFn } from "@tanstack/react-start";
9
+ * import { createFileRoute } from "@tanstack/react-router";
10
+ * import { flowSessionHandlers, resolveFlowSessionRequest } from "@flow-industries/id/start/server";
11
+ *
12
+ * export const getFlowSession = createServerFn({ method: "GET" }).handler(resolveFlowSessionRequest);
13
+ * export const Route = createFileRoute("/flow/session")({ server: { handlers: flowSessionHandlers() } });
14
+ *
15
+ * // src/routes/__root.tsx
16
+ * beforeLoad: () => flowSessionContext(getFlowSession),
17
+ * // and in the root component:
18
+ * <FlowRoot session={session}>{children}</FlowRoot>
19
+ * ```
20
+ *
21
+ * The server fn stays in app source because TanStack's compiler splits it
22
+ * into the server bundle there; everything it does lives here.
23
+ */
24
+ import { useMemo } from "react";
25
+ import { createFlow, createStaticFlow, getFlow } from "../client";
26
+ import { FlowIdProvider } from "../react";
27
+ /**
28
+ * The `beforeLoad` body: during SSR resolve the session server-side (via the
29
+ * app's `getFlowSession` server fn, which appends rotated Set-Cookie values);
30
+ * in the browser snapshot the live singleton instead — no network.
31
+ */
32
+ export async function flowSessionContext(getSession) {
33
+ if (typeof window === "undefined")
34
+ return { session: await getSession() };
35
+ const flow = getFlow();
36
+ const session = flow?.user && flow.jwt
37
+ ? { user: flow.user, jwt: flow.jwt, address: flow.address }
38
+ : null;
39
+ return { session };
40
+ }
41
+ /**
42
+ * Renders the subtree under a request-scoped static Flow on the server and
43
+ * the seeded browser singleton on the client, so the first client render
44
+ * matches the SSR HTML and no bootstrap refresh runs while the hydrated JWT
45
+ * is fresh.
46
+ */
47
+ export function FlowRoot({ session, autoGuest, host, additionalAudiences, onAdditionalSessions, children, }) {
48
+ const flow = useMemo(() => typeof window === "undefined"
49
+ ? createStaticFlow(session, { host })
50
+ : createFlow({
51
+ initialState: session,
52
+ autoGuest,
53
+ host,
54
+ additionalAudiences,
55
+ onAdditionalSessions,
56
+ }), [session, autoGuest, host, additionalAudiences, onAdditionalSessions]);
57
+ return _jsx(FlowIdProvider, { flow: flow, children: children });
58
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Server half of the TanStack Start integration: the session-route handlers
3
+ * and the SSR resolve used by the app's `getFlowSession` server fn. Import
4
+ * only from app-source files the Start compiler splits server-side (the
5
+ * session route file) — see `@flow-industries/id/start` for the wiring.
6
+ */
7
+ import type { FlowSessionState, SessionRouteOptions } from "../types";
8
+ /**
9
+ * The `getFlowSession` server-fn handler: resolves the visitor's session
10
+ * from the request cookies during SSR. A server-side refresh rotates the
11
+ * token, so the rotated Set-Cookie values are appended to the response here
12
+ * rather than left to callers. The audience is pinned by `APP_ORIGIN`
13
+ * (mandatory behind a TLS-terminating proxy), falling back to the request
14
+ * origin in dev.
15
+ */
16
+ export declare function resolveFlowSessionRequest(): Promise<FlowSessionState | null>;
17
+ /**
18
+ * Route handlers for the app's session route file:
19
+ * `createFileRoute("/flow/session")({ server: { handlers: flowSessionHandlers() } })`.
20
+ * The route path must match the client's `sessionPath` (default
21
+ * `/flow/session`).
22
+ */
23
+ export declare function flowSessionHandlers(opts?: SessionRouteOptions): {
24
+ GET: ({ request }: {
25
+ request: Request;
26
+ }) => Promise<Response>;
27
+ POST: ({ request }: {
28
+ request: Request;
29
+ }) => Promise<Response>;
30
+ DELETE: ({ request }: {
31
+ request: Request;
32
+ }) => Promise<Response>;
33
+ };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Server half of the TanStack Start integration: the session-route handlers
3
+ * and the SSR resolve used by the app's `getFlowSession` server fn. Import
4
+ * only from app-source files the Start compiler splits server-side (the
5
+ * session route file) — see `@flow-industries/id/start` for the wiring.
6
+ */
7
+ import { getRequestHeader, getRequestUrl, setResponseHeader, } from "@tanstack/react-start/server";
8
+ import { defaultAudience, defaultIdHost } from "../id-host";
9
+ import { resolveSession } from "../server";
10
+ import { handleSessionRequest } from "../session-route";
11
+ /**
12
+ * The `getFlowSession` server-fn handler: resolves the visitor's session
13
+ * from the request cookies during SSR. A server-side refresh rotates the
14
+ * token, so the rotated Set-Cookie values are appended to the response here
15
+ * rather than left to callers. The audience is pinned by `APP_ORIGIN`
16
+ * (mandatory behind a TLS-terminating proxy), falling back to the request
17
+ * origin in dev.
18
+ */
19
+ export async function resolveFlowSessionRequest() {
20
+ const audience = defaultAudience(getRequestUrl().origin);
21
+ const { state, setCookies } = await resolveSession(getRequestHeader("cookie"), { audience, issuerUrl: defaultIdHost(audience) });
22
+ if (setCookies.length > 0)
23
+ setResponseHeader("Set-Cookie", setCookies);
24
+ return state;
25
+ }
26
+ /**
27
+ * Route handlers for the app's session route file:
28
+ * `createFileRoute("/flow/session")({ server: { handlers: flowSessionHandlers() } })`.
29
+ * The route path must match the client's `sessionPath` (default
30
+ * `/flow/session`).
31
+ */
32
+ export function flowSessionHandlers(opts = {}) {
33
+ const handler = ({ request }) => handleSessionRequest(request, opts);
34
+ return { GET: handler, POST: handler, DELETE: handler };
35
+ }
@@ -14,11 +14,24 @@ export type FlowCredential = {
14
14
  id: string;
15
15
  publicKey: string;
16
16
  };
17
+ /**
18
+ * A session minted for one of the ADDITIONAL audiences a login requested
19
+ * (AUTH-39): its own refresh-token lineage and access JWT, both bound to
20
+ * `audience`. Installed once into that origin's own HttpOnly cookies via its
21
+ * `/flow/session` route (the embed bootstrap) — never held by page JS beyond
22
+ * the one-time handoff.
23
+ */
24
+ export type AdditionalSession = {
25
+ audience: string;
26
+ jwt: string;
27
+ refreshToken: string;
28
+ };
17
29
  export type AuthResponse = {
18
30
  jwt: string;
19
31
  refreshToken: string;
20
32
  user: FlowUser;
21
33
  credential: FlowCredential;
34
+ additionalSessions?: AdditionalSession[];
22
35
  };
23
36
  export type AuthConfig = {
24
37
  rpId: string;
@@ -8,7 +8,7 @@ export type AuthOutcome = "success" | "failure" | "info";
8
8
  export type AuthMode = "sign-up" | "sign-in";
9
9
  /** Client-only funnel steps reported via the `/api/events` beacon. */
10
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.refresh.succeeded" | "auth.refresh.failed" | "auth.refresh.reuse" | "auth.refresh.grace" | "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";
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.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.done_shown";
12
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_reuse_detected" | "guest_rate_limited" | "guest_global_limit" | "guest_username_exhausted" | "already_upgraded" | "malformed_token" | "unknown_key" | "verification_failed" | "internal_error";
13
13
  /** One flat record per event = one row in the `auth_events` stream. */
14
14
  export interface AuthEventRecord {
@@ -1,11 +1,11 @@
1
- export type { AuthConfig, AuthResponse, AuthResponseWithWebAuthn, FlowCredential, FlowUser, PasskeyPluginOptions, VerifiedFlowJWT, VerifyOptions, WebAuthnSignature, } from "./auth";
1
+ export type { AdditionalSession, AuthConfig, AuthResponse, AuthResponseWithWebAuthn, FlowCredential, FlowUser, PasskeyPluginOptions, VerifiedFlowJWT, VerifyOptions, WebAuthnSignature, } from "./auth";
2
2
  export type { BoundaryError, DialogCustomFeatures, DialogCustomLabels, DialogError, DialogReferrer, DialogState, 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
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, RefreshCookieConfig, ResolveAccountParams, ResolvedAccessKeyOptions, RootCredential, RunLoginParams, RunLoginResult, SendCallsArgs, SendTransactionArgs, Session, SigningContext, SignMessageArgs, SignTypedDataArgs, Store, StoredAccessKey, WagmiConnectCapabilities, WagmiConnectParams, } from "./sdk";
9
- export type { ResolvedFlowSession, ResolveSessionOptions } from "./server";
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";
9
+ export type { ResolvedFlowSession, ResolveSessionOptions, SessionRouteOptions, SessionRouteResponse, SessionRouteSession, } from "./server";
10
10
  export type { CoinAsset, IdentifiedTx, TxApprove, TxConvert, TxSend, TxSwap, } from "./tx";
11
11
  export type { ActionDayContext, ActionEventKind, ActionSessionState, ActiveActionResponse, LevelProgress, PublicProfile, XpGrantResult, XpRecentGrant, XpSummary, } from "./xp";
@@ -1,4 +1,4 @@
1
- import type { FlowCredential, FlowUser, WebAuthnSignature } from "./auth";
1
+ import type { AdditionalSession, FlowCredential, FlowUser, WebAuthnSignature } from "./auth";
2
2
  import type { Address } from "./sdk";
3
3
  export type MethodName = "wallet_connect" | "personal_sign" | "eth_signTypedData" | "eth_sendTransaction" | "wallet_sendCalls" | "wallet_guest" | "wallet_signout";
4
4
  export type ConnectCapabilities = {
@@ -14,6 +14,12 @@ export type ConnectCapabilities = {
14
14
  * cookie session; see the register endpoint.
15
15
  */
16
16
  guestToken?: string;
17
+ /**
18
+ * Extra audiences to mint refresh tokens for in the same login (AUTH-39).
19
+ * The server refuses any entry outside its explicit origin -> audiences
20
+ * allowlist; allowed entries come back as `additionalSessions`.
21
+ */
22
+ additionalAudiences?: readonly string[];
17
23
  };
18
24
  export type ConnectRequest = [{
19
25
  capabilities?: ConnectCapabilities;
@@ -25,6 +31,7 @@ export type ConnectResponse = {
25
31
  user: FlowUser;
26
32
  credential: FlowCredential;
27
33
  webauthn?: WebAuthnSignature;
34
+ additionalSessions?: AdditionalSession[];
28
35
  };
29
36
  export type SignMessageRequest = [message: string, address: string];
30
37
  export type SignMessageResponse = `0x${string}`;
@@ -74,7 +81,9 @@ export type RestoreResponse = {
74
81
  credential: FlowCredential | null;
75
82
  address: Address | null;
76
83
  };
77
- export type GuestRequest = [];
84
+ export type GuestRequest = [] | [{
85
+ additionalAudiences?: readonly string[];
86
+ }];
78
87
  /**
79
88
  * A guest has no passkey and no derivable wallet address yet, so `credential`
80
89
  * and `address` are always null — the shape mirrors RestoreResponse so the SDK
@@ -86,6 +95,7 @@ export type GuestResponse = {
86
95
  user: FlowUser;
87
96
  credential: null;
88
97
  address: null;
98
+ additionalSessions?: AdditionalSession[];
89
99
  };
90
100
  export type SignOutRequest = [];
91
101
  export type SignOutResponse = {
@@ -1,7 +1,7 @@
1
1
  import type { ReactNode } from "react";
2
2
  import type { Chain, Hex, PrepareTransactionRequestParameters, SendTransactionParameters, SignTypedDataParameters, Transport, WalletClient } from "viem";
3
3
  import type { SendCallsParameters } from "viem/actions";
4
- import type { FlowCredential, FlowUser, WebAuthnSignature } from "./auth";
4
+ import type { AdditionalSession, FlowCredential, FlowUser, WebAuthnSignature } from "./auth";
5
5
  import type { ConnectCapabilities, ConnectResponse } from "./protocol";
6
6
  import type { RoomsApi } from "./rooms";
7
7
  export type Address = `0x${string}`;
@@ -72,14 +72,14 @@ export type CreateFlowOptions = {
72
72
  */
73
73
  autoGuest?: boolean;
74
74
  /**
75
- * If true, the session lives in first-party cookies on the app's own
76
- * origin: the JWT is mirrored there from memory and the rotating refresh
77
- * token is stored there alone (not in localStorage), so the app's server
78
- * can resolve and rotate the session per-request (see
79
- * `@flow-industries/id/server`). Off by default — a backend that never
80
- * reads them shouldn't start receiving refresh tokens on every request.
75
+ * Path of the app's first-party session route (see
76
+ * `@flow-industries/id/server`). The refresh token lives in an HttpOnly
77
+ * cookie owned by the app's server; the client resolves, installs, and
78
+ * revokes the session exclusively through this same-origin route. Override
79
+ * only when the app cannot claim the default path — the route registration
80
+ * must use the same value.
81
81
  */
82
- cookies?: boolean;
82
+ sessionPath?: string;
83
83
  /**
84
84
  * Session state resolved server-side (via `resolveSession`) to seed the
85
85
  * store synchronously, so the first client render matches the SSR HTML and
@@ -88,6 +88,22 @@ export type CreateFlowOptions = {
88
88
  * bootstrap.
89
89
  */
90
90
  initialState?: FlowSessionState | null;
91
+ /**
92
+ * Extra audiences every dialog mint (login, guest) requests refresh tokens
93
+ * for, on top of the app's own (AUTH-39). Only audiences the auth server
94
+ * explicitly allowlists for this origin succeed — anything else fails the
95
+ * mint. The minted sessions are delivered once via `onAdditionalSessions`
96
+ * and never stored; the app forwards each to its own origin (e.g. the
97
+ * same-site chat embed's `/flow/session`) to be sealed HttpOnly.
98
+ */
99
+ additionalAudiences?: readonly string[];
100
+ /**
101
+ * One-time delivery of the additional-audience sessions minted at login or
102
+ * guest creation. Fired after the app's own session installs; the receiver
103
+ * must hand each session to its audience's origin (the embed bootstrap) and
104
+ * drop it — nothing here is retained by the SDK.
105
+ */
106
+ onAdditionalSessions?: (sessions: AdditionalSession[]) => void;
91
107
  };
92
108
  export type FlowState = {
93
109
  user: FlowUser | null;
@@ -115,10 +131,6 @@ export type FlowCookieNames = {
115
131
  jwt: string;
116
132
  refresh: string;
117
133
  };
118
- export type RefreshCookieConfig = {
119
- name: string;
120
- secure: boolean;
121
- };
122
134
  export type LoginOptions = {
123
135
  mode?: "iframe" | "popup";
124
136
  signUp?: boolean;
@@ -301,6 +313,7 @@ export type RunLoginResult = {
301
313
  session: Session;
302
314
  refreshToken: string;
303
315
  webauthn: ConnectResponse["webauthn"];
316
+ additionalSessions?: AdditionalSession[];
304
317
  };
305
318
  export type FinalizeAccessKeyParams = {
306
319
  address: Address;
@@ -1,5 +1,5 @@
1
- import type { VerifiedFlowJWT } from "./auth";
2
- import type { FlowSessionState } from "./sdk";
1
+ import type { FlowCredential, FlowUser, VerifiedFlowJWT } from "./auth";
2
+ import type { Address, FlowSessionState } from "./sdk";
3
3
  import type { PublicProfile } from "./xp";
4
4
  export type ResolveSessionOptions = {
5
5
  /** The calling app's origin, e.g. `https://flow.game` — must match the JWT `aud`. */
@@ -26,3 +26,31 @@ export type ResolvedFlowSession = {
26
26
  /** Public profile, only when requested via `profile: true` and resolvable. */
27
27
  profile?: PublicProfile;
28
28
  };
29
+ export type SessionRouteOptions = {
30
+ /**
31
+ * The app's public origin, e.g. `https://flow.game` — the JWT audience and
32
+ * what cookie names derive from. Defaults to the `APP_ORIGIN` env var, then
33
+ * the request's own origin (fine in dev; behind a TLS-terminating proxy set
34
+ * `APP_ORIGIN`).
35
+ */
36
+ audience?: string;
37
+ /** Flow ID origin; defaults per `defaultIdHost` (`FLOW_ID_HOST` on servers). */
38
+ issuerUrl?: string;
39
+ /** Route path for `createSessionHandler`'s own path check. */
40
+ path?: string;
41
+ };
42
+ /**
43
+ * A session as the session route reports it to the browser client.
44
+ * `credential`/`address` are present only when the token was rotated
45
+ * server-side — the rotation response is authoritative for signing state,
46
+ * while the JWT hot path knows neither and the client must keep what it has.
47
+ */
48
+ export type SessionRouteSession = {
49
+ user: FlowUser;
50
+ jwt: string;
51
+ credential?: FlowCredential | null;
52
+ address?: Address | null;
53
+ };
54
+ export type SessionRouteResponse = {
55
+ state: SessionRouteSession | null;
56
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flow-industries/id",
3
- "version": "0.11.0",
3
+ "version": "0.13.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",
@@ -33,6 +33,14 @@
33
33
  "./server": {
34
34
  "types": "./dist/sdk/server.d.ts",
35
35
  "import": "./dist/sdk/server.js"
36
+ },
37
+ "./start": {
38
+ "types": "./dist/sdk/start/index.d.ts",
39
+ "import": "./dist/sdk/start/index.js"
40
+ },
41
+ "./start/server": {
42
+ "types": "./dist/sdk/start/server.d.ts",
43
+ "import": "./dist/sdk/start/server.js"
36
44
  }
37
45
  },
38
46
  "files": [
@@ -66,7 +74,8 @@
66
74
  "viem": ">=2.47.10",
67
75
  "ox": ">=0.14.0",
68
76
  "react": ">=18",
69
- "@wagmi/core": ">=3.0.0"
77
+ "@wagmi/core": ">=3.0.0",
78
+ "@tanstack/react-start": ">=1.168.0"
70
79
  },
71
80
  "peerDependenciesMeta": {
72
81
  "react": {
@@ -74,6 +83,9 @@
74
83
  },
75
84
  "@wagmi/core": {
76
85
  "optional": true
86
+ },
87
+ "@tanstack/react-start": {
88
+ "optional": true
77
89
  }
78
90
  },
79
91
  "devDependencies": {
@@ -83,6 +95,7 @@
83
95
  "@react-email/ui": "^6.6.0",
84
96
  "@tailwindcss/vite": "^4.1.18",
85
97
  "@tanstack/react-router-devtools": "^1.160.0",
98
+ "@tanstack/react-start": "^1.168.28",
86
99
  "@tanstack/router-cli": "^1.167.17",
87
100
  "@tanstack/router-plugin": "^1.160.0",
88
101
  "@types/bun": "latest",
@@ -1 +0,0 @@
1
- export {};