@flow-industries/id 0.22.1 → 0.23.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 +18 -9
- package/dist/sdk/browser-contract.d.ts +30 -0
- package/dist/sdk/browser-contract.js +19 -0
- package/dist/sdk/browser-session-route.d.ts +8 -0
- package/dist/sdk/browser-session-route.js +196 -0
- package/dist/sdk/client/create-flow.js +176 -166
- package/dist/sdk/client/flow-widget.js +1 -1
- package/dist/sdk/client/idb.d.ts +1 -0
- package/dist/sdk/client/idb.js +24 -0
- package/dist/sdk/client/open-profile.js +9 -1
- package/dist/sdk/client/profile-button.d.ts +1 -13
- package/dist/sdk/client/profile-button.js +114 -177
- package/dist/sdk/client/session.d.ts +1 -10
- package/dist/sdk/client/session.js +0 -37
- package/dist/sdk/client/static-flow.js +1 -0
- package/dist/sdk/react/hooks.d.ts +1 -1
- package/dist/sdk/react/profile-button.d.ts +1 -12
- package/dist/sdk/react/profile-button.js +1 -12
- package/dist/sdk/session-route.js +101 -4
- package/dist/sdk/types/dialog.d.ts +0 -3
- package/dist/sdk/types/index.d.ts +1 -1
- package/dist/sdk/types/landing.d.ts +1 -0
- package/dist/sdk/types/sdk.d.ts +7 -21
- package/dist/sdk/types/server.d.ts +6 -0
- package/dist/sdk/wagmi/index.js +0 -3
- package/package.json +4 -3
|
@@ -1,208 +1,145 @@
|
|
|
1
1
|
import { resolveIdHost } from "../id-host";
|
|
2
2
|
import { getFlow, requireFlow } from "./create-flow";
|
|
3
|
-
import { answerTokenRequest, bridgeToWindow, hostIdentity, makeIframe, OVERLAY_STYLE, sendBridgeNotification, } from "./iframe-host";
|
|
4
|
-
import { setOverlayFocus } from "./overlay-focus";
|
|
5
3
|
const POSITION_STYLE = {
|
|
6
4
|
"top-right": { top: "0", right: "0" },
|
|
7
5
|
"top-left": { top: "0", left: "0" },
|
|
8
6
|
"bottom-right": { bottom: "0", right: "0" },
|
|
9
7
|
"bottom-left": { bottom: "0", left: "0" },
|
|
10
8
|
};
|
|
11
|
-
/**
|
|
12
|
-
* Mounts the universal Flow ID "profile button" — a persistent inline iframe
|
|
13
|
-
* showing the signed-in user's avatar + username (the collapsed pill). Clicking
|
|
14
|
-
* it opens the profile dialog in a SEPARATE fullscreen overlay iframe, so the
|
|
15
|
-
* pill stays in place behind the dialog's dimmed backdrop rather than morphing
|
|
16
|
-
* into it. Both iframes ride the dialog SPA at `${host}/dialog/` (routes
|
|
17
|
-
* `/dialog/profile-pill` and `/dialog/profile`) so the chrome matches the auth
|
|
18
|
-
* dialog and the widget is first-party to Flow ID (reads `/api/me`, uploads
|
|
19
|
-
* avatars with the cookie session).
|
|
20
|
-
*
|
|
21
|
-
* Resolves the Flow singleton automatically (createFlow() must have run), so
|
|
22
|
-
* callers don't thread the instance through. No-op under SSR.
|
|
23
|
-
*/
|
|
9
|
+
/** Mount a native account button that opens Flow ID in a normal browser tab. */
|
|
24
10
|
export function createProfileButton(options) {
|
|
25
11
|
if (!("document" in globalThis))
|
|
26
12
|
return { setTheme() { }, destroy() { } };
|
|
27
13
|
const flow = options.flow ?? getFlow() ?? requireFlow();
|
|
28
14
|
const host = resolveIdHost(options.host ?? flow.host);
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
Object.assign(el.style, {
|
|
15
|
+
const createdContainer = options.container
|
|
16
|
+
? null
|
|
17
|
+
: document.createElement("div");
|
|
18
|
+
const container = options.container ?? createdContainer;
|
|
19
|
+
if (!container)
|
|
20
|
+
return { setTheme() { }, destroy() { } };
|
|
21
|
+
if (createdContainer) {
|
|
22
|
+
Object.assign(createdContainer.style, {
|
|
38
23
|
position: "fixed",
|
|
39
24
|
zIndex: "2147483000",
|
|
40
|
-
// Never steal input from the host (e.g. the Godot canvas) — only the
|
|
41
|
-
// iframe re-enables pointer events for itself.
|
|
42
|
-
pointerEvents: "none",
|
|
43
25
|
padding: options.padding ?? "1.5rem",
|
|
44
26
|
...POSITION_STYLE[options.position ?? "top-right"],
|
|
45
27
|
});
|
|
46
|
-
document.body.appendChild(
|
|
47
|
-
createdContainer = el;
|
|
48
|
-
container = el;
|
|
49
|
-
}
|
|
50
|
-
function makeFrame(title) {
|
|
51
|
-
const frame = makeIframe(`${host}/dialog/`, title);
|
|
52
|
-
frame.dataset.flowProfile = "";
|
|
53
|
-
frame.style.display = "block";
|
|
54
|
-
return frame;
|
|
55
|
-
}
|
|
56
|
-
// A page may host several same-origin Flow iframes (this pill, this dialog,
|
|
57
|
-
// the auth login dialog). Origin filtering can't tell them apart, so each
|
|
58
|
-
// bridge is bound to its own iframe window — otherwise their `ready`/`close`
|
|
59
|
-
// handshakes would cross-fire. Identity is pinned to the host origin so the
|
|
60
|
-
// username is never broadcast with "*".
|
|
61
|
-
function makeBridge(frame, route) {
|
|
62
|
-
const bridge = bridgeToWindow(frame.contentWindow, {
|
|
63
|
-
targetOrigin: hostOrigin,
|
|
64
|
-
});
|
|
65
|
-
sendBridgeNotification(bridge, "__internal", {
|
|
66
|
-
type: "init",
|
|
67
|
-
mode: "iframe",
|
|
68
|
-
referrer: { title: document.title },
|
|
69
|
-
theme: { colorScheme: theme },
|
|
70
|
-
route,
|
|
71
|
-
});
|
|
72
|
-
return bridge;
|
|
73
|
-
}
|
|
74
|
-
// ----- the persistent pill (stays mounted; sizes to its own content) -----
|
|
75
|
-
const pill = makeFrame("Flow ID profile button");
|
|
76
|
-
let pillWidth = 0;
|
|
77
|
-
let pillHeight = 0;
|
|
78
|
-
function applyPillSize() {
|
|
79
|
-
Object.assign(pill.style, {
|
|
80
|
-
position: "static",
|
|
81
|
-
width: `${pillWidth}px`,
|
|
82
|
-
height: `${pillHeight}px`,
|
|
83
|
-
pointerEvents: pillWidth > 0 ? "auto" : "none",
|
|
84
|
-
});
|
|
28
|
+
document.body.appendChild(createdContainer);
|
|
85
29
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
30
|
+
const button = document.createElement("button");
|
|
31
|
+
button.type = "button";
|
|
32
|
+
button.dataset.flowProfile = "";
|
|
33
|
+
Object.assign(button.style, {
|
|
34
|
+
display: "flex",
|
|
35
|
+
alignItems: "center",
|
|
36
|
+
gap: "8px",
|
|
37
|
+
border: "1px solid color-mix(in srgb, currentColor 18%, transparent)",
|
|
38
|
+
borderRadius: "999px",
|
|
39
|
+
padding: "10px 16px",
|
|
40
|
+
background: "Canvas",
|
|
41
|
+
color: "CanvasText",
|
|
42
|
+
font: "inherit",
|
|
43
|
+
cursor: "pointer",
|
|
44
|
+
pointerEvents: "auto",
|
|
45
|
+
maxWidth: "240px",
|
|
46
|
+
overflow: "hidden",
|
|
47
|
+
textOverflow: "ellipsis",
|
|
48
|
+
whiteSpace: "nowrap",
|
|
101
49
|
});
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
50
|
+
const notice = document.createElement("span");
|
|
51
|
+
notice.setAttribute("role", "status");
|
|
52
|
+
Object.assign(notice.style, {
|
|
53
|
+
display: "block",
|
|
54
|
+
font: "12px sans-serif",
|
|
55
|
+
maxWidth: "240px",
|
|
56
|
+
});
|
|
57
|
+
const avatar = document.createElement("span");
|
|
58
|
+
Object.assign(avatar.style, {
|
|
59
|
+
display: "flex",
|
|
60
|
+
alignItems: "center",
|
|
61
|
+
justifyContent: "center",
|
|
62
|
+
width: "28px",
|
|
63
|
+
height: "28px",
|
|
64
|
+
borderRadius: "50%",
|
|
65
|
+
overflow: "hidden",
|
|
66
|
+
background: "color-mix(in srgb, currentColor 10%, transparent)",
|
|
67
|
+
flexShrink: "0",
|
|
68
|
+
});
|
|
69
|
+
const name = document.createElement("span");
|
|
70
|
+
button.append(avatar, name);
|
|
71
|
+
const fallback = document.createElement("a");
|
|
72
|
+
fallback.href = `${host}/account`;
|
|
73
|
+
fallback.target = "_blank";
|
|
74
|
+
fallback.rel = "noopener noreferrer";
|
|
75
|
+
fallback.textContent = "Open Flow ID";
|
|
76
|
+
fallback.hidden = true;
|
|
77
|
+
let lastUsername;
|
|
78
|
+
let profileRequest = null;
|
|
79
|
+
function update() {
|
|
80
|
+
const user = flow.getState().user;
|
|
81
|
+
name.textContent = user?.username ?? "Flow ID";
|
|
82
|
+
button.setAttribute("aria-label", `Open ${user?.username ?? "your"} Flow ID account in a new tab`);
|
|
83
|
+
if (user?.username === lastUsername)
|
|
108
84
|
return;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
});
|
|
114
|
-
document.body.appendChild(dialog);
|
|
115
|
-
dialogBridge = makeBridge(dialog, "/dialog/profile");
|
|
116
|
-
dialogBridge.on("close", () => hideDialog());
|
|
117
|
-
dialogBridge.on("__internal", (payload) => {
|
|
118
|
-
if (payload.type === "profile-login") {
|
|
119
|
-
hideDialog();
|
|
120
|
-
void flow
|
|
121
|
-
.login(payload.mode === "signup" ? { signUp: true } : { signIn: true })
|
|
122
|
-
.catch(() => { });
|
|
123
|
-
}
|
|
124
|
-
else if (payload.type === "profile-logout") {
|
|
125
|
-
hideDialog();
|
|
126
|
-
void flow.logout().catch(() => { });
|
|
127
|
-
}
|
|
128
|
-
else if (payload.type === "profile-token-request") {
|
|
129
|
-
if (dialogBridge)
|
|
130
|
-
void answerTokenRequest(dialogBridge, flow, payload.id);
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
sendBridgeNotification(dialogBridge, "__internal", {
|
|
134
|
-
type: "profile-identity",
|
|
135
|
-
identity: hostIdentity(flow),
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
// The dialog owns its open/close animation entirely; the host only signals
|
|
139
|
-
// visibility and toggles hit-testing. The overlay iframe is NEVER set to
|
|
140
|
-
// display:none — that pauses the iframe's requestAnimationFrame, so motion's
|
|
141
|
-
// engine is asleep when the next open fires and the animation jumps straight
|
|
142
|
-
// to its end (instant). Keeping it always-rendered, transparent and
|
|
143
|
-
// click-through while hidden keeps every open/close animation reliable.
|
|
144
|
-
function showDialog() {
|
|
145
|
-
ensureDialog();
|
|
146
|
-
if (!dialog)
|
|
85
|
+
lastUsername = user?.username;
|
|
86
|
+
profileRequest?.abort();
|
|
87
|
+
avatar.textContent = user?.username.slice(0, 1).toUpperCase() ?? "F";
|
|
88
|
+
if (!user || user.isGuest)
|
|
147
89
|
return;
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
90
|
+
const controller = new AbortController();
|
|
91
|
+
profileRequest = controller;
|
|
92
|
+
void fetch(`${host}/api/users/${encodeURIComponent(user.username)}`, {
|
|
93
|
+
signal: controller.signal,
|
|
94
|
+
credentials: "omit",
|
|
95
|
+
})
|
|
96
|
+
.then(async (response) => {
|
|
97
|
+
if (!response.ok)
|
|
98
|
+
return;
|
|
99
|
+
// SAFETY: Flow ID owns the public profile endpoint and its optional image field.
|
|
100
|
+
const profile = (await response.json());
|
|
101
|
+
if (controller.signal.aborted || !profile.image)
|
|
102
|
+
return;
|
|
103
|
+
const image = document.createElement("img");
|
|
104
|
+
image.src = profile.image;
|
|
105
|
+
image.alt = "";
|
|
106
|
+
Object.assign(image.style, {
|
|
107
|
+
width: "100%",
|
|
108
|
+
height: "100%",
|
|
109
|
+
objectFit: "cover",
|
|
110
|
+
});
|
|
111
|
+
avatar.replaceChildren(image);
|
|
112
|
+
})
|
|
113
|
+
.catch(() => { });
|
|
153
114
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
115
|
+
button.addEventListener("click", () => {
|
|
116
|
+
const opened = flow.openAccount();
|
|
117
|
+
notice.textContent = opened
|
|
118
|
+
? ""
|
|
119
|
+
: "Your browser blocked the new tab. Open Flow ID below to sign in.";
|
|
120
|
+
fallback.hidden = opened;
|
|
121
|
+
});
|
|
122
|
+
function refresh() {
|
|
123
|
+
lastUsername = undefined;
|
|
124
|
+
update();
|
|
162
125
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
let lastIdentityKey;
|
|
167
|
-
function pushIdentity() {
|
|
168
|
-
const identity = hostIdentity(flow);
|
|
169
|
-
const key = identity ? `${identity.username}|${identity.isGuest}` : "none";
|
|
170
|
-
if (key === lastIdentityKey)
|
|
171
|
-
return;
|
|
172
|
-
lastIdentityKey = key;
|
|
173
|
-
sendBridgeNotification(pillBridge, "__internal", {
|
|
174
|
-
type: "profile-identity",
|
|
175
|
-
identity,
|
|
176
|
-
});
|
|
177
|
-
if (dialogBridge)
|
|
178
|
-
sendBridgeNotification(dialogBridge, "__internal", {
|
|
179
|
-
type: "profile-identity",
|
|
180
|
-
identity,
|
|
181
|
-
});
|
|
126
|
+
window.addEventListener("focus", refresh);
|
|
127
|
+
function setTheme(theme) {
|
|
128
|
+
button.style.colorScheme = theme;
|
|
182
129
|
}
|
|
183
|
-
|
|
184
|
-
|
|
130
|
+
setTheme(options.theme ?? "light dark");
|
|
131
|
+
container.append(button, notice, fallback);
|
|
132
|
+
update();
|
|
133
|
+
const unsubscribe = flow.subscribe(update);
|
|
185
134
|
return {
|
|
186
|
-
setTheme
|
|
187
|
-
if (next === theme)
|
|
188
|
-
return;
|
|
189
|
-
theme = next;
|
|
190
|
-
sendBridgeNotification(pillBridge, "__internal", {
|
|
191
|
-
type: "set-theme",
|
|
192
|
-
theme: { colorScheme: next },
|
|
193
|
-
});
|
|
194
|
-
sendBridgeNotification(dialogBridge, "__internal", {
|
|
195
|
-
type: "set-theme",
|
|
196
|
-
theme: { colorScheme: next },
|
|
197
|
-
});
|
|
198
|
-
},
|
|
135
|
+
setTheme,
|
|
199
136
|
destroy() {
|
|
200
|
-
hideDialog();
|
|
201
137
|
unsubscribe();
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
138
|
+
profileRequest?.abort();
|
|
139
|
+
window.removeEventListener("focus", refresh);
|
|
140
|
+
fallback.remove();
|
|
141
|
+
button.remove();
|
|
142
|
+
notice.remove();
|
|
206
143
|
createdContainer?.remove();
|
|
207
144
|
},
|
|
208
145
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Address, FlowCredential, FlowState,
|
|
1
|
+
import type { Address, FlowCredential, FlowState, Store } from "../types";
|
|
2
2
|
/**
|
|
3
3
|
* Reads the active credential from IndexedDB and seeds the in-memory store
|
|
4
4
|
* with `{ credential, address }`. Run on every page load so signing operations
|
|
@@ -9,15 +9,6 @@ import type { Address, FlowCredential, FlowState, RunLoginParams, RunLoginResult
|
|
|
9
9
|
* not an error.
|
|
10
10
|
*/
|
|
11
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
|
-
* Capabilities are intent only (signUp / signIn / signInHeadless / extras like
|
|
17
|
-
* accessKeyHash). Remembering "which account did this user use last" is
|
|
18
|
-
* the dialog's job — the client doesn't track or pass credentialIds.
|
|
19
|
-
*/
|
|
20
|
-
export declare function runLogin(params: RunLoginParams): Promise<RunLoginResult>;
|
|
21
12
|
/**
|
|
22
13
|
* Clears local-only auth state: active credential in IDB, the access key for
|
|
23
14
|
* the current address (if any), and the in-memory store. Does NOT touch the
|
|
@@ -2,7 +2,6 @@ import * as OxAddress from "ox/Address";
|
|
|
2
2
|
import * as PublicKey from "ox/PublicKey";
|
|
3
3
|
import { hex0x } from "../hex";
|
|
4
4
|
import { idb } from "./idb";
|
|
5
|
-
import { METHODS } from "./methods";
|
|
6
5
|
import { initialFlowState } from "./store";
|
|
7
6
|
const CREDENTIAL_KEY = "flow.activeCredential";
|
|
8
7
|
/**
|
|
@@ -21,42 +20,6 @@ export async function restoreCredential(store) {
|
|
|
21
20
|
const address = credentialToAddress(credential);
|
|
22
21
|
store.setState({ credential, address });
|
|
23
22
|
}
|
|
24
|
-
/**
|
|
25
|
-
* Drives the interactive `wallet_connect` flow through the dialog and
|
|
26
|
-
* commits the result to local state.
|
|
27
|
-
*
|
|
28
|
-
* Capabilities are intent only (signUp / signIn / signInHeadless / extras like
|
|
29
|
-
* accessKeyHash). Remembering "which account did this user use last" is
|
|
30
|
-
* the dialog's job — the client doesn't track or pass credentialIds.
|
|
31
|
-
*/
|
|
32
|
-
export async function runLogin(params) {
|
|
33
|
-
const { dialog, store, options, extraCapabilities } = params;
|
|
34
|
-
const signUp = Boolean(options?.signUp);
|
|
35
|
-
const capabilities = {
|
|
36
|
-
createAccount: signUp ? true : undefined,
|
|
37
|
-
signIn: !signUp && options?.signIn ? true : undefined,
|
|
38
|
-
signInHeadless: !signUp && options?.signInHeadless ? true : undefined,
|
|
39
|
-
...(extraCapabilities ?? {}),
|
|
40
|
-
};
|
|
41
|
-
/* SAFETY: the connect method's response shape, which the dialog route answers. */
|
|
42
|
-
const result = (await dialog.request(METHODS.connect, [
|
|
43
|
-
{ capabilities },
|
|
44
|
-
]));
|
|
45
|
-
const { jwt, refreshToken, user, credential, webauthn, additionalSessions } = result;
|
|
46
|
-
// An email-only login carries no credential — mirror the guest handling:
|
|
47
|
-
// nothing is persisted to IDB (reserved for a real passkey credential) and
|
|
48
|
-
// the address stays null until a passkey is added.
|
|
49
|
-
const address = credential ? credentialToAddress(credential) : null;
|
|
50
|
-
if (credential)
|
|
51
|
-
await idb.set(CREDENTIAL_KEY, credential);
|
|
52
|
-
store.setState({ user, jwt, credential, address });
|
|
53
|
-
return {
|
|
54
|
-
session: { user, jwt, credential, address },
|
|
55
|
-
refreshToken,
|
|
56
|
-
webauthn,
|
|
57
|
-
additionalSessions: additionalSessions ? additionalSessions : undefined,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
23
|
/**
|
|
61
24
|
* Clears local-only auth state: active credential in IDB, the access key for
|
|
62
25
|
* the current address (if any), and the in-memory store. Does NOT touch the
|
|
@@ -44,6 +44,7 @@ export function createStaticFlow(state, options = {}) {
|
|
|
44
44
|
return snapshot.user?.isGuest === true;
|
|
45
45
|
},
|
|
46
46
|
login: () => unavailable("login"),
|
|
47
|
+
openAccount: () => unavailable("openAccount"),
|
|
47
48
|
logout: () => unavailable("logout"),
|
|
48
49
|
restore: async () => false,
|
|
49
50
|
ensureGuest: () => unavailable("ensureGuest"),
|
|
@@ -28,7 +28,7 @@ export declare function useFlowId(): {
|
|
|
28
28
|
address: `0x${string}` | null;
|
|
29
29
|
isAuthenticated: boolean;
|
|
30
30
|
isGuest: boolean;
|
|
31
|
-
login: (options?: import("../types").LoginOptions) => Promise<
|
|
31
|
+
login: (options?: import("../types").LoginOptions) => Promise<never>;
|
|
32
32
|
logout: () => Promise<void>;
|
|
33
33
|
restore: () => Promise<boolean>;
|
|
34
34
|
ensureGuest: () => Promise<boolean>;
|
|
@@ -1,14 +1,3 @@
|
|
|
1
1
|
import type { ProfileButtonProps } from "../types";
|
|
2
|
-
/**
|
|
3
|
-
* Drops the universal Flow ID profile button into a React tree. Renders a host
|
|
4
|
-
* element (style it via `className` for placement/padding — e.g. top-right) that
|
|
5
|
-
* the pill iframe mounts into; the iframe itself is sized to its content and
|
|
6
|
-
* expands to the profile dialog on click. Resolves the Flow instance from a
|
|
7
|
-
* `<FlowIdProvider>` or the createFlow() singleton.
|
|
8
|
-
*
|
|
9
|
-
* A changing `theme` recolors the mounted pill over its open bridge rather than
|
|
10
|
-
* remounting it: `theme` is deliberately absent from the mount effect's deps,
|
|
11
|
-
* because tearing the iframe down on a light/dark toggle reloads the widget and
|
|
12
|
-
* drops the pill out of the host's layout mid-toggle.
|
|
13
|
-
*/
|
|
2
|
+
/** Mount the native account button with access to the current Flow session. */
|
|
14
3
|
export declare function ProfileButton({ className, host, theme }: ProfileButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -2,18 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { useEffect, useRef } from "react";
|
|
3
3
|
import { createProfileButton } from "../client/profile-button";
|
|
4
4
|
import { useFlow } from "./hooks";
|
|
5
|
-
/**
|
|
6
|
-
* Drops the universal Flow ID profile button into a React tree. Renders a host
|
|
7
|
-
* element (style it via `className` for placement/padding — e.g. top-right) that
|
|
8
|
-
* the pill iframe mounts into; the iframe itself is sized to its content and
|
|
9
|
-
* expands to the profile dialog on click. Resolves the Flow instance from a
|
|
10
|
-
* `<FlowIdProvider>` or the createFlow() singleton.
|
|
11
|
-
*
|
|
12
|
-
* A changing `theme` recolors the mounted pill over its open bridge rather than
|
|
13
|
-
* remounting it: `theme` is deliberately absent from the mount effect's deps,
|
|
14
|
-
* because tearing the iframe down on a light/dark toggle reloads the widget and
|
|
15
|
-
* drops the pill out of the host's layout mid-toggle.
|
|
16
|
-
*/
|
|
5
|
+
/** Mount the native account button with access to the current Flow session. */
|
|
17
6
|
export function ProfileButton({ className, host, theme }) {
|
|
18
7
|
const flow = useFlow();
|
|
19
8
|
const ref = useRef(null);
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { browserCompletionSchema } from "./browser-contract";
|
|
3
|
+
import { finishBrowserSession, startBrowserSession, } from "./browser-session-route";
|
|
4
|
+
import { clearCookieString } from "./cookies";
|
|
2
5
|
/**
|
|
3
6
|
* The first-party session route every consumer app serves from its own
|
|
4
7
|
* origin — the only path between a browser and the refresh token. The
|
|
@@ -21,13 +24,20 @@ import { z } from "zod";
|
|
|
21
24
|
*/
|
|
22
25
|
import { cookieNamesFor, parseCookieHeader } from "./cookies";
|
|
23
26
|
import { DEFAULT_SESSION_PATH, defaultAudience, resolveIdHost, } from "./id-host";
|
|
24
|
-
import { claimsToUser, clearingCookies, isJwtVerificationFailure, resolveSessionCore, sessionCookies, } from "./session-core";
|
|
27
|
+
import { claimsToUser, clearingCookies, isJwtVerificationFailure, refreshSessionCore, resolveSessionCore, sessionCookies, } from "./session-core";
|
|
25
28
|
import { verifyFlowJWT } from "./verify";
|
|
26
29
|
/** The body the browser SDK posts to its own app's session route. */
|
|
27
30
|
const installBodySchema = z.object({
|
|
28
31
|
refreshToken: z.string().min(1).optional(),
|
|
29
32
|
jwt: z.string().min(1).optional(),
|
|
30
33
|
mint: z.array(z.string()).optional(),
|
|
34
|
+
action: z.enum(["login", "account", "guest"]).optional(),
|
|
35
|
+
returnTo: z.string().optional(),
|
|
36
|
+
signUp: z.boolean().optional(),
|
|
37
|
+
accessKeyHash: z
|
|
38
|
+
.string()
|
|
39
|
+
.regex(/^0x[a-fA-F0-9]{64}$/)
|
|
40
|
+
.optional(),
|
|
31
41
|
});
|
|
32
42
|
const additionalBodySchema = z.object({
|
|
33
43
|
additionalSessions: z
|
|
@@ -72,17 +82,100 @@ function isCrossSite(request, audience) {
|
|
|
72
82
|
export async function handleSessionRequest(request, opts = {}) {
|
|
73
83
|
const audience = opts.audience ?? defaultAudience(new URL(request.url).origin);
|
|
74
84
|
const issuerUrl = resolveIdHost(opts.issuerUrl, audience);
|
|
85
|
+
const callback = new URL(request.url);
|
|
86
|
+
if (request.method === "GET" && callback.searchParams.has("flow_state")) {
|
|
87
|
+
return finishBrowserSession(request, audience, issuerUrl);
|
|
88
|
+
}
|
|
75
89
|
if (isCrossSite(request, audience)) {
|
|
76
90
|
return json({ state: null }, 403);
|
|
77
91
|
}
|
|
78
92
|
switch (request.method) {
|
|
79
93
|
case "GET": {
|
|
94
|
+
if (new URL(request.url).searchParams.get("refresh") === "1") {
|
|
95
|
+
const token = parseCookieHeader(request.headers.get("Cookie"))[cookieNamesFor(audience).refresh];
|
|
96
|
+
if (!token)
|
|
97
|
+
return json({ state: null }, 200);
|
|
98
|
+
const result = await refreshSessionCore(issuerUrl, audience, token);
|
|
99
|
+
if (result === "dead")
|
|
100
|
+
return json({ state: null }, 200, clearingCookies(audience));
|
|
101
|
+
if (!result)
|
|
102
|
+
return json({ state: null }, 502);
|
|
103
|
+
return json({ state: result.session }, 200, result.setCookies);
|
|
104
|
+
}
|
|
80
105
|
const { session, setCookies } = await resolveSessionCore(request.headers.get("Cookie"), audience, issuerUrl);
|
|
81
|
-
|
|
106
|
+
const completionName = `${cookieNamesFor(audience).refresh}.completion`;
|
|
107
|
+
const raw = parseCookieHeader(request.headers.get("Cookie"))[completionName];
|
|
108
|
+
let loginCompletion;
|
|
109
|
+
if (raw) {
|
|
110
|
+
setCookies.push(clearCookieString(completionName, {
|
|
111
|
+
secure: audience.startsWith("https:"),
|
|
112
|
+
}));
|
|
113
|
+
try {
|
|
114
|
+
const parsed = browserCompletionSchema.safeParse(JSON.parse(decodeURIComponent(atob(raw))));
|
|
115
|
+
if (parsed.success &&
|
|
116
|
+
parsed.data.expires > Date.now() &&
|
|
117
|
+
parsed.data.userId === session?.user.id)
|
|
118
|
+
loginCompletion = parsed.data;
|
|
119
|
+
}
|
|
120
|
+
catch { }
|
|
121
|
+
}
|
|
122
|
+
return json({ state: session, loginCompletion }, 200, setCookies);
|
|
82
123
|
}
|
|
83
124
|
case "POST": {
|
|
84
125
|
const parsed = installBodySchema.safeParse(await request.json().catch(() => null));
|
|
85
126
|
const body = parsed.success ? parsed.data : null;
|
|
127
|
+
if (body?.action === "login" || body?.action === "account") {
|
|
128
|
+
try {
|
|
129
|
+
return await startBrowserSession(request, audience, issuerUrl, {
|
|
130
|
+
...body,
|
|
131
|
+
action: body.action,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return json({ state: null }, 502);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (body?.action === "guest") {
|
|
139
|
+
const existing = await resolveSessionCore(request.headers.get("Cookie"), audience, issuerUrl);
|
|
140
|
+
if (existing.session)
|
|
141
|
+
return json({ state: existing.session }, 200, existing.setCookies);
|
|
142
|
+
const previousRefresh = parseCookieHeader(request.headers.get("Cookie"))[cookieNamesFor(audience).refresh];
|
|
143
|
+
if (previousRefresh && existing.setCookies.length === 0)
|
|
144
|
+
return json({ state: null }, 502);
|
|
145
|
+
try {
|
|
146
|
+
const headers = new Headers({ "Content-Type": "application/json" });
|
|
147
|
+
for (const name of [
|
|
148
|
+
"cf-connecting-ip",
|
|
149
|
+
"x-forwarded-for",
|
|
150
|
+
"user-agent",
|
|
151
|
+
]) {
|
|
152
|
+
const value = request.headers.get(name);
|
|
153
|
+
if (value)
|
|
154
|
+
headers.set(name, value);
|
|
155
|
+
}
|
|
156
|
+
const result = await fetch(`${issuerUrl}/api/auth/passkey/guest`, {
|
|
157
|
+
method: "POST",
|
|
158
|
+
headers,
|
|
159
|
+
body: JSON.stringify({ audience }),
|
|
160
|
+
});
|
|
161
|
+
if (!result.ok)
|
|
162
|
+
return json({ state: null }, result.status);
|
|
163
|
+
// SAFETY: the configured issuer returns this session token pair, verified below.
|
|
164
|
+
const data = (await result.json());
|
|
165
|
+
const claims = await verifyFlowJWT(data.jwt, { audience, issuerUrl });
|
|
166
|
+
return json({
|
|
167
|
+
state: {
|
|
168
|
+
user: claimsToUser(claims),
|
|
169
|
+
jwt: data.jwt,
|
|
170
|
+
credential: null,
|
|
171
|
+
address: null,
|
|
172
|
+
},
|
|
173
|
+
}, 200, sessionCookies(audience, data.jwt, data.refreshToken));
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return json({ state: null }, 502);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
86
179
|
// Reconcile mint: the browser asks its own server to obtain additional-
|
|
87
180
|
// audience sessions (e.g. talk.flow.game) authorized by the PRIMARY
|
|
88
181
|
// refresh cookie, for an embed that booted with no session of its own.
|
|
@@ -137,12 +230,16 @@ export async function handleSessionRequest(request, opts = {}) {
|
|
|
137
230
|
// an unreachable issuer just leaves the lineage to expire on its own.
|
|
138
231
|
if (token) {
|
|
139
232
|
try {
|
|
140
|
-
await fetch(`${issuerUrl}/api/session/revoke`, {
|
|
233
|
+
const response = await fetch(`${issuerUrl}/api/session/revoke`, {
|
|
141
234
|
method: "POST",
|
|
142
235
|
headers: { Authorization: `Bearer ${token}` },
|
|
143
236
|
});
|
|
237
|
+
if (!response.ok)
|
|
238
|
+
return json({ state: null }, 502);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return json({ state: null }, 502);
|
|
144
242
|
}
|
|
145
|
-
catch { }
|
|
146
243
|
}
|
|
147
244
|
const headers = new Headers({ "Cache-Control": "no-store" });
|
|
148
245
|
for (const cookie of clearingCookies(audience)) {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { RpcRequest } from "./protocol";
|
|
2
1
|
/**
|
|
3
2
|
* Async view state for a dialog panel — the three states every fetched
|
|
4
3
|
* section renders (spinner / retry row / content), so "empty" stays
|
|
@@ -91,7 +90,6 @@ export type DialogState = {
|
|
|
91
90
|
customLabels?: DialogCustomLabels;
|
|
92
91
|
};
|
|
93
92
|
export type SignUpProps = {
|
|
94
|
-
request: RpcRequest;
|
|
95
93
|
rpId: string;
|
|
96
94
|
accessKeyHash?: string | undefined;
|
|
97
95
|
guestToken?: string | undefined;
|
|
@@ -99,7 +97,6 @@ export type SignUpProps = {
|
|
|
99
97
|
onSignIn: () => void;
|
|
100
98
|
};
|
|
101
99
|
export type SignInWithEmailProps = {
|
|
102
|
-
request: RpcRequest;
|
|
103
100
|
onBack: () => void;
|
|
104
101
|
onHome: () => void;
|
|
105
102
|
};
|
|
@@ -16,7 +16,7 @@ export { getConnectCapabilities, isPersonalSignParams, isSendCallsParams, isSend
|
|
|
16
16
|
export type { ReportOperation, ReportRequest, ReportResponse, ReportsApi, ReportTransport, } from "./reports";
|
|
17
17
|
export type { CreateRoomEventInput, RoomEventArchive, RoomEventAttendance, RoomEventFrequency, RoomEventInstance, RoomEventInterest, RoomEventInterestState, RoomEventList, RoomEventOccurrence, RoomEventRecurrence, RoomEventRoomRef, RoomEventRun, RoomEventSeries, RoomEventStatus, SetInterestInput, UpdateRoomEventInput, } from "./room-events";
|
|
18
18
|
export type { CreateRoomInput, GlobalRole, ModerationSubject, MyRooms, PlayerPositionReport, RoleChangeVerdict, RoomCapabilities, RoomCapability, RoomChannel, RoomDetail, RoomList, RoomMemberEntry, RoomMembers, RoomOccupancy, RoomOwner, RoomPlayerPosition, RoomPresenceEntry, RoomPresenceSnapshot, RoomRestrictionKind, RoomRole, RoomSummary, RoomSurfaceSettings, RoomsApi, RoomVisibility, StaffEntry, UpdateRoomInput, VerifyRoomContext, Viewer, VoiceDecisionReason, VoiceDecisionSubject, VoiceRoomDecision, } from "./rooms";
|
|
19
|
-
export type { AccessKeyOptions, AccessKeyPreparation, Address, ColorScheme, CreateDialogHostOptions, CreateFlowOptions, DialogHost, DialogOpenOptions, FinalizeAccessKeyParams, Flow, FlowConnectorParameters, FlowCookieNames, FlowIdProviderProps, FlowSessionState, FlowState, FlowWidgetHandle, FlowWidgetName, FlowWidgetProps, Listener, LoginOptions, MountFlowWidgetOptions, MountProfileOptions, OpenProfileOptions, PrepareArgsWithAuth, PrepareTransactionRequestPhase, ProfileButtonHandle, ProfileButtonProps, ProfilePosition, ResolveAccountParams, ResolvedAccessKeyOptions, RootCredential,
|
|
19
|
+
export type { AccessKeyOptions, AccessKeyPreparation, Address, ColorScheme, CreateDialogHostOptions, CreateFlowOptions, DialogHost, DialogOpenOptions, FinalizeAccessKeyParams, Flow, FlowConnectorParameters, FlowCookieNames, FlowIdProviderProps, FlowSessionState, FlowState, FlowWidgetHandle, FlowWidgetName, FlowWidgetProps, Listener, LoginOptions, MountFlowWidgetOptions, MountProfileOptions, OpenProfileOptions, PrepareArgsWithAuth, PrepareTransactionRequestPhase, ProfileButtonHandle, ProfileButtonProps, ProfilePosition, ResolveAccountParams, ResolvedAccessKeyOptions, RootCredential, SendCallsArgs, SendTransactionArgs, Session, SigningContext, SignMessageArgs, SignTypedDataArgs, Store, StoredAccessKey, UseOpenProfileOptions, WagmiConnectCapabilities, WagmiConnectParams, } from "./sdk";
|
|
20
20
|
export type { GracefulShutdownCleanup, GracefulShutdownOptions, GracefulShutdownServer, GracefulShutdownSignal, ResolvedFlowSession, ResolveSessionOptions, SessionRouteOptions, SessionRouteResponse, SessionRouteSession, } from "./server";
|
|
21
21
|
export type { CosmeticOffer, PaintDescriptor, SettingDescriptor, Settings, SettingsMigration, StoredSettings, SurfaceRegistry, } from "./settings";
|
|
22
22
|
export type { CreateStudySubjectInput, ListStudySessionsInput, StudyCatalog, StudyCategory, StudyCategoryTotals, StudyField, StudyRange, StudySessionEntry, StudySessionPage, StudySessionsResult, StudySubject, StudySubjectCreation, StudySubjectList, StudySubjectRefusal, StudySubjectRemoval, StudySubjectRemovalResponse, StudySubjectTotals, StudySubjectUpdate, StudySummary, UpdateStudySubjectInput, } from "./study";
|