@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
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CreateDialogHostOptions, DialogHost } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* Creates a host that owns the dialog iframe (or popup) and exposes an RPC
|
|
4
|
+
* channel into the Flow auth dialog at `id.flow.industries`.
|
|
5
|
+
*
|
|
6
|
+
* The iframe is mounted lazily on first use, kept hidden until `open()` is
|
|
7
|
+
* called, and reused across requests so a single passkey-bound iframe handles
|
|
8
|
+
* login, signing, and silent restore. The iframe is required (not just
|
|
9
|
+
* `fetch`) because it's first-party to id.flow.industries and is the only way
|
|
10
|
+
* to read the cross-origin cookie session in third-party-cookie-blocking
|
|
11
|
+
* browsers (Safari, Firefox).
|
|
12
|
+
*
|
|
13
|
+
* Returned methods:
|
|
14
|
+
* - `request()` — open the dialog UI and dispatch an RPC (interactive flows)
|
|
15
|
+
* - `requestSilent()` — dispatch without showing the dialog (silent restore)
|
|
16
|
+
* - `open()`/`close()` — manual visibility control
|
|
17
|
+
* - `destroy()` — tear down the iframe and abort pending requests
|
|
18
|
+
*/
|
|
19
|
+
export declare function createDialogHost(options: CreateDialogHostOptions): DialogHost;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import * as Messenger from "../dialog/remote/Messenger";
|
|
2
|
+
const HIDDEN_STYLE = {
|
|
3
|
+
position: "fixed",
|
|
4
|
+
inset: "0",
|
|
5
|
+
width: "100%",
|
|
6
|
+
height: "100%",
|
|
7
|
+
border: "none",
|
|
8
|
+
zIndex: "2147483647",
|
|
9
|
+
display: "none",
|
|
10
|
+
pointerEvents: "none",
|
|
11
|
+
};
|
|
12
|
+
const VISIBLE_STYLE = {
|
|
13
|
+
display: "block",
|
|
14
|
+
pointerEvents: "auto",
|
|
15
|
+
background: "transparent",
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Creates a host that owns the dialog iframe (or popup) and exposes an RPC
|
|
19
|
+
* channel into the Flow auth dialog at `id.flow.industries`.
|
|
20
|
+
*
|
|
21
|
+
* The iframe is mounted lazily on first use, kept hidden until `open()` is
|
|
22
|
+
* called, and reused across requests so a single passkey-bound iframe handles
|
|
23
|
+
* login, signing, and silent restore. The iframe is required (not just
|
|
24
|
+
* `fetch`) because it's first-party to id.flow.industries and is the only way
|
|
25
|
+
* to read the cross-origin cookie session in third-party-cookie-blocking
|
|
26
|
+
* browsers (Safari, Firefox).
|
|
27
|
+
*
|
|
28
|
+
* Returned methods:
|
|
29
|
+
* - `request()` — open the dialog UI and dispatch an RPC (interactive flows)
|
|
30
|
+
* - `requestSilent()` — dispatch without showing the dialog (silent restore)
|
|
31
|
+
* - `open()`/`close()` — manual visibility control
|
|
32
|
+
* - `destroy()` — tear down the iframe and abort pending requests
|
|
33
|
+
*/
|
|
34
|
+
export function createDialogHost(options) {
|
|
35
|
+
const { host, container = document.body } = options;
|
|
36
|
+
let iframe = null;
|
|
37
|
+
let messenger = null;
|
|
38
|
+
const pending = new Map();
|
|
39
|
+
/**
|
|
40
|
+
* Mounts the iframe and wires up the postMessage bridge. Idempotent — safe
|
|
41
|
+
* to call before every request. The `allow` attribute grants WebAuthn and
|
|
42
|
+
* clipboard permissions to the dialog (required for passkey ceremonies).
|
|
43
|
+
*/
|
|
44
|
+
function ensureFrame() {
|
|
45
|
+
if (iframe)
|
|
46
|
+
return;
|
|
47
|
+
iframe = document.createElement("iframe");
|
|
48
|
+
iframe.src = `${host}`;
|
|
49
|
+
iframe.dataset.flowId = "";
|
|
50
|
+
iframe.allow = "publickey-credentials-create; publickey-credentials-get; clipboard-write";
|
|
51
|
+
Object.assign(iframe.style, HIDDEN_STYLE);
|
|
52
|
+
iframe.style.colorScheme = "normal";
|
|
53
|
+
container.appendChild(iframe);
|
|
54
|
+
messenger = Messenger.bridge({
|
|
55
|
+
from: Messenger.fromWindow(window),
|
|
56
|
+
to: Messenger.fromWindow(iframe.contentWindow),
|
|
57
|
+
waitForReady: true,
|
|
58
|
+
});
|
|
59
|
+
messenger.on("rpc-response", (payload) => {
|
|
60
|
+
const id = payload?.id;
|
|
61
|
+
const handler = pending.get(id);
|
|
62
|
+
if (!handler)
|
|
63
|
+
return;
|
|
64
|
+
pending.delete(id);
|
|
65
|
+
if (payload.error) {
|
|
66
|
+
handler.reject(new Error(payload.error.message ?? "Request failed"));
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
handler.resolve(payload.result);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
messenger.on("close", () => {
|
|
73
|
+
close();
|
|
74
|
+
});
|
|
75
|
+
messenger.on("__internal", (payload) => {
|
|
76
|
+
if (payload.type === "switch" && payload.mode === "popup") {
|
|
77
|
+
switchToPopup();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Replaces the iframe channel with a popup. Some browsers (older Safari,
|
|
83
|
+
* embedded webviews) refuse to run WebAuthn inside an iframe; popping out
|
|
84
|
+
* to a top-level window is the documented escape hatch.
|
|
85
|
+
*/
|
|
86
|
+
function switchToPopup() {
|
|
87
|
+
const popup = window.open(`${host}`, "flow-id", "width=400,height=600,popup=true");
|
|
88
|
+
if (!popup)
|
|
89
|
+
return;
|
|
90
|
+
if (messenger)
|
|
91
|
+
messenger.destroy();
|
|
92
|
+
messenger = Messenger.bridge({
|
|
93
|
+
from: Messenger.fromWindow(window),
|
|
94
|
+
to: Messenger.fromWindow(popup),
|
|
95
|
+
waitForReady: true,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
let initSent = false;
|
|
99
|
+
/**
|
|
100
|
+
* Sends the one-time init handshake to the dialog (referrer info, theme,
|
|
101
|
+
* mode). Guarded so reusing the iframe across requests doesn't re-init it
|
|
102
|
+
* — the dialog only expects this once.
|
|
103
|
+
*/
|
|
104
|
+
function sendInit(opts) {
|
|
105
|
+
if (initSent)
|
|
106
|
+
return;
|
|
107
|
+
initSent = true;
|
|
108
|
+
messenger?.send("__internal", {
|
|
109
|
+
type: "init",
|
|
110
|
+
mode: "iframe",
|
|
111
|
+
referrer: opts?.referrer ?? { title: document.title },
|
|
112
|
+
theme: opts?.theme,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
function show() {
|
|
116
|
+
if (!iframe)
|
|
117
|
+
return;
|
|
118
|
+
Object.assign(iframe.style, VISIBLE_STYLE);
|
|
119
|
+
}
|
|
120
|
+
function hide() {
|
|
121
|
+
if (!iframe)
|
|
122
|
+
return;
|
|
123
|
+
Object.assign(iframe.style, { display: "none", pointerEvents: "none" });
|
|
124
|
+
}
|
|
125
|
+
function open(opts) {
|
|
126
|
+
if (opts?.mode === "popup") {
|
|
127
|
+
switchToPopup();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
ensureFrame();
|
|
131
|
+
show();
|
|
132
|
+
sendInit(opts);
|
|
133
|
+
}
|
|
134
|
+
function close() {
|
|
135
|
+
hide();
|
|
136
|
+
pending.forEach((handler, id) => {
|
|
137
|
+
handler.reject(new Error("Dialog closed"));
|
|
138
|
+
pending.delete(id);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function destroy() {
|
|
142
|
+
close();
|
|
143
|
+
messenger?.destroy();
|
|
144
|
+
iframe?.remove();
|
|
145
|
+
iframe = null;
|
|
146
|
+
messenger = null;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Sends a JSON-RPC request to the dialog and registers a resolver keyed by
|
|
150
|
+
* request id. The matching `rpc-response` message in `ensureFrame()`
|
|
151
|
+
* dispatches the result back. Pending entries are rejected if the dialog
|
|
152
|
+
* is closed before responding.
|
|
153
|
+
*/
|
|
154
|
+
function dispatchRequest(method, params) {
|
|
155
|
+
const id = Math.floor(Math.random() * 1e9);
|
|
156
|
+
const rpcRequest = { id, method, params, jsonrpc: "2.0" };
|
|
157
|
+
return new Promise((resolve, reject) => {
|
|
158
|
+
pending.set(id, { resolve, reject });
|
|
159
|
+
messenger.send("rpc-requests", [
|
|
160
|
+
{ request: rpcRequest, status: "pending" },
|
|
161
|
+
]);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/** Opens the dialog UI and dispatches an interactive RPC (login, sign, etc.). */
|
|
165
|
+
async function request(method, params) {
|
|
166
|
+
ensureFrame();
|
|
167
|
+
open();
|
|
168
|
+
return dispatchRequest(method, params);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Dispatches an RPC without showing the dialog UI. Used for headless flows
|
|
172
|
+
* (silent restore, refresh, sign-out) where the dialog route runs invisibly
|
|
173
|
+
* and resolves immediately. The iframe is still mounted so postMessage
|
|
174
|
+
* works, but its `display` stays `none`.
|
|
175
|
+
*/
|
|
176
|
+
async function requestSilent(method, params) {
|
|
177
|
+
ensureFrame();
|
|
178
|
+
sendInit();
|
|
179
|
+
return dispatchRequest(method, params);
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
open,
|
|
183
|
+
close,
|
|
184
|
+
destroy,
|
|
185
|
+
request,
|
|
186
|
+
requestSilent,
|
|
187
|
+
get messenger() {
|
|
188
|
+
ensureFrame();
|
|
189
|
+
return messenger;
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
let defaultGetStoreFunc;
|
|
2
|
+
function createStore(dbName, storeName) {
|
|
3
|
+
let dbp;
|
|
4
|
+
const getDB = () => {
|
|
5
|
+
if (dbp)
|
|
6
|
+
return dbp;
|
|
7
|
+
const request = indexedDB.open(dbName);
|
|
8
|
+
request.onupgradeneeded = () => request.result.createObjectStore(storeName);
|
|
9
|
+
dbp = promisify(request);
|
|
10
|
+
dbp.then((db) => {
|
|
11
|
+
db.onclose = () => {
|
|
12
|
+
dbp = undefined;
|
|
13
|
+
};
|
|
14
|
+
}, () => { });
|
|
15
|
+
return dbp;
|
|
16
|
+
};
|
|
17
|
+
return (txMode, callback) => getDB().then((db) => callback(db.transaction(storeName, txMode).objectStore(storeName)));
|
|
18
|
+
}
|
|
19
|
+
function promisify(request) {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
request.oncomplete = request.onsuccess = () => resolve(request.result);
|
|
22
|
+
request.onabort = request.onerror = () => reject(request.error);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export const idb = {
|
|
26
|
+
get(key) {
|
|
27
|
+
if (!defaultGetStoreFunc)
|
|
28
|
+
defaultGetStoreFunc = createStore("flow-id", "keyval");
|
|
29
|
+
return defaultGetStoreFunc("readonly", (store) => promisify(store.get(key)));
|
|
30
|
+
},
|
|
31
|
+
set(key, value) {
|
|
32
|
+
if (!defaultGetStoreFunc)
|
|
33
|
+
defaultGetStoreFunc = createStore("flow-id", "keyval");
|
|
34
|
+
return defaultGetStoreFunc("readwrite", (store) => {
|
|
35
|
+
store.put(value, key);
|
|
36
|
+
return promisify(store.transaction);
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
delete(key) {
|
|
40
|
+
if (!defaultGetStoreFunc)
|
|
41
|
+
defaultGetStoreFunc = createStore("flow-id", "keyval");
|
|
42
|
+
return defaultGetStoreFunc("readwrite", (store) => {
|
|
43
|
+
store.delete(key);
|
|
44
|
+
return promisify(store.transaction);
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createFlow, getFlow, requireFlow, resetFlow } from "./create-flow";
|
|
2
|
+
export { createDialogHost } from "./dialog-host";
|
|
3
|
+
export { METHODS } from "./methods";
|
|
4
|
+
export type { AccessKeyOptions, Address, ConnectCapabilities, ConnectResponse, CreateFlowOptions, DialogHost, Flow, FlowCredential, FlowState, FlowUser, LoginOptions, MethodName, Session, } from "../types";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const METHODS: {
|
|
2
|
+
readonly connect: "wallet_connect";
|
|
3
|
+
readonly signMessage: "personal_sign";
|
|
4
|
+
readonly signTypedData: "eth_signTypedData";
|
|
5
|
+
readonly sendTransaction: "eth_sendTransaction";
|
|
6
|
+
readonly sendCalls: "wallet_sendCalls";
|
|
7
|
+
readonly restore: "wallet_restore";
|
|
8
|
+
readonly signOut: "wallet_signout";
|
|
9
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { FlowUser, FlowCredential, WebAuthnSignature } from "../types";
|
|
2
|
+
export declare const METHODS: {
|
|
3
|
+
readonly connect: "wallet_connect";
|
|
4
|
+
readonly signMessage: "personal_sign";
|
|
5
|
+
readonly signTypedData: "eth_signTypedData";
|
|
6
|
+
readonly sendTransaction: "eth_sendTransaction";
|
|
7
|
+
readonly sendCalls: "wallet_sendCalls";
|
|
8
|
+
readonly restore: "wallet_restore";
|
|
9
|
+
readonly signOut: "wallet_signout";
|
|
10
|
+
};
|
|
11
|
+
export type Method = (typeof METHODS)[keyof typeof METHODS];
|
|
12
|
+
export type ConnectCapabilities = {
|
|
13
|
+
createAccount?: boolean;
|
|
14
|
+
signIn?: boolean;
|
|
15
|
+
accessKeyHash?: string;
|
|
16
|
+
headless?: boolean;
|
|
17
|
+
credentialId?: string;
|
|
18
|
+
};
|
|
19
|
+
export type ConnectRequest = [{
|
|
20
|
+
capabilities?: ConnectCapabilities;
|
|
21
|
+
}];
|
|
22
|
+
export type ConnectResponse = {
|
|
23
|
+
jwt: string;
|
|
24
|
+
user: FlowUser;
|
|
25
|
+
credential: FlowCredential;
|
|
26
|
+
webauthn?: WebAuthnSignature;
|
|
27
|
+
};
|
|
28
|
+
export type SignMessageRequest = [message: string, address: string];
|
|
29
|
+
export type SignMessageResponse = `0x${string}`;
|
|
30
|
+
export type TypedDataDomain = {
|
|
31
|
+
name?: string;
|
|
32
|
+
version?: string;
|
|
33
|
+
chainId?: number;
|
|
34
|
+
verifyingContract?: string;
|
|
35
|
+
salt?: string;
|
|
36
|
+
};
|
|
37
|
+
export type TypedDataField = {
|
|
38
|
+
name: string;
|
|
39
|
+
type: string;
|
|
40
|
+
};
|
|
41
|
+
export type TypedData = {
|
|
42
|
+
domain: TypedDataDomain;
|
|
43
|
+
types: Record<string, readonly TypedDataField[]>;
|
|
44
|
+
primaryType: string;
|
|
45
|
+
message: Record<string, unknown>;
|
|
46
|
+
};
|
|
47
|
+
export type SignTypedDataRequest = [address: string, typedData: TypedData];
|
|
48
|
+
export type SignTypedDataResponse = `0x${string}`;
|
|
49
|
+
export type Call = {
|
|
50
|
+
to?: string;
|
|
51
|
+
value?: string;
|
|
52
|
+
data?: string;
|
|
53
|
+
};
|
|
54
|
+
export type TransactionArgs = Call & {
|
|
55
|
+
chainId?: string;
|
|
56
|
+
};
|
|
57
|
+
export type SendTransactionRequest = [TransactionArgs];
|
|
58
|
+
export type SendTransactionResponse = `0x${string}`;
|
|
59
|
+
export type SendCallsRequest = [
|
|
60
|
+
{
|
|
61
|
+
calls: readonly Call[];
|
|
62
|
+
chainId?: string;
|
|
63
|
+
capabilities?: Record<string, unknown>;
|
|
64
|
+
}
|
|
65
|
+
];
|
|
66
|
+
export type SendCallsResponse = {
|
|
67
|
+
id: string;
|
|
68
|
+
};
|
|
69
|
+
export type RestoreRequest = [];
|
|
70
|
+
export type RestoreResponse = {
|
|
71
|
+
jwt: string;
|
|
72
|
+
user: FlowUser;
|
|
73
|
+
credential: FlowCredential;
|
|
74
|
+
address: `0x${string}`;
|
|
75
|
+
};
|
|
76
|
+
export type SignOutRequest = [];
|
|
77
|
+
export type SignOutResponse = {
|
|
78
|
+
ok: true;
|
|
79
|
+
};
|
|
80
|
+
export type MethodParams = {
|
|
81
|
+
[METHODS.connect]: ConnectRequest;
|
|
82
|
+
[METHODS.signMessage]: SignMessageRequest;
|
|
83
|
+
[METHODS.signTypedData]: SignTypedDataRequest;
|
|
84
|
+
[METHODS.sendTransaction]: SendTransactionRequest;
|
|
85
|
+
[METHODS.sendCalls]: SendCallsRequest;
|
|
86
|
+
[METHODS.restore]: RestoreRequest;
|
|
87
|
+
[METHODS.signOut]: SignOutRequest;
|
|
88
|
+
};
|
|
89
|
+
export type MethodResult = {
|
|
90
|
+
[METHODS.connect]: ConnectResponse;
|
|
91
|
+
[METHODS.signMessage]: SignMessageResponse;
|
|
92
|
+
[METHODS.signTypedData]: SignTypedDataResponse;
|
|
93
|
+
[METHODS.sendTransaction]: SendTransactionResponse;
|
|
94
|
+
[METHODS.sendCalls]: SendCallsResponse;
|
|
95
|
+
[METHODS.restore]: RestoreResponse;
|
|
96
|
+
[METHODS.signOut]: SignOutResponse;
|
|
97
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Address, FlowCredential, FlowState, RunLoginParams, RunLoginResult, Store } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* Reads the active credential from IndexedDB and seeds the in-memory store
|
|
4
|
+
* with `{ credential, address }`. Run on every page load so signing operations
|
|
5
|
+
* can derive the address before the JWT is refreshed — the credential is
|
|
6
|
+
* the durable identity, the JWT is just an access token.
|
|
7
|
+
*
|
|
8
|
+
* Idempotent and silent: missing credential is a normal "first visit" state,
|
|
9
|
+
* not an error.
|
|
10
|
+
*/
|
|
11
|
+
export declare function restoreCredential(store: Store<FlowState>): Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* Drives the interactive `wallet_connect` flow through the dialog and
|
|
14
|
+
* commits the result to local state.
|
|
15
|
+
*
|
|
16
|
+
* The capabilities bag is built from the call site (signUp/signIn intent +
|
|
17
|
+
* any extras like accessKeyHash) plus a "last credential" hint pulled from
|
|
18
|
+
* IDB. The hint lets the dialog pre-fill the username and skip a step when
|
|
19
|
+
* the user has signed in here before — without it every login looks like a
|
|
20
|
+
* first-time sign-in.
|
|
21
|
+
*
|
|
22
|
+
* Persists the credential under both `activeCredential` (current session)
|
|
23
|
+
* and `lastActiveCredential` (next-time hint) so the hint survives logout.
|
|
24
|
+
*/
|
|
25
|
+
export declare function runLogin(params: RunLoginParams): Promise<RunLoginResult>;
|
|
26
|
+
/**
|
|
27
|
+
* Clears local-only auth state: active credential in IDB, the access key for
|
|
28
|
+
* the current address (if any), and the in-memory store. Does NOT touch the
|
|
29
|
+
* server cookie session — the caller (createFlow.logout) handles that
|
|
30
|
+
* separately via `wallet_signout`.
|
|
31
|
+
*
|
|
32
|
+
* The `lastActiveCredential` IDB key is intentionally preserved so the next
|
|
33
|
+
* login can pre-fill the username.
|
|
34
|
+
*/
|
|
35
|
+
export declare function runLogout(store: Store<FlowState>): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Derives the user's wallet address from their passkey public key by
|
|
38
|
+
* computing keccak256(pubkey)[12:] — the standard Ethereum address scheme,
|
|
39
|
+
* but applied to the WebAuthn P256 key embedded in the passkey credential.
|
|
40
|
+
*
|
|
41
|
+
* This means the address is fully derivable from the passkey alone (no
|
|
42
|
+
* server needed) and stays stable across sessions and devices that share
|
|
43
|
+
* the credential.
|
|
44
|
+
*/
|
|
45
|
+
export declare function credentialToAddress(credential: FlowCredential): Address;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import * as OxAddress from "ox/Address";
|
|
2
|
+
import * as PublicKey from "ox/PublicKey";
|
|
3
|
+
import { idb } from "./idb";
|
|
4
|
+
import { METHODS } from "./methods";
|
|
5
|
+
import { initialFlowState } from "./store";
|
|
6
|
+
const CREDENTIAL_KEY = "flow.activeCredential";
|
|
7
|
+
const LAST_CREDENTIAL_KEY = "flow.lastActiveCredential";
|
|
8
|
+
/**
|
|
9
|
+
* Reads the active credential from IndexedDB and seeds the in-memory store
|
|
10
|
+
* with `{ credential, address }`. Run on every page load so signing operations
|
|
11
|
+
* can derive the address before the JWT is refreshed — the credential is
|
|
12
|
+
* the durable identity, the JWT is just an access token.
|
|
13
|
+
*
|
|
14
|
+
* Idempotent and silent: missing credential is a normal "first visit" state,
|
|
15
|
+
* not an error.
|
|
16
|
+
*/
|
|
17
|
+
export async function restoreCredential(store) {
|
|
18
|
+
const credential = await idb.get(CREDENTIAL_KEY);
|
|
19
|
+
if (!credential)
|
|
20
|
+
return;
|
|
21
|
+
const address = credentialToAddress(credential);
|
|
22
|
+
store.setState({ credential, address });
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Drives the interactive `wallet_connect` flow through the dialog and
|
|
26
|
+
* commits the result to local state.
|
|
27
|
+
*
|
|
28
|
+
* The capabilities bag is built from the call site (signUp/signIn intent +
|
|
29
|
+
* any extras like accessKeyHash) plus a "last credential" hint pulled from
|
|
30
|
+
* IDB. The hint lets the dialog pre-fill the username and skip a step when
|
|
31
|
+
* the user has signed in here before — without it every login looks like a
|
|
32
|
+
* first-time sign-in.
|
|
33
|
+
*
|
|
34
|
+
* Persists the credential under both `activeCredential` (current session)
|
|
35
|
+
* and `lastActiveCredential` (next-time hint) so the hint survives logout.
|
|
36
|
+
*/
|
|
37
|
+
export async function runLogin(params) {
|
|
38
|
+
const { dialog, store, options, extraCapabilities } = params;
|
|
39
|
+
const lastCredential = await idb.get(LAST_CREDENTIAL_KEY);
|
|
40
|
+
const capabilities = {
|
|
41
|
+
...(options?.signUp ? { createAccount: true } : {}),
|
|
42
|
+
...(lastCredential && !options?.signUp
|
|
43
|
+
? { credentialId: lastCredential.id }
|
|
44
|
+
: {}),
|
|
45
|
+
...(extraCapabilities ?? {}),
|
|
46
|
+
};
|
|
47
|
+
const result = (await dialog.request(METHODS.connect, [
|
|
48
|
+
{ capabilities },
|
|
49
|
+
]));
|
|
50
|
+
const { jwt, user, credential, webauthn } = result;
|
|
51
|
+
const address = credentialToAddress(credential);
|
|
52
|
+
await idb.set(CREDENTIAL_KEY, credential);
|
|
53
|
+
await idb.set(LAST_CREDENTIAL_KEY, credential);
|
|
54
|
+
store.setState({ user, jwt, credential, address });
|
|
55
|
+
return {
|
|
56
|
+
session: { user, jwt, credential, address },
|
|
57
|
+
webauthn,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Clears local-only auth state: active credential in IDB, the access key for
|
|
62
|
+
* the current address (if any), and the in-memory store. Does NOT touch the
|
|
63
|
+
* server cookie session — the caller (createFlow.logout) handles that
|
|
64
|
+
* separately via `wallet_signout`.
|
|
65
|
+
*
|
|
66
|
+
* The `lastActiveCredential` IDB key is intentionally preserved so the next
|
|
67
|
+
* login can pre-fill the username.
|
|
68
|
+
*/
|
|
69
|
+
export async function runLogout(store) {
|
|
70
|
+
const state = store.getSnapshot();
|
|
71
|
+
if (state.address) {
|
|
72
|
+
const { clearAccessKey } = await import("./access-key");
|
|
73
|
+
await clearAccessKey(state.address);
|
|
74
|
+
}
|
|
75
|
+
await idb.delete(CREDENTIAL_KEY);
|
|
76
|
+
store.setState(initialFlowState);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Derives the user's wallet address from their passkey public key by
|
|
80
|
+
* computing keccak256(pubkey)[12:] — the standard Ethereum address scheme,
|
|
81
|
+
* but applied to the WebAuthn P256 key embedded in the passkey credential.
|
|
82
|
+
*
|
|
83
|
+
* This means the address is fully derivable from the passkey alone (no
|
|
84
|
+
* server needed) and stays stable across sessions and devices that share
|
|
85
|
+
* the credential.
|
|
86
|
+
*/
|
|
87
|
+
export function credentialToAddress(credential) {
|
|
88
|
+
const pub = PublicKey.from(`0x${credential.publicKey.replace(/^0x/, "")}`);
|
|
89
|
+
return OxAddress.fromPublicKey(pub);
|
|
90
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Hex, type WalletClient } from "viem";
|
|
2
|
+
import type { SendCallsArgs, SendTransactionArgs, SignMessageArgs, SignTypedDataArgs, SigningContext } from "../types";
|
|
3
|
+
/**
|
|
4
|
+
* Constructs a viem WalletClient bound to the user's Flow account. Wraps the
|
|
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.
|
|
9
|
+
*/
|
|
10
|
+
export declare function buildWalletClient(ctx: SigningContext, chainId?: number): Promise<WalletClient>;
|
|
11
|
+
export declare function signMessage(ctx: SigningContext, args: SignMessageArgs): Promise<Hex>;
|
|
12
|
+
export declare function signTypedData(ctx: SigningContext, args: SignTypedDataArgs): Promise<Hex>;
|
|
13
|
+
export declare function sendTransaction(ctx: SigningContext, args: SendTransactionArgs): Promise<Hex>;
|
|
14
|
+
export declare function sendCalls(ctx: SigningContext, args: SendCallsArgs): Promise<{
|
|
15
|
+
id: string;
|
|
16
|
+
}>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createWalletClient, http, } from "viem";
|
|
2
|
+
import { sendCalls as viemSendCalls, sendTransaction as viemSendTransaction, signMessage as viemSignMessage, signTypedData as viemSignTypedData, } from "viem/actions";
|
|
3
|
+
import { Account, walletNamespaceCompat } from "viem/tempo";
|
|
4
|
+
import { buildAccessKeyAccount, clearAccessKey, isExpired, loadAccessKey, } from "./access-key";
|
|
5
|
+
function requireAuth(state) {
|
|
6
|
+
if (!state.credential || !state.address) {
|
|
7
|
+
throw new Error("Not authenticated — call flow.login() first");
|
|
8
|
+
}
|
|
9
|
+
return { credential: state.credential, address: state.address };
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Picks the right viem account for signing — access key if one is present
|
|
13
|
+
* and unexpired, otherwise the root passkey account (which prompts WebAuthn
|
|
14
|
+
* on every signature).
|
|
15
|
+
*
|
|
16
|
+
* `strict: true` flips the expired-access-key case from "silently fall back
|
|
17
|
+
* to passkey" to "throw" — useful when consumers want to enforce that
|
|
18
|
+
* background signing only ever uses the access key.
|
|
19
|
+
*/
|
|
20
|
+
async function resolveAccount(params) {
|
|
21
|
+
const rootAccount = Account.fromWebAuthnP256(params.credential, { ...(params.rpId ? { rpId: params.rpId } : {}) });
|
|
22
|
+
const stored = await loadAccessKey(params.address);
|
|
23
|
+
if (!stored)
|
|
24
|
+
return rootAccount;
|
|
25
|
+
if (isExpired(stored)) {
|
|
26
|
+
await clearAccessKey(params.address);
|
|
27
|
+
if (params.strict)
|
|
28
|
+
throw new Error("Access key expired");
|
|
29
|
+
return rootAccount;
|
|
30
|
+
}
|
|
31
|
+
return buildAccessKeyAccount(stored, params.credential, params.rpId);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Constructs a viem WalletClient bound to the user's Flow account. Wraps the
|
|
35
|
+
* 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.
|
|
39
|
+
*/
|
|
40
|
+
export async function buildWalletClient(ctx, chainId) {
|
|
41
|
+
const { credential, address } = requireAuth(ctx.getState());
|
|
42
|
+
const chain = ctx.getChain(chainId);
|
|
43
|
+
if (!chain)
|
|
44
|
+
throw new Error("Chain not configured — pass chains to createFlow()");
|
|
45
|
+
const account = await resolveAccount({
|
|
46
|
+
credential,
|
|
47
|
+
address,
|
|
48
|
+
rpId: ctx.rpId,
|
|
49
|
+
strict: ctx.strict,
|
|
50
|
+
});
|
|
51
|
+
const transport = ctx.getTransport(chain.id) ?? http();
|
|
52
|
+
return createWalletClient({
|
|
53
|
+
account,
|
|
54
|
+
chain,
|
|
55
|
+
transport: walletNamespaceCompat(transport, { account }),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export async function signMessage(ctx, args) {
|
|
59
|
+
const client = await buildWalletClient(ctx, args.chainId);
|
|
60
|
+
return viemSignMessage(client, {
|
|
61
|
+
account: client.account,
|
|
62
|
+
message: args.message,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
export async function signTypedData(ctx, args) {
|
|
66
|
+
const { chainId, ...rest } = args;
|
|
67
|
+
const client = await buildWalletClient(ctx, chainId);
|
|
68
|
+
return viemSignTypedData(client, {
|
|
69
|
+
account: client.account,
|
|
70
|
+
...rest,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
export async function sendTransaction(ctx, args) {
|
|
74
|
+
const { chainId, ...rest } = args;
|
|
75
|
+
const client = await buildWalletClient(ctx, chainId);
|
|
76
|
+
return viemSendTransaction(client, {
|
|
77
|
+
account: client.account,
|
|
78
|
+
chain: client.chain,
|
|
79
|
+
...rest,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
export async function sendCalls(ctx, args) {
|
|
83
|
+
const { chainId, ...rest } = args;
|
|
84
|
+
const client = await buildWalletClient(ctx, chainId);
|
|
85
|
+
const result = await viemSendCalls(client, {
|
|
86
|
+
account: client.account,
|
|
87
|
+
chain: client.chain,
|
|
88
|
+
...rest,
|
|
89
|
+
});
|
|
90
|
+
return { id: result.id };
|
|
91
|
+
}
|