@flow-industries/id 0.2.0 → 0.2.6
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 +16 -238
- package/dist/sdk/client/access-key.d.ts +12 -0
- package/dist/sdk/client/access-key.js +27 -3
- package/dist/sdk/client/create-flow.js +10 -5
- package/dist/sdk/client/session.js +5 -2
- package/dist/sdk/client/signing.d.ts +3 -3
- package/dist/sdk/client/signing.js +62 -6
- package/dist/sdk/types/index.d.ts +1 -1
- package/dist/sdk/types/sdk.d.ts +37 -3
- package/dist/sdk/wagmi/index.js +16 -3
- package/package.json +11 -4
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
|
|
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
|
|
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
|
-
|
|
13
|
+
For Wagmi apps:
|
|
192
14
|
|
|
193
|
-
```
|
|
194
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
21
|
+
Full documentation at **[docs.flow.industries/en/auth](https://docs.flow.industries/en/auth)**.
|
|
223
22
|
|
|
224
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
+
const expiry = stored.expiry ?? auth?.expiry;
|
|
96
|
+
if (!expiry)
|
|
73
97
|
return false;
|
|
74
|
-
return
|
|
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
|
-
...(
|
|
42
|
-
...(
|
|
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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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";
|
package/dist/sdk/types/sdk.d.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|
package/dist/sdk/wagmi/index.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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,7 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flow-industries/id",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"
|
|
3
|
+
"version": "0.2.6",
|
|
4
|
+
"main": "./dist/sdk/client/index.js",
|
|
5
|
+
"module": "./dist/sdk/client/index.js",
|
|
6
|
+
"types": "./dist/sdk/client/index.d.ts",
|
|
5
7
|
"type": "module",
|
|
6
8
|
"sideEffects": false,
|
|
7
9
|
"publishConfig": {
|
|
@@ -56,8 +58,12 @@
|
|
|
56
58
|
"@wagmi/core": ">=3.0.0"
|
|
57
59
|
},
|
|
58
60
|
"peerDependenciesMeta": {
|
|
59
|
-
"react": {
|
|
60
|
-
|
|
61
|
+
"react": {
|
|
62
|
+
"optional": true
|
|
63
|
+
},
|
|
64
|
+
"@wagmi/core": {
|
|
65
|
+
"optional": true
|
|
66
|
+
}
|
|
61
67
|
},
|
|
62
68
|
"devDependencies": {
|
|
63
69
|
"@better-auth/cli": "^1.4.17",
|
|
@@ -75,6 +81,7 @@
|
|
|
75
81
|
"vite": "^7.3.1"
|
|
76
82
|
},
|
|
77
83
|
"dependencies": {
|
|
84
|
+
"@flow-industries/id": "^0.2.5",
|
|
78
85
|
"@flow-industries/ui": "^0.11.1",
|
|
79
86
|
"@tanstack/query-sync-storage-persister": "^5.90.22",
|
|
80
87
|
"@tanstack/react-query": "^5.90.20",
|