fastscript 1.0.0 → 3.0.0
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/CHANGELOG.md +38 -7
- package/LICENSE +33 -21
- package/README.md +605 -73
- package/node_modules/@fastscript/core-private/BOUNDARY.json +15 -0
- package/node_modules/@fastscript/core-private/README.md +5 -0
- package/node_modules/@fastscript/core-private/package.json +34 -0
- package/node_modules/@fastscript/core-private/src/asset-optimizer.mjs +67 -0
- package/node_modules/@fastscript/core-private/src/audit-log.mjs +50 -0
- package/node_modules/@fastscript/core-private/src/auth-flows.mjs +29 -0
- package/node_modules/@fastscript/core-private/src/auth.mjs +115 -0
- package/node_modules/@fastscript/core-private/src/bench.mjs +45 -0
- package/node_modules/@fastscript/core-private/src/build.mjs +670 -0
- package/node_modules/@fastscript/core-private/src/cache.mjs +248 -0
- package/node_modules/@fastscript/core-private/src/check.mjs +22 -0
- package/node_modules/@fastscript/core-private/src/cli.mjs +95 -0
- package/node_modules/@fastscript/core-private/src/compat.mjs +128 -0
- package/node_modules/@fastscript/core-private/src/create.mjs +278 -0
- package/node_modules/@fastscript/core-private/src/csp.mjs +26 -0
- package/node_modules/@fastscript/core-private/src/db-cli.mjs +185 -0
- package/node_modules/@fastscript/core-private/src/db-postgres-collection.mjs +110 -0
- package/node_modules/@fastscript/core-private/src/db-postgres.mjs +40 -0
- package/node_modules/@fastscript/core-private/src/db.mjs +103 -0
- package/node_modules/@fastscript/core-private/src/deploy.mjs +662 -0
- package/node_modules/@fastscript/core-private/src/dev.mjs +5 -0
- package/node_modules/@fastscript/core-private/src/docs-search.mjs +35 -0
- package/node_modules/@fastscript/core-private/src/env.mjs +118 -0
- package/node_modules/@fastscript/core-private/src/export.mjs +83 -0
- package/node_modules/@fastscript/core-private/src/fs-diagnostics.mjs +70 -0
- package/node_modules/@fastscript/core-private/src/fs-error-codes.mjs +141 -0
- package/node_modules/@fastscript/core-private/src/fs-formatter.mjs +66 -0
- package/node_modules/@fastscript/core-private/src/fs-linter.mjs +274 -0
- package/node_modules/@fastscript/core-private/src/fs-normalize.mjs +121 -0
- package/node_modules/@fastscript/core-private/src/fs-parser.mjs +1120 -0
- package/node_modules/@fastscript/core-private/src/generated/docs-search-index.mjs +3182 -0
- package/node_modules/@fastscript/core-private/src/i18n.mjs +25 -0
- package/node_modules/@fastscript/core-private/src/interop.mjs +16 -0
- package/node_modules/@fastscript/core-private/src/jobs.mjs +378 -0
- package/node_modules/@fastscript/core-private/src/logger.mjs +27 -0
- package/node_modules/@fastscript/core-private/src/metrics.mjs +45 -0
- package/node_modules/@fastscript/core-private/src/middleware.mjs +14 -0
- package/node_modules/@fastscript/core-private/src/migrate.mjs +81 -0
- package/node_modules/@fastscript/core-private/src/migration-wizard.mjs +16 -0
- package/node_modules/@fastscript/core-private/src/module-loader.mjs +46 -0
- package/node_modules/@fastscript/core-private/src/oauth-providers.mjs +103 -0
- package/node_modules/@fastscript/core-private/src/observability.mjs +21 -0
- package/node_modules/@fastscript/core-private/src/plugins.mjs +194 -0
- package/node_modules/@fastscript/core-private/src/retention.mjs +57 -0
- package/node_modules/@fastscript/core-private/src/routes.mjs +178 -0
- package/node_modules/@fastscript/core-private/src/scheduler.mjs +104 -0
- package/node_modules/@fastscript/core-private/src/security.mjs +233 -0
- package/node_modules/@fastscript/core-private/src/server-runtime.mjs +849 -0
- package/node_modules/@fastscript/core-private/src/serverless-handler.mjs +20 -0
- package/node_modules/@fastscript/core-private/src/session-policy.mjs +38 -0
- package/node_modules/@fastscript/core-private/src/start.mjs +10 -0
- package/node_modules/@fastscript/core-private/src/storage.mjs +155 -0
- package/node_modules/@fastscript/core-private/src/style-primitives.mjs +538 -0
- package/node_modules/@fastscript/core-private/src/style-system.mjs +461 -0
- package/node_modules/@fastscript/core-private/src/tenant.mjs +55 -0
- package/node_modules/@fastscript/core-private/src/typecheck.mjs +1466 -0
- package/node_modules/@fastscript/core-private/src/validate.mjs +22 -0
- package/node_modules/@fastscript/core-private/src/validation.mjs +88 -0
- package/node_modules/@fastscript/core-private/src/webhook.mjs +81 -0
- package/node_modules/@fastscript/core-private/src/worker.mjs +24 -0
- package/package.json +108 -14
- package/src/asset-optimizer.mjs +67 -0
- package/src/audit-log.mjs +50 -0
- package/src/auth.mjs +1 -115
- package/src/bench.mjs +20 -7
- package/src/benchmark-discipline.mjs +39 -0
- package/src/build.mjs +1 -234
- package/src/cache.mjs +210 -20
- package/src/cli.mjs +65 -6
- package/src/compat.mjs +8 -10
- package/src/conversion-manifest.mjs +101 -0
- package/src/create.mjs +71 -17
- package/src/csp.mjs +26 -0
- package/src/db-cli.mjs +152 -8
- package/src/db-postgres-collection.mjs +110 -0
- package/src/deploy.mjs +1 -65
- package/src/diagnostics.mjs +100 -0
- package/src/docs-search.mjs +35 -0
- package/src/env.mjs +34 -5
- package/src/fs-diagnostics.mjs +70 -0
- package/src/fs-error-codes.mjs +126 -0
- package/src/fs-formatter.mjs +66 -0
- package/src/fs-linter.mjs +274 -0
- package/src/fs-normalize.mjs +52 -239
- package/src/fs-parser.mjs +1 -0
- package/src/generated/docs-search-index.mjs +3591 -0
- package/src/i18n.mjs +25 -0
- package/src/jobs.mjs +283 -32
- package/src/metrics.mjs +45 -0
- package/src/migrate-rollback.mjs +144 -0
- package/src/migrate.mjs +1275 -47
- package/src/migration-wizard.mjs +42 -0
- package/src/module-loader.mjs +22 -11
- package/src/oauth-providers.mjs +103 -0
- package/src/permissions-cli.mjs +112 -0
- package/src/plugins.mjs +194 -0
- package/src/profile.mjs +95 -0
- package/src/regression-guard.mjs +245 -0
- package/src/retention.mjs +57 -0
- package/src/routes.mjs +178 -0
- package/src/runtime-permissions.mjs +299 -0
- package/src/scheduler.mjs +104 -0
- package/src/security.mjs +197 -19
- package/src/server-runtime.mjs +1 -339
- package/src/serverless-handler.mjs +20 -0
- package/src/session-policy.mjs +38 -0
- package/src/storage.mjs +1 -56
- package/src/style-system.mjs +461 -0
- package/src/tenant.mjs +55 -0
- package/src/trace.mjs +95 -0
- package/src/typecheck.mjs +1 -0
- package/src/validate.mjs +13 -1
- package/src/validation.mjs +14 -5
- package/src/webhook.mjs +1 -71
- package/src/worker.mjs +23 -4
- package/src/language-spec.mjs +0 -58
package/src/server-runtime.mjs
CHANGED
|
@@ -1,339 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { existsSync, readFileSync, statSync, watch } from "node:fs";
|
|
3
|
-
import { extname, join, resolve } from "node:path";
|
|
4
|
-
import { pathToFileURL } from "node:url";
|
|
5
|
-
import { runBuild } from "./build.mjs";
|
|
6
|
-
import { parseCookies, serializeCookie, createSessionManager, requireUser } from "./auth.mjs";
|
|
7
|
-
import { createFileDatabase } from "./db.mjs";
|
|
8
|
-
import { composeMiddleware } from "./middleware.mjs";
|
|
9
|
-
import { readJsonBody, validateShape } from "./validation.mjs";
|
|
10
|
-
import { loadEnv, validateAppEnv } from "./env.mjs";
|
|
11
|
-
import { createLogger } from "./logger.mjs";
|
|
12
|
-
import { createJobQueue } from "./jobs.mjs";
|
|
13
|
-
import { securityHeaders, rateLimit, csrf } from "./security.mjs";
|
|
14
|
-
import { createFileCache } from "./cache.mjs";
|
|
15
|
-
import { createTracer } from "./observability.mjs";
|
|
16
|
-
import { createLocalStorage } from "./storage.mjs";
|
|
17
|
-
|
|
18
|
-
const DIST_DIR = resolve("dist");
|
|
19
|
-
const DB_DIR = resolve(".fastscript");
|
|
20
|
-
|
|
21
|
-
function contentType(path) {
|
|
22
|
-
const ext = extname(path);
|
|
23
|
-
if (ext === ".html") return "text/html; charset=utf-8";
|
|
24
|
-
if (ext === ".js") return "application/javascript; charset=utf-8";
|
|
25
|
-
if (ext === ".css") return "text/css; charset=utf-8";
|
|
26
|
-
if (ext === ".json") return "application/json; charset=utf-8";
|
|
27
|
-
if (ext === ".map") return "application/json; charset=utf-8";
|
|
28
|
-
return "text/plain; charset=utf-8";
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function readManifest() {
|
|
32
|
-
const path = join(DIST_DIR, "fastscript-manifest.json");
|
|
33
|
-
return JSON.parse(readFileSync(path, "utf8"));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function match(routePath, pathname) {
|
|
37
|
-
const a = routePath.split("/").filter(Boolean);
|
|
38
|
-
const b = pathname.split("/").filter(Boolean);
|
|
39
|
-
if (a.length !== b.length) return null;
|
|
40
|
-
const params = {};
|
|
41
|
-
for (let i = 0; i < a.length; i += 1) {
|
|
42
|
-
if (a[i].startsWith(":")) params[a[i].slice(1)] = b[i];
|
|
43
|
-
else if (a[i] !== b[i]) return null;
|
|
44
|
-
}
|
|
45
|
-
return params;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function resolveRoute(routes, pathname) {
|
|
49
|
-
for (const route of routes) {
|
|
50
|
-
const params = match(route.path, pathname);
|
|
51
|
-
if (params) return { route, params };
|
|
52
|
-
}
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async function importDist(modulePath) {
|
|
57
|
-
const abs = join(DIST_DIR, modulePath.replace(/^\.\//, ""));
|
|
58
|
-
const url = `${pathToFileURL(abs).href}?t=${Date.now()}`;
|
|
59
|
-
return import(url);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function createHelpers(res) {
|
|
63
|
-
return {
|
|
64
|
-
json(body, status = 200, headers = {}) {
|
|
65
|
-
return { status, json: body, headers };
|
|
66
|
-
},
|
|
67
|
-
text(body, status = 200, headers = {}) {
|
|
68
|
-
return { status, body, headers };
|
|
69
|
-
},
|
|
70
|
-
redirect(location, status = 302) {
|
|
71
|
-
return { status, headers: { location } };
|
|
72
|
-
},
|
|
73
|
-
setCookie(name, value, opts = {}) {
|
|
74
|
-
const current = res.getHeader("set-cookie");
|
|
75
|
-
const next = serializeCookie(name, value, opts);
|
|
76
|
-
if (!current) res.setHeader("set-cookie", [next]);
|
|
77
|
-
else res.setHeader("set-cookie", Array.isArray(current) ? [...current, next] : [String(current), next]);
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function writeResponse(res, payload) {
|
|
83
|
-
if (!payload) {
|
|
84
|
-
res.writeHead(204);
|
|
85
|
-
res.end();
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
const status = payload.status ?? 200;
|
|
89
|
-
const headers = payload.headers ?? {};
|
|
90
|
-
if (payload.cookies && payload.cookies.length) headers["set-cookie"] = payload.cookies;
|
|
91
|
-
if (payload.json !== undefined) {
|
|
92
|
-
res.writeHead(status, { "content-type": "application/json; charset=utf-8", ...headers });
|
|
93
|
-
res.end(JSON.stringify(payload.json));
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
res.writeHead(status, { "content-type": "text/plain; charset=utf-8", ...headers });
|
|
97
|
-
res.end(payload.body ?? "");
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function htmlDoc(content, ssrData, hasStyles) {
|
|
101
|
-
const safe = JSON.stringify(ssrData ?? {}).replace(/</g, "\\u003c");
|
|
102
|
-
return `<!doctype html>
|
|
103
|
-
<html>
|
|
104
|
-
<head>
|
|
105
|
-
<meta charset="utf-8" />
|
|
106
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
107
|
-
<title>FastScript</title>
|
|
108
|
-
${hasStyles ? '<link rel="stylesheet" href="/styles.css" />' : ""}
|
|
109
|
-
</head>
|
|
110
|
-
<body>
|
|
111
|
-
<div id="app">${content}</div>
|
|
112
|
-
<script>window.__FASTSCRIPT_SSR=${safe}</script>
|
|
113
|
-
<script type="module" src="/router.js"></script>
|
|
114
|
-
</body>
|
|
115
|
-
</html>`;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export async function runServer({ mode = "development", watchMode = false, buildOnStart = true, port = 4173 } = {}) {
|
|
119
|
-
loadEnv({ mode });
|
|
120
|
-
await validateAppEnv();
|
|
121
|
-
|
|
122
|
-
const logger = createLogger({ service: "fastscript-server" });
|
|
123
|
-
const tracer = createTracer({ service: "fastscript-server" });
|
|
124
|
-
if (buildOnStart) await runBuild();
|
|
125
|
-
|
|
126
|
-
const sessions = createSessionManager({ dir: DB_DIR, cookieName: "fs_session", secret: process.env.SESSION_SECRET || "fastscript-dev-secret" });
|
|
127
|
-
const db = createFileDatabase({ dir: DB_DIR, name: "appdb" });
|
|
128
|
-
const queue = createJobQueue({ dir: DB_DIR });
|
|
129
|
-
const cache = createFileCache({ dir: join(DB_DIR, "cache") });
|
|
130
|
-
const storage = createLocalStorage({ dir: join(DB_DIR, "storage") });
|
|
131
|
-
|
|
132
|
-
if (watchMode) {
|
|
133
|
-
let timer = null;
|
|
134
|
-
watch(resolve("app"), { recursive: true }, () => {
|
|
135
|
-
clearTimeout(timer);
|
|
136
|
-
timer = setTimeout(async () => {
|
|
137
|
-
try {
|
|
138
|
-
await runBuild();
|
|
139
|
-
logger.info("rebuild complete");
|
|
140
|
-
} catch (error) {
|
|
141
|
-
logger.error("rebuild failed", { error: error.message });
|
|
142
|
-
}
|
|
143
|
-
}, 120);
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const server = createServer(async (req, res) => {
|
|
148
|
-
const requestId = logger.requestId();
|
|
149
|
-
const start = Date.now();
|
|
150
|
-
const span = tracer.span("request", { requestId, path: req.url, method: req.method });
|
|
151
|
-
res.setHeader("x-request-id", requestId);
|
|
152
|
-
|
|
153
|
-
try {
|
|
154
|
-
const url = new URL(req.url || "/", "http://localhost");
|
|
155
|
-
const pathname = url.pathname;
|
|
156
|
-
const manifest = readManifest();
|
|
157
|
-
const helpers = createHelpers(res);
|
|
158
|
-
const cookies = parseCookies(req.headers.cookie || "");
|
|
159
|
-
const session = sessions.read(cookies[sessions.cookieName]);
|
|
160
|
-
sessions.sweepExpired();
|
|
161
|
-
|
|
162
|
-
const ctx = {
|
|
163
|
-
req,
|
|
164
|
-
res,
|
|
165
|
-
requestId,
|
|
166
|
-
pathname,
|
|
167
|
-
method: (req.method || "GET").toUpperCase(),
|
|
168
|
-
params: {},
|
|
169
|
-
query: Object.fromEntries(url.searchParams.entries()),
|
|
170
|
-
cookies,
|
|
171
|
-
user: session?.user ?? null,
|
|
172
|
-
db,
|
|
173
|
-
queue,
|
|
174
|
-
cache,
|
|
175
|
-
storage,
|
|
176
|
-
auth: {
|
|
177
|
-
login: (user, opts = {}) => {
|
|
178
|
-
const token = sessions.create(user, opts.maxAge ?? 60 * 60 * 24 * 7);
|
|
179
|
-
helpers.setCookie(sessions.cookieName, token, { path: "/", httpOnly: true, maxAge: opts.maxAge ?? 60 * 60 * 24 * 7 });
|
|
180
|
-
return token;
|
|
181
|
-
},
|
|
182
|
-
logout: () => {
|
|
183
|
-
sessions.delete(cookies[sessions.cookieName]);
|
|
184
|
-
helpers.setCookie(sessions.cookieName, "", { path: "/", httpOnly: true, maxAge: 0 });
|
|
185
|
-
},
|
|
186
|
-
requireUser: () => requireUser(session?.user ?? null),
|
|
187
|
-
rotate: (opts = {}) => {
|
|
188
|
-
const token = sessions.rotate(cookies[sessions.cookieName], opts.maxAge ?? 60 * 60 * 24 * 7);
|
|
189
|
-
if (token) helpers.setCookie(sessions.cookieName, token, { path: "/", httpOnly: true, maxAge: opts.maxAge ?? 60 * 60 * 24 * 7 });
|
|
190
|
-
return token;
|
|
191
|
-
},
|
|
192
|
-
},
|
|
193
|
-
input: {
|
|
194
|
-
body: null,
|
|
195
|
-
query: Object.fromEntries(url.searchParams.entries()),
|
|
196
|
-
async readJson() {
|
|
197
|
-
if (ctx.input.body !== null) return ctx.input.body;
|
|
198
|
-
ctx.input.body = await readJsonBody(req);
|
|
199
|
-
return ctx.input.body;
|
|
200
|
-
},
|
|
201
|
-
validateQuery(schema) {
|
|
202
|
-
return validateShape(schema, ctx.query, "query").value;
|
|
203
|
-
},
|
|
204
|
-
async validateBody(schema) {
|
|
205
|
-
const body = await ctx.input.readJson();
|
|
206
|
-
return validateShape(schema, body, "body").value;
|
|
207
|
-
},
|
|
208
|
-
},
|
|
209
|
-
helpers,
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
const isBodyMethod = !["GET", "HEAD"].includes(ctx.method);
|
|
213
|
-
const contentTypeHeader = String(req.headers["content-type"] || "");
|
|
214
|
-
if (isBodyMethod && contentTypeHeader.includes("application/json")) {
|
|
215
|
-
ctx.input.body = await ctx.input.readJson();
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const target = join(DIST_DIR, pathname === "/" ? "index.html" : pathname.slice(1));
|
|
219
|
-
if (pathname.startsWith("/__storage/")) {
|
|
220
|
-
const key = pathname.slice("/__storage/".length);
|
|
221
|
-
const file = storage.get(key);
|
|
222
|
-
if (!file) {
|
|
223
|
-
writeResponse(res, { status: 404, body: "Not found" });
|
|
224
|
-
span.end({ status: 404, kind: "storage" });
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
res.writeHead(200, { "content-type": "application/octet-stream" });
|
|
228
|
-
res.end(file);
|
|
229
|
-
span.end({ status: 200, kind: "storage" });
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
if (existsSync(target) && statSync(target).isFile() && !pathname.endsWith(".html")) {
|
|
233
|
-
const body = readFileSync(target);
|
|
234
|
-
res.writeHead(200, { "content-type": contentType(target) });
|
|
235
|
-
res.end(body);
|
|
236
|
-
logger.info("static", { requestId, path: pathname, status: 200, ms: Date.now() - start });
|
|
237
|
-
span.end({ status: 200, kind: "static" });
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const middlewareList = [];
|
|
242
|
-
middlewareList.push(securityHeaders(), rateLimit());
|
|
243
|
-
if (process.env.CSRF_PROTECT === "1") middlewareList.push(csrf());
|
|
244
|
-
if (manifest.middleware) {
|
|
245
|
-
const mm = await importDist(manifest.middleware);
|
|
246
|
-
if (Array.isArray(mm.middlewares)) middlewareList.push(...mm.middlewares);
|
|
247
|
-
else if (typeof mm.middleware === "function") middlewareList.push(mm.middleware);
|
|
248
|
-
else if (typeof mm.default === "function") middlewareList.push(mm.default);
|
|
249
|
-
}
|
|
250
|
-
const runWithMiddleware = composeMiddleware(middlewareList);
|
|
251
|
-
|
|
252
|
-
const out = await runWithMiddleware(ctx, async () => {
|
|
253
|
-
if (pathname.startsWith("/api/")) {
|
|
254
|
-
const apiHit = resolveRoute(manifest.apiRoutes, pathname);
|
|
255
|
-
if (!apiHit) return { status: 404, body: "API route not found" };
|
|
256
|
-
ctx.params = apiHit.params;
|
|
257
|
-
const mod = await importDist(apiHit.route.module);
|
|
258
|
-
const handler = mod[ctx.method];
|
|
259
|
-
if (typeof handler !== "function") return { status: 405, body: `Method ${ctx.method} not allowed` };
|
|
260
|
-
if (mod.schemas?.[ctx.method]) {
|
|
261
|
-
ctx.input.body = await ctx.input.readJson();
|
|
262
|
-
validateShape(mod.schemas[ctx.method], ctx.input.body, "body");
|
|
263
|
-
}
|
|
264
|
-
return handler(ctx, helpers);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const hit = resolveRoute(manifest.routes, pathname);
|
|
268
|
-
if (!hit) {
|
|
269
|
-
if (manifest.notFound) {
|
|
270
|
-
const nfMod = await importDist(manifest.notFound);
|
|
271
|
-
const body = nfMod.default ? nfMod.default({ pathname }) : "<h1>404</h1>";
|
|
272
|
-
return { status: 404, html: body, data: null };
|
|
273
|
-
}
|
|
274
|
-
return { status: 404, body: "Not found" };
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
ctx.params = hit.params;
|
|
278
|
-
const mod = await importDist(hit.route.module);
|
|
279
|
-
|
|
280
|
-
if (!["GET", "HEAD"].includes(ctx.method) && typeof mod[ctx.method] === "function") {
|
|
281
|
-
if (mod.schemas?.[ctx.method]) {
|
|
282
|
-
ctx.input.body = await ctx.input.readJson();
|
|
283
|
-
validateShape(mod.schemas[ctx.method], ctx.input.body, "body");
|
|
284
|
-
}
|
|
285
|
-
return mod[ctx.method](ctx, helpers);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
let data = {};
|
|
289
|
-
if (typeof mod.load === "function") data = (await mod.load({ ...ctx, params: hit.params, pathname })) || {};
|
|
290
|
-
let html = mod.default ? mod.default({ ...data, params: hit.params, pathname, user: ctx.user }) : "";
|
|
291
|
-
|
|
292
|
-
if (manifest.layout) {
|
|
293
|
-
const layout = await importDist(manifest.layout);
|
|
294
|
-
html = layout.default ? layout.default({ content: html, pathname, user: ctx.user }) : html;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
return { status: 200, html, data };
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
if (out?.html !== undefined) {
|
|
301
|
-
const hasStyles = existsSync(join(DIST_DIR, "styles.css"));
|
|
302
|
-
const payload = { pathname, data: out.data ?? null };
|
|
303
|
-
res.writeHead(out.status ?? 200, { "content-type": "text/html; charset=utf-8" });
|
|
304
|
-
res.end(htmlDoc(out.html, payload, hasStyles));
|
|
305
|
-
logger.info("ssr", { requestId, path: pathname, status: out.status ?? 200, ms: Date.now() - start });
|
|
306
|
-
span.end({ status: out.status ?? 200, kind: "ssr" });
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
writeResponse(res, out);
|
|
311
|
-
logger.info("response", { requestId, path: pathname, status: out?.status ?? 200, ms: Date.now() - start });
|
|
312
|
-
span.end({ status: out?.status ?? 200, kind: "response" });
|
|
313
|
-
} catch (error) {
|
|
314
|
-
const status = error?.status && Number.isInteger(error.status) ? error.status : 500;
|
|
315
|
-
const payload = {
|
|
316
|
-
ok: false,
|
|
317
|
-
error: {
|
|
318
|
-
message: error?.message || "Unknown error",
|
|
319
|
-
status,
|
|
320
|
-
details: error?.details || null,
|
|
321
|
-
},
|
|
322
|
-
};
|
|
323
|
-
const wantsJson = (req.headers.accept || "").includes("application/json") || (req.url || "").startsWith("/api/");
|
|
324
|
-
if (wantsJson) {
|
|
325
|
-
res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
326
|
-
res.end(JSON.stringify(payload));
|
|
327
|
-
} else {
|
|
328
|
-
res.writeHead(status, { "content-type": "text/html; charset=utf-8" });
|
|
329
|
-
res.end(`<!doctype html><html><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><title>Error</title><style>body{background:#050505;color:#fff;font:16px/1.6 ui-sans-serif,system-ui;padding:40px}code{color:#9f92ff}</style></head><body><h1>Something went wrong</h1><p>Please retry or roll back to the previous deploy.</p><p>Request ID: <code>${requestId}</code></p></body></html>`);
|
|
330
|
-
}
|
|
331
|
-
logger.error("request_error", { requestId, status, path: req.url, error: payload.error.message });
|
|
332
|
-
span.end({ status, error: payload.error.message, kind: "error" });
|
|
333
|
-
}
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
server.listen(port, () => {
|
|
337
|
-
logger.info("server_started", { mode, port, watchMode });
|
|
338
|
-
});
|
|
339
|
-
}
|
|
1
|
+
export * from "@fastscript/core-private/server-runtime";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { runServer } from "./server-runtime.mjs";
|
|
2
|
+
|
|
3
|
+
let serverPromise = null;
|
|
4
|
+
|
|
5
|
+
async function getServer() {
|
|
6
|
+
if (!serverPromise) {
|
|
7
|
+
serverPromise = runServer({
|
|
8
|
+
mode: process.env.NODE_ENV || "production",
|
|
9
|
+
watchMode: false,
|
|
10
|
+
buildOnStart: false,
|
|
11
|
+
listen: false,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
return serverPromise;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default async function handler(req, res) {
|
|
18
|
+
const server = await getServer();
|
|
19
|
+
server.emit("request", req, res);
|
|
20
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
function parseBool(value, fallback = false) {
|
|
2
|
+
if (value === undefined || value === null || value === "") return fallback;
|
|
3
|
+
if (value === true || value === "true" || value === "1") return true;
|
|
4
|
+
if (value === false || value === "false" || value === "0") return false;
|
|
5
|
+
return fallback;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function resolveSessionPolicy({ env = process.env, mode = env.NODE_ENV || "development" } = {}) {
|
|
9
|
+
const production = mode === "production";
|
|
10
|
+
const secret = String(env.SESSION_SECRET || "");
|
|
11
|
+
const cookieName = String(env.SESSION_COOKIE_NAME || "fs_session");
|
|
12
|
+
const secure = parseBool(env.SESSION_COOKIE_SECURE, production);
|
|
13
|
+
const sameSite = String(env.SESSION_COOKIE_SAMESITE || "Lax");
|
|
14
|
+
const maxAgeSec = Number(env.SESSION_MAX_AGE_SEC || 60 * 60 * 24 * 7);
|
|
15
|
+
const rotateOnRead = parseBool(env.SESSION_ROTATE_ON_READ, production);
|
|
16
|
+
|
|
17
|
+
if (production) {
|
|
18
|
+
if (!secret) throw new Error("SESSION_SECRET is required in production.");
|
|
19
|
+
if (secret.length < 32) throw new Error("SESSION_SECRET must be at least 32 characters in production.");
|
|
20
|
+
if (!secure) throw new Error("SESSION_COOKIE_SECURE must be enabled in production.");
|
|
21
|
+
if (!["Lax", "Strict", "None"].includes(sameSite)) {
|
|
22
|
+
throw new Error("SESSION_COOKIE_SAMESITE must be one of: Lax, Strict, None.");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
secret: secret || "fastscript-dev-secret",
|
|
28
|
+
cookie: {
|
|
29
|
+
name: cookieName,
|
|
30
|
+
secure,
|
|
31
|
+
sameSite,
|
|
32
|
+
maxAgeSec: Number.isFinite(maxAgeSec) && maxAgeSec > 0 ? maxAgeSec : 60 * 60 * 24 * 7,
|
|
33
|
+
httpOnly: true,
|
|
34
|
+
path: "/",
|
|
35
|
+
},
|
|
36
|
+
rotateOnRead,
|
|
37
|
+
};
|
|
38
|
+
}
|
package/src/storage.mjs
CHANGED
|
@@ -1,56 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname, join, resolve } from "node:path";
|
|
4
|
-
|
|
5
|
-
function sha(input) {
|
|
6
|
-
return createHash("sha1").update(input).digest("hex");
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function createLocalStorage({ dir = ".fastscript/storage" } = {}) {
|
|
10
|
-
const root = resolve(dir);
|
|
11
|
-
mkdirSync(root, { recursive: true });
|
|
12
|
-
return {
|
|
13
|
-
type: "local",
|
|
14
|
-
put(key, content) {
|
|
15
|
-
const file = join(root, key);
|
|
16
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
17
|
-
writeFileSync(file, content);
|
|
18
|
-
return { key, etag: sha(Buffer.isBuffer(content) ? content : Buffer.from(String(content))) };
|
|
19
|
-
},
|
|
20
|
-
get(key) {
|
|
21
|
-
const file = join(root, key);
|
|
22
|
-
if (!existsSync(file)) return null;
|
|
23
|
-
return readFileSync(file);
|
|
24
|
-
},
|
|
25
|
-
delete(key) {
|
|
26
|
-
rmSync(join(root, key), { force: true });
|
|
27
|
-
},
|
|
28
|
-
url(key) {
|
|
29
|
-
return `/__storage/${key}`;
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function createS3CompatibleStorage({ bucket, endpoint, region = "auto", presignBaseUrl } = {}) {
|
|
35
|
-
return {
|
|
36
|
-
type: "s3-compatible",
|
|
37
|
-
bucket,
|
|
38
|
-
endpoint,
|
|
39
|
-
region,
|
|
40
|
-
// Designed for presigned URL workflows.
|
|
41
|
-
async putWithPresignedUrl(url, content, contentType = "application/octet-stream") {
|
|
42
|
-
const res = await fetch(url, { method: "PUT", headers: { "content-type": contentType }, body: content });
|
|
43
|
-
if (!res.ok) throw new Error(`S3 upload failed: ${res.status}`);
|
|
44
|
-
return true;
|
|
45
|
-
},
|
|
46
|
-
async getWithPresignedUrl(url) {
|
|
47
|
-
const res = await fetch(url);
|
|
48
|
-
if (!res.ok) throw new Error(`S3 download failed: ${res.status}`);
|
|
49
|
-
return Buffer.from(await res.arrayBuffer());
|
|
50
|
-
},
|
|
51
|
-
presignPath(key, action = "get") {
|
|
52
|
-
if (!presignBaseUrl) throw new Error("presignBaseUrl is required for presignPath");
|
|
53
|
-
return `${presignBaseUrl}?bucket=${encodeURIComponent(bucket)}&key=${encodeURIComponent(key)}&action=${encodeURIComponent(action)}`;
|
|
54
|
-
},
|
|
55
|
-
};
|
|
56
|
-
}
|
|
1
|
+
export * from "@fastscript/core-private/storage";
|