@flow-industries/id 0.2.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 (54) hide show
  1. package/README.md +255 -0
  2. package/dist/sdk/chains.d.ts +2 -0
  3. package/dist/sdk/chains.js +2 -0
  4. package/dist/sdk/client/access-key.d.ts +37 -0
  5. package/dist/sdk/client/access-key.js +97 -0
  6. package/dist/sdk/client/create-flow.d.ts +41 -0
  7. package/dist/sdk/client/create-flow.js +259 -0
  8. package/dist/sdk/client/dialog-host.d.ts +19 -0
  9. package/dist/sdk/client/dialog-host.js +192 -0
  10. package/dist/sdk/client/idb.d.ts +5 -0
  11. package/dist/sdk/client/idb.js +47 -0
  12. package/dist/sdk/client/index.d.ts +4 -0
  13. package/dist/sdk/client/index.js +3 -0
  14. package/dist/sdk/client/methods.d.ts +9 -0
  15. package/dist/sdk/client/methods.js +9 -0
  16. package/dist/sdk/client/protocol.d.ts +97 -0
  17. package/dist/sdk/client/protocol.js +9 -0
  18. package/dist/sdk/client/session.d.ts +45 -0
  19. package/dist/sdk/client/session.js +90 -0
  20. package/dist/sdk/client/signing.d.ts +16 -0
  21. package/dist/sdk/client/signing.js +91 -0
  22. package/dist/sdk/client/store.d.ts +3 -0
  23. package/dist/sdk/client/store.js +41 -0
  24. package/dist/sdk/client/types.d.ts +35 -0
  25. package/dist/sdk/client/types.js +0 -0
  26. package/dist/sdk/dialog/remote/Messenger.d.ts +29 -0
  27. package/dist/sdk/dialog/remote/Messenger.js +146 -0
  28. package/dist/sdk/react/hooks.d.ts +33 -0
  29. package/dist/sdk/react/hooks.js +51 -0
  30. package/dist/sdk/react/index.d.ts +3 -0
  31. package/dist/sdk/react/index.js +2 -0
  32. package/dist/sdk/react/provider.d.ts +12 -0
  33. package/dist/sdk/react/provider.js +15 -0
  34. package/dist/sdk/types/auth.d.ts +45 -0
  35. package/dist/sdk/types/auth.js +0 -0
  36. package/dist/sdk/types/dialog.d.ts +55 -0
  37. package/dist/sdk/types/dialog.js +0 -0
  38. package/dist/sdk/types/index.d.ts +7 -0
  39. package/dist/sdk/types/index.js +1 -0
  40. package/dist/sdk/types/messenger.d.ts +162 -0
  41. package/dist/sdk/types/messenger.js +0 -0
  42. package/dist/sdk/types/protocol.d.ts +114 -0
  43. package/dist/sdk/types/protocol.js +17 -0
  44. package/dist/sdk/types/sdk.d.ts +166 -0
  45. package/dist/sdk/types/sdk.js +0 -0
  46. package/dist/sdk/types/tx.d.ts +35 -0
  47. package/dist/sdk/types/tx.js +0 -0
  48. package/dist/sdk/types.d.ts +32 -0
  49. package/dist/sdk/types.js +0 -0
  50. package/dist/sdk/verify.d.ts +15 -0
  51. package/dist/sdk/verify.js +30 -0
  52. package/dist/sdk/wagmi/index.d.ts +25 -0
  53. package/dist/sdk/wagmi/index.js +139 -0
  54. package/package.json +99 -0
package/README.md ADDED
@@ -0,0 +1,255 @@
1
+ # Flow ID
2
+
3
+ Passkey-first identity for Flow applications. One passkey bound to `id.flow.industries`, usable across all Flow apps, with optional Tempo chain support.
4
+
5
+ **npm:** [`@flow-industries/id`](https://www.npmjs.com/package/@flow-industries/id)
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ bun add @flow-industries/id wagmi viem @tanstack/react-query
11
+ ```
12
+
13
+ ## Integration
14
+
15
+ ### Option 1: Wagmi connector
16
+
17
+ Best for apps that use wagmi/viem and want standard React hooks (`useAccount`, `useConnect`, `useSendTransaction`).
18
+
19
+ **Set up the config:**
20
+
21
+ ```ts
22
+ // config.ts
23
+ import { createConfig, http, createStorage, webSocket } from "wagmi"
24
+ import { tempoModerato } from "viem/chains"
25
+ import { withFeePayer } from "viem/tempo"
26
+ import { flow } from "@flow-industries/id"
27
+
28
+ const alphaUsd = "0x20c0000000000000000000000000000000000001"
29
+
30
+ export const config = createConfig({
31
+ chains: [tempoModerato.extend({ feeToken: alphaUsd })],
32
+ connectors: [
33
+ flow({
34
+ host: "https://id.flow.industries/dialog",
35
+ rpId: "id.flow.industries",
36
+ accessKey: true, // enables in-page tx signing without passkey prompts
37
+ }),
38
+ ],
39
+ storage: createStorage({ storage: localStorage }),
40
+ transports: {
41
+ [tempoModerato.id]: withFeePayer(
42
+ webSocket(), // regular transactions
43
+ http("/fee-payer"), // sponsored transactions
44
+ ),
45
+ },
46
+ })
47
+ ```
48
+
49
+ **Wrap your app:**
50
+
51
+ ```tsx
52
+ // main.tsx
53
+ import { WagmiProvider } from "wagmi"
54
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
55
+ import { config } from "./config"
56
+
57
+ const queryClient = new QueryClient()
58
+
59
+ createRoot(document.getElementById("root")!).render(
60
+ <WagmiProvider config={config}>
61
+ <QueryClientProvider client={queryClient}>
62
+ <App />
63
+ </QueryClientProvider>
64
+ </WagmiProvider>
65
+ )
66
+ ```
67
+
68
+ **Authentication:**
69
+
70
+ ```tsx
71
+ import { useAccount, useConnect, useConnectors, useDisconnect } from "wagmi"
72
+
73
+ function Auth() {
74
+ const { connect, isPending, error } = useConnect()
75
+ const [connector] = useConnectors()
76
+ const account = useAccount()
77
+ const { disconnect } = useDisconnect()
78
+
79
+ if (account.isConnected) {
80
+ return (
81
+ <div>
82
+ <p>Connected: {account.address}</p>
83
+ <button onClick={() => disconnect()}>Sign out</button>
84
+ </div>
85
+ )
86
+ }
87
+
88
+ return (
89
+ <div>
90
+ {/* Sign up — opens dialog with username + passkey creation */}
91
+ <button onClick={() => connect({
92
+ connector,
93
+ capabilities: { type: "sign-up" },
94
+ } as any)}>
95
+ Sign up
96
+ </button>
97
+
98
+ {/* Sign in — passkey prompt, no dialog UI */}
99
+ <button onClick={() => connect({
100
+ connector,
101
+ capabilities: { type: "sign-in" },
102
+ } as any)}>
103
+ Sign in
104
+ </button>
105
+
106
+ {/* Welcome screen — opens dialog, user chooses */}
107
+ <button onClick={() => connect({ connector })}>
108
+ Sign in with Flow
109
+ </button>
110
+
111
+ {error && <p>{error.message}</p>}
112
+ </div>
113
+ )
114
+ }
115
+ ```
116
+
117
+ **Transactions (Tempo):**
118
+
119
+ ```tsx
120
+ import { Hooks } from "wagmi/tempo"
121
+ import { Value } from "ox"
122
+
123
+ const alphaUsd = "0x20c0000000000000000000000000000000000001"
124
+
125
+ function Transfer() {
126
+ const transfer = Hooks.token.useTransferSync()
127
+
128
+ return (
129
+ <button onClick={() => transfer.mutate({
130
+ to: "0x...",
131
+ token: alphaUsd,
132
+ amount: Value.from("10", 6),
133
+ })}>
134
+ Send 10 AlphaUSD
135
+ </button>
136
+ )
137
+ }
138
+ ```
139
+
140
+ With `accessKey: true`, transactions sign with an in-page access key — no passkey prompt per transaction. The access key is provisioned automatically during sign-up/sign-in.
141
+
142
+ **Sponsored transactions:**
143
+
144
+ ```tsx
145
+ transfer.mutate({
146
+ to: "0x...",
147
+ token: alphaUsd,
148
+ amount: Value.from("10", 6),
149
+ feePayer: true, // routes through fee payer relay
150
+ })
151
+ ```
152
+
153
+ ### Option 2: Direct dialog host
154
+
155
+ Best for apps that don't use wagmi, or want full control over the dialog lifecycle.
156
+
157
+ ```ts
158
+ import { createDialogHost } from "@flow-industries/id"
159
+
160
+ const dialog = createDialogHost({
161
+ host: "https://id.flow.industries/dialog",
162
+ })
163
+ ```
164
+
165
+ **Sign up:**
166
+
167
+ ```ts
168
+ const result = await dialog.request("wallet_connect", [
169
+ { capabilities: { createAccount: true } },
170
+ ])
171
+ // result: { user: { id, username }, credential: { id, publicKey } }
172
+ ```
173
+
174
+ **Sign in:**
175
+
176
+ ```ts
177
+ const result = await dialog.request("wallet_connect", [
178
+ { capabilities: { signIn: true } },
179
+ ])
180
+ // result: { user: { id, username }, credential: { id, publicKey } }
181
+ ```
182
+
183
+ **Welcome screen** (user chooses sign up or sign in):
184
+
185
+ ```ts
186
+ const result = await dialog.request("wallet_connect", [
187
+ { capabilities: {} },
188
+ ])
189
+ ```
190
+
191
+ **Check session:**
192
+
193
+ ```ts
194
+ const res = await fetch("https://id.flow.industries/api/me", {
195
+ credentials: "include",
196
+ })
197
+ const { session } = await res.json()
198
+ ```
199
+
200
+ ## How it works
201
+
202
+ 1. Your app opens the Flow ID dialog (iframe at `id.flow.industries`)
203
+ 2. The dialog handles passkey creation/authentication + username onboarding
204
+ 3. A session cookie is set on `id.flow.industries`
205
+ 4. The credential (id + publicKey) is returned to your app
206
+ 5. For Tempo chain apps: an access key is provisioned for in-page transaction signing
207
+
208
+ The passkey is bound to `id.flow.industries` via WebAuthn's rpId, so the same passkey works across all Flow apps (`flow.game`, `flow.talk`, etc.) through the shared dialog.
209
+
210
+ ## API
211
+
212
+ ### `flow(options)`
213
+
214
+ Creates a wagmi connector.
215
+
216
+ | Option | Type | Description |
217
+ |---|---|---|
218
+ | `host` | `string` | Dialog URL (e.g. `https://id.flow.industries/dialog`) |
219
+ | `rpId` | `string?` | WebAuthn relying party ID (e.g. `id.flow.industries`) |
220
+ | `accessKey` | `boolean \| { expiry?: number; strict?: boolean }` | Enable Tempo access key for in-page signing. Default expiry: 24h. |
221
+
222
+ ### `createDialogHost(options)`
223
+
224
+ Creates a direct dialog interface.
225
+
226
+ | Option | Type | Description |
227
+ |---|---|---|
228
+ | `host` | `string` | Dialog URL |
229
+ | `container` | `HTMLElement?` | DOM element to attach iframe to (default: `document.body`) |
230
+
231
+ Returns: `{ open, close, destroy, request, messenger }`
232
+
233
+ ## Server endpoints
234
+
235
+ | Endpoint | Description |
236
+ |---|---|
237
+ | `GET /api/config` | Returns `{ rpId, rpName }` |
238
+ | `GET /api/me` | Current session/user |
239
+ | `POST /fee-payer` | Tempo fee sponsorship relay |
240
+ | `GET /keys/challenge` | Generate WebAuthn challenge |
241
+ | `GET /keys/:credentialId` | Get stored public key |
242
+ | `POST /keys/:credentialId` | Store public key |
243
+ | `POST /api/auth/passkey/register` | Create user + passkey + session |
244
+ | `POST /api/auth/passkey/challenge` | Generate sign-in challenge |
245
+ | `POST /api/auth/passkey/verify` | Verify passkey + create session |
246
+
247
+ ## Development
248
+
249
+ ```bash
250
+ bun install
251
+ bun run db:push # create/update database tables
252
+ bun run dev # starts server (:3000) + dialog (:5175) + playground (:5176)
253
+ ```
254
+
255
+ Playground at `http://localhost:5176` — has Wagmi and Direct integration demos with Tempo testnet support (faucet, transfers, fee sponsorship).
@@ -0,0 +1,2 @@
1
+ export { tempo, tempoModerato } from "viem/chains";
2
+ export { withFeePayer } from "viem/tempo";
@@ -0,0 +1,2 @@
1
+ export { tempo, tempoModerato } from "viem/chains";
2
+ export { withFeePayer } from "viem/tempo";
@@ -0,0 +1,37 @@
1
+ import { Account } from "viem/tempo";
2
+ import type { AccessKeyPreparation, Address as Hex, FinalizeAccessKeyParams, FlowCredential, ResolvedAccessKeyOptions, StoredAccessKey } from "../types";
3
+ /**
4
+ * Step 1 of access-key creation — runs BEFORE the dialog opens.
5
+ *
6
+ * Generates an ephemeral P256 key pair (lives in non-extractable WebCrypto)
7
+ * and builds an unsigned KeyAuthorization that grants this key permission to
8
+ * sign on behalf of the root account until `expiry`. Returns the key pair
9
+ * plus a signing payload (`accessKeyHash`) that gets passed into the dialog
10
+ * so the same WebAuthn ceremony that signs the user in also signs the
11
+ * authorization — saving a second tap.
12
+ *
13
+ * Pairs with `finalizeAccessKey()` after the dialog returns the WebAuthn
14
+ * signature.
15
+ */
16
+ export declare function prepareAccessKey(options: ResolvedAccessKeyOptions, chainId: number): Promise<AccessKeyPreparation>;
17
+ /**
18
+ * Step 2 — runs AFTER the dialog returns the WebAuthn signature.
19
+ *
20
+ * Wraps the passkey signature in a SignatureEnvelope, attaches it to the
21
+ * unsigned KeyAuthorization from `prepareAccessKey`, and persists the
22
+ * complete (now-signed) access key in IndexedDB keyed by wallet address.
23
+ * Subsequent signing calls load this and use the access key directly,
24
+ * bypassing the passkey UI.
25
+ */
26
+ export declare function finalizeAccessKey(params: FinalizeAccessKeyParams): Promise<void>;
27
+ export declare function loadAccessKey(address: Hex): Promise<StoredAccessKey | undefined>;
28
+ export declare function isExpired(stored: StoredAccessKey): boolean;
29
+ export declare function clearAccessKey(address: Hex): Promise<void>;
30
+ /**
31
+ * Constructs a viem/tempo Account that signs with the access key but
32
+ * authorizes against the root passkey credential. The runtime layer uses
33
+ * this to mint transactions without invoking WebAuthn — the `access` field
34
+ * carries the passkey-signed authorization that proves the access key is
35
+ * allowed to act for the address.
36
+ */
37
+ export declare function buildAccessKeyAccount(stored: StoredAccessKey, rootCredential: FlowCredential, rpId?: string): ReturnType<typeof Account.fromWebCryptoP256>;
@@ -0,0 +1,97 @@
1
+ import { Account, WebCryptoP256 } from "viem/tempo";
2
+ import { KeyAuthorization, SignatureEnvelope } from "ox/tempo";
3
+ import * as Address from "ox/Address";
4
+ import * as PublicKey from "ox/PublicKey";
5
+ import { idb } from "./idb";
6
+ /**
7
+ * Step 1 of access-key creation — runs BEFORE the dialog opens.
8
+ *
9
+ * Generates an ephemeral P256 key pair (lives in non-extractable WebCrypto)
10
+ * and builds an unsigned KeyAuthorization that grants this key permission to
11
+ * sign on behalf of the root account until `expiry`. Returns the key pair
12
+ * plus a signing payload (`accessKeyHash`) that gets passed into the dialog
13
+ * so the same WebAuthn ceremony that signs the user in also signs the
14
+ * authorization — saving a second tap.
15
+ *
16
+ * Pairs with `finalizeAccessKey()` after the dialog returns the WebAuthn
17
+ * signature.
18
+ */
19
+ export async function prepareAccessKey(options, chainId) {
20
+ const keyPair = await WebCryptoP256.createKeyPair();
21
+ const accessKeyAddress = Address.fromPublicKey(keyPair.publicKey);
22
+ const keyAuthUnsigned = KeyAuthorization.from({
23
+ address: accessKeyAddress,
24
+ chainId: BigInt(chainId),
25
+ expiry: options.expiry,
26
+ type: "p256",
27
+ });
28
+ const accessKeyHash = KeyAuthorization.getSignPayload(keyAuthUnsigned);
29
+ return { keyPair, keyAuthUnsigned, accessKeyHash };
30
+ }
31
+ /**
32
+ * Step 2 — runs AFTER the dialog returns the WebAuthn signature.
33
+ *
34
+ * Wraps the passkey signature in a SignatureEnvelope, attaches it to the
35
+ * unsigned KeyAuthorization from `prepareAccessKey`, and persists the
36
+ * complete (now-signed) access key in IndexedDB keyed by wallet address.
37
+ * Subsequent signing calls load this and use the access key directly,
38
+ * bypassing the passkey UI.
39
+ */
40
+ export async function finalizeAccessKey(params) {
41
+ const { address, credential, webauthn, preparation } = params;
42
+ const signatureEnvelope = SignatureEnvelope.from({
43
+ metadata: {
44
+ authenticatorData: webauthn.metadata.authenticatorData,
45
+ clientDataJSON: webauthn.metadata.clientDataJSON,
46
+ challengeIndex: webauthn.metadata.challengeIndex,
47
+ typeIndex: webauthn.metadata.typeIndex,
48
+ },
49
+ signature: {
50
+ r: BigInt(webauthn.signature.r),
51
+ s: BigInt(webauthn.signature.s),
52
+ },
53
+ publicKey: PublicKey.from(`0x${credential.publicKey.replace(/^0x/, "")}`),
54
+ type: "webAuthn",
55
+ });
56
+ const keyAuthorization = KeyAuthorization.from({
57
+ ...preparation.keyAuthUnsigned,
58
+ signature: signatureEnvelope,
59
+ });
60
+ const stored = {
61
+ privateKey: preparation.keyPair.privateKey,
62
+ publicKey: preparation.keyPair.publicKey,
63
+ keyAuthorization,
64
+ };
65
+ await idb.set(accessKeyStorageKey(address), stored);
66
+ }
67
+ export async function loadAccessKey(address) {
68
+ return idb.get(accessKeyStorageKey(address));
69
+ }
70
+ export function isExpired(stored) {
71
+ const auth = stored.keyAuthorization;
72
+ if (!auth?.expiry)
73
+ return false;
74
+ return auth.expiry < Date.now() / 1000;
75
+ }
76
+ export async function clearAccessKey(address) {
77
+ await idb.delete(accessKeyStorageKey(address));
78
+ }
79
+ /**
80
+ * Constructs a viem/tempo Account that signs with the access key but
81
+ * authorizes against the root passkey credential. The runtime layer uses
82
+ * this to mint transactions without invoking WebAuthn — the `access` field
83
+ * carries the passkey-signed authorization that proves the access key is
84
+ * allowed to act for the address.
85
+ */
86
+ export function buildAccessKeyAccount(stored, rootCredential, rpId) {
87
+ const rootAccount = Account.fromWebAuthnP256(rootCredential, {
88
+ ...(rpId ? { rpId } : {}),
89
+ });
90
+ return Account.fromWebCryptoP256({
91
+ privateKey: stored.privateKey,
92
+ publicKey: stored.publicKey,
93
+ }, { access: rootAccount });
94
+ }
95
+ function accessKeyStorageKey(address) {
96
+ return `accessKey:${address.toLowerCase()}`;
97
+ }
@@ -0,0 +1,41 @@
1
+ import type { CreateFlowOptions, Flow } from "../types";
2
+ import { credentialToAddress } from "./session";
3
+ /**
4
+ * Creates the Flow SDK instance — the primary entry point for consumer apps.
5
+ *
6
+ * The instance owns:
7
+ * - A reactive store for `{ user, jwt, credential, address }`
8
+ * - A lazily-constructed dialog host (iframe to `id.flow.industries`)
9
+ * - Bound methods for login/logout/restore and signing
10
+ *
11
+ * On construction it kicks off two async tasks: rehydrating the credential
12
+ * from IndexedDB (so signing works even before the JWT is refreshed) and,
13
+ * unless `autoRestore: false`, attempting a silent JWT refresh through the
14
+ * hidden dialog iframe. Both run in the background; consumers can subscribe
15
+ * to state changes via `flow.subscribe`.
16
+ *
17
+ * Most signing methods dynamically import their implementation modules so
18
+ * apps that only use identity (no chain ops) don't pay for the viem/tempo
19
+ * bundle.
20
+ */
21
+ export declare function createFlow(options?: CreateFlowOptions): Flow;
22
+ /**
23
+ * Returns the current Flow singleton, or `null` if `createFlow()` hasn't
24
+ * been called yet. The singleton is set automatically by `createFlow()` —
25
+ * this is the fallback that lets `flowConnector()`, `<FlowIdProvider>`,
26
+ * and the React hooks "just work" without consumers having to thread the
27
+ * Flow instance through every integration point.
28
+ */
29
+ export declare function getFlow(): Flow | null;
30
+ /**
31
+ * Returns the current Flow singleton or throws if uninitialized. Use when
32
+ * you need the instance and `createFlow()` was definitely supposed to have
33
+ * been called by now (e.g., inside a React hook).
34
+ */
35
+ export declare function requireFlow(): Flow;
36
+ /**
37
+ * Clears the singleton. Intended for tests; production code shouldn't
38
+ * call this.
39
+ */
40
+ export declare function resetFlow(): void;
41
+ export { credentialToAddress };
@@ -0,0 +1,259 @@
1
+ import { createDialogHost } from "./dialog-host";
2
+ import { idb } from "./idb";
3
+ import { METHODS } from "./methods";
4
+ import { credentialToAddress, restoreCredential, runLogin, runLogout, } from "./session";
5
+ import { createStore, initialFlowState } from "./store";
6
+ const DEFAULT_HOST = "https://id.flow.industries";
7
+ /**
8
+ * Normalizes user-supplied access-key configuration into a fully-resolved
9
+ * shape. Accepts `true` for defaults, a partial options object, or omitted
10
+ * (returns undefined to disable access keys entirely).
11
+ *
12
+ * Default expiry is 24h from now (Unix seconds), `strict: false` so the SDK
13
+ * silently falls back to the root passkey if the access key is missing or
14
+ * expired rather than erroring.
15
+ */
16
+ function resolveAccessKey(input) {
17
+ if (!input)
18
+ return undefined;
19
+ const expiry = Math.floor((Date.now() + 24 * 60 * 60 * 1000) / 1000);
20
+ if (input === true)
21
+ return { expiry, strict: false };
22
+ return {
23
+ expiry: input.expiry ?? expiry,
24
+ strict: input.strict ?? false,
25
+ };
26
+ }
27
+ /**
28
+ * Creates the Flow SDK instance — the primary entry point for consumer apps.
29
+ *
30
+ * The instance owns:
31
+ * - A reactive store for `{ user, jwt, credential, address }`
32
+ * - A lazily-constructed dialog host (iframe to `id.flow.industries`)
33
+ * - Bound methods for login/logout/restore and signing
34
+ *
35
+ * On construction it kicks off two async tasks: rehydrating the credential
36
+ * from IndexedDB (so signing works even before the JWT is refreshed) and,
37
+ * unless `autoRestore: false`, attempting a silent JWT refresh through the
38
+ * hidden dialog iframe. Both run in the background; consumers can subscribe
39
+ * to state changes via `flow.subscribe`.
40
+ *
41
+ * Most signing methods dynamically import their implementation modules so
42
+ * apps that only use identity (no chain ops) don't pay for the viem/tempo
43
+ * bundle.
44
+ */
45
+ export function createFlow(options = {}) {
46
+ const host = (options.host ?? DEFAULT_HOST).replace(/\/+$/, "");
47
+ const dialogUrl = `${host}/dialog/`;
48
+ const chains = options.chains ?? [];
49
+ const getChain = (chainId) => {
50
+ if (chainId == null)
51
+ return chains[0];
52
+ return chains.find((c) => c.id === chainId);
53
+ };
54
+ const transports = {};
55
+ const getTransport = (chainId) => {
56
+ return transports[chainId];
57
+ };
58
+ const accessKeyOptions = resolveAccessKey(options.accessKey);
59
+ const store = createStore({ ...initialFlowState });
60
+ let dialog = null;
61
+ const getDialog = () => {
62
+ if (!dialog)
63
+ dialog = createDialogHost({ host: dialogUrl });
64
+ return dialog;
65
+ };
66
+ /**
67
+ * Mints a fresh JWT from the user's existing cookie session via the hidden
68
+ * dialog iframe. The iframe is first-party to id.flow.industries so the
69
+ * `flow_id.session_token` cookie is sent automatically — this is the only
70
+ * way to read the session from a third-party app (browsers block reading
71
+ * cross-origin cookies).
72
+ *
73
+ * Returns true if a session was found and state was populated, false if the
74
+ * user has no active session (in which case the caller should fall back to
75
+ * `flow.login()`). Never throws — restore failures are treated as "no session".
76
+ */
77
+ async function restore() {
78
+ const dialogHost = getDialog();
79
+ try {
80
+ const result = await dialogHost.requestSilent(METHODS.restore, []);
81
+ store.setState({
82
+ user: result.user,
83
+ jwt: result.jwt,
84
+ credential: result.credential,
85
+ address: result.address,
86
+ });
87
+ await idb.set("flow.activeCredential", result.credential);
88
+ await idb.set("flow.lastActiveCredential", result.credential);
89
+ return true;
90
+ }
91
+ catch {
92
+ return false;
93
+ }
94
+ }
95
+ // refreshJwt is semantically the same as restore — both mint a fresh JWT
96
+ // from the cookie session. We expose two names because consumers reach for
97
+ // one or the other based on intent (page-load vs near-expiry).
98
+ const refreshJwt = restore;
99
+ void (async () => {
100
+ await restoreCredential(store);
101
+ if (options.autoRestore !== false) {
102
+ await restore();
103
+ }
104
+ })();
105
+ function buildSigningContext() {
106
+ return {
107
+ getState: () => store.getSnapshot(),
108
+ getChain,
109
+ getTransport,
110
+ ...(options.rpId ? { rpId: options.rpId } : {}),
111
+ ...(accessKeyOptions?.strict ? { strict: accessKeyOptions.strict } : {}),
112
+ };
113
+ }
114
+ /**
115
+ * Opens the dialog iframe and runs the interactive sign-in or sign-up flow.
116
+ * Resolves with the resulting Session once the user completes authentication;
117
+ * rejects if the user cancels or the dialog fails.
118
+ *
119
+ * If access keys are enabled, this also derives an ephemeral P256 key pair
120
+ * locally and signs a KeyAuthorization with the passkey during the same
121
+ * WebAuthn ceremony — so future signing calls can use the access key without
122
+ * prompting for a passkey tap each time. The KeyAuthorization is finalized
123
+ * (persisted to IDB) only after the dialog confirms login succeeded.
124
+ */
125
+ async function login(loginOpts = {}) {
126
+ const dialogHost = getDialog();
127
+ let accessKeyModule;
128
+ let accessKeyPrep;
129
+ let extraCapabilities;
130
+ if (accessKeyOptions) {
131
+ const chain = getChain();
132
+ if (!chain) {
133
+ throw new Error("accessKey requires chains to be configured");
134
+ }
135
+ accessKeyModule = await import("./access-key");
136
+ accessKeyPrep = await accessKeyModule.prepareAccessKey(accessKeyOptions, chain.id);
137
+ extraCapabilities = { accessKeyHash: accessKeyPrep.accessKeyHash };
138
+ }
139
+ const { session, webauthn } = await runLogin({
140
+ dialog: dialogHost,
141
+ store,
142
+ options: loginOpts,
143
+ ...(extraCapabilities ? { extraCapabilities } : {}),
144
+ });
145
+ if (accessKeyModule && accessKeyPrep && webauthn) {
146
+ await accessKeyModule.finalizeAccessKey({
147
+ address: session.address,
148
+ credential: session.credential,
149
+ webauthn,
150
+ preparation: accessKeyPrep,
151
+ });
152
+ }
153
+ dialogHost.close();
154
+ return session;
155
+ }
156
+ /**
157
+ * Performs a full sign-out: tells the server to invalidate the cookie session
158
+ * (so other Flow apps can't silently restore it), clears local credential and
159
+ * access-key state, and resets the in-memory store.
160
+ *
161
+ * Server-side sign-out is best-effort — if the network call fails (e.g.,
162
+ * offline) we still clear local state so the UI reflects "signed out". The
163
+ * cookie will eventually expire on its own.
164
+ */
165
+ async function logout() {
166
+ const dialogHost = getDialog();
167
+ try {
168
+ await dialogHost.requestSilent(METHODS.signOut, []);
169
+ }
170
+ catch {
171
+ // Server-side sign-out failed (offline?) — still clear local state.
172
+ }
173
+ await runLogout(store);
174
+ dialog?.close();
175
+ }
176
+ const flow = {
177
+ get user() {
178
+ return store.getSnapshot().user;
179
+ },
180
+ get jwt() {
181
+ return store.getSnapshot().jwt;
182
+ },
183
+ get credential() {
184
+ return store.getSnapshot().credential;
185
+ },
186
+ get address() {
187
+ return store.getSnapshot().address;
188
+ },
189
+ get isAuthenticated() {
190
+ return store.getSnapshot().user !== null;
191
+ },
192
+ login,
193
+ logout,
194
+ restore,
195
+ refreshJwt,
196
+ signMessage: async (args) => {
197
+ const mod = await import("./signing");
198
+ return mod.signMessage(buildSigningContext(), args);
199
+ },
200
+ signTypedData: async (args) => {
201
+ const mod = await import("./signing");
202
+ return mod.signTypedData(buildSigningContext(), args);
203
+ },
204
+ sendTransaction: async (args) => {
205
+ const mod = await import("./signing");
206
+ return mod.sendTransaction(buildSigningContext(), args);
207
+ },
208
+ sendCalls: async (args) => {
209
+ const mod = await import("./signing");
210
+ return mod.sendCalls(buildSigningContext(), args);
211
+ },
212
+ walletClient: async (args) => {
213
+ const mod = await import("./signing");
214
+ return mod.buildWalletClient(buildSigningContext(), args?.chainId);
215
+ },
216
+ subscribe: store.subscribe,
217
+ getState: store.getSnapshot,
218
+ get dialog() {
219
+ return getDialog();
220
+ },
221
+ };
222
+ if (currentFlow && currentFlow !== flow) {
223
+ console.warn("[flow] createFlow() called more than once — replacing the previous singleton. " +
224
+ "Pass `flow` explicitly to wagmi/React if you need multiple instances.");
225
+ }
226
+ currentFlow = flow;
227
+ return flow;
228
+ }
229
+ let currentFlow = null;
230
+ /**
231
+ * Returns the current Flow singleton, or `null` if `createFlow()` hasn't
232
+ * been called yet. The singleton is set automatically by `createFlow()` —
233
+ * this is the fallback that lets `flowConnector()`, `<FlowIdProvider>`,
234
+ * and the React hooks "just work" without consumers having to thread the
235
+ * Flow instance through every integration point.
236
+ */
237
+ export function getFlow() {
238
+ return currentFlow;
239
+ }
240
+ /**
241
+ * Returns the current Flow singleton or throws if uninitialized. Use when
242
+ * you need the instance and `createFlow()` was definitely supposed to have
243
+ * been called by now (e.g., inside a React hook).
244
+ */
245
+ export function requireFlow() {
246
+ if (!currentFlow) {
247
+ throw new Error("Flow has not been initialized. Call createFlow() before using this API, " +
248
+ "or pass an explicit Flow instance.");
249
+ }
250
+ return currentFlow;
251
+ }
252
+ /**
253
+ * Clears the singleton. Intended for tests; production code shouldn't
254
+ * call this.
255
+ */
256
+ export function resetFlow() {
257
+ currentFlow = null;
258
+ }
259
+ export { credentialToAddress };