@flow-industries/id 0.2.0 → 0.2.5

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 CHANGED
@@ -1,255 +1,33 @@
1
1
  # Flow ID
2
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.
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
5
  **npm:** [`@flow-industries/id`](https://www.npmjs.com/package/@flow-industries/id)
6
6
 
7
7
  ## Installation
8
8
 
9
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
- ])
10
+ bun add @flow-industries/id viem
189
11
  ```
190
12
 
191
- **Check session:**
13
+ For Wagmi apps:
192
14
 
193
- ```ts
194
- const res = await fetch("https://id.flow.industries/api/me", {
195
- credentials: "include",
196
- })
197
- const { session } = await res.json()
15
+ ```bash
16
+ bun add @flow-industries/id wagmi viem @tanstack/react-query
198
17
  ```
199
18
 
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. |
19
+ ## Documentation
221
20
 
222
- ### `createDialogHost(options)`
21
+ Full documentation at **[docs.flow.industries/en/auth](https://docs.flow.industries/en/auth)**.
223
22
 
224
- Creates a direct dialog interface.
23
+ - [Getting started](https://docs.flow.industries/en/auth/getting-started) - install the SDK and ship a login button
24
+ - [Concepts](https://docs.flow.industries/en/auth/concepts) - passkeys, audience-bound JWTs, sessions, and access keys
25
+ - [SDK](https://docs.flow.industries/en/auth/sdk/create-flow) - `createFlow`, React hooks, Wagmi connector, and direct dialog control
26
+ - [Wagmi](https://docs.flow.industries/en/auth/sdk/wagmi) - use Flow ID as a Wagmi connector
27
+ - [Signing](https://docs.flow.industries/en/auth/signing) - messages, typed data, transactions, and batch calls
28
+ - [JWT verify](https://docs.flow.industries/en/auth/jwt-verify) - verify Flow sessions on your backend
29
+ - [API reference](https://docs.flow.industries/en/auth/api) and [self-hosting](https://docs.flow.industries/en/auth/self-hosting)
225
30
 
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
- ```
31
+ ## License
254
32
 
255
- Playground at `http://localhost:5176` — has Wagmi and Direct integration demos with Tempo testnet support (faucet, transfers, fee sponsorship).
33
+ MIT
@@ -25,6 +25,18 @@ export declare function prepareAccessKey(options: ResolvedAccessKeyOptions, chai
25
25
  */
26
26
  export declare function finalizeAccessKey(params: FinalizeAccessKeyParams): Promise<void>;
27
27
  export declare function loadAccessKey(address: Hex): Promise<StoredAccessKey | undefined>;
28
+ /**
29
+ * Atomically reads and clears the pending KeyAuthorization for an address.
30
+ * Called by the chain-level prepareTransactionRequest hook — first
31
+ * invocation returns the auth so it rides with the outbound tx, subsequent
32
+ * invocations return null so we don't re-attach.
33
+ *
34
+ * Removing the field from storage is what marks the access key as
35
+ * on-chain-registered. We don't wait for a receipt because the auth is
36
+ * consumed at consensus, not execution — the keychain entry persists even
37
+ * if the call inside the tx reverts.
38
+ */
39
+ export declare function consumePendingAuthorization(address: Hex): Promise<unknown | null>;
28
40
  export declare function isExpired(stored: StoredAccessKey): boolean;
29
41
  export declare function clearAccessKey(address: Hex): Promise<void>;
30
42
  /**
@@ -26,7 +26,7 @@ export async function prepareAccessKey(options, chainId) {
26
26
  type: "p256",
27
27
  });
28
28
  const accessKeyHash = KeyAuthorization.getSignPayload(keyAuthUnsigned);
29
- return { keyPair, keyAuthUnsigned, accessKeyHash };
29
+ return { keyPair, keyAuthUnsigned, accessKeyHash, expiry: options.expiry };
30
30
  }
31
31
  /**
32
32
  * Step 2 — runs AFTER the dialog returns the WebAuthn signature.
@@ -60,6 +60,7 @@ export async function finalizeAccessKey(params) {
60
60
  const stored = {
61
61
  privateKey: preparation.keyPair.privateKey,
62
62
  publicKey: preparation.keyPair.publicKey,
63
+ expiry: preparation.expiry,
63
64
  keyAuthorization,
64
65
  };
65
66
  await idb.set(accessKeyStorageKey(address), stored);
@@ -67,11 +68,34 @@ export async function finalizeAccessKey(params) {
67
68
  export async function loadAccessKey(address) {
68
69
  return idb.get(accessKeyStorageKey(address));
69
70
  }
71
+ /**
72
+ * Atomically reads and clears the pending KeyAuthorization for an address.
73
+ * Called by the chain-level prepareTransactionRequest hook — first
74
+ * invocation returns the auth so it rides with the outbound tx, subsequent
75
+ * invocations return null so we don't re-attach.
76
+ *
77
+ * Removing the field from storage is what marks the access key as
78
+ * on-chain-registered. We don't wait for a receipt because the auth is
79
+ * consumed at consensus, not execution — the keychain entry persists even
80
+ * if the call inside the tx reverts.
81
+ */
82
+ export async function consumePendingAuthorization(address) {
83
+ const stored = await loadAccessKey(address);
84
+ if (!stored?.keyAuthorization)
85
+ return null;
86
+ const auth = stored.keyAuthorization;
87
+ await idb.set(accessKeyStorageKey(address), {
88
+ ...stored,
89
+ keyAuthorization: undefined,
90
+ });
91
+ return auth;
92
+ }
70
93
  export function isExpired(stored) {
71
94
  const auth = stored.keyAuthorization;
72
- if (!auth?.expiry)
95
+ const expiry = stored.expiry ?? auth?.expiry;
96
+ if (!expiry)
73
97
  return false;
74
- return auth.expiry < Date.now() / 1000;
98
+ return expiry < Date.now() / 1000;
75
99
  }
76
100
  export async function clearAccessKey(address) {
77
101
  await idb.delete(accessKeyStorageKey(address));
@@ -43,6 +43,15 @@ function resolveAccessKey(input) {
43
43
  * bundle.
44
44
  */
45
45
  export function createFlow(options = {}) {
46
+ // Idempotent: subsequent calls return the existing singleton instead of
47
+ // replacing it. React StrictMode (effects fire twice) and HMR (module
48
+ // re-eval) both re-invoke createFlow with the same options — replacing
49
+ // would orphan the first instance's store subscriptions, dialog iframe,
50
+ // and IDB writes while components/hooks that captured it keep reading
51
+ // stale state. For genuine multi-instance scenarios (tests), call
52
+ // resetFlow() first or pass explicit Flow instances.
53
+ if (currentFlow)
54
+ return currentFlow;
46
55
  const host = (options.host ?? DEFAULT_HOST).replace(/\/+$/, "");
47
56
  const dialogUrl = `${host}/dialog/`;
48
57
  const chains = options.chains ?? [];
@@ -51,7 +60,7 @@ export function createFlow(options = {}) {
51
60
  return chains[0];
52
61
  return chains.find((c) => c.id === chainId);
53
62
  };
54
- const transports = {};
63
+ const transports = options.transports ?? {};
55
64
  const getTransport = (chainId) => {
56
65
  return transports[chainId];
57
66
  };
@@ -219,10 +228,6 @@ export function createFlow(options = {}) {
219
228
  return getDialog();
220
229
  },
221
230
  };
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
231
  currentFlow = flow;
227
232
  return flow;
228
233
  }
@@ -37,9 +37,12 @@ export async function restoreCredential(store) {
37
37
  export async function runLogin(params) {
38
38
  const { dialog, store, options, extraCapabilities } = params;
39
39
  const lastCredential = await idb.get(LAST_CREDENTIAL_KEY);
40
+ const signUp = Boolean(options?.signUp);
40
41
  const capabilities = {
41
- ...(options?.signUp ? { createAccount: true } : {}),
42
- ...(lastCredential && !options?.signUp
42
+ ...(signUp ? { createAccount: true } : {}),
43
+ ...(!signUp && options?.signIn ? { signIn: true } : {}),
44
+ ...(!signUp && options?.headless ? { headless: true } : {}),
45
+ ...(lastCredential && !signUp
43
46
  ? { credentialId: lastCredential.id }
44
47
  : {}),
45
48
  ...(extraCapabilities ?? {}),
@@ -3,9 +3,9 @@ import type { SendCallsArgs, SendTransactionArgs, SignMessageArgs, SignTypedData
3
3
  /**
4
4
  * Constructs a viem WalletClient bound to the user's Flow account. Wraps the
5
5
  * transport in `walletNamespaceCompat` so Tempo's wallet RPC namespace is
6
- * available alongside the standard JSON-RPC methods. Used by every signing
7
- * helper below; consumers can also call this directly via `flow.walletClient()`
8
- * if they want to drive viem actions themselves.
6
+ * available alongside the standard JSON-RPC methods, and extends the chain
7
+ * so a freshly-provisioned access key's KeyAuthorization rides with its
8
+ * first outbound tx (see `withAccessKeyAuthorization`).
9
9
  */
10
10
  export declare function buildWalletClient(ctx: SigningContext, chainId?: number): Promise<WalletClient>;
11
11
  export declare function signMessage(ctx: SigningContext, args: SignMessageArgs): Promise<Hex>;
@@ -1,7 +1,7 @@
1
- import { createWalletClient, http, } from "viem";
1
+ import { createWalletClient, defineChain, http, } from "viem";
2
2
  import { sendCalls as viemSendCalls, sendTransaction as viemSendTransaction, signMessage as viemSignMessage, signTypedData as viemSignTypedData, } from "viem/actions";
3
3
  import { Account, walletNamespaceCompat } from "viem/tempo";
4
- import { buildAccessKeyAccount, clearAccessKey, isExpired, loadAccessKey, } from "./access-key";
4
+ import { buildAccessKeyAccount, clearAccessKey, consumePendingAuthorization, isExpired, loadAccessKey, } from "./access-key";
5
5
  function requireAuth(state) {
6
6
  if (!state.credential || !state.address) {
7
7
  throw new Error("Not authenticated — call flow.login() first");
@@ -30,12 +30,68 @@ async function resolveAccount(params) {
30
30
  }
31
31
  return buildAccessKeyAccount(stored, params.credential, params.rpId);
32
32
  }
33
+ /**
34
+ * Wraps a chain so its prepareTransactionRequest pipeline pulls the pending
35
+ * KeyAuthorization for `address` out of IDB and merges it into the outbound
36
+ * tx request. This is how Tempo's keychain learns about the access key —
37
+ * the auth rides with the first tx and the chain registers it at consensus.
38
+ *
39
+ * Doing this at the chain level (not the wallet client) means every viem
40
+ * tx-sending path picks it up uniformly: wagmi's writeContract, direct
41
+ * sendTransaction, raw sendRawTransaction. No wallet-client-method
42
+ * overrides, no per-call SDK wrapper logic.
43
+ *
44
+ * Mirrors the @wagmi/core/tempo webAuthn connector pattern.
45
+ */
46
+ function withAccessKeyAuthorization(chain, address) {
47
+ const inheritedPrepare = chain.prepareTransactionRequest;
48
+ const hook = async (args, { phase }) => {
49
+ const argsWithAuth = args;
50
+ // Preserve auth threaded by an earlier phase invocation; otherwise
51
+ // consume one from IDB. Atomic read+clear so re-runs don't
52
+ // re-attach an already-shipped auth.
53
+ const keyAuthorization = argsWithAuth.keyAuthorization ??
54
+ (await consumePendingAuthorization(address));
55
+ // Delegate to the chain's existing prepareTransactionRequest
56
+ // (Tempo's chainConfig adds gas adjustments based on signature type
57
+ // and handles expiring nonces — we'd break those without this).
58
+ const inheritedResult = await runInheritedPrepare(inheritedPrepare, args, phase);
59
+ return {
60
+ ...args,
61
+ ...inheritedResult,
62
+ ...(keyAuthorization ? { keyAuthorization } : {}),
63
+ };
64
+ };
65
+ return defineChain({
66
+ ...chain,
67
+ prepareTransactionRequest: [
68
+ hook,
69
+ {
70
+ runAt: [
71
+ "afterFillParameters",
72
+ "beforeFillParameters",
73
+ "beforeFillTransaction",
74
+ ],
75
+ },
76
+ ],
77
+ });
78
+ }
79
+ async function runInheritedPrepare(inherited, args, phase) {
80
+ if (!inherited)
81
+ return {};
82
+ const [fn, options] = typeof inherited === "function" ? [inherited, undefined] : inherited;
83
+ if (!fn)
84
+ return {};
85
+ if (options && !options.runAt.includes(phase))
86
+ return {};
87
+ return (await fn(args, { phase })) ?? {};
88
+ }
33
89
  /**
34
90
  * Constructs a viem WalletClient bound to the user's Flow account. Wraps the
35
91
  * transport in `walletNamespaceCompat` so Tempo's wallet RPC namespace is
36
- * available alongside the standard JSON-RPC methods. Used by every signing
37
- * helper below; consumers can also call this directly via `flow.walletClient()`
38
- * if they want to drive viem actions themselves.
92
+ * available alongside the standard JSON-RPC methods, and extends the chain
93
+ * so a freshly-provisioned access key's KeyAuthorization rides with its
94
+ * first outbound tx (see `withAccessKeyAuthorization`).
39
95
  */
40
96
  export async function buildWalletClient(ctx, chainId) {
41
97
  const { credential, address } = requireAuth(ctx.getState());
@@ -51,7 +107,7 @@ export async function buildWalletClient(ctx, chainId) {
51
107
  const transport = ctx.getTransport(chain.id) ?? http();
52
108
  return createWalletClient({
53
109
  account,
54
- chain,
110
+ chain: withAccessKeyAuthorization(chain, address),
55
111
  transport: walletNamespaceCompat(transport, { account }),
56
112
  });
57
113
  }
@@ -1,5 +1,5 @@
1
1
  export type { AuthConfig, AuthResponse, AuthResponseWithWebAuthn, FlowCredential, FlowUser, PasskeyPluginOptions, VerifiedFlowJWT, VerifyOptions, WebAuthnSignature, } from "./auth";
2
- export type { AccessKeyOptions, AccessKeyPreparation, Address, CreateDialogHostOptions, CreateFlowOptions, DialogHost, DialogOpenOptions, FinalizeAccessKeyParams, Flow, FlowIdProviderProps, FlowConnectorParameters, FlowState, Listener, LoginOptions, ResolveAccountParams, ResolvedAccessKeyOptions, RootCredential, RunLoginParams, RunLoginResult, SendCallsArgs, SendTransactionArgs, Session, SignMessageArgs, SignTypedDataArgs, SigningContext, StoredAccessKey, Store, } from "./sdk";
2
+ export type { AccessKeyOptions, AccessKeyPreparation, Address, CreateDialogHostOptions, CreateFlowOptions, DialogHost, DialogOpenOptions, FinalizeAccessKeyParams, Flow, FlowIdProviderProps, FlowConnectorParameters, FlowState, Listener, LoginOptions, PrepareArgsWithAuth, PrepareTransactionRequestPhase, ResolveAccountParams, ResolvedAccessKeyOptions, RootCredential, RunLoginParams, RunLoginResult, SendCallsArgs, SendTransactionArgs, Session, SignMessageArgs, SignTypedDataArgs, SigningContext, StoredAccessKey, Store, WagmiConnectCapabilities, WagmiConnectParams, } from "./sdk";
3
3
  export { isPersonalSignParams, isSendCallsParams, isSendTransactionParams, } from "./protocol";
4
4
  export type { Call, ConnectCapabilities, ConnectRequest, ConnectResponse, MethodName, MethodParams, MethodResult, RestoreRequest, RestoreResponse, RpcRequest, SendCallsParams, SendCallsRequest, SendCallsResponse, SendTransactionParams, SendTransactionRequest, SendTransactionResponse, SignMessageRequest, SignMessageResponse, SignOutRequest, SignOutResponse, SignTypedDataRequest, SignTypedDataResponse, TransactionArgs, TypedData, TypedDataDomain, TypedDataField, } from "./protocol";
5
5
  export type { Bridge, BridgeParameters, FlowAccount, FlowRemote, FlowRemoteConfig, FromWindowOptions, MessageResponse, Messenger, OneOf, Payload, QueuedRequest, ReadyOptions, RemoteFlowState, RemoteState, Schema, Storage, Topic, WithReady, } from "./messenger";
@@ -1,9 +1,23 @@
1
- import type { Chain, Hex, SendTransactionParameters, SignTypedDataParameters, Transport, WalletClient } from "viem";
1
+ import type { Chain, Hex, PrepareTransactionRequestParameters, SendTransactionParameters, SignTypedDataParameters, Transport, WalletClient } from "viem";
2
2
  import type { SendCallsParameters } from "viem/actions";
3
3
  import type { ReactNode } from "react";
4
4
  import type { FlowCredential, FlowUser, WebAuthnSignature } from "./auth";
5
5
  import type { ConnectCapabilities, ConnectResponse } from "./protocol";
6
6
  export type Address = `0x${string}`;
7
+ /**
8
+ * Phases viem invokes a chain's `prepareTransactionRequest` hook at. Inlined
9
+ * here because viem doesn't export the type itself; we only need the union.
10
+ */
11
+ export type PrepareTransactionRequestPhase = "beforeFillTransaction" | "beforeFillParameters" | "afterFillParameters";
12
+ /**
13
+ * Tempo extends viem's `PrepareTransactionRequestParameters` with a
14
+ * `keyAuthorization` field that rides on the tx envelope. viem's base type
15
+ * doesn't know about it, so this widening lets the chain-level access-key
16
+ * injection hook read and write that field without opaque casts.
17
+ */
18
+ export type PrepareArgsWithAuth = PrepareTransactionRequestParameters & {
19
+ keyAuthorization?: unknown;
20
+ };
7
21
  export type CreateDialogHostOptions = {
8
22
  host: string;
9
23
  container?: HTMLElement;
@@ -19,7 +33,15 @@ export type ResolvedAccessKeyOptions = {
19
33
  export type StoredAccessKey = {
20
34
  privateKey: unknown;
21
35
  publicKey: unknown;
22
- keyAuthorization: unknown;
36
+ expiry: number;
37
+ /**
38
+ * Signed KeyAuthorization granting this access key permission to act for
39
+ * the parent address. Present until the first tx ships it on-chain — the
40
+ * chain-level prepareTransactionRequest hook reads and clears it so
41
+ * subsequent txs run without re-attaching. Absence of this field on a
42
+ * stored access key means it's already on-chain-registered.
43
+ */
44
+ keyAuthorization?: unknown;
23
45
  };
24
46
  export type AccessKeyPreparation = {
25
47
  keyPair: {
@@ -28,11 +50,13 @@ export type AccessKeyPreparation = {
28
50
  };
29
51
  keyAuthUnsigned: unknown;
30
52
  accessKeyHash: string;
53
+ expiry: number;
31
54
  };
32
55
  export type CreateFlowOptions = {
33
56
  host?: string;
34
57
  rpId?: string;
35
58
  chains?: readonly Chain[];
59
+ transports?: Partial<Record<number, Transport>>;
36
60
  accessKey?: boolean | AccessKeyOptions;
37
61
  /**
38
62
  * If true (default), createFlow attempts a silent restore on startup —
@@ -49,6 +73,8 @@ export type FlowState = {
49
73
  export type LoginOptions = {
50
74
  mode?: "iframe" | "popup";
51
75
  signUp?: boolean;
76
+ signIn?: boolean;
77
+ headless?: boolean;
52
78
  };
53
79
  export type Session = {
54
80
  user: FlowUser;
@@ -127,13 +153,21 @@ export type Flow = {
127
153
  getState(): FlowState;
128
154
  dialog: DialogHost;
129
155
  };
130
- export type FlowConnectorParameters = Omit<CreateFlowOptions, "chains"> & {
156
+ export type FlowConnectorParameters = Omit<CreateFlowOptions, "chains" | "transports"> & {
131
157
  /**
132
158
  * Pass an existing Flow instance so wagmi shares state with direct SDK usage.
133
159
  * If omitted, the connector creates its own internal Flow (separate state).
134
160
  */
135
161
  flow?: Flow;
136
162
  };
163
+ export type WagmiConnectCapabilities = ConnectCapabilities & {
164
+ type?: "sign-up" | "sign-in";
165
+ };
166
+ export type WagmiConnectParams = {
167
+ capabilities?: WagmiConnectCapabilities;
168
+ chainId?: number;
169
+ isReconnecting?: boolean;
170
+ };
137
171
  export type FlowIdProviderProps = {
138
172
  /**
139
173
  * Explicit Flow instance to scope to descendants. Optional — if omitted,
@@ -2,6 +2,17 @@ import { createConnector } from "@wagmi/core";
2
2
  import { getAddress, SwitchChainError } from "viem";
3
3
  import { createFlow, getFlow as getSingletonFlow } from "../client/create-flow";
4
4
  flowConnector.type = "flow";
5
+ function loginOptionsFromCapabilities(capabilities) {
6
+ const signUp = capabilities?.type === "sign-up" || capabilities?.createAccount;
7
+ const signIn = capabilities?.type === "sign-in" || capabilities?.signIn;
8
+ return {
9
+ ...(signUp ? { signUp: true } : {}),
10
+ ...(!signUp && signIn ? { signIn: true } : {}),
11
+ ...(!signUp && (capabilities?.headless || capabilities?.type === "sign-in")
12
+ ? { headless: true }
13
+ : {}),
14
+ };
15
+ }
5
16
  /**
6
17
  * Wagmi connector adapter for Flow. Drop into your wagmi config with
7
18
  * `connectors: [flowConnector()]` — no arguments needed in the common case.
@@ -35,6 +46,7 @@ export function flowConnector(parameters = {}) {
35
46
  flow = createFlow({
36
47
  ...rest,
37
48
  chains: [...config.chains],
49
+ transports: config.transports,
38
50
  });
39
51
  return flow;
40
52
  };
@@ -63,7 +75,8 @@ export function flowConnector(parameters = {}) {
63
75
  },
64
76
  async connect(params = {}) {
65
77
  const f = getFlow();
66
- const isReconnecting = params.isReconnecting;
78
+ const connectParams = params;
79
+ const isReconnecting = connectParams.isReconnecting;
67
80
  // Wagmi calls connect({ isReconnecting: true }) on mount when it
68
81
  // thinks there's a previous session. Without this branch we'd open
69
82
  // the dialog (full sign-in) on every page load. Silent restore
@@ -77,13 +90,13 @@ export function flowConnector(parameters = {}) {
77
90
  }
78
91
  }
79
92
  else {
80
- await f.login();
93
+ await f.login(loginOptionsFromCapabilities(connectParams.capabilities));
81
94
  }
82
95
  }
83
96
  const address = f.address;
84
97
  if (!address)
85
98
  throw new Error("Failed to connect");
86
- const chainId = params.chainId ?? config.chains[0]?.id;
99
+ const chainId = connectParams.chainId ?? config.chains[0]?.id;
87
100
  if (chainId == null) {
88
101
  throw new Error("Flow connector requires at least one chain to be configured");
89
102
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flow-industries/id",
3
- "version": "0.2.0",
3
+ "version": "0.2.5",
4
4
  "module": "src/index.ts",
5
5
  "type": "module",
6
6
  "sideEffects": false,