zitejs 0.9.138 → 0.10.0-start.1
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/cli.js +10 -3
- package/dist/cjs/dev/index.js +3 -0
- package/dist/cjs/start/describeBuilt.d.ts +3 -0
- package/dist/cjs/start/describeBuilt.js +106 -0
- package/dist/cjs/start/entries.d.ts +11 -0
- package/dist/cjs/start/entries.js +64 -0
- package/dist/esm/cli.js +10 -3
- package/dist/esm/dev/index.js +3 -0
- package/dist/esm/start/describe.d.ts +40 -0
- package/dist/esm/start/describe.js +122 -0
- package/dist/esm/start/describeBuilt.d.ts +3 -0
- package/dist/esm/start/describeBuilt.js +66 -0
- package/dist/esm/start/entries.d.ts +11 -0
- package/dist/esm/start/entries.js +56 -0
- package/dist/esm/start/host.d.ts +3 -0
- package/dist/esm/start/host.js +4 -0
- package/dist/esm/start/index.d.ts +64 -0
- package/dist/esm/start/index.js +123 -0
- package/dist/esm/start/instance.d.ts +12 -0
- package/dist/esm/start/instance.js +89 -0
- package/dist/esm/start/router.d.ts +17 -0
- package/dist/esm/start/router.js +34 -0
- package/dist/esm/start/scope.d.ts +15 -0
- package/dist/esm/start/scope.js +58 -0
- package/dist/esm/start/server.d.ts +47 -0
- package/dist/esm/start/server.js +81 -0
- package/dist/esm/start/types.d.ts +32 -0
- package/dist/esm/start/types.js +1 -0
- package/dist/esm/start/vite.d.ts +9 -0
- package/dist/esm/start/vite.js +114 -0
- package/package.json +53 -2
- package/dist/cjs/meta/index.d.ts +0 -14
- package/dist/cjs/meta/index.js +0 -12
- package/dist/cjs/notifications/index.d.ts +0 -25
- package/dist/cjs/notifications/index.js +0 -12
- package/dist/esm/meta/index.d.ts +0 -14
- package/dist/esm/meta/index.js +0 -8
- package/dist/esm/notifications/index.d.ts +0 -25
- package/dist/esm/notifications/index.js +0 -8
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { User } from "./types.js";
|
|
2
|
+
export type { User, ZiteRequestContext } from "./types.js";
|
|
3
|
+
export { createZiteRouter, type ZiteRouterContext } from "./router.js";
|
|
4
|
+
declare module "@tanstack/start-client-core" {
|
|
5
|
+
interface ServerFnOptions {
|
|
6
|
+
/** What this function does, in a sentence. Shown in the editor. */
|
|
7
|
+
description?: string;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
declare const STATUS: {
|
|
11
|
+
readonly BAD_REQUEST: 400;
|
|
12
|
+
readonly UNAUTHORIZED: 401;
|
|
13
|
+
readonly FORBIDDEN: 403;
|
|
14
|
+
readonly NOT_FOUND: 404;
|
|
15
|
+
readonly CONFLICT: 409;
|
|
16
|
+
readonly RATE_LIMITED: 429;
|
|
17
|
+
readonly INTERNAL_ERROR: 500;
|
|
18
|
+
};
|
|
19
|
+
export type ZiteErrorCode = keyof typeof STATUS;
|
|
20
|
+
/** Fail with a status: `throw new ZiteError('FORBIDDEN', 'Only the owner can edit this')`. */
|
|
21
|
+
export declare class ZiteError extends Error {
|
|
22
|
+
readonly code: ZiteErrorCode;
|
|
23
|
+
constructor(code: ZiteErrorCode, message?: string);
|
|
24
|
+
}
|
|
25
|
+
export declare const hasRole: (user: User, role: string) => boolean;
|
|
26
|
+
/** The message a server function fails with when it needs a session; the platform's client middleware sends the user to sign in. */
|
|
27
|
+
export declare const SIGN_IN_REQUIRED = "ZITE_SIGN_IN_REQUIRED";
|
|
28
|
+
export declare const isSignInRequired: (error: unknown) => boolean;
|
|
29
|
+
/**
|
|
30
|
+
* The platform's sign-in page, returning to `redirectTo` (default: the current
|
|
31
|
+
* page). For guards: `throw redirect({ href: loginHref(), reloadDocument: true })`.
|
|
32
|
+
*/
|
|
33
|
+
export declare function loginHref(redirectTo?: string): string;
|
|
34
|
+
/** What a zitejs middleware means, for describe. Keyed by identity, so composing or re-exporting keeps it. */
|
|
35
|
+
export type ZiteMiddlewareKind = {
|
|
36
|
+
kind: "signedIn" | "anyone" | "webhook" | "factory";
|
|
37
|
+
} | {
|
|
38
|
+
kind: "role";
|
|
39
|
+
role: string;
|
|
40
|
+
};
|
|
41
|
+
/** @internal */
|
|
42
|
+
export declare const ziteMiddlewareKind: (middleware: unknown) => ZiteMiddlewareKind | undefined;
|
|
43
|
+
/** Only signed-in users. Handlers get `context.user: User`. */
|
|
44
|
+
export declare const signedIn: import("@tanstack/react-start").RequestMiddlewareAfterServer<{}, undefined, {
|
|
45
|
+
user: User;
|
|
46
|
+
}>;
|
|
47
|
+
/** Anyone, signed in or not. Handlers get `context.user: User | null`. */
|
|
48
|
+
export declare const anyone: import("@tanstack/react-start").RequestMiddlewareAfterServer<{}, undefined, {
|
|
49
|
+
user: User | null;
|
|
50
|
+
}>;
|
|
51
|
+
/** Webhooks delivered by the platform, which already checked the URL token. */
|
|
52
|
+
export declare const webhook: import("@tanstack/react-start").RequestMiddlewareAfterServer<{}, undefined, undefined>;
|
|
53
|
+
/** Signed-in users with this role. */
|
|
54
|
+
export declare const role: (name: string) => import("@tanstack/react-start").RequestMiddlewareAfterServer<{}, readonly [import("@tanstack/react-start").RequestMiddlewareAfterServer<{}, undefined, {
|
|
55
|
+
user: User;
|
|
56
|
+
}>], undefined>;
|
|
57
|
+
declare const compiled: import("@tanstack/react-start").FunctionMiddlewareAfterClient<{}, unknown, undefined, undefined, undefined>;
|
|
58
|
+
declare const builder: import("@tanstack/react-start").ServerFnBuilder<import("@tanstack/react-start").Register, import("@tanstack/react-start").Method, true>;
|
|
59
|
+
type SignedInFn = ReturnType<typeof builder.middleware<readonly [typeof compiled, typeof signedIn]>>;
|
|
60
|
+
type PublicFn = ReturnType<typeof builder.middleware<readonly [typeof compiled, typeof anyone]>>;
|
|
61
|
+
/** A server function only signed-in users can call. */
|
|
62
|
+
export declare const signedInFn: SignedInFn;
|
|
63
|
+
/** A server function anyone can call (external apps only). */
|
|
64
|
+
export declare const publicFn: PublicFn;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { createMiddleware, createServerFn } from "@tanstack/react-start";
|
|
2
|
+
import { setResponseStatus } from "@tanstack/react-start/server";
|
|
3
|
+
import { USAGE_TOKEN_QUERY_PARAM } from "../auth/constants.js";
|
|
4
|
+
import { currentHost } from "./host.js";
|
|
5
|
+
import { getHost, inScope, runInScope } from "./scope.js";
|
|
6
|
+
export { createZiteRouter } from "./router.js";
|
|
7
|
+
const STATUS = {
|
|
8
|
+
BAD_REQUEST: 400,
|
|
9
|
+
UNAUTHORIZED: 401,
|
|
10
|
+
FORBIDDEN: 403,
|
|
11
|
+
NOT_FOUND: 404,
|
|
12
|
+
CONFLICT: 409,
|
|
13
|
+
RATE_LIMITED: 429,
|
|
14
|
+
INTERNAL_ERROR: 500,
|
|
15
|
+
};
|
|
16
|
+
/** Fail with a status: `throw new ZiteError('FORBIDDEN', 'Only the owner can edit this')`. */
|
|
17
|
+
export class ZiteError extends Error {
|
|
18
|
+
code;
|
|
19
|
+
constructor(code, message) {
|
|
20
|
+
super(message ?? code);
|
|
21
|
+
this.name = "ZiteError";
|
|
22
|
+
this.code = code;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export const hasRole = (user, role) => user.roles.includes(role);
|
|
26
|
+
/** The message a server function fails with when it needs a session; the platform's client middleware sends the user to sign in. */
|
|
27
|
+
export const SIGN_IN_REQUIRED = "ZITE_SIGN_IN_REQUIRED";
|
|
28
|
+
export const isSignInRequired = (error) => error instanceof Error && error.message === SIGN_IN_REQUIRED;
|
|
29
|
+
/**
|
|
30
|
+
* The platform's sign-in page, returning to `redirectTo` (default: the current
|
|
31
|
+
* page). For guards: `throw redirect({ href: loginHref(), reloadDocument: true })`.
|
|
32
|
+
*/
|
|
33
|
+
export function loginHref(redirectTo) {
|
|
34
|
+
const current = typeof window !== "undefined" ? window.location.href : currentHost()?.url;
|
|
35
|
+
// Absolute on purpose: better-auth drops a relative callback with an encoded
|
|
36
|
+
// space, fragment or non-ASCII path, and never checks an absolute one.
|
|
37
|
+
const target = current ? new URL(redirectTo ?? current, current) : undefined;
|
|
38
|
+
const params = new URLSearchParams();
|
|
39
|
+
if (target && !(target.pathname === "/" && !target.search && !target.hash))
|
|
40
|
+
params.set("redirectUrl", target.toString());
|
|
41
|
+
else if (!target && redirectTo)
|
|
42
|
+
params.set("redirectUrl", redirectTo);
|
|
43
|
+
// The editor preview's sign-in page needs the preview's token.
|
|
44
|
+
const usageToken = current ? new URL(current).searchParams.get(USAGE_TOKEN_QUERY_PARAM) : null;
|
|
45
|
+
if (usageToken)
|
|
46
|
+
params.set(USAGE_TOKEN_QUERY_PARAM, usageToken);
|
|
47
|
+
const query = params.toString();
|
|
48
|
+
return query ? `/auth/login?${query}` : "/auth/login";
|
|
49
|
+
}
|
|
50
|
+
const kinds = new WeakMap();
|
|
51
|
+
const tag = (middleware, kind) => {
|
|
52
|
+
kinds.set(middleware, kind);
|
|
53
|
+
return middleware;
|
|
54
|
+
};
|
|
55
|
+
/** @internal */
|
|
56
|
+
export const ziteMiddlewareKind = (middleware) => typeof middleware === "object" && middleware !== null ? kinds.get(middleware) : undefined;
|
|
57
|
+
// Identity comes from server-only storage, never TanStack's context, which
|
|
58
|
+
// merges whatever context the browser sends.
|
|
59
|
+
const host = () => {
|
|
60
|
+
const current = getHost();
|
|
61
|
+
if (!current)
|
|
62
|
+
throw new ZiteError("UNAUTHORIZED", "Missing platform context");
|
|
63
|
+
return current;
|
|
64
|
+
};
|
|
65
|
+
// Server functions report a status through TanStack's error envelope; server
|
|
66
|
+
// routes need a Response. A nested call's failure is the caller's to handle.
|
|
67
|
+
const withStatus = async (isServerFn, fn) => {
|
|
68
|
+
const nested = inScope();
|
|
69
|
+
try {
|
|
70
|
+
return await fn();
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (!(error instanceof ZiteError))
|
|
74
|
+
throw error;
|
|
75
|
+
if (!isServerFn)
|
|
76
|
+
return new Response(error.message, { status: STATUS[error.code] });
|
|
77
|
+
if (!nested)
|
|
78
|
+
setResponseStatus(STATUS[error.code]);
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
/** Only signed-in users. Handlers get `context.user: User`. */
|
|
83
|
+
export const signedIn = tag(createMiddleware().server(({ next, serverFnMeta }) => withStatus(!!serverFnMeta, async () => {
|
|
84
|
+
const { user } = host();
|
|
85
|
+
if (!user)
|
|
86
|
+
throw new ZiteError("UNAUTHORIZED", SIGN_IN_REQUIRED);
|
|
87
|
+
return runInScope({ kind: "signedIn", user }, () => next({ context: { user } }));
|
|
88
|
+
})), { kind: "signedIn" });
|
|
89
|
+
/** Anyone, signed in or not. Handlers get `context.user: User | null`. */
|
|
90
|
+
export const anyone = tag(createMiddleware().server(({ next }) => {
|
|
91
|
+
const { user } = host();
|
|
92
|
+
return runInScope({ kind: "anyone", user }, () => next({ context: { user } }));
|
|
93
|
+
}), { kind: "anyone" });
|
|
94
|
+
/** Webhooks delivered by the platform, which already checked the URL token. */
|
|
95
|
+
export const webhook = tag(createMiddleware().server(({ next }) => {
|
|
96
|
+
if (host().invoke?.kind !== "webhook")
|
|
97
|
+
return new Response("Not found", { status: 404 });
|
|
98
|
+
return runInScope({ kind: "webhook", user: null }, () => next());
|
|
99
|
+
}), { kind: "webhook" });
|
|
100
|
+
/** Signed-in users with this role. */
|
|
101
|
+
export const role = (name) => tag(createMiddleware()
|
|
102
|
+
.middleware([signedIn])
|
|
103
|
+
.server(({ next, context, serverFnMeta }) => withStatus(!!serverFnMeta, async () => {
|
|
104
|
+
if (!hasRole(context.user, name))
|
|
105
|
+
throw new ZiteError("FORBIDDEN", `Requires the ${name} role`);
|
|
106
|
+
return next();
|
|
107
|
+
})), { kind: "role", role: name });
|
|
108
|
+
// TanStack's compiler tags every server function it compiles with an id. A
|
|
109
|
+
// builder it didn't compile (an object property, one returned from a helper)
|
|
110
|
+
// has none and would run its handler wherever it was called, so it fails here.
|
|
111
|
+
const compiled = tag(createMiddleware({ type: "function" }).client(({ next, serverFnMeta }) => {
|
|
112
|
+
if (!serverFnMeta?.id) {
|
|
113
|
+
throw new Error("This server function was not compiled by TanStack Start. Assign signedInFn(...)/publicFn(...)" +
|
|
114
|
+
".handler(...) directly to its own top-level const, not an object property or a helper's return value.");
|
|
115
|
+
}
|
|
116
|
+
return next();
|
|
117
|
+
}), { kind: "factory" });
|
|
118
|
+
// TanStack doesn't export its builder-after-middleware type, so name it through the public builder.
|
|
119
|
+
const builder = createServerFn();
|
|
120
|
+
/** A server function only signed-in users can call. */
|
|
121
|
+
export const signedInFn = createServerFn().middleware([compiled, signedIn]);
|
|
122
|
+
/** A server function anyone can call (external apps only). */
|
|
123
|
+
export const publicFn = createServerFn().middleware([compiled, anyone]);
|
|
@@ -0,0 +1,12 @@
|
|
|
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";
|
|
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]>;
|
|
@@ -0,0 +1,89 @@
|
|
|
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 { getHost, inScope } from "./scope.js";
|
|
7
|
+
// TanStack drops its default CSRF check once an app has a start instance.
|
|
8
|
+
const csrf = createCsrfMiddleware({ filter: (ctx) => ctx.handlerType === "serverFn" });
|
|
9
|
+
// The host answers a signed-out call with a plain 401, the app with a ZiteError;
|
|
10
|
+
// either way the browser goes to sign in rather than showing an error.
|
|
11
|
+
const signInOn401 = createMiddleware({ type: "function" }).client(async ({ next }) => {
|
|
12
|
+
try {
|
|
13
|
+
return await next();
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (!isSignInRequired(error))
|
|
17
|
+
throw error;
|
|
18
|
+
const href = loginHref();
|
|
19
|
+
if (typeof window !== "undefined")
|
|
20
|
+
window.location.assign(href);
|
|
21
|
+
throw redirect({ href, reloadDocument: true });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
const PREVIEW_LIMIT = 8_000;
|
|
25
|
+
const preview = (value) => {
|
|
26
|
+
if (value === undefined)
|
|
27
|
+
return undefined;
|
|
28
|
+
if (typeof value === "object" && value !== null && Symbol.asyncIterator in value)
|
|
29
|
+
return "[stream]";
|
|
30
|
+
try {
|
|
31
|
+
const json = JSON.stringify(value);
|
|
32
|
+
return json && json.length > PREVIEW_LIMIT ? `${json.slice(0, PREVIEW_LIMIT)}…` : json;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return "[unserializable]";
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const errorOf = (error) => error instanceof ZiteError
|
|
39
|
+
? { code: error.code, message: error.message }
|
|
40
|
+
: { code: "INTERNAL_ERROR", message: error instanceof Error ? error.message : String(error) };
|
|
41
|
+
// Records each call for the run log. The host's own record of the request is the
|
|
42
|
+
// floor; this adds what only the app sees: decoded input, output and errors.
|
|
43
|
+
const runCapture = createMiddleware({ type: "function" }).server(async ({ next, data, serverFnMeta }) => {
|
|
44
|
+
const runs = getHost()?.runs;
|
|
45
|
+
const startedAt = Date.now();
|
|
46
|
+
const record = (outcome) => runs?.push({
|
|
47
|
+
fn: serverFnMeta.id,
|
|
48
|
+
name: serverFnMeta.name,
|
|
49
|
+
nested: inScope(),
|
|
50
|
+
startedAt,
|
|
51
|
+
durationMs: Date.now() - startedAt,
|
|
52
|
+
input: preview(data),
|
|
53
|
+
...("error" in outcome ? { error: errorOf(outcome.error) } : { output: preview(outcome.output) }),
|
|
54
|
+
});
|
|
55
|
+
try {
|
|
56
|
+
const out = await next();
|
|
57
|
+
const failed = out.error;
|
|
58
|
+
record(failed === undefined ? { output: out.result } : { error: failed });
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
record({ error });
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
/** Carries a ZiteError to the browser as itself, so `instanceof ZiteError` and `.code` work there. */
|
|
67
|
+
const ziteErrorAdapter = createSerializationAdapter({
|
|
68
|
+
key: "zite-error",
|
|
69
|
+
test: (value) => value instanceof ZiteError,
|
|
70
|
+
toSerializable: (error) => ({ code: error.code, message: error.message }),
|
|
71
|
+
fromSerializable: ({ code, message }) => new ZiteError(code, message),
|
|
72
|
+
});
|
|
73
|
+
/**
|
|
74
|
+
* The start instance every Start app builds with: the platform's middleware and
|
|
75
|
+
* adapters first, then whatever the app's own `src/start.ts` sets.
|
|
76
|
+
*/
|
|
77
|
+
export function createZiteStart(app) {
|
|
78
|
+
return createStart(async () => {
|
|
79
|
+
const own = app ? await app.getOptions() : undefined;
|
|
80
|
+
// Spreading the app's optional arrays loses their tuple types, which the
|
|
81
|
+
// route tree registers for typed context and serialization.
|
|
82
|
+
return {
|
|
83
|
+
...own,
|
|
84
|
+
requestMiddleware: [csrf, ...(own?.requestMiddleware ?? [])],
|
|
85
|
+
functionMiddleware: [signInOn401, runCapture, ...(own?.functionMiddleware ?? [])],
|
|
86
|
+
serializationAdapters: [ziteErrorAdapter, ...(own?.serializationAdapters ?? [])],
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
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 {};
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
return router;
|
|
34
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { User, ZiteRequestContext } from "./types.js";
|
|
2
|
+
export type AccessKind = "signedIn" | "anyone" | "webhook" | "workflow";
|
|
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;
|
|
@@ -0,0 +1,58 @@
|
|
|
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();
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type AccessKind } from "./scope.js";
|
|
2
|
+
import type { RunRecord, ZiteRequestContext } from "./types.js";
|
|
3
|
+
export { runWithHost } from "./scope.js";
|
|
4
|
+
export { describeApp } from "./describe.js";
|
|
5
|
+
export type { RunRecord } from "./types.js";
|
|
6
|
+
export type PlatformCallInput = {
|
|
7
|
+
integrationId: string;
|
|
8
|
+
className: string;
|
|
9
|
+
methodName: string;
|
|
10
|
+
params: unknown;
|
|
11
|
+
access: AccessKind;
|
|
12
|
+
userId: string | null;
|
|
13
|
+
requestId: string;
|
|
14
|
+
};
|
|
15
|
+
/** The `ZITE` service binding the host's Platform entrypoint gives every app script. */
|
|
16
|
+
export type PlatformBinding = {
|
|
17
|
+
call(call: PlatformCallInput): Promise<unknown>;
|
|
18
|
+
/** One batch per request, after its response has ended. */
|
|
19
|
+
runs(batch: {
|
|
20
|
+
requestId: string;
|
|
21
|
+
runs: RunRecord[];
|
|
22
|
+
}): Promise<void>;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Routes every zitejs data, integration and email call through `call`, and only
|
|
26
|
+
* inside an access scope. The access kind travels with the call so the platform
|
|
27
|
+
* can record what a page read and, later, narrow anonymous reads.
|
|
28
|
+
*/
|
|
29
|
+
export declare function installPlatformTransport(call: (input: PlatformCallInput) => Promise<unknown>): void;
|
|
30
|
+
/** The host's `x-zite-*` headers. Only the host can set them: the front strips them from public traffic. */
|
|
31
|
+
export declare function hostContextFromRequest(request: Request): ZiteRequestContext;
|
|
32
|
+
/** Every server function TanStack compiled, written in by `zite()` at build. Empty in dev, where TanStack doesn't call its id hook. */
|
|
33
|
+
export declare const builtFunctionIds: () => string[];
|
|
34
|
+
type Fetch = (request: Request, ...rest: unknown[]) => Promise<Response> | Response;
|
|
35
|
+
/**
|
|
36
|
+
* The fetch handler of a Start app's server entry. Describe calls get the app's
|
|
37
|
+
* description; everything else runs TanStack with the host's identity in
|
|
38
|
+
* server-only storage. Platform calls go to the `ZITE` binding from the Worker's
|
|
39
|
+
* env, or to `platform` when a test or local dev supplies one.
|
|
40
|
+
*/
|
|
41
|
+
export declare function createZiteFetch(options: {
|
|
42
|
+
handler: {
|
|
43
|
+
fetch: Fetch;
|
|
44
|
+
};
|
|
45
|
+
getRouter: () => unknown;
|
|
46
|
+
platform?: PlatformBinding;
|
|
47
|
+
}): Fetch;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describeApp } from "./describe.js";
|
|
2
|
+
import { getHost, requireScope, runWithHost } from "./scope.js";
|
|
3
|
+
export { runWithHost } from "./scope.js";
|
|
4
|
+
export { describeApp } from "./describe.js";
|
|
5
|
+
/**
|
|
6
|
+
* Routes every zitejs data, integration and email call through `call`, and only
|
|
7
|
+
* inside an access scope. The access kind travels with the call so the platform
|
|
8
|
+
* can record what a page read and, later, narrow anonymous reads.
|
|
9
|
+
*/
|
|
10
|
+
export function installPlatformTransport(call) {
|
|
11
|
+
const sdkCall = (integrationId, className, methodName, params) => {
|
|
12
|
+
const scope = requireScope(`${className}.${methodName}`);
|
|
13
|
+
return call({
|
|
14
|
+
integrationId,
|
|
15
|
+
className,
|
|
16
|
+
methodName,
|
|
17
|
+
params,
|
|
18
|
+
access: scope.kind,
|
|
19
|
+
userId: scope.user?.id ?? null,
|
|
20
|
+
requestId: getHost()?.requestId ?? "",
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
globalThis.__wrapSdkCall = sdkCall;
|
|
24
|
+
}
|
|
25
|
+
const decodeUser = (value) => JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(value.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0))));
|
|
26
|
+
/** The host's `x-zite-*` headers. Only the host can set them: the front strips them from public traffic. */
|
|
27
|
+
export function hostContextFromRequest(request) {
|
|
28
|
+
const { headers } = request;
|
|
29
|
+
const user = headers.get("x-zite-user");
|
|
30
|
+
const [kind, name = ""] = headers.get("x-zite-invoke")?.split(":") ?? [];
|
|
31
|
+
return {
|
|
32
|
+
requestId: headers.get("x-zite-request-id") ?? crypto.randomUUID(),
|
|
33
|
+
url: request.url,
|
|
34
|
+
user: user ? decodeUser(user) : null,
|
|
35
|
+
invoke: kind === "webhook" || kind === "describe" || kind === "workflow" ? { kind, name } : null,
|
|
36
|
+
runs: [],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** Every server function TanStack compiled, written in by `zite()` at build. Empty in dev, where TanStack doesn't call its id hook. */
|
|
40
|
+
export const builtFunctionIds = () => "__ZITE_FN_IDS__".split(",").filter((id) => id && !id.startsWith("__ZITE"));
|
|
41
|
+
// Server functions also run while a streamed page renders, so the run log is
|
|
42
|
+
// complete only once the body has been read to the end.
|
|
43
|
+
const onBodyEnd = (response, done) => {
|
|
44
|
+
if (!response.body) {
|
|
45
|
+
done();
|
|
46
|
+
return response;
|
|
47
|
+
}
|
|
48
|
+
return new Response(response.body.pipeThrough(new TransformStream({ flush: done })), response);
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* The fetch handler of a Start app's server entry. Describe calls get the app's
|
|
52
|
+
* description; everything else runs TanStack with the host's identity in
|
|
53
|
+
* server-only storage. Platform calls go to the `ZITE` binding from the Worker's
|
|
54
|
+
* env, or to `platform` when a test or local dev supplies one.
|
|
55
|
+
*/
|
|
56
|
+
export function createZiteFetch(options) {
|
|
57
|
+
let binding = options.platform;
|
|
58
|
+
// Fails closed: no binding means no platform access, never a fallback.
|
|
59
|
+
const platform = () => {
|
|
60
|
+
if (!binding)
|
|
61
|
+
throw new Error("The Zite platform binding is missing");
|
|
62
|
+
return binding;
|
|
63
|
+
};
|
|
64
|
+
installPlatformTransport((call) => platform().call(call));
|
|
65
|
+
return async (request, ...rest) => {
|
|
66
|
+
binding ??= rest[0]?.ZITE;
|
|
67
|
+
if (request.headers.get("x-zite-invoke") === "describe") {
|
|
68
|
+
return Response.json(await describeApp({ fnIds: builtFunctionIds(), getRouter: options.getRouter, request }));
|
|
69
|
+
}
|
|
70
|
+
const host = hostContextFromRequest(request);
|
|
71
|
+
const response = await runWithHost(host, () => options.handler.fetch(request));
|
|
72
|
+
return onBodyEnd(response, () => {
|
|
73
|
+
if (!host.runs.length || !binding)
|
|
74
|
+
return;
|
|
75
|
+
const flushed = binding.runs({ requestId: host.requestId, runs: host.runs }).catch((error) => {
|
|
76
|
+
console.error("[zite] couldn't record runs", error);
|
|
77
|
+
});
|
|
78
|
+
rest[1]?.waitUntil(flushed);
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type User = {
|
|
2
|
+
id: string;
|
|
3
|
+
email: string;
|
|
4
|
+
name: string;
|
|
5
|
+
roles: ReadonlyArray<string>;
|
|
6
|
+
};
|
|
7
|
+
/** One server function call, as the platform's run log shows it. Input and output are truncated JSON. */
|
|
8
|
+
export type RunRecord = {
|
|
9
|
+
fn: string;
|
|
10
|
+
name?: string;
|
|
11
|
+
nested: boolean;
|
|
12
|
+
startedAt: number;
|
|
13
|
+
durationMs: number;
|
|
14
|
+
input?: string;
|
|
15
|
+
output?: string;
|
|
16
|
+
error?: {
|
|
17
|
+
code: string;
|
|
18
|
+
message: string;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
/** What the host resolved for this request. Set by the generated server entry, never by the browser. */
|
|
22
|
+
export type ZiteRequestContext = {
|
|
23
|
+
requestId: string;
|
|
24
|
+
url: string;
|
|
25
|
+
user: User | null;
|
|
26
|
+
invoke: {
|
|
27
|
+
kind: "webhook" | "describe" | "workflow";
|
|
28
|
+
name: string;
|
|
29
|
+
} | null;
|
|
30
|
+
/** Filled by run capture, flushed to the platform once the response ends. */
|
|
31
|
+
runs: RunRecord[];
|
|
32
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PluginOption } from "vite";
|
|
2
|
+
export type ZiteOptions = {
|
|
3
|
+
/** A server entry the caller owns, relative to the app root. By default zite() writes the platform's. */
|
|
4
|
+
serverEntry?: string;
|
|
5
|
+
/** Build for Cloudflare Workers, the target the platform deploys. Defaults to true; false builds for Node. */
|
|
6
|
+
cloudflare?: boolean;
|
|
7
|
+
};
|
|
8
|
+
/** The one plugin a Zite app's `vite.config.ts` uses. */
|
|
9
|
+
export declare function zite(options?: ZiteOptions): PluginOption[];
|