@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.d.ts +265 -257
- package/index.js +2204 -1905
- package/package.json +9 -6
- package/readme.md +12 -10
- package/src/jsx/jsx-runtime.js +14 -12
package/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
// src/
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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}')
|
|
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
|
-
|
|
47
|
-
|
|
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: "
|
|
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: {
|
|
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
|
|
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/
|
|
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/
|
|
92
|
-
var
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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/
|
|
133
|
-
function
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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 {
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
return
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
199
|
-
var
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
var
|
|
203
|
-
function
|
|
204
|
-
if (
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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/
|
|
219
|
-
var
|
|
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/
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
function
|
|
330
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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(
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
533
|
+
const payload = JSON.stringify(body);
|
|
534
|
+
fill(headers2, "application/json", Buffer.byteLength(payload));
|
|
535
|
+
return payload;
|
|
369
536
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
376
|
-
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
501
|
-
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
-
|
|
534
|
-
|
|
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
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
|
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/
|
|
567
|
-
var
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
|
|
572
|
-
const
|
|
573
|
-
if (
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
const
|
|
578
|
-
if (
|
|
579
|
-
if (
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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 (
|
|
607
|
-
|
|
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/
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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
|
-
|
|
648
|
-
return
|
|
852
|
+
socket(pathOrMid, optionsOrMid, ...middleware) {
|
|
853
|
+
return this.handle("socket", pathOrMid, optionsOrMid, ...middleware);
|
|
649
854
|
}
|
|
650
|
-
|
|
651
|
-
return
|
|
855
|
+
get(pathOrMid, optionsOrMid, ...middleware) {
|
|
856
|
+
return this.handle("get", pathOrMid, optionsOrMid, ...middleware);
|
|
652
857
|
}
|
|
653
|
-
|
|
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
|
-
|
|
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
|
-
|
|
692
|
-
|
|
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
|
-
|
|
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
|
-
|
|
822
|
-
this.
|
|
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
|
-
|
|
827
|
-
|
|
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
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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
|
-
|
|
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
|
-
|
|
933
|
-
|
|
934
|
-
|
|
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/
|
|
945
|
-
|
|
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" ?
|
|
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
|
-
|
|
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,
|
|
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
|
|
984
|
-
if (
|
|
985
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
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
|
|
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
|
|
1048
|
-
ctx
|
|
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
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
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
|
-
|
|
1143
|
-
|
|
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:
|
|
1173
|
-
|
|
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
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1411
|
-
const
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
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
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
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
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
const
|
|
1442
|
-
const
|
|
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 =
|
|
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(
|
|
1499
|
+
const to = await target(redirects.login, "/", user, ctx);
|
|
1466
1500
|
if (inCookie(strategy)) {
|
|
1467
|
-
return cookies(
|
|
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(
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
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
|
-
|
|
1526
|
+
r2.post("/auth/logout", SPEC, async (ctx) => {
|
|
1531
1527
|
const payload = await read(ctx, strategy).catch(() => void 0);
|
|
1532
|
-
|
|
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(
|
|
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
|
|
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
|
|
1562
|
-
if (!
|
|
1573
|
+
let entry = cache2.get(issuer);
|
|
1574
|
+
if (!entry || refresh && Date.now() - entry.at > 6e4) {
|
|
1563
1575
|
const keys = (async () => {
|
|
1564
|
-
const
|
|
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, [
|
|
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
|
-
|
|
1580
|
-
cache2.set(issuer,
|
|
1592
|
+
entry = { at: Date.now(), keys };
|
|
1593
|
+
cache2.set(issuer, entry);
|
|
1581
1594
|
}
|
|
1582
|
-
return
|
|
1595
|
+
return entry.keys;
|
|
1583
1596
|
}
|
|
1584
|
-
|
|
1585
|
-
const
|
|
1586
|
-
|
|
1587
|
-
const
|
|
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 =
|
|
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] :
|
|
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
|
|
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
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
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
|
|
1631
|
-
if (
|
|
1632
|
-
const
|
|
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
|
|
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
|
|
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
|
-
|
|
1650
|
-
|
|
1646
|
+
unb64url(sig),
|
|
1647
|
+
enc3.encode(`${head}.${body}`)
|
|
1651
1648
|
);
|
|
1652
|
-
if (!ok) throw
|
|
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
|
|
1655
|
-
if (claims2.nbf && now < claims2.nbf) throw
|
|
1656
|
-
if (claims2.iss !== issuer) throw
|
|
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
|
|
1659
|
-
const
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
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
|
|
1755
|
-
if ("providers" in auth2) return
|
|
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
|
|
1755
|
+
"Invalid `auth`: it takes a string, a function, `{ providers }`, `{ issuer, audience }`, or a library instance."
|
|
1785
1756
|
);
|
|
1786
1757
|
}
|
|
1787
1758
|
|
|
1788
|
-
// src/
|
|
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/
|
|
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/
|
|
1857
|
+
// src/boot/secrets.ts
|
|
1898
1858
|
function resolveSecrets(option) {
|
|
1899
1859
|
const given = option ?? globalThis.env.SECRETS?.split(",");
|
|
1900
|
-
const list = (
|
|
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/
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
};
|
|
1923
|
-
const
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
}
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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 ? ` · ${escapeHtml(code)}` : ""} · ${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
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
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
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
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
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
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/
|
|
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 =
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
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
|
|
2891
|
+
var ValidationError = class extends errors_default {
|
|
2271
2892
|
source;
|
|
2272
2893
|
issues;
|
|
2273
2894
|
constructor(source, issues) {
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
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/
|
|
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/
|
|
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
|
-
|
|
2338
|
-
|
|
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.
|
|
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
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
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/
|
|
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/
|
|
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/
|
|
2715
|
-
async function
|
|
2716
|
-
if (
|
|
2717
|
-
|
|
2718
|
-
|
|
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/
|
|
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/
|
|
2902
|
-
var
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
"
|
|
2906
|
-
"
|
|
2907
|
-
|
|
2908
|
-
"
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
];
|
|
2912
|
-
|
|
2913
|
-
|
|
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/
|
|
2917
|
-
var
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
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
|
-
|
|
2925
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
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
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
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
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3041
|
-
|
|
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
|
-
|
|
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,
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
}
|
|
3069
|
-
|
|
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
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
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
|
-
|
|
3491
|
+
const res = await app.fetch(
|
|
3201
3492
|
new Request(url, {
|
|
3493
|
+
...options,
|
|
3202
3494
|
method,
|
|
3203
|
-
|
|
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
|
-
|
|
3581
|
+
errors_default as ServerError,
|
|
3283
3582
|
ValidationError,
|
|
3284
3583
|
default2 as bucket,
|
|
3285
3584
|
cache,
|