@server/next 0.48.3 → 0.49.1

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