@trypattern/sdk 0.1.7 → 0.1.9
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/dist/auth/user.d.ts +2 -1
- package/dist/auth/user.js +2 -1
- package/dist/cli/app.js +2 -2
- package/dist/cli/publishing.js +3 -3
- package/dist/components/AppFrame.d.ts +2 -1
- package/dist/components/AppFrame.js +2 -2
- package/dist/components/Appear.d.ts +4 -1
- package/dist/components/Appear.js +82 -3
- package/dist/components/Link.js +8 -3
- package/dist/components/ListRow.d.ts +2 -1
- package/dist/components/ListRow.js +7 -4
- package/dist/components/Sidebar.js +2 -1
- package/dist/patternAccess.d.ts +21 -0
- package/dist/patternAccess.js +110 -0
- package/dist/publishWire.d.ts +316 -0
- package/dist/publishWire.js +107 -0
- package/dist/server/auth.js +3 -3
- package/dist/server/createServer.d.ts +87 -3
- package/dist/server/createServer.js +6 -0
- package/dist/server/database.d.ts +74 -0
- package/dist/server/database.js +93 -0
- package/dist/server/keyset.d.ts +13 -0
- package/dist/server/keyset.js +52 -0
- package/dist/server/patternKeys.d.ts +1 -0
- package/dist/server/patternKeys.js +3 -0
- package/dist/server/patternViewer.d.ts +6 -0
- package/dist/server/patternViewer.js +27 -0
- package/dist/server/services.js +1 -1
- package/dist/server/session.js +3 -4
- package/dist/server/tables.d.ts +16 -3
- package/dist/server/tables.js +15 -6
- package/dist/serviceClients.js +3 -5
- package/package.json +1 -1
package/dist/auth/user.d.ts
CHANGED
|
@@ -8,4 +8,5 @@ export declare const userSchema: z.ZodObject<{
|
|
|
8
8
|
}, z.core.$strip>;
|
|
9
9
|
export type User = z.output<typeof userSchema>;
|
|
10
10
|
export type StoredUser = Omit<User, 'isAdmin'>;
|
|
11
|
-
export declare const USER_COOKIE = "pattern_user";
|
|
11
|
+
export declare const USER_COOKIE = "__Host-pattern_user";
|
|
12
|
+
export declare const SESSION_COOKIE = "__Host-pattern_session";
|
package/dist/auth/user.js
CHANGED
|
@@ -6,4 +6,5 @@ export const userSchema = z.object({
|
|
|
6
6
|
avatarUrl: z.string().nullable(),
|
|
7
7
|
isAdmin: z.boolean().default(false)
|
|
8
8
|
});
|
|
9
|
-
export const USER_COOKIE = 'pattern_user';
|
|
9
|
+
export const USER_COOKIE = '__Host-pattern_user';
|
|
10
|
+
export const SESSION_COOKIE = '__Host-pattern_session';
|
package/dist/cli/app.js
CHANGED
|
@@ -270,7 +270,7 @@ and restore sign in the same way when they need to, so this is rarely run on its
|
|
|
270
270
|
},
|
|
271
271
|
pull: {
|
|
272
272
|
summary: 'Download a site from onpattern.app and install it, ready for app start',
|
|
273
|
-
details: `app pull
|
|
273
|
+
details: `app pull acmeflower
|
|
274
274
|
Downloads the source of the site's latest live version into this folder when it is empty, or into
|
|
275
275
|
./<name> otherwise, writes pattern.json, installs, and runs app setup. Never needs git; when the
|
|
276
276
|
Command Line Tools are installed it also makes a first commit. Signs in first if needed (see app login).
|
|
@@ -1283,7 +1283,7 @@ async function setup() {
|
|
|
1283
1283
|
problems.push(`Node ${haveNode} is older than Node ${majorOf(pins.node)} (.nvmrc says ${pins.node})\n fix pnpm env use --global ${majorOf(pins.node)}`);
|
|
1284
1284
|
}
|
|
1285
1285
|
if (!isAtLeastMajor(havePnpm, pins.pnpm)) {
|
|
1286
|
-
problems.push(`pnpm ${havePnpm || '(not found)'} is older than pnpm ${majorOf(pins.pnpm)} (package.json devEngines says ${pins.pnpm} or newer)\n fix npm install --global pnpm@
|
|
1286
|
+
problems.push(`pnpm ${havePnpm || '(not found)'} is older than pnpm ${majorOf(pins.pnpm)} (package.json devEngines says ${pins.pnpm} or newer)\n fix npm install --global pnpm@12`);
|
|
1287
1287
|
}
|
|
1288
1288
|
const versions = { node: haveNode, pnpm: havePnpm, expected: pins };
|
|
1289
1289
|
if (problems.length > 0) {
|
package/dist/cli/publishing.js
CHANGED
|
@@ -5,6 +5,7 @@ import { basename, dirname, extname, join, matchesGlob, relative } from 'node:pa
|
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import { DASHBOARD_URL, UPLOAD, appSummary, isPrivatePath, loginPoll, loginStarted, nameProblem, publishMessage, publishSummary, setupCodeExchanged, sourcePathProblem } from "../publishWire.js";
|
|
7
7
|
import { appInfo } from "../appTitle.js";
|
|
8
|
+
import { SESSION_COOKIE } from "../auth/user.js";
|
|
8
9
|
import { PROJECT_FILE, readProjectFile } from "../vite/projectFile.js";
|
|
9
10
|
const CONFIG_DIR = join(homedir(), '.config', 'pattern');
|
|
10
11
|
const CREDENTIALS_FILE = join(CONFIG_DIR, 'credentials.json');
|
|
@@ -13,7 +14,6 @@ const LOGIN_MINUTES = 10;
|
|
|
13
14
|
const POLL_MS = 2000;
|
|
14
15
|
const LOGIN_WAIT_MS = 100_000;
|
|
15
16
|
const PUBLISH_WAIT_MS = 300_000;
|
|
16
|
-
const SESSION_COOKIE = 'pattern_session';
|
|
17
17
|
const LAUNCHER = 'app';
|
|
18
18
|
const STARTER_WORKSPACE = 'pnpm-workspace.yaml';
|
|
19
19
|
const STARTER_BUILD_SETTINGS = 'allowBuilds:\n esbuild: false\n workerd: false\n';
|
|
@@ -258,7 +258,7 @@ async function ensurePnpm(io, cwd) {
|
|
|
258
258
|
if (await hasPnpm(io, cwd))
|
|
259
259
|
return true;
|
|
260
260
|
io.say('Installing pnpm');
|
|
261
|
-
const installed = await io.run('npm', ['install', '--global', 'pnpm@
|
|
261
|
+
const installed = await io.run('npm', ['install', '--global', 'pnpm@12'], cwd);
|
|
262
262
|
return installed.code === 0 && (await hasPnpm(io, cwd));
|
|
263
263
|
}
|
|
264
264
|
function firstChange(title, url, folder) {
|
|
@@ -277,7 +277,7 @@ export async function pull(io, name, cwd, setupCode) {
|
|
|
277
277
|
return {
|
|
278
278
|
code: 2,
|
|
279
279
|
data: { error: 'missing name' },
|
|
280
|
-
text: '✗ Which site? app pull
|
|
280
|
+
text: '✗ Which site? app pull acmeflower'
|
|
281
281
|
};
|
|
282
282
|
}
|
|
283
283
|
const target = folderFor(name, cwd);
|
|
@@ -2,6 +2,7 @@ import type { ReactNode } from 'react';
|
|
|
2
2
|
import type { MenuEntry } from './MenuItems.tsx';
|
|
3
3
|
import type { SidebarLink } from './Sidebar.tsx';
|
|
4
4
|
type AppFrameProps = {
|
|
5
|
+
workspace?: string;
|
|
5
6
|
links: readonly SidebarLink[];
|
|
6
7
|
workspaceMenu?: readonly MenuEntry[];
|
|
7
8
|
isSidebarToggled: boolean;
|
|
@@ -10,5 +11,5 @@ type AppFrameProps = {
|
|
|
10
11
|
onSearch?: () => void;
|
|
11
12
|
children: ReactNode;
|
|
12
13
|
};
|
|
13
|
-
export declare function AppFrame({ links, workspaceMenu, isSidebarToggled, onToggleSidebar, onCloseSidebar, onSearch, children }: AppFrameProps): import("react").JSX.Element;
|
|
14
|
+
export declare function AppFrame({ workspace, links, workspaceMenu, isSidebarToggled, onToggleSidebar, onCloseSidebar, onSearch, children }: AppFrameProps): import("react").JSX.Element;
|
|
14
15
|
export {};
|
|
@@ -6,7 +6,7 @@ import { FrameContext } from "./frame.js";
|
|
|
6
6
|
import { Scrim } from "./Scrim.js";
|
|
7
7
|
import { Sidebar } from "./Sidebar.js";
|
|
8
8
|
import { useAnimatesOnChange } from "../hooks/useAnimatesOnChange.js";
|
|
9
|
-
export function AppFrame({ links, workspaceMenu = [], isSidebarToggled, onToggleSidebar, onCloseSidebar, onSearch, children }) {
|
|
9
|
+
export function AppFrame({ workspace, links, workspaceMenu = [], isSidebarToggled, onToggleSidebar, onCloseSidebar, onSearch, children }) {
|
|
10
10
|
const appSettings = useModel('appSettings');
|
|
11
11
|
const account = useModel('account');
|
|
12
12
|
const menu = account.user === null
|
|
@@ -17,7 +17,7 @@ export function AppFrame({ links, workspaceMenu = [], isSidebarToggled, onToggle
|
|
|
17
17
|
{ id: 'signOut', label: 'Log out', onSelect: account.signOut }
|
|
18
18
|
];
|
|
19
19
|
const isSidebarAnimating = useAnimatesOnChange(isSidebarToggled, SIDEBAR_ANIMATION_MS);
|
|
20
|
-
return (_jsx(FrameContext, { value: { onToggleSidebar }, children: _jsxs("div", { ...stylex.props(styles.frame), children: [_jsx(Sidebar, { workspace: appSettings.title, workspaceMenu: menu, links: links, isToggled: isSidebarToggled, isAnimating: isSidebarAnimating, onFollowLink: onCloseSidebar, onSearch: onSearch === undefined
|
|
20
|
+
return (_jsx(FrameContext, { value: { onToggleSidebar }, children: _jsxs("div", { ...stylex.props(styles.frame), children: [_jsx(Sidebar, { workspace: workspace ?? appSettings.title, workspaceMenu: menu, links: links, isToggled: isSidebarToggled, isAnimating: isSidebarAnimating, onFollowLink: onCloseSidebar, onSearch: onSearch === undefined
|
|
21
21
|
? undefined
|
|
22
22
|
: () => {
|
|
23
23
|
onCloseSidebar();
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
+
import type { SpaceName } from './spacing.ts';
|
|
2
3
|
type AppearProps = {
|
|
4
|
+
isVisible?: boolean;
|
|
5
|
+
spaceBefore?: SpaceName;
|
|
3
6
|
children: ReactNode;
|
|
4
7
|
};
|
|
5
|
-
export declare function Appear({ children }: AppearProps): import("react").JSX.Element;
|
|
8
|
+
export declare function Appear({ isVisible, spaceBefore, children }: AppearProps): import("react").JSX.Element;
|
|
6
9
|
export {};
|
|
@@ -1,21 +1,100 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import * as stylex from '@stylexjs/stylex';
|
|
3
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
3
4
|
import { breakpoints, motion, space } from "../theme/tokens.stylex.js";
|
|
4
|
-
export function Appear({ children }) {
|
|
5
|
-
|
|
5
|
+
export function Appear({ isVisible = true, spaceBefore = 'none', children }) {
|
|
6
|
+
const inner = useRef(null);
|
|
7
|
+
const measured = useRef(0);
|
|
8
|
+
const lastShown = useRef(children);
|
|
9
|
+
const [height, setHeight] = useState(0);
|
|
10
|
+
const [isMoving, setIsMoving] = useState(true);
|
|
11
|
+
const [isGone, setIsGone] = useState(!isVisible);
|
|
12
|
+
if (isVisible)
|
|
13
|
+
lastShown.current = children;
|
|
14
|
+
if (isVisible && isGone)
|
|
15
|
+
setIsGone(false);
|
|
16
|
+
useLayoutEffect(() => {
|
|
17
|
+
const node = inner.current;
|
|
18
|
+
if (node === null)
|
|
19
|
+
return undefined;
|
|
20
|
+
const observer = new ResizeObserver(() => {
|
|
21
|
+
const next = node.offsetHeight;
|
|
22
|
+
if (next === measured.current)
|
|
23
|
+
return;
|
|
24
|
+
measured.current = next;
|
|
25
|
+
setIsMoving(!prefersStill());
|
|
26
|
+
setHeight(next);
|
|
27
|
+
});
|
|
28
|
+
observer.observe(node);
|
|
29
|
+
return () => observer.disconnect();
|
|
30
|
+
}, []);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (isVisible)
|
|
33
|
+
return;
|
|
34
|
+
if (prefersStill()) {
|
|
35
|
+
setIsGone(true);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
setIsMoving(true);
|
|
39
|
+
}, [isVisible]);
|
|
40
|
+
function settle() {
|
|
41
|
+
setIsMoving(false);
|
|
42
|
+
if (!isVisible)
|
|
43
|
+
setIsGone(true);
|
|
44
|
+
}
|
|
45
|
+
return (_jsx("div", { ...stylex.props(styles.frame, styles.height(isVisible ? height : 0), (isMoving || !isVisible) && styles.clipped), onTransitionEnd: (event) => {
|
|
46
|
+
if (event.target === event.currentTarget)
|
|
47
|
+
settle();
|
|
48
|
+
}, onTransitionCancel: (event) => {
|
|
49
|
+
if (event.target === event.currentTarget)
|
|
50
|
+
settle();
|
|
51
|
+
}, children: _jsx("div", { ref: inner, ...stylex.props(styles.appear, spacesBefore[spaceBefore], !isVisible && styles.hidden), children: isGone ? null : lastShown.current }) }));
|
|
52
|
+
}
|
|
53
|
+
function prefersStill() {
|
|
54
|
+
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
6
55
|
}
|
|
7
56
|
const enter = stylex.keyframes({
|
|
8
57
|
from: { opacity: 0, transform: `translateY(${space.xs})` }
|
|
9
58
|
});
|
|
10
59
|
const styles = stylex.create({
|
|
60
|
+
frame: {
|
|
61
|
+
flexShrink: 0,
|
|
62
|
+
transitionTimingFunction: motion.slideEasing,
|
|
63
|
+
transitionDuration: motion.slide,
|
|
64
|
+
transitionProperty: 'height'
|
|
65
|
+
},
|
|
66
|
+
height: (height) => ({
|
|
67
|
+
height: `${height}px`
|
|
68
|
+
}),
|
|
69
|
+
clipped: {
|
|
70
|
+
overflow: 'hidden'
|
|
71
|
+
},
|
|
11
72
|
appear: {
|
|
12
73
|
display: 'flex',
|
|
13
74
|
flexDirection: 'column',
|
|
75
|
+
opacity: 1,
|
|
76
|
+
transitionTimingFunction: motion.easeOut,
|
|
77
|
+
transitionDuration: motion.fast,
|
|
78
|
+
transitionProperty: 'opacity',
|
|
14
79
|
animationName: {
|
|
15
80
|
default: enter,
|
|
16
81
|
[breakpoints.reducedMotion]: 'none'
|
|
17
82
|
},
|
|
18
|
-
animationDuration: motion.
|
|
83
|
+
animationDuration: motion.slide,
|
|
19
84
|
animationTimingFunction: motion.easeOut
|
|
85
|
+
},
|
|
86
|
+
hidden: {
|
|
87
|
+
opacity: 0
|
|
20
88
|
}
|
|
21
89
|
});
|
|
90
|
+
const spacesBefore = stylex.create({
|
|
91
|
+
none: { paddingTop: 0 },
|
|
92
|
+
xxs: { paddingTop: space.xxs },
|
|
93
|
+
xs: { paddingTop: space.xs },
|
|
94
|
+
sm: { paddingTop: space.sm },
|
|
95
|
+
md: { paddingTop: space.md },
|
|
96
|
+
lg: { paddingTop: space.lg },
|
|
97
|
+
xl: { paddingTop: space.xl },
|
|
98
|
+
xxl: { paddingTop: space.xxl },
|
|
99
|
+
xxxl: { paddingTop: space.xxxl }
|
|
100
|
+
});
|
package/dist/components/Link.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import * as stylex from '@stylexjs/stylex';
|
|
3
3
|
import { Link as RouterLink } from 'react-router';
|
|
4
|
-
import { colors, motion, radii } from "../theme/tokens.stylex.js";
|
|
4
|
+
import { borders, colors, motion, radii } from "../theme/tokens.stylex.js";
|
|
5
5
|
import { focusRing } from "./controlLook.js";
|
|
6
6
|
import { interact, textOf } from "./interaction.js";
|
|
7
7
|
export function Link({ to, href, tone = 'brand', children }) {
|
|
8
|
-
const className = stylex.props(styles.link, looks[tone], focusRing.visible).className;
|
|
8
|
+
const className = stylex.props(styles.link, looks[tone], href !== undefined && styles.leaves, focusRing.visible).className;
|
|
9
9
|
if (href !== undefined) {
|
|
10
10
|
return (_jsx("a", { href: href, target: "_blank", rel: "noreferrer", onClick: () => interact({
|
|
11
11
|
primitive: 'Link',
|
|
@@ -21,11 +21,16 @@ export function Link({ to, href, tone = 'brand', children }) {
|
|
|
21
21
|
}
|
|
22
22
|
const styles = stylex.create({
|
|
23
23
|
link: {
|
|
24
|
-
|
|
24
|
+
textDecorationLine: 'none',
|
|
25
25
|
borderRadius: radii.xs,
|
|
26
26
|
transitionTimingFunction: motion.easeOut,
|
|
27
27
|
transitionDuration: motion.fast,
|
|
28
28
|
transitionProperty: 'color'
|
|
29
|
+
},
|
|
30
|
+
leaves: {
|
|
31
|
+
textDecorationLine: 'underline',
|
|
32
|
+
textDecorationThickness: borders.thin,
|
|
33
|
+
textUnderlineOffset: '0.25em'
|
|
29
34
|
}
|
|
30
35
|
});
|
|
31
36
|
const looks = stylex.create({
|
|
@@ -4,9 +4,10 @@ type ListRowProps = {
|
|
|
4
4
|
to?: AppPath;
|
|
5
5
|
onClick?: () => void;
|
|
6
6
|
isMuted?: boolean;
|
|
7
|
+
isCurrent?: boolean;
|
|
7
8
|
leading?: ReactNode;
|
|
8
9
|
trailing?: ReactNode;
|
|
9
10
|
children: ReactNode;
|
|
10
11
|
};
|
|
11
|
-
export declare function ListRow({ to, onClick, isMuted, leading, trailing, children }: ListRowProps): import("react").JSX.Element;
|
|
12
|
+
export declare function ListRow({ to, onClick, isMuted, isCurrent, leading, trailing, children }: ListRowProps): import("react").JSX.Element;
|
|
12
13
|
export {};
|
|
@@ -5,9 +5,9 @@ import { Link as RouterLink } from 'react-router';
|
|
|
5
5
|
import { colors, controls, radii, space } from "../theme/tokens.stylex.js";
|
|
6
6
|
import { focusRing } from "./controlLook.js";
|
|
7
7
|
import { interact, textOf } from "./interaction.js";
|
|
8
|
-
export function ListRow({ to, onClick, isMuted = false, leading, trailing, children }) {
|
|
8
|
+
export function ListRow({ to, onClick, isMuted = false, isCurrent = false, leading, trailing, children }) {
|
|
9
9
|
const content = (_jsxs(_Fragment, { children: [leading, _jsx("span", { ...stylex.props(styles.main), children: children }), trailing === undefined ? null : (_jsx("span", { ...stylex.props(styles.trailing), children: trailing }))] }));
|
|
10
|
-
const look = stylex.props(styles.row, (to !== undefined || onClick !== undefined) && styles.interactive, isMuted && styles.muted, focusRing.visible, focusRing.inside);
|
|
10
|
+
const look = stylex.props(styles.row, (to !== undefined || onClick !== undefined) && styles.interactive, isMuted && styles.muted, isCurrent && styles.current, focusRing.visible, focusRing.inside);
|
|
11
11
|
const report = () => {
|
|
12
12
|
interact({
|
|
13
13
|
primitive: 'ListRow',
|
|
@@ -17,10 +17,10 @@ export function ListRow({ to, onClick, isMuted = false, leading, trailing, child
|
|
|
17
17
|
onClick?.();
|
|
18
18
|
};
|
|
19
19
|
if (to !== undefined) {
|
|
20
|
-
return (_jsx(RouterLink, { to: to, onClick: report, ...look, children: content }));
|
|
20
|
+
return (_jsx(RouterLink, { to: to, onClick: report, "aria-current": isCurrent ? 'page' : undefined, ...look, children: content }));
|
|
21
21
|
}
|
|
22
22
|
if (onClick !== undefined) {
|
|
23
|
-
return (_jsx(BaseButton, { onClick: report, className: look.className, children: content }));
|
|
23
|
+
return (_jsx(BaseButton, { onClick: report, "aria-pressed": isCurrent, className: look.className, children: content }));
|
|
24
24
|
}
|
|
25
25
|
return _jsx("div", { ...look, children: content });
|
|
26
26
|
}
|
|
@@ -46,6 +46,9 @@ const styles = stylex.create({
|
|
|
46
46
|
muted: {
|
|
47
47
|
color: colors.tertiaryForeground
|
|
48
48
|
},
|
|
49
|
+
current: {
|
|
50
|
+
backgroundColor: colors.transparentHover
|
|
51
|
+
},
|
|
49
52
|
main: {
|
|
50
53
|
display: 'flex',
|
|
51
54
|
flexGrow: 1,
|
|
@@ -15,7 +15,8 @@ export function Sidebar({ workspace, workspaceMenu, links, isToggled, isAnimatin
|
|
|
15
15
|
interact({ primitive: 'AppFrame', event: 'navigate', label: link.label });
|
|
16
16
|
onFollowLink();
|
|
17
17
|
};
|
|
18
|
-
return (_jsxs("nav", { ...stylex.props(styles.sidebar, isToggled && styles.sidebarToggled, isAnimating && styles.sidebarAnimating), children: [_jsx("div", { ...stylex.props(styles.header), children: _jsx(Menu, { trigger: _jsxs(_Fragment, { children: [_jsx("span", { ...stylex.props(styles.logo), children: _jsx(Text, { variant: "caption", children: workspace.charAt(0) }) }), _jsx(Text, { variant: "label", isTruncated: true, children: workspace }), _jsx(Icon, { name: "chevronDown" })] }), items: workspaceMenu }) }), onSearch === undefined ? null : (_jsx(Button, { variant: "secondary", icon: "search", shortcut: ['mod', 'k'], isFullWidth: true, onClick: onSearch, children: "Search" })), _jsx("div", { ...stylex.props(styles.body), children: _jsx(ScrollArea, { children: _jsxs("div", { ...stylex.props(styles.group), children: [_jsx("span", { ...stylex.props(styles.groupLabel), children: _jsx(Text, { variant: "label", tone: "tertiary", children: "Workspace" }) }), links.map((item) => (_jsxs(NavLink, { onClick: () => followLink(item), to: item.to, end: item.to === '/'
|
|
18
|
+
return (_jsxs("nav", { ...stylex.props(styles.sidebar, isToggled && styles.sidebarToggled, isAnimating && styles.sidebarAnimating), children: [_jsx("div", { ...stylex.props(styles.header), children: _jsx(Menu, { trigger: _jsxs(_Fragment, { children: [_jsx("span", { ...stylex.props(styles.logo), children: _jsx(Text, { variant: "caption", children: workspace.charAt(0) }) }), _jsx(Text, { variant: "label", isTruncated: true, children: workspace }), _jsx(Icon, { name: "chevronDown" })] }), items: workspaceMenu }) }), onSearch === undefined ? null : (_jsx(Button, { variant: "secondary", icon: "search", shortcut: ['mod', 'k'], isFullWidth: true, onClick: onSearch, children: "Search" })), _jsx("div", { ...stylex.props(styles.body), children: _jsx(ScrollArea, { children: _jsxs("div", { ...stylex.props(styles.group), children: [_jsx("span", { ...stylex.props(styles.groupLabel), children: _jsx(Text, { variant: "label", tone: "tertiary", children: "Workspace" }) }), links.map((item) => (_jsxs(NavLink, { onClick: () => followLink(item), to: item.to, end: item.to === '/' ||
|
|
19
|
+
links.some((other) => String(other.to).startsWith(`${String(item.to)}/`)), className: ({ isActive }) => stylex.props(styles.item, isActive && styles.itemCurrent, focusRing.visible, focusRing.inside).className ?? '', children: [_jsx(Icon, { name: item.icon }), _jsx(Text, { variant: "label", isTruncated: true, style: styles.itemLabel, children: item.label }), typeof item.count !== 'number' ? null : (_jsx(Badge, { variant: "transparent", children: item.count }))] }, item.to)))] }) }) })] }));
|
|
19
20
|
}
|
|
20
21
|
const styles = stylex.create({
|
|
21
22
|
sidebar: {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const TABLES_READ = "tables:read";
|
|
3
|
+
export declare const ASSERTION_SECONDS = 60;
|
|
4
|
+
export declare const NEXT_PAGE_HEADER = "x-pattern-next";
|
|
5
|
+
export declare const assertionKey: z.ZodObject<{
|
|
6
|
+
kid: z.ZodString;
|
|
7
|
+
kty: z.ZodLiteral<"OKP">;
|
|
8
|
+
crv: z.ZodLiteral<"Ed25519">;
|
|
9
|
+
x: z.ZodString;
|
|
10
|
+
d: z.ZodString;
|
|
11
|
+
}, z.core.$strip>;
|
|
12
|
+
export type AssertionKey = z.output<typeof assertionKey>;
|
|
13
|
+
export declare function signTablesAssertion(key: AssertionKey, { sub, aud }: {
|
|
14
|
+
sub: string;
|
|
15
|
+
aud: string;
|
|
16
|
+
}, now: number): Promise<string>;
|
|
17
|
+
export declare function verifiedTablesAssertion(token: string, { hostname, keys, now }: {
|
|
18
|
+
hostname: string;
|
|
19
|
+
keys: Readonly<Record<string, string>>;
|
|
20
|
+
now: number;
|
|
21
|
+
}): Promise<string | undefined>;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const TABLES_READ = 'tables:read';
|
|
3
|
+
export const ASSERTION_SECONDS = 60;
|
|
4
|
+
export const NEXT_PAGE_HEADER = 'x-pattern-next';
|
|
5
|
+
const CLOCK_SKEW_SECONDS = 30;
|
|
6
|
+
const ED25519 = { name: 'Ed25519' };
|
|
7
|
+
export const assertionKey = z.object({
|
|
8
|
+
kid: z.string().min(1),
|
|
9
|
+
kty: z.literal('OKP'),
|
|
10
|
+
crv: z.literal('Ed25519'),
|
|
11
|
+
x: z.string(),
|
|
12
|
+
d: z.string()
|
|
13
|
+
});
|
|
14
|
+
const assertionHeader = z.object({
|
|
15
|
+
alg: z.literal('EdDSA'),
|
|
16
|
+
typ: z.literal('JWT'),
|
|
17
|
+
kid: z.string()
|
|
18
|
+
});
|
|
19
|
+
const assertionClaims = z.object({
|
|
20
|
+
sub: z.email(),
|
|
21
|
+
aud: z.string(),
|
|
22
|
+
scope: z.literal(TABLES_READ),
|
|
23
|
+
iat: z.int(),
|
|
24
|
+
exp: z.int()
|
|
25
|
+
});
|
|
26
|
+
const encoder = new TextEncoder();
|
|
27
|
+
function base64url(bytes) {
|
|
28
|
+
return btoa(String.fromCharCode(...bytes))
|
|
29
|
+
.replaceAll('+', '-')
|
|
30
|
+
.replaceAll('/', '_')
|
|
31
|
+
.replaceAll('=', '');
|
|
32
|
+
}
|
|
33
|
+
function bytesOf(text) {
|
|
34
|
+
try {
|
|
35
|
+
const binary = atob(text.replaceAll('-', '+').replaceAll('_', '/'));
|
|
36
|
+
return Uint8Array.from(binary, (letter) => letter.charCodeAt(0));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function jsonOf(text) {
|
|
43
|
+
const bytes = bytesOf(text);
|
|
44
|
+
if (bytes === undefined)
|
|
45
|
+
return undefined;
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function encoded(value) {
|
|
54
|
+
return base64url(encoder.encode(JSON.stringify(value)));
|
|
55
|
+
}
|
|
56
|
+
export async function signTablesAssertion(key, { sub, aud }, now) {
|
|
57
|
+
const privateKey = await crypto.subtle.importKey('jwk', { kty: key.kty, crv: key.crv, x: key.x, d: key.d }, ED25519, false, ['sign']);
|
|
58
|
+
const signed = `${encoded({ alg: 'EdDSA', typ: 'JWT', kid: key.kid })}.${encoded({
|
|
59
|
+
sub,
|
|
60
|
+
aud,
|
|
61
|
+
scope: TABLES_READ,
|
|
62
|
+
iat: now,
|
|
63
|
+
exp: now + ASSERTION_SECONDS
|
|
64
|
+
})}`;
|
|
65
|
+
const signature = await crypto.subtle.sign(ED25519, privateKey, encoder.encode(signed));
|
|
66
|
+
return `${signed}.${base64url(new Uint8Array(signature))}`;
|
|
67
|
+
}
|
|
68
|
+
async function isSignedBy(publicKey, signed, signature) {
|
|
69
|
+
const keyBytes = bytesOf(publicKey);
|
|
70
|
+
const signatureBytes = bytesOf(signature);
|
|
71
|
+
if (keyBytes === undefined || signatureBytes === undefined)
|
|
72
|
+
return false;
|
|
73
|
+
try {
|
|
74
|
+
const key = await crypto.subtle.importKey('raw', keyBytes, ED25519, false, [
|
|
75
|
+
'verify'
|
|
76
|
+
]);
|
|
77
|
+
return await crypto.subtle.verify(ED25519, key, signatureBytes, encoder.encode(signed));
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export async function verifiedTablesAssertion(token, { hostname, keys, now }) {
|
|
84
|
+
const [header, claims, signature, ...rest] = token.split('.');
|
|
85
|
+
if (header === undefined ||
|
|
86
|
+
claims === undefined ||
|
|
87
|
+
signature === undefined ||
|
|
88
|
+
rest.length > 0) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
const parsedHeader = assertionHeader.safeParse(jsonOf(header));
|
|
92
|
+
if (!parsedHeader.success)
|
|
93
|
+
return undefined;
|
|
94
|
+
const { kid } = parsedHeader.data;
|
|
95
|
+
const publicKey = Object.hasOwn(keys, kid) ? keys[kid] : undefined;
|
|
96
|
+
if (publicKey === undefined)
|
|
97
|
+
return undefined;
|
|
98
|
+
if (!(await isSignedBy(publicKey, `${header}.${claims}`, signature))) {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
const parsed = assertionClaims.safeParse(jsonOf(claims));
|
|
102
|
+
if (!parsed.success)
|
|
103
|
+
return undefined;
|
|
104
|
+
const { sub, aud, iat, exp } = parsed.data;
|
|
105
|
+
const isFresh = exp > iat &&
|
|
106
|
+
exp - iat <= ASSERTION_SECONDS &&
|
|
107
|
+
iat <= now + CLOCK_SKEW_SECONDS &&
|
|
108
|
+
now < exp;
|
|
109
|
+
return aud === hostname && isFresh ? sub.toLowerCase() : undefined;
|
|
110
|
+
}
|