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.
- package/dist/cjs/internal/appMode.d.ts +3 -0
- package/dist/cjs/internal/appMode.js +15 -0
- package/dist/cjs/upload/index.js +12 -2
- package/dist/cjs/upload/index.test.js +39 -0
- package/dist/esm/internal/appMode.d.ts +3 -0
- package/dist/esm/internal/appMode.js +10 -0
- package/dist/esm/start/airtable/index.d.ts +1 -1
- package/dist/esm/start/airtable/index.js +14 -30
- package/dist/esm/start/credentials.d.ts +1 -18
- package/dist/esm/start/credentials.js +1 -29
- package/dist/esm/start/data/index.d.ts +2 -5
- package/dist/esm/start/data/index.js +4 -22
- package/dist/esm/start/outbound.js +3 -10
- package/dist/esm/start/router.d.ts +6 -1
- package/dist/esm/start/router.js +10 -4
- package/dist/esm/start/server.d.ts +2 -5
- package/dist/esm/start/server.js +5 -10
- package/dist/esm/start/types.d.ts +12 -15
- package/dist/esm/start/version.d.ts +1 -1
- package/dist/esm/start/version.js +1 -1
- package/dist/esm/upload/index.js +12 -2
- package/dist/esm/upload/index.test.js +39 -0
- package/package.json +1 -1
|
@@ -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;
|
package/dist/cjs/upload/index.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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,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
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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 =
|
|
38
|
-
|
|
39
|
-
:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
246
|
-
const result =
|
|
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
|
|
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
|
package/dist/esm/start/router.js
CHANGED
|
@@ -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: () => (
|
|
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.
|
|
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>;
|
package/dist/esm/start/server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
:
|
|
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
|
|
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
|
|
103
|
-
runs
|
|
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.
|
|
109
|
-
refreshConnection
|
|
110
|
-
/** A call on Zite's own AI key, which the host makes with the real key.
|
|
111
|
-
aiFetch
|
|
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
|
|
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
|
|
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
|
|
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`.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
2
|
+
export const ZITEJS_VERSION = "0.10.0-start.14";
|
package/dist/esm/upload/index.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
+
});
|