@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.
- package/dist/sdk/client/create-flow.d.ts +3 -3
- package/dist/sdk/client/create-flow.js +131 -114
- package/dist/sdk/client/index.d.ts +2 -1
- package/dist/sdk/client/index.js +1 -0
- package/dist/sdk/client/profile-button.js +2 -2
- package/dist/sdk/client/refresh-store.d.ts +13 -14
- package/dist/sdk/client/refresh-store.js +48 -58
- package/dist/sdk/client/session.js +2 -1
- package/dist/sdk/client/static-flow.js +2 -2
- package/dist/sdk/cookies.d.ts +8 -19
- package/dist/sdk/cookies.js +9 -25
- package/dist/sdk/id-host.d.ts +36 -0
- package/dist/sdk/id-host.js +57 -0
- package/dist/sdk/server.d.ts +8 -18
- package/dist/sdk/server.js +17 -173
- package/dist/sdk/session-core.d.ts +49 -0
- package/dist/sdk/session-core.js +176 -0
- package/dist/sdk/session-route.d.ts +33 -0
- package/dist/sdk/session-route.js +127 -0
- package/dist/sdk/start/index.d.ts +52 -0
- package/dist/sdk/start/index.js +58 -0
- package/dist/sdk/start/server.d.ts +33 -0
- package/dist/sdk/start/server.js +35 -0
- package/dist/sdk/types/auth.d.ts +13 -0
- package/dist/sdk/types/events.d.ts +1 -1
- package/dist/sdk/types/index.d.ts +3 -3
- package/dist/sdk/types/protocol.d.ts +12 -2
- package/dist/sdk/types/sdk.d.ts +25 -12
- package/dist/sdk/types/server.d.ts +30 -2
- package/package.json +15 -2
- package/dist/sdk/client/refresh-store.test.d.ts +0 -1
- package/dist/sdk/client/refresh-store.test.js +0 -90
|
@@ -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
|
+
}
|
package/dist/sdk/types/auth.d.ts
CHANGED
|
@@ -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,
|
|
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 = {
|
package/dist/sdk/types/sdk.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
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
|
-
|
|
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.
|
|
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 {};
|