@flow-industries/id 0.21.2 → 0.22.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.
@@ -13,11 +13,8 @@ import { createStore, initialFlowState } from "./store";
13
13
  * see the result instead of racing it. Falls back to running `fn` directly
14
14
  * where Web Locks is unavailable.
15
15
  */
16
- function withOriginLock(name, fn) {
17
- const locks =
18
- /* SAFETY: the SDK stashes its singleton on globalThis so two bundles of it share one
19
- instance; the property is namespaced and written only here. */
20
- globalThis.navigator?.locks;
16
+ async function withOriginLock(name, fn) {
17
+ const locks = globalThis.navigator?.locks;
21
18
  if (locks?.request)
22
19
  return locks.request(name, fn);
23
20
  return fn();
@@ -0,0 +1,18 @@
1
+ import type { ActiveAction, ClockAnchor, FocusSessionEvent, FocusSessionState } from "../types";
2
+ /** A re-read only replaces the anchor when it moves the display by more than
3
+ * this, so request timing never makes the clock jitter. */
4
+ export declare const REANCHOR_TOLERANCE_SECONDS = 2;
5
+ export declare function anchorFor(active: ActiveAction, readAt: number): ClockAnchor;
6
+ export declare function elapsedSeconds(anchor: ClockAnchor, now: number): number;
7
+ /** Whether a fresh server sample should replace the current anchor: yes when
8
+ * the pause state flipped or the server disagrees with the extrapolated clock
9
+ * by more than the tolerance (another surface paused, a replaced session). */
10
+ export declare function shouldReanchor(current: ClockAnchor, next: ActiveAction, now: number): boolean;
11
+ /** `mm:ss`, rolling into `h:mm:ss` past an hour. */
12
+ export declare function formatElapsed(seconds: number): string;
13
+ export declare const INITIAL_FOCUS_STATE: FocusSessionState;
14
+ export declare function focusSessionReducer(state: FocusSessionState, event: FocusSessionEvent): FocusSessionState;
15
+ /** The pill and panel headline: "Meditate", or "Practice · Chinese" when a
16
+ * subject is attached. `fallback` covers a source the catalog no longer
17
+ * labels. */
18
+ export declare function actionHeadline(action: Pick<ActiveAction, "label" | "source" | "subject">, fallback: string): string;
@@ -0,0 +1,140 @@
1
+ /** A re-read only replaces the anchor when it moves the display by more than
2
+ * this, so request timing never makes the clock jitter. */
3
+ export const REANCHOR_TOLERANCE_SECONDS = 2;
4
+ export function anchorFor(active, readAt) {
5
+ return {
6
+ accruedSeconds: active.accruedSeconds,
7
+ paused: active.paused,
8
+ readAt,
9
+ };
10
+ }
11
+ export function elapsedSeconds(anchor, now) {
12
+ if (anchor.paused)
13
+ return anchor.accruedSeconds;
14
+ return anchor.accruedSeconds + Math.max(0, now - anchor.readAt) / 1000;
15
+ }
16
+ /** Whether a fresh server sample should replace the current anchor: yes when
17
+ * the pause state flipped or the server disagrees with the extrapolated clock
18
+ * by more than the tolerance (another surface paused, a replaced session). */
19
+ export function shouldReanchor(current, next, now) {
20
+ if (current.paused !== next.paused)
21
+ return true;
22
+ return (Math.abs(elapsedSeconds(current, now) - next.accruedSeconds) >
23
+ REANCHOR_TOLERANCE_SECONDS);
24
+ }
25
+ /** `mm:ss`, rolling into `h:mm:ss` past an hour. */
26
+ export function formatElapsed(seconds) {
27
+ const whole = Math.max(0, Math.floor(seconds));
28
+ const hours = Math.floor(whole / 3600);
29
+ const minutes = Math.floor((whole % 3600) / 60);
30
+ const secs = whole % 60;
31
+ const mmss = `${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
32
+ return hours > 0 ? `${hours}:${mmss}` : mmss;
33
+ }
34
+ export const INITIAL_FOCUS_STATE = {
35
+ phase: "unknown",
36
+ active: null,
37
+ anchor: null,
38
+ finished: null,
39
+ conflict: null,
40
+ busy: false,
41
+ failed: false,
42
+ };
43
+ function withActive(state, active, at) {
44
+ const sameSession = state.active?.sessionId === active.sessionId;
45
+ const anchor = sameSession && state.anchor && !shouldReanchor(state.anchor, active, at)
46
+ ? state.anchor
47
+ : anchorFor(active, at);
48
+ return {
49
+ ...state,
50
+ phase: "active",
51
+ active,
52
+ anchor,
53
+ conflict: null,
54
+ failed: false,
55
+ };
56
+ }
57
+ export function focusSessionReducer(state, event) {
58
+ switch (event.type) {
59
+ case "read": {
60
+ if (event.active === null) {
61
+ return {
62
+ ...state,
63
+ phase: "idle",
64
+ active: null,
65
+ anchor: null,
66
+ failed: false,
67
+ };
68
+ }
69
+ return withActive(state, event.active, event.at);
70
+ }
71
+ case "read-failed":
72
+ return { ...state, failed: true };
73
+ case "request":
74
+ return { ...state, busy: true, failed: false };
75
+ case "request-failed":
76
+ return { ...state, busy: false, failed: true };
77
+ case "started":
78
+ return {
79
+ ...withActive(state, event.session, event.at),
80
+ anchor: anchorFor(event.session, event.at),
81
+ busy: false,
82
+ finished: null,
83
+ };
84
+ case "conflict":
85
+ return { ...state, busy: false, conflict: event.active };
86
+ case "paused": {
87
+ if (!state.active || !state.anchor)
88
+ return { ...state, busy: false };
89
+ const frozen = elapsedSeconds(state.anchor, event.at);
90
+ return {
91
+ ...state,
92
+ busy: false,
93
+ active: { ...state.active, paused: true, accruedSeconds: frozen },
94
+ anchor: { accruedSeconds: frozen, paused: true, readAt: event.at },
95
+ };
96
+ }
97
+ case "resumed": {
98
+ if (!state.active || !state.anchor)
99
+ return { ...state, busy: false };
100
+ return {
101
+ ...state,
102
+ busy: false,
103
+ active: { ...state.active, paused: false },
104
+ anchor: { ...state.anchor, paused: false, readAt: event.at },
105
+ };
106
+ }
107
+ case "finished": {
108
+ const finished = state.active
109
+ ? {
110
+ source: state.active.source,
111
+ label: state.active.label,
112
+ subject: state.active.subject,
113
+ xpGranted: event.result.xpGranted,
114
+ durationSeconds: event.result.durationSeconds,
115
+ }
116
+ : null;
117
+ return {
118
+ ...state,
119
+ phase: "idle",
120
+ active: null,
121
+ anchor: null,
122
+ busy: false,
123
+ finished,
124
+ };
125
+ }
126
+ case "dismiss-finished":
127
+ return { ...state, finished: null };
128
+ case "dismiss-conflict":
129
+ return { ...state, conflict: null };
130
+ case "reset":
131
+ return INITIAL_FOCUS_STATE;
132
+ }
133
+ }
134
+ /** The pill and panel headline: "Meditate", or "Practice · Chinese" when a
135
+ * subject is attached. `fallback` covers a source the catalog no longer
136
+ * labels. */
137
+ export function actionHeadline(action, fallback) {
138
+ const label = action.label ?? fallback;
139
+ return action.subject ? `${label} · ${action.subject.name}` : label;
140
+ }
@@ -0,0 +1,11 @@
1
+ import type { FocusApi, UserActionSurface } from "../types";
2
+ export type { ActionCatalogEntry, ActionCatalogResponse, ActionCurve, ActionEventRequest, ActionEventResponse, ActionSubjectRef, ActionTransitionKind, ActiveAction, CatalogFieldRef, ClockAnchor, CreateStudySubjectInput, FinishActionRequest, FinishActionResponse, FinishedSummary, FocusApi, FocusPhase, FocusSessionEvent, FocusSessionState, ListStudySessionsOptions, ListStudySubjectsOptions, StartActionResult, StartFocusRequest, StudyCatalog, StudyCategory, StudyField, StudyRange, StudySearchHit, StudySessionPage, StudySubject, StudySubjectList, StudySubjectRemovalResponse, StudySubjectResponse, StudySummary, UpdateStudySubjectInput, UserActionSurface, XpEstimateContext, } from "../types";
3
+ export { estimateActionXp, flowMultiplier } from "../xp/curve";
4
+ export { ActionsRequestError, isActiveSessionConflict } from "./actions";
5
+ export * from "./focus-session";
6
+ export { StudyRequestError } from "./study";
7
+ export * from "./study-search";
8
+ export declare const MEDITATION_SOURCE = "game.action.meditation";
9
+ export declare const STUDY_SOURCE = "action.study";
10
+ /** Compose action and study requests using the host's credentials and surface. */
11
+ export declare function createFocusApi(host: string, getToken: () => Promise<string | null>, surface: UserActionSurface): FocusApi;
@@ -0,0 +1,30 @@
1
+ import { createActionsApi } from "./actions";
2
+ import { createStudyApi } from "./study";
3
+ export { estimateActionXp, flowMultiplier } from "../xp/curve";
4
+ export { ActionsRequestError, isActiveSessionConflict } from "./actions";
5
+ export * from "./focus-session";
6
+ export { StudyRequestError } from "./study";
7
+ export * from "./study-search";
8
+ export const MEDITATION_SOURCE = "game.action.meditation";
9
+ export const STUDY_SOURCE = "action.study";
10
+ /** Compose action and study requests using the host's credentials and surface. */
11
+ export function createFocusApi(host, getToken, surface) {
12
+ const actions = createActionsApi(host, getToken);
13
+ const study = createStudyApi(host, getToken);
14
+ return {
15
+ catalog: actions.catalog,
16
+ active: actions.me,
17
+ start(request) {
18
+ return actions.start({ ...request, surface });
19
+ },
20
+ event: actions.event,
21
+ finish: actions.finish,
22
+ studyCatalog: study.catalog,
23
+ subjects: study.subjects,
24
+ createSubject: study.createSubject,
25
+ updateSubject: study.updateSubject,
26
+ deleteSubject: study.deleteSubject,
27
+ summary: study.summary,
28
+ sessions: study.sessions,
29
+ };
30
+ }
@@ -1,10 +1,11 @@
1
1
  export { defaultIdHost, isLocalHostname } from "../id-host";
2
- export type { AccessKeyOptions, ActionCatalogEntry, ActionCatalogResponse, ActionCurve, ActionEventRequest, ActionEventResponse, ActionSubjectRef, ActionSurface, ActionsApi, ActionTransitionKind, ActiveActionResponse, ActiveSessionConflict, AdditionalSession, Address, ConnectCapabilities, ConnectResponse, CreateFlowOptions, CreateStudySubjectInput, CurrentAction, DialogHost, FinishActionRequest, FinishActionResponse, Flow, FlowCredential, FlowSessionState, FlowState, FlowUser, FlowWidgetHandle, FlowWidgetName, ListStudySessionsOptions, ListStudySubjectsOptions, LoginOptions, MethodName, MountFlowWidgetOptions, MountProfileOptions, OpenProfileOptions, ProfileButtonHandle, ProfilePosition, RoomMemberEntry, Session, StartActionInput, StartActionResponse, StartActionResult, StudyApi, StudyCatalog, StudyCategory, StudyCategoryTotals, StudyField, StudyRange, StudySessionEntry, StudySessionPage, StudySubject, StudySubjectList, StudySubjectRefusal, StudySubjectRemovalResponse, StudySubjectResponse, StudySubjectTotals, StudySummary, UpdateStudySubjectInput, UserActionSurface, XpEstimateContext, XpMode, } from "../types";
2
+ export type { AccessKeyOptions, ActionCatalogEntry, ActionCatalogResponse, ActionCurve, ActionEventRequest, ActionEventResponse, ActionSubjectRef, ActionSurface, ActionsApi, ActionTransitionKind, ActiveAction, ActiveActionResponse, ActiveSessionConflict, AdditionalSession, Address, CatalogFieldRef, ClockAnchor, ConnectCapabilities, ConnectResponse, CreateFlowOptions, CreateStudySubjectInput, CurrentAction, DialogHost, FinishActionRequest, FinishActionResponse, FinishedSummary, Flow, FlowCredential, FlowSessionState, FlowState, FlowUser, FlowWidgetHandle, FlowWidgetName, FocusApi, FocusPhase, FocusSessionEvent, FocusSessionState, ListStudySessionsOptions, ListStudySubjectsOptions, LoginOptions, MethodName, MountFlowWidgetOptions, MountProfileOptions, OpenProfileOptions, ProfileButtonHandle, ProfilePosition, RoomMemberEntry, Session, StartActionInput, StartActionResponse, StartActionResult, StartFocusRequest, StudyApi, StudyCatalog, StudyCategory, StudyCategoryTotals, StudyField, StudyRange, StudySearchHit, StudySessionEntry, StudySessionPage, StudySubject, StudySubjectList, StudySubjectRefusal, StudySubjectRemovalResponse, StudySubjectResponse, StudySubjectTotals, StudySummary, UpdateStudySubjectInput, UserActionSurface, XpEstimateContext, XpMode, } from "../types";
3
3
  export { consistencyCurve, estimateActionXp, FLOW_SCORE_MAX, flowMultiplier, } from "../xp/curve";
4
4
  export { ActionsRequestError, createActionsApi, isActiveSessionConflict, } from "./actions";
5
5
  export { createFlow, getFlow, requireFlow, resetFlow } from "./create-flow";
6
6
  export { createDialogHost } from "./dialog-host";
7
7
  export { createFlowWidget } from "./flow-widget";
8
+ export { actionHeadline, anchorFor, catalogFieldIndex, createFocusApi, elapsedSeconds, fieldsCoveredBySubjects, focusSessionReducer, formatElapsed, hitLabel, INITIAL_FOCUS_STATE, MEDITATION_SOURCE, matchScore, normalizeSearchText, REANCHOR_TOLERANCE_SECONDS, recentSubjects, STUDY_SOURCE, searchStudy, shouldReanchor, } from "./focus";
8
9
  export { METHODS } from "./methods";
9
10
  export { closeProfile, openProfile } from "./open-profile";
10
11
  export { createProfileButton } from "./profile-button";
@@ -4,6 +4,7 @@ export { ActionsRequestError, createActionsApi, isActiveSessionConflict, } from
4
4
  export { createFlow, getFlow, requireFlow, resetFlow } from "./create-flow";
5
5
  export { createDialogHost } from "./dialog-host";
6
6
  export { createFlowWidget } from "./flow-widget";
7
+ export { actionHeadline, anchorFor, catalogFieldIndex, createFocusApi, elapsedSeconds, fieldsCoveredBySubjects, focusSessionReducer, formatElapsed, hitLabel, INITIAL_FOCUS_STATE, MEDITATION_SOURCE, matchScore, normalizeSearchText, REANCHOR_TOLERANCE_SECONDS, recentSubjects, STUDY_SOURCE, searchStudy, shouldReanchor, } from "./focus";
7
8
  export { METHODS } from "./methods";
8
9
  export { closeProfile, openProfile } from "./open-profile";
9
10
  export { createProfileButton } from "./profile-button";
@@ -18,7 +18,7 @@ export type JsonApiError = (status: number, message: string, reason?: string) =>
18
18
  * raw status and payload for the few responses a client treats as values (a
19
19
  * start's 409); `request` is the common path that throws on any non-2xx.
20
20
  */
21
- export declare function createJsonApi(host: string, prefix: string, getToken: () => Promise<string | null>, makeError: JsonApiError): {
21
+ export declare function createJsonApi(host: string, prefix: string, getToken: () => Promise<string | null>, makeError: JsonApiError, mapFailure?: (exchange: JsonExchange) => Error): {
22
22
  exchange: (path: string, options?: JsonRequestOptions) => Promise<JsonExchange>;
23
23
  request: <T>(path: string, options?: JsonRequestOptions) => Promise<T>;
24
24
  failure: (exchange: JsonExchange) => Error;
@@ -11,10 +11,12 @@ const failureSchema = z.object({
11
11
  * raw status and payload for the few responses a client treats as values (a
12
12
  * start's 409); `request` is the common path that throws on any non-2xx.
13
13
  */
14
- export function createJsonApi(host, prefix, getToken, makeError) {
14
+ export function createJsonApi(host, prefix, getToken, makeError, mapFailure) {
15
15
  /** The error a non-2xx exchange throws: the route's own wording when it
16
16
  * sent one, and its machine code as `reason`. */
17
17
  function failure(exchange) {
18
+ if (mapFailure)
19
+ return mapFailure(exchange);
18
20
  const parsed = failureSchema.safeParse(exchange.payload);
19
21
  const { error, reason } = parsed.success ? parsed.data : {};
20
22
  return makeError(exchange.status, error ?? `Request failed (${exchange.status})`, reason ?? error);
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { createJsonApi } from "./json-api";
2
3
  /** Every rooms endpoint answers a failure as `{ error, reason? }`. */
3
4
  const failureSchema = z.object({
4
5
  error: z.string().optional(),
@@ -22,32 +23,12 @@ export class RoomsRequestError extends Error {
22
23
  * bearer and fail fast with a 401-shaped error when no session exists.
23
24
  */
24
25
  export function createRoomsApi(host, getToken) {
25
- async function request(path, options = {}) {
26
- const headers = {};
27
- const token = await getToken();
28
- if (token)
29
- headers.Authorization = `Bearer ${token}`;
30
- else if (options.authRequired) {
31
- throw new RoomsRequestError(401, "Sign in first");
32
- }
33
- if (options.body !== undefined)
34
- headers["Content-Type"] = "application/json";
35
- const res = await fetch(`${host}/api/rooms${path}`, {
36
- method: options.method ?? "GET",
37
- headers,
38
- body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
39
- });
40
- const payload = await res.json().catch(() => ({}));
41
- if (!res.ok) {
42
- const failure = failureSchema.safeParse(payload);
43
- throw new RoomsRequestError(res.status, failure.success && failure.data.error
44
- ? failure.data.error
45
- : `Rooms request failed (${res.status})`, failure.success ? failure.data.reason : undefined);
46
- }
47
- /* SAFETY: each caller names the response type of the rooms endpoint it just called; a
48
- non-ok status has already thrown above with the server's own wording. */
49
- return payload;
50
- }
26
+ const { request } = createJsonApi(host, "/api/rooms", getToken, (status, message, reason) => new RoomsRequestError(status, message, reason), ({ status, payload }) => {
27
+ const parsed = failureSchema.safeParse(payload);
28
+ return new RoomsRequestError(status, parsed.success && parsed.data.error
29
+ ? parsed.data.error
30
+ : `Rooms request failed (${status})`, parsed.success ? parsed.data.reason : undefined);
31
+ });
51
32
  const slugPath = (slug) => `/${encodeURIComponent(slug)}`;
52
33
  const act = (slug, action, body) => request(`${slugPath(slug)}/${action}`, {
53
34
  method: "POST",
@@ -0,0 +1,20 @@
1
+ import type { CatalogFieldRef, StudyCatalog, StudySearchHit, StudySubject } from "../types";
2
+ export declare function normalizeSearchText(text: string): string;
3
+ /** 4 exact, 3 prefix, 2 a later word's prefix, 1 substring, 0 no match.
4
+ * Both sides are normalized here so callers can pass raw labels. */
5
+ export declare function matchScore(query: string, candidate: string): number;
6
+ export declare function catalogFieldIndex(catalog: StudyCatalog): Map<string, CatalogFieldRef>;
7
+ /** Live subjects newest-used first; auth stamps `lastUsedAt` at creation, so
8
+ * a subject never studied sorts by when it was added. */
9
+ export declare function recentSubjects(subjects: readonly StudySubject[], limit?: number): StudySubject[];
10
+ /** Catalog fields the user already keeps under the field's own name are
11
+ * hidden from results — the subject hit covers them and picking the field
12
+ * again would only re-create the same subject. A renamed subject ("Math for
13
+ * my test" under Algebra) leaves the field visible for a second subject. */
14
+ export declare function fieldsCoveredBySubjects(subjects: readonly StudySubject[], index: Map<string, CatalogFieldRef>): Set<string>;
15
+ export declare function searchStudy(query: string, options: {
16
+ catalog: StudyCatalog;
17
+ subjects: readonly StudySubject[];
18
+ limit?: number;
19
+ }): StudySearchHit[];
20
+ export declare function hitLabel(hit: StudySearchHit): string;
@@ -0,0 +1,116 @@
1
+ export function normalizeSearchText(text) {
2
+ return text
3
+ .normalize("NFD")
4
+ .replace(/\p{M}+/gu, "")
5
+ .toLowerCase()
6
+ .replace(/\s+/g, " ")
7
+ .trim();
8
+ }
9
+ /** 4 exact, 3 prefix, 2 a later word's prefix, 1 substring, 0 no match.
10
+ * Both sides are normalized here so callers can pass raw labels. */
11
+ export function matchScore(query, candidate) {
12
+ const normalizedQuery = normalizeSearchText(query);
13
+ const normalizedCandidate = normalizeSearchText(candidate);
14
+ if (!normalizedQuery || !normalizedCandidate)
15
+ return 0;
16
+ if (normalizedCandidate === normalizedQuery)
17
+ return 4;
18
+ if (normalizedCandidate.startsWith(normalizedQuery))
19
+ return 3;
20
+ if (normalizedCandidate.includes(` ${normalizedQuery}`))
21
+ return 2;
22
+ if (normalizedCandidate.includes(normalizedQuery))
23
+ return 1;
24
+ return 0;
25
+ }
26
+ export function catalogFieldIndex(catalog) {
27
+ const index = new Map();
28
+ for (const category of catalog.categories) {
29
+ for (const field of category.fields) {
30
+ index.set(field.slug, { category, field });
31
+ }
32
+ }
33
+ return index;
34
+ }
35
+ /** Live subjects newest-used first; auth stamps `lastUsedAt` at creation, so
36
+ * a subject never studied sorts by when it was added. */
37
+ export function recentSubjects(subjects, limit = 5) {
38
+ return [...subjects]
39
+ .sort((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt))
40
+ .slice(0, limit);
41
+ }
42
+ /** Catalog fields the user already keeps under the field's own name are
43
+ * hidden from results — the subject hit covers them and picking the field
44
+ * again would only re-create the same subject. A renamed subject ("Math for
45
+ * my test" under Algebra) leaves the field visible for a second subject. */
46
+ export function fieldsCoveredBySubjects(subjects, index) {
47
+ const covered = new Set();
48
+ for (const subject of subjects) {
49
+ if (!subject.field)
50
+ continue;
51
+ const ref = index.get(subject.field);
52
+ if (!ref)
53
+ continue;
54
+ if (normalizeSearchText(subject.name) === normalizeSearchText(ref.field.label)) {
55
+ covered.add(subject.field);
56
+ }
57
+ }
58
+ return covered;
59
+ }
60
+ function bestAliasMatch(query, field) {
61
+ let best = null;
62
+ for (const alias of field.aliases ?? []) {
63
+ const score = matchScore(query, alias);
64
+ if (score > (best?.score ?? 0))
65
+ best = { alias, score };
66
+ }
67
+ return best;
68
+ }
69
+ export function searchStudy(query, options) {
70
+ const { catalog, subjects, limit = 12 } = options;
71
+ if (!normalizeSearchText(query))
72
+ return [];
73
+ const index = catalogFieldIndex(catalog);
74
+ const covered = fieldsCoveredBySubjects(subjects, index);
75
+ const hits = [];
76
+ for (const subject of subjects) {
77
+ const ref = subject.field ? (index.get(subject.field) ?? null) : null;
78
+ let score = matchScore(query, subject.name);
79
+ if (ref) {
80
+ score = Math.max(score, matchScore(query, ref.field.label));
81
+ const alias = bestAliasMatch(query, ref.field);
82
+ if (alias)
83
+ score = Math.max(score, alias.score);
84
+ }
85
+ if (score > 0) {
86
+ hits.push({ kind: "subject", id: subject.id, subject, ref, score });
87
+ }
88
+ }
89
+ for (const ref of index.values()) {
90
+ if (covered.has(ref.field.slug))
91
+ continue;
92
+ const labelScore = matchScore(query, ref.field.label);
93
+ const alias = bestAliasMatch(query, ref.field);
94
+ const score = Math.max(labelScore, alias?.score ?? 0);
95
+ if (score === 0)
96
+ continue;
97
+ hits.push({
98
+ kind: "field",
99
+ id: ref.field.slug,
100
+ ref,
101
+ alias: alias && alias.score > labelScore ? alias.alias : null,
102
+ score,
103
+ });
104
+ }
105
+ hits.sort((a, b) => {
106
+ if (b.score !== a.score)
107
+ return b.score - a.score;
108
+ if (a.kind !== b.kind)
109
+ return a.kind === "subject" ? -1 : 1;
110
+ return hitLabel(a).localeCompare(hitLabel(b));
111
+ });
112
+ return hits.slice(0, limit);
113
+ }
114
+ export function hitLabel(hit) {
115
+ return hit.kind === "subject" ? hit.subject.name : hit.ref.field.label;
116
+ }
@@ -0,0 +1 @@
1
+ export declare function useCooldown(initial: number): [number, (next: number) => void];
@@ -0,0 +1,12 @@
1
+ import { useEffect, useState } from "react";
2
+ export function useCooldown(initial) {
3
+ const [cooldown, setCooldown] = useState(initial);
4
+ const cooldownActive = cooldown > 0;
5
+ useEffect(() => {
6
+ if (!cooldownActive)
7
+ return;
8
+ const timer = setInterval(() => setCooldown((c) => (c <= 1 ? 0 : c - 1)), 1000);
9
+ return () => clearInterval(timer);
10
+ }, [cooldownActive]);
11
+ return [cooldown, setCooldown];
12
+ }
@@ -160,3 +160,15 @@ export type VerifiedFlowJWT = JWTPayload & {
160
160
  */
161
161
  guest?: boolean;
162
162
  };
163
+ export type PasskeyEnrollment = {
164
+ userId: string;
165
+ username: string;
166
+ credential: FlowCredential;
167
+ };
168
+ export type AccountRegistration = {
169
+ username: string;
170
+ email: string;
171
+ subscribedToNews: boolean;
172
+ walletAddress: string | null;
173
+ credential: FlowCredential | null;
174
+ };
@@ -1,3 +1,4 @@
1
+ import type { RpcRequest } from "./protocol";
1
2
  /**
2
3
  * Async view state for a dialog panel — the three states every fetched
3
4
  * section renders (spinner / retry row / content), so "empty" stays
@@ -89,3 +90,27 @@ export type DialogState = {
89
90
  customFeatures?: DialogCustomFeatures;
90
91
  customLabels?: DialogCustomLabels;
91
92
  };
93
+ export type SignUpProps = {
94
+ request: RpcRequest;
95
+ rpId: string;
96
+ accessKeyHash?: string | undefined;
97
+ guestToken?: string | undefined;
98
+ onHome: () => void;
99
+ onSignIn: () => void;
100
+ };
101
+ export type SignInWithEmailProps = {
102
+ request: RpcRequest;
103
+ onBack: () => void;
104
+ onHome: () => void;
105
+ };
106
+ /** Cookie-authoritative identity for account data operations. */
107
+ export type AccountDataIdentity = {
108
+ id: string;
109
+ username: string;
110
+ isGuest: boolean;
111
+ };
112
+ /** Validated export file and any collections capped by the server. */
113
+ export type AccountDataDownload = {
114
+ blob: Blob;
115
+ truncated: string[];
116
+ };
@@ -0,0 +1,109 @@
1
+ import type { ActionCatalogResponse, ActionEventRequest, ActionEventResponse, ActionSubjectRef, ActiveActionResponse, CreateStudySubjectInput, FinishActionRequest, FinishActionResponse, ListStudySessionsOptions, ListStudySubjectsOptions, StartActionInput, StartActionResult, StudyCatalog, StudyCategory, StudyField, StudyRange, StudySessionPage, StudySubject, StudySubjectList, StudySubjectRemovalResponse, StudySubjectResponse, StudySummary, UpdateStudySubjectInput } from "./index";
2
+ /** The running session as the header pill and the panel see it: the shape of
3
+ * `GET /api/actions/me`, of a start's `session` and of a 409's `active`. */
4
+ export type ActiveAction = ActiveActionResponse;
5
+ /** A start as the UI requests it. The surface is not the caller's to choose:
6
+ * `createFocusApi` stamps every start with the injected host surface. `sessionId` may be
7
+ * omitted; the SDK mints the idempotency key. */
8
+ export type StartFocusRequest = Omit<StartActionInput, "surface">;
9
+ export interface FocusApi {
10
+ catalog(): Promise<ActionCatalogResponse>;
11
+ /** The caller's running session, or null when idle. */
12
+ active(): Promise<ActiveAction | null>;
13
+ /** Resolves to the 409 conflict as a value; every other refusal throws. */
14
+ start(request: StartFocusRequest): Promise<StartActionResult>;
15
+ event(request: ActionEventRequest): Promise<ActionEventResponse>;
16
+ finish(request: FinishActionRequest): Promise<FinishActionResponse>;
17
+ studyCatalog(): Promise<StudyCatalog>;
18
+ subjects(options?: ListStudySubjectsOptions): Promise<StudySubjectList>;
19
+ createSubject(request: CreateStudySubjectInput): Promise<StudySubjectResponse>;
20
+ /** Rename (`{ name }`), re-anchor (`{ field }`), archive or restore
21
+ * (`{ archived }`) a subject. Archiving is what the picker means by
22
+ * "remove"; the host subject-management surface also offers deletion. */
23
+ updateSubject(id: string, patch: UpdateStudySubjectInput): Promise<StudySubjectResponse>;
24
+ /** Removes a subject outright when nothing references it; auth archives it
25
+ * instead when sessions do, which the `outcome` reports. */
26
+ deleteSubject(id: string): Promise<StudySubjectRemovalResponse>;
27
+ /** Finished study time per subject and per category; the running session
28
+ * is not in it and is folded in live from the provider. */
29
+ summary(range: StudyRange): Promise<StudySummary>;
30
+ /** One subject's finished sessions, newest first, keyset-paged on
31
+ * `nextBefore`. */
32
+ sessions(options: ListStudySessionsOptions): Promise<StudySessionPage>;
33
+ }
34
+ export interface CatalogFieldRef {
35
+ category: StudyCategory;
36
+ field: StudyField;
37
+ }
38
+ export type StudySearchHit = {
39
+ kind: "subject";
40
+ id: string;
41
+ subject: StudySubject;
42
+ ref: CatalogFieldRef | null;
43
+ score: number;
44
+ } | {
45
+ kind: "field";
46
+ id: string;
47
+ ref: CatalogFieldRef;
48
+ /** The alias that matched when the label itself did not. */
49
+ alias: string | null;
50
+ score: number;
51
+ };
52
+ export interface ClockAnchor {
53
+ accruedSeconds: number;
54
+ paused: boolean;
55
+ readAt: number;
56
+ }
57
+ export type FocusPhase = "unknown" | "idle" | "active";
58
+ /** What Stop leaves behind, shown exactly once before it is dismissed. */
59
+ export interface FinishedSummary {
60
+ source: string;
61
+ label: string | null;
62
+ subject: ActionSubjectRef | null;
63
+ xpGranted: number;
64
+ durationSeconds: number;
65
+ }
66
+ export interface FocusSessionState {
67
+ phase: FocusPhase;
68
+ active: ActiveAction | null;
69
+ anchor: ClockAnchor | null;
70
+ finished: FinishedSummary | null;
71
+ /** The other session a plain start ran into, awaiting "switch?". */
72
+ conflict: ActiveAction | null;
73
+ busy: boolean;
74
+ failed: boolean;
75
+ }
76
+ export type FocusSessionEvent = {
77
+ type: "read";
78
+ active: ActiveAction | null;
79
+ at: number;
80
+ } | {
81
+ type: "read-failed";
82
+ } | {
83
+ type: "request";
84
+ } | {
85
+ type: "request-failed";
86
+ } | {
87
+ type: "started";
88
+ session: ActiveAction;
89
+ at: number;
90
+ } | {
91
+ type: "conflict";
92
+ active: ActiveAction;
93
+ } | {
94
+ type: "paused";
95
+ at: number;
96
+ } | {
97
+ type: "resumed";
98
+ at: number;
99
+ } | {
100
+ type: "finished";
101
+ result: FinishActionResponse;
102
+ at: number;
103
+ } | {
104
+ type: "dismiss-finished";
105
+ } | {
106
+ type: "dismiss-conflict";
107
+ } | {
108
+ type: "reset";
109
+ };
File without changes
@@ -1,19 +1,23 @@
1
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";
2
2
  export type { ActionCurve, ActionEventRequest, ActionEventResponse, ActionsApi, ActiveActionState, FinishActionRequest, ListStudySessionsOptions, ListStudySubjectsOptions, StartActionInput, StartActionResult, StudyApi, StudySubjectResponse, UseActiveActionOptions, XpEstimateContext, } from "./actions";
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
+ export type { AccountRegistration, AccountSession, AccountSessionsResponse, AdditionalSession, AuthConfig, AuthResponse, AuthResponseWithWebAuthn, BioNormalization, BioRejection, BioWriteRefusal, BioWriteResult, FlowCredential, FlowUser, PasskeyEnrollment, PasskeyPluginOptions, RevokeAllSessionsResponse, RevokeSessionResponse, SecurityActivityEntry, SecurityActivityResponse, SessionUser, VerifiedFlowJWT, VerifyOptions, WebAuthnSignature, } from "./auth";
4
4
  export type { BodyRegion, CosmeticItem, CosmeticMaterial, CosmeticPaint, CosmeticSlot, EquipConflict, EquippedCosmetic, EquippedCosmetics, EquipRegion, PaintSlot, SettingsConflict, } from "./cosmetics";
5
5
  export { BODY_REGIONS, COSMETIC_MATERIALS } from "./cosmetics";
6
- export type { BoundaryError, DialogCustomFeatures, DialogCustomLabels, DialogError, DialogReferrer, DialogState, Loadable, ProfileData, ProfileIdentity, } from "./dialog";
6
+ export type { AccountDataDownload, AccountDataIdentity, BoundaryError, DialogCustomFeatures, DialogCustomLabels, DialogError, DialogReferrer, DialogState, Loadable, ProfileData, ProfileIdentity, SignInWithEmailProps, SignUpProps, } from "./dialog";
7
7
  export type { AuthErrorCode, AuthEventName, AuthEventRecord, AuthMode, AuthOutcome, BeaconBody, FunnelStep, } from "./events";
8
+ export type { ActiveAction, CatalogFieldRef, ClockAnchor, FinishedSummary, FocusApi, FocusPhase, FocusSessionEvent, FocusSessionState, StartFocusRequest, StudySearchHit, } from "./focus";
8
9
  export type { GameEventReport, GameLifecycleEventName, GameLifecycleFeedEvent, GameLifecycleFeedPage, GamePlayerSession, } from "./game";
9
10
  export { GAME_LIFECYCLE_EVENT_NAMES } from "./game";
11
+ export type { AccountProfileData } from "./landing";
10
12
  export type { Bridge, BridgeParameters, FlowAccount, FlowRemote, FlowRemoteConfig, FromWindowOptions, MessageResponse, Messenger, OneOf, Payload, QueuedRequest, ReadyOptions, RemoteFlowState, RemoteState, Schema, Storage, Topic, WithReady, } from "./messenger";
13
+ export type { OtpVerificationFailure, OtpVerificationResult, VerificationAdapter, } from "./otp";
11
14
  export type { Call, ConnectCapabilities, ConnectRequest, ConnectResponse, GuestRequest, GuestResponse, MethodName, MethodParams, MethodResult, RestoreResponse, RpcRequest, SendCallsParams, SendCallsRequest, SendCallsResponse, SendTransactionParams, SendTransactionRequest, SendTransactionResponse, SignMessageRequest, SignMessageResponse, SignOutRequest, SignOutResponse, SignTypedDataRequest, SignTypedDataResponse, TransactionArgs, TypedData, TypedDataDomain, TypedDataField, } from "./protocol";
12
15
  export { getConnectCapabilities, isPersonalSignParams, isSendCallsParams, isSendTransactionParams, } from "./protocol";
13
16
  export type { CreateRoomEventInput, RoomEventArchive, RoomEventAttendance, RoomEventFrequency, RoomEventInstance, RoomEventInterest, RoomEventInterestState, RoomEventList, RoomEventOccurrence, RoomEventRecurrence, RoomEventRoomRef, RoomEventRun, RoomEventSeries, RoomEventStatus, SetInterestInput, UpdateRoomEventInput, } from "./room-events";
14
17
  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";
15
18
  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, RunLoginParams, RunLoginResult, SendCallsArgs, SendTransactionArgs, Session, SigningContext, SignMessageArgs, SignTypedDataArgs, Store, StoredAccessKey, UseOpenProfileOptions, WagmiConnectCapabilities, WagmiConnectParams, } from "./sdk";
16
19
  export type { GracefulShutdownCleanup, GracefulShutdownOptions, GracefulShutdownServer, GracefulShutdownSignal, ResolvedFlowSession, ResolveSessionOptions, SessionRouteOptions, SessionRouteResponse, SessionRouteSession, } from "./server";
20
+ export type { CosmeticOffer, PaintDescriptor, SettingDescriptor, Settings, SettingsMigration, StoredSettings, SurfaceRegistry, } from "./settings";
17
21
  export type { CreateStudySubjectInput, ListStudySessionsInput, StudyCatalog, StudyCategory, StudyCategoryTotals, StudyField, StudyRange, StudySessionEntry, StudySessionPage, StudySessionsResult, StudySubject, StudySubjectCreation, StudySubjectList, StudySubjectRefusal, StudySubjectRemoval, StudySubjectRemovalResponse, StudySubjectTotals, StudySubjectUpdate, StudySummary, UpdateStudySubjectInput, } from "./study";
18
22
  export type { CoinAsset, IdentifiedTx, TxApprove, TxConvert, TxSend, TxSwap, } from "./tx";
19
- export type { ActionCatalogEntry, ActionCatalogResponse, ActionDayContext, ActionEventKind, ActionSessionState, ActionSubjectRef, ActionSurface, ActionTransitionKind, ActiveActionResponse, ActiveSessionConflict, CurrentAction, FinishActionResponse, LevelProgress, PublicProfile, PublicProfileRefusal, PublicProfileResult, PublicProfileSignal, PublicUserProfile, StartActionResponse, StartUserSessionResult, UserActionSurface, XpGrantResult, XpMode, XpRecentGrant, XpSummary, } from "./xp";
23
+ export type { ActionCatalogEntry, ActionCatalogResponse, ActionDayContext, ActionDurations, ActionEventKind, ActionSessionState, ActionSubjectRef, ActionSurface, ActionTimelineEvent, ActionTransitionKind, ActiveActionResponse, ActiveSessionConflict, CurrentAction, FinishActionResponse, LevelProgress, PublicProfile, PublicProfileRefusal, PublicProfileResult, PublicProfileSignal, PublicUserProfile, StartActionResponse, StartUserSessionResult, UserActionSurface, XpGrantResult, XpMode, XpRecentGrant, XpSummary, } from "./xp";
@@ -0,0 +1,9 @@
1
+ import type { FlowUser } from "./auth";
2
+ export type AccountProfileData = FlowUser & {
3
+ email?: string | null;
4
+ emailVerified?: boolean;
5
+ subscribedToNews?: boolean;
6
+ createdAt?: string;
7
+ image?: string | null;
8
+ walletAddress?: string | null;
9
+ };
File without changes
@@ -0,0 +1,20 @@
1
+ import type { AuthErrorCode } from "./events";
2
+ export type OtpVerificationFailure = {
3
+ ok: false;
4
+ errorCode: AuthErrorCode;
5
+ apiCode: "INVALID_OTP" | "OTP_EXPIRED" | "TOO_MANY_ATTEMPTS";
6
+ status: 400 | 403;
7
+ message: string;
8
+ attemptsRemaining?: number;
9
+ };
10
+ export type OtpVerificationResult = {
11
+ ok: true;
12
+ } | OtpVerificationFailure;
13
+ export interface VerificationAdapter {
14
+ findVerificationValue(identifier: string): Promise<{
15
+ id: string;
16
+ value: string;
17
+ expiresAt: Date;
18
+ } | null | undefined>;
19
+ deleteVerificationValue(id: string): Promise<void>;
20
+ }
File without changes
@@ -0,0 +1,162 @@
1
+ import type { RawValue } from "../json";
2
+ import type { CosmeticItem, CosmeticMaterial, CosmeticSlot, EquipRegion, SettingsConflict } from "./cosmetics";
3
+ export interface PaintDescriptor {
4
+ key: string;
5
+ group: string;
6
+ label: string;
7
+ description?: string;
8
+ type: "paint";
9
+ default: string;
10
+ options: {
11
+ value: string;
12
+ label: string;
13
+ }[];
14
+ /**
15
+ * The item this colour lands on, and the material on it that is recoloured.
16
+ * Both travel on the descriptor rather than being read back out of the key: a
17
+ * form grouping its pickers by item, and a renderer deciding which surface a
18
+ * colour applies to, each want one of them, and neither should be taking a
19
+ * key apart to get it.
20
+ */
21
+ cosmetic: string;
22
+ material: CosmeticMaterial;
23
+ }
24
+ export interface CosmeticOffer extends CosmeticItem {
25
+ /**
26
+ * Every equip region this item cannot share a body with — its own, plus
27
+ * everything they overlap. Another item is blocked by this one exactly when
28
+ * its `regions` intersect this list, so a client tests a set instead of
29
+ * re-implementing the rule.
30
+ */
31
+ conflicts: readonly EquipRegion[];
32
+ /**
33
+ * The colour pickers this item brings once it is worn: the SAME descriptors
34
+ * {@link SurfaceRegistry.derived} emits for it, from the same builder, so a
35
+ * client rendering its draft and a client reading the derived list for a
36
+ * saved loadout draw the same rows.
37
+ *
38
+ * An offer, not a setting. A colour is a setting the player HAS only while
39
+ * its item is equipped — that is what {@link sanitize} prunes on — and
40
+ * nothing here widens the stored blob or the write path.
41
+ */
42
+ paint: readonly PaintDescriptor[];
43
+ }
44
+ export type SettingDescriptor = {
45
+ key: string;
46
+ group: string;
47
+ label: string;
48
+ description?: string;
49
+ type: "number";
50
+ default: number;
51
+ min: number;
52
+ max: number;
53
+ step: number;
54
+ } | {
55
+ key: string;
56
+ group: string;
57
+ label: string;
58
+ description?: string;
59
+ type: "boolean";
60
+ default: boolean;
61
+ } | {
62
+ key: string;
63
+ group: string;
64
+ label: string;
65
+ description?: string;
66
+ type: "enum";
67
+ default: string;
68
+ options: {
69
+ value: string;
70
+ label: string;
71
+ }[];
72
+ } | {
73
+ key: string;
74
+ group: string;
75
+ label: string;
76
+ description?: string;
77
+ /** Free text with a shape, for values no fixed option list can cover —
78
+ * a Discord snowflake, a channel id. `pattern` is a source string (not a
79
+ * RegExp) so the descriptor stays JSON-serializable for the schema
80
+ * endpoint the settings form reads. */
81
+ type: "text";
82
+ default: string;
83
+ maxLength: number;
84
+ pattern?: string;
85
+ } | {
86
+ key: string;
87
+ group: string;
88
+ label: string;
89
+ description?: string;
90
+ /** One cosmetic slot: the stored value is a catalog cosmetic id, or ""
91
+ * for nothing equipped. `items` is the catalog projection the customizer
92
+ * reads off the schema endpoint so it can render a draft loadout with no
93
+ * catalog data of its own — which picks would collide, and which colour
94
+ * pickers a pick brings with it. UX only; the conflict rule is enforced
95
+ * on save, and a draft is never a claim about what may be saved.
96
+ *
97
+ * The default is an item id, not "": a player who has never dressed is
98
+ * wearing the starting outfit, and saying so here is what lets "" mean
99
+ * the one thing it should — a player who chose to wear nothing there. */
100
+ type: "cosmetic";
101
+ default: string;
102
+ slot: CosmeticSlot;
103
+ /**
104
+ * Whether an avatar may go with this slot empty. False is the slot a
105
+ * client offers a "None" chip for; true is one where `""` is refused on
106
+ * save. Projected here rather than restated in the customizer for the
107
+ * reason {@link CosmeticOffer.conflicts} is: the rule belongs to the
108
+ * slot, and a second hand-maintained copy of it goes quiet (AUTH-222).
109
+ */
110
+ required: boolean;
111
+ items: readonly CosmeticOffer[];
112
+ } | PaintDescriptor;
113
+ export type StoredSettings = Record<string, RawValue>;
114
+ export type Settings = Record<string, number | boolean | string>;
115
+ export interface SettingsMigration {
116
+ to: number;
117
+ up: (stored: StoredSettings) => StoredSettings;
118
+ }
119
+ export interface SurfaceRegistry {
120
+ /**
121
+ * Bumped only when a setting in this surface is renamed or retyped. A stored
122
+ * blob stamps the version it was last normalized to; {@link normalize} walks
123
+ * an old blob up to this version on read, preserving the user's chosen value
124
+ * instead of dropping it. Adding or removing a setting does NOT bump it.
125
+ */
126
+ version: number;
127
+ settings: readonly SettingDescriptor[];
128
+ /**
129
+ * The steps {@link normalize} walks an out-of-date blob up through, in
130
+ * ascending {@link SettingsMigration.to} order. A surface that has never
131
+ * renamed anything has none, and its blobs need no walk.
132
+ */
133
+ migrations?: readonly SettingsMigration[];
134
+ /**
135
+ * Enum defaults are drawn from the option list seeded by the user id instead
136
+ * of the descriptor default, so every unsaved user gets their own stable
137
+ * random pick — identical on every read and on every surface that reads it.
138
+ */
139
+ randomDefaults?: boolean;
140
+ /**
141
+ * Cross-field rule for the whole surface, evaluated on the MERGED blob by
142
+ * the one settings write path. Per-descriptor {@link sanitize} cannot
143
+ * express it: an equip-region collision is a relation between two keys, not
144
+ * a property of either. Living on the surface rather than in a route is what
145
+ * makes it impossible for a second write path to skip.
146
+ */
147
+ validate?: (settings: Settings) => SettingsConflict | null;
148
+ /**
149
+ * The settings this surface has only because of what is stored in it — the
150
+ * appearance surface's colour pickers for the cosmetics a player is
151
+ * currently wearing.
152
+ *
153
+ * Derived rather than fixed is the whole mechanism: a setting that exists
154
+ * only while its cosmetic is equipped is dropped by {@link sanitize} the
155
+ * moment the cosmetic is not, so unequipping an item takes its colour with
156
+ * it and re-equipping cannot resurrect a colour the player never chose.
157
+ *
158
+ * Reads the surface's FIXED settings only, never its own output, so the
159
+ * descriptor list is resolved in one pass and cannot recurse.
160
+ */
161
+ derived?: (settings: Settings) => readonly SettingDescriptor[];
162
+ }
File without changes
@@ -212,3 +212,11 @@ export interface CurrentAction {
212
212
  since: string;
213
213
  subject?: string;
214
214
  }
215
+ export interface ActionTimelineEvent {
216
+ kind: ActionEventKind;
217
+ at: Date;
218
+ }
219
+ export interface ActionDurations {
220
+ accruedSeconds: number;
221
+ focusedSeconds: number;
222
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flow-industries/id",
3
- "version": "0.21.2",
3
+ "version": "0.22.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -50,6 +50,10 @@
50
50
  "./start/server": {
51
51
  "types": "./dist/sdk/start/server.d.ts",
52
52
  "import": "./dist/sdk/start/server.js"
53
+ },
54
+ "./focus": {
55
+ "types": "./dist/sdk/client/focus.d.ts",
56
+ "import": "./dist/sdk/client/focus.js"
53
57
  }
54
58
  },
55
59
  "files": [
@@ -126,7 +130,7 @@
126
130
  ],
127
131
  "dependencies": {
128
132
  "@aws-sdk/client-s3": "^3.1073.0",
129
- "@flow-industries/ui": "^0.21.0",
133
+ "@flow-industries/ui": "^0.21.1",
130
134
  "@hono/otel": "^1.1.2",
131
135
  "@openobserve/browser-logs": "^0.3.1",
132
136
  "@openobserve/browser-rum": "^0.3.1",