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.
- package/CHANGELOG.md +32 -7
- package/LICENSE +33 -21
- package/README.md +567 -73
- package/node_modules/@fastscript/core-private/BOUNDARY.json +15 -0
- package/node_modules/@fastscript/core-private/README.md +5 -0
- package/node_modules/@fastscript/core-private/package.json +34 -0
- package/node_modules/@fastscript/core-private/src/asset-optimizer.mjs +67 -0
- package/node_modules/@fastscript/core-private/src/audit-log.mjs +50 -0
- package/node_modules/@fastscript/core-private/src/auth-flows.mjs +29 -0
- package/node_modules/@fastscript/core-private/src/auth.mjs +115 -0
- package/node_modules/@fastscript/core-private/src/bench.mjs +45 -0
- package/node_modules/@fastscript/core-private/src/build.mjs +670 -0
- package/node_modules/@fastscript/core-private/src/cache.mjs +248 -0
- package/node_modules/@fastscript/core-private/src/check.mjs +22 -0
- package/node_modules/@fastscript/core-private/src/cli.mjs +95 -0
- package/node_modules/@fastscript/core-private/src/compat.mjs +128 -0
- package/node_modules/@fastscript/core-private/src/create.mjs +278 -0
- package/node_modules/@fastscript/core-private/src/csp.mjs +26 -0
- package/node_modules/@fastscript/core-private/src/db-cli.mjs +185 -0
- package/node_modules/@fastscript/core-private/src/db-postgres-collection.mjs +110 -0
- package/node_modules/@fastscript/core-private/src/db-postgres.mjs +40 -0
- package/node_modules/@fastscript/core-private/src/db.mjs +103 -0
- package/node_modules/@fastscript/core-private/src/deploy.mjs +662 -0
- package/node_modules/@fastscript/core-private/src/dev.mjs +5 -0
- package/node_modules/@fastscript/core-private/src/docs-search.mjs +35 -0
- package/node_modules/@fastscript/core-private/src/env.mjs +118 -0
- package/node_modules/@fastscript/core-private/src/export.mjs +83 -0
- package/node_modules/@fastscript/core-private/src/fs-diagnostics.mjs +70 -0
- package/node_modules/@fastscript/core-private/src/fs-error-codes.mjs +141 -0
- package/node_modules/@fastscript/core-private/src/fs-formatter.mjs +66 -0
- package/node_modules/@fastscript/core-private/src/fs-linter.mjs +274 -0
- package/node_modules/@fastscript/core-private/src/fs-normalize.mjs +91 -0
- package/node_modules/@fastscript/core-private/src/fs-parser.mjs +980 -0
- package/node_modules/@fastscript/core-private/src/generated/docs-search-index.mjs +3182 -0
- package/node_modules/@fastscript/core-private/src/i18n.mjs +25 -0
- package/node_modules/@fastscript/core-private/src/interop.mjs +16 -0
- package/node_modules/@fastscript/core-private/src/jobs.mjs +378 -0
- package/node_modules/@fastscript/core-private/src/logger.mjs +27 -0
- package/node_modules/@fastscript/core-private/src/metrics.mjs +45 -0
- package/node_modules/@fastscript/core-private/src/middleware.mjs +14 -0
- package/node_modules/@fastscript/core-private/src/migrate.mjs +81 -0
- package/node_modules/@fastscript/core-private/src/migration-wizard.mjs +16 -0
- package/node_modules/@fastscript/core-private/src/module-loader.mjs +46 -0
- package/node_modules/@fastscript/core-private/src/oauth-providers.mjs +103 -0
- package/node_modules/@fastscript/core-private/src/observability.mjs +21 -0
- package/node_modules/@fastscript/core-private/src/plugins.mjs +194 -0
- package/node_modules/@fastscript/core-private/src/retention.mjs +57 -0
- package/node_modules/@fastscript/core-private/src/routes.mjs +178 -0
- package/node_modules/@fastscript/core-private/src/scheduler.mjs +104 -0
- package/node_modules/@fastscript/core-private/src/security.mjs +233 -0
- package/node_modules/@fastscript/core-private/src/server-runtime.mjs +849 -0
- package/node_modules/@fastscript/core-private/src/serverless-handler.mjs +20 -0
- package/node_modules/@fastscript/core-private/src/session-policy.mjs +38 -0
- package/node_modules/@fastscript/core-private/src/start.mjs +10 -0
- package/node_modules/@fastscript/core-private/src/storage.mjs +155 -0
- package/node_modules/@fastscript/core-private/src/style-primitives.mjs +538 -0
- package/node_modules/@fastscript/core-private/src/style-system.mjs +461 -0
- package/node_modules/@fastscript/core-private/src/tenant.mjs +55 -0
- package/node_modules/@fastscript/core-private/src/typecheck.mjs +1464 -0
- package/node_modules/@fastscript/core-private/src/validate.mjs +22 -0
- package/node_modules/@fastscript/core-private/src/validation.mjs +88 -0
- package/node_modules/@fastscript/core-private/src/webhook.mjs +81 -0
- package/node_modules/@fastscript/core-private/src/worker.mjs +24 -0
- package/package.json +86 -13
- package/src/asset-optimizer.mjs +67 -0
- package/src/audit-log.mjs +50 -0
- package/src/auth.mjs +1 -115
- package/src/bench.mjs +20 -7
- package/src/build.mjs +1 -234
- package/src/cache.mjs +210 -20
- package/src/cli.mjs +29 -5
- package/src/compat.mjs +8 -10
- package/src/create.mjs +71 -17
- package/src/csp.mjs +26 -0
- package/src/db-cli.mjs +152 -8
- package/src/db-postgres-collection.mjs +110 -0
- package/src/deploy.mjs +1 -65
- package/src/docs-search.mjs +35 -0
- package/src/env.mjs +34 -5
- package/src/fs-diagnostics.mjs +70 -0
- package/src/fs-error-codes.mjs +126 -0
- package/src/fs-formatter.mjs +66 -0
- package/src/fs-linter.mjs +274 -0
- package/src/fs-normalize.mjs +21 -238
- package/src/fs-parser.mjs +1 -0
- package/src/generated/docs-search-index.mjs +3220 -0
- package/src/i18n.mjs +25 -0
- package/src/jobs.mjs +283 -32
- package/src/metrics.mjs +45 -0
- package/src/migration-wizard.mjs +16 -0
- package/src/module-loader.mjs +11 -12
- package/src/oauth-providers.mjs +103 -0
- package/src/plugins.mjs +194 -0
- package/src/retention.mjs +57 -0
- package/src/routes.mjs +178 -0
- package/src/scheduler.mjs +104 -0
- package/src/security.mjs +197 -19
- package/src/server-runtime.mjs +1 -339
- package/src/serverless-handler.mjs +20 -0
- package/src/session-policy.mjs +38 -0
- package/src/storage.mjs +1 -56
- package/src/style-system.mjs +461 -0
- package/src/tenant.mjs +55 -0
- package/src/typecheck.mjs +1 -0
- package/src/validate.mjs +5 -1
- package/src/validation.mjs +14 -5
- package/src/webhook.mjs +1 -71
- package/src/worker.mjs +23 -4
- package/src/language-spec.mjs +0 -58
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { generateCspPolicy } from "./csp.mjs";
|
|
5
|
+
|
|
6
|
+
function nowMs() {
|
|
7
|
+
return Date.now();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function parseBool(value, fallback = false) {
|
|
11
|
+
if (value === undefined || value === null || value === "") return fallback;
|
|
12
|
+
if (value === true || value === "true" || value === "1") return true;
|
|
13
|
+
if (value === false || value === "false" || value === "0") return false;
|
|
14
|
+
return fallback;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readJson(path, fallback) {
|
|
18
|
+
if (!existsSync(path)) return fallback;
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
21
|
+
} catch {
|
|
22
|
+
return fallback;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function writeJson(path, value) {
|
|
27
|
+
writeFileSync(path, JSON.stringify(value, null, 2), "utf8");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createMemoryWindowStore() {
|
|
31
|
+
const rows = new Map();
|
|
32
|
+
return {
|
|
33
|
+
async increment(key, windowMs) {
|
|
34
|
+
const now = nowMs();
|
|
35
|
+
const row = rows.get(key) || { count: 0, resetAt: now + windowMs };
|
|
36
|
+
if (now > row.resetAt) {
|
|
37
|
+
row.count = 0;
|
|
38
|
+
row.resetAt = now + windowMs;
|
|
39
|
+
}
|
|
40
|
+
row.count += 1;
|
|
41
|
+
rows.set(key, row);
|
|
42
|
+
return { count: row.count, resetAt: row.resetAt };
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createFileWindowStore({ dir = ".fastscript/security", name = "rate-limit" } = {}) {
|
|
48
|
+
const root = resolve(dir);
|
|
49
|
+
mkdirSync(root, { recursive: true });
|
|
50
|
+
const file = join(root, `${name}.json`);
|
|
51
|
+
const state = readJson(file, { rows: {} });
|
|
52
|
+
|
|
53
|
+
function persist() {
|
|
54
|
+
writeJson(file, state);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
async increment(key, windowMs) {
|
|
59
|
+
const now = nowMs();
|
|
60
|
+
const row = state.rows[key] || { count: 0, resetAt: now + windowMs };
|
|
61
|
+
if (now > row.resetAt) {
|
|
62
|
+
row.count = 0;
|
|
63
|
+
row.resetAt = now + windowMs;
|
|
64
|
+
}
|
|
65
|
+
row.count += 1;
|
|
66
|
+
state.rows[key] = row;
|
|
67
|
+
persist();
|
|
68
|
+
return { count: row.count, resetAt: row.resetAt };
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function createRedisWindowStore({ url = process.env.REDIS_URL, prefix = "fastscript:rate" } = {}) {
|
|
74
|
+
const mod = await import("redis");
|
|
75
|
+
const client = mod.createClient({ url });
|
|
76
|
+
await client.connect();
|
|
77
|
+
return {
|
|
78
|
+
async increment(key, windowMs) {
|
|
79
|
+
const rowKey = `${prefix}:${key}`;
|
|
80
|
+
const count = Number(await client.incr(rowKey));
|
|
81
|
+
if (count === 1) await client.pExpire(rowKey, windowMs);
|
|
82
|
+
const ttl = Number(await client.pTTL(rowKey));
|
|
83
|
+
return { count, resetAt: nowMs() + Math.max(0, ttl) };
|
|
84
|
+
},
|
|
85
|
+
async close() {
|
|
86
|
+
await client.quit();
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function createWindowStore({ driver = process.env.RATE_LIMIT_DRIVER || "memory", ...opts } = {}) {
|
|
92
|
+
const mode = String(driver || "memory").toLowerCase();
|
|
93
|
+
if (mode === "redis") {
|
|
94
|
+
try {
|
|
95
|
+
return await createRedisWindowStore(opts);
|
|
96
|
+
} catch {
|
|
97
|
+
return createMemoryWindowStore();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (mode === "file") return createFileWindowStore(opts);
|
|
101
|
+
return createMemoryWindowStore();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function securityHeaders({
|
|
105
|
+
csp,
|
|
106
|
+
target = process.env.DEPLOY_TARGET || "node",
|
|
107
|
+
hsts = "max-age=31536000; includeSubDomains",
|
|
108
|
+
} = {}) {
|
|
109
|
+
return async function securityHeadersMiddleware(ctx, next) {
|
|
110
|
+
const mode = process.env.NODE_ENV || "development";
|
|
111
|
+
const cspPolicy = csp || generateCspPolicy({ target, mode });
|
|
112
|
+
ctx.res.setHeader("x-content-type-options", "nosniff");
|
|
113
|
+
ctx.res.setHeader("x-frame-options", "DENY");
|
|
114
|
+
ctx.res.setHeader("referrer-policy", "strict-origin-when-cross-origin");
|
|
115
|
+
ctx.res.setHeader("permissions-policy", "geolocation=(), microphone=(), camera=()");
|
|
116
|
+
ctx.res.setHeader("content-security-policy", cspPolicy);
|
|
117
|
+
if (mode === "production") {
|
|
118
|
+
ctx.res.setHeader("strict-transport-security", hsts);
|
|
119
|
+
}
|
|
120
|
+
return next();
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function rateLimit({
|
|
125
|
+
windowMs = Number(process.env.RATE_LIMIT_WINDOW_MS || 60_000),
|
|
126
|
+
max = Number(process.env.RATE_LIMIT_MAX || 120),
|
|
127
|
+
key = (ctx) => ctx.req.socket.remoteAddress || "anon",
|
|
128
|
+
store,
|
|
129
|
+
} = {}) {
|
|
130
|
+
const lazy = { store: store || null, pending: null };
|
|
131
|
+
async function getStore() {
|
|
132
|
+
if (lazy.store) return lazy.store;
|
|
133
|
+
if (!lazy.pending) lazy.pending = createWindowStore({ name: "rate-limit" }).then((s) => { lazy.store = s; return s; });
|
|
134
|
+
return lazy.pending;
|
|
135
|
+
}
|
|
136
|
+
return async function rateLimitMiddleware(ctx, next) {
|
|
137
|
+
const s = await getStore();
|
|
138
|
+
const row = await s.increment(String(key(ctx)), windowMs);
|
|
139
|
+
ctx.res.setHeader("x-ratelimit-limit", String(max));
|
|
140
|
+
ctx.res.setHeader("x-ratelimit-remaining", String(Math.max(0, max - row.count)));
|
|
141
|
+
if (row.count > max) {
|
|
142
|
+
return {
|
|
143
|
+
status: 429,
|
|
144
|
+
json: { ok: false, error: "rate_limited" },
|
|
145
|
+
headers: { "retry-after": String(Math.ceil((row.resetAt - nowMs()) / 1000)) },
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return next();
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function requestQuota({
|
|
153
|
+
windowMs = Number(process.env.QUOTA_WINDOW_MS || 60_000),
|
|
154
|
+
max = Number(process.env.QUOTA_MAX || 600),
|
|
155
|
+
store,
|
|
156
|
+
} = {}) {
|
|
157
|
+
const lazy = { store: store || null, pending: null };
|
|
158
|
+
async function getStore() {
|
|
159
|
+
if (lazy.store) return lazy.store;
|
|
160
|
+
if (!lazy.pending) lazy.pending = createWindowStore({ name: "request-quotas" }).then((s) => { lazy.store = s; return s; });
|
|
161
|
+
return lazy.pending;
|
|
162
|
+
}
|
|
163
|
+
return async function requestQuotaMiddleware(ctx, next) {
|
|
164
|
+
const principal = ctx.user?.id || ctx.req.socket.remoteAddress || "anon";
|
|
165
|
+
const route = (ctx.pathname || "/").split("/").slice(0, 2).join("/") || "/";
|
|
166
|
+
const key = `${principal}:${route}`;
|
|
167
|
+
const s = await getStore();
|
|
168
|
+
const row = await s.increment(key, windowMs);
|
|
169
|
+
if (row.count > max) {
|
|
170
|
+
return {
|
|
171
|
+
status: 429,
|
|
172
|
+
json: { ok: false, error: "quota_exceeded", route, principal },
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return next();
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function abuseGuard({
|
|
180
|
+
scoreWindowMs = Number(process.env.ABUSE_WINDOW_MS || 120_000),
|
|
181
|
+
threshold = Number(process.env.ABUSE_THRESHOLD || 80),
|
|
182
|
+
blockMs = Number(process.env.ABUSE_BLOCK_MS || 300_000),
|
|
183
|
+
} = {}) {
|
|
184
|
+
const score = new Map();
|
|
185
|
+
const blocked = new Map();
|
|
186
|
+
return async function abuseGuardMiddleware(ctx, next) {
|
|
187
|
+
const key = ctx.req.socket.remoteAddress || "anon";
|
|
188
|
+
const now = nowMs();
|
|
189
|
+
const blockUntil = blocked.get(key) || 0;
|
|
190
|
+
if (blockUntil > now) {
|
|
191
|
+
return { status: 429, json: { ok: false, error: "abuse_blocked", retryAt: blockUntil } };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const row = score.get(key) || { value: 0, resetAt: now + scoreWindowMs };
|
|
195
|
+
if (now > row.resetAt) {
|
|
196
|
+
row.value = 0;
|
|
197
|
+
row.resetAt = now + scoreWindowMs;
|
|
198
|
+
}
|
|
199
|
+
const method = (ctx.method || "GET").toUpperCase();
|
|
200
|
+
row.value += ["POST", "PUT", "PATCH", "DELETE"].includes(method) ? 3 : 1;
|
|
201
|
+
if ((ctx.pathname || "").startsWith("/api/")) row.value += 1;
|
|
202
|
+
score.set(key, row);
|
|
203
|
+
|
|
204
|
+
if (row.value >= threshold) {
|
|
205
|
+
blocked.set(key, now + blockMs);
|
|
206
|
+
return { status: 429, json: { ok: false, error: "abuse_detected", blockedForMs: blockMs } };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return next();
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function csrf({ cookieName = "fs_csrf", headerName = "x-csrf-token" } = {}) {
|
|
214
|
+
return async function csrfMiddleware(ctx, next) {
|
|
215
|
+
const method = ctx.method || "GET";
|
|
216
|
+
if (["GET", "HEAD", "OPTIONS"].includes(method)) {
|
|
217
|
+
const token = ctx.cookies[cookieName] || createHash("sha256").update(`${nowMs()}-${Math.random()}`).digest("hex");
|
|
218
|
+
ctx.helpers.setCookie(cookieName, token, { path: "/", sameSite: "Lax", secure: parseBool(process.env.SESSION_COOKIE_SECURE, false) });
|
|
219
|
+
return next();
|
|
220
|
+
}
|
|
221
|
+
const cookie = ctx.cookies[cookieName];
|
|
222
|
+
const hasCookieHeader = Boolean(ctx.req.headers.cookie);
|
|
223
|
+
if (!cookie && !hasCookieHeader) {
|
|
224
|
+
return next();
|
|
225
|
+
}
|
|
226
|
+
const header = ctx.req.headers[headerName];
|
|
227
|
+
const token = Array.isArray(header) ? header[0] : header;
|
|
228
|
+
if (!cookie || !token || cookie !== token) {
|
|
229
|
+
return { status: 403, json: { ok: false, error: "csrf_invalid" } };
|
|
230
|
+
}
|
|
231
|
+
return next();
|
|
232
|
+
};
|
|
233
|
+
}
|