@flow-industries/id 0.21.0 → 0.21.2
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 +32 -0
- package/dist/sdk/client/create-flow.js +1 -0
- package/dist/sdk/client/dialog-host.js +13 -1
- package/dist/sdk/client/flow-widget.js +6 -1
- package/dist/sdk/client/iframe-host.d.ts +5 -3
- package/dist/sdk/client/iframe-host.js +6 -3
- package/dist/sdk/client/open-profile.js +23 -6
- package/dist/sdk/client/overlay-focus.d.ts +1 -0
- package/dist/sdk/client/overlay-focus.js +24 -0
- package/dist/sdk/client/profile-button.js +9 -4
- package/dist/sdk/react/open-profile.js +2 -2
- package/dist/sdk/types/account-data.d.ts +269 -0
- package/dist/sdk/types/account-data.js +0 -0
- package/dist/sdk/types/events.d.ts +5 -1
- package/dist/sdk/types/index.d.ts +1 -0
- package/dist/sdk/types/sdk.d.ts +10 -6
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -47,6 +47,38 @@ Full documentation at **[docs.flow.industries/en/auth](https://docs.flow.industr
|
|
|
47
47
|
- [JWT verify](https://docs.flow.industries/en/auth/jwt-verify) - verify Flow sessions on your backend
|
|
48
48
|
- [API reference](https://docs.flow.industries/en/auth/api) and [self-hosting](https://docs.flow.industries/en/auth/self-hosting)
|
|
49
49
|
|
|
50
|
+
## Public profile overlays
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { closeProfile, openProfile } from "@flow-industries/id";
|
|
54
|
+
import { isValidUsername } from "@flow-industries/id/usernames";
|
|
55
|
+
|
|
56
|
+
if (isValidUsername(username)) {
|
|
57
|
+
openProfile(username, { onClose: () => resumeInput() });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
closeProfile();
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`@flow-industries/id/usernames` also exports `USERNAME_REGEX` and
|
|
64
|
+
`MAX_USERNAME_LENGTH`. It has no browser or server dependencies.
|
|
65
|
+
|
|
66
|
+
`OpenProfileOptions.onClose?: () => void` fires once after the overlay stops
|
|
67
|
+
accepting input, before its exit animation finishes. It covers the close button,
|
|
68
|
+
backdrop, Escape, `closeProfile()`, and sign-in/sign-out handoffs. The latest
|
|
69
|
+
successful `openProfile` call owns the callback, even when switching profiles
|
|
70
|
+
while open; an omitted callback clears the previous one. Invalid usernames and
|
|
71
|
+
SSR return `false` without replacing it. Closing an already closed overlay is a
|
|
72
|
+
no-op. A callback may open another profile.
|
|
73
|
+
|
|
74
|
+
React callers pass the same option to
|
|
75
|
+
`useOpenProfile({ onClose: () => resumeInput() })` from
|
|
76
|
+
`@flow-industries/id/react`; the callback is captured when its returned opener
|
|
77
|
+
is called. The overlay belongs to the page and survives component unmounts.
|
|
78
|
+
|
|
79
|
+
Open from the document that owns the full viewport. Canvas hosts must release
|
|
80
|
+
pointer lock before opening, then use `onClose` to restore their input state.
|
|
81
|
+
|
|
50
82
|
## License
|
|
51
83
|
|
|
52
84
|
MIT
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { bridgeToWindow, makeIframe } from "./iframe-host";
|
|
2
|
+
import { setOverlayFocus } from "./overlay-focus";
|
|
2
3
|
const HIDDEN_STYLE = {
|
|
3
4
|
position: "fixed",
|
|
4
5
|
inset: "0",
|
|
@@ -35,6 +36,12 @@ export function createDialogHost(options) {
|
|
|
35
36
|
const { host, container = document.body } = options;
|
|
36
37
|
let iframe = null;
|
|
37
38
|
let messenger = null;
|
|
39
|
+
let opener = null;
|
|
40
|
+
function captureFocus() {
|
|
41
|
+
const active = document.activeElement;
|
|
42
|
+
if (active instanceof HTMLElement && active !== iframe)
|
|
43
|
+
opener = active;
|
|
44
|
+
}
|
|
38
45
|
const pending = new Map();
|
|
39
46
|
/**
|
|
40
47
|
* Mounts the iframe and wires up the postMessage bridge. Idempotent — safe
|
|
@@ -43,8 +50,9 @@ export function createDialogHost(options) {
|
|
|
43
50
|
function ensureFrame() {
|
|
44
51
|
if (iframe)
|
|
45
52
|
return;
|
|
46
|
-
iframe = makeIframe(`${host}
|
|
53
|
+
iframe = makeIframe(`${host}`, "Flow ID");
|
|
47
54
|
iframe.dataset.flowId = "";
|
|
55
|
+
iframe.inert = true;
|
|
48
56
|
Object.assign(iframe.style, HIDDEN_STYLE);
|
|
49
57
|
container.appendChild(iframe);
|
|
50
58
|
// Bind the bridge to this iframe's own window — a page may host more than
|
|
@@ -109,6 +117,8 @@ export function createDialogHost(options) {
|
|
|
109
117
|
if (!iframe)
|
|
110
118
|
return;
|
|
111
119
|
Object.assign(iframe.style, VISIBLE_STYLE);
|
|
120
|
+
setOverlayFocus(iframe, true, opener);
|
|
121
|
+
opener = null;
|
|
112
122
|
// The dialog owns its open/close animation and mounts the overlay (playing
|
|
113
123
|
// the enter animation) on this signal — same mechanism as the profile
|
|
114
124
|
// widget, so every dialog appears and disappears identically.
|
|
@@ -124,6 +134,7 @@ export function createDialogHost(options) {
|
|
|
124
134
|
// requestAnimationFrame, making the close (and the next open) skip straight
|
|
125
135
|
// to the end. While hidden the overlay is transparent and click-through.
|
|
126
136
|
iframe.style.pointerEvents = "none";
|
|
137
|
+
setOverlayFocus(iframe, false);
|
|
127
138
|
void (
|
|
128
139
|
// SAFETY: the payload map types this topic; the dialog reads exactly these fields.
|
|
129
140
|
messenger?.send("__internal", { type: "dialog-hidden" }));
|
|
@@ -190,6 +201,7 @@ export function createDialogHost(options) {
|
|
|
190
201
|
return dispatchRequest(method, params);
|
|
191
202
|
}
|
|
192
203
|
return {
|
|
204
|
+
captureFocus,
|
|
193
205
|
open,
|
|
194
206
|
close,
|
|
195
207
|
destroy,
|
|
@@ -11,6 +11,11 @@ const WIDGET_ROUTE = {
|
|
|
11
11
|
xp: "/dialog/xp-widget",
|
|
12
12
|
"action-timer": "/dialog/action-timer",
|
|
13
13
|
};
|
|
14
|
+
// `satisfies` the full widget union so a new widget cannot ship untitled.
|
|
15
|
+
const WIDGET_TITLE = {
|
|
16
|
+
xp: "Flow ID XP",
|
|
17
|
+
"action-timer": "Flow ID action timer",
|
|
18
|
+
};
|
|
14
19
|
/**
|
|
15
20
|
* Mounts one of the inline Flow ID widgets — the XP strip, the action timer —
|
|
16
21
|
* as a transparent iframe the embedder positions and sizes itself.
|
|
@@ -42,7 +47,7 @@ export function createFlowWidget(options) {
|
|
|
42
47
|
const hostOrigin = new URL(host).origin;
|
|
43
48
|
let theme = options.theme ?? "light dark";
|
|
44
49
|
const route = WIDGET_ROUTE[options.widget];
|
|
45
|
-
const frame = makeIframe(`${host}${route}
|
|
50
|
+
const frame = makeIframe(`${host}${route}`, WIDGET_TITLE[options.widget]);
|
|
46
51
|
frame.style.background = "transparent";
|
|
47
52
|
const container = options.container ?? document.body;
|
|
48
53
|
// Appended before bridging: `contentWindow` is null until the frame is in the
|
|
@@ -16,10 +16,12 @@ export declare const OVERLAY_STYLE: Partial<CSSStyleDeclaration>;
|
|
|
16
16
|
/**
|
|
17
17
|
* Creates a Flow dialog iframe element: WebAuthn-permitted, borderless, and
|
|
18
18
|
* `color-scheme: normal` so the iframe itself stays transparent (the rendered
|
|
19
|
-
* theme is applied to nested card wrappers, not the iframe).
|
|
20
|
-
*
|
|
19
|
+
* theme is applied to nested card wrappers, not the iframe). `title` is the
|
|
20
|
+
* accessible name screen readers announce for the frame (WCAG 4.1.2), so every
|
|
21
|
+
* Flow surface must say what it is. The caller owns positioning/visibility and
|
|
22
|
+
* any `data-*` marker.
|
|
21
23
|
*/
|
|
22
|
-
export declare function makeIframe(src: string): HTMLIFrameElement;
|
|
24
|
+
export declare function makeIframe(src: string, title: string): HTMLIFrameElement;
|
|
23
25
|
/**
|
|
24
26
|
* Builds a postMessage bridge to a child window (iframe `contentWindow` or
|
|
25
27
|
* popup). Inbound is filtered by `source` so multiple same-origin Flow frames
|
|
@@ -21,12 +21,15 @@ export const OVERLAY_STYLE = {
|
|
|
21
21
|
/**
|
|
22
22
|
* Creates a Flow dialog iframe element: WebAuthn-permitted, borderless, and
|
|
23
23
|
* `color-scheme: normal` so the iframe itself stays transparent (the rendered
|
|
24
|
-
* theme is applied to nested card wrappers, not the iframe).
|
|
25
|
-
*
|
|
24
|
+
* theme is applied to nested card wrappers, not the iframe). `title` is the
|
|
25
|
+
* accessible name screen readers announce for the frame (WCAG 4.1.2), so every
|
|
26
|
+
* Flow surface must say what it is. The caller owns positioning/visibility and
|
|
27
|
+
* any `data-*` marker.
|
|
26
28
|
*/
|
|
27
|
-
export function makeIframe(src) {
|
|
29
|
+
export function makeIframe(src, title) {
|
|
28
30
|
const frame = document.createElement("iframe");
|
|
29
31
|
frame.src = src;
|
|
32
|
+
frame.title = title;
|
|
30
33
|
frame.allow = IFRAME_ALLOW;
|
|
31
34
|
frame.style.border = "none";
|
|
32
35
|
frame.style.colorScheme = "normal";
|
|
@@ -2,6 +2,7 @@ import { resolveIdHost } from "../id-host";
|
|
|
2
2
|
import { isValidUsername } from "../usernames";
|
|
3
3
|
import { getFlow, requireFlow } from "./create-flow";
|
|
4
4
|
import { answerTokenRequest, bridgeToWindow, hostIdentity, makeIframe, OVERLAY_STYLE, } from "./iframe-host";
|
|
5
|
+
import { setOverlayFocus } from "./overlay-focus";
|
|
5
6
|
// One overlay per page, kept for the page's lifetime. The alternative — a
|
|
6
7
|
// handle per call site — would put a second fullscreen iframe over the host the
|
|
7
8
|
// moment two parts of an app (a scoreboard and a chat line) both showed a
|
|
@@ -21,13 +22,17 @@ function createViewer(options) {
|
|
|
21
22
|
// meant for the page underneath with nothing on screen to explain it.
|
|
22
23
|
let ready = false;
|
|
23
24
|
let visible = false;
|
|
25
|
+
let onClose;
|
|
24
26
|
function applyHitTesting() {
|
|
25
|
-
if (frame)
|
|
26
|
-
|
|
27
|
+
if (!frame)
|
|
28
|
+
return;
|
|
29
|
+
frame.style.pointerEvents = ready && visible ? "auto" : "none";
|
|
30
|
+
setOverlayFocus(frame, ready && visible);
|
|
27
31
|
}
|
|
28
32
|
function mount(username) {
|
|
29
|
-
const el = makeIframe(`${host}/dialog
|
|
33
|
+
const el = makeIframe(`${host}/dialog/`, "Flow ID profile");
|
|
30
34
|
el.dataset.flowProfileView = "";
|
|
35
|
+
el.inert = true;
|
|
31
36
|
Object.assign(el.style, OVERLAY_STYLE, { pointerEvents: "none" });
|
|
32
37
|
// Appended before bridging: `contentWindow` is null until the frame is in
|
|
33
38
|
// the document, and the bridge is bound to that window.
|
|
@@ -78,7 +83,7 @@ function createViewer(options) {
|
|
|
78
83
|
});
|
|
79
84
|
return opened;
|
|
80
85
|
}
|
|
81
|
-
function open(username) {
|
|
86
|
+
function open(username, nextOnClose) {
|
|
82
87
|
const mounted = bridge;
|
|
83
88
|
const live = mounted ?? mount(username);
|
|
84
89
|
// A frame created just now is already on this profile; an existing one is
|
|
@@ -93,6 +98,8 @@ function createViewer(options) {
|
|
|
93
98
|
type: "profile-identity",
|
|
94
99
|
identity: hostIdentity(flow),
|
|
95
100
|
});
|
|
101
|
+
window.addEventListener("keydown", onKeyDown);
|
|
102
|
+
onClose = nextOnClose;
|
|
96
103
|
visible = true;
|
|
97
104
|
applyHitTesting();
|
|
98
105
|
void live.send("__internal", { type: "dialog-shown" });
|
|
@@ -104,11 +111,21 @@ function createViewer(options) {
|
|
|
104
111
|
// always-rendered, transparent and click-through while hidden keeps every
|
|
105
112
|
// open animation reliable, on the second open and the tenth.
|
|
106
113
|
function close() {
|
|
107
|
-
if (!frame || !bridge)
|
|
114
|
+
if (!visible || !frame || !bridge)
|
|
108
115
|
return;
|
|
109
116
|
visible = false;
|
|
117
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
110
118
|
applyHitTesting();
|
|
111
119
|
void bridge.send("__internal", { type: "dialog-hidden" });
|
|
120
|
+
const notifyClose = onClose;
|
|
121
|
+
onClose = undefined;
|
|
122
|
+
notifyClose?.();
|
|
123
|
+
}
|
|
124
|
+
function onKeyDown(event) {
|
|
125
|
+
if (event.key !== "Escape" || event.defaultPrevented)
|
|
126
|
+
return;
|
|
127
|
+
event.preventDefault();
|
|
128
|
+
close();
|
|
112
129
|
}
|
|
113
130
|
function setTheme(next) {
|
|
114
131
|
if (next === theme)
|
|
@@ -163,7 +180,7 @@ export function openProfile(username, options = {}) {
|
|
|
163
180
|
viewer = live;
|
|
164
181
|
if (options.theme)
|
|
165
182
|
live.setTheme(options.theme);
|
|
166
|
-
live.open(username);
|
|
183
|
+
live.open(username, options.onClose);
|
|
167
184
|
return true;
|
|
168
185
|
}
|
|
169
186
|
/**
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function setOverlayFocus(frame: HTMLIFrameElement, visible: boolean, opener?: HTMLElement | null): void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const triggers = new WeakMap();
|
|
2
|
+
export function setOverlayFocus(frame, visible, opener) {
|
|
3
|
+
if (visible) {
|
|
4
|
+
if (triggers.has(frame))
|
|
5
|
+
return;
|
|
6
|
+
const active = opener ?? document.activeElement;
|
|
7
|
+
triggers.set(frame, active instanceof HTMLElement && active !== frame ? active : null);
|
|
8
|
+
frame.inert = false;
|
|
9
|
+
frame.focus();
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const restore = document.activeElement === frame;
|
|
13
|
+
frame.inert = true;
|
|
14
|
+
const trigger = triggers.get(frame);
|
|
15
|
+
triggers.delete(frame);
|
|
16
|
+
if (restore && trigger?.isConnected) {
|
|
17
|
+
requestAnimationFrame(() => {
|
|
18
|
+
if (trigger.isConnected &&
|
|
19
|
+
(document.activeElement === frame ||
|
|
20
|
+
document.activeElement === document.body))
|
|
21
|
+
trigger.focus();
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolveIdHost } from "../id-host";
|
|
2
2
|
import { getFlow, requireFlow } from "./create-flow";
|
|
3
3
|
import { answerTokenRequest, bridgeToWindow, hostIdentity, makeIframe, OVERLAY_STYLE, } from "./iframe-host";
|
|
4
|
+
import { setOverlayFocus } from "./overlay-focus";
|
|
4
5
|
const POSITION_STYLE = {
|
|
5
6
|
"top-right": { top: "0", right: "0" },
|
|
6
7
|
"top-left": { top: "0", left: "0" },
|
|
@@ -46,8 +47,8 @@ export function createProfileButton(options) {
|
|
|
46
47
|
createdContainer = el;
|
|
47
48
|
container = el;
|
|
48
49
|
}
|
|
49
|
-
function makeFrame() {
|
|
50
|
-
const frame = makeIframe(`${host}/dialog
|
|
50
|
+
function makeFrame(title) {
|
|
51
|
+
const frame = makeIframe(`${host}/dialog/`, title);
|
|
51
52
|
frame.dataset.flowProfile = "";
|
|
52
53
|
frame.style.display = "block";
|
|
53
54
|
return frame;
|
|
@@ -71,7 +72,7 @@ export function createProfileButton(options) {
|
|
|
71
72
|
return bridge;
|
|
72
73
|
}
|
|
73
74
|
// ----- the persistent pill (stays mounted; sizes to its own content) -----
|
|
74
|
-
const pill = makeFrame();
|
|
75
|
+
const pill = makeFrame("Flow ID profile button");
|
|
75
76
|
let pillWidth = 0;
|
|
76
77
|
let pillHeight = 0;
|
|
77
78
|
function applyPillSize() {
|
|
@@ -105,7 +106,8 @@ export function createProfileButton(options) {
|
|
|
105
106
|
function ensureDialog() {
|
|
106
107
|
if (dialog)
|
|
107
108
|
return;
|
|
108
|
-
dialog = makeFrame();
|
|
109
|
+
dialog = makeFrame("Flow ID profile");
|
|
110
|
+
dialog.inert = true;
|
|
109
111
|
Object.assign(dialog.style, OVERLAY_STYLE, {
|
|
110
112
|
pointerEvents: "none",
|
|
111
113
|
});
|
|
@@ -144,12 +146,14 @@ export function createProfileButton(options) {
|
|
|
144
146
|
if (!dialog)
|
|
145
147
|
return;
|
|
146
148
|
dialog.style.pointerEvents = "auto";
|
|
149
|
+
setOverlayFocus(dialog, true);
|
|
147
150
|
void dialogBridge?.send("__internal", { type: "dialog-shown" });
|
|
148
151
|
}
|
|
149
152
|
function hideDialog() {
|
|
150
153
|
if (!dialog)
|
|
151
154
|
return;
|
|
152
155
|
dialog.style.pointerEvents = "none";
|
|
156
|
+
setOverlayFocus(dialog, false);
|
|
153
157
|
void dialogBridge?.send("__internal", { type: "dialog-hidden" });
|
|
154
158
|
}
|
|
155
159
|
// ----- keep both iframes' identity in sync with the Flow session -----
|
|
@@ -186,6 +190,7 @@ export function createProfileButton(options) {
|
|
|
186
190
|
});
|
|
187
191
|
},
|
|
188
192
|
destroy() {
|
|
193
|
+
hideDialog();
|
|
189
194
|
unsubscribe();
|
|
190
195
|
pillBridge.destroy();
|
|
191
196
|
dialogBridge?.destroy();
|
|
@@ -19,6 +19,6 @@ import { useFlow } from "./hooks";
|
|
|
19
19
|
*/
|
|
20
20
|
export function useOpenProfile(options = {}) {
|
|
21
21
|
const flow = useFlow();
|
|
22
|
-
const { host, theme } = options;
|
|
23
|
-
return useCallback((username) => openProfile(username, { flow, host, theme }), [flow, host, theme]);
|
|
22
|
+
const { host, theme, onClose } = options;
|
|
23
|
+
return useCallback((username) => openProfile(username, { flow, host, theme, onClose }), [flow, host, theme, onClose]);
|
|
24
24
|
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import type { RawRecord, RawValue } from "../json";
|
|
2
|
+
import type { AccountSession, SecurityActivityEntry } from "./auth";
|
|
3
|
+
import type { RoomEventRecurrence } from "./room-events";
|
|
4
|
+
/**
|
|
5
|
+
* Self-serve account deletion (AUTH-246), the personal-data export (AUTH-247)
|
|
6
|
+
* and the erasure ledger downstream services replay (AUTH-119).
|
|
7
|
+
*/
|
|
8
|
+
/** Body of `POST /api/account/delete`: the caller's current username, typed
|
|
9
|
+
* back as the confirmation. */
|
|
10
|
+
export type AccountDeleteRequest = {
|
|
11
|
+
confirm: string;
|
|
12
|
+
};
|
|
13
|
+
export type AccountDeleteResponse = {
|
|
14
|
+
ok: true;
|
|
15
|
+
};
|
|
16
|
+
/** What one account deletion removed, beyond the user row's own cascade. */
|
|
17
|
+
export type AccountErasureCounts = {
|
|
18
|
+
/** Non-system rooms the account owned, deleted with everything inside them. */
|
|
19
|
+
ownedRoomsDeleted: number;
|
|
20
|
+
/** Cookie sessions the cascade took down. */
|
|
21
|
+
sessionsDeleted: number;
|
|
22
|
+
/** Passkeys the cascade took down. */
|
|
23
|
+
passkeysDeleted: number;
|
|
24
|
+
/** OTP / quota rows keyed on the account's email or id. */
|
|
25
|
+
verificationsDeleted: number;
|
|
26
|
+
/** Per-user rate-limit rows (never the ip-keyed ones). */
|
|
27
|
+
rateLimitsDeleted: number;
|
|
28
|
+
};
|
|
29
|
+
/** Result of `deleteAccount`: `missing` is the idempotent second call. */
|
|
30
|
+
export type AccountErasureResult = {
|
|
31
|
+
outcome: "missing";
|
|
32
|
+
} | {
|
|
33
|
+
outcome: "deleted";
|
|
34
|
+
userId: string;
|
|
35
|
+
isGuest: boolean;
|
|
36
|
+
/** The stored avatar URL, for the post-commit object delete. */
|
|
37
|
+
image: string | null;
|
|
38
|
+
counts: AccountErasureCounts;
|
|
39
|
+
};
|
|
40
|
+
/** One row of the erasure ledger as `GET /api/account/erasures` serves it. */
|
|
41
|
+
export type AccountErasureEntry = {
|
|
42
|
+
seq: number;
|
|
43
|
+
/** The deleted account's Flow `user.id` — the JWT `sub` consumers keyed on. */
|
|
44
|
+
sub: string;
|
|
45
|
+
deletedRoomIds: string[];
|
|
46
|
+
erasedAt: string;
|
|
47
|
+
};
|
|
48
|
+
export type AccountErasureFeed = {
|
|
49
|
+
/** Ascending by `seq`. */
|
|
50
|
+
erasures: AccountErasureEntry[];
|
|
51
|
+
/** Feed this back as `after` for the next page; equals the request's
|
|
52
|
+
* `after` when the page was empty. */
|
|
53
|
+
nextAfter: number;
|
|
54
|
+
};
|
|
55
|
+
export type AccountExportAccount = {
|
|
56
|
+
id: string;
|
|
57
|
+
username: string;
|
|
58
|
+
name: string | null;
|
|
59
|
+
email: string | null;
|
|
60
|
+
emailVerified: boolean;
|
|
61
|
+
isGuest: boolean;
|
|
62
|
+
role: string;
|
|
63
|
+
image: string | null;
|
|
64
|
+
bio: string | null;
|
|
65
|
+
walletAddress: string | null;
|
|
66
|
+
subscribedToNews: boolean;
|
|
67
|
+
createdAt: string;
|
|
68
|
+
};
|
|
69
|
+
/** A passkey as the owner sees it: metadata only, never the public key,
|
|
70
|
+
* credential id or signature counter. */
|
|
71
|
+
export type AccountExportPasskey = {
|
|
72
|
+
id: string;
|
|
73
|
+
name: string | null;
|
|
74
|
+
deviceType: string | null;
|
|
75
|
+
backedUp: boolean | null;
|
|
76
|
+
transports: string | null;
|
|
77
|
+
aaguid: string | null;
|
|
78
|
+
createdAt: string;
|
|
79
|
+
};
|
|
80
|
+
export type AccountExportSettingsSurface = {
|
|
81
|
+
version: number;
|
|
82
|
+
settings: Record<string, RawValue>;
|
|
83
|
+
};
|
|
84
|
+
export type AccountExportXpSummary = {
|
|
85
|
+
totalXp: number;
|
|
86
|
+
flowScore: number;
|
|
87
|
+
lastVisitDate: string | null;
|
|
88
|
+
updatedAt: string;
|
|
89
|
+
};
|
|
90
|
+
export type AccountExportXpGrant = {
|
|
91
|
+
id: string;
|
|
92
|
+
source: string;
|
|
93
|
+
baseAmount: number;
|
|
94
|
+
flowScore: number;
|
|
95
|
+
amount: number;
|
|
96
|
+
metadata: RawRecord | null;
|
|
97
|
+
createdAt: string;
|
|
98
|
+
};
|
|
99
|
+
export type AccountExportActionEvent = {
|
|
100
|
+
id: string;
|
|
101
|
+
sessionId: string;
|
|
102
|
+
kind: string;
|
|
103
|
+
accruedSeconds: number;
|
|
104
|
+
at: string;
|
|
105
|
+
};
|
|
106
|
+
export type AccountExportRoomSettings = {
|
|
107
|
+
roomId: string;
|
|
108
|
+
surface: string;
|
|
109
|
+
settings: RawRecord;
|
|
110
|
+
version: number;
|
|
111
|
+
};
|
|
112
|
+
export type AccountExportActionSession = {
|
|
113
|
+
id: string;
|
|
114
|
+
source: string;
|
|
115
|
+
state: string;
|
|
116
|
+
subjectId: string | null;
|
|
117
|
+
accruedSeconds: number;
|
|
118
|
+
focusedSeconds: number;
|
|
119
|
+
xpGranted: number | null;
|
|
120
|
+
metadata: RawRecord | null;
|
|
121
|
+
startedAt: string;
|
|
122
|
+
updatedAt: string;
|
|
123
|
+
finishedAt: string | null;
|
|
124
|
+
};
|
|
125
|
+
export type AccountExportStudySubject = {
|
|
126
|
+
id: string;
|
|
127
|
+
field: string | null;
|
|
128
|
+
name: string;
|
|
129
|
+
archivedAt: string | null;
|
|
130
|
+
createdAt: string;
|
|
131
|
+
lastUsedAt: string;
|
|
132
|
+
};
|
|
133
|
+
export type AccountExportCharacterStateTime = {
|
|
134
|
+
day: string;
|
|
135
|
+
state: string;
|
|
136
|
+
seconds: number;
|
|
137
|
+
updatedAt: string;
|
|
138
|
+
};
|
|
139
|
+
export type AccountExportOwnedRoom = {
|
|
140
|
+
id: string;
|
|
141
|
+
slug: string;
|
|
142
|
+
displayName: string;
|
|
143
|
+
description: string | null;
|
|
144
|
+
visibility: string;
|
|
145
|
+
isSystem: boolean;
|
|
146
|
+
createdAt: string;
|
|
147
|
+
updatedAt: string;
|
|
148
|
+
};
|
|
149
|
+
export type AccountExportRoomMembership = {
|
|
150
|
+
roomId: string;
|
|
151
|
+
roomSlug: string;
|
|
152
|
+
role: string;
|
|
153
|
+
joinedAt: string;
|
|
154
|
+
};
|
|
155
|
+
/** A restriction placed on the caller. The issuing moderator is deliberately
|
|
156
|
+
* absent — the export carries only the caller's own identity. */
|
|
157
|
+
export type AccountExportRoomRestriction = {
|
|
158
|
+
id: string;
|
|
159
|
+
roomId: string;
|
|
160
|
+
kind: string;
|
|
161
|
+
reason: string | null;
|
|
162
|
+
expiresAt: string | null;
|
|
163
|
+
createdAt: string;
|
|
164
|
+
};
|
|
165
|
+
export type AccountExportRoomPresence = {
|
|
166
|
+
roomId: string;
|
|
167
|
+
channel: string;
|
|
168
|
+
joinedAt: string;
|
|
169
|
+
updatedAt: string;
|
|
170
|
+
};
|
|
171
|
+
export type AccountExportRoomPlayerState = {
|
|
172
|
+
roomId: string;
|
|
173
|
+
x: number;
|
|
174
|
+
y: number;
|
|
175
|
+
z: number;
|
|
176
|
+
yaw: number;
|
|
177
|
+
worldId: string;
|
|
178
|
+
updatedAt: string;
|
|
179
|
+
};
|
|
180
|
+
export type AccountExportRooms = {
|
|
181
|
+
settings: AccountExportRoomSettings[];
|
|
182
|
+
owned: AccountExportOwnedRoom[];
|
|
183
|
+
memberships: AccountExportRoomMembership[];
|
|
184
|
+
restrictions: AccountExportRoomRestriction[];
|
|
185
|
+
presence: AccountExportRoomPresence[];
|
|
186
|
+
playerState: AccountExportRoomPlayerState[];
|
|
187
|
+
};
|
|
188
|
+
export type AccountExportAuthoredEvent = {
|
|
189
|
+
id: string;
|
|
190
|
+
roomId: string;
|
|
191
|
+
title: string;
|
|
192
|
+
description: string | null;
|
|
193
|
+
coverUrl: string | null;
|
|
194
|
+
startsAt: string;
|
|
195
|
+
endsAt: string | null;
|
|
196
|
+
timezone: string;
|
|
197
|
+
recurrence: RoomEventRecurrence | null;
|
|
198
|
+
reminderOffsets: number[] | null;
|
|
199
|
+
status: string;
|
|
200
|
+
createdAt: string;
|
|
201
|
+
updatedAt: string;
|
|
202
|
+
};
|
|
203
|
+
export type AccountExportRsvp = {
|
|
204
|
+
occurrenceId: string;
|
|
205
|
+
createdAt: string;
|
|
206
|
+
};
|
|
207
|
+
export type AccountExportFollow = {
|
|
208
|
+
eventId: string;
|
|
209
|
+
createdAt: string;
|
|
210
|
+
};
|
|
211
|
+
export type AccountExportAttendance = {
|
|
212
|
+
occurrenceId: string;
|
|
213
|
+
firstSeenAt: string;
|
|
214
|
+
lastSeenAt: string;
|
|
215
|
+
};
|
|
216
|
+
export type AccountExportRoomEvents = {
|
|
217
|
+
authored: AccountExportAuthoredEvent[];
|
|
218
|
+
rsvps: AccountExportRsvp[];
|
|
219
|
+
follows: AccountExportFollow[];
|
|
220
|
+
attendance: AccountExportAttendance[];
|
|
221
|
+
};
|
|
222
|
+
export type AccountExportGameEvent = {
|
|
223
|
+
id: string;
|
|
224
|
+
event: string;
|
|
225
|
+
occurredAt: string;
|
|
226
|
+
recordedAt: string;
|
|
227
|
+
serverId: string;
|
|
228
|
+
sessionId: string | null;
|
|
229
|
+
roomId: string | null;
|
|
230
|
+
roomSlug: string | null;
|
|
231
|
+
reason: string | null;
|
|
232
|
+
detail: RawRecord | null;
|
|
233
|
+
};
|
|
234
|
+
/** Where the rest of a person's Flow data lives, so one export names every
|
|
235
|
+
* service that has to be asked. */
|
|
236
|
+
export type AccountExportOtherService = {
|
|
237
|
+
service: string;
|
|
238
|
+
export: string;
|
|
239
|
+
holds: string;
|
|
240
|
+
};
|
|
241
|
+
/** The whole personal-data export (`GET /api/account/export`), version 1. */
|
|
242
|
+
export type AccountExport = {
|
|
243
|
+
format: "flow-id-export";
|
|
244
|
+
version: 1;
|
|
245
|
+
exportedAt: string;
|
|
246
|
+
account: AccountExportAccount;
|
|
247
|
+
passkeys: AccountExportPasskey[];
|
|
248
|
+
sessions: AccountSession[];
|
|
249
|
+
settings: Record<string, AccountExportSettingsSurface>;
|
|
250
|
+
xp: {
|
|
251
|
+
summary: AccountExportXpSummary | null;
|
|
252
|
+
ledger: AccountExportXpGrant[];
|
|
253
|
+
};
|
|
254
|
+
actions: {
|
|
255
|
+
events: AccountExportActionEvent[];
|
|
256
|
+
sessions: AccountExportActionSession[];
|
|
257
|
+
};
|
|
258
|
+
study: {
|
|
259
|
+
subjects: AccountExportStudySubject[];
|
|
260
|
+
};
|
|
261
|
+
characterStateTime: AccountExportCharacterStateTime[];
|
|
262
|
+
rooms: AccountExportRooms;
|
|
263
|
+
roomEvents: AccountExportRoomEvents;
|
|
264
|
+
gameEvents: AccountExportGameEvent[];
|
|
265
|
+
securityActivity: SecurityActivityEntry[];
|
|
266
|
+
otherServices: AccountExportOtherService[];
|
|
267
|
+
/** Dotted names of every collection that hit the row cap (newest rows kept). */
|
|
268
|
+
truncated: string[];
|
|
269
|
+
};
|
|
File without changes
|
|
@@ -9,7 +9,7 @@ export type AuthOutcome = "success" | "failure" | "info";
|
|
|
9
9
|
export type AuthMode = "sign-up" | "sign-in";
|
|
10
10
|
/** Client-only funnel steps reported via the `/api/events` beacon. */
|
|
11
11
|
export type FunnelStep = "mode_selected" | "email_entered" | "ceremony_started" | "ceremony_failed" | "done_shown";
|
|
12
|
-
export type AuthEventName = "auth.username.checked" | "auth.signup.succeeded" | "auth.signup.failed" | "auth.otp.sent" | "auth.otp.verified" | "auth.otp.failed" | "auth.email.verify.sent" | "auth.email.verify.succeeded" | "auth.email.verify.failed" | "auth.email.changed" | "auth.avatar.upload.succeeded" | "auth.avatar.upload.failed" | "auth.bio.update.succeeded" | "auth.bio.update.failed" | "auth.profile.view.failed" | "auth.profile.view.uncapped" | "auth.challenge.issued" | "auth.signin.succeeded" | "auth.signin.failed" | "auth.passkey.added" | "auth.passkey.add_failed" | "auth.restore.succeeded" | "auth.restore.failed" | "auth.restore.no_session" | "auth.refresh.succeeded" | "auth.refresh.failed" | "auth.refresh.reuse" | "auth.refresh.grace" | "auth.jwt.verified" | "auth.jwt.rejected" | "auth.signout" | "auth.session.revoked" | "auth.session.revoked_all" | "auth.audience.rejected" | "auth.audience.minted" | "auth.guest.created" | "auth.guest.restored" | "auth.guest.upgraded" | "auth.guest.failed" | "auth.role.granted" | "auth.role.revoked" | "room.restriction.issued" | "room.restriction.lifted" | "auth.funnel.mode_selected" | "auth.funnel.email_entered" | "auth.funnel.ceremony_started" | "auth.funnel.ceremony_failed" | "auth.funnel.done_shown" | "room_event.activated" | "room_event.completed" | "room_event.reminder_dispatched" | "room_event.reminder_failed";
|
|
12
|
+
export type AuthEventName = "auth.username.checked" | "auth.signup.succeeded" | "auth.signup.failed" | "auth.otp.sent" | "auth.otp.verified" | "auth.otp.failed" | "auth.email.verify.sent" | "auth.email.verify.succeeded" | "auth.email.verify.failed" | "auth.email.changed" | "auth.avatar.upload.succeeded" | "auth.avatar.upload.failed" | "auth.bio.update.succeeded" | "auth.bio.update.failed" | "auth.profile.view.failed" | "auth.profile.view.uncapped" | "auth.challenge.issued" | "auth.signin.succeeded" | "auth.signin.failed" | "auth.passkey.added" | "auth.passkey.add_failed" | "auth.restore.succeeded" | "auth.restore.failed" | "auth.restore.no_session" | "auth.refresh.succeeded" | "auth.refresh.failed" | "auth.refresh.reuse" | "auth.refresh.grace" | "auth.jwt.verified" | "auth.jwt.rejected" | "auth.signout" | "auth.session.revoked" | "auth.session.revoked_all" | "auth.audience.rejected" | "auth.audience.minted" | "auth.guest.created" | "auth.guest.restored" | "auth.guest.upgraded" | "auth.guest.failed" | "auth.role.granted" | "auth.role.revoked" | "auth.account.deleted" | "auth.account.exported" | "room.restriction.issued" | "room.restriction.lifted" | "auth.funnel.mode_selected" | "auth.funnel.email_entered" | "auth.funnel.ceremony_started" | "auth.funnel.ceremony_failed" | "auth.funnel.done_shown" | "room_event.activated" | "room_event.completed" | "room_event.reminder_dispatched" | "room_event.reminder_failed";
|
|
13
13
|
export type AuthErrorCode = "username_taken" | "credential_taken" | "email_taken" | "email_not_verified" | "no_email" | "image_upload_forbidden" | "image_upload_rate_limited" | "invalid_image_type" | "image_too_large" | "image_upload_failed" | "bio_forbidden" | "bio_invalid" | "bio_too_long" | "profile_rate_limited" | "profile_ip_missing" | "otp_invalid" | "otp_expired" | "otp_attempts_exceeded" | "otp_resend_cooldown" | "otp_resend_limit" | "otp_global_limit" | "otp_send_failed" | "challenge_expired" | "unknown_credential" | "invalid_assertion_type" | "invalid_assertion_origin" | "user_verification_required" | "invalid_signature" | "user_not_found" | "missing_audience" | "audience_not_allowed" | "audience_mismatch" | "no_session" | "no_passkey" | "refresh_token_invalid" | "refresh_token_expired" | "refresh_epoch_stale" | "refresh_user_epoch_stale" | "refresh_reuse_detected" | "guest_rate_limited" | "guest_global_limit" | "guest_username_exhausted" | "guest_session" | "passkey_exists" | "already_upgraded" | "malformed_token" | "unknown_key" | "verification_failed" | "webauthn_not_allowed" | "webauthn_security" | "webauthn_invalid_state" | "webauthn_not_supported" | "webauthn_constraint" | "webauthn_aborted" | "webauthn_unknown" | "internal_error";
|
|
14
14
|
/** One flat record per event = one row in the `auth_events` stream. */
|
|
15
15
|
export interface AuthEventRecord {
|
|
@@ -47,6 +47,10 @@ export interface AuthEventRecord {
|
|
|
47
47
|
occurrenceId: string | null;
|
|
48
48
|
dispatchKind: string | null;
|
|
49
49
|
recipients: number | null;
|
|
50
|
+
/** Account-lifecycle fields: whether the account was a guest, and what an
|
|
51
|
+
* erasure or export touched, as per-section row counts. */
|
|
52
|
+
guest: boolean | null;
|
|
53
|
+
counts: Record<string, number> | null;
|
|
50
54
|
}
|
|
51
55
|
/** Body accepted by `POST /api/events` from the dialog. */
|
|
52
56
|
export interface BeaconBody {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export type { AccountDeleteRequest, AccountDeleteResponse, AccountErasureCounts, AccountErasureEntry, AccountErasureFeed, AccountErasureResult, AccountExport, AccountExportAccount, AccountExportActionEvent, AccountExportActionSession, AccountExportAttendance, AccountExportAuthoredEvent, AccountExportCharacterStateTime, AccountExportFollow, AccountExportGameEvent, AccountExportOtherService, AccountExportOwnedRoom, AccountExportPasskey, AccountExportRoomEvents, AccountExportRoomMembership, AccountExportRoomPlayerState, AccountExportRoomPresence, AccountExportRoomRestriction, AccountExportRoomSettings, AccountExportRooms, AccountExportRsvp, AccountExportSettingsSurface, AccountExportStudySubject, AccountExportXpGrant, AccountExportXpSummary, } from "./account-data";
|
|
1
2
|
export type { ActionCurve, ActionEventRequest, ActionEventResponse, ActionsApi, ActiveActionState, FinishActionRequest, ListStudySessionsOptions, ListStudySubjectsOptions, StartActionInput, StartActionResult, StudyApi, StudySubjectResponse, UseActiveActionOptions, XpEstimateContext, } from "./actions";
|
|
2
3
|
export type { AccountSession, AccountSessionsResponse, AdditionalSession, AuthConfig, AuthResponse, AuthResponseWithWebAuthn, BioNormalization, BioRejection, BioWriteRefusal, BioWriteResult, FlowCredential, FlowUser, PasskeyPluginOptions, RevokeAllSessionsResponse, RevokeSessionResponse, SecurityActivityEntry, SecurityActivityResponse, SessionUser, VerifiedFlowJWT, VerifyOptions, WebAuthnSignature, } from "./auth";
|
|
3
4
|
export type { BodyRegion, CosmeticItem, CosmeticMaterial, CosmeticPaint, CosmeticSlot, EquipConflict, EquippedCosmetic, EquippedCosmetics, EquipRegion, PaintSlot, SettingsConflict, } from "./cosmetics";
|
package/dist/sdk/types/sdk.d.ts
CHANGED
|
@@ -230,6 +230,14 @@ export type ProfileButtonHandle = {
|
|
|
230
230
|
destroy: () => void;
|
|
231
231
|
};
|
|
232
232
|
export type OpenProfileOptions = {
|
|
233
|
+
/**
|
|
234
|
+
* Called once when the overlay closes, including its close button, backdrop,
|
|
235
|
+
* Escape, closeProfile(), or a sign-in/sign-out handoff. Runs after the
|
|
236
|
+
* overlay stops accepting input, without waiting for the exit animation.
|
|
237
|
+
* The latest successful open replaces this callback; omitting it clears the
|
|
238
|
+
* previous callback. Rejected opens and repeated closes do not notify.
|
|
239
|
+
*/
|
|
240
|
+
onClose?: () => void;
|
|
233
241
|
/**
|
|
234
242
|
* The Flow ID origin serving the overlay (e.g. https://id.flow.industries).
|
|
235
243
|
* Defaults to the resolved Flow instance's `host`, then the SDK default.
|
|
@@ -249,12 +257,7 @@ export type OpenProfileOptions = {
|
|
|
249
257
|
*/
|
|
250
258
|
flow?: Flow;
|
|
251
259
|
};
|
|
252
|
-
export type UseOpenProfileOptions =
|
|
253
|
-
/** Override the Flow ID origin; defaults to the resolved Flow's `host`. */
|
|
254
|
-
host?: string;
|
|
255
|
-
/** Color scheme for the dialog chrome; defaults to "light dark" (the OS). */
|
|
256
|
-
theme?: ColorScheme;
|
|
257
|
-
};
|
|
260
|
+
export type UseOpenProfileOptions = Pick<OpenProfileOptions, "host" | "theme" | "onClose">;
|
|
258
261
|
/** The inline Flow ID widgets an embedder can mount alongside its own UI. */
|
|
259
262
|
export type FlowWidgetName = "xp" | "action-timer";
|
|
260
263
|
export type MountFlowWidgetOptions = {
|
|
@@ -309,6 +312,7 @@ export type FlowWidgetProps = {
|
|
|
309
312
|
theme?: ColorScheme;
|
|
310
313
|
};
|
|
311
314
|
export type DialogHost = {
|
|
315
|
+
captureFocus: () => void;
|
|
312
316
|
open: (options?: DialogOpenOptions) => void;
|
|
313
317
|
close: () => void;
|
|
314
318
|
destroy: () => void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flow-industries/id",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
"types": "./dist/sdk/client/index.d.ts",
|
|
20
20
|
"import": "./dist/sdk/client/index.js"
|
|
21
21
|
},
|
|
22
|
+
"./usernames": {
|
|
23
|
+
"types": "./dist/sdk/usernames.d.ts",
|
|
24
|
+
"import": "./dist/sdk/usernames.js"
|
|
25
|
+
},
|
|
22
26
|
"./react": {
|
|
23
27
|
"types": "./dist/sdk/react/index.d.ts",
|
|
24
28
|
"import": "./dist/sdk/react/index.js"
|
|
@@ -69,6 +73,7 @@
|
|
|
69
73
|
"usernames:refresh": "bun run scripts/generate-reserved-usernames.ts",
|
|
70
74
|
"lint": "biome check",
|
|
71
75
|
"test": "bun test src",
|
|
76
|
+
"test:sdk-package": "bash scripts/test-sdk-package.sh",
|
|
72
77
|
"format": "biome format --write",
|
|
73
78
|
"check": "biome check --write",
|
|
74
79
|
"typecheck": "tsr generate && tsc --noEmit"
|
|
@@ -121,7 +126,7 @@
|
|
|
121
126
|
],
|
|
122
127
|
"dependencies": {
|
|
123
128
|
"@aws-sdk/client-s3": "^3.1073.0",
|
|
124
|
-
"@flow-industries/ui": "^0.
|
|
129
|
+
"@flow-industries/ui": "^0.21.0",
|
|
125
130
|
"@hono/otel": "^1.1.2",
|
|
126
131
|
"@openobserve/browser-logs": "^0.3.1",
|
|
127
132
|
"@openobserve/browser-rum": "^0.3.1",
|