@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.
- package/README.md +255 -0
- package/dist/sdk/chains.d.ts +2 -0
- package/dist/sdk/chains.js +2 -0
- package/dist/sdk/client/access-key.d.ts +37 -0
- package/dist/sdk/client/access-key.js +97 -0
- package/dist/sdk/client/create-flow.d.ts +41 -0
- package/dist/sdk/client/create-flow.js +259 -0
- package/dist/sdk/client/dialog-host.d.ts +19 -0
- package/dist/sdk/client/dialog-host.js +192 -0
- package/dist/sdk/client/idb.d.ts +5 -0
- package/dist/sdk/client/idb.js +47 -0
- package/dist/sdk/client/index.d.ts +4 -0
- package/dist/sdk/client/index.js +3 -0
- package/dist/sdk/client/methods.d.ts +9 -0
- package/dist/sdk/client/methods.js +9 -0
- package/dist/sdk/client/protocol.d.ts +97 -0
- package/dist/sdk/client/protocol.js +9 -0
- package/dist/sdk/client/session.d.ts +45 -0
- package/dist/sdk/client/session.js +90 -0
- package/dist/sdk/client/signing.d.ts +16 -0
- package/dist/sdk/client/signing.js +91 -0
- package/dist/sdk/client/store.d.ts +3 -0
- package/dist/sdk/client/store.js +41 -0
- package/dist/sdk/client/types.d.ts +35 -0
- package/dist/sdk/client/types.js +0 -0
- package/dist/sdk/dialog/remote/Messenger.d.ts +29 -0
- package/dist/sdk/dialog/remote/Messenger.js +146 -0
- package/dist/sdk/react/hooks.d.ts +33 -0
- package/dist/sdk/react/hooks.js +51 -0
- package/dist/sdk/react/index.d.ts +3 -0
- package/dist/sdk/react/index.js +2 -0
- package/dist/sdk/react/provider.d.ts +12 -0
- package/dist/sdk/react/provider.js +15 -0
- package/dist/sdk/types/auth.d.ts +45 -0
- package/dist/sdk/types/auth.js +0 -0
- package/dist/sdk/types/dialog.d.ts +55 -0
- package/dist/sdk/types/dialog.js +0 -0
- package/dist/sdk/types/index.d.ts +7 -0
- package/dist/sdk/types/index.js +1 -0
- package/dist/sdk/types/messenger.d.ts +162 -0
- package/dist/sdk/types/messenger.js +0 -0
- package/dist/sdk/types/protocol.d.ts +114 -0
- package/dist/sdk/types/protocol.js +17 -0
- package/dist/sdk/types/sdk.d.ts +166 -0
- package/dist/sdk/types/sdk.js +0 -0
- package/dist/sdk/types/tx.d.ts +35 -0
- package/dist/sdk/types/tx.js +0 -0
- package/dist/sdk/types.d.ts +32 -0
- package/dist/sdk/types.js +0 -0
- package/dist/sdk/verify.d.ts +15 -0
- package/dist/sdk/verify.js +30 -0
- package/dist/sdk/wagmi/index.d.ts +25 -0
- package/dist/sdk/wagmi/index.js +139 -0
- 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,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 };
|