@trypattern/sdk 0.1.3 → 0.1.5

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.
@@ -2,7 +2,7 @@ import { appearance } from "../appearance.js";
2
2
  import { command } from "../model.js";
3
3
  import { Model } from "../registered.js";
4
4
  export class Account extends Model({
5
- state: { appearance: appearance.default('system') },
5
+ state: { appearance: appearance.default('light') },
6
6
  persist: ['appearance']
7
7
  }) {
8
8
  get user() {
package/dist/cli/app.js CHANGED
@@ -13,6 +13,7 @@ import { homedir } from 'node:os';
13
13
  import { delimiter, dirname, extname, join, relative, resolve } from 'node:path';
14
14
  import { pathToFileURL } from 'node:url';
15
15
  import { createHash } from 'node:crypto';
16
+ import { z } from 'zod';
16
17
  import { resetLocalDatabase } from "../vite/localWorker.js";
17
18
  import { login, publish, pull, restore, versions as listVersions } from "./publishing.js";
18
19
  import { toolBin } from "./tools.js";
@@ -73,7 +74,7 @@ const CONTACT_SHEET = join(SCREENSHOTS_DIR, 'contact-sheet.png');
73
74
  const SCREENSHOT_WORKER = join(SDK_SOURCE, 'cli', `screenshot${SDK_EXTENSION}`);
74
75
  const VISUAL_URL_ENV = 'PATTERN_VISUAL_URL';
75
76
  const GLOBAL_FLAGS = ['--json', '--help', '-h'];
76
- const VALUE_FLAGS = new Set(['--commands', '--name', '--as']);
77
+ const VALUE_FLAGS = new Set(['--commands', '--name', '--as', '--code']);
77
78
  const SHIM_SOURCE = `#!/bin/sh
78
79
  ${SHIM_MARKER_LINE}
79
80
  dir=$PWD
@@ -266,8 +267,13 @@ and restore sign in the same way when they need to, so this is rarely run on its
266
267
  details: `app pull sundayparty
267
268
  Downloads the source of the site's latest live version into this folder when it is empty, or into
268
269
  ./<name> otherwise, writes pattern.json, installs, and runs app setup. Never needs git; when the
269
- Command Line Tools are installed it also makes a first commit. Signs in first if needed (see app login).`,
270
- run: (argument) => pull(publishingIo(), argument, process.cwd()),
270
+ Command Line Tools are installed it also makes a first commit. Signs in first if needed (see app login).
271
+ --code <setup code> signs in with the one-time setup code from onpattern.app instead of the browser.`,
272
+ run: (argument, options) => {
273
+ const code = options.get('--code');
274
+ return pull(publishingIo(), argument, process.cwd(), typeof code === 'string' ? code : undefined);
275
+ },
276
+ flags: ['--code'],
271
277
  takesArgument: true,
272
278
  runsAnywhere: true
273
279
  },
@@ -297,7 +303,7 @@ every row and column added since, which version 3's code ignores.`,
297
303
  },
298
304
  setup: {
299
305
  summary: 'Verify Node/pnpm major versions, pnpm install, install the global `app` shim',
300
- details: `Idempotent. Fails unless Node and pnpm have the major versions in .nvmrc and package.json "packageManager".
306
+ details: `Idempotent. Fails unless Node and pnpm have the major versions in .nvmrc and package.json devEngines.
301
307
  Then runs \`pnpm install\` and installs ${SHIM_PATH}, a tiny shim that walks up from the
302
308
  current directory to the nearest project launcher (an executable ./app containing the line
303
309
  ${LAUNCHER_MARKER_LINE}) and runs it, so each project's CLI lives in its repo and the shim
@@ -1217,13 +1223,16 @@ async function build() {
1217
1223
  function majorOf(version) {
1218
1224
  return version.split('.')[0] ?? '';
1219
1225
  }
1226
+ const packagePins = z.object({
1227
+ devEngines: z.object({
1228
+ packageManager: z.object({ version: z.string() })
1229
+ })
1230
+ });
1220
1231
  function readPins() {
1221
1232
  const node = readText(join(ROOT, '.nvmrc')).trim().replace(/^v/, '');
1222
- const pkg = JSON.parse(readText(join(ROOT, 'package.json')));
1223
- const pm = typeof pkg === 'object' && pkg !== null && 'packageManager' in pkg
1224
- ? String(pkg.packageManager)
1225
- : '';
1226
- return { node, pnpm: pm.replace(/^pnpm@/, '').split('+')[0] ?? '' };
1233
+ const pkg = packagePins.safeParse(JSON.parse(readText(join(ROOT, 'package.json'))));
1234
+ const pnpm = pkg.success ? pkg.data.devEngines.packageManager.version : '';
1235
+ return { node, pnpm: pnpm.replace(/^>=/, '') };
1227
1236
  }
1228
1237
  function isPatternShim(source) {
1229
1238
  const lines = source.split('\n');
@@ -1260,7 +1269,7 @@ async function setup() {
1260
1269
  problems.push(`Node ${haveNode} is not Node ${majorOf(pins.node)} (.nvmrc says ${pins.node})\n fix pnpm env use --global ${majorOf(pins.node)}`);
1261
1270
  }
1262
1271
  if (majorOf(havePnpm) !== majorOf(pins.pnpm)) {
1263
- problems.push(`pnpm ${havePnpm || '(not found)'} is not pnpm ${majorOf(pins.pnpm)} (package.json packageManager says ${pins.pnpm})\n fix curl -fsSL https://get.pnpm.io/install.sh | sh -`);
1272
+ problems.push(`pnpm ${havePnpm || '(not found)'} is not pnpm ${majorOf(pins.pnpm)} (package.json devEngines says ${pins.pnpm} or newer)\n fix curl -fsSL https://get.pnpm.io/install.sh | sh -`);
1264
1273
  }
1265
1274
  const versions = { node: haveNode, pnpm: havePnpm, expected: pins };
1266
1275
  if (problems.length > 0) {
@@ -1302,7 +1311,7 @@ async function setup() {
1302
1311
  function help() {
1303
1312
  const width = Math.max(...Object.keys(COMMANDS).map((n) => n.length));
1304
1313
  const lines = Object.entries(COMMANDS).map(([name, cmd]) => `app ${name.padEnd(width)} ${cmd.summary}`);
1305
- lines.push('Flags: --json (machine-readable stdout), <command> --help (details and its own flags: start --no-open, test --visual, screenshot --commands --name --as --signed-out)');
1314
+ lines.push('Flags: --json (machine-readable stdout), <command> --help (details and its own flags: start --no-open, test --visual, screenshot --commands --name --as --signed-out, pull --code)');
1306
1315
  return {
1307
1316
  code: 0,
1308
1317
  data: {
@@ -14,7 +14,7 @@ export type PublishingIo = {
14
14
  sdkVersion: string;
15
15
  };
16
16
  export declare function login(io: PublishingIo): Promise<Outcome>;
17
- export declare function pull(io: PublishingIo, name: string | undefined, cwd: string): Promise<Outcome>;
17
+ export declare function pull(io: PublishingIo, name: string | undefined, cwd: string, setupCode: string | undefined): Promise<Outcome>;
18
18
  export declare function publish(io: PublishingIo, root: string, isStarter: boolean): Promise<Outcome>;
19
19
  export declare function versions(io: PublishingIo, root: string): Promise<Outcome>;
20
20
  export declare function restore(io: PublishingIo, root: string, number: string | undefined): Promise<Outcome>;
@@ -3,7 +3,7 @@ import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, st
3
3
  import { homedir, hostname, userInfo } from 'node:os';
4
4
  import { basename, dirname, extname, join, matchesGlob, relative } from 'node:path';
5
5
  import { z } from 'zod';
6
- import { DASHBOARD_URL, UPLOAD, appSummary, isPrivatePath, loginPoll, loginStarted, nameProblem, publishSummary, sourcePathProblem } from "../publishWire.js";
6
+ import { DASHBOARD_URL, UPLOAD, appSummary, isPrivatePath, loginPoll, loginStarted, nameProblem, publishSummary, setupCodeExchanged, sourcePathProblem } from "../publishWire.js";
7
7
  import { appInfo } from "../appTitle.js";
8
8
  import { PROJECT_FILE, readProjectFile } from "../vite/projectFile.js";
9
9
  const CONFIG_DIR = join(homedir(), '.config', 'pattern');
@@ -11,7 +11,7 @@ const CREDENTIALS_FILE = join(CONFIG_DIR, 'credentials.json');
11
11
  const PENDING_FILE = join(CONFIG_DIR, 'pending-login.json');
12
12
  const LOGIN_MINUTES = 10;
13
13
  const POLL_MS = 2000;
14
- const LOGIN_WAIT_MS = 120_000;
14
+ const LOGIN_WAIT_MS = 100_000;
15
15
  const PUBLISH_WAIT_MS = 300_000;
16
16
  const SESSION_COOKIE = 'pattern_session';
17
17
  const LAUNCHER = 'app';
@@ -154,7 +154,7 @@ async function signIn(io, again) {
154
154
  return {
155
155
  code: 3,
156
156
  data: { waitingFor: 'approval', code: pending.code, url: pending.url },
157
- text: `✗ Not allowed yet. The browser page ${pending.url} shows the code ${pending.code}.\n next once they click Allow there, run ${again} again`
157
+ text: `✗ Not allowed yet. The browser page ${pending.url} shows the code ${pending.code}.\n next run ${again} again: it waits for Allow`
158
158
  };
159
159
  }
160
160
  const response = await fetch(`${dashboard()}/api/auth/cli/logins`, {
@@ -172,9 +172,23 @@ async function signIn(io, again) {
172
172
  return {
173
173
  code: 3,
174
174
  data: { waitingFor: 'approval', code: started.code, url: started.url },
175
- text: `Opened ${started.url} in the browser.\n code ${started.code}\n next tell them: "Check the code on the page is ${started.code}, then click Allow." Then run ${again} again.`
175
+ text: `Opened ${started.url} in the browser.\n code ${started.code}\n next tell them: "Check the code on the page is ${started.code}, then click Allow." Then, without waiting for them to reply, run ${again} again: it waits for Allow.`
176
176
  };
177
177
  }
178
+ async function useSetupCode(io, code) {
179
+ const response = await fetch(`${dashboard()}/api/auth/cli/setup-codes/exchange`, {
180
+ method: 'POST',
181
+ headers: { 'content-type': 'application/json' },
182
+ body: JSON.stringify({ code, label: labelOfThisMachine() })
183
+ }).catch(() => undefined);
184
+ const exchanged = setupCodeExchanged.safeParse(await response?.json().catch(() => null));
185
+ if (response?.ok === true && exchanged.success) {
186
+ saveCookie(exchanged.data.cookie);
187
+ io.say('✓ Connected with the setup code');
188
+ return;
189
+ }
190
+ io.say('✗ That setup code has expired or was already used');
191
+ }
178
192
  async function signedIn(io, again, work) {
179
193
  if (savedCookie() === undefined) {
180
194
  const outcome = await signIn(io, again);
@@ -237,7 +251,7 @@ function folderFor(name, cwd) {
237
251
  function commandLineToolsReady() {
238
252
  return spawnSync('xcode-select', ['-p'], { stdio: 'ignore' }).status === 0;
239
253
  }
240
- export async function pull(io, name, cwd) {
254
+ export async function pull(io, name, cwd, setupCode) {
241
255
  if (name === undefined || nameProblem(name) !== undefined) {
242
256
  return {
243
257
  code: 2,
@@ -254,6 +268,8 @@ export async function pull(io, name, cwd) {
254
268
  };
255
269
  }
256
270
  try {
271
+ if (setupCode !== undefined)
272
+ await useSetupCode(io, setupCode);
257
273
  const result = await signedIn(io, `app pull ${name}`, async () => {
258
274
  const app = await callJson(`/api/apps/${name}`, appSummary);
259
275
  const response = await call(`/api/apps/${name}/source`);
@@ -0,0 +1,6 @@
1
+ import type { ReactNode } from 'react';
2
+ type AppearProps = {
3
+ children: ReactNode;
4
+ };
5
+ export declare function Appear({ children }: AppearProps): import("react").JSX.Element;
6
+ export {};
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as stylex from '@stylexjs/stylex';
3
+ import { breakpoints, motion, space } from "../theme/tokens.stylex.js";
4
+ export function Appear({ children }) {
5
+ return _jsx("div", { ...stylex.props(styles.appear), children: children });
6
+ }
7
+ const enter = stylex.keyframes({
8
+ from: { opacity: 0, transform: `translateY(${space.xs})` }
9
+ });
10
+ const styles = stylex.create({
11
+ appear: {
12
+ display: 'flex',
13
+ flexDirection: 'column',
14
+ animationName: {
15
+ default: enter,
16
+ [breakpoints.reducedMotion]: 'none'
17
+ },
18
+ animationDuration: motion.base,
19
+ animationTimingFunction: motion.easeOut
20
+ }
21
+ });
@@ -3,7 +3,7 @@ import type { Placement } from './Placement.ts';
3
3
  type BadgeProps = {
4
4
  children: ReactNode;
5
5
  variant?: 'primary' | 'tertiary' | 'outline' | 'transparent';
6
- tone?: 'neutral' | 'brand' | 'danger';
6
+ tone?: 'neutral' | 'brand' | 'positive' | 'danger';
7
7
  size?: 'sm' | 'md';
8
8
  style?: Placement;
9
9
  };
@@ -7,6 +7,7 @@ export function Badge({ children, variant = 'tertiary', tone = 'neutral', size =
7
7
  const toneKeys = {
8
8
  neutral: 'Neutral',
9
9
  brand: 'Brand',
10
+ positive: 'Positive',
10
11
  danger: 'Danger'
11
12
  };
12
13
  const styles = stylex.create({
@@ -41,6 +42,10 @@ const looks = stylex.create({
41
42
  color: colors.onBrandSolidForeground,
42
43
  backgroundColor: colors.brandSolid
43
44
  },
45
+ primaryPositive: {
46
+ color: colors.onPositiveSolidForeground,
47
+ backgroundColor: colors.positiveSolid
48
+ },
44
49
  primaryDanger: {
45
50
  color: colors.onDangerSolidForeground,
46
51
  backgroundColor: colors.dangerSolid
@@ -53,6 +58,10 @@ const looks = stylex.create({
53
58
  color: colors.onBrandSubtleForeground,
54
59
  backgroundColor: colors.brandSubtle
55
60
  },
61
+ tertiaryPositive: {
62
+ color: colors.onPositiveSubtleForeground,
63
+ backgroundColor: colors.positiveSubtle
64
+ },
56
65
  tertiaryDanger: {
57
66
  color: colors.dangerText,
58
67
  backgroundColor: colors.dangerSubtleHover
@@ -67,6 +76,11 @@ const looks = stylex.create({
67
76
  backgroundColor: 'transparent',
68
77
  boxShadow: `inset 0 0 0 ${borders.thin} ${colors.brandBorderDefault}`
69
78
  },
79
+ outlinePositive: {
80
+ color: colors.positiveForeground,
81
+ backgroundColor: 'transparent',
82
+ boxShadow: `inset 0 0 0 ${borders.thin} ${colors.positiveBorderDefault}`
83
+ },
70
84
  outlineDanger: {
71
85
  color: colors.dangerForeground,
72
86
  backgroundColor: 'transparent',
@@ -80,6 +94,10 @@ const looks = stylex.create({
80
94
  color: colors.brandForeground,
81
95
  backgroundColor: 'transparent'
82
96
  },
97
+ transparentPositive: {
98
+ color: colors.positiveForeground,
99
+ backgroundColor: 'transparent'
100
+ },
83
101
  transparentDanger: {
84
102
  color: colors.dangerForeground,
85
103
  backgroundColor: 'transparent'
@@ -1,7 +1,7 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import type { AppPath } from '../registered.ts';
3
3
  type LinkProps = {
4
- tone?: 'brand' | 'secondary';
4
+ tone?: 'brand' | 'positive' | 'secondary';
5
5
  children: ReactNode;
6
6
  } & ({
7
7
  to: AppPath;
@@ -35,6 +35,12 @@ const looks = stylex.create({
35
35
  ':hover': colors.brandSolidHover
36
36
  }
37
37
  },
38
+ positive: {
39
+ color: {
40
+ default: colors.positiveForeground,
41
+ ':hover': colors.positiveSolidHover
42
+ }
43
+ },
38
44
  secondary: {
39
45
  color: {
40
46
  default: colors.secondaryForeground,
@@ -10,7 +10,7 @@ export function SignInScreen() {
10
10
  const appSettings = useModel('appSettings');
11
11
  const signIn = useModel('signIn');
12
12
  const isBusy = signIn.signingIn !== null;
13
- return (_jsx("main", { ...stylex.props(styles.page), children: _jsxs("div", { ...stylex.props(styles.column), children: [_jsx("span", { ...stylex.props(styles.mark), children: _jsx(Text, { variant: "title", children: appSettings.title.charAt(0) }) }), _jsxs(Text, { as: "h1", variant: "title", tone: "primary", align: "center", children: ["Sign in to ", appSettings.title] }), signIn.step === 'choose' ? (_jsxs(_Fragment, { children: [_jsx(Button, { size: "lg", disabled: isBusy, isPending: signIn.signingIn === 'google', onClick: signIn.continueWithGoogle, children: "Continue with Google" }), _jsxs(Form, { onSubmit: (values) => signIn.sendEmailCode({ email: values['email'] ?? '' }), children: [_jsx(TextField, { name: "email", type: "email", required: true, autoComplete: "email", placeholder: "you@example.com", label: "Email", isLabelHidden: true, size: "lg" }), _jsx(Button, { type: "submit", variant: "secondary", size: "lg", disabled: isBusy, isPending: signIn.signingIn === 'email', children: "Email me a code" })] }), import.meta.env.DEV ? (_jsxs(Button, { variant: "tertiary", size: "lg", disabled: isBusy, isPending: signIn.signingIn === 'dev', onClick: () => signIn.signInAsDevUser({ email: DEV_USER }), children: ["Dev: sign in as ", DEV_USER] })) : null] })) : (_jsxs(_Fragment, { children: [_jsxs(Text, { as: "p", tone: "secondary", align: "center", children: ["We sent a 6-digit code to ", signIn.email, "."] }), _jsxs(Form, { onSubmit: (values) => signIn.verifyEmailCode({ code: values['code'] ?? '' }), children: [_jsx(TextField, { name: "code", required: true, autoFocus: true, inputMode: "numeric", autoComplete: "one-time-code", pattern: "[0-9]{6}", maxLength: 6, placeholder: "123456", label: "Code", isLabelHidden: true, size: "lg" }), _jsx(Button, { type: "submit", size: "lg", disabled: isBusy, isPending: signIn.signingIn === 'email', children: "Sign in" })] }), _jsx(Button, { variant: "transparent", size: "lg", disabled: isBusy, onClick: signIn.startOver, children: "Use a different email" })] })), signIn.notice === null ? null : (_jsx(Text, { as: "p", tone: "secondary", align: "center", children: signIn.notice })), _jsx(Text, { as: "p", variant: "caption", tone: "tertiary", align: "center", children: "Sign-in by Pattern" })] }) }));
13
+ return (_jsx("main", { ...stylex.props(styles.page), children: _jsxs("div", { ...stylex.props(styles.column), children: [_jsx("span", { ...stylex.props(styles.mark), children: _jsx(Text, { variant: "title", children: appSettings.title.charAt(0) }) }), _jsxs(Text, { as: "h1", variant: "title", tone: "primary", align: "center", children: ["Sign in to ", appSettings.title] }), signIn.step === 'choose' ? (_jsxs(_Fragment, { children: [_jsx(Button, { size: "lg", disabled: isBusy, isPending: signIn.signingIn === 'google', onClick: signIn.continueWithGoogle, children: "Continue with Google" }), _jsxs(Form, { onSubmit: (values) => signIn.sendEmailCode({ email: values['email'] ?? '' }), children: [_jsx(TextField, { name: "email", type: "email", autoComplete: "email", placeholder: "you@example.com", label: "Email", isLabelHidden: true, size: "lg", disabled: true }), _jsx(Button, { type: "submit", variant: "secondary", size: "lg", disabled: true, children: "Email me a code" }), _jsx(Text, { variant: "caption", tone: "tertiary", align: "center", children: "Email sign-in coming soon" })] }), import.meta.env.DEV ? (_jsxs(Button, { variant: "tertiary", size: "lg", disabled: isBusy, isPending: signIn.signingIn === 'dev', onClick: () => signIn.signInAsDevUser({ email: DEV_USER }), children: ["Dev: sign in as ", DEV_USER] })) : null] })) : (_jsxs(_Fragment, { children: [_jsxs(Text, { as: "p", tone: "secondary", align: "center", children: ["We sent a 6-digit code to ", signIn.email, "."] }), _jsxs(Form, { onSubmit: (values) => signIn.verifyEmailCode({ code: values['code'] ?? '' }), children: [_jsx(TextField, { name: "code", required: true, autoFocus: true, inputMode: "numeric", autoComplete: "one-time-code", pattern: "[0-9]{6}", maxLength: 6, placeholder: "123456", label: "Code", isLabelHidden: true, size: "lg" }), _jsx(Button, { type: "submit", size: "lg", disabled: isBusy, isPending: signIn.signingIn === 'email', children: "Sign in" })] }), _jsx(Button, { variant: "transparent", size: "lg", disabled: isBusy, onClick: signIn.startOver, children: "Use a different email" })] })), signIn.notice === null ? null : (_jsx(Text, { as: "p", tone: "secondary", align: "center", children: signIn.notice })), _jsx(Text, { as: "p", variant: "caption", tone: "tertiary", align: "center", children: "Sign-in by Pattern" })] }) }));
14
14
  }
15
15
  const DEV_USER = 'ada@acme.test';
16
16
  const styles = stylex.create({
@@ -14,7 +14,7 @@ export function TextField({ label, isLabelHidden = false, description, error, on
14
14
  if (event.currentTarget.value !== valueOnFocus.current) {
15
15
  interact({ primitive: 'TextField', event: 'change', label });
16
16
  }
17
- }, className: stylex.props(styles.input, controlSizes[size], isInvalid && styles.invalid).className }), description === undefined ? null : (_jsx(Field.Description, { className: stylex.props(styles.note).className, children: description })), error === undefined ? null : (_jsx(Field.Error, { match: true, className: stylex.props(styles.error).className, children: error }))] }));
17
+ }, className: stylex.props(styles.input, controlSizes[size], isInvalid && styles.invalid, input.disabled === true && styles.disabled).className }), description === undefined ? null : (_jsx(Field.Description, { className: stylex.props(styles.note).className, children: description })), error === undefined ? null : (_jsx(Field.Error, { match: true, className: stylex.props(styles.error).className, children: error }))] }));
18
18
  }
19
19
  const focusRing = `inset 0 0 0 ${borders.hairline} ${colors.brandBorderFocus}, 0 0 0 ${borders.thin} ${colors.brandBorderFocus}, ${shadows.default}`;
20
20
  const invalidRing = `inset 0 0 0 ${borders.hairline} ${colors.dangerBorderDefault}, 0 0 0 ${borders.thin} ${colors.dangerBorderDefault}, ${shadows.default}`;
@@ -55,6 +55,15 @@ const styles = stylex.create({
55
55
  ':focus-visible': invalidRing
56
56
  }
57
57
  },
58
+ disabled: {
59
+ color: colors.disabledForeground,
60
+ cursor: 'not-allowed',
61
+ backgroundColor: colors.disabled,
62
+ boxShadow: 'none',
63
+ '::placeholder': {
64
+ color: colors.disabledForeground
65
+ }
66
+ },
58
67
  note: {
59
68
  fontSize: fontSizes.captionLg,
60
69
  lineHeight: lineHeights.caption,
@@ -99,6 +99,18 @@ export declare const loginToApprove: z.ZodObject<{
99
99
  code: z.ZodString;
100
100
  label: z.ZodString;
101
101
  }, z.core.$strip>;
102
+ export declare const setupCode: z.ZodObject<{
103
+ code: z.ZodString;
104
+ expiresAt: z.ZodString;
105
+ }, z.core.$strip>;
106
+ export declare const setupCodeExchanged: z.ZodObject<{
107
+ cookie: z.ZodString;
108
+ }, z.core.$strip>;
109
+ export declare const connection: z.ZodObject<{
110
+ label: z.ZodString;
111
+ connectedAt: z.ZodString;
112
+ }, z.core.$strip>;
113
+ export type Connection = z.output<typeof connection>;
102
114
  export declare const publishServices: {
103
115
  readonly apps: {
104
116
  readonly list: NoInfer<import("./services.ts").ServiceCall<"/apps", {}, z.ZodArray<z.ZodObject<{
@@ -259,6 +271,19 @@ export declare const publishServices: {
259
271
  finishedAt: z.ZodNullable<z.ZodString>;
260
272
  }, z.core.$strip>>>;
261
273
  };
274
+ readonly setup: {
275
+ readonly newCode: NoInfer<import("./services.ts").ServiceCall<"/apps/:name/setup-code", {}, z.ZodObject<{
276
+ code: z.ZodString;
277
+ expiresAt: z.ZodString;
278
+ }, z.core.$strip>>>;
279
+ readonly connection: NoInfer<import("./services.ts").ServiceCall<"/apps/:name/connection", {}, z.ZodNullable<z.ZodObject<{
280
+ label: z.ZodString;
281
+ connectedAt: z.ZodString;
282
+ }, z.core.$strip>>>>;
283
+ readonly disconnect: NoInfer<import("./services.ts").ServiceCall<"/apps/:name/connection/disconnect", {}, z.ZodObject<{
284
+ ok: z.ZodBoolean;
285
+ }, z.core.$strip>>>;
286
+ };
262
287
  readonly logins: {
263
288
  readonly read: NoInfer<import("./services.ts").ServiceCall<"/cli/logins/:approval", {}, z.ZodObject<{
264
289
  code: z.ZodString;
@@ -126,6 +126,12 @@ export const loginPoll = z.union([
126
126
  z.object({ status: z.literal('approved'), cookie: z.string() })
127
127
  ]);
128
128
  export const loginToApprove = z.object({ code: z.string(), label: z.string() });
129
+ export const setupCode = z.object({ code: z.string(), expiresAt: z.string() });
130
+ export const setupCodeExchanged = z.object({ cookie: z.string() });
131
+ export const connection = z.object({
132
+ label: z.string(),
133
+ connectedAt: z.string()
134
+ });
129
135
  export const publishServices = defineServices({
130
136
  apps: {
131
137
  list: call({ get: '/apps', reply: z.array(appSummary) }),
@@ -158,6 +164,17 @@ export const publishServices = defineServices({
158
164
  reply: publishSummary
159
165
  })
160
166
  },
167
+ setup: {
168
+ newCode: call({ post: '/apps/:name/setup-code', reply: setupCode }),
169
+ connection: call({
170
+ get: '/apps/:name/connection',
171
+ reply: connection.nullable()
172
+ }),
173
+ disconnect: call({
174
+ post: '/apps/:name/connection/disconnect',
175
+ reply: z.object({ ok: z.boolean() })
176
+ })
177
+ },
161
178
  logins: {
162
179
  read: call({ get: '/cli/logins/:approval', reply: loginToApprove }),
163
180
  approve: call({
package/dist/react.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { AppProvider, useModel } from './hooks/appContext.ts';
2
2
  export { AppFrame } from './components/AppFrame.tsx';
3
+ export { Appear } from './components/Appear.tsx';
3
4
  export { AppearancePicker } from './components/AppearancePicker.tsx';
4
5
  export { Avatar } from './components/Avatar.tsx';
5
6
  export { Badge } from './components/Badge.tsx';
package/dist/react.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { AppProvider, useModel } from "./hooks/appContext.js";
2
2
  export { AppFrame } from "./components/AppFrame.js";
3
+ export { Appear } from "./components/Appear.js";
3
4
  export { AppearancePicker } from "./components/AppearancePicker.js";
4
5
  export { Avatar } from "./components/Avatar.js";
5
6
  export { Badge } from "./components/Badge.js";
@@ -44,6 +44,7 @@ const CLICK_HANDLERS = new Set([
44
44
  ]);
45
45
  const PRIMITIVES = new Set([
46
46
  'AppFrame',
47
+ 'Appear',
47
48
  'AppearancePicker',
48
49
  'Avatar',
49
50
  'Badge',
@@ -5,7 +5,7 @@ export { refused } from '../services.ts';
5
5
  export { serveServices } from './services.ts';
6
6
  export type { ServiceHandlers } from './services.ts';
7
7
  export { sessionCookieFor } from './session.ts';
8
- export { sessions } from './sessions.ts';
8
+ export { deleteSession, sessions } from './sessions.ts';
9
9
  export { readTitle, seedTitleSql, settings, writeTitle } from './settings.ts';
10
10
  export { exchangeReply, exchangeRequest, sendCodeReply, sendCodeRequest, verifiedIdentity, verifyCodeReply, verifyCodeRequest } from './signInWire.ts';
11
11
  export type { VerifiedIdentity } from './signInWire.ts';
@@ -3,7 +3,7 @@ export { seedSql, serveTables, sqlTable, sqlTables } from "./tables.js";
3
3
  export { refused } from "../services.js";
4
4
  export { serveServices } from "./services.js";
5
5
  export { sessionCookieFor } from "./session.js";
6
- export { sessions } from "./sessions.js";
6
+ export { deleteSession, sessions } from "./sessions.js";
7
7
  export { readTitle, seedTitleSql, settings, writeTitle } from "./settings.js";
8
8
  export { exchangeReply, exchangeRequest, sendCodeReply, sendCodeRequest, verifiedIdentity, verifyCodeReply, verifyCodeRequest } from "./signInWire.js";
9
9
  export { findUser, users } from "./users.js";
@@ -17,7 +17,10 @@ export declare function issuePass(c: Context<ServerEnv>, user: StoredUser, sessi
17
17
  recheckAt: number;
18
18
  }>;
19
19
  export declare function startSession(c: Context<ServerEnv>, user: StoredUser): Promise<void>;
20
- export declare function sessionCookieFor(env: ServerEnv['Bindings'], user: StoredUser): Promise<string>;
20
+ export declare function sessionCookieFor(env: ServerEnv['Bindings'], user: StoredUser): Promise<{
21
+ cookie: string;
22
+ sessionId: `${string}-${string}-${string}-${string}-${string}`;
23
+ }>;
21
24
  export declare function readPass(c: Context<ServerEnv>): Promise<{
22
25
  sessionId: string;
23
26
  userId: string;
@@ -37,14 +37,15 @@ export async function startSession(c, user) {
37
37
  await issuePass(c, user, await createSession(c.env.DB, user.id));
38
38
  }
39
39
  export async function sessionCookieFor(env, user) {
40
+ const sessionId = await createSession(env.DB, user.id);
40
41
  const pass = {
41
- sessionId: await createSession(env.DB, user.id),
42
+ sessionId,
42
43
  userId: user.id,
43
44
  email: user.email,
44
45
  recheckAt: nowInSeconds() + RECHECK_SECONDS
45
46
  };
46
47
  const cookie = await serializeSigned(SESSION_COOKIE, JSON.stringify(pass), env.SESSION_SECRET);
47
- return cookie.split(';')[0] ?? cookie;
48
+ return { cookie: cookie.split(';')[0] ?? cookie, sessionId };
48
49
  }
49
50
  export async function readPass(c) {
50
51
  const value = await getSignedCookie(c, c.env.SESSION_SECRET, SESSION_COOKIE);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trypattern/sdk",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",