zitejs 0.10.0-start.10 → 0.10.0-start.13
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/auth/index.test.js +30 -0
- package/dist/cjs/start/entries.js +4 -2
- package/dist/esm/auth/index.test.js +30 -0
- package/dist/esm/start/aiUsage.d.ts +23 -0
- package/dist/esm/start/aiUsage.js +146 -0
- package/dist/esm/start/airtable/index.d.ts +1 -4
- package/dist/esm/start/airtable/index.js +13 -13
- package/dist/esm/start/credentials.d.ts +19 -0
- package/dist/esm/start/credentials.js +30 -0
- package/dist/esm/start/entries.js +4 -2
- package/dist/esm/start/instance.d.ts +1 -0
- package/dist/esm/start/instance.js +12 -3
- package/dist/esm/start/outbound.d.ts +11 -0
- package/dist/esm/start/outbound.js +417 -0
- package/dist/esm/start/quiet.d.ts +2 -0
- package/dist/esm/start/quiet.js +27 -0
- package/dist/esm/start/redactSecrets.d.ts +8 -0
- package/dist/esm/start/redactSecrets.js +23 -0
- package/dist/esm/start/router.d.ts +5 -0
- package/dist/esm/start/router.js +32 -0
- package/dist/esm/start/server.d.ts +7 -0
- package/dist/esm/start/server.js +82 -30
- package/dist/esm/start/types.d.ts +48 -3
- package/dist/esm/start/version.d.ts +1 -1
- package/dist/esm/start/version.js +1 -1
- package/package.json +1 -1
package/dist/esm/start/server.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// For the generated server entry only. Apps never import this.
|
|
2
|
+
import { requestHandler } from "@tanstack/react-start/server";
|
|
3
|
+
import { createSsrRpc } from "@tanstack/react-start/ssr-rpc";
|
|
4
|
+
import { runWithStartContext } from "@tanstack/start-storage-context";
|
|
1
5
|
import { asZiteError, isZiteErrorCode, ZiteError, ZITE_ERROR_STATUS } from "./errors.js";
|
|
2
6
|
import { describeApp } from "./describe.js";
|
|
3
7
|
import { runRegistration } from "./registration.js";
|
|
@@ -5,6 +9,9 @@ import { isAuthHookName, loadAuthHooks } from "./authHooks.js";
|
|
|
5
9
|
import { errorOf, preview } from "./runLog.js";
|
|
6
10
|
import { DATA_INTEGRATION_ID, dataContextOf, runDataCall } from "./data/index.js";
|
|
7
11
|
import { airtableSettingsOf, runAirtableCall } from "./airtable/index.js";
|
|
12
|
+
import { withoutConsole } from "./quiet.js";
|
|
13
|
+
import { createOutboundFetch, rememberConnection } from "./outbound.js";
|
|
14
|
+
import { INTEGRATIONS_PLATFORM_ID } from "./credentials.js";
|
|
8
15
|
import { getHost, requireScope, runInScope, runWithHost } from "./scope.js";
|
|
9
16
|
import { ZITEJS_VERSION } from "./version.js";
|
|
10
17
|
import { loadWorkflows, loggedInput, runWorkflow } from "./workflowRun.js";
|
|
@@ -20,6 +27,11 @@ export function installPlatformTransport() {
|
|
|
20
27
|
const sdkCall = async (integrationId, className, methodName, params) => {
|
|
21
28
|
const scope = requireScope(`${className}.${methodName}`);
|
|
22
29
|
const host = getHost();
|
|
30
|
+
const getsConnection = integrationId === INTEGRATIONS_PLATFORM_ID && methodName === "get";
|
|
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));
|
|
23
35
|
// Fails closed: no platform means no platform access, never a fallback.
|
|
24
36
|
if (!host?.platform)
|
|
25
37
|
throw new Error("The Zite platform binding is missing");
|
|
@@ -38,6 +50,8 @@ export function installPlatformTransport() {
|
|
|
38
50
|
}
|
|
39
51
|
}
|
|
40
52
|
const result = await session.call({ integrationId, className, methodName, params, access: scope.kind });
|
|
53
|
+
if (getsConnection)
|
|
54
|
+
return connectionAnswer(host, name, result);
|
|
41
55
|
if ("value" in result)
|
|
42
56
|
return result.value;
|
|
43
57
|
const { code, message, userFacingMessage } = result.error;
|
|
@@ -46,6 +60,15 @@ export function installPlatformTransport() {
|
|
|
46
60
|
globalThis.__wrapSdkCall = sdkCall;
|
|
47
61
|
watchOutboundFetches();
|
|
48
62
|
}
|
|
63
|
+
/** `integrations.get`'s answer, whose values then name the calls made with them. */
|
|
64
|
+
const connectionAnswer = (host, name, result) => {
|
|
65
|
+
if ("value" in result) {
|
|
66
|
+
rememberConnection(host, name, result.value);
|
|
67
|
+
return result.value;
|
|
68
|
+
}
|
|
69
|
+
const { code, message, userFacingMessage } = result.error;
|
|
70
|
+
throw new ZiteError({ code: isZiteErrorCode(code) ? code : "INTERNAL_ERROR", message, userFacingMessage });
|
|
71
|
+
};
|
|
49
72
|
/** A data call the app made itself, recorded as the host recorded the ones it made. */
|
|
50
73
|
const recordCall = async (host, call, run) => {
|
|
51
74
|
const at = Date.now();
|
|
@@ -67,31 +90,18 @@ const recordCall = async (host, call, run) => {
|
|
|
67
90
|
throw error;
|
|
68
91
|
}
|
|
69
92
|
};
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
return new URL(input instanceof Request ? input.url : input).host;
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
return "?";
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
// A page that fetched depends on more than the reads the host saw, so the host
|
|
80
|
-
// must hear of it before the fetch goes out. Next patches fetch for the same reason.
|
|
93
|
+
// Keyed on globalThis so two copies of zitejs in one bundle wrap fetch once.
|
|
94
|
+
const WATCHED_KEY = Symbol.for("zitejs:outbound-fetch");
|
|
95
|
+
// Next patches fetch for the same reasons: the page cache must hear of each, and the run log too.
|
|
81
96
|
const watchOutboundFetches = () => {
|
|
82
|
-
|
|
97
|
+
const marks = globalThis;
|
|
98
|
+
if (marks[WATCHED_KEY])
|
|
83
99
|
return;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
globalThis.fetch = async (input, init) => {
|
|
87
|
-
const watch = getHost()?.watch;
|
|
88
|
-
if (watch)
|
|
89
|
-
await watch.uncacheable(`fetch ${fetchedHost(input)}`);
|
|
90
|
-
return nativeFetch(input, init);
|
|
91
|
-
};
|
|
100
|
+
marks[WATCHED_KEY] = true;
|
|
101
|
+
globalThis.fetch = createOutboundFetch(globalThis.fetch.bind(globalThis));
|
|
92
102
|
};
|
|
93
103
|
const decodeUser = (value) => JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(value.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0))));
|
|
94
|
-
const INVOKE_KINDS = new Set(["webhook", "describe", "workflow", "register", "warm", "authHook"]);
|
|
104
|
+
const INVOKE_KINDS = new Set(["webhook", "describe", "workflow", "register", "warm", "authHook", "function"]);
|
|
95
105
|
// What a request without props may ask for: neither runs app code on anyone's behalf.
|
|
96
106
|
const UNHOSTED_INVOKE_KINDS = new Set(["warm", "describe"]);
|
|
97
107
|
/** What the host dispatched this request with. Headers are ignored: props are the only thing it alone can set. */
|
|
@@ -106,6 +116,7 @@ export function hostContextFromProps(request, props) {
|
|
|
106
116
|
baseId: props.baseId,
|
|
107
117
|
previewClaim: props.previewClaim,
|
|
108
118
|
env: props.env,
|
|
119
|
+
envConnections: props.envConnections,
|
|
109
120
|
runRoot: props.runRoot,
|
|
110
121
|
runs: [],
|
|
111
122
|
platform: props.platform,
|
|
@@ -206,6 +217,38 @@ async function invokeWorkflow(request, host, workflows) {
|
|
|
206
217
|
});
|
|
207
218
|
return Response.json(result, { status: result.ok ? 200 : 500 });
|
|
208
219
|
}
|
|
220
|
+
// The host sends `{ input }`, an editor's test run of a server function. It runs
|
|
221
|
+
// as a call from a page would on the server: its own middleware decides access
|
|
222
|
+
// for the user the host dispatched with, validates the input and records the run.
|
|
223
|
+
async function invokeFunction(request, options) {
|
|
224
|
+
if (request.method !== "POST" || new URL(request.url).pathname !== INVOKE_PATH)
|
|
225
|
+
return new Response("Not found", { status: 404 });
|
|
226
|
+
const id = getHost()?.invoke?.name ?? "";
|
|
227
|
+
if (!builtFunctionIds().includes(id)) {
|
|
228
|
+
return Response.json({ ok: false, error: { code: "NOT_FOUND", message: `No server function ${id}` } }, { status: 404 });
|
|
229
|
+
}
|
|
230
|
+
const input = (await request.json().catch(() => ({})))?.input;
|
|
231
|
+
const startContext = {
|
|
232
|
+
request,
|
|
233
|
+
getRouter: options.getRouter,
|
|
234
|
+
startOptions: (await options.startInstance?.getOptions()) ?? {},
|
|
235
|
+
executedRequestMiddlewares: new Set(),
|
|
236
|
+
contextAfterGlobalMiddlewares: {},
|
|
237
|
+
handlerType: "serverFn",
|
|
238
|
+
};
|
|
239
|
+
// In TanStack's request event too, which the function's own status setting reads.
|
|
240
|
+
return requestHandler(async () => {
|
|
241
|
+
try {
|
|
242
|
+
const answer = (await runWithStartContext(startContext, () => createSsrRpc(id)({ data: input, context: {} })));
|
|
243
|
+
if (answer.error !== undefined)
|
|
244
|
+
throw answer.error;
|
|
245
|
+
return Response.json({ ok: true, output: answer.result ?? null });
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
return Response.json({ ok: false, error: errorOf(error) }, { status: 500 });
|
|
249
|
+
}
|
|
250
|
+
})(request, undefined);
|
|
251
|
+
}
|
|
209
252
|
// The host sends `{ op, input }`: `subscribe` with the platform's notification
|
|
210
253
|
// URL and secret, or `unsubscribe` with what `subscribe` returned.
|
|
211
254
|
async function invokeRegistration(request, host, workflows) {
|
|
@@ -263,7 +306,7 @@ export function createZiteHandlers(options) {
|
|
|
263
306
|
const serve = async (request, ctx, watch) => {
|
|
264
307
|
const host = ctx?.props?.platform
|
|
265
308
|
? hostContextFromProps(request, ctx.props)
|
|
266
|
-
: hostContextFromRequest(request, options.platform);
|
|
309
|
+
: { ...hostContextFromRequest(request, options.platform), credentials: options.credentials };
|
|
267
310
|
if (watch)
|
|
268
311
|
host.watch = watch;
|
|
269
312
|
setRequestEnv(host);
|
|
@@ -283,24 +326,33 @@ export function createZiteHandlers(options) {
|
|
|
283
326
|
}
|
|
284
327
|
let response = await runWithHost(host, async () => host.invoke?.kind === "workflow"
|
|
285
328
|
? invokeWorkflow(request, host, await loadedWorkflows())
|
|
286
|
-
: host.invoke?.kind === "
|
|
287
|
-
?
|
|
288
|
-
: host.invoke?.kind === "
|
|
289
|
-
?
|
|
290
|
-
:
|
|
329
|
+
: host.invoke?.kind === "function"
|
|
330
|
+
? invokeFunction(request, options)
|
|
331
|
+
: host.invoke?.kind === "register"
|
|
332
|
+
? withoutConsole(async () => invokeRegistration(request, host, await loadedWorkflows()))
|
|
333
|
+
: host.invoke?.kind === "authHook"
|
|
334
|
+
? invokeAuthHook(request, host)
|
|
335
|
+
: options.handler.fetch(request));
|
|
291
336
|
const status = response.status === 500 ? pageStatus(host) : undefined;
|
|
292
337
|
if (status)
|
|
293
338
|
response = new Response(response.body, { status, headers: response.headers });
|
|
294
339
|
return onBodyEnd(response, () => {
|
|
295
340
|
const { platform } = host;
|
|
296
|
-
if ((!host.runs.length && !host.calls?.length && !host.pending?.length) || !platform)
|
|
341
|
+
if ((!host.runs.length && !host.calls?.length && !host.fetches?.length && !host.pending?.length) || !platform)
|
|
297
342
|
return;
|
|
298
343
|
// On the request's session when it has one: one call, not another binding round trip.
|
|
299
344
|
const flushed = (async () => {
|
|
300
345
|
await Promise.all(host.pending ?? []);
|
|
301
|
-
if (!host.runs.length && !host.calls?.length)
|
|
346
|
+
if (!host.runs.length && !host.calls?.length && !host.fetches?.length)
|
|
302
347
|
return;
|
|
303
|
-
const batch = {
|
|
348
|
+
const batch = {
|
|
349
|
+
runs: host.runs,
|
|
350
|
+
...(host.calls?.length && { calls: host.calls }),
|
|
351
|
+
...(host.fetches?.length && { fetches: host.fetches }),
|
|
352
|
+
};
|
|
353
|
+
// Only a session takes fetches.
|
|
354
|
+
if (host.fetches?.length)
|
|
355
|
+
host.session ??= Promise.resolve(platform.session());
|
|
304
356
|
const session = host.session ? await host.session : undefined;
|
|
305
357
|
await (session?.runs ? session.runs(batch) : platform.runs(batch));
|
|
306
358
|
})().catch((error) => {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { AiUsageRecord } from "./aiUsage.js";
|
|
2
|
+
import type { CredentialSource } from "./credentials.js";
|
|
1
3
|
export type Role = {
|
|
2
4
|
id: string;
|
|
3
5
|
name: string;
|
|
@@ -76,13 +78,37 @@ export type CallRecord = {
|
|
|
76
78
|
message: string;
|
|
77
79
|
};
|
|
78
80
|
};
|
|
81
|
+
/** An outbound fetch, by host and path (never its query), with the connection whose token it carried. */
|
|
82
|
+
export type FetchRecord = {
|
|
83
|
+
at: number;
|
|
84
|
+
method: string;
|
|
85
|
+
/** Whole, with every secret the request knows redacted. */
|
|
86
|
+
url: string;
|
|
87
|
+
/** As classic's run log shows it: cut short, secrets redacted. */
|
|
88
|
+
requestBody?: string;
|
|
89
|
+
status?: number;
|
|
90
|
+
durationMs: number;
|
|
91
|
+
error?: string;
|
|
92
|
+
connection?: string;
|
|
93
|
+
/** Sent again with a refreshed token after a 401. */
|
|
94
|
+
retried?: true;
|
|
95
|
+
/** On Zite's own AI key, which the host meters itself. */
|
|
96
|
+
ziteAi?: true;
|
|
97
|
+
/** What a call on the org's own AI key used, for analytics. */
|
|
98
|
+
ai?: AiUsageRecord;
|
|
99
|
+
};
|
|
79
100
|
export type PlatformSession = {
|
|
80
101
|
call(call: PlatformCallInput): Promise<PlatformCallResult>;
|
|
81
|
-
/** As `PlatformBinding.runs`, on a session the request already holds. Hosts before 0.10.0-start.4 lack it. */
|
|
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`. */
|
|
82
103
|
runs?(batch: {
|
|
83
104
|
runs: RunRecord[];
|
|
84
105
|
calls?: CallRecord[];
|
|
106
|
+
fetches?: FetchRecord[];
|
|
85
107
|
}): 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>;
|
|
86
112
|
/** The build's base and SDK names, which the app's data dispatch translates against. */
|
|
87
113
|
dataContext?(): Promise<{
|
|
88
114
|
baseId: string | null;
|
|
@@ -120,7 +146,7 @@ export type PlatformBinding = {
|
|
|
120
146
|
/** A demo is the published build serving a read-only visitor: as live, but every write is refused. */
|
|
121
147
|
export type HostMode = "live" | "preview" | "demo" | "validate";
|
|
122
148
|
export type HostInvoke = {
|
|
123
|
-
kind: "webhook" | "describe" | "workflow" | "register" | "warm" | "authHook";
|
|
149
|
+
kind: "webhook" | "describe" | "workflow" | "register" | "warm" | "authHook" | "function";
|
|
124
150
|
name: string;
|
|
125
151
|
};
|
|
126
152
|
/** The per-request props the host dispatches with. Only the host can set them. */
|
|
@@ -136,8 +162,10 @@ export type HostProps = {
|
|
|
136
162
|
baseId?: string;
|
|
137
163
|
/** An editor preview's access, which zitejs keeps on the app's links and calls. */
|
|
138
164
|
previewClaim?: string;
|
|
139
|
-
/** `process.env` as classic sets it: connections' vars,
|
|
165
|
+
/** `process.env` as classic sets it: connections' vars, then the user's own secrets. */
|
|
140
166
|
env?: Record<string, string>;
|
|
167
|
+
/** Which connection each token var in `env` holds. Hosts before 0.10.0-start.13 don't send it. */
|
|
168
|
+
envConnections?: Record<string, string>;
|
|
141
169
|
/** A resumed run's first attempt, whose saved steps it replays. Hosts before 0.10.0-start.10 don't send it. */
|
|
142
170
|
runRoot?: string;
|
|
143
171
|
};
|
|
@@ -153,8 +181,25 @@ export type ZiteRequestContext = {
|
|
|
153
181
|
previewClaim?: string;
|
|
154
182
|
/** Classic's connection env vars the host sent, set into `process.env` for the request. */
|
|
155
183
|
env?: Record<string, string>;
|
|
184
|
+
envConnections?: Record<string, string>;
|
|
156
185
|
/** A resumed run's first attempt, whose saved steps it replays. */
|
|
157
186
|
runRoot?: string;
|
|
187
|
+
/** Where `integrations.get` reads, when not the host: an entry run outside Zite's names its own. */
|
|
188
|
+
credentials?: CredentialSource;
|
|
189
|
+
/** The connection each token this request was handed belongs to. */
|
|
190
|
+
connectionTokens?: Map<string, string>;
|
|
191
|
+
/** `integrations.get`'s answers this request, by name. */
|
|
192
|
+
connectionAnswers?: Map<string, unknown>;
|
|
193
|
+
/** One refresh per connection per request, however many calls got a 401. */
|
|
194
|
+
refreshing?: Map<string, Promise<string | undefined>>;
|
|
195
|
+
/** The outbound fetches, flushed with its runs. */
|
|
196
|
+
fetches?: FetchRecord[];
|
|
197
|
+
/** The secrets its fetch log redacts, longest first; cleared when a connection is learned. */
|
|
198
|
+
secretValues?: string[];
|
|
199
|
+
/** The isolate's refresh count `secretValues` was read at. */
|
|
200
|
+
secretsAt?: number;
|
|
201
|
+
/** How much of its fetch log is bodies so far. */
|
|
202
|
+
fetchLogChars?: number;
|
|
158
203
|
/** Filled by run capture, flushed to the platform once the response ends. */
|
|
159
204
|
runs: RunRecord[];
|
|
160
205
|
/** The data calls this request made, flushed with its runs. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const ZITEJS_VERSION = "0.10.0-start.
|
|
1
|
+
export declare const ZITEJS_VERSION = "0.10.0-start.13";
|
|
@@ -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.13";
|