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
package/src/storage.mjs CHANGED
@@ -1,56 +1 @@
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
- }
1
+ export * from "@fastscript/core-private/storage";
@@ -0,0 +1,461 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { validatePrimitiveMarkup } from "@fastscript/core-private/style-primitives";
4
+
5
+ const TOKENS_PATH = join("app", "design", "tokens.json");
6
+ const CLASS_ALLOWLIST_PATH = join("app", "design", "class-allowlist.json");
7
+ const GENERATED_CSS_PATH = join("app", "styles.generated.css");
8
+
9
+ const DEFAULT_TOKENS = {
10
+ color: {
11
+ bg: "#050505",
12
+ surface: "#090909",
13
+ text: "#ffffff",
14
+ muted: "#8a8a8a",
15
+ border: "#1f1f1f",
16
+ accent: "#9f92ff",
17
+ accentSoft: "#d3d3ff",
18
+ },
19
+ space: {
20
+ 1: "4px",
21
+ 2: "8px",
22
+ 3: "12px",
23
+ 4: "16px",
24
+ 5: "20px",
25
+ 6: "24px",
26
+ 8: "32px",
27
+ 10: "40px",
28
+ 12: "48px",
29
+ },
30
+ radius: {
31
+ sm: "8px",
32
+ md: "12px",
33
+ lg: "16px",
34
+ },
35
+ shadow: {
36
+ soft: "0 10px 40px rgba(0,0,0,0.22)",
37
+ },
38
+ };
39
+
40
+ const DEFAULT_CLASS_ALLOWLIST = [
41
+ "nav",
42
+ "page",
43
+ "footer",
44
+ "hero",
45
+ "eyebrow",
46
+ "hero-links",
47
+ "grid",
48
+ ];
49
+
50
+ const ALLOWED_STYLE_PROPERTIES = new Set([
51
+ "padding",
52
+ "margin",
53
+ "gap",
54
+ "top",
55
+ "right",
56
+ "bottom",
57
+ "left",
58
+ "bg",
59
+ "text",
60
+ "border",
61
+ "size",
62
+ "weight",
63
+ "display",
64
+ "direction",
65
+ "align",
66
+ "justify",
67
+ ]);
68
+
69
+ const SPACING_PROPS = new Set(["padding", "margin", "gap", "top", "right", "bottom", "left"]);
70
+ const COLOR_PROPS = new Set(["bg", "text", "border"]);
71
+ const ALLOWED_SPACING_VALUES = new Set(Array.from({ length: 14 }, (_, i) => String(i)));
72
+ const ALLOWED_COLOR_NAMES = new Set(["primary", "secondary", "accent", "neutral", "success", "warning", "error"]);
73
+ const ALLOWED_COLOR_SHADES = new Set(["50", "100", "200", "300", "400", "500", "600", "700", "800", "900"]);
74
+ const ALLOWED_TEXT_SIZES = new Set(["xs", "sm", "base", "lg", "xl", "2xl", "3xl", "4xl", "5xl", "6xl"]);
75
+ const ALLOWED_FONT_WEIGHTS = new Set(["thin", "light", "normal", "medium", "semibold", "bold", "extrabold", "black"]);
76
+ const ALLOWED_DISPLAYS = new Set(["flex", "grid", "block", "inline", "inline-block", "none"]);
77
+ const ALLOWED_DIRECTIONS = new Set(["row", "column"]);
78
+ const ALLOWED_ALIGN = new Set(["start", "center", "end", "stretch"]);
79
+ const ALLOWED_JUSTIFY = new Set(["start", "center", "end", "between", "around"]);
80
+ const ALLOWED_BREAKPOINTS = new Set(["sm", "md", "lg", "xl"]);
81
+
82
+ function walk(dir) {
83
+ const out = [];
84
+ if (!existsSync(dir)) return out;
85
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
86
+ const full = join(dir, entry.name);
87
+ if (entry.isDirectory()) out.push(...walk(full));
88
+ else out.push(full);
89
+ }
90
+ return out;
91
+ }
92
+
93
+ function readJson(path, fallback) {
94
+ if (!existsSync(path)) return fallback;
95
+ try {
96
+ return JSON.parse(readFileSync(path, "utf8"));
97
+ } catch {
98
+ return fallback;
99
+ }
100
+ }
101
+
102
+ function toCssVars(tokens, prefix) {
103
+ return Object.entries(tokens || {})
104
+ .map(([name, value]) => ` --fs-${prefix}-${name}: ${value};`)
105
+ .join("\n");
106
+ }
107
+
108
+ function utilityRules(tokens) {
109
+ const lines = [];
110
+ for (const key of Object.keys(tokens?.color || {})) {
111
+ lines.push(`.u-text-${key} { color: var(--fs-color-${key}); }`);
112
+ lines.push(`.u-bg-${key} { background: var(--fs-color-${key}); }`);
113
+ lines.push(`.u-border-${key} { border-color: var(--fs-color-${key}); }`);
114
+ }
115
+ for (const key of Object.keys(tokens?.space || {})) {
116
+ lines.push(`.u-m-${key} { margin: var(--fs-space-${key}); }`);
117
+ lines.push(`.u-mt-${key} { margin-top: var(--fs-space-${key}); }`);
118
+ lines.push(`.u-mb-${key} { margin-bottom: var(--fs-space-${key}); }`);
119
+ lines.push(`.u-p-${key} { padding: var(--fs-space-${key}); }`);
120
+ lines.push(`.u-gap-${key} { gap: var(--fs-space-${key}); }`);
121
+ }
122
+ for (const key of Object.keys(tokens?.radius || {})) {
123
+ lines.push(`.u-radius-${key} { border-radius: var(--fs-radius-${key}); }`);
124
+ }
125
+ return lines.join("\n");
126
+ }
127
+
128
+ function primitiveRules(tokens) {
129
+ const lines = [
130
+ ".fs-box,.fs-stack,.fs-row,.fs-grid,.fs-section,.fs-container,.fs-screen,.fs-card,.fs-panel,.fs-field,.fs-alert,.fs-empty{box-sizing:border-box;min-width:0;}",
131
+ ".fs-stack{display:flex;flex-direction:column;}",
132
+ ".fs-row{display:flex;flex-direction:row;}",
133
+ ".fs-grid{display:grid;grid-template-columns:repeat(var(--fs-grid-cols,1),minmax(0,1fr));}",
134
+ ".fs-container{width:min(100%,72rem);margin-inline:auto;}",
135
+ ".fs-screen{min-height:100dvh;}",
136
+ ".fs-text,.fs-label,.fs-badge,.fs-link,.fs-code{margin:0;color:var(--fs-color-text);}",
137
+ ".fs-heading{margin:0;color:var(--fs-color-text);line-height:1.05;letter-spacing:-0.03em;}",
138
+ ".fs-code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;}",
139
+ ".fs-badge{display:inline-flex;align-items:center;gap:.4rem;padding:.18rem .55rem;border-radius:999px;border:1px solid var(--fs-color-border);background:var(--fs-color-surface);font-size:.78rem;}",
140
+ ".fs-link{text-decoration:none;color:var(--fs-color-accent);}",
141
+ ".fs-link:hover{text-decoration:underline;}",
142
+ ".fs-card,.fs-panel,.fs-field,.fs-alert,.fs-empty{border:1px solid var(--fs-color-border);background:var(--fs-color-surface);border-radius:var(--fs-radius-md);}",
143
+ ".fs-card,.fs-panel{box-shadow:var(--fs-shadow-soft);}",
144
+ ".fs-alert{border-color:var(--fs-color-accent);}",
145
+ ".fs-input,.fs-textarea,.fs-select{width:100%;border:1px solid var(--fs-color-border);background:var(--fs-color-surface);color:var(--fs-color-text);border-radius:var(--fs-radius-md);padding:.82rem 1rem;outline:none;}",
146
+ ".fs-input:focus,.fs-textarea:focus,.fs-select:focus{border-color:var(--fs-color-accent);box-shadow:0 0 0 3px rgba(159,146,255,.18);}",
147
+ ".fs-textarea{min-height:8rem;resize:vertical;}",
148
+ ".fs-button{display:inline-flex;align-items:center;justify-content:center;gap:.55rem;border-radius:999px;border:1px solid transparent;font-weight:600;text-decoration:none;cursor:pointer;transition:transform .18s ease,background .18s ease,border-color .18s ease,box-shadow .18s ease,color .18s ease;}",
149
+ ".fs-button:hover{transform:translateY(-1px);}",
150
+ ".fs-button-tone-primary{background:var(--fs-color-accent);color:var(--fs-color-bg);box-shadow:var(--fs-shadow-soft);}",
151
+ ".fs-button-tone-secondary{background:var(--fs-color-surface);color:var(--fs-color-text);border-color:var(--fs-color-border);}",
152
+ ".fs-button-tone-accent{background:var(--fs-color-accentSoft,var(--fs-color-accent));color:var(--fs-color-bg);}",
153
+ ".fs-button-tone-success{background:var(--fs-color-green,var(--fs-color-accent));color:var(--fs-color-bg);}",
154
+ ".fs-button-tone-warning{background:var(--fs-color-amber,var(--fs-color-accent));color:var(--fs-color-bg);}",
155
+ ".fs-button-tone-error{background:var(--fs-color-danger,var(--fs-color-accent));color:var(--fs-color-bg);}",
156
+ ".fs-button-tone-ghost{background:transparent;color:var(--fs-color-text);border-color:var(--fs-color-border);}",
157
+ ".fs-button-tone-muted{background:var(--fs-color-surface);color:var(--fs-color-muted);border-color:var(--fs-color-border);}",
158
+ ".fs-button-size-xs{padding:.45rem .72rem;font-size:.78rem;}",
159
+ ".fs-button-size-sm{padding:.58rem .9rem;font-size:.88rem;}",
160
+ ".fs-button-size-md{padding:.78rem 1.15rem;font-size:.98rem;}",
161
+ ".fs-button-size-lg{padding:.95rem 1.35rem;font-size:1.05rem;}",
162
+ ".fs-button-size-xl{padding:1.08rem 1.55rem;font-size:1.12rem;}",
163
+ ".fs-align-start{align-items:flex-start;}",
164
+ ".fs-align-center{align-items:center;}",
165
+ ".fs-align-end{align-items:flex-end;}",
166
+ ".fs-align-stretch{align-items:stretch;}",
167
+ ".fs-justify-start{justify-content:flex-start;}",
168
+ ".fs-justify-center{justify-content:center;}",
169
+ ".fs-justify-end{justify-content:flex-end;}",
170
+ ".fs-justify-between{justify-content:space-between;}",
171
+ ".fs-justify-around{justify-content:space-around;}",
172
+ ".fs-surface-plain{background:transparent;border-color:transparent;box-shadow:none;}",
173
+ ".fs-surface-subtle{background:var(--fs-color-surface2,var(--fs-color-surface));}",
174
+ ".fs-surface-panel{background:var(--fs-color-surface);}",
175
+ ".fs-surface-card{background:var(--fs-color-surface2,var(--fs-color-surface));}",
176
+ ".fs-surface-elevated{background:var(--fs-color-surface3,var(--fs-color-surface));box-shadow:var(--fs-shadow-glow,var(--fs-shadow-soft));}",
177
+ ".fs-surface-inverted{background:var(--fs-color-text);color:var(--fs-color-bg);}",
178
+ ".fs-tone-default{color:var(--fs-color-text);}",
179
+ ".fs-tone-muted{color:var(--fs-color-muted);}",
180
+ ".fs-tone-primary,.fs-tone-accent{color:var(--fs-color-accent);}",
181
+ ".fs-tone-secondary{color:var(--fs-color-accentSoft,var(--fs-color-accent));}",
182
+ ".fs-tone-success{color:var(--fs-color-green,var(--fs-color-accent));}",
183
+ ".fs-tone-warning{color:var(--fs-color-amber,var(--fs-color-accent));}",
184
+ ".fs-tone-error{color:var(--fs-color-danger,var(--fs-color-accent));}",
185
+ ".fs-tone-inverse{color:var(--fs-color-bg);}",
186
+ ".fs-weight-thin{font-weight:100;}",
187
+ ".fs-weight-light{font-weight:300;}",
188
+ ".fs-weight-normal{font-weight:400;}",
189
+ ".fs-weight-medium{font-weight:500;}",
190
+ ".fs-weight-semibold{font-weight:600;}",
191
+ ".fs-weight-bold{font-weight:700;}",
192
+ ".fs-weight-extrabold{font-weight:800;}",
193
+ ".fs-weight-black{font-weight:900;}",
194
+ ".fs-heading-size-xs{font-size:1rem;}",
195
+ ".fs-heading-size-sm{font-size:1.15rem;}",
196
+ ".fs-heading-size-md{font-size:1.35rem;}",
197
+ ".fs-heading-size-lg{font-size:1.7rem;}",
198
+ ".fs-heading-size-xl{font-size:2.15rem;}",
199
+ ".fs-heading-size-2xl{font-size:2.75rem;}",
200
+ ".fs-heading-size-3xl{font-size:3.4rem;}",
201
+ ".fs-heading-size-4xl{font-size:4.35rem;}",
202
+ ".fs-heading-size-5xl{font-size:5.5rem;}",
203
+ ".fs-text-size-xs{font-size:.75rem;line-height:1.45;}",
204
+ ".fs-text-size-sm{font-size:.9rem;line-height:1.55;}",
205
+ ".fs-text-size-md{font-size:1rem;line-height:1.65;}",
206
+ ".fs-text-size-lg{font-size:1.1rem;line-height:1.7;}",
207
+ ".fs-text-size-xl{font-size:1.25rem;line-height:1.75;}",
208
+ ".fs-text-size-2xl{font-size:1.5rem;line-height:1.45;}",
209
+ ".fs-text-size-3xl{font-size:1.85rem;line-height:1.35;}",
210
+ ".fs-text-size-4xl{font-size:2.3rem;line-height:1.25;}",
211
+ ".fs-text-size-5xl{font-size:3rem;line-height:1.15;}",
212
+ ".fs-enter-fade{animation:fs-fade-in .35s ease both;}",
213
+ ".fs-enter-fade-up{animation:fs-fade-up .45s cubic-bezier(.16,1,.3,1) both;}",
214
+ ".fs-enter-scale-in{animation:fs-scale-in .35s ease both;}",
215
+ ".fs-enter-slide{animation:fs-slide-in .42s cubic-bezier(.16,1,.3,1) both;}",
216
+ ".fs-hover-lift{transition:transform .18s ease,box-shadow .18s ease;}",
217
+ ".fs-hover-lift:hover{transform:translateY(-2px);box-shadow:var(--fs-shadow-glow,var(--fs-shadow-soft));}",
218
+ ".fs-hover-pulse:hover{animation:fs-pulse 1.2s ease infinite;}",
219
+ ".fs-loader{display:inline-flex;width:1.1rem;height:1.1rem;border-radius:999px;border:2px solid var(--fs-color-border);border-top-color:var(--fs-color-accent);animation:fs-spin .8s linear infinite;}",
220
+ ".fs-spacer{flex:1 1 auto;}",
221
+ "@keyframes fs-fade-in{from{opacity:0}to{opacity:1}}",
222
+ "@keyframes fs-fade-up{from{opacity:0;transform:translateY(18px)}to{opacity:1;transform:translateY(0)}}",
223
+ "@keyframes fs-scale-in{from{opacity:0;transform:scale(.96)}to{opacity:1;transform:scale(1)}}",
224
+ "@keyframes fs-slide-in{from{opacity:0;transform:translateX(-14px)}to{opacity:1;transform:translateX(0)}}",
225
+ "@keyframes fs-pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.03)}}",
226
+ "@keyframes fs-spin{to{transform:rotate(360deg)}}",
227
+ ];
228
+
229
+ for (const key of Object.keys(tokens?.space || {})) {
230
+ lines.push(`.fs-pad-${key}{padding:var(--fs-space-${key});}`);
231
+ lines.push(`.fs-gap-${key}{gap:var(--fs-space-${key});}`);
232
+ }
233
+ for (const key of Object.keys(tokens?.radius || {})) {
234
+ lines.push(`.fs-radius-${key}{border-radius:var(--fs-radius-${key});}`);
235
+ }
236
+ for (const key of Object.keys(tokens?.shadow || {})) {
237
+ lines.push(`.fs-shadow-${key}{box-shadow:var(--fs-shadow-${key});}`);
238
+ }
239
+ for (let cols = 1; cols <= 12; cols += 1) {
240
+ lines.push(`.fs-grid-cols-${cols}{--fs-grid-cols:${cols};}`);
241
+ }
242
+ return lines.join("\n");
243
+ }
244
+
245
+ function classNamesIn(source) {
246
+ const out = [];
247
+ const regex = /class\s*=\s*["'`]([^"'`]+)["'`]/g;
248
+ let m = null;
249
+ while ((m = regex.exec(source)) !== null) {
250
+ const value = (m[1] || "").trim();
251
+ if (!value) continue;
252
+ out.push(...value.split(/\s+/g));
253
+ }
254
+ return out;
255
+ }
256
+
257
+ function extractStyleBlocks(source) {
258
+ const blocks = [];
259
+ const text = String(source || "");
260
+ const matcher = /\bstyle\s*\{/g;
261
+ let match = null;
262
+ while ((match = matcher.exec(text)) !== null) {
263
+ const open = text.indexOf("{", match.index);
264
+ if (open < 0) continue;
265
+ let depth = 0;
266
+ let close = -1;
267
+ for (let i = open; i < text.length; i += 1) {
268
+ const ch = text[i];
269
+ if (ch === "{") depth += 1;
270
+ else if (ch === "}") {
271
+ depth -= 1;
272
+ if (depth === 0) {
273
+ close = i;
274
+ break;
275
+ }
276
+ }
277
+ }
278
+ if (close < 0) {
279
+ blocks.push({ content: text.slice(open + 1), broken: true });
280
+ continue;
281
+ }
282
+ blocks.push({ content: text.slice(open + 1, close), broken: false });
283
+ matcher.lastIndex = close + 1;
284
+ }
285
+ return blocks;
286
+ }
287
+
288
+ function validateStyleDeclaration(prop, value, file, errors) {
289
+ const key = String(prop || "").trim();
290
+ const raw = String(value || "").trim();
291
+ if (!ALLOWED_STYLE_PROPERTIES.has(key)) {
292
+ errors.push(`${file}: style block uses unsupported property "${key}"`);
293
+ return;
294
+ }
295
+
296
+ if (SPACING_PROPS.has(key)) {
297
+ if (!ALLOWED_SPACING_VALUES.has(raw)) {
298
+ errors.push(`${file}: style "${key}" must be one of 0..13 (got "${raw}")`);
299
+ }
300
+ return;
301
+ }
302
+
303
+ if (COLOR_PROPS.has(key)) {
304
+ const m = /^([a-z]+)-(\d{2,3})$/.exec(raw);
305
+ if (!m || !ALLOWED_COLOR_NAMES.has(m[1]) || !ALLOWED_COLOR_SHADES.has(m[2])) {
306
+ errors.push(`${file}: style "${key}" must match {color}-{shade} using approved tokens (got "${raw}")`);
307
+ }
308
+ return;
309
+ }
310
+
311
+ if (key === "size" && !ALLOWED_TEXT_SIZES.has(raw)) {
312
+ errors.push(`${file}: style "size" must be one of ${[...ALLOWED_TEXT_SIZES].join(", ")} (got "${raw}")`);
313
+ return;
314
+ }
315
+ if (key === "weight" && !ALLOWED_FONT_WEIGHTS.has(raw)) {
316
+ errors.push(`${file}: style "weight" must be one of ${[...ALLOWED_FONT_WEIGHTS].join(", ")} (got "${raw}")`);
317
+ return;
318
+ }
319
+ if (key === "display" && !ALLOWED_DISPLAYS.has(raw)) {
320
+ errors.push(`${file}: style "display" must be one of ${[...ALLOWED_DISPLAYS].join(", ")} (got "${raw}")`);
321
+ return;
322
+ }
323
+ if (key === "direction" && !ALLOWED_DIRECTIONS.has(raw)) {
324
+ errors.push(`${file}: style "direction" must be one of ${[...ALLOWED_DIRECTIONS].join(", ")} (got "${raw}")`);
325
+ return;
326
+ }
327
+ if (key === "align" && !ALLOWED_ALIGN.has(raw)) {
328
+ errors.push(`${file}: style "align" must be one of ${[...ALLOWED_ALIGN].join(", ")} (got "${raw}")`);
329
+ return;
330
+ }
331
+ if (key === "justify" && !ALLOWED_JUSTIFY.has(raw)) {
332
+ errors.push(`${file}: style "justify" must be one of ${[...ALLOWED_JUSTIFY].join(", ")} (got "${raw}")`);
333
+ }
334
+ }
335
+
336
+ function validateStyleBlockContent(content, file, errors) {
337
+ const text = String(content || "");
338
+ let i = 0;
339
+
340
+ function skipWs() {
341
+ while (i < text.length && /\s/.test(text[i])) i += 1;
342
+ }
343
+
344
+ function parseBlock(expectClose) {
345
+ while (i < text.length) {
346
+ skipWs();
347
+ if (i >= text.length) break;
348
+
349
+ if (text[i] === "}") {
350
+ if (!expectClose) {
351
+ errors.push(`${file}: style block has unexpected "}"`);
352
+ }
353
+ i += 1;
354
+ return;
355
+ }
356
+
357
+ const chunk = text.slice(i);
358
+ const bp = /^@([a-zA-Z][\w-]*)\s*\{/.exec(chunk);
359
+ if (bp) {
360
+ const name = bp[1];
361
+ if (!ALLOWED_BREAKPOINTS.has(name)) {
362
+ errors.push(`${file}: style breakpoint "@${name}" is invalid (allowed: @sm @md @lg @xl)`);
363
+ }
364
+ i += bp[0].length;
365
+ parseBlock(true);
366
+ continue;
367
+ }
368
+
369
+ const decl = /^([a-zA-Z][\w-]*)\s*:\s*([^@;{}\n]+)\s*;?/.exec(chunk);
370
+ if (decl) {
371
+ validateStyleDeclaration(decl[1], decl[2], file, errors);
372
+ i += decl[0].length;
373
+ continue;
374
+ }
375
+
376
+ let lineEnd = text.indexOf("\n", i);
377
+ if (lineEnd < 0) lineEnd = text.length;
378
+ const snippet = text.slice(i, Math.min(i + 60, lineEnd)).trim();
379
+ if (snippet) errors.push(`${file}: invalid style syntax near "${snippet}"`);
380
+ i = lineEnd === i ? i + 1 : lineEnd + 1;
381
+ }
382
+
383
+ if (expectClose) {
384
+ errors.push(`${file}: style breakpoint block is missing closing "}"`);
385
+ }
386
+ }
387
+
388
+ parseBlock(false);
389
+ }
390
+
391
+ export function ensureDesignSystem({ root = process.cwd() } = {}) {
392
+ const tokenPath = resolve(root, TOKENS_PATH);
393
+ const allowlistPath = resolve(root, CLASS_ALLOWLIST_PATH);
394
+ const generatedPath = resolve(root, GENERATED_CSS_PATH);
395
+
396
+ if (!existsSync(tokenPath)) {
397
+ mkdirSync(resolve(tokenPath, ".."), { recursive: true });
398
+ writeFileSync(tokenPath, JSON.stringify(DEFAULT_TOKENS, null, 2), "utf8");
399
+ }
400
+ if (!existsSync(allowlistPath)) {
401
+ mkdirSync(resolve(allowlistPath, ".."), { recursive: true });
402
+ writeFileSync(allowlistPath, JSON.stringify(DEFAULT_CLASS_ALLOWLIST, null, 2), "utf8");
403
+ }
404
+
405
+ const tokens = readJson(tokenPath, DEFAULT_TOKENS);
406
+ const css = `:root {\n${toCssVars(tokens.color, "color")}\n${toCssVars(tokens.space, "space")}\n${toCssVars(tokens.radius, "radius")}\n${toCssVars(tokens.shadow, "shadow")}\n}\n\n${utilityRules(tokens)}\n\n${primitiveRules(tokens)}\n`;
407
+ const current = existsSync(generatedPath) ? readFileSync(generatedPath, "utf8") : null;
408
+ if (current !== css) {
409
+ writeFileSync(generatedPath, css, "utf8");
410
+ }
411
+ return { tokenPath, allowlistPath, generatedPath, tokens };
412
+ }
413
+
414
+ export function validateAppStyles({ root = process.cwd() } = {}) {
415
+ const tokens = readJson(resolve(root, TOKENS_PATH), DEFAULT_TOKENS);
416
+ const allowlist = new Set(readJson(resolve(root, CLASS_ALLOWLIST_PATH), DEFAULT_CLASS_ALLOWLIST));
417
+ const files = [
418
+ ...walk(resolve(root, "app", "pages")).filter((f) => /\.(fs|js|mjs|cjs)$/.test(f)),
419
+ ...walk(resolve(root, "app", "api")).filter((f) => /\.(fs|js|mjs|cjs)$/.test(f)),
420
+ ];
421
+ const stylesheetFiles = [
422
+ resolve(root, "app", "styles.css"),
423
+ ].filter((p) => existsSync(p));
424
+
425
+ const errors = [];
426
+
427
+ for (const file of files) {
428
+ const source = readFileSync(file, "utf8");
429
+ if (/\bstyle\s*=/.test(source)) {
430
+ errors.push(`${file}: inline style attributes are not allowed`);
431
+ }
432
+ const styleBlocks = extractStyleBlocks(source);
433
+ for (const block of styleBlocks) {
434
+ if (block.broken) {
435
+ errors.push(`${file}: style block is missing closing "}"`);
436
+ continue;
437
+ }
438
+ validateStyleBlockContent(block.content, file, errors);
439
+ }
440
+ validatePrimitiveMarkup(source, file, tokens, errors);
441
+ const classes = classNamesIn(source);
442
+ for (const cls of classes) {
443
+ if (cls.startsWith("u-")) continue;
444
+ if (!allowlist.has(cls)) errors.push(`${file}: class "${cls}" is not in app/design/class-allowlist.json`);
445
+ }
446
+ }
447
+
448
+ for (const file of stylesheetFiles) {
449
+ const source = readFileSync(file, "utf8");
450
+ const rawHex = source.match(/#[0-9a-fA-F]{3,8}\b/g) || [];
451
+ if (rawHex.length) {
452
+ errors.push(`${file}: raw hex colors are not allowed (${[...new Set(rawHex)].join(", ")})`);
453
+ }
454
+ }
455
+
456
+ if (errors.length) {
457
+ const error = new Error(`Style validation failed:\n${errors.join("\n")}`);
458
+ error.status = 1;
459
+ throw error;
460
+ }
461
+ }
package/src/tenant.mjs ADDED
@@ -0,0 +1,55 @@
1
+ function prefix(tenantId, value) {
2
+ return `${tenantId}:${value}`;
3
+ }
4
+
5
+ export function resolveTenantId(req, { headerName = "x-tenant-id", fallback = "public" } = {}) {
6
+ const raw = req?.headers?.[headerName];
7
+ const value = Array.isArray(raw) ? raw[0] : raw;
8
+ const tenant = String(value || fallback).trim();
9
+ return tenant || fallback;
10
+ }
11
+
12
+ export function scopeDbByTenant(db, tenantId) {
13
+ if (!db || typeof db.collection !== "function") return db;
14
+ const scope = String(tenantId || "public");
15
+ return {
16
+ ...db,
17
+ collection(name) {
18
+ return db.collection(prefix(scope, name));
19
+ },
20
+ query(collection, predicate) {
21
+ return db.query(prefix(scope, collection), predicate);
22
+ },
23
+ first(collection, predicate) {
24
+ return db.first(prefix(scope, collection), predicate);
25
+ },
26
+ where(collection, filters) {
27
+ return db.where(prefix(scope, collection), filters);
28
+ },
29
+ };
30
+ }
31
+
32
+ export function scopeCacheByTenant(cache, tenantId) {
33
+ if (!cache) return cache;
34
+ const scope = String(tenantId || "public");
35
+ const k = (key) => prefix(scope, key);
36
+ return {
37
+ ...cache,
38
+ async get(key) {
39
+ return cache.get(k(key));
40
+ },
41
+ async set(key, value, ttlMs = 0) {
42
+ return cache.set(k(key), value, ttlMs);
43
+ },
44
+ async setWithTags(key, value, opts = {}) {
45
+ const tags = (opts.tags || []).map((tag) => prefix(scope, tag));
46
+ return cache.setWithTags(k(key), value, { ...opts, tags });
47
+ },
48
+ async del(key) {
49
+ return cache.del(k(key));
50
+ },
51
+ async invalidateTag(tag) {
52
+ return cache.invalidateTag(prefix(scope, tag));
53
+ },
54
+ };
55
+ }
@@ -0,0 +1 @@
1
+ export * from "@fastscript/core-private/typecheck";
package/src/validate.mjs CHANGED
@@ -4,9 +4,13 @@ import { runBench } from "./bench.mjs";
4
4
  import { runCompat } from "./compat.mjs";
5
5
  import { runExport } from "./export.mjs";
6
6
  import { runDbMigrate, runDbSeed } from "./db-cli.mjs";
7
+ import { runTypeCheck } from "./typecheck.mjs";
8
+ import { runLint } from "./fs-linter.mjs";
7
9
 
8
10
  export async function runValidate() {
9
11
  await runCheck();
12
+ await runLint(["--mode", "fail"]);
13
+ await runTypeCheck(["--mode", "fail"]);
10
14
  await runBuild();
11
15
  await runBench();
12
16
  await runCompat();
@@ -14,5 +18,5 @@ export async function runValidate() {
14
18
  await runDbSeed();
15
19
  await runExport(["--to", "js", "--out", "exported-js-app"]);
16
20
  await runExport(["--to", "ts", "--out", "exported-ts-app"]);
17
- console.log("validate complete: check/build/bench/compat/db/export all passed");
21
+ console.log("validate complete: check/lint/typecheck/build/bench/compat/db/export all passed");
18
22
  }
@@ -1,12 +1,22 @@
1
- export async function readBody(req) {
1
+ export async function readBody(req, { maxBytes = 1024 * 1024 } = {}) {
2
2
  const chunks = [];
3
- for await (const chunk of req) chunks.push(Buffer.from(chunk));
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
+ }
4
14
  const text = Buffer.concat(chunks).toString("utf8");
5
15
  return text;
6
16
  }
7
17
 
8
- export async function readJsonBody(req) {
9
- const raw = await readBody(req);
18
+ export async function readJsonBody(req, { maxBytes = 1024 * 1024 } = {}) {
19
+ const raw = await readBody(req, { maxBytes });
10
20
  if (!raw.trim()) return {};
11
21
  try {
12
22
  return JSON.parse(raw);
@@ -76,4 +86,3 @@ export function validateShape(schema, input, scope = "input") {
76
86
  }
77
87
  return { ok: true, value: out };
78
88
  }
79
-