shokupan 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -8
- package/dist/cli.cjs +1 -1
- package/dist/cli.js +1 -1
- package/dist/context.d.ts +90 -7
- package/dist/index.cjs +746 -453
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +690 -419
- package/dist/index.js.map +1 -1
- package/dist/json-parser-B3dnQmCC.js +35 -0
- package/dist/json-parser-B3dnQmCC.js.map +1 -0
- package/dist/json-parser-COdZ0fqY.cjs +35 -0
- package/dist/json-parser-COdZ0fqY.cjs.map +1 -0
- package/dist/{openapi-analyzer-z-7AoFRC.cjs → openapi-analyzer-Bei1sVWp.cjs} +33 -16
- package/dist/openapi-analyzer-Bei1sVWp.cjs.map +1 -0
- package/dist/{openapi-analyzer-D7y6Qa38.js → openapi-analyzer-Ce_7JxZh.js} +33 -16
- package/dist/openapi-analyzer-Ce_7JxZh.js.map +1 -0
- package/dist/plugins/proxy.d.ts +2 -0
- package/dist/plugins/rate-limit.d.ts +1 -0
- package/dist/plugins/scalar.d.ts +1 -1
- package/dist/router.d.ts +125 -55
- package/dist/{server-adapter-BWrEJbKL.js → server-adapter-0xH174zz.js} +4 -2
- package/dist/server-adapter-0xH174zz.js.map +1 -0
- package/dist/{server-adapter-fVKP60e0.cjs → server-adapter-DFhwlK8e.cjs} +4 -2
- package/dist/server-adapter-DFhwlK8e.cjs.map +1 -0
- package/dist/shokupan.d.ts +66 -7
- package/dist/types.d.ts +63 -3
- package/dist/util/datastore.d.ts +6 -0
- package/dist/util/json-parser.d.ts +12 -0
- package/dist/util/plugin-deps.d.ts +25 -0
- package/package.json +73 -13
- package/dist/buntest.d.ts +0 -1
- package/dist/openapi-analyzer-D7y6Qa38.js.map +0 -1
- package/dist/openapi-analyzer-z-7AoFRC.cjs.map +0 -1
- package/dist/server-adapter-BWrEJbKL.js.map +0 -1
- package/dist/server-adapter-fVKP60e0.cjs.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,12 +1,32 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
2
24
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
25
|
const promises = require("node:fs/promises");
|
|
4
26
|
const eta$2 = require("eta");
|
|
5
27
|
const promises$1 = require("fs/promises");
|
|
6
28
|
const path = require("path");
|
|
7
29
|
const node_async_hooks = require("node:async_hooks");
|
|
8
|
-
const node = require("@surrealdb/node");
|
|
9
|
-
const surrealdb = require("surrealdb");
|
|
10
30
|
const api = require("@opentelemetry/api");
|
|
11
31
|
const os = require("node:os");
|
|
12
32
|
const arctic = require("arctic");
|
|
@@ -14,9 +34,7 @@ const jose = require("jose");
|
|
|
14
34
|
const zlib = require("node:zlib");
|
|
15
35
|
const Ajv = require("ajv");
|
|
16
36
|
const addFormats = require("ajv-formats");
|
|
17
|
-
const
|
|
18
|
-
const classValidator = require("class-validator");
|
|
19
|
-
const openapiAnalyzer = require("./openapi-analyzer-z-7AoFRC.cjs");
|
|
37
|
+
const openapiAnalyzer = require("./openapi-analyzer-Bei1sVWp.cjs");
|
|
20
38
|
const crypto = require("crypto");
|
|
21
39
|
const events = require("events");
|
|
22
40
|
function _interopNamespaceDefault(e) {
|
|
@@ -101,8 +119,82 @@ class ShokupanResponse {
|
|
|
101
119
|
return this._headers !== null;
|
|
102
120
|
}
|
|
103
121
|
}
|
|
122
|
+
function isValidCookieDomain(domain, currentHost) {
|
|
123
|
+
const hostWithoutPort = currentHost.split(":")[0];
|
|
124
|
+
if (domain === hostWithoutPort) return true;
|
|
125
|
+
if (domain.startsWith(".")) {
|
|
126
|
+
const domainWithoutDot = domain.slice(1);
|
|
127
|
+
return hostWithoutPort.endsWith(domainWithoutDot);
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
const VALID_HTTP_STATUSES = /* @__PURE__ */ new Set([
|
|
132
|
+
100,
|
|
133
|
+
101,
|
|
134
|
+
102,
|
|
135
|
+
103,
|
|
136
|
+
200,
|
|
137
|
+
201,
|
|
138
|
+
202,
|
|
139
|
+
203,
|
|
140
|
+
204,
|
|
141
|
+
205,
|
|
142
|
+
206,
|
|
143
|
+
207,
|
|
144
|
+
208,
|
|
145
|
+
226,
|
|
146
|
+
300,
|
|
147
|
+
301,
|
|
148
|
+
302,
|
|
149
|
+
303,
|
|
150
|
+
304,
|
|
151
|
+
305,
|
|
152
|
+
306,
|
|
153
|
+
307,
|
|
154
|
+
308,
|
|
155
|
+
400,
|
|
156
|
+
401,
|
|
157
|
+
402,
|
|
158
|
+
403,
|
|
159
|
+
404,
|
|
160
|
+
405,
|
|
161
|
+
406,
|
|
162
|
+
407,
|
|
163
|
+
408,
|
|
164
|
+
409,
|
|
165
|
+
410,
|
|
166
|
+
411,
|
|
167
|
+
412,
|
|
168
|
+
413,
|
|
169
|
+
414,
|
|
170
|
+
415,
|
|
171
|
+
416,
|
|
172
|
+
417,
|
|
173
|
+
418,
|
|
174
|
+
421,
|
|
175
|
+
422,
|
|
176
|
+
423,
|
|
177
|
+
424,
|
|
178
|
+
425,
|
|
179
|
+
426,
|
|
180
|
+
428,
|
|
181
|
+
429,
|
|
182
|
+
431,
|
|
183
|
+
451,
|
|
184
|
+
500,
|
|
185
|
+
501,
|
|
186
|
+
502,
|
|
187
|
+
503,
|
|
188
|
+
504,
|
|
189
|
+
505,
|
|
190
|
+
506,
|
|
191
|
+
507,
|
|
192
|
+
508,
|
|
193
|
+
510,
|
|
194
|
+
511
|
|
195
|
+
]);
|
|
196
|
+
const VALID_REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
104
197
|
class ShokupanContext {
|
|
105
|
-
// Raw body for compression optimization
|
|
106
198
|
constructor(request, server, state, app, signal, enableMiddlewareTracking = false) {
|
|
107
199
|
this.request = request;
|
|
108
200
|
this.server = server;
|
|
@@ -125,7 +217,6 @@ class ShokupanContext {
|
|
|
125
217
|
}
|
|
126
218
|
this.response = new ShokupanResponse();
|
|
127
219
|
}
|
|
128
|
-
_url;
|
|
129
220
|
params = {};
|
|
130
221
|
// Router assigns this, but default to empty object
|
|
131
222
|
state;
|
|
@@ -134,6 +225,19 @@ class ShokupanContext {
|
|
|
134
225
|
_debug;
|
|
135
226
|
_finalResponse;
|
|
136
227
|
_rawBody;
|
|
228
|
+
// Raw body for compression optimization
|
|
229
|
+
// Body caching to avoid double parsing
|
|
230
|
+
_url;
|
|
231
|
+
_cachedBody;
|
|
232
|
+
_bodyType;
|
|
233
|
+
_bodyParsed = false;
|
|
234
|
+
_bodyParseError;
|
|
235
|
+
// Cached URL properties to avoid repeated parsing
|
|
236
|
+
_cachedHostname;
|
|
237
|
+
_cachedProtocol;
|
|
238
|
+
_cachedHost;
|
|
239
|
+
_cachedOrigin;
|
|
240
|
+
_cachedQuery;
|
|
137
241
|
get url() {
|
|
138
242
|
if (!this._url) {
|
|
139
243
|
const urlString = this.request.url || "http://localhost/";
|
|
@@ -182,16 +286,24 @@ class ShokupanContext {
|
|
|
182
286
|
* Request query params
|
|
183
287
|
*/
|
|
184
288
|
get query() {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
289
|
+
if (this._cachedQuery) return this._cachedQuery;
|
|
290
|
+
const q = /* @__PURE__ */ Object.create(null);
|
|
291
|
+
const blocklist = ["__proto__", "constructor", "prototype"];
|
|
292
|
+
const entries = Object.entries(this.url.searchParams);
|
|
293
|
+
for (let i = 0; i < entries.length; i++) {
|
|
294
|
+
const [key, value] = entries[i];
|
|
295
|
+
if (blocklist.includes(key)) continue;
|
|
296
|
+
if (Object.prototype.hasOwnProperty.call(q, key)) {
|
|
297
|
+
if (Array.isArray(q[key])) {
|
|
298
|
+
q[key].push(value);
|
|
299
|
+
} else {
|
|
300
|
+
q[key] = [q[key], value];
|
|
301
|
+
}
|
|
191
302
|
} else {
|
|
192
|
-
q[key] =
|
|
303
|
+
q[key] = value;
|
|
193
304
|
}
|
|
194
305
|
}
|
|
306
|
+
this._cachedQuery = q;
|
|
195
307
|
return q;
|
|
196
308
|
}
|
|
197
309
|
/**
|
|
@@ -204,31 +316,31 @@ class ShokupanContext {
|
|
|
204
316
|
* Request hostname (e.g. "localhost")
|
|
205
317
|
*/
|
|
206
318
|
get hostname() {
|
|
207
|
-
return this.url.hostname;
|
|
319
|
+
return this._cachedHostname ??= this.url.hostname;
|
|
208
320
|
}
|
|
209
321
|
/**
|
|
210
322
|
* Request host (e.g. "localhost:3000")
|
|
211
323
|
*/
|
|
212
324
|
get host() {
|
|
213
|
-
return this.url.host;
|
|
325
|
+
return this._cachedHost ??= this.url.host;
|
|
214
326
|
}
|
|
215
327
|
/**
|
|
216
328
|
* Request protocol (e.g. "http:", "https:")
|
|
217
329
|
*/
|
|
218
330
|
get protocol() {
|
|
219
|
-
return this.url.protocol;
|
|
331
|
+
return this._cachedProtocol ??= this.url.protocol;
|
|
220
332
|
}
|
|
221
333
|
/**
|
|
222
334
|
* Whether request is secure (https)
|
|
223
335
|
*/
|
|
224
336
|
get secure() {
|
|
225
|
-
return this.
|
|
337
|
+
return this.protocol === "https:";
|
|
226
338
|
}
|
|
227
339
|
/**
|
|
228
340
|
* Request origin (e.g. "http://localhost:3000")
|
|
229
341
|
*/
|
|
230
342
|
get origin() {
|
|
231
|
-
return this.url.origin;
|
|
343
|
+
return this._cachedOrigin ??= this.url.origin;
|
|
232
344
|
}
|
|
233
345
|
/**
|
|
234
346
|
* Request headers
|
|
@@ -265,6 +377,12 @@ class ShokupanContext {
|
|
|
265
377
|
* @param options Cookie options
|
|
266
378
|
*/
|
|
267
379
|
setCookie(name, value, options = {}) {
|
|
380
|
+
if (options.domain) {
|
|
381
|
+
const currentHost = this.hostname;
|
|
382
|
+
if (!isValidCookieDomain(options.domain, currentHost)) {
|
|
383
|
+
throw new Error(`Invalid cookie domain: ${options.domain} for host ${currentHost}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
268
386
|
let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
|
|
269
387
|
if (options.maxAge) cookie += `; Max-Age=${Math.floor(options.maxAge)}`;
|
|
270
388
|
if (options.domain) cookie += `; Domain=${options.domain}`;
|
|
@@ -324,6 +442,91 @@ class ShokupanContext {
|
|
|
324
442
|
}
|
|
325
443
|
return h;
|
|
326
444
|
}
|
|
445
|
+
/**
|
|
446
|
+
* Read request body with caching to avoid double parsing.
|
|
447
|
+
* The body is only parsed once and cached for subsequent reads.
|
|
448
|
+
*/
|
|
449
|
+
async body() {
|
|
450
|
+
if (this._bodyParseError) {
|
|
451
|
+
throw this._bodyParseError;
|
|
452
|
+
}
|
|
453
|
+
if (this._bodyParsed) {
|
|
454
|
+
return this._cachedBody;
|
|
455
|
+
}
|
|
456
|
+
const contentType = this.request.headers.get("content-type") || "";
|
|
457
|
+
if (contentType.includes("application/json") || contentType.includes("+json")) {
|
|
458
|
+
const rawText = await this.readRawBody();
|
|
459
|
+
const parserType = this.app?.applicationConfig?.jsonParser || "native";
|
|
460
|
+
if (parserType === "native") {
|
|
461
|
+
this._cachedBody = JSON.parse(rawText);
|
|
462
|
+
} else {
|
|
463
|
+
const { getJSONParser } = await Promise.resolve().then(() => require("./json-parser-COdZ0fqY.cjs"));
|
|
464
|
+
const parser = getJSONParser(parserType);
|
|
465
|
+
this._cachedBody = parser(rawText);
|
|
466
|
+
}
|
|
467
|
+
this._bodyType = "json";
|
|
468
|
+
} else if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
|
|
469
|
+
this._cachedBody = await this.request.formData();
|
|
470
|
+
this._bodyType = "formData";
|
|
471
|
+
} else {
|
|
472
|
+
this._cachedBody = await this.readRawBody();
|
|
473
|
+
this._bodyType = "text";
|
|
474
|
+
}
|
|
475
|
+
this._bodyParsed = true;
|
|
476
|
+
return this._cachedBody;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Pre-parse the request body before handler execution.
|
|
480
|
+
* This improves performance and enables Node.js compatibility for large payloads.
|
|
481
|
+
* Errors are deferred until the body is actually accessed in the handler.
|
|
482
|
+
*/
|
|
483
|
+
async parseBody() {
|
|
484
|
+
if (this._bodyParsed) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if (this.request.method === "GET" || this.request.method === "HEAD") {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
try {
|
|
491
|
+
await this.body();
|
|
492
|
+
} catch (error) {
|
|
493
|
+
this._bodyParseError = error;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Read raw body from ReadableStream efficiently.
|
|
498
|
+
* This is much faster than request.text() for large payloads.
|
|
499
|
+
* Also handles the case where body is already a string (e.g., in tests).
|
|
500
|
+
*/
|
|
501
|
+
async readRawBody() {
|
|
502
|
+
if (typeof this.request.body === "string") {
|
|
503
|
+
return this.request.body;
|
|
504
|
+
}
|
|
505
|
+
const reader = this.request.body?.getReader();
|
|
506
|
+
if (!reader) {
|
|
507
|
+
return "";
|
|
508
|
+
}
|
|
509
|
+
const chunks = [];
|
|
510
|
+
let totalSize = 0;
|
|
511
|
+
try {
|
|
512
|
+
while (true) {
|
|
513
|
+
const { done, value } = await reader.read();
|
|
514
|
+
if (done) break;
|
|
515
|
+
chunks.push(value);
|
|
516
|
+
totalSize += value.length;
|
|
517
|
+
}
|
|
518
|
+
} finally {
|
|
519
|
+
reader.releaseLock();
|
|
520
|
+
}
|
|
521
|
+
const result = new Uint8Array(totalSize);
|
|
522
|
+
let offset = 0;
|
|
523
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
524
|
+
const chunk = chunks[i];
|
|
525
|
+
result.set(chunk, offset);
|
|
526
|
+
offset += chunk.length;
|
|
527
|
+
}
|
|
528
|
+
return new TextDecoder().decode(result);
|
|
529
|
+
}
|
|
327
530
|
/**
|
|
328
531
|
* Send a response
|
|
329
532
|
* @param body Response body
|
|
@@ -332,31 +535,24 @@ class ShokupanContext {
|
|
|
332
535
|
*/
|
|
333
536
|
send(body, options) {
|
|
334
537
|
const headers = this.mergeHeaders(options?.headers);
|
|
335
|
-
const status = options?.status ?? this.response.status;
|
|
538
|
+
const status = options?.status ?? this.response.status ?? 200;
|
|
539
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
|
|
540
|
+
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
541
|
+
}
|
|
336
542
|
if (typeof body === "string" || body instanceof ArrayBuffer || body instanceof Uint8Array) {
|
|
337
543
|
this._rawBody = body;
|
|
338
544
|
}
|
|
339
545
|
this._finalResponse = new Response(body, { status, headers });
|
|
340
546
|
return this._finalResponse;
|
|
341
547
|
}
|
|
342
|
-
/**
|
|
343
|
-
* Read request body
|
|
344
|
-
*/
|
|
345
|
-
async body() {
|
|
346
|
-
const contentType = this.request.headers.get("content-type") || "";
|
|
347
|
-
if (contentType.includes("application/json") || contentType.includes("+json")) {
|
|
348
|
-
return this.request.json();
|
|
349
|
-
}
|
|
350
|
-
if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
|
|
351
|
-
return this.request.formData();
|
|
352
|
-
}
|
|
353
|
-
return this.request.text();
|
|
354
|
-
}
|
|
355
548
|
/**
|
|
356
549
|
* Respond with a JSON object
|
|
357
550
|
*/
|
|
358
551
|
json(data, status, headers) {
|
|
359
|
-
const finalStatus = status ?? this.response.status;
|
|
552
|
+
const finalStatus = status ?? this.response.status ?? 200;
|
|
553
|
+
if (!VALID_HTTP_STATUSES.has(finalStatus)) {
|
|
554
|
+
throw new Error(`Invalid HTTP status code: ${finalStatus}`);
|
|
555
|
+
}
|
|
360
556
|
const jsonString = JSON.stringify(data);
|
|
361
557
|
this._rawBody = jsonString;
|
|
362
558
|
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
@@ -375,7 +571,10 @@ class ShokupanContext {
|
|
|
375
571
|
* Respond with a text string
|
|
376
572
|
*/
|
|
377
573
|
text(data, status, headers) {
|
|
378
|
-
const finalStatus = status ?? this.response.status;
|
|
574
|
+
const finalStatus = status ?? this.response.status ?? 200;
|
|
575
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
|
|
576
|
+
throw new Error(`Invalid HTTP status code: ${finalStatus}`);
|
|
577
|
+
}
|
|
379
578
|
this._rawBody = data;
|
|
380
579
|
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
381
580
|
this._finalResponse = new Response(data, {
|
|
@@ -393,7 +592,10 @@ class ShokupanContext {
|
|
|
393
592
|
* Respond with HTML content
|
|
394
593
|
*/
|
|
395
594
|
html(html, status, headers) {
|
|
396
|
-
const finalStatus = status ?? this.response.status;
|
|
595
|
+
const finalStatus = status ?? this.response.status ?? 200;
|
|
596
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
|
|
597
|
+
throw new Error(`Invalid HTTP status code: ${finalStatus}`);
|
|
598
|
+
}
|
|
397
599
|
const finalHeaders = this.mergeHeaders(headers);
|
|
398
600
|
finalHeaders.set("content-type", "text/html; charset=utf-8");
|
|
399
601
|
this._rawBody = html;
|
|
@@ -404,6 +606,9 @@ class ShokupanContext {
|
|
|
404
606
|
* Respond with a redirect
|
|
405
607
|
*/
|
|
406
608
|
redirect(url, status = 302) {
|
|
609
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_REDIRECT_STATUSES.has(status)) {
|
|
610
|
+
throw new Error(`Invalid redirect status code: ${status}`);
|
|
611
|
+
}
|
|
407
612
|
const headers = this.mergeHeaders();
|
|
408
613
|
headers.set("Location", url);
|
|
409
614
|
this._finalResponse = new Response(null, { status, headers });
|
|
@@ -414,6 +619,9 @@ class ShokupanContext {
|
|
|
414
619
|
* DOES NOT CHAIN!
|
|
415
620
|
*/
|
|
416
621
|
status(status) {
|
|
622
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
|
|
623
|
+
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
624
|
+
}
|
|
417
625
|
const headers = this.mergeHeaders();
|
|
418
626
|
this._finalResponse = new Response(null, { status, headers });
|
|
419
627
|
return this._finalResponse;
|
|
@@ -424,6 +632,9 @@ class ShokupanContext {
|
|
|
424
632
|
async file(path2, fileOptions, responseOptions) {
|
|
425
633
|
const headers = this.mergeHeaders(responseOptions?.headers);
|
|
426
634
|
const status = responseOptions?.status ?? this.response.status;
|
|
635
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
|
|
636
|
+
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
637
|
+
}
|
|
427
638
|
if (typeof Bun !== "undefined") {
|
|
428
639
|
this._finalResponse = new Response(Bun.file(path2, fileOptions), { status, headers });
|
|
429
640
|
return this._finalResponse;
|
|
@@ -447,6 +658,10 @@ class ShokupanContext {
|
|
|
447
658
|
* @param headers HTTP Headers
|
|
448
659
|
*/
|
|
449
660
|
async jsx(element, args, status, headers) {
|
|
661
|
+
status ??= 200;
|
|
662
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
|
|
663
|
+
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
664
|
+
}
|
|
450
665
|
if (!this.renderer) {
|
|
451
666
|
throw new Error("No JSX renderer configured");
|
|
452
667
|
}
|
|
@@ -461,17 +676,32 @@ function RateLimitMiddleware(options = {}) {
|
|
|
461
676
|
const statusCode = options.statusCode || 429;
|
|
462
677
|
const headers = options.headers !== false;
|
|
463
678
|
const mode = options.mode || "user";
|
|
679
|
+
const trustedProxies = options.trustedProxies || [];
|
|
464
680
|
const keyGenerator = options.keyGenerator || ((ctx) => {
|
|
465
681
|
if (mode === "absolute") {
|
|
466
682
|
return "global";
|
|
467
683
|
}
|
|
468
|
-
|
|
684
|
+
const xForwardedFor = ctx.headers.get("x-forwarded-for");
|
|
685
|
+
if (xForwardedFor && trustedProxies.length > 0) {
|
|
686
|
+
const ips = xForwardedFor.split(",").map((ip) => ip.trim());
|
|
687
|
+
for (let i = ips.length - 1; i >= 0; i--) {
|
|
688
|
+
const ip = ips[i];
|
|
689
|
+
if (!trustedProxies.includes(ip)) {
|
|
690
|
+
if (/^[\d.:a-fA-F]+$/.test(ip)) {
|
|
691
|
+
return ip;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return ctx.server?.requestIP?.(ctx.request)?.address || "unknown";
|
|
469
697
|
});
|
|
470
698
|
const skip = options.skip || (() => false);
|
|
471
699
|
const hits = /* @__PURE__ */ new Map();
|
|
472
700
|
const interval = setInterval(() => {
|
|
473
701
|
const now = Date.now();
|
|
474
|
-
|
|
702
|
+
const entries = Array.from(hits.entries());
|
|
703
|
+
for (let i = 0; i < entries.length; i++) {
|
|
704
|
+
const [key, record] = entries[i];
|
|
475
705
|
if (record.resetTime <= now) {
|
|
476
706
|
hits.delete(key);
|
|
477
707
|
}
|
|
@@ -724,7 +954,9 @@ function deepMerge(target, ...sources) {
|
|
|
724
954
|
if (!sources.length) return target;
|
|
725
955
|
const source = sources.shift();
|
|
726
956
|
if (isObject(target) && isObject(source)) {
|
|
727
|
-
|
|
957
|
+
const sourceKeys = Object.keys(source);
|
|
958
|
+
for (let i = 0; i < sourceKeys.length; i++) {
|
|
959
|
+
const key = sourceKeys[i];
|
|
728
960
|
if (isObject(source[key])) {
|
|
729
961
|
if (!target[key]) Object.assign(target, { [key]: {} });
|
|
730
962
|
deepMerge(target[key], source[key]);
|
|
@@ -748,15 +980,17 @@ function deepMerge(target, ...sources) {
|
|
|
748
980
|
}
|
|
749
981
|
return deepMerge(target, ...sources);
|
|
750
982
|
}
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
983
|
+
const REGEX_PATTERNS = {
|
|
984
|
+
QUERY_INT: /parseInt\(ctx\.query\.(\w+)\)/g,
|
|
985
|
+
QUERY_FLOAT: /parseFloat\(ctx\.query\.(\w+)\)/g,
|
|
986
|
+
QUERY_NUMBER: /Number\(ctx\.query\.(\w+)\)/g,
|
|
987
|
+
QUERY_BOOL: /(?:Boolean\(ctx\.query\.(\w+)\)|!+ctx\.query\.(\w+))/g,
|
|
988
|
+
QUERY_GENERIC: /ctx\.query\.(\w+)/g,
|
|
989
|
+
PARAM_INT: /parseInt\(ctx\.params\.(\w+)\)/g,
|
|
990
|
+
PARAM_FLOAT: /parseFloat\(ctx\.params\.(\w+)\)/g,
|
|
991
|
+
HEADER_GET: /ctx\.get\(['"](\w+)['"]\)/g,
|
|
992
|
+
ERROR_STATUS: /ctx\.(?:json|text|html)\([^)]+,\s*(\d{3,})\)/g
|
|
993
|
+
};
|
|
760
994
|
function analyzeHandler(handler) {
|
|
761
995
|
const handlerSource = handler.toString();
|
|
762
996
|
const inferredSpec = {};
|
|
@@ -766,29 +1000,20 @@ function analyzeHandler(handler) {
|
|
|
766
1000
|
};
|
|
767
1001
|
}
|
|
768
1002
|
const queryParams = /* @__PURE__ */ new Map();
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
if (match[1] && !queryParams.has(match[1])) {
|
|
777
|
-
queryParams.set(match[1], { type: "number" });
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
for (const match of handlerSource.matchAll(REGEX_QUERY_BOOL)) {
|
|
781
|
-
const name = match[1] || match[2];
|
|
782
|
-
if (name && !queryParams.has(name)) {
|
|
783
|
-
queryParams.set(name, { type: "boolean" });
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
for (const match of handlerSource.matchAll(REGEX_QUERY_GENERIC)) {
|
|
787
|
-
const name = match[1];
|
|
788
|
-
if (name && !queryParams.has(name)) {
|
|
789
|
-
queryParams.set(name, { type: "string" });
|
|
1003
|
+
const processMatches = (regex, type, format) => {
|
|
1004
|
+
const matches = Array.from(handlerSource.matchAll(regex));
|
|
1005
|
+
for (const match of matches) {
|
|
1006
|
+
const name = match[1] || match[2];
|
|
1007
|
+
if (name && !queryParams.has(name)) {
|
|
1008
|
+
queryParams.set(name, { type, format });
|
|
1009
|
+
}
|
|
790
1010
|
}
|
|
791
|
-
}
|
|
1011
|
+
};
|
|
1012
|
+
processMatches(REGEX_PATTERNS.QUERY_INT, "integer", "int32");
|
|
1013
|
+
processMatches(REGEX_PATTERNS.QUERY_FLOAT, "number", "float");
|
|
1014
|
+
processMatches(REGEX_PATTERNS.QUERY_NUMBER, "number");
|
|
1015
|
+
processMatches(REGEX_PATTERNS.QUERY_BOOL, "boolean");
|
|
1016
|
+
processMatches(REGEX_PATTERNS.QUERY_GENERIC, "string");
|
|
792
1017
|
if (queryParams.size > 0) {
|
|
793
1018
|
if (!inferredSpec.parameters) inferredSpec.parameters = [];
|
|
794
1019
|
queryParams.forEach((schema, paramName) => {
|
|
@@ -800,12 +1025,15 @@ function analyzeHandler(handler) {
|
|
|
800
1025
|
});
|
|
801
1026
|
}
|
|
802
1027
|
const pathParams = /* @__PURE__ */ new Map();
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
1028
|
+
const processPathMatches = (regex, type, format) => {
|
|
1029
|
+
const matches = Array.from(handlerSource.matchAll(regex));
|
|
1030
|
+
for (const match of matches) {
|
|
1031
|
+
const name = match[1];
|
|
1032
|
+
if (name) pathParams.set(name, { type, format });
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
processPathMatches(REGEX_PATTERNS.PARAM_INT, "integer", "int32");
|
|
1036
|
+
processPathMatches(REGEX_PATTERNS.PARAM_FLOAT, "number", "float");
|
|
809
1037
|
if (pathParams.size > 0) {
|
|
810
1038
|
if (!inferredSpec.parameters) inferredSpec.parameters = [];
|
|
811
1039
|
pathParams.forEach((schema, paramName) => {
|
|
@@ -817,7 +1045,8 @@ function analyzeHandler(handler) {
|
|
|
817
1045
|
});
|
|
818
1046
|
});
|
|
819
1047
|
}
|
|
820
|
-
|
|
1048
|
+
const headerMatches = Array.from(handlerSource.matchAll(REGEX_PATTERNS.HEADER_GET));
|
|
1049
|
+
for (const match of headerMatches) {
|
|
821
1050
|
if (match[1]) {
|
|
822
1051
|
if (!inferredSpec.parameters) inferredSpec.parameters = [];
|
|
823
1052
|
inferredSpec.parameters.push({
|
|
@@ -836,13 +1065,19 @@ function analyzeHandler(handler) {
|
|
|
836
1065
|
}
|
|
837
1066
|
if (handlerSource.includes("ctx.html(")) {
|
|
838
1067
|
responses["200"] = {
|
|
839
|
-
description: "Successful response",
|
|
1068
|
+
description: "Successful HTML response",
|
|
1069
|
+
content: { "text/html": { schema: { type: "string" } } }
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
if (handlerSource.includes("ctx.jsx(")) {
|
|
1073
|
+
responses["200"] = {
|
|
1074
|
+
description: "Successful HTML response (Rendered JSX)",
|
|
840
1075
|
content: { "text/html": { schema: { type: "string" } } }
|
|
841
1076
|
};
|
|
842
1077
|
}
|
|
843
1078
|
if (handlerSource.includes("ctx.text(")) {
|
|
844
1079
|
responses["200"] = {
|
|
845
|
-
description: "Successful response",
|
|
1080
|
+
description: "Successful text response",
|
|
846
1081
|
content: { "text/plain": { schema: { type: "string" } } }
|
|
847
1082
|
};
|
|
848
1083
|
}
|
|
@@ -853,7 +1088,18 @@ function analyzeHandler(handler) {
|
|
|
853
1088
|
};
|
|
854
1089
|
}
|
|
855
1090
|
if (handlerSource.includes("ctx.redirect(")) {
|
|
856
|
-
|
|
1091
|
+
let hasSpecificRedirect = false;
|
|
1092
|
+
const redirectMatches = Array.from(handlerSource.matchAll(/ctx\.redirect\([^,]+,\s*(\d{3})\)/g));
|
|
1093
|
+
for (const match of redirectMatches) {
|
|
1094
|
+
const status = match[1];
|
|
1095
|
+
if (/^30[12378]$/.test(status)) {
|
|
1096
|
+
responses[status] = { description: `Redirect (${status})` };
|
|
1097
|
+
hasSpecificRedirect = true;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
if (!hasSpecificRedirect) {
|
|
1101
|
+
responses["302"] = { description: "Redirect" };
|
|
1102
|
+
}
|
|
857
1103
|
}
|
|
858
1104
|
if (!responses["200"] && /return\s+\{/.test(handlerSource)) {
|
|
859
1105
|
responses["200"] = {
|
|
@@ -861,7 +1107,8 @@ function analyzeHandler(handler) {
|
|
|
861
1107
|
content: { "application/json": { schema: { type: "object" } } }
|
|
862
1108
|
};
|
|
863
1109
|
}
|
|
864
|
-
|
|
1110
|
+
const errorStatusMatches = Array.from(handlerSource.matchAll(REGEX_PATTERNS.ERROR_STATUS));
|
|
1111
|
+
for (const match of errorStatusMatches) {
|
|
865
1112
|
const statusCode = match[1];
|
|
866
1113
|
if (statusCode && statusCode !== "200") {
|
|
867
1114
|
responses[statusCode] = { description: `Error response (${statusCode})` };
|
|
@@ -872,6 +1119,52 @@ function analyzeHandler(handler) {
|
|
|
872
1119
|
}
|
|
873
1120
|
return { inferredSpec };
|
|
874
1121
|
}
|
|
1122
|
+
async function getAstRoutes(applications) {
|
|
1123
|
+
const astRoutes = [];
|
|
1124
|
+
const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
|
|
1125
|
+
if (seen.has(app.name)) return [];
|
|
1126
|
+
const newSeen = new Set(seen);
|
|
1127
|
+
newSeen.add(app.name);
|
|
1128
|
+
const expanded = [];
|
|
1129
|
+
for (const route of app.routes) {
|
|
1130
|
+
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1131
|
+
const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
|
|
1132
|
+
let joined = cleanPrefix + cleanPath;
|
|
1133
|
+
if (joined.length > 1 && joined.endsWith("/")) {
|
|
1134
|
+
joined = joined.slice(0, -1);
|
|
1135
|
+
}
|
|
1136
|
+
expanded.push({
|
|
1137
|
+
...route,
|
|
1138
|
+
path: joined || "/"
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
if (app.mounted) {
|
|
1142
|
+
for (const mount of app.mounted) {
|
|
1143
|
+
const targetApp = applications.find((a) => a.name === mount.target || a.className === mount.target);
|
|
1144
|
+
if (targetApp) {
|
|
1145
|
+
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1146
|
+
const mountPrefix = mount.prefix.startsWith("/") ? mount.prefix : "/" + mount.prefix;
|
|
1147
|
+
expanded.push(...getExpandedRoutes(targetApp, cleanPrefix + mountPrefix, newSeen));
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
return expanded;
|
|
1152
|
+
};
|
|
1153
|
+
applications.forEach((app) => {
|
|
1154
|
+
astRoutes.push(...getExpandedRoutes(app));
|
|
1155
|
+
});
|
|
1156
|
+
const dedupedRoutes = /* @__PURE__ */ new Map();
|
|
1157
|
+
for (const route of astRoutes) {
|
|
1158
|
+
const key = `${route.method.toUpperCase()}:${route.path}`;
|
|
1159
|
+
let score = 0;
|
|
1160
|
+
if (route.responseSchema) score += 10;
|
|
1161
|
+
if (route.handlerSource) score += 5;
|
|
1162
|
+
if (!dedupedRoutes.has(key) || score > dedupedRoutes.get(key).score) {
|
|
1163
|
+
dedupedRoutes.set(key, { route, score });
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
return Array.from(dedupedRoutes.values()).map((v) => v.route);
|
|
1167
|
+
}
|
|
875
1168
|
async function generateOpenApi(rootRouter, options = {}) {
|
|
876
1169
|
const paths = {};
|
|
877
1170
|
const tagGroups = /* @__PURE__ */ new Map();
|
|
@@ -879,61 +1172,11 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
879
1172
|
const defaultTagName = options.defaultTag || "Application";
|
|
880
1173
|
let astRoutes = [];
|
|
881
1174
|
try {
|
|
882
|
-
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./openapi-analyzer-
|
|
1175
|
+
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./openapi-analyzer-Bei1sVWp.cjs"));
|
|
883
1176
|
const analyzer = new OpenAPIAnalyzer(process.cwd());
|
|
884
1177
|
const { applications } = await analyzer.analyze();
|
|
885
|
-
|
|
886
|
-
applications.forEach((app) => {
|
|
887
|
-
appMap.set(app.name, app);
|
|
888
|
-
if (app.name !== app.className) {
|
|
889
|
-
appMap.set(app.className, app);
|
|
890
|
-
}
|
|
891
|
-
});
|
|
892
|
-
const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
|
|
893
|
-
if (seen.has(app.name)) return [];
|
|
894
|
-
const newSeen = new Set(seen);
|
|
895
|
-
newSeen.add(app.name);
|
|
896
|
-
const expanded = [];
|
|
897
|
-
for (const route of app.routes) {
|
|
898
|
-
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
899
|
-
const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
|
|
900
|
-
let joined = cleanPrefix + cleanPath;
|
|
901
|
-
if (joined.length > 1 && joined.endsWith("/")) {
|
|
902
|
-
joined = joined.slice(0, -1);
|
|
903
|
-
}
|
|
904
|
-
expanded.push({
|
|
905
|
-
...route,
|
|
906
|
-
path: joined || "/"
|
|
907
|
-
});
|
|
908
|
-
}
|
|
909
|
-
if (app.mounted) {
|
|
910
|
-
for (const mount of app.mounted) {
|
|
911
|
-
const targetApp = appMap.get(mount.target);
|
|
912
|
-
if (targetApp) {
|
|
913
|
-
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
914
|
-
const mountPrefix = mount.prefix.startsWith("/") ? mount.prefix : "/" + mount.prefix;
|
|
915
|
-
expanded.push(...getExpandedRoutes(targetApp, cleanPrefix + mountPrefix, newSeen));
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
return expanded;
|
|
920
|
-
};
|
|
921
|
-
applications.forEach((app) => {
|
|
922
|
-
astRoutes.push(...getExpandedRoutes(app));
|
|
923
|
-
});
|
|
924
|
-
const dedupedRoutes = /* @__PURE__ */ new Map();
|
|
925
|
-
for (const route of astRoutes) {
|
|
926
|
-
const key = `${route.method.toUpperCase()}:${route.path}`;
|
|
927
|
-
let score = 0;
|
|
928
|
-
if (route.responseSchema) score += 10;
|
|
929
|
-
if (route.handlerSource) score += 5;
|
|
930
|
-
if (!dedupedRoutes.has(key) || score > dedupedRoutes.get(key).score) {
|
|
931
|
-
dedupedRoutes.set(key, { route, score });
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
astRoutes = Array.from(dedupedRoutes.values()).map((v) => v.route);
|
|
1178
|
+
astRoutes = await getAstRoutes(applications);
|
|
935
1179
|
} catch (e) {
|
|
936
|
-
console.warn("OpenAPI AST analysis failed or skipped:", e);
|
|
937
1180
|
}
|
|
938
1181
|
const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName) => {
|
|
939
1182
|
let group = currentGroup;
|
|
@@ -990,33 +1233,15 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
990
1233
|
(r) => r.method.toUpperCase() === route.method.toUpperCase() && r.path === fullPath
|
|
991
1234
|
);
|
|
992
1235
|
if (!astMatch) {
|
|
993
|
-
|
|
994
|
-
if (route.handler.originalHandler) {
|
|
995
|
-
runtimeSource = route.handler.originalHandler.toString();
|
|
996
|
-
}
|
|
1236
|
+
const runtimeSource = (route.handler.originalHandler || route.handler).toString();
|
|
997
1237
|
const runtimeHandlerSrc = runtimeSource.replace(/\s+/g, " ");
|
|
998
1238
|
const sameMethodRoutes = astRoutes.filter((r) => r.method.toUpperCase() === route.method.toUpperCase());
|
|
999
1239
|
astMatch = sameMethodRoutes.find((r) => {
|
|
1000
1240
|
const astHandlerSrc = (r.handlerSource || r.handlerName || "").replace(/\s+/g, " ");
|
|
1001
1241
|
if (!astHandlerSrc || astHandlerSrc.length < 20) return false;
|
|
1002
|
-
|
|
1003
|
-
return match;
|
|
1242
|
+
return runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(r.handlerSource.substring(0, 50));
|
|
1004
1243
|
});
|
|
1005
1244
|
}
|
|
1006
|
-
const potentialMatches = astRoutes.filter(
|
|
1007
|
-
(r) => r.method.toUpperCase() === route.method.toUpperCase() && r.path === fullPath
|
|
1008
|
-
);
|
|
1009
|
-
if (potentialMatches.length > 1) {
|
|
1010
|
-
const runtimeHandlerSrc = route.handler.toString().replace(/\s+/g, " ");
|
|
1011
|
-
const preciseMatch = potentialMatches.find((r) => {
|
|
1012
|
-
const astHandlerSrc = (r.handlerSource || r.handlerName || "").replace(/\s+/g, " ");
|
|
1013
|
-
const match = runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(r.handlerSource.substring(0, 50));
|
|
1014
|
-
return match;
|
|
1015
|
-
});
|
|
1016
|
-
if (preciseMatch) {
|
|
1017
|
-
astMatch = preciseMatch;
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
1245
|
if (astMatch) {
|
|
1021
1246
|
if (astMatch.summary) operation.summary = astMatch.summary;
|
|
1022
1247
|
if (astMatch.description) operation.description = astMatch.description;
|
|
@@ -1024,25 +1249,19 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1024
1249
|
if (astMatch.operationId) operation.operationId = astMatch.operationId;
|
|
1025
1250
|
if (astMatch.requestTypes?.body) {
|
|
1026
1251
|
operation.requestBody = {
|
|
1027
|
-
content: {
|
|
1028
|
-
"application/json": { schema: astMatch.requestTypes.body }
|
|
1029
|
-
}
|
|
1252
|
+
content: { "application/json": { schema: astMatch.requestTypes.body } }
|
|
1030
1253
|
};
|
|
1031
1254
|
}
|
|
1032
1255
|
if (astMatch.responseSchema) {
|
|
1033
1256
|
operation.responses["200"] = {
|
|
1034
1257
|
description: "Successful response",
|
|
1035
|
-
content: {
|
|
1036
|
-
"application/json": { schema: astMatch.responseSchema }
|
|
1037
|
-
}
|
|
1258
|
+
content: { "application/json": { schema: astMatch.responseSchema } }
|
|
1038
1259
|
};
|
|
1039
1260
|
} else if (astMatch.responseType) {
|
|
1040
1261
|
const contentType = astMatch.responseType === "string" ? "text/plain" : "application/json";
|
|
1041
1262
|
operation.responses["200"] = {
|
|
1042
1263
|
description: "Successful response",
|
|
1043
|
-
content: {
|
|
1044
|
-
[contentType]: { schema: { type: astMatch.responseType } }
|
|
1045
|
-
}
|
|
1264
|
+
content: { [contentType]: { schema: { type: astMatch.responseType } } }
|
|
1046
1265
|
};
|
|
1047
1266
|
}
|
|
1048
1267
|
const params = [];
|
|
@@ -1093,15 +1312,7 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1093
1312
|
deepMerge(operation, inferredSpec);
|
|
1094
1313
|
}
|
|
1095
1314
|
if (route.handlerSpec) {
|
|
1096
|
-
|
|
1097
|
-
if (spec.summary) operation.summary = spec.summary;
|
|
1098
|
-
if (spec.description) operation.description = spec.description;
|
|
1099
|
-
if (spec.operationId) operation.operationId = spec.operationId;
|
|
1100
|
-
if (spec.tags) operation.tags = spec.tags;
|
|
1101
|
-
if (spec.security) operation.security = spec.security;
|
|
1102
|
-
if (spec.responses) {
|
|
1103
|
-
operation.responses = { ...operation.responses, ...spec.responses };
|
|
1104
|
-
}
|
|
1315
|
+
deepMerge(operation, route.handlerSpec);
|
|
1105
1316
|
}
|
|
1106
1317
|
if (!operation.tags || operation.tags.length === 0) operation.tags = [tag];
|
|
1107
1318
|
if (operation.tags) {
|
|
@@ -1120,11 +1331,13 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1120
1331
|
paths[fullPath][methodLower] = operation;
|
|
1121
1332
|
}
|
|
1122
1333
|
}
|
|
1123
|
-
|
|
1334
|
+
const controllers = router[$childControllers];
|
|
1335
|
+
for (const controller of controllers) {
|
|
1124
1336
|
const controllerName = controller.constructor.name || "UnknownController";
|
|
1125
1337
|
tagGroups.get(group)?.add(controllerName);
|
|
1126
1338
|
}
|
|
1127
|
-
|
|
1339
|
+
const childRouters = router[$childRouters];
|
|
1340
|
+
for (const child of childRouters) {
|
|
1128
1341
|
const mountPath = child[$mountPath];
|
|
1129
1342
|
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1130
1343
|
const cleanMount = mountPath.startsWith("/") ? mountPath : "/" + mountPath;
|
|
@@ -1134,7 +1347,7 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1134
1347
|
};
|
|
1135
1348
|
collect(rootRouter);
|
|
1136
1349
|
const xTagGroups = [];
|
|
1137
|
-
for (const [name, tags] of tagGroups) {
|
|
1350
|
+
for (const [name, tags] of tagGroups.entries()) {
|
|
1138
1351
|
xTagGroups.push({ name, tags: Array.from(tags).sort() });
|
|
1139
1352
|
}
|
|
1140
1353
|
return {
|
|
@@ -1156,12 +1369,23 @@ function serveStatic(config, prefix) {
|
|
|
1156
1369
|
let relative = ctx.path.slice(normalizedPrefix.length);
|
|
1157
1370
|
if (!relative.startsWith("/") && relative.length > 0) relative = "/" + relative;
|
|
1158
1371
|
if (relative.length === 0) relative = "/";
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1372
|
+
if (relative.includes("\0")) {
|
|
1373
|
+
return ctx.json({ error: "Forbidden" }, 403);
|
|
1374
|
+
}
|
|
1375
|
+
try {
|
|
1376
|
+
relative = decodeURIComponent(relative);
|
|
1377
|
+
} catch (e) {
|
|
1378
|
+
return ctx.json({ error: "Bad Request" }, 400);
|
|
1379
|
+
}
|
|
1380
|
+
if (relative.includes("\0")) {
|
|
1381
|
+
return ctx.json({ error: "Forbidden" }, 403);
|
|
1382
|
+
}
|
|
1383
|
+
if (relative.includes("../") || relative.includes("..\\")) {
|
|
1162
1384
|
return ctx.json({ error: "Forbidden" }, 403);
|
|
1163
1385
|
}
|
|
1164
|
-
|
|
1386
|
+
const requestPath = path.resolve(path.join(rootPath, relative));
|
|
1387
|
+
const normalizedRoot = path.resolve(rootPath);
|
|
1388
|
+
if (!requestPath.startsWith(normalizedRoot + path.sep) && requestPath !== normalizedRoot) {
|
|
1165
1389
|
return ctx.json({ error: "Forbidden" }, 403);
|
|
1166
1390
|
}
|
|
1167
1391
|
if (config.hooks?.onRequest) {
|
|
@@ -1169,7 +1393,8 @@ function serveStatic(config, prefix) {
|
|
|
1169
1393
|
if (res) return res;
|
|
1170
1394
|
}
|
|
1171
1395
|
if (config.exclude) {
|
|
1172
|
-
for (
|
|
1396
|
+
for (let i = 0; i < config.exclude.length; i++) {
|
|
1397
|
+
const pattern = config.exclude[i];
|
|
1173
1398
|
if (pattern instanceof RegExp) {
|
|
1174
1399
|
if (pattern.test(relative)) return ctx.json({ error: "Forbidden" }, 403);
|
|
1175
1400
|
} else if (typeof pattern === "string") {
|
|
@@ -1188,7 +1413,8 @@ function serveStatic(config, prefix) {
|
|
|
1188
1413
|
stats = await promises$1.stat(requestPath);
|
|
1189
1414
|
} catch (e) {
|
|
1190
1415
|
if (config.extensions) {
|
|
1191
|
-
for (
|
|
1416
|
+
for (let i = 0; i < config.extensions.length; i++) {
|
|
1417
|
+
const ext = config.extensions[i];
|
|
1192
1418
|
const p = requestPath + (ext.startsWith(".") ? ext : "." + ext);
|
|
1193
1419
|
try {
|
|
1194
1420
|
const s = await promises$1.stat(p);
|
|
@@ -1217,7 +1443,8 @@ function serveStatic(config, prefix) {
|
|
|
1217
1443
|
indexes = [config.index];
|
|
1218
1444
|
}
|
|
1219
1445
|
let foundIndex = false;
|
|
1220
|
-
for (
|
|
1446
|
+
for (let i = 0; i < indexes.length; i++) {
|
|
1447
|
+
const idx = indexes[i];
|
|
1221
1448
|
const idxPath = path.join(finalPath, idx);
|
|
1222
1449
|
try {
|
|
1223
1450
|
const idxStats = await promises$1.stat(idxPath);
|
|
@@ -1297,38 +1524,39 @@ class RouterTrie {
|
|
|
1297
1524
|
};
|
|
1298
1525
|
}
|
|
1299
1526
|
insert(method, path2, handler) {
|
|
1300
|
-
let
|
|
1527
|
+
let node = this.root;
|
|
1301
1528
|
const segments = this.splitPath(path2);
|
|
1302
|
-
for (
|
|
1529
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1530
|
+
const segment = segments[i];
|
|
1303
1531
|
if (segment === "**") {
|
|
1304
|
-
if (!
|
|
1305
|
-
|
|
1532
|
+
if (!node.recursiveChild) {
|
|
1533
|
+
node.recursiveChild = this.createNode();
|
|
1306
1534
|
}
|
|
1307
|
-
|
|
1535
|
+
node = node.recursiveChild;
|
|
1308
1536
|
} else if (segment === "*") {
|
|
1309
|
-
if (!
|
|
1310
|
-
|
|
1537
|
+
if (!node.wildcardChild) {
|
|
1538
|
+
node.wildcardChild = this.createNode();
|
|
1311
1539
|
}
|
|
1312
|
-
|
|
1540
|
+
node = node.wildcardChild;
|
|
1313
1541
|
} else if (segment.startsWith(":")) {
|
|
1314
1542
|
const paramName = segment.slice(1);
|
|
1315
|
-
if (!
|
|
1316
|
-
|
|
1317
|
-
|
|
1543
|
+
if (!node.paramChild) {
|
|
1544
|
+
node.paramChild = this.createNode();
|
|
1545
|
+
node.paramChild.paramName = paramName;
|
|
1318
1546
|
}
|
|
1319
|
-
|
|
1320
|
-
|
|
1547
|
+
node = node.paramChild;
|
|
1548
|
+
node.paramName = paramName;
|
|
1321
1549
|
} else {
|
|
1322
|
-
if (!
|
|
1323
|
-
|
|
1550
|
+
if (!node.children[segment]) {
|
|
1551
|
+
node.children[segment] = this.createNode();
|
|
1324
1552
|
}
|
|
1325
|
-
|
|
1553
|
+
node = node.children[segment];
|
|
1326
1554
|
}
|
|
1327
1555
|
}
|
|
1328
|
-
if (!
|
|
1329
|
-
|
|
1556
|
+
if (!node.handlers) {
|
|
1557
|
+
node.handlers = {};
|
|
1330
1558
|
}
|
|
1331
|
-
|
|
1559
|
+
node.handlers[method] = handler;
|
|
1332
1560
|
}
|
|
1333
1561
|
search(method, path2) {
|
|
1334
1562
|
const segments = this.splitPath(path2);
|
|
@@ -1345,34 +1573,34 @@ class RouterTrie {
|
|
|
1345
1573
|
}
|
|
1346
1574
|
return null;
|
|
1347
1575
|
}
|
|
1348
|
-
findNode(
|
|
1576
|
+
findNode(node, segments, index, params) {
|
|
1349
1577
|
if (index === segments.length) {
|
|
1350
|
-
if (
|
|
1351
|
-
if (
|
|
1352
|
-
return
|
|
1578
|
+
if (node.handlers) return node;
|
|
1579
|
+
if (node.recursiveChild && node.recursiveChild.handlers) {
|
|
1580
|
+
return node.recursiveChild;
|
|
1353
1581
|
}
|
|
1354
1582
|
return null;
|
|
1355
1583
|
}
|
|
1356
1584
|
const segment = segments[index];
|
|
1357
|
-
const child =
|
|
1585
|
+
const child = node.children[segment];
|
|
1358
1586
|
if (child) {
|
|
1359
1587
|
const result = this.findNode(child, segments, index + 1, params);
|
|
1360
1588
|
if (result) return result;
|
|
1361
1589
|
}
|
|
1362
|
-
if (
|
|
1363
|
-
params[
|
|
1364
|
-
const result = this.findNode(
|
|
1590
|
+
if (node.paramChild) {
|
|
1591
|
+
params[node.paramChild.paramName] = segment;
|
|
1592
|
+
const result = this.findNode(node.paramChild, segments, index + 1, params);
|
|
1365
1593
|
if (result) return result;
|
|
1366
|
-
delete params[
|
|
1594
|
+
delete params[node.paramChild.paramName];
|
|
1367
1595
|
}
|
|
1368
|
-
if (
|
|
1369
|
-
const result = this.findNode(
|
|
1596
|
+
if (node.wildcardChild) {
|
|
1597
|
+
const result = this.findNode(node.wildcardChild, segments, index + 1, params);
|
|
1370
1598
|
if (result) return result;
|
|
1371
1599
|
}
|
|
1372
|
-
if (
|
|
1600
|
+
if (node.recursiveChild) {
|
|
1373
1601
|
const remaining = segments.length - index;
|
|
1374
1602
|
for (let k = 0; k <= remaining; k++) {
|
|
1375
|
-
const result = this.findNode(
|
|
1603
|
+
const result = this.findNode(node.recursiveChild, segments, index + k, params);
|
|
1376
1604
|
if (result) return result;
|
|
1377
1605
|
}
|
|
1378
1606
|
}
|
|
@@ -1386,40 +1614,68 @@ class RouterTrie {
|
|
|
1386
1614
|
}
|
|
1387
1615
|
}
|
|
1388
1616
|
const asyncContext = new node_async_hooks.AsyncLocalStorage();
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
return
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1617
|
+
let db;
|
|
1618
|
+
let dbPromise = null;
|
|
1619
|
+
let RecordId;
|
|
1620
|
+
async function ensureDb() {
|
|
1621
|
+
if (db) return db;
|
|
1622
|
+
if (dbPromise) return dbPromise;
|
|
1623
|
+
dbPromise = (async () => {
|
|
1624
|
+
try {
|
|
1625
|
+
const { createNodeEngines } = await import("@surrealdb/node");
|
|
1626
|
+
const surreal = await import("surrealdb");
|
|
1627
|
+
const Surreal = surreal.Surreal;
|
|
1628
|
+
RecordId = surreal.RecordId;
|
|
1629
|
+
const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
|
|
1630
|
+
const _db = new Surreal({
|
|
1631
|
+
engines: createNodeEngines()
|
|
1632
|
+
});
|
|
1633
|
+
await _db.connect(engine, { namespace: "vendor", database: "shokupan" });
|
|
1634
|
+
await _db.query(`
|
|
1635
|
+
DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1636
|
+
DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
|
|
1637
|
+
DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
|
|
1638
|
+
DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
|
|
1639
|
+
DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
|
|
1640
|
+
DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1641
|
+
`);
|
|
1642
|
+
db = _db;
|
|
1643
|
+
return db;
|
|
1644
|
+
} catch (e) {
|
|
1645
|
+
dbPromise = null;
|
|
1646
|
+
if (e.code === "ERR_MODULE_NOT_FOUND" || e.message.includes("Cannot find module")) {
|
|
1647
|
+
throw new Error("SurrealDB dependencies not found. To use the datastore, please install 'surrealdb' and '@surrealdb/node'.");
|
|
1648
|
+
}
|
|
1649
|
+
throw e;
|
|
1650
|
+
}
|
|
1651
|
+
})();
|
|
1652
|
+
return dbPromise;
|
|
1653
|
+
}
|
|
1403
1654
|
const datastore = {
|
|
1404
|
-
get(store, key) {
|
|
1405
|
-
|
|
1655
|
+
async get(store, key) {
|
|
1656
|
+
await ensureDb();
|
|
1657
|
+
return db.select(new RecordId(store, key));
|
|
1406
1658
|
},
|
|
1407
|
-
set(store, key, value) {
|
|
1408
|
-
|
|
1659
|
+
async set(store, key, value) {
|
|
1660
|
+
await ensureDb();
|
|
1661
|
+
return db.create(new RecordId(store, key)).content(value);
|
|
1409
1662
|
},
|
|
1410
1663
|
async query(query, vars) {
|
|
1664
|
+
await ensureDb();
|
|
1411
1665
|
try {
|
|
1412
|
-
const r = await db.query(query, vars)
|
|
1413
|
-
return r;
|
|
1666
|
+
const r = await db.query(query, vars);
|
|
1667
|
+
return Array.isArray(r) ? r : r?.collect ? await r.collect() : r;
|
|
1414
1668
|
} catch (e) {
|
|
1415
1669
|
console.error("DS ERROR:", e);
|
|
1416
1670
|
throw e;
|
|
1417
1671
|
}
|
|
1418
1672
|
},
|
|
1419
|
-
ready
|
|
1673
|
+
get ready() {
|
|
1674
|
+
return ensureDb().then(() => void 0);
|
|
1675
|
+
}
|
|
1420
1676
|
};
|
|
1421
1677
|
process.on("exit", async () => {
|
|
1422
|
-
await db.close();
|
|
1678
|
+
if (db) await db.close();
|
|
1423
1679
|
});
|
|
1424
1680
|
const tracer = api.trace.getTracer("shokupan.middleware");
|
|
1425
1681
|
function traceHandler(fn, name) {
|
|
@@ -1492,6 +1748,8 @@ class ShokupanRouter {
|
|
|
1492
1748
|
[$parent] = null;
|
|
1493
1749
|
[$childRouters] = [];
|
|
1494
1750
|
[$childControllers] = [];
|
|
1751
|
+
hookCache = /* @__PURE__ */ new Map();
|
|
1752
|
+
hooksInitialized = false;
|
|
1495
1753
|
middleware = [];
|
|
1496
1754
|
get rootConfig() {
|
|
1497
1755
|
return this[$appRoot]?.applicationConfig;
|
|
@@ -1509,7 +1767,8 @@ class ShokupanRouter {
|
|
|
1509
1767
|
getComponentRegistry() {
|
|
1510
1768
|
const controllerRoutesMap = /* @__PURE__ */ new Map();
|
|
1511
1769
|
const localRoutes = [];
|
|
1512
|
-
for (
|
|
1770
|
+
for (let i = 0; i < this[$routes].length; i++) {
|
|
1771
|
+
const r = this[$routes][i];
|
|
1513
1772
|
const entry = {
|
|
1514
1773
|
type: "route",
|
|
1515
1774
|
path: r.path,
|
|
@@ -1646,7 +1905,8 @@ class ShokupanRouter {
|
|
|
1646
1905
|
const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
|
|
1647
1906
|
const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
|
|
1648
1907
|
let routesAttached = 0;
|
|
1649
|
-
for (
|
|
1908
|
+
for (let i = 0; i < Array.from(methods).length; i++) {
|
|
1909
|
+
const name = Array.from(methods)[i];
|
|
1650
1910
|
if (name === "constructor") continue;
|
|
1651
1911
|
if (["arguments", "caller", "callee"].includes(name)) continue;
|
|
1652
1912
|
const originalHandler = instance[name];
|
|
@@ -1658,7 +1918,8 @@ class ShokupanRouter {
|
|
|
1658
1918
|
method = config.method;
|
|
1659
1919
|
subPath = config.path;
|
|
1660
1920
|
} else {
|
|
1661
|
-
for (
|
|
1921
|
+
for (let j = 0; j < HTTPMethods.length; j++) {
|
|
1922
|
+
const m = HTTPMethods[j];
|
|
1662
1923
|
if (name.toUpperCase().startsWith(m)) {
|
|
1663
1924
|
method = m;
|
|
1664
1925
|
const rest = name.slice(m.length);
|
|
@@ -1673,8 +1934,8 @@ class ShokupanRouter {
|
|
|
1673
1934
|
buffer = "";
|
|
1674
1935
|
}
|
|
1675
1936
|
};
|
|
1676
|
-
for (let
|
|
1677
|
-
const char = rest[
|
|
1937
|
+
for (let i2 = 0; i2 < rest.length; i2++) {
|
|
1938
|
+
const char = rest[i2];
|
|
1678
1939
|
if (char === "$") {
|
|
1679
1940
|
flush();
|
|
1680
1941
|
subPath += "/:";
|
|
@@ -1712,7 +1973,8 @@ class ShokupanRouter {
|
|
|
1712
1973
|
if (routeArgs?.length > 0) {
|
|
1713
1974
|
args = [];
|
|
1714
1975
|
const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
|
|
1715
|
-
for (
|
|
1976
|
+
for (let k = 0; k < sortedArgs.length; k++) {
|
|
1977
|
+
const arg = sortedArgs[k];
|
|
1716
1978
|
switch (arg.type) {
|
|
1717
1979
|
case RouteParamType.BODY:
|
|
1718
1980
|
try {
|
|
@@ -1742,7 +2004,9 @@ class ShokupanRouter {
|
|
|
1742
2004
|
args[arg.index] = vals.length > 1 ? vals : vals[0];
|
|
1743
2005
|
} else {
|
|
1744
2006
|
const query = {};
|
|
1745
|
-
|
|
2007
|
+
const keys = Object.keys(url.searchParams);
|
|
2008
|
+
for (let k2 = 0; k2 < keys.length; k2++) {
|
|
2009
|
+
const key = keys[k2];
|
|
1746
2010
|
const vals = url.searchParams.getAll(key);
|
|
1747
2011
|
query[key] = vals.length > 1 ? vals : vals[0];
|
|
1748
2012
|
}
|
|
@@ -1799,9 +2063,11 @@ class ShokupanRouter {
|
|
|
1799
2063
|
path: r.path,
|
|
1800
2064
|
handler: r.handler
|
|
1801
2065
|
}));
|
|
1802
|
-
for (
|
|
2066
|
+
for (let i = 0; i < this[$childRouters].length; i++) {
|
|
2067
|
+
const child = this[$childRouters][i];
|
|
1803
2068
|
const childRoutes = child.getRoutes();
|
|
1804
|
-
for (
|
|
2069
|
+
for (let j = 0; j < childRoutes.length; j++) {
|
|
2070
|
+
const route = childRoutes[j];
|
|
1805
2071
|
const cleanPrefix = child[$mountPath].endsWith("/") ? child[$mountPath].slice(0, -1) : child[$mountPath];
|
|
1806
2072
|
const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
|
|
1807
2073
|
const fullPath = cleanPrefix + cleanPath || "/";
|
|
@@ -1815,12 +2081,12 @@ class ShokupanRouter {
|
|
|
1815
2081
|
return routes;
|
|
1816
2082
|
}
|
|
1817
2083
|
/**
|
|
1818
|
-
* Makes
|
|
1819
|
-
* This is useful for
|
|
2084
|
+
* Makes an internal request through this router's full routing pipeline.
|
|
2085
|
+
* This is useful for calling other routes internally and supports streaming responses.
|
|
1820
2086
|
* @param options The request options.
|
|
1821
|
-
* @returns The
|
|
2087
|
+
* @returns The raw Response object.
|
|
1822
2088
|
*/
|
|
1823
|
-
async
|
|
2089
|
+
async internalRequest(arg) {
|
|
1824
2090
|
const options = typeof arg === "string" ? { path: arg } : arg;
|
|
1825
2091
|
const store = asyncContext.getStore();
|
|
1826
2092
|
store?.get("req");
|
|
@@ -1839,9 +2105,10 @@ class ShokupanRouter {
|
|
|
1839
2105
|
return this.root[$dispatch](req);
|
|
1840
2106
|
}
|
|
1841
2107
|
/**
|
|
1842
|
-
* Processes a request
|
|
2108
|
+
* Processes a request for testing purposes.
|
|
2109
|
+
* Returns a simplified { status, headers, data } object instead of a Response.
|
|
1843
2110
|
*/
|
|
1844
|
-
async
|
|
2111
|
+
async testRequest(options) {
|
|
1845
2112
|
let url = options.url || options.path || "/";
|
|
1846
2113
|
if (!url.startsWith("http")) {
|
|
1847
2114
|
const base = `http://${this.rootConfig?.hostname || "localhost"}:${this.rootConfig?.port || 3e3}`;
|
|
@@ -1850,7 +2117,9 @@ class ShokupanRouter {
|
|
|
1850
2117
|
}
|
|
1851
2118
|
if (options.query) {
|
|
1852
2119
|
const u = new URL(url);
|
|
1853
|
-
|
|
2120
|
+
const entries = Object.entries(options.query);
|
|
2121
|
+
for (let i = 0; i < entries.length; i++) {
|
|
2122
|
+
const [k, v] = entries[i];
|
|
1854
2123
|
u.searchParams.set(k, v);
|
|
1855
2124
|
}
|
|
1856
2125
|
url = u.toString();
|
|
@@ -1895,28 +2164,17 @@ class ShokupanRouter {
|
|
|
1895
2164
|
data: result
|
|
1896
2165
|
};
|
|
1897
2166
|
}
|
|
1898
|
-
|
|
1899
|
-
if (!this.
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
}
|
|
1906
|
-
wrapWithHooks(handler, hooks) {
|
|
1907
|
-
const hookList = Array.isArray(hooks) ? hooks : [hooks];
|
|
1908
|
-
const hasStart = hookList.some((h) => !!h.onRequestStart);
|
|
1909
|
-
const hasEnd = hookList.some((h) => !!h.onRequestEnd);
|
|
1910
|
-
const hasError = hookList.some((h) => !!h.onError);
|
|
2167
|
+
wrapWithHooks(handler) {
|
|
2168
|
+
if (!this.hooksInitialized) {
|
|
2169
|
+
this.ensureHooksInitialized();
|
|
2170
|
+
}
|
|
2171
|
+
const hasStart = this.hookCache.get("onRequestStart")?.length > 0;
|
|
2172
|
+
const hasEnd = this.hookCache.get("onRequestEnd")?.length > 0;
|
|
2173
|
+
const hasError = this.hookCache.get("onError")?.length > 0;
|
|
1911
2174
|
if (!hasStart && !hasEnd && !hasError) return handler;
|
|
1912
2175
|
const originalHandler = handler;
|
|
1913
2176
|
const wrapped = async (ctx) => {
|
|
1914
|
-
|
|
1915
|
-
for (let i = 0; i < hookList.length; i++) {
|
|
1916
|
-
const h = hookList[i];
|
|
1917
|
-
if (typeof h.onRequestStart === "function") await h.onRequestStart(ctx);
|
|
1918
|
-
}
|
|
1919
|
-
}
|
|
2177
|
+
await this.runHooks("onRequestStart", ctx);
|
|
1920
2178
|
const debug = ctx._debug;
|
|
1921
2179
|
let debugId;
|
|
1922
2180
|
let previousNode;
|
|
@@ -1930,17 +2188,11 @@ class ShokupanRouter {
|
|
|
1930
2188
|
try {
|
|
1931
2189
|
const res = await originalHandler(ctx);
|
|
1932
2190
|
debug?.trackStep(debugId, "handler", performance.now() - start, "success");
|
|
1933
|
-
|
|
1934
|
-
const h = hookList[i];
|
|
1935
|
-
if (typeof h.onRequestEnd === "function") await h.onRequestEnd(ctx);
|
|
1936
|
-
}
|
|
2191
|
+
await this.runHooks("onRequestEnd", ctx);
|
|
1937
2192
|
return res;
|
|
1938
2193
|
} catch (err) {
|
|
1939
2194
|
debug?.trackStep(debugId, "handler", performance.now() - start, "error", err);
|
|
1940
|
-
|
|
1941
|
-
const h = hookList[i];
|
|
1942
|
-
if (typeof h.onError === "function") await h.onError(err, ctx);
|
|
1943
|
-
}
|
|
2195
|
+
await this.runHooks("onError", ctx, err);
|
|
1944
2196
|
throw err;
|
|
1945
2197
|
} finally {
|
|
1946
2198
|
if (debug && previousNode) debug.setNode(previousNode);
|
|
@@ -1962,18 +2214,19 @@ class ShokupanRouter {
|
|
|
1962
2214
|
result = this.trie.search("GET", path2);
|
|
1963
2215
|
if (result) return result;
|
|
1964
2216
|
}
|
|
1965
|
-
for (
|
|
2217
|
+
for (let i = 0; i < this[$childRouters].length; i++) {
|
|
2218
|
+
const child = this[$childRouters][i];
|
|
1966
2219
|
const prefix = child[$mountPath];
|
|
1967
2220
|
if (path2 === prefix || path2.startsWith(prefix + "/")) {
|
|
1968
2221
|
const subPath = path2.slice(prefix.length) || "/";
|
|
1969
2222
|
const match = child.find(method, subPath);
|
|
1970
|
-
if (match) return
|
|
2223
|
+
if (match) return match;
|
|
1971
2224
|
}
|
|
1972
2225
|
if (prefix.endsWith("/")) {
|
|
1973
2226
|
if (path2.startsWith(prefix)) {
|
|
1974
2227
|
const subPath = path2.slice(prefix.length) || "/";
|
|
1975
2228
|
const match = child.find(method, subPath);
|
|
1976
|
-
if (match) return
|
|
2229
|
+
if (match) return match;
|
|
1977
2230
|
}
|
|
1978
2231
|
}
|
|
1979
2232
|
}
|
|
@@ -1981,10 +2234,13 @@ class ShokupanRouter {
|
|
|
1981
2234
|
}
|
|
1982
2235
|
parsePath(path2) {
|
|
1983
2236
|
const keys = [];
|
|
2237
|
+
if (path2.length > 2048) {
|
|
2238
|
+
throw new Error("Path too long");
|
|
2239
|
+
}
|
|
1984
2240
|
const pattern = path2.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => {
|
|
1985
2241
|
keys.push(key);
|
|
1986
|
-
return "([^/]
|
|
1987
|
-
}).replace(/\*\*/g, "
|
|
2242
|
+
return "([^/]{1,255})";
|
|
2243
|
+
}).replace(/\*\*/g, ".{0,1000}").replace(/\*/g, "[^/]{1,255}");
|
|
1988
2244
|
return {
|
|
1989
2245
|
regex: new RegExp(`^${pattern}$`),
|
|
1990
2246
|
keys
|
|
@@ -1995,17 +2251,23 @@ class ShokupanRouter {
|
|
|
1995
2251
|
/**
|
|
1996
2252
|
* Adds a route to the router.
|
|
1997
2253
|
*
|
|
1998
|
-
* @param
|
|
1999
|
-
* @param
|
|
2000
|
-
* @param
|
|
2001
|
-
* @param
|
|
2002
|
-
* @param
|
|
2254
|
+
* @param arg - Route configuration object
|
|
2255
|
+
* @param arg.method - HTTP method
|
|
2256
|
+
* @param arg.path - URL path
|
|
2257
|
+
* @param arg.spec - OpenAPI specification for the route
|
|
2258
|
+
* @param arg.handler - Route handler function
|
|
2259
|
+
* @param arg.regex - Custom regex for path matching
|
|
2260
|
+
* @param arg.group - Group for the route
|
|
2261
|
+
* @param arg.requestTimeout - Timeout for this route in milliseconds
|
|
2262
|
+
* @param arg.renderer - JSX renderer for the route
|
|
2263
|
+
* @param arg.controller - Controller for the route
|
|
2003
2264
|
*/
|
|
2004
2265
|
add({ method, path: path2, spec, handler, regex: customRegex, group, requestTimeout, renderer, controller }) {
|
|
2005
2266
|
const { regex, keys } = customRegex ? { regex: customRegex, keys: [] } : this.parsePath(path2);
|
|
2006
2267
|
if (this.currentGuards.length > 0) {
|
|
2007
2268
|
spec = spec || {};
|
|
2008
|
-
for (
|
|
2269
|
+
for (let i = 0; i < this.currentGuards.length; i++) {
|
|
2270
|
+
const guard = this.currentGuards[i];
|
|
2009
2271
|
if (guard.spec) {
|
|
2010
2272
|
if (guard.spec.responses) {
|
|
2011
2273
|
spec.responses = spec.responses || {};
|
|
@@ -2034,7 +2296,8 @@ class ShokupanRouter {
|
|
|
2034
2296
|
if (routeGuards.length > 0) {
|
|
2035
2297
|
const innerHandler = wrappedHandler;
|
|
2036
2298
|
wrappedHandler = async (ctx) => {
|
|
2037
|
-
for (
|
|
2299
|
+
for (let i = 0; i < routeGuards.length; i++) {
|
|
2300
|
+
const guard = routeGuards[i];
|
|
2038
2301
|
let guardPassed = false;
|
|
2039
2302
|
let nextCalled = false;
|
|
2040
2303
|
const next = () => {
|
|
@@ -2128,7 +2391,7 @@ class ShokupanRouter {
|
|
|
2128
2391
|
wrappedHandler.originalHandler = trackingHandler.originalHandler || trackingHandler;
|
|
2129
2392
|
let bakedHandler = wrappedHandler;
|
|
2130
2393
|
if (this.config?.hooks) {
|
|
2131
|
-
bakedHandler = this.wrapWithHooks(wrappedHandler
|
|
2394
|
+
bakedHandler = this.wrapWithHooks(wrappedHandler);
|
|
2132
2395
|
}
|
|
2133
2396
|
this[$routes].push({
|
|
2134
2397
|
method,
|
|
@@ -2285,6 +2548,67 @@ class ShokupanRouter {
|
|
|
2285
2548
|
generateApiSpec(options = {}) {
|
|
2286
2549
|
return generateOpenApi(this, options);
|
|
2287
2550
|
}
|
|
2551
|
+
ensureHooksInitialized() {
|
|
2552
|
+
const hooks = this.config?.hooks;
|
|
2553
|
+
if (hooks) {
|
|
2554
|
+
const hookList = Array.isArray(hooks) ? hooks : [hooks];
|
|
2555
|
+
const hookTypes = [
|
|
2556
|
+
"onRequestStart",
|
|
2557
|
+
"onRequestEnd",
|
|
2558
|
+
"onResponseStart",
|
|
2559
|
+
"onResponseEnd",
|
|
2560
|
+
"onError",
|
|
2561
|
+
"beforeValidate",
|
|
2562
|
+
"afterValidate",
|
|
2563
|
+
"onRequestTimeout",
|
|
2564
|
+
"onReadTimeout",
|
|
2565
|
+
"onWriteTimeout"
|
|
2566
|
+
];
|
|
2567
|
+
for (let i = 0; i < hookTypes.length; i++) {
|
|
2568
|
+
const type = hookTypes[i];
|
|
2569
|
+
const fns = [];
|
|
2570
|
+
for (let j = 0; j < hookList.length; j++) {
|
|
2571
|
+
const h = hookList[j];
|
|
2572
|
+
if (h[type]) fns.push(h[type]);
|
|
2573
|
+
}
|
|
2574
|
+
if (fns.length > 0) {
|
|
2575
|
+
this.hookCache.set(type, fns);
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
this.hooksInitialized = true;
|
|
2580
|
+
}
|
|
2581
|
+
async runHooks(name, ...args) {
|
|
2582
|
+
if (!this.hooksInitialized) {
|
|
2583
|
+
this.ensureHooksInitialized();
|
|
2584
|
+
}
|
|
2585
|
+
const fns = this.hookCache.get(name);
|
|
2586
|
+
if (!fns) return;
|
|
2587
|
+
const ctx = args?.[0] instanceof ShokupanContext ? args[0] : void 0;
|
|
2588
|
+
const debug = ctx?._debug;
|
|
2589
|
+
if (debug) {
|
|
2590
|
+
await Promise.all(fns.map(async (fn, index) => {
|
|
2591
|
+
const hookId = `hook_${name}_${fn.name || index}`;
|
|
2592
|
+
const previousNode = debug.getCurrentNode();
|
|
2593
|
+
debug.trackEdge(previousNode, hookId);
|
|
2594
|
+
debug.setNode(hookId);
|
|
2595
|
+
const start = performance.now();
|
|
2596
|
+
try {
|
|
2597
|
+
await fn(...args);
|
|
2598
|
+
const duration = performance.now() - start;
|
|
2599
|
+
debug.trackStep(hookId, "hook", duration, "success");
|
|
2600
|
+
} catch (error) {
|
|
2601
|
+
const duration = performance.now() - start;
|
|
2602
|
+
debug.trackStep(hookId, "hook", duration, "error", error);
|
|
2603
|
+
throw error;
|
|
2604
|
+
} finally {
|
|
2605
|
+
if (previousNode) debug.setNode(previousNode);
|
|
2606
|
+
}
|
|
2607
|
+
}));
|
|
2608
|
+
} else {
|
|
2609
|
+
await Promise.all(fns.map((fn) => fn(...args)));
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2288
2612
|
}
|
|
2289
2613
|
class SystemCpuMonitor {
|
|
2290
2614
|
constructor(intervalMs = 1e3) {
|
|
@@ -2342,15 +2666,13 @@ class Shokupan extends ShokupanRouter {
|
|
|
2342
2666
|
openApiSpec;
|
|
2343
2667
|
composedMiddleware;
|
|
2344
2668
|
cpuMonitor;
|
|
2345
|
-
hookCache = /* @__PURE__ */ new Map();
|
|
2346
|
-
hooksInitialized = false;
|
|
2347
2669
|
get logger() {
|
|
2348
2670
|
return this.applicationConfig.logger;
|
|
2349
2671
|
}
|
|
2350
2672
|
constructor(applicationConfig = {}) {
|
|
2351
2673
|
const config = Object.assign({}, defaults, applicationConfig);
|
|
2352
2674
|
const { hooks, ...routerConfig } = config;
|
|
2353
|
-
super(routerConfig);
|
|
2675
|
+
super({ ...routerConfig, hooks });
|
|
2354
2676
|
this[$isApplication] = true;
|
|
2355
2677
|
this[$appRoot] = this;
|
|
2356
2678
|
this.applicationConfig = config;
|
|
@@ -2365,7 +2687,6 @@ class Shokupan extends ShokupanRouter {
|
|
|
2365
2687
|
* Adds middleware to the application.
|
|
2366
2688
|
*/
|
|
2367
2689
|
use(middleware) {
|
|
2368
|
-
let trackedMiddleware = middleware;
|
|
2369
2690
|
const { file, line } = getCallerInfo();
|
|
2370
2691
|
if (!middleware.metadata) {
|
|
2371
2692
|
middleware.metadata = {
|
|
@@ -2376,32 +2697,36 @@ class Shokupan extends ShokupanRouter {
|
|
|
2376
2697
|
pluginName: middleware.pluginName
|
|
2377
2698
|
};
|
|
2378
2699
|
}
|
|
2379
|
-
|
|
2380
|
-
const
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2700
|
+
if (this.applicationConfig.enableMiddlewareTracking) {
|
|
2701
|
+
const trackedMiddleware = async (ctx, next) => {
|
|
2702
|
+
const c = ctx;
|
|
2703
|
+
if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
|
|
2704
|
+
const metadata = middleware.metadata || {};
|
|
2705
|
+
const start = performance.now();
|
|
2706
|
+
const item = {
|
|
2707
|
+
name: metadata.pluginName ? `${metadata.pluginName} (${metadata.name})` : metadata.name || middleware.name || "middleware",
|
|
2708
|
+
file: metadata.file || file,
|
|
2709
|
+
line: metadata.line || line,
|
|
2710
|
+
isBuiltin: metadata.isBuiltin,
|
|
2711
|
+
startTime: start,
|
|
2712
|
+
duration: -1
|
|
2713
|
+
};
|
|
2714
|
+
c.handlerStack.push(item);
|
|
2715
|
+
try {
|
|
2716
|
+
return await middleware(ctx, next);
|
|
2717
|
+
} finally {
|
|
2718
|
+
item.duration = performance.now() - start;
|
|
2719
|
+
}
|
|
2397
2720
|
}
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2721
|
+
return middleware(ctx, next);
|
|
2722
|
+
};
|
|
2723
|
+
trackedMiddleware.metadata = middleware.metadata;
|
|
2724
|
+
Object.defineProperty(trackedMiddleware, "name", { value: middleware.name || "middleware" });
|
|
2725
|
+
trackedMiddleware.order = this.middleware.length;
|
|
2726
|
+
this.middleware.push(trackedMiddleware);
|
|
2727
|
+
} else {
|
|
2728
|
+
this.middleware.push(middleware);
|
|
2729
|
+
}
|
|
2405
2730
|
return this;
|
|
2406
2731
|
}
|
|
2407
2732
|
startupHooks = [];
|
|
@@ -2432,17 +2757,13 @@ class Shokupan extends ShokupanRouter {
|
|
|
2432
2757
|
if (finalPort < 0 || finalPort > 65535) {
|
|
2433
2758
|
throw new Error("Invalid port number");
|
|
2434
2759
|
}
|
|
2435
|
-
|
|
2436
|
-
await hook();
|
|
2437
|
-
}
|
|
2760
|
+
await Promise.all(this.startupHooks.map((hook) => hook()));
|
|
2438
2761
|
if (this.applicationConfig.enableOpenApiGen) {
|
|
2439
2762
|
this.openApiSpec = await generateOpenApi(this);
|
|
2440
|
-
|
|
2441
|
-
await hook(this.openApiSpec);
|
|
2442
|
-
}
|
|
2763
|
+
await Promise.all(this.specAvailableHooks.map((hook) => hook(this.openApiSpec)));
|
|
2443
2764
|
}
|
|
2444
2765
|
if (port === 0 && process.platform === "linux") ;
|
|
2445
|
-
if (this.applicationConfig.autoBackpressureFeedback) {
|
|
2766
|
+
if (this.applicationConfig.autoBackpressureFeedback === true) {
|
|
2446
2767
|
this.cpuMonitor = new SystemCpuMonitor();
|
|
2447
2768
|
this.cpuMonitor.start();
|
|
2448
2769
|
}
|
|
@@ -2470,7 +2791,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
2470
2791
|
};
|
|
2471
2792
|
let factory = this.applicationConfig.serverFactory;
|
|
2472
2793
|
if (!factory && typeof Bun === "undefined") {
|
|
2473
|
-
const { createHttpServer } = await Promise.resolve().then(() => require("./server-adapter-
|
|
2794
|
+
const { createHttpServer } = await Promise.resolve().then(() => require("./server-adapter-DFhwlK8e.cjs"));
|
|
2474
2795
|
factory = createHttpServer();
|
|
2475
2796
|
}
|
|
2476
2797
|
const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
|
|
@@ -2483,7 +2804,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
2483
2804
|
/**
|
|
2484
2805
|
* Processes a request by wrapping the standard fetch method.
|
|
2485
2806
|
*/
|
|
2486
|
-
async
|
|
2807
|
+
async testRequest(options) {
|
|
2487
2808
|
let url = options.url || options.path || "/";
|
|
2488
2809
|
if (!url.startsWith("http")) {
|
|
2489
2810
|
const base = `http://${this.applicationConfig.hostname || "localhost"}:${this.applicationConfig.port || 3e3}`;
|
|
@@ -2492,7 +2813,9 @@ class Shokupan extends ShokupanRouter {
|
|
|
2492
2813
|
}
|
|
2493
2814
|
if (options.query) {
|
|
2494
2815
|
const u = new URL(url);
|
|
2495
|
-
|
|
2816
|
+
const entries = Object.entries(options.query);
|
|
2817
|
+
for (let i = 0; i < entries.length; i++) {
|
|
2818
|
+
const [k, v] = entries[i];
|
|
2496
2819
|
u.searchParams.set(k, v);
|
|
2497
2820
|
}
|
|
2498
2821
|
url = u.toString();
|
|
@@ -2561,18 +2884,18 @@ class Shokupan extends ShokupanRouter {
|
|
|
2561
2884
|
if (this.cpuMonitor && this.cpuMonitor.getUsage() > (this.applicationConfig.autoBackpressureLevel ?? 60)) {
|
|
2562
2885
|
const msg = "Too Many Requests (CPU Backpressure)";
|
|
2563
2886
|
const res = ctx.text(msg, 429);
|
|
2564
|
-
await this.
|
|
2887
|
+
await this.runHooks("onResponseEnd", ctx, res);
|
|
2565
2888
|
return res;
|
|
2566
2889
|
}
|
|
2567
2890
|
try {
|
|
2568
|
-
|
|
2569
|
-
await this.executeHook("onRequestStart", ctx);
|
|
2570
|
-
}
|
|
2891
|
+
await this.runHooks("onRequestStart", ctx);
|
|
2571
2892
|
const fn = this.composedMiddleware ??= compose(this.middleware);
|
|
2572
2893
|
const result = await fn(ctx, async () => {
|
|
2894
|
+
const bodyParsing = ["POST", "PUT", "PATCH", "DELETE"].includes(req.method) ? ctx.parseBody() : Promise.resolve();
|
|
2573
2895
|
const match = this.find(req.method, ctx.path);
|
|
2574
2896
|
if (match) {
|
|
2575
2897
|
ctx.params = match.params;
|
|
2898
|
+
await bodyParsing;
|
|
2576
2899
|
return match.handler(ctx);
|
|
2577
2900
|
}
|
|
2578
2901
|
return null;
|
|
@@ -2595,12 +2918,8 @@ class Shokupan extends ShokupanRouter {
|
|
|
2595
2918
|
} else {
|
|
2596
2919
|
response = ctx.text(String(result));
|
|
2597
2920
|
}
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
}
|
|
2601
|
-
if (this.hasHook("onResponseStart")) {
|
|
2602
|
-
await this.executeHook("onResponseStart", ctx, response);
|
|
2603
|
-
}
|
|
2921
|
+
await this.runHooks("onRequestEnd", ctx);
|
|
2922
|
+
await this.runHooks("onResponseStart", ctx, response);
|
|
2604
2923
|
return response;
|
|
2605
2924
|
} catch (err) {
|
|
2606
2925
|
console.error(err);
|
|
@@ -2609,9 +2928,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
2609
2928
|
const status = err.status || err.statusCode || 500;
|
|
2610
2929
|
const body = { error: err.message || "Internal Server Error" };
|
|
2611
2930
|
if (err.errors) body.errors = err.errors;
|
|
2612
|
-
|
|
2613
|
-
await this.executeHook("onError", err, ctx);
|
|
2614
|
-
}
|
|
2931
|
+
await this.runHooks("onError", ctx, err);
|
|
2615
2932
|
return ctx.json(body, status);
|
|
2616
2933
|
}
|
|
2617
2934
|
};
|
|
@@ -2622,9 +2939,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
2622
2939
|
const timeoutPromise = new Promise((_, reject) => {
|
|
2623
2940
|
timeoutId = setTimeout(async () => {
|
|
2624
2941
|
controller.abort();
|
|
2625
|
-
|
|
2626
|
-
await this.executeHook("onRequestTimeout", ctx);
|
|
2627
|
-
}
|
|
2942
|
+
await this.runHooks("onRequestTimeout", ctx);
|
|
2628
2943
|
reject(new Error("Request Timeout"));
|
|
2629
2944
|
}, timeoutMs);
|
|
2630
2945
|
});
|
|
@@ -2637,56 +2952,10 @@ class Shokupan extends ShokupanRouter {
|
|
|
2637
2952
|
console.error("Unexpected error in request execution:", err);
|
|
2638
2953
|
return ctx.text("Internal Server Error", 500);
|
|
2639
2954
|
}).then(async (res) => {
|
|
2640
|
-
|
|
2641
|
-
await this.executeHook("onResponseEnd", ctx, res);
|
|
2642
|
-
}
|
|
2955
|
+
await this.runHooks("onResponseEnd", ctx, res);
|
|
2643
2956
|
return res;
|
|
2644
2957
|
});
|
|
2645
2958
|
}
|
|
2646
|
-
ensureHooksInitialized() {
|
|
2647
|
-
const hooks = this.applicationConfig.hooks;
|
|
2648
|
-
if (hooks) {
|
|
2649
|
-
const hookList = Array.isArray(hooks) ? hooks : [hooks];
|
|
2650
|
-
const hookTypes = [
|
|
2651
|
-
"onRequestStart",
|
|
2652
|
-
"onRequestEnd",
|
|
2653
|
-
"onResponseStart",
|
|
2654
|
-
"onResponseEnd",
|
|
2655
|
-
"onError",
|
|
2656
|
-
"beforeValidate",
|
|
2657
|
-
"afterValidate",
|
|
2658
|
-
"onRequestTimeout",
|
|
2659
|
-
"onReadTimeout",
|
|
2660
|
-
"onWriteTimeout"
|
|
2661
|
-
];
|
|
2662
|
-
for (const type of hookTypes) {
|
|
2663
|
-
const fns = [];
|
|
2664
|
-
for (const h of hookList) {
|
|
2665
|
-
if (h[type]) fns.push(h[type]);
|
|
2666
|
-
}
|
|
2667
|
-
if (fns.length > 0) {
|
|
2668
|
-
this.hookCache.set(type, fns);
|
|
2669
|
-
}
|
|
2670
|
-
}
|
|
2671
|
-
}
|
|
2672
|
-
this.hooksInitialized = true;
|
|
2673
|
-
}
|
|
2674
|
-
async executeHook(name, ...args) {
|
|
2675
|
-
if (!this.hooksInitialized) {
|
|
2676
|
-
this.ensureHooksInitialized();
|
|
2677
|
-
}
|
|
2678
|
-
const fns = this.hookCache.get(name);
|
|
2679
|
-
if (!fns) return;
|
|
2680
|
-
for (const fn of fns) {
|
|
2681
|
-
await fn(...args);
|
|
2682
|
-
}
|
|
2683
|
-
}
|
|
2684
|
-
hasHook(name) {
|
|
2685
|
-
if (!this.hooksInitialized) {
|
|
2686
|
-
this.ensureHooksInitialized();
|
|
2687
|
-
}
|
|
2688
|
-
return this.hookCache.has(name);
|
|
2689
|
-
}
|
|
2690
2959
|
}
|
|
2691
2960
|
class AuthPlugin extends ShokupanRouter {
|
|
2692
2961
|
constructor(authConfig) {
|
|
@@ -2734,7 +3003,9 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
2734
3003
|
return jwt;
|
|
2735
3004
|
}
|
|
2736
3005
|
init() {
|
|
2737
|
-
|
|
3006
|
+
const providerEntries = Object.entries(this.authConfig.providers);
|
|
3007
|
+
for (let i = 0; i < providerEntries.length; i++) {
|
|
3008
|
+
const [providerName, providerConfig] = providerEntries[i];
|
|
2738
3009
|
if (!providerConfig) continue;
|
|
2739
3010
|
const provider = this.getProviderInstance(providerName, providerConfig);
|
|
2740
3011
|
if (!provider) {
|
|
@@ -2757,9 +3028,10 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
2757
3028
|
} else {
|
|
2758
3029
|
return ctx.text("Provider config error", 500);
|
|
2759
3030
|
}
|
|
2760
|
-
|
|
3031
|
+
const isSecure = ctx.secure;
|
|
3032
|
+
ctx.res.headers.set("Set-Cookie", `oauth_state=${state}; Path=/; HttpOnly; SameSite=Lax${isSecure ? "; Secure" : ""}; Max-Age=600`);
|
|
2761
3033
|
if (codeVerifier) {
|
|
2762
|
-
ctx.res.headers.append("Set-Cookie", `oauth_verifier=${codeVerifier}; Path=/; HttpOnly; Max-Age=600`);
|
|
3034
|
+
ctx.res.headers.append("Set-Cookie", `oauth_verifier=${codeVerifier}; Path=/; HttpOnly; SameSite=Lax${isSecure ? "; Secure" : ""}; Max-Age=600`);
|
|
2763
3035
|
}
|
|
2764
3036
|
return ctx.redirect(url.toString());
|
|
2765
3037
|
});
|
|
@@ -2800,7 +3072,7 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
2800
3072
|
return ctx.json({ token: jwt, user });
|
|
2801
3073
|
} catch (e) {
|
|
2802
3074
|
console.error("Auth Error", e);
|
|
2803
|
-
return ctx.text("Authentication failed
|
|
3075
|
+
return ctx.text("Authentication failed. Please try again.", 500);
|
|
2804
3076
|
}
|
|
2805
3077
|
});
|
|
2806
3078
|
}
|
|
@@ -3011,14 +3283,21 @@ function Cors(options = {}) {
|
|
|
3011
3283
|
const origin = ctx.headers.get("origin");
|
|
3012
3284
|
const set = (k, v) => headers.set(k, v);
|
|
3013
3285
|
const append = (k, v) => headers.append(k, v);
|
|
3286
|
+
if (origin === "null" && opts.origin !== "null") {
|
|
3287
|
+
return next();
|
|
3288
|
+
}
|
|
3014
3289
|
if (opts.origin === "*") {
|
|
3015
3290
|
set("Access-Control-Allow-Origin", "*");
|
|
3016
3291
|
} else if (typeof opts.origin === "string") {
|
|
3017
3292
|
set("Access-Control-Allow-Origin", opts.origin);
|
|
3018
3293
|
} else if (Array.isArray(opts.origin)) {
|
|
3019
|
-
if (origin
|
|
3020
|
-
|
|
3021
|
-
|
|
3294
|
+
if (origin) {
|
|
3295
|
+
const normalizedOrigin = origin.toLowerCase();
|
|
3296
|
+
const normalizedAllowed = opts.origin.map((o) => o.toLowerCase());
|
|
3297
|
+
if (normalizedAllowed.includes(normalizedOrigin)) {
|
|
3298
|
+
set("Access-Control-Allow-Origin", origin);
|
|
3299
|
+
append("Vary", "Origin");
|
|
3300
|
+
}
|
|
3022
3301
|
}
|
|
3023
3302
|
} else if (typeof opts.origin === "function") {
|
|
3024
3303
|
const allowed = opts.origin(ctx);
|
|
@@ -3062,7 +3341,9 @@ function Cors(options = {}) {
|
|
|
3062
3341
|
}
|
|
3063
3342
|
const response = await next();
|
|
3064
3343
|
if (response instanceof Response) {
|
|
3065
|
-
|
|
3344
|
+
const headerEntries = Array.from(headers.entries());
|
|
3345
|
+
for (let i = 0; i < headerEntries.length; i++) {
|
|
3346
|
+
const [key, value] = headerEntries[i];
|
|
3066
3347
|
response.headers.set(key, value);
|
|
3067
3348
|
}
|
|
3068
3349
|
}
|
|
@@ -3132,6 +3413,8 @@ function useExpress(expressMiddleware) {
|
|
|
3132
3413
|
});
|
|
3133
3414
|
};
|
|
3134
3415
|
}
|
|
3416
|
+
let plainToInstance;
|
|
3417
|
+
let validateOrReject;
|
|
3135
3418
|
class ValidationError extends Error {
|
|
3136
3419
|
constructor(errors) {
|
|
3137
3420
|
super("Validation Error");
|
|
@@ -3196,9 +3479,21 @@ function isClass(schema) {
|
|
|
3196
3479
|
}
|
|
3197
3480
|
}
|
|
3198
3481
|
async function validateClassValidator(schema, data) {
|
|
3199
|
-
|
|
3482
|
+
if (!plainToInstance || !validateOrReject) {
|
|
3483
|
+
try {
|
|
3484
|
+
const ct = await import("class-transformer");
|
|
3485
|
+
const cv = await import("class-validator");
|
|
3486
|
+
plainToInstance = ct.plainToInstance;
|
|
3487
|
+
validateOrReject = cv.validateOrReject;
|
|
3488
|
+
} catch (e) {
|
|
3489
|
+
throw new Error(
|
|
3490
|
+
"class-transformer and class-validator are required for class-based validation. Install them with: bun add class-transformer class-validator reflect-metadata"
|
|
3491
|
+
);
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
const object = plainToInstance(schema, data);
|
|
3200
3495
|
try {
|
|
3201
|
-
await
|
|
3496
|
+
await validateOrReject(object);
|
|
3202
3497
|
return object;
|
|
3203
3498
|
} catch (errors) {
|
|
3204
3499
|
const formattedErrors = Array.isArray(errors) ? errors.map((err) => ({
|
|
@@ -3210,30 +3505,8 @@ async function validateClassValidator(schema, data) {
|
|
|
3210
3505
|
}
|
|
3211
3506
|
}
|
|
3212
3507
|
const safelyGetBody = async (ctx) => {
|
|
3213
|
-
const req = ctx.req;
|
|
3214
|
-
if (req._bodyParsed) {
|
|
3215
|
-
return req._bodyValue;
|
|
3216
|
-
}
|
|
3217
3508
|
try {
|
|
3218
|
-
|
|
3219
|
-
if (typeof req.json === "function") {
|
|
3220
|
-
data = await req.json();
|
|
3221
|
-
} else {
|
|
3222
|
-
data = req.body;
|
|
3223
|
-
if (typeof data === "string") {
|
|
3224
|
-
try {
|
|
3225
|
-
data = JSON.parse(data);
|
|
3226
|
-
} catch {
|
|
3227
|
-
}
|
|
3228
|
-
}
|
|
3229
|
-
}
|
|
3230
|
-
req._bodyParsed = true;
|
|
3231
|
-
req._bodyValue = data;
|
|
3232
|
-
Object.defineProperty(req, "json", {
|
|
3233
|
-
value: async () => req._bodyValue,
|
|
3234
|
-
configurable: true
|
|
3235
|
-
});
|
|
3236
|
-
return data;
|
|
3509
|
+
return await ctx.body();
|
|
3237
3510
|
} catch (e) {
|
|
3238
3511
|
return {};
|
|
3239
3512
|
}
|
|
@@ -3280,9 +3553,7 @@ function validate(config) {
|
|
|
3280
3553
|
body = await safelyGetBody(ctx);
|
|
3281
3554
|
dataToValidate.body = body;
|
|
3282
3555
|
}
|
|
3283
|
-
|
|
3284
|
-
await ctx.app.executeHook("beforeValidate", ctx, dataToValidate);
|
|
3285
|
-
}
|
|
3556
|
+
await ctx.app.runHooks("beforeValidate", ctx, dataToValidate);
|
|
3286
3557
|
if (validators.params) {
|
|
3287
3558
|
ctx.params = await validators.params(ctx.params);
|
|
3288
3559
|
}
|
|
@@ -3298,21 +3569,20 @@ function validate(config) {
|
|
|
3298
3569
|
if (validators.body) {
|
|
3299
3570
|
const b = body ?? await safelyGetBody(ctx);
|
|
3300
3571
|
validBody = await validators.body(b);
|
|
3572
|
+
ctx._cachedBody = validBody;
|
|
3301
3573
|
const req = ctx.req;
|
|
3302
|
-
req._bodyValue = validBody;
|
|
3303
3574
|
Object.defineProperty(req, "json", {
|
|
3304
3575
|
value: async () => validBody,
|
|
3576
|
+
writable: true,
|
|
3305
3577
|
configurable: true
|
|
3306
3578
|
});
|
|
3307
3579
|
ctx.body = validBody;
|
|
3308
3580
|
}
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
await ctx.app?.executeHook("afterValidate", ctx, validatedData);
|
|
3315
|
-
}
|
|
3581
|
+
const validatedData = { ...dataToValidate };
|
|
3582
|
+
if (config.params) validatedData.params = ctx.params;
|
|
3583
|
+
if (config.query) validatedData.query = validQuery;
|
|
3584
|
+
if (config.body) validatedData.body = validBody;
|
|
3585
|
+
await ctx.app.runHooks("afterValidate", ctx, validatedData);
|
|
3316
3586
|
return next();
|
|
3317
3587
|
};
|
|
3318
3588
|
}
|
|
@@ -3335,12 +3605,14 @@ function openApiValidator() {
|
|
|
3335
3605
|
if (cache.validators.has(ctx.path)) {
|
|
3336
3606
|
matchPath = ctx.path;
|
|
3337
3607
|
} else {
|
|
3338
|
-
|
|
3608
|
+
const pathEntries = Array.from(cache.paths.entries());
|
|
3609
|
+
for (let i = 0; i < pathEntries.length; i++) {
|
|
3610
|
+
const [path2, { regex, paramNames }] = pathEntries[i];
|
|
3339
3611
|
const match = regex.exec(ctx.path);
|
|
3340
3612
|
if (match) {
|
|
3341
3613
|
matchPath = path2;
|
|
3342
|
-
paramNames.forEach((name,
|
|
3343
|
-
matchParams[name] = match[
|
|
3614
|
+
paramNames.forEach((name, i2) => {
|
|
3615
|
+
matchParams[name] = match[i2 + 1];
|
|
3344
3616
|
});
|
|
3345
3617
|
break;
|
|
3346
3618
|
}
|
|
@@ -3397,7 +3669,9 @@ function openApiValidator() {
|
|
|
3397
3669
|
function compileValidators(spec) {
|
|
3398
3670
|
const validators = /* @__PURE__ */ new Map();
|
|
3399
3671
|
const paths = /* @__PURE__ */ new Map();
|
|
3400
|
-
|
|
3672
|
+
const pathEntries = Object.entries(spec.paths || {});
|
|
3673
|
+
for (let i = 0; i < pathEntries.length; i++) {
|
|
3674
|
+
const [path2, pathItem] = pathEntries[i];
|
|
3401
3675
|
if (path2.includes("{")) {
|
|
3402
3676
|
const paramNames = [];
|
|
3403
3677
|
const regexStr = "^" + path2.replace(/{([^}]+)}/g, (_, name) => {
|
|
@@ -3410,7 +3684,9 @@ function compileValidators(spec) {
|
|
|
3410
3684
|
});
|
|
3411
3685
|
}
|
|
3412
3686
|
const pathValidators = {};
|
|
3413
|
-
|
|
3687
|
+
const methodEntries = Object.entries(pathItem);
|
|
3688
|
+
for (let k = 0; k < methodEntries.length; k++) {
|
|
3689
|
+
const [method, operation] = methodEntries[k];
|
|
3414
3690
|
if (method === "parameters" || method === "summary" || method === "description") continue;
|
|
3415
3691
|
const oper = operation;
|
|
3416
3692
|
const opValidators = {};
|
|
@@ -3424,7 +3700,8 @@ function compileValidators(spec) {
|
|
|
3424
3700
|
const queryRequired = [];
|
|
3425
3701
|
const pathRequired = [];
|
|
3426
3702
|
const headerRequired = [];
|
|
3427
|
-
for (
|
|
3703
|
+
for (let j = 0; j < parameters.length; j++) {
|
|
3704
|
+
const param = parameters[j];
|
|
3428
3705
|
if (param.in === "query") {
|
|
3429
3706
|
queryProps[param.name] = param.schema || {};
|
|
3430
3707
|
if (param.required) queryRequired.push(param.name);
|
|
@@ -3585,14 +3862,18 @@ function SecurityHeaders(options = {}) {
|
|
|
3585
3862
|
if (opt === void 0 || opt === true) {
|
|
3586
3863
|
set("Content-Security-Policy", "default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests");
|
|
3587
3864
|
} else if (typeof opt === "object") {
|
|
3588
|
-
|
|
3865
|
+
const optEntries = Object.entries(opt);
|
|
3866
|
+
for (let i = 0; i < optEntries.length; i++) {
|
|
3867
|
+
const [key, val] = optEntries[i];
|
|
3589
3868
|
}
|
|
3590
3869
|
}
|
|
3591
3870
|
}
|
|
3592
3871
|
if (options.hidePoweredBy !== false) ;
|
|
3593
3872
|
const response = await next();
|
|
3594
3873
|
if (response instanceof Response) {
|
|
3595
|
-
|
|
3874
|
+
const headerEntries = Object.entries(headers);
|
|
3875
|
+
for (let i = 0; i < headerEntries.length; i++) {
|
|
3876
|
+
const [k, v] = headerEntries[i];
|
|
3596
3877
|
response.headers.set(k, v);
|
|
3597
3878
|
}
|
|
3598
3879
|
return response;
|
|
@@ -3678,7 +3959,9 @@ class MemoryStore extends events.EventEmitter {
|
|
|
3678
3959
|
}
|
|
3679
3960
|
all(cb) {
|
|
3680
3961
|
const result = {};
|
|
3681
|
-
|
|
3962
|
+
const sessionKeys = Object.keys(this.sessions);
|
|
3963
|
+
for (let i = 0; i < sessionKeys.length; i++) {
|
|
3964
|
+
const sid = sessionKeys[i];
|
|
3682
3965
|
try {
|
|
3683
3966
|
result[sid] = JSON.parse(this.sessions[sid]);
|
|
3684
3967
|
} catch {
|
|
@@ -3701,11 +3984,17 @@ function unsign(input, secret) {
|
|
|
3701
3984
|
if (typeof secret !== "string") throw new TypeError("Secret string must be provided.");
|
|
3702
3985
|
const tentValue = input.slice(0, input.lastIndexOf("."));
|
|
3703
3986
|
const expectedInput = sign(tentValue, secret);
|
|
3704
|
-
const
|
|
3705
|
-
const
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3987
|
+
const maxLength = Math.max(expectedInput.length, input.length);
|
|
3988
|
+
const paddedExpected = Buffer.alloc(maxLength);
|
|
3989
|
+
const paddedInput = Buffer.alloc(maxLength);
|
|
3990
|
+
Buffer.from(expectedInput).copy(paddedExpected);
|
|
3991
|
+
Buffer.from(input).copy(paddedInput);
|
|
3992
|
+
try {
|
|
3993
|
+
const valid = require("crypto").timingSafeEqual(paddedExpected, paddedInput);
|
|
3994
|
+
return valid ? tentValue : false;
|
|
3995
|
+
} catch {
|
|
3996
|
+
return false;
|
|
3997
|
+
}
|
|
3709
3998
|
}
|
|
3710
3999
|
function Session(options) {
|
|
3711
4000
|
const store = options.store || new MemoryStore();
|
|
@@ -3764,7 +4053,9 @@ function Session(options) {
|
|
|
3764
4053
|
sessObj.regenerate = (cb) => {
|
|
3765
4054
|
store.destroy(sessObj.id, (err) => {
|
|
3766
4055
|
sessionID = generateId(ctx);
|
|
3767
|
-
|
|
4056
|
+
const keys = Object.keys(sessObj);
|
|
4057
|
+
for (let i = 0; i < keys.length; i++) {
|
|
4058
|
+
const key = keys[i];
|
|
3768
4059
|
if (key !== "cookie" && key !== "id" && typeof sessObj[key] !== "function") {
|
|
3769
4060
|
delete sessObj[key];
|
|
3770
4061
|
}
|
|
@@ -3779,7 +4070,9 @@ function Session(options) {
|
|
|
3779
4070
|
store.get(sessObj.id, (err, sess2) => {
|
|
3780
4071
|
if (err) return cb(err);
|
|
3781
4072
|
if (!sess2) return cb(new Error("Session not found"));
|
|
3782
|
-
|
|
4073
|
+
const keys = Object.keys(sessObj);
|
|
4074
|
+
for (let i = 0; i < keys.length; i++) {
|
|
4075
|
+
const key = keys[i];
|
|
3783
4076
|
if (key !== "cookie" && key !== "id" && typeof sessObj[key] !== "function") {
|
|
3784
4077
|
delete sessObj[key];
|
|
3785
4078
|
}
|