@flow-industries/id 0.22.0 → 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 +69 -8
- package/contracts/v1/openapi.json +818 -0
- package/contracts/v1/reports.json +188 -0
- package/contracts/v1/sdk-exports.json +1165 -0
- 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/dialog-host.js +11 -6
- package/dist/sdk/client/flow-widget.js +4 -4
- package/dist/sdk/client/idb.d.ts +1 -0
- package/dist/sdk/client/idb.js +24 -0
- package/dist/sdk/client/iframe-host.d.ts +2 -1
- package/dist/sdk/client/iframe-host.js +11 -1
- package/dist/sdk/client/index.d.ts +2 -0
- package/dist/sdk/client/index.js +1 -0
- package/dist/sdk/client/open-profile.js +19 -8
- package/dist/sdk/client/profile-button.d.ts +1 -13
- package/dist/sdk/client/profile-button.js +114 -170
- package/dist/sdk/client/reports.d.ts +9 -0
- package/dist/sdk/client/reports.js +30 -0
- 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/contracts/http.d.ts +155 -0
- package/dist/sdk/contracts/http.js +91 -0
- package/dist/sdk/contracts/reports.d.ts +130 -0
- package/dist/sdk/contracts/reports.js +67 -0
- package/dist/sdk/dialog/remote/Messenger.d.ts +3 -0
- package/dist/sdk/dialog/remote/Messenger.js +9 -3
- 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 +2 -1
- package/dist/sdk/types/landing.d.ts +1 -0
- package/dist/sdk/types/reports.d.ts +7 -0
- package/dist/sdk/types/reports.js +0 -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 +18 -9
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolveIdHost } from "../id-host";
|
|
2
2
|
import { isValidUsername } from "../usernames";
|
|
3
3
|
import { getFlow, requireFlow } from "./create-flow";
|
|
4
|
-
import { answerTokenRequest, bridgeToWindow, hostIdentity, makeIframe, OVERLAY_STYLE, } from "./iframe-host";
|
|
4
|
+
import { answerTokenRequest, bridgeToWindow, hostIdentity, makeIframe, OVERLAY_STYLE, sendBridgeNotification, } from "./iframe-host";
|
|
5
5
|
import { setOverlayFocus } from "./overlay-focus";
|
|
6
6
|
// One overlay per page, kept for the page's lifetime. The alternative — a
|
|
7
7
|
// handle per call site — would put a second fullscreen iframe over the host the
|
|
@@ -30,7 +30,7 @@ function createViewer(options) {
|
|
|
30
30
|
setOverlayFocus(frame, ready && visible);
|
|
31
31
|
}
|
|
32
32
|
function mount(username) {
|
|
33
|
-
const el = makeIframe(`${host}/dialog
|
|
33
|
+
const el = makeIframe(`${host}/dialog/?v=2`, "Flow ID profile");
|
|
34
34
|
el.dataset.flowProfileView = "";
|
|
35
35
|
el.inert = true;
|
|
36
36
|
Object.assign(el.style, OVERLAY_STYLE, { pointerEvents: "none" });
|
|
@@ -47,7 +47,7 @@ function createViewer(options) {
|
|
|
47
47
|
// `/dialog/` and `init` asks it to land on the profile. Sending the route
|
|
48
48
|
// here rather than as a follow-up message keeps the sign-in root from ever
|
|
49
49
|
// mounting, even for the frame it takes to navigate away from it.
|
|
50
|
-
|
|
50
|
+
sendBridgeNotification(opened, "__internal", {
|
|
51
51
|
type: "init",
|
|
52
52
|
mode: "iframe",
|
|
53
53
|
referrer: { title: document.title },
|
|
@@ -90,11 +90,14 @@ function createViewer(options) {
|
|
|
90
90
|
// re-pointed in place. Tearing it down and rebuilding would reload the
|
|
91
91
|
// dialog and lose the open animation on every profile after the first.
|
|
92
92
|
if (mounted)
|
|
93
|
-
|
|
93
|
+
sendBridgeNotification(live, "__internal", {
|
|
94
|
+
type: "profile-view",
|
|
95
|
+
username,
|
|
96
|
+
});
|
|
94
97
|
// Pushed on every open rather than tracked with a subscription: it is only
|
|
95
98
|
// read while a profile is on screen, and sending it here means the "this is
|
|
96
99
|
// you" link is decided from the session as it stands at that moment.
|
|
97
|
-
|
|
100
|
+
sendBridgeNotification(live, "__internal", {
|
|
98
101
|
type: "profile-identity",
|
|
99
102
|
identity: hostIdentity(flow),
|
|
100
103
|
});
|
|
@@ -102,7 +105,7 @@ function createViewer(options) {
|
|
|
102
105
|
onClose = nextOnClose;
|
|
103
106
|
visible = true;
|
|
104
107
|
applyHitTesting();
|
|
105
|
-
|
|
108
|
+
sendBridgeNotification(live, "__internal", { type: "dialog-shown" });
|
|
106
109
|
}
|
|
107
110
|
// Visibility is hit-testing plus a message, and nothing else. The overlay
|
|
108
111
|
// iframe is NEVER set to `display: none` — that pauses the iframe's
|
|
@@ -116,7 +119,7 @@ function createViewer(options) {
|
|
|
116
119
|
visible = false;
|
|
117
120
|
window.removeEventListener("keydown", onKeyDown);
|
|
118
121
|
applyHitTesting();
|
|
119
|
-
|
|
122
|
+
sendBridgeNotification(bridge, "__internal", { type: "dialog-hidden" });
|
|
120
123
|
const notifyClose = onClose;
|
|
121
124
|
onClose = undefined;
|
|
122
125
|
notifyClose?.();
|
|
@@ -131,7 +134,7 @@ function createViewer(options) {
|
|
|
131
134
|
if (next === theme)
|
|
132
135
|
return;
|
|
133
136
|
theme = next;
|
|
134
|
-
|
|
137
|
+
sendBridgeNotification(bridge, "__internal", {
|
|
135
138
|
type: "set-theme",
|
|
136
139
|
theme: { colorScheme: next },
|
|
137
140
|
});
|
|
@@ -176,6 +179,14 @@ export function openProfile(username, options = {}) {
|
|
|
176
179
|
return false;
|
|
177
180
|
if (!("document" in globalThis))
|
|
178
181
|
return false;
|
|
182
|
+
const current = options.flow ?? getFlow();
|
|
183
|
+
if (current?.user?.username === username) {
|
|
184
|
+
closeProfile();
|
|
185
|
+
const opened = current.openAccount();
|
|
186
|
+
if (opened)
|
|
187
|
+
queueMicrotask(() => options.onClose?.());
|
|
188
|
+
return opened;
|
|
189
|
+
}
|
|
179
190
|
const live = viewer ?? createViewer(options);
|
|
180
191
|
viewer = live;
|
|
181
192
|
if (options.theme)
|
|
@@ -1,15 +1,3 @@
|
|
|
1
1
|
import type { MountProfileOptions, ProfileButtonHandle } from "../types";
|
|
2
|
-
/**
|
|
3
|
-
* Mounts the universal Flow ID "profile button" — a persistent inline iframe
|
|
4
|
-
* showing the signed-in user's avatar + username (the collapsed pill). Clicking
|
|
5
|
-
* it opens the profile dialog in a SEPARATE fullscreen overlay iframe, so the
|
|
6
|
-
* pill stays in place behind the dialog's dimmed backdrop rather than morphing
|
|
7
|
-
* into it. Both iframes ride the dialog SPA at `${host}/dialog/` (routes
|
|
8
|
-
* `/dialog/profile-pill` and `/dialog/profile`) so the chrome matches the auth
|
|
9
|
-
* dialog and the widget is first-party to Flow ID (reads `/api/me`, uploads
|
|
10
|
-
* avatars with the cookie session).
|
|
11
|
-
*
|
|
12
|
-
* Resolves the Flow singleton automatically (createFlow() must have run), so
|
|
13
|
-
* callers don't thread the instance through. No-op under SSR.
|
|
14
|
-
*/
|
|
2
|
+
/** Mount a native account button that opens Flow ID in a normal browser tab. */
|
|
15
3
|
export declare function createProfileButton(options: MountProfileOptions): ProfileButtonHandle;
|
|
@@ -1,201 +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, } 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
|
-
bridge.send("__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",
|
|
49
|
+
});
|
|
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",
|
|
101
68
|
});
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
void dialogBridge.send("__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
|
-
void
|
|
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(() => { });
|
|
151
114
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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();
|
|
158
125
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
let lastIdentityKey;
|
|
163
|
-
function pushIdentity() {
|
|
164
|
-
const identity = hostIdentity(flow);
|
|
165
|
-
const key = identity ? `${identity.username}|${identity.isGuest}` : "none";
|
|
166
|
-
if (key === lastIdentityKey)
|
|
167
|
-
return;
|
|
168
|
-
lastIdentityKey = key;
|
|
169
|
-
void pillBridge.send("__internal", { type: "profile-identity", identity });
|
|
170
|
-
if (dialogBridge)
|
|
171
|
-
void dialogBridge.send("__internal", {
|
|
172
|
-
type: "profile-identity",
|
|
173
|
-
identity,
|
|
174
|
-
});
|
|
126
|
+
window.addEventListener("focus", refresh);
|
|
127
|
+
function setTheme(theme) {
|
|
128
|
+
button.style.colorScheme = theme;
|
|
175
129
|
}
|
|
176
|
-
|
|
177
|
-
|
|
130
|
+
setTheme(options.theme ?? "light dark");
|
|
131
|
+
container.append(button, notice, fallback);
|
|
132
|
+
update();
|
|
133
|
+
const unsubscribe = flow.subscribe(update);
|
|
178
134
|
return {
|
|
179
|
-
setTheme
|
|
180
|
-
if (next === theme)
|
|
181
|
-
return;
|
|
182
|
-
theme = next;
|
|
183
|
-
void pillBridge.send("__internal", {
|
|
184
|
-
type: "set-theme",
|
|
185
|
-
theme: { colorScheme: next },
|
|
186
|
-
});
|
|
187
|
-
void dialogBridge?.send("__internal", {
|
|
188
|
-
type: "set-theme",
|
|
189
|
-
theme: { colorScheme: next },
|
|
190
|
-
});
|
|
191
|
-
},
|
|
135
|
+
setTheme,
|
|
192
136
|
destroy() {
|
|
193
|
-
hideDialog();
|
|
194
137
|
unsubscribe();
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
138
|
+
profileRequest?.abort();
|
|
139
|
+
window.removeEventListener("focus", refresh);
|
|
140
|
+
fallback.remove();
|
|
141
|
+
button.remove();
|
|
142
|
+
notice.remove();
|
|
199
143
|
createdContainer?.remove();
|
|
200
144
|
},
|
|
201
145
|
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ReportsApi, ReportTransport } from "../types/reports";
|
|
2
|
+
export type { ReportOperation, ReportRequest, ReportResponse, } from "../types/reports";
|
|
3
|
+
export declare class ReportRequestError extends Error {
|
|
4
|
+
readonly status: number;
|
|
5
|
+
readonly response: string;
|
|
6
|
+
constructor(status: number, response: string);
|
|
7
|
+
}
|
|
8
|
+
/** Typed service reports. Keep the reporting bearer on trusted servers only. */
|
|
9
|
+
export declare function createReportsApi(host: string, token: string, transport?: ReportTransport): ReportsApi;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { httpOperations } from "../contracts/http";
|
|
2
|
+
export class ReportRequestError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
response;
|
|
5
|
+
constructor(status, response) {
|
|
6
|
+
super(`Flow report failed (${status})`);
|
|
7
|
+
this.status = status;
|
|
8
|
+
this.response = response;
|
|
9
|
+
this.name = "ReportRequestError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** Typed service reports. Keep the reporting bearer on trusted servers only. */
|
|
13
|
+
export function createReportsApi(host, token, transport = fetch) {
|
|
14
|
+
return async function report(operation, body) {
|
|
15
|
+
const contract = httpOperations[operation];
|
|
16
|
+
const request = contract.request.parse(body);
|
|
17
|
+
const response = await transport(new URL(contract.path, host), {
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: {
|
|
20
|
+
authorization: `Bearer ${token}`,
|
|
21
|
+
"content-type": "application/json",
|
|
22
|
+
},
|
|
23
|
+
body: JSON.stringify(request),
|
|
24
|
+
});
|
|
25
|
+
if (!response.ok)
|
|
26
|
+
throw new ReportRequestError(response.status, await response.text());
|
|
27
|
+
// SAFETY: the selected operation owns both this response schema and the mapped response type.
|
|
28
|
+
return contract.response.parse(await response.json());
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -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"),
|