@trypattern/sdk 0.0.0-stage → 0.1.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 +127 -2
- package/dist/access.d.ts +31 -0
- package/dist/access.js +72 -0
- package/dist/actions.d.ts +57 -0
- package/dist/actions.js +198 -0
- package/dist/api.d.ts +20 -0
- package/dist/api.js +34 -0
- package/dist/app.d.ts +106 -0
- package/dist/app.js +224 -0
- package/dist/appearance.d.ts +7 -0
- package/dist/appearance.js +2 -0
- package/dist/auth/sessionApi.d.ts +41 -0
- package/dist/auth/sessionApi.js +158 -0
- package/dist/auth/sessionStore.d.ts +20 -0
- package/dist/auth/sessionStore.js +40 -0
- package/dist/auth/user.d.ts +11 -0
- package/dist/auth/user.js +9 -0
- package/dist/builtins/Account.d.ts +25 -0
- package/dist/builtins/Account.js +15 -0
- package/dist/builtins/Palette.d.ts +29 -0
- package/dist/builtins/Palette.js +63 -0
- package/dist/builtins/SignIn.d.ts +28 -0
- package/dist/builtins/SignIn.js +60 -0
- package/dist/builtins/Toasts.d.ts +32 -0
- package/dist/builtins/Toasts.js +24 -0
- package/dist/builtins/actions.d.ts +2 -0
- package/dist/builtins/actions.js +45 -0
- package/dist/cache.d.ts +24 -0
- package/dist/cache.js +112 -0
- package/dist/cli/app.d.ts +2 -0
- package/dist/cli/app.js +1329 -0
- package/dist/cli/screenshot.d.ts +1 -0
- package/dist/cli/screenshot.js +96 -0
- package/dist/cli/tools.d.ts +12 -0
- package/dist/cli/tools.js +35 -0
- package/dist/components/AppFrame.d.ts +14 -0
- package/dist/components/AppFrame.js +64 -0
- package/dist/components/AppearancePicker.d.ts +1 -0
- package/dist/components/AppearancePicker.js +12 -0
- package/dist/components/Avatar.d.ts +9 -0
- package/dist/components/Avatar.js +63 -0
- package/dist/components/Badge.d.ts +11 -0
- package/dist/components/Badge.js +87 -0
- package/dist/components/Button.d.ts +23 -0
- package/dist/components/Button.js +249 -0
- package/dist/components/Card.d.ts +12 -0
- package/dist/components/Card.js +38 -0
- package/dist/components/CommandMenu.d.ts +23 -0
- package/dist/components/CommandMenu.js +214 -0
- package/dist/components/CommandPalette.d.ts +1 -0
- package/dist/components/CommandPalette.js +7 -0
- package/dist/components/Content.d.ts +7 -0
- package/dist/components/Content.js +24 -0
- package/dist/components/ContextMenu.d.ts +8 -0
- package/dist/components/ContextMenu.js +23 -0
- package/dist/components/Dialog.d.ts +11 -0
- package/dist/components/Dialog.js +81 -0
- package/dist/components/Divider.d.ts +6 -0
- package/dist/components/Divider.js +15 -0
- package/dist/components/EmptyState.d.ts +8 -0
- package/dist/components/EmptyState.js +24 -0
- package/dist/components/Form.d.ts +9 -0
- package/dist/components/Form.js +23 -0
- package/dist/components/Icon.d.ts +10 -0
- package/dist/components/Icon.js +22 -0
- package/dist/components/IconButton.d.ts +14 -0
- package/dist/components/IconButton.js +53 -0
- package/dist/components/Kbd.d.ts +6 -0
- package/dist/components/Kbd.js +67 -0
- package/dist/components/Link.d.ts +9 -0
- package/dist/components/Link.js +35 -0
- package/dist/components/ListRow.d.ts +12 -0
- package/dist/components/ListRow.js +62 -0
- package/dist/components/LoadingRows.d.ts +5 -0
- package/dist/components/LoadingRows.js +6 -0
- package/dist/components/Menu.d.ts +12 -0
- package/dist/components/Menu.js +33 -0
- package/dist/components/MenuItems.d.ts +21 -0
- package/dist/components/MenuItems.js +32 -0
- package/dist/components/Placement.d.ts +21 -0
- package/dist/components/Placement.js +1 -0
- package/dist/components/Popup.d.ts +8 -0
- package/dist/components/Popup.js +43 -0
- package/dist/components/PopupItem.d.ts +8 -0
- package/dist/components/PopupItem.js +51 -0
- package/dist/components/PropertyRow.d.ts +7 -0
- package/dist/components/PropertyRow.js +23 -0
- package/dist/components/Row.d.ts +16 -0
- package/dist/components/Row.js +27 -0
- package/dist/components/Screen.d.ts +13 -0
- package/dist/components/Screen.js +56 -0
- package/dist/components/Scrim.d.ts +8 -0
- package/dist/components/Scrim.js +40 -0
- package/dist/components/ScrollArea.d.ts +15 -0
- package/dist/components/ScrollArea.js +13 -0
- package/dist/components/SegmentedControl.d.ts +17 -0
- package/dist/components/SegmentedControl.js +68 -0
- package/dist/components/Select.d.ts +17 -0
- package/dist/components/Select.js +107 -0
- package/dist/components/SettingsRow.d.ts +8 -0
- package/dist/components/SettingsRow.js +32 -0
- package/dist/components/Sidebar.d.ts +20 -0
- package/dist/components/Sidebar.js +138 -0
- package/dist/components/SignInScreen.d.ts +1 -0
- package/dist/components/SignInScreen.js +40 -0
- package/dist/components/Size.d.ts +1 -0
- package/dist/components/Size.js +1 -0
- package/dist/components/Skeleton.d.ts +8 -0
- package/dist/components/Skeleton.js +48 -0
- package/dist/components/Spinner.d.ts +6 -0
- package/dist/components/Spinner.js +54 -0
- package/dist/components/Stack.d.ts +14 -0
- package/dist/components/Stack.js +19 -0
- package/dist/components/Switch.d.ts +10 -0
- package/dist/components/Switch.js +71 -0
- package/dist/components/Text.d.ts +21 -0
- package/dist/components/Text.js +77 -0
- package/dist/components/TextField.d.ts +25 -0
- package/dist/components/TextField.js +68 -0
- package/dist/components/Toaster.d.ts +1 -0
- package/dist/components/Toaster.js +83 -0
- package/dist/components/Tooltip.d.ts +8 -0
- package/dist/components/Tooltip.js +43 -0
- package/dist/components/controlLook.d.ts +60 -0
- package/dist/components/controlLook.js +67 -0
- package/dist/components/frame.d.ts +4 -0
- package/dist/components/frame.js +2 -0
- package/dist/components/icons.d.ts +58 -0
- package/dist/components/icons.js +105 -0
- package/dist/components/interaction.d.ts +12 -0
- package/dist/components/interaction.js +22 -0
- package/dist/components/spacing.d.ts +118 -0
- package/dist/components/spacing.js +46 -0
- package/dist/components/tones.d.ts +28 -0
- package/dist/components/tones.js +12 -0
- package/dist/entityStore.d.ts +46 -0
- package/dist/entityStore.js +160 -0
- package/dist/flows/accessFlows.d.ts +9 -0
- package/dist/flows/accessFlows.js +151 -0
- package/dist/flows/browser.d.ts +32 -0
- package/dist/flows/browser.js +164 -0
- package/dist/flows/contactSheet.d.ts +1 -0
- package/dist/flows/contactSheet.js +63 -0
- package/dist/flows/flowBrowser.d.ts +23 -0
- package/dist/flows/flowBrowser.js +21 -0
- package/dist/flows/flowSetup.d.ts +3 -0
- package/dist/flows/flowSetup.js +35 -0
- package/dist/flows/flows.d.ts +41 -0
- package/dist/flows/flows.js +148 -0
- package/dist/flows/screenshotName.d.ts +1 -0
- package/dist/flows/screenshotName.js +6 -0
- package/dist/gate.d.ts +8 -0
- package/dist/gate.js +52 -0
- package/dist/hooks/appContext.d.ts +14 -0
- package/dist/hooks/appContext.js +5 -0
- package/dist/hooks/appHooks.d.ts +21 -0
- package/dist/hooks/appHooks.js +24 -0
- package/dist/hooks/useAnimatesOnChange.d.ts +1 -0
- package/dist/hooks/useAnimatesOnChange.js +14 -0
- package/dist/hooks/useStoreState.d.ts +6 -0
- package/dist/hooks/useStoreState.js +5 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +19 -0
- package/dist/memoryApi.d.ts +19 -0
- package/dist/memoryApi.js +42 -0
- package/dist/model.d.ts +94 -0
- package/dist/model.js +243 -0
- package/dist/page.d.ts +11 -0
- package/dist/page.js +9 -0
- package/dist/platform.d.ts +1 -0
- package/dist/platform.js +5 -0
- package/dist/react.d.ts +46 -0
- package/dist/react.js +38 -0
- package/dist/registered.d.ts +54 -0
- package/dist/registered.js +17 -0
- package/dist/routes.d.ts +38 -0
- package/dist/routes.js +21 -0
- package/dist/rules/checkAllFiles.d.ts +1 -0
- package/dist/rules/checkAllFiles.js +741 -0
- package/dist/rules/checkApiRouting.d.ts +1 -0
- package/dist/rules/checkApiRouting.js +112 -0
- package/dist/rules/lintPlugin.d.ts +252 -0
- package/dist/rules/lintPlugin.js +1157 -0
- package/dist/rules/messages.d.ts +48 -0
- package/dist/rules/messages.js +48 -0
- package/dist/rules/themeTokens.d.ts +6 -0
- package/dist/rules/themeTokens.js +64 -0
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +1 -0
- package/dist/server/auth.d.ts +115 -0
- package/dist/server/auth.js +131 -0
- package/dist/server/authService.d.ts +9 -0
- package/dist/server/authService.js +50 -0
- package/dist/server/createServer.d.ts +357 -0
- package/dist/server/createServer.js +62 -0
- package/dist/server/env.d.ts +15 -0
- package/dist/server/env.js +1 -0
- package/dist/server/index.d.ts +8 -0
- package/dist/server/index.js +5 -0
- package/dist/server/session.d.ts +28 -0
- package/dist/server/session.js +54 -0
- package/dist/server/sessions.d.ts +92 -0
- package/dist/server/sessions.js +47 -0
- package/dist/server/signInWire.d.ts +52 -0
- package/dist/server/signInWire.js +22 -0
- package/dist/server/tables.d.ts +242 -0
- package/dist/server/tables.js +246 -0
- package/dist/server/users.d.ts +189 -0
- package/dist/server/users.js +49 -0
- package/dist/shortcuts.d.ts +3 -0
- package/dist/shortcuts.js +40 -0
- package/dist/start.d.ts +20 -0
- package/dist/start.js +121 -0
- package/dist/state.d.ts +14 -0
- package/dist/state.js +17 -0
- package/dist/store.d.ts +43 -0
- package/dist/store.js +198 -0
- package/dist/subscribers.d.ts +5 -0
- package/dist/subscribers.js +13 -0
- package/dist/table.d.ts +143 -0
- package/dist/table.js +91 -0
- package/dist/tables.d.ts +77 -0
- package/dist/tables.js +216 -0
- package/dist/theme/reset.css +84 -0
- package/dist/theme/scales.d.ts +76 -0
- package/dist/theme/scales.js +82 -0
- package/dist/theme/tokens.stylex.d.ts +218 -0
- package/dist/theme/tokens.stylex.js +233 -0
- package/dist/vite/commandBridge.d.ts +2 -0
- package/dist/vite/commandBridge.js +46 -0
- package/dist/vite/designManifest.d.ts +25 -0
- package/dist/vite/designManifest.js +191 -0
- package/dist/vite/localWorker.d.ts +4 -0
- package/dist/vite/localWorker.js +59 -0
- package/dist/vite/rootAlias.d.ts +2 -0
- package/dist/vite/rootAlias.js +15 -0
- package/dist/vite/vite.d.ts +7 -0
- package/dist/vite/vite.js +88 -0
- package/dist/wire.d.ts +7 -0
- package/dist/wire.js +1 -0
- package/package.json +99 -3
- package/presets/lintPlugin.js +7 -0
- package/presets/oxlint.json +167 -0
- package/presets/register.d.ts +19 -0
- package/presets/server.d.ts +2 -0
- package/presets/tsconfig.app.json +29 -0
- package/presets/tsconfig.server.json +27 -0
package/dist/app.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { viewerOf } from "./access.js";
|
|
3
|
+
import { SessionStore } from "./auth/sessionStore.js";
|
|
4
|
+
import { ActionsStore } from "./actions.js";
|
|
5
|
+
import { Account } from "./builtins/Account.js";
|
|
6
|
+
import { BUILT_IN_GROUPS, builtInActions } from "./builtins/actions.js";
|
|
7
|
+
import { Palette } from "./builtins/Palette.js";
|
|
8
|
+
import { SignIn } from "./builtins/SignIn.js";
|
|
9
|
+
import { Toasts } from "./builtins/Toasts.js";
|
|
10
|
+
import { PageStore } from "./page.js";
|
|
11
|
+
import { assertNotBuilding, snapshotsOf } from "./store.js";
|
|
12
|
+
import { ModelBase, command, createModels, modelSnapshotsOf, parseMessage, sendMessage } from "./model.js";
|
|
13
|
+
import { isRoutedPath, pageIds, registerRoutes } from "./routes.js";
|
|
14
|
+
import { PublicTableStore, TableStore, publicStoreName } from "./tables.js";
|
|
15
|
+
function dataStores(tables, api, cache, viewer) {
|
|
16
|
+
const stores = { tables: {}, publicViews: {} };
|
|
17
|
+
for (const [key, table] of Object.entries(tables)) {
|
|
18
|
+
const found = Reflect.get(api, key);
|
|
19
|
+
const tableApi = found;
|
|
20
|
+
const publicView = table.public.length > 0
|
|
21
|
+
? new PublicTableStore(table, { api: tableApi, cache })
|
|
22
|
+
: undefined;
|
|
23
|
+
if (publicView !== undefined) {
|
|
24
|
+
stores.publicViews[publicStoreName(key)] = publicView;
|
|
25
|
+
}
|
|
26
|
+
if (table.access.read !== 'server') {
|
|
27
|
+
stores.tables[key] = new TableStore(table, {
|
|
28
|
+
api: tableApi,
|
|
29
|
+
cache,
|
|
30
|
+
viewer,
|
|
31
|
+
...(publicView === undefined ? {} : { publicView })
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return stores;
|
|
36
|
+
}
|
|
37
|
+
function clashOf(groups) {
|
|
38
|
+
const seen = new Set();
|
|
39
|
+
for (const name of groups.flat()) {
|
|
40
|
+
if (seen.has(name))
|
|
41
|
+
return name;
|
|
42
|
+
seen.add(name);
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const builtInModels = { account: Account, palette: Palette, signIn: SignIn };
|
|
47
|
+
const appPathInput = z.custom((value) => typeof value === 'string' && isRoutedPath(value), { error: 'Not a page in src/routes.tsx' });
|
|
48
|
+
export function defineApp(definition) {
|
|
49
|
+
const pages = new Set(pageIds(definition.routes));
|
|
50
|
+
registerRoutes(definition.routes);
|
|
51
|
+
if (Object.hasOwn(definition.models, 'app')) {
|
|
52
|
+
throw new Error('A model can\'t be called "app": app.navigate is the built-in command that changes the page');
|
|
53
|
+
}
|
|
54
|
+
for (const name of Object.keys(builtInModels)) {
|
|
55
|
+
if (Object.hasOwn(definition.models, name)) {
|
|
56
|
+
throw new Error(`${name} is a built-in model of @trypattern/sdk: its state, answers and commands come from Pattern. Name yours for what it holds.`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return class App {
|
|
60
|
+
static definition = definition;
|
|
61
|
+
name = definition.name;
|
|
62
|
+
router;
|
|
63
|
+
models;
|
|
64
|
+
stores;
|
|
65
|
+
#commandGroups;
|
|
66
|
+
#page = new PageStore();
|
|
67
|
+
#cache;
|
|
68
|
+
#gate;
|
|
69
|
+
#session;
|
|
70
|
+
#data;
|
|
71
|
+
#navigations = new Set();
|
|
72
|
+
#failures = [];
|
|
73
|
+
#path;
|
|
74
|
+
#user;
|
|
75
|
+
constructor(router, { api, cache, gate }) {
|
|
76
|
+
this.router = router;
|
|
77
|
+
this.#cache = cache;
|
|
78
|
+
this.#gate = gate;
|
|
79
|
+
const session = new SessionStore({ api: api.session });
|
|
80
|
+
const data = dataStores(definition.tables, api.tables, cache, () => viewerOf(session.state.user));
|
|
81
|
+
const custom = Object.fromEntries(Object.entries(definition.stores ?? {}).map(([key, Store]) => [
|
|
82
|
+
key,
|
|
83
|
+
new Store(cache.persisted(`stores.${key}`))
|
|
84
|
+
]));
|
|
85
|
+
const toasts = new Toasts(cache.persisted('stores.toasts'));
|
|
86
|
+
const clash = clashOf([
|
|
87
|
+
['session', 'actions', 'toasts', 'page'],
|
|
88
|
+
Object.keys(data.tables),
|
|
89
|
+
Object.keys(data.publicViews),
|
|
90
|
+
Object.keys(custom)
|
|
91
|
+
]);
|
|
92
|
+
if (clash !== undefined) {
|
|
93
|
+
throw new Error(`Two stores are called "${clash}". The built-in session, actions, toasts and page stores, each table, each table's public view and each store in src/stores/index.ts need their own name.`);
|
|
94
|
+
}
|
|
95
|
+
const actions = new ActionsStore(this.#page, [...(definition.actions ?? []), ...builtInActions], [
|
|
96
|
+
...(definition.actionGroups ?? []),
|
|
97
|
+
...BUILT_IN_GROUPS.filter((group) => !(definition.actionGroups ?? []).includes(group))
|
|
98
|
+
], (message) => this.#runAction(message));
|
|
99
|
+
this.stores = {
|
|
100
|
+
session,
|
|
101
|
+
actions,
|
|
102
|
+
toasts,
|
|
103
|
+
page: this.#page,
|
|
104
|
+
...data.tables,
|
|
105
|
+
...data.publicViews,
|
|
106
|
+
...custom
|
|
107
|
+
};
|
|
108
|
+
this.#session = session;
|
|
109
|
+
this.#data = data;
|
|
110
|
+
const services = {
|
|
111
|
+
navigate: (to) => {
|
|
112
|
+
assertNotBuilding('navigate');
|
|
113
|
+
return this.#navigate(to);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
this.models = createModels({ ...builtInModels, ...definition.models }, this.stores, services, (model) => cache.persisted(model));
|
|
117
|
+
this.#commandGroups = {
|
|
118
|
+
...this.models,
|
|
119
|
+
app: {
|
|
120
|
+
navigate: command({ input: { to: appPathInput } }, ({ to }) => this.#navigate(to)),
|
|
121
|
+
pressKey: command({ input: { key: z.string() } }, ({ key }) => this.pressKey(key))
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
this.#path = router.state.location.pathname;
|
|
125
|
+
this.#page.moved(this.#currentPage(), this.#currentParams());
|
|
126
|
+
actions.watch(this.models);
|
|
127
|
+
this.#user = session.state.user?.id ?? null;
|
|
128
|
+
router.subscribe(() => this.#routeChanged());
|
|
129
|
+
session.subscribe(() => this.#sessionChanged());
|
|
130
|
+
void session.confirm();
|
|
131
|
+
for (const view of Object.values(data.publicViews))
|
|
132
|
+
void view.load();
|
|
133
|
+
this.#loadReadableTables();
|
|
134
|
+
}
|
|
135
|
+
send(command) {
|
|
136
|
+
return this.#send(command);
|
|
137
|
+
}
|
|
138
|
+
pressKey(key) {
|
|
139
|
+
return this.stores.actions.pressKey(key);
|
|
140
|
+
}
|
|
141
|
+
parse(raw) {
|
|
142
|
+
return parseMessage(raw, this.#commandGroups);
|
|
143
|
+
}
|
|
144
|
+
getSnapshot() {
|
|
145
|
+
return {
|
|
146
|
+
page: this.#currentPage(),
|
|
147
|
+
path: this.router.state.location.pathname,
|
|
148
|
+
models: modelSnapshotsOf(this.models),
|
|
149
|
+
stores: snapshotsOf(this.stores)
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
async whenIdle() {
|
|
153
|
+
do {
|
|
154
|
+
await Promise.allSettled(this.#navigations);
|
|
155
|
+
await this.#gate.whenIdle();
|
|
156
|
+
} while (this.#navigations.size > 0);
|
|
157
|
+
const failure = this.#failures.shift();
|
|
158
|
+
if (failure !== undefined)
|
|
159
|
+
throw failure;
|
|
160
|
+
}
|
|
161
|
+
#runAction(message) {
|
|
162
|
+
const parsed = this.parse(message);
|
|
163
|
+
if ('error' in parsed) {
|
|
164
|
+
return Promise.reject(new Error(String(parsed.error)));
|
|
165
|
+
}
|
|
166
|
+
return this.#send(parsed);
|
|
167
|
+
}
|
|
168
|
+
#send(command) {
|
|
169
|
+
return sendMessage(this.#commandGroups, command).catch((failure) => {
|
|
170
|
+
this.#failures.push(failure);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
#currentPage() {
|
|
174
|
+
const id = this.router.state.matches.at(-1)?.route.id;
|
|
175
|
+
return id !== undefined && pages.has(id) ? id : null;
|
|
176
|
+
}
|
|
177
|
+
#currentParams() {
|
|
178
|
+
return this.router.state.matches.at(-1)?.params ?? {};
|
|
179
|
+
}
|
|
180
|
+
#navigate(to) {
|
|
181
|
+
const navigation = this.router.navigate(to).finally(() => {
|
|
182
|
+
this.#navigations.delete(navigation);
|
|
183
|
+
});
|
|
184
|
+
this.#navigations.add(navigation);
|
|
185
|
+
return navigation;
|
|
186
|
+
}
|
|
187
|
+
#routeChanged() {
|
|
188
|
+
const path = this.router.state.location.pathname;
|
|
189
|
+
if (path === this.#path)
|
|
190
|
+
return;
|
|
191
|
+
this.#path = path;
|
|
192
|
+
this.#page.moved(this.#currentPage(), this.#currentParams());
|
|
193
|
+
this.#openCurrentModel();
|
|
194
|
+
}
|
|
195
|
+
#sessionChanged() {
|
|
196
|
+
const user = this.#session.state.user?.id ?? null;
|
|
197
|
+
if (user === this.#user)
|
|
198
|
+
return;
|
|
199
|
+
this.#user = user;
|
|
200
|
+
this.#cache.clear();
|
|
201
|
+
for (const store of Object.values(this.#data.tables))
|
|
202
|
+
store.forget();
|
|
203
|
+
this.#loadReadableTables();
|
|
204
|
+
}
|
|
205
|
+
#loadReadableTables() {
|
|
206
|
+
for (const store of Object.values(this.#data.tables))
|
|
207
|
+
void store.load();
|
|
208
|
+
this.#openCurrentModel();
|
|
209
|
+
}
|
|
210
|
+
#openCurrentModel() {
|
|
211
|
+
const match = this.router.state.matches.at(-1);
|
|
212
|
+
if (match === undefined)
|
|
213
|
+
return;
|
|
214
|
+
const model = Object.hasOwn(this.models, match.route.id)
|
|
215
|
+
? Reflect.get(this.models, match.route.id)
|
|
216
|
+
: undefined;
|
|
217
|
+
if (!(model instanceof ModelBase))
|
|
218
|
+
return;
|
|
219
|
+
void Promise.resolve(model.onPageOpen(match.params)).catch((failure) => {
|
|
220
|
+
this.#failures.push(failure);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Gate } from '../gate.ts';
|
|
2
|
+
import type { MemoryApiOptions } from '../memoryApi.ts';
|
|
3
|
+
import type { User } from './user.ts';
|
|
4
|
+
export type SessionApi = {
|
|
5
|
+
rememberedUser: () => User | undefined;
|
|
6
|
+
me: () => Promise<User | undefined>;
|
|
7
|
+
devSignIn: (email: string) => Promise<User>;
|
|
8
|
+
sendEmailCode: (email: string) => Promise<EmailCodeOutcome>;
|
|
9
|
+
verifyEmailCode: (code: string) => Promise<User | undefined>;
|
|
10
|
+
signOut: () => Promise<void>;
|
|
11
|
+
startGoogleSignIn: () => void;
|
|
12
|
+
onSignedOut: (listener: () => void) => void;
|
|
13
|
+
};
|
|
14
|
+
export type EmailCodeOutcome = 'sent' | 'refused';
|
|
15
|
+
export type SessionCall = 'me' | 'devSignIn' | 'sendEmailCode' | 'verifyEmailCode' | 'signOut';
|
|
16
|
+
export declare const sessionCalls: readonly SessionCall[];
|
|
17
|
+
export declare class HttpSessionApi implements SessionApi {
|
|
18
|
+
#private;
|
|
19
|
+
constructor(gate: Gate);
|
|
20
|
+
rememberedUser(): User | undefined;
|
|
21
|
+
me(): Promise<User | undefined>;
|
|
22
|
+
devSignIn(email: string): Promise<User>;
|
|
23
|
+
sendEmailCode(email: string): Promise<EmailCodeOutcome>;
|
|
24
|
+
verifyEmailCode(code: string): Promise<User | undefined>;
|
|
25
|
+
signOut(): Promise<void>;
|
|
26
|
+
startGoogleSignIn(): void;
|
|
27
|
+
onSignedOut(listener: () => void): void;
|
|
28
|
+
reportSignedOut(): void;
|
|
29
|
+
}
|
|
30
|
+
export declare class MemorySessionApi implements SessionApi {
|
|
31
|
+
#private;
|
|
32
|
+
constructor(signedInAs: string | null, admins: readonly string[], options: MemoryApiOptions);
|
|
33
|
+
rememberedUser(): User | undefined;
|
|
34
|
+
me(): Promise<User | undefined>;
|
|
35
|
+
devSignIn(email: string): Promise<User>;
|
|
36
|
+
sendEmailCode(email: string): Promise<EmailCodeOutcome>;
|
|
37
|
+
verifyEmailCode(code: string): Promise<User | undefined>;
|
|
38
|
+
signOut(): Promise<void>;
|
|
39
|
+
startGoogleSignIn(): void;
|
|
40
|
+
onSignedOut(): void;
|
|
41
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { adminEmails } from "../access.js";
|
|
2
|
+
import { MemoryServer } from "../memoryApi.js";
|
|
3
|
+
import { ownerIdOf } from "../table.js";
|
|
4
|
+
import { USER_COOKIE, userSchema } from "./user.js";
|
|
5
|
+
export const sessionCalls = [
|
|
6
|
+
'me',
|
|
7
|
+
'devSignIn',
|
|
8
|
+
'sendEmailCode',
|
|
9
|
+
'verifyEmailCode',
|
|
10
|
+
'signOut'
|
|
11
|
+
];
|
|
12
|
+
const MEMORY_EMAIL_CODE = '123456';
|
|
13
|
+
const JSON_HEADERS = { 'content-type': 'application/json' };
|
|
14
|
+
export class HttpSessionApi {
|
|
15
|
+
#gate;
|
|
16
|
+
#signedOutListeners = new Set();
|
|
17
|
+
constructor(gate) {
|
|
18
|
+
this.#gate = gate;
|
|
19
|
+
}
|
|
20
|
+
rememberedUser() {
|
|
21
|
+
const prefix = `${USER_COOKIE}=`;
|
|
22
|
+
const cookie = document.cookie
|
|
23
|
+
.split('; ')
|
|
24
|
+
.find((part) => part.startsWith(prefix));
|
|
25
|
+
if (cookie === undefined)
|
|
26
|
+
return undefined;
|
|
27
|
+
try {
|
|
28
|
+
const json = JSON.parse(decodeURIComponent(cookie.slice(prefix.length)));
|
|
29
|
+
const parsed = userSchema.safeParse(json);
|
|
30
|
+
return parsed.success ? parsed.data : undefined;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
me() {
|
|
37
|
+
return this.#gate.pass('session.me', async () => {
|
|
38
|
+
const response = await fetch('/api/me');
|
|
39
|
+
if (response.status === 401)
|
|
40
|
+
return undefined;
|
|
41
|
+
if (!response.ok)
|
|
42
|
+
throw new Error('session.me failed');
|
|
43
|
+
return userSchema.parse(await response.json());
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
devSignIn(email) {
|
|
47
|
+
return this.#gate.pass('session.devSignIn', async () => {
|
|
48
|
+
const response = await fetch('/api/auth/dev-sign-in', {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: JSON_HEADERS,
|
|
51
|
+
body: JSON.stringify({ email })
|
|
52
|
+
});
|
|
53
|
+
if (!response.ok)
|
|
54
|
+
throw new Error('session.devSignIn failed');
|
|
55
|
+
return userSchema.parse(await response.json());
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
sendEmailCode(email) {
|
|
59
|
+
return this.#gate.pass('session.sendEmailCode', async () => {
|
|
60
|
+
const response = await fetch('/api/auth/email', {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: JSON_HEADERS,
|
|
63
|
+
body: JSON.stringify({ email })
|
|
64
|
+
});
|
|
65
|
+
return response.ok ? 'sent' : 'refused';
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
verifyEmailCode(code) {
|
|
69
|
+
return this.#gate.pass('session.verifyEmailCode', async () => {
|
|
70
|
+
const response = await fetch('/api/auth/email/verify', {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: JSON_HEADERS,
|
|
73
|
+
body: JSON.stringify({ code })
|
|
74
|
+
});
|
|
75
|
+
if (response.status === 401)
|
|
76
|
+
return undefined;
|
|
77
|
+
if (!response.ok)
|
|
78
|
+
throw new Error('session.verifyEmailCode failed');
|
|
79
|
+
return userSchema.parse(await response.json());
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
signOut() {
|
|
83
|
+
return this.#gate.pass('session.signOut', async () => {
|
|
84
|
+
await fetch('/api/auth/sign-out', { method: 'POST' });
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
startGoogleSignIn() {
|
|
88
|
+
location.assign('/api/auth/google');
|
|
89
|
+
}
|
|
90
|
+
onSignedOut(listener) {
|
|
91
|
+
this.#signedOutListeners.add(listener);
|
|
92
|
+
}
|
|
93
|
+
reportSignedOut() {
|
|
94
|
+
for (const listener of this.#signedOutListeners)
|
|
95
|
+
listener();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function devUser(email, admins) {
|
|
99
|
+
const name = email.split('@')[0] ?? email;
|
|
100
|
+
return {
|
|
101
|
+
id: ownerIdOf(email),
|
|
102
|
+
email,
|
|
103
|
+
name,
|
|
104
|
+
avatarUrl: null,
|
|
105
|
+
isAdmin: admins.has(email.toLowerCase())
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export class MemorySessionApi {
|
|
109
|
+
#server;
|
|
110
|
+
#admins;
|
|
111
|
+
#user;
|
|
112
|
+
#emailCodeFor;
|
|
113
|
+
constructor(signedInAs, admins, options) {
|
|
114
|
+
this.#server = new MemoryServer('session', options);
|
|
115
|
+
this.#admins = adminEmails(admins);
|
|
116
|
+
this.#user =
|
|
117
|
+
signedInAs === null ? undefined : devUser(signedInAs, this.#admins);
|
|
118
|
+
}
|
|
119
|
+
rememberedUser() {
|
|
120
|
+
return this.#user;
|
|
121
|
+
}
|
|
122
|
+
me() {
|
|
123
|
+
return this.#server.reply('me', () => this.#user);
|
|
124
|
+
}
|
|
125
|
+
devSignIn(email) {
|
|
126
|
+
return this.#server.reply('devSignIn', () => {
|
|
127
|
+
this.#user = devUser(email, this.#admins);
|
|
128
|
+
return this.#user;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
sendEmailCode(email) {
|
|
132
|
+
return this.#server.reply('sendEmailCode', () => {
|
|
133
|
+
this.#emailCodeFor = email;
|
|
134
|
+
return 'sent';
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
verifyEmailCode(code) {
|
|
138
|
+
return this.#server.reply('verifyEmailCode', () => {
|
|
139
|
+
if (this.#emailCodeFor === undefined || code !== MEMORY_EMAIL_CODE) {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
this.#user = devUser(this.#emailCodeFor, this.#admins);
|
|
143
|
+
this.#emailCodeFor = undefined;
|
|
144
|
+
return this.#user;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
signOut() {
|
|
148
|
+
return this.#server.reply('signOut', () => {
|
|
149
|
+
this.#user = undefined;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
startGoogleSignIn() {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
onSignedOut() {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { StoreBase } from '../store.ts';
|
|
2
|
+
import type { EmailCodeOutcome, SessionApi } from './sessionApi.ts';
|
|
3
|
+
import type { User } from './user.ts';
|
|
4
|
+
export type SessionState = {
|
|
5
|
+
user: User | null;
|
|
6
|
+
};
|
|
7
|
+
export declare class SessionStore extends StoreBase<SessionState, {
|
|
8
|
+
api: SessionApi;
|
|
9
|
+
}> {
|
|
10
|
+
constructor(deps: {
|
|
11
|
+
api: SessionApi;
|
|
12
|
+
});
|
|
13
|
+
protected initialState(): SessionState;
|
|
14
|
+
confirm(): Promise<void>;
|
|
15
|
+
devSignIn(email: string): Promise<void>;
|
|
16
|
+
sendEmailCode(email: string): Promise<EmailCodeOutcome>;
|
|
17
|
+
verifyEmailCode(code: string): Promise<boolean>;
|
|
18
|
+
continueWithGoogle(): void;
|
|
19
|
+
signOut(): Promise<void>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { StoreBase } from "../store.js";
|
|
2
|
+
export class SessionStore extends StoreBase {
|
|
3
|
+
constructor(deps) {
|
|
4
|
+
super(deps);
|
|
5
|
+
deps.api.onSignedOut(() => void this.confirm());
|
|
6
|
+
}
|
|
7
|
+
initialState() {
|
|
8
|
+
return { user: this.deps.api.rememberedUser() ?? null };
|
|
9
|
+
}
|
|
10
|
+
async confirm() {
|
|
11
|
+
if (this.state.user === null)
|
|
12
|
+
return;
|
|
13
|
+
try {
|
|
14
|
+
this.set({ user: (await this.deps.api.me()) ?? null });
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async devSignIn(email) {
|
|
21
|
+
this.set({ user: await this.deps.api.devSignIn(email) });
|
|
22
|
+
}
|
|
23
|
+
sendEmailCode(email) {
|
|
24
|
+
return this.deps.api.sendEmailCode(email);
|
|
25
|
+
}
|
|
26
|
+
async verifyEmailCode(code) {
|
|
27
|
+
const user = await this.deps.api.verifyEmailCode(code);
|
|
28
|
+
if (user === undefined)
|
|
29
|
+
return false;
|
|
30
|
+
this.set({ user });
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
continueWithGoogle() {
|
|
34
|
+
this.deps.api.startGoogleSignIn();
|
|
35
|
+
}
|
|
36
|
+
async signOut() {
|
|
37
|
+
this.set({ user: null });
|
|
38
|
+
await this.deps.api.signOut();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const userSchema: z.ZodObject<{
|
|
3
|
+
id: z.ZodString;
|
|
4
|
+
email: z.ZodString;
|
|
5
|
+
name: z.ZodString;
|
|
6
|
+
avatarUrl: z.ZodNullable<z.ZodString>;
|
|
7
|
+
isAdmin: z.ZodDefault<z.ZodBoolean>;
|
|
8
|
+
}, z.core.$strip>;
|
|
9
|
+
export type User = z.output<typeof userSchema>;
|
|
10
|
+
export type StoredUser = Omit<User, 'isAdmin'>;
|
|
11
|
+
export declare const USER_COOKIE = "pattern_user";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
declare const Account_base: import("../model.ts").ModelClass<import("../registered.ts").AppStores, import("../registered.ts").ModelServices, {
|
|
2
|
+
appearance: import("zod").ZodDefault<import("zod").ZodEnum<{
|
|
3
|
+
system: "system";
|
|
4
|
+
light: "light";
|
|
5
|
+
dark: "dark";
|
|
6
|
+
}>>;
|
|
7
|
+
}, readonly ["appearance"]>;
|
|
8
|
+
export declare class Account extends Account_base {
|
|
9
|
+
get user(): {
|
|
10
|
+
id: string;
|
|
11
|
+
email: string;
|
|
12
|
+
name: string;
|
|
13
|
+
avatarUrl: string | null;
|
|
14
|
+
isAdmin: boolean;
|
|
15
|
+
} | null;
|
|
16
|
+
setAppearance: import("../model.ts").Command<{
|
|
17
|
+
appearance: import("zod").ZodEnum<{
|
|
18
|
+
system: "system";
|
|
19
|
+
light: "light";
|
|
20
|
+
dark: "dark";
|
|
21
|
+
}>;
|
|
22
|
+
}, void>;
|
|
23
|
+
signOut: import("../model.ts").Command<{}, Promise<void>>;
|
|
24
|
+
}
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { appearance } from "../appearance.js";
|
|
2
|
+
import { command } from "../model.js";
|
|
3
|
+
import { Model } from "../registered.js";
|
|
4
|
+
export class Account extends Model({
|
|
5
|
+
state: { appearance: appearance.default('system') },
|
|
6
|
+
persist: ['appearance']
|
|
7
|
+
}) {
|
|
8
|
+
get user() {
|
|
9
|
+
return this.stores.session.state.user;
|
|
10
|
+
}
|
|
11
|
+
setAppearance = command({ input: { appearance } }, ({ appearance }) => {
|
|
12
|
+
this.set({ appearance });
|
|
13
|
+
});
|
|
14
|
+
signOut = command(() => this.stores.session.signOut());
|
|
15
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { AppAction } from '../registered.ts';
|
|
3
|
+
declare const Palette_base: import("../model.ts").ModelClass<import("../registered.ts").AppStores, import("../registered.ts").ModelServices, {
|
|
4
|
+
isOpen: z.ZodDefault<z.ZodBoolean>;
|
|
5
|
+
query: z.ZodDefault<z.ZodString>;
|
|
6
|
+
highlightedId: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
7
|
+
}, []>;
|
|
8
|
+
export declare class Palette extends Palette_base {
|
|
9
|
+
get results(): AppAction[];
|
|
10
|
+
get highlighted(): string | null;
|
|
11
|
+
open: import("../model.ts").Command<{}, void>;
|
|
12
|
+
close: import("../model.ts").Command<{}, void>;
|
|
13
|
+
toggle: import("../model.ts").Command<{}, void>;
|
|
14
|
+
setQuery: import("../model.ts").Command<{
|
|
15
|
+
query: z.ZodString;
|
|
16
|
+
}, void>;
|
|
17
|
+
highlight: import("../model.ts").Command<{
|
|
18
|
+
id: z.ZodString;
|
|
19
|
+
}, void>;
|
|
20
|
+
moveHighlight: import("../model.ts").Command<{
|
|
21
|
+
by: z.ZodNumber;
|
|
22
|
+
}, void>;
|
|
23
|
+
submit: import("../model.ts").Command<{}, Promise<void> | undefined>;
|
|
24
|
+
run: import("../model.ts").Command<{
|
|
25
|
+
id: z.ZodString;
|
|
26
|
+
}, Promise<void> | undefined>;
|
|
27
|
+
private runAction;
|
|
28
|
+
}
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { rankActions } from "../actions.js";
|
|
3
|
+
import { command } from "../model.js";
|
|
4
|
+
import { Model } from "../registered.js";
|
|
5
|
+
export class Palette extends Model({
|
|
6
|
+
state: {
|
|
7
|
+
isOpen: z.boolean().default(false),
|
|
8
|
+
query: z.string().default(''),
|
|
9
|
+
highlightedId: z.string().nullable().default(null)
|
|
10
|
+
}
|
|
11
|
+
}) {
|
|
12
|
+
get results() {
|
|
13
|
+
return rankActions(this.stores.actions.state.items, this.state.query);
|
|
14
|
+
}
|
|
15
|
+
get highlighted() {
|
|
16
|
+
const { highlightedId } = this.state;
|
|
17
|
+
const { results } = this;
|
|
18
|
+
if (results.some((result) => result.id === highlightedId)) {
|
|
19
|
+
return highlightedId;
|
|
20
|
+
}
|
|
21
|
+
return results[0]?.id ?? null;
|
|
22
|
+
}
|
|
23
|
+
open = command(() => {
|
|
24
|
+
this.set({ isOpen: true, query: '', highlightedId: null });
|
|
25
|
+
});
|
|
26
|
+
close = command(() => {
|
|
27
|
+
this.set({ isOpen: false });
|
|
28
|
+
});
|
|
29
|
+
toggle = command(() => {
|
|
30
|
+
if (this.state.isOpen)
|
|
31
|
+
this.close();
|
|
32
|
+
else
|
|
33
|
+
this.open();
|
|
34
|
+
});
|
|
35
|
+
setQuery = command({ input: { query: z.string() } }, ({ query }) => {
|
|
36
|
+
this.set({ query, highlightedId: null });
|
|
37
|
+
});
|
|
38
|
+
highlight = command({ input: { id: z.string() } }, ({ id }) => {
|
|
39
|
+
if (this.results.some((result) => result.id === id)) {
|
|
40
|
+
this.set({ highlightedId: id });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
moveHighlight = command({ input: { by: z.number().int() } }, ({ by }) => {
|
|
44
|
+
const { results } = this;
|
|
45
|
+
if (results.length === 0)
|
|
46
|
+
return;
|
|
47
|
+
const index = results.findIndex((result) => result.id === this.highlighted);
|
|
48
|
+
const next = (((index + by) % results.length) + results.length) % results.length;
|
|
49
|
+
this.set({ highlightedId: results[next]?.id ?? null });
|
|
50
|
+
});
|
|
51
|
+
submit = command(() => {
|
|
52
|
+
const chosen = this.results.find((result) => result.id === this.highlighted);
|
|
53
|
+
return chosen === undefined ? undefined : this.runAction(chosen);
|
|
54
|
+
});
|
|
55
|
+
run = command({ input: { id: z.string() } }, ({ id }) => {
|
|
56
|
+
const chosen = this.results.find((result) => result.id === id);
|
|
57
|
+
return chosen === undefined ? undefined : this.runAction(chosen);
|
|
58
|
+
});
|
|
59
|
+
async runAction(chosen) {
|
|
60
|
+
this.close();
|
|
61
|
+
await this.stores.actions.run(chosen);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
declare const SignIn_base: import("../model.ts").ModelClass<import("../registered.ts").AppStores, import("../registered.ts").ModelServices, {
|
|
3
|
+
step: z.ZodDefault<z.ZodEnum<{
|
|
4
|
+
code: "code";
|
|
5
|
+
choose: "choose";
|
|
6
|
+
}>>;
|
|
7
|
+
email: z.ZodDefault<z.ZodString>;
|
|
8
|
+
signingIn: z.ZodDefault<z.ZodNullable<z.ZodEnum<{
|
|
9
|
+
email: "email";
|
|
10
|
+
google: "google";
|
|
11
|
+
dev: "dev";
|
|
12
|
+
}>>>;
|
|
13
|
+
notice: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
14
|
+
}, []>;
|
|
15
|
+
export declare class SignIn extends SignIn_base {
|
|
16
|
+
continueWithGoogle: import("../model.ts").Command<{}, void>;
|
|
17
|
+
sendEmailCode: import("../model.ts").Command<{
|
|
18
|
+
email: z.ZodEmail;
|
|
19
|
+
}, Promise<void>>;
|
|
20
|
+
verifyEmailCode: import("../model.ts").Command<{
|
|
21
|
+
code: z.ZodString;
|
|
22
|
+
}, Promise<void>>;
|
|
23
|
+
startOver: import("../model.ts").Command<{}, void>;
|
|
24
|
+
signInAsDevUser: import("../model.ts").Command<{
|
|
25
|
+
email: z.ZodEmail;
|
|
26
|
+
}, Promise<void>>;
|
|
27
|
+
}
|
|
28
|
+
export {};
|