fastscript 1.0.0 → 2.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.
Files changed (109) hide show
  1. package/CHANGELOG.md +32 -7
  2. package/LICENSE +33 -21
  3. package/README.md +567 -73
  4. package/node_modules/@fastscript/core-private/BOUNDARY.json +15 -0
  5. package/node_modules/@fastscript/core-private/README.md +5 -0
  6. package/node_modules/@fastscript/core-private/package.json +34 -0
  7. package/node_modules/@fastscript/core-private/src/asset-optimizer.mjs +67 -0
  8. package/node_modules/@fastscript/core-private/src/audit-log.mjs +50 -0
  9. package/node_modules/@fastscript/core-private/src/auth-flows.mjs +29 -0
  10. package/node_modules/@fastscript/core-private/src/auth.mjs +115 -0
  11. package/node_modules/@fastscript/core-private/src/bench.mjs +45 -0
  12. package/node_modules/@fastscript/core-private/src/build.mjs +670 -0
  13. package/node_modules/@fastscript/core-private/src/cache.mjs +248 -0
  14. package/node_modules/@fastscript/core-private/src/check.mjs +22 -0
  15. package/node_modules/@fastscript/core-private/src/cli.mjs +95 -0
  16. package/node_modules/@fastscript/core-private/src/compat.mjs +128 -0
  17. package/node_modules/@fastscript/core-private/src/create.mjs +278 -0
  18. package/node_modules/@fastscript/core-private/src/csp.mjs +26 -0
  19. package/node_modules/@fastscript/core-private/src/db-cli.mjs +185 -0
  20. package/node_modules/@fastscript/core-private/src/db-postgres-collection.mjs +110 -0
  21. package/node_modules/@fastscript/core-private/src/db-postgres.mjs +40 -0
  22. package/node_modules/@fastscript/core-private/src/db.mjs +103 -0
  23. package/node_modules/@fastscript/core-private/src/deploy.mjs +662 -0
  24. package/node_modules/@fastscript/core-private/src/dev.mjs +5 -0
  25. package/node_modules/@fastscript/core-private/src/docs-search.mjs +35 -0
  26. package/node_modules/@fastscript/core-private/src/env.mjs +118 -0
  27. package/node_modules/@fastscript/core-private/src/export.mjs +83 -0
  28. package/node_modules/@fastscript/core-private/src/fs-diagnostics.mjs +70 -0
  29. package/node_modules/@fastscript/core-private/src/fs-error-codes.mjs +141 -0
  30. package/node_modules/@fastscript/core-private/src/fs-formatter.mjs +66 -0
  31. package/node_modules/@fastscript/core-private/src/fs-linter.mjs +274 -0
  32. package/node_modules/@fastscript/core-private/src/fs-normalize.mjs +91 -0
  33. package/node_modules/@fastscript/core-private/src/fs-parser.mjs +980 -0
  34. package/node_modules/@fastscript/core-private/src/generated/docs-search-index.mjs +3182 -0
  35. package/node_modules/@fastscript/core-private/src/i18n.mjs +25 -0
  36. package/node_modules/@fastscript/core-private/src/interop.mjs +16 -0
  37. package/node_modules/@fastscript/core-private/src/jobs.mjs +378 -0
  38. package/node_modules/@fastscript/core-private/src/logger.mjs +27 -0
  39. package/node_modules/@fastscript/core-private/src/metrics.mjs +45 -0
  40. package/node_modules/@fastscript/core-private/src/middleware.mjs +14 -0
  41. package/node_modules/@fastscript/core-private/src/migrate.mjs +81 -0
  42. package/node_modules/@fastscript/core-private/src/migration-wizard.mjs +16 -0
  43. package/node_modules/@fastscript/core-private/src/module-loader.mjs +46 -0
  44. package/node_modules/@fastscript/core-private/src/oauth-providers.mjs +103 -0
  45. package/node_modules/@fastscript/core-private/src/observability.mjs +21 -0
  46. package/node_modules/@fastscript/core-private/src/plugins.mjs +194 -0
  47. package/node_modules/@fastscript/core-private/src/retention.mjs +57 -0
  48. package/node_modules/@fastscript/core-private/src/routes.mjs +178 -0
  49. package/node_modules/@fastscript/core-private/src/scheduler.mjs +104 -0
  50. package/node_modules/@fastscript/core-private/src/security.mjs +233 -0
  51. package/node_modules/@fastscript/core-private/src/server-runtime.mjs +849 -0
  52. package/node_modules/@fastscript/core-private/src/serverless-handler.mjs +20 -0
  53. package/node_modules/@fastscript/core-private/src/session-policy.mjs +38 -0
  54. package/node_modules/@fastscript/core-private/src/start.mjs +10 -0
  55. package/node_modules/@fastscript/core-private/src/storage.mjs +155 -0
  56. package/node_modules/@fastscript/core-private/src/style-primitives.mjs +538 -0
  57. package/node_modules/@fastscript/core-private/src/style-system.mjs +461 -0
  58. package/node_modules/@fastscript/core-private/src/tenant.mjs +55 -0
  59. package/node_modules/@fastscript/core-private/src/typecheck.mjs +1464 -0
  60. package/node_modules/@fastscript/core-private/src/validate.mjs +22 -0
  61. package/node_modules/@fastscript/core-private/src/validation.mjs +88 -0
  62. package/node_modules/@fastscript/core-private/src/webhook.mjs +81 -0
  63. package/node_modules/@fastscript/core-private/src/worker.mjs +24 -0
  64. package/package.json +86 -13
  65. package/src/asset-optimizer.mjs +67 -0
  66. package/src/audit-log.mjs +50 -0
  67. package/src/auth.mjs +1 -115
  68. package/src/bench.mjs +20 -7
  69. package/src/build.mjs +1 -234
  70. package/src/cache.mjs +210 -20
  71. package/src/cli.mjs +29 -5
  72. package/src/compat.mjs +8 -10
  73. package/src/create.mjs +71 -17
  74. package/src/csp.mjs +26 -0
  75. package/src/db-cli.mjs +152 -8
  76. package/src/db-postgres-collection.mjs +110 -0
  77. package/src/deploy.mjs +1 -65
  78. package/src/docs-search.mjs +35 -0
  79. package/src/env.mjs +34 -5
  80. package/src/fs-diagnostics.mjs +70 -0
  81. package/src/fs-error-codes.mjs +126 -0
  82. package/src/fs-formatter.mjs +66 -0
  83. package/src/fs-linter.mjs +274 -0
  84. package/src/fs-normalize.mjs +21 -238
  85. package/src/fs-parser.mjs +1 -0
  86. package/src/generated/docs-search-index.mjs +3220 -0
  87. package/src/i18n.mjs +25 -0
  88. package/src/jobs.mjs +283 -32
  89. package/src/metrics.mjs +45 -0
  90. package/src/migration-wizard.mjs +16 -0
  91. package/src/module-loader.mjs +11 -12
  92. package/src/oauth-providers.mjs +103 -0
  93. package/src/plugins.mjs +194 -0
  94. package/src/retention.mjs +57 -0
  95. package/src/routes.mjs +178 -0
  96. package/src/scheduler.mjs +104 -0
  97. package/src/security.mjs +197 -19
  98. package/src/server-runtime.mjs +1 -339
  99. package/src/serverless-handler.mjs +20 -0
  100. package/src/session-policy.mjs +38 -0
  101. package/src/storage.mjs +1 -56
  102. package/src/style-system.mjs +461 -0
  103. package/src/tenant.mjs +55 -0
  104. package/src/typecheck.mjs +1 -0
  105. package/src/validate.mjs +5 -1
  106. package/src/validation.mjs +14 -5
  107. package/src/webhook.mjs +1 -71
  108. package/src/worker.mjs +23 -4
  109. package/src/language-spec.mjs +0 -58
@@ -0,0 +1,22 @@
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
+ import { runTypeCheck } from "./typecheck.mjs";
8
+ import { runLint } from "./fs-linter.mjs";
9
+
10
+ export async function runValidate() {
11
+ await runCheck();
12
+ await runLint(["--mode", "fail"]);
13
+ await runTypeCheck(["--mode", "fail"]);
14
+ await runBuild();
15
+ await runBench();
16
+ await runCompat();
17
+ await runDbMigrate();
18
+ await runDbSeed();
19
+ await runExport(["--to", "js", "--out", "exported-js-app"]);
20
+ await runExport(["--to", "ts", "--out", "exported-ts-app"]);
21
+ console.log("validate complete: check/lint/typecheck/build/bench/compat/db/export all passed");
22
+ }
@@ -0,0 +1,88 @@
1
+ export async function readBody(req, { maxBytes = 1024 * 1024 } = {}) {
2
+ const chunks = [];
3
+ let total = 0;
4
+ for await (const chunk of req) {
5
+ const buf = Buffer.from(chunk);
6
+ total += buf.byteLength;
7
+ if (total > maxBytes) {
8
+ const error = new Error(`Request body too large (max ${maxBytes} bytes)`);
9
+ error.status = 413;
10
+ throw error;
11
+ }
12
+ chunks.push(buf);
13
+ }
14
+ const text = Buffer.concat(chunks).toString("utf8");
15
+ return text;
16
+ }
17
+
18
+ export async function readJsonBody(req, { maxBytes = 1024 * 1024 } = {}) {
19
+ const raw = await readBody(req, { maxBytes });
20
+ if (!raw.trim()) return {};
21
+ try {
22
+ return JSON.parse(raw);
23
+ } catch {
24
+ const error = new Error("Invalid JSON body");
25
+ error.status = 400;
26
+ throw error;
27
+ }
28
+ }
29
+
30
+ export function validateShape(schema, input, scope = "input") {
31
+ if (!schema || typeof schema !== "object") return { ok: true, value: input ?? {} };
32
+ const errors = [];
33
+ const out = {};
34
+ const source = input && typeof input === "object" ? input : {};
35
+
36
+ for (const [key, rule] of Object.entries(schema)) {
37
+ const value = source[key];
38
+ const optional = typeof rule === "string" && rule.endsWith("?");
39
+ const t = typeof rule === "string" ? rule.replace(/\?$/, "") : String(rule);
40
+
41
+ if (value === undefined || value === null) {
42
+ if (!optional) errors.push(`${scope}.${key} is required`);
43
+ continue;
44
+ }
45
+
46
+ if (t === "array") {
47
+ if (!Array.isArray(value)) errors.push(`${scope}.${key} must be array`);
48
+ else out[key] = value;
49
+ continue;
50
+ }
51
+
52
+ if (t === "int") {
53
+ const n = Number(value);
54
+ if (!Number.isInteger(n)) errors.push(`${scope}.${key} must be integer`);
55
+ else out[key] = n;
56
+ continue;
57
+ }
58
+
59
+ if (t === "float" || t === "number") {
60
+ const n = Number(value);
61
+ if (!Number.isFinite(n)) errors.push(`${scope}.${key} must be number`);
62
+ else out[key] = n;
63
+ continue;
64
+ }
65
+
66
+ if (t === "bool" || t === "boolean") {
67
+ if (typeof value !== "boolean") errors.push(`${scope}.${key} must be boolean`);
68
+ else out[key] = value;
69
+ continue;
70
+ }
71
+
72
+ if (t === "str" || t === "string") {
73
+ if (typeof value !== "string") errors.push(`${scope}.${key} must be string`);
74
+ else out[key] = value;
75
+ continue;
76
+ }
77
+
78
+ out[key] = value;
79
+ }
80
+
81
+ if (errors.length) {
82
+ const error = new Error(`Validation failed: ${errors.join("; ")}`);
83
+ error.status = 400;
84
+ error.details = errors;
85
+ throw error;
86
+ }
87
+ return { ok: true, value: out };
88
+ }
@@ -0,0 +1,81 @@
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, { maxBytes = 1024 * 1024 } = {}) {
6
+ const chunks = [];
7
+ let total = 0;
8
+ for await (const chunk of req) {
9
+ const buf = Buffer.from(chunk);
10
+ total += buf.byteLength;
11
+ if (total > maxBytes) {
12
+ const error = new Error(`Webhook payload too large (max ${maxBytes} bytes)`);
13
+ error.status = 413;
14
+ throw error;
15
+ }
16
+ chunks.push(buf);
17
+ }
18
+ return Buffer.concat(chunks);
19
+ }
20
+
21
+ export function signPayload(payload, secret, algo = "sha256") {
22
+ const mac = createHmac(algo, secret).update(payload).digest("hex");
23
+ return `${algo}=${mac}`;
24
+ }
25
+
26
+ export function verifySignature(payload, header, secret, algo = "sha256") {
27
+ if (!header || typeof header !== "string") return false;
28
+ const expected = signPayload(payload, secret, algo);
29
+ const a = Buffer.from(expected);
30
+ const b = Buffer.from(header);
31
+ if (a.length !== b.length) return false;
32
+ return timingSafeEqual(a, b);
33
+ }
34
+
35
+ export function isReplay(tsSeconds, maxSkewSec = 300) {
36
+ const ts = Number(tsSeconds);
37
+ if (!Number.isFinite(ts)) return true;
38
+ const now = Math.floor(Date.now() / 1000);
39
+ return Math.abs(now - ts) > maxSkewSec;
40
+ }
41
+
42
+ function replayStore({ dir = ".fastscript", ttlSec = 600 } = {}) {
43
+ const root = resolve(dir);
44
+ mkdirSync(root, { recursive: true });
45
+ const file = join(root, "webhook-replay.json");
46
+ let state = { seen: {} };
47
+ if (existsSync(file)) {
48
+ try { state = JSON.parse(readFileSync(file, "utf8")); } catch {}
49
+ }
50
+ function persist() {
51
+ writeFileSync(file, JSON.stringify(state, null, 2), "utf8");
52
+ }
53
+ return {
54
+ has(id) {
55
+ const now = Date.now();
56
+ for (const [k, exp] of Object.entries(state.seen)) if (exp < now) delete state.seen[k];
57
+ persist();
58
+ return Boolean(state.seen[id]);
59
+ },
60
+ add(id) {
61
+ state.seen[id] = Date.now() + ttlSec * 1000;
62
+ persist();
63
+ },
64
+ };
65
+ }
66
+
67
+ export async function verifyWebhookRequest(req, { secret, signatureHeader = "x-signature", timestampHeader = "x-timestamp", idHeader = "x-event-id", maxSkewSec = 300, replayDir = ".fastscript", maxBytes = 1024 * 1024 } = {}) {
68
+ const raw = await readRawBody(req, { maxBytes });
69
+ const sig = req.headers[signatureHeader];
70
+ const ts = req.headers[timestampHeader];
71
+ const eventId = req.headers[idHeader];
72
+ const id = Array.isArray(eventId) ? eventId[0] : eventId;
73
+ if (isReplay(ts, maxSkewSec)) return { ok: false, reason: "replay_window" };
74
+ if (!verifySignature(raw, Array.isArray(sig) ? sig[0] : sig, secret)) return { ok: false, reason: "bad_signature" };
75
+ if (id) {
76
+ const store = replayStore({ dir: replayDir, ttlSec: Math.max(maxSkewSec, 600) });
77
+ if (store.has(id)) return { ok: false, reason: "duplicate_event" };
78
+ store.add(id);
79
+ }
80
+ return { ok: true, raw };
81
+ }
@@ -0,0 +1,24 @@
1
+ import { replayDeadLetter, runWorker } from "./jobs.mjs";
2
+
3
+ export async function runWorkerCommand(args = []) {
4
+ if (args[0] === "replay-dead-letter") {
5
+ const limitArg = args.indexOf("--limit");
6
+ const nameArg = args.indexOf("--name");
7
+ const limit = limitArg >= 0 ? Number(args[limitArg + 1] || 20) : 20;
8
+ const name = nameArg >= 0 ? args[nameArg + 1] || null : null;
9
+ const replayed = await replayDeadLetter({
10
+ dir: ".fastscript",
11
+ limit,
12
+ name,
13
+ driver: process.env.JOBS_DRIVER || "file",
14
+ });
15
+ console.log(`dead-letter replayed: ${replayed.length}`);
16
+ return;
17
+ }
18
+
19
+ await runWorker({
20
+ dir: ".fastscript",
21
+ pollMs: Number(process.env.WORKER_POLL_MS || 350),
22
+ driver: process.env.JOBS_DRIVER || "file",
23
+ });
24
+ }
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "fastscript",
3
- "version": "1.0.0",
4
- "description": "JavaScript-first full-stack framework that is simpler and faster.",
5
- "license": "MIT",
3
+ "version": "2.0.0",
4
+ "description": "JavaScript-first full-stack language runtime with first-class .fs source.",
5
+ "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
7
+ "publishConfig": {
8
+ "access": "public",
9
+ "registry": "https://registry.npmjs.org/"
10
+ },
7
11
  "bin": {
8
12
  "fastscript": "src/cli.mjs"
9
13
  },
@@ -17,48 +21,117 @@
17
21
  "dev": "node ./src/cli.mjs dev",
18
22
  "start": "node ./src/cli.mjs start",
19
23
  "build": "node ./src/cli.mjs build",
24
+ "build:ssg": "node ./src/cli.mjs ssg",
20
25
  "create": "node ./src/cli.mjs create",
26
+ "create:startup-mvp": "node ./src/cli.mjs create startup-mvp --template startup-mvp",
27
+ "create:fullstack": "node ./src/cli.mjs create fullstack --template fullstack",
21
28
  "check": "node ./src/cli.mjs check",
22
29
  "migrate": "node ./src/cli.mjs migrate",
30
+ "wizard:migrate": "node ./src/cli.mjs wizard:migrate app/pages/index.fs",
23
31
  "bench": "node ./src/cli.mjs bench",
24
32
  "export:js": "node ./src/cli.mjs export --to js --out exported-js-app",
25
33
  "export:ts": "node ./src/cli.mjs export --to ts --out exported-ts-app",
26
34
  "compat": "node ./src/cli.mjs compat",
27
35
  "validate": "node ./src/cli.mjs validate",
36
+ "repo:lock": "node ./scripts/ensure-canonical-repo.mjs",
37
+ "typecheck": "node ./src/cli.mjs typecheck --mode fail",
38
+ "typecheck:pass": "node ./src/cli.mjs typecheck --mode pass",
39
+ "format": "node ./src/cli.mjs format --write",
40
+ "format:check": "node ./src/cli.mjs format --check",
41
+ "lint:fs": "node ./src/cli.mjs lint --mode fail",
42
+ "lint:fs:pass": "node ./src/cli.mjs lint --mode pass",
28
43
  "db:migrate": "node ./src/cli.mjs db:migrate",
29
44
  "db:seed": "node ./src/cli.mjs db:seed",
45
+ "db:rollback": "node ./src/cli.mjs db:rollback --count 1",
30
46
  "deploy:node": "node ./src/cli.mjs deploy --target node",
31
47
  "deploy:vercel": "node ./src/cli.mjs deploy --target vercel",
32
48
  "deploy:cloudflare": "node ./src/cli.mjs deploy --target cloudflare",
33
49
  "worker": "node ./src/cli.mjs worker",
50
+ "worker:replay-dead-letter": "node ./src/cli.mjs worker replay-dead-letter --limit 50",
51
+ "style:generate": "node ./scripts/style-generate.mjs",
52
+ "style:check": "node ./scripts/style-check.mjs",
34
53
  "smoke:dev": "node ./scripts/smoke-dev.mjs",
35
54
  "smoke:start": "node ./scripts/smoke-start.mjs",
55
+ "rollback:drill": "node ./scripts/rollback-drill.mjs",
56
+ "soak:window": "node ./scripts/soak-window.mjs",
36
57
  "test:middleware": "node ./scripts/test-middleware.mjs",
37
58
  "test:auth": "node ./scripts/test-auth.mjs",
38
59
  "test:db": "node ./scripts/test-db.mjs",
60
+ "test:db-cli": "node ./scripts/test-db-cli.mjs",
61
+ "test:fs-diag": "node ./scripts/test-fs-diagnostics.mjs",
39
62
  "test:roundtrip": "node ./scripts/test-roundtrip.mjs",
40
63
  "test:validation": "node ./scripts/test-validation.mjs",
41
64
  "test:webhook-storage": "node ./scripts/test-webhook-storage.mjs",
42
65
  "test:jobs": "node ./scripts/test-jobs.mjs",
43
- "test:language-spec": "node ./scripts/test-language-spec.mjs",
44
- "test:parser-fuzz": "node ./scripts/test-parser-fuzz.mjs",
45
- "test:normalizer-stress": "node ./scripts/test-normalizer-stress.mjs",
66
+ "test:plugins": "node ./scripts/test-plugins.mjs",
67
+ "test:metrics": "node ./scripts/test-metrics.mjs",
68
+ "test:sourcemap-fidelity": "node ./scripts/test-sourcemap-fidelity.mjs",
69
+ "test:cache-parity": "node ./scripts/test-cache-parity.mjs",
70
+ "test:typecheck": "node ./scripts/test-typecheck.mjs",
71
+ "test:v2:ambient-runtime": "node ./scripts/test-v2-ambient-runtime.mjs",
72
+ "test:v2:stdlib-methods": "node ./scripts/test-v2-stdlib-methods.mjs",
73
+ "test:v2:stdlib-matrix": "node ./scripts/test-v2-stdlib-matrix.mjs",
74
+ "test:v2:dom-globals": "node ./scripts/test-v2-dom-globals.mjs",
75
+ "test:v2:dom-patterns": "node ./scripts/test-v2-dom-patterns.mjs",
76
+ "test:v2:inference-corpus": "node ./scripts/test-v2-inference-corpus.mjs",
77
+ "test:v2:inference-patterns": "node ./scripts/test-v2-inference-patterns.mjs",
78
+ "test:v2:zero-js-app": "node ./scripts/test-v2-zero-js-app.mjs",
79
+ "test:v2:zero-js-corpora": "node ./scripts/test-v2-zero-js-corpora.mjs",
80
+ "test:runtime-scope-diagnostics": "node ./scripts/test-runtime-scope-diagnostics.mjs",
81
+ "test:runtime-context-rules": "node ./scripts/test-runtime-context-rules.mjs",
82
+ "test:format-lint": "node ./scripts/test-format-lint.mjs",
83
+ "test:style-rules": "node ./scripts/test-style-rules.mjs",
84
+ "test:style-primitives": "node ./scripts/test-style-primitives.mjs",
85
+ "test:routes": "node ./scripts/test-routes.mjs",
86
+ "test:deploy-adapters": "node ./scripts/test-deploy-adapters.mjs",
87
+ "test:typecheck-depth": "node ./scripts/test-typecheck-depth.mjs",
46
88
  "test:determinism": "node ./scripts/test-determinism.mjs",
89
+ "test:parser-fuzz": "node ./scripts/test-parser-fuzz.mjs",
47
90
  "test:runtime-contract": "node ./scripts/test-runtime-contract.mjs",
48
- "bench:language": "node ./scripts/bench-language-normalize.mjs",
91
+ "test:security-baseline": "node ./scripts/test-security-baseline.mjs",
92
+ "test:interop-matrix": "node ./scripts/interop-matrix.mjs --mode test",
93
+ "test:vscode-language": "node ./vscode/fastscript-language/lsp/smoke-test.cjs",
94
+ "test:conformance": "node ./scripts/test-conformance.mjs",
95
+ "test:conformance:update": "node ./scripts/test-conformance.mjs --update",
49
96
  "bench:report": "node ./scripts/bench-report.mjs",
50
- "test:core": "npm run test:middleware && npm run test:auth && npm run test:db && npm run test:validation && npm run test:webhook-storage && npm run test:jobs && npm run test:roundtrip && npm run test:language-spec && npm run test:parser-fuzz && npm run test:normalizer-stress && npm run test:determinism && npm run test:runtime-contract",
51
- "qa:gate": "npm run validate && npm run test:core",
52
- "qa:all": "npm run qa:gate && npm run smoke:dev && npm run smoke:start && npm run bench:language && npm run bench:report",
97
+ "benchmark:suite": "node ./scripts/benchmark-suite.mjs",
98
+ "interop:report": "node ./scripts/interop-matrix.mjs --mode report",
99
+ "sbom:generate": "node ./scripts/generate-sbom.mjs",
100
+ "proof:publish": "node ./scripts/publish-proof-pack.mjs",
101
+ "docs:index": "node ./scripts/build-docs-index.mjs",
102
+ "docs:api-ref": "node ./scripts/generate-api-reference.mjs",
103
+ "plugins:marketplace-sync": "node ./scripts/plugin-marketplace-sync.mjs",
104
+ "security:rotate-secrets": "node ./scripts/secret-rotation.mjs",
105
+ "backup:create": "node ./scripts/backup.mjs",
106
+ "backup:restore": "node ./scripts/restore.mjs",
107
+ "backup:verify": "node ./scripts/backup-verify.mjs",
108
+ "retention:sweep": "node ./scripts/retention-sweep.mjs",
109
+ "kpi:track": "node ./scripts/kpi-track.mjs",
110
+ "deploy:zero-downtime": "node ./scripts/zero-downtime-deploy.mjs",
111
+ "merge:gate": "node ./scripts/release-merge-gate.mjs",
112
+ "test:core": "npm run test:middleware && npm run test:auth && npm run test:db && npm run test:db-cli && npm run test:validation && npm run test:webhook-storage && npm run test:jobs && npm run test:plugins && npm run test:fs-diag && npm run test:metrics && npm run test:roundtrip && npm run test:sourcemap-fidelity && npm run test:cache-parity && npm run test:typecheck && npm run test:typecheck-depth && npm run test:format-lint && npm run test:style-rules && npm run test:style-primitives && npm run test:routes && npm run test:runtime-contract && npm run test:determinism && npm run test:parser-fuzz && npm run test:security-baseline && npm run test:deploy-adapters && npm run test:interop-matrix && npm run test:vscode-language && npm run test:runtime-context-rules && npm run test:runtime-scope-diagnostics && npm run test:v2:ambient-runtime && npm run test:v2:stdlib-methods && npm run test:v2:stdlib-matrix && npm run test:v2:dom-globals && npm run test:v2:dom-patterns && npm run test:v2:inference-corpus && npm run test:v2:inference-patterns && npm run test:v2:zero-js-app && npm run test:v2:zero-js-corpora && npm run test:conformance",
113
+ "qa:gate": "npm run repo:lock && npm run format:check && npm run lint:fs && npm run typecheck && npm run validate && npm run test:core && npm run smoke:dev && npm run smoke:start",
114
+ "qa:all": "npm run repo:lock && npm run format:check && npm run lint:fs && npm run typecheck && npm run docs:index && npm run docs:api-ref && npm run validate && npm run test:core && npm run smoke:dev && npm run smoke:start && npm run bench:report && npm run benchmark:suite && npm run interop:report && npm run sbom:generate && npm run proof:publish && npm run backup:create && npm run backup:verify",
53
115
  "release:patch": "node ./scripts/release.mjs patch",
54
116
  "release:minor": "node ./scripts/release.mjs minor",
55
117
  "release:major": "node ./scripts/release.mjs major",
56
- "pack:check": "npm pack --dry-run"
118
+ "pack:check": "node ./scripts/prepare-npm-release.mjs --pack-dry-run",
119
+ "hooks:install": "node ./scripts/install-hooks.mjs",
120
+ "public:bundle": "node ./scripts/prepare-public-docs-bundle.mjs",
121
+ "release:npm:prepare": "node ./scripts/prepare-npm-release.mjs"
57
122
  },
58
123
  "engines": {
59
124
  "node": ">=20.0.0"
60
125
  },
61
126
  "dependencies": {
62
- "esbuild": "^0.25.11"
63
- }
127
+ "acorn": "^8.16.0",
128
+ "acorn-walk": "^8.3.5",
129
+ "astring": "^1.9.0",
130
+ "esbuild": "^0.25.11",
131
+ "@fastscript/core-private": "2.0.0"
132
+ },
133
+ "bundleDependencies": [
134
+ "@fastscript/core-private"
135
+ ],
136
+ "private": false
64
137
  }
@@ -0,0 +1,67 @@
1
+ import { createHash } from "node:crypto";
2
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
3
+ import { dirname, extname, join, relative, resolve } from "node:path";
4
+
5
+ function walk(dir) {
6
+ if (!existsSync(dir)) return [];
7
+ const out = [];
8
+ for (const entry of readdirSync(dir)) {
9
+ const full = join(dir, entry);
10
+ const st = statSync(full);
11
+ if (st.isDirectory()) out.push(...walk(full));
12
+ else out.push(full);
13
+ }
14
+ return out;
15
+ }
16
+
17
+ function hashFile(path) {
18
+ return createHash("sha1").update(readFileSync(path)).digest("hex").slice(0, 8);
19
+ }
20
+
21
+ export async function optimizeImageAssets({ appDir = "app", distDir = "dist" } = {}) {
22
+ const sourceRoot = resolve(appDir, "assets", "images");
23
+ const targetRoot = resolve(distDir, "assets", "images");
24
+ mkdirSync(targetRoot, { recursive: true });
25
+ const files = walk(sourceRoot).filter((file) => [".png", ".jpg", ".jpeg", ".webp", ".svg", ".gif"].includes(extname(file).toLowerCase()));
26
+ const manifest = {};
27
+
28
+ for (const file of files) {
29
+ const rel = relative(sourceRoot, file).replace(/\\/g, "/");
30
+ const ext = extname(rel);
31
+ const base = rel.slice(0, -ext.length);
32
+ const hashed = `${base}.${hashFile(file)}${ext}`;
33
+ const out = join(targetRoot, hashed);
34
+ mkdirSync(dirname(out), { recursive: true });
35
+ cpSync(file, out);
36
+ manifest[`/assets/images/${rel}`] = `/assets/images/${hashed}`;
37
+ }
38
+
39
+ writeFileSync(resolve(distDir, "image-manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
40
+ return manifest;
41
+ }
42
+
43
+ export async function optimizeFontAssets({ appDir = "app", distDir = "dist" } = {}) {
44
+ const sourceRoot = resolve(appDir, "assets", "fonts");
45
+ const targetRoot = resolve(distDir, "assets", "fonts");
46
+ mkdirSync(targetRoot, { recursive: true });
47
+ const files = walk(sourceRoot).filter((file) => [".woff2", ".woff", ".ttf", ".otf"].includes(extname(file).toLowerCase()));
48
+ const manifest = {};
49
+ const css = [];
50
+
51
+ for (const file of files) {
52
+ const rel = relative(sourceRoot, file).replace(/\\/g, "/");
53
+ const ext = extname(rel);
54
+ const base = rel.slice(0, -ext.length);
55
+ const hashed = `${base}.${hashFile(file)}${ext}`;
56
+ const out = join(targetRoot, hashed);
57
+ mkdirSync(dirname(out), { recursive: true });
58
+ cpSync(file, out);
59
+ manifest[`/assets/fonts/${rel}`] = `/assets/fonts/${hashed}`;
60
+ const family = base.split("/").pop().replace(/[-_]/g, " ");
61
+ css.push(`@font-face{font-family:"${family}";src:url('/assets/fonts/${hashed}') format('${ext.replace(".", "")}');font-display:swap;}`);
62
+ }
63
+
64
+ writeFileSync(resolve(distDir, "font-manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
65
+ if (css.length) writeFileSync(resolve(distDir, "fonts.generated.css"), `${css.join("\n")}\n`, "utf8");
66
+ return manifest;
67
+ }
@@ -0,0 +1,50 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { dirname, resolve } from "node:path";
4
+
5
+ function sha(input) {
6
+ return createHash("sha256").update(input).digest("hex");
7
+ }
8
+
9
+ function readLines(path) {
10
+ if (!existsSync(path)) return [];
11
+ return readFileSync(path, "utf8").split(/\r?\n/).filter(Boolean);
12
+ }
13
+
14
+ export function createAuditLog({ file = ".fastscript/audit.log" } = {}) {
15
+ const path = resolve(file);
16
+ mkdirSync(dirname(path), { recursive: true });
17
+
18
+ function append(event) {
19
+ const lines = readLines(path);
20
+ let prevHash = "genesis";
21
+ if (lines.length) {
22
+ const last = JSON.parse(lines[lines.length - 1]);
23
+ prevHash = last.hash || "genesis";
24
+ }
25
+ const record = {
26
+ ts: new Date().toISOString(),
27
+ prevHash,
28
+ ...event,
29
+ };
30
+ record.hash = sha(JSON.stringify({ ...record, hash: undefined }));
31
+ lines.push(JSON.stringify(record));
32
+ writeFileSync(path, `${lines.join("\n")}\n`, "utf8");
33
+ return record;
34
+ }
35
+
36
+ function verify() {
37
+ const lines = readLines(path);
38
+ let prevHash = "genesis";
39
+ for (const line of lines) {
40
+ const row = JSON.parse(line);
41
+ if (row.prevHash !== prevHash) return { ok: false, row };
42
+ const expected = sha(JSON.stringify({ ...row, hash: undefined }));
43
+ if (row.hash !== expected) return { ok: false, row };
44
+ prevHash = row.hash;
45
+ }
46
+ return { ok: true, count: lines.length };
47
+ }
48
+
49
+ return { append, verify, file: path };
50
+ }
package/src/auth.mjs CHANGED
@@ -1,115 +1 @@
1
- import { createHmac, randomBytes } from "node:crypto";
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
- import { join } from "node:path";
4
-
5
- function readJson(path, fallback) {
6
- if (!existsSync(path)) return fallback;
7
- try {
8
- return JSON.parse(readFileSync(path, "utf8"));
9
- } catch {
10
- return fallback;
11
- }
12
- }
13
-
14
- function writeJson(path, value) {
15
- writeFileSync(path, JSON.stringify(value, null, 2), "utf8");
16
- }
17
-
18
- export function parseCookies(header) {
19
- const out = {};
20
- if (!header) return out;
21
- for (const part of header.split(";")) {
22
- const [k, ...rest] = part.trim().split("=");
23
- if (!k) continue;
24
- out[k] = decodeURIComponent(rest.join("=") || "");
25
- }
26
- return out;
27
- }
28
-
29
- export function serializeCookie(name, value, opts = {}) {
30
- const bits = [`${name}=${encodeURIComponent(value)}`];
31
- bits.push(`Path=${opts.path ?? "/"}`);
32
- if (opts.httpOnly !== false) bits.push("HttpOnly");
33
- if (opts.sameSite) bits.push(`SameSite=${opts.sameSite}`);
34
- else bits.push("SameSite=Lax");
35
- if (opts.secure) bits.push("Secure");
36
- if (typeof opts.maxAge === "number") bits.push(`Max-Age=${opts.maxAge}`);
37
- return bits.join("; ");
38
- }
39
-
40
- function sign(payload, secret) {
41
- return createHmac("sha256", secret).update(payload).digest("hex");
42
- }
43
-
44
- export function createSessionManager({ dir = ".fastscript", cookieName = "fs_session", secret = "fastscript-dev-secret" } = {}) {
45
- mkdirSync(dir, { recursive: true });
46
- const path = join(dir, "sessions.json");
47
- const state = readJson(path, { sessions: {} });
48
-
49
- function persist() {
50
- writeJson(path, state);
51
- }
52
-
53
- return {
54
- cookieName,
55
- sweepExpired() {
56
- let removed = 0;
57
- const now = Date.now();
58
- for (const [id, row] of Object.entries(state.sessions)) {
59
- if (!row || row.exp < now) {
60
- delete state.sessions[id];
61
- removed += 1;
62
- }
63
- }
64
- if (removed) persist();
65
- return removed;
66
- },
67
- create(user, maxAgeSec = 60 * 60 * 24 * 7) {
68
- const id = randomBytes(12).toString("hex");
69
- const exp = Date.now() + maxAgeSec * 1000;
70
- state.sessions[id] = { user, exp };
71
- persist();
72
- const sig = sign(`${id}.${exp}`, secret);
73
- return `${id}.${exp}.${sig}`;
74
- },
75
- read(token) {
76
- if (!token) return null;
77
- const [id, expStr, sig] = token.split(".");
78
- if (!id || !expStr || !sig) return null;
79
- const expected = sign(`${id}.${expStr}`, secret);
80
- if (expected !== sig) return null;
81
- const exp = Number(expStr);
82
- if (!Number.isFinite(exp) || exp < Date.now()) return null;
83
- const row = state.sessions[id];
84
- if (!row || row.exp < Date.now()) {
85
- if (row) {
86
- delete state.sessions[id];
87
- persist();
88
- }
89
- return null;
90
- }
91
- return row;
92
- },
93
- delete(token) {
94
- const [id] = String(token || "").split(".");
95
- if (!id) return;
96
- delete state.sessions[id];
97
- persist();
98
- },
99
- rotate(token, maxAgeSec = 60 * 60 * 24 * 7) {
100
- const row = this.read(token);
101
- if (!row?.user) return null;
102
- this.delete(token);
103
- return this.create(row.user, maxAgeSec);
104
- },
105
- };
106
- }
107
-
108
- export function requireUser(user) {
109
- if (!user) {
110
- const error = new Error("Unauthorized");
111
- error.status = 401;
112
- throw error;
113
- }
114
- return user;
115
- }
1
+ export * from "@fastscript/core-private/auth";
package/src/bench.mjs CHANGED
@@ -3,8 +3,8 @@ import { gzipSync } from "node:zlib";
3
3
  import { join, resolve } from "node:path";
4
4
 
5
5
  const DIST = resolve("dist");
6
- const JS_BUDGET_BYTES = 30 * 1024;
7
- const CSS_BUDGET_BYTES = 10 * 1024;
6
+ const JS_BUDGET_BYTES = Number(process.env.FASTSCRIPT_JS_BUDGET_KB || 30) * 1024;
7
+ const CSS_BUDGET_BYTES = Number(process.env.FASTSCRIPT_CSS_BUDGET_KB || 15) * 1024;
8
8
 
9
9
  function gzipSize(path) {
10
10
  if (!existsSync(path)) return 0;
@@ -16,15 +16,29 @@ function kb(bytes) {
16
16
  return `${(bytes / 1024).toFixed(2)}KB`;
17
17
  }
18
18
 
19
+ function loadJson(path) {
20
+ if (!existsSync(path)) return null;
21
+ return JSON.parse(readFileSync(path, "utf8"));
22
+ }
23
+
24
+ function resolveAsset(distPath, assetManifest, logicalName) {
25
+ const direct = join(distPath, logicalName);
26
+ if (existsSync(direct)) return direct;
27
+ const mapped = assetManifest?.mapping?.[logicalName];
28
+ if (!mapped) return direct;
29
+ return join(distPath, mapped.replace(/^\.\//, ""));
30
+ }
31
+
19
32
  export async function runBench() {
20
33
  const manifestPath = join(DIST, "fastscript-manifest.json");
21
34
  if (!existsSync(manifestPath)) {
22
35
  throw new Error("Missing dist build output. Run: fastscript build");
23
36
  }
24
37
 
25
- const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
26
- const jsAssets = [join(DIST, "router.js")];
27
- const cssAssets = [join(DIST, "styles.css")];
38
+ const manifest = loadJson(manifestPath);
39
+ const assetManifest = loadJson(join(DIST, "asset-manifest.json"));
40
+ const jsAssets = [resolveAsset(DIST, assetManifest, "router.js")];
41
+ const cssAssets = [resolveAsset(DIST, assetManifest, "styles.css")];
28
42
 
29
43
  if (manifest.layout) jsAssets.push(join(DIST, manifest.layout.replace(/^\.\//, "")));
30
44
  const root = manifest.routes.find((r) => r.path === "/");
@@ -33,7 +47,7 @@ export async function runBench() {
33
47
  const totalJs = jsAssets.reduce((sum, p) => sum + gzipSize(p), 0);
34
48
  const totalCss = cssAssets.reduce((sum, p) => sum + gzipSize(p), 0);
35
49
 
36
- console.log(`3G budget check -> JS: ${kb(totalJs)} / 30.00KB, CSS: ${kb(totalCss)} / 10.00KB`);
50
+ console.log(`3G budget check -> JS: ${kb(totalJs)} / ${kb(JS_BUDGET_BYTES)}, CSS: ${kb(totalCss)} / ${kb(CSS_BUDGET_BYTES)}`);
37
51
 
38
52
  const errors = [];
39
53
  if (totalJs > JS_BUDGET_BYTES) errors.push(`JS budget exceeded by ${kb(totalJs - JS_BUDGET_BYTES)}`);
@@ -43,4 +57,3 @@ export async function runBench() {
43
57
  throw new Error(errors.join("\n"));
44
58
  }
45
59
  }
46
-