zitejs 0.9.120 → 0.9.122

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.
Files changed (45) hide show
  1. package/dist/cjs/auth/config.d.ts +3 -0
  2. package/dist/cjs/auth/config.js +7 -0
  3. package/dist/cjs/auth/index.js +30 -4
  4. package/dist/cjs/auth/useAuth.test.js +5 -19
  5. package/dist/cjs/backend/index.d.ts +30 -3
  6. package/dist/cjs/backend/index.js +2 -2
  7. package/dist/cjs/bundle/index.d.ts +1 -1
  8. package/dist/cjs/bundle/index.js +6 -2
  9. package/dist/cjs/check/index.js +5 -13
  10. package/dist/cjs/cli.js +2 -2
  11. package/dist/cjs/dev/index.js +22 -29
  12. package/dist/cjs/sourceRoots.d.ts +19 -0
  13. package/dist/cjs/sourceRoots.js +33 -0
  14. package/dist/cjs/sync/lib.js +9 -7
  15. package/dist/cjs/sync/lib.test.js +5 -0
  16. package/dist/cjs/upload/index.js +19 -0
  17. package/dist/cjs/upload/index.test.js +30 -3
  18. package/dist/cjs/vite/domTagNames.d.ts +1 -0
  19. package/dist/cjs/vite/domTagNames.js +257 -0
  20. package/dist/cjs/vite/index.js +20 -11
  21. package/dist/cjs/vite/index.test.d.ts +1 -0
  22. package/dist/cjs/vite/index.test.js +53 -0
  23. package/dist/esm/auth/config.d.ts +3 -0
  24. package/dist/esm/auth/config.js +6 -0
  25. package/dist/esm/auth/index.js +28 -2
  26. package/dist/esm/auth/useAuth.test.js +5 -19
  27. package/dist/esm/backend/index.d.ts +30 -3
  28. package/dist/esm/backend/index.js +2 -2
  29. package/dist/esm/bundle/index.d.ts +1 -1
  30. package/dist/esm/bundle/index.js +6 -2
  31. package/dist/esm/check/index.js +6 -14
  32. package/dist/esm/cli.js +2 -2
  33. package/dist/esm/dev/index.js +22 -29
  34. package/dist/esm/sourceRoots.d.ts +19 -0
  35. package/dist/esm/sourceRoots.js +28 -0
  36. package/dist/esm/sync/lib.js +9 -7
  37. package/dist/esm/sync/lib.test.js +5 -0
  38. package/dist/esm/upload/index.js +19 -0
  39. package/dist/esm/upload/index.test.js +30 -3
  40. package/dist/esm/vite/domTagNames.d.ts +1 -0
  41. package/dist/esm/vite/domTagNames.js +254 -0
  42. package/dist/esm/vite/index.js +20 -11
  43. package/dist/esm/vite/index.test.d.ts +1 -0
  44. package/dist/esm/vite/index.test.js +48 -0
  45. package/package.json +1 -1
@@ -2,3 +2,6 @@ export declare function getFlowId(): string;
2
2
  export declare function getApiUrl(): string;
3
3
  export declare function getAuthPageUrl(): string;
4
4
  export declare function getDbToken(): string;
5
+ /** Undefined for apps built before the platform sent this — treat as unknown,
6
+ * never as a default side. */
7
+ export declare function getAccessMode(): 'internal' | 'external' | undefined;
@@ -4,6 +4,7 @@ exports.getFlowId = getFlowId;
4
4
  exports.getApiUrl = getApiUrl;
5
5
  exports.getAuthPageUrl = getAuthPageUrl;
6
6
  exports.getDbToken = getDbToken;
7
+ exports.getAccessMode = getAccessMode;
7
8
  const env_js_1 = require("../internal/env.js");
8
9
  const API_ENVIRONMENTS = {
9
10
  production: 'https://api.fillout.com',
@@ -36,3 +37,9 @@ function getAuthPageUrl() {
36
37
  function getDbToken() {
37
38
  return (0, env_js_1.getEnv)('ZITE_DB_TOKEN', 'VITE_ZITE_DB_TOKEN') ?? '';
38
39
  }
40
+ /** Undefined for apps built before the platform sent this — treat as unknown,
41
+ * never as a default side. */
42
+ function getAccessMode() {
43
+ const mode = (0, env_js_1.getEnv)('ZITE_ACCESS_MODE', 'VITE_ZITE_ACCESS_MODE');
44
+ return mode === 'internal' || mode === 'external' ? mode : undefined;
45
+ }
@@ -5,10 +5,12 @@ exports.useAuth = useAuth;
5
5
  exports.loginWithRedirect = loginWithRedirect;
6
6
  exports.logout = logout;
7
7
  exports.updateProfile = updateProfile;
8
- const react_1 = require("better-auth/react");
8
+ const react_1 = require("react");
9
+ const react_2 = require("better-auth/react");
9
10
  const plugins_1 = require("better-auth/client/plugins");
10
11
  const constants_js_1 = require("./constants.js");
11
- const authClient = (0, react_1.createAuthClient)({
12
+ const config_js_1 = require("./config.js");
13
+ const authClient = (0, react_2.createAuthClient)({
12
14
  baseURL: '',
13
15
  plugins: [
14
16
  (0, plugins_1.magicLinkClient)(),
@@ -21,6 +23,22 @@ const authClient = (0, react_1.createAuthClient)({
21
23
  ],
22
24
  });
23
25
  exports.useSession = authClient.useSession;
26
+ /**
27
+ * An internal app has no signed-out state — SSO runs before the bundle — so an
28
+ * empty session means it died underneath the app. Only `internal`: elsewhere a
29
+ * signed-out visitor is legitimate.
30
+ */
31
+ function shouldRedirectSignedOut(isPending, hasUser) {
32
+ if (isPending || hasUser)
33
+ return false;
34
+ if ((0, config_js_1.getAccessMode)() !== 'internal')
35
+ return false;
36
+ if (typeof window === 'undefined')
37
+ return false;
38
+ // `loginWithRedirect` won't navigate from an auth page, so claiming a
39
+ // redirect here would hang `isLoading` forever.
40
+ return !window.location.pathname.startsWith('/auth/');
41
+ }
24
42
  /**
25
43
  * `loginWithRedirect` and `logout` are returned as well as exported. The
26
44
  * pre-monorepo `useAuth()` returned them, so app code that destructures them
@@ -29,9 +47,17 @@ exports.useSession = authClient.useSession;
29
47
  */
30
48
  function useAuth() {
31
49
  const { data, isPending } = (0, exports.useSession)();
50
+ const user = data?.user ?? null;
51
+ const redirecting = shouldRedirectSignedOut(isPending, user !== null);
52
+ (0, react_1.useEffect)(() => {
53
+ if (redirecting)
54
+ loginWithRedirect();
55
+ }, [redirecting]);
32
56
  return {
33
- user: data?.user ?? null,
34
- isLoading: isPending,
57
+ user,
58
+ // Loading, not signed out: the signed-out branch is the empty screen this
59
+ // exists to prevent.
60
+ isLoading: isPending || redirecting,
35
61
  loginWithRedirect,
36
62
  logout,
37
63
  };
@@ -1,18 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const vitest_1 = require("vitest");
4
- /**
5
- * The signed-out redirect on internal apps.
6
- *
7
- * A separate file from `index.test.ts` because it has to replace
8
- * `better-auth/react` wholesale to drive `useSession`, and that would strip the
9
- * real client the export tests next door assert against.
10
- *
11
- * The value here is entirely in WHICH signed-out renders it reacts to. An
12
- * external app has a real logged-out state, and an app built before the
13
- * platform sent an access mode tells us nothing — redirecting either turns a
14
- * working public page into a forced sign-in.
15
- */
4
+ // Separate from index.test.ts: driving `useSession` means replacing
5
+ // `better-auth/react`, which would strip the real client asserted against there.
16
6
  const { useSessionMock } = vitest_1.vi.hoisted(() => ({ useSessionMock: vitest_1.vi.fn() }));
17
7
  vitest_1.vi.mock('better-auth/react', () => ({
18
8
  createAuthClient: () => ({
@@ -27,8 +17,7 @@ vitest_1.vi.mock('better-auth/client/plugins', () => ({
27
17
  magicLinkClient: () => ({}),
28
18
  inferAdditionalFields: () => ({}),
29
19
  }));
30
- // `useAuth` takes only `useEffect` from React. Running it inline is the whole
31
- // of what a render would do here, and avoids pulling in a renderer.
20
+ // `useAuth` uses only `useEffect`; running it inline avoids pulling in a renderer.
32
21
  vitest_1.vi.mock('react', () => ({ useEffect: (fn) => fn() }));
33
22
  const index_js_1 = require("./index.js");
34
23
  const APP_URL = 'https://app.zite.so/dashboard';
@@ -57,8 +46,6 @@ const render = (session, { accessMode, href = APP_URL } = {}) => {
57
46
  (0, vitest_1.it)('sends a signed-out visitor on an internal app to sign-in', () => {
58
47
  const { result, location } = render(signedOut, { accessMode: 'internal' });
59
48
  (0, vitest_1.expect)(location.href.startsWith('/auth/login')).toBe(true);
60
- // Reported as loading rather than signed out: the signed-out branch of an
61
- // internal app is the empty screen this exists to prevent.
62
49
  (0, vitest_1.expect)(result.isLoading).toBe(true);
63
50
  });
64
51
  (0, vitest_1.it)('leaves a signed-out visitor on an external app alone', () => {
@@ -67,7 +54,7 @@ const render = (session, { accessMode, href = APP_URL } = {}) => {
67
54
  (0, vitest_1.expect)(result.isLoading).toBe(false);
68
55
  (0, vitest_1.expect)(result.user).toBe(null);
69
56
  });
70
- // Every app published before the platform started sending an access mode.
57
+ // i.e. every app published before the platform sent an access mode.
71
58
  (0, vitest_1.it)('leaves an app that never declared an access mode alone', () => {
72
59
  const { result, location } = render(signedOut);
73
60
  (0, vitest_1.expect)(location.href).toBe(APP_URL);
@@ -84,8 +71,7 @@ const render = (session, { accessMode, href = APP_URL } = {}) => {
84
71
  (0, vitest_1.expect)(result.isLoading).toBe(false);
85
72
  (0, vitest_1.expect)(result.user).toMatchObject({ id: 'u1' });
86
73
  });
87
- // `loginWithRedirect` refuses to navigate away from an auth page, so claiming
88
- // a redirect here would hang the caller on `isLoading` forever.
74
+ // Claiming a redirect it can't make would hang `isLoading` forever.
89
75
  (0, vitest_1.it)('does not claim to be loading on an auth page it cannot leave', () => {
90
76
  const { result } = render(signedOut, {
91
77
  accessMode: 'internal',
@@ -3,6 +3,27 @@ export type { ZiteSchedule };
3
3
  export type ZiteWebhook = {
4
4
  paused?: boolean;
5
5
  };
6
+ /**
7
+ * A provider trigger: what should fire the endpoint, declared as a literal.
8
+ * The platform registers it with the provider at publish, verifies each
9
+ * delivery and queues it; the endpoint only sees the payload. A `poll`
10
+ * trigger runs the endpoint on a cadence with `input.__poll.cursor` from the
11
+ * previous run, and the endpoint returns `{ cursor }` to advance it.
12
+ */
13
+ export type ZiteTrigger = {
14
+ /**
15
+ * The connection's name in `zite.config.json`. Registered with the
16
+ * provider by `src/triggers/<endpointId>/subscribe.ts` and taken down
17
+ * by `unsubscribe.ts`, which the platform runs at publish and teardown.
18
+ */
19
+ integration: string;
20
+ paused?: boolean;
21
+ } | {
22
+ provider: "poll";
23
+ /** `<n>m` (1–59) or `<n>h` (1–23) */
24
+ every: string;
25
+ paused?: boolean;
26
+ };
6
27
  export interface ZiteRequestContext {
7
28
  user: {
8
29
  id: string;
@@ -58,7 +79,7 @@ export type ZiteStreamInterface = {
58
79
  write: (data: unknown) => Promise<void>;
59
80
  forward: (asyncIterable: AsyncIterable<string>) => Promise<string>;
60
81
  };
61
- export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream extends boolean = false, TSchedule extends ZiteSchedule | undefined = undefined, TWebhook extends ZiteWebhook | undefined = undefined, TRawInput = TInput> {
82
+ export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream extends boolean = false, TSchedule extends ZiteSchedule | undefined = undefined, TWebhook extends ZiteWebhook | undefined = undefined, TRawInput = TInput, TTrigger extends ZiteTrigger | undefined = undefined> {
62
83
  description?: string;
63
84
  inputSchema?: SchemaLike<TInput, TRawInput>;
64
85
  /**
@@ -82,6 +103,12 @@ export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream ext
82
103
  * `{ user: null }` for a webhook fire exactly as it does for a cron one.
83
104
  */
84
105
  webhook?: TWebhook;
106
+ /**
107
+ * When set, the platform fires this endpoint from the named provider. Like
108
+ * `schedule` and `webhook`, this widens `context`: a provider delivery has
109
+ * no signed-in user.
110
+ */
111
+ trigger?: TTrigger;
85
112
  /**
86
113
  * Set by {@link createEndpoint}, not by hand. The worker validates
87
114
  * `inputSchema` for bundles built before this did, and skips ones that
@@ -90,7 +117,7 @@ export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream ext
90
117
  validatesInput?: boolean;
91
118
  execute: (params: {
92
119
  input: TInput;
93
- context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : TWebhook extends ZiteWebhook ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;
120
+ context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : TWebhook extends ZiteWebhook ? ZiteRequestContext | ZiteScheduledContext : TTrigger extends ZiteTrigger ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;
94
121
  } & (TStream extends true ? {
95
122
  stream: ZiteStreamInterface;
96
123
  } : {})) => Promise<TOutput> | TOutput;
@@ -102,4 +129,4 @@ export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream ext
102
129
  * field could be missing and the endpoint ran anyway. Parsing here is also what
103
130
  * makes `TRawInput` -> `TInput` (defaults, transforms) true at runtime.
104
131
  */
105
- export declare function createEndpoint<TInput = unknown, TOutput = unknown, TStream extends boolean = false, TSchedule extends ZiteSchedule | undefined = undefined, TWebhook extends ZiteWebhook | undefined = undefined, TRawInput = TInput>(config: EndpointConfig<TInput, TOutput, TStream, TSchedule, TWebhook, TRawInput>): EndpointConfig<TInput, TOutput, TStream, TSchedule, TWebhook, TRawInput>;
132
+ export declare function createEndpoint<TInput = unknown, TOutput = unknown, TStream extends boolean = false, TSchedule extends ZiteSchedule | undefined = undefined, TWebhook extends ZiteWebhook | undefined = undefined, TRawInput = TInput, TTrigger extends ZiteTrigger | undefined = undefined>(config: EndpointConfig<TInput, TOutput, TStream, TSchedule, TWebhook, TRawInput, TTrigger>): EndpointConfig<TInput, TOutput, TStream, TSchedule, TWebhook, TRawInput, TTrigger>;
@@ -32,7 +32,7 @@ class ZiteError extends Error {
32
32
  }
33
33
  exports.ZiteError = ZiteError;
34
34
  /** Injected by the runner on a platform-fired run — see `isPlatformTriggeredInput`. */
35
- const PLATFORM_TRIGGER_KEYS = ["__cron", "__webhook"];
35
+ const PLATFORM_TRIGGER_KEYS = ["__cron", "__webhook", "__poll"];
36
36
  function isPlatformTriggeredInput(input) {
37
37
  if (typeof input !== "object" || input === null)
38
38
  return false;
@@ -60,7 +60,7 @@ function describeIssues(error) {
60
60
  */
61
61
  function createEndpoint(config) {
62
62
  const { inputSchema, execute } = config;
63
- const firesWithoutARequest = Boolean(config.schedule || config.webhook);
63
+ const firesWithoutARequest = Boolean(config.schedule || config.webhook || config.trigger);
64
64
  // An `inputSchema` that isn't a validator stays inert, as it was.
65
65
  if (!inputSchema || typeof inputSchema.parse !== "function")
66
66
  return config;
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * Usage:
10
10
  * npx zitejs bundle # bundle all endpoints
11
- * npx zitejs bundle --app admin-panel # bundle endpoints for a specific app
11
+ * npx zitejs bundle --app admin-panel # bundle endpoints for one app or automation, by dir name
12
12
  * npx zitejs bundle --script <path> # bundle a one-off script
13
13
  *
14
14
  * Output: JSON to stdout
@@ -45,7 +45,7 @@ exports.runBundle = runBundle;
45
45
  *
46
46
  * Usage:
47
47
  * npx zitejs bundle # bundle all endpoints
48
- * npx zitejs bundle --app admin-panel # bundle endpoints for a specific app
48
+ * npx zitejs bundle --app admin-panel # bundle endpoints for one app or automation, by dir name
49
49
  * npx zitejs bundle --script <path> # bundle a one-off script
50
50
  *
51
51
  * Output: JSON to stdout
@@ -61,6 +61,7 @@ const path = __importStar(require("path"));
61
61
  const fs = __importStar(require("fs"));
62
62
  const parser_1 = require("@babel/parser");
63
63
  const node_module_1 = require("node:module");
64
+ const sourceRoots_js_1 = require("../sourceRoots.js");
64
65
  // workerd's `nodejs_compat` does not provide these. Left external they bundle
65
66
  // fine and then kill the worker at load time with `No such module`; excluded,
66
67
  // esbuild fails the one endpoint with a resolvable error instead. Pinned by
@@ -615,7 +616,10 @@ async function runBundle() {
615
616
  const appFlag = args.indexOf('--app');
616
617
  let baseDir = process.cwd();
617
618
  if (appFlag !== -1 && args[appFlag + 1]) {
618
- baseDir = path.resolve(baseDir, 'apps', args[appFlag + 1]);
619
+ const name = args[appFlag + 1];
620
+ // A dir that exists under no root still resolves under `apps/`, so the
621
+ // error names the path a caller expects rather than "not found".
622
+ baseDir = path.resolve(baseDir, (0, sourceRoots_js_1.findSourceDir)(name, baseDir)?.path ?? path.join('apps', name));
619
623
  }
620
624
  // If explicit endpoint names passed, use those; otherwise find all.
621
625
  // Skipping `--app`'s VALUE as well as the flag: it is a positional arg, so
@@ -4,14 +4,7 @@ exports.runCheck = runCheck;
4
4
  const child_process_1 = require("child_process");
5
5
  const fs_1 = require("fs");
6
6
  const path_1 = require("path");
7
- function findAppDirs() {
8
- const appsDir = 'apps';
9
- if (!(0, fs_1.existsSync)(appsDir))
10
- return [];
11
- return (0, fs_1.readdirSync)(appsDir, { withFileTypes: true })
12
- .filter(d => d.isDirectory())
13
- .map(d => d.name);
14
- }
7
+ const sourceRoots_js_1 = require("../sourceRoots.js");
15
8
  /**
16
9
  * An argument array, not a command string, so no shell is involved.
17
10
  *
@@ -66,15 +59,14 @@ function bundleFailures(output) {
66
59
  return failures;
67
60
  }
68
61
  async function runCheck() {
69
- const appDirs = findAppDirs();
62
+ const appDirs = (0, sourceRoots_js_1.listSourceDirs)();
70
63
  if (appDirs.length === 0) {
71
- console.error('No apps found in apps/ directory.');
64
+ console.error('No apps or automations found under apps/ or automations/.');
72
65
  process.exit(1);
73
66
  }
74
67
  let allPassed = true;
75
- for (const app of appDirs) {
76
- const appPath = (0, path_1.join)('apps', app);
77
- console.log(`\n── ${app} ──`);
68
+ for (const { dir: app, path: appPath } of appDirs) {
69
+ console.log(`\n── ${appPath} ──`);
78
70
  // Only tsconfig.app.json compiles anything. The app's tsconfig.json is a
79
71
  // solution-style config — `"files": []` plus a reference — so
80
72
  // `tsc --noEmit -p tsconfig.json` type-checks zero files and exits 0. It
package/dist/cjs/cli.js CHANGED
@@ -73,8 +73,8 @@ async function main() {
73
73
  console.error('Commands:');
74
74
  console.error(' sync Generate .zite/db.ts from database schema (single-app)');
75
75
  console.error(' dev Run sync then watch for changes (like npx convex dev)');
76
- console.error(' generate One-shot sync + regenerate all apps .zite/ files (monorepo)');
77
- console.error(' check Run tsc --noEmit and vite build for all apps');
76
+ console.error(' generate Regenerate every app and automation .zite/ (monorepo)');
77
+ console.error(' check Run tsc --noEmit (and vite build where there is one) for every app and automation');
78
78
  console.error(' bundle Bundle src/api/*.ts endpoints for cloudflare-lambda');
79
79
  process.exit(1);
80
80
  }
@@ -5,6 +5,7 @@ exports.runDev = runDev;
5
5
  const fs_1 = require("fs");
6
6
  const fs_2 = require("fs");
7
7
  const path_1 = require("path");
8
+ const sourceRoots_js_1 = require("../sourceRoots.js");
8
9
  const index_js_1 = require("../sync/index.js");
9
10
  const lib_js_1 = require("../sync/lib.js");
10
11
  const debounceTimers = new Map();
@@ -14,17 +15,9 @@ function debounce(key, fn, ms) {
14
15
  clearTimeout(existing);
15
16
  debounceTimers.set(key, setTimeout(fn, ms));
16
17
  }
17
- function findAppDirs() {
18
- const appsDir = "apps";
19
- if (!(0, fs_2.existsSync)(appsDir))
20
- return [];
21
- return (0, fs_2.readdirSync)(appsDir, { withFileTypes: true })
22
- .filter((d) => d.isDirectory())
23
- .map((d) => d.name);
24
- }
25
18
  function getFlowId(appDir) {
26
19
  try {
27
- const configPath = (0, path_1.join)("apps", appDir, "zite.config.json");
20
+ const configPath = (0, path_1.join)(appDir.path, "zite.config.json");
28
21
  if ((0, fs_2.existsSync)(configPath)) {
29
22
  const config = JSON.parse((0, fs_2.readFileSync)(configPath, "utf-8"));
30
23
  return config.id;
@@ -44,11 +37,11 @@ const readJsonFile = (path) => {
44
37
  }
45
38
  };
46
39
  function getEmailIntegrationId(appDir) {
47
- return (0, lib_js_1.findEmailIntegrationId)(readJsonFile((0, path_1.join)("apps", appDir, "zite.config.json")), readJsonFile("zite.config.json"));
40
+ return (0, lib_js_1.findEmailIntegrationId)(readJsonFile((0, path_1.join)(appDir.path, "zite.config.json")), readJsonFile("zite.config.json"));
48
41
  }
49
42
  function getDeclaredEnvVarNames(appDir) {
50
43
  try {
51
- const configPath = (0, path_1.join)("apps", appDir, "zite.config.json");
44
+ const configPath = (0, path_1.join)(appDir.path, "zite.config.json");
52
45
  if (!(0, fs_2.existsSync)(configPath))
53
46
  return [];
54
47
  const config = JSON.parse((0, fs_2.readFileSync)(configPath, "utf-8"));
@@ -59,7 +52,7 @@ function getDeclaredEnvVarNames(appDir) {
59
52
  }
60
53
  }
61
54
  function regenerateAppTypedWrappers(appDir) {
62
- const outDir = (0, path_1.join)("apps", appDir, ".zite");
55
+ const outDir = (0, path_1.join)(appDir.path, ".zite");
63
56
  (0, fs_2.mkdirSync)(outDir, { recursive: true });
64
57
  // user.ts and auth.ts no longer generated — User type is fixed in zitejs/auth
65
58
  // Email integration: generate the Email client at .zite/integrations/email.ts.
@@ -74,7 +67,7 @@ function regenerateAppTypedWrappers(appDir) {
74
67
  (0, fs_2.writeFileSync)((0, path_1.join)(outDir, "backend.ts"), (0, lib_js_1.generateBackendWrapperTs)(getDeclaredEnvVarNames(appDir)));
75
68
  }
76
69
  function regenerateAppApiTs(appDir) {
77
- const apiDir = (0, path_1.join)("apps", appDir, "src", "api");
70
+ const apiDir = (0, path_1.join)(appDir.path, "src", "api");
78
71
  if (!(0, fs_2.existsSync)(apiDir))
79
72
  return;
80
73
  // Sorted because `readdir` order is unspecified — it is whatever the
@@ -94,14 +87,14 @@ function regenerateAppApiTs(appDir) {
94
87
  }));
95
88
  const content = (0, lib_js_1.generateApiTs)(endpointFiles);
96
89
  if (content) {
97
- const outDir = (0, path_1.join)("apps", appDir, ".zite");
90
+ const outDir = (0, path_1.join)(appDir.path, ".zite");
98
91
  (0, fs_2.mkdirSync)(outDir, { recursive: true });
99
92
  (0, fs_2.writeFileSync)((0, path_1.join)(outDir, "api.ts"), content);
100
- console.log(`Updated apps/${appDir}/.zite/api.ts`);
93
+ console.log(`Updated ${appDir.path}/.zite/api.ts`);
101
94
  }
102
95
  }
103
96
  function regenerateAppAirtableSdk(appDir) {
104
- const lockPath = (0, path_1.join)("apps", appDir, "zite.lock");
97
+ const lockPath = (0, path_1.join)(appDir.path, "zite.lock");
105
98
  console.log(`[airtable-sdk] Checking ${lockPath} exists: ${(0, fs_2.existsSync)(lockPath)}`);
106
99
  if (!(0, fs_2.existsSync)(lockPath))
107
100
  return;
@@ -122,15 +115,15 @@ function regenerateAppAirtableSdk(appDir) {
122
115
  };
123
116
  const content = (0, lib_js_1.generateAirtableTs)(airtableLock);
124
117
  if (content) {
125
- const outDir = (0, path_1.join)("apps", appDir, ".zite", "integrations");
118
+ const outDir = (0, path_1.join)(appDir.path, ".zite", "integrations");
126
119
  (0, fs_2.mkdirSync)(outDir, { recursive: true });
127
120
  (0, fs_2.writeFileSync)((0, path_1.join)(outDir, "airtable.ts"), content);
128
- console.log(`Updated apps/${appDir}/.zite/integrations/airtable.ts`);
121
+ console.log(`Updated ${appDir.path}/.zite/integrations/airtable.ts`);
129
122
  }
130
123
  }
131
124
  }
132
125
  catch (err) {
133
- console.error(`[airtable-sdk] Error processing lock for ${appDir}:`, err);
126
+ console.error(`[airtable-sdk] Error processing lock for ${appDir.path}:`, err);
134
127
  }
135
128
  }
136
129
  async function runGenerate() {
@@ -154,8 +147,8 @@ async function runGenerate() {
154
147
  catch (err) {
155
148
  console.warn("DB SDK generation failed:", err instanceof Error ? err.message : err);
156
149
  }
157
- // 2. Find all apps and regenerate their .zite/ files
158
- const appDirs = findAppDirs();
150
+ // 2. Find every app and automation and regenerate their .zite/ files
151
+ const appDirs = (0, sourceRoots_js_1.listSourceDirs)();
159
152
  for (const app of appDirs) {
160
153
  regenerateAppApiTs(app);
161
154
  regenerateAppTypedWrappers(app);
@@ -175,33 +168,33 @@ async function runDev() {
175
168
  console.warn("Initial sync failed (continuing with watcher):", err instanceof Error ? err.message : err);
176
169
  }
177
170
  console.log("");
178
- // 2. Find all apps and regenerate their .zite/ files
179
- const appDirs = findAppDirs();
171
+ // 2. Find every app and automation and regenerate their .zite/ files
172
+ const appDirs = (0, sourceRoots_js_1.listSourceDirs)();
180
173
  for (const app of appDirs) {
181
174
  regenerateAppApiTs(app);
182
175
  regenerateAppTypedWrappers(app);
183
176
  }
184
- // 3. Watch each app's src/api/ for endpoint changes
177
+ // 3. Watch each dir's src/api/ for endpoint changes
185
178
  let watchingAny = false;
186
179
  for (const app of appDirs) {
187
- const apiDir = (0, path_1.join)("apps", app, "src", "api");
180
+ const apiDir = (0, path_1.join)(app.path, "src", "api");
188
181
  if (!(0, fs_2.existsSync)(apiDir))
189
182
  continue;
190
183
  watchingAny = true;
191
- console.log(`Watching apps/${app}/src/api/ for endpoint changes...`);
184
+ console.log(`Watching ${app.path}/src/api/ for endpoint changes...`);
192
185
  (0, fs_1.watch)(apiDir, { recursive: true }, (_event, filename) => {
193
186
  if (!filename)
194
187
  return;
195
188
  if (!filename.endsWith(".ts") && !filename.endsWith(".js"))
196
189
  return;
197
- debounce(app, () => {
198
- console.log(`Endpoint changed in ${app}: ${filename}`);
190
+ debounce(app.path, () => {
191
+ console.log(`Endpoint changed in ${app.path}: ${filename}`);
199
192
  regenerateAppApiTs(app);
200
193
  }, 200);
201
194
  });
202
195
  }
203
196
  if (!watchingAny) {
204
- console.log("No apps with src/api/ found.");
197
+ console.log("No apps or automations with src/api/ found.");
205
198
  }
206
199
  // 4. Watch zite.schema.json for schema drift
207
200
  if ((0, fs_2.existsSync)("zite.schema.json")) {
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The repo-root directories a project's own source lives under: `apps/` for
3
+ * apps and `automations/` for automations (endpoints with no frontend). The
4
+ * backend keeps the same list; a dir name is unique across both roots, so a
5
+ * bare name still identifies one dir.
6
+ */
7
+ export declare const SOURCE_ROOTS: readonly ["apps", "automations"];
8
+ export type SourceRoot = (typeof SOURCE_ROOTS)[number];
9
+ export interface SourceDir {
10
+ root: SourceRoot;
11
+ /** The bare directory name. */
12
+ dir: string;
13
+ /** `<root>/<dir>`, relative to the repo root. */
14
+ path: string;
15
+ }
16
+ /** Every source dir under every root, in root order then name order. */
17
+ export declare function listSourceDirs(cwd?: string): SourceDir[];
18
+ /** The source dir with this bare name, under whichever root holds it. */
19
+ export declare function findSourceDir(name: string, cwd?: string): SourceDir | undefined;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SOURCE_ROOTS = void 0;
4
+ exports.listSourceDirs = listSourceDirs;
5
+ exports.findSourceDir = findSourceDir;
6
+ const fs_1 = require("fs");
7
+ const path_1 = require("path");
8
+ /**
9
+ * The repo-root directories a project's own source lives under: `apps/` for
10
+ * apps and `automations/` for automations (endpoints with no frontend). The
11
+ * backend keeps the same list; a dir name is unique across both roots, so a
12
+ * bare name still identifies one dir.
13
+ */
14
+ exports.SOURCE_ROOTS = ["apps", "automations"];
15
+ /** Every source dir under every root, in root order then name order. */
16
+ function listSourceDirs(cwd = ".") {
17
+ const dirs = [];
18
+ for (const root of exports.SOURCE_ROOTS) {
19
+ const rootPath = (0, path_1.join)(cwd, root);
20
+ if (!(0, fs_1.existsSync)(rootPath))
21
+ continue;
22
+ for (const entry of (0, fs_1.readdirSync)(rootPath, { withFileTypes: true })) {
23
+ if (!entry.isDirectory())
24
+ continue;
25
+ dirs.push({ root, dir: entry.name, path: (0, path_1.join)(root, entry.name) });
26
+ }
27
+ }
28
+ return dirs;
29
+ }
30
+ /** The source dir with this bare name, under whichever root holds it. */
31
+ function findSourceDir(name, cwd = ".") {
32
+ return listSourceDirs(cwd).find((d) => d.dir === name);
33
+ }
@@ -1230,10 +1230,10 @@ function generateBackendWrapperTs(envVarNames = []) {
1230
1230
  "// Auto-generated type-narrowing wrapper. Do not edit manually.",
1231
1231
  "// Re-exports createEndpoint with context.user typed to the app User.",
1232
1232
  "",
1233
- "import type { ZiteRequestContext as _ZiteRequestContext, ZiteScheduledContext, ZiteErrorCode, ZiteSchedule, ZiteStreamInterface, ZiteWebhook } from 'zitejs/backend/base';",
1233
+ "import type { ZiteRequestContext as _ZiteRequestContext, ZiteScheduledContext, ZiteErrorCode, ZiteSchedule, ZiteStreamInterface, ZiteTrigger, ZiteWebhook } from 'zitejs/backend/base';",
1234
1234
  "import type { User } from 'zitejs/auth';",
1235
1235
  "",
1236
- "export type { ZiteErrorCode, ZiteSchedule, ZiteScheduledContext, ZiteStreamInterface, ZiteWebhook };",
1236
+ "export type { ZiteErrorCode, ZiteSchedule, ZiteScheduledContext, ZiteStreamInterface, ZiteTrigger, ZiteWebhook };",
1237
1237
  // The pre-monorepo SDK put this in scope for every endpoint, so migrated
1238
1238
  // code can name it. `createEndpoint` infers the same thing without it.
1239
1239
  "export type InferSchemaType<T> = T extends { _output: infer U } ? U : T;",
@@ -1308,7 +1308,7 @@ function generateBackendWrapperTs(envVarNames = []) {
1308
1308
  // TStream mirrors zitejs/backend/base. Without it `stream: true` endpoints
1309
1309
  // get no `stream` argument here — and this wrapper, not the base module, is
1310
1310
  // what `zitejs/backend` resolves to in every app.
1311
- "export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream extends boolean = false, TSchedule extends ZiteSchedule | undefined = undefined, TWebhook extends ZiteWebhook | undefined = undefined, TRawInput = TInput> {",
1311
+ "export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream extends boolean = false, TSchedule extends ZiteSchedule | undefined = undefined, TWebhook extends ZiteWebhook | undefined = undefined, TRawInput = TInput, TTrigger extends ZiteTrigger | undefined = undefined> {",
1312
1312
  " description?: string;",
1313
1313
  " inputSchema?: SchemaLike<TInput, TRawInput>;",
1314
1314
  // Apps compile against this copy, not the base module's.
@@ -1320,17 +1320,19 @@ function generateBackendWrapperTs(envVarNames = []) {
1320
1320
  " schedule?: TSchedule;",
1321
1321
  " /** When set, an inbound webhook can also trigger this endpoint. Like `schedule`, it widens `context` — a webhook fire has no session. */",
1322
1322
  " webhook?: TWebhook;",
1323
+ " /** When set, a workspace connection's events (registered by `src/triggers/<endpointId>/subscribe.ts`) or a poll also fire this endpoint. Like `webhook`, it widens `context` — a trigger fire has no session. */",
1324
+ " trigger?: TTrigger;",
1323
1325
  " execute: (",
1324
1326
  " params: {",
1325
1327
  " input: TInput;",
1326
- " context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : TWebhook extends ZiteWebhook ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;",
1328
+ " context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : TWebhook extends ZiteWebhook ? ZiteRequestContext | ZiteScheduledContext : TTrigger extends ZiteTrigger ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;",
1327
1329
  " } & (TStream extends true ? { stream: ZiteStreamInterface } : {}),",
1328
1330
  " ) => Promise<TOutput> | TOutput;",
1329
1331
  "}",
1330
1332
  "",
1331
- "export function createEndpoint<TInput = unknown, TOutput = unknown, TStream extends boolean = false, TSchedule extends ZiteSchedule | undefined = undefined, TWebhook extends ZiteWebhook | undefined = undefined, TRawInput = TInput>(",
1332
- " config: EndpointConfig<TInput, TOutput, TStream, TSchedule, TWebhook, TRawInput>,",
1333
- "): EndpointConfig<TInput, TOutput, TStream, TSchedule, TWebhook, TRawInput> {",
1333
+ "export function createEndpoint<TInput = unknown, TOutput = unknown, TStream extends boolean = false, TSchedule extends ZiteSchedule | undefined = undefined, TWebhook extends ZiteWebhook | undefined = undefined, TRawInput = TInput, TTrigger extends ZiteTrigger | undefined = undefined>(",
1334
+ " config: EndpointConfig<TInput, TOutput, TStream, TSchedule, TWebhook, TRawInput, TTrigger>,",
1335
+ "): EndpointConfig<TInput, TOutput, TStream, TSchedule, TWebhook, TRawInput, TTrigger> {",
1334
1336
  " return config;",
1335
1337
  "}",
1336
1338
  "",
@@ -381,6 +381,11 @@ const duplicateIdentifierErrorsIn = (source) => {
381
381
  (0, vitest_1.it)('exports createEndpoint', () => {
382
382
  (0, vitest_1.expect)(output).toContain('createEndpoint');
383
383
  });
384
+ (0, vitest_1.it)('types the trigger field like schedule and webhook', () => {
385
+ (0, vitest_1.expect)(output).toContain('trigger?: TTrigger;');
386
+ (0, vitest_1.expect)(output).toContain('TTrigger extends ZiteTrigger | undefined = undefined');
387
+ (0, vitest_1.expect)(output).toContain('ZiteTrigger, ZiteWebhook }');
388
+ });
384
389
  (0, vitest_1.it)('exports ZiteError', () => {
385
390
  (0, vitest_1.expect)(output).toContain('ZiteError');
386
391
  });
@@ -113,6 +113,25 @@ async function performUpload(data, filename) {
113
113
  if (!putRes.ok) {
114
114
  throw new FileUploadError('Upload failed');
115
115
  }
116
+ // Gateways that predate the storage ledger omit the token. The file is
117
+ // already at fileUrl, so those uploads still succeed.
118
+ if (session.completionToken) {
119
+ const completeRes = await fetch((0, config_js_1.getApiUrl)() +
120
+ '/v1/zite/public/' +
121
+ flowId +
122
+ '/complete-upload?mode=' +
123
+ mode, {
124
+ method: 'POST',
125
+ headers: {
126
+ 'Content-Type': 'application/json',
127
+ ...authHeaders(),
128
+ },
129
+ body: JSON.stringify({ completionToken: session.completionToken }),
130
+ });
131
+ if (!completeRes.ok) {
132
+ throw new FileUploadError(await readErrorMessage(completeRes, 'Upload failed'));
133
+ }
134
+ }
116
135
  return session.fileUrl;
117
136
  }
118
137
  async function uploadFile({ data, filename, }) {