fastscript 0.1.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.
Files changed (96) hide show
  1. package/.github/workflows/ci.yml +17 -0
  2. package/CHANGELOG.md +5 -0
  3. package/Dockerfile +9 -0
  4. package/LICENSE +21 -0
  5. package/README.md +102 -0
  6. package/app/api/auth.js +10 -0
  7. package/app/api/hello.js +3 -0
  8. package/app/api/upload.js +9 -0
  9. package/app/api/webhook.js +10 -0
  10. package/app/db/migrations/001_init.js +6 -0
  11. package/app/db/seed.js +5 -0
  12. package/app/env.schema.js +6 -0
  13. package/app/middleware.fs +7 -0
  14. package/app/pages/404.fs +3 -0
  15. package/app/pages/_layout.fs +17 -0
  16. package/app/pages/benchmarks.fs +15 -0
  17. package/app/pages/docs/index.fs +16 -0
  18. package/app/pages/index.fs +22 -0
  19. package/app/pages/private.fs +12 -0
  20. package/app/pages/showcase.fs +14 -0
  21. package/app/styles.css +14 -0
  22. package/docs/AI_CONTEXT_PACK_V1.md +25 -0
  23. package/docs/DEPLOY_GUIDE.md +14 -0
  24. package/docs/INCIDENT_PLAYBOOK.md +18 -0
  25. package/docs/INTEROP_RULES.md +7 -0
  26. package/docs/PLUGIN_API_CONTRACT.md +22 -0
  27. package/ecosystem.config.cjs +1 -0
  28. package/examples/fullstack/README.md +10 -0
  29. package/examples/fullstack/app/api/orders.js +8 -0
  30. package/examples/fullstack/app/db/migrations/001_init.js +3 -0
  31. package/examples/fullstack/app/db/seed.js +3 -0
  32. package/examples/fullstack/app/jobs/send-order-email.js +4 -0
  33. package/examples/fullstack/app/pages/_layout.fs +1 -0
  34. package/examples/fullstack/app/pages/index.fs +3 -0
  35. package/examples/startup-mvp/README.md +8 -0
  36. package/examples/startup-mvp/app/api/cart.js +9 -0
  37. package/examples/startup-mvp/app/api/checkout.js +8 -0
  38. package/examples/startup-mvp/app/db/migrations/001_products.js +6 -0
  39. package/examples/startup-mvp/app/jobs/send-receipt.js +4 -0
  40. package/examples/startup-mvp/app/pages/_layout.fs +3 -0
  41. package/examples/startup-mvp/app/pages/dashboard/index.fs +9 -0
  42. package/examples/startup-mvp/app/pages/index.fs +18 -0
  43. package/package.json +50 -0
  44. package/scripts/bench-report.mjs +36 -0
  45. package/scripts/release.mjs +21 -0
  46. package/scripts/smoke-dev.mjs +78 -0
  47. package/scripts/smoke-start.mjs +41 -0
  48. package/scripts/test-auth.mjs +26 -0
  49. package/scripts/test-db.mjs +31 -0
  50. package/scripts/test-jobs.mjs +15 -0
  51. package/scripts/test-middleware.mjs +37 -0
  52. package/scripts/test-roundtrip.mjs +44 -0
  53. package/scripts/test-validation.mjs +17 -0
  54. package/scripts/test-webhook-storage.mjs +22 -0
  55. package/spec/FASTSCRIPT_1000_BUILD_LIST.md +1090 -0
  56. package/src/auth-flows.mjs +29 -0
  57. package/src/auth.mjs +115 -0
  58. package/src/bench.mjs +46 -0
  59. package/src/build.mjs +222 -0
  60. package/src/cache.mjs +58 -0
  61. package/src/check.mjs +22 -0
  62. package/src/cli.mjs +71 -0
  63. package/src/compat.mjs +122 -0
  64. package/src/create.mjs +190 -0
  65. package/src/db-cli.mjs +45 -0
  66. package/src/db-postgres.mjs +40 -0
  67. package/src/db.mjs +103 -0
  68. package/src/deploy.mjs +65 -0
  69. package/src/dev.mjs +5 -0
  70. package/src/env.mjs +89 -0
  71. package/src/export.mjs +83 -0
  72. package/src/fs-normalize.mjs +100 -0
  73. package/src/interop.mjs +16 -0
  74. package/src/jobs.mjs +127 -0
  75. package/src/logger.mjs +27 -0
  76. package/src/middleware.mjs +14 -0
  77. package/src/migrate.mjs +81 -0
  78. package/src/observability.mjs +21 -0
  79. package/src/security.mjs +55 -0
  80. package/src/server-runtime.mjs +339 -0
  81. package/src/start.mjs +10 -0
  82. package/src/storage.mjs +56 -0
  83. package/src/validate.mjs +18 -0
  84. package/src/validation.mjs +79 -0
  85. package/src/webhook.mjs +71 -0
  86. package/src/worker.mjs +5 -0
  87. package/vercel.json +15 -0
  88. package/vscode/fastscript-language/README.md +12 -0
  89. package/vscode/fastscript-language/extension.js +24 -0
  90. package/vscode/fastscript-language/language-configuration.json +6 -0
  91. package/vscode/fastscript-language/lsp/server.cjs +27 -0
  92. package/vscode/fastscript-language/lsp/smoke-test.cjs +1 -0
  93. package/vscode/fastscript-language/package.json +36 -0
  94. package/vscode/fastscript-language/snippets/fastscript.code-snippets +24 -0
  95. package/vscode/fastscript-language/syntaxes/fastscript.tmLanguage.json +21 -0
  96. package/wrangler.toml +5 -0
@@ -0,0 +1,339 @@
1
+ import { createServer } from "node:http";
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
+ }
package/src/start.mjs ADDED
@@ -0,0 +1,10 @@
1
+ import { existsSync } from "node:fs";
2
+ import { runServer } from "./server-runtime.mjs";
3
+
4
+ export async function runStart() {
5
+ if (!existsSync("dist/fastscript-manifest.json")) {
6
+ throw new Error("Missing production build. Run: fastscript build");
7
+ }
8
+ const port = Number(process.env.PORT || 4173);
9
+ await runServer({ mode: process.env.NODE_ENV || "production", watchMode: false, buildOnStart: false, port });
10
+ }
@@ -0,0 +1,56 @@
1
+ import { createHash } from "node:crypto";
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
+ }
@@ -0,0 +1,18 @@
1
+ import { runCheck } from "./check.mjs";
2
+ import { runBuild } from "./build.mjs";
3
+ import { runBench } from "./bench.mjs";
4
+ import { runCompat } from "./compat.mjs";
5
+ import { runExport } from "./export.mjs";
6
+ import { runDbMigrate, runDbSeed } from "./db-cli.mjs";
7
+
8
+ export async function runValidate() {
9
+ await runCheck();
10
+ await runBuild();
11
+ await runBench();
12
+ await runCompat();
13
+ await runDbMigrate();
14
+ await runDbSeed();
15
+ await runExport(["--to", "js", "--out", "exported-js-app"]);
16
+ await runExport(["--to", "ts", "--out", "exported-ts-app"]);
17
+ console.log("validate complete: check/build/bench/compat/db/export all passed");
18
+ }
@@ -0,0 +1,79 @@
1
+ export async function readBody(req) {
2
+ const chunks = [];
3
+ for await (const chunk of req) chunks.push(Buffer.from(chunk));
4
+ const text = Buffer.concat(chunks).toString("utf8");
5
+ return text;
6
+ }
7
+
8
+ export async function readJsonBody(req) {
9
+ const raw = await readBody(req);
10
+ if (!raw.trim()) return {};
11
+ try {
12
+ return JSON.parse(raw);
13
+ } catch {
14
+ const error = new Error("Invalid JSON body");
15
+ error.status = 400;
16
+ throw error;
17
+ }
18
+ }
19
+
20
+ export function validateShape(schema, input, scope = "input") {
21
+ if (!schema || typeof schema !== "object") return { ok: true, value: input ?? {} };
22
+ const errors = [];
23
+ const out = {};
24
+ const source = input && typeof input === "object" ? input : {};
25
+
26
+ for (const [key, rule] of Object.entries(schema)) {
27
+ const value = source[key];
28
+ const optional = typeof rule === "string" && rule.endsWith("?");
29
+ const t = typeof rule === "string" ? rule.replace(/\?$/, "") : String(rule);
30
+
31
+ if (value === undefined || value === null) {
32
+ if (!optional) errors.push(`${scope}.${key} is required`);
33
+ continue;
34
+ }
35
+
36
+ if (t === "array") {
37
+ if (!Array.isArray(value)) errors.push(`${scope}.${key} must be array`);
38
+ else out[key] = value;
39
+ continue;
40
+ }
41
+
42
+ if (t === "int") {
43
+ const n = Number(value);
44
+ if (!Number.isInteger(n)) errors.push(`${scope}.${key} must be integer`);
45
+ else out[key] = n;
46
+ continue;
47
+ }
48
+
49
+ if (t === "float" || t === "number") {
50
+ const n = Number(value);
51
+ if (!Number.isFinite(n)) errors.push(`${scope}.${key} must be number`);
52
+ else out[key] = n;
53
+ continue;
54
+ }
55
+
56
+ if (t === "bool" || t === "boolean") {
57
+ if (typeof value !== "boolean") errors.push(`${scope}.${key} must be boolean`);
58
+ else out[key] = value;
59
+ continue;
60
+ }
61
+
62
+ if (t === "str" || t === "string") {
63
+ if (typeof value !== "string") errors.push(`${scope}.${key} must be string`);
64
+ else out[key] = value;
65
+ continue;
66
+ }
67
+
68
+ out[key] = value;
69
+ }
70
+
71
+ if (errors.length) {
72
+ const error = new Error(`Validation failed: ${errors.join("; ")}`);
73
+ error.status = 400;
74
+ error.details = errors;
75
+ throw error;
76
+ }
77
+ return { ok: true, value: out };
78
+ }
79
+
@@ -0,0 +1,71 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+
5
+ export async function readRawBody(req) {
6
+ const chunks = [];
7
+ for await (const chunk of req) chunks.push(Buffer.from(chunk));
8
+ return Buffer.concat(chunks);
9
+ }
10
+
11
+ export function signPayload(payload, secret, algo = "sha256") {
12
+ const mac = createHmac(algo, secret).update(payload).digest("hex");
13
+ return `${algo}=${mac}`;
14
+ }
15
+
16
+ export function verifySignature(payload, header, secret, algo = "sha256") {
17
+ if (!header || typeof header !== "string") return false;
18
+ const expected = signPayload(payload, secret, algo);
19
+ const a = Buffer.from(expected);
20
+ const b = Buffer.from(header);
21
+ if (a.length !== b.length) return false;
22
+ return timingSafeEqual(a, b);
23
+ }
24
+
25
+ export function isReplay(tsSeconds, maxSkewSec = 300) {
26
+ const ts = Number(tsSeconds);
27
+ if (!Number.isFinite(ts)) return true;
28
+ const now = Math.floor(Date.now() / 1000);
29
+ return Math.abs(now - ts) > maxSkewSec;
30
+ }
31
+
32
+ function replayStore({ dir = ".fastscript", ttlSec = 600 } = {}) {
33
+ const root = resolve(dir);
34
+ mkdirSync(root, { recursive: true });
35
+ const file = join(root, "webhook-replay.json");
36
+ let state = { seen: {} };
37
+ if (existsSync(file)) {
38
+ try { state = JSON.parse(readFileSync(file, "utf8")); } catch {}
39
+ }
40
+ function persist() {
41
+ writeFileSync(file, JSON.stringify(state, null, 2), "utf8");
42
+ }
43
+ return {
44
+ has(id) {
45
+ const now = Date.now();
46
+ for (const [k, exp] of Object.entries(state.seen)) if (exp < now) delete state.seen[k];
47
+ persist();
48
+ return Boolean(state.seen[id]);
49
+ },
50
+ add(id) {
51
+ state.seen[id] = Date.now() + ttlSec * 1000;
52
+ persist();
53
+ },
54
+ };
55
+ }
56
+
57
+ export async function verifyWebhookRequest(req, { secret, signatureHeader = "x-signature", timestampHeader = "x-timestamp", idHeader = "x-event-id", maxSkewSec = 300, replayDir = ".fastscript" } = {}) {
58
+ const raw = await readRawBody(req);
59
+ const sig = req.headers[signatureHeader];
60
+ const ts = req.headers[timestampHeader];
61
+ const eventId = req.headers[idHeader];
62
+ const id = Array.isArray(eventId) ? eventId[0] : eventId;
63
+ if (isReplay(ts, maxSkewSec)) return { ok: false, reason: "replay_window" };
64
+ if (!verifySignature(raw, Array.isArray(sig) ? sig[0] : sig, secret)) return { ok: false, reason: "bad_signature" };
65
+ if (id) {
66
+ const store = replayStore({ dir: replayDir, ttlSec: Math.max(maxSkewSec, 600) });
67
+ if (store.has(id)) return { ok: false, reason: "duplicate_event" };
68
+ store.add(id);
69
+ }
70
+ return { ok: true, raw };
71
+ }
package/src/worker.mjs ADDED
@@ -0,0 +1,5 @@
1
+ import { runWorker } from "./jobs.mjs";
2
+
3
+ export async function runWorkerCommand() {
4
+ await runWorker({ dir: ".fastscript", pollMs: Number(process.env.WORKER_POLL_MS || 350) });
5
+ }
package/vercel.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "version": 2,
3
+ "builds": [
4
+ {
5
+ "src": "package.json",
6
+ "use": "@vercel/node"
7
+ }
8
+ ],
9
+ "routes": [
10
+ {
11
+ "src": "/(.*)",
12
+ "dest": "/src/cli.mjs"
13
+ }
14
+ ]
15
+ }
@@ -0,0 +1,12 @@
1
+ # FastScript VS Code Extension
2
+
3
+ Supports:
4
+ - `.fs` syntax highlighting
5
+ - snippets
6
+ - language configuration
7
+ - LSP diagnostics starter
8
+
9
+ ## Publish
10
+ - `cd vscode/fastscript-language`
11
+ - `npm install`
12
+ - package/publish with `vsce`
@@ -0,0 +1,24 @@
1
+ const path = require('node:path');
2
+ const vscode = require('vscode');
3
+ const { LanguageClient, TransportKind } = require('vscode-languageclient/node');
4
+
5
+ let client;
6
+
7
+ function activate(context) {
8
+ const serverModule = context.asAbsolutePath(path.join('lsp', 'server.cjs'));
9
+ const serverOptions = { run: { module: serverModule, transport: TransportKind.ipc }, debug: { module: serverModule, transport: TransportKind.ipc } };
10
+ const clientOptions = { documentSelector: [{ scheme: 'file', language: 'fastscript' }] };
11
+ client = new LanguageClient('fastscript-lsp', 'FastScript Language Server', serverOptions, clientOptions);
12
+ context.subscriptions.push(client.start());
13
+ vscode.languages.setLanguageConfiguration('fastscript', {
14
+ comments: { lineComment: '//' },
15
+ brackets: [['{', '}'], ['[', ']'], ['(', ')']],
16
+ });
17
+ }
18
+
19
+ function deactivate() {
20
+ if (!client) return undefined;
21
+ return client.stop();
22
+ }
23
+
24
+ module.exports = { activate, deactivate };
@@ -0,0 +1,6 @@
1
+ {
2
+ "comments": { "lineComment": "//", "blockComment": ["/*", "*/"] },
3
+ "brackets": [["{", "}"], ["[", "]"], ["(", ")"]],
4
+ "autoClosingPairs": [{"open":"{","close":"}"},{"open":"[","close":"]"},{"open":"(","close":")"},{"open":"\"","close":"\""},{"open":"'","close":"'"}],
5
+ "surroundingPairs": [["{", "}"], ["[", "]"], ["(", ")"], ["\"", "\""], ["'", "'"]]
6
+ }
@@ -0,0 +1,27 @@
1
+ const { createConnection, ProposedFeatures, TextDocuments, DiagnosticSeverity } = require('vscode-languageserver/node');
2
+ const { TextDocument } = require('vscode-languageserver-textdocument');
3
+
4
+ const connection = createConnection(ProposedFeatures.all);
5
+ const documents = new TextDocuments(TextDocument);
6
+
7
+ connection.onInitialize(() => ({ capabilities: { textDocumentSync: documents.syncKind } }));
8
+
9
+ documents.onDidChangeContent((change) => {
10
+ const text = change.document.getText();
11
+ const diagnostics = [];
12
+ const lines = text.split(/\r?\n/);
13
+ lines.forEach((line, i) => {
14
+ if (line.includes('TODO_ERROR')) {
15
+ diagnostics.push({
16
+ severity: DiagnosticSeverity.Error,
17
+ range: { start: { line: i, character: 0 }, end: { line: i, character: line.length } },
18
+ message: 'Remove TODO_ERROR token',
19
+ source: 'fastscript-lsp',
20
+ });
21
+ }
22
+ });
23
+ connection.sendDiagnostics({ uri: change.document.uri, diagnostics });
24
+ });
25
+
26
+ documents.listen(connection);
27
+ connection.listen();
@@ -0,0 +1 @@
1
+ console.log('fastscript lsp smoke test placeholder: install deps in vscode/fastscript-language first');