zitejs 0.10.0-start.13 → 0.10.0-start.14

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.
@@ -0,0 +1,3 @@
1
+ export declare const keepRenderedMode: (mode: string | undefined) => void;
2
+ /** The mode the platform served this request or page in; undefined outside a Start app. */
3
+ export declare const platformMode: () => string | undefined;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.platformMode = exports.keepRenderedMode = void 0;
4
+ // Classic's upload client reads this too, so it imports nothing of Start: the
5
+ // request's host comes through the key src/start/host.ts reads it under.
6
+ const HOST_STORE_KEY = Symbol.for("zitejs:host-context");
7
+ // What a Start page was rendered with, kept in the browser when it hydrates.
8
+ const RENDERED_MODE = Symbol.for("zitejs:rendered-mode");
9
+ const keepRenderedMode = (mode) => {
10
+ globalThis[RENDERED_MODE] = mode;
11
+ };
12
+ exports.keepRenderedMode = keepRenderedMode;
13
+ /** The mode the platform served this request or page in; undefined outside a Start app. */
14
+ const platformMode = () => globalThis[HOST_STORE_KEY]?.getStore()?.mode ?? globalThis[RENDERED_MODE];
15
+ exports.platformMode = platformMode;
@@ -6,6 +6,7 @@ exports.uploadFile = uploadFile;
6
6
  exports.useUpload = useUpload;
7
7
  const react_1 = require("react");
8
8
  const config_js_1 = require("../auth/config.js");
9
+ const appMode_js_1 = require("../internal/appMode.js");
9
10
  const constants_js_1 = require("../auth/constants.js");
10
11
  class FileUploadError extends Error {
11
12
  constructor(message) {
@@ -14,7 +15,16 @@ class FileUploadError extends Error {
14
15
  }
15
16
  }
16
17
  exports.FileUploadError = FileUploadError;
17
- function getZiteAppMode(hostname) {
18
+ /** As the platform served the page or request; a classic app's from its hostname. */
19
+ function getZiteAppMode() {
20
+ const served = (0, appMode_js_1.platformMode)();
21
+ if (served)
22
+ return served === 'preview' || served === 'validate' ? 'preview' : 'live';
23
+ // On a server, outside any request the platform served: there's no telling a preview from live.
24
+ if (typeof window === 'undefined') {
25
+ throw new FileUploadError('Upload from a page, or from a request the platform is serving');
26
+ }
27
+ const { hostname } = window.location;
18
28
  const maybeId = hostname.split('.')[0];
19
29
  if (maybeId?.startsWith(constants_js_1.ZITE_APP_EDITOR_HOSTNAME_PREFIX))
20
30
  return 'preview';
@@ -82,7 +92,7 @@ function authHeaders() {
82
92
  }
83
93
  async function performUpload(data, filename) {
84
94
  const blob = toUploadBlob(data);
85
- const mode = getZiteAppMode(window.location.hostname);
95
+ const mode = getZiteAppMode();
86
96
  const flowId = (0, config_js_1.getFlowId)();
87
97
  const sessionRes = await fetch((0, config_js_1.getApiUrl)() + '/v1/zite/public/' + flowId + '/upload-session?mode=' + mode, {
88
98
  method: 'POST',
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const vitest_1 = require("vitest");
4
+ const node_async_hooks_1 = require("node:async_hooks");
5
+ const appMode_js_1 = require("../internal/appMode.js");
4
6
  const index_js_1 = require("./index.js");
5
7
  vitest_1.vi.mock('../auth/config.js', () => ({
6
8
  getApiUrl: () => 'https://api.example.com',
@@ -93,3 +95,40 @@ const fetchMock = vitest_1.vi.fn();
93
95
  await (0, vitest_1.expect)(upload).rejects.toThrow('File size exceeds maximum of 20 MB');
94
96
  });
95
97
  });
98
+ // SDK-10: a Start app's mode is the platform's, never guessed from where the page is served.
99
+ (0, vitest_1.describe)('the mode an upload goes under', () => {
100
+ const sessionUrl = async () => {
101
+ fetchMock.mockResolvedValueOnce({
102
+ ok: true,
103
+ json: async () => ({ success: true, presignedUrl: 'https://s3.example.com/put', fileUrl: 'https://uploads.zite.com/f.txt' }),
104
+ });
105
+ fetchMock.mockResolvedValueOnce({ ok: true });
106
+ await (0, index_js_1.uploadFile)({ data: 'hi', filename: 'f.txt' });
107
+ return String(fetchMock.mock.calls[0][0]);
108
+ };
109
+ (0, vitest_1.afterEach)(() => (0, appMode_js_1.keepRenderedMode)(undefined));
110
+ (0, vitest_1.it)('is the one the page was rendered in, on any host', async () => {
111
+ (0, appMode_js_1.keepRenderedMode)('preview');
112
+ (0, vitest_1.expect)(await sessionUrl()).toContain('mode=preview');
113
+ });
114
+ (0, vitest_1.it)("is the request's own on the server, where there's no page", async () => {
115
+ vitest_1.vi.stubGlobal('window', undefined);
116
+ const store = new node_async_hooks_1.AsyncLocalStorage();
117
+ globalThis[Symbol.for('zitejs:host-context')] = store;
118
+ try {
119
+ (0, vitest_1.expect)(await store.run({ mode: 'validate' }, sessionUrl)).toContain('mode=preview');
120
+ }
121
+ finally {
122
+ delete globalThis[Symbol.for('zitejs:host-context')];
123
+ }
124
+ });
125
+ (0, vitest_1.it)('is refused on a server outside any request the platform served, never guessed as live', async () => {
126
+ vitest_1.vi.stubGlobal('window', undefined);
127
+ await (0, vitest_1.expect)((0, index_js_1.uploadFile)({ data: 'hi', filename: 'f.txt' })).rejects.toThrow(index_js_1.FileUploadError);
128
+ (0, vitest_1.expect)(fetchMock).not.toHaveBeenCalled();
129
+ });
130
+ (0, vitest_1.it)("is a classic app's from its hostname, as before", async () => {
131
+ vitest_1.vi.stubGlobal('window', { location: { hostname: 'zite-editor-app_123.zite.so' } });
132
+ (0, vitest_1.expect)(await sessionUrl()).toContain('mode=preview');
133
+ });
134
+ });
@@ -0,0 +1,3 @@
1
+ export declare const keepRenderedMode: (mode: string | undefined) => void;
2
+ /** The mode the platform served this request or page in; undefined outside a Start app. */
3
+ export declare const platformMode: () => string | undefined;
@@ -0,0 +1,10 @@
1
+ // Classic's upload client reads this too, so it imports nothing of Start: the
2
+ // request's host comes through the key src/start/host.ts reads it under.
3
+ const HOST_STORE_KEY = Symbol.for("zitejs:host-context");
4
+ // What a Start page was rendered with, kept in the browser when it hydrates.
5
+ const RENDERED_MODE = Symbol.for("zitejs:rendered-mode");
6
+ export const keepRenderedMode = (mode) => {
7
+ globalThis[RENDERED_MODE] = mode;
8
+ };
9
+ /** The mode the platform served this request or page in; undefined outside a Start app. */
10
+ export const platformMode = () => globalThis[HOST_STORE_KEY]?.getStore()?.mode ?? globalThis[RENDERED_MODE];
@@ -1,6 +1,6 @@
1
1
  import { type AirtableSettings } from "./dispatch.js";
2
2
  import type { PlatformSession, ZiteRequestContext } from "../types.js";
3
- /** The build's Airtable integration `integrationId`, or undefined when it isn't one, or the host can't say. */
3
+ /** The build's Airtable integration `integrationId`, or undefined when it isn't one. */
4
4
  export declare const airtableSettingsOf: (session: PlatformSession, integrationId: string) => Promise<AirtableSettings | undefined>;
5
5
  /** One generated Airtable table call, run here with the connection's token, which the outbound fetch refreshes on a 401. */
6
6
  export declare function runAirtableCall(host: ZiteRequestContext, session: PlatformSession, integrationId: string, settings: AirtableSettings, methodName: string, params: unknown, access: "signedIn" | "anyone" | "webhook" | "workflow"): Promise<unknown>;
@@ -5,44 +5,29 @@ import { rememberConnection } from "../outbound.js";
5
5
  const AIRTABLE_API = "https://api.airtable.com";
6
6
  // A build's integrations don't change, so per isolate: only answers, never a pending call.
7
7
  const settingsHeld = new Map();
8
- let unsupported = false;
9
- const NOT_IMPLEMENTED = /does not implement (?:the )?method/i;
10
- /** The build's Airtable integration `integrationId`, or undefined when it isn't one, or the host can't say. */
8
+ /** The build's Airtable integration `integrationId`, or undefined when it isn't one. */
11
9
  export const airtableSettingsOf = async (session, integrationId) => {
12
- if (unsupported || !session.integrationSettings)
13
- return undefined;
14
10
  if (settingsHeld.has(integrationId))
15
11
  return settingsHeld.get(integrationId) ?? undefined;
16
- try {
17
- const answer = await session.integrationSettings(integrationId);
18
- const settings = answer?.serviceType === "airtable" && typeof answer.baseId === "string"
19
- ? { baseId: answer.baseId, nameMappings: answer.nameMappings ?? null }
20
- : null;
21
- settingsHeld.set(integrationId, settings);
22
- return settings ?? undefined;
23
- }
24
- catch (error) {
25
- if (error instanceof Error && NOT_IMPLEMENTED.test(error.message)) {
26
- unsupported = true;
27
- return undefined;
28
- }
29
- throw error;
30
- }
12
+ const answer = await session.integrationSettings(integrationId);
13
+ const settings = answer?.serviceType === "airtable" && typeof answer.baseId === "string"
14
+ ? { baseId: answer.baseId, nameMappings: answer.nameMappings ?? null }
15
+ : null;
16
+ settingsHeld.set(integrationId, settings);
17
+ return settings ?? undefined;
31
18
  };
32
19
  /** One generated Airtable table call, run here with the connection's token, which the outbound fetch refreshes on a 401. */
33
20
  export async function runAirtableCall(host, session, integrationId, settings, methodName, params, access) {
34
21
  // As classic: a demo reaches no connected account.
35
22
  if (host.mode === "demo")
36
23
  throw new ZiteError("FORBIDDEN", "This action is not available in demo mode");
37
- const connection = host.credentials
38
- ? await host.credentials.get(integrationId)
39
- : await session.call({
40
- integrationId: INTEGRATIONS_PLATFORM_ID,
41
- className: "Connections",
42
- methodName: "get",
43
- params: { name: integrationId },
44
- access,
45
- });
24
+ const connection = await session.call({
25
+ integrationId: INTEGRATIONS_PLATFORM_ID,
26
+ className: "Connections",
27
+ methodName: "get",
28
+ params: { name: integrationId },
29
+ access,
30
+ });
46
31
  if ("error" in connection) {
47
32
  const { code, message, userFacingMessage } = connection.error;
48
33
  throw new ZiteError({ code: isZiteErrorCode(code) ? code : "INTERNAL_ERROR", message, userFacingMessage });
@@ -66,5 +51,4 @@ export async function runAirtableCall(host, session, integrationId, settings, me
66
51
  /** For tests: forget what this isolate learned. */
67
52
  export const resetAirtableSettings = () => {
68
53
  settingsHeld.clear();
69
- unsupported = false;
70
54
  };
@@ -1,19 +1,2 @@
1
- /**
2
- * Where `integrations.get` reads a connection. In Zite's cloud the host
3
- * answers it on the request's session; an app run elsewhere names its own
4
- * source in its server entry: memory or the environment here, a shared cache
5
- * once one exists.
6
- */
7
- import type { PlatformCallInput, PlatformCallResult, PlatformSession } from "./types.js";
8
- export type CredentialSource = {
9
- get(name: string): Promise<PlatformCallResult>;
10
- /** After a provider answered 401: the connection refreshed, where the source can. */
11
- refresh?(name: string): Promise<PlatformCallResult>;
12
- };
1
+ /** The platform id `integrations.get` is answered under. */
13
2
  export declare const INTEGRATIONS_PLATFORM_ID = "__connections__";
14
- /** The host's, on the request's session. */
15
- export declare const sessionCredentials: (session: PlatformSession, access: PlatformCallInput["access"]) => CredentialSource;
16
- /** Connections held in memory, by name, as `integrations.get` answers them; `refresh` fetches one anew. */
17
- export declare const memoryCredentials: (connections: Record<string, unknown>, refresh?: (name: string) => Promise<unknown>) => CredentialSource;
18
- /** Connections from `ZITE_CONNECTIONS`, JSON by name, for an app run outside Zite. */
19
- export declare const envCredentials: (env?: Record<string, string | undefined>) => CredentialSource;
@@ -1,30 +1,2 @@
1
- /**
2
- * Where `integrations.get` reads a connection. In Zite's cloud the host
3
- * answers it on the request's session; an app run elsewhere names its own
4
- * source in its server entry: memory or the environment here, a shared cache
5
- * once one exists.
6
- */
1
+ /** The platform id `integrations.get` is answered under. */
7
2
  export const INTEGRATIONS_PLATFORM_ID = "__connections__";
8
- /** The host's, on the request's session. */
9
- export const sessionCredentials = (session, access) => ({
10
- get: (name) => session.call({ integrationId: INTEGRATIONS_PLATFORM_ID, className: "Connections", methodName: "get", params: { name }, access }),
11
- ...(session.refreshConnection && { refresh: (name) => session.refreshConnection(name) }),
12
- });
13
- const notFound = (name) => ({
14
- error: { code: "BAD_REQUEST", message: `Integration ${name} not found` },
15
- });
16
- /** Connections held in memory, by name, as `integrations.get` answers them; `refresh` fetches one anew. */
17
- export const memoryCredentials = (connections, refresh) => ({
18
- get: async (name) => (Object.hasOwn(connections, name) ? { value: connections[name] } : notFound(name)),
19
- ...(refresh && {
20
- refresh: async (name) => {
21
- const fresh = await refresh(name);
22
- if (fresh === undefined)
23
- return notFound(name);
24
- connections[name] = fresh;
25
- return { value: fresh };
26
- },
27
- }),
28
- });
29
- /** Connections from `ZITE_CONNECTIONS`, JSON by name, for an app run outside Zite. */
30
- export const envCredentials = (env = process.env) => memoryCredentials(JSON.parse(env.ZITE_CONNECTIONS ?? "{}"));
@@ -6,11 +6,8 @@ type Found = {
6
6
  baseId: string | null;
7
7
  nameMappings: DataNameMappings | null;
8
8
  };
9
- /**
10
- * The build's data context, or undefined from a host that predates the app's own
11
- * dispatch, whose `databases` platform call still answers.
12
- */
13
- export declare const dataContextOf: (session: PlatformSession) => Promise<Found | undefined>;
9
+ /** The build's base and SDK names, asked of the platform once per isolate. */
10
+ export declare const dataContextOf: (session: PlatformSession) => Promise<Found>;
14
11
  /** One `zite.*` call, translated here and sent to base-runner through the platform. */
15
12
  export declare function runDataCall(session: PlatformSession, found: Found, mode: HostMode, methodName: string, sent: unknown): Promise<unknown>;
16
13
  /** For tests: forget what this isolate learned. */
@@ -8,7 +8,6 @@ const TABLES_REUSE_MS = 60_000;
8
8
  // Only answers are kept, never a pending call: one request's RPC session mustn't hold up another's.
9
9
  let context;
10
10
  let tables;
11
- let unsupported = false;
12
11
  const tablesOf = async (baseRunner, baseId) => {
13
12
  if (tables?.baseId === baseId && Date.now() - tables.at < TABLES_REUSE_MS)
14
13
  return tables.tables;
@@ -16,29 +15,13 @@ const tablesOf = async (baseRunner, baseId) => {
16
15
  tables = { baseId, tables: base.tables, at: Date.now() };
17
16
  return base.tables;
18
17
  };
19
- // What Workers RPC throws for a method the host doesn't have yet.
20
- const NOT_IMPLEMENTED = /does not implement (?:the )?method/i;
21
- /**
22
- * The build's data context, or undefined from a host that predates the app's own
23
- * dispatch, whose `databases` platform call still answers.
24
- */
18
+ /** The build's base and SDK names, asked of the platform once per isolate. */
25
19
  export const dataContextOf = async (session) => {
26
- if (unsupported || !session.dataContext || !session.dataFetch)
27
- return undefined;
28
20
  if (context)
29
21
  return context;
30
- try {
31
- const answer = await session.dataContext();
32
- context = { baseId: answer.baseId, nameMappings: answer.nameMappings ?? null };
33
- return context;
34
- }
35
- catch (error) {
36
- if (error instanceof Error && NOT_IMPLEMENTED.test(error.message)) {
37
- unsupported = true;
38
- return undefined;
39
- }
40
- throw error;
41
- }
22
+ const answer = await session.dataContext();
23
+ context = { baseId: answer.baseId, nameMappings: answer.nameMappings ?? null };
24
+ return context;
42
25
  };
43
26
  /**
44
27
  * The table id classic's generated client sends: its SDK name mapped through the
@@ -72,5 +55,4 @@ export async function runDataCall(session, found, mode, methodName, sent) {
72
55
  export const resetDataDispatch = () => {
73
56
  context = undefined;
74
57
  tables = undefined;
75
- unsupported = false;
76
58
  };
@@ -6,7 +6,6 @@
6
6
  * host over the app's platform binding.
7
7
  */
8
8
  import { AI_USAGE_HOSTS, meterAiResponse, withStreamUsage } from "./aiUsage.js";
9
- import { sessionCredentials } from "./credentials.js";
10
9
  import { REDACTED, redactKnown, secretForms } from "./redactSecrets.js";
11
10
  import { getHost } from "./scope.js";
12
11
  const MAX_FETCHES = 500;
@@ -227,12 +226,6 @@ const sessionOf = async (host) => {
227
226
  host.session ??= Promise.resolve(host.watch ? host.platform.session(host.watch) : host.platform.session());
228
227
  return host.session;
229
228
  };
230
- const credentialsOf = async (host) => {
231
- if (host.credentials)
232
- return host.credentials;
233
- const session = await sessionOf(host);
234
- return session && sessionCredentials(session, "anyone");
235
- };
236
229
  /** The token to send instead of `token`, refreshed once per connection per request. */
237
230
  const freshToken = async (host, token, connection) => {
238
231
  const known = replacements.get(token);
@@ -242,8 +235,8 @@ const freshToken = async (host, token, connection) => {
242
235
  let pending = refreshing.get(connection);
243
236
  if (!pending) {
244
237
  pending = (async () => {
245
- const source = await credentialsOf(host);
246
- const result = source?.refresh ? await source.refresh(connection) : undefined;
238
+ const session = await sessionOf(host);
239
+ const result = session ? await session.refreshConnection(connection) : undefined;
247
240
  if (!result || !("value" in result))
248
241
  return undefined;
249
242
  const fresh = result.value;
@@ -351,7 +344,7 @@ export const createOutboundFetch = (nativeFetch) => async (input, init) => {
351
344
  if (ai) {
352
345
  try {
353
346
  const session = await sessionOf(host);
354
- if (!session?.aiFetch) {
347
+ if (!session) {
355
348
  const refused = aiUnavailable();
356
349
  record({ status: refused.status });
357
350
  return refused;
@@ -1,6 +1,6 @@
1
1
  import { QueryClient } from "@tanstack/react-query";
2
2
  import { type AnyRoute, type RouterConstructorOptions, type RouterHistory } from "@tanstack/react-router";
3
- import type { User } from "./types.js";
3
+ import type { HostMode, User } from "./types.js";
4
4
  export declare const QUERY_STALE_TIME_MS = 30000;
5
5
  /** The router context every Zite app's root route declares: `createRootRouteWithContext<ZiteRouterContext>()`. */
6
6
  export type ZiteRouterContext = {
@@ -9,7 +9,12 @@ export type ZiteRouterContext = {
9
9
  };
10
10
  type Dehydrated = {
11
11
  user: User | null;
12
+ mode?: HostMode;
12
13
  };
14
+ /** @internal What a page carries to the browser beside its queries: the signed-in user, and the mode it was served in. */
15
+ export declare const dehydrateZite: (context: ZiteRouterContext) => Dehydrated;
16
+ /** @internal Takes in what the page carried. */
17
+ export declare const hydrateZite: (context: ZiteRouterContext, dehydrated: Dehydrated) => void;
13
18
  type ZiteRouterOptions<TRouteTree extends AnyRoute> = Omit<RouterConstructorOptions<TRouteTree, "never", false, RouterHistory, Dehydrated>, "context" | "dehydrate" | "hydrate">;
14
19
  /**
15
20
  * @internal Tells the editor's preview the page crashed, as classic's runtime reports an emptied #root: a Start page
@@ -5,8 +5,16 @@ import { setupRouterSsrQueryIntegration } from "@tanstack/react-router-ssr-query
5
5
  import { isSignInError, toClientError } from "./errors.js";
6
6
  import { carryPreviewClaim, PREVIEW_CLAIM_PARAM, previewClaim } from "./previewClaim.js";
7
7
  import { currentHost } from "./host.js";
8
+ import { keepRenderedMode } from "../internal/appMode.js";
8
9
  // Long enough that a page's queries don't refetch as the browser picks it up, short enough to stay fresh.
9
10
  export const QUERY_STALE_TIME_MS = 30_000;
11
+ /** @internal What a page carries to the browser beside its queries: the signed-in user, and the mode it was served in. */
12
+ export const dehydrateZite = (context) => ({ user: context.user, mode: currentHost()?.mode });
13
+ /** @internal Takes in what the page carried. */
14
+ export const hydrateZite = (context, dehydrated) => {
15
+ context.user = dehydrated.user;
16
+ keepRenderedMode(dehydrated.mode);
17
+ };
10
18
  const ziteWindow = () => (typeof window === "undefined" ? undefined : window);
11
19
  const box = { maxWidth: 480, margin: "15vh auto", padding: 24, fontFamily: "system-ui, sans-serif", textAlign: "center" };
12
20
  /**
@@ -97,10 +105,8 @@ export function createZiteRouter(options) {
97
105
  options.defaultOnCatch?.(error, info);
98
106
  },
99
107
  context,
100
- dehydrate: () => ({ user: context.user }),
101
- hydrate: (dehydrated) => {
102
- context.user = dehydrated.user;
103
- },
108
+ dehydrate: () => dehydrateZite(context),
109
+ hydrate: (dehydrated) => hydrateZite(context, dehydrated),
104
110
  });
105
111
  setupRouterSsrQueryIntegration({ router, queryClient });
106
112
  const mark = watchReady(router);
@@ -1,4 +1,3 @@
1
- import { type CredentialSource } from "./credentials.js";
2
1
  import type { HostProps, PlatformBinding, RenderWatch, ZiteRequestContext } from "./types.js";
3
2
  export { runWithHost } from "./scope.js";
4
3
  export { describeApp } from "./describe.js";
@@ -36,12 +35,10 @@ export type ZiteServerOptions = {
36
35
  };
37
36
  getRouter: () => unknown;
38
37
  platform?: PlatformBinding;
39
- /** The app's start instance, which a test run of a server function runs under. Entries before 0.10.0-start.11 don't pass it. */
40
- startInstance?: {
38
+ /** The app's start instance, which a test run of a server function runs under. */
39
+ startInstance: {
41
40
  getOptions: () => unknown;
42
41
  };
43
- /** Where `integrations.get` reads outside Zite; the host's requests always read the host. */
44
- credentials?: CredentialSource;
45
42
  };
46
43
  type HostedContext = WorkerContext & {
47
44
  props?: Partial<HostProps>;
@@ -29,9 +29,6 @@ export function installPlatformTransport() {
29
29
  const host = getHost();
30
30
  const getsConnection = integrationId === INTEGRATIONS_PLATFORM_ID && methodName === "get";
31
31
  const name = String(params?.name ?? "");
32
- // An app run outside Zite reads its connections from its own source, with no platform at all.
33
- if (getsConnection && host?.credentials)
34
- return connectionAnswer(host, name, await host.credentials.get(name));
35
32
  // Fails closed: no platform means no platform access, never a fallback.
36
33
  if (!host?.platform)
37
34
  throw new Error("The Zite platform binding is missing");
@@ -39,11 +36,9 @@ export function installPlatformTransport() {
39
36
  const session = await host.session;
40
37
  if (integrationId === DATA_INTEGRATION_ID) {
41
38
  const found = await dataContextOf(session);
42
- if (found) {
43
- return recordCall(host, { integrationId, className, methodName, params }, () => runDataCall(session, found, host.mode, methodName, params));
44
- }
39
+ return recordCall(host, { integrationId, className, methodName, params }, () => runDataCall(session, found, host.mode, methodName, params));
45
40
  }
46
- else if (!integrationId.startsWith("__")) {
41
+ if (!integrationId.startsWith("__")) {
47
42
  const airtable = await airtableSettingsOf(session, integrationId);
48
43
  if (airtable) {
49
44
  return recordCall(host, { integrationId, className, methodName, params }, () => runAirtableCall(host, session, integrationId, airtable, methodName, params, scope.kind));
@@ -231,7 +226,7 @@ async function invokeFunction(request, options) {
231
226
  const startContext = {
232
227
  request,
233
228
  getRouter: options.getRouter,
234
- startOptions: (await options.startInstance?.getOptions()) ?? {},
229
+ startOptions: (await options.startInstance.getOptions()) ?? {},
235
230
  executedRequestMiddlewares: new Set(),
236
231
  contextAfterGlobalMiddlewares: {},
237
232
  handlerType: "serverFn",
@@ -306,7 +301,7 @@ export function createZiteHandlers(options) {
306
301
  const serve = async (request, ctx, watch) => {
307
302
  const host = ctx?.props?.platform
308
303
  ? hostContextFromProps(request, ctx.props)
309
- : { ...hostContextFromRequest(request, options.platform), credentials: options.credentials };
304
+ : hostContextFromRequest(request, options.platform);
310
305
  if (watch)
311
306
  host.watch = watch;
312
307
  setRequestEnv(host);
@@ -354,7 +349,7 @@ export function createZiteHandlers(options) {
354
349
  if (host.fetches?.length)
355
350
  host.session ??= Promise.resolve(platform.session());
356
351
  const session = host.session ? await host.session : undefined;
357
- await (session?.runs ? session.runs(batch) : platform.runs(batch));
352
+ await (session ? session.runs(batch) : platform.runs(batch));
358
353
  })().catch((error) => {
359
354
  console.error("[zite] couldn't record runs", error);
360
355
  });
@@ -1,5 +1,4 @@
1
1
  import type { AiUsageRecord } from "./aiUsage.js";
2
- import type { CredentialSource } from "./credentials.js";
3
2
  export type Role = {
4
3
  id: string;
5
4
  name: string;
@@ -99,29 +98,29 @@ export type FetchRecord = {
99
98
  };
100
99
  export type PlatformSession = {
101
100
  call(call: PlatformCallInput): Promise<PlatformCallResult>;
102
- /** As `PlatformBinding.runs`, on a session the request already holds. Hosts before 0.10.0-start.4 lack it; before 0.10.0-start.13, `fetches`. */
103
- runs?(batch: {
101
+ /** As `PlatformBinding.runs`, on a session the request already holds, with its data calls and fetches. */
102
+ runs(batch: {
104
103
  runs: RunRecord[];
105
104
  calls?: CallRecord[];
106
105
  fetches?: FetchRecord[];
107
106
  }): Promise<void>;
108
- /** After a provider answered 401: the connection refreshed, as `integrations.get` now answers it. Hosts before 0.10.0-start.13 lack it. */
109
- refreshConnection?(name: string): Promise<PlatformCallResult>;
110
- /** A call on Zite's own AI key, which the host makes with the real key. Hosts before 0.10.0-start.13 lack it. */
111
- aiFetch?(request: Request): Promise<Response>;
107
+ /** After a provider answered 401: the connection refreshed, as `integrations.get` now answers it. */
108
+ refreshConnection(name: string): Promise<PlatformCallResult>;
109
+ /** A call on Zite's own AI key, which the host makes with the real key. */
110
+ aiFetch(request: Request): Promise<Response>;
112
111
  /** The build's base and SDK names, which the app's data dispatch translates against. */
113
- dataContext?(): Promise<{
112
+ dataContext(): Promise<{
114
113
  baseId: string | null;
115
114
  nameMappings: unknown;
116
115
  }>;
117
116
  /** One base-runner request; the host holds it to the build's base and adds the key and acting user. */
118
- dataFetch?(request: {
117
+ dataFetch(request: {
119
118
  method: string;
120
119
  path: string;
121
120
  body?: string;
122
121
  }): Promise<Response>;
123
122
  /** An integration's settings as the build declares them; null for one it doesn't, or that runs on the platform. */
124
- integrationSettings?(integrationId: string): Promise<{
123
+ integrationSettings(integrationId: string): Promise<{
125
124
  serviceType: string;
126
125
  baseId?: string;
127
126
  nameMappings?: unknown;
@@ -156,7 +155,7 @@ export type HostProps = {
156
155
  user: User | null;
157
156
  mode: HostMode;
158
157
  invoke: HostInvoke | null;
159
- /** The app's live URL, demo included: `process.env.ZITE_APP_URL`. Hosts before 0.10.0-start.7 don't send it. */
158
+ /** The app's live URL, demo included: `process.env.ZITE_APP_URL`. */
160
159
  appUrl?: string;
161
160
  /** The app's database: `process.env.ZITE_BASE_ID`. */
162
161
  baseId?: string;
@@ -164,9 +163,9 @@ export type HostProps = {
164
163
  previewClaim?: string;
165
164
  /** `process.env` as classic sets it: connections' vars, then the user's own secrets. */
166
165
  env?: Record<string, string>;
167
- /** Which connection each token var in `env` holds. Hosts before 0.10.0-start.13 don't send it. */
166
+ /** Which connection each token var in `env` holds. */
168
167
  envConnections?: Record<string, string>;
169
- /** A resumed run's first attempt, whose saved steps it replays. Hosts before 0.10.0-start.10 don't send it. */
168
+ /** A resumed run's first attempt, whose saved steps it replays. */
170
169
  runRoot?: string;
171
170
  };
172
171
  /** What the host resolved for this request. Set by the generated server entry, never by the browser. */
@@ -184,8 +183,6 @@ export type ZiteRequestContext = {
184
183
  envConnections?: Record<string, string>;
185
184
  /** A resumed run's first attempt, whose saved steps it replays. */
186
185
  runRoot?: string;
187
- /** Where `integrations.get` reads, when not the host: an entry run outside Zite's names its own. */
188
- credentials?: CredentialSource;
189
186
  /** The connection each token this request was handed belongs to. */
190
187
  connectionTokens?: Map<string, string>;
191
188
  /** `integrations.get`'s answers this request, by name. */
@@ -1 +1 @@
1
- export declare const ZITEJS_VERSION = "0.10.0-start.13";
1
+ export declare const ZITEJS_VERSION = "0.10.0-start.14";
@@ -1,2 +1,2 @@
1
1
  // Written by scripts/write-version.mjs from package.json.
2
- export const ZITEJS_VERSION = "0.10.0-start.13";
2
+ export const ZITEJS_VERSION = "0.10.0-start.14";
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useState } from 'react';
2
2
  import { getApiUrl, getFlowId } from '../auth/config.js';
3
+ import { platformMode } from '../internal/appMode.js';
3
4
  import { ZITE_APP_ACTION_HOSTNAME_PREFIX, ZITE_APP_EDITOR_HOSTNAME_PREFIX, ZITE_SANDBOX_DOMAINS, } from '../auth/constants.js';
4
5
  export class FileUploadError extends Error {
5
6
  constructor(message) {
@@ -7,7 +8,16 @@ export class FileUploadError extends Error {
7
8
  this.name = 'FileUploadError';
8
9
  }
9
10
  }
10
- function getZiteAppMode(hostname) {
11
+ /** As the platform served the page or request; a classic app's from its hostname. */
12
+ function getZiteAppMode() {
13
+ const served = platformMode();
14
+ if (served)
15
+ return served === 'preview' || served === 'validate' ? 'preview' : 'live';
16
+ // On a server, outside any request the platform served: there's no telling a preview from live.
17
+ if (typeof window === 'undefined') {
18
+ throw new FileUploadError('Upload from a page, or from a request the platform is serving');
19
+ }
20
+ const { hostname } = window.location;
11
21
  const maybeId = hostname.split('.')[0];
12
22
  if (maybeId?.startsWith(ZITE_APP_EDITOR_HOSTNAME_PREFIX))
13
23
  return 'preview';
@@ -75,7 +85,7 @@ function authHeaders() {
75
85
  }
76
86
  async function performUpload(data, filename) {
77
87
  const blob = toUploadBlob(data);
78
- const mode = getZiteAppMode(window.location.hostname);
88
+ const mode = getZiteAppMode();
79
89
  const flowId = getFlowId();
80
90
  const sessionRes = await fetch(getApiUrl() + '/v1/zite/public/' + flowId + '/upload-session?mode=' + mode, {
81
91
  method: 'POST',
@@ -1,4 +1,6 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { AsyncLocalStorage } from 'node:async_hooks';
3
+ import { keepRenderedMode } from '../internal/appMode.js';
2
4
  import { FileUploadError, toUploadBlob, uploadFile } from './index.js';
3
5
  vi.mock('../auth/config.js', () => ({
4
6
  getApiUrl: () => 'https://api.example.com',
@@ -91,3 +93,40 @@ describe('uploadFile', () => {
91
93
  await expect(upload).rejects.toThrow('File size exceeds maximum of 20 MB');
92
94
  });
93
95
  });
96
+ // SDK-10: a Start app's mode is the platform's, never guessed from where the page is served.
97
+ describe('the mode an upload goes under', () => {
98
+ const sessionUrl = async () => {
99
+ fetchMock.mockResolvedValueOnce({
100
+ ok: true,
101
+ json: async () => ({ success: true, presignedUrl: 'https://s3.example.com/put', fileUrl: 'https://uploads.zite.com/f.txt' }),
102
+ });
103
+ fetchMock.mockResolvedValueOnce({ ok: true });
104
+ await uploadFile({ data: 'hi', filename: 'f.txt' });
105
+ return String(fetchMock.mock.calls[0][0]);
106
+ };
107
+ afterEach(() => keepRenderedMode(undefined));
108
+ it('is the one the page was rendered in, on any host', async () => {
109
+ keepRenderedMode('preview');
110
+ expect(await sessionUrl()).toContain('mode=preview');
111
+ });
112
+ it("is the request's own on the server, where there's no page", async () => {
113
+ vi.stubGlobal('window', undefined);
114
+ const store = new AsyncLocalStorage();
115
+ globalThis[Symbol.for('zitejs:host-context')] = store;
116
+ try {
117
+ expect(await store.run({ mode: 'validate' }, sessionUrl)).toContain('mode=preview');
118
+ }
119
+ finally {
120
+ delete globalThis[Symbol.for('zitejs:host-context')];
121
+ }
122
+ });
123
+ it('is refused on a server outside any request the platform served, never guessed as live', async () => {
124
+ vi.stubGlobal('window', undefined);
125
+ await expect(uploadFile({ data: 'hi', filename: 'f.txt' })).rejects.toThrow(FileUploadError);
126
+ expect(fetchMock).not.toHaveBeenCalled();
127
+ });
128
+ it("is a classic app's from its hostname, as before", async () => {
129
+ vi.stubGlobal('window', { location: { hostname: 'zite-editor-app_123.zite.so' } });
130
+ expect(await sessionUrl()).toContain('mode=preview');
131
+ });
132
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zitejs",
3
- "version": "0.10.0-start.13",
3
+ "version": "0.10.0-start.14",
4
4
  "description": "The Zite framework — build apps on Zite Database",
5
5
  "type": "module",
6
6
  "sideEffects": false,