zitejs 0.9.142 → 0.9.143

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/cjs/integrations/rules.d.ts +8 -0
  2. package/dist/cjs/integrations/rules.js +19 -0
  3. package/dist/cjs/meta/index.d.ts +14 -0
  4. package/dist/cjs/meta/index.js +12 -0
  5. package/dist/cjs/notifications/index.d.ts +25 -0
  6. package/dist/cjs/notifications/index.js +12 -0
  7. package/dist/esm/integrations/rules.d.ts +8 -0
  8. package/dist/esm/integrations/rules.js +19 -0
  9. package/dist/esm/meta/index.d.ts +14 -0
  10. package/dist/esm/meta/index.js +8 -0
  11. package/dist/esm/notifications/index.d.ts +25 -0
  12. package/dist/esm/notifications/index.js +8 -0
  13. package/package.json +1 -1
  14. package/dist/cjs/start/describeBuilt.d.ts +0 -3
  15. package/dist/cjs/start/describeBuilt.js +0 -106
  16. package/dist/cjs/start/entries.d.ts +0 -20
  17. package/dist/cjs/start/entries.js +0 -104
  18. package/dist/cjs/start/problems.d.ts +0 -19
  19. package/dist/cjs/start/problems.js +0 -11
  20. package/dist/esm/start/describe.d.ts +0 -64
  21. package/dist/esm/start/describe.js +0 -176
  22. package/dist/esm/start/describeBuilt.d.ts +0 -3
  23. package/dist/esm/start/describeBuilt.js +0 -66
  24. package/dist/esm/start/descriptions.d.ts +0 -4
  25. package/dist/esm/start/descriptions.js +0 -47
  26. package/dist/esm/start/entries.d.ts +0 -20
  27. package/dist/esm/start/entries.js +0 -93
  28. package/dist/esm/start/host.d.ts +0 -3
  29. package/dist/esm/start/host.js +0 -4
  30. package/dist/esm/start/index.d.ts +0 -79
  31. package/dist/esm/start/index.js +0 -157
  32. package/dist/esm/start/instance.d.ts +0 -12
  33. package/dist/esm/start/instance.js +0 -73
  34. package/dist/esm/start/problems.d.ts +0 -19
  35. package/dist/esm/start/problems.js +0 -8
  36. package/dist/esm/start/registration.d.ts +0 -13
  37. package/dist/esm/start/registration.js +0 -21
  38. package/dist/esm/start/router.d.ts +0 -17
  39. package/dist/esm/start/router.js +0 -37
  40. package/dist/esm/start/runLog.d.ts +0 -5
  41. package/dist/esm/start/runLog.js +0 -18
  42. package/dist/esm/start/scope.d.ts +0 -15
  43. package/dist/esm/start/scope.js +0 -58
  44. package/dist/esm/start/server.d.ts +0 -51
  45. package/dist/esm/start/server.js +0 -215
  46. package/dist/esm/start/types.d.ts +0 -94
  47. package/dist/esm/start/types.js +0 -1
  48. package/dist/esm/start/vite.d.ts +0 -9
  49. package/dist/esm/start/vite.js +0 -144
  50. package/dist/esm/start/worker.d.ts +0 -11
  51. package/dist/esm/start/worker.js +0 -20
  52. package/dist/esm/start/workflow.d.ts +0 -50
  53. package/dist/esm/start/workflow.js +0 -13
  54. package/dist/esm/start/workflow.test-d.d.ts +0 -1
  55. package/dist/esm/start/workflow.test-d.js +0 -39
  56. package/dist/esm/start/workflowModules.d.ts +0 -1
  57. package/dist/esm/start/workflowModules.js +0 -3
  58. package/dist/esm/start/workflowRun.d.ts +0 -80
  59. package/dist/esm/start/workflowRun.js +0 -107
@@ -1,12 +0,0 @@
1
- import { type AnySerializationAdapter } from "@tanstack/react-router";
2
- import type { SSROption } from "@tanstack/router-core";
3
- import type { AnyFunctionMiddleware, AnyRequestMiddleware, StartInstance } from "@tanstack/start-client-core";
4
- import { ZiteError } from "./index.js";
5
- /**
6
- * The start instance every Start app builds with: the platform's middleware and
7
- * adapters first, then whatever the app's own `src/start.ts` sets.
8
- */
9
- export declare function createZiteStart<const TAdapters extends ReadonlyArray<AnySerializationAdapter> = [], TDefaultSsr extends SSROption = SSROption, const TRequest extends ReadonlyArray<AnyRequestMiddleware> = [], const TFunction extends ReadonlyArray<AnyFunctionMiddleware> = []>(app?: StartInstance<TAdapters, TDefaultSsr, TRequest, TFunction>): StartInstance<readonly [import("@tanstack/react-router").SerializationAdapter<ZiteError, {
10
- code: "BAD_REQUEST" | "UNAUTHORIZED" | "FORBIDDEN" | "NOT_FOUND" | "CONFLICT" | "RATE_LIMITED" | "INTERNAL_ERROR" | "INVALID_INPUT";
11
- message: string;
12
- }, never>, ...TAdapters], TDefaultSsr, readonly [import("@tanstack/react-start").RequestMiddlewareAfterServer<{}, undefined, undefined>, ...TRequest], readonly [import("@tanstack/react-start").FunctionMiddlewareAfterClient<{}, unknown, undefined, undefined, undefined>, import("@tanstack/react-start").FunctionMiddlewareAfterServer<{}, unknown, undefined, undefined, undefined, undefined, undefined>, ...TFunction]>;
@@ -1,73 +0,0 @@
1
- // For the generated `.zite/start.ts` only. Apps put their own options in
2
- // `src/start.ts`, which the generated instance composes after the platform's.
3
- import { createCsrfMiddleware, createMiddleware, createStart } from "@tanstack/react-start";
4
- import { createSerializationAdapter, redirect } from "@tanstack/react-router";
5
- import { isSignInRequired, loginHref, ZiteError } from "./index.js";
6
- import { errorOf, preview } from "./runLog.js";
7
- import { getHost, inScope } from "./scope.js";
8
- // TanStack drops its default CSRF check once an app has a start instance.
9
- const csrf = createCsrfMiddleware({ filter: (ctx) => ctx.handlerType === "serverFn" });
10
- // The host answers a signed-out call with a plain 401, the app with a ZiteError;
11
- // either way the browser goes to sign in rather than showing an error.
12
- const signInOn401 = createMiddleware({ type: "function" }).client(async ({ next }) => {
13
- try {
14
- return await next();
15
- }
16
- catch (error) {
17
- if (!isSignInRequired(error))
18
- throw error;
19
- const href = loginHref();
20
- if (typeof window !== "undefined")
21
- window.location.assign(href);
22
- throw redirect({ href, reloadDocument: true });
23
- }
24
- });
25
- // Records each call for the run log. The host's own record of the request is the
26
- // floor; this adds what only the app sees: decoded input, output and errors.
27
- const runCapture = createMiddleware({ type: "function" }).server(async ({ next, data, serverFnMeta }) => {
28
- const runs = getHost()?.runs;
29
- const startedAt = Date.now();
30
- const record = (outcome) => runs?.push({
31
- fn: serverFnMeta.id,
32
- name: serverFnMeta.name,
33
- nested: inScope(),
34
- startedAt,
35
- durationMs: Date.now() - startedAt,
36
- input: preview(data),
37
- ...("error" in outcome ? { error: errorOf(outcome.error) } : { output: preview(outcome.output) }),
38
- });
39
- try {
40
- const out = await next();
41
- const failed = out.error;
42
- record(failed === undefined ? { output: out.result } : { error: failed });
43
- return out;
44
- }
45
- catch (error) {
46
- record({ error });
47
- throw error;
48
- }
49
- });
50
- /** Carries a ZiteError to the browser as itself, so `instanceof ZiteError` and `.code` work there. */
51
- const ziteErrorAdapter = createSerializationAdapter({
52
- key: "zite-error",
53
- test: (value) => value instanceof ZiteError,
54
- toSerializable: (error) => ({ code: error.code, message: error.message }),
55
- fromSerializable: ({ code, message }) => new ZiteError(code, message),
56
- });
57
- /**
58
- * The start instance every Start app builds with: the platform's middleware and
59
- * adapters first, then whatever the app's own `src/start.ts` sets.
60
- */
61
- export function createZiteStart(app) {
62
- return createStart(async () => {
63
- const own = app ? await app.getOptions() : undefined;
64
- // Spreading the app's optional arrays loses their tuple types, which the
65
- // route tree registers for typed context and serialization.
66
- return {
67
- ...own,
68
- requestMiddleware: [csrf, ...(own?.requestMiddleware ?? [])],
69
- functionMiddleware: [signInOn401, runCapture, ...(own?.functionMiddleware ?? [])],
70
- serializationAdapters: [ziteErrorAdapter, ...(own?.serializationAdapters ?? [])],
71
- };
72
- });
73
- }
@@ -1,19 +0,0 @@
1
- type WithProblems = {
2
- problems: string[];
3
- };
4
- type Described = {
5
- functions: Array<WithProblems & {
6
- id: string;
7
- file?: string;
8
- }>;
9
- routes: Array<WithProblems & {
10
- path: string;
11
- }>;
12
- workflows?: Array<WithProblems & {
13
- id: string;
14
- file: string;
15
- }>;
16
- };
17
- /** Every problem in a description, one line each, named by its rule. Any of them fails the build. */
18
- export declare function descriptionProblems(description: Described): string[];
19
- export {};
@@ -1,8 +0,0 @@
1
- /** Every problem in a description, one line each, named by its rule. Any of them fails the build. */
2
- export function descriptionProblems(description) {
3
- return [
4
- ...description.functions.flatMap((f) => f.problems.map((p) => `function ${f.id}${f.file ? ` (${f.file})` : ""}: ${p}`)),
5
- ...description.routes.flatMap((r) => r.problems.map((p) => `route ${r.path}: ${p}`)),
6
- ...(description.workflows ?? []).flatMap((w) => w.problems.map((p) => `workflow ${w.id} (${w.file}): ${p}`)),
7
- ];
8
- }
@@ -1,13 +0,0 @@
1
- import type { LoadedWorkflow } from "./workflowRun.js";
2
- export type RegistrationResult = {
3
- ok: true;
4
- output: unknown;
5
- } | {
6
- ok: false;
7
- error: {
8
- code: string;
9
- message: string;
10
- };
11
- };
12
- /** Undefined when the workflow has no such function. */
13
- export declare function runRegistration(workflows: LoadedWorkflow[], name: string, op: unknown, input: unknown): Promise<RegistrationResult | undefined>;
@@ -1,21 +0,0 @@
1
- // Server-only: a provider trigger's `subscribe` or `unsubscribe`, which the host
2
- // runs at publish. Not a run: nothing is logged, since what `subscribe` returns
3
- // carries the provider's signing secret.
4
- import { errorOf } from "./runLog.js";
5
- import { runInScope } from "./scope.js";
6
- /** Undefined when the workflow has no such function. */
7
- export async function runRegistration(workflows, name, op, input) {
8
- const trigger = workflows.find((w) => w.id === name)?.workflow.config.trigger;
9
- if (!trigger || !("subscribe" in trigger))
10
- return undefined;
11
- const fn = op === "subscribe" ? trigger.subscribe : op === "unsubscribe" ? trigger.unsubscribe : undefined;
12
- if (typeof fn !== "function")
13
- return undefined;
14
- try {
15
- const output = await runInScope({ kind: "workflow", user: null }, () => fn(input));
16
- return { ok: true, output: output ?? null };
17
- }
18
- catch (error) {
19
- return { ok: false, error: errorOf(error) };
20
- }
21
- }
@@ -1,17 +0,0 @@
1
- import { type AnyRoute, type RouterConstructorOptions, type RouterHistory } from "@tanstack/react-router";
2
- import type { User } from "./types.js";
3
- /** The router context every Zite app's root route declares: `createRootRouteWithContext<ZiteRouterContext>()`. */
4
- export type ZiteRouterContext = {
5
- user: User | null;
6
- };
7
- type Dehydrated = {
8
- user: User | null;
9
- };
10
- type ZiteRouterOptions<TRouteTree extends AnyRoute> = Omit<RouterConstructorOptions<TRouteTree, "never", false, RouterHistory, Dehydrated>, "context" | "dehydrate" | "hydrate">;
11
- /**
12
- * The router for `src/router.tsx`. The signed-in user is in `context.user`: on
13
- * the server from the platform, in the browser from the page it rendered, so
14
- * no request asks for it again.
15
- */
16
- export declare function createZiteRouter<TRouteTree extends AnyRoute>(options: ZiteRouterOptions<TRouteTree>): import("@tanstack/router-core").RouterCore<TRouteTree, "never", false, RouterHistory, Dehydrated>;
17
- export {};
@@ -1,37 +0,0 @@
1
- import { createElement as h } from "react";
2
- import { createRouter, Link, useRouter, } from "@tanstack/react-router";
3
- import { currentHost } from "./host.js";
4
- const box = { maxWidth: 480, margin: "15vh auto", padding: 24, fontFamily: "system-ui, sans-serif", textAlign: "center" };
5
- function ZiteErrorComponent({ error }) {
6
- const router = useRouter();
7
- const message = error instanceof Error && error.name === "ZiteError" ? error.message : "Something went wrong.";
8
- return h("div", { role: "alert", style: box }, h("p", { style: { fontWeight: 600 } }, message), h("button", { type: "button", onClick: () => router.invalidate() }, "Try again"));
9
- }
10
- function ZiteNotFound() {
11
- return h("div", { style: box }, h("p", { style: { fontWeight: 600 } }, "Page not found"), h(Link, { to: "/" }, "Go home"));
12
- }
13
- /**
14
- * The router for `src/router.tsx`. The signed-in user is in `context.user`: on
15
- * the server from the platform, in the browser from the page it rendered, so
16
- * no request asks for it again.
17
- */
18
- export function createZiteRouter(options) {
19
- const context = { user: currentHost()?.user ?? null };
20
- const router = createRouter({
21
- defaultPreload: "intent",
22
- defaultPreloadStaleTime: 0,
23
- scrollRestoration: true,
24
- defaultErrorComponent: ZiteErrorComponent,
25
- defaultNotFoundComponent: ZiteNotFound,
26
- ...options,
27
- context,
28
- dehydrate: () => ({ user: context.user }),
29
- hydrate: (dehydrated) => {
30
- context.user = dehydrated.user;
31
- },
32
- });
33
- const host = currentHost();
34
- if (host)
35
- host.router = router;
36
- return router;
37
- }
@@ -1,5 +0,0 @@
1
- export declare const preview: (value: unknown) => string | undefined;
2
- export declare const errorOf: (error: unknown) => {
3
- code: string;
4
- message: string;
5
- };
@@ -1,18 +0,0 @@
1
- import { ZiteError } from "./index.js";
2
- const PREVIEW_LIMIT = 8_000;
3
- export const preview = (value) => {
4
- if (value === undefined)
5
- return undefined;
6
- if (typeof value === "object" && value !== null && Symbol.asyncIterator in value)
7
- return "[stream]";
8
- try {
9
- const json = JSON.stringify(value);
10
- return json && json.length > PREVIEW_LIMIT ? `${json.slice(0, PREVIEW_LIMIT)}…` : json;
11
- }
12
- catch {
13
- return "[unserializable]";
14
- }
15
- };
16
- export const errorOf = (error) => error instanceof ZiteError
17
- ? { code: error.code, message: error.message }
18
- : { code: "INTERNAL_ERROR", message: error instanceof Error ? error.message : String(error) };
@@ -1,15 +0,0 @@
1
- import type { AccessKind, User, ZiteRequestContext } from "./types.js";
2
- export type { AccessKind } from "./types.js";
3
- export type AccessScope = {
4
- kind: AccessKind;
5
- user: User | null;
6
- };
7
- export declare const runInScope: <T>(scope: AccessScope, fn: () => T) => T;
8
- /** True inside another server function's scope, i.e. a nested server-side call. */
9
- export declare const inScope: () => boolean;
10
- export declare class NoAccessDecisionError extends Error {
11
- constructor(what: string);
12
- }
13
- export declare const requireScope: (what: string) => AccessScope;
14
- export declare const runWithHost: <T>(host: ZiteRequestContext, fn: () => T) => T;
15
- export declare const getHost: () => ZiteRequestContext | undefined;
@@ -1,58 +0,0 @@
1
- // Server-only. Only zitejs's access middleware opens an access scope, and the
2
- // platform transport refuses calls outside one, so code with no access decision
3
- // can't reach data, integrations or email however it was written.
4
- import { AsyncLocalStorage } from "node:async_hooks";
5
- import { HOST_STORE_KEY } from "./host.js";
6
- // Keyed on globalThis so two copies of zitejs in one bundle share the stores.
7
- const stores = globalThis;
8
- const scopeStorage = (stores[Symbol.for("zitejs:access-scope")] ??=
9
- new AsyncLocalStorage());
10
- const hostStorage = (stores[HOST_STORE_KEY] ??=
11
- new AsyncLocalStorage());
12
- const isAsyncIterable = (value) => typeof value === "object" &&
13
- value !== null &&
14
- Symbol.asyncIterator in value &&
15
- !(value instanceof ReadableStream);
16
- // TanStack resumes a streamed async generator from its serializer, outside the
17
- // middleware's async context, so each step re-enters the scope it started in.
18
- const rescope = (scope, source) => (async function* () {
19
- const iterator = source[Symbol.asyncIterator]();
20
- try {
21
- while (true) {
22
- const step = await scopeStorage.run(scope, () => iterator.next());
23
- if (step.done)
24
- return step.value;
25
- yield step.value;
26
- }
27
- }
28
- finally {
29
- await scopeStorage.run(scope, async () => iterator.return?.());
30
- }
31
- })();
32
- const rescopeResult = (scope, out) => {
33
- const result = out?.result;
34
- return isAsyncIterable(result) ? { ...out, result: rescope(scope, result) } : out;
35
- };
36
- export const runInScope = (scope, fn) => {
37
- const out = scopeStorage.run(scope, fn);
38
- return (out instanceof Promise
39
- ? out.then((value) => rescopeResult(scope, value))
40
- : rescopeResult(scope, out));
41
- };
42
- /** True inside another server function's scope, i.e. a nested server-side call. */
43
- export const inScope = () => scopeStorage.getStore() !== undefined;
44
- export class NoAccessDecisionError extends Error {
45
- constructor(what) {
46
- super(`${what} ran outside an access decision. Define server functions with signedInFn or publicFn, ` +
47
- "and give server routes signedIn, anyone or webhook middleware. Loaders get data by calling a server function.");
48
- this.name = "NoAccessDecisionError";
49
- }
50
- }
51
- export const requireScope = (what) => {
52
- const scope = scopeStorage.getStore();
53
- if (!scope)
54
- throw new NoAccessDecisionError(what);
55
- return scope;
56
- };
57
- export const runWithHost = (host, fn) => hostStorage.run(host, fn);
58
- export const getHost = () => hostStorage.getStore();
@@ -1,51 +0,0 @@
1
- import type { HostProps, PlatformBinding, RenderWatch, ZiteRequestContext } from "./types.js";
2
- export { runWithHost } from "./scope.js";
3
- export { describeApp } from "./describe.js";
4
- export type { HostProps, PlatformBinding, PlatformCallInput, PlatformCallResult, PlatformSession, RenderWatch, RunRecord } from "./types.js";
5
- /**
6
- * Routes every zitejs data, integration and email call to this request's
7
- * platform, on one session, and only inside an access scope. The access kind
8
- * travels with the call so the platform can record what a page read and narrow
9
- * anonymous reads.
10
- */
11
- export declare function installPlatformTransport(): void;
12
- /** What the host dispatched this request with. Headers are ignored: props are the only thing it alone can set. */
13
- export declare function hostContextFromProps(request: Request, props: HostProps): ZiteRequestContext;
14
- /**
15
- * A request the host didn't dispatch: `zitejs describe` under `vite preview`,
16
- * and tests, which pass their own platform. Deployed builds have no public URL,
17
- * so in production every request carries props.
18
- */
19
- export declare function hostContextFromRequest(request: Request, platform?: PlatformBinding): ZiteRequestContext;
20
- /** Every server function TanStack compiled, written in by `zite()` at build. Empty in dev, where TanStack doesn't call its id hook. */
21
- export declare const builtFunctionIds: () => string[];
22
- /** Each server function's `description` option, written in by `zite()` at build. */
23
- export declare const builtFunctionDescriptions: () => Record<string, string>;
24
- type Fetch = (request: Request, ...rest: unknown[]) => Promise<Response> | Response;
25
- type WorkerContext = {
26
- waitUntil(promise: Promise<unknown>): void;
27
- };
28
- export type ZiteServerOptions = {
29
- handler: {
30
- fetch: Fetch;
31
- };
32
- getRouter: () => unknown;
33
- platform?: PlatformBinding;
34
- };
35
- type HostedContext = WorkerContext & {
36
- props?: Partial<HostProps>;
37
- };
38
- export type ZiteHandlers = {
39
- fetch: Fetch;
40
- /** A page the host may cache: as `fetch`, with every read and outbound fetch reported to `watch`. */
41
- render(request: Request, ctx: HostedContext, watch: RenderWatch): Promise<Response>;
42
- };
43
- /**
44
- * The handlers of a Start app's server entry. A request the host dispatched
45
- * carries its identity and platform in props; anything else (describe under
46
- * `vite preview`, tests) reads `x-zite-*` headers and uses `platform` if one
47
- * is given.
48
- */
49
- export declare function createZiteHandlers(options: ZiteServerOptions): ZiteHandlers;
50
- /** The fetch handler alone, for entries that take no RPC. */
51
- export declare function createZiteFetch(options: ZiteServerOptions): Fetch;
@@ -1,215 +0,0 @@
1
- import { ZiteError, ZITE_ERROR_STATUS } from "./index.js";
2
- import { describeApp } from "./describe.js";
3
- import { runRegistration } from "./registration.js";
4
- import { preview } from "./runLog.js";
5
- import { getHost, requireScope, runWithHost } from "./scope.js";
6
- import { loadWorkflows, loggedInput, runWorkflow } from "./workflowRun.js";
7
- export { runWithHost } from "./scope.js";
8
- export { describeApp } from "./describe.js";
9
- /**
10
- * Routes every zitejs data, integration and email call to this request's
11
- * platform, on one session, and only inside an access scope. The access kind
12
- * travels with the call so the platform can record what a page read and narrow
13
- * anonymous reads.
14
- */
15
- export function installPlatformTransport() {
16
- const sdkCall = async (integrationId, className, methodName, params) => {
17
- const scope = requireScope(`${className}.${methodName}`);
18
- const host = getHost();
19
- // Fails closed: no platform means no platform access, never a fallback.
20
- if (!host?.platform)
21
- throw new Error("The Zite platform binding is missing");
22
- host.session ??= Promise.resolve(host.watch ? host.platform.session(host.watch) : host.platform.session());
23
- const result = await (await host.session).call({ integrationId, className, methodName, params, access: scope.kind });
24
- if ("value" in result)
25
- return result.value;
26
- const { code, message } = result.error;
27
- throw new ZiteError(code in ZITE_ERROR_STATUS ? code : "INTERNAL_ERROR", message);
28
- };
29
- globalThis.__wrapSdkCall = sdkCall;
30
- watchOutboundFetches();
31
- }
32
- let fetchesWatched = false;
33
- const fetchedHost = (input) => {
34
- try {
35
- return new URL(input instanceof Request ? input.url : input).host;
36
- }
37
- catch {
38
- return "?";
39
- }
40
- };
41
- // A page that fetched depends on more than the reads the host saw, so the host
42
- // must hear of it before the fetch goes out. Next patches fetch for the same reason.
43
- const watchOutboundFetches = () => {
44
- if (fetchesWatched)
45
- return;
46
- fetchesWatched = true;
47
- const nativeFetch = globalThis.fetch.bind(globalThis);
48
- globalThis.fetch = async (input, init) => {
49
- const watch = getHost()?.watch;
50
- if (watch)
51
- await watch.uncacheable(`fetch ${fetchedHost(input)}`);
52
- return nativeFetch(input, init);
53
- };
54
- };
55
- const decodeUser = (value) => JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(value.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0))));
56
- const INVOKE_KINDS = new Set(["webhook", "describe", "workflow", "register", "warm"]);
57
- /** What the host dispatched this request with. Headers are ignored: props are the only thing it alone can set. */
58
- export function hostContextFromProps(request, props) {
59
- return {
60
- requestId: props.requestId,
61
- url: request.url,
62
- user: props.user,
63
- mode: props.mode,
64
- invoke: props.invoke,
65
- runs: [],
66
- platform: props.platform,
67
- };
68
- }
69
- /**
70
- * A request the host didn't dispatch: `zitejs describe` under `vite preview`,
71
- * and tests, which pass their own platform. Deployed builds have no public URL,
72
- * so in production every request carries props.
73
- */
74
- export function hostContextFromRequest(request, platform) {
75
- const { headers } = request;
76
- const user = headers.get("x-zite-user");
77
- const [kind, name = ""] = headers.get("x-zite-invoke")?.split(":") ?? [];
78
- return {
79
- requestId: headers.get("x-zite-request-id") ?? crypto.randomUUID(),
80
- url: request.url,
81
- user: user ? decodeUser(user) : null,
82
- mode: "live",
83
- invoke: INVOKE_KINDS.has(kind) ? { kind: kind, name } : null,
84
- runs: [],
85
- platform,
86
- };
87
- }
88
- /** Every server function TanStack compiled, written in by `zite()` at build. Empty in dev, where TanStack doesn't call its id hook. */
89
- export const builtFunctionIds = () => "__ZITE_FN_IDS__".split(",").filter((id) => id && !id.startsWith("__ZITE"));
90
- const parseDescriptions = (json) => {
91
- try {
92
- return JSON.parse(json);
93
- }
94
- catch {
95
- return {};
96
- }
97
- };
98
- /** Each server function's `description` option, written in by `zite()` at build. */
99
- export const builtFunctionDescriptions = () => parseDescriptions("__ZITE_FN_DESCRIPTIONS__");
100
- // Server functions also run while a streamed page renders, so the run log is
101
- // complete only once the body has been read to the end.
102
- const onBodyEnd = (response, done) => {
103
- if (!response.body) {
104
- done();
105
- return response;
106
- }
107
- return new Response(response.body.pipeThrough(new TransformStream({ flush: done })), response);
108
- };
109
- const INVOKE_PATH = "/_zite/invoke";
110
- // The host sends `{ input }`; a platform-fired run's input carries the runner's
111
- // `__cron`, `__poll` or `__webhook` state, exactly as for classic endpoints.
112
- async function invokeWorkflow(request, host, workflows) {
113
- if (request.method !== "POST" || new URL(request.url).pathname !== INVOKE_PATH)
114
- return new Response("Not found", { status: 404 });
115
- const name = host.invoke?.name;
116
- const found = workflows.find((w) => w.id === name);
117
- if (!found)
118
- return Response.json({ ok: false, error: { code: "NOT_FOUND", message: `No workflow named ${name}` } }, { status: 404 });
119
- const input = (await request.json().catch(() => ({})))?.input;
120
- const startedAt = Date.now();
121
- const result = await runWorkflow(found.workflow, input);
122
- host.runs.push({
123
- fn: `workflow:${found.id}`,
124
- name: found.id,
125
- nested: false,
126
- startedAt,
127
- durationMs: Date.now() - startedAt,
128
- input: preview(loggedInput(input)),
129
- ...(result.ok ? { output: preview(result.output) } : { error: result.error }),
130
- });
131
- return Response.json(result, { status: result.ok ? 200 : 500 });
132
- }
133
- // The host sends `{ op, input }`: `subscribe` with the platform's notification
134
- // URL and secret, or `unsubscribe` with what `subscribe` returned.
135
- async function invokeRegistration(request, host, workflows) {
136
- if (request.method !== "POST" || new URL(request.url).pathname !== INVOKE_PATH)
137
- return new Response("Not found", { status: 404 });
138
- const name = host.invoke?.name ?? "";
139
- const body = (await request.json().catch(() => null));
140
- const result = await runRegistration(workflows, name, body?.op, body?.input);
141
- if (!result) {
142
- return Response.json({ ok: false, error: { code: "NOT_FOUND", message: `${name} has no ${String(body?.op)}` } }, { status: 404 });
143
- }
144
- return Response.json(result, { status: result.ok ? 200 : 500 });
145
- }
146
- const isZiteError = (error) => error instanceof Error && error.name === "ZiteError" && error.code in ZITE_ERROR_STATUS;
147
- // TanStack renders any loader error as a 500. A page refused with a ZiteError
148
- // (a missing role, a record that isn't there) answers with that error's status.
149
- const pageStatus = (host) => {
150
- const matches = host.router?.state?.matches;
151
- const failed = matches?.find((match) => match.status === "error")?.error;
152
- return isZiteError(failed) ? ZITE_ERROR_STATUS[failed.code] : undefined;
153
- };
154
- /**
155
- * The handlers of a Start app's server entry. A request the host dispatched
156
- * carries its identity and platform in props; anything else (describe under
157
- * `vite preview`, tests) reads `x-zite-*` headers and uses `platform` if one
158
- * is given.
159
- */
160
- export function createZiteHandlers(options) {
161
- installPlatformTransport();
162
- // Loaded on first use, so a page request never pays for importing workflow modules.
163
- let workflows;
164
- const loadedWorkflows = () => (workflows ??= loadWorkflows());
165
- const serve = async (request, ctx, watch) => {
166
- const host = ctx?.props?.platform
167
- ? hostContextFromProps(request, ctx.props)
168
- : hostContextFromRequest(request, options.platform);
169
- if (watch)
170
- host.watch = watch;
171
- // Starting this isolate was the point: the platform warms a build before its
172
- // first visitors, and no app code runs.
173
- if (host.invoke?.kind === "warm")
174
- return new Response(null, { status: 204 });
175
- if (host.invoke?.kind === "describe") {
176
- return Response.json(await describeApp({
177
- fnIds: builtFunctionIds(),
178
- descriptions: builtFunctionDescriptions(),
179
- getRouter: options.getRouter,
180
- request,
181
- workflows: await loadedWorkflows(),
182
- }));
183
- }
184
- let response = await runWithHost(host, async () => host.invoke?.kind === "workflow"
185
- ? invokeWorkflow(request, host, await loadedWorkflows())
186
- : host.invoke?.kind === "register"
187
- ? invokeRegistration(request, host, await loadedWorkflows())
188
- : options.handler.fetch(request));
189
- const status = response.status === 500 ? pageStatus(host) : undefined;
190
- if (status)
191
- response = new Response(response.body, { status, headers: response.headers });
192
- return onBodyEnd(response, () => {
193
- const { platform } = host;
194
- if (!host.runs.length || !platform)
195
- return;
196
- const batch = { runs: host.runs };
197
- // On the request's session when it has one: one call, not another binding round trip.
198
- const flushed = (async () => {
199
- const session = host.session ? await host.session : undefined;
200
- await (session?.runs ? session.runs(batch) : platform.runs(batch));
201
- })().catch((error) => {
202
- console.error("[zite] couldn't record runs", error);
203
- });
204
- ctx?.waitUntil(flushed);
205
- });
206
- };
207
- return {
208
- fetch: (request, ...rest) => serve(request, rest[1]),
209
- render: (request, ctx, watch) => serve(request, ctx, watch),
210
- };
211
- }
212
- /** The fetch handler alone, for entries that take no RPC. */
213
- export function createZiteFetch(options) {
214
- return createZiteHandlers(options).fetch;
215
- }