@flow-industries/id 0.23.0 → 0.23.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/sdk/browser-session-route.d.ts +2 -2
- package/dist/sdk/browser-session-route.js +11 -6
- package/dist/sdk/client/create-flow.js +1 -1
- package/dist/sdk/client/dialog-host.d.ts +3 -15
- package/dist/sdk/client/dialog-host.js +6 -20
- package/dist/sdk/client/flow-widget.js +1 -1
- package/dist/sdk/client/methods.d.ts +0 -3
- package/dist/sdk/client/methods.js +0 -3
- package/dist/sdk/client/open-profile.js +2 -20
- package/dist/sdk/client/session.d.ts +1 -1
- package/dist/sdk/client/session.js +1 -1
- package/dist/sdk/issuer-api.d.ts +3 -0
- package/dist/sdk/issuer-api.js +28 -0
- package/dist/sdk/server.js +6 -4
- package/dist/sdk/session-core.d.ts +2 -2
- package/dist/sdk/session-core.js +12 -11
- package/dist/sdk/session-route.js +20 -10
- package/dist/sdk/types/auth.d.ts +2 -0
- package/dist/sdk/types/index.d.ts +2 -2
- package/dist/sdk/types/index.js +1 -1
- package/dist/sdk/types/messenger.d.ts +0 -5
- package/dist/sdk/types/protocol.d.ts +4 -40
- package/dist/sdk/types/protocol.js +0 -12
- package/dist/sdk/types/sdk.d.ts +4 -3
- package/dist/sdk/types/server.d.ts +4 -0
- package/dist/sdk/verify.js +9 -4
- package/package.json +3 -4
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ app path; read the new session after navigation instead of continuing after
|
|
|
53
53
|
|
|
54
54
|
`ProfileButton` and `flow.openAccount()` open `/account` in a normal new tab.
|
|
55
55
|
The source app stays loaded and refreshes its session on refocus. Sign-in and
|
|
56
|
-
own-account management no longer use an embedded dialog. See the
|
|
56
|
+
own-account management no longer use an embedded dialog. The remaining dialog supports signing, public profiles, and widgets. SDK `ConnectResponse` remains a deprecated type alias for source compatibility; sign-in itself returns through the first-party callback. See the
|
|
57
57
|
[integration guide](docs/integration-guide.md) for server wiring and migration.
|
|
58
58
|
|
|
59
59
|
## Public profile overlays
|
|
@@ -143,3 +143,5 @@ The current OpenAPI artifact describes the service report boundary. The remainin
|
|
|
143
143
|
account, room management, study, settings, session and passkey HTTP operations
|
|
144
144
|
still need complete request/response descriptions and generated client coverage
|
|
145
145
|
before the full HTTP contract work is complete.
|
|
146
|
+
|
|
147
|
+
Server deployments may set `FLOW_ID_API_URL` to a trusted internal HTTP(S) origin for API and JWKS requests, or pass `apiUrl` to server session/verification helpers. `issuerUrl` remains the public JWT issuer and browser navigation origin. The transport URL must contain no credentials, path, query, or fragment.
|
|
@@ -4,5 +4,5 @@ export declare function startBrowserSession(request: Request, audience: string,
|
|
|
4
4
|
returnTo?: string;
|
|
5
5
|
signUp?: boolean;
|
|
6
6
|
accessKeyHash?: string;
|
|
7
|
-
}): Promise<Response>;
|
|
8
|
-
export declare function finishBrowserSession(request: Request, audience: string, issuerUrl: string): Promise<Response>;
|
|
7
|
+
}, apiUrl?: string): Promise<Response>;
|
|
8
|
+
export declare function finishBrowserSession(request: Request, audience: string, issuerUrl: string, apiUrl?: string): Promise<Response>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { browserSigningSchema } from "./browser-contract";
|
|
3
3
|
import { clearCookieString, cookieNamesFor, parseCookieHeader, serializeCookie, } from "./cookies";
|
|
4
|
+
import { fetchIssuerApi, resolveIssuerApiUrl } from "./issuer-api";
|
|
4
5
|
import { claimsToUser, sessionCookies } from "./session-core";
|
|
5
6
|
import { verifyFlowJWT } from "./verify";
|
|
6
7
|
const opaquePattern = /^[A-Za-z0-9_-]{43}$/;
|
|
@@ -60,7 +61,7 @@ export function safeReturnTo(value, audience, sessionPath = "/flow/session") {
|
|
|
60
61
|
return null;
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
|
-
export async function startBrowserSession(request, audience, issuerUrl, body) {
|
|
64
|
+
export async function startBrowserSession(request, audience, issuerUrl, body, apiUrl = resolveIssuerApiUrl(issuerUrl)) {
|
|
64
65
|
const cookies = parseCookieHeader(request.headers.get("Cookie"));
|
|
65
66
|
const refresh = cookies[cookieNamesFor(audience).refresh];
|
|
66
67
|
const headers = new Headers({ "Content-Type": "application/json" });
|
|
@@ -74,7 +75,7 @@ export async function startBrowserSession(request, audience, issuerUrl, body) {
|
|
|
74
75
|
if (body.action === "account") {
|
|
75
76
|
if (!refresh)
|
|
76
77
|
return reply({ error: "signed_out" }, 401);
|
|
77
|
-
const response = await
|
|
78
|
+
const response = await fetchIssuerApi(`${apiUrl}/api/auth/browser/account-handoff`, { method: "POST", headers, body: JSON.stringify({ audience }) });
|
|
78
79
|
if (!response.ok)
|
|
79
80
|
return reply({ error: "account_unavailable" }, response.status);
|
|
80
81
|
const { handoff } = z
|
|
@@ -88,7 +89,7 @@ export async function startBrowserSession(request, audience, issuerUrl, body) {
|
|
|
88
89
|
const state = secret();
|
|
89
90
|
const verifier = secret();
|
|
90
91
|
const callback = `${audience}${new URL(request.url).pathname}`;
|
|
91
|
-
const response = await
|
|
92
|
+
const response = await fetchIssuerApi(`${apiUrl}/api/auth/browser/transaction`, {
|
|
92
93
|
method: "POST",
|
|
93
94
|
headers,
|
|
94
95
|
body: JSON.stringify({
|
|
@@ -121,7 +122,7 @@ export async function startBrowserSession(request, audience, issuerUrl, body) {
|
|
|
121
122
|
}),
|
|
122
123
|
]);
|
|
123
124
|
}
|
|
124
|
-
export async function finishBrowserSession(request, audience, issuerUrl) {
|
|
125
|
+
export async function finishBrowserSession(request, audience, issuerUrl, apiUrl = resolveIssuerApiUrl(issuerUrl)) {
|
|
125
126
|
const url = new URL(request.url);
|
|
126
127
|
const state = url.searchParams.get("flow_state") ?? "";
|
|
127
128
|
if (!opaquePattern.test(state))
|
|
@@ -154,7 +155,7 @@ export async function finishBrowserSession(request, audience, issuerUrl) {
|
|
|
154
155
|
if (!opaquePattern.test(code))
|
|
155
156
|
return reply({ error: "invalid_login_code" }, 400);
|
|
156
157
|
try {
|
|
157
|
-
const result = await
|
|
158
|
+
const result = await fetchIssuerApi(`${apiUrl}/api/auth/browser/token`, {
|
|
158
159
|
method: "POST",
|
|
159
160
|
headers: { "Content-Type": "application/json" },
|
|
160
161
|
body: JSON.stringify({
|
|
@@ -170,7 +171,11 @@ export async function finishBrowserSession(request, audience, issuerUrl) {
|
|
|
170
171
|
message: "Return to your app and start sign-in again.",
|
|
171
172
|
}, 400, [clear]);
|
|
172
173
|
const session = exchangeSchema.parse(await result.json());
|
|
173
|
-
const claims = await verifyFlowJWT(session.jwt, {
|
|
174
|
+
const claims = await verifyFlowJWT(session.jwt, {
|
|
175
|
+
audience,
|
|
176
|
+
issuerUrl,
|
|
177
|
+
apiUrl,
|
|
178
|
+
});
|
|
174
179
|
if (!claimsToUser(claims).id)
|
|
175
180
|
throw new Error("Invalid session");
|
|
176
181
|
for (const value of sessionCookies(audience, session.jwt, session.refreshToken))
|
|
@@ -72,7 +72,7 @@ export function createFlow(options = {}) {
|
|
|
72
72
|
if (currentFlow)
|
|
73
73
|
return currentFlow;
|
|
74
74
|
const host = resolveIdHost(options.host);
|
|
75
|
-
const dialogUrl = `${host}/dialog
|
|
75
|
+
const dialogUrl = `${host}/dialog/`;
|
|
76
76
|
const sessionPath = options.sessionPath ?? DEFAULT_SESSION_PATH;
|
|
77
77
|
const rpId = options.rpId ??
|
|
78
78
|
(isLocalHostname(window.location.hostname) ? "localhost" : undefined);
|
|
@@ -1,19 +1,7 @@
|
|
|
1
1
|
import type { CreateDialogHostOptions, DialogHost } from "../types";
|
|
2
2
|
/**
|
|
3
|
-
* Creates
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* The iframe is mounted lazily on first use, kept hidden until `open()` is
|
|
7
|
-
* called, and reused across requests so a single passkey-bound iframe handles
|
|
8
|
-
* login, signing, and silent restore. The iframe is required (not just
|
|
9
|
-
* `fetch`) because it's first-party to id.flow.industries and is the only way
|
|
10
|
-
* to read the cross-origin cookie session in third-party-cookie-blocking
|
|
11
|
-
* browsers (Safari, Firefox).
|
|
12
|
-
*
|
|
13
|
-
* Returned methods:
|
|
14
|
-
* - `request()` — open the dialog UI and dispatch an RPC (interactive flows)
|
|
15
|
-
* - `requestSilent()` — dispatch without showing the dialog (silent restore)
|
|
16
|
-
* - `open()`/`close()` — manual visibility control
|
|
17
|
-
* - `destroy()` — tear down the iframe and abort pending requests
|
|
3
|
+
* Creates the retained signing dialog host. Its iframe mounts lazily and is
|
|
4
|
+
* reused across signing requests. Authentication uses the first-party session
|
|
5
|
+
* handler and top-level authorization page independently.
|
|
18
6
|
*/
|
|
19
7
|
export declare function createDialogHost(options: CreateDialogHostOptions): DialogHost;
|
|
@@ -16,21 +16,9 @@ const VISIBLE_STYLE = {
|
|
|
16
16
|
background: "transparent",
|
|
17
17
|
};
|
|
18
18
|
/**
|
|
19
|
-
* Creates
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* The iframe is mounted lazily on first use, kept hidden until `open()` is
|
|
23
|
-
* called, and reused across requests so a single passkey-bound iframe handles
|
|
24
|
-
* login, signing, and silent restore. The iframe is required (not just
|
|
25
|
-
* `fetch`) because it's first-party to id.flow.industries and is the only way
|
|
26
|
-
* to read the cross-origin cookie session in third-party-cookie-blocking
|
|
27
|
-
* browsers (Safari, Firefox).
|
|
28
|
-
*
|
|
29
|
-
* Returned methods:
|
|
30
|
-
* - `request()` — open the dialog UI and dispatch an RPC (interactive flows)
|
|
31
|
-
* - `requestSilent()` — dispatch without showing the dialog (silent restore)
|
|
32
|
-
* - `open()`/`close()` — manual visibility control
|
|
33
|
-
* - `destroy()` — tear down the iframe and abort pending requests
|
|
19
|
+
* Creates the retained signing dialog host. Its iframe mounts lazily and is
|
|
20
|
+
* reused across signing requests. Authentication uses the first-party session
|
|
21
|
+
* handler and top-level authorization page independently.
|
|
34
22
|
*/
|
|
35
23
|
export function createDialogHost(options) {
|
|
36
24
|
const { host, container = document.body } = options;
|
|
@@ -187,17 +175,15 @@ export function createDialogHost(options) {
|
|
|
187
175
|
});
|
|
188
176
|
});
|
|
189
177
|
}
|
|
190
|
-
/** Opens the dialog UI and dispatches
|
|
178
|
+
/** Opens the dialog UI and dispatches a signing RPC. */
|
|
191
179
|
async function request(method, params) {
|
|
192
180
|
ensureFrame();
|
|
193
181
|
open();
|
|
194
182
|
return dispatchRequest(method, params);
|
|
195
183
|
}
|
|
196
184
|
/**
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
* and resolves immediately. The iframe is still mounted so postMessage
|
|
200
|
-
* works, but its `display` stays `none`.
|
|
185
|
+
* @deprecated Use request() for signing. Retained for callers that manage
|
|
186
|
+
* visibility manually; this does not support background authentication.
|
|
201
187
|
*/
|
|
202
188
|
async function requestSilent(method, params) {
|
|
203
189
|
ensureFrame();
|
|
@@ -47,7 +47,7 @@ export function createFlowWidget(options) {
|
|
|
47
47
|
const hostOrigin = new URL(host).origin;
|
|
48
48
|
let theme = options.theme ?? "light dark";
|
|
49
49
|
const route = WIDGET_ROUTE[options.widget];
|
|
50
|
-
const frame = makeIframe(`${host}${route}
|
|
50
|
+
const frame = makeIframe(`${host}${route}`, WIDGET_TITLE[options.widget]);
|
|
51
51
|
frame.style.background = "transparent";
|
|
52
52
|
const container = options.container ?? document.body;
|
|
53
53
|
// Appended before bridging: `contentWindow` is null until the frame is in the
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
export declare const METHODS: {
|
|
2
|
-
readonly connect: "wallet_connect";
|
|
3
2
|
readonly signMessage: "personal_sign";
|
|
4
3
|
readonly signTypedData: "eth_signTypedData";
|
|
5
4
|
readonly sendTransaction: "eth_sendTransaction";
|
|
6
5
|
readonly sendCalls: "wallet_sendCalls";
|
|
7
|
-
readonly guest: "wallet_guest";
|
|
8
|
-
readonly signOut: "wallet_signout";
|
|
9
6
|
};
|
|
@@ -30,7 +30,7 @@ function createViewer(options) {
|
|
|
30
30
|
setOverlayFocus(frame, ready && visible);
|
|
31
31
|
}
|
|
32
32
|
function mount(username) {
|
|
33
|
-
const el = makeIframe(`${host}/dialog
|
|
33
|
+
const el = makeIframe(`${host}/dialog/`, "Flow ID profile");
|
|
34
34
|
el.dataset.flowProfileView = "";
|
|
35
35
|
el.inert = true;
|
|
36
36
|
Object.assign(el.style, OVERLAY_STYLE, { pointerEvents: "none" });
|
|
@@ -43,10 +43,6 @@ function createViewer(options) {
|
|
|
43
43
|
targetOrigin: hostOrigin,
|
|
44
44
|
});
|
|
45
45
|
bridge = opened;
|
|
46
|
-
// The dialog SPA has no deep-link history fallback, so the frame loads at
|
|
47
|
-
// `/dialog/` and `init` asks it to land on the profile. Sending the route
|
|
48
|
-
// here rather than as a follow-up message keeps the sign-in root from ever
|
|
49
|
-
// mounting, even for the frame it takes to navigate away from it.
|
|
50
46
|
sendBridgeNotification(opened, "__internal", {
|
|
51
47
|
type: "init",
|
|
52
48
|
mode: "iframe",
|
|
@@ -63,21 +59,7 @@ function createViewer(options) {
|
|
|
63
59
|
.catch(() => { });
|
|
64
60
|
opened.on("close", () => close());
|
|
65
61
|
opened.on("__internal", (payload) => {
|
|
66
|
-
|
|
67
|
-
// here, which offers the same sign-in and sign-out the pill's dialog
|
|
68
|
-
// does. Those run on the host — it owns the session — so dropping them
|
|
69
|
-
// would leave the buttons doing nothing at all.
|
|
70
|
-
if (payload.type === "profile-login") {
|
|
71
|
-
close();
|
|
72
|
-
void flow
|
|
73
|
-
.login(payload.mode === "signup" ? { signUp: true } : { signIn: true })
|
|
74
|
-
.catch(() => { });
|
|
75
|
-
}
|
|
76
|
-
else if (payload.type === "profile-logout") {
|
|
77
|
-
close();
|
|
78
|
-
void flow.logout().catch(() => { });
|
|
79
|
-
}
|
|
80
|
-
else if (payload.type === "profile-token-request") {
|
|
62
|
+
if (payload.type === "profile-token-request") {
|
|
81
63
|
void answerTokenRequest(opened, flow, payload.id);
|
|
82
64
|
}
|
|
83
65
|
});
|
|
@@ -13,7 +13,7 @@ export declare function restoreCredential(store: Store<FlowState>): Promise<void
|
|
|
13
13
|
* Clears local-only auth state: active credential in IDB, the access key for
|
|
14
14
|
* the current address (if any), and the in-memory store. Does NOT touch the
|
|
15
15
|
* server cookie session — the caller (createFlow.logout) handles that
|
|
16
|
-
* separately
|
|
16
|
+
* separately through the first-party session handler.
|
|
17
17
|
*/
|
|
18
18
|
export declare function runLogout(store: Store<FlowState>): Promise<void>;
|
|
19
19
|
/**
|
|
@@ -24,7 +24,7 @@ export async function restoreCredential(store) {
|
|
|
24
24
|
* Clears local-only auth state: active credential in IDB, the access key for
|
|
25
25
|
* the current address (if any), and the in-memory store. Does NOT touch the
|
|
26
26
|
* server cookie session — the caller (createFlow.logout) handles that
|
|
27
|
-
* separately
|
|
27
|
+
* separately through the first-party session handler.
|
|
28
28
|
*/
|
|
29
29
|
export async function runLogout(store) {
|
|
30
30
|
const state = store.getSnapshot();
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Resolve a trusted server transport without changing the public JWT issuer. */
|
|
2
|
+
export function resolveIssuerApiUrl(issuerUrl, override) {
|
|
3
|
+
const configured = override ??
|
|
4
|
+
(globalThis.process !== undefined
|
|
5
|
+
? process.env.FLOW_ID_API_URL
|
|
6
|
+
: undefined);
|
|
7
|
+
const url = new URL(configured ?? issuerUrl);
|
|
8
|
+
if (!["http:", "https:"].includes(url.protocol) ||
|
|
9
|
+
url.username ||
|
|
10
|
+
url.password ||
|
|
11
|
+
url.search ||
|
|
12
|
+
url.hash ||
|
|
13
|
+
url.pathname !== "/") {
|
|
14
|
+
throw new Error("Flow ID API URL must be an HTTP(S) origin without credentials, path, query, or fragment");
|
|
15
|
+
}
|
|
16
|
+
return url.origin;
|
|
17
|
+
}
|
|
18
|
+
export async function fetchIssuerApi(url, init) {
|
|
19
|
+
const response = await fetch(url, { ...init, redirect: "error" });
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
console.warn("Flow ID API request failed", {
|
|
22
|
+
path: new URL(url).pathname,
|
|
23
|
+
status: response.status,
|
|
24
|
+
contentType: response.headers.get("content-type")?.split(";")[0] ?? null,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return response;
|
|
28
|
+
}
|
package/dist/sdk/server.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { resolveIdHost } from "./id-host";
|
|
2
|
+
import { fetchIssuerApi, resolveIssuerApiUrl } from "./issuer-api";
|
|
2
3
|
import { resolveSessionCore } from "./session-core";
|
|
3
4
|
export { createSessionHandler, handleSessionRequest } from "./session-route";
|
|
4
5
|
export { verifyFlowJWT } from "./verify";
|
|
5
|
-
async function fetchProfile(
|
|
6
|
+
async function fetchProfile(apiUrl, jwt, audience) {
|
|
6
7
|
try {
|
|
7
|
-
const res = await
|
|
8
|
+
const res = await fetchIssuerApi(`${apiUrl}/api/session/verify`, {
|
|
8
9
|
method: "POST",
|
|
9
10
|
headers: { "Content-Type": "application/json" },
|
|
10
11
|
body: JSON.stringify({ token: jwt, audience }),
|
|
@@ -29,12 +30,13 @@ async function fetchProfile(issuerUrl, jwt, audience) {
|
|
|
29
30
|
*/
|
|
30
31
|
export async function resolveSession(cookieHeader, opts) {
|
|
31
32
|
const issuerUrl = resolveIdHost(opts.issuerUrl, opts.audience);
|
|
32
|
-
const
|
|
33
|
+
const apiUrl = resolveIssuerApiUrl(issuerUrl, opts.apiUrl);
|
|
34
|
+
const { session, claims, setCookies } = await resolveSessionCore(cookieHeader, opts.audience, issuerUrl, apiUrl);
|
|
33
35
|
const state = session
|
|
34
36
|
? { user: session.user, jwt: session.jwt, address: session.address ?? null }
|
|
35
37
|
: null;
|
|
36
38
|
const profile = state && opts.profile
|
|
37
|
-
? await fetchProfile(
|
|
39
|
+
? await fetchProfile(apiUrl, state.jwt, opts.audience)
|
|
38
40
|
: undefined;
|
|
39
41
|
return {
|
|
40
42
|
state,
|
|
@@ -27,7 +27,7 @@ export type ResolvedSessionCore = {
|
|
|
27
27
|
* happened) but resolves signed-out rather than trusting it. Returns "dead"
|
|
28
28
|
* for a definitive rejection and null for a transient network failure.
|
|
29
29
|
*/
|
|
30
|
-
export declare function refreshSessionCore(issuerUrl: string, audience: string, refreshToken: string): Promise<ResolvedSessionCore | "dead" | null>;
|
|
30
|
+
export declare function refreshSessionCore(issuerUrl: string, audience: string, refreshToken: string, apiUrl?: string): Promise<ResolvedSessionCore | "dead" | null>;
|
|
31
31
|
/**
|
|
32
32
|
* Resolves a session from a request's Cookie header.
|
|
33
33
|
*
|
|
@@ -41,4 +41,4 @@ export declare function refreshSessionCore(issuerUrl: string, audience: string,
|
|
|
41
41
|
* network failure leaves the cookies alone so a later request can retry.
|
|
42
42
|
* 3. Neither cookie → signed-out `session: null`.
|
|
43
43
|
*/
|
|
44
|
-
export declare function resolveSessionCore(cookieHeader: string | null | undefined, audience: string, issuerUrl: string): Promise<ResolvedSessionCore>;
|
|
44
|
+
export declare function resolveSessionCore(cookieHeader: string | null | undefined, audience: string, issuerUrl: string, apiUrl?: string): Promise<ResolvedSessionCore>;
|
package/dist/sdk/session-core.js
CHANGED
|
@@ -5,6 +5,7 @@ import { errField } from "./driver-error";
|
|
|
5
5
|
* `session-route.ts` compose it into the public entry points.
|
|
6
6
|
*/
|
|
7
7
|
import { clearCookieString, cookieNamesFor, JWT_COOKIE_MAX_AGE_S, parseCookieHeader, REFRESH_COOKIE_MAX_AGE_S, serializeCookie, } from "./cookies";
|
|
8
|
+
import { fetchIssuerApi, resolveIssuerApiUrl } from "./issuer-api";
|
|
8
9
|
import { isExpiring } from "./token-expiry";
|
|
9
10
|
import { verifyFlowJWT } from "./verify";
|
|
10
11
|
export function claimsToUser(claims) {
|
|
@@ -16,9 +17,9 @@ export function claimsToUser(claims) {
|
|
|
16
17
|
isGuest: claims.guest !== false,
|
|
17
18
|
};
|
|
18
19
|
}
|
|
19
|
-
async function refreshSession(
|
|
20
|
+
async function refreshSession(token, apiUrl) {
|
|
20
21
|
try {
|
|
21
|
-
const res = await
|
|
22
|
+
const res = await fetchIssuerApi(`${apiUrl}/api/session/refresh`, {
|
|
22
23
|
method: "POST",
|
|
23
24
|
headers: { Authorization: `Bearer ${token}` },
|
|
24
25
|
});
|
|
@@ -42,11 +43,11 @@ async function refreshSession(issuerUrl, token) {
|
|
|
42
43
|
// other machines).
|
|
43
44
|
const REFRESH_RESULT_TTL_MS = 30_000;
|
|
44
45
|
const refreshInFlight = new Map();
|
|
45
|
-
function refreshOnce(issuerUrl, token) {
|
|
46
|
-
const key = `${issuerUrl}\n${token}`;
|
|
46
|
+
function refreshOnce(issuerUrl, token, apiUrl) {
|
|
47
|
+
const key = `${issuerUrl}\n${apiUrl}\n${token}`;
|
|
47
48
|
let flight = refreshInFlight.get(key);
|
|
48
49
|
if (!flight) {
|
|
49
|
-
flight = refreshSession(
|
|
50
|
+
flight = refreshSession(token, apiUrl);
|
|
50
51
|
refreshInFlight.set(key, flight);
|
|
51
52
|
void flight.finally(() => {
|
|
52
53
|
const timer = setTimeout(() => refreshInFlight.delete(key), REFRESH_RESULT_TTL_MS);
|
|
@@ -100,14 +101,14 @@ export function clearingCookies(audience) {
|
|
|
100
101
|
* happened) but resolves signed-out rather than trusting it. Returns "dead"
|
|
101
102
|
* for a definitive rejection and null for a transient network failure.
|
|
102
103
|
*/
|
|
103
|
-
export async function refreshSessionCore(issuerUrl, audience, refreshToken) {
|
|
104
|
-
const result = await refreshOnce(issuerUrl, refreshToken);
|
|
104
|
+
export async function refreshSessionCore(issuerUrl, audience, refreshToken, apiUrl = resolveIssuerApiUrl(issuerUrl)) {
|
|
105
|
+
const result = await refreshOnce(issuerUrl, refreshToken, apiUrl);
|
|
105
106
|
if (result === "dead" || !result)
|
|
106
107
|
return result;
|
|
107
108
|
const successorCookies = sessionCookies(audience, result.jwt, result.refreshToken);
|
|
108
109
|
let claims;
|
|
109
110
|
try {
|
|
110
|
-
claims = await verifyFlowJWT(result.jwt, { audience, issuerUrl });
|
|
111
|
+
claims = await verifyFlowJWT(result.jwt, { audience, issuerUrl, apiUrl });
|
|
111
112
|
}
|
|
112
113
|
catch (err) {
|
|
113
114
|
if (isJwtVerificationFailure(err)) {
|
|
@@ -143,13 +144,13 @@ export async function refreshSessionCore(issuerUrl, audience, refreshToken) {
|
|
|
143
144
|
* network failure leaves the cookies alone so a later request can retry.
|
|
144
145
|
* 3. Neither cookie → signed-out `session: null`.
|
|
145
146
|
*/
|
|
146
|
-
export async function resolveSessionCore(cookieHeader, audience, issuerUrl) {
|
|
147
|
+
export async function resolveSessionCore(cookieHeader, audience, issuerUrl, apiUrl = resolveIssuerApiUrl(issuerUrl)) {
|
|
147
148
|
const names = cookieNamesFor(audience);
|
|
148
149
|
const cookies = parseCookieHeader(cookieHeader);
|
|
149
150
|
const jwt = cookies[names.jwt];
|
|
150
151
|
if (jwt && !isExpiring(jwt)) {
|
|
151
152
|
try {
|
|
152
|
-
const claims = await verifyFlowJWT(jwt, { audience, issuerUrl });
|
|
153
|
+
const claims = await verifyFlowJWT(jwt, { audience, issuerUrl, apiUrl });
|
|
153
154
|
return {
|
|
154
155
|
session: { user: claimsToUser(claims), jwt },
|
|
155
156
|
claims,
|
|
@@ -165,7 +166,7 @@ export async function resolveSessionCore(cookieHeader, audience, issuerUrl) {
|
|
|
165
166
|
const refreshToken = cookies[names.refresh];
|
|
166
167
|
if (!refreshToken)
|
|
167
168
|
return { session: null, claims: null, setCookies: [] };
|
|
168
|
-
const refreshed = await refreshSessionCore(issuerUrl, audience, refreshToken);
|
|
169
|
+
const refreshed = await refreshSessionCore(issuerUrl, audience, refreshToken, apiUrl);
|
|
169
170
|
if (refreshed === "dead") {
|
|
170
171
|
return {
|
|
171
172
|
session: null,
|
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { browserCompletionSchema } from "./browser-contract";
|
|
3
3
|
import { finishBrowserSession, startBrowserSession, } from "./browser-session-route";
|
|
4
4
|
import { clearCookieString } from "./cookies";
|
|
5
|
+
import { fetchIssuerApi, resolveIssuerApiUrl } from "./issuer-api";
|
|
5
6
|
/**
|
|
6
7
|
* The first-party session route every consumer app serves from its own
|
|
7
8
|
* origin — the only path between a browser and the refresh token. The
|
|
@@ -82,9 +83,10 @@ function isCrossSite(request, audience) {
|
|
|
82
83
|
export async function handleSessionRequest(request, opts = {}) {
|
|
83
84
|
const audience = opts.audience ?? defaultAudience(new URL(request.url).origin);
|
|
84
85
|
const issuerUrl = resolveIdHost(opts.issuerUrl, audience);
|
|
86
|
+
const apiUrl = resolveIssuerApiUrl(issuerUrl, opts.apiUrl);
|
|
85
87
|
const callback = new URL(request.url);
|
|
86
88
|
if (request.method === "GET" && callback.searchParams.has("flow_state")) {
|
|
87
|
-
return finishBrowserSession(request, audience, issuerUrl);
|
|
89
|
+
return finishBrowserSession(request, audience, issuerUrl, apiUrl);
|
|
88
90
|
}
|
|
89
91
|
if (isCrossSite(request, audience)) {
|
|
90
92
|
return json({ state: null }, 403);
|
|
@@ -95,14 +97,14 @@ export async function handleSessionRequest(request, opts = {}) {
|
|
|
95
97
|
const token = parseCookieHeader(request.headers.get("Cookie"))[cookieNamesFor(audience).refresh];
|
|
96
98
|
if (!token)
|
|
97
99
|
return json({ state: null }, 200);
|
|
98
|
-
const result = await refreshSessionCore(issuerUrl, audience, token);
|
|
100
|
+
const result = await refreshSessionCore(issuerUrl, audience, token, apiUrl);
|
|
99
101
|
if (result === "dead")
|
|
100
102
|
return json({ state: null }, 200, clearingCookies(audience));
|
|
101
103
|
if (!result)
|
|
102
104
|
return json({ state: null }, 502);
|
|
103
105
|
return json({ state: result.session }, 200, result.setCookies);
|
|
104
106
|
}
|
|
105
|
-
const { session, setCookies } = await resolveSessionCore(request.headers.get("Cookie"), audience, issuerUrl);
|
|
107
|
+
const { session, setCookies } = await resolveSessionCore(request.headers.get("Cookie"), audience, issuerUrl, apiUrl);
|
|
106
108
|
const completionName = `${cookieNamesFor(audience).refresh}.completion`;
|
|
107
109
|
const raw = parseCookieHeader(request.headers.get("Cookie"))[completionName];
|
|
108
110
|
let loginCompletion;
|
|
@@ -129,14 +131,14 @@ export async function handleSessionRequest(request, opts = {}) {
|
|
|
129
131
|
return await startBrowserSession(request, audience, issuerUrl, {
|
|
130
132
|
...body,
|
|
131
133
|
action: body.action,
|
|
132
|
-
});
|
|
134
|
+
}, apiUrl);
|
|
133
135
|
}
|
|
134
136
|
catch {
|
|
135
137
|
return json({ state: null }, 502);
|
|
136
138
|
}
|
|
137
139
|
}
|
|
138
140
|
if (body?.action === "guest") {
|
|
139
|
-
const existing = await resolveSessionCore(request.headers.get("Cookie"), audience, issuerUrl);
|
|
141
|
+
const existing = await resolveSessionCore(request.headers.get("Cookie"), audience, issuerUrl, apiUrl);
|
|
140
142
|
if (existing.session)
|
|
141
143
|
return json({ state: existing.session }, 200, existing.setCookies);
|
|
142
144
|
const previousRefresh = parseCookieHeader(request.headers.get("Cookie"))[cookieNamesFor(audience).refresh];
|
|
@@ -153,7 +155,7 @@ export async function handleSessionRequest(request, opts = {}) {
|
|
|
153
155
|
if (value)
|
|
154
156
|
headers.set(name, value);
|
|
155
157
|
}
|
|
156
|
-
const result = await
|
|
158
|
+
const result = await fetchIssuerApi(`${apiUrl}/api/auth/passkey/guest`, {
|
|
157
159
|
method: "POST",
|
|
158
160
|
headers,
|
|
159
161
|
body: JSON.stringify({ audience }),
|
|
@@ -162,7 +164,11 @@ export async function handleSessionRequest(request, opts = {}) {
|
|
|
162
164
|
return json({ state: null }, result.status);
|
|
163
165
|
// SAFETY: the configured issuer returns this session token pair, verified below.
|
|
164
166
|
const data = (await result.json());
|
|
165
|
-
const claims = await verifyFlowJWT(data.jwt, {
|
|
167
|
+
const claims = await verifyFlowJWT(data.jwt, {
|
|
168
|
+
audience,
|
|
169
|
+
issuerUrl,
|
|
170
|
+
apiUrl,
|
|
171
|
+
});
|
|
166
172
|
return json({
|
|
167
173
|
state: {
|
|
168
174
|
user: claimsToUser(claims),
|
|
@@ -188,7 +194,7 @@ export async function handleSessionRequest(request, opts = {}) {
|
|
|
188
194
|
let additionalSessions = [];
|
|
189
195
|
if (token && audiences.length > 0) {
|
|
190
196
|
try {
|
|
191
|
-
const res = await
|
|
197
|
+
const res = await fetchIssuerApi(`${apiUrl}/api/session/additional`, {
|
|
192
198
|
method: "POST",
|
|
193
199
|
headers: {
|
|
194
200
|
"Content-Type": "application/json",
|
|
@@ -216,7 +222,11 @@ export async function handleSessionRequest(request, opts = {}) {
|
|
|
216
222
|
// for another app from being installed here; a failed install must not
|
|
217
223
|
// clear cookies that may hold a live session.
|
|
218
224
|
try {
|
|
219
|
-
const claims = await verifyFlowJWT(jwt, {
|
|
225
|
+
const claims = await verifyFlowJWT(jwt, {
|
|
226
|
+
audience,
|
|
227
|
+
issuerUrl,
|
|
228
|
+
apiUrl,
|
|
229
|
+
});
|
|
220
230
|
return json({ state: { user: claimsToUser(claims), jwt } }, 200, sessionCookies(audience, jwt, refreshToken));
|
|
221
231
|
}
|
|
222
232
|
catch (err) {
|
|
@@ -230,7 +240,7 @@ export async function handleSessionRequest(request, opts = {}) {
|
|
|
230
240
|
// an unreachable issuer just leaves the lineage to expire on its own.
|
|
231
241
|
if (token) {
|
|
232
242
|
try {
|
|
233
|
-
const response = await
|
|
243
|
+
const response = await fetchIssuerApi(`${apiUrl}/api/session/revoke`, {
|
|
234
244
|
method: "POST",
|
|
235
245
|
headers: { Authorization: `Bearer ${token}` },
|
|
236
246
|
});
|
package/dist/sdk/types/auth.d.ts
CHANGED
|
@@ -99,6 +99,8 @@ export type PasskeyPluginOptions = {
|
|
|
99
99
|
export type VerifyOptions = {
|
|
100
100
|
audience: string;
|
|
101
101
|
issuerUrl?: string;
|
|
102
|
+
/** Trusted server API origin; defaults to FLOW_ID_API_URL, then the public issuer. */
|
|
103
|
+
apiUrl?: string;
|
|
102
104
|
};
|
|
103
105
|
/**
|
|
104
106
|
* One entry in the signed-in user's session list (`GET /api/account/sessions`).
|
|
@@ -11,8 +11,8 @@ export { GAME_LIFECYCLE_EVENT_NAMES } from "./game";
|
|
|
11
11
|
export type { AccountProfileData } from "./landing";
|
|
12
12
|
export type { Bridge, BridgeParameters, FlowAccount, FlowRemote, FlowRemoteConfig, FromWindowOptions, MessageResponse, Messenger, OneOf, Payload, QueuedRequest, ReadyOptions, RemoteFlowState, RemoteState, Schema, Storage, Topic, WithReady, } from "./messenger";
|
|
13
13
|
export type { OtpVerificationFailure, OtpVerificationResult, VerificationAdapter, } from "./otp";
|
|
14
|
-
export type { Call, ConnectCapabilities,
|
|
15
|
-
export {
|
|
14
|
+
export type { Call, ConnectCapabilities, ConnectResponse, MethodName, MethodParams, MethodResult, RestoreResponse, RpcRequest, SendCallsParams, SendCallsRequest, SendCallsResponse, SendTransactionParams, SendTransactionRequest, SendTransactionResponse, SignMessageRequest, SignMessageResponse, SignTypedDataRequest, SignTypedDataResponse, TransactionArgs, TypedData, TypedDataDomain, TypedDataField, } from "./protocol";
|
|
15
|
+
export { isPersonalSignParams, isSendCallsParams, isSendTransactionParams, } from "./protocol";
|
|
16
16
|
export type { ReportOperation, ReportRequest, ReportResponse, ReportsApi, ReportTransport, } from "./reports";
|
|
17
17
|
export type { CreateRoomEventInput, RoomEventArchive, RoomEventAttendance, RoomEventFrequency, RoomEventInstance, RoomEventInterest, RoomEventInterestState, RoomEventList, RoomEventOccurrence, RoomEventRecurrence, RoomEventRoomRef, RoomEventRun, RoomEventSeries, RoomEventStatus, SetInterestInput, UpdateRoomEventInput, } from "./room-events";
|
|
18
18
|
export type { CreateRoomInput, GlobalRole, ModerationSubject, MyRooms, PlayerPositionReport, RoleChangeVerdict, RoomCapabilities, RoomCapability, RoomChannel, RoomDetail, RoomList, RoomMemberEntry, RoomMembers, RoomOccupancy, RoomOwner, RoomPlayerPosition, RoomPresenceEntry, RoomPresenceSnapshot, RoomRestrictionKind, RoomRole, RoomSummary, RoomSurfaceSettings, RoomsApi, RoomVisibility, StaffEntry, UpdateRoomInput, VerifyRoomContext, Viewer, VoiceDecisionReason, VoiceDecisionSubject, VoiceRoomDecision, } from "./rooms";
|
package/dist/sdk/types/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { BODY_REGIONS, COSMETIC_MATERIALS } from "./cosmetics";
|
|
2
2
|
export { GAME_LIFECYCLE_EVENT_NAMES } from "./game";
|
|
3
|
-
export {
|
|
3
|
+
export { isPersonalSignParams, isSendCallsParams, isSendTransactionParams, } from "./protocol";
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import type { RawRecord, RawValue } from "../json";
|
|
2
2
|
import type { AdditionalSession, FlowCredential, FlowUser, WebAuthnSignature } from "./auth";
|
|
3
3
|
import type { Address } from "./sdk";
|
|
4
|
-
export type MethodName = "
|
|
4
|
+
export type MethodName = "personal_sign" | "eth_signTypedData" | "eth_sendTransaction" | "wallet_sendCalls";
|
|
5
5
|
export type ConnectCapabilities = {
|
|
6
6
|
createAccount?: boolean;
|
|
7
7
|
signIn?: boolean;
|
|
8
8
|
accessKeyHash?: string;
|
|
9
|
+
/** @deprecated Sign-in always uses the dedicated top-level page. */
|
|
9
10
|
signInHeadless?: boolean;
|
|
10
11
|
credentialId?: string;
|
|
11
12
|
/**
|
|
12
|
-
*
|
|
13
|
-
* in place even where the guest cookie is unreadable (the iOS top-level
|
|
14
|
-
* sign-up popup). The server verifies it before letting it stand in for the
|
|
15
|
-
* cookie session; see the register endpoint.
|
|
13
|
+
* @deprecated Guest proof is managed by the first-party session handler.
|
|
16
14
|
*/
|
|
17
15
|
guestToken?: string;
|
|
18
16
|
/**
|
|
@@ -22,9 +20,7 @@ export type ConnectCapabilities = {
|
|
|
22
20
|
*/
|
|
23
21
|
additionalAudiences?: readonly string[];
|
|
24
22
|
};
|
|
25
|
-
|
|
26
|
-
capabilities?: ConnectCapabilities;
|
|
27
|
-
}];
|
|
23
|
+
/** @deprecated Login navigates; use SessionRouteResponse for completed sessions. */
|
|
28
24
|
export type ConnectResponse = {
|
|
29
25
|
jwt: string;
|
|
30
26
|
/** First-party rotating refresh token; the SDK persists it in the app origin. */
|
|
@@ -83,43 +79,17 @@ export type RestoreResponse = {
|
|
|
83
79
|
credential: FlowCredential | null;
|
|
84
80
|
address: Address | null;
|
|
85
81
|
};
|
|
86
|
-
export type GuestRequest = [] | [{
|
|
87
|
-
additionalAudiences?: readonly string[];
|
|
88
|
-
}];
|
|
89
|
-
/**
|
|
90
|
-
* A guest has no passkey and no derivable wallet address yet, so `credential`
|
|
91
|
-
* and `address` are always null — the shape mirrors RestoreResponse so the SDK
|
|
92
|
-
* can commit either to the same store fields.
|
|
93
|
-
*/
|
|
94
|
-
export type GuestResponse = {
|
|
95
|
-
jwt: string;
|
|
96
|
-
refreshToken: string;
|
|
97
|
-
user: FlowUser;
|
|
98
|
-
credential: null;
|
|
99
|
-
address: null;
|
|
100
|
-
additionalSessions?: AdditionalSession[];
|
|
101
|
-
};
|
|
102
|
-
export type SignOutRequest = [];
|
|
103
|
-
export type SignOutResponse = {
|
|
104
|
-
ok: true;
|
|
105
|
-
};
|
|
106
82
|
export type MethodParams = {
|
|
107
|
-
wallet_connect: ConnectRequest;
|
|
108
83
|
personal_sign: SignMessageRequest;
|
|
109
84
|
eth_signTypedData: SignTypedDataRequest;
|
|
110
85
|
eth_sendTransaction: SendTransactionRequest;
|
|
111
86
|
wallet_sendCalls: SendCallsRequest;
|
|
112
|
-
wallet_guest: GuestRequest;
|
|
113
|
-
wallet_signout: SignOutRequest;
|
|
114
87
|
};
|
|
115
88
|
export type MethodResult = {
|
|
116
|
-
wallet_connect: ConnectResponse;
|
|
117
89
|
personal_sign: SignMessageResponse;
|
|
118
90
|
eth_signTypedData: SignTypedDataResponse;
|
|
119
91
|
eth_sendTransaction: SendTransactionResponse;
|
|
120
92
|
wallet_sendCalls: SendCallsResponse;
|
|
121
|
-
wallet_guest: GuestResponse;
|
|
122
|
-
wallet_signout: SignOutResponse;
|
|
123
93
|
};
|
|
124
94
|
export type RpcRequest = {
|
|
125
95
|
id: string | number;
|
|
@@ -143,12 +113,6 @@ export type SendCallsParams = {
|
|
|
143
113
|
chainId?: string;
|
|
144
114
|
capabilities?: RawRecord;
|
|
145
115
|
};
|
|
146
|
-
/**
|
|
147
|
-
* Unwraps the connect capabilities from a wallet_connect RPC request (they ride
|
|
148
|
-
* as the first param), or undefined. Several dialog surfaces read them (mode,
|
|
149
|
-
* accessKeyHash, guestToken), so the cast lives here rather than being repeated.
|
|
150
|
-
*/
|
|
151
|
-
export declare function getConnectCapabilities(request: RpcRequest | undefined): ConnectCapabilities | undefined;
|
|
152
116
|
export declare function isPersonalSignParams(params: readonly unknown[] | undefined): params is readonly [string, string];
|
|
153
117
|
export declare function isSendTransactionParams(params: readonly unknown[] | undefined): params is readonly [SendTransactionParams];
|
|
154
118
|
export declare function isSendCallsParams(params: readonly unknown[] | undefined): params is readonly [SendCallsParams];
|
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unwraps the connect capabilities from a wallet_connect RPC request (they ride
|
|
3
|
-
* as the first param), or undefined. Several dialog surfaces read them (mode,
|
|
4
|
-
* accessKeyHash, guestToken), so the cast lives here rather than being repeated.
|
|
5
|
-
*/
|
|
6
|
-
export function getConnectCapabilities(request) {
|
|
7
|
-
const first = request?.params?.[0];
|
|
8
|
-
const found = Object.entries(Object(first)).find(([k]) => k === "capabilities")?.[1];
|
|
9
|
-
/* SAFETY: the field is read off this app's own wallet_connect params, whose capabilities
|
|
10
|
-
block is defined by ConnectCapabilities; a missing one answers undefined. */
|
|
11
|
-
return found;
|
|
12
|
-
}
|
|
13
1
|
export function isPersonalSignParams(params) {
|
|
14
2
|
if (!params || params.length < 2)
|
|
15
3
|
return false;
|
package/dist/sdk/types/sdk.d.ts
CHANGED
|
@@ -90,8 +90,8 @@ export type CreateFlowOptions = {
|
|
|
90
90
|
*/
|
|
91
91
|
initialState?: FlowSessionState | null;
|
|
92
92
|
/**
|
|
93
|
-
* Extra audiences
|
|
94
|
-
*
|
|
93
|
+
* Extra audiences to receive sessions after the app session is installed.
|
|
94
|
+
* Only audiences the auth server
|
|
95
95
|
* explicitly allowlists for this origin succeed — anything else fails the
|
|
96
96
|
* mint. The minted sessions are delivered once via `onAdditionalSessions`
|
|
97
97
|
* and never stored; the app forwards each to its own origin (e.g. the
|
|
@@ -229,7 +229,7 @@ export type ProfileButtonHandle = {
|
|
|
229
229
|
export type OpenProfileOptions = {
|
|
230
230
|
/**
|
|
231
231
|
* Called once when the overlay closes, including its close button, backdrop,
|
|
232
|
-
* Escape, closeProfile(), or
|
|
232
|
+
* Escape, closeProfile(), or an own-account tab handoff. Runs after the
|
|
233
233
|
* overlay stops accepting input, without waiting for the exit animation.
|
|
234
234
|
* The latest successful open replaces this callback; omitting it clears the
|
|
235
235
|
* previous callback. Rejected opens and repeated closes do not notify.
|
|
@@ -314,6 +314,7 @@ export type DialogHost = {
|
|
|
314
314
|
close: () => void;
|
|
315
315
|
destroy: () => void;
|
|
316
316
|
request: <T = unknown>(method: string, params?: readonly unknown[]) => Promise<T>;
|
|
317
|
+
/** @deprecated Manual signing-dialog visibility only; authentication redirects. */
|
|
317
318
|
requestSilent: <T = unknown>(method: string, params?: readonly unknown[]) => Promise<T>;
|
|
318
319
|
messenger: import("./messenger").Messenger;
|
|
319
320
|
};
|
|
@@ -6,6 +6,8 @@ export type ResolveSessionOptions = {
|
|
|
6
6
|
audience: string;
|
|
7
7
|
/** Flow ID origin; defaults to `https://id.flow.industries`. */
|
|
8
8
|
issuerUrl?: string;
|
|
9
|
+
/** Trusted server API origin; defaults to FLOW_ID_API_URL, then the public issuer. */
|
|
10
|
+
apiUrl?: string;
|
|
9
11
|
/**
|
|
10
12
|
* Also fetch the public profile (avatar, tier, level) for a resolved
|
|
11
13
|
* session via `/api/session/verify` — one extra round-trip; off by default.
|
|
@@ -36,6 +38,8 @@ export type SessionRouteOptions = {
|
|
|
36
38
|
audience?: string;
|
|
37
39
|
/** Flow ID origin; defaults per `defaultIdHost` (`FLOW_ID_HOST` on servers). */
|
|
38
40
|
issuerUrl?: string;
|
|
41
|
+
/** Trusted server API origin; defaults to FLOW_ID_API_URL, then the public issuer. */
|
|
42
|
+
apiUrl?: string;
|
|
39
43
|
/** Route path for `createSessionHandler`'s own path check. */
|
|
40
44
|
path?: string;
|
|
41
45
|
};
|
package/dist/sdk/verify.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
1
|
+
import { createRemoteJWKSet, customFetch, jwtVerify } from "jose";
|
|
2
|
+
import { fetchIssuerApi, resolveIssuerApiUrl } from "./issuer-api";
|
|
2
3
|
const jwksCache = new Map();
|
|
3
4
|
const DEFAULT_ISSUER_URL = "https://id.flow.industries";
|
|
4
5
|
/**
|
|
@@ -22,10 +23,14 @@ const DEFAULT_ISSUER_URL = "https://id.flow.industries";
|
|
|
22
23
|
*/
|
|
23
24
|
export async function verifyFlowJWT(token, opts) {
|
|
24
25
|
const issuerUrl = opts.issuerUrl ?? DEFAULT_ISSUER_URL;
|
|
25
|
-
|
|
26
|
+
const apiUrl = resolveIssuerApiUrl(issuerUrl, opts.apiUrl);
|
|
27
|
+
const cacheKey = `${issuerUrl}\n${apiUrl}`;
|
|
28
|
+
let jwks = jwksCache.get(cacheKey);
|
|
26
29
|
if (!jwks) {
|
|
27
|
-
jwks = createRemoteJWKSet(new URL(`${
|
|
28
|
-
|
|
30
|
+
jwks = createRemoteJWKSet(new URL(`${apiUrl}/api/auth/jwks`), {
|
|
31
|
+
[customFetch]: fetchIssuerApi,
|
|
32
|
+
});
|
|
33
|
+
jwksCache.set(cacheKey, jwks);
|
|
29
34
|
}
|
|
30
35
|
const { payload } = await jwtVerify(token, jwks, {
|
|
31
36
|
issuer: issuerUrl,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flow-industries/id",
|
|
3
|
-
"version": "0.23.
|
|
3
|
+
"version": "0.23.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -66,10 +66,9 @@
|
|
|
66
66
|
"scripts": {
|
|
67
67
|
"dev": "./dev.sh",
|
|
68
68
|
"build:dialog": "bunx vite build",
|
|
69
|
-
"build:legacy-dialog": "bunx vite build --config vite.legacy.config.ts",
|
|
70
69
|
"build:playground": "bunx vite build --config vite.playground.config.ts",
|
|
71
70
|
"build:landing": "bunx vite build --config vite.landing.config.ts",
|
|
72
|
-
"build": "
|
|
71
|
+
"build": "bunx vite build && bunx vite build --config vite.playground.config.ts && bunx vite build --config vite.landing.config.ts",
|
|
73
72
|
"build:sdk": "tsc --project tsconfig.sdk.json",
|
|
74
73
|
"prepublishOnly": "bun run build:sdk",
|
|
75
74
|
"start": "bun run src/index.ts",
|
|
@@ -85,7 +84,7 @@
|
|
|
85
84
|
"test:sdk-package": "bash scripts/test-sdk-package.sh",
|
|
86
85
|
"format": "biome format --write",
|
|
87
86
|
"check": "biome check --write",
|
|
88
|
-
"typecheck": "tsr generate &&
|
|
87
|
+
"typecheck": "tsr generate && tsc --noEmit",
|
|
89
88
|
"contracts:generate": "bun scripts/generate-contracts.ts",
|
|
90
89
|
"contracts:check": "bun scripts/generate-contracts.ts --check && bun scripts/check-contract-compatibility.ts",
|
|
91
90
|
"db:check": "bun scripts/db-check.ts",
|