@flow-industries/id 0.17.0 → 0.19.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/README.md +19 -0
- package/dist/sdk/client/access-key.d.ts +1 -1
- package/dist/sdk/client/access-key.js +20 -6
- package/dist/sdk/client/create-flow.js +19 -11
- package/dist/sdk/client/dialog-host.js +8 -2
- package/dist/sdk/client/flow-widget.js +13 -2
- package/dist/sdk/client/idb.d.ts +2 -2
- package/dist/sdk/client/idb.js +17 -2
- package/dist/sdk/client/profile-button.js +18 -3
- package/dist/sdk/client/rooms.js +19 -4
- package/dist/sdk/client/session.js +8 -5
- package/dist/sdk/client/signing.js +22 -7
- package/dist/sdk/client/store.js +4 -5
- package/dist/sdk/cookies.d.ts +3 -1
- package/dist/sdk/cookies.js +11 -9
- package/dist/sdk/dialog/remote/Messenger.d.ts +0 -5
- package/dist/sdk/dialog/remote/Messenger.js +26 -14
- package/dist/sdk/driver-error.d.ts +8 -0
- package/dist/sdk/driver-error.js +17 -0
- package/dist/sdk/hex.d.ts +6 -0
- package/dist/sdk/hex.js +6 -0
- package/dist/sdk/id-host.d.ts +1 -1
- package/dist/sdk/id-host.js +4 -5
- package/dist/sdk/json.d.ts +17 -0
- package/dist/sdk/json.js +5 -0
- package/dist/sdk/react/flow-widget.d.ts +4 -0
- package/dist/sdk/react/flow-widget.js +19 -4
- package/dist/sdk/react/hooks.d.ts +8 -4
- package/dist/sdk/react/hooks.js +1 -10
- package/dist/sdk/react/profile-button.d.ts +5 -0
- package/dist/sdk/react/profile-button.js +20 -4
- package/dist/sdk/server.js +2 -1
- package/dist/sdk/session-core.d.ts +1 -6
- package/dist/sdk/session-core.js +7 -4
- package/dist/sdk/session-route.d.ts +0 -20
- package/dist/sdk/session-route.js +25 -9
- package/dist/sdk/start/graceful-shutdown.d.ts +8 -0
- package/dist/sdk/start/graceful-shutdown.js +81 -0
- package/dist/sdk/start/index.d.ts +2 -0
- package/dist/sdk/start/index.js +3 -2
- package/dist/sdk/token-expiry.js +3 -1
- package/dist/sdk/types/cosmetics.d.ts +162 -0
- package/dist/sdk/types/cosmetics.js +78 -0
- package/dist/sdk/types/game.d.ts +58 -0
- package/dist/sdk/types/game.js +8 -0
- package/dist/sdk/types/index.d.ts +8 -4
- package/dist/sdk/types/index.js +2 -0
- package/dist/sdk/types/messenger.d.ts +1 -1
- package/dist/sdk/types/protocol.d.ts +6 -5
- package/dist/sdk/types/protocol.js +10 -4
- package/dist/sdk/types/room-events.d.ts +49 -1
- package/dist/sdk/types/rooms.d.ts +53 -3
- package/dist/sdk/types/rooms.js +0 -1
- package/dist/sdk/types/sdk.d.ts +22 -5
- package/dist/sdk/types/server.d.ts +18 -0
- package/dist/sdk/verify.js +1 -0
- package/dist/sdk/wagmi/index.d.ts +7 -3
- package/dist/sdk/wagmi/index.js +19 -32
- package/package.json +11 -8
- package/dist/sdk/client/refresh-store.d.ts +0 -22
- package/dist/sdk/client/refresh-store.js +0 -66
package/dist/sdk/types/sdk.d.ts
CHANGED
|
@@ -1,6 +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 { RawValue } from "../json";
|
|
4
5
|
import type { AdditionalSession, FlowCredential, FlowUser, WebAuthnSignature } from "./auth";
|
|
5
6
|
import type { ConnectCapabilities, ConnectResponse } from "./protocol";
|
|
6
7
|
import type { RoomsApi } from "./rooms";
|
|
@@ -187,6 +188,11 @@ export type DialogOpenOptions = {
|
|
|
187
188
|
icon?: string;
|
|
188
189
|
};
|
|
189
190
|
};
|
|
191
|
+
/**
|
|
192
|
+
* Color scheme for Flow ID iframe chrome. "light dark" follows the OS, which
|
|
193
|
+
* is the default; a host with its own theme toggle passes the resolved value.
|
|
194
|
+
*/
|
|
195
|
+
export type ColorScheme = "light" | "dark" | "light dark";
|
|
190
196
|
/** Anchor for an auto-created (host-less) profile widget container. */
|
|
191
197
|
export type ProfilePosition = "top-right" | "top-left" | "bottom-right" | "bottom-left";
|
|
192
198
|
export type MountProfileOptions = {
|
|
@@ -209,11 +215,17 @@ export type MountProfileOptions = {
|
|
|
209
215
|
* Color scheme for the widget chrome. Set this to match the host (the dark
|
|
210
216
|
* game UI passes "dark"). Defaults to "light dark" (follows the OS).
|
|
211
217
|
*/
|
|
212
|
-
theme?:
|
|
218
|
+
theme?: ColorScheme;
|
|
213
219
|
/** Explicit Flow instance. Defaults to the createFlow() singleton. */
|
|
214
220
|
flow?: Flow;
|
|
215
221
|
};
|
|
216
222
|
export type ProfileButtonHandle = {
|
|
223
|
+
/**
|
|
224
|
+
* Recolors the mounted pill (and its profile dialog) in place. A host with a
|
|
225
|
+
* theme toggle calls this instead of remounting: rebuilding the iframe would
|
|
226
|
+
* reload the widget and flash the pill out of the layout on every toggle.
|
|
227
|
+
*/
|
|
228
|
+
setTheme: (theme: ColorScheme) => void;
|
|
217
229
|
/** Tears down the iframe, listeners, and any auto-created container. */
|
|
218
230
|
destroy: () => void;
|
|
219
231
|
};
|
|
@@ -237,7 +249,7 @@ export type MountFlowWidgetOptions = {
|
|
|
237
249
|
* Color scheme for the widget chrome. Set this to match the host (the dark
|
|
238
250
|
* game UI passes "dark"). Defaults to "light dark" (follows the OS).
|
|
239
251
|
*/
|
|
240
|
-
theme?:
|
|
252
|
+
theme?: ColorScheme;
|
|
241
253
|
/** Explicit Flow instance. Defaults to the createFlow() singleton. */
|
|
242
254
|
flow?: Flow;
|
|
243
255
|
};
|
|
@@ -251,7 +263,12 @@ export type FlowWidgetHandle = {
|
|
|
251
263
|
* Sends a widget-specific message (e.g. `{ type: "xp-refresh" }`), pinned to
|
|
252
264
|
* the Flow ID origin.
|
|
253
265
|
*/
|
|
254
|
-
post: (message:
|
|
266
|
+
post: (message: RawValue) => void;
|
|
267
|
+
/**
|
|
268
|
+
* Recolors the mounted widget in place, for the same reason the pill exposes
|
|
269
|
+
* it: a remount reloads the frame and flashes it out of the host's layout.
|
|
270
|
+
*/
|
|
271
|
+
setTheme: (theme: ColorScheme) => void;
|
|
255
272
|
/** Tears down the iframe and its bridge. */
|
|
256
273
|
destroy: () => void;
|
|
257
274
|
};
|
|
@@ -263,7 +280,7 @@ export type FlowWidgetProps = {
|
|
|
263
280
|
/** Override the Flow ID origin; defaults to the resolved Flow's `host`. */
|
|
264
281
|
host?: string;
|
|
265
282
|
/** Color scheme for the widget chrome; defaults to "light dark" (the OS). */
|
|
266
|
-
theme?:
|
|
283
|
+
theme?: ColorScheme;
|
|
267
284
|
};
|
|
268
285
|
export type DialogHost = {
|
|
269
286
|
open: (options?: DialogOpenOptions) => void;
|
|
@@ -364,7 +381,7 @@ export type ProfileButtonProps = {
|
|
|
364
381
|
/** Override the Flow ID origin; defaults to the resolved Flow's `host`. */
|
|
365
382
|
host?: string;
|
|
366
383
|
/** Color scheme for the widget chrome; defaults to "light dark" (the OS). */
|
|
367
|
-
theme?:
|
|
384
|
+
theme?: ColorScheme;
|
|
368
385
|
};
|
|
369
386
|
export type RunLoginParams = {
|
|
370
387
|
dialog: DialogHost;
|
|
@@ -60,3 +60,21 @@ export type SessionRouteResponse = {
|
|
|
60
60
|
*/
|
|
61
61
|
additionalSessions?: AdditionalSession[];
|
|
62
62
|
};
|
|
63
|
+
export type GracefulShutdownSignal = "SIGTERM" | "SIGINT";
|
|
64
|
+
export type GracefulShutdownServer = {
|
|
65
|
+
/** Stop accepting connections; `true` also closes active connections. */
|
|
66
|
+
stop(closeActiveConnections?: boolean): Promise<void> | void;
|
|
67
|
+
};
|
|
68
|
+
export type GracefulShutdownOptions = {
|
|
69
|
+
/**
|
|
70
|
+
* Total time allowed for active HTTP requests and `onShutdown` to finish.
|
|
71
|
+
* Defaults to 20 seconds, leaving Kubernetes headroom around the app drain.
|
|
72
|
+
*/
|
|
73
|
+
timeoutMs?: number;
|
|
74
|
+
/**
|
|
75
|
+
* Consumer cleanup started after the server stops accepting connections.
|
|
76
|
+
* Close WebSockets, flush presence, and finish other process-owned state here.
|
|
77
|
+
*/
|
|
78
|
+
onShutdown?: (signal: GracefulShutdownSignal) => Promise<void> | void;
|
|
79
|
+
};
|
|
80
|
+
export type GracefulShutdownCleanup = () => void;
|
package/dist/sdk/verify.js
CHANGED
|
@@ -33,5 +33,6 @@ export async function verifyFlowJWT(token, opts) {
|
|
|
33
33
|
});
|
|
34
34
|
if (!payload.sub)
|
|
35
35
|
throw new Error("JWT missing sub");
|
|
36
|
+
/* SAFETY: jwtVerify already checked the signature, issuer and audience against this issuer's own keys, and `sub` is checked above. */
|
|
36
37
|
return payload;
|
|
37
38
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RawValue } from "../json";
|
|
1
2
|
import type { FlowConnectorParameters } from "../types";
|
|
2
3
|
/**
|
|
3
4
|
* Wagmi connector adapter for Flow. Drop into your wagmi config with
|
|
@@ -17,9 +18,12 @@ import type { FlowConnectorParameters } from "../types";
|
|
|
17
18
|
* wagmi state stays in sync when login/logout happens via the SDK rather
|
|
18
19
|
* than through wagmi's own `connect()`.
|
|
19
20
|
*/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
/** The minimal EIP-1193 surface wagmi needs from this connector. */
|
|
22
|
+
type FlowProvider = {
|
|
23
|
+
request: (...args: RawValue[]) => Promise<RawValue>;
|
|
24
|
+
};
|
|
25
|
+
export declare function flowConnector(parameters?: FlowConnectorParameters): import("@wagmi/core").CreateConnectorFn<FlowProvider, Record<string, unknown>, Record<string, unknown>>;
|
|
23
26
|
export declare namespace flowConnector {
|
|
24
27
|
var type: "flow";
|
|
25
28
|
}
|
|
29
|
+
export {};
|
package/dist/sdk/wagmi/index.js
CHANGED
|
@@ -5,33 +5,14 @@ flowConnector.type = "flow";
|
|
|
5
5
|
function loginOptionsFromCapabilities(capabilities) {
|
|
6
6
|
const signUp = capabilities?.type === "sign-up" || capabilities?.createAccount;
|
|
7
7
|
const signIn = capabilities?.type === "sign-in" || capabilities?.signIn;
|
|
8
|
+
const headless = !signUp &&
|
|
9
|
+
(capabilities?.signInHeadless || capabilities?.type === "sign-in");
|
|
8
10
|
return {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
(capabilities?.signInHeadless || capabilities?.type === "sign-in")
|
|
13
|
-
? { signInHeadless: true }
|
|
14
|
-
: {}),
|
|
11
|
+
signUp: signUp ? true : undefined,
|
|
12
|
+
signIn: !signUp && signIn ? true : undefined,
|
|
13
|
+
signInHeadless: headless ? true : undefined,
|
|
15
14
|
};
|
|
16
15
|
}
|
|
17
|
-
/**
|
|
18
|
-
* Wagmi connector adapter for Flow. Drop into your wagmi config with
|
|
19
|
-
* `connectors: [flowConnector()]` — no arguments needed in the common case.
|
|
20
|
-
*
|
|
21
|
-
* Resolution order for the underlying Flow instance:
|
|
22
|
-
* 1. `parameters.flow` (explicit override — useful for tests)
|
|
23
|
-
* 2. The Flow singleton set by a prior `createFlow()` call
|
|
24
|
-
* 3. Lazily created on first connector use (also sets the singleton)
|
|
25
|
-
*
|
|
26
|
-
* This means wagmi-only apps get a working Flow without ever calling
|
|
27
|
-
* `createFlow`, and apps mixing wagmi + direct SDK calls automatically
|
|
28
|
-
* share state because they all resolve to the same singleton.
|
|
29
|
-
*
|
|
30
|
-
* The `setup()` method subscribes to the Flow store and emits wagmi
|
|
31
|
-
* `connect`/`disconnect` events whenever the address changes — that's how
|
|
32
|
-
* wagmi state stays in sync when login/logout happens via the SDK rather
|
|
33
|
-
* than through wagmi's own `connect()`.
|
|
34
|
-
*/
|
|
35
16
|
export function flowConnector(parameters = {}) {
|
|
36
17
|
let flow = parameters.flow ?? null;
|
|
37
18
|
return createConnector((config) => {
|
|
@@ -64,6 +45,8 @@ export function flowConnector(parameters = {}) {
|
|
|
64
45
|
return;
|
|
65
46
|
lastAddress = next;
|
|
66
47
|
if (next) {
|
|
48
|
+
/* SAFETY: wagmi types `accounts` as a readonly tuple of its own `Address` brand;
|
|
49
|
+
`next` is the checksummed address this connector just resolved. */
|
|
67
50
|
config.emitter.emit("connect", {
|
|
68
51
|
accounts: [next],
|
|
69
52
|
chainId: config.chains[0]?.id ?? 1,
|
|
@@ -76,6 +59,7 @@ export function flowConnector(parameters = {}) {
|
|
|
76
59
|
},
|
|
77
60
|
async connect(params = {}) {
|
|
78
61
|
const f = getFlow();
|
|
62
|
+
// SAFETY: wagmi passes its own connect options; WagmiConnectParams names the subset read.
|
|
79
63
|
const connectParams = params;
|
|
80
64
|
const isReconnecting = connectParams.isReconnecting;
|
|
81
65
|
// Wagmi calls connect({ isReconnecting: true }) on mount when it
|
|
@@ -135,15 +119,18 @@ export function flowConnector(parameters = {}) {
|
|
|
135
119
|
},
|
|
136
120
|
async getClient({ chainId } = {}) {
|
|
137
121
|
const f = getFlow();
|
|
138
|
-
|
|
122
|
+
/* SAFETY: wagmi types getClient against viem's Client generic over its own chain
|
|
123
|
+
config; the Flow wallet client implements that surface. */
|
|
124
|
+
return f.walletClient(chainId ? { chainId } : {});
|
|
139
125
|
},
|
|
140
|
-
async getProvider(
|
|
141
|
-
const client = await this.getClient({
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
126
|
+
async getProvider(options = {}) {
|
|
127
|
+
const client = await this.getClient?.({ chainId: options.chainId });
|
|
128
|
+
/* SAFETY: the wallet client viem builds always carries `request`; wagmi's Client type
|
|
129
|
+
does not name it, which is the only reason this is stated rather than inferred. */
|
|
130
|
+
const request = Object.entries(Object(client)).find(([k]) => k === "request")?.[1];
|
|
131
|
+
/* SAFETY: the wallet client viem builds always carries `request`; wagmi's Client type
|
|
132
|
+
does not name it, which is the only reason this is read by name. */
|
|
133
|
+
return { request: request };
|
|
147
134
|
},
|
|
148
135
|
onAccountsChanged() { },
|
|
149
136
|
onChainChanged(chain) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flow-industries/id",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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",
|
|
@@ -47,10 +47,7 @@
|
|
|
47
47
|
"dist/sdk"
|
|
48
48
|
],
|
|
49
49
|
"scripts": {
|
|
50
|
-
"dev": "
|
|
51
|
-
"dev:dialog": "bunx vite",
|
|
52
|
-
"dev:playground": "bunx vite --config vite.playground.config.ts",
|
|
53
|
-
"dev:landing": "bunx vite --config vite.landing.config.ts",
|
|
50
|
+
"dev": "./dev.sh",
|
|
54
51
|
"build:dialog": "bunx vite build",
|
|
55
52
|
"build:playground": "bunx vite build --config vite.playground.config.ts",
|
|
56
53
|
"build:landing": "bunx vite build --config vite.landing.config.ts",
|
|
@@ -59,12 +56,12 @@
|
|
|
59
56
|
"prepublishOnly": "bun run build:sdk",
|
|
60
57
|
"start": "bun run src/index.ts",
|
|
61
58
|
"worker": "bun run src/worker.ts",
|
|
62
|
-
"email:dev": "email dev --dir src/emails --port 3010",
|
|
63
59
|
"db:generate": "bunx @better-auth/cli generate",
|
|
64
60
|
"db:migrate:gen": "bunx drizzle-kit generate",
|
|
65
61
|
"db:migrate": "bunx drizzle-kit migrate",
|
|
66
62
|
"db:push": "bunx drizzle-kit push",
|
|
67
63
|
"db:studio": "bunx drizzle-kit studio",
|
|
64
|
+
"usernames:refresh": "bun run scripts/generate-reserved-usernames.ts",
|
|
68
65
|
"lint": "biome check",
|
|
69
66
|
"test": "bun test src",
|
|
70
67
|
"format": "biome format --write",
|
|
@@ -91,9 +88,9 @@
|
|
|
91
88
|
},
|
|
92
89
|
"devDependencies": {
|
|
93
90
|
"@better-auth/cli": "^1.4.17",
|
|
94
|
-
"@biomejs/biome": "2.
|
|
91
|
+
"@biomejs/biome": "2.5.8",
|
|
95
92
|
"@electric-sql/pglite": "^0.5.4",
|
|
96
|
-
"@flow-industries/lint": "0.
|
|
93
|
+
"@flow-industries/lint": "0.3.0",
|
|
97
94
|
"@react-email/ui": "^6.6.0",
|
|
98
95
|
"@tailwindcss/vite": "^4.1.18",
|
|
99
96
|
"@tanstack/react-router-devtools": "^1.160.0",
|
|
@@ -104,6 +101,7 @@
|
|
|
104
101
|
"@types/react": "^19.2.13",
|
|
105
102
|
"@types/react-dom": "^19.2.3",
|
|
106
103
|
"@vitejs/plugin-react": "^5.1.3",
|
|
104
|
+
"biome-anti-slop": "0.1.0",
|
|
107
105
|
"drizzle-kit": "^0.31.8",
|
|
108
106
|
"pino-pretty": "^13.1.3",
|
|
109
107
|
"react-email": "^6.6.0",
|
|
@@ -112,6 +110,10 @@
|
|
|
112
110
|
"typescript": "^5",
|
|
113
111
|
"vite": "^7.3.1"
|
|
114
112
|
},
|
|
113
|
+
"trustedDependencies": [
|
|
114
|
+
"esbuild",
|
|
115
|
+
"sharp"
|
|
116
|
+
],
|
|
115
117
|
"dependencies": {
|
|
116
118
|
"@aws-sdk/client-s3": "^3.1073.0",
|
|
117
119
|
"@flow-industries/ui": "^0.19.0",
|
|
@@ -146,6 +148,7 @@
|
|
|
146
148
|
"tempo.ts": "^0.14.2",
|
|
147
149
|
"viem": "2.47.10",
|
|
148
150
|
"wagmi": "^3.4.2",
|
|
151
|
+
"zod": "^4.3.6",
|
|
149
152
|
"zustand": "^5.0.11"
|
|
150
153
|
}
|
|
151
154
|
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { RefreshCookieConfig } from "../types";
|
|
2
|
-
/**
|
|
3
|
-
* First-party persistence for the rotating refresh token. It lives in the
|
|
4
|
-
* consumer app's OWN localStorage (origin-scoped), so it survives reloads and
|
|
5
|
-
* navigations and — unlike the old id.flow.industries cookie — is never a
|
|
6
|
-
* third-party cookie, so iOS ITP can't block it. Keyed by `host` so pointing
|
|
7
|
-
* the SDK at a different Flow ID origin (dev vs prod) doesn't collide.
|
|
8
|
-
*/
|
|
9
|
-
export interface RefreshStore {
|
|
10
|
-
get(): string | null;
|
|
11
|
-
set(token: string): void;
|
|
12
|
-
clear(): void;
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* With a cookie config (`createFlow({ cookies: true })`) the token is
|
|
16
|
-
* additionally mirrored into a first-party cookie so the app's own server can
|
|
17
|
-
* refresh the session during SSR. Reads prefer the cookie: the only writer
|
|
18
|
-
* that updates one side without the other is that server (Set-Cookie only),
|
|
19
|
-
* and its value is always a successor of whatever localStorage still holds;
|
|
20
|
-
* every client-side write hits both in the same call.
|
|
21
|
-
*/
|
|
22
|
-
export declare function makeRefreshStore(host: string, cookie?: RefreshCookieConfig): RefreshStore;
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { clearBrowserCookie, REFRESH_COOKIE_MAX_AGE_S, readBrowserCookie, writeBrowserCookie, } from "../cookies";
|
|
2
|
-
/**
|
|
3
|
-
* With a cookie config (`createFlow({ cookies: true })`) the token is
|
|
4
|
-
* additionally mirrored into a first-party cookie so the app's own server can
|
|
5
|
-
* refresh the session during SSR. Reads prefer the cookie: the only writer
|
|
6
|
-
* that updates one side without the other is that server (Set-Cookie only),
|
|
7
|
-
* and its value is always a successor of whatever localStorage still holds;
|
|
8
|
-
* every client-side write hits both in the same call.
|
|
9
|
-
*/
|
|
10
|
-
export function makeRefreshStore(host, cookie) {
|
|
11
|
-
const key = `flow.id.refresh:${host}`;
|
|
12
|
-
const readLocal = () => {
|
|
13
|
-
try {
|
|
14
|
-
return localStorage.getItem(key);
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
const readCookie = () => {
|
|
21
|
-
if (!cookie || typeof document === "undefined")
|
|
22
|
-
return null;
|
|
23
|
-
return readBrowserCookie(document.cookie, cookie.name);
|
|
24
|
-
};
|
|
25
|
-
const store = {
|
|
26
|
-
get() {
|
|
27
|
-
return readCookie() ?? readLocal();
|
|
28
|
-
},
|
|
29
|
-
set(token) {
|
|
30
|
-
// Never persist a missing token: localStorage.setItem coerces undefined
|
|
31
|
-
// to the literal "undefined", which later reads back as a bogus bearer
|
|
32
|
-
// and 401s as refresh_token_invalid. Fail safe by ignoring it instead.
|
|
33
|
-
if (!token)
|
|
34
|
-
return;
|
|
35
|
-
try {
|
|
36
|
-
localStorage.setItem(key, token);
|
|
37
|
-
}
|
|
38
|
-
catch { }
|
|
39
|
-
if (cookie) {
|
|
40
|
-
writeBrowserCookie(cookie.name, token, {
|
|
41
|
-
maxAge: REFRESH_COOKIE_MAX_AGE_S,
|
|
42
|
-
secure: cookie.secure,
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
clear() {
|
|
47
|
-
try {
|
|
48
|
-
localStorage.removeItem(key);
|
|
49
|
-
}
|
|
50
|
-
catch { }
|
|
51
|
-
if (cookie)
|
|
52
|
-
clearBrowserCookie(cookie.name, { secure: cookie.secure });
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
// One-time reconcile so exactly one canonical value exists before the first
|
|
56
|
-
// refresh: an SSR rotation updates only the cookie, leaving localStorage one
|
|
57
|
-
// generation behind — presenting that stale token would look like reuse.
|
|
58
|
-
const fromCookie = readCookie();
|
|
59
|
-
if (fromCookie && fromCookie !== readLocal()) {
|
|
60
|
-
try {
|
|
61
|
-
localStorage.setItem(key, fromCookie);
|
|
62
|
-
}
|
|
63
|
-
catch { }
|
|
64
|
-
}
|
|
65
|
-
return store;
|
|
66
|
-
}
|