zitejs 0.9.119 → 0.9.121

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 (57) 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.d.ts +1 -0
  5. package/dist/cjs/auth/useAuth.test.js +82 -0
  6. package/dist/cjs/backend/index.d.ts +30 -3
  7. package/dist/cjs/backend/index.js +2 -2
  8. package/dist/cjs/bundle/index.d.ts +1 -1
  9. package/dist/cjs/bundle/index.js +6 -2
  10. package/dist/cjs/check/index.js +5 -13
  11. package/dist/cjs/cli.js +2 -2
  12. package/dist/cjs/dev/index.js +31 -44
  13. package/dist/cjs/meta/index.d.ts +14 -0
  14. package/dist/cjs/meta/index.js +12 -0
  15. package/dist/cjs/notifications/index.d.ts +25 -0
  16. package/dist/cjs/notifications/index.js +12 -0
  17. package/dist/cjs/sourceRoots.d.ts +19 -0
  18. package/dist/cjs/sourceRoots.js +33 -0
  19. package/dist/cjs/sync/lib.d.ts +6 -0
  20. package/dist/cjs/sync/lib.js +13 -0
  21. package/dist/cjs/sync/lib.test.js +28 -0
  22. package/dist/cjs/upload/index.js +19 -0
  23. package/dist/cjs/upload/index.test.js +30 -3
  24. package/dist/cjs/vite/domTagNames.d.ts +1 -0
  25. package/dist/cjs/vite/domTagNames.js +257 -0
  26. package/dist/cjs/vite/index.js +20 -11
  27. package/dist/cjs/vite/index.test.d.ts +1 -0
  28. package/dist/cjs/vite/index.test.js +53 -0
  29. package/dist/esm/auth/config.d.ts +3 -0
  30. package/dist/esm/auth/config.js +6 -0
  31. package/dist/esm/auth/index.js +28 -2
  32. package/dist/esm/auth/useAuth.test.d.ts +1 -0
  33. package/dist/esm/auth/useAuth.test.js +80 -0
  34. package/dist/esm/backend/index.d.ts +30 -3
  35. package/dist/esm/backend/index.js +2 -2
  36. package/dist/esm/bundle/index.d.ts +1 -1
  37. package/dist/esm/bundle/index.js +6 -2
  38. package/dist/esm/check/index.js +6 -14
  39. package/dist/esm/cli.js +2 -2
  40. package/dist/esm/dev/index.js +32 -45
  41. package/dist/esm/meta/index.d.ts +14 -0
  42. package/dist/esm/meta/index.js +8 -0
  43. package/dist/esm/notifications/index.d.ts +25 -0
  44. package/dist/esm/notifications/index.js +8 -0
  45. package/dist/esm/sourceRoots.d.ts +19 -0
  46. package/dist/esm/sourceRoots.js +28 -0
  47. package/dist/esm/sync/lib.d.ts +6 -0
  48. package/dist/esm/sync/lib.js +12 -0
  49. package/dist/esm/sync/lib.test.js +29 -1
  50. package/dist/esm/upload/index.js +19 -0
  51. package/dist/esm/upload/index.test.js +30 -3
  52. package/dist/esm/vite/domTagNames.d.ts +1 -0
  53. package/dist/esm/vite/domTagNames.js +254 -0
  54. package/dist/esm/vite/index.js +20 -11
  55. package/dist/esm/vite/index.test.d.ts +1 -0
  56. package/dist/esm/vite/index.test.js +48 -0
  57. package/package.json +1 -1
@@ -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>;
@@ -27,7 +27,7 @@ export class ZiteError extends Error {
27
27
  }
28
28
  }
29
29
  /** Injected by the runner on a platform-fired run — see `isPlatformTriggeredInput`. */
30
- const PLATFORM_TRIGGER_KEYS = ["__cron", "__webhook"];
30
+ const PLATFORM_TRIGGER_KEYS = ["__cron", "__webhook", "__poll"];
31
31
  function isPlatformTriggeredInput(input) {
32
32
  if (typeof input !== "object" || input === null)
33
33
  return false;
@@ -55,7 +55,7 @@ function describeIssues(error) {
55
55
  */
56
56
  export function createEndpoint(config) {
57
57
  const { inputSchema, execute } = config;
58
- const firesWithoutARequest = Boolean(config.schedule || config.webhook);
58
+ const firesWithoutARequest = Boolean(config.schedule || config.webhook || config.trigger);
59
59
  // An `inputSchema` that isn't a validator stays inert, as it was.
60
60
  if (!inputSchema || typeof inputSchema.parse !== "function")
61
61
  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
@@ -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
@@ -24,6 +24,7 @@ import * as path from 'path';
24
24
  import * as fs from 'fs';
25
25
  import { parse } from '@babel/parser';
26
26
  import { builtinModules } from 'node:module';
27
+ import { findSourceDir } from '../sourceRoots.js';
27
28
  // workerd's `nodejs_compat` does not provide these. Left external they bundle
28
29
  // fine and then kill the worker at load time with `No such module`; excluded,
29
30
  // esbuild fails the one endpoint with a resolvable error instead. Pinned by
@@ -578,7 +579,10 @@ export async function runBundle() {
578
579
  const appFlag = args.indexOf('--app');
579
580
  let baseDir = process.cwd();
580
581
  if (appFlag !== -1 && args[appFlag + 1]) {
581
- baseDir = path.resolve(baseDir, 'apps', args[appFlag + 1]);
582
+ const name = args[appFlag + 1];
583
+ // A dir that exists under no root still resolves under `apps/`, so the
584
+ // error names the path a caller expects rather than "not found".
585
+ baseDir = path.resolve(baseDir, findSourceDir(name, baseDir)?.path ?? path.join('apps', name));
582
586
  }
583
587
  // If explicit endpoint names passed, use those; otherwise find all.
584
588
  // Skipping `--app`'s VALUE as well as the flag: it is a positional arg, so
@@ -1,14 +1,7 @@
1
1
  import { execFileSync } from 'child_process';
2
- import { existsSync, readdirSync } from 'fs';
2
+ import { existsSync } from 'fs';
3
3
  import { join } from 'path';
4
- function findAppDirs() {
5
- const appsDir = 'apps';
6
- if (!existsSync(appsDir))
7
- return [];
8
- return readdirSync(appsDir, { withFileTypes: true })
9
- .filter(d => d.isDirectory())
10
- .map(d => d.name);
11
- }
4
+ import { listSourceDirs } from '../sourceRoots.js';
12
5
  /**
13
6
  * An argument array, not a command string, so no shell is involved.
14
7
  *
@@ -63,15 +56,14 @@ function bundleFailures(output) {
63
56
  return failures;
64
57
  }
65
58
  export async function runCheck() {
66
- const appDirs = findAppDirs();
59
+ const appDirs = listSourceDirs();
67
60
  if (appDirs.length === 0) {
68
- console.error('No apps found in apps/ directory.');
61
+ console.error('No apps or automations found under apps/ or automations/.');
69
62
  process.exit(1);
70
63
  }
71
64
  let allPassed = true;
72
- for (const app of appDirs) {
73
- const appPath = join('apps', app);
74
- console.log(`\n── ${app} ──`);
65
+ for (const { dir: app, path: appPath } of appDirs) {
66
+ console.log(`\n── ${appPath} ──`);
75
67
  // Only tsconfig.app.json compiles anything. The app's tsconfig.json is a
76
68
  // solution-style config — `"files": []` plus a reference — so
77
69
  // `tsc --noEmit -p tsconfig.json` type-checks zero files and exits 0. It
package/dist/esm/cli.js CHANGED
@@ -38,8 +38,8 @@ async function main() {
38
38
  console.error('Commands:');
39
39
  console.error(' sync Generate .zite/db.ts from database schema (single-app)');
40
40
  console.error(' dev Run sync then watch for changes (like npx convex dev)');
41
- console.error(' generate One-shot sync + regenerate all apps .zite/ files (monorepo)');
42
- console.error(' check Run tsc --noEmit and vite build for all apps');
41
+ console.error(' generate Regenerate every app and automation .zite/ (monorepo)');
42
+ console.error(' check Run tsc --noEmit (and vite build where there is one) for every app and automation');
43
43
  console.error(' bundle Bundle src/api/*.ts endpoints for cloudflare-lambda');
44
44
  process.exit(1);
45
45
  }
@@ -1,8 +1,9 @@
1
1
  import { watch } from "fs";
2
2
  import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, } from "fs";
3
3
  import { join } from "path";
4
+ import { listSourceDirs } from "../sourceRoots.js";
4
5
  import { runSync } from "../sync/index.js";
5
- import { generateDbTs, generateApiTs, generateBackendWrapperTs, generateAirtableTs, generateEmailSdk, } from "../sync/lib.js";
6
+ import { generateDbTs, generateApiTs, generateBackendWrapperTs, generateAirtableTs, findEmailIntegrationId, generateEmailSdk, } from "../sync/lib.js";
6
7
  const debounceTimers = new Map();
7
8
  function debounce(key, fn, ms) {
8
9
  const existing = debounceTimers.get(key);
@@ -10,17 +11,9 @@ function debounce(key, fn, ms) {
10
11
  clearTimeout(existing);
11
12
  debounceTimers.set(key, setTimeout(fn, ms));
12
13
  }
13
- function findAppDirs() {
14
- const appsDir = "apps";
15
- if (!existsSync(appsDir))
16
- return [];
17
- return readdirSync(appsDir, { withFileTypes: true })
18
- .filter((d) => d.isDirectory())
19
- .map((d) => d.name);
20
- }
21
14
  function getFlowId(appDir) {
22
15
  try {
23
- const configPath = join("apps", appDir, "zite.config.json");
16
+ const configPath = join(appDir.path, "zite.config.json");
24
17
  if (existsSync(configPath)) {
25
18
  const config = JSON.parse(readFileSync(configPath, "utf-8"));
26
19
  return config.id;
@@ -29,28 +22,22 @@ function getFlowId(appDir) {
29
22
  catch { }
30
23
  return undefined;
31
24
  }
32
- /**
33
- * Find the connected email integration's key in an app's zite.config.json,
34
- * if any. The key is the integrationId used by the runtime SDK bridge.
35
- */
36
- function getEmailIntegrationId(appDir) {
25
+ const readJsonFile = (path) => {
37
26
  try {
38
- const configPath = join("apps", appDir, "zite.config.json");
39
- if (!existsSync(configPath))
40
- return undefined;
41
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
42
- const integrations = config.integrations ?? {};
43
- for (const [id, int] of Object.entries(integrations)) {
44
- if (int?.type === "email")
45
- return id;
46
- }
27
+ return existsSync(path)
28
+ ? JSON.parse(readFileSync(path, "utf-8"))
29
+ : undefined;
47
30
  }
48
- catch { }
49
- return undefined;
31
+ catch {
32
+ return undefined;
33
+ }
34
+ };
35
+ function getEmailIntegrationId(appDir) {
36
+ return findEmailIntegrationId(readJsonFile(join(appDir.path, "zite.config.json")), readJsonFile("zite.config.json"));
50
37
  }
51
38
  function getDeclaredEnvVarNames(appDir) {
52
39
  try {
53
- const configPath = join("apps", appDir, "zite.config.json");
40
+ const configPath = join(appDir.path, "zite.config.json");
54
41
  if (!existsSync(configPath))
55
42
  return [];
56
43
  const config = JSON.parse(readFileSync(configPath, "utf-8"));
@@ -61,7 +48,7 @@ function getDeclaredEnvVarNames(appDir) {
61
48
  }
62
49
  }
63
50
  function regenerateAppTypedWrappers(appDir) {
64
- const outDir = join("apps", appDir, ".zite");
51
+ const outDir = join(appDir.path, ".zite");
65
52
  mkdirSync(outDir, { recursive: true });
66
53
  // user.ts and auth.ts no longer generated — User type is fixed in zitejs/auth
67
54
  // Email integration: generate the Email client at .zite/integrations/email.ts.
@@ -76,7 +63,7 @@ function regenerateAppTypedWrappers(appDir) {
76
63
  writeFileSync(join(outDir, "backend.ts"), generateBackendWrapperTs(getDeclaredEnvVarNames(appDir)));
77
64
  }
78
65
  function regenerateAppApiTs(appDir) {
79
- const apiDir = join("apps", appDir, "src", "api");
66
+ const apiDir = join(appDir.path, "src", "api");
80
67
  if (!existsSync(apiDir))
81
68
  return;
82
69
  // Sorted because `readdir` order is unspecified — it is whatever the
@@ -96,14 +83,14 @@ function regenerateAppApiTs(appDir) {
96
83
  }));
97
84
  const content = generateApiTs(endpointFiles);
98
85
  if (content) {
99
- const outDir = join("apps", appDir, ".zite");
86
+ const outDir = join(appDir.path, ".zite");
100
87
  mkdirSync(outDir, { recursive: true });
101
88
  writeFileSync(join(outDir, "api.ts"), content);
102
- console.log(`Updated apps/${appDir}/.zite/api.ts`);
89
+ console.log(`Updated ${appDir.path}/.zite/api.ts`);
103
90
  }
104
91
  }
105
92
  function regenerateAppAirtableSdk(appDir) {
106
- const lockPath = join("apps", appDir, "zite.lock");
93
+ const lockPath = join(appDir.path, "zite.lock");
107
94
  console.log(`[airtable-sdk] Checking ${lockPath} exists: ${existsSync(lockPath)}`);
108
95
  if (!existsSync(lockPath))
109
96
  return;
@@ -124,15 +111,15 @@ function regenerateAppAirtableSdk(appDir) {
124
111
  };
125
112
  const content = generateAirtableTs(airtableLock);
126
113
  if (content) {
127
- const outDir = join("apps", appDir, ".zite", "integrations");
114
+ const outDir = join(appDir.path, ".zite", "integrations");
128
115
  mkdirSync(outDir, { recursive: true });
129
116
  writeFileSync(join(outDir, "airtable.ts"), content);
130
- console.log(`Updated apps/${appDir}/.zite/integrations/airtable.ts`);
117
+ console.log(`Updated ${appDir.path}/.zite/integrations/airtable.ts`);
131
118
  }
132
119
  }
133
120
  }
134
121
  catch (err) {
135
- console.error(`[airtable-sdk] Error processing lock for ${appDir}:`, err);
122
+ console.error(`[airtable-sdk] Error processing lock for ${appDir.path}:`, err);
136
123
  }
137
124
  }
138
125
  export async function runGenerate() {
@@ -156,8 +143,8 @@ export async function runGenerate() {
156
143
  catch (err) {
157
144
  console.warn("DB SDK generation failed:", err instanceof Error ? err.message : err);
158
145
  }
159
- // 2. Find all apps and regenerate their .zite/ files
160
- const appDirs = findAppDirs();
146
+ // 2. Find every app and automation and regenerate their .zite/ files
147
+ const appDirs = listSourceDirs();
161
148
  for (const app of appDirs) {
162
149
  regenerateAppApiTs(app);
163
150
  regenerateAppTypedWrappers(app);
@@ -177,33 +164,33 @@ export async function runDev() {
177
164
  console.warn("Initial sync failed (continuing with watcher):", err instanceof Error ? err.message : err);
178
165
  }
179
166
  console.log("");
180
- // 2. Find all apps and regenerate their .zite/ files
181
- const appDirs = findAppDirs();
167
+ // 2. Find every app and automation and regenerate their .zite/ files
168
+ const appDirs = listSourceDirs();
182
169
  for (const app of appDirs) {
183
170
  regenerateAppApiTs(app);
184
171
  regenerateAppTypedWrappers(app);
185
172
  }
186
- // 3. Watch each app's src/api/ for endpoint changes
173
+ // 3. Watch each dir's src/api/ for endpoint changes
187
174
  let watchingAny = false;
188
175
  for (const app of appDirs) {
189
- const apiDir = join("apps", app, "src", "api");
176
+ const apiDir = join(app.path, "src", "api");
190
177
  if (!existsSync(apiDir))
191
178
  continue;
192
179
  watchingAny = true;
193
- console.log(`Watching apps/${app}/src/api/ for endpoint changes...`);
180
+ console.log(`Watching ${app.path}/src/api/ for endpoint changes...`);
194
181
  watch(apiDir, { recursive: true }, (_event, filename) => {
195
182
  if (!filename)
196
183
  return;
197
184
  if (!filename.endsWith(".ts") && !filename.endsWith(".js"))
198
185
  return;
199
- debounce(app, () => {
200
- console.log(`Endpoint changed in ${app}: ${filename}`);
186
+ debounce(app.path, () => {
187
+ console.log(`Endpoint changed in ${app.path}: ${filename}`);
201
188
  regenerateAppApiTs(app);
202
189
  }, 200);
203
190
  });
204
191
  }
205
192
  if (!watchingAny) {
206
- console.log("No apps with src/api/ found.");
193
+ console.log("No apps or automations with src/api/ found.");
207
194
  }
208
195
  // 4. Watch zite.schema.json for schema drift
209
196
  if (existsSync("zite.schema.json")) {
@@ -0,0 +1,14 @@
1
+ export type ZiteProjectUser = {
2
+ uuid: string;
3
+ firstName: string | null;
4
+ lastName: string | null;
5
+ email: string;
6
+ profilePictureUrl: string | null;
7
+ };
8
+ export type MetaListUsersResult = {
9
+ users: ZiteProjectUser[];
10
+ };
11
+ export declare class ZiteMeta {
12
+ static listUsers(): Promise<MetaListUsersResult>;
13
+ }
14
+ export declare const Meta: typeof ZiteMeta;
@@ -0,0 +1,8 @@
1
+ import { getSdkCall } from '../internal/sdkCall.js';
2
+ const META_SDK_INTEGRATION_ID = '__meta__';
3
+ export class ZiteMeta {
4
+ static listUsers() {
5
+ return getSdkCall()(META_SDK_INTEGRATION_ID, 'ZiteMeta', 'listUsers', {});
6
+ }
7
+ }
8
+ export const Meta = ZiteMeta;
@@ -0,0 +1,25 @@
1
+ export interface NotificationLink {
2
+ path?: string;
3
+ params?: Record<string, string>;
4
+ }
5
+ export type NotificationsCreateParams = {
6
+ recipients: string[];
7
+ title: string;
8
+ body?: string;
9
+ link?: NotificationLink;
10
+ path?: string;
11
+ params?: Record<string, string>;
12
+ payload?: Record<string, unknown>;
13
+ idempotencyKey?: string;
14
+ };
15
+ export type NotificationsCreateResult = {
16
+ created: number;
17
+ } | {
18
+ created: 0;
19
+ preview: true;
20
+ wouldCreate: number;
21
+ };
22
+ export declare class ZiteNotifications {
23
+ static create(params: NotificationsCreateParams): Promise<NotificationsCreateResult>;
24
+ }
25
+ export declare const Notifications: typeof ZiteNotifications;
@@ -0,0 +1,8 @@
1
+ import { getSdkCall } from '../internal/sdkCall.js';
2
+ const NOTIFICATIONS_SDK_INTEGRATION_ID = '__notifications__';
3
+ export class ZiteNotifications {
4
+ static create(params) {
5
+ return getSdkCall()(NOTIFICATIONS_SDK_INTEGRATION_ID, 'ZiteNotifications', 'create', params);
6
+ }
7
+ }
8
+ export const Notifications = ZiteNotifications;
@@ -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,28 @@
1
+ import { existsSync, readdirSync } from "fs";
2
+ import { join } from "path";
3
+ /**
4
+ * The repo-root directories a project's own source lives under: `apps/` for
5
+ * apps and `automations/` for automations (endpoints with no frontend). The
6
+ * backend keeps the same list; a dir name is unique across both roots, so a
7
+ * bare name still identifies one dir.
8
+ */
9
+ export const SOURCE_ROOTS = ["apps", "automations"];
10
+ /** Every source dir under every root, in root order then name order. */
11
+ export function listSourceDirs(cwd = ".") {
12
+ const dirs = [];
13
+ for (const root of SOURCE_ROOTS) {
14
+ const rootPath = join(cwd, root);
15
+ if (!existsSync(rootPath))
16
+ continue;
17
+ for (const entry of readdirSync(rootPath, { withFileTypes: true })) {
18
+ if (!entry.isDirectory())
19
+ continue;
20
+ dirs.push({ root, dir: entry.name, path: join(root, entry.name) });
21
+ }
22
+ }
23
+ return dirs;
24
+ }
25
+ /** The source dir with this bare name, under whichever root holds it. */
26
+ export function findSourceDir(name, cwd = ".") {
27
+ return listSourceDirs(cwd).find((d) => d.dir === name);
28
+ }
@@ -82,6 +82,12 @@ export declare function normalizeAirtableLockNames(lock: AirtableLock): {
82
82
  };
83
83
  export declare function generateAirtableTs(inputLock: AirtableLock): string | null;
84
84
  export declare function generateBackendWrapperTs(envVarNames?: string[]): string;
85
+ /**
86
+ * The key of the email integration an app sends through, which is the
87
+ * integrationId the runtime SDK bridge dispatches on. The app's own entry wins;
88
+ * otherwise it is the workspace email the app has `integrationSettings` for.
89
+ */
90
+ export declare function findEmailIntegrationId(appConfig: unknown, workspaceConfig: unknown): string | undefined;
85
91
  /**
86
92
  * Generate `.zite/integrations/email.ts` — the `Email` client for an app with
87
93
  * an email integration connected. Mirrors the airtable SDK generation: a thin
@@ -1333,6 +1333,18 @@ export function generateBackendWrapperTs(envVarNames = []) {
1333
1333
  "",
1334
1334
  ].join("\n");
1335
1335
  }
1336
+ /**
1337
+ * The key of the email integration an app sends through, which is the
1338
+ * integrationId the runtime SDK bridge dispatches on. The app's own entry wins;
1339
+ * otherwise it is the workspace email the app has `integrationSettings` for.
1340
+ */
1341
+ export function findEmailIntegrationId(appConfig, workspaceConfig) {
1342
+ const app = (appConfig ?? {});
1343
+ const workspace = (workspaceConfig ?? {});
1344
+ const isEmail = (config, id) => config.integrations?.[id]?.type === "email";
1345
+ return (Object.keys(app.integrations ?? {}).find((id) => isEmail(app, id)) ??
1346
+ Object.keys(app.integrationSettings ?? {}).find((id) => isEmail(workspace, id)));
1347
+ }
1336
1348
  /**
1337
1349
  * Generate `.zite/integrations/email.ts` — the `Email` client for an app with
1338
1350
  * an email integration connected. Mirrors the airtable SDK generation: a thin
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import ts from 'typescript';
3
- import { generateAirtableTs, generateApiTs, generateBackendWrapperTs, generateDbTs, generateEmailSdk, } from './lib.js';
3
+ import { findEmailIntegrationId, generateAirtableTs, generateApiTs, generateBackendWrapperTs, generateDbTs, generateEmailSdk, } from './lib.js';
4
4
  /**
5
5
  * Syntax errors in the emitted source. `.zite/db.ts` sits at the repo root and
6
6
  * every app imports it, so anything unparseable here fails typecheck for the
@@ -380,3 +380,31 @@ describe('generateBackendWrapperTs', () => {
380
380
  expect(output).toContain('ZiteError');
381
381
  });
382
382
  });
383
+ describe('findEmailIntegrationId', () => {
384
+ const workspace = {
385
+ integrations: {
386
+ slack: { type: 'slack' },
387
+ 'team-email': { type: 'email' },
388
+ },
389
+ };
390
+ it('finds the app\'s own email entry', () => {
391
+ expect(findEmailIntegrationId({ integrations: { mail: { type: 'email' } } }, workspace)).toBe('mail');
392
+ });
393
+ it('finds the workspace email the app has settings for', () => {
394
+ expect(findEmailIntegrationId({ integrationSettings: { slack: {}, 'team-email': { fromName: 'Ops' } } }, workspace)).toBe('team-email');
395
+ });
396
+ // An app that never opted in generates no client, so `zitejs/email` stays
397
+ // unresolved there instead of sending as a sender it never chose.
398
+ it('ignores a workspace email the app has no settings for', () => {
399
+ expect(findEmailIntegrationId({}, workspace)).toBeUndefined();
400
+ });
401
+ it('prefers the app\'s own entry over a workspace one', () => {
402
+ expect(findEmailIntegrationId({
403
+ integrations: { mail: { type: 'email' } },
404
+ integrationSettings: { 'team-email': {} },
405
+ }, workspace)).toBe('mail');
406
+ });
407
+ it('tolerates missing configs', () => {
408
+ expect(findEmailIntegrationId(undefined, undefined)).toBeUndefined();
409
+ });
410
+ });
@@ -106,6 +106,25 @@ async function performUpload(data, filename) {
106
106
  if (!putRes.ok) {
107
107
  throw new FileUploadError('Upload failed');
108
108
  }
109
+ // Gateways that predate the storage ledger omit the token. The file is
110
+ // already at fileUrl, so those uploads still succeed.
111
+ if (session.completionToken) {
112
+ const completeRes = await fetch(getApiUrl() +
113
+ '/v1/zite/public/' +
114
+ flowId +
115
+ '/complete-upload?mode=' +
116
+ mode, {
117
+ method: 'POST',
118
+ headers: {
119
+ 'Content-Type': 'application/json',
120
+ ...authHeaders(),
121
+ },
122
+ body: JSON.stringify({ completionToken: session.completionToken }),
123
+ });
124
+ if (!completeRes.ok) {
125
+ throw new FileUploadError(await readErrorMessage(completeRes, 'Upload failed'));
126
+ }
127
+ }
109
128
  return session.fileUrl;
110
129
  }
111
130
  export async function uploadFile({ data, filename, }) {
@@ -23,7 +23,7 @@ describe('toUploadBlob', () => {
23
23
  });
24
24
  });
25
25
  describe('uploadFile', () => {
26
- it('requests a session then PUTs the bytes to the presigned URL', async () => {
26
+ it('requests a session, PUTs the bytes, then completes the upload', async () => {
27
27
  const file = new File(['hello-world'], 'hello.txt', { type: 'text/plain' });
28
28
  fetchMock
29
29
  .mockResolvedValueOnce({
@@ -33,12 +33,14 @@ describe('uploadFile', () => {
33
33
  presignedUrl: 'https://s3.example.com/put',
34
34
  fileUrl: 'https://uploads.zite.com/orgid-1/zite-uploads/app_123/hello.txt',
35
35
  contentType: 'text/plain',
36
+ completionToken: 'signed-token',
36
37
  }),
37
38
  })
38
- .mockResolvedValueOnce({ ok: true });
39
+ .mockResolvedValueOnce({ ok: true })
40
+ .mockResolvedValueOnce({ ok: true, json: async () => ({ success: true }) });
39
41
  const result = await uploadFile({ data: file, filename: 'hello.txt' });
40
42
  expect(result.fileUrl).toContain('hello.txt');
41
- expect(fetchMock).toHaveBeenCalledTimes(2);
43
+ expect(fetchMock).toHaveBeenCalledTimes(3);
42
44
  expect(fetchMock.mock.calls[0][0]).toBe('https://api.example.com/v1/zite/public/app_123/upload-session?mode=live');
43
45
  const sessionInit = fetchMock.mock.calls[0][1];
44
46
  expect(JSON.parse(sessionInit.body)).toEqual({
@@ -48,6 +50,31 @@ describe('uploadFile', () => {
48
50
  });
49
51
  expect(fetchMock.mock.calls[1][0]).toBe('https://s3.example.com/put');
50
52
  expect(fetchMock.mock.calls[1][1].method).toBe('PUT');
53
+ expect(fetchMock.mock.calls[2][0]).toBe('https://api.example.com/v1/zite/public/app_123/complete-upload?mode=live');
54
+ const completeInit = fetchMock.mock.calls[2][1];
55
+ expect(completeInit.method).toBe('POST');
56
+ expect(JSON.parse(completeInit.body)).toEqual({
57
+ completionToken: 'signed-token',
58
+ });
59
+ });
60
+ it('skips completion when the gateway does not issue a token', async () => {
61
+ fetchMock
62
+ .mockResolvedValueOnce({
63
+ ok: true,
64
+ json: async () => ({
65
+ success: true,
66
+ presignedUrl: 'https://s3.example.com/put',
67
+ fileUrl: 'https://uploads.zite.com/orgid-1/zite-uploads/app_123/hello.txt',
68
+ contentType: 'text/plain',
69
+ }),
70
+ })
71
+ .mockResolvedValueOnce({ ok: true });
72
+ const result = await uploadFile({
73
+ data: new File(['hello-world'], 'hello.txt', { type: 'text/plain' }),
74
+ filename: 'hello.txt',
75
+ });
76
+ expect(result.fileUrl).toContain('hello.txt');
77
+ expect(fetchMock).toHaveBeenCalledTimes(2);
51
78
  });
52
79
  it('surfaces the plan-limit message from the session endpoint', async () => {
53
80
  fetchMock.mockResolvedValueOnce({
@@ -0,0 +1 @@
1
+ export declare const domTagNames: ReadonlySet<string>;