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.
@@ -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
- let fetchesWatched = false;
71
- const fetchedHost = (input) => {
72
- try {
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
- if (fetchesWatched)
97
+ const marks = globalThis;
98
+ if (marks[WATCHED_KEY])
83
99
  return;
84
- fetchesWatched = true;
85
- const nativeFetch = globalThis.fetch.bind(globalThis);
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 === "register"
287
- ? invokeRegistration(request, host, await loadedWorkflows())
288
- : host.invoke?.kind === "authHook"
289
- ? invokeAuthHook(request, host)
290
- : options.handler.fetch(request));
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 = { runs: host.runs, ...(host.calls?.length && { calls: host.calls }) };
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, secrets as placeholders egress swaps but S3's, then the user's own secrets. */
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.10";
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.10";
2
+ export const ZITEJS_VERSION = "0.10.0-start.13";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zitejs",
3
- "version": "0.10.0-start.10",
3
+ "version": "0.10.0-start.13",
4
4
  "description": "The Zite framework — build apps on Zite Database",
5
5
  "type": "module",
6
6
  "sideEffects": false,