@server/next 0.48.2 → 0.49.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 (3) hide show
  1. package/index.d.ts +260 -257
  2. package/index.js +2038 -1833
  3. package/package.json +7 -5
package/index.js CHANGED
@@ -1,7 +1,10 @@
1
- // src/ServerError.ts
1
+ // src/errors/index.ts
2
+ var registry = {};
3
+ var definition = (code) => registry[code];
2
4
  var ServerError = class _ServerError extends Error {
3
5
  code;
4
6
  status;
7
+ hint;
5
8
  constructor(code, status2, message, vars = {}) {
6
9
  let messageStr;
7
10
  if (typeof message === "function") {
@@ -21,42 +24,109 @@ var ServerError = class _ServerError extends Error {
21
24
  this.code = code;
22
25
  this.message = messageStr;
23
26
  this.status = status2;
27
+ this.hint = registry[code]?.hint;
24
28
  }
25
29
  static extend(errors) {
26
30
  for (const code in errors) {
27
- const error = errors[code];
28
- if (typeof error === "string") {
29
- _ServerError[code] = (vars = {}) => new _ServerError(code, 500, error, vars);
30
- } else {
31
- _ServerError[code] = (vars = {}) => new _ServerError(code, error.status, error.message, vars);
32
- }
31
+ const raw = errors[code];
32
+ const def = typeof raw === "string" ? { status: 500, message: raw } : raw;
33
+ registry[code] = def;
34
+ _ServerError[code] = (vars = {}) => new _ServerError(code, def.status, def.message, vars);
33
35
  }
34
36
  return errors;
35
37
  }
36
38
  };
37
- var TypedServerError = ServerError;
38
- var ServerError_default = TypedServerError;
39
-
40
- // src/errors/index.ts
41
- ServerError_default.extend({
39
+ ServerError.extend({
40
+ NOT_FOUND: {
41
+ status: 404,
42
+ message: "Not Found",
43
+ hint: "No route matched. Register a catch-all last to answer with your own page: `.get(() => <MissingPage />)`, since routes are tried in the order they were added and the first match wins."
44
+ },
45
+ METHOD_NOT_ALLOWED: {
46
+ status: 405,
47
+ message: 'The HTTP method "{method}" is not supported',
48
+ hint: "Only GET, POST, PUT, PATCH, DELETE, HEAD and OPTIONS are routed. A client sending anything else is usually a proxy or a scanner."
49
+ },
42
50
  PATH_TRAVERSAL: {
43
51
  status: 400,
44
- message: "The route param '{param}' tries to climb the path ('{value}'). If this route legitimately receives paths, set security: { traversalProtection: false }"
52
+ message: "The route param '{param}' tries to climb the path ('{value}')",
53
+ hint: "A route param pointed outside where it belongs. If this route legitimately receives paths, set `security: { traversalProtection: false }`."
45
54
  },
46
- AUTH_INVALID_TOKEN: { status: 401, message: "Invalid Authorization token" },
47
- AUTH_NO_CODE: {
55
+ INVALID_REQUEST: {
56
+ status: 422,
57
+ message: "Invalid request {source}",
58
+ hint: "The route's schema rejected the request. The failing fields are on `error.issues`, which a custom `onError` can shape into an API response."
59
+ },
60
+ VALIDATION_FAILED: {
61
+ status: 500,
62
+ message: "Server Error",
63
+ hint: "The handler returned something its own `response` schema rejects, so this is a bug in the route rather than in the request."
64
+ },
65
+ BODY_TOO_LARGE: {
66
+ status: 413,
67
+ message: "Request body exceeds the {limit} limit",
68
+ hint: "Raise it with `security: { maxBodySize: '10mb' }`, or `maxBodySize: false` to disable the cap. It only bounds what is held in memory; uploaded files stream to `uploads` and have their own limits."
69
+ },
70
+ BODY_INVALID_MULTIPART: {
71
+ status: 400,
72
+ message: "A multipart/form-data body needs a boundary",
73
+ hint: "The client set `Content-Type: multipart/form-data` by hand. Let it be set automatically (send a FormData and omit the header) so the boundary is included."
74
+ },
75
+ UPLOAD_NOT_CONFIGURED: {
76
+ status: 500,
77
+ message: 'A file ("{name}") was uploaded but `uploads` is not configured',
78
+ hint: "Set `uploads: './uploads'` (or a Bucket) to store files, or `uploads: false` to ignore file fields on purpose."
79
+ },
80
+ UPLOAD_TOO_LARGE: {
81
+ status: 413,
82
+ message: 'File "{name}" is too large ({size} bytes, limit is {limit})',
83
+ hint: "Raise it with `uploads: { bucket, maxFileSize: '50mb' }`. `maxTotalSize` bounds one request's files together, and both default to 10mb and 100mb."
84
+ },
85
+ UPLOAD_TOO_MANY_FILES: {
86
+ status: 413,
87
+ message: "Too many files in one request (the limit is {limit})",
88
+ hint: "Raise it with `uploads: { bucket, maxFiles: 500 }`. It defaults to 100, which bounds how many objects one request can create."
89
+ },
90
+ UPLOAD_TOO_SMALL: {
48
91
  status: 400,
49
- message: "Missing the OAuth 'code' in the request body"
92
+ message: 'File "{name}" is too small ({size} bytes, minimum is {limit})',
93
+ hint: "Set or lower `uploads: { bucket, minSize: '1kb' }`."
94
+ },
95
+ UPLOAD_TYPE_NOT_ALLOWED: {
96
+ status: 415,
97
+ message: 'File type not allowed for "{name}" (got "{type}", allowed: {allowed})',
98
+ hint: "`fileType` accepts extensions ('.jpg') and MIME types ('image/jpeg'). It is checked against the file's real format when the bytes identify one, so a mislabelled file is refused even if its name matches."
99
+ },
100
+ AUTH_INVALID_TOKEN: {
101
+ status: 401,
102
+ message: "Invalid Authorization token",
103
+ hint: "The bearer token did not verify: check the issuer and audience, and that the token has not expired."
50
104
  },
51
105
  AUTH_INVALID_HEADER: {
52
106
  status: 401,
53
- message: "Invalid authorization header {type}, must send 'Bearer {TOKEN}' (with space)"
107
+ message: "Invalid authorization header {type}, must send 'Bearer {TOKEN}' (with space)",
108
+ hint: "The Authorization header must read `Bearer <token>`, with a space."
54
109
  },
55
- AUTH_INVALID_STATE: { status: 403, message: "Invalid OAuth state" }
110
+ AUTH_INVALID_STATE: {
111
+ status: 403,
112
+ message: "Invalid OAuth state",
113
+ hint: "The OAuth state cookie was missing or did not match. It is signed with `secrets`, lives for 10 minutes, and needs the callback to be on the same origin as the login."
114
+ },
115
+ AUTH_ISSUER_UNREACHABLE: {
116
+ status: 502,
117
+ message: "Cannot reach the OIDC issuer at {url}",
118
+ hint: "The issuer's discovery document could not be fetched. Check the `issuer` URL (it must serve /.well-known/openid-configuration) and that this server has network access to it."
119
+ },
120
+ AUTH_NO_CODE: {
121
+ status: 400,
122
+ message: "Missing the OAuth 'code' in the callback URL",
123
+ hint: "The provider redirected back without a `code`. Check the callback URL registered with the provider matches /auth/callback/<name>."
124
+ }
56
125
  });
57
- var errors_default = ServerError_default;
126
+ var TypedServerError = ServerError;
127
+ var errors_default = TypedServerError;
58
128
 
59
- // src/polyfill.ts
129
+ // src/boot/polyfill.ts
60
130
  globalThis.env = {};
61
131
  if (typeof globalThis.Netlify !== "undefined") {
62
132
  Object.assign(
@@ -68,17 +138,9 @@ if (typeof process !== "undefined") {
68
138
  Object.assign(globalThis.env, process.env);
69
139
  }
70
140
 
71
- // src/helpers/StatusError.ts
72
- var StatusError = class extends Error {
73
- status;
74
- constructor(msg, status2 = 500) {
75
- super(msg);
76
- this.status = status2;
77
- }
78
- };
79
-
80
- // src/helpers/bucket.ts
141
+ // src/body/bucket.ts
81
142
  import FileSystem from "bucket/fs";
143
+ var isBucketFile = (value) => Boolean(value) && typeof value.stream === "function" && typeof value.bytes === "function" && typeof value.exists === "function" && typeof value.name === "string";
82
144
  function bucket(root) {
83
145
  if (!root) return null;
84
146
  if (typeof root === "string") return FileSystem(root);
@@ -88,135 +150,111 @@ function bucket(root) {
88
150
  );
89
151
  }
90
152
 
91
- // src/helpers/createId.ts
92
- var alphabet = "useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict";
93
- var random = (bytes) => crypto.getRandomValues(new Uint8Array(bytes));
94
- var cyrb53 = (str, seed = 0) => {
95
- if (typeof str !== "string") str = String(str);
96
- let h1 = 3735928559 ^ seed;
97
- let h2 = 1103547991 ^ seed;
98
- for (let i = 0, ch; i < str.length; i++) {
99
- ch = str.charCodeAt(i);
100
- h1 = Math.imul(h1 ^ ch, 2654435761);
101
- h2 = Math.imul(h2 ^ ch, 1597334677);
102
- }
103
- h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507);
104
- h1 ^= Math.imul(h2 ^ h2 >>> 13, 3266489909);
105
- h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507);
106
- h2 ^= Math.imul(h1 ^ h1 >>> 13, 3266489909);
107
- return 4294967296 * (2097151 & h2) + (h1 >>> 0);
108
- };
109
- var hash = (str, size) => {
110
- let chars = "";
111
- let num = cyrb53(str);
112
- for (let i = 0; i < size; i++) {
113
- if (num < alphabet.length) num = cyrb53(str, i);
114
- chars += alphabet[num % alphabet.length];
115
- num = Math.floor(num / alphabet.length);
116
- }
117
- return chars;
118
- };
119
- var randomId = (size = 16) => {
120
- let id = "";
121
- const bytes = random(size);
122
- while (size--) {
123
- id += alphabet[bytes[size] & 61];
153
+ // src/util/duration.ts
154
+ var times = /(-?(?:\d+\.?\d*|\d*\.?\d+)(?:e[-+]?\d+)?)\s*([\p{L}]*)/iu;
155
+ parse.millisecond = parse.ms = 1e-3;
156
+ parse.second = parse.sec = parse.s = parse[""] = 1;
157
+ parse.minute = parse.min = parse.m = parse.s * 60;
158
+ parse.hour = parse.hr = parse.h = parse.m * 60;
159
+ parse.day = parse.d = parse.h * 24;
160
+ parse.week = parse.wk = parse.w = parse.d * 7;
161
+ parse.year = parse.yr = parse.y = parse.d * 365.25;
162
+ parse.month = parse.b = parse.y / 12;
163
+ function parse(str) {
164
+ if (str === null || str === void 0) return null;
165
+ if (typeof str === "number") return str;
166
+ if (typeof str !== "string") {
167
+ throw new Error(`Not a string: ${str} (${typeof str})`);
124
168
  }
125
- return id;
126
- };
127
- function createId(source, size = 16) {
128
- if (source) return hash(source, size);
129
- return randomId(size);
169
+ str = str.toLowerCase().replace(/[,_]/g, "");
170
+ const [_, value, units] = times.exec(str) || [];
171
+ if (!units) return null;
172
+ const unitValue = parse[units] || parse[units.replace(/s$/, "")];
173
+ if (!unitValue) return null;
174
+ const result = unitValue * parseFloat(value);
175
+ return Math.abs(Math.round(result * 1e3));
130
176
  }
131
177
 
132
- // src/helpers/upload.ts
133
- function resolveUploads(up) {
134
- if (!up) return null;
135
- if (typeof up === "object" && "bucket" in up) {
136
- const { bucket: bucket2, maxSize, minSize, fileType: fileType2 } = up;
137
- if (maxSize != null) parseBytes(maxSize);
138
- if (minSize != null) parseBytes(minSize);
139
- return { bucket: bucket(bucket2), maxSize, minSize, fileType: fileType2 };
178
+ // src/http/etag.ts
179
+ function etag(bytes) {
180
+ let h = 2166136261;
181
+ for (let i = 0; i < bytes.length; i++) {
182
+ h ^= bytes[i];
183
+ h = Math.imul(h, 16777619);
140
184
  }
141
- return { bucket: bucket(up) };
142
- }
143
- function parseBytes(value) {
144
- if (typeof value === "number") return value;
145
- const units = {
146
- b: 1,
147
- kb: 1024,
148
- mb: 1024 ** 2,
149
- gb: 1024 ** 3
150
- };
151
- const match = value.toLowerCase().match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)$/);
152
- if (!match) throw new Error(`Invalid size: "${value}"`);
153
- return parseFloat(match[1]) * (units[match[2]] ?? 1);
185
+ return `"${bytes.length.toString(16)}-${(h >>> 0).toString(16)}"`;
154
186
  }
155
- function getExt(filename) {
156
- const i = filename.lastIndexOf(".");
157
- if (i <= 0) return ".bin";
158
- return filename.slice(i).toLowerCase();
187
+
188
+ // src/http/setIfAbsent.ts
189
+ function setIfAbsent(headers2, key, value) {
190
+ if (value && !headers2.has(key)) headers2.set(key, value);
159
191
  }
160
- async function saveFileToBucket(originalName, data, bucket2, contentType) {
161
- const ext = getExt(originalName);
162
- const id = `${createId()}${ext}`;
163
- const file2 = bucket2.file(id);
164
- await file2.write(data, { type: contentType });
165
- return {
166
- name: originalName,
167
- path: file2.path,
168
- type: contentType,
169
- size: data.length
170
- };
192
+
193
+ // src/http/cache.ts
194
+ function resolveCache(value) {
195
+ if (value === false || value === 0) return "no-store";
196
+ if (typeof value === "number") return `public, max-age=${Math.round(value)}`;
197
+ if (typeof value !== "string") return null;
198
+ const ms = parse(value);
199
+ return ms === null ? null : `public, max-age=${Math.round(ms / 1e3)}`;
171
200
  }
172
- function validateFile(originalName, data, contentType, limits) {
173
- const { maxSize, minSize, fileType: fileType2 } = limits;
174
- if (maxSize !== void 0 && data.length > parseBytes(maxSize)) {
175
- throw new Error(
176
- `File "${originalName}" is too large (${data.length} bytes, limit is ${maxSize})`
177
- );
201
+ async function applyCache(out, ctx) {
202
+ if (ctx.method !== "get" && ctx.method !== "head" || out.status !== 200) {
203
+ return out;
178
204
  }
179
- if (minSize !== void 0 && data.length < parseBytes(minSize)) {
180
- throw new Error(
181
- `File "${originalName}" is too small (${data.length} bytes, minimum is ${minSize})`
182
- );
205
+ setIfAbsent(out.headers, "cache-control", resolveCache(ctx.options.cache));
206
+ if (out.headers.has("etag") || !out.headers.has("content-length")) return out;
207
+ const bytes = new Uint8Array(await out.arrayBuffer());
208
+ const tag = etag(bytes);
209
+ const headers2 = new Headers(out.headers);
210
+ headers2.set("etag", tag);
211
+ if (ctx.headers["if-none-match"] === tag) {
212
+ headers2.delete("content-length");
213
+ return new Response(null, { status: 304, headers: headers2 });
183
214
  }
184
- if (fileType2 && fileType2.length > 0) {
185
- const ext = getExt(originalName);
186
- const mime = contentType.toLowerCase();
187
- const allowed = fileType2.some(
188
- (t) => t.toLowerCase() === mime || t.toLowerCase() === ext
189
- );
190
- if (!allowed) {
191
- throw new Error(
192
- `File type not allowed for "${originalName}" (got "${contentType}", allowed: ${fileType2.join(", ")})`
193
- );
215
+ return new Response(bytes, { status: 200, headers: headers2 });
216
+ }
217
+
218
+ // src/http/createCookies.ts
219
+ var EXPIRED = (/* @__PURE__ */ new Date(0)).toUTCString();
220
+ function normalizeExpires(expires) {
221
+ if (expires === null || expires === void 0) return void 0;
222
+ if (expires === 0) return EXPIRED;
223
+ if (typeof expires === "string") {
224
+ if (/^[\d._]+\w+$/.test(expires)) {
225
+ return new Date(Date.now() + parse(expires)).toUTCString();
226
+ } else {
227
+ return expires;
194
228
  }
195
229
  }
230
+ if (typeof expires === "number") {
231
+ return new Date(Date.now() + expires).toUTCString();
232
+ }
233
+ if (expires instanceof Date) {
234
+ return expires.toUTCString();
235
+ }
236
+ return void 0;
196
237
  }
197
-
198
- // src/helpers/bodyLimit.ts
199
- var INF = Number.POSITIVE_INFINITY;
200
- var DEFAULT_MAX = "1mb";
201
- var resolveMax = (max) => max === false ? INF : parseBytes(max == null ? DEFAULT_MAX : max);
202
- var UNITS = ["b", "kb", "mb", "gb", "tb"];
203
- function human(bytes) {
204
- if (!Number.isFinite(bytes) || bytes <= 0) return `${bytes}`;
205
- const i = Math.min(
206
- Math.floor(Math.log(bytes) / Math.log(1024)),
207
- UNITS.length - 1
208
- );
209
- const value = bytes / 1024 ** i;
210
- const rounded = i === 0 ? Math.round(value) : Math.round(value * 10) / 10;
211
- return `${rounded}${UNITS[i]}`;
238
+ var clearCookie = (name) => `${name}=; Path=/; Max-Age=0; HttpOnly`;
239
+ var pendingClear = /* @__PURE__ */ new WeakMap();
240
+ var clearOnSend = (ctx, name) => {
241
+ pendingClear.set(ctx, name);
242
+ };
243
+ var toClear = (ctx) => pendingClear.get(ctx);
244
+ function createCookies(key, val) {
245
+ if (val.value === null) val.expires = EXPIRED;
246
+ const { value, path, expires, maxAge, httpOnly, secure, sameSite } = val;
247
+ let str = `${key}=${encodeURIComponent(value ?? "")};Path=${path || "/"}`;
248
+ if (typeof expires !== "undefined") str += `;Expires=${normalizeExpires(expires)}`;
249
+ if (typeof maxAge === "number") str += `;Max-Age=${maxAge}`;
250
+ if (httpOnly) str += ";HttpOnly";
251
+ if (secure) str += ";Secure";
252
+ if (sameSite) str += `;SameSite=${sameSite}`;
253
+ return str;
212
254
  }
213
- var tooLarge = (max) => new StatusError(
214
- `Request body exceeds the ${human(max)} limit. Raise it with security: { maxBody: '10mb' }, or maxBody: false to disable it.`,
215
- 413
216
- );
217
255
 
218
- // src/helpers/mimes.ts
219
- var mimes_default = {
256
+ // src/http/mimes.ts
257
+ var mimes = {
220
258
  aac: "audio/aac",
221
259
  abw: "application/x-abiword",
222
260
  arc: "application/x-freearc",
@@ -236,6 +274,7 @@ var mimes_default = {
236
274
  eot: "application/vnd.ms-fontobject",
237
275
  epub: "application/epub+zip",
238
276
  gz: "application/gzip",
277
+ heic: "image/heic",
239
278
  gif: "image/gif",
240
279
  htm: "text/html; charset=utf-8",
241
280
  html: "text/html; charset=utf-8",
@@ -296,656 +335,562 @@ var mimes_default = {
296
335
  "3g2": "video/3gpp2",
297
336
  "7z": "application/x-7z-compressed"
298
337
  };
338
+ var mimes_default = mimes;
339
+ var mimeOf = (path) => {
340
+ const ext = path.split(".").pop()?.toLowerCase();
341
+ return ext ? mimes[ext] : void 0;
342
+ };
299
343
 
300
- // src/helpers/parseBody.ts
301
- function getBoundary(header) {
302
- if (!header) return null;
303
- if (header.includes("multipart/form-data") && !header.includes("boundary=")) {
304
- console.error("Do not set the `Content-Type` manually for FormData");
344
+ // src/http/disposition.ts
345
+ var encodeExt = (name) => encodeURIComponent(name).replace(
346
+ /['()*]/g,
347
+ (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
348
+ );
349
+ function disposition(name) {
350
+ if (!name) return "attachment";
351
+ const clean2 = name.replace(/[\r\n]/g, "").split(/[\\/]/).pop() || "";
352
+ if (!clean2) return "attachment";
353
+ const ascii2 = clean2.replace(/[^\x20-\x7e]/g, "?");
354
+ const value = `attachment; filename="${ascii2.replace(/["\\]/g, "\\$&")}"`;
355
+ if (clean2 === ascii2) return value;
356
+ return `${value}; filename*=UTF-8''${encodeExt(clean2)}`;
357
+ }
358
+
359
+ // src/http/fileType.ts
360
+ function fileType(file2) {
361
+ return file2.type || mimeOf(file2.path || file2.name || "");
362
+ }
363
+
364
+ // src/util/bytes.ts
365
+ var UNITS = ["b", "kb", "mb", "gb", "tb"];
366
+ function parseBytes(value) {
367
+ if (typeof value === "number") return value;
368
+ const units = {
369
+ b: 1,
370
+ kb: 1024,
371
+ mb: 1024 ** 2,
372
+ gb: 1024 ** 3,
373
+ tb: 1024 ** 4
374
+ };
375
+ const match = value.toLowerCase().match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb|tb)$/);
376
+ if (!match) throw new Error(`Invalid size: "${value}"`);
377
+ return parseFloat(match[1]) * (units[match[2]] ?? 1);
378
+ }
379
+ function formatBytes(bytes) {
380
+ if (!Number.isFinite(bytes) || bytes <= 0) return "0b";
381
+ const i = Math.min(
382
+ Math.floor(Math.log(bytes) / Math.log(1024)),
383
+ UNITS.length - 1
384
+ );
385
+ const value = bytes / 1024 ** i;
386
+ const rounded = i === 0 ? Math.round(value) : Math.round(value * 10) / 10;
387
+ return `${rounded}${UNITS[i]}`;
388
+ }
389
+
390
+ // src/body/bodyLimit.ts
391
+ var INF = Number.POSITIVE_INFINITY;
392
+ var DEFAULT_MAX = "1mb";
393
+ var resolveMax = (max) => max === false ? INF : parseBytes(max == null ? DEFAULT_MAX : max);
394
+ var tooLarge = (max) => errors_default.BODY_TOO_LARGE({ limit: formatBytes(max) });
395
+
396
+ // src/http/security.ts
397
+ function resolveSecurity(security) {
398
+ const off = security === false;
399
+ const o = security && typeof security === "object" ? security : {};
400
+ const val = (v, def) => v === false ? null : v === true || v == null ? def : v;
401
+ const map2 = off ? {} : {
402
+ "x-frame-options": val(o.frameguard, "SAMEORIGIN"),
403
+ "x-content-type-options": o.noSniff === false ? null : "nosniff",
404
+ "referrer-policy": val(
405
+ o.referrerPolicy,
406
+ "strict-origin-when-cross-origin"
407
+ ),
408
+ "x-xss-protection": o.xssProtection === false ? null : "0",
409
+ // Opt-in: default off
410
+ "content-security-policy": val(o.csp, null),
411
+ "cross-origin-opener-policy": val(o.coop, null),
412
+ "cross-origin-resource-policy": val(o.corp, null),
413
+ "permissions-policy": o.permissionsPolicy ?? null
414
+ };
415
+ const headers2 = {};
416
+ for (const key in map2) {
417
+ const value = map2[key];
418
+ if (value) headers2[key] = value;
305
419
  }
306
- const items = header.split(";");
307
- for (const item of items) {
308
- const trimmedItem = item.trim();
309
- if (trimmedItem.startsWith("boundary=")) {
310
- return trimmedItem.split("=")[1].trim();
420
+ return {
421
+ trustProxy: o.trustProxy ?? true,
422
+ traversalProtection: off ? false : o.traversalProtection !== false,
423
+ // Cap on the bytes buffered per request (see bodyLimit). `false` (or
424
+ // turning security off entirely) resolves to Infinity, meaning no limit.
425
+ maxBodySize: off ? INF : resolveMax(o.maxBodySize),
426
+ headers: headers2,
427
+ hsts: off ? null : val(o.hsts, "max-age=15552000; includeSubDomains")
428
+ };
429
+ }
430
+ var CLIMBS = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
431
+ var ABSOLUTE = /^(?:[\\/]|[a-zA-Z]:)/;
432
+ function checkTraversal(params, ctx) {
433
+ if (!ctx.options.security?.traversalProtection) return;
434
+ for (const param in params) {
435
+ const value = params[param];
436
+ if (typeof value !== "string") continue;
437
+ if (CLIMBS.test(value) || ABSOLUTE.test(value)) {
438
+ throw errors_default.PATH_TRAVERSAL({ param, value });
311
439
  }
312
440
  }
313
- return null;
314
- }
315
- function getMatching(string, regex) {
316
- const matches = string.match(regex);
317
- return matches?.[1] ?? "";
318
441
  }
319
- function isProbablyText(buffer) {
320
- for (let i = 0; i < Math.min(buffer.length, 512); i++) {
321
- const byte = buffer[i];
322
- if (byte === 0) return false;
323
- if (byte < 7 || byte > 13 && byte < 32) return false;
442
+ function applySecurity(res, ctx) {
443
+ const security = ctx.options.security;
444
+ if (!security) return;
445
+ for (const key in security.headers) {
446
+ setIfAbsent(res.headers, key, security.headers[key]);
447
+ }
448
+ if (ctx.platform.production) {
449
+ setIfAbsent(res.headers, "strict-transport-security", security.hsts);
324
450
  }
325
- return true;
326
- }
327
- var extByMime = {};
328
- for (const ext in mimes_default) extByMime[mimes_default[ext]] = ext;
329
- function extFromType(type2) {
330
- const base = (type2 || "").split(";")[0].trim().toLowerCase();
331
- const ext = extByMime[base];
332
- if (ext) return `.${ext}`;
333
- const sub = base.split("/")[1];
334
- return sub && /^[a-z0-9]+$/.test(sub) ? `.${sub}` : ".bin";
335
451
  }
336
- var asIterable = (s) => s;
337
- function toStream(input) {
338
- if (input instanceof ReadableStream) return input;
452
+
453
+ // src/util/isReadableStream.ts
454
+ function isReadableStream(obj) {
455
+ return obj !== null && typeof obj === "object" && typeof obj.pipe === "function" && typeof obj.read === "function" && typeof obj.on === "function";
456
+ }
457
+
458
+ // src/util/iteratorToReadable.ts
459
+ var enc = new TextEncoder();
460
+ function iteratorToReadable(iterable) {
461
+ const iterator = iterable[Symbol.asyncIterator]?.() ?? iterable[Symbol.iterator]();
462
+ let cancelled = false;
339
463
  return new ReadableStream({
340
- start(controller) {
341
- controller.enqueue(input);
342
- controller.close();
464
+ async pull(controller) {
465
+ try {
466
+ const { value, done } = await iterator.next();
467
+ if (cancelled) return;
468
+ if (done) {
469
+ controller.close();
470
+ return;
471
+ }
472
+ controller.enqueue(
473
+ value instanceof Uint8Array ? value : enc.encode(typeof value === "string" ? value : String(value))
474
+ );
475
+ } catch (err) {
476
+ controller.error(err);
477
+ }
478
+ },
479
+ async cancel(reason) {
480
+ cancelled = true;
481
+ await iterator.return?.(reason);
343
482
  }
344
483
  });
345
484
  }
346
- async function toBuffer(input, max = INF) {
347
- if (!(input instanceof ReadableStream)) {
348
- if (input.length > max) throw tooLarge(max);
349
- return input;
350
- }
351
- const chunks = [];
352
- let total = 0;
353
- for await (const chunk of asIterable(input)) {
354
- total += chunk.byteLength;
355
- if (total > max) throw tooLarge(max);
356
- chunks.push(Buffer.from(chunk));
485
+
486
+ // src/util/toWeb.ts
487
+ function toWeb(nodeStream) {
488
+ if (typeof ReadableStream === "undefined") {
489
+ throw new Error("Environment not supported, please report this as a bug");
357
490
  }
358
- return Buffer.concat(chunks);
491
+ return new ReadableStream({
492
+ start(controller) {
493
+ nodeStream.on("data", (chunk) => controller.enqueue(chunk));
494
+ nodeStream.on("end", () => controller.close());
495
+ nodeStream.on("error", (err) => controller.error(err));
496
+ },
497
+ cancel() {
498
+ nodeStream.destroy();
499
+ }
500
+ });
359
501
  }
360
- function parseUrlEncoded(text) {
361
- const out = {};
362
- for (const [key, value] of new URLSearchParams(text)) {
363
- const existing = out[key];
364
- if (existing === void 0) out[key] = value;
365
- else if (Array.isArray(existing)) existing.push(value);
366
- else out[key] = [existing, value];
367
- }
368
- return out;
502
+
503
+ // src/pipeline/serialize.ts
504
+ var TAG = /^\s*<[a-zA-Z!/]/;
505
+ var isHtml = (body) => TAG.test(body);
506
+ function fill(headers2, type2, length) {
507
+ setIfAbsent(headers2, "content-type", type2);
508
+ if (length != null) setIfAbsent(headers2, "content-length", String(length));
509
+ }
510
+ function serialize(body, headers2) {
511
+ if (body instanceof Blob) {
512
+ fill(headers2, body.type);
513
+ return body;
514
+ }
515
+ if (typeof body === "string") {
516
+ fill(headers2, isHtml(body) ? mimes_default.html : mimes_default.text, Buffer.byteLength(body));
517
+ return body;
518
+ }
519
+ if (body instanceof Uint8Array) {
520
+ fill(headers2, null, body.length);
521
+ return body;
522
+ }
523
+ if (typeof body?.getReader === "function") return body;
524
+ if (isReadableStream(body)) return toWeb(body);
525
+ if (body?.[Symbol.asyncIterator] || !Array.isArray(body) && body?.[Symbol.iterator]) {
526
+ return iteratorToReadable(body);
527
+ }
528
+ const payload = JSON.stringify(body);
529
+ fill(headers2, "application/json", Buffer.byteLength(payload));
530
+ return payload;
369
531
  }
370
- function addField(body, name, value) {
371
- if (body[name] === void 0) {
372
- body[name] = value;
373
- return;
532
+
533
+ // src/reply.ts
534
+ var EXPIRED2 = (/* @__PURE__ */ new Date(0)).toUTCString();
535
+ var Reply = class _Reply {
536
+ res;
537
+ constructor() {
538
+ this.res = {
539
+ headers: new Headers()
540
+ };
374
541
  }
375
- if (!Array.isArray(body[name])) body[name] = [body[name]];
376
- body[name].push(value);
377
- }
378
- function startPart(headerStr, bucket2, limits) {
379
- const name = getMatching(headerStr, /name="(.+?)"/).trim().replace(/\[\]$/, "");
380
- if (!name) return { kind: "skip" };
381
- const filename = getMatching(headerStr, /filename="(.+?)"/).trim();
382
- if (!filename) return { kind: "text", name, chunks: [] };
383
- const type2 = getMatching(headerStr, /Content-Type:\s*([^\r\n]+)/i).trim() || "application/octet-stream";
384
- if (!bucket2) return { kind: "drop" };
385
- if (limits) {
386
- return { kind: "validated", name, filename, type: type2, bucket: bucket2, limits, chunks: [] };
542
+ status(status2) {
543
+ this.res.status = status2;
544
+ return this;
387
545
  }
388
- const id = `${createId()}${getExt(filename)}`;
389
- let controller;
390
- const readable = new ReadableStream({
391
- start(c) {
392
- controller = c;
393
- }
394
- });
395
- const file2 = bucket2.file(id);
396
- return {
397
- kind: "file",
398
- name,
399
- filename,
400
- type: type2,
401
- id,
402
- controller,
403
- file: file2,
404
- write: file2.write(readable, { type: type2 }),
405
- size: 0
406
- };
407
- }
408
- function feedPart(part, data) {
409
- if (data.length === 0) return;
410
- if (part.kind === "text" || part.kind === "validated") part.chunks.push(data);
411
- else if (part.kind === "file") {
412
- part.controller.enqueue(data);
413
- part.size += data.length;
546
+ type(type2) {
547
+ if (!type2) return this;
548
+ type2 = mimes_default[type2.replace(/^\./, "")] || type2;
549
+ this.res.headers.set("content-type", type2);
550
+ return this;
414
551
  }
415
- }
416
- async function endPart(part, body) {
417
- if (part.kind === "text") {
418
- const buf = Buffer.concat(part.chunks);
419
- const value = isProbablyText(buf) ? buf.toString("utf-8").trim() : buf;
420
- addField(body, part.name, value);
421
- } else if (part.kind === "validated") {
422
- const buf = Buffer.concat(part.chunks);
423
- validateFile(part.filename, buf, part.type, part.limits);
424
- const ref = await saveFileToBucket(part.filename, buf, part.bucket, part.type);
425
- addField(body, part.name, ref);
426
- } else if (part.kind === "file") {
427
- part.controller.close();
428
- await part.write;
429
- addField(body, part.name, {
430
- name: part.filename,
431
- path: part.file.path,
432
- type: part.type,
433
- size: part.size
434
- });
552
+ download(name) {
553
+ const ext = name?.split(".").pop();
554
+ if (ext && !this.res.headers.get("content-type")) this.type(ext);
555
+ return this.headers("content-disposition", disposition(name));
435
556
  }
436
- }
437
- var BREAK = Buffer.from("\r\n\r\n");
438
- async function parseMultipart(stream, boundary, bucket2, limits, max = INF) {
439
- const delim = Buffer.from(`\r
440
- --${boundary}`);
441
- const body = {};
442
- let buf = Buffer.from("\r\n");
443
- let state = "boundary";
444
- let part = null;
445
- let textBytes = 0;
446
- const feed = (p, data) => {
447
- if (p.kind === "text") {
448
- textBytes += data.length;
449
- if (textBytes > max) throw tooLarge(max);
557
+ headers(key, value) {
558
+ if (typeof key !== "string") {
559
+ Object.entries(key).map(([key2, value2]) => this.headers(key2, value2));
560
+ return this;
450
561
  }
451
- feedPart(p, data);
452
- };
453
- for await (const chunk of asIterable(stream)) {
454
- buf = Buffer.concat([buf, Buffer.from(chunk)]);
455
- let advanced = true;
456
- while (advanced) {
457
- advanced = false;
458
- if (state === "boundary") {
459
- const i = buf.indexOf(delim);
460
- if (i === -1) {
461
- if (buf.length >= delim.length) {
462
- buf = buf.subarray(buf.length - delim.length + 1);
463
- }
464
- break;
465
- }
466
- if (buf.length < i + delim.length + 2) break;
467
- const after = i + delim.length;
468
- if (buf[after] === 45 && buf[after + 1] === 45) return body;
469
- buf = buf.subarray(after + 2);
470
- state = "headers";
471
- advanced = true;
472
- } else if (state === "headers") {
473
- const i = buf.indexOf(BREAK);
474
- if (i === -1) break;
475
- part = startPart(buf.subarray(0, i).toString("utf-8"), bucket2, limits);
476
- buf = buf.subarray(i + BREAK.length);
477
- state = "body";
478
- advanced = true;
479
- } else {
480
- const i = buf.indexOf(delim);
481
- if (i === -1) {
482
- const safe = buf.length - (delim.length - 1);
483
- if (safe > 0 && part) {
484
- feed(part, buf.subarray(0, safe));
485
- buf = buf.subarray(safe);
486
- }
487
- break;
488
- }
489
- if (part) {
490
- feed(part, buf.subarray(0, i));
491
- await endPart(part, body);
492
- part = null;
493
- }
494
- buf = buf.subarray(i);
495
- state = "boundary";
496
- advanced = true;
497
- }
562
+ if (Array.isArray(value)) {
563
+ this.res.headers.delete(key);
564
+ for (const val of value) this.res.headers.append(key, val);
565
+ return this;
498
566
  }
499
- }
500
- if (part) await endPart(part, body);
501
- return body;
502
- }
503
- async function streamToBucket(stream, type2, bucket2) {
504
- const id = `${createId()}${extFromType(type2)}`;
505
- const file2 = bucket2.file(id);
506
- let size = 0;
507
- let controller;
508
- const readable = new ReadableStream({
509
- start(c) {
510
- controller = c;
567
+ if (key.toLowerCase() === "set-cookie") {
568
+ this.res.headers.append(key, value);
569
+ } else {
570
+ this.res.headers.set(key, value);
511
571
  }
512
- });
513
- const write = file2.write(readable, { type: type2 });
514
- for await (const chunk of asIterable(stream)) {
515
- controller.enqueue(chunk);
516
- size += chunk.byteLength;
572
+ return this;
517
573
  }
518
- controller.close();
519
- await write;
520
- if (!size) return void 0;
521
- return { name: id, path: file2.path, type: type2, size };
522
- }
523
- async function parseBody(input, contentType, dest, max = INF) {
524
- const type2 = Array.isArray(contentType) ? contentType[0] : contentType;
525
- let bucket2;
526
- let limits;
527
- if (dest && "bucket" in dest) {
528
- bucket2 = dest.bucket;
529
- const { maxSize, minSize, fileType: fileType2 } = dest;
530
- if (maxSize != null || minSize != null || fileType2 != null) {
531
- limits = { maxSize, minSize, fileType: fileType2 };
574
+ cache(value) {
575
+ const resolved = resolveCache(value);
576
+ if (resolved) this.res.headers.set("cache-control", resolved);
577
+ return this;
578
+ }
579
+ cookies(key, value) {
580
+ if (typeof key === "object") {
581
+ Object.entries(key).map(([key2, value2]) => this.cookies(key2, value2));
582
+ return this;
532
583
  }
533
- } else {
534
- bucket2 = dest;
584
+ if (Array.isArray(value)) {
585
+ Object.values(value).map((val) => this.cookies(key, val));
586
+ return this;
587
+ }
588
+ if (value === null) return this.cookies(key, { expires: EXPIRED2 });
589
+ if (typeof value !== "object") return this.cookies(key, { value });
590
+ return this.headers("set-cookie", createCookies(key, value));
535
591
  }
536
- const boundary = type2 && /multipart\/form-data/i.test(type2) ? getBoundary(type2) : null;
537
- if (boundary) {
538
- return parseMultipart(toStream(input), boundary, bucket2, limits, max);
592
+ json(body) {
593
+ if (body === void 0) body = null;
594
+ setIfAbsent(this.res.headers, "content-type", "application/json");
595
+ return this.send(JSON.stringify(body));
539
596
  }
540
- if (!type2 || /^text\//i.test(type2)) {
541
- const buf = await toBuffer(input, max);
542
- return buf.length ? buf.toString("utf-8") : void 0;
597
+ redirect(path) {
598
+ this.headers("location", path);
599
+ if (this.res.status == null) this.res.status = 302;
600
+ return this.send();
543
601
  }
544
- if (/application\/json/i.test(type2)) {
545
- const buf = await toBuffer(input, max);
546
- return buf.length ? JSON.parse(buf.toString("utf-8")) : void 0;
602
+ async file(path) {
603
+ if (typeof path !== "string") {
604
+ if (!await path.exists()) return new Response(null, { status: 404 });
605
+ return this.type(fileType(path)).send(path.stream());
606
+ }
607
+ if (CLIMBS.test(path)) {
608
+ return new Response(null, { status: 404 });
609
+ }
610
+ try {
611
+ const fs = await import("fs");
612
+ const ext = path.split(".").pop();
613
+ await fs.promises.access(path);
614
+ const stream = fs.createReadStream(path);
615
+ return this.type(ext).send(stream);
616
+ } catch (error) {
617
+ if (error.code === "ENOENT" || error.code === "EISDIR") {
618
+ return new Response(null, { status: 404 });
619
+ }
620
+ throw error;
621
+ }
547
622
  }
548
- if (/application\/x-www-form-urlencoded/i.test(type2)) {
549
- const buf = await toBuffer(input, max);
550
- return buf.length ? parseUrlEncoded(buf.toString("utf-8")) : void 0;
551
- }
552
- if (!bucket2) {
553
- const buf = await toBuffer(input, max);
554
- return buf.length ? buf : void 0;
623
+ // Accepts everything a route can return, so `send(x)` and `return x` agree.
624
+ // Async because a bucket file has to be read before its status is known;
625
+ // routes await whatever they return, so this is invisible in normal use.
626
+ async send(input = "") {
627
+ const { status: status2 = 200, headers: headers2 } = this.res;
628
+ let body = input;
629
+ if (status2 === 101 || status2 === 204 || status2 === 205 || status2 === 304) {
630
+ return new Response(null, { status: status2, headers: headers2 });
631
+ }
632
+ if (body === null) body = "";
633
+ if (typeof body?.then === "function") body = await body;
634
+ if (typeof body === "function") body = body();
635
+ if (typeof body?.then === "function") {
636
+ throw new Error(
637
+ "Cannot render an async component: components must be synchronous. Await the data before rendering and pass it in as props."
638
+ );
639
+ }
640
+ if (body instanceof _Reply) body = await body.send();
641
+ if (body instanceof Response) {
642
+ const merged = new Headers(body.headers);
643
+ for (const [key, value] of headers2) {
644
+ if (key === "set-cookie") continue;
645
+ merged.set(key, value);
646
+ }
647
+ for (const cookie of headers2.getSetCookie?.() ?? []) {
648
+ merged.append("set-cookie", cookie);
649
+ }
650
+ if (body.url && /^(br|gzip)$/.test(merged.get("content-encoding") || "")) {
651
+ merged.delete("content-encoding");
652
+ }
653
+ return new Response(body.body, {
654
+ status: this.res.status ?? body.status,
655
+ headers: merged
656
+ });
657
+ }
658
+ if (isBucketFile(body)) {
659
+ return this.file(body);
660
+ }
661
+ return new Response(serialize(body, headers2), { status: status2, headers: headers2 });
555
662
  }
556
- if (limits) {
557
- const buf = await toBuffer(input);
558
- if (!buf.length) return void 0;
559
- const name = `upload${extFromType(type2)}`;
560
- validateFile(name, buf, type2, limits);
561
- return saveFileToBucket(name, buf, bucket2, type2);
663
+ };
664
+ var r = () => new Reply();
665
+ var status = (...args) => r().status(...args);
666
+ var headers = (...args) => r().headers(...args);
667
+ var type = (...args) => r().type(...args);
668
+ var cache = (...args) => r().cache(...args);
669
+ var download = (...args) => r().download(...args);
670
+ var cookies = (...args) => r().cookies(...args);
671
+ var send = (...args) => r().send(...args);
672
+ var json = (...args) => r().json(...args);
673
+ var file = (...args) => r().file(...args);
674
+ var redirect = (...args) => r().redirect(...args);
675
+
676
+ // src/body/sniff.ts
677
+ var ascii = (text) => [...text].map((char) => char.charCodeAt(0));
678
+ var SIGNATURES = [
679
+ { type: "image/png", magic: [137, 80, 78, 71, 13, 10, 26, 10] },
680
+ { type: "image/jpeg", magic: [255, 216, 255] },
681
+ { type: "image/gif", magic: ascii("GIF87a") },
682
+ { type: "image/gif", magic: ascii("GIF89a") },
683
+ // RIFF containers: the format is at byte 8, so the whole thing is one match
684
+ { type: "image/webp", magic: [...ascii("RIFF"), null, null, null, null, ...ascii("WEBP")] },
685
+ { type: "audio/wav", magic: [...ascii("RIFF"), null, null, null, null, ...ascii("WAVE")] },
686
+ { type: "image/bmp", magic: ascii("BM") },
687
+ { type: "image/tiff", magic: [73, 73, 42, 0] },
688
+ { type: "image/tiff", magic: [77, 77, 0, 42] },
689
+ { type: "image/vnd.microsoft.icon", magic: [0, 0, 1, 0] },
690
+ { type: "image/avif", magic: ascii("ftypavif"), offset: 4 },
691
+ { type: "image/heic", magic: ascii("ftypheic"), offset: 4 },
692
+ { type: "application/pdf", magic: ascii("%PDF-") },
693
+ { type: "application/zip", magic: [80, 75, 3, 4] },
694
+ { type: "application/gzip", magic: [31, 139] },
695
+ { type: "video/mp4", magic: ascii("ftyp"), offset: 4 },
696
+ { type: "video/webm", magic: [26, 69, 223, 163] },
697
+ { type: "audio/ogg", magic: ascii("OggS") },
698
+ { type: "audio/mpeg", magic: ascii("ID3") }
699
+ ];
700
+ var HEAD_SIZE = 32;
701
+ var matches = (head, { magic, offset = 0 }) => {
702
+ if (head.length < offset + magic.length) return false;
703
+ return magic.every((byte, i) => byte === null || head[offset + i] === byte);
704
+ };
705
+ function sniff(head) {
706
+ for (const signature of SIGNATURES) {
707
+ if (matches(head, signature)) return signature.type;
562
708
  }
563
- return streamToBucket(toStream(input), type2, bucket2);
709
+ return null;
710
+ }
711
+ var KNOWN = new Set(SIGNATURES.map((s) => s.type));
712
+ var isSniffable = (type2) => KNOWN.has((type2 || "").split(";")[0].trim().toLowerCase());
713
+ var CONTAINED = {
714
+ "application/zip": (type2) => type2.endsWith("+zip") || type2.startsWith("application/vnd.openxmlformats-officedocument.") || type2.startsWith("application/vnd.oasis.opendocument.") || type2 === "application/java-archive" || type2 === "application/vnd.android.package-archive"
715
+ };
716
+ function resolveType(sniffed, declared) {
717
+ if (!sniffed) return declared;
718
+ const inside = CONTAINED[sniffed];
719
+ const claim = (declared || "").split(";")[0].trim().toLowerCase();
720
+ return inside?.(claim) ? claim : sniffed;
564
721
  }
565
722
 
566
- // src/helpers/body.ts
567
- var sources = /* @__PURE__ */ new WeakMap();
568
- function setBodySource(ctx, source) {
569
- sources.set(ctx, source);
723
+ // src/body/upload.ts
724
+ var DEFAULT_FILE_SIZE = "10mb";
725
+ var DEFAULT_TOTAL_SIZE = "100mb";
726
+ var DEFAULT_FILES = 100;
727
+ function resolveUploads(up) {
728
+ if (up === false) return false;
729
+ if (!up) return null;
730
+ if (typeof up === "object" && "bucket" in up) {
731
+ const { bucket: bucket2, maxFileSize, maxTotalSize, maxFiles, minSize, fileType: fileType2 } = up;
732
+ if (maxFileSize != null) parseBytes(maxFileSize);
733
+ if (maxTotalSize != null) parseBytes(maxTotalSize);
734
+ if (minSize != null) parseBytes(minSize);
735
+ return {
736
+ bucket: bucket(bucket2),
737
+ maxFileSize: maxFileSize ?? DEFAULT_FILE_SIZE,
738
+ maxTotalSize: maxTotalSize ?? DEFAULT_TOTAL_SIZE,
739
+ maxFiles: maxFiles ?? DEFAULT_FILES,
740
+ minSize,
741
+ fileType: fileType2
742
+ };
743
+ }
744
+ return {
745
+ bucket: bucket(up),
746
+ maxFileSize: DEFAULT_FILE_SIZE,
747
+ maxTotalSize: DEFAULT_TOTAL_SIZE,
748
+ maxFiles: DEFAULT_FILES
749
+ };
570
750
  }
571
- async function resolveBody(ctx, mode = "parse", max = resolveMax(void 0)) {
572
- const source = sources.get(ctx);
573
- if (!source) return void 0;
574
- const contentType = String(ctx.headers["content-type"] || "");
575
- const isMultipart = /multipart\/form-data/i.test(contentType);
576
- const declared = Number(ctx.headers["content-length"]);
577
- const trustDeclared = !isMultipart && !ctx.options.uploads;
578
- if (max !== INF && trustDeclared && declared > max) throw tooLarge(max);
579
- if (mode === "stream") return source.getStream();
580
- if (mode === "raw") {
581
- const raw = await source.getBuffer();
582
- if (raw.length > max) throw tooLarge(max);
583
- if (!raw.length) return void 0;
584
- if (!ctx.headers["content-length"]) {
585
- ctx.headers["content-length"] = String(raw.length);
586
- }
587
- return raw;
751
+ function getExt(filename) {
752
+ const i = filename.lastIndexOf(".");
753
+ if (i <= 0) return ".bin";
754
+ return filename.slice(i).toLowerCase();
755
+ }
756
+ function validateFile(originalName, contentType, limits, sniffed) {
757
+ const { fileType: fileType2 } = limits;
758
+ if (!fileType2 || fileType2.length === 0) return;
759
+ if (sniffed === null && isSniffable(contentType)) {
760
+ throw errors_default.UPLOAD_TYPE_NOT_ALLOWED({
761
+ name: originalName,
762
+ type: contentType,
763
+ allowed: fileType2
764
+ });
588
765
  }
589
- const stream = source.getStream();
590
- if (!stream) return void 0;
591
- let size = 0;
592
- const counted = stream.pipeThrough(
593
- new TransformStream({
594
- transform(chunk, controller) {
595
- size += chunk.byteLength;
596
- controller.enqueue(chunk);
597
- }
598
- })
599
- );
600
- const parsed = await parseBody(
601
- counted,
602
- ctx.headers["content-type"],
603
- ctx.options.uploads,
604
- max
766
+ const ext = getExt(originalName);
767
+ const mime = contentType.toLowerCase();
768
+ const allowed = fileType2.some(
769
+ (t) => t.toLowerCase() === mime || t.toLowerCase() === ext
605
770
  );
606
- if (size && !ctx.headers["content-length"]) {
607
- ctx.headers["content-length"] = String(size);
771
+ if (!allowed) {
772
+ throw errors_default.UPLOAD_TYPE_NOT_ALLOWED({
773
+ name: originalName,
774
+ type: contentType,
775
+ allowed: fileType2
776
+ });
608
777
  }
609
- return parsed;
610
778
  }
611
779
 
612
- // src/helpers/createCookies.ts
613
- var EXPIRED = (/* @__PURE__ */ new Date(0)).toUTCString();
614
- var times = /(-?(?:\d+\.?\d*|\d*\.?\d+)(?:e[-+]?\d+)?)\s*([\p{L}]*)/iu;
615
- parse.millisecond = parse.ms = 1e-3;
616
- parse.second = parse.sec = parse.s = parse[""] = 1;
617
- parse.minute = parse.min = parse.m = parse.s * 60;
618
- parse.hour = parse.hr = parse.h = parse.m * 60;
619
- parse.day = parse.d = parse.h * 24;
620
- parse.week = parse.wk = parse.w = parse.d * 7;
621
- parse.year = parse.yr = parse.y = parse.d * 365.25;
622
- parse.month = parse.b = parse.y / 12;
623
- function parse(str) {
624
- if (str === null || str === void 0) return null;
625
- if (typeof str === "number") return str;
626
- if (typeof str !== "string") {
627
- throw new Error(`Not a string: ${str} (${typeof str})`);
780
+ // src/router.ts
781
+ function checkParserConflict(options, globalParser) {
782
+ const parser = options.parser ?? globalParser ?? "parse";
783
+ if (options.body && parser !== "parse") {
784
+ throw new Error(
785
+ `A \`parser: '${parser}'\` route never parses the body, so its \`body\` schema cannot run. Remove one, or set \`parser: 'parse'\` on the route.`
786
+ );
628
787
  }
629
- str = str.toLowerCase().replace(/[,_]/g, "");
630
- const [_, value, units] = times.exec(str) || [];
631
- if (!units) return null;
632
- const unitValue = parse[units] || parse[units.replace(/s$/, "")];
633
- if (!unitValue) return null;
634
- const result = unitValue * parseFloat(value);
635
- return Math.abs(Math.round(result * 1e3));
636
788
  }
637
- function normalizeExpires(expires) {
638
- if (expires === null || expires === void 0) return void 0;
639
- if (expires === 0) return EXPIRED;
640
- if (typeof expires === "string") {
641
- if (/^[\d._]+\w+$/.test(expires)) {
642
- return new Date(Date.now() + parse(expires)).toUTCString();
643
- } else {
644
- return expires;
789
+ var Router = class _Router {
790
+ // Assigned by the Server subclass; a bare router has none. Declared here so
791
+ // route registration can check options against the global config.
792
+ settings;
793
+ // Cross-cutting middleware added with .use(); they run on every request
794
+ middleware = [];
795
+ // Routes per method, each carrying its own (already-flattened) chain of fns
796
+ handlers = {
797
+ socket: [],
798
+ get: [],
799
+ head: [],
800
+ post: [],
801
+ put: [],
802
+ patch: [],
803
+ delete: [],
804
+ options: []
805
+ };
806
+ // For the router we can just return itself since it's not the final export,
807
+ // but then on the root it'll return some fancy wrappers
808
+ self() {
809
+ return this;
810
+ }
811
+ // Registers one route: bakes the current middleware + the route's own
812
+ // functions into a single flat `fns` list. A plain options object may sit
813
+ // between the path and the handlers, and it's pulled out here.
814
+ handle(method, pathOrFn, ...rest) {
815
+ let path = "*";
816
+ if (typeof pathOrFn === "string") {
817
+ path = pathOrFn;
818
+ } else if (pathOrFn != null) {
819
+ rest.unshift(pathOrFn);
820
+ }
821
+ let options = {};
822
+ if (rest[0] != null && typeof rest[0] !== "function") {
823
+ options = rest.shift();
645
824
  }
825
+ checkParserConflict(options, this.settings?.parser);
826
+ if (options.uploads !== void 0) {
827
+ options.uploads = resolveUploads(options.uploads);
828
+ }
829
+ const base = method === "socket" ? [] : this.middleware;
830
+ const fns = [...base, ...rest].filter((fn) => fn != null);
831
+ this.handlers[method].push({ path, options, fns });
832
+ return this.self();
646
833
  }
647
- if (typeof expires === "number") {
648
- return new Date(Date.now() + expires).toUTCString();
834
+ socket(pathOrMid, optionsOrMid, ...middleware) {
835
+ return this.handle("socket", pathOrMid, optionsOrMid, ...middleware);
649
836
  }
650
- if (expires instanceof Date) {
651
- return expires.toUTCString();
837
+ get(pathOrMid, optionsOrMid, ...middleware) {
838
+ return this.handle("get", pathOrMid, optionsOrMid, ...middleware);
652
839
  }
653
- return void 0;
654
- }
655
- function createCookies(key, val) {
656
- if (val.value === null) val.expires = EXPIRED;
657
- const { value, path, expires, maxAge, httpOnly, secure, sameSite } = val;
658
- let str = `${key}=${encodeURIComponent(value ?? "")};Path=${path || "/"}`;
659
- if (typeof expires !== "undefined") str += `;Expires=${normalizeExpires(expires)}`;
660
- if (typeof maxAge === "number") str += `;Max-Age=${maxAge}`;
661
- if (httpOnly) str += ";HttpOnly";
662
- if (secure) str += ";Secure";
663
- if (sameSite) str += `;SameSite=${sameSite}`;
664
- return str;
665
- }
666
-
667
- // src/helpers/etag.ts
668
- function etag(bytes) {
669
- let h = 2166136261;
670
- for (let i = 0; i < bytes.length; i++) {
671
- h ^= bytes[i];
672
- h = Math.imul(h, 16777619);
840
+ head(pathOrMid, optionsOrMid, ...middleware) {
841
+ return this.handle("head", pathOrMid, optionsOrMid, ...middleware);
673
842
  }
674
- return `"${bytes.length.toString(16)}-${(h >>> 0).toString(16)}"`;
675
- }
676
-
677
- // src/helpers/cache.ts
678
- function resolveCache(value) {
679
- if (value === false || value === 0) return "no-store";
680
- if (typeof value === "number") return `public, max-age=${Math.round(value)}`;
681
- if (typeof value !== "string") return null;
682
- const ms = parse(value);
683
- return ms === null ? null : `public, max-age=${Math.round(ms / 1e3)}`;
684
- }
685
- async function applyCache(out, ctx) {
686
- if (ctx.method !== "get" || out.status !== 200) return out;
687
- if (!out.headers.has("cache-control")) {
688
- const value = resolveCache(ctx.options.cache);
689
- if (value) out.headers.set("cache-control", value);
843
+ post(pathOrMid, optionsOrMid, ...middleware) {
844
+ return this.handle("post", pathOrMid, optionsOrMid, ...middleware);
690
845
  }
691
- if (out.headers.has("etag") || !out.headers.has("content-length")) return out;
692
- const bytes = new Uint8Array(await out.arrayBuffer());
693
- const tag = etag(bytes);
694
- const headers2 = new Headers(out.headers);
695
- headers2.set("etag", tag);
696
- if (ctx.headers["if-none-match"] === tag) {
697
- headers2.delete("content-length");
698
- return new Response(null, { status: 304, headers: headers2 });
846
+ put(pathOrMid, optionsOrMid, ...middleware) {
847
+ return this.handle("put", pathOrMid, optionsOrMid, ...middleware);
699
848
  }
700
- return new Response(bytes, { status: 200, headers: headers2 });
701
- }
702
-
703
- // src/helpers/clientIp.ts
704
- var first = (v) => (Array.isArray(v) ? v[0] : v) || "";
705
- var normalize = (ip) => ip.replace(/^::ffff:/, "");
706
- function clientIp(headers2, opts = {}) {
707
- const { remoteAddress = "", trustProxy = false } = opts;
708
- const cf = first(headers2["cf-connecting-ip"]);
709
- if (cf) return normalize(cf);
710
- const nf = first(headers2["x-nf-client-connection-ip"]);
711
- if (nf) return normalize(nf);
712
- if (trustProxy) {
713
- const xff = first(headers2["x-forwarded-for"]);
714
- if (xff) return normalize(xff.split(",")[0].trim());
715
- const real = first(headers2["x-real-ip"]);
716
- if (real) return normalize(real);
717
- }
718
- return normalize(remoteAddress);
719
- }
720
-
721
- // src/helpers/disposition.ts
722
- var encodeExt = (name) => encodeURIComponent(name).replace(
723
- /['()*]/g,
724
- (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
725
- );
726
- function disposition(name) {
727
- if (!name) return "attachment";
728
- const clean2 = name.replace(/[\r\n]/g, "").split(/[\\/]/).pop() || "";
729
- if (!clean2) return "attachment";
730
- const ascii = clean2.replace(/[^\x20-\x7e]/g, "?");
731
- const value = `attachment; filename="${ascii.replace(/["\\]/g, "\\$&")}"`;
732
- if (clean2 === ascii) return value;
733
- return `${value}; filename*=UTF-8''${encodeExt(clean2)}`;
734
- }
735
-
736
- // src/helpers/fileType.ts
737
- function fileType(file2) {
738
- if (file2.type) return file2.type;
739
- const name = file2.path || file2.name || "";
740
- const ext = name.split(".").pop()?.toLowerCase();
741
- return ext ? mimes_default[ext] : void 0;
742
- }
743
-
744
- // src/helpers/isHtml.ts
745
- var TAG = /^\s*<[a-zA-Z!/]/;
746
- function isHtml(body) {
747
- return TAG.test(body);
748
- }
749
-
750
- // src/helpers/isReadableStream.ts
751
- function isReadableStream(obj) {
752
- return obj !== null && typeof obj === "object" && typeof obj.pipe === "function" && typeof obj.read === "function" && typeof obj.on === "function";
753
- }
754
-
755
- // src/reply.ts
756
- var EXPIRED2 = (/* @__PURE__ */ new Date(0)).toUTCString();
757
- var Reply = class _Reply {
758
- res;
759
- constructor() {
760
- this.res = {
761
- headers: new Headers()
762
- };
763
- }
764
- status(status2) {
765
- this.res.status = status2;
766
- return this;
767
- }
768
- type(type2) {
769
- if (!type2) return this;
770
- type2 = mimes_default[type2.replace(/^\./, "")] || type2;
771
- this.res.headers.set("content-type", type2);
772
- return this;
773
- }
774
- download(name) {
775
- const ext = name?.split(".").pop();
776
- if (ext && !this.res.headers.get("content-type")) this.type(ext);
777
- return this.headers("content-disposition", disposition(name));
778
- }
779
- headers(key, value) {
780
- if (typeof key !== "string") {
781
- Object.entries(key).map(([key2, value2]) => this.headers(key2, value2));
782
- return this;
783
- }
784
- if (Array.isArray(value)) {
785
- this.res.headers.delete(key);
786
- for (const val of value) this.res.headers.append(key, val);
787
- return this;
788
- }
789
- if (key.toLowerCase() === "set-cookie") {
790
- this.res.headers.append(key, value);
791
- } else {
792
- this.res.headers.set(key, value);
793
- }
794
- return this;
795
- }
796
- cache(value) {
797
- const resolved = resolveCache(value);
798
- if (resolved) this.res.headers.set("cache-control", resolved);
799
- return this;
800
- }
801
- cookies(key, value) {
802
- if (typeof key === "object") {
803
- Object.entries(key).map(([key2, value2]) => this.cookies(key2, value2));
804
- return this;
805
- }
806
- if (Array.isArray(value)) {
807
- Object.values(value).map((val) => this.cookies(key, val));
808
- return this;
809
- }
810
- if (value === null) return this.cookies(key, { expires: EXPIRED2 });
811
- if (typeof value !== "object") return this.cookies(key, { value });
812
- return this.headers("set-cookie", createCookies(key, value));
813
- }
814
- json(body) {
815
- if (body === void 0) body = null;
816
- if (!this.res.headers.get("content-type")) {
817
- this.res.headers.set("content-type", "application/json");
818
- }
819
- return this.send(JSON.stringify(body));
849
+ patch(pathOrMid, optionsOrMid, ...middleware) {
850
+ return this.handle("patch", pathOrMid, optionsOrMid, ...middleware);
820
851
  }
821
- redirect(path) {
822
- this.headers("location", path);
823
- if (this.res.status == null) this.res.status = 302;
824
- return this.send();
852
+ delete(pathOrMid, optionsOrMid, ...middleware) {
853
+ return this.handle("delete", pathOrMid, optionsOrMid, ...middleware);
825
854
  }
826
- async file(path) {
827
- if (typeof path !== "string") {
828
- if (!await path.exists()) return new Response(null, { status: 404 });
829
- return this.type(fileType(path)).send(path.stream());
830
- }
831
- if (/(?:^|[\\/])\.\.(?:[\\/]|$)/.test(path)) {
832
- return new Response(null, { status: 404 });
833
- }
834
- try {
835
- const fs = await import("fs");
836
- const ext = path.split(".").pop();
837
- await fs.promises.access(path);
838
- const stream = fs.createReadStream(path);
839
- return this.type(ext).send(stream);
840
- } catch (error) {
841
- if (error.code === "ENOENT" || error.code === "EISDIR") {
842
- return new Response(null, { status: 404 });
843
- }
844
- throw error;
845
- }
855
+ options(pathOrMid, optionsOrMid, ...middleware) {
856
+ return this.handle("options", pathOrMid, optionsOrMid, ...middleware);
846
857
  }
847
- // Accepts everything a route can return, so `send(x)` and `return x` agree.
848
- // Async because a bucket file has to be read before its status is known;
849
- // routes await whatever they return, so this is invisible in normal use.
850
- async send(input = "") {
851
- const { status: status2 = 200, headers: headers2 } = this.res;
852
- let body = input;
853
- if (status2 === 101 || status2 === 204 || status2 === 205 || status2 === 304) {
854
- return new Response(null, { status: status2, headers: headers2 });
855
- }
856
- if (body === null) body = "";
857
- if (typeof body?.then === "function") body = await body;
858
- if (typeof body === "function") body = body();
859
- if (typeof body?.then === "function") {
860
- throw new Error(
861
- "Cannot render an async component: components must be synchronous. Await the data before rendering and pass it in as props."
862
- );
863
- }
864
- if (body instanceof _Reply) body = await body.send();
865
- if (body instanceof Response) {
866
- const merged = new Headers(body.headers);
867
- for (const [key, value] of headers2) {
868
- if (key === "set-cookie") continue;
869
- merged.set(key, value);
870
- }
871
- for (const cookie of headers2.getSetCookie?.() ?? []) {
872
- merged.append("set-cookie", cookie);
873
- }
874
- if (body.url && /^(br|gzip)$/.test(merged.get("content-encoding") || "")) {
875
- merged.delete("content-encoding");
876
- }
877
- return new Response(body.body, {
878
- status: this.res.status ?? body.status,
879
- headers: merged
880
- });
881
- }
882
- if (body && typeof body.stream === "function" && typeof body.bytes === "function" && typeof body.exists === "function" && typeof body.name === "string") {
883
- return this.file(body);
884
- }
885
- if (body instanceof Blob) {
886
- if (!headers2.get("content-type") && body.type) {
887
- headers2.set("content-type", body.type);
888
- }
889
- return new Response(body, { status: status2, headers: headers2 });
890
- }
891
- if (typeof body === "string") {
892
- if (!headers2.get("content-type")) {
893
- headers2.set("content-type", isHtml(body) ? mimes_default.html : mimes_default.text);
894
- }
895
- if (!headers2.has("content-length")) {
896
- headers2.set("content-length", String(Buffer.byteLength(body)));
897
- }
898
- return new Response(body, { status: status2, headers: headers2 });
899
- }
900
- const name = body?.constructor?.name;
901
- if (body instanceof Uint8Array) {
902
- if (!headers2.has("content-length")) {
903
- headers2.set("content-length", String(body.length));
858
+ use(...args) {
859
+ for (const arg of args) {
860
+ if (arg instanceof _Router) {
861
+ for (const m of Object.keys(arg.handlers)) {
862
+ for (const route of arg.handlers[m]) {
863
+ checkParserConflict(route.options, this.settings?.parser);
864
+ const base = m === "socket" ? [] : this.middleware;
865
+ this.handlers[m].push({
866
+ path: route.path,
867
+ options: route.options,
868
+ fns: [...base, ...route.fns]
869
+ });
870
+ }
871
+ }
872
+ } else {
873
+ this.middleware.push(arg);
904
874
  }
905
- return new Response(body, { status: status2, headers: headers2 });
906
- }
907
- if (typeof body?.getReader === "function") {
908
- return new Response(body, { status: status2, headers: headers2 });
909
- }
910
- if (name === "PassThrough" || name === "Readable") {
911
- return new Response(toWeb(body), { status: status2, headers: headers2 });
912
- }
913
- if (isReadableStream(body)) {
914
- return new Response(toWeb(body), { status: status2, headers: headers2 });
915
- }
916
- if (!Array.isArray(body) && body?.[Symbol.iterator]) {
917
- return new Response(iteratorToReadable(body), { status: status2, headers: headers2 });
918
- }
919
- if (body?.[Symbol.asyncIterator]) {
920
- return new Response(iteratorAsyncToReadable(body), { status: status2, headers: headers2 });
921
875
  }
922
- if (!headers2.get("content-type")) {
923
- headers2.set("content-type", "application/json");
924
- }
925
- const payload = JSON.stringify(body);
926
- if (!headers2.has("content-length")) {
927
- headers2.set("content-length", String(Buffer.byteLength(payload)));
928
- }
929
- return new Response(payload, { status: status2, headers: headers2 });
876
+ return this.self();
930
877
  }
931
878
  };
932
- var r = () => new Reply();
933
- var status = (...args) => r().status(...args);
934
- var headers = (...args) => r().headers(...args);
935
- var type = (...args) => r().type(...args);
936
- var cache = (...args) => r().cache(...args);
937
- var download = (...args) => r().download(...args);
938
- var cookies = (...args) => r().cookies(...args);
939
- var send = (...args) => r().send(...args);
940
- var json = (...args) => r().json(...args);
941
- var file = (...args) => r().file(...args);
942
- var redirect = (...args) => r().redirect(...args);
879
+ function router() {
880
+ return new Router();
881
+ }
943
882
 
944
- // src/helpers/jwt.ts
945
- var enc = new TextEncoder();
883
+ // src/util/toArray.ts
884
+ function toArray(value) {
885
+ if (value == null) return [];
886
+ return Array.isArray(value) ? [...value] : [value];
887
+ }
888
+
889
+ // src/auth/jwt.ts
890
+ var enc2 = new TextEncoder();
946
891
  var dec = new TextDecoder();
947
892
  var b64url = (data) => {
948
- const bytes = typeof data === "string" ? enc.encode(data) : data;
893
+ const bytes = typeof data === "string" ? enc2.encode(data) : data;
949
894
  let bin = "";
950
895
  for (const b of bytes) bin += String.fromCharCode(b);
951
896
  return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
@@ -958,9 +903,25 @@ var unb64url = (seg) => {
958
903
  for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
959
904
  return bytes;
960
905
  };
906
+ var decodeJwt = (token) => {
907
+ const parts = token.split(".");
908
+ if (parts.length !== 3) return null;
909
+ const [head, body, sig] = parts;
910
+ try {
911
+ return {
912
+ head,
913
+ body,
914
+ sig,
915
+ header: JSON.parse(dec.decode(unb64url(head))),
916
+ claims: JSON.parse(dec.decode(unb64url(body)))
917
+ };
918
+ } catch {
919
+ return null;
920
+ }
921
+ };
961
922
  var hmacKey = (secret) => crypto.subtle.importKey(
962
923
  "raw",
963
- enc.encode(secret),
924
+ enc2.encode(secret),
964
925
  { name: "HMAC", hash: "SHA-256" },
965
926
  false,
966
927
  ["sign", "verify"]
@@ -976,38 +937,26 @@ async function signJwt(payload, secret, expires) {
976
937
  const body = b64url(JSON.stringify(claims2));
977
938
  const data = `${head}.${body}`;
978
939
  const key = await hmacKey(secret);
979
- const sig = await crypto.subtle.sign("HMAC", key, enc.encode(data));
940
+ const sig = await crypto.subtle.sign("HMAC", key, enc2.encode(data));
980
941
  return `${data}.${b64url(new Uint8Array(sig))}`;
981
942
  }
982
943
  async function verifyJwt(token, secret) {
983
- const parts = token.split(".");
984
- if (parts.length !== 3) return null;
985
- const [head, body, sig] = parts;
986
- let header;
987
- try {
988
- header = JSON.parse(dec.decode(unb64url(head)));
989
- } catch {
990
- return null;
991
- }
992
- if (header?.alg !== "HS256") return null;
944
+ const t = decodeJwt(token);
945
+ if (!t) return null;
946
+ if (t.header?.alg !== "HS256") return null;
993
947
  let ok = false;
994
- for (const candidate of Array.isArray(secret) ? secret : [secret]) {
948
+ for (const candidate of toArray(secret)) {
995
949
  const key = await hmacKey(candidate);
996
950
  ok = await crypto.subtle.verify(
997
951
  "HMAC",
998
952
  key,
999
- unb64url(sig),
1000
- enc.encode(`${head}.${body}`)
953
+ unb64url(t.sig),
954
+ enc2.encode(`${t.head}.${t.body}`)
1001
955
  );
1002
956
  if (ok) break;
1003
957
  }
1004
958
  if (!ok) return null;
1005
- let payload;
1006
- try {
1007
- payload = JSON.parse(dec.decode(unb64url(body)));
1008
- } catch {
1009
- return null;
1010
- }
959
+ const payload = t.claims;
1011
960
  if (payload?.exp && Math.floor(Date.now() / 1e3) >= payload.exp) return null;
1012
961
  return payload;
1013
962
  }
@@ -1021,22 +970,21 @@ function seconds(expires) {
1021
970
  if (!ms) throw new Error(`Invalid \`expires\`: "${expires}"`);
1022
971
  return Math.round(ms / 1e3);
1023
972
  }
1024
- var looksLikeOurs = (token) => {
1025
- const parts = token.split(".");
1026
- if (parts.length !== 3) return false;
1027
- try {
1028
- const header = JSON.parse(atob(parts[0].replace(/-/g, "+").replace(/_/g, "/")));
1029
- return header?.alg === "HS256";
1030
- } catch {
1031
- return false;
1032
- }
1033
- };
973
+ var looksLikeOurs = (token) => decodeJwt(token)?.header?.alg === "HS256";
974
+ var authCookie = (ctx, value, expires) => ({
975
+ value,
976
+ path: "/",
977
+ expires,
978
+ httpOnly: true,
979
+ secure: ctx.platform.production,
980
+ sameSite: "Lax"
981
+ });
1034
982
  var bearer = (ctx) => {
1035
983
  const header = ctx.headers.authorization;
1036
984
  if (!header) return;
1037
985
  const [type2, token] = header.trim().split(" ");
1038
986
  if (type2?.toLowerCase() !== "bearer") return;
1039
- if (!token) throw ServerError_default.AUTH_INVALID_HEADER({ type: type2 });
987
+ if (!token) throw errors_default.AUTH_INVALID_HEADER({ type: type2 });
1040
988
  return token;
1041
989
  };
1042
990
  async function read(ctx, strategy) {
@@ -1044,8 +992,8 @@ async function read(ctx, strategy) {
1044
992
  if (!token) return;
1045
993
  const payload = await verifyJwt(token, ctx.options.secrets);
1046
994
  if (!payload) {
1047
- if (!inCookie(strategy)) throw ServerError_default.AUTH_INVALID_TOKEN();
1048
- ctx.clearCookie = NAME;
995
+ if (!inCookie(strategy)) throw errors_default.AUTH_INVALID_TOKEN();
996
+ clearOnSend(ctx, NAME);
1049
997
  ctx.options.log?.message(
1050
998
  "auth",
1051
999
  looksLikeOurs(token) ? "discarded a session cookie signed with a key that is not in SECRETS. If you rotated it, keep the previous value: secrets: [current, previous]" : "discarded a session cookie that was not issued by this app"
@@ -1061,29 +1009,78 @@ var meta = (payload, strategy) => ({
1061
1009
  provider: payload.provider
1062
1010
  });
1063
1011
  var issue = (ctx, payload, expires) => signJwt(payload, ctx.options.secrets[0], seconds(expires));
1064
-
1065
- // src/auth/providers/index.ts
1066
- import {
1067
- AmazonCognito,
1068
- AniList,
1069
- Apple,
1070
- Atlassian,
1071
- Auth0,
1072
- Authentik,
1073
- Autodesk,
1074
- BattleNet,
1075
- Bitbucket,
1076
- Box,
1077
- Bungie,
1078
- Coinbase,
1079
- Discord,
1080
- DonationAlerts,
1081
- Dribbble,
1082
- Dropbox,
1083
- Etsy,
1084
- EpicGames,
1085
- Facebook,
1086
- Figma,
1012
+ function validate(strategy, expires, config2) {
1013
+ if (!["session", "cookie", "token", "jwt"].includes(strategy)) {
1014
+ throw new Error(
1015
+ `Unknown strategy "${strategy}"; it takes 'session', 'cookie', 'token' or 'jwt'.`
1016
+ );
1017
+ }
1018
+ seconds(expires);
1019
+ const { onLogin, getUser, toPublicUser } = config2;
1020
+ if (onLogin && !getUser) {
1021
+ throw new Error("`onLogin` needs a `getUser`: something has to resolve the id it returns.");
1022
+ }
1023
+ if (isSigned(strategy)) {
1024
+ if (getUser && !toPublicUser) {
1025
+ throw new Error(
1026
+ `The \`${strategy}\` strategy signs the user into the credential, so it needs a \`toPublicUser\` to say what goes in. Signing the whole row would publish whatever else is on it.`
1027
+ );
1028
+ }
1029
+ } else if (!getUser) {
1030
+ throw new Error(
1031
+ `The \`${strategy}\` strategy puts an id in the credential, so it needs a \`getUser\` to resolve it. With no database, use \`cookie\` or \`jwt\`.`
1032
+ );
1033
+ }
1034
+ }
1035
+ var publicProfile = ({ id, email, name, avatar }) => ({
1036
+ id,
1037
+ email,
1038
+ name,
1039
+ avatar
1040
+ });
1041
+ async function credentialPayload(config2, strategy, ctx, profile) {
1042
+ const { onLogin, getUser, toPublicUser } = config2;
1043
+ if (!getUser) return { user: publicProfile(profile) };
1044
+ let id;
1045
+ try {
1046
+ id = await onLogin(profile, ctx);
1047
+ } catch (error) {
1048
+ error.expose = true;
1049
+ throw error;
1050
+ }
1051
+ if (id === void 0 || id === null) {
1052
+ throw new Error("`onLogin` must return the id the credential points at");
1053
+ }
1054
+ if (!isSigned(strategy)) return { sub: String(id) };
1055
+ const user = await getUser(String(id), ctx);
1056
+ if (user === void 0 || user === null) {
1057
+ throw new Error(`getUser returned nothing for the id "${id}" that onLogin just returned`);
1058
+ }
1059
+ return { user: await toPublicUser(user) };
1060
+ }
1061
+
1062
+ // src/auth/providers/index.ts
1063
+ import {
1064
+ AmazonCognito,
1065
+ AniList,
1066
+ Apple,
1067
+ Atlassian,
1068
+ Auth0,
1069
+ Authentik,
1070
+ Autodesk,
1071
+ BattleNet,
1072
+ Bitbucket,
1073
+ Box,
1074
+ Bungie,
1075
+ Coinbase,
1076
+ Discord,
1077
+ DonationAlerts,
1078
+ Dribbble,
1079
+ Dropbox,
1080
+ Etsy,
1081
+ EpicGames,
1082
+ Facebook,
1083
+ Figma,
1087
1084
  Gitea,
1088
1085
  GitHub,
1089
1086
  GitLab,
@@ -1138,10 +1135,9 @@ var passthrough = (options) => {
1138
1135
  const { id, secret, scope, issuer, ...rest } = options;
1139
1136
  return rest;
1140
1137
  };
1141
- var scopeOf = (options, fallback) => {
1142
- const scope = options.scope ?? fallback;
1143
- return Array.isArray(scope) ? scope.join(" ") : scope;
1144
- };
1138
+ var scopeOf = (options, fallback) => toArray(options.scope ?? fallback).join(" ");
1139
+ var callbackPath = (name) => `/auth/callback/${name}`;
1140
+ var callbackUrl = (ctx, name) => `${ctx.url.origin}${callbackPath(name)}`;
1145
1141
  var search = (base, params) => {
1146
1142
  const query = new URLSearchParams();
1147
1143
  for (const [key, value] of Object.entries(params)) {
@@ -1161,7 +1157,6 @@ var nowhere = {
1161
1157
  function antarcticProvider(name, Client) {
1162
1158
  const client = (ctx, options) => {
1163
1159
  const { id, secret } = credentials(name, options);
1164
- if (!id) throw new Error(`${name.toUpperCase()}_ID is not set`);
1165
1160
  return new Client({
1166
1161
  // Whatever that provider needs beyond the standard four: Auth0 takes a
1167
1162
  // `domain`, Keycloak a `realm`, Gitea a `baseURL`, Mastodon an
@@ -1169,8 +1164,9 @@ function antarcticProvider(name, Client) {
1169
1164
  ...passthrough(options),
1170
1165
  clientId: id,
1171
1166
  clientSecret: secret,
1172
- redirectURI: `${ctx.url.origin}/auth/callback/${name}`,
1173
- scopes: options.scope ? Array.isArray(options.scope) ? options.scope : options.scope.split(" ") : void 0,
1167
+ redirectURI: callbackUrl(ctx, name),
1168
+ // One list whether given as an array or a space-separated string
1169
+ scopes: options.scope ? toArray(options.scope).flatMap((s) => s.split(" ")) : void 0,
1174
1170
  store: nowhere
1175
1171
  });
1176
1172
  };
@@ -1201,6 +1197,92 @@ function antarcticProvider(name, Client) {
1201
1197
  };
1202
1198
  }
1203
1199
 
1200
+ // src/util/createId.ts
1201
+ var alphabet = "useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict";
1202
+ var random = (bytes) => crypto.getRandomValues(new Uint8Array(bytes));
1203
+ function createId(size = 16) {
1204
+ let id = "";
1205
+ const bytes = random(size);
1206
+ while (size--) {
1207
+ id += alphabet[bytes[size] & 61];
1208
+ }
1209
+ return id;
1210
+ }
1211
+
1212
+ // src/auth/discovery.ts
1213
+ var bare = (url) => url.replace(/\/+$/, "");
1214
+ var discovered = /* @__PURE__ */ new Map();
1215
+ function discover(issuer) {
1216
+ const base = bare(issuer);
1217
+ let doc = discovered.get(base);
1218
+ if (!doc) {
1219
+ const url = `${base}/.well-known/openid-configuration`;
1220
+ doc = fetch(url).catch(() => null).then((r2) => {
1221
+ if (!r2?.ok) throw errors_default.AUTH_ISSUER_UNREACHABLE({ url });
1222
+ return r2.json();
1223
+ });
1224
+ doc.catch(() => discovered.delete(base));
1225
+ discovered.set(base, doc);
1226
+ }
1227
+ return doc;
1228
+ }
1229
+
1230
+ // src/auth/providers/oidc.ts
1231
+ var claims = (token) => {
1232
+ const t = token ? decodeJwt(token) : null;
1233
+ if (!t) throw new Error("The issuer returned no usable id_token");
1234
+ return t.claims;
1235
+ };
1236
+ function oidcProvider(name) {
1237
+ return {
1238
+ async authorize(ctx, options) {
1239
+ const doc = await discover(options.issuer);
1240
+ const state = createId();
1241
+ const url = search(doc.authorization_endpoint, {
1242
+ client_id: credentials(name, options).id,
1243
+ response_type: "code",
1244
+ scope: scopeOf(options, "openid email profile"),
1245
+ redirect_uri: callbackUrl(ctx, name),
1246
+ state,
1247
+ ...passthrough(options)
1248
+ });
1249
+ return { url, state };
1250
+ },
1251
+ async exchange(ctx, options, code) {
1252
+ const doc = await discover(options.issuer);
1253
+ const { id, secret } = credentials(name, options);
1254
+ const body = new URLSearchParams({
1255
+ client_id: id,
1256
+ client_secret: secret,
1257
+ code,
1258
+ grant_type: "authorization_code"
1259
+ });
1260
+ body.set("redirect_uri", callbackUrl(ctx, name));
1261
+ const res = await fetch(doc.token_endpoint, {
1262
+ method: "POST",
1263
+ headers: {
1264
+ accept: "application/json",
1265
+ "content-type": "application/x-www-form-urlencoded"
1266
+ },
1267
+ body
1268
+ });
1269
+ if (!res.ok) throw new Error(`${name}: token exchange failed`);
1270
+ const token = await res.json();
1271
+ const raw = claims(token.id_token);
1272
+ return {
1273
+ provider: name,
1274
+ id: String(raw.sub),
1275
+ email: raw.email,
1276
+ name: raw.name,
1277
+ avatar: raw.picture,
1278
+ accessToken: token.access_token,
1279
+ refreshToken: token.refresh_token,
1280
+ raw
1281
+ };
1282
+ }
1283
+ };
1284
+ }
1285
+
1204
1286
  // src/auth/providers/index.ts
1205
1287
  var CLASSES = {
1206
1288
  amazoncognito: AmazonCognito,
@@ -1284,78 +1366,40 @@ for (const [alias, target2] of Object.entries(ALIASES)) {
1284
1366
  var ISSUERS = {
1285
1367
  paypal: "https://www.paypal.com"
1286
1368
  };
1287
- var providers_default = providers;
1288
-
1289
- // src/auth/providers/oidc.ts
1290
- var discovered = /* @__PURE__ */ new Map();
1291
- function discover(issuer) {
1292
- let doc = discovered.get(issuer);
1293
- if (!doc) {
1294
- const url = `${issuer.replace(/\/$/, "")}/.well-known/openid-configuration`;
1295
- doc = fetch(url).then((r2) => {
1296
- if (!r2.ok) throw new Error(`Cannot reach the OIDC issuer at ${url}`);
1297
- return r2.json();
1298
- });
1299
- doc.catch(() => discovered.delete(issuer));
1300
- discovered.set(issuer, doc);
1369
+ function normalizeProviders(given) {
1370
+ if (typeof given === "string") return { [given]: {} };
1371
+ if (Array.isArray(given)) {
1372
+ return Object.fromEntries(given.map((name) => [name, {}]));
1301
1373
  }
1302
- return doc;
1374
+ const map2 = {};
1375
+ for (const [name, raw] of Object.entries(given)) {
1376
+ map2[name] = typeof raw === "string" ? { issuer: raw } : { ...raw };
1377
+ }
1378
+ return map2;
1303
1379
  }
1304
- var claims = (token) => {
1305
- const body = token.split(".")[1];
1306
- if (!body) throw new Error("The issuer returned no usable id_token");
1307
- let b64 = body.replace(/-/g, "+").replace(/_/g, "/");
1308
- b64 += "=".repeat((4 - b64.length % 4) % 4);
1309
- return JSON.parse(atob(b64));
1310
- };
1311
- function oidcProvider(name) {
1312
- return {
1313
- async authorize(ctx, options) {
1314
- const doc = await discover(options.issuer);
1315
- const state = createId();
1316
- const url = search(doc.authorization_endpoint, {
1317
- client_id: credentials(name, options).id,
1318
- response_type: "code",
1319
- scope: scopeOf(options, "openid email profile"),
1320
- redirect_uri: `${ctx.url.origin}/auth/callback/${name}`,
1321
- state,
1322
- ...passthrough(options)
1323
- });
1324
- return { url, state };
1325
- },
1326
- async exchange(ctx, options, code) {
1327
- const doc = await discover(options.issuer);
1328
- const { id, secret } = credentials(name, options);
1329
- const body = new URLSearchParams({
1330
- client_id: id,
1331
- client_secret: secret,
1332
- code,
1333
- grant_type: "authorization_code"
1334
- });
1335
- body.set("redirect_uri", `${ctx.url.origin}/auth/callback/${name}`);
1336
- const res = await fetch(doc.token_endpoint, {
1337
- method: "POST",
1338
- headers: {
1339
- accept: "application/json",
1340
- "content-type": "application/x-www-form-urlencoded"
1341
- },
1342
- body
1343
- });
1344
- if (!res.ok) throw new Error(`${name}: token exchange failed`);
1345
- const token = await res.json();
1346
- const raw = claims(token.id_token);
1347
- return {
1348
- provider: name,
1349
- id: String(raw.sub),
1350
- email: raw.email,
1351
- name: raw.name,
1352
- avatar: raw.picture,
1353
- accessToken: token.access_token,
1354
- refreshToken: token.refresh_token,
1355
- raw
1356
- };
1380
+ function resolveProvider(name, options) {
1381
+ if (!options.issuer && !providers[name] && ISSUERS[name]) {
1382
+ options.issuer = ISSUERS[name];
1383
+ }
1384
+ if (options.issuer) return oidcProvider(name);
1385
+ if (providers[name]) return providers[name];
1386
+ throw new Error(
1387
+ `Unknown provider "${name}". Give it an \`issuer\` to use any OIDC provider, or pick one of "${Object.keys(providers).join('", "')}".`
1388
+ );
1389
+ }
1390
+ function parseProviders(given) {
1391
+ const map2 = normalizeProviders(given);
1392
+ const list = Object.entries(map2).map(([name, options]) => {
1393
+ const provider = resolveProvider(name, options);
1394
+ if (!credentials(name, options).id) {
1395
+ throw new Error(
1396
+ `Provider "${name}" has no client id: set the ${name.toUpperCase()}_ID environment variable (usually along ${name.toUpperCase()}_SECRET), or pass \`{ id, secret }\` in its options.`
1397
+ );
1357
1398
  }
1358
- };
1399
+ return { name, options, provider };
1400
+ });
1401
+ if (!list.length) throw new Error("Auth needs at least one provider");
1402
+ return list;
1359
1403
  }
1360
1404
 
1361
1405
  // src/auth/state.ts
@@ -1363,21 +1407,14 @@ var NAME2 = "oauth_state";
1363
1407
  var EXPIRES = "10m";
1364
1408
  async function startState(ctx, pending) {
1365
1409
  const value = await signJwt(pending, ctx.options.secrets[0], 10 * 60);
1366
- return {
1367
- value,
1368
- path: "/",
1369
- expires: EXPIRES,
1370
- httpOnly: true,
1371
- secure: ctx.platform.production,
1372
- sameSite: "Lax"
1373
- };
1410
+ return authCookie(ctx, value, EXPIRES);
1374
1411
  }
1375
1412
  async function readState(ctx, received) {
1376
1413
  const cookie = ctx.cookies[NAME2];
1377
- if (!cookie || !received) throw ServerError_default.AUTH_INVALID_STATE();
1414
+ if (!cookie || !received) throw errors_default.AUTH_INVALID_STATE();
1378
1415
  const pending = await verifyJwt(cookie, ctx.options.secrets);
1379
1416
  if (!pending || pending.state !== received) {
1380
- throw ServerError_default.AUTH_INVALID_STATE();
1417
+ throw errors_default.AUTH_INVALID_STATE();
1381
1418
  }
1382
1419
  return pending;
1383
1420
  }
@@ -1385,93 +1422,56 @@ async function readState(ctx, received) {
1385
1422
  // src/auth/flow.ts
1386
1423
  var SPEC = { schema: { tags: "auth" } };
1387
1424
  var wantsJson = (ctx) => String(ctx.headers.accept || "").includes("application/json");
1388
- function parseProviders(given) {
1389
- const map2 = typeof given === "string" ? { [given]: {} } : Array.isArray(given) ? Object.fromEntries(given.map((name) => [name, {}])) : { ...given };
1390
- const out = [];
1391
- for (const [name, raw] of Object.entries(map2)) {
1392
- const options = typeof raw === "string" ? { issuer: raw } : { ...raw };
1393
- if (!options.issuer && !providers_default[name] && ISSUERS[name]) {
1394
- options.issuer = ISSUERS[name];
1395
- }
1396
- if (options.issuer) {
1397
- out.push({ name, options, provider: oidcProvider(name) });
1398
- } else if (providers_default[name]) {
1399
- out.push({ name, options, provider: providers_default[name] });
1400
- } else {
1401
- throw new Error(
1402
- `Unknown provider "${name}". Give it an \`issuer\` to use any OIDC provider, or pick one of "${Object.keys(providers_default).join('", "')}".`
1403
- );
1404
- }
1405
- }
1406
- if (!out.length) throw new Error("Auth needs at least one provider");
1407
- return out;
1408
- }
1409
1425
  var target = async (where, fallback, user, ctx) => typeof where === "function" ? where(user, ctx) : where ?? fallback;
1410
- function entry(config2) {
1426
+ var errorRedirect = async (redirects, ctx, message) => {
1427
+ const to = await target(redirects.error, "/", null, ctx);
1428
+ return redirect(`${to}?error=${encodeURIComponent(message)}`);
1429
+ };
1430
+ function failureMessage(error, name) {
1431
+ if (error?.expose) return error.message;
1432
+ console.error(`[server:auth] ${name} callback failed:`, error);
1433
+ return "Could not sign you in";
1434
+ }
1435
+ var spendState = (res) => {
1436
+ res.headers.append("set-cookie", clearCookie(NAME2));
1437
+ return res;
1438
+ };
1439
+ var loginRoute = ({ provider, options }) => async (ctx) => {
1440
+ const { url, state, payload } = await provider.authorize(ctx, options);
1441
+ const cookie = await startState(ctx, { state, payload });
1442
+ if (wantsJson(ctx)) {
1443
+ return cookies(NAME2, cookie).json({ url });
1444
+ }
1445
+ return cookies(NAME2, cookie).redirect(url);
1446
+ };
1447
+ var callbackRoute = ({ name, options, provider }, redirects, finish) => async (ctx) => {
1448
+ const query = ctx.url.query;
1449
+ if (query.error) return errorRedirect(redirects, ctx, query.error);
1450
+ const pending = await readState(ctx, query.state);
1451
+ if (!query.code) throw errors_default.AUTH_NO_CODE();
1452
+ try {
1453
+ const profile = await provider.exchange(ctx, options, query.code, pending);
1454
+ return spendState(await finish(ctx, profile));
1455
+ } catch (error) {
1456
+ const message = failureMessage(error, name);
1457
+ return spendState(await errorRedirect(redirects, ctx, message));
1458
+ }
1459
+ };
1460
+ function flowEntry(config2) {
1411
1461
  const list = parseProviders(config2.providers);
1412
1462
  const strategy = config2.strategy ?? "session";
1413
- if (!["session", "cookie", "token", "jwt"].includes(strategy)) {
1414
- throw new Error(
1415
- `Unknown strategy "${strategy}"; it takes 'session', 'cookie', 'token' or 'jwt'.`
1416
- );
1417
- }
1418
1463
  const expires = config2.expires ?? "30d";
1419
- seconds(expires);
1420
- const { onLogin, getUser, toPublicUser, onLogout } = config2;
1421
- if (onLogin && !getUser) {
1422
- throw new Error("`onLogin` needs a `getUser`: something has to resolve the id it returns.");
1423
- }
1424
- if (isSigned(strategy)) {
1425
- if (getUser && !toPublicUser) {
1426
- throw new Error(
1427
- `The \`${strategy}\` strategy signs the user into the credential, so it needs a \`toPublicUser\` to say what goes in. Signing the whole row would publish whatever else is on it.`
1428
- );
1429
- }
1430
- } else if (!getUser) {
1431
- throw new Error(
1432
- `The \`${strategy}\` strategy puts an id in the credential, so it needs a \`getUser\` to resolve it. With no database, use \`cookie\` or \`jwt\`.`
1433
- );
1434
- }
1435
- const publicProfile = ({ id, email, name, avatar }) => ({
1436
- id,
1437
- email,
1438
- name,
1439
- avatar
1440
- });
1441
- const redirects = typeof config2.redirect === "object" ? config2.redirect : {};
1442
- const loginTo = typeof config2.redirect === "object" ? redirects.login : config2.redirect;
1464
+ validate(strategy, expires, config2);
1465
+ const { getUser, onLogout } = config2;
1466
+ const redirects = typeof config2.redirect === "object" ? config2.redirect : { login: config2.redirect };
1443
1467
  const finish = async (ctx, profile) => {
1444
- const payload = getUser ? await (async () => {
1445
- let id;
1446
- try {
1447
- id = await onLogin(profile, ctx);
1448
- } catch (error) {
1449
- error.expose = true;
1450
- throw error;
1451
- }
1452
- if (id === void 0 || id === null) {
1453
- throw new Error("`onLogin` must return the id the credential points at");
1454
- }
1455
- if (!isSigned(strategy)) return { sub: String(id) };
1456
- const user2 = await getUser(String(id), ctx);
1457
- if (user2 === void 0 || user2 === null) {
1458
- throw new Error(`getUser returned nothing for the id "${id}" that onLogin just returned`);
1459
- }
1460
- return { user: await toPublicUser(user2) };
1461
- })() : { user: publicProfile(profile) };
1468
+ const payload = await credentialPayload(config2, strategy, ctx, profile);
1462
1469
  const signed = { ...payload, provider: profile.provider };
1463
1470
  const token = await issue(ctx, signed, expires);
1464
1471
  const user = signed.user ?? await getUser(signed.sub, ctx);
1465
- const to = await target(loginTo, "/", user, ctx);
1472
+ const to = await target(redirects.login, "/", user, ctx);
1466
1473
  if (inCookie(strategy)) {
1467
- return cookies("session", {
1468
- value: token,
1469
- path: "/",
1470
- expires,
1471
- httpOnly: true,
1472
- secure: ctx.platform.production,
1473
- sameSite: "Lax"
1474
- }).redirect(to);
1474
+ return cookies(NAME, authCookie(ctx, token, expires)).redirect(to);
1475
1475
  }
1476
1476
  return redirect(`${to}#token=${token}`);
1477
1477
  };
@@ -1485,70 +1485,49 @@ function entry(config2) {
1485
1485
  if (!payload.sub) return;
1486
1486
  return getUser(payload.sub, ctx);
1487
1487
  },
1488
- routes(app) {
1489
- for (const { name, options, provider } of list) {
1490
- app.get(`/auth/login/${name}`, SPEC, async (ctx) => {
1491
- const { url, state, payload } = await provider.authorize(ctx, options);
1492
- const cookie = await startState(ctx, { state, payload });
1493
- if (wantsJson(ctx)) {
1494
- return cookies(NAME2, cookie).json({ url });
1495
- }
1496
- return cookies(NAME2, cookie).redirect(url);
1497
- });
1498
- const callback = async (ctx) => {
1499
- const query = ctx.url.query;
1500
- if (query.error) {
1501
- const to = await target(redirects.error, "/", null, ctx);
1502
- return redirect(`${to}?error=${encodeURIComponent(query.error)}`);
1503
- }
1504
- const pending = await readState(ctx, query.state);
1505
- if (!query.code) throw ServerError_default.AUTH_NO_CODE();
1506
- let res;
1507
- try {
1508
- const profile = await provider.exchange(
1509
- ctx,
1510
- options,
1511
- query.code,
1512
- pending
1513
- );
1514
- res = await finish(ctx, profile);
1515
- } catch (error) {
1516
- const to = await target(redirects.error, "/", null, ctx);
1517
- let message = "Could not sign you in";
1518
- if (error?.expose) message = error.message;
1519
- else console.error(`[server:auth] ${name} callback failed:`, error);
1520
- res = await redirect(`${to}?error=${encodeURIComponent(message)}`);
1521
- }
1522
- res.headers.append(
1523
- "set-cookie",
1524
- `${NAME2}=; Path=/; Max-Age=0; HttpOnly`
1525
- );
1526
- return res;
1527
- };
1528
- app.get(`/auth/callback/${name}`, SPEC, callback);
1488
+ routes() {
1489
+ const r2 = router();
1490
+ for (const one of list) {
1491
+ r2.get(`/auth/login/${one.name}`, SPEC, loginRoute(one));
1492
+ r2.get(callbackPath(one.name), SPEC, callbackRoute(one, redirects, finish));
1529
1493
  }
1530
- app.post("/auth/logout", SPEC, async (ctx) => {
1494
+ r2.post("/auth/logout", SPEC, async (ctx) => {
1531
1495
  const payload = await read(ctx, strategy).catch(() => void 0);
1532
1496
  if (onLogout && payload?.sub) await onLogout(payload.sub, ctx);
1533
1497
  const to = await target(redirects.logout, "/", null, ctx);
1534
1498
  if (!inCookie(strategy)) return status(204);
1535
- return cookies("session", { value: null }).redirect(to);
1499
+ return cookies(NAME, { value: null }).redirect(to);
1536
1500
  });
1501
+ return r2;
1502
+ }
1503
+ };
1504
+ }
1505
+
1506
+ // src/auth/instance.ts
1507
+ function instanceEntry(instance) {
1508
+ const path = (instance.path ?? "/api/auth").replace(/\/$/, "");
1509
+ const raw = { parser: "stream" };
1510
+ const forward = (ctx) => instance.handler(
1511
+ new Request(ctx.url.href, {
1512
+ method: ctx.method,
1513
+ headers: ctx.headers,
1514
+ body: ctx.body,
1515
+ // Required by fetch whenever a body is a stream
1516
+ ...ctx.body ? { duplex: "half" } : {}
1517
+ })
1518
+ );
1519
+ return {
1520
+ name: `instance:${path}`,
1521
+ user: async (ctx) => instance.user?.(ctx),
1522
+ routes: () => {
1523
+ const wildcard = `${path}/*`;
1524
+ return router().get(wildcard, raw, forward).post(wildcard, raw, forward).put(wildcard, raw, forward).patch(wildcard, raw, forward).delete(wildcard, raw, forward);
1537
1525
  }
1538
1526
  };
1539
1527
  }
1540
1528
 
1541
1529
  // src/auth/verify.ts
1542
- var enc2 = new TextEncoder();
1543
- var dec2 = new TextDecoder();
1544
- var unb64url2 = (seg) => {
1545
- let b64 = seg.replace(/-/g, "+").replace(/_/g, "/");
1546
- b64 += "=".repeat((4 - b64.length % 4) % 4);
1547
- const bin = atob(b64);
1548
- const bytes = new Uint8Array(bin.length);
1549
- for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
1550
- return bytes;
1551
- };
1530
+ var enc3 = new TextEncoder();
1552
1531
  var ALGS = {
1553
1532
  RS256: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
1554
1533
  RS384: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-384" },
@@ -1558,11 +1537,10 @@ var ALGS = {
1558
1537
  };
1559
1538
  var cache2 = /* @__PURE__ */ new Map();
1560
1539
  function keysOf(issuer, refresh = false) {
1561
- let entry3 = cache2.get(issuer);
1562
- if (!entry3 || refresh && Date.now() - entry3.at > 6e4) {
1540
+ let entry = cache2.get(issuer);
1541
+ if (!entry || refresh && Date.now() - entry.at > 6e4) {
1563
1542
  const keys = (async () => {
1564
- const url = `${issuer.replace(/\/$/, "")}/.well-known/openid-configuration`;
1565
- const discovery = await fetch(url).then((r2) => r2.json());
1543
+ const discovery = await discover(issuer);
1566
1544
  const set = await fetch(discovery.jwks_uri).then((r2) => r2.json());
1567
1545
  const out = /* @__PURE__ */ new Map();
1568
1546
  for (const jwk of set.keys ?? []) {
@@ -1576,90 +1554,73 @@ function keysOf(issuer, refresh = false) {
1576
1554
  return out;
1577
1555
  })();
1578
1556
  keys.catch(() => cache2.delete(issuer));
1579
- entry3 = { at: Date.now(), keys };
1580
- cache2.set(issuer, entry3);
1557
+ entry = { at: Date.now(), keys };
1558
+ cache2.set(issuer, entry);
1581
1559
  }
1582
- return entry3.keys;
1560
+ return entry.keys;
1583
1561
  }
1584
- var bearer2 = (ctx) => {
1585
- const header = ctx.headers.authorization;
1586
- if (!header) return;
1587
- const [type2, token] = header.trim().split(" ");
1588
- if (type2?.toLowerCase() !== "bearer") return;
1589
- if (!token) throw ServerError_default.AUTH_INVALID_HEADER({ type: type2 });
1590
- return token;
1591
- };
1592
- function entry2(options) {
1593
- const { issuer, audience } = options;
1594
- const claimNames = options.audienceClaim ? Array.isArray(options.audienceClaim) ? options.audienceClaim : [options.audienceClaim] : ["aud"];
1562
+ function verifyEntry(options) {
1563
+ const issuer = bare(options.issuer);
1564
+ const { audience } = options;
1565
+ const claimNames = toArray(options.audienceClaim ?? "aud");
1595
1566
  if (!audience) {
1596
1567
  throw new Error(
1597
1568
  "`issuer` needs an `audience`: one issuer serves many applications, and without it a token minted for another one is accepted here."
1598
1569
  );
1599
1570
  }
1600
- const allowed = Array.isArray(audience) ? audience : [audience];
1571
+ const allowed = toArray(audience);
1601
1572
  return {
1602
1573
  name: `verify:${issuer}`,
1603
1574
  async user(ctx) {
1604
- const token = options.cookie ? ctx.cookies[options.cookie] : bearer2(ctx);
1575
+ const token = options.cookie ? ctx.cookies[options.cookie] : bearer(ctx);
1605
1576
  if (!token) return;
1606
1577
  let claims2;
1607
1578
  try {
1608
1579
  claims2 = await check(token, issuer, allowed, claimNames);
1609
1580
  } catch (error) {
1581
+ if (error?.code === "AUTH_ISSUER_UNREACHABLE") throw error;
1610
1582
  if (!options.cookie) throw error;
1611
- ctx.clearCookie = options.cookie;
1583
+ clearOnSend(ctx, options.cookie);
1612
1584
  ctx.options.log?.message(
1613
1585
  "auth",
1614
1586
  `discarded a ${options.cookie} cookie that ${issuer} did not sign, or that has expired`
1615
1587
  );
1616
1588
  return;
1617
1589
  }
1618
- ctx.auth = {
1619
- issuedAt: new Date((claims2.iat ?? 0) * 1e3),
1620
- expiresAt: claims2.exp ? new Date(claims2.exp * 1e3) : void 0,
1621
- strategy: options.cookie ? "cookie" : "jwt",
1622
- provider: issuer
1623
- };
1590
+ ctx.auth = meta(
1591
+ { iat: claims2.iat ?? 0, exp: claims2.exp, provider: issuer },
1592
+ options.cookie ? "cookie" : "jwt"
1593
+ );
1624
1594
  if (!options.getUser) return claims2;
1625
1595
  return options.getUser(claims2.sub, ctx);
1626
1596
  }
1627
1597
  };
1628
1598
  }
1629
1599
  async function check(token, issuer, allowed, claimNames) {
1630
- const parts = token.split(".");
1631
- if (parts.length !== 3) throw ServerError_default.AUTH_INVALID_TOKEN();
1632
- const [head, body, sig] = parts;
1633
- let header;
1634
- let claims2;
1635
- try {
1636
- header = JSON.parse(dec2.decode(unb64url2(head)));
1637
- claims2 = JSON.parse(dec2.decode(unb64url2(body)));
1638
- } catch {
1639
- throw ServerError_default.AUTH_INVALID_TOKEN();
1640
- }
1600
+ const t = decodeJwt(token);
1601
+ if (!t) throw errors_default.AUTH_INVALID_TOKEN();
1602
+ const { head, body, sig, header, claims: claims2 } = t;
1641
1603
  const algorithm = ALGS[header?.alg];
1642
- if (!algorithm) throw ServerError_default.AUTH_INVALID_TOKEN();
1604
+ if (!algorithm) throw errors_default.AUTH_INVALID_TOKEN();
1643
1605
  let key = (await keysOf(issuer)).get(header.kid);
1644
1606
  if (!key) key = (await keysOf(issuer, true)).get(header.kid);
1645
- if (!key) throw ServerError_default.AUTH_INVALID_TOKEN();
1607
+ if (!key) throw errors_default.AUTH_INVALID_TOKEN();
1646
1608
  const ok = await crypto.subtle.verify(
1647
1609
  algorithm.name === "ECDSA" ? { name: "ECDSA", hash: algorithm.hash } : algorithm,
1648
1610
  key,
1649
- unb64url2(sig),
1650
- enc2.encode(`${head}.${body}`)
1611
+ unb64url(sig),
1612
+ enc3.encode(`${head}.${body}`)
1651
1613
  );
1652
- if (!ok) throw ServerError_default.AUTH_INVALID_TOKEN();
1614
+ if (!ok) throw errors_default.AUTH_INVALID_TOKEN();
1653
1615
  const now = Math.floor(Date.now() / 1e3);
1654
- if (claims2.exp && now >= claims2.exp) throw ServerError_default.AUTH_INVALID_TOKEN();
1655
- if (claims2.nbf && now < claims2.nbf) throw ServerError_default.AUTH_INVALID_TOKEN();
1656
- if (claims2.iss !== issuer) throw ServerError_default.AUTH_INVALID_TOKEN();
1616
+ if (claims2.exp && now >= claims2.exp) throw errors_default.AUTH_INVALID_TOKEN();
1617
+ if (claims2.nbf && now < claims2.nbf) throw errors_default.AUTH_INVALID_TOKEN();
1618
+ if (bare(claims2.iss ?? "") !== issuer) throw errors_default.AUTH_INVALID_TOKEN();
1657
1619
  const name = claimNames.find((one) => claims2[one] !== void 0);
1658
- if (!name) throw ServerError_default.AUTH_INVALID_TOKEN();
1659
- const value = claims2[name];
1660
- const aud = Array.isArray(value) ? value : [value];
1620
+ if (!name) throw errors_default.AUTH_INVALID_TOKEN();
1621
+ const aud = toArray(claims2[name]);
1661
1622
  if (!aud.some((one) => allowed.includes(one))) {
1662
- throw ServerError_default.AUTH_INVALID_TOKEN();
1623
+ throw errors_default.AUTH_INVALID_TOKEN();
1663
1624
  }
1664
1625
  return claims2;
1665
1626
  }
@@ -1692,20 +1653,8 @@ var VENDORS = {
1692
1653
  docs: "https://supabase.com/docs/guides/auth/jwts"
1693
1654
  }
1694
1655
  };
1695
- var vendors_default = VENDORS;
1696
-
1697
- // src/auth/parse.ts
1698
- function parseAuth(auth2) {
1699
- if (!auth2) return null;
1700
- if (Array.isArray(auth2)) {
1701
- throw new Error(
1702
- "`auth` takes one method. For several login options, list them under `providers` instead: auth: { providers: ['github', 'google'], ... }."
1703
- );
1704
- }
1705
- return toEntry(auth2);
1706
- }
1707
1656
  function vendorEntry(strategy, name) {
1708
- const vendor = vendors_default[name];
1657
+ const vendor = VENDORS[name];
1709
1658
  const KEY = name.toUpperCase();
1710
1659
  if (strategy !== "jwt" && strategy !== "cookie") {
1711
1660
  throw new Error(
@@ -1729,63 +1678,50 @@ function vendorEntry(strategy, name) {
1729
1678
  `${KEY}_AUDIENCE is not set. It should be ${vendor.audience}. One issuer serves many applications, all signed with the same keys, so without it a token minted for another one is accepted here.`
1730
1679
  );
1731
1680
  }
1732
- return entry2({
1681
+ return verifyEntry({
1733
1682
  issuer,
1734
1683
  audience,
1735
1684
  ...vendor.claim ? { audienceClaim: vendor.claim } : {},
1736
1685
  ...strategy === "cookie" ? { cookie: vendor.cookie } : {}
1737
1686
  });
1738
1687
  }
1739
- function toEntry(auth2) {
1740
- if (typeof auth2 === "string") {
1741
- const [strategy, name] = auth2.split(":");
1742
- if (!name) {
1743
- throw new Error(
1744
- `Invalid auth "${auth2}": the string form is "<strategy>:<name>", like "cookie:github" to log people in, or "jwt:clerk" to check a token a vendor issued.`
1745
- );
1746
- }
1747
- if (vendors_default[name]) return vendorEntry(strategy, name);
1748
- return entry({ strategy, providers: name });
1688
+
1689
+ // src/auth/parse.ts
1690
+ function parseAuth(auth2) {
1691
+ if (!auth2) return null;
1692
+ if (Array.isArray(auth2)) {
1693
+ throw new Error(
1694
+ "`auth` takes one method. For several login options, list them under `providers` instead: auth: { providers: ['github', 'google'], ... }."
1695
+ );
1696
+ }
1697
+ return toEntry(auth2);
1698
+ }
1699
+ function fromString(auth2) {
1700
+ const [strategy, name] = auth2.split(":");
1701
+ if (!name) {
1702
+ throw new Error(
1703
+ `Invalid auth "${auth2}": the string form is "<strategy>:<name>", like "cookie:github" to log people in, or "jwt:clerk" to check a token a vendor issued.`
1704
+ );
1749
1705
  }
1706
+ if (VENDORS[name]) return vendorEntry(strategy, name);
1707
+ return flowEntry({ strategy, providers: name });
1708
+ }
1709
+ function toEntry(auth2) {
1710
+ if (typeof auth2 === "string") return fromString(auth2);
1750
1711
  if (typeof auth2 === "function") {
1751
1712
  return { name: "function", user: async (ctx) => auth2(ctx) };
1752
1713
  }
1753
1714
  if (auth2 && typeof auth2 === "object") {
1754
- if ("issuer" in auth2) return entry2(auth2);
1755
- if ("providers" in auth2) return entry(auth2);
1756
- if ("handler" in auth2) {
1757
- const instance = auth2;
1758
- const path = (instance.path ?? "/api/auth").replace(/\/$/, "");
1759
- const raw = { parser: "stream" };
1760
- const forward = (ctx) => instance.handler(
1761
- new Request(ctx.url.href, {
1762
- method: ctx.method,
1763
- headers: ctx.headers,
1764
- body: ctx.body,
1765
- // Required by fetch whenever a body is a stream
1766
- ...ctx.body ? { duplex: "half" } : {}
1767
- })
1768
- );
1769
- return {
1770
- name: `instance:${path}`,
1771
- user: async (ctx) => instance.user?.(ctx),
1772
- routes: (app) => {
1773
- const wildcard = `${path}/*`;
1774
- app.get(wildcard, raw, forward);
1775
- app.post(wildcard, raw, forward);
1776
- app.put(wildcard, raw, forward);
1777
- app.patch(wildcard, raw, forward);
1778
- app.delete(wildcard, raw, forward);
1779
- }
1780
- };
1781
- }
1715
+ if ("issuer" in auth2) return verifyEntry(auth2);
1716
+ if ("providers" in auth2) return flowEntry(auth2);
1717
+ if ("handler" in auth2) return instanceEntry(auth2);
1782
1718
  }
1783
1719
  throw new Error(
1784
1720
  "Invalid `auth`: it takes a string, a function, `{ providers }`, `{ issuer, audience }`, a library instance, or an array of those."
1785
1721
  );
1786
1722
  }
1787
1723
 
1788
- // src/helpers/color.ts
1724
+ // src/boot/color.ts
1789
1725
  var map = {
1790
1726
  reset: 0,
1791
1727
  bright: 1,
@@ -1822,7 +1758,7 @@ function color(str, ...vals) {
1822
1758
  return color(str[0] + vals.map((v, i) => v + str[i + 1]).join(""));
1823
1759
  }
1824
1760
 
1825
- // src/helpers/logger.ts
1761
+ // src/boot/logger.ts
1826
1762
  var STATUS_TEXT = {
1827
1763
  200: "OK",
1828
1764
  201: "Created",
@@ -1847,17 +1783,6 @@ var STATUS_TEXT = {
1847
1783
  502: "Bad Gateway",
1848
1784
  503: "Service Unavailable"
1849
1785
  };
1850
- var UNITS2 = ["b", "kb", "mb", "gb", "tb"];
1851
- function formatBytes(bytes) {
1852
- if (!bytes || bytes < 0) return "0b";
1853
- const i = Math.min(
1854
- Math.floor(Math.log(bytes) / Math.log(1024)),
1855
- UNITS2.length - 1
1856
- );
1857
- const value = bytes / 1024 ** i;
1858
- const rounded = i === 0 ? Math.round(value) : Math.round(value * 10) / 10;
1859
- return `${rounded}${UNITS2[i]}`;
1860
- }
1861
1786
  var SCOPE_COLORS = {
1862
1787
  start: "green",
1863
1788
  api: "cyan"
@@ -1894,72 +1819,88 @@ function createLogger(level) {
1894
1819
  };
1895
1820
  }
1896
1821
 
1897
- // src/helpers/secrets.ts
1822
+ // src/boot/secrets.ts
1898
1823
  function resolveSecrets(option) {
1899
1824
  const given = option ?? globalThis.env.SECRETS?.split(",");
1900
- const list = (Array.isArray(given) ? given : [given]).map((one) => one?.trim()).filter(Boolean);
1825
+ const list = toArray(given).map((one) => one?.trim()).filter(Boolean);
1901
1826
  return list.length ? list : [`unsafe-${createId()}`];
1902
1827
  }
1903
1828
 
1904
- // src/helpers/security.ts
1905
- function resolveSecurity(security) {
1906
- const off = security === false;
1907
- const o = security && typeof security === "object" ? security : {};
1908
- const val = (v, def) => v === false ? null : v === true || v == null ? def : v;
1909
- const map2 = off ? {} : {
1910
- "x-frame-options": val(o.frameguard, "SAMEORIGIN"),
1911
- "x-content-type-options": o.noSniff === false ? null : "nosniff",
1912
- "referrer-policy": val(
1913
- o.referrerPolicy,
1914
- "strict-origin-when-cross-origin"
1915
- ),
1916
- "x-xss-protection": o.xssProtection === false ? null : "0",
1917
- // Opt-in: default off
1918
- "content-security-policy": val(o.csp, null),
1919
- "cross-origin-opener-policy": val(o.coop, null),
1920
- "cross-origin-resource-policy": val(o.corp, null),
1921
- "permissions-policy": o.permissionsPolicy ?? null
1922
- };
1923
- const headers2 = {};
1924
- for (const key in map2) {
1925
- const value = map2[key];
1926
- if (value) headers2[key] = value;
1927
- }
1928
- return {
1929
- trustProxy: o.trustProxy ?? true,
1930
- traversalProtection: off ? false : o.traversalProtection !== false,
1931
- // Cap on the bytes buffered per request (see bodyLimit). `false` (or
1932
- // turning security off entirely) resolves to Infinity, meaning no limit.
1933
- maxBody: off ? INF : resolveMax(o.maxBody),
1934
- headers: headers2,
1935
- hsts: off ? null : val(o.hsts, "max-age=15552000; includeSubDomains")
1936
- };
1937
- }
1938
- var CLIMBS = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
1939
- var ABSOLUTE = /^(?:[\\/]|[a-zA-Z]:)/;
1940
- function checkTraversal(params, ctx) {
1941
- if (!ctx.options.security?.traversalProtection) return;
1942
- for (const param in params) {
1943
- const value = params[param];
1944
- if (typeof value !== "string") continue;
1945
- if (CLIMBS.test(value) || ABSOLUTE.test(value)) {
1946
- throw errors_default.PATH_TRAVERSAL({ param, value });
1947
- }
1829
+ // src/errors/render.ts
1830
+ var DOCS = "https://server-js.com/documentation/errors";
1831
+ var wantsHtml = (ctx) => String(ctx?.headers?.accept || "").includes("text/html");
1832
+ var escapeHtml = (str) => String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1833
+ var safeCode = (code) => typeof code === "string" && /^[A-Za-z0-9_]{1,64}$/.test(code) ? code : null;
1834
+ var inline = (str) => escapeHtml(str).replace(/`([^`]+)`/g, "<code>$1</code>");
1835
+ function logLines(error) {
1836
+ const code = error?.code ? `${error.code}: ` : "";
1837
+ const hint = error?.hint ? `
1838
+ ${error.hint}` : "";
1839
+ const valid = safeCode(error?.code);
1840
+ const docs = valid ? `
1841
+ ${DOCS}#${valid.toLowerCase()}` : "";
1842
+ return `${code}${error?.message ?? error}${hint}${docs}`;
1843
+ }
1844
+ function devPage(error, ctx) {
1845
+ const status2 = Number(error?.status) || 500;
1846
+ const code = safeCode(error?.code);
1847
+ const link = code ? `${DOCS}#${code.toLowerCase()}` : null;
1848
+ const title = code ?? error?.name ?? "Error";
1849
+ const stack = error?.stack ? escapeHtml(String(error.stack)) : "";
1850
+ return `<!DOCTYPE html>
1851
+ <html lang="en"><head><meta charset="utf-8" />
1852
+ <title>${status2} ${escapeHtml(title)}</title>
1853
+ <style>
1854
+ :root { color-scheme: light dark; }
1855
+ body { margin: 0; padding: 3rem 1.5rem; font: 15px/1.6 ui-sans-serif, system-ui, sans-serif; }
1856
+ main { max-width: 46rem; margin: 0 auto; }
1857
+ .status { font-size: .8rem; letter-spacing: .08em; text-transform: uppercase; opacity: .6; }
1858
+ h1 { font-size: 1.5rem; }
1859
+ code { font-family: ui-monospace, monospace; font-size: .9em; background: color-mix(in srgb, currentColor 10%, transparent); padding: .1em .3em; border-radius: .2em; }
1860
+ pre { overflow-x: auto; font-size: .8rem; opacity: .7; background: light-dark(#eee, #1a1a1a); padding: 1rem; border-radius: .3rem; }
1861
+ footer { font-size: .8rem; opacity: .6; margin-top: 2rem; }
1862
+ </style></head>
1863
+ <body><main>
1864
+ <p class="status">${status2}${code ? ` &middot; ${escapeHtml(code)}` : ""} &middot; ${escapeHtml(ctx.method.toUpperCase())} ${escapeHtml(ctx.url.pathname)}</p>
1865
+ <h1>${escapeHtml(String(error?.message ?? error))}</h1>
1866
+ ${error?.hint ? `<p>${inline(error.hint)}</p>` : ""}
1867
+ ${link ? `<p><a href="${link}">${link}</a></p>` : ""}
1868
+ ${stack ? `<pre>${stack}</pre>` : ""}
1869
+ <footer>You are seeing this because the app is in development. In production this is a plain ${status2}.</footer>
1870
+ </main></body></html>`;
1871
+ }
1872
+ function defaultOnError(error, ctx) {
1873
+ const status2 = Number(error?.status) || 500;
1874
+ if (status2 >= 500) console.error(`[server:error] ${logLines(error)}`);
1875
+ if (env.NODE_ENV !== "production" && wantsHtml(ctx)) {
1876
+ return new Response(devPage(error, ctx), {
1877
+ status: status2,
1878
+ headers: {
1879
+ "content-type": "text/html; charset=utf-8",
1880
+ // The page renders a message, a stack and a path, none of which
1881
+ // it controls. Nothing may execute or be fetched, so an escaping
1882
+ // miss is inert rather than exploitable.
1883
+ "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'"
1884
+ }
1885
+ });
1948
1886
  }
1887
+ const body = status2 < 500 ? error?.message : "Server Error";
1888
+ return new Response(body || "Server Error", { status: status2 });
1949
1889
  }
1950
- function applySecurity(res, ctx) {
1951
- const security = ctx.options.security;
1952
- if (!security) return;
1953
- for (const key in security.headers) {
1954
- if (!res.headers.has(key)) res.headers.set(key, security.headers[key]);
1955
- }
1956
- if (security.hsts && ctx.platform.production && !res.headers.has("strict-transport-security")) {
1957
- res.headers.set("strict-transport-security", security.hsts);
1890
+
1891
+ // src/boot/config.ts
1892
+ var announced = false;
1893
+ function announceDevelopment() {
1894
+ if (announced || env.NODE_ENV === "production" || env.NODE_ENV === "test") {
1895
+ return;
1958
1896
  }
1897
+ announced = true;
1898
+ console.warn(
1899
+ "[server:app] Running in development mode. Set NODE_ENV=production when you deploy."
1900
+ );
1959
1901
  }
1960
-
1961
- // src/helpers/config.ts
1962
1902
  function config(options = {}) {
1903
+ announceDevelopment();
1963
1904
  const env2 = globalThis.env;
1964
1905
  const opts = options;
1965
1906
  if (typeof opts.body === "string") {
@@ -1974,6 +1915,12 @@ function config(options = {}) {
1974
1915
  );
1975
1916
  }
1976
1917
  }
1918
+ const sec = opts.security;
1919
+ if (sec && typeof sec === "object" && sec.maxBody !== void 0) {
1920
+ throw new Error(
1921
+ "The `security.maxBody` option is now `security.maxBodySize`, to sit alongside the `uploads` limits `maxFileSize` and `maxTotalSize`."
1922
+ );
1923
+ }
1977
1924
  if (opts.secret !== void 0) {
1978
1925
  throw new Error(
1979
1926
  "The `secret` option is now `secrets`, and takes one key or several: `secrets: [current, previous]` signs with the first and verifies with any, so rotating a key no longer signs everyone out."
@@ -2039,7 +1986,6 @@ function config(options = {}) {
2039
1986
  const publicDir = options.public || env2.PUBLIC;
2040
1987
  settings.public = publicDir ? bucket(publicDir) : null;
2041
1988
  settings.uploads = resolveUploads(options.uploads);
2042
- const production = env2.NODE_ENV === "production";
2043
1989
  if (options.auth || env2.AUTH) {
2044
1990
  settings.auth = parseAuth(
2045
1991
  options.auth || env2.AUTH || null
@@ -2056,11 +2002,7 @@ function config(options = {}) {
2056
2002
  else if (typeof o === "string") settings.openapi = { path: o };
2057
2003
  else settings.openapi = { path: "/openapi.json", ...o };
2058
2004
  }
2059
- settings.onError = options.onError || ((error) => {
2060
- return new Response(error.message || "Server Error", {
2061
- status: error.status || 500
2062
- });
2063
- });
2005
+ settings.onError = options.onError || defaultOnError;
2064
2006
  settings.onResponse = options.onResponse;
2065
2007
  const loc = (v) => typeof v === "string" ? v : "enabled";
2066
2008
  if (settings.auth) log.message("auth", `${settings.auth.name} enabled`);
@@ -2075,70 +2017,17 @@ function config(options = {}) {
2075
2017
  return settings;
2076
2018
  }
2077
2019
 
2078
- // src/helpers/cors.ts
2079
- var localhost = /^https?:\/\/localhost(:\d+)?$/;
2080
- function cors(config2, origin = "") {
2081
- origin = origin?.toLowerCase();
2082
- if (config2 === true) return origin || null;
2083
- if (config2 === "*") return "*";
2084
- if (!origin) return null;
2085
- if (localhost.test(origin)) return origin;
2086
- const arr = Array.isArray(config2) ? config2 : typeof config2 === "string" ? config2.split(/\s*,\s*/g) : [];
2087
- if (arr.includes(origin)) return origin;
2088
- console.warn(`CORS: Origin "${origin}" not allowed. Allowed "${config2}"`);
2089
- return null;
2090
- }
2091
- function applyCors(res, ctx) {
2092
- const settings = ctx.options.cors;
2093
- if (!settings) return;
2094
- const requestOrigin = ctx.headers.origin || "";
2095
- let origin = cors(settings.origin, requestOrigin);
2096
- if (!origin) return;
2097
- if (settings.credentials && origin === "*") {
2098
- if (!requestOrigin) return;
2099
- origin = requestOrigin.toLowerCase();
2100
- }
2101
- res.headers.set("Access-Control-Allow-Origin", origin);
2102
- res.headers.set("Access-Control-Allow-Methods", settings.methods);
2103
- res.headers.set("Access-Control-Allow-Headers", settings.headers);
2104
- if (settings.credentials) {
2105
- res.headers.set("Access-Control-Allow-Credentials", "true");
2106
- }
2107
- if (origin !== "*") res.headers.append("Vary", "Origin");
2108
- if (ctx.method === "options") {
2109
- res.headers.set("Access-Control-Max-Age", "86400");
2110
- }
2111
- }
2112
-
2113
- // src/helpers/forwarded.ts
2114
- var first2 = (value) => {
2115
- const one = Array.isArray(value) ? value[0] : value;
2116
- return one?.split(",")[0].trim() || void 0;
2117
- };
2118
- function forwarded(url, headers2, trustProxy) {
2119
- if (!trustProxy) return;
2120
- const proto = first2(headers2["x-forwarded-proto"]);
2121
- if (proto === "http" || proto === "https") url.protocol = `${proto}:`;
2122
- const host = first2(headers2["x-forwarded-host"]);
2123
- const port = first2(headers2["x-forwarded-port"]);
2124
- if (host?.includes(":")) {
2125
- url.host = host;
2126
- } else if (host) {
2127
- url.hostname = host;
2128
- url.port = port ?? "";
2129
- } else if (port) {
2130
- url.port = port;
2131
- }
2132
- }
2133
-
2134
- // src/helpers/createWebsocket.ts
2020
+ // src/ws/createWebsocket.ts
2135
2021
  function createWebsocket(sockets, handlers) {
2136
2022
  const run2 = (event, socket, body) => {
2137
2023
  const routes = handlers.socket?.filter((r2) => r2.path === event) ?? [];
2138
2024
  const user = socket.user ?? socket.data?.user;
2025
+ const ctx = { socket, sockets, body, user };
2139
2026
  for (const route of routes) {
2140
2027
  for (const fn of route.fns) {
2141
- fn({ socket, sockets, body, user });
2028
+ Promise.resolve(fn(ctx)).catch((error) => {
2029
+ console.error(`[server:socket] ${event} handler failed:`, error);
2030
+ });
2142
2031
  }
2143
2032
  }
2144
2033
  };
@@ -2155,23 +2044,7 @@ function createWebsocket(sockets, handlers) {
2155
2044
  };
2156
2045
  }
2157
2046
 
2158
- // src/helpers/define.ts
2159
- function define(obj, key, cb) {
2160
- Object.defineProperty(obj, key, {
2161
- configurable: true,
2162
- get() {
2163
- const value = cb(obj);
2164
- Object.defineProperty(obj, key, {
2165
- configurable: true,
2166
- writable: true,
2167
- value
2168
- });
2169
- return obj[key];
2170
- }
2171
- });
2172
- }
2173
-
2174
- // src/helpers/getMachine.ts
2047
+ // src/boot/getMachine.ts
2175
2048
  function getProvider() {
2176
2049
  if (typeof globalThis.Netlify !== "undefined") return "netlify";
2177
2050
  return null;
@@ -2195,310 +2068,40 @@ function getMachine() {
2195
2068
  };
2196
2069
  }
2197
2070
 
2198
- // src/parseResponse.ts
2199
- async function parseResponse(out, ctx) {
2200
- if (!out && typeof out !== "string") return null;
2201
- if (typeof out === "function") {
2202
- out = await out(ctx);
2203
- if (!out && typeof out !== "string") return null;
2204
- }
2205
- if (typeof out === "number") {
2206
- out = new Response(null, { status: out });
2207
- }
2208
- if (!(out instanceof Response) || out.url) {
2209
- out = await send(out);
2210
- }
2211
- applyCors(out, ctx);
2212
- applySecurity(out, ctx);
2213
- out = await applyCache(out, ctx);
2214
- if (ctx.clearCookie) {
2215
- out.headers.append(
2216
- "set-cookie",
2217
- `${ctx.clearCookie}=; Path=/; Max-Age=0; HttpOnly`
2218
- );
2219
- }
2220
- if (ctx.time?.times?.length > 1) {
2221
- out.headers.set("Server-Timing", ctx.time.headers());
2071
+ // src/auth/index.ts
2072
+ function auth(app) {
2073
+ const entry = app.settings.auth;
2074
+ app.use(async function middle(ctx) {
2075
+ ctx.user = await entry.user(ctx);
2076
+ });
2077
+ if (entry.routes) app.use(entry.routes());
2078
+ }
2079
+
2080
+ // src/http/parseRange.ts
2081
+ function parseRange(header, size) {
2082
+ if (!header) return null;
2083
+ const match = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
2084
+ if (!match) return null;
2085
+ const [, rawStart, rawEnd] = match;
2086
+ if (rawStart === "" && rawEnd === "") return null;
2087
+ let start;
2088
+ let end;
2089
+ if (rawStart === "") {
2090
+ const n = Number(rawEnd);
2091
+ if (n <= 0) return "unsatisfiable";
2092
+ start = Math.max(0, size - n);
2093
+ end = size - 1;
2094
+ } else {
2095
+ start = Number(rawStart);
2096
+ end = rawEnd === "" ? size - 1 : Number(rawEnd);
2222
2097
  }
2223
- return out;
2224
- }
2225
-
2226
- // src/pathPattern.ts
2227
- function pathPattern(pattern, path) {
2228
- if (pattern === "*" && path === "/") return {};
2229
- pattern = `/${pattern.replace(/^\//, "")}`;
2230
- pattern = pattern.replace(/\/$/, "") || "/";
2231
- path = path.replace(/\/$/, "") || "/";
2232
- if (pattern === path) return {};
2233
- const params = {};
2234
- const pathParts = path.split("/").slice(1).map((u) => decodeURIComponent(u));
2235
- const pattParts = pattern.split("/").slice(1);
2236
- let allSame = true;
2237
- for (let i = 0; i < Math.max(pathParts.length, pattParts.length); i++) {
2238
- const patt = pattParts[i] || "";
2239
- const part = pathParts[i] || "";
2240
- const last = pattParts[pattParts.length - 1];
2241
- const key = patt.replace(/^:/, "").replace(/\?$/, "").replace(/\(\w*\)/, "");
2242
- if (patt === part) continue;
2243
- if (patt.endsWith("?") && !part) continue;
2244
- if (patt.startsWith(":")) {
2245
- params[key] = part;
2246
- if (/\(\w*\)/.test(patt)) {
2247
- if (patt.includes("(number)")) {
2248
- const value = Number(part);
2249
- params[key] = Number.isNaN(value) ? void 0 : value;
2250
- }
2251
- if (patt.includes("(date)")) {
2252
- const value = new Date(part);
2253
- params[key] = Number.isNaN(value.getTime()) ? void 0 : value;
2254
- }
2255
- }
2256
- continue;
2257
- }
2258
- if (!patt && last === "*" && part || patt === "*" && part) {
2259
- params["*"] = params["*"] || [];
2260
- params["*"].push(part);
2261
- continue;
2262
- }
2263
- allSame = false;
2264
- }
2265
- if (allSame) return params;
2266
- return null;
2267
- }
2268
-
2269
- // src/errors/ValidationError.ts
2270
- var ValidationError = class extends StatusError {
2271
- source;
2272
- issues;
2273
- constructor(source, issues) {
2274
- if (source === "response") {
2275
- super("Server Error", 500);
2276
- } else {
2277
- super(`Invalid request ${source}`, 422);
2278
- }
2279
- this.source = source;
2280
- this.issues = issues;
2281
- }
2282
- };
2283
-
2284
- // src/helpers/validate.ts
2285
- async function run(schema, value, source) {
2286
- const result = await schema["~standard"].validate(value);
2287
- if (result.issues) throw new ValidationError(source, result.issues);
2288
- return result.value;
2289
- }
2290
- async function validateRequest(ctx, options) {
2291
- if (options.body) {
2292
- ctx.body = await run(options.body, ctx.body ?? {}, "body");
2293
- }
2294
- if (options.query) {
2295
- const query = await run(options.query, ctx.url.query || {}, "query");
2296
- replace2(ctx.url.query, query);
2297
- }
2298
- if (options.params) {
2299
- const params = await run(options.params, ctx.url.params || {}, "params");
2300
- replace2(ctx.url.params, params);
2301
- }
2302
- }
2303
- async function validateResponse(out, options) {
2304
- if (!options.response) return out;
2305
- if (out?.constructor !== Object && !Array.isArray(out)) return out;
2306
- return await run(options.response, out, "response");
2307
- }
2308
- function replace2(target2, values) {
2309
- for (const key of Object.keys(target2)) delete target2[key];
2310
- Object.assign(target2, values);
2311
- }
2312
-
2313
- // src/helpers/handleRequest.ts
2314
- async function handleRequest(app, ctx) {
2315
- let res = await getResponse(app, ctx);
2316
- if (res && ctx.options.onResponse) {
2317
- const replaced = await ctx.options.onResponse(res, ctx);
2318
- if (replaced) res = replaced;
2319
- }
2320
- if (res) ctx.options.log.request(ctx, res);
2321
- if (res?.body && ctx.method === "head") {
2322
- res.body.cancel().catch(() => {
2323
- });
2324
- res = new Response(null, { status: res.status, headers: res.headers });
2325
- }
2326
- return res;
2327
- }
2328
- async function getResponse(app, ctx) {
2329
- try {
2330
- let matched = false;
2331
- const routes = ctx.method === "head" ? [...app.handlers.head, ...app.handlers.get] : app.handlers[ctx.method];
2332
- for (const route of routes) {
2333
- const params = pathPattern(route.path, ctx.url.pathname || "/");
2334
- if (!params) continue;
2335
- matched = true;
2336
- define(ctx.url, "params", () => params);
2337
- if (Object.keys(route.options).length) {
2338
- ctx.options = { ...app.settings, ...route.options };
2339
- }
2340
- checkTraversal(params, ctx);
2341
- ctx.body = await resolveBody(
2342
- ctx,
2343
- ctx.options.parser,
2344
- ctx.options.security.maxBody
2345
- );
2346
- await validateRequest(ctx, route.options);
2347
- for (const cb of route.fns) {
2348
- const res = await cb(ctx);
2349
- const out = await parseResponse(
2350
- await validateResponse(res, route.options),
2351
- ctx
2352
- );
2353
- if (out) return out;
2354
- }
2355
- break;
2356
- }
2357
- if (!matched) {
2358
- ctx.body = await resolveBody(
2359
- ctx,
2360
- ctx.options.parser,
2361
- ctx.options.security.maxBody
2362
- );
2363
- for (const mw of app.middleware) {
2364
- const out = await parseResponse(await mw(ctx), ctx);
2365
- if (out) return out;
2366
- }
2367
- }
2368
- if (ctx.platform.provider === "netlify") return;
2369
- throw new ServerError_default("NOT_FOUND", 404, "Not Found");
2370
- } catch (error) {
2371
- const res = await ctx.options.onError(error, ctx);
2372
- applyCors(res, ctx);
2373
- applySecurity(res, ctx);
2374
- return res;
2375
- }
2376
- }
2377
-
2378
- // src/helpers/iteratorAsyncToReadable.ts
2379
- function iteratorAsyncToReadable(asyncGenerator) {
2380
- let cancelled = false;
2381
- return new ReadableStream({
2382
- async pull(controller) {
2383
- try {
2384
- const { value, done } = await asyncGenerator.next();
2385
- if (cancelled) return;
2386
- if (done) {
2387
- controller.close();
2388
- return;
2389
- }
2390
- controller.enqueue(new TextEncoder().encode(value));
2391
- } catch (err) {
2392
- console.error("Stream error:", err);
2393
- controller.error(err);
2394
- }
2395
- },
2396
- // Return the generator so its `finally {}` runs and releases resources.
2397
- async cancel(reason) {
2398
- cancelled = true;
2399
- await asyncGenerator.return?.(reason);
2400
- }
2401
- });
2402
- }
2403
-
2404
- // src/helpers/iteratorToReadable.ts
2405
- function iteratorToReadable(generator) {
2406
- return new ReadableStream({
2407
- async start(controller) {
2408
- for await (const chunk of generator) {
2409
- controller.enqueue(chunk);
2410
- }
2411
- controller.close();
2412
- }
2413
- });
2414
- }
2415
-
2416
- // src/helpers/parseCookies.ts
2417
- function parseCookies(cookies2) {
2418
- if (!cookies2) return {};
2419
- const cookieStr = Array.isArray(cookies2) ? cookies2[0] : cookies2;
2420
- if (!cookieStr) return {};
2421
- return Object.fromEntries(
2422
- cookieStr.split(/;\s*/).map((part) => {
2423
- const [key, ...rest] = part.split("=");
2424
- const value = rest.join("=");
2425
- try {
2426
- return [key, decodeURIComponent(value)];
2427
- } catch {
2428
- return [key, value];
2429
- }
2430
- })
2431
- );
2432
- }
2433
-
2434
- // src/helpers/parseHeaders.ts
2435
- var parseHeaders_default = (raw) => {
2436
- const headers2 = {};
2437
- raw.forEach((value, originalKey) => {
2438
- const key = originalKey.toLowerCase();
2439
- if (headers2[key]) {
2440
- if (!Array.isArray(headers2[key])) {
2441
- headers2[key] = [headers2[key]];
2442
- }
2443
- headers2[key].push(value);
2444
- } else {
2445
- headers2[key] = value;
2446
- }
2447
- });
2448
- return headers2;
2449
- };
2450
-
2451
- // src/helpers/toWeb.ts
2452
- function toWeb(nodeStream) {
2453
- if (typeof ReadableStream === "undefined") {
2454
- throw new Error("Environment not supported, please report this as a bug");
2455
- }
2456
- return new ReadableStream({
2457
- start(controller) {
2458
- nodeStream.on("data", (chunk) => controller.enqueue(chunk));
2459
- nodeStream.on("end", () => controller.close());
2460
- nodeStream.on("error", (err) => controller.error(err));
2461
- },
2462
- cancel() {
2463
- nodeStream.destroy();
2464
- }
2465
- });
2466
- }
2467
-
2468
- // src/auth/index.ts
2469
- function auth(app) {
2470
- const entry3 = app.settings.auth;
2471
- app.use(async function middle(ctx) {
2472
- ctx.user = await entry3.user(ctx);
2473
- });
2474
- entry3.routes?.(app);
2475
- }
2476
-
2477
- // src/helpers/parseRange.ts
2478
- function parseRange(header, size) {
2479
- if (!header) return null;
2480
- const match = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
2481
- if (!match) return null;
2482
- const [, rawStart, rawEnd] = match;
2483
- if (rawStart === "" && rawEnd === "") return null;
2484
- let start;
2485
- let end;
2486
- if (rawStart === "") {
2487
- const n = Number(rawEnd);
2488
- if (n <= 0) return "unsatisfiable";
2489
- start = Math.max(0, size - n);
2490
- end = size - 1;
2491
- } else {
2492
- start = Number(rawStart);
2493
- end = rawEnd === "" ? size - 1 : Number(rawEnd);
2494
- }
2495
- if (!Number.isFinite(start) || !Number.isFinite(end)) return null;
2496
- if (size === 0 || start > end || start >= size) return "unsatisfiable";
2497
- return { start, end: Math.min(end, size - 1) };
2098
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return null;
2099
+ if (size === 0 || start > end || start >= size) return "unsatisfiable";
2100
+ return { start, end: Math.min(end, size - 1) };
2498
2101
  }
2499
2102
 
2500
2103
  // src/middle/assets.ts
2501
- var CACHE_CONTROL = "public, max-age=3600";
2104
+ var DEFAULT_CACHE = "public, max-age=3600";
2502
2105
  async function assets(ctx) {
2503
2106
  if (!ctx.options.public) return;
2504
2107
  if (ctx.method !== "get" && ctx.method !== "head") return;
@@ -2510,8 +2113,10 @@ async function assets(ctx) {
2510
2113
  const meta2 = info ? await info() : null;
2511
2114
  if (info ? !meta2 : !await file2.exists()) return;
2512
2115
  const ext = ctx.url.pathname.split(".").pop()?.toLowerCase();
2513
- const ctype = ext && mimes_default[ext] || meta2?.type || ext;
2514
- const headers2 = { "cache-control": CACHE_CONTROL };
2116
+ const ctype = mimeOf(ctx.url.pathname) || meta2?.type || ext;
2117
+ const headers2 = {
2118
+ "cache-control": resolveCache(ctx.options.cache) ?? DEFAULT_CACHE
2119
+ };
2515
2120
  let tag;
2516
2121
  if (meta2) {
2517
2122
  const stamp = meta2.modified ? meta2.modified.getTime() : 0;
@@ -2577,30 +2182,10 @@ async function toJsonSchema(schema) {
2577
2182
  }
2578
2183
  } catch {
2579
2184
  }
2580
- return zodToSchema(schema);
2581
- }
2582
- function zodToSchema(schema) {
2583
- const type2 = schema?.def?.type || "string";
2584
- if (type2 === "object") {
2585
- const shape = schema.def.shape;
2586
- const properties = {};
2587
- const req = [];
2588
- for (const key in shape) {
2589
- const field = shape[key];
2590
- properties[key] = zodToSchema(field);
2591
- if (!field.isOptional() && !field.isNullable()) {
2592
- req.push(key);
2593
- }
2594
- }
2595
- const required = req.length ? req : void 0;
2596
- return { type: type2, properties, required };
2597
- }
2598
- if (type2 === "array") {
2599
- return { type: type2, items: zodToSchema(schema.def.element) };
2600
- }
2601
- return { type: type2 };
2185
+ return void 0;
2602
2186
  }
2603
- var pkgProm = fsp.readFile("package.json", "utf-8").then((data) => JSON.parse(data)).catch(() => ({}));
2187
+ var pkgProm;
2188
+ var getPkg = () => pkgProm ??= fsp.readFile("package.json", "utf-8").then((data) => JSON.parse(data)).catch(() => ({}));
2604
2189
  var generateOpenApiPaths = async (handlers, specPath) => {
2605
2190
  const paths = {};
2606
2191
  for (const [method, routes] of Object.entries(handlers)) {
@@ -2619,14 +2204,18 @@ var generateOpenApiPaths = async (handlers, specPath) => {
2619
2204
  let requestBody;
2620
2205
  if (meta2?.body) {
2621
2206
  const schema = await toJsonSchema(meta2.body);
2622
- requestBody = { content: { "application/json": { schema } } };
2207
+ if (schema) {
2208
+ requestBody = { content: { "application/json": { schema } } };
2209
+ }
2623
2210
  }
2624
2211
  let responses;
2625
2212
  if (meta2?.response) {
2626
2213
  const schema = await toJsonSchema(meta2.response);
2627
- responses = {
2628
- 200: { description: "OK", content: { "application/json": { schema } } }
2629
- };
2214
+ if (schema) {
2215
+ responses = {
2216
+ 200: { description: "OK", content: { "application/json": { schema } } }
2217
+ };
2218
+ }
2630
2219
  }
2631
2220
  const parameters = [];
2632
2221
  const matched = Array.from(path.matchAll(/:[\w()]+/gi));
@@ -2641,7 +2230,7 @@ var generateOpenApiPaths = async (handlers, specPath) => {
2641
2230
  });
2642
2231
  if (meta2?.query) {
2643
2232
  const schema = await toJsonSchema(meta2.query);
2644
- for (const [name, prop] of Object.entries(schema.properties ?? {})) {
2233
+ for (const [name, prop] of Object.entries(schema?.properties ?? {})) {
2645
2234
  parameters.push({
2646
2235
  name,
2647
2236
  in: "query",
@@ -2663,7 +2252,7 @@ var generateOpenApiPaths = async (handlers, specPath) => {
2663
2252
  return paths;
2664
2253
  };
2665
2254
  var openapi_default = async (ctx) => {
2666
- const pkg = await pkgProm;
2255
+ const pkg = await getPkg();
2667
2256
  const { title, description, version } = ctx.options.openapi ?? {};
2668
2257
  const domain = pkg.homepage || ctx.url.origin;
2669
2258
  return {
@@ -2681,43 +2270,743 @@ var openapi_default = async (ctx) => {
2681
2270
  };
2682
2271
  };
2683
2272
 
2684
- // src/middle/preflight.ts
2685
- function preflight(ctx) {
2686
- if (ctx.method !== "options") return;
2687
- if (!ctx.headers["access-control-request-method"]) return;
2688
- const handled = ctx.app.handlers.options.some(
2689
- (route) => pathPattern(route.path, ctx.url.pathname)
2273
+ // src/pipeline/pathPattern.ts
2274
+ function pathPattern(pattern, path) {
2275
+ if (pattern === "*" && path === "/") return {};
2276
+ pattern = `/${pattern.replace(/^\//, "")}`;
2277
+ pattern = pattern.replace(/\/$/, "") || "/";
2278
+ path = path.replace(/\/$/, "") || "/";
2279
+ if (pattern === path) return {};
2280
+ const params = {};
2281
+ const pathParts = path.split("/").slice(1).map((u) => decodeURIComponent(u));
2282
+ const pattParts = pattern.split("/").slice(1);
2283
+ let allSame = true;
2284
+ for (let i = 0; i < Math.max(pathParts.length, pattParts.length); i++) {
2285
+ const patt = pattParts[i] || "";
2286
+ const part = pathParts[i] || "";
2287
+ const last = pattParts[pattParts.length - 1];
2288
+ const key = patt.replace(/^:/, "").replace(/\?$/, "").replace(/\(\w*\)/, "");
2289
+ if (patt === part) continue;
2290
+ if (patt.endsWith("?") && !part) continue;
2291
+ if (patt.startsWith(":")) {
2292
+ params[key] = part;
2293
+ if (/\(\w*\)/.test(patt)) {
2294
+ if (patt.includes("(number)")) {
2295
+ const value = Number(part);
2296
+ params[key] = Number.isNaN(value) ? void 0 : value;
2297
+ }
2298
+ if (patt.includes("(date)")) {
2299
+ const value = new Date(part);
2300
+ params[key] = Number.isNaN(value.getTime()) ? void 0 : value;
2301
+ }
2302
+ }
2303
+ continue;
2304
+ }
2305
+ if (!patt && last === "*" && part || patt === "*" && part) {
2306
+ params["*"] = params["*"] || [];
2307
+ params["*"].push(part);
2308
+ continue;
2309
+ }
2310
+ allSame = false;
2311
+ }
2312
+ if (allSame) return params;
2313
+ return null;
2314
+ }
2315
+
2316
+ // src/middle/preflight.ts
2317
+ function preflight(ctx) {
2318
+ if (ctx.method !== "options") return;
2319
+ if (!ctx.headers["access-control-request-method"]) return;
2320
+ const handled = ctx.app.handlers.options.some(
2321
+ (route) => pathPattern(route.path, ctx.url.pathname)
2322
+ );
2323
+ if (handled) return;
2324
+ return 204;
2325
+ }
2326
+
2327
+ // src/middle/timer.ts
2328
+ var createTime = () => {
2329
+ const times2 = [["init", performance.now()]];
2330
+ const time = (name) => times2.push([name, performance.now()]);
2331
+ time.times = times2;
2332
+ time.headers = () => {
2333
+ const r2 = (t) => Math.round(t);
2334
+ const times3 = time.times;
2335
+ const timing = times3.slice(1).map(([name, time2], i) => `${name};dur=${r2(time2 - times3[i][1])}`).join(", ");
2336
+ return timing;
2337
+ };
2338
+ return time;
2339
+ };
2340
+ function timer(ctx) {
2341
+ ctx.time = createTime();
2342
+ }
2343
+
2344
+ // src/auth/socketUser.ts
2345
+ async function socketUser(app, headers2, cookies2) {
2346
+ if (!app.settings.auth) return void 0;
2347
+ const ctx = {
2348
+ options: app.settings,
2349
+ headers: headers2,
2350
+ cookies: cookies2,
2351
+ platform: app.platform,
2352
+ app
2353
+ };
2354
+ return app.settings.auth.user(ctx);
2355
+ }
2356
+
2357
+ // src/http/cors.ts
2358
+ var localhost = /^https?:\/\/localhost(:\d+)?$/;
2359
+ function cors(config2, origin = "") {
2360
+ origin = origin?.toLowerCase();
2361
+ if (config2 === true) return origin || null;
2362
+ if (config2 === "*") return "*";
2363
+ if (!origin) return null;
2364
+ if (localhost.test(origin)) return origin;
2365
+ const arr = typeof config2 === "string" ? config2.split(/\s*,\s*/g) : [];
2366
+ if (arr.includes(origin)) return origin;
2367
+ console.warn(`CORS: Origin "${origin}" not allowed. Allowed "${config2}"`);
2368
+ return null;
2369
+ }
2370
+ function applyCors(res, ctx) {
2371
+ const settings = ctx.options.cors;
2372
+ if (!settings) return;
2373
+ const requestOrigin = ctx.headers.origin || "";
2374
+ let origin = cors(settings.origin, requestOrigin);
2375
+ if (!origin) return;
2376
+ if (settings.credentials && origin === "*") {
2377
+ if (!requestOrigin) return;
2378
+ origin = requestOrigin.toLowerCase();
2379
+ }
2380
+ res.headers.set("Access-Control-Allow-Origin", origin);
2381
+ res.headers.set("Access-Control-Allow-Methods", settings.methods);
2382
+ res.headers.set("Access-Control-Allow-Headers", settings.headers);
2383
+ if (settings.credentials) {
2384
+ res.headers.set("Access-Control-Allow-Credentials", "true");
2385
+ }
2386
+ if (origin !== "*") res.headers.append("Vary", "Origin");
2387
+ if (ctx.method === "options") {
2388
+ res.headers.set("Access-Control-Max-Age", "86400");
2389
+ }
2390
+ }
2391
+
2392
+ // src/pipeline/parseResponse.ts
2393
+ async function parseResponse(out, ctx) {
2394
+ if (!out && typeof out !== "string") return null;
2395
+ if (typeof out === "function") {
2396
+ out = await out(ctx);
2397
+ if (!out && typeof out !== "string") return null;
2398
+ }
2399
+ if (typeof out === "number") {
2400
+ return new Response(null, { status: out });
2401
+ }
2402
+ if (!(out instanceof Response) || out.url) {
2403
+ out = await send(out);
2404
+ }
2405
+ return out;
2406
+ }
2407
+ async function finalize(out, ctx) {
2408
+ applyCors(out, ctx);
2409
+ applySecurity(out, ctx);
2410
+ out = await applyCache(out, ctx);
2411
+ const stale = toClear(ctx);
2412
+ if (stale) {
2413
+ out.headers.append("set-cookie", clearCookie(stale));
2414
+ }
2415
+ if (ctx.time?.times?.length > 1) {
2416
+ out.headers.set("Server-Timing", ctx.time.headers());
2417
+ }
2418
+ return out;
2419
+ }
2420
+
2421
+ // src/body/bodyParts.ts
2422
+ var asIterable = (s) => s;
2423
+ function getMatching(string, regex) {
2424
+ const matches2 = string.match(regex);
2425
+ return matches2?.[1] ?? "";
2426
+ }
2427
+ function isProbablyText(buffer) {
2428
+ for (let i = 0; i < Math.min(buffer.length, 512); i++) {
2429
+ const byte = buffer[i];
2430
+ if (byte === 0) return false;
2431
+ if (byte < 7 || byte > 13 && byte < 32) return false;
2432
+ }
2433
+ return true;
2434
+ }
2435
+ var extByMime = {};
2436
+ for (const ext in mimes_default) extByMime[mimes_default[ext]] = ext;
2437
+ function addField(body, name, value) {
2438
+ if (body[name] === void 0) {
2439
+ body[name] = value;
2440
+ return;
2441
+ }
2442
+ if (!Array.isArray(body[name])) body[name] = [body[name]];
2443
+ body[name].push(value);
2444
+ }
2445
+ function makeFilePart(name, filename, declared, bucket2, limits, budget) {
2446
+ return {
2447
+ kind: "file",
2448
+ name,
2449
+ filename,
2450
+ declared,
2451
+ bucket: bucket2,
2452
+ limits,
2453
+ budget,
2454
+ head: [],
2455
+ headSize: 0,
2456
+ opened: null,
2457
+ size: 0
2458
+ };
2459
+ }
2460
+ function startPart(headerStr, bucket2, limits, budget) {
2461
+ const name = getMatching(headerStr, /name="(.+?)"/).trim().replace(/\[\]$/, "");
2462
+ if (!name) return { kind: "skip" };
2463
+ const filename = getMatching(headerStr, /filename="(.+?)"/).trim();
2464
+ if (!filename) return { kind: "text", name, chunks: [] };
2465
+ const type2 = getMatching(headerStr, /Content-Type:\s*([^\r\n]+)/i).trim() || "application/octet-stream";
2466
+ if (bucket2 === false) return { kind: "drop" };
2467
+ if (!bucket2) throw errors_default.UPLOAD_NOT_CONFIGURED({ name: filename });
2468
+ budget.files++;
2469
+ const { maxFiles } = limits;
2470
+ if (maxFiles != null && budget.files > maxFiles) {
2471
+ throw errors_default.UPLOAD_TOO_MANY_FILES({ limit: String(maxFiles) });
2472
+ }
2473
+ return makeFilePart(name, filename, type2, bucket2, limits, budget);
2474
+ }
2475
+ async function abortFile(part, error) {
2476
+ if (part.opened) {
2477
+ try {
2478
+ part.opened.controller.error(error);
2479
+ await part.opened.write.catch(() => {
2480
+ });
2481
+ } catch {
2482
+ }
2483
+ await part.opened.file.remove().catch(() => {
2484
+ });
2485
+ }
2486
+ throw error;
2487
+ }
2488
+ async function checkSize(part, added) {
2489
+ part.budget.used += added;
2490
+ const { maxFileSize, maxTotalSize } = part.limits;
2491
+ if (maxFileSize != null && part.size > parseBytes(maxFileSize)) {
2492
+ await abortFile(
2493
+ part,
2494
+ errors_default.UPLOAD_TOO_LARGE({
2495
+ name: part.filename,
2496
+ size: String(part.size),
2497
+ limit: String(maxFileSize)
2498
+ })
2499
+ );
2500
+ }
2501
+ if (maxTotalSize != null && part.budget.used > parseBytes(maxTotalSize)) {
2502
+ await abortFile(
2503
+ part,
2504
+ errors_default.UPLOAD_TOO_LARGE({
2505
+ name: part.filename,
2506
+ size: String(part.budget.used),
2507
+ limit: `${maxTotalSize} for the whole request`
2508
+ })
2509
+ );
2510
+ }
2511
+ }
2512
+ function openFile(part) {
2513
+ const head = Buffer.concat(part.head);
2514
+ const sniffed = sniff(head);
2515
+ const type2 = resolveType(sniffed, part.declared);
2516
+ validateFile(part.filename, type2, part.limits, sniffed);
2517
+ const ext = sniffed ? extByMime[type2] : void 0;
2518
+ const id = `${createId()}${ext ? `.${ext}` : ""}`;
2519
+ let controller;
2520
+ const readable = new ReadableStream({
2521
+ start(c) {
2522
+ controller = c;
2523
+ }
2524
+ });
2525
+ const file2 = part.bucket.file(id);
2526
+ part.opened = { type: type2, file: file2, controller, write: file2.write(readable, { type: type2 }) };
2527
+ }
2528
+ async function feedPart(part, data) {
2529
+ if (data.length === 0) return;
2530
+ if (part.kind === "text") {
2531
+ part.chunks.push(data);
2532
+ return;
2533
+ }
2534
+ if (part.kind !== "file") return;
2535
+ if (!part.opened) {
2536
+ part.head.push(data);
2537
+ part.headSize += data.length;
2538
+ if (part.headSize < HEAD_SIZE) return;
2539
+ openFile(part);
2540
+ const head = Buffer.concat(part.head);
2541
+ part.opened.controller.enqueue(head);
2542
+ part.size += head.length;
2543
+ await checkSize(part, head.length);
2544
+ return;
2545
+ }
2546
+ part.opened.controller.enqueue(data);
2547
+ part.size += data.length;
2548
+ await checkSize(part, data.length);
2549
+ }
2550
+ async function endPart(part, body) {
2551
+ if (part.kind === "text") {
2552
+ const buf = Buffer.concat(part.chunks);
2553
+ const value = isProbablyText(buf) ? buf.toString("utf-8").trim() : buf;
2554
+ addField(body, part.name, value);
2555
+ return;
2556
+ }
2557
+ if (part.kind !== "file") return;
2558
+ if (!part.opened) {
2559
+ openFile(part);
2560
+ const head = Buffer.concat(part.head);
2561
+ if (head.length) {
2562
+ part.opened.controller.enqueue(head);
2563
+ part.size += head.length;
2564
+ await checkSize(part, head.length);
2565
+ }
2566
+ }
2567
+ const opened = part.opened;
2568
+ opened.controller.close();
2569
+ await opened.write;
2570
+ const { minSize } = part.limits;
2571
+ if (minSize != null && part.size < parseBytes(minSize)) {
2572
+ await opened.file.remove().catch(() => {
2573
+ });
2574
+ throw errors_default.UPLOAD_TOO_SMALL({
2575
+ name: part.filename,
2576
+ size: String(part.size),
2577
+ limit: String(minSize)
2578
+ });
2579
+ }
2580
+ addField(body, part.name, {
2581
+ name: part.filename,
2582
+ path: opened.file.path,
2583
+ type: opened.type,
2584
+ size: part.size
2585
+ });
2586
+ }
2587
+
2588
+ // src/body/multipart.ts
2589
+ function getBoundary(header) {
2590
+ if (!header) return null;
2591
+ for (const item of header.split(";")) {
2592
+ const part = item.trim();
2593
+ const eq = part.indexOf("=");
2594
+ if (eq === -1) continue;
2595
+ if (part.slice(0, eq).trim().toLowerCase() !== "boundary") continue;
2596
+ const value = part.slice(eq + 1).trim().replace(/^"(.*)"$/, "$1");
2597
+ return value || null;
2598
+ }
2599
+ return null;
2600
+ }
2601
+ var BREAK = Buffer.from("\r\n\r\n");
2602
+ async function parseMultipart(stream, boundary, bucket2, limits, max = INF) {
2603
+ const budget = { used: 0, max: INF, files: 0 };
2604
+ const delim = Buffer.from(`\r
2605
+ --${boundary}`);
2606
+ const body = {};
2607
+ let buf = Buffer.from("\r\n");
2608
+ let state = "boundary";
2609
+ let part = null;
2610
+ let textBytes = 0;
2611
+ const feed = (p, data) => {
2612
+ if (p.kind === "text") {
2613
+ textBytes += data.length;
2614
+ if (textBytes > max) throw tooLarge(max);
2615
+ }
2616
+ return feedPart(p, data);
2617
+ };
2618
+ for await (const chunk of asIterable(stream)) {
2619
+ buf = Buffer.concat([buf, Buffer.from(chunk)]);
2620
+ let advanced = true;
2621
+ while (advanced) {
2622
+ advanced = false;
2623
+ if (state === "boundary") {
2624
+ const i = buf.indexOf(delim);
2625
+ if (i === -1) {
2626
+ if (buf.length >= delim.length) {
2627
+ buf = buf.subarray(buf.length - delim.length + 1);
2628
+ }
2629
+ break;
2630
+ }
2631
+ if (buf.length < i + delim.length + 2) break;
2632
+ const after = i + delim.length;
2633
+ if (buf[after] === 45 && buf[after + 1] === 45) return body;
2634
+ buf = buf.subarray(after + 2);
2635
+ state = "headers";
2636
+ advanced = true;
2637
+ } else if (state === "headers") {
2638
+ const i = buf.indexOf(BREAK);
2639
+ if (i === -1) break;
2640
+ part = startPart(buf.subarray(0, i).toString("utf-8"), bucket2, limits, budget);
2641
+ buf = buf.subarray(i + BREAK.length);
2642
+ state = "body";
2643
+ advanced = true;
2644
+ } else {
2645
+ const i = buf.indexOf(delim);
2646
+ if (i === -1) {
2647
+ const safe = buf.length - (delim.length - 1);
2648
+ if (safe > 0 && part) {
2649
+ await feed(part, buf.subarray(0, safe));
2650
+ buf = buf.subarray(safe);
2651
+ }
2652
+ break;
2653
+ }
2654
+ if (part) {
2655
+ await feed(part, buf.subarray(0, i));
2656
+ await endPart(part, body);
2657
+ part = null;
2658
+ }
2659
+ buf = buf.subarray(i);
2660
+ state = "boundary";
2661
+ advanced = true;
2662
+ }
2663
+ }
2664
+ }
2665
+ if (part) await endPart(part, body);
2666
+ return body;
2667
+ }
2668
+
2669
+ // src/body/parseBody.ts
2670
+ function toStream(input) {
2671
+ if (input instanceof ReadableStream) return input;
2672
+ return new ReadableStream({
2673
+ start(controller) {
2674
+ controller.enqueue(input);
2675
+ controller.close();
2676
+ }
2677
+ });
2678
+ }
2679
+ async function toBuffer(input, max = INF) {
2680
+ if (!(input instanceof ReadableStream)) {
2681
+ if (input.length > max) throw tooLarge(max);
2682
+ return input;
2683
+ }
2684
+ const chunks = [];
2685
+ let total = 0;
2686
+ for await (const chunk of asIterable(input)) {
2687
+ total += chunk.byteLength;
2688
+ if (total > max) throw tooLarge(max);
2689
+ chunks.push(Buffer.from(chunk));
2690
+ }
2691
+ return Buffer.concat(chunks);
2692
+ }
2693
+ function parseUrlEncoded(text) {
2694
+ const out = {};
2695
+ for (const [key, value] of new URLSearchParams(text)) {
2696
+ const existing = out[key];
2697
+ if (existing === void 0) out[key] = value;
2698
+ else if (Array.isArray(existing)) existing.push(value);
2699
+ else out[key] = [existing, value];
2700
+ }
2701
+ return out;
2702
+ }
2703
+ async function streamRawToBucket(stream, type2, bucket2, limits) {
2704
+ const part = makeFilePart("body", "upload", type2, bucket2, limits, {
2705
+ used: 0,
2706
+ max: INF,
2707
+ files: 0
2708
+ });
2709
+ for await (const chunk of asIterable(stream)) {
2710
+ await feedPart(part, Buffer.from(chunk));
2711
+ }
2712
+ const body = {};
2713
+ await endPart(part, body);
2714
+ return part.size ? body.body : void 0;
2715
+ }
2716
+ async function parseBody(input, contentType, dest, max = INF, length) {
2717
+ const type2 = Array.isArray(contentType) ? contentType[0] : contentType;
2718
+ let bucket2;
2719
+ let limits = {};
2720
+ if (dest && typeof dest === "object" && "bucket" in dest) {
2721
+ bucket2 = dest.bucket;
2722
+ const { maxFileSize: maxFileSize2, maxTotalSize, maxFiles, minSize, fileType: fileType2 } = dest;
2723
+ limits = { maxFileSize: maxFileSize2, maxTotalSize, maxFiles, minSize, fileType: fileType2 };
2724
+ } else {
2725
+ bucket2 = dest;
2726
+ }
2727
+ if (type2 && /multipart\/form-data/i.test(type2)) {
2728
+ const boundary = getBoundary(type2);
2729
+ if (!boundary) throw errors_default.BODY_INVALID_MULTIPART();
2730
+ return parseMultipart(toStream(input), boundary, bucket2, limits, max);
2731
+ }
2732
+ if (!type2 || /^text\//i.test(type2)) {
2733
+ const buf = await toBuffer(input, max);
2734
+ return buf.length ? buf.toString("utf-8") : void 0;
2735
+ }
2736
+ if (/^application\/([\w.+-]+\+)?json\b/i.test(type2)) {
2737
+ const buf = await toBuffer(input, max);
2738
+ return buf.length ? JSON.parse(buf.toString("utf-8")) : void 0;
2739
+ }
2740
+ if (/application\/x-www-form-urlencoded/i.test(type2)) {
2741
+ const buf = await toBuffer(input, max);
2742
+ return buf.length ? parseUrlEncoded(buf.toString("utf-8")) : void 0;
2743
+ }
2744
+ if (bucket2 === false) {
2745
+ const buf = await toBuffer(input, max);
2746
+ return buf.length ? buf : void 0;
2747
+ }
2748
+ if (!bucket2) throw errors_default.UPLOAD_NOT_CONFIGURED({ name: "the request body" });
2749
+ const { maxFileSize } = limits;
2750
+ if (length != null && maxFileSize != null && length > parseBytes(maxFileSize)) {
2751
+ throw errors_default.UPLOAD_TOO_LARGE({
2752
+ name: "the request body",
2753
+ size: String(length),
2754
+ limit: String(maxFileSize)
2755
+ });
2756
+ }
2757
+ return streamRawToBucket(toStream(input), type2, bucket2, limits);
2758
+ }
2759
+
2760
+ // src/body/body.ts
2761
+ var sources = /* @__PURE__ */ new WeakMap();
2762
+ function setBodySource(ctx, source) {
2763
+ sources.set(ctx, source);
2764
+ }
2765
+ async function resolveBody(ctx, mode = "parse", max = resolveMax(void 0)) {
2766
+ const source = sources.get(ctx);
2767
+ if (!source) return void 0;
2768
+ const contentType = String(ctx.headers["content-type"] || "");
2769
+ const isMultipart = /multipart\/form-data/i.test(contentType);
2770
+ const declared = Number(ctx.headers["content-length"]);
2771
+ const trustDeclared = !isMultipart && !ctx.options.uploads;
2772
+ if (max !== INF && trustDeclared && declared > max) throw tooLarge(max);
2773
+ if (mode === "stream") return source.getStream();
2774
+ if (mode === "raw") {
2775
+ const raw = await source.getBuffer();
2776
+ if (raw.length > max) throw tooLarge(max);
2777
+ if (!raw.length) return void 0;
2778
+ if (!ctx.headers["content-length"]) {
2779
+ ctx.headers["content-length"] = String(raw.length);
2780
+ }
2781
+ return raw;
2782
+ }
2783
+ const stream = source.getStream();
2784
+ if (!stream) return void 0;
2785
+ let size = 0;
2786
+ const counted = stream.pipeThrough(
2787
+ new TransformStream({
2788
+ transform(chunk, controller) {
2789
+ size += chunk.byteLength;
2790
+ controller.enqueue(chunk);
2791
+ }
2792
+ })
2793
+ );
2794
+ const parsed = await parseBody(
2795
+ counted,
2796
+ ctx.headers["content-type"],
2797
+ ctx.options.uploads,
2798
+ max,
2799
+ Number.isFinite(declared) ? declared : void 0
2800
+ );
2801
+ if (size && !ctx.headers["content-length"]) {
2802
+ ctx.headers["content-length"] = String(size);
2803
+ }
2804
+ return parsed;
2805
+ }
2806
+
2807
+ // src/util/define.ts
2808
+ function define(obj, key, cb) {
2809
+ Object.defineProperty(obj, key, {
2810
+ configurable: true,
2811
+ get() {
2812
+ const value = cb(obj);
2813
+ Object.defineProperty(obj, key, {
2814
+ configurable: true,
2815
+ writable: true,
2816
+ value
2817
+ });
2818
+ return obj[key];
2819
+ }
2820
+ });
2821
+ }
2822
+
2823
+ // src/errors/ValidationError.ts
2824
+ var ValidationError = class extends errors_default {
2825
+ source;
2826
+ issues;
2827
+ constructor(source, issues) {
2828
+ const code = source === "response" ? "VALIDATION_FAILED" : "INVALID_REQUEST";
2829
+ const { status: status2, message } = definition(code);
2830
+ super(code, status2, message, { source });
2831
+ this.source = source;
2832
+ this.issues = issues;
2833
+ }
2834
+ };
2835
+
2836
+ // src/pipeline/validate.ts
2837
+ async function run(schema, value, source) {
2838
+ const result = await schema["~standard"].validate(value);
2839
+ if (result.issues) throw new ValidationError(source, result.issues);
2840
+ return result.value;
2841
+ }
2842
+ async function validateRequest(ctx, options) {
2843
+ if (options.body) {
2844
+ ctx.body = await run(options.body, ctx.body ?? {}, "body");
2845
+ }
2846
+ if (options.query) {
2847
+ const query = await run(options.query, ctx.url.query || {}, "query");
2848
+ replace2(ctx.url.query, query);
2849
+ }
2850
+ if (options.params) {
2851
+ const params = await run(options.params, ctx.url.params || {}, "params");
2852
+ replace2(ctx.url.params, params);
2853
+ }
2854
+ }
2855
+ async function validateResponse(out, options) {
2856
+ if (!options.response) return out;
2857
+ if (out?.constructor !== Object && !Array.isArray(out)) return out;
2858
+ return await run(options.response, out, "response");
2859
+ }
2860
+ function replace2(target2, values) {
2861
+ for (const key of Object.keys(target2)) delete target2[key];
2862
+ Object.assign(target2, values);
2863
+ }
2864
+
2865
+ // src/context/isValidMethod.ts
2866
+ var methods = [
2867
+ "get",
2868
+ "post",
2869
+ "put",
2870
+ "patch",
2871
+ "delete",
2872
+ "head",
2873
+ "options",
2874
+ "socket"
2875
+ ];
2876
+ function isValidMethod(method) {
2877
+ return methods.includes(method);
2878
+ }
2879
+
2880
+ // src/pipeline/handleRequest.ts
2881
+ async function handleRequest(app, ctx) {
2882
+ let res = await getResponse(app, ctx);
2883
+ if (res) res = await finalize(res, ctx);
2884
+ if (res && ctx.options.onResponse) {
2885
+ const replaced = await ctx.options.onResponse(res, ctx);
2886
+ if (replaced) res = replaced;
2887
+ }
2888
+ if (res) ctx.options.log.request(ctx, res);
2889
+ if (res?.body && ctx.method === "head") {
2890
+ res.body.cancel().catch(() => {
2891
+ });
2892
+ res = new Response(null, { status: res.status, headers: res.headers });
2893
+ }
2894
+ return res;
2895
+ }
2896
+ async function getResponse(app, ctx) {
2897
+ try {
2898
+ if (!isValidMethod(ctx.method)) {
2899
+ throw errors_default.METHOD_NOT_ALLOWED({ method: ctx.method });
2900
+ }
2901
+ let matched = false;
2902
+ const routes = ctx.method === "head" ? [...app.handlers.head, ...app.handlers.get] : app.handlers[ctx.method];
2903
+ for (const route of routes) {
2904
+ const params = pathPattern(route.path, ctx.url.pathname || "/");
2905
+ if (!params) continue;
2906
+ matched = true;
2907
+ define(ctx.url, "params", () => params);
2908
+ const { parser, cache: cache3, uploads } = route.options;
2909
+ if (parser !== void 0 || cache3 !== void 0 || uploads !== void 0) {
2910
+ ctx.options = { ...app.settings };
2911
+ if (parser !== void 0) ctx.options.parser = parser;
2912
+ if (cache3 !== void 0) ctx.options.cache = cache3;
2913
+ if (uploads !== void 0) ctx.options.uploads = uploads;
2914
+ }
2915
+ checkTraversal(params, ctx);
2916
+ ctx.body = await resolveBody(
2917
+ ctx,
2918
+ ctx.options.parser,
2919
+ ctx.options.security.maxBodySize
2920
+ );
2921
+ await validateRequest(ctx, route.options);
2922
+ for (const cb of route.fns) {
2923
+ const res = await cb(ctx);
2924
+ const out = await parseResponse(
2925
+ await validateResponse(res, route.options),
2926
+ ctx
2927
+ );
2928
+ if (out) return out;
2929
+ }
2930
+ break;
2931
+ }
2932
+ if (!matched) {
2933
+ ctx.body = await resolveBody(
2934
+ ctx,
2935
+ ctx.options.parser,
2936
+ ctx.options.security.maxBodySize
2937
+ );
2938
+ for (const mw of app.middleware) {
2939
+ const out = await parseResponse(await mw(ctx), ctx);
2940
+ if (out) return out;
2941
+ }
2942
+ }
2943
+ if (ctx.platform.provider === "netlify") return;
2944
+ throw errors_default.NOT_FOUND();
2945
+ } catch (error) {
2946
+ return ctx.options.onError(error, ctx);
2947
+ }
2948
+ }
2949
+
2950
+ // src/http/parseCookies.ts
2951
+ function parseCookies(cookies2) {
2952
+ if (!cookies2) return {};
2953
+ const cookieStr = Array.isArray(cookies2) ? cookies2[0] : cookies2;
2954
+ if (!cookieStr) return {};
2955
+ return Object.fromEntries(
2956
+ cookieStr.split(/;\s*/).map((part) => {
2957
+ const [key, ...rest] = part.split("=");
2958
+ const value = rest.join("=");
2959
+ try {
2960
+ return [key, decodeURIComponent(value)];
2961
+ } catch {
2962
+ return [key, value];
2963
+ }
2964
+ })
2690
2965
  );
2691
- if (handled) return;
2692
- return 204;
2693
2966
  }
2694
2967
 
2695
- // src/middle/timer.ts
2696
- var createTime = () => {
2697
- const times2 = [["init", performance.now()]];
2698
- const time = (name) => times2.push([name, performance.now()]);
2699
- time.times = times2;
2700
- time.headers = () => {
2701
- const r2 = (t) => Math.round(t);
2702
- const times3 = time.times;
2703
- const timing = times3.slice(1).map(([name, time2], i) => `${name};dur=${r2(time2 - times3[i][1])}`).join(", ");
2704
- return timing;
2705
- };
2706
- return time;
2968
+ // src/http/parseHeaders.ts
2969
+ var parseHeaders_default = (raw) => {
2970
+ const headers2 = {};
2971
+ raw.forEach((value, originalKey) => {
2972
+ const key = originalKey.toLowerCase();
2973
+ if (headers2[key]) {
2974
+ if (!Array.isArray(headers2[key])) {
2975
+ headers2[key] = [headers2[key]];
2976
+ }
2977
+ headers2[key].push(value);
2978
+ } else {
2979
+ headers2[key] = value;
2980
+ }
2981
+ });
2982
+ return headers2;
2707
2983
  };
2708
- function timer(ctx) {
2709
- ctx.time = createTime();
2710
- }
2711
2984
 
2712
- // src/auth/socketUser.ts
2713
- async function socketUser(app, headers2, cookies2) {
2714
- if (!app.settings.auth) return void 0;
2715
- const ctx = { options: app.settings, headers: headers2, cookies: cookies2 };
2716
- return app.settings.auth.user(ctx);
2985
+ // src/context/writeResponse.ts
2986
+ async function writeResponse(out, response) {
2987
+ response.writeHead(out.status || 200, parseHeaders_default(out.headers));
2988
+ try {
2989
+ if (out.body instanceof ReadableStream) {
2990
+ const reader = out.body.getReader();
2991
+ response.on("close", () => reader.cancel().catch(() => {
2992
+ }));
2993
+ while (true) {
2994
+ const { value, done } = await reader.read();
2995
+ if (done) break;
2996
+ response.write(value);
2997
+ }
2998
+ } else {
2999
+ response.write(out.body || "");
3000
+ }
3001
+ response.end();
3002
+ } catch {
3003
+ if (!response.destroyed) response.destroy();
3004
+ }
2717
3005
  }
2718
3006
 
2719
- // src/helpers/wsNode.ts
3007
+ // src/ws/wsNode.ts
2720
3008
  var GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
3009
+ var MAX_MESSAGE = 16 * 1024 ** 2;
2721
3010
  var CONTINUATION = 0;
2722
3011
  var TEXT = 1;
2723
3012
  var BINARY = 2;
@@ -2747,6 +3036,7 @@ var NodeWebSocket = class {
2747
3036
  handlers;
2748
3037
  buffer;
2749
3038
  fragments;
3039
+ fragmentSize;
2750
3040
  fragmentOpcode;
2751
3041
  closed;
2752
3042
  readyState;
@@ -2758,6 +3048,7 @@ var NodeWebSocket = class {
2758
3048
  this.handlers = handlers;
2759
3049
  this.buffer = Buffer.alloc(0);
2760
3050
  this.fragments = [];
3051
+ this.fragmentSize = 0;
2761
3052
  this.fragmentOpcode = TEXT;
2762
3053
  this.closed = false;
2763
3054
  this.readyState = 1;
@@ -2811,6 +3102,10 @@ var NodeWebSocket = class {
2811
3102
  len = Number(buf.readBigUInt64BE(2));
2812
3103
  offset = 10;
2813
3104
  }
3105
+ if (len > MAX_MESSAGE) {
3106
+ this.close(1009, "Message too big");
3107
+ return;
3108
+ }
2814
3109
  let mask = null;
2815
3110
  if (masked) {
2816
3111
  if (buf.length < offset + 4) return;
@@ -2838,13 +3133,22 @@ var NodeWebSocket = class {
2838
3133
  if (opcode === PONG) return;
2839
3134
  if (opcode === CONTINUATION) {
2840
3135
  this.fragments.push(payload);
3136
+ this.fragmentSize += payload.length;
2841
3137
  } else {
2842
3138
  this.fragments = [payload];
3139
+ this.fragmentSize = payload.length;
2843
3140
  this.fragmentOpcode = opcode;
2844
3141
  }
3142
+ if (this.fragmentSize > MAX_MESSAGE) {
3143
+ this.fragments = [];
3144
+ this.fragmentSize = 0;
3145
+ this.close(1009, "Message too big");
3146
+ return;
3147
+ }
2845
3148
  if (!fin) return;
2846
3149
  const full = this.fragments.length === 1 ? this.fragments[0] : Buffer.concat(this.fragments);
2847
3150
  this.fragments = [];
3151
+ this.fragmentSize = 0;
2848
3152
  const body = this.fragmentOpcode === TEXT ? full.toString("utf8") : full;
2849
3153
  this.handlers.onMessage(body);
2850
3154
  }
@@ -2896,54 +3200,63 @@ Sec-WebSocket-Accept: ${accept}\r
2896
3200
  // src/context/node.ts
2897
3201
  import { TLSSocket } from "tls";
2898
3202
 
2899
- // src/context/isValidMethod.ts
2900
- var methods = [
2901
- "get",
2902
- "post",
2903
- "put",
2904
- "patch",
2905
- "delete",
2906
- "head",
2907
- "options",
2908
- "socket"
2909
- ];
2910
- function isValidMethod(method) {
2911
- return methods.includes(method);
3203
+ // src/http/clientIp.ts
3204
+ var first = (v) => (Array.isArray(v) ? v[0] : v) || "";
3205
+ var normalize = (ip) => ip.replace(/^::ffff:/, "");
3206
+ function clientIp(headers2, opts = {}) {
3207
+ const { remoteAddress = "", trustProxy = false } = opts;
3208
+ const cf = first(headers2["cf-connecting-ip"]);
3209
+ if (cf) return normalize(cf);
3210
+ const nf = first(headers2["x-nf-client-connection-ip"]);
3211
+ if (nf) return normalize(nf);
3212
+ if (trustProxy) {
3213
+ const xff = first(headers2["x-forwarded-for"]);
3214
+ if (xff) return normalize(xff.split(",")[0].trim());
3215
+ const real = first(headers2["x-real-ip"]);
3216
+ if (real) return normalize(real);
3217
+ }
3218
+ return normalize(remoteAddress);
2912
3219
  }
2913
3220
 
2914
- // src/context/node.ts
2915
- var chunkArray = (arr) => arr.length > 2 ? [[arr[0], arr[1]], ...chunkArray(arr.slice(2))] : [arr];
2916
- async function createNode(req, app, signal = new AbortController().signal) {
2917
- const init = performance.now();
2918
- const method = req.method?.toLowerCase() || "get";
2919
- if (!isValidMethod(method)) {
2920
- throw new Error(`Invalid HTTP method: ${method}`);
3221
+ // src/http/forwarded.ts
3222
+ var first2 = (value) => {
3223
+ const one = Array.isArray(value) ? value[0] : value;
3224
+ return one?.split(",")[0].trim() || void 0;
3225
+ };
3226
+ function forwarded(url, headers2, trustProxy) {
3227
+ if (!trustProxy) return;
3228
+ const proto = first2(headers2["x-forwarded-proto"]);
3229
+ if (proto === "http" || proto === "https") url.protocol = `${proto}:`;
3230
+ const host = first2(headers2["x-forwarded-host"]);
3231
+ const port = first2(headers2["x-forwarded-port"]);
3232
+ if (host?.includes(":")) {
3233
+ url.host = host;
3234
+ } else if (host) {
3235
+ url.hostname = host;
3236
+ url.port = port ?? "";
3237
+ } else if (port) {
3238
+ url.port = port;
2921
3239
  }
2922
- const chunks = chunkArray(req.rawHeaders);
2923
- const headers2 = parseHeaders_default(new Headers(chunks));
3240
+ }
3241
+
3242
+ // src/context/create.ts
3243
+ function createContext(app, { method: rawMethod, headers: rawHeaders, url: rawUrl, signal, remoteAddress, source }) {
3244
+ const init = performance.now();
3245
+ const method = rawMethod?.toLowerCase() || "get";
3246
+ const headers2 = parseHeaders_default(rawHeaders);
2924
3247
  const cookies2 = parseCookies(headers2.cookie);
2925
- const scheme = req.socket instanceof TLSSocket ? "https" : "http";
2926
- const host = headers2.host || `localhost:${app.settings.port}`;
2927
- const path = (req.url || "/").replace(/\/$/, "") || "/";
2928
- const baseUrl = `${scheme}://${host}`;
2929
- const url = new URL(path, baseUrl);
3248
+ const url = new URL(rawUrl.replace(/\/$/, ""));
2930
3249
  forwarded(url, headers2, app.settings.security.trustProxy);
2931
3250
  define(
2932
3251
  url,
2933
3252
  "query",
2934
3253
  (url2) => Object.fromEntries(url2.searchParams.entries())
2935
3254
  );
2936
- const source = {
2937
- getBuffer: () => new Promise((resolve, reject) => {
2938
- const chunks2 = [];
2939
- req.on("data", (chunk) => chunks2.push(chunk)).on("end", () => resolve(Buffer.concat(chunks2))).on("error", reject);
2940
- }),
2941
- getStream: () => toWeb(req)
2942
- };
2943
3255
  const ctx = {
2944
3256
  options: app.settings,
2945
3257
  platform: app.platform,
2946
3258
  url,
3259
+ // Possibly not a real Method: handleRequest rejects it inside its boundary
2947
3260
  method,
2948
3261
  body: void 0,
2949
3262
  headers: headers2,
@@ -2952,7 +3265,7 @@ async function createNode(req, app, signal = new AbortController().signal) {
2952
3265
  init,
2953
3266
  app,
2954
3267
  ip: clientIp(headers2, {
2955
- remoteAddress: req.socket.remoteAddress || "",
3268
+ remoteAddress,
2956
3269
  trustProxy: app.settings.security.trustProxy
2957
3270
  })
2958
3271
  };
@@ -2960,45 +3273,43 @@ async function createNode(req, app, signal = new AbortController().signal) {
2960
3273
  return ctx;
2961
3274
  }
2962
3275
 
3276
+ // src/context/node.ts
3277
+ var chunkArray = (arr) => arr.length > 2 ? [[arr[0], arr[1]], ...chunkArray(arr.slice(2))] : [arr];
3278
+ async function createNode(req, app, signal = new AbortController().signal) {
3279
+ const headers2 = new Headers(chunkArray(req.rawHeaders));
3280
+ const scheme = req.socket instanceof TLSSocket ? "https" : "http";
3281
+ const host = headers2.get("host") || `localhost:${app.settings.port}`;
3282
+ return createContext(app, {
3283
+ method: req.method || "get",
3284
+ headers: headers2,
3285
+ url: `${scheme}://${host}${req.url || "/"}`,
3286
+ signal,
3287
+ remoteAddress: req.socket.remoteAddress || "",
3288
+ source: {
3289
+ getBuffer: () => new Promise((resolve, reject) => {
3290
+ const chunks = [];
3291
+ req.on("data", (chunk) => chunks.push(chunk)).on("end", () => resolve(Buffer.concat(chunks))).on("error", reject);
3292
+ }),
3293
+ // Normalize the node stream to the web ReadableStream every reader expects
3294
+ getStream: () => toWeb(req)
3295
+ }
3296
+ });
3297
+ }
3298
+
2963
3299
  // src/context/winter.ts
2964
3300
  async function createWinter(req, app, server2) {
2965
- const init = performance.now();
2966
- const method = req.method.toLowerCase();
2967
- if (!isValidMethod(method)) {
2968
- throw new Error(`Invalid HTTP method: ${method}`);
2969
- }
2970
- const headers2 = parseHeaders_default(req.headers);
2971
- const cookies2 = parseCookies(headers2.cookie);
2972
- const baseUrl = req.url.replace(/\/$/, "") || "/";
2973
- const url = new URL(baseUrl);
2974
- forwarded(url, headers2, app.settings.security.trustProxy);
2975
- define(
2976
- url,
2977
- "query",
2978
- (url2) => Object.fromEntries(url2.searchParams.entries())
2979
- );
2980
- const source = {
2981
- getBuffer: async () => Buffer.from(await req.arrayBuffer()),
2982
- getStream: () => req.body ?? void 0
2983
- };
2984
- const ctx = {
2985
- options: app.settings,
2986
- platform: app.platform,
2987
- url,
2988
- method,
2989
- body: void 0,
2990
- headers: headers2,
2991
- cookies: cookies2,
3301
+ return createContext(app, {
3302
+ method: req.method,
3303
+ headers: req.headers,
3304
+ url: req.url,
2992
3305
  signal: req.signal,
2993
- init,
2994
- app,
2995
- ip: clientIp(headers2, {
2996
- remoteAddress: server2?.requestIP?.(req)?.address || "",
2997
- trustProxy: app.settings.security.trustProxy
2998
- })
2999
- };
3000
- setBodySource(ctx, source);
3001
- return ctx;
3306
+ remoteAddress: server2?.requestIP?.(req)?.address || "",
3307
+ source: {
3308
+ // req.body is already a web ReadableStream, so no conversion is needed
3309
+ getBuffer: async () => Buffer.from(await req.arrayBuffer()),
3310
+ getStream: () => req.body ?? void 0
3311
+ }
3312
+ });
3002
3313
  }
3003
3314
 
3004
3315
  // src/context/handlers.ts
@@ -3017,10 +3328,14 @@ var Winter = async (app, request, env2) => {
3017
3328
  if (env2.upgrade(request, { data: { user } })) return;
3018
3329
  }
3019
3330
  }
3020
- Object.assign(globalThis.env, env2);
3021
- const ctx = await createWinter(request, app, env2);
3022
- const res = await handleRequest(app, ctx);
3023
- return res;
3331
+ const isRuntimeServer = typeof env2?.upgrade === "function" || typeof env2?.requestIP === "function";
3332
+ if (env2 && !isRuntimeServer) Object.assign(globalThis.env, env2);
3333
+ try {
3334
+ const ctx = await createWinter(request, app, env2);
3335
+ return await handleRequest(app, ctx);
3336
+ } catch {
3337
+ return new Response("Server Error", { status: 500 });
3338
+ }
3024
3339
  };
3025
3340
  var Node = async (app) => {
3026
3341
  const http = await import("http");
@@ -3030,27 +3345,16 @@ var Node = async (app) => {
3030
3345
  response.on("close", () => {
3031
3346
  if (!response.writableFinished) controller.abort();
3032
3347
  });
3033
- const ctx = await createNode(request, app, controller.signal);
3034
- if ("error" in ctx) throw ctx.error;
3035
- const out = await handleRequest(app, ctx);
3036
- response.writeHead(out.status || 200, parseHeaders_default(out.headers));
3348
+ let out;
3037
3349
  try {
3038
- if (out.body instanceof ReadableStream) {
3039
- const reader = out.body.getReader();
3040
- response.on("close", () => reader.cancel().catch(() => {
3041
- }));
3042
- while (true) {
3043
- const { value, done } = await reader.read();
3044
- if (done) break;
3045
- response.write(value);
3046
- }
3047
- } else {
3048
- response.write(out.body || "");
3049
- }
3050
- response.end();
3350
+ const ctx = await createNode(request, app, controller.signal);
3351
+ out = await handleRequest(app, ctx);
3051
3352
  } catch {
3052
- if (!response.destroyed) response.destroy();
3353
+ response.writeHead(500);
3354
+ response.end("Server Error");
3355
+ return;
3053
3356
  }
3357
+ await writeResponse(out, response);
3054
3358
  }
3055
3359
  );
3056
3360
  await attachWebsocket(server2, app);
@@ -3059,115 +3363,14 @@ var Node = async (app) => {
3059
3363
  });
3060
3364
  return server2;
3061
3365
  };
3062
- var Netlify = async (app, request, context) => {
3063
- request.context = context;
3064
- if (typeof Netlify === "undefined") {
3065
- throw new Error("Netlify doesn't exist");
3066
- }
3067
- const ctx = await createWinter(request, app);
3068
- const res = await handleRequest(app, ctx);
3069
- return res;
3070
- };
3071
-
3072
- // src/router.ts
3073
- function checkParserConflict(options, globalParser) {
3074
- const parser = options.parser ?? globalParser ?? "parse";
3075
- if (options.body && parser !== "parse") {
3076
- throw new Error(
3077
- `A \`parser: '${parser}'\` route never parses the body, so its \`body\` schema cannot run. Remove one, or set \`parser: 'parse'\` on the route.`
3078
- );
3079
- }
3080
- }
3081
- var Router = class _Router {
3082
- // Cross-cutting middleware added with .use(); they run on every request
3083
- middleware = [];
3084
- // Routes per method, each carrying its own (already-flattened) chain of fns
3085
- handlers = {
3086
- socket: [],
3087
- get: [],
3088
- head: [],
3089
- post: [],
3090
- put: [],
3091
- patch: [],
3092
- delete: [],
3093
- options: []
3094
- };
3095
- // For the router we can just return itself since it's not the final export,
3096
- // but then on the root it'll return some fancy wrappers
3097
- self() {
3098
- return this;
3099
- }
3100
- // Registers one route: bakes the current middleware + the route's own
3101
- // functions into a single flat `fns` list. A plain options object may sit
3102
- // between the path and the handlers, and it's pulled out here.
3103
- handle(method, pathOrFn, ...rest) {
3104
- let path = "*";
3105
- if (typeof pathOrFn === "string") {
3106
- path = pathOrFn;
3107
- } else if (pathOrFn != null) {
3108
- rest.unshift(pathOrFn);
3109
- }
3110
- let options = {};
3111
- if (rest[0] != null && typeof rest[0] !== "function") {
3112
- options = rest.shift();
3113
- }
3114
- checkParserConflict(options, this.settings?.parser);
3115
- if (options.uploads !== void 0) {
3116
- options.uploads = resolveUploads(options.uploads);
3117
- }
3118
- const base = method === "socket" ? [] : this.middleware;
3119
- const fns = [...base, ...rest].filter((fn) => fn != null);
3120
- this.handlers[method].push({ path, options, fns });
3121
- return this.self();
3122
- }
3123
- socket(pathOrMid, optionsOrMid, ...middleware) {
3124
- return this.handle("socket", pathOrMid, optionsOrMid, ...middleware);
3125
- }
3126
- get(pathOrMid, optionsOrMid, ...middleware) {
3127
- return this.handle("get", pathOrMid, optionsOrMid, ...middleware);
3128
- }
3129
- head(pathOrMid, optionsOrMid, ...middleware) {
3130
- return this.handle("head", pathOrMid, optionsOrMid, ...middleware);
3131
- }
3132
- post(pathOrMid, optionsOrMid, ...middleware) {
3133
- return this.handle("post", pathOrMid, optionsOrMid, ...middleware);
3134
- }
3135
- put(pathOrMid, optionsOrMid, ...middleware) {
3136
- return this.handle("put", pathOrMid, optionsOrMid, ...middleware);
3137
- }
3138
- patch(pathOrMid, optionsOrMid, ...middleware) {
3139
- return this.handle("patch", pathOrMid, optionsOrMid, ...middleware);
3140
- }
3141
- delete(pathOrMid, optionsOrMid, ...middleware) {
3142
- return this.handle("delete", pathOrMid, optionsOrMid, ...middleware);
3143
- }
3144
- options(pathOrMid, optionsOrMid, ...middleware) {
3145
- return this.handle("options", pathOrMid, optionsOrMid, ...middleware);
3146
- }
3147
- use(...args) {
3148
- for (const arg of args) {
3149
- if (arg instanceof _Router) {
3150
- for (const m of Object.keys(arg.handlers)) {
3151
- for (const route of arg.handlers[m]) {
3152
- checkParserConflict(route.options, this.settings?.parser);
3153
- const base = m === "socket" ? [] : this.middleware;
3154
- this.handlers[m].push({
3155
- path: route.path,
3156
- options: route.options,
3157
- fns: [...base, ...route.fns]
3158
- });
3159
- }
3160
- }
3161
- } else {
3162
- this.middleware.push(arg);
3163
- }
3164
- }
3165
- return this.self();
3366
+ var Netlify = async (app, request, _context) => {
3367
+ try {
3368
+ const ctx = await createWinter(request, app);
3369
+ return await handleRequest(app, ctx);
3370
+ } catch {
3371
+ return new Response("Server Error", { status: 500 });
3166
3372
  }
3167
3373
  };
3168
- function router() {
3169
- return new Router();
3170
- }
3171
3374
 
3172
3375
  // src/ServerTest.ts
3173
3376
  function isSerializable(body) {
@@ -3184,10 +3387,11 @@ function isSerializable(body) {
3184
3387
  function ServerTest(app) {
3185
3388
  const port = app.settings.port;
3186
3389
  const fetch2 = async (method, path, options = {}) => {
3187
- if (!options.headers) options.headers = {};
3188
- if (isSerializable(options.body)) {
3189
- options.headers["content-type"] = "application/json";
3190
- options.body = JSON.stringify(options.body);
3390
+ const headers2 = new Headers(options.headers);
3391
+ let body = options.body;
3392
+ if (isSerializable(body)) {
3393
+ headers2.set("content-type", "application/json");
3394
+ body = JSON.stringify(body);
3191
3395
  }
3192
3396
  if (/^[a-z][a-z0-9+.-]*:\/\//i.test(path) && !/^https?:\/\//i.test(path)) {
3193
3397
  throw new Error(
@@ -3197,8 +3401,10 @@ function ServerTest(app) {
3197
3401
  const url = /^https?:\/\//i.test(path) ? path : `http://localhost:${port}${path}`;
3198
3402
  return await app.fetch(
3199
3403
  new Request(url, {
3404
+ ...options,
3200
3405
  method,
3201
- ...options
3406
+ headers: headers2,
3407
+ body
3202
3408
  })
3203
3409
  );
3204
3410
  };
@@ -3216,7 +3422,6 @@ function ServerTest(app) {
3216
3422
  // src/index.ts
3217
3423
  import { default as default2 } from "bucket";
3218
3424
  var Server = class extends Router {
3219
- settings;
3220
3425
  platform;
3221
3426
  sockets;
3222
3427
  websocket;
@@ -3231,7 +3436,7 @@ var Server = class extends Router {
3231
3436
  this.sockets = [];
3232
3437
  this.websocket = createWebsocket(this.sockets, this.handlers);
3233
3438
  if (this.platform.runtime === "node") {
3234
- this.node();
3439
+ this.node().catch((error) => console.error("[server:start]", error));
3235
3440
  } else if (this.platform.runtime === "bun") {
3236
3441
  this.settings.log.start(`http://localhost:${this.settings.port}/`);
3237
3442
  }
@@ -3277,7 +3482,7 @@ function server(options) {
3277
3482
  }
3278
3483
  export {
3279
3484
  Server,
3280
- ServerError_default as ServerError,
3485
+ errors_default as ServerError,
3281
3486
  ValidationError,
3282
3487
  default2 as bucket,
3283
3488
  cache,