tw-framework 1.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/LICENSE +21 -0
- package/README.md +67 -0
- package/dist/adapters/aws/README.md +37 -0
- package/dist/adapters/aws/deploy-s3.sh +39 -0
- package/dist/adapters/bun/server.ts +30 -0
- package/dist/adapters/caddy/Caddyfile +25 -0
- package/dist/adapters/cloudflare/_headers +12 -0
- package/dist/adapters/cloudflare/_redirects +4 -0
- package/dist/adapters/cloudflare/wrangler.toml +13 -0
- package/dist/adapters/digitalocean/.do/app.yaml +15 -0
- package/dist/adapters/docker/.dockerignore +7 -0
- package/dist/adapters/docker/Dockerfile +35 -0
- package/dist/adapters/docker/Dockerfile.static +27 -0
- package/dist/adapters/docker/docker-compose.yml +8 -0
- package/dist/adapters/firebase/firebase.json +31 -0
- package/dist/adapters/fly/fly.toml +32 -0
- package/dist/adapters/github-pages/.github/workflows/deploy.yml +41 -0
- package/dist/adapters/github-pages/.nojekyll +1 -0
- package/dist/adapters/index.ts +228 -0
- package/dist/adapters/netlify/netlify.toml +23 -0
- package/dist/adapters/nginx/nginx.conf +43 -0
- package/dist/adapters/node/server.mjs +214 -0
- package/dist/adapters/railway/railway.toml +18 -0
- package/dist/adapters/render/render.yaml +27 -0
- package/dist/adapters/vercel/vercel.json +6 -0
- package/dist/node-adapter.ts +187 -0
- package/dist/testing.cjs +1167 -0
- package/dist/tw-runtime-client.mjs +13833 -0
- package/dist/tw.mjs +54515 -0
- package/package.json +50 -0
package/dist/testing.cjs
ADDED
|
@@ -0,0 +1,1167 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __esm = (fn, res) => function __init() {
|
|
8
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
9
|
+
};
|
|
10
|
+
var __export = (target, all) => {
|
|
11
|
+
for (var name in all)
|
|
12
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
13
|
+
};
|
|
14
|
+
var __copyProps = (to, from, except, desc) => {
|
|
15
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
16
|
+
for (let key of __getOwnPropNames(from))
|
|
17
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
18
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
19
|
+
}
|
|
20
|
+
return to;
|
|
21
|
+
};
|
|
22
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
23
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
24
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
25
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
26
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
27
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
28
|
+
mod
|
|
29
|
+
));
|
|
30
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
31
|
+
|
|
32
|
+
// ../../packages/server/tw/routing/revalidate.ts
|
|
33
|
+
function revalidatePath(pathname, opts) {
|
|
34
|
+
return activePipeline ? activePipeline.revalidatePath(pathname, opts) : 0;
|
|
35
|
+
}
|
|
36
|
+
var activePipeline;
|
|
37
|
+
var init_revalidate = __esm({
|
|
38
|
+
"../../packages/server/tw/routing/revalidate.ts"() {
|
|
39
|
+
activePipeline = null;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// ../../packages/server/tw/routing/signal-stream.ts
|
|
44
|
+
function getSignalHub() {
|
|
45
|
+
if (!hub) hub = new SignalHub();
|
|
46
|
+
return hub;
|
|
47
|
+
}
|
|
48
|
+
function setSignal(name, value, opts) {
|
|
49
|
+
let session = opts?.session;
|
|
50
|
+
if (!session && opts?.request) {
|
|
51
|
+
try {
|
|
52
|
+
const cookies = opts.request.cookies;
|
|
53
|
+
if (cookies && typeof cookies.get === "function") {
|
|
54
|
+
session = cookies.get("tw_session") ?? void 0;
|
|
55
|
+
} else {
|
|
56
|
+
const header = opts.request.headers?.get?.("cookie") ?? "";
|
|
57
|
+
const m = /(?:^|;\s*)tw_session=([^;]+)/.exec(header);
|
|
58
|
+
if (m) session = decodeURIComponent(m[1].trim().replace(/^"|"$/g, ""));
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return getSignalHub().setSignal(name, value, session ? { session } : void 0);
|
|
64
|
+
}
|
|
65
|
+
var BATCH_MS, HISTORY_LIMIT, SignalHub, hub;
|
|
66
|
+
var init_signal_stream = __esm({
|
|
67
|
+
"../../packages/server/tw/routing/signal-stream.ts"() {
|
|
68
|
+
BATCH_MS = 100;
|
|
69
|
+
HISTORY_LIMIT = 1e3;
|
|
70
|
+
SignalHub = class {
|
|
71
|
+
clients = /* @__PURE__ */ new Map();
|
|
72
|
+
nextClientId = 1;
|
|
73
|
+
seq = 0;
|
|
74
|
+
values = /* @__PURE__ */ new Map();
|
|
75
|
+
kinds = /* @__PURE__ */ new Map();
|
|
76
|
+
history = [];
|
|
77
|
+
pending = /* @__PURE__ */ new Map();
|
|
78
|
+
pendingSession;
|
|
79
|
+
timer = null;
|
|
80
|
+
/**
|
|
81
|
+
* Server-side mutation (route handlers): queue an update, batch-flush.
|
|
82
|
+
* With `session` (derived from the request's tw_session cookie) private
|
|
83
|
+
* signals are delivered only to clients authenticated as that session.
|
|
84
|
+
*/
|
|
85
|
+
setSignal(name, value, opts) {
|
|
86
|
+
const kind = this.kinds.get(name) ?? "public";
|
|
87
|
+
this.values.set(name, value);
|
|
88
|
+
this.kinds.set(name, kind);
|
|
89
|
+
this.pending.set(name, value);
|
|
90
|
+
if (opts?.session) this.pendingSession = opts.session;
|
|
91
|
+
if (this.timer == null) {
|
|
92
|
+
this.timer = setTimeout(() => this.flush(), BATCH_MS);
|
|
93
|
+
}
|
|
94
|
+
return { queued: true, kind };
|
|
95
|
+
}
|
|
96
|
+
/** Latest known values (also used for fresh snapshots). */
|
|
97
|
+
snapshot() {
|
|
98
|
+
return { seq: this.seq, snapshot: Object.fromEntries(this.values) };
|
|
99
|
+
}
|
|
100
|
+
register(declared, session) {
|
|
101
|
+
const id = this.nextClientId++;
|
|
102
|
+
for (const [name, kind] of declared) {
|
|
103
|
+
if (!this.kinds.has(name)) this.kinds.set(name, kind);
|
|
104
|
+
}
|
|
105
|
+
const client = {
|
|
106
|
+
id,
|
|
107
|
+
declared,
|
|
108
|
+
session,
|
|
109
|
+
send: () => {
|
|
110
|
+
},
|
|
111
|
+
close: () => this.clients.delete(id)
|
|
112
|
+
};
|
|
113
|
+
this.clients.set(id, client);
|
|
114
|
+
return client;
|
|
115
|
+
}
|
|
116
|
+
/** Build the replay/snapshot preamble for a connecting client. */
|
|
117
|
+
connectPayloads(since, declared, session) {
|
|
118
|
+
const out = [];
|
|
119
|
+
const hasCoverage = since <= this.seq && this.history.length > 0 && since >= this.history[0].seq - 1;
|
|
120
|
+
if (hasCoverage) {
|
|
121
|
+
for (const entry of this.history) {
|
|
122
|
+
if (entry.seq > since && this.sessionMatches(session, entry) && this.deliversTo(declared, entry.names)) {
|
|
123
|
+
out.push(entry.payload);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} else if (this.values.size > 0) {
|
|
127
|
+
const snap = {};
|
|
128
|
+
for (const [name, value] of this.values) {
|
|
129
|
+
if (this.sessionMatches(session, { names: [name], session: this.valueSessions.get(name) }) && this.deliversTo(declared, [name])) snap[name] = value;
|
|
130
|
+
}
|
|
131
|
+
if (Object.keys(snap).length > 0) {
|
|
132
|
+
out.push(this.frame({ v: 1, seq: this.seq, snapshot: snap }));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
/** latest-known session per signal (for snapshot filtering) */
|
|
138
|
+
valueSessions = /* @__PURE__ */ new Map();
|
|
139
|
+
sessionMatches(clientSession, entry) {
|
|
140
|
+
if (!entry.session) return true;
|
|
141
|
+
return entry.session === clientSession;
|
|
142
|
+
}
|
|
143
|
+
deliversTo(declared, names) {
|
|
144
|
+
return names.some((n) => {
|
|
145
|
+
const kind = this.kinds.get(n) ?? "public";
|
|
146
|
+
return kind === "public" || declared.has(n);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
flush() {
|
|
150
|
+
this.timer = null;
|
|
151
|
+
if (this.pending.size === 0) return;
|
|
152
|
+
const updates = [...this.pending.entries()];
|
|
153
|
+
const session = this.pendingSession;
|
|
154
|
+
this.pending.clear();
|
|
155
|
+
this.pendingSession = void 0;
|
|
156
|
+
this.seq++;
|
|
157
|
+
const names = updates.map(([n]) => n);
|
|
158
|
+
for (const [n] of updates) this.valueSessions.set(n, session);
|
|
159
|
+
const payload = this.frame({ v: 1, seq: this.seq, updates });
|
|
160
|
+
this.history.push({ seq: this.seq, payload, names, session });
|
|
161
|
+
if (this.history.length > HISTORY_LIMIT) this.history.shift();
|
|
162
|
+
for (const client of this.clients.values()) {
|
|
163
|
+
if (this.sessionMatches(client.session, { names, session }) && this.deliversTo(client.declared, names)) client.send(payload);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
frame(obj) {
|
|
167
|
+
return JSON.stringify(obj).replace(/</g, "\\u003c");
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
hub = null;
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// ../../packages/server/tw/routing/twm-rules.ts
|
|
175
|
+
var twm_rules_exports = {};
|
|
176
|
+
__export(twm_rules_exports, {
|
|
177
|
+
corsHeadersFor: () => corsHeadersFor,
|
|
178
|
+
evaluateRules: () => evaluateRules,
|
|
179
|
+
isRuleDsl: () => isRuleDsl,
|
|
180
|
+
parseRules: () => parseRules
|
|
181
|
+
});
|
|
182
|
+
function isRuleDsl(source) {
|
|
183
|
+
const stripped = source.split("\n").map((l) => l.replace(/\/\/.*$/, "").trim()).filter((l) => l.length > 0).join("\n");
|
|
184
|
+
return stripped.startsWith('rule "') || stripped.startsWith("rule '");
|
|
185
|
+
}
|
|
186
|
+
function tokenize(src, file) {
|
|
187
|
+
const toks = [];
|
|
188
|
+
const s = src;
|
|
189
|
+
let i = 0;
|
|
190
|
+
while (i < s.length) {
|
|
191
|
+
const c = s[i];
|
|
192
|
+
if (/\s/.test(c)) {
|
|
193
|
+
i++;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (c === "/" && s[i + 1] === "/") {
|
|
197
|
+
while (i < s.length && s[i] !== "\n") i++;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (c === "/" && s[i + 1] === "*") {
|
|
201
|
+
i += 2;
|
|
202
|
+
while (i < s.length && !(s[i] === "*" && s[i + 1] === "/")) i++;
|
|
203
|
+
i += 2;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (c === '"' || c === "'") {
|
|
207
|
+
let j = i + 1;
|
|
208
|
+
let out = "";
|
|
209
|
+
while (j < s.length && s[j] !== c) {
|
|
210
|
+
if (s[j] === "\\") {
|
|
211
|
+
out += s[j + 1] ?? "";
|
|
212
|
+
j += 2;
|
|
213
|
+
} else {
|
|
214
|
+
out += s[j];
|
|
215
|
+
j++;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (j >= s.length) throw new Error(`${file}: unterminated string at offset ${i}`);
|
|
219
|
+
toks.push({ t: "str", v: out });
|
|
220
|
+
i = j + 1;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (/[0-9]/.test(c)) {
|
|
224
|
+
let j = i;
|
|
225
|
+
while (j < s.length && /[0-9.]/.test(s[j])) j++;
|
|
226
|
+
toks.push({ t: "num", v: s.slice(i, j) });
|
|
227
|
+
i = j;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (/[A-Za-z_]/.test(c)) {
|
|
231
|
+
let j = i;
|
|
232
|
+
while (j < s.length && /[A-Za-z0-9_]/.test(s[j])) j++;
|
|
233
|
+
toks.push({ t: "ident", v: s.slice(i, j) });
|
|
234
|
+
i = j;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if ("{}[],".includes(c)) {
|
|
238
|
+
toks.push({ t: "punc", v: c });
|
|
239
|
+
i++;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
throw new Error(`${file}: unexpected character '${c}' at offset ${i}`);
|
|
243
|
+
}
|
|
244
|
+
return toks;
|
|
245
|
+
}
|
|
246
|
+
function parseRules(source, file = "middleware.twm") {
|
|
247
|
+
return new Parser(tokenize(source, file), file).parseRules();
|
|
248
|
+
}
|
|
249
|
+
function sweepRateBuckets(now) {
|
|
250
|
+
if (rateBuckets.size <= RATE_BUCKETS_MAX) {
|
|
251
|
+
for (const [k, b] of rateBuckets) {
|
|
252
|
+
if (b.resetAt <= now) rateBuckets.delete(k);
|
|
253
|
+
}
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const live = [...rateBuckets.entries()].filter(([, b]) => b.resetAt > now);
|
|
257
|
+
live.sort((a, b) => a[1].resetAt - b[1].resetAt);
|
|
258
|
+
rateBuckets.clear();
|
|
259
|
+
for (const [k, b] of live.slice(live.length - RATE_BUCKETS_MAX)) {
|
|
260
|
+
rateBuckets.set(k, b);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function b64urlDecode(s) {
|
|
264
|
+
const pad = "=".repeat((4 - s.length % 4) % 4);
|
|
265
|
+
return Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/") + pad, "base64");
|
|
266
|
+
}
|
|
267
|
+
function verifyJwt_hs256(token, secret) {
|
|
268
|
+
const parts = token.split(".");
|
|
269
|
+
if (parts.length !== 3) return false;
|
|
270
|
+
const [h, p, sig] = parts;
|
|
271
|
+
let header, payload;
|
|
272
|
+
try {
|
|
273
|
+
header = JSON.parse(b64urlDecode(h).toString("utf-8"));
|
|
274
|
+
payload = JSON.parse(b64urlDecode(p).toString("utf-8"));
|
|
275
|
+
} catch {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
if (header?.alg !== "HS256") return false;
|
|
279
|
+
const expected = (0, import_node_crypto.createHmac)("sha256", secret).update(`${h}.${p}`).digest();
|
|
280
|
+
let got;
|
|
281
|
+
try {
|
|
282
|
+
got = b64urlDecode(sig);
|
|
283
|
+
} catch {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
if (expected.length !== got.length || !(0, import_node_crypto.timingSafeEqual)(expected, got)) return false;
|
|
287
|
+
if (typeof payload?.exp === "number" && payload.exp * 1e3 < Date.now()) return false;
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
function matchPath(pattern, pathname) {
|
|
291
|
+
if (pattern === "/**" || pattern === "/*") return true;
|
|
292
|
+
if (pattern.endsWith("/**")) {
|
|
293
|
+
const prefix = pattern.slice(0, -3);
|
|
294
|
+
return pathname === prefix || pathname.startsWith(prefix + "/");
|
|
295
|
+
}
|
|
296
|
+
if (pattern.endsWith("/*")) {
|
|
297
|
+
const prefix = pattern.slice(0, -2);
|
|
298
|
+
return pathname === prefix || pathname.startsWith(prefix + "/");
|
|
299
|
+
}
|
|
300
|
+
return pathname === pattern;
|
|
301
|
+
}
|
|
302
|
+
function ruleBlocked(rule, request, pathname) {
|
|
303
|
+
const headers = request.headers;
|
|
304
|
+
if (rule.userAgent) {
|
|
305
|
+
const ua = (headers.get("user-agent") ?? "").toLowerCase();
|
|
306
|
+
if (!ua) {
|
|
307
|
+
if (rule.userAgent.emptyIsBlocked) return true;
|
|
308
|
+
} else {
|
|
309
|
+
const blocked = (rule.userAgent.block ?? []).some((b) => b.toLowerCase() && ua.includes(b.toLowerCase()));
|
|
310
|
+
const allowed = (rule.userAgent.allow ?? []).some((a) => ua.includes(a.toLowerCase()));
|
|
311
|
+
if (blocked && !allowed) return true;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (rule.path) {
|
|
315
|
+
const p = rule.path;
|
|
316
|
+
if ((p.prefixes ?? []).some((x) => pathname === x || pathname.startsWith(x.endsWith("/") ? x : x + "/"))) return true;
|
|
317
|
+
if ((p.contains ?? []).some((x) => pathname.includes(x))) return true;
|
|
318
|
+
if ((p.extensions ?? []).some((x) => pathname.toLowerCase().endsWith(x.startsWith(".") ? x.toLowerCase() : "." + x.toLowerCase()))) return true;
|
|
319
|
+
if (p.denyTraversal && (pathname.includes("..") || pathname.toLowerCase().includes("%2e%2e"))) return true;
|
|
320
|
+
if (p.denyNullBytes && (pathname.includes("\0") || pathname.toLowerCase().includes("%00"))) return true;
|
|
321
|
+
}
|
|
322
|
+
if (rule.auth) {
|
|
323
|
+
const cookieName = rule.auth.cookie ?? "token";
|
|
324
|
+
const cookieHeader = headers.get("cookie") ?? "";
|
|
325
|
+
const escaped = cookieName.replace(/[.*+?^${}()|[\\\\]/g, "\\$&");
|
|
326
|
+
const m = new RegExp(`(?:^|;\\s*)${escaped}=([^;]+)`).exec(cookieHeader);
|
|
327
|
+
const token = m ? m[1].trim() : "";
|
|
328
|
+
const secret = rule.auth.jwtSecretEnv ? process.env[rule.auth.jwtSecretEnv] ?? "" : "";
|
|
329
|
+
if (!token || !secret || !verifyJwt_hs256(token, secret)) return true;
|
|
330
|
+
}
|
|
331
|
+
if (rule.rateLimit) {
|
|
332
|
+
const rl = rule.rateLimit;
|
|
333
|
+
const max = rl.requests ?? 60;
|
|
334
|
+
const windowMs = (rl.window ?? 60) * 1e3;
|
|
335
|
+
const id = rl.identity === "ip" ? (headers.get("x-forwarded-for") ?? "").split(",")[0].trim() || "local" : pathname;
|
|
336
|
+
const key = `${rule.name}:${id}`;
|
|
337
|
+
const now = Date.now();
|
|
338
|
+
const bucket = rateBuckets.get(key);
|
|
339
|
+
if (!bucket || bucket.resetAt <= now) {
|
|
340
|
+
rateBuckets.set(key, { count: 1, resetAt: now + windowMs });
|
|
341
|
+
} else if (++bucket.count > max) {
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (rule.origin) {
|
|
346
|
+
const o = rule.origin;
|
|
347
|
+
let origin = headers.get("origin") ?? "";
|
|
348
|
+
if (!origin && o.allowReferer) origin = headers.get("referer") ?? "";
|
|
349
|
+
if (!origin) {
|
|
350
|
+
if (o.require) return true;
|
|
351
|
+
} else {
|
|
352
|
+
const originRoot = origin.replace(/\/$/, "");
|
|
353
|
+
if (!(o.allow ?? []).some((a) => originRoot === a.replace(/\/$/, ""))) return true;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (rule.methods && !rule.methods.includes((request.method ?? "GET").toUpperCase())) {
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
function corsHeadersFor(rules, request) {
|
|
362
|
+
const origin = request.headers.get("origin");
|
|
363
|
+
if (!origin) return {};
|
|
364
|
+
let pathname = "/";
|
|
365
|
+
try {
|
|
366
|
+
pathname = new URL(request.url).pathname;
|
|
367
|
+
} catch {
|
|
368
|
+
}
|
|
369
|
+
for (const rule of rules) {
|
|
370
|
+
if (!rule.origin) continue;
|
|
371
|
+
if (!matchPath(rule.match, pathname)) continue;
|
|
372
|
+
const o = rule.origin;
|
|
373
|
+
let effective = origin;
|
|
374
|
+
if (!effective && o.allowReferer) effective = request.headers.get("referer") ?? "";
|
|
375
|
+
const allowed = (o.allow ?? []).some((a) => effective.replace(/\/$/, "") === a.replace(/\/$/, ""));
|
|
376
|
+
if (!allowed) continue;
|
|
377
|
+
const headers = {
|
|
378
|
+
"Access-Control-Allow-Origin": origin,
|
|
379
|
+
"Vary": "Origin"
|
|
380
|
+
};
|
|
381
|
+
const isPreflight = (request.method ?? "").toUpperCase() === "OPTIONS" && !!request.headers.get("access-control-request-method");
|
|
382
|
+
if (isPreflight) {
|
|
383
|
+
const method = request.headers.get("access-control-request-method").toUpperCase();
|
|
384
|
+
headers["Access-Control-Allow-Methods"] = rule.methods?.length ? rule.methods.join(", ") : [method, "GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"].join(", ");
|
|
385
|
+
const reqHeaders = request.headers.get("access-control-request-headers");
|
|
386
|
+
if (reqHeaders) headers["Access-Control-Allow-Headers"] = reqHeaders;
|
|
387
|
+
headers["Access-Control-Max-Age"] = "600";
|
|
388
|
+
}
|
|
389
|
+
return headers;
|
|
390
|
+
}
|
|
391
|
+
return {};
|
|
392
|
+
}
|
|
393
|
+
function evaluateRules(rules, request) {
|
|
394
|
+
let pathname = "/";
|
|
395
|
+
try {
|
|
396
|
+
pathname = new URL(request.url).pathname;
|
|
397
|
+
} catch {
|
|
398
|
+
}
|
|
399
|
+
sweepRateBuckets(Date.now());
|
|
400
|
+
for (const rule of rules) {
|
|
401
|
+
if (!matchPath(rule.match, pathname)) continue;
|
|
402
|
+
if (!ruleBlocked(rule, request, pathname)) continue;
|
|
403
|
+
const r = rule.response;
|
|
404
|
+
const headers = { "X-TW-Middleware": rule.name };
|
|
405
|
+
if (!Number.isFinite(r.status) || r.status < 100 || r.status > 599) {
|
|
406
|
+
console.error(`[twm-rules] rule "${rule.name}" has invalid status ${r.status}; using 500`);
|
|
407
|
+
}
|
|
408
|
+
let body = null;
|
|
409
|
+
if (r.json !== void 0) {
|
|
410
|
+
headers["content-type"] = "application/json";
|
|
411
|
+
body = JSON.stringify(r.json);
|
|
412
|
+
} else if (r.html !== void 0) {
|
|
413
|
+
headers["content-type"] = "text/html; charset=utf-8";
|
|
414
|
+
body = r.html;
|
|
415
|
+
} else if (r.text !== void 0) {
|
|
416
|
+
headers["content-type"] = "text/plain; charset=utf-8";
|
|
417
|
+
body = r.text;
|
|
418
|
+
}
|
|
419
|
+
return new Response(body, {
|
|
420
|
+
status: Number.isFinite(r.status) && r.status >= 100 && r.status <= 599 ? r.status : 500,
|
|
421
|
+
headers
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
var import_node_crypto, Parser, rateBuckets, RATE_BUCKETS_MAX;
|
|
427
|
+
var init_twm_rules = __esm({
|
|
428
|
+
"../../packages/server/tw/routing/twm-rules.ts"() {
|
|
429
|
+
import_node_crypto = require("node:crypto");
|
|
430
|
+
Parser = class {
|
|
431
|
+
constructor(toks, file) {
|
|
432
|
+
this.toks = toks;
|
|
433
|
+
this.file = file;
|
|
434
|
+
}
|
|
435
|
+
pos = 0;
|
|
436
|
+
peek() {
|
|
437
|
+
return this.toks[this.pos];
|
|
438
|
+
}
|
|
439
|
+
next() {
|
|
440
|
+
return this.toks[this.pos++];
|
|
441
|
+
}
|
|
442
|
+
expectPunc(v) {
|
|
443
|
+
const t = this.next();
|
|
444
|
+
if (!t || t.t !== "punc" || t.v !== v) {
|
|
445
|
+
throw new Error(`${this.file}: expected '${v}' but got ${t ? JSON.stringify(t.v) : "end of file"}`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
expectIdent(v) {
|
|
449
|
+
const t = this.next();
|
|
450
|
+
if (!t || t.t !== "ident" || v && t.v !== v) {
|
|
451
|
+
throw new Error(`${this.file}: expected ${v ? `'${v}'` : "an identifier"} but got ${t ? JSON.stringify(t.v) : "end of file"}`);
|
|
452
|
+
}
|
|
453
|
+
return t.v;
|
|
454
|
+
}
|
|
455
|
+
expectString() {
|
|
456
|
+
const t = this.next();
|
|
457
|
+
if (!t || t.t !== "str") {
|
|
458
|
+
throw new Error(`${this.file}: expected a string but got ${t ? JSON.stringify(t.v) : "end of file"}`);
|
|
459
|
+
}
|
|
460
|
+
return t.v;
|
|
461
|
+
}
|
|
462
|
+
strArray() {
|
|
463
|
+
this.expectPunc("[");
|
|
464
|
+
const out = [];
|
|
465
|
+
while (this.peek() && this.peek().v !== "]") {
|
|
466
|
+
out.push(this.expectString());
|
|
467
|
+
if (this.peek()?.v === ",") this.next();
|
|
468
|
+
}
|
|
469
|
+
this.expectPunc("]");
|
|
470
|
+
return out;
|
|
471
|
+
}
|
|
472
|
+
value() {
|
|
473
|
+
const t = this.peek();
|
|
474
|
+
if (!t) throw new Error(`${this.file}: expected a value`);
|
|
475
|
+
if (t.v === "[") return this.strArray();
|
|
476
|
+
this.pos++;
|
|
477
|
+
if (t.t === "str") return t.v;
|
|
478
|
+
if (t.t === "num") return Number(t.v);
|
|
479
|
+
if (t.t === "ident" && t.v === "true") return true;
|
|
480
|
+
if (t.t === "ident" && t.v === "false") return false;
|
|
481
|
+
throw new Error(`${this.file}: unexpected value ${JSON.stringify(t.v)}`);
|
|
482
|
+
}
|
|
483
|
+
/** Parse `key value` pairs (and one inline `json { ... }` object) until `}`. */
|
|
484
|
+
fields(allowJsonBlock = false) {
|
|
485
|
+
this.expectPunc("{");
|
|
486
|
+
const out = {};
|
|
487
|
+
while (this.peek() && this.peek().v !== "}") {
|
|
488
|
+
if (this.peek().v === ",") {
|
|
489
|
+
this.pos++;
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
const key = this.expectIdent();
|
|
493
|
+
if (allowJsonBlock && key === "json" && this.peek()?.v === "{") {
|
|
494
|
+
out.json = this.jsonObject();
|
|
495
|
+
} else {
|
|
496
|
+
out[key] = this.value();
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
this.expectPunc("}");
|
|
500
|
+
return out;
|
|
501
|
+
}
|
|
502
|
+
/** `json { error "msg" }` — string/number/bool values only. */
|
|
503
|
+
jsonObject() {
|
|
504
|
+
this.expectPunc("{");
|
|
505
|
+
const out = {};
|
|
506
|
+
while (this.peek() && this.peek().v !== "}") {
|
|
507
|
+
if (this.peek().v === ",") {
|
|
508
|
+
this.pos++;
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
const key = this.expectIdent();
|
|
512
|
+
out[key] = this.value();
|
|
513
|
+
}
|
|
514
|
+
this.expectPunc("}");
|
|
515
|
+
return out;
|
|
516
|
+
}
|
|
517
|
+
/** Array-or-undefined with a clear error for wrong types. */
|
|
518
|
+
strArrayField(rule, block, key, v) {
|
|
519
|
+
if (v === void 0) return void 0;
|
|
520
|
+
if (!Array.isArray(v) || v.some((x) => typeof x !== "string")) {
|
|
521
|
+
throw new Error(`${this.file}: '${key}' in '${block}' of rule "${rule}" must be a string array like ["a", "b"]`);
|
|
522
|
+
}
|
|
523
|
+
return v;
|
|
524
|
+
}
|
|
525
|
+
numField(rule, block, key, v) {
|
|
526
|
+
if (v === void 0) return void 0;
|
|
527
|
+
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
528
|
+
throw new Error(`${this.file}: '${key}' in '${block}' of rule "${rule}" must be a number`);
|
|
529
|
+
}
|
|
530
|
+
return v;
|
|
531
|
+
}
|
|
532
|
+
parseRules() {
|
|
533
|
+
const rules = [];
|
|
534
|
+
while (this.peek()) {
|
|
535
|
+
this.expectIdent("rule");
|
|
536
|
+
const name = this.expectString();
|
|
537
|
+
this.expectPunc("{");
|
|
538
|
+
const rule = { name, match: "/**" };
|
|
539
|
+
while (this.peek() && this.peek().v !== "}") {
|
|
540
|
+
const key = this.expectIdent();
|
|
541
|
+
switch (key) {
|
|
542
|
+
case "match":
|
|
543
|
+
rule.match = this.expectString();
|
|
544
|
+
break;
|
|
545
|
+
case "user_agent": {
|
|
546
|
+
const f = this.fields();
|
|
547
|
+
rule.userAgent = {
|
|
548
|
+
allow: this.strArrayField(name, "user_agent", "allow", f.allow),
|
|
549
|
+
block: this.strArrayField(name, "user_agent", "block", f.block),
|
|
550
|
+
emptyIsBlocked: f.empty_is_blocked === true
|
|
551
|
+
};
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
case "path": {
|
|
555
|
+
const f = this.fields();
|
|
556
|
+
rule.path = {
|
|
557
|
+
prefixes: this.strArrayField(name, "path", "prefixes", f.prefixes),
|
|
558
|
+
contains: this.strArrayField(name, "path", "contains", f.contains),
|
|
559
|
+
extensions: this.strArrayField(name, "path", "extensions", f.extensions),
|
|
560
|
+
denyTraversal: f.deny_traversal === true,
|
|
561
|
+
denyNullBytes: f.deny_null_bytes === true
|
|
562
|
+
};
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
case "auth": {
|
|
566
|
+
const f = this.fields();
|
|
567
|
+
rule.auth = { cookie: f.cookie, jwtSecretEnv: f.jwt_secret_env };
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
case "rate_limit": {
|
|
571
|
+
const f = this.fields();
|
|
572
|
+
rule.rateLimit = {
|
|
573
|
+
requests: this.numField(name, "rate_limit", "requests", f.requests),
|
|
574
|
+
window: this.numField(name, "rate_limit", "window", f.window),
|
|
575
|
+
identity: f.identity === "ip" ? "ip" : "path"
|
|
576
|
+
};
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
579
|
+
case "origin": {
|
|
580
|
+
const f = this.fields();
|
|
581
|
+
rule.origin = {
|
|
582
|
+
allow: this.strArrayField(name, "origin", "allow", f.allow),
|
|
583
|
+
require: f.require === true,
|
|
584
|
+
allowReferer: f.allow_referer === true
|
|
585
|
+
};
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
case "methods":
|
|
589
|
+
rule.methods = this.strArray().map((m) => m.toUpperCase());
|
|
590
|
+
break;
|
|
591
|
+
case "response": {
|
|
592
|
+
const f = this.fields(true);
|
|
593
|
+
const status = this.numField(name, "response", "status", f.status);
|
|
594
|
+
const bodyForms = [f.json, f.html, f.text].filter((v) => v !== void 0);
|
|
595
|
+
if (bodyForms.length > 1) {
|
|
596
|
+
throw new Error(`${this.file}: rule "${name}" response has multiple body forms (json/html/text) \u2014 pick one`);
|
|
597
|
+
}
|
|
598
|
+
rule.response = {
|
|
599
|
+
status: status ?? 403,
|
|
600
|
+
json: f.json,
|
|
601
|
+
html: f.html,
|
|
602
|
+
text: f.text
|
|
603
|
+
};
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
default:
|
|
607
|
+
throw new Error(`${this.file}: unknown rule section '${key}' in rule "${name}"`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
this.expectPunc("}");
|
|
611
|
+
if (!rule.response) {
|
|
612
|
+
throw new Error(`${this.file}: rule "${name}" has no response block`);
|
|
613
|
+
}
|
|
614
|
+
rules.push(rule);
|
|
615
|
+
}
|
|
616
|
+
return rules;
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
rateBuckets = /* @__PURE__ */ new Map();
|
|
620
|
+
RATE_BUCKETS_MAX = 1e4;
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
// ../../packages/shared/tw/node-import.ts
|
|
625
|
+
var node_import_exports = {};
|
|
626
|
+
__export(node_import_exports, {
|
|
627
|
+
twImportTs: () => twImportTs
|
|
628
|
+
});
|
|
629
|
+
async function twImportTs(tsPath) {
|
|
630
|
+
if (!globalThis.__TW_NODE) {
|
|
631
|
+
return import(tsPath);
|
|
632
|
+
}
|
|
633
|
+
const esbuildMain = globalThis.__TW_ESBUILD_MODULE;
|
|
634
|
+
const esbuild = await (esbuildMain ? import(esbuildMain) : import("esbuild"));
|
|
635
|
+
const safe = tsPath.replace(/[^a-zA-Z0-9]/g, "_").slice(-120);
|
|
636
|
+
const cachePath = (0, import_node_path.join)((0, import_node_os.tmpdir)(), "tw-" + safe + ".mjs");
|
|
637
|
+
await esbuild.build({
|
|
638
|
+
entryPoints: [tsPath],
|
|
639
|
+
bundle: true,
|
|
640
|
+
format: "esm",
|
|
641
|
+
platform: "node",
|
|
642
|
+
target: "node18",
|
|
643
|
+
outfile: cachePath,
|
|
644
|
+
logLevel: "silent"
|
|
645
|
+
});
|
|
646
|
+
return import((0, import_node_url.pathToFileURL)(cachePath).href);
|
|
647
|
+
}
|
|
648
|
+
var import_node_path, import_node_os, import_node_url;
|
|
649
|
+
var init_node_import = __esm({
|
|
650
|
+
"../../packages/shared/tw/node-import.ts"() {
|
|
651
|
+
import_node_path = require("node:path");
|
|
652
|
+
import_node_os = require("node:os");
|
|
653
|
+
import_node_url = require("node:url");
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
// ../../packages/server/tw/routing/twm-loader.ts
|
|
658
|
+
var twm_loader_exports = {};
|
|
659
|
+
__export(twm_loader_exports, {
|
|
660
|
+
clearRouteCache: () => clearRouteCache,
|
|
661
|
+
clearTWMCache: () => clearTWMCache,
|
|
662
|
+
executeMiddleware: () => executeMiddleware,
|
|
663
|
+
executeRouteHandler: () => executeRouteHandler,
|
|
664
|
+
loadTWMModule: () => loadTWMModule,
|
|
665
|
+
revalidateRoute: () => revalidateRoute,
|
|
666
|
+
shouldMatchMiddleware: () => shouldMatchMiddleware
|
|
667
|
+
});
|
|
668
|
+
function clearTWMCache() {
|
|
669
|
+
moduleCache.clear();
|
|
670
|
+
}
|
|
671
|
+
async function loadLibModule(rootDir, name) {
|
|
672
|
+
let rel = name.replace(/^@\//, "").replace(/^\.\//, "").replace(/^lib\//, "");
|
|
673
|
+
const cacheKey = rootDir + "/" + rel;
|
|
674
|
+
if (libCache.has(cacheKey)) return libCache.get(cacheKey);
|
|
675
|
+
const libPath = (0, import_node_path2.join)(rootDir, "lib", rel + ".ts");
|
|
676
|
+
if (!(0, import_node_fs.existsSync)(libPath)) {
|
|
677
|
+
throw new Error("Module '" + name + "' not found. Create lib/" + name + ".ts");
|
|
678
|
+
}
|
|
679
|
+
try {
|
|
680
|
+
const { twImportTs: twImportTs2 } = await Promise.resolve().then(() => (init_node_import(), node_import_exports));
|
|
681
|
+
const mod = await twImportTs2(libPath);
|
|
682
|
+
libCache.set(cacheKey, mod);
|
|
683
|
+
return mod;
|
|
684
|
+
} catch (e) {
|
|
685
|
+
throw new Error("Failed to load lib/" + name + ".ts: " + e.message);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
function compileTwm(source) {
|
|
689
|
+
const cleaned = source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*/gm, "");
|
|
690
|
+
const importRegex = /import\s+\{([^}]+)\}\s+from\s+["']([^"']+)["'];?/g;
|
|
691
|
+
const codeWithoutImports = cleaned.replace(importRegex, "");
|
|
692
|
+
return codeWithoutImports.replace(/\bexport\s+(?=(async\s+)?(function|const|let|var|class))/g, "").replace(/\bfn\s+delete\b/g, "function deleteFn").replace(/\bfn\s+/g, "function ");
|
|
693
|
+
}
|
|
694
|
+
function extractImports(source) {
|
|
695
|
+
const cleaned = source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*/gm, "");
|
|
696
|
+
const importRegex = /import\s+\{([^}]+)\}\s+from\s+["']([^"']+)["'];?/g;
|
|
697
|
+
const imports = [];
|
|
698
|
+
let match;
|
|
699
|
+
while ((match = importRegex.exec(cleaned)) !== null) {
|
|
700
|
+
const names = match[1].split(",").map(function(s) {
|
|
701
|
+
return s.trim();
|
|
702
|
+
}).filter(Boolean);
|
|
703
|
+
imports.push({ names, module: match[2] });
|
|
704
|
+
}
|
|
705
|
+
return imports;
|
|
706
|
+
}
|
|
707
|
+
function stripImports(source) {
|
|
708
|
+
const importRegex = /import\s+\{([^}]+)\}\s+from\s+["']([^"']+)["'];?/g;
|
|
709
|
+
return source.replace(importRegex, "");
|
|
710
|
+
}
|
|
711
|
+
async function loadTWMModule(filePath, rootDir) {
|
|
712
|
+
const cacheKey = filePath;
|
|
713
|
+
const cached = moduleCache.get(cacheKey);
|
|
714
|
+
if (cached) return cached;
|
|
715
|
+
if (!(0, import_node_fs.existsSync)(filePath)) {
|
|
716
|
+
throw new Error("TWM module not found: " + filePath);
|
|
717
|
+
}
|
|
718
|
+
const source = (0, import_node_fs.readFileSync)(filePath, "utf-8");
|
|
719
|
+
const rd = rootDir || process.cwd();
|
|
720
|
+
if (isRuleDsl(source)) {
|
|
721
|
+
try {
|
|
722
|
+
const rules = parseRules(source, filePath);
|
|
723
|
+
const mod = { rules };
|
|
724
|
+
moduleCache.set(cacheKey, mod);
|
|
725
|
+
return mod;
|
|
726
|
+
} catch (err) {
|
|
727
|
+
console.error("[twm-loader] Error parsing rules in " + filePath + ":", err.message);
|
|
728
|
+
const mod = {};
|
|
729
|
+
moduleCache.set(cacheKey, mod);
|
|
730
|
+
return mod;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
const imports = extractImports(source);
|
|
734
|
+
const jsCode = compileTwm(source);
|
|
735
|
+
const codeWithoutImports = stripImports(jsCode);
|
|
736
|
+
const injected = {};
|
|
737
|
+
for (const imp of imports) {
|
|
738
|
+
if (imp.module === "tw" || imp.module === "@tw/server") {
|
|
739
|
+
for (const name of imp.names) {
|
|
740
|
+
if (name === "revalidatePath") injected.revalidatePath = revalidatePath;
|
|
741
|
+
else if (name === "revalidateRoute") injected.revalidateRoute = revalidateRoute;
|
|
742
|
+
else if (name === "setSignal") injected.setSignal = setSignal;
|
|
743
|
+
}
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
try {
|
|
747
|
+
const mod = await loadLibModule(rd, imp.module);
|
|
748
|
+
for (const name of imp.names) {
|
|
749
|
+
if (name in mod) {
|
|
750
|
+
injected[name] = mod[name];
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
} catch (e) {
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
const actionNames = /* @__PURE__ */ new Set();
|
|
757
|
+
for (const m of codeWithoutImports.matchAll(/\bfunction\s+(action[A-Za-z0-9_]*)\s*\(/g)) actionNames.add(m[1]);
|
|
758
|
+
for (const m of codeWithoutImports.matchAll(/\b(?:const|let|var)\s+(action[A-Za-z0-9_]*)\s*=/g)) actionNames.add(m[1]);
|
|
759
|
+
const moduleCode = codeWithoutImports + "\nreturn { get: typeof get !== 'undefined' ? get : null, post: typeof post !== 'undefined' ? post : null, put: typeof put !== 'undefined' ? put : null, patch: typeof patch !== 'undefined' ? patch : null, deleteFn: typeof deleteFn !== 'undefined' ? deleteFn : null, GET: typeof GET !== 'undefined' ? GET : null, POST: typeof POST !== 'undefined' ? POST : null, PUT: typeof PUT !== 'undefined' ? PUT : null, PATCH: typeof PATCH !== 'undefined' ? PATCH : null, DELETE: typeof DELETE !== 'undefined' ? DELETE : null, middleware: typeof middleware !== 'undefined' ? middleware : null, generate: typeof generate !== 'undefined' ? generate : null, config: typeof config !== 'undefined' ? config : null, action: typeof action !== 'undefined' ? action : null, revalidate: typeof revalidate !== 'undefined' ? revalidate : null, " + [...actionNames].map((n) => `${n}: typeof ${n} !== 'undefined' ? ${n} : null`).join(", ") + " };";
|
|
760
|
+
const injectedKeys = Object.keys(injected);
|
|
761
|
+
let module2 = {};
|
|
762
|
+
try {
|
|
763
|
+
const moduleFunc = new Function(...injectedKeys, moduleCode);
|
|
764
|
+
const handlers = moduleFunc(...injectedKeys.map(function(k) {
|
|
765
|
+
return injected[k];
|
|
766
|
+
}));
|
|
767
|
+
const H = handlers;
|
|
768
|
+
module2 = {
|
|
769
|
+
get: H.get || H.GET || void 0,
|
|
770
|
+
post: H.post || H.POST || void 0,
|
|
771
|
+
put: H.put || H.PUT || void 0,
|
|
772
|
+
delete: H.deleteFn || H.DELETE || void 0,
|
|
773
|
+
patch: H.patch || H.PATCH || void 0,
|
|
774
|
+
middleware: H.middleware || void 0,
|
|
775
|
+
generate: H.generate || void 0,
|
|
776
|
+
config: H.config || void 0,
|
|
777
|
+
action: H.action || void 0,
|
|
778
|
+
revalidate: typeof H.revalidate === "number" && H.revalidate > 0 ? H.revalidate : void 0
|
|
779
|
+
};
|
|
780
|
+
for (const n of actionNames) if (H[n]) module2[n] = H[n];
|
|
781
|
+
} catch (err) {
|
|
782
|
+
console.error("[twm-loader] Error parsing " + filePath + ":", err);
|
|
783
|
+
}
|
|
784
|
+
moduleCache.set(cacheKey, module2);
|
|
785
|
+
return module2;
|
|
786
|
+
}
|
|
787
|
+
function parseCookies(headerVal) {
|
|
788
|
+
const out = {};
|
|
789
|
+
if (!headerVal) return out;
|
|
790
|
+
for (const part of headerVal.split(";")) {
|
|
791
|
+
const i = part.indexOf("=");
|
|
792
|
+
if (i < 0) continue;
|
|
793
|
+
const k = part.slice(0, i).trim();
|
|
794
|
+
const v = part.slice(i + 1).trim();
|
|
795
|
+
if (k) {
|
|
796
|
+
try {
|
|
797
|
+
out[k] = decodeURIComponent(v);
|
|
798
|
+
} catch {
|
|
799
|
+
out[k] = v;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
return out;
|
|
804
|
+
}
|
|
805
|
+
function sameOriginAllowed(req) {
|
|
806
|
+
try {
|
|
807
|
+
const host = new URL(req.url).host;
|
|
808
|
+
const origin = req.headers["origin"] || req.headers["Origin"];
|
|
809
|
+
if (origin) return new URL(origin).host === host;
|
|
810
|
+
const referer = req.headers["referer"] || req.headers["Referer"];
|
|
811
|
+
if (referer) return new URL(referer).host === host;
|
|
812
|
+
} catch {
|
|
813
|
+
}
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
function clearRouteCache() {
|
|
817
|
+
routeCache.clear();
|
|
818
|
+
}
|
|
819
|
+
function revalidateRoute(pathname, opts) {
|
|
820
|
+
const want = pathname.startsWith("/") ? pathname : "/" + pathname;
|
|
821
|
+
let dropped = 0;
|
|
822
|
+
for (const [key, entry] of routeCache) {
|
|
823
|
+
const hit = opts?.prefix ? entry.pathname === want || entry.pathname.startsWith(want.endsWith("/") ? want : want + "/") : entry.pathname === want;
|
|
824
|
+
if (hit) {
|
|
825
|
+
routeCache.delete(key);
|
|
826
|
+
dropped++;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return dropped;
|
|
830
|
+
}
|
|
831
|
+
async function executeRouteHandler(filePath, method, request, rootDir) {
|
|
832
|
+
const mod = await loadTWMModule(filePath, rootDir);
|
|
833
|
+
const methodLower = method.toLowerCase();
|
|
834
|
+
let earlyAction;
|
|
835
|
+
if (method.toUpperCase() === "POST") {
|
|
836
|
+
try {
|
|
837
|
+
earlyAction = new URL(request?.url ?? "http://local").searchParams.get("_action") ?? new URL(request?.url ?? "http://local").searchParams.get("action") ?? void 0;
|
|
838
|
+
} catch {
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
const handler = methodLower === "delete" ? mod.delete : mod[methodLower] ?? (methodLower === "head" ? mod.get : void 0) ?? mod[method];
|
|
842
|
+
if (!handler && earlyAction === void 0) {
|
|
843
|
+
return { status: 405, json: { ok: false, error: "Method " + method + " not allowed" } };
|
|
844
|
+
}
|
|
845
|
+
const MAX_BODY_BYTES = 10 * 1024 * 1024;
|
|
846
|
+
const contentLength = Number(request?.headers?.get?.("content-length") ?? "0");
|
|
847
|
+
if (Number.isFinite(contentLength) && contentLength > MAX_BODY_BYTES) {
|
|
848
|
+
return { status: 413, json: { ok: false, error: "Payload too large" } };
|
|
849
|
+
}
|
|
850
|
+
let parsedBody = {};
|
|
851
|
+
if (typeof request?.json === "function") {
|
|
852
|
+
try {
|
|
853
|
+
parsedBody = await request.json();
|
|
854
|
+
} catch {
|
|
855
|
+
parsedBody = {};
|
|
856
|
+
}
|
|
857
|
+
} else if (request?.body && typeof request.body === "object") {
|
|
858
|
+
parsedBody = request.body;
|
|
859
|
+
}
|
|
860
|
+
let query = {};
|
|
861
|
+
try {
|
|
862
|
+
const u = new URL(request.url);
|
|
863
|
+
query = Object.fromEntries(u.searchParams.entries());
|
|
864
|
+
} catch {
|
|
865
|
+
}
|
|
866
|
+
const headersObj = typeof request.headers?.entries === "function" ? Object.fromEntries(request.headers.entries()) : request.headers ?? {};
|
|
867
|
+
const cookies = parseCookies(headersObj["cookie"] ?? headersObj["Cookie"]);
|
|
868
|
+
const req = {
|
|
869
|
+
method: request.method ?? method,
|
|
870
|
+
url: request.url ?? "",
|
|
871
|
+
params: request?.params ?? {},
|
|
872
|
+
headers: headersObj,
|
|
873
|
+
query,
|
|
874
|
+
body: parsedBody,
|
|
875
|
+
cookies: {
|
|
876
|
+
...cookies,
|
|
877
|
+
get: (name) => name in cookies ? cookies[name] : void 0,
|
|
878
|
+
getAll: () => ({ ...cookies }),
|
|
879
|
+
has: (name) => name in cookies
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
if (request?.session !== void 0) req.session = request.session;
|
|
883
|
+
if (request?.ip !== void 0) req.ip = request.ip;
|
|
884
|
+
const actionName = method.toUpperCase() === "POST" ? query._action ?? query.action : void 0;
|
|
885
|
+
if (actionName !== void 0) {
|
|
886
|
+
if (!sameOriginAllowed(req)) {
|
|
887
|
+
return { status: 403, json: { ok: false, error: "Forbidden: server actions require a same-origin request" } };
|
|
888
|
+
}
|
|
889
|
+
const fn = actionName === "" ? mod.action : mod["action" + actionName.charAt(0).toUpperCase() + actionName.slice(1)];
|
|
890
|
+
if (typeof fn !== "function") {
|
|
891
|
+
return { status: 404, json: { ok: false, error: "Unknown action: " + actionName } };
|
|
892
|
+
}
|
|
893
|
+
let actionResult;
|
|
894
|
+
try {
|
|
895
|
+
actionResult = await fn(req);
|
|
896
|
+
} catch (err) {
|
|
897
|
+
console.error("[tw] action error (" + filePath + "):", err?.message ?? err);
|
|
898
|
+
return { status: 500, json: { ok: false, error: "Internal Server Error" } };
|
|
899
|
+
}
|
|
900
|
+
if (!actionResult || typeof actionResult !== "object") {
|
|
901
|
+
return { status: 500, json: { ok: false, error: "Internal Server Error" } };
|
|
902
|
+
}
|
|
903
|
+
return {
|
|
904
|
+
status: typeof actionResult.status === "number" ? actionResult.status : 200,
|
|
905
|
+
json: actionResult.json || actionResult.body || {},
|
|
906
|
+
html: typeof actionResult.html === "string" ? actionResult.html : void 0,
|
|
907
|
+
text: typeof actionResult.text === "string" ? actionResult.text : void 0,
|
|
908
|
+
headers: actionResult.headers
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
const rv = mod.revalidate;
|
|
912
|
+
const isCacheableMethod = method.toUpperCase() === "GET" || method.toUpperCase() === "HEAD";
|
|
913
|
+
let routeCacheKey = null;
|
|
914
|
+
if (rv && isCacheableMethod && !actionName) {
|
|
915
|
+
try {
|
|
916
|
+
const u = new URL(request.url ?? "http://local");
|
|
917
|
+
routeCacheKey = filePath + ":" + u.pathname + u.search;
|
|
918
|
+
} catch {
|
|
919
|
+
routeCacheKey = filePath;
|
|
920
|
+
}
|
|
921
|
+
const hit = routeCache.get(routeCacheKey);
|
|
922
|
+
if (hit && Date.now() < hit.expiresAt) {
|
|
923
|
+
return {
|
|
924
|
+
...hit.result,
|
|
925
|
+
headers: { ...hit.result.headers ?? {}, "x-tw-route-cache": "HIT" }
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
let result;
|
|
930
|
+
try {
|
|
931
|
+
result = await handler(req);
|
|
932
|
+
} catch (err) {
|
|
933
|
+
console.error("[tw] route handler error (" + filePath + "):", err?.message ?? err);
|
|
934
|
+
return { status: 500, json: { ok: false, error: "Internal Server Error" } };
|
|
935
|
+
}
|
|
936
|
+
if (!result || typeof result !== "object") {
|
|
937
|
+
console.error("[tw] route handler returned no result (" + filePath + ")");
|
|
938
|
+
return { status: 500, json: { ok: false, error: "Internal Server Error" } };
|
|
939
|
+
}
|
|
940
|
+
const finalResult = {
|
|
941
|
+
status: typeof result.status === "number" ? result.status : 200,
|
|
942
|
+
json: result.json || result.body || {},
|
|
943
|
+
html: typeof result.html === "string" ? result.html : void 0,
|
|
944
|
+
text: typeof result.text === "string" ? result.text : void 0,
|
|
945
|
+
headers: result.headers
|
|
946
|
+
};
|
|
947
|
+
if (routeCacheKey && finalResult.status === 200) {
|
|
948
|
+
let cachedPath = "";
|
|
949
|
+
try {
|
|
950
|
+
cachedPath = new URL(request.url ?? "http://local").pathname;
|
|
951
|
+
} catch {
|
|
952
|
+
cachedPath = "";
|
|
953
|
+
}
|
|
954
|
+
routeCache.set(routeCacheKey, { result: finalResult, expiresAt: Date.now() + rv * 1e3, pathname: cachedPath });
|
|
955
|
+
}
|
|
956
|
+
return finalResult;
|
|
957
|
+
}
|
|
958
|
+
async function executeMiddleware(filePath, request, rootDir) {
|
|
959
|
+
const mod = await loadTWMModule(filePath, rootDir);
|
|
960
|
+
if (mod.rules) {
|
|
961
|
+
const block = evaluateRules(mod.rules, request);
|
|
962
|
+
if (block) return block;
|
|
963
|
+
const { corsHeadersFor: corsHeadersFor2 } = await Promise.resolve().then(() => (init_twm_rules(), twm_rules_exports));
|
|
964
|
+
const cors = corsHeadersFor2(mod.rules, request);
|
|
965
|
+
const req = request;
|
|
966
|
+
const isPreflight = (req.method ?? req.method ?? "").toUpperCase() === "OPTIONS" && !!req.headers?.get?.("access-control-request-method");
|
|
967
|
+
if (isPreflight) {
|
|
968
|
+
return new Response(null, { status: 204, headers: cors });
|
|
969
|
+
}
|
|
970
|
+
if (Object.keys(cors).length > 0) return { headers: cors };
|
|
971
|
+
return null;
|
|
972
|
+
}
|
|
973
|
+
if (!mod.middleware) {
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
return mod.middleware(request);
|
|
977
|
+
}
|
|
978
|
+
function shouldMatchMiddleware(matchers, pathname) {
|
|
979
|
+
if (!matchers || matchers.length === 0) return true;
|
|
980
|
+
for (const pattern of matchers) {
|
|
981
|
+
if (pattern === "/*" || pattern === "/**") return true;
|
|
982
|
+
if (pattern === pathname) return true;
|
|
983
|
+
if (pattern.endsWith("/*")) {
|
|
984
|
+
const prefix = pattern.slice(0, -2);
|
|
985
|
+
if (pathname === prefix || pathname.startsWith(prefix + "/")) return true;
|
|
986
|
+
}
|
|
987
|
+
if (pattern.endsWith("/**")) {
|
|
988
|
+
const prefix = pattern.slice(0, -3);
|
|
989
|
+
if (pathname === prefix || pathname.startsWith(prefix + "/")) return true;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
994
|
+
var import_node_fs, import_node_path2, moduleCache, libCache, routeCache;
|
|
995
|
+
var init_twm_loader = __esm({
|
|
996
|
+
"../../packages/server/tw/routing/twm-loader.ts"() {
|
|
997
|
+
import_node_fs = require("node:fs");
|
|
998
|
+
init_revalidate();
|
|
999
|
+
init_signal_stream();
|
|
1000
|
+
import_node_path2 = require("node:path");
|
|
1001
|
+
init_twm_rules();
|
|
1002
|
+
moduleCache = /* @__PURE__ */ new Map();
|
|
1003
|
+
libCache = /* @__PURE__ */ new Map();
|
|
1004
|
+
routeCache = /* @__PURE__ */ new Map();
|
|
1005
|
+
}
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
// ../../packages/server/tw/testing/index.ts
|
|
1009
|
+
var testing_exports = {};
|
|
1010
|
+
__export(testing_exports, {
|
|
1011
|
+
testPage: () => testPage,
|
|
1012
|
+
testRoute: () => testRoute
|
|
1013
|
+
});
|
|
1014
|
+
module.exports = __toCommonJS(testing_exports);
|
|
1015
|
+
var import_node_path3 = require("node:path");
|
|
1016
|
+
var import_node_fs2 = require("node:fs");
|
|
1017
|
+
var import_meta = {};
|
|
1018
|
+
function resolveRouteFile(rootDir, pathname) {
|
|
1019
|
+
const homeDir = (0, import_node_path3.join)(rootDir, "home");
|
|
1020
|
+
if (!(0, import_node_fs2.existsSync)(homeDir)) return null;
|
|
1021
|
+
const segments = pathname.split("/").filter(Boolean).map(decodeURIComponent);
|
|
1022
|
+
const isDir = (p) => {
|
|
1023
|
+
try {
|
|
1024
|
+
return (0, import_node_fs2.existsSync)(p) && (0, import_node_fs2.statSync)(p).isDirectory();
|
|
1025
|
+
} catch {
|
|
1026
|
+
return false;
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
function walk(dir, i, params) {
|
|
1030
|
+
if (i >= segments.length) {
|
|
1031
|
+
const f = (0, import_node_path3.join)(dir, "route.twm");
|
|
1032
|
+
return (0, import_node_fs2.existsSync)(f) ? { file: f, params } : null;
|
|
1033
|
+
}
|
|
1034
|
+
const seg = segments[i];
|
|
1035
|
+
const exact = (0, import_node_path3.join)(dir, seg);
|
|
1036
|
+
if (isDir(exact)) {
|
|
1037
|
+
const r = walk(exact, i + 1, params);
|
|
1038
|
+
if (r) return r;
|
|
1039
|
+
}
|
|
1040
|
+
for (const entry of (0, import_node_fs2.readdirSync)(dir)) {
|
|
1041
|
+
if (/^\(.*\)$/.test(entry) && isDir((0, import_node_path3.join)(dir, entry))) {
|
|
1042
|
+
const r = walk((0, import_node_path3.join)(dir, entry), i, params);
|
|
1043
|
+
if (r) return r;
|
|
1044
|
+
}
|
|
1045
|
+
const m = /^\[([^\].]+)\]$/.exec(entry);
|
|
1046
|
+
if (m && isDir((0, import_node_path3.join)(dir, entry))) {
|
|
1047
|
+
const r = walk((0, import_node_path3.join)(dir, entry), i + 1, { ...params, [m[1]]: seg });
|
|
1048
|
+
if (r) return r;
|
|
1049
|
+
}
|
|
1050
|
+
const c = /^\[\.\.\.(.+)\]$/.exec(entry) || /^\[\[\.\.\.(.+)\]\]$/.exec(entry);
|
|
1051
|
+
if (c && isDir((0, import_node_path3.join)(dir, entry))) {
|
|
1052
|
+
const f = (0, import_node_path3.join)(dir, entry, "route.twm");
|
|
1053
|
+
if ((0, import_node_fs2.existsSync)(f)) return { file: f, params: { ...params, [c[1]]: segments.slice(i).join("/") } };
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return null;
|
|
1057
|
+
}
|
|
1058
|
+
return walk(homeDir, 0, {});
|
|
1059
|
+
}
|
|
1060
|
+
async function testRoute(rootDir, method, pathname, init = {}) {
|
|
1061
|
+
const { executeRouteHandler: executeRouteHandler2 } = await Promise.resolve().then(() => (init_twm_loader(), twm_loader_exports));
|
|
1062
|
+
const resolved = resolveRouteFile(rootDir, pathname);
|
|
1063
|
+
if (!resolved) {
|
|
1064
|
+
return { status: 404, json: { ok: false, error: "route not found: " + pathname } };
|
|
1065
|
+
}
|
|
1066
|
+
const queryStr = new URLSearchParams(init.query ?? {}).toString();
|
|
1067
|
+
const url = "http://test.local" + pathname + (queryStr ? "?" + queryStr : "");
|
|
1068
|
+
const headers = new Headers(init.headers ?? {});
|
|
1069
|
+
let body = init.body;
|
|
1070
|
+
if (body !== void 0 && typeof body === "object") {
|
|
1071
|
+
body = JSON.stringify(body);
|
|
1072
|
+
if (!headers.has("content-type")) headers.set("content-type", "application/json");
|
|
1073
|
+
}
|
|
1074
|
+
const request = new Request(url, { method: method.toUpperCase(), headers, body });
|
|
1075
|
+
request.params = resolved.params;
|
|
1076
|
+
return executeRouteHandler2(resolved.file, method.toUpperCase(), request, rootDir);
|
|
1077
|
+
}
|
|
1078
|
+
async function testPage(rootDir, pathname) {
|
|
1079
|
+
const homeDir = (0, import_node_path3.join)(rootDir, "home");
|
|
1080
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
1081
|
+
const isDir = (p) => {
|
|
1082
|
+
try {
|
|
1083
|
+
return (0, import_node_fs2.existsSync)(p) && (0, import_node_fs2.statSync)(p).isDirectory();
|
|
1084
|
+
} catch {
|
|
1085
|
+
return false;
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
function walk(dir, i, params) {
|
|
1089
|
+
if (i >= segments.length) {
|
|
1090
|
+
const f = (0, import_node_path3.join)(dir, "page.tw");
|
|
1091
|
+
return (0, import_node_fs2.existsSync)(f) ? { file: f, params } : null;
|
|
1092
|
+
}
|
|
1093
|
+
const seg = segments[i];
|
|
1094
|
+
const exact = (0, import_node_path3.join)(dir, seg);
|
|
1095
|
+
if (isDir(exact)) {
|
|
1096
|
+
const r = walk(exact, i + 1, params);
|
|
1097
|
+
if (r) return r;
|
|
1098
|
+
}
|
|
1099
|
+
for (const entry of (0, import_node_fs2.readdirSync)(dir)) {
|
|
1100
|
+
if (/^\(.*\)$/.test(entry) && isDir((0, import_node_path3.join)(dir, entry))) {
|
|
1101
|
+
const r = walk((0, import_node_path3.join)(dir, entry), i, params);
|
|
1102
|
+
if (r) return r;
|
|
1103
|
+
}
|
|
1104
|
+
const m = /^\[([^\].]+)\]$/.exec(entry);
|
|
1105
|
+
if (m && isDir((0, import_node_path3.join)(dir, entry))) {
|
|
1106
|
+
const r = walk((0, import_node_path3.join)(dir, entry), i + 1, { ...params, [m[1]]: seg });
|
|
1107
|
+
if (r) return r;
|
|
1108
|
+
}
|
|
1109
|
+
const c = /^\[\.\.\.(.+)\]$/.exec(entry);
|
|
1110
|
+
if (c && isDir((0, import_node_path3.join)(dir, entry))) {
|
|
1111
|
+
const f = (0, import_node_path3.join)(dir, entry, "page.tw");
|
|
1112
|
+
if ((0, import_node_fs2.existsSync)(f)) return { file: f, params: { ...params, [c[1]]: segments.slice(i).join("/") } };
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
return null;
|
|
1116
|
+
}
|
|
1117
|
+
const resolved = walk(homeDir, 0, {});
|
|
1118
|
+
if (!resolved) return { status: 404, html: "" };
|
|
1119
|
+
const { compileSync, generateWithLayoutChain, registerComponentTemplate, clearComponentRegistry } = await findCompiler();
|
|
1120
|
+
clearComponentRegistry();
|
|
1121
|
+
const compDir = (0, import_node_path3.join)(rootDir, "components");
|
|
1122
|
+
if ((0, import_node_fs2.existsSync)(compDir)) {
|
|
1123
|
+
for (const f of (0, import_node_fs2.readdirSync)(compDir).filter((f2) => f2.endsWith(".tw"))) {
|
|
1124
|
+
const fp = (0, import_node_path3.join)(compDir, f);
|
|
1125
|
+
const r = compileSync((0, import_node_fs2.readFileSync)(fp, "utf-8"), { filePath: fp, transforms: false, diagnostics: false });
|
|
1126
|
+
if (r.ast) registerComponentTemplate(f.replace(/\.tw$/, ""), r.ast);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
const result = compileSync((0, import_node_fs2.readFileSync)(resolved.file, "utf-8"), { filePath: resolved.file, stateVars: resolved.params });
|
|
1130
|
+
let html = result.html;
|
|
1131
|
+
const pageProgram = result.ast;
|
|
1132
|
+
const layouts = [];
|
|
1133
|
+
let d = (0, import_node_path3.dirname)(resolved.file);
|
|
1134
|
+
while (true) {
|
|
1135
|
+
const lp = (0, import_node_path3.join)(d, "layout.tw");
|
|
1136
|
+
if ((0, import_node_fs2.existsSync)(lp) && d.startsWith(homeDir)) layouts.unshift(lp);
|
|
1137
|
+
if (d === homeDir || !d.startsWith(homeDir)) break;
|
|
1138
|
+
const parent = (0, import_node_path3.dirname)(d);
|
|
1139
|
+
if (parent === d) break;
|
|
1140
|
+
d = parent;
|
|
1141
|
+
}
|
|
1142
|
+
if (layouts.length > 0) {
|
|
1143
|
+
const progs = layouts.map((lp) => compileSync((0, import_node_fs2.readFileSync)(lp, "utf-8"), { filePath: lp }).ast).filter(Boolean);
|
|
1144
|
+
if (progs.length > 0 && generateWithLayoutChain) {
|
|
1145
|
+
html = generateWithLayoutChain(progs, pageProgram, resolved.params);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
const css = result.css;
|
|
1149
|
+
if (css) html = html.replace("</head>", "<style>" + css + "</style>\n</head>");
|
|
1150
|
+
return { status: 200, html };
|
|
1151
|
+
}
|
|
1152
|
+
async function findCompiler() {
|
|
1153
|
+
const candidates = [
|
|
1154
|
+
(0, import_node_path3.join)(rootDirGlobal, "node_modules/tw-framework/packages/compiler/tw/index.ts"),
|
|
1155
|
+
(0, import_node_path3.join)(rootDirGlobal, "packages/compiler/tw/index.ts"),
|
|
1156
|
+
process.env.TW_COMPILER_PATH ?? ""
|
|
1157
|
+
].filter(Boolean);
|
|
1158
|
+
for (const c of candidates) if ((0, import_node_fs2.existsSync)(c)) return import(c);
|
|
1159
|
+
const selfDir = import_meta.dir ?? (typeof __dirname !== "undefined" ? __dirname : "");
|
|
1160
|
+
return import((0, import_node_path3.join)(selfDir, "../../../compiler/tw/index.ts"));
|
|
1161
|
+
}
|
|
1162
|
+
var rootDirGlobal = process.cwd();
|
|
1163
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1164
|
+
0 && (module.exports = {
|
|
1165
|
+
testPage,
|
|
1166
|
+
testRoute
|
|
1167
|
+
});
|