@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.
Files changed (61) hide show
  1. package/README.md +19 -0
  2. package/dist/sdk/client/access-key.d.ts +1 -1
  3. package/dist/sdk/client/access-key.js +20 -6
  4. package/dist/sdk/client/create-flow.js +19 -11
  5. package/dist/sdk/client/dialog-host.js +8 -2
  6. package/dist/sdk/client/flow-widget.js +13 -2
  7. package/dist/sdk/client/idb.d.ts +2 -2
  8. package/dist/sdk/client/idb.js +17 -2
  9. package/dist/sdk/client/profile-button.js +18 -3
  10. package/dist/sdk/client/rooms.js +19 -4
  11. package/dist/sdk/client/session.js +8 -5
  12. package/dist/sdk/client/signing.js +22 -7
  13. package/dist/sdk/client/store.js +4 -5
  14. package/dist/sdk/cookies.d.ts +3 -1
  15. package/dist/sdk/cookies.js +11 -9
  16. package/dist/sdk/dialog/remote/Messenger.d.ts +0 -5
  17. package/dist/sdk/dialog/remote/Messenger.js +26 -14
  18. package/dist/sdk/driver-error.d.ts +8 -0
  19. package/dist/sdk/driver-error.js +17 -0
  20. package/dist/sdk/hex.d.ts +6 -0
  21. package/dist/sdk/hex.js +6 -0
  22. package/dist/sdk/id-host.d.ts +1 -1
  23. package/dist/sdk/id-host.js +4 -5
  24. package/dist/sdk/json.d.ts +17 -0
  25. package/dist/sdk/json.js +5 -0
  26. package/dist/sdk/react/flow-widget.d.ts +4 -0
  27. package/dist/sdk/react/flow-widget.js +19 -4
  28. package/dist/sdk/react/hooks.d.ts +8 -4
  29. package/dist/sdk/react/hooks.js +1 -10
  30. package/dist/sdk/react/profile-button.d.ts +5 -0
  31. package/dist/sdk/react/profile-button.js +20 -4
  32. package/dist/sdk/server.js +2 -1
  33. package/dist/sdk/session-core.d.ts +1 -6
  34. package/dist/sdk/session-core.js +7 -4
  35. package/dist/sdk/session-route.d.ts +0 -20
  36. package/dist/sdk/session-route.js +25 -9
  37. package/dist/sdk/start/graceful-shutdown.d.ts +8 -0
  38. package/dist/sdk/start/graceful-shutdown.js +81 -0
  39. package/dist/sdk/start/index.d.ts +2 -0
  40. package/dist/sdk/start/index.js +3 -2
  41. package/dist/sdk/token-expiry.js +3 -1
  42. package/dist/sdk/types/cosmetics.d.ts +162 -0
  43. package/dist/sdk/types/cosmetics.js +78 -0
  44. package/dist/sdk/types/game.d.ts +58 -0
  45. package/dist/sdk/types/game.js +8 -0
  46. package/dist/sdk/types/index.d.ts +8 -4
  47. package/dist/sdk/types/index.js +2 -0
  48. package/dist/sdk/types/messenger.d.ts +1 -1
  49. package/dist/sdk/types/protocol.d.ts +6 -5
  50. package/dist/sdk/types/protocol.js +10 -4
  51. package/dist/sdk/types/room-events.d.ts +49 -1
  52. package/dist/sdk/types/rooms.d.ts +53 -3
  53. package/dist/sdk/types/rooms.js +0 -1
  54. package/dist/sdk/types/sdk.d.ts +22 -5
  55. package/dist/sdk/types/server.d.ts +18 -0
  56. package/dist/sdk/verify.js +1 -0
  57. package/dist/sdk/wagmi/index.d.ts +7 -3
  58. package/dist/sdk/wagmi/index.js +19 -32
  59. package/package.json +11 -8
  60. package/dist/sdk/client/refresh-store.d.ts +0 -22
  61. package/dist/sdk/client/refresh-store.js +0 -66
package/README.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  Passkey-first identity for Flow applications. One passkey bound to `id.flow.industries`, usable across Flow apps with audience-bound JWTs and optional Tempo signing.
4
4
 
5
+ ## Develop
6
+
7
+ ```bash
8
+ cp .env.example .env
9
+ bun install
10
+ bun run dev
11
+ ```
12
+
13
+ `bun run dev` is the only supported local application launcher. It starts the
14
+ API, dialog, playground, and landing page on the first free block of
15
+ four consecutive ports. Configure `DATABASE_URL` and `BETTER_AUTH_SECRET` in
16
+ `.env` first.
17
+
18
+ The launcher validates locally stored JWT signing keys before starting Auth.
19
+ If `BETTER_AUTH_SECRET` changed, only keys that can no longer be decrypted are
20
+ rotated so local session creation keeps working after environment changes.
21
+ It also refuses to start when migrations fail instead of serving against a
22
+ partially migrated schema.
23
+
5
24
  **npm:** [`@flow-industries/id`](https://www.npmjs.com/package/@flow-industries/id)
6
25
 
7
26
  ## Installation
@@ -38,7 +38,7 @@ export declare function loadAccessKey(address: Hex): Promise<StoredAccessKey | u
38
38
  * consumed at consensus, not execution — the keychain entry persists even
39
39
  * if the call inside the tx reverts.
40
40
  */
41
- export declare function consumePendingAuthorization(address: Hex): Promise<unknown | null>;
41
+ export declare function consumePendingAuthorization(address: Hex): Promise<StoredAccessKey["keyAuthorization"] | null>;
42
42
  export declare function isExpired(stored: StoredAccessKey): boolean;
43
43
  export declare function clearAccessKey(address: Hex): Promise<void>;
44
44
  /**
@@ -2,6 +2,7 @@ import * as Address from "ox/Address";
2
2
  import * as PublicKey from "ox/PublicKey";
3
3
  import { KeyAuthorization, SignatureEnvelope } from "ox/tempo";
4
4
  import { Account, WebCryptoP256 } from "viem/tempo";
5
+ import { hex0x } from "../hex";
5
6
  import { idb } from "./idb";
6
7
  /**
7
8
  * Step 1 of access-key creation — runs BEFORE the dialog opens.
@@ -27,6 +28,9 @@ export async function prepareAccessKey(options, chainId) {
27
28
  expiry: options.expiry,
28
29
  type: "p256",
29
30
  });
31
+ /* SAFETY: tempo's KeyAuthorization.getSignPayload is typed for the signed variant only, but
32
+ the sign payload is by definition computed from the UNSIGNED authorization built above, and
33
+ it returns the hash as a hex string. */
30
34
  const accessKeyHash = KeyAuthorization.getSignPayload(keyAuthUnsigned);
31
35
  return { keyPair, keyAuthUnsigned, accessKeyHash, expiry: options.expiry };
32
36
  }
@@ -43,7 +47,7 @@ export async function finalizeAccessKey(params) {
43
47
  const { address, credential, webauthn, preparation } = params;
44
48
  const signatureEnvelope = SignatureEnvelope.from({
45
49
  metadata: {
46
- authenticatorData: webauthn.metadata.authenticatorData,
50
+ authenticatorData: hex0x(webauthn.metadata.authenticatorData),
47
51
  clientDataJSON: webauthn.metadata.clientDataJSON,
48
52
  challengeIndex: webauthn.metadata.challengeIndex,
49
53
  typeIndex: webauthn.metadata.typeIndex,
@@ -52,11 +56,14 @@ export async function finalizeAccessKey(params) {
52
56
  r: BigInt(webauthn.signature.r),
53
57
  s: BigInt(webauthn.signature.s),
54
58
  },
55
- publicKey: PublicKey.from(`0x${credential.publicKey.replace(/^0x/, "")}`),
59
+ publicKey: PublicKey.from(hex0x(credential.publicKey)),
56
60
  type: "webAuthn",
57
61
  });
62
+ /* SAFETY: this is the same authorization prepared in step 1, now carrying the passkey
63
+ signature. tempo types `from` against its signed union, which the spread cannot reconstruct
64
+ structurally, so the shape is named once here. */
58
65
  const keyAuthorization = KeyAuthorization.from({
59
- ...preparation.keyAuthUnsigned,
66
+ ...Object(preparation.keyAuthUnsigned),
60
67
  signature: signatureEnvelope,
61
68
  });
62
69
  const stored = {
@@ -92,9 +99,13 @@ export async function consumePendingAuthorization(address) {
92
99
  });
93
100
  return auth;
94
101
  }
102
+ /** Older stored keys kept the expiry inside the authorization; tempo does not type that field. */
103
+ function expiryOf(keyAuthorization) {
104
+ const found = Object.entries(Object(keyAuthorization)).find(([k]) => k === "expiry")?.[1];
105
+ return Number(found) === found ? found : undefined;
106
+ }
95
107
  export function isExpired(stored) {
96
- const auth = stored.keyAuthorization;
97
- const expiry = stored.expiry ?? auth?.expiry;
108
+ const expiry = stored.expiry ?? expiryOf(stored.keyAuthorization);
98
109
  if (!expiry)
99
110
  return false;
100
111
  return expiry < Date.now() / 1000;
@@ -110,8 +121,11 @@ export async function clearAccessKey(address) {
110
121
  * allowed to act for the address.
111
122
  */
112
123
  export function buildAccessKeyAccount(stored, rootCredential, rpId) {
124
+ /* SAFETY: tempo's Account constructors are typed against porto's own credential and key
125
+ types, which Flow's equivalents mirror field-for-field but are not nominally the same. The
126
+ four assertions below are that nominal gap, not a claim about the runtime values. */
113
127
  const rootAccount = Account.fromWebAuthnP256(rootCredential, {
114
- ...(rpId ? { rpId } : {}),
128
+ rpId: rpId ? rpId : undefined,
115
129
  });
116
130
  return Account.fromWebCryptoP256({
117
131
  privateKey: stored.privateKey,
@@ -14,7 +14,10 @@ import { createStore, initialFlowState } from "./store";
14
14
  * where Web Locks is unavailable.
15
15
  */
16
16
  function withOriginLock(name, fn) {
17
- const locks = globalThis.navigator?.locks;
17
+ const locks =
18
+ /* SAFETY: the SDK stashes its singleton on globalThis so two bundles of it share one
19
+ instance; the property is namespaced and written only here. */
20
+ globalThis.navigator?.locks;
18
21
  if (locks?.request)
19
22
  return locks.request(name, fn);
20
23
  return fn();
@@ -65,7 +68,7 @@ export function createFlow(options = {}) {
65
68
  // and IDB writes while components/hooks that captured it keep reading
66
69
  // stale state. For genuine multi-instance scenarios (tests), call
67
70
  // resetFlow() first or pass explicit Flow instances.
68
- if (typeof window === "undefined") {
71
+ if (!("window" in globalThis)) {
69
72
  throw new Error("createFlow() is browser-only (IndexedDB, iframes, fetch with " +
70
73
  "cookies). For server rendering, resolve state with resolveSession() " +
71
74
  "from @flow-industries/id/server and render with createStaticFlow().");
@@ -112,13 +115,15 @@ export function createFlow(options = {}) {
112
115
  * knows neither, and overwriting would erase state restored from IDB.
113
116
  */
114
117
  function commitSession(session) {
115
- store.setState({
118
+ const next = {
116
119
  user: session.user,
117
120
  jwt: session.jwt,
118
- ...(session.credential !== undefined
119
- ? { credential: session.credential, address: session.address ?? null }
120
- : {}),
121
- });
121
+ };
122
+ if (session.credential !== undefined) {
123
+ next.credential = session.credential;
124
+ next.address = session.address ?? null;
125
+ }
126
+ store.setState(next);
122
127
  // A guest carries a null credential; never persist that — the IDB store is
123
128
  // reserved for a real passkey credential and writing null would erase a
124
129
  // previously stored one.
@@ -141,6 +146,7 @@ export function createFlow(options = {}) {
141
146
  });
142
147
  if (!res.ok)
143
148
  return false;
149
+ // SAFETY: the app's own /flow/session route answers this shape.
144
150
  const { state } = (await res.json());
145
151
  if (!state)
146
152
  return false;
@@ -168,6 +174,7 @@ export function createFlow(options = {}) {
168
174
  });
169
175
  if (!res.ok)
170
176
  return null;
177
+ // SAFETY: the app's own /flow/session route answers this shape.
171
178
  const { additionalSessions } = (await res.json());
172
179
  return additionalSessions?.find((s) => s.audience === audience) ?? null;
173
180
  }
@@ -298,7 +305,8 @@ export function createFlow(options = {}) {
298
305
  // Only a guest has no credential. The server's idempotency path can
299
306
  // return a full session here; in that case keep any credential/
300
307
  // address already in state rather than stripping signing.
301
- ...(result.user.isGuest ? { credential: null, address: null } : {}),
308
+ credential: result.user.isGuest ? null : undefined,
309
+ address: result.user.isGuest ? null : undefined,
302
310
  });
303
311
  await installSession(result.refreshToken, result.jwt);
304
312
  deliverAdditionalSessions(result.additionalSessions);
@@ -337,8 +345,8 @@ export function createFlow(options = {}) {
337
345
  getState: () => store.getSnapshot(),
338
346
  getChain,
339
347
  getTransport,
340
- ...(rpId ? { rpId } : {}),
341
- ...(accessKeyOptions?.strict ? { strict: accessKeyOptions.strict } : {}),
348
+ rpId: rpId ? rpId : undefined,
349
+ strict: accessKeyOptions?.strict ? accessKeyOptions.strict : undefined,
342
350
  };
343
351
  }
344
352
  /**
@@ -389,7 +397,7 @@ export function createFlow(options = {}) {
389
397
  dialog: dialogHost,
390
398
  store,
391
399
  options: loginOpts,
392
- ...(extraCapabilities ? { extraCapabilities } : {}),
400
+ extraCapabilities: extraCapabilities ? extraCapabilities : undefined,
393
401
  });
394
402
  await installSession(refreshToken, session.jwt);
395
403
  deliverAdditionalSessions(additionalSessions);
@@ -97,6 +97,7 @@ export function createDialogHost(options) {
97
97
  if (initSent)
98
98
  return;
99
99
  initSent = true;
100
+ // SAFETY: the payload map types this topic; the dialog reads exactly these fields.
100
101
  messenger?.send("__internal", {
101
102
  type: "init",
102
103
  mode: "iframe",
@@ -111,7 +112,9 @@ export function createDialogHost(options) {
111
112
  // The dialog owns its open/close animation and mounts the overlay (playing
112
113
  // the enter animation) on this signal — same mechanism as the profile
113
114
  // widget, so every dialog appears and disappears identically.
114
- void messenger?.send("__internal", { type: "dialog-shown" });
115
+ void (
116
+ // SAFETY: the payload map types this topic; the dialog reads exactly these fields.
117
+ messenger?.send("__internal", { type: "dialog-shown" }));
115
118
  }
116
119
  function hide() {
117
120
  if (!iframe)
@@ -121,7 +124,9 @@ export function createDialogHost(options) {
121
124
  // requestAnimationFrame, making the close (and the next open) skip straight
122
125
  // to the end. While hidden the overlay is transparent and click-through.
123
126
  iframe.style.pointerEvents = "none";
124
- void messenger?.send("__internal", { type: "dialog-hidden" });
127
+ void (
128
+ // SAFETY: the payload map types this topic; the dialog reads exactly these fields.
129
+ messenger?.send("__internal", { type: "dialog-hidden" }));
125
130
  }
126
131
  function open(opts) {
127
132
  if (opts?.mode === "popup") {
@@ -160,6 +165,7 @@ export function createDialogHost(options) {
160
165
  const rpcRequest = { id, method, params, jsonrpc: "2.0" };
161
166
  return new Promise((resolve, reject) => {
162
167
  pending.set(id, { resolve, reject });
168
+ // SAFETY: the rpc-requests payload shape.
163
169
  messenger.send("rpc-requests", [
164
170
  { request: rpcRequest, status: "pending" },
165
171
  ]);
@@ -30,16 +30,17 @@ const WIDGET_ROUTE = {
30
30
  * No-op under SSR.
31
31
  */
32
32
  export function createFlowWidget(options) {
33
- if (typeof document === "undefined")
33
+ if (!("document" in globalThis))
34
34
  return {
35
35
  frame: null,
36
36
  post() { },
37
+ setTheme() { },
37
38
  destroy() { },
38
39
  };
39
40
  const flow = options.flow ?? getFlow() ?? requireFlow();
40
41
  const host = resolveIdHost(options.host ?? flow.host);
41
42
  const hostOrigin = new URL(host).origin;
42
- const theme = options.theme ?? "light dark";
43
+ let theme = options.theme ?? "light dark";
43
44
  const route = WIDGET_ROUTE[options.widget];
44
45
  const frame = makeIframe(`${host}${route}`);
45
46
  frame.style.background = "transparent";
@@ -47,6 +48,7 @@ export function createFlowWidget(options) {
47
48
  // Appended before bridging: `contentWindow` is null until the frame is in the
48
49
  // document, and the bridge is bound to that window.
49
50
  container.appendChild(frame);
51
+ /* SAFETY: the iframe was just appended, so its contentWindow exists. */
50
52
  const bridge = bridgeToWindow(frame.contentWindow, {
51
53
  targetOrigin: hostOrigin,
52
54
  });
@@ -70,6 +72,15 @@ export function createFlowWidget(options) {
70
72
  post(message) {
71
73
  frame.contentWindow?.postMessage(message, hostOrigin);
72
74
  },
75
+ setTheme(next) {
76
+ if (next === theme)
77
+ return;
78
+ theme = next;
79
+ void bridge.send("__internal", {
80
+ type: "set-theme",
81
+ theme: { colorScheme: next },
82
+ });
83
+ },
73
84
  destroy() {
74
85
  bridge.destroy();
75
86
  frame.remove();
@@ -1,5 +1,5 @@
1
1
  export declare const idb: {
2
2
  get<T = unknown>(key: string): Promise<T | undefined>;
3
- set(key: string, value: unknown): Promise<unknown>;
4
- delete(key: string): Promise<unknown>;
3
+ set<T>(key: string, value: T): Promise<void>;
4
+ delete(key: string): Promise<void>;
5
5
  };
@@ -16,10 +16,25 @@ function createStore(dbName, storeName) {
16
16
  };
17
17
  return (txMode, callback) => getDB().then((db) => callback(db.transaction(storeName, txMode).objectStore(storeName)));
18
18
  }
19
+ /**
20
+ * `IDBRequest` settles on `success`/`error` and `IDBTransaction` on `complete`/`abort`, and only
21
+ * the request carries `result`. Both are handled with one promise by reading the members each
22
+ * type actually declares, rather than casting the union away.
23
+ */
19
24
  function promisify(request) {
20
25
  return new Promise((resolve, reject) => {
21
- request.oncomplete = request.onsuccess = () => resolve(request.result);
22
- request.onabort = request.onerror = () => reject(request.error);
26
+ const settleError = () => reject(request.error);
27
+ if (request instanceof IDBTransaction) {
28
+ /* SAFETY: a transaction has no result; callers of the transaction overload discard it. */
29
+ request.oncomplete = () => resolve(undefined);
30
+ request.onabort = settleError;
31
+ request.onerror = settleError;
32
+ return;
33
+ }
34
+ /* SAFETY: IndexedDB stores structured clones with no schema of its own, so the caller names
35
+ the type it previously stored under this key. */
36
+ request.onsuccess = () => resolve(request.result);
37
+ request.onerror = settleError;
23
38
  });
24
39
  }
25
40
  export const idb = {
@@ -28,12 +28,14 @@ const OVERLAY_STYLE = {
28
28
  * callers don't thread the instance through. No-op under SSR.
29
29
  */
30
30
  export function createProfileButton(options) {
31
- if (typeof document === "undefined")
32
- return { destroy() { } };
31
+ if (!("document" in globalThis))
32
+ return { setTheme() { }, destroy() { } };
33
33
  const flow = options.flow ?? getFlow() ?? requireFlow();
34
34
  const host = resolveIdHost(options.host ?? flow.host);
35
35
  const hostOrigin = new URL(host).origin;
36
- const theme = options.theme ?? "light dark";
36
+ // Mutable so `setTheme` recolors over the open bridge, and so a dialog opened
37
+ // after a toggle is initialized with the theme the host is actually showing.
38
+ let theme = options.theme ?? "light dark";
37
39
  let createdContainer = null;
38
40
  let container = options.container ?? null;
39
41
  if (!container) {
@@ -183,6 +185,19 @@ export function createProfileButton(options) {
183
185
  pushIdentity();
184
186
  const unsubscribe = flow.subscribe(pushIdentity);
185
187
  return {
188
+ setTheme(next) {
189
+ if (next === theme)
190
+ return;
191
+ theme = next;
192
+ void pillBridge.send("__internal", {
193
+ type: "set-theme",
194
+ theme: { colorScheme: next },
195
+ });
196
+ void dialogBridge?.send("__internal", {
197
+ type: "set-theme",
198
+ theme: { colorScheme: next },
199
+ });
200
+ },
186
201
  destroy() {
187
202
  unsubscribe();
188
203
  pillBridge.destroy();
@@ -1,3 +1,9 @@
1
+ import { z } from "zod";
2
+ /** Every rooms endpoint answers a failure as `{ error, reason? }`. */
3
+ const failureSchema = z.object({
4
+ error: z.string().optional(),
5
+ reason: z.string().optional(),
6
+ });
1
7
  /** Thrown on any non-2xx rooms response; `reason` carries the machine-readable
2
8
  * cause when the API provides one (e.g. "banned", "private"). */
3
9
  export class RoomsRequestError extends Error {
@@ -31,12 +37,15 @@ export function createRoomsApi(host, getToken) {
31
37
  headers,
32
38
  body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
33
39
  });
34
- const payload = (await res.json().catch(() => ({})));
40
+ const payload = await res.json().catch(() => ({}));
35
41
  if (!res.ok) {
36
- throw new RoomsRequestError(res.status, typeof payload.error === "string"
37
- ? payload.error
38
- : `Rooms request failed (${res.status})`, typeof payload.reason === "string" ? payload.reason : undefined);
42
+ const failure = failureSchema.safeParse(payload);
43
+ throw new RoomsRequestError(res.status, failure.success && failure.data.error
44
+ ? failure.data.error
45
+ : `Rooms request failed (${res.status})`, failure.success ? failure.data.reason : undefined);
39
46
  }
47
+ /* SAFETY: each caller names the response type of the rooms endpoint it just called; a
48
+ non-ok status has already thrown above with the server's own wording. */
40
49
  return payload;
41
50
  }
42
51
  const slugPath = (slug) => `/${encodeURIComponent(slug)}`;
@@ -82,5 +91,11 @@ export function createRoomsApi(host, getToken) {
82
91
  unban: (slug, userId) => act(slug, "unban", { userId }),
83
92
  mute: (slug, userId, options) => act(slug, "mute", { userId, ...options }),
84
93
  unmute: (slug, userId) => act(slug, "unmute", { userId }),
94
+ timeout: (slug, userId, options) => act(slug, "timeout", {
95
+ userId,
96
+ seconds: options.seconds,
97
+ reason: options.reason,
98
+ }),
99
+ removeTimeout: (slug, userId) => act(slug, "timeout/remove", { userId }),
85
100
  };
86
101
  }
@@ -1,5 +1,6 @@
1
1
  import * as OxAddress from "ox/Address";
2
2
  import * as PublicKey from "ox/PublicKey";
3
+ import { hex0x } from "../hex";
3
4
  import { idb } from "./idb";
4
5
  import { METHODS } from "./methods";
5
6
  import { initialFlowState } from "./store";
@@ -32,11 +33,12 @@ export async function runLogin(params) {
32
33
  const { dialog, store, options, extraCapabilities } = params;
33
34
  const signUp = Boolean(options?.signUp);
34
35
  const capabilities = {
35
- ...(signUp ? { createAccount: true } : {}),
36
- ...(!signUp && options?.signIn ? { signIn: true } : {}),
37
- ...(!signUp && options?.signInHeadless ? { signInHeadless: true } : {}),
36
+ createAccount: signUp ? true : undefined,
37
+ signIn: !signUp && options?.signIn ? true : undefined,
38
+ signInHeadless: !signUp && options?.signInHeadless ? true : undefined,
38
39
  ...(extraCapabilities ?? {}),
39
40
  };
41
+ /* SAFETY: the connect method's response shape, which the dialog route answers. */
40
42
  const result = (await dialog.request(METHODS.connect, [
41
43
  { capabilities },
42
44
  ]));
@@ -52,7 +54,7 @@ export async function runLogin(params) {
52
54
  session: { user, jwt, credential, address },
53
55
  refreshToken,
54
56
  webauthn,
55
- ...(additionalSessions ? { additionalSessions } : {}),
57
+ additionalSessions: additionalSessions ? additionalSessions : undefined,
56
58
  };
57
59
  }
58
60
  /**
@@ -80,6 +82,7 @@ export async function runLogout(store) {
80
82
  * the credential.
81
83
  */
82
84
  export function credentialToAddress(credential) {
83
- const pub = PublicKey.from(`0x${credential.publicKey.replace(/^0x/, "")}`);
85
+ const pub = PublicKey.from(hex0x(credential.publicKey));
86
+ // SAFETY: ox brands its Address; Flow's Address is the same checksummed 0x-string.
84
87
  return OxAddress.fromPublicKey(pub);
85
88
  }
@@ -23,7 +23,11 @@ function requireAuth(state) {
23
23
  * background signing only ever uses the access key.
24
24
  */
25
25
  async function resolveAccount(params) {
26
- const rootAccount = Account.fromWebAuthnP256(params.credential, { ...(params.rpId ? { rpId: params.rpId } : {}) });
26
+ /* SAFETY: tempo types its account constructors against porto's own credential and options
27
+ types, which Flow's equivalents mirror field-for-field but are not nominally the same. */
28
+ const rootAccount = Account.fromWebAuthnP256(params.credential, {
29
+ rpId: params.rpId,
30
+ });
27
31
  const stored = await loadAccessKey(params.address);
28
32
  if (!stored)
29
33
  return rootAccount;
@@ -51,6 +55,8 @@ async function resolveAccount(params) {
51
55
  function withAccessKeyAuthorization(chain, address) {
52
56
  const inheritedPrepare = chain.prepareTransactionRequest;
53
57
  const hook = async (args, { phase }) => {
58
+ /* SAFETY: viem's parameter type is a wide union; PrepareArgsWithAuth names only the
59
+ key-authorization field this hook reads and writes. */
54
60
  const argsWithAuth = args;
55
61
  // Preserve auth threaded by an earlier phase invocation; otherwise
56
62
  // consume one from IDB. Atomic read+clear so re-runs don't
@@ -61,11 +67,14 @@ function withAccessKeyAuthorization(chain, address) {
61
67
  // (Tempo's chainConfig adds gas adjustments based on signature type
62
68
  // and handles expiring nonces — we'd break those without this).
63
69
  const inheritedResult = await runInheritedPrepare(inheritedPrepare, args, phase);
64
- return {
65
- ...args,
66
- ...inheritedResult,
67
- ...(keyAuthorization ? { keyAuthorization } : {}),
68
- };
70
+ const prepared = { ...args, ...inheritedResult };
71
+ const authorized = keyAuthorization
72
+ ? { ...prepared, keyAuthorization }
73
+ : prepared;
74
+ /* SAFETY: this is the caller's own request plus the inherited fields and, when the account
75
+ authorized one, the key authorization. viem's parameter type is a 30-way union that its
76
+ own spread cannot reconstruct, so the shape is named here once. */
77
+ return authorized;
69
78
  };
70
79
  return defineChain({
71
80
  ...chain,
@@ -84,7 +93,7 @@ function withAccessKeyAuthorization(chain, address) {
84
93
  async function runInheritedPrepare(inherited, args, phase) {
85
94
  if (!inherited)
86
95
  return {};
87
- const [fn, options] = typeof inherited === "function" ? [inherited, undefined] : inherited;
96
+ const [fn, options] = inherited instanceof Function ? [inherited, undefined] : inherited;
88
97
  if (!fn)
89
98
  return {};
90
99
  if (options && !options.runAt.includes(phase))
@@ -113,6 +122,7 @@ export async function buildWalletClient(ctx, chainId) {
113
122
  return createWalletClient({
114
123
  account,
115
124
  chain: withAccessKeyAuthorization(chain, address),
125
+ // SAFETY: the compat wrapper only reads `account`; its options type is viem-internal.
116
126
  transport: walletNamespaceCompat(transport, {
117
127
  account,
118
128
  }),
@@ -128,6 +138,8 @@ export async function signMessage(ctx, args) {
128
138
  export async function signTypedData(ctx, args) {
129
139
  const { chainId, ...rest } = args;
130
140
  const client = await buildWalletClient(ctx, chainId);
141
+ /* SAFETY: viem's parameter type is generic over chain and account, which this wrapper
142
+ resolves at runtime; the fields spread below are the caller's own validated args. */
131
143
  return viemSignTypedData(client, {
132
144
  account: client.account,
133
145
  ...rest,
@@ -136,6 +148,8 @@ export async function signTypedData(ctx, args) {
136
148
  export async function sendTransaction(ctx, args) {
137
149
  const { chainId, ...rest } = args;
138
150
  const client = await buildWalletClient(ctx, chainId);
151
+ /* SAFETY: viem's parameter type is generic over chain and account, which this wrapper
152
+ resolves at runtime; the fields spread below are the caller's own validated args. */
139
153
  return viemSendTransaction(client, {
140
154
  account: client.account,
141
155
  chain: client.chain,
@@ -145,6 +159,7 @@ export async function sendTransaction(ctx, args) {
145
159
  export async function sendCalls(ctx, args) {
146
160
  const { chainId, ...rest } = args;
147
161
  const client = await buildWalletClient(ctx, chainId);
162
+ // SAFETY: viem's generic parameter type, resolved at runtime.
148
163
  const result = await viemSendCalls(client, {
149
164
  account: client.account,
150
165
  chain: client.chain,
@@ -25,15 +25,14 @@ export const initialFlowState = {
25
25
  credential: null,
26
26
  address: null,
27
27
  };
28
+ /** `Object(x) === x` holds only for objects, so this rules out primitives without a `typeof`. */
29
+ const isObject = (value) => Object(value) === value;
28
30
  function shallowEqual(a, b) {
29
31
  if (a === b)
30
32
  return true;
31
- if (typeof a !== "object" ||
32
- typeof b !== "object" ||
33
- a === null ||
34
- b === null) {
33
+ if (!isObject(a) || !isObject(b))
35
34
  return false;
36
- }
35
+ /* SAFETY: `a` was just narrowed to an object, so its own keys are keys of T. */
37
36
  const ak = Object.keys(a);
38
37
  if (Object.keys(b).length !== ak.length)
39
38
  return false;
@@ -30,5 +30,7 @@ export declare function serializeCookie(name: string, value: string, opts: {
30
30
  export declare function clearCookieString(name: string, opts: {
31
31
  secure: boolean;
32
32
  }): string;
33
+ /** Cookie names to their values, as parsed off a request. */
34
+ export type CookieJar = Record<string, string>;
33
35
  /** Parses a Cookie request header into name → value; first occurrence wins (RFC 6265 practice). */
34
- export declare function parseCookieHeader(header: string | null | undefined): Record<string, string>;
36
+ export declare function parseCookieHeader(header: string | null | undefined): CookieJar;
@@ -49,17 +49,19 @@ export function clearCookieString(name, opts) {
49
49
  }
50
50
  /** Parses a Cookie request header into name → value; first occurrence wins (RFC 6265 practice). */
51
51
  export function parseCookieHeader(header) {
52
- const out = {};
53
52
  if (!header)
54
- return out;
55
- for (const part of header.split(";")) {
53
+ return {};
54
+ const seen = new Set();
55
+ const entries = header.split(";").flatMap((part) => {
56
56
  const eq = part.indexOf("=");
57
57
  if (eq === -1)
58
- continue;
58
+ return [];
59
59
  const name = part.slice(0, eq).trim();
60
- if (!name || name in out)
61
- continue;
62
- out[name] = part.slice(eq + 1).trim();
63
- }
64
- return out;
60
+ // First occurrence wins (RFC 6265 practice), so a repeat is dropped.
61
+ if (!name || seen.has(name))
62
+ return [];
63
+ seen.add(name);
64
+ return [[name, part.slice(eq + 1).trim()]];
65
+ });
66
+ return Object.fromEntries(entries);
65
67
  }
@@ -20,9 +20,4 @@ export declare function fromWindow(w: Window, options?: FromWindowOptions): Mess
20
20
  * dropped against an iframe that hasn't booted yet.
21
21
  */
22
22
  export declare function bridge(parameters: BridgeParameters): Bridge;
23
- /**
24
- * No-op bridge used in non-browser environments (SSR, tests, Node) where
25
- * `window` doesn't exist. All sends resolve to undefined and never deliver.
26
- * Lets the SDK be imported from anywhere without crashing on module load.
27
- */
28
23
  export declare function noop(): Bridge;