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
@@ -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,6 +1,7 @@
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
6
  import { generateDbTs, generateApiTs, generateBackendWrapperTs, generateAirtableTs, findEmailIntegrationId, generateEmailSdk, } from "../sync/lib.js";
6
7
  const debounceTimers = new Map();
@@ -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;
@@ -40,11 +33,11 @@ const readJsonFile = (path) => {
40
33
  }
41
34
  };
42
35
  function getEmailIntegrationId(appDir) {
43
- return findEmailIntegrationId(readJsonFile(join("apps", appDir, "zite.config.json")), readJsonFile("zite.config.json"));
36
+ return findEmailIntegrationId(readJsonFile(join(appDir.path, "zite.config.json")), readJsonFile("zite.config.json"));
44
37
  }
45
38
  function getDeclaredEnvVarNames(appDir) {
46
39
  try {
47
- const configPath = join("apps", appDir, "zite.config.json");
40
+ const configPath = join(appDir.path, "zite.config.json");
48
41
  if (!existsSync(configPath))
49
42
  return [];
50
43
  const config = JSON.parse(readFileSync(configPath, "utf-8"));
@@ -55,7 +48,7 @@ function getDeclaredEnvVarNames(appDir) {
55
48
  }
56
49
  }
57
50
  function regenerateAppTypedWrappers(appDir) {
58
- const outDir = join("apps", appDir, ".zite");
51
+ const outDir = join(appDir.path, ".zite");
59
52
  mkdirSync(outDir, { recursive: true });
60
53
  // user.ts and auth.ts no longer generated — User type is fixed in zitejs/auth
61
54
  // Email integration: generate the Email client at .zite/integrations/email.ts.
@@ -70,7 +63,7 @@ function regenerateAppTypedWrappers(appDir) {
70
63
  writeFileSync(join(outDir, "backend.ts"), generateBackendWrapperTs(getDeclaredEnvVarNames(appDir)));
71
64
  }
72
65
  function regenerateAppApiTs(appDir) {
73
- const apiDir = join("apps", appDir, "src", "api");
66
+ const apiDir = join(appDir.path, "src", "api");
74
67
  if (!existsSync(apiDir))
75
68
  return;
76
69
  // Sorted because `readdir` order is unspecified — it is whatever the
@@ -90,14 +83,14 @@ function regenerateAppApiTs(appDir) {
90
83
  }));
91
84
  const content = generateApiTs(endpointFiles);
92
85
  if (content) {
93
- const outDir = join("apps", appDir, ".zite");
86
+ const outDir = join(appDir.path, ".zite");
94
87
  mkdirSync(outDir, { recursive: true });
95
88
  writeFileSync(join(outDir, "api.ts"), content);
96
- console.log(`Updated apps/${appDir}/.zite/api.ts`);
89
+ console.log(`Updated ${appDir.path}/.zite/api.ts`);
97
90
  }
98
91
  }
99
92
  function regenerateAppAirtableSdk(appDir) {
100
- const lockPath = join("apps", appDir, "zite.lock");
93
+ const lockPath = join(appDir.path, "zite.lock");
101
94
  console.log(`[airtable-sdk] Checking ${lockPath} exists: ${existsSync(lockPath)}`);
102
95
  if (!existsSync(lockPath))
103
96
  return;
@@ -118,15 +111,15 @@ function regenerateAppAirtableSdk(appDir) {
118
111
  };
119
112
  const content = generateAirtableTs(airtableLock);
120
113
  if (content) {
121
- const outDir = join("apps", appDir, ".zite", "integrations");
114
+ const outDir = join(appDir.path, ".zite", "integrations");
122
115
  mkdirSync(outDir, { recursive: true });
123
116
  writeFileSync(join(outDir, "airtable.ts"), content);
124
- console.log(`Updated apps/${appDir}/.zite/integrations/airtable.ts`);
117
+ console.log(`Updated ${appDir.path}/.zite/integrations/airtable.ts`);
125
118
  }
126
119
  }
127
120
  }
128
121
  catch (err) {
129
- console.error(`[airtable-sdk] Error processing lock for ${appDir}:`, err);
122
+ console.error(`[airtable-sdk] Error processing lock for ${appDir.path}:`, err);
130
123
  }
131
124
  }
132
125
  export async function runGenerate() {
@@ -150,8 +143,8 @@ export async function runGenerate() {
150
143
  catch (err) {
151
144
  console.warn("DB SDK generation failed:", err instanceof Error ? err.message : err);
152
145
  }
153
- // 2. Find all apps and regenerate their .zite/ files
154
- const appDirs = findAppDirs();
146
+ // 2. Find every app and automation and regenerate their .zite/ files
147
+ const appDirs = listSourceDirs();
155
148
  for (const app of appDirs) {
156
149
  regenerateAppApiTs(app);
157
150
  regenerateAppTypedWrappers(app);
@@ -171,33 +164,33 @@ export async function runDev() {
171
164
  console.warn("Initial sync failed (continuing with watcher):", err instanceof Error ? err.message : err);
172
165
  }
173
166
  console.log("");
174
- // 2. Find all apps and regenerate their .zite/ files
175
- const appDirs = findAppDirs();
167
+ // 2. Find every app and automation and regenerate their .zite/ files
168
+ const appDirs = listSourceDirs();
176
169
  for (const app of appDirs) {
177
170
  regenerateAppApiTs(app);
178
171
  regenerateAppTypedWrappers(app);
179
172
  }
180
- // 3. Watch each app's src/api/ for endpoint changes
173
+ // 3. Watch each dir's src/api/ for endpoint changes
181
174
  let watchingAny = false;
182
175
  for (const app of appDirs) {
183
- const apiDir = join("apps", app, "src", "api");
176
+ const apiDir = join(app.path, "src", "api");
184
177
  if (!existsSync(apiDir))
185
178
  continue;
186
179
  watchingAny = true;
187
- console.log(`Watching apps/${app}/src/api/ for endpoint changes...`);
180
+ console.log(`Watching ${app.path}/src/api/ for endpoint changes...`);
188
181
  watch(apiDir, { recursive: true }, (_event, filename) => {
189
182
  if (!filename)
190
183
  return;
191
184
  if (!filename.endsWith(".ts") && !filename.endsWith(".js"))
192
185
  return;
193
- debounce(app, () => {
194
- console.log(`Endpoint changed in ${app}: ${filename}`);
186
+ debounce(app.path, () => {
187
+ console.log(`Endpoint changed in ${app.path}: ${filename}`);
195
188
  regenerateAppApiTs(app);
196
189
  }, 200);
197
190
  });
198
191
  }
199
192
  if (!watchingAny) {
200
- console.log("No apps with src/api/ found.");
193
+ console.log("No apps or automations with src/api/ found.");
201
194
  }
202
195
  // 4. Watch zite.schema.json for schema drift
203
196
  if (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,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
+ }
@@ -1216,10 +1216,10 @@ export function generateBackendWrapperTs(envVarNames = []) {
1216
1216
  "// Auto-generated type-narrowing wrapper. Do not edit manually.",
1217
1217
  "// Re-exports createEndpoint with context.user typed to the app User.",
1218
1218
  "",
1219
- "import type { ZiteRequestContext as _ZiteRequestContext, ZiteScheduledContext, ZiteErrorCode, ZiteSchedule, ZiteStreamInterface, ZiteWebhook } from 'zitejs/backend/base';",
1219
+ "import type { ZiteRequestContext as _ZiteRequestContext, ZiteScheduledContext, ZiteErrorCode, ZiteSchedule, ZiteStreamInterface, ZiteTrigger, ZiteWebhook } from 'zitejs/backend/base';",
1220
1220
  "import type { User } from 'zitejs/auth';",
1221
1221
  "",
1222
- "export type { ZiteErrorCode, ZiteSchedule, ZiteScheduledContext, ZiteStreamInterface, ZiteWebhook };",
1222
+ "export type { ZiteErrorCode, ZiteSchedule, ZiteScheduledContext, ZiteStreamInterface, ZiteTrigger, ZiteWebhook };",
1223
1223
  // The pre-monorepo SDK put this in scope for every endpoint, so migrated
1224
1224
  // code can name it. `createEndpoint` infers the same thing without it.
1225
1225
  "export type InferSchemaType<T> = T extends { _output: infer U } ? U : T;",
@@ -1294,7 +1294,7 @@ export function generateBackendWrapperTs(envVarNames = []) {
1294
1294
  // TStream mirrors zitejs/backend/base. Without it `stream: true` endpoints
1295
1295
  // get no `stream` argument here — and this wrapper, not the base module, is
1296
1296
  // what `zitejs/backend` resolves to in every app.
1297
- "export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream extends boolean = false, TSchedule extends ZiteSchedule | undefined = undefined, TWebhook extends ZiteWebhook | undefined = undefined, TRawInput = TInput> {",
1297
+ "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> {",
1298
1298
  " description?: string;",
1299
1299
  " inputSchema?: SchemaLike<TInput, TRawInput>;",
1300
1300
  // Apps compile against this copy, not the base module's.
@@ -1306,17 +1306,19 @@ export function generateBackendWrapperTs(envVarNames = []) {
1306
1306
  " schedule?: TSchedule;",
1307
1307
  " /** When set, an inbound webhook can also trigger this endpoint. Like `schedule`, it widens `context` — a webhook fire has no session. */",
1308
1308
  " webhook?: TWebhook;",
1309
+ " /** 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. */",
1310
+ " trigger?: TTrigger;",
1309
1311
  " execute: (",
1310
1312
  " params: {",
1311
1313
  " input: TInput;",
1312
- " context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : TWebhook extends ZiteWebhook ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;",
1314
+ " context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : TWebhook extends ZiteWebhook ? ZiteRequestContext | ZiteScheduledContext : TTrigger extends ZiteTrigger ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;",
1313
1315
  " } & (TStream extends true ? { stream: ZiteStreamInterface } : {}),",
1314
1316
  " ) => Promise<TOutput> | TOutput;",
1315
1317
  "}",
1316
1318
  "",
1317
- "export function createEndpoint<TInput = unknown, TOutput = unknown, TStream extends boolean = false, TSchedule extends ZiteSchedule | undefined = undefined, TWebhook extends ZiteWebhook | undefined = undefined, TRawInput = TInput>(",
1318
- " config: EndpointConfig<TInput, TOutput, TStream, TSchedule, TWebhook, TRawInput>,",
1319
- "): EndpointConfig<TInput, TOutput, TStream, TSchedule, TWebhook, TRawInput> {",
1319
+ "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>(",
1320
+ " config: EndpointConfig<TInput, TOutput, TStream, TSchedule, TWebhook, TRawInput, TTrigger>,",
1321
+ "): EndpointConfig<TInput, TOutput, TStream, TSchedule, TWebhook, TRawInput, TTrigger> {",
1320
1322
  " return config;",
1321
1323
  "}",
1322
1324
  "",
@@ -376,6 +376,11 @@ describe('generateBackendWrapperTs', () => {
376
376
  it('exports createEndpoint', () => {
377
377
  expect(output).toContain('createEndpoint');
378
378
  });
379
+ it('types the trigger field like schedule and webhook', () => {
380
+ expect(output).toContain('trigger?: TTrigger;');
381
+ expect(output).toContain('TTrigger extends ZiteTrigger | undefined = undefined');
382
+ expect(output).toContain('ZiteTrigger, ZiteWebhook }');
383
+ });
379
384
  it('exports ZiteError', () => {
380
385
  expect(output).toContain('ZiteError');
381
386
  });
@@ -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>;