bosbun 0.0.3 → 0.0.4
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/package.json +1 -1
- package/src/core/cookies.ts +20 -4
- package/src/core/env.ts +18 -0
- package/src/core/html.ts +17 -11
- package/src/core/prerender.ts +8 -2
- package/src/core/renderer.ts +57 -11
- package/src/core/server.ts +43 -23
- package/templates/default/.env.example +44 -3
package/package.json
CHANGED
package/src/core/cookies.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { Cookies, CookieOptions } from "./hooks.ts";
|
|
2
2
|
|
|
3
|
+
// ─── Cookie Validation ───────────────────────────────────
|
|
4
|
+
/** Rejects characters that could inject into Set-Cookie headers. */
|
|
5
|
+
const UNSAFE_COOKIE_VALUE = /[;\r\n]/;
|
|
6
|
+
const VALID_SAMESITE = new Set(["Strict", "Lax", "None"]);
|
|
7
|
+
|
|
3
8
|
// ─── Cookie Helpers ──────────────────────────────────────
|
|
4
9
|
|
|
5
10
|
function parseCookies(header: string): Record<string, string> {
|
|
@@ -9,7 +14,10 @@ function parseCookies(header: string): Record<string, string> {
|
|
|
9
14
|
if (idx === -1) continue;
|
|
10
15
|
const name = pair.slice(0, idx).trim();
|
|
11
16
|
const value = pair.slice(idx + 1).trim();
|
|
12
|
-
if (name)
|
|
17
|
+
if (name) {
|
|
18
|
+
try { result[name] = decodeURIComponent(value); }
|
|
19
|
+
catch { result[name] = value; }
|
|
20
|
+
}
|
|
13
21
|
}
|
|
14
22
|
return result;
|
|
15
23
|
}
|
|
@@ -32,13 +40,21 @@ export class CookieJar implements Cookies {
|
|
|
32
40
|
|
|
33
41
|
set(name: string, value: string, options?: CookieOptions): void {
|
|
34
42
|
let header = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
|
|
35
|
-
|
|
36
|
-
if (
|
|
43
|
+
const path = options?.path ?? "/";
|
|
44
|
+
if (UNSAFE_COOKIE_VALUE.test(path)) throw new Error(`Invalid cookie path: ${path}`);
|
|
45
|
+
header += `; Path=${path}`;
|
|
46
|
+
if (options?.domain) {
|
|
47
|
+
if (UNSAFE_COOKIE_VALUE.test(options.domain)) throw new Error(`Invalid cookie domain: ${options.domain}`);
|
|
48
|
+
header += `; Domain=${options.domain}`;
|
|
49
|
+
}
|
|
37
50
|
if (options?.maxAge != null) header += `; Max-Age=${options.maxAge}`;
|
|
38
51
|
if (options?.expires) header += `; Expires=${options.expires.toUTCString()}`;
|
|
39
52
|
if (options?.httpOnly) header += "; HttpOnly";
|
|
40
53
|
if (options?.secure) header += "; Secure";
|
|
41
|
-
if (options?.sameSite)
|
|
54
|
+
if (options?.sameSite) {
|
|
55
|
+
if (!VALID_SAMESITE.has(options.sameSite)) throw new Error(`Invalid cookie sameSite: ${options.sameSite}`);
|
|
56
|
+
header += `; SameSite=${options.sameSite}`;
|
|
57
|
+
}
|
|
42
58
|
this._outgoing.push(header);
|
|
43
59
|
}
|
|
44
60
|
|
package/src/core/env.ts
CHANGED
|
@@ -14,6 +14,9 @@ const FRAMEWORK_VARS = new Set([
|
|
|
14
14
|
"CORS_EXPOSED_HEADERS",
|
|
15
15
|
"CORS_CREDENTIALS",
|
|
16
16
|
"CORS_MAX_AGE",
|
|
17
|
+
"LOAD_TIMEOUT",
|
|
18
|
+
"METADATA_TIMEOUT",
|
|
19
|
+
"PRERENDER_TIMEOUT",
|
|
17
20
|
]);
|
|
18
21
|
|
|
19
22
|
// ─── .env File Parser ────────────────────────────────────
|
|
@@ -81,6 +84,11 @@ export function loadEnv(mode: string, dir?: string): Record<string, string> {
|
|
|
81
84
|
console.log(`✓ Loaded ${loaded.join(", ")}`);
|
|
82
85
|
}
|
|
83
86
|
|
|
87
|
+
// Track declared keys so html.ts only exposes .env-declared PUBLIC_* vars
|
|
88
|
+
for (const key of Object.keys(merged)) {
|
|
89
|
+
_declaredKeys.add(key);
|
|
90
|
+
}
|
|
91
|
+
|
|
84
92
|
// Apply to process.env — system env wins (don't overwrite existing)
|
|
85
93
|
for (const [key, value] of Object.entries(merged)) {
|
|
86
94
|
if (!(key in process.env)) {
|
|
@@ -99,6 +107,16 @@ export function loadEnv(mode: string, dir?: string): Record<string, string> {
|
|
|
99
107
|
return result;
|
|
100
108
|
}
|
|
101
109
|
|
|
110
|
+
// ─── Declared Key Tracking ───────────────────────────
|
|
111
|
+
// Track which keys were declared in .env files so html.ts only exposes those to the client.
|
|
112
|
+
|
|
113
|
+
const _declaredKeys = new Set<string>();
|
|
114
|
+
|
|
115
|
+
/** Returns the set of env var keys that were declared in .env files. */
|
|
116
|
+
export function getDeclaredEnvKeys(): ReadonlySet<string> {
|
|
117
|
+
return _declaredKeys;
|
|
118
|
+
}
|
|
119
|
+
|
|
102
120
|
// ─── Classifier ──────────────────────────────────────────
|
|
103
121
|
|
|
104
122
|
export interface ClassifiedEnv {
|
package/src/core/html.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { getDeclaredEnvKeys } from "./env.ts";
|
|
2
3
|
|
|
3
4
|
// ─── Dist Manifest ───────────────────────────────────────
|
|
4
5
|
// Maps hashed filenames → script/link tags.
|
|
@@ -24,25 +25,30 @@ export function safeJsonStringify(data: unknown): string {
|
|
|
24
25
|
"\u2028": "\\u2028",
|
|
25
26
|
"\u2029": "\\u2029",
|
|
26
27
|
};
|
|
27
|
-
|
|
28
|
+
let json: string;
|
|
29
|
+
try {
|
|
30
|
+
json = JSON.stringify(data);
|
|
31
|
+
} catch {
|
|
32
|
+
console.error("safeJsonStringify: failed to serialize data (circular reference?)");
|
|
33
|
+
json = "null";
|
|
34
|
+
}
|
|
35
|
+
return json.replace(/[<>&\u2028\u2029]/g, c => map[c]);
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
// ─── Public Env Injection ─────────────────────────────────
|
|
31
39
|
|
|
32
40
|
/**
|
|
33
|
-
* Collect PUBLIC_* (non-static) vars
|
|
34
|
-
*
|
|
35
|
-
*
|
|
41
|
+
* Collect PUBLIC_* (non-static) vars that were declared in .env files.
|
|
42
|
+
* Only exposes keys tracked by loadEnv() — never leaks system env vars
|
|
43
|
+
* that happen to start with PUBLIC_.
|
|
36
44
|
*/
|
|
37
45
|
function getPublicDynamicEnv(): Record<string, string> {
|
|
38
|
-
|
|
39
|
-
// by inspecting process.env keys that start with PUBLIC_ but not PUBLIC_STATIC_.
|
|
40
|
-
// We only expose keys that came from .env files — tracked in process.env via loadEnv.
|
|
41
|
-
// At runtime the server module exports are inlined; we collect from process.env here.
|
|
46
|
+
const declared = getDeclaredEnvKeys();
|
|
42
47
|
const result: Record<string, string> = {};
|
|
43
|
-
for (const
|
|
44
|
-
if (key.startsWith("PUBLIC_") && !key.startsWith("PUBLIC_STATIC_")
|
|
45
|
-
|
|
48
|
+
for (const key of declared) {
|
|
49
|
+
if (key.startsWith("PUBLIC_") && !key.startsWith("PUBLIC_STATIC_")) {
|
|
50
|
+
const value = process.env[key];
|
|
51
|
+
if (value !== undefined) result[key] = value;
|
|
46
52
|
}
|
|
47
53
|
}
|
|
48
54
|
return result;
|
package/src/core/prerender.ts
CHANGED
|
@@ -5,6 +5,8 @@ import type { RouteManifest } from "./types.ts";
|
|
|
5
5
|
const CORE_DIR = import.meta.dir;
|
|
6
6
|
const BUNIA_NODE_MODULES = join(CORE_DIR, "..", "..", "node_modules");
|
|
7
7
|
|
|
8
|
+
const PRERENDER_TIMEOUT = Number(process.env.PRERENDER_TIMEOUT) || 5_000; // 5s default
|
|
9
|
+
|
|
8
10
|
// ─── Prerendering ─────────────────────────────────────────
|
|
9
11
|
|
|
10
12
|
async function detectPrerenderRoutes(manifest: RouteManifest): Promise<string[]> {
|
|
@@ -61,7 +63,7 @@ export async function prerenderStaticRoutes(manifest: RouteManifest): Promise<vo
|
|
|
61
63
|
|
|
62
64
|
for (const routePath of paths) {
|
|
63
65
|
try {
|
|
64
|
-
const res = await fetch(`${base}${routePath}
|
|
66
|
+
const res = await fetch(`${base}${routePath}`, { signal: AbortSignal.timeout(PRERENDER_TIMEOUT) });
|
|
65
67
|
const html = await res.text();
|
|
66
68
|
const outPath = routePath === "/"
|
|
67
69
|
? "./dist/prerendered/index.html"
|
|
@@ -70,7 +72,11 @@ export async function prerenderStaticRoutes(manifest: RouteManifest): Promise<vo
|
|
|
70
72
|
writeFileSync(outPath, html);
|
|
71
73
|
console.log(` ✅ ${routePath} → ${outPath}`);
|
|
72
74
|
} catch (err) {
|
|
73
|
-
|
|
75
|
+
if (err instanceof DOMException && err.name === "TimeoutError") {
|
|
76
|
+
console.error(` ❌ Prerender timed out for ${routePath} after ${PRERENDER_TIMEOUT / 1000}s — increase PRERENDER_TIMEOUT to raise the limit`);
|
|
77
|
+
} else {
|
|
78
|
+
console.error(` ❌ Failed to prerender ${routePath}:`, err);
|
|
79
|
+
}
|
|
74
80
|
}
|
|
75
81
|
}
|
|
76
82
|
|
package/src/core/renderer.ts
CHANGED
|
@@ -8,6 +8,34 @@ import App from "./client/App.svelte";
|
|
|
8
8
|
import { buildHtml, buildHtmlShell, buildHtmlShellOpen, buildMetadataChunk, buildHtmlTail, compress, safeJsonStringify, isDev } from "./html.ts";
|
|
9
9
|
import type { Metadata } from "./hooks.ts";
|
|
10
10
|
|
|
11
|
+
// ─── Timeout Helpers ─────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
class LoadTimeoutError extends Error {
|
|
14
|
+
constructor(label: string, ms: number) {
|
|
15
|
+
super(`${label} timed out after ${ms}ms`);
|
|
16
|
+
this.name = "LoadTimeoutError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseTimeout(raw: string | undefined, fallback: number): number {
|
|
21
|
+
if (!raw || raw === "Infinity") return 0;
|
|
22
|
+
const n = parseInt(raw, 10);
|
|
23
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const LOAD_TIMEOUT = parseTimeout(process.env.LOAD_TIMEOUT, 5000);
|
|
27
|
+
const METADATA_TIMEOUT = parseTimeout(process.env.METADATA_TIMEOUT, 3000);
|
|
28
|
+
|
|
29
|
+
function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
|
|
30
|
+
if (ms <= 0) return promise;
|
|
31
|
+
return Promise.race([
|
|
32
|
+
promise,
|
|
33
|
+
new Promise<never>((_, reject) =>
|
|
34
|
+
setTimeout(() => reject(new LoadTimeoutError(label, ms)), ms)
|
|
35
|
+
),
|
|
36
|
+
]);
|
|
37
|
+
}
|
|
38
|
+
|
|
11
39
|
// ─── Session-Aware Fetch ─────────────────────────────────
|
|
12
40
|
// Passed to load() functions so they can call internal APIs
|
|
13
41
|
// with the current user's cookies automatically forwarded.
|
|
@@ -57,7 +85,7 @@ export async function loadRouteData(
|
|
|
57
85
|
for (let d = 0; d < ls.depth; d++) Object.assign(merged, layoutData[d] ?? {});
|
|
58
86
|
return merged;
|
|
59
87
|
};
|
|
60
|
-
layoutData[ls.depth] = (await mod.load({ params, url, locals, cookies, parent, fetch, metadata: null })) ?? {};
|
|
88
|
+
layoutData[ls.depth] = (await withTimeout(mod.load({ params, url, locals, cookies, parent, fetch, metadata: null }), LOAD_TIMEOUT, `layout load (depth=${ls.depth}, ${url.pathname})`)) ?? {};
|
|
61
89
|
}
|
|
62
90
|
} catch (err) {
|
|
63
91
|
if (err instanceof HttpError || err instanceof Redirect) throw err;
|
|
@@ -79,7 +107,7 @@ export async function loadRouteData(
|
|
|
79
107
|
for (const d of layoutData) if (d) Object.assign(merged, d);
|
|
80
108
|
return merged;
|
|
81
109
|
};
|
|
82
|
-
pageData = (await mod.load({ params, url, locals, cookies, parent, fetch, metadata: metadataData })) ?? {};
|
|
110
|
+
pageData = (await withTimeout(mod.load({ params, url, locals, cookies, parent, fetch, metadata: metadataData }), LOAD_TIMEOUT, `page load (${url.pathname})`)) ?? {};
|
|
83
111
|
}
|
|
84
112
|
} catch (err) {
|
|
85
113
|
if (err instanceof HttpError || err instanceof Redirect) throw err;
|
|
@@ -136,7 +164,7 @@ async function loadMetadata(
|
|
|
136
164
|
const mod = await route.pageServer();
|
|
137
165
|
if (typeof mod.metadata === "function") {
|
|
138
166
|
const fetch = makeFetch(req, url);
|
|
139
|
-
return (await mod.metadata({ params, url, locals, cookies, fetch })) ?? null;
|
|
167
|
+
return (await withTimeout(mod.metadata({ params, url, locals, cookies, fetch }), METADATA_TIMEOUT, `metadata (${url.pathname})`)) ?? null;
|
|
140
168
|
}
|
|
141
169
|
} catch (err) {
|
|
142
170
|
if (isDev) console.error("Metadata load error:", err);
|
|
@@ -147,32 +175,49 @@ async function loadMetadata(
|
|
|
147
175
|
|
|
148
176
|
// ─── Streaming SSR Renderer ──────────────────────────────
|
|
149
177
|
|
|
150
|
-
export function renderSSRStream(
|
|
178
|
+
export async function renderSSRStream(
|
|
151
179
|
url: URL,
|
|
152
180
|
locals: Record<string, any>,
|
|
153
181
|
req: Request,
|
|
154
182
|
cookies: Cookies,
|
|
155
|
-
): Response | null {
|
|
183
|
+
): Promise<Response | null> {
|
|
156
184
|
const match = findMatch(serverRoutes, url.pathname);
|
|
157
185
|
if (!match) return null;
|
|
158
186
|
|
|
159
187
|
const { route, params } = match;
|
|
160
|
-
|
|
188
|
+
|
|
189
|
+
// ── Pre-stream phase: resolve metadata before committing to a 200 ──
|
|
190
|
+
// Errors here return a proper error response with correct status code.
|
|
191
|
+
let metadata: Metadata | null = null;
|
|
192
|
+
try {
|
|
193
|
+
metadata = await loadMetadata(route, params, url, locals, cookies, req);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
if (err instanceof Redirect) {
|
|
196
|
+
return Response.redirect(err.location, err.status);
|
|
197
|
+
}
|
|
198
|
+
if (err instanceof HttpError) {
|
|
199
|
+
return renderErrorPage(err.status, err.message, url, req);
|
|
200
|
+
}
|
|
201
|
+
if (isDev) console.error("Metadata load error:", err);
|
|
202
|
+
else console.error("Metadata load error:", (err as Error).message ?? err);
|
|
203
|
+
// Continue with null metadata — don't break the page for a metadata failure
|
|
204
|
+
}
|
|
161
205
|
|
|
162
206
|
// Kick off imports immediately (parallel with data loading)
|
|
163
207
|
const pageModPromise = route.pageModule();
|
|
164
208
|
const layoutModsPromise = Promise.all(route.layoutModules.map((l: () => Promise<any>) => l()));
|
|
165
209
|
|
|
210
|
+
const enc = new TextEncoder();
|
|
211
|
+
|
|
166
212
|
const stream = new ReadableStream<Uint8Array>({
|
|
167
213
|
async start(controller) {
|
|
168
214
|
// Chunk 1: head opening (CSS, modulepreload — cached)
|
|
169
215
|
controller.enqueue(enc.encode(buildHtmlShellOpen()));
|
|
170
216
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const metadata = await loadMetadata(route, params, url, locals, cookies, req);
|
|
174
|
-
controller.enqueue(enc.encode(buildMetadataChunk(metadata)));
|
|
217
|
+
// Chunk 2: metadata tags, close </head>, open <body> + spinner
|
|
218
|
+
controller.enqueue(enc.encode(buildMetadataChunk(metadata)));
|
|
175
219
|
|
|
220
|
+
try {
|
|
176
221
|
// Pass metadata.data to load() so it can reuse fetched data
|
|
177
222
|
const metadataData = metadata?.data ?? null;
|
|
178
223
|
|
|
@@ -203,6 +248,7 @@ export function renderSSRStream(
|
|
|
203
248
|
controller.enqueue(enc.encode(buildHtmlTail(body, head, data.pageData, data.layoutData, data.csr)));
|
|
204
249
|
controller.close();
|
|
205
250
|
} catch (err) {
|
|
251
|
+
// Head is closed and body is open at this point — HTML structure is valid
|
|
206
252
|
if (err instanceof Redirect) {
|
|
207
253
|
controller.enqueue(enc.encode(
|
|
208
254
|
`<script>location.replace(${safeJsonStringify(err.location)})</script></body></html>`
|
|
@@ -212,7 +258,7 @@ export function renderSSRStream(
|
|
|
212
258
|
}
|
|
213
259
|
if (err instanceof HttpError) {
|
|
214
260
|
controller.enqueue(enc.encode(
|
|
215
|
-
`<script>location.replace("/__bunia/error?status=${err.status}&message
|
|
261
|
+
`<script>location.replace("/__bunia/error?status=${err.status}&message="+encodeURIComponent(${safeJsonStringify(err.message)}))</script></body></html>`
|
|
216
262
|
));
|
|
217
263
|
controller.close();
|
|
218
264
|
return;
|
package/src/core/server.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Elysia } from "elysia";
|
|
2
2
|
import { staticPlugin } from "@elysiajs/static";
|
|
3
3
|
import { existsSync } from "fs";
|
|
4
|
-
import { join } from "path";
|
|
4
|
+
import { join, resolve as resolvePath } from "path";
|
|
5
5
|
|
|
6
6
|
import { findMatch } from "./matcher.ts";
|
|
7
7
|
import { apiRoutes, serverRoutes } from "bunia:routes";
|
|
@@ -94,6 +94,13 @@ function isValidRoutePath(path: string, origin: string): boolean {
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/** Resolve a file path and verify it stays within the allowed base directory. Returns null if traversal detected. */
|
|
98
|
+
function safePath(base: string, untrusted: string): string | null {
|
|
99
|
+
const root = resolvePath(base);
|
|
100
|
+
const full = resolvePath(join(base, untrusted));
|
|
101
|
+
return full.startsWith(root + "/") || full === root ? full : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
97
104
|
/** Extract action name from URL searchParams — `?/login` → "login", no slash key → "default". */
|
|
98
105
|
function parseActionName(url: URL): string {
|
|
99
106
|
for (const key of url.searchParams.keys()) {
|
|
@@ -143,35 +150,48 @@ async function resolve(event: RequestEvent): Promise<Response> {
|
|
|
143
150
|
if (isStaticPath(path)) {
|
|
144
151
|
// dist/client: serve with cache headers based on whether filename is hashed
|
|
145
152
|
if (path.startsWith("/dist/client/")) {
|
|
146
|
-
const
|
|
147
|
-
if (
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
153
|
+
const resolved = safePath("./dist/client", path.split("?")[0].slice("/dist/client".length));
|
|
154
|
+
if (resolved) {
|
|
155
|
+
const file = Bun.file(resolved);
|
|
156
|
+
if (await file.exists()) {
|
|
157
|
+
const filename = path.split("/").pop() ?? "";
|
|
158
|
+
const isHashed = /\-[a-z0-9]{8,}\.[a-z]+$/.test(filename);
|
|
159
|
+
const cacheControl = !isDev && isHashed
|
|
160
|
+
? "public, max-age=31536000, immutable"
|
|
161
|
+
: "no-cache";
|
|
162
|
+
return new Response(file, { headers: { "Cache-Control": cacheControl } });
|
|
163
|
+
}
|
|
154
164
|
}
|
|
155
165
|
return new Response("Not Found", { status: 404 });
|
|
156
166
|
}
|
|
157
|
-
const
|
|
158
|
-
if (
|
|
159
|
-
|
|
160
|
-
|
|
167
|
+
const pubPath = safePath("./public", path);
|
|
168
|
+
if (pubPath) {
|
|
169
|
+
const pub = Bun.file(pubPath);
|
|
170
|
+
if (await pub.exists()) return new Response(pub);
|
|
171
|
+
}
|
|
172
|
+
const distPath = safePath("./dist", path);
|
|
173
|
+
if (distPath) {
|
|
174
|
+
const dist = Bun.file(distPath);
|
|
175
|
+
if (await dist.exists()) return new Response(dist);
|
|
176
|
+
}
|
|
161
177
|
return new Response("Not Found", { status: 404 });
|
|
162
178
|
}
|
|
163
179
|
|
|
164
180
|
// Prerendered pages — serve static HTML built at build time
|
|
165
|
-
const
|
|
166
|
-
|
|
181
|
+
const prerenderPath = safePath(
|
|
182
|
+
"./dist/prerendered",
|
|
183
|
+
path === "/" ? "index.html" : `${path}/index.html`,
|
|
167
184
|
);
|
|
168
|
-
if (
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
185
|
+
if (prerenderPath) {
|
|
186
|
+
const prerenderFile = Bun.file(prerenderPath);
|
|
187
|
+
if (await prerenderFile.exists()) {
|
|
188
|
+
return new Response(prerenderFile, {
|
|
189
|
+
headers: {
|
|
190
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
191
|
+
"Cache-Control": "public, max-age=3600",
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
}
|
|
175
195
|
}
|
|
176
196
|
|
|
177
197
|
// API routes (+server.ts)
|
|
@@ -262,7 +282,7 @@ async function resolve(event: RequestEvent): Promise<Response> {
|
|
|
262
282
|
}
|
|
263
283
|
|
|
264
284
|
// SSR pages (+page.svelte) — streaming by default
|
|
265
|
-
const streamResponse = renderSSRStream(url, locals, request, cookies);
|
|
285
|
+
const streamResponse = await renderSSRStream(url, locals, request, cookies);
|
|
266
286
|
if (!streamResponse) return renderErrorPage(404, "Not Found", url, request);
|
|
267
287
|
return streamResponse;
|
|
268
288
|
}
|
|
@@ -14,16 +14,57 @@
|
|
|
14
14
|
# Import in your code:
|
|
15
15
|
# import { PUBLIC_STATIC_APP_NAME, DB_PASSWORD } from 'bunia:env';
|
|
16
16
|
#
|
|
17
|
-
# Framework vars (PORT, NODE_ENV, BODY_SIZE_LIMIT, CSRF_ALLOWED_ORIGINS
|
|
18
|
-
# NOT exposed via bunia:env —
|
|
17
|
+
# Framework vars (PORT, NODE_ENV, BODY_SIZE_LIMIT, CSRF_ALLOWED_ORIGINS,
|
|
18
|
+
# CORS_*, LOAD_TIMEOUT, METADATA_TIMEOUT, PRERENDER_TIMEOUT) are NOT exposed via bunia:env —
|
|
19
|
+
# access them via process.env directly.
|
|
19
20
|
# ────────────────────────────────────────────────────────────────────────────────
|
|
20
21
|
|
|
21
22
|
# Public build-time constant (safe to expose to client)
|
|
22
23
|
PUBLIC_STATIC_APP_NAME=My Bunia App
|
|
23
24
|
|
|
24
|
-
# Framework vars — access via process.env
|
|
25
|
+
# ─── Framework vars — access via process.env (not via bunia:env) ─────────────
|
|
26
|
+
|
|
27
|
+
# Server port. Defaults to 9000 in production, 9001 in dev (proxied via :9000).
|
|
25
28
|
# PORT=9000
|
|
26
29
|
|
|
30
|
+
# Maximum request body size. Supports K/M/G suffixes or "Infinity". Defaults to 512K.
|
|
31
|
+
# BODY_SIZE_LIMIT=512K
|
|
32
|
+
|
|
33
|
+
# Timeout for load() functions (layout + page) in milliseconds. Defaults to 5000 (5s).
|
|
34
|
+
# Set to 0 or Infinity to disable.
|
|
35
|
+
# LOAD_TIMEOUT=5000
|
|
36
|
+
|
|
37
|
+
# Timeout for metadata() functions in milliseconds. Defaults to 3000 (3s).
|
|
38
|
+
# Set to 0 or Infinity to disable.
|
|
39
|
+
# METADATA_TIMEOUT=3000
|
|
40
|
+
|
|
41
|
+
# Timeout for prerender fetches during build in milliseconds. Defaults to 5000 (5s).
|
|
42
|
+
# Set to 0 or Infinity to disable.
|
|
43
|
+
# PRERENDER_TIMEOUT=5000
|
|
44
|
+
|
|
45
|
+
# Comma-separated list of allowed origins for CSRF validation.
|
|
46
|
+
# Leave unset to allow same-origin requests only.
|
|
47
|
+
# CSRF_ALLOWED_ORIGINS=
|
|
48
|
+
|
|
49
|
+
# Comma-separated list of origins allowed to make cross-origin requests.
|
|
50
|
+
# Leave unset to disable CORS.
|
|
51
|
+
# CORS_ALLOWED_ORIGINS=
|
|
52
|
+
|
|
53
|
+
# Comma-separated HTTP methods to allow in CORS requests. Default: GET, HEAD, PUT, PATCH, POST, DELETE
|
|
54
|
+
# CORS_ALLOWED_METHODS=GET, POST
|
|
55
|
+
|
|
56
|
+
# Comma-separated request headers to allow in CORS requests. Default: Content-Type, Authorization
|
|
57
|
+
# CORS_ALLOWED_HEADERS=Content-Type, Authorization
|
|
58
|
+
|
|
59
|
+
# Comma-separated response headers to expose to the browser. Default: none
|
|
60
|
+
# CORS_EXPOSED_HEADERS=
|
|
61
|
+
|
|
62
|
+
# Allow cookies and auth credentials in cross-origin requests. Default: false
|
|
63
|
+
# CORS_CREDENTIALS=true
|
|
64
|
+
|
|
65
|
+
# Preflight response cache duration in seconds. Default: 86400 (24 hours)
|
|
66
|
+
# CORS_MAX_AGE=86400
|
|
67
|
+
|
|
27
68
|
# Public runtime var (safe to expose to client, can change without rebuild)
|
|
28
69
|
# PUBLIC_API_URL=https://api.example.com
|
|
29
70
|
|