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