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.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { Eta } from "eta";
|
|
3
3
|
import { stat, readdir, readFile as readFile$1 } from "fs/promises";
|
|
4
|
-
import { resolve, join, basename } from "path";
|
|
4
|
+
import { resolve, join, sep, basename } from "path";
|
|
5
5
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
|
-
import { createNodeEngines } from "@surrealdb/node";
|
|
7
|
-
import { Surreal, RecordId } from "surrealdb";
|
|
8
6
|
import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
|
|
9
7
|
import * as os from "node:os";
|
|
10
8
|
import { OAuth2Client, Okta, Auth0, Apple, MicrosoftEntraId, Google, GitHub, generateState, generateCodeVerifier } from "arctic";
|
|
@@ -12,9 +10,7 @@ import * as jose from "jose";
|
|
|
12
10
|
import * as zlib from "node:zlib";
|
|
13
11
|
import Ajv from "ajv";
|
|
14
12
|
import addFormats from "ajv-formats";
|
|
15
|
-
import {
|
|
16
|
-
import { validateOrReject } from "class-validator";
|
|
17
|
-
import { OpenAPIAnalyzer } from "./openapi-analyzer-D7y6Qa38.js";
|
|
13
|
+
import { OpenAPIAnalyzer } from "./openapi-analyzer-Ce_7JxZh.js";
|
|
18
14
|
import { randomUUID, createHmac } from "crypto";
|
|
19
15
|
import { EventEmitter } from "events";
|
|
20
16
|
class ShokupanResponse {
|
|
@@ -80,8 +76,82 @@ class ShokupanResponse {
|
|
|
80
76
|
return this._headers !== null;
|
|
81
77
|
}
|
|
82
78
|
}
|
|
79
|
+
function isValidCookieDomain(domain, currentHost) {
|
|
80
|
+
const hostWithoutPort = currentHost.split(":")[0];
|
|
81
|
+
if (domain === hostWithoutPort) return true;
|
|
82
|
+
if (domain.startsWith(".")) {
|
|
83
|
+
const domainWithoutDot = domain.slice(1);
|
|
84
|
+
return hostWithoutPort.endsWith(domainWithoutDot);
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
const VALID_HTTP_STATUSES = /* @__PURE__ */ new Set([
|
|
89
|
+
100,
|
|
90
|
+
101,
|
|
91
|
+
102,
|
|
92
|
+
103,
|
|
93
|
+
200,
|
|
94
|
+
201,
|
|
95
|
+
202,
|
|
96
|
+
203,
|
|
97
|
+
204,
|
|
98
|
+
205,
|
|
99
|
+
206,
|
|
100
|
+
207,
|
|
101
|
+
208,
|
|
102
|
+
226,
|
|
103
|
+
300,
|
|
104
|
+
301,
|
|
105
|
+
302,
|
|
106
|
+
303,
|
|
107
|
+
304,
|
|
108
|
+
305,
|
|
109
|
+
306,
|
|
110
|
+
307,
|
|
111
|
+
308,
|
|
112
|
+
400,
|
|
113
|
+
401,
|
|
114
|
+
402,
|
|
115
|
+
403,
|
|
116
|
+
404,
|
|
117
|
+
405,
|
|
118
|
+
406,
|
|
119
|
+
407,
|
|
120
|
+
408,
|
|
121
|
+
409,
|
|
122
|
+
410,
|
|
123
|
+
411,
|
|
124
|
+
412,
|
|
125
|
+
413,
|
|
126
|
+
414,
|
|
127
|
+
415,
|
|
128
|
+
416,
|
|
129
|
+
417,
|
|
130
|
+
418,
|
|
131
|
+
421,
|
|
132
|
+
422,
|
|
133
|
+
423,
|
|
134
|
+
424,
|
|
135
|
+
425,
|
|
136
|
+
426,
|
|
137
|
+
428,
|
|
138
|
+
429,
|
|
139
|
+
431,
|
|
140
|
+
451,
|
|
141
|
+
500,
|
|
142
|
+
501,
|
|
143
|
+
502,
|
|
144
|
+
503,
|
|
145
|
+
504,
|
|
146
|
+
505,
|
|
147
|
+
506,
|
|
148
|
+
507,
|
|
149
|
+
508,
|
|
150
|
+
510,
|
|
151
|
+
511
|
|
152
|
+
]);
|
|
153
|
+
const VALID_REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
83
154
|
class ShokupanContext {
|
|
84
|
-
// Raw body for compression optimization
|
|
85
155
|
constructor(request, server, state, app, signal, enableMiddlewareTracking = false) {
|
|
86
156
|
this.request = request;
|
|
87
157
|
this.server = server;
|
|
@@ -104,7 +174,6 @@ class ShokupanContext {
|
|
|
104
174
|
}
|
|
105
175
|
this.response = new ShokupanResponse();
|
|
106
176
|
}
|
|
107
|
-
_url;
|
|
108
177
|
params = {};
|
|
109
178
|
// Router assigns this, but default to empty object
|
|
110
179
|
state;
|
|
@@ -113,6 +182,19 @@ class ShokupanContext {
|
|
|
113
182
|
_debug;
|
|
114
183
|
_finalResponse;
|
|
115
184
|
_rawBody;
|
|
185
|
+
// Raw body for compression optimization
|
|
186
|
+
// Body caching to avoid double parsing
|
|
187
|
+
_url;
|
|
188
|
+
_cachedBody;
|
|
189
|
+
_bodyType;
|
|
190
|
+
_bodyParsed = false;
|
|
191
|
+
_bodyParseError;
|
|
192
|
+
// Cached URL properties to avoid repeated parsing
|
|
193
|
+
_cachedHostname;
|
|
194
|
+
_cachedProtocol;
|
|
195
|
+
_cachedHost;
|
|
196
|
+
_cachedOrigin;
|
|
197
|
+
_cachedQuery;
|
|
116
198
|
get url() {
|
|
117
199
|
if (!this._url) {
|
|
118
200
|
const urlString = this.request.url || "http://localhost/";
|
|
@@ -161,16 +243,24 @@ class ShokupanContext {
|
|
|
161
243
|
* Request query params
|
|
162
244
|
*/
|
|
163
245
|
get query() {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
246
|
+
if (this._cachedQuery) return this._cachedQuery;
|
|
247
|
+
const q = /* @__PURE__ */ Object.create(null);
|
|
248
|
+
const blocklist = ["__proto__", "constructor", "prototype"];
|
|
249
|
+
const entries = Object.entries(this.url.searchParams);
|
|
250
|
+
for (let i = 0; i < entries.length; i++) {
|
|
251
|
+
const [key, value] = entries[i];
|
|
252
|
+
if (blocklist.includes(key)) continue;
|
|
253
|
+
if (Object.prototype.hasOwnProperty.call(q, key)) {
|
|
254
|
+
if (Array.isArray(q[key])) {
|
|
255
|
+
q[key].push(value);
|
|
256
|
+
} else {
|
|
257
|
+
q[key] = [q[key], value];
|
|
258
|
+
}
|
|
170
259
|
} else {
|
|
171
|
-
q[key] =
|
|
260
|
+
q[key] = value;
|
|
172
261
|
}
|
|
173
262
|
}
|
|
263
|
+
this._cachedQuery = q;
|
|
174
264
|
return q;
|
|
175
265
|
}
|
|
176
266
|
/**
|
|
@@ -183,31 +273,31 @@ class ShokupanContext {
|
|
|
183
273
|
* Request hostname (e.g. "localhost")
|
|
184
274
|
*/
|
|
185
275
|
get hostname() {
|
|
186
|
-
return this.url.hostname;
|
|
276
|
+
return this._cachedHostname ??= this.url.hostname;
|
|
187
277
|
}
|
|
188
278
|
/**
|
|
189
279
|
* Request host (e.g. "localhost:3000")
|
|
190
280
|
*/
|
|
191
281
|
get host() {
|
|
192
|
-
return this.url.host;
|
|
282
|
+
return this._cachedHost ??= this.url.host;
|
|
193
283
|
}
|
|
194
284
|
/**
|
|
195
285
|
* Request protocol (e.g. "http:", "https:")
|
|
196
286
|
*/
|
|
197
287
|
get protocol() {
|
|
198
|
-
return this.url.protocol;
|
|
288
|
+
return this._cachedProtocol ??= this.url.protocol;
|
|
199
289
|
}
|
|
200
290
|
/**
|
|
201
291
|
* Whether request is secure (https)
|
|
202
292
|
*/
|
|
203
293
|
get secure() {
|
|
204
|
-
return this.
|
|
294
|
+
return this.protocol === "https:";
|
|
205
295
|
}
|
|
206
296
|
/**
|
|
207
297
|
* Request origin (e.g. "http://localhost:3000")
|
|
208
298
|
*/
|
|
209
299
|
get origin() {
|
|
210
|
-
return this.url.origin;
|
|
300
|
+
return this._cachedOrigin ??= this.url.origin;
|
|
211
301
|
}
|
|
212
302
|
/**
|
|
213
303
|
* Request headers
|
|
@@ -244,6 +334,12 @@ class ShokupanContext {
|
|
|
244
334
|
* @param options Cookie options
|
|
245
335
|
*/
|
|
246
336
|
setCookie(name, value, options = {}) {
|
|
337
|
+
if (options.domain) {
|
|
338
|
+
const currentHost = this.hostname;
|
|
339
|
+
if (!isValidCookieDomain(options.domain, currentHost)) {
|
|
340
|
+
throw new Error(`Invalid cookie domain: ${options.domain} for host ${currentHost}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
247
343
|
let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
|
|
248
344
|
if (options.maxAge) cookie += `; Max-Age=${Math.floor(options.maxAge)}`;
|
|
249
345
|
if (options.domain) cookie += `; Domain=${options.domain}`;
|
|
@@ -303,6 +399,91 @@ class ShokupanContext {
|
|
|
303
399
|
}
|
|
304
400
|
return h;
|
|
305
401
|
}
|
|
402
|
+
/**
|
|
403
|
+
* Read request body with caching to avoid double parsing.
|
|
404
|
+
* The body is only parsed once and cached for subsequent reads.
|
|
405
|
+
*/
|
|
406
|
+
async body() {
|
|
407
|
+
if (this._bodyParseError) {
|
|
408
|
+
throw this._bodyParseError;
|
|
409
|
+
}
|
|
410
|
+
if (this._bodyParsed) {
|
|
411
|
+
return this._cachedBody;
|
|
412
|
+
}
|
|
413
|
+
const contentType = this.request.headers.get("content-type") || "";
|
|
414
|
+
if (contentType.includes("application/json") || contentType.includes("+json")) {
|
|
415
|
+
const rawText = await this.readRawBody();
|
|
416
|
+
const parserType = this.app?.applicationConfig?.jsonParser || "native";
|
|
417
|
+
if (parserType === "native") {
|
|
418
|
+
this._cachedBody = JSON.parse(rawText);
|
|
419
|
+
} else {
|
|
420
|
+
const { getJSONParser } = await import("./json-parser-B3dnQmCC.js");
|
|
421
|
+
const parser = getJSONParser(parserType);
|
|
422
|
+
this._cachedBody = parser(rawText);
|
|
423
|
+
}
|
|
424
|
+
this._bodyType = "json";
|
|
425
|
+
} else if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
|
|
426
|
+
this._cachedBody = await this.request.formData();
|
|
427
|
+
this._bodyType = "formData";
|
|
428
|
+
} else {
|
|
429
|
+
this._cachedBody = await this.readRawBody();
|
|
430
|
+
this._bodyType = "text";
|
|
431
|
+
}
|
|
432
|
+
this._bodyParsed = true;
|
|
433
|
+
return this._cachedBody;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Pre-parse the request body before handler execution.
|
|
437
|
+
* This improves performance and enables Node.js compatibility for large payloads.
|
|
438
|
+
* Errors are deferred until the body is actually accessed in the handler.
|
|
439
|
+
*/
|
|
440
|
+
async parseBody() {
|
|
441
|
+
if (this._bodyParsed) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (this.request.method === "GET" || this.request.method === "HEAD") {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
await this.body();
|
|
449
|
+
} catch (error) {
|
|
450
|
+
this._bodyParseError = error;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Read raw body from ReadableStream efficiently.
|
|
455
|
+
* This is much faster than request.text() for large payloads.
|
|
456
|
+
* Also handles the case where body is already a string (e.g., in tests).
|
|
457
|
+
*/
|
|
458
|
+
async readRawBody() {
|
|
459
|
+
if (typeof this.request.body === "string") {
|
|
460
|
+
return this.request.body;
|
|
461
|
+
}
|
|
462
|
+
const reader = this.request.body?.getReader();
|
|
463
|
+
if (!reader) {
|
|
464
|
+
return "";
|
|
465
|
+
}
|
|
466
|
+
const chunks = [];
|
|
467
|
+
let totalSize = 0;
|
|
468
|
+
try {
|
|
469
|
+
while (true) {
|
|
470
|
+
const { done, value } = await reader.read();
|
|
471
|
+
if (done) break;
|
|
472
|
+
chunks.push(value);
|
|
473
|
+
totalSize += value.length;
|
|
474
|
+
}
|
|
475
|
+
} finally {
|
|
476
|
+
reader.releaseLock();
|
|
477
|
+
}
|
|
478
|
+
const result = new Uint8Array(totalSize);
|
|
479
|
+
let offset = 0;
|
|
480
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
481
|
+
const chunk = chunks[i];
|
|
482
|
+
result.set(chunk, offset);
|
|
483
|
+
offset += chunk.length;
|
|
484
|
+
}
|
|
485
|
+
return new TextDecoder().decode(result);
|
|
486
|
+
}
|
|
306
487
|
/**
|
|
307
488
|
* Send a response
|
|
308
489
|
* @param body Response body
|
|
@@ -311,31 +492,24 @@ class ShokupanContext {
|
|
|
311
492
|
*/
|
|
312
493
|
send(body, options) {
|
|
313
494
|
const headers = this.mergeHeaders(options?.headers);
|
|
314
|
-
const status = options?.status ?? this.response.status;
|
|
495
|
+
const status = options?.status ?? this.response.status ?? 200;
|
|
496
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
|
|
497
|
+
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
498
|
+
}
|
|
315
499
|
if (typeof body === "string" || body instanceof ArrayBuffer || body instanceof Uint8Array) {
|
|
316
500
|
this._rawBody = body;
|
|
317
501
|
}
|
|
318
502
|
this._finalResponse = new Response(body, { status, headers });
|
|
319
503
|
return this._finalResponse;
|
|
320
504
|
}
|
|
321
|
-
/**
|
|
322
|
-
* Read request body
|
|
323
|
-
*/
|
|
324
|
-
async body() {
|
|
325
|
-
const contentType = this.request.headers.get("content-type") || "";
|
|
326
|
-
if (contentType.includes("application/json") || contentType.includes("+json")) {
|
|
327
|
-
return this.request.json();
|
|
328
|
-
}
|
|
329
|
-
if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
|
|
330
|
-
return this.request.formData();
|
|
331
|
-
}
|
|
332
|
-
return this.request.text();
|
|
333
|
-
}
|
|
334
505
|
/**
|
|
335
506
|
* Respond with a JSON object
|
|
336
507
|
*/
|
|
337
508
|
json(data, status, headers) {
|
|
338
|
-
const finalStatus = status ?? this.response.status;
|
|
509
|
+
const finalStatus = status ?? this.response.status ?? 200;
|
|
510
|
+
if (!VALID_HTTP_STATUSES.has(finalStatus)) {
|
|
511
|
+
throw new Error(`Invalid HTTP status code: ${finalStatus}`);
|
|
512
|
+
}
|
|
339
513
|
const jsonString = JSON.stringify(data);
|
|
340
514
|
this._rawBody = jsonString;
|
|
341
515
|
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
@@ -354,7 +528,10 @@ class ShokupanContext {
|
|
|
354
528
|
* Respond with a text string
|
|
355
529
|
*/
|
|
356
530
|
text(data, status, headers) {
|
|
357
|
-
const finalStatus = status ?? this.response.status;
|
|
531
|
+
const finalStatus = status ?? this.response.status ?? 200;
|
|
532
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
|
|
533
|
+
throw new Error(`Invalid HTTP status code: ${finalStatus}`);
|
|
534
|
+
}
|
|
358
535
|
this._rawBody = data;
|
|
359
536
|
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
360
537
|
this._finalResponse = new Response(data, {
|
|
@@ -372,7 +549,10 @@ class ShokupanContext {
|
|
|
372
549
|
* Respond with HTML content
|
|
373
550
|
*/
|
|
374
551
|
html(html, status, headers) {
|
|
375
|
-
const finalStatus = status ?? this.response.status;
|
|
552
|
+
const finalStatus = status ?? this.response.status ?? 200;
|
|
553
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
|
|
554
|
+
throw new Error(`Invalid HTTP status code: ${finalStatus}`);
|
|
555
|
+
}
|
|
376
556
|
const finalHeaders = this.mergeHeaders(headers);
|
|
377
557
|
finalHeaders.set("content-type", "text/html; charset=utf-8");
|
|
378
558
|
this._rawBody = html;
|
|
@@ -383,6 +563,9 @@ class ShokupanContext {
|
|
|
383
563
|
* Respond with a redirect
|
|
384
564
|
*/
|
|
385
565
|
redirect(url, status = 302) {
|
|
566
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_REDIRECT_STATUSES.has(status)) {
|
|
567
|
+
throw new Error(`Invalid redirect status code: ${status}`);
|
|
568
|
+
}
|
|
386
569
|
const headers = this.mergeHeaders();
|
|
387
570
|
headers.set("Location", url);
|
|
388
571
|
this._finalResponse = new Response(null, { status, headers });
|
|
@@ -393,6 +576,9 @@ class ShokupanContext {
|
|
|
393
576
|
* DOES NOT CHAIN!
|
|
394
577
|
*/
|
|
395
578
|
status(status) {
|
|
579
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
|
|
580
|
+
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
581
|
+
}
|
|
396
582
|
const headers = this.mergeHeaders();
|
|
397
583
|
this._finalResponse = new Response(null, { status, headers });
|
|
398
584
|
return this._finalResponse;
|
|
@@ -403,6 +589,9 @@ class ShokupanContext {
|
|
|
403
589
|
async file(path, fileOptions, responseOptions) {
|
|
404
590
|
const headers = this.mergeHeaders(responseOptions?.headers);
|
|
405
591
|
const status = responseOptions?.status ?? this.response.status;
|
|
592
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
|
|
593
|
+
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
594
|
+
}
|
|
406
595
|
if (typeof Bun !== "undefined") {
|
|
407
596
|
this._finalResponse = new Response(Bun.file(path, fileOptions), { status, headers });
|
|
408
597
|
return this._finalResponse;
|
|
@@ -426,6 +615,10 @@ class ShokupanContext {
|
|
|
426
615
|
* @param headers HTTP Headers
|
|
427
616
|
*/
|
|
428
617
|
async jsx(element, args, status, headers) {
|
|
618
|
+
status ??= 200;
|
|
619
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
|
|
620
|
+
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
621
|
+
}
|
|
429
622
|
if (!this.renderer) {
|
|
430
623
|
throw new Error("No JSX renderer configured");
|
|
431
624
|
}
|
|
@@ -440,17 +633,32 @@ function RateLimitMiddleware(options = {}) {
|
|
|
440
633
|
const statusCode = options.statusCode || 429;
|
|
441
634
|
const headers = options.headers !== false;
|
|
442
635
|
const mode = options.mode || "user";
|
|
636
|
+
const trustedProxies = options.trustedProxies || [];
|
|
443
637
|
const keyGenerator = options.keyGenerator || ((ctx) => {
|
|
444
638
|
if (mode === "absolute") {
|
|
445
639
|
return "global";
|
|
446
640
|
}
|
|
447
|
-
|
|
641
|
+
const xForwardedFor = ctx.headers.get("x-forwarded-for");
|
|
642
|
+
if (xForwardedFor && trustedProxies.length > 0) {
|
|
643
|
+
const ips = xForwardedFor.split(",").map((ip) => ip.trim());
|
|
644
|
+
for (let i = ips.length - 1; i >= 0; i--) {
|
|
645
|
+
const ip = ips[i];
|
|
646
|
+
if (!trustedProxies.includes(ip)) {
|
|
647
|
+
if (/^[\d.:a-fA-F]+$/.test(ip)) {
|
|
648
|
+
return ip;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return ctx.server?.requestIP?.(ctx.request)?.address || "unknown";
|
|
448
654
|
});
|
|
449
655
|
const skip = options.skip || (() => false);
|
|
450
656
|
const hits = /* @__PURE__ */ new Map();
|
|
451
657
|
const interval = setInterval(() => {
|
|
452
658
|
const now = Date.now();
|
|
453
|
-
|
|
659
|
+
const entries = Array.from(hits.entries());
|
|
660
|
+
for (let i = 0; i < entries.length; i++) {
|
|
661
|
+
const [key, record] = entries[i];
|
|
454
662
|
if (record.resetTime <= now) {
|
|
455
663
|
hits.delete(key);
|
|
456
664
|
}
|
|
@@ -703,7 +911,9 @@ function deepMerge(target, ...sources) {
|
|
|
703
911
|
if (!sources.length) return target;
|
|
704
912
|
const source = sources.shift();
|
|
705
913
|
if (isObject(target) && isObject(source)) {
|
|
706
|
-
|
|
914
|
+
const sourceKeys = Object.keys(source);
|
|
915
|
+
for (let i = 0; i < sourceKeys.length; i++) {
|
|
916
|
+
const key = sourceKeys[i];
|
|
707
917
|
if (isObject(source[key])) {
|
|
708
918
|
if (!target[key]) Object.assign(target, { [key]: {} });
|
|
709
919
|
deepMerge(target[key], source[key]);
|
|
@@ -727,15 +937,17 @@ function deepMerge(target, ...sources) {
|
|
|
727
937
|
}
|
|
728
938
|
return deepMerge(target, ...sources);
|
|
729
939
|
}
|
|
730
|
-
const
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
940
|
+
const REGEX_PATTERNS = {
|
|
941
|
+
QUERY_INT: /parseInt\(ctx\.query\.(\w+)\)/g,
|
|
942
|
+
QUERY_FLOAT: /parseFloat\(ctx\.query\.(\w+)\)/g,
|
|
943
|
+
QUERY_NUMBER: /Number\(ctx\.query\.(\w+)\)/g,
|
|
944
|
+
QUERY_BOOL: /(?:Boolean\(ctx\.query\.(\w+)\)|!+ctx\.query\.(\w+))/g,
|
|
945
|
+
QUERY_GENERIC: /ctx\.query\.(\w+)/g,
|
|
946
|
+
PARAM_INT: /parseInt\(ctx\.params\.(\w+)\)/g,
|
|
947
|
+
PARAM_FLOAT: /parseFloat\(ctx\.params\.(\w+)\)/g,
|
|
948
|
+
HEADER_GET: /ctx\.get\(['"](\w+)['"]\)/g,
|
|
949
|
+
ERROR_STATUS: /ctx\.(?:json|text|html)\([^)]+,\s*(\d{3,})\)/g
|
|
950
|
+
};
|
|
739
951
|
function analyzeHandler(handler) {
|
|
740
952
|
const handlerSource = handler.toString();
|
|
741
953
|
const inferredSpec = {};
|
|
@@ -745,29 +957,20 @@ function analyzeHandler(handler) {
|
|
|
745
957
|
};
|
|
746
958
|
}
|
|
747
959
|
const queryParams = /* @__PURE__ */ new Map();
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
if (match[1] && !queryParams.has(match[1])) {
|
|
756
|
-
queryParams.set(match[1], { type: "number" });
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
for (const match of handlerSource.matchAll(REGEX_QUERY_BOOL)) {
|
|
760
|
-
const name = match[1] || match[2];
|
|
761
|
-
if (name && !queryParams.has(name)) {
|
|
762
|
-
queryParams.set(name, { type: "boolean" });
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
for (const match of handlerSource.matchAll(REGEX_QUERY_GENERIC)) {
|
|
766
|
-
const name = match[1];
|
|
767
|
-
if (name && !queryParams.has(name)) {
|
|
768
|
-
queryParams.set(name, { type: "string" });
|
|
960
|
+
const processMatches = (regex, type, format) => {
|
|
961
|
+
const matches = Array.from(handlerSource.matchAll(regex));
|
|
962
|
+
for (const match of matches) {
|
|
963
|
+
const name = match[1] || match[2];
|
|
964
|
+
if (name && !queryParams.has(name)) {
|
|
965
|
+
queryParams.set(name, { type, format });
|
|
966
|
+
}
|
|
769
967
|
}
|
|
770
|
-
}
|
|
968
|
+
};
|
|
969
|
+
processMatches(REGEX_PATTERNS.QUERY_INT, "integer", "int32");
|
|
970
|
+
processMatches(REGEX_PATTERNS.QUERY_FLOAT, "number", "float");
|
|
971
|
+
processMatches(REGEX_PATTERNS.QUERY_NUMBER, "number");
|
|
972
|
+
processMatches(REGEX_PATTERNS.QUERY_BOOL, "boolean");
|
|
973
|
+
processMatches(REGEX_PATTERNS.QUERY_GENERIC, "string");
|
|
771
974
|
if (queryParams.size > 0) {
|
|
772
975
|
if (!inferredSpec.parameters) inferredSpec.parameters = [];
|
|
773
976
|
queryParams.forEach((schema, paramName) => {
|
|
@@ -779,12 +982,15 @@ function analyzeHandler(handler) {
|
|
|
779
982
|
});
|
|
780
983
|
}
|
|
781
984
|
const pathParams = /* @__PURE__ */ new Map();
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
985
|
+
const processPathMatches = (regex, type, format) => {
|
|
986
|
+
const matches = Array.from(handlerSource.matchAll(regex));
|
|
987
|
+
for (const match of matches) {
|
|
988
|
+
const name = match[1];
|
|
989
|
+
if (name) pathParams.set(name, { type, format });
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
processPathMatches(REGEX_PATTERNS.PARAM_INT, "integer", "int32");
|
|
993
|
+
processPathMatches(REGEX_PATTERNS.PARAM_FLOAT, "number", "float");
|
|
788
994
|
if (pathParams.size > 0) {
|
|
789
995
|
if (!inferredSpec.parameters) inferredSpec.parameters = [];
|
|
790
996
|
pathParams.forEach((schema, paramName) => {
|
|
@@ -796,7 +1002,8 @@ function analyzeHandler(handler) {
|
|
|
796
1002
|
});
|
|
797
1003
|
});
|
|
798
1004
|
}
|
|
799
|
-
|
|
1005
|
+
const headerMatches = Array.from(handlerSource.matchAll(REGEX_PATTERNS.HEADER_GET));
|
|
1006
|
+
for (const match of headerMatches) {
|
|
800
1007
|
if (match[1]) {
|
|
801
1008
|
if (!inferredSpec.parameters) inferredSpec.parameters = [];
|
|
802
1009
|
inferredSpec.parameters.push({
|
|
@@ -815,13 +1022,19 @@ function analyzeHandler(handler) {
|
|
|
815
1022
|
}
|
|
816
1023
|
if (handlerSource.includes("ctx.html(")) {
|
|
817
1024
|
responses["200"] = {
|
|
818
|
-
description: "Successful response",
|
|
1025
|
+
description: "Successful HTML response",
|
|
1026
|
+
content: { "text/html": { schema: { type: "string" } } }
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
if (handlerSource.includes("ctx.jsx(")) {
|
|
1030
|
+
responses["200"] = {
|
|
1031
|
+
description: "Successful HTML response (Rendered JSX)",
|
|
819
1032
|
content: { "text/html": { schema: { type: "string" } } }
|
|
820
1033
|
};
|
|
821
1034
|
}
|
|
822
1035
|
if (handlerSource.includes("ctx.text(")) {
|
|
823
1036
|
responses["200"] = {
|
|
824
|
-
description: "Successful response",
|
|
1037
|
+
description: "Successful text response",
|
|
825
1038
|
content: { "text/plain": { schema: { type: "string" } } }
|
|
826
1039
|
};
|
|
827
1040
|
}
|
|
@@ -832,7 +1045,18 @@ function analyzeHandler(handler) {
|
|
|
832
1045
|
};
|
|
833
1046
|
}
|
|
834
1047
|
if (handlerSource.includes("ctx.redirect(")) {
|
|
835
|
-
|
|
1048
|
+
let hasSpecificRedirect = false;
|
|
1049
|
+
const redirectMatches = Array.from(handlerSource.matchAll(/ctx\.redirect\([^,]+,\s*(\d{3})\)/g));
|
|
1050
|
+
for (const match of redirectMatches) {
|
|
1051
|
+
const status = match[1];
|
|
1052
|
+
if (/^30[12378]$/.test(status)) {
|
|
1053
|
+
responses[status] = { description: `Redirect (${status})` };
|
|
1054
|
+
hasSpecificRedirect = true;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
if (!hasSpecificRedirect) {
|
|
1058
|
+
responses["302"] = { description: "Redirect" };
|
|
1059
|
+
}
|
|
836
1060
|
}
|
|
837
1061
|
if (!responses["200"] && /return\s+\{/.test(handlerSource)) {
|
|
838
1062
|
responses["200"] = {
|
|
@@ -840,7 +1064,8 @@ function analyzeHandler(handler) {
|
|
|
840
1064
|
content: { "application/json": { schema: { type: "object" } } }
|
|
841
1065
|
};
|
|
842
1066
|
}
|
|
843
|
-
|
|
1067
|
+
const errorStatusMatches = Array.from(handlerSource.matchAll(REGEX_PATTERNS.ERROR_STATUS));
|
|
1068
|
+
for (const match of errorStatusMatches) {
|
|
844
1069
|
const statusCode = match[1];
|
|
845
1070
|
if (statusCode && statusCode !== "200") {
|
|
846
1071
|
responses[statusCode] = { description: `Error response (${statusCode})` };
|
|
@@ -851,6 +1076,52 @@ function analyzeHandler(handler) {
|
|
|
851
1076
|
}
|
|
852
1077
|
return { inferredSpec };
|
|
853
1078
|
}
|
|
1079
|
+
async function getAstRoutes(applications) {
|
|
1080
|
+
const astRoutes = [];
|
|
1081
|
+
const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
|
|
1082
|
+
if (seen.has(app.name)) return [];
|
|
1083
|
+
const newSeen = new Set(seen);
|
|
1084
|
+
newSeen.add(app.name);
|
|
1085
|
+
const expanded = [];
|
|
1086
|
+
for (const route of app.routes) {
|
|
1087
|
+
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1088
|
+
const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
|
|
1089
|
+
let joined = cleanPrefix + cleanPath;
|
|
1090
|
+
if (joined.length > 1 && joined.endsWith("/")) {
|
|
1091
|
+
joined = joined.slice(0, -1);
|
|
1092
|
+
}
|
|
1093
|
+
expanded.push({
|
|
1094
|
+
...route,
|
|
1095
|
+
path: joined || "/"
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
if (app.mounted) {
|
|
1099
|
+
for (const mount of app.mounted) {
|
|
1100
|
+
const targetApp = applications.find((a) => a.name === mount.target || a.className === mount.target);
|
|
1101
|
+
if (targetApp) {
|
|
1102
|
+
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1103
|
+
const mountPrefix = mount.prefix.startsWith("/") ? mount.prefix : "/" + mount.prefix;
|
|
1104
|
+
expanded.push(...getExpandedRoutes(targetApp, cleanPrefix + mountPrefix, newSeen));
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
return expanded;
|
|
1109
|
+
};
|
|
1110
|
+
applications.forEach((app) => {
|
|
1111
|
+
astRoutes.push(...getExpandedRoutes(app));
|
|
1112
|
+
});
|
|
1113
|
+
const dedupedRoutes = /* @__PURE__ */ new Map();
|
|
1114
|
+
for (const route of astRoutes) {
|
|
1115
|
+
const key = `${route.method.toUpperCase()}:${route.path}`;
|
|
1116
|
+
let score = 0;
|
|
1117
|
+
if (route.responseSchema) score += 10;
|
|
1118
|
+
if (route.handlerSource) score += 5;
|
|
1119
|
+
if (!dedupedRoutes.has(key) || score > dedupedRoutes.get(key).score) {
|
|
1120
|
+
dedupedRoutes.set(key, { route, score });
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
return Array.from(dedupedRoutes.values()).map((v) => v.route);
|
|
1124
|
+
}
|
|
854
1125
|
async function generateOpenApi(rootRouter, options = {}) {
|
|
855
1126
|
const paths = {};
|
|
856
1127
|
const tagGroups = /* @__PURE__ */ new Map();
|
|
@@ -858,61 +1129,11 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
858
1129
|
const defaultTagName = options.defaultTag || "Application";
|
|
859
1130
|
let astRoutes = [];
|
|
860
1131
|
try {
|
|
861
|
-
const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./openapi-analyzer-
|
|
1132
|
+
const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./openapi-analyzer-Ce_7JxZh.js");
|
|
862
1133
|
const analyzer = new OpenAPIAnalyzer2(process.cwd());
|
|
863
1134
|
const { applications } = await analyzer.analyze();
|
|
864
|
-
|
|
865
|
-
applications.forEach((app) => {
|
|
866
|
-
appMap.set(app.name, app);
|
|
867
|
-
if (app.name !== app.className) {
|
|
868
|
-
appMap.set(app.className, app);
|
|
869
|
-
}
|
|
870
|
-
});
|
|
871
|
-
const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
|
|
872
|
-
if (seen.has(app.name)) return [];
|
|
873
|
-
const newSeen = new Set(seen);
|
|
874
|
-
newSeen.add(app.name);
|
|
875
|
-
const expanded = [];
|
|
876
|
-
for (const route of app.routes) {
|
|
877
|
-
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
878
|
-
const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
|
|
879
|
-
let joined = cleanPrefix + cleanPath;
|
|
880
|
-
if (joined.length > 1 && joined.endsWith("/")) {
|
|
881
|
-
joined = joined.slice(0, -1);
|
|
882
|
-
}
|
|
883
|
-
expanded.push({
|
|
884
|
-
...route,
|
|
885
|
-
path: joined || "/"
|
|
886
|
-
});
|
|
887
|
-
}
|
|
888
|
-
if (app.mounted) {
|
|
889
|
-
for (const mount of app.mounted) {
|
|
890
|
-
const targetApp = appMap.get(mount.target);
|
|
891
|
-
if (targetApp) {
|
|
892
|
-
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
893
|
-
const mountPrefix = mount.prefix.startsWith("/") ? mount.prefix : "/" + mount.prefix;
|
|
894
|
-
expanded.push(...getExpandedRoutes(targetApp, cleanPrefix + mountPrefix, newSeen));
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
return expanded;
|
|
899
|
-
};
|
|
900
|
-
applications.forEach((app) => {
|
|
901
|
-
astRoutes.push(...getExpandedRoutes(app));
|
|
902
|
-
});
|
|
903
|
-
const dedupedRoutes = /* @__PURE__ */ new Map();
|
|
904
|
-
for (const route of astRoutes) {
|
|
905
|
-
const key = `${route.method.toUpperCase()}:${route.path}`;
|
|
906
|
-
let score = 0;
|
|
907
|
-
if (route.responseSchema) score += 10;
|
|
908
|
-
if (route.handlerSource) score += 5;
|
|
909
|
-
if (!dedupedRoutes.has(key) || score > dedupedRoutes.get(key).score) {
|
|
910
|
-
dedupedRoutes.set(key, { route, score });
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
astRoutes = Array.from(dedupedRoutes.values()).map((v) => v.route);
|
|
1135
|
+
astRoutes = await getAstRoutes(applications);
|
|
914
1136
|
} catch (e) {
|
|
915
|
-
console.warn("OpenAPI AST analysis failed or skipped:", e);
|
|
916
1137
|
}
|
|
917
1138
|
const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName) => {
|
|
918
1139
|
let group = currentGroup;
|
|
@@ -969,33 +1190,15 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
969
1190
|
(r) => r.method.toUpperCase() === route.method.toUpperCase() && r.path === fullPath
|
|
970
1191
|
);
|
|
971
1192
|
if (!astMatch) {
|
|
972
|
-
|
|
973
|
-
if (route.handler.originalHandler) {
|
|
974
|
-
runtimeSource = route.handler.originalHandler.toString();
|
|
975
|
-
}
|
|
1193
|
+
const runtimeSource = (route.handler.originalHandler || route.handler).toString();
|
|
976
1194
|
const runtimeHandlerSrc = runtimeSource.replace(/\s+/g, " ");
|
|
977
1195
|
const sameMethodRoutes = astRoutes.filter((r) => r.method.toUpperCase() === route.method.toUpperCase());
|
|
978
1196
|
astMatch = sameMethodRoutes.find((r) => {
|
|
979
1197
|
const astHandlerSrc = (r.handlerSource || r.handlerName || "").replace(/\s+/g, " ");
|
|
980
1198
|
if (!astHandlerSrc || astHandlerSrc.length < 20) return false;
|
|
981
|
-
|
|
982
|
-
return match;
|
|
1199
|
+
return runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(r.handlerSource.substring(0, 50));
|
|
983
1200
|
});
|
|
984
1201
|
}
|
|
985
|
-
const potentialMatches = astRoutes.filter(
|
|
986
|
-
(r) => r.method.toUpperCase() === route.method.toUpperCase() && r.path === fullPath
|
|
987
|
-
);
|
|
988
|
-
if (potentialMatches.length > 1) {
|
|
989
|
-
const runtimeHandlerSrc = route.handler.toString().replace(/\s+/g, " ");
|
|
990
|
-
const preciseMatch = potentialMatches.find((r) => {
|
|
991
|
-
const astHandlerSrc = (r.handlerSource || r.handlerName || "").replace(/\s+/g, " ");
|
|
992
|
-
const match = runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(r.handlerSource.substring(0, 50));
|
|
993
|
-
return match;
|
|
994
|
-
});
|
|
995
|
-
if (preciseMatch) {
|
|
996
|
-
astMatch = preciseMatch;
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
1202
|
if (astMatch) {
|
|
1000
1203
|
if (astMatch.summary) operation.summary = astMatch.summary;
|
|
1001
1204
|
if (astMatch.description) operation.description = astMatch.description;
|
|
@@ -1003,25 +1206,19 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1003
1206
|
if (astMatch.operationId) operation.operationId = astMatch.operationId;
|
|
1004
1207
|
if (astMatch.requestTypes?.body) {
|
|
1005
1208
|
operation.requestBody = {
|
|
1006
|
-
content: {
|
|
1007
|
-
"application/json": { schema: astMatch.requestTypes.body }
|
|
1008
|
-
}
|
|
1209
|
+
content: { "application/json": { schema: astMatch.requestTypes.body } }
|
|
1009
1210
|
};
|
|
1010
1211
|
}
|
|
1011
1212
|
if (astMatch.responseSchema) {
|
|
1012
1213
|
operation.responses["200"] = {
|
|
1013
1214
|
description: "Successful response",
|
|
1014
|
-
content: {
|
|
1015
|
-
"application/json": { schema: astMatch.responseSchema }
|
|
1016
|
-
}
|
|
1215
|
+
content: { "application/json": { schema: astMatch.responseSchema } }
|
|
1017
1216
|
};
|
|
1018
1217
|
} else if (astMatch.responseType) {
|
|
1019
1218
|
const contentType = astMatch.responseType === "string" ? "text/plain" : "application/json";
|
|
1020
1219
|
operation.responses["200"] = {
|
|
1021
1220
|
description: "Successful response",
|
|
1022
|
-
content: {
|
|
1023
|
-
[contentType]: { schema: { type: astMatch.responseType } }
|
|
1024
|
-
}
|
|
1221
|
+
content: { [contentType]: { schema: { type: astMatch.responseType } } }
|
|
1025
1222
|
};
|
|
1026
1223
|
}
|
|
1027
1224
|
const params = [];
|
|
@@ -1072,15 +1269,7 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1072
1269
|
deepMerge(operation, inferredSpec);
|
|
1073
1270
|
}
|
|
1074
1271
|
if (route.handlerSpec) {
|
|
1075
|
-
|
|
1076
|
-
if (spec.summary) operation.summary = spec.summary;
|
|
1077
|
-
if (spec.description) operation.description = spec.description;
|
|
1078
|
-
if (spec.operationId) operation.operationId = spec.operationId;
|
|
1079
|
-
if (spec.tags) operation.tags = spec.tags;
|
|
1080
|
-
if (spec.security) operation.security = spec.security;
|
|
1081
|
-
if (spec.responses) {
|
|
1082
|
-
operation.responses = { ...operation.responses, ...spec.responses };
|
|
1083
|
-
}
|
|
1272
|
+
deepMerge(operation, route.handlerSpec);
|
|
1084
1273
|
}
|
|
1085
1274
|
if (!operation.tags || operation.tags.length === 0) operation.tags = [tag];
|
|
1086
1275
|
if (operation.tags) {
|
|
@@ -1099,11 +1288,13 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1099
1288
|
paths[fullPath][methodLower] = operation;
|
|
1100
1289
|
}
|
|
1101
1290
|
}
|
|
1102
|
-
|
|
1291
|
+
const controllers = router[$childControllers];
|
|
1292
|
+
for (const controller of controllers) {
|
|
1103
1293
|
const controllerName = controller.constructor.name || "UnknownController";
|
|
1104
1294
|
tagGroups.get(group)?.add(controllerName);
|
|
1105
1295
|
}
|
|
1106
|
-
|
|
1296
|
+
const childRouters = router[$childRouters];
|
|
1297
|
+
for (const child of childRouters) {
|
|
1107
1298
|
const mountPath = child[$mountPath];
|
|
1108
1299
|
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1109
1300
|
const cleanMount = mountPath.startsWith("/") ? mountPath : "/" + mountPath;
|
|
@@ -1113,7 +1304,7 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1113
1304
|
};
|
|
1114
1305
|
collect(rootRouter);
|
|
1115
1306
|
const xTagGroups = [];
|
|
1116
|
-
for (const [name, tags] of tagGroups) {
|
|
1307
|
+
for (const [name, tags] of tagGroups.entries()) {
|
|
1117
1308
|
xTagGroups.push({ name, tags: Array.from(tags).sort() });
|
|
1118
1309
|
}
|
|
1119
1310
|
return {
|
|
@@ -1135,12 +1326,23 @@ function serveStatic(config, prefix) {
|
|
|
1135
1326
|
let relative = ctx.path.slice(normalizedPrefix.length);
|
|
1136
1327
|
if (!relative.startsWith("/") && relative.length > 0) relative = "/" + relative;
|
|
1137
1328
|
if (relative.length === 0) relative = "/";
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1329
|
+
if (relative.includes("\0")) {
|
|
1330
|
+
return ctx.json({ error: "Forbidden" }, 403);
|
|
1331
|
+
}
|
|
1332
|
+
try {
|
|
1333
|
+
relative = decodeURIComponent(relative);
|
|
1334
|
+
} catch (e) {
|
|
1335
|
+
return ctx.json({ error: "Bad Request" }, 400);
|
|
1336
|
+
}
|
|
1337
|
+
if (relative.includes("\0")) {
|
|
1338
|
+
return ctx.json({ error: "Forbidden" }, 403);
|
|
1339
|
+
}
|
|
1340
|
+
if (relative.includes("../") || relative.includes("..\\")) {
|
|
1141
1341
|
return ctx.json({ error: "Forbidden" }, 403);
|
|
1142
1342
|
}
|
|
1143
|
-
|
|
1343
|
+
const requestPath = resolve(join(rootPath, relative));
|
|
1344
|
+
const normalizedRoot = resolve(rootPath);
|
|
1345
|
+
if (!requestPath.startsWith(normalizedRoot + sep) && requestPath !== normalizedRoot) {
|
|
1144
1346
|
return ctx.json({ error: "Forbidden" }, 403);
|
|
1145
1347
|
}
|
|
1146
1348
|
if (config.hooks?.onRequest) {
|
|
@@ -1148,7 +1350,8 @@ function serveStatic(config, prefix) {
|
|
|
1148
1350
|
if (res) return res;
|
|
1149
1351
|
}
|
|
1150
1352
|
if (config.exclude) {
|
|
1151
|
-
for (
|
|
1353
|
+
for (let i = 0; i < config.exclude.length; i++) {
|
|
1354
|
+
const pattern = config.exclude[i];
|
|
1152
1355
|
if (pattern instanceof RegExp) {
|
|
1153
1356
|
if (pattern.test(relative)) return ctx.json({ error: "Forbidden" }, 403);
|
|
1154
1357
|
} else if (typeof pattern === "string") {
|
|
@@ -1167,7 +1370,8 @@ function serveStatic(config, prefix) {
|
|
|
1167
1370
|
stats = await stat(requestPath);
|
|
1168
1371
|
} catch (e) {
|
|
1169
1372
|
if (config.extensions) {
|
|
1170
|
-
for (
|
|
1373
|
+
for (let i = 0; i < config.extensions.length; i++) {
|
|
1374
|
+
const ext = config.extensions[i];
|
|
1171
1375
|
const p = requestPath + (ext.startsWith(".") ? ext : "." + ext);
|
|
1172
1376
|
try {
|
|
1173
1377
|
const s = await stat(p);
|
|
@@ -1196,7 +1400,8 @@ function serveStatic(config, prefix) {
|
|
|
1196
1400
|
indexes = [config.index];
|
|
1197
1401
|
}
|
|
1198
1402
|
let foundIndex = false;
|
|
1199
|
-
for (
|
|
1403
|
+
for (let i = 0; i < indexes.length; i++) {
|
|
1404
|
+
const idx = indexes[i];
|
|
1200
1405
|
const idxPath = join(finalPath, idx);
|
|
1201
1406
|
try {
|
|
1202
1407
|
const idxStats = await stat(idxPath);
|
|
@@ -1278,7 +1483,8 @@ class RouterTrie {
|
|
|
1278
1483
|
insert(method, path, handler) {
|
|
1279
1484
|
let node = this.root;
|
|
1280
1485
|
const segments = this.splitPath(path);
|
|
1281
|
-
for (
|
|
1486
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1487
|
+
const segment = segments[i];
|
|
1282
1488
|
if (segment === "**") {
|
|
1283
1489
|
if (!node.recursiveChild) {
|
|
1284
1490
|
node.recursiveChild = this.createNode();
|
|
@@ -1365,40 +1571,68 @@ class RouterTrie {
|
|
|
1365
1571
|
}
|
|
1366
1572
|
}
|
|
1367
1573
|
const asyncContext = new AsyncLocalStorage();
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
return
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1574
|
+
let db;
|
|
1575
|
+
let dbPromise = null;
|
|
1576
|
+
let RecordId;
|
|
1577
|
+
async function ensureDb() {
|
|
1578
|
+
if (db) return db;
|
|
1579
|
+
if (dbPromise) return dbPromise;
|
|
1580
|
+
dbPromise = (async () => {
|
|
1581
|
+
try {
|
|
1582
|
+
const { createNodeEngines } = await import("@surrealdb/node");
|
|
1583
|
+
const surreal = await import("surrealdb");
|
|
1584
|
+
const Surreal = surreal.Surreal;
|
|
1585
|
+
RecordId = surreal.RecordId;
|
|
1586
|
+
const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
|
|
1587
|
+
const _db = new Surreal({
|
|
1588
|
+
engines: createNodeEngines()
|
|
1589
|
+
});
|
|
1590
|
+
await _db.connect(engine, { namespace: "vendor", database: "shokupan" });
|
|
1591
|
+
await _db.query(`
|
|
1592
|
+
DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1593
|
+
DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
|
|
1594
|
+
DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
|
|
1595
|
+
DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
|
|
1596
|
+
DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
|
|
1597
|
+
DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1598
|
+
`);
|
|
1599
|
+
db = _db;
|
|
1600
|
+
return db;
|
|
1601
|
+
} catch (e) {
|
|
1602
|
+
dbPromise = null;
|
|
1603
|
+
if (e.code === "ERR_MODULE_NOT_FOUND" || e.message.includes("Cannot find module")) {
|
|
1604
|
+
throw new Error("SurrealDB dependencies not found. To use the datastore, please install 'surrealdb' and '@surrealdb/node'.");
|
|
1605
|
+
}
|
|
1606
|
+
throw e;
|
|
1607
|
+
}
|
|
1608
|
+
})();
|
|
1609
|
+
return dbPromise;
|
|
1610
|
+
}
|
|
1382
1611
|
const datastore = {
|
|
1383
|
-
get(store, key) {
|
|
1612
|
+
async get(store, key) {
|
|
1613
|
+
await ensureDb();
|
|
1384
1614
|
return db.select(new RecordId(store, key));
|
|
1385
1615
|
},
|
|
1386
|
-
set(store, key, value) {
|
|
1616
|
+
async set(store, key, value) {
|
|
1617
|
+
await ensureDb();
|
|
1387
1618
|
return db.create(new RecordId(store, key)).content(value);
|
|
1388
1619
|
},
|
|
1389
1620
|
async query(query, vars) {
|
|
1621
|
+
await ensureDb();
|
|
1390
1622
|
try {
|
|
1391
|
-
const r = await db.query(query, vars)
|
|
1392
|
-
return r;
|
|
1623
|
+
const r = await db.query(query, vars);
|
|
1624
|
+
return Array.isArray(r) ? r : r?.collect ? await r.collect() : r;
|
|
1393
1625
|
} catch (e) {
|
|
1394
1626
|
console.error("DS ERROR:", e);
|
|
1395
1627
|
throw e;
|
|
1396
1628
|
}
|
|
1397
1629
|
},
|
|
1398
|
-
ready
|
|
1630
|
+
get ready() {
|
|
1631
|
+
return ensureDb().then(() => void 0);
|
|
1632
|
+
}
|
|
1399
1633
|
};
|
|
1400
1634
|
process.on("exit", async () => {
|
|
1401
|
-
await db.close();
|
|
1635
|
+
if (db) await db.close();
|
|
1402
1636
|
});
|
|
1403
1637
|
const tracer = trace.getTracer("shokupan.middleware");
|
|
1404
1638
|
function traceHandler(fn, name) {
|
|
@@ -1471,6 +1705,8 @@ class ShokupanRouter {
|
|
|
1471
1705
|
[$parent] = null;
|
|
1472
1706
|
[$childRouters] = [];
|
|
1473
1707
|
[$childControllers] = [];
|
|
1708
|
+
hookCache = /* @__PURE__ */ new Map();
|
|
1709
|
+
hooksInitialized = false;
|
|
1474
1710
|
middleware = [];
|
|
1475
1711
|
get rootConfig() {
|
|
1476
1712
|
return this[$appRoot]?.applicationConfig;
|
|
@@ -1488,7 +1724,8 @@ class ShokupanRouter {
|
|
|
1488
1724
|
getComponentRegistry() {
|
|
1489
1725
|
const controllerRoutesMap = /* @__PURE__ */ new Map();
|
|
1490
1726
|
const localRoutes = [];
|
|
1491
|
-
for (
|
|
1727
|
+
for (let i = 0; i < this[$routes].length; i++) {
|
|
1728
|
+
const r = this[$routes][i];
|
|
1492
1729
|
const entry = {
|
|
1493
1730
|
type: "route",
|
|
1494
1731
|
path: r.path,
|
|
@@ -1625,7 +1862,8 @@ class ShokupanRouter {
|
|
|
1625
1862
|
const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
|
|
1626
1863
|
const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
|
|
1627
1864
|
let routesAttached = 0;
|
|
1628
|
-
for (
|
|
1865
|
+
for (let i = 0; i < Array.from(methods).length; i++) {
|
|
1866
|
+
const name = Array.from(methods)[i];
|
|
1629
1867
|
if (name === "constructor") continue;
|
|
1630
1868
|
if (["arguments", "caller", "callee"].includes(name)) continue;
|
|
1631
1869
|
const originalHandler = instance[name];
|
|
@@ -1637,7 +1875,8 @@ class ShokupanRouter {
|
|
|
1637
1875
|
method = config.method;
|
|
1638
1876
|
subPath = config.path;
|
|
1639
1877
|
} else {
|
|
1640
|
-
for (
|
|
1878
|
+
for (let j = 0; j < HTTPMethods.length; j++) {
|
|
1879
|
+
const m = HTTPMethods[j];
|
|
1641
1880
|
if (name.toUpperCase().startsWith(m)) {
|
|
1642
1881
|
method = m;
|
|
1643
1882
|
const rest = name.slice(m.length);
|
|
@@ -1652,8 +1891,8 @@ class ShokupanRouter {
|
|
|
1652
1891
|
buffer = "";
|
|
1653
1892
|
}
|
|
1654
1893
|
};
|
|
1655
|
-
for (let
|
|
1656
|
-
const char = rest[
|
|
1894
|
+
for (let i2 = 0; i2 < rest.length; i2++) {
|
|
1895
|
+
const char = rest[i2];
|
|
1657
1896
|
if (char === "$") {
|
|
1658
1897
|
flush();
|
|
1659
1898
|
subPath += "/:";
|
|
@@ -1691,7 +1930,8 @@ class ShokupanRouter {
|
|
|
1691
1930
|
if (routeArgs?.length > 0) {
|
|
1692
1931
|
args = [];
|
|
1693
1932
|
const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
|
|
1694
|
-
for (
|
|
1933
|
+
for (let k = 0; k < sortedArgs.length; k++) {
|
|
1934
|
+
const arg = sortedArgs[k];
|
|
1695
1935
|
switch (arg.type) {
|
|
1696
1936
|
case RouteParamType.BODY:
|
|
1697
1937
|
try {
|
|
@@ -1721,7 +1961,9 @@ class ShokupanRouter {
|
|
|
1721
1961
|
args[arg.index] = vals.length > 1 ? vals : vals[0];
|
|
1722
1962
|
} else {
|
|
1723
1963
|
const query = {};
|
|
1724
|
-
|
|
1964
|
+
const keys = Object.keys(url.searchParams);
|
|
1965
|
+
for (let k2 = 0; k2 < keys.length; k2++) {
|
|
1966
|
+
const key = keys[k2];
|
|
1725
1967
|
const vals = url.searchParams.getAll(key);
|
|
1726
1968
|
query[key] = vals.length > 1 ? vals : vals[0];
|
|
1727
1969
|
}
|
|
@@ -1778,9 +2020,11 @@ class ShokupanRouter {
|
|
|
1778
2020
|
path: r.path,
|
|
1779
2021
|
handler: r.handler
|
|
1780
2022
|
}));
|
|
1781
|
-
for (
|
|
2023
|
+
for (let i = 0; i < this[$childRouters].length; i++) {
|
|
2024
|
+
const child = this[$childRouters][i];
|
|
1782
2025
|
const childRoutes = child.getRoutes();
|
|
1783
|
-
for (
|
|
2026
|
+
for (let j = 0; j < childRoutes.length; j++) {
|
|
2027
|
+
const route = childRoutes[j];
|
|
1784
2028
|
const cleanPrefix = child[$mountPath].endsWith("/") ? child[$mountPath].slice(0, -1) : child[$mountPath];
|
|
1785
2029
|
const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
|
|
1786
2030
|
const fullPath = cleanPrefix + cleanPath || "/";
|
|
@@ -1794,12 +2038,12 @@ class ShokupanRouter {
|
|
|
1794
2038
|
return routes;
|
|
1795
2039
|
}
|
|
1796
2040
|
/**
|
|
1797
|
-
* Makes
|
|
1798
|
-
* This is useful for
|
|
2041
|
+
* Makes an internal request through this router's full routing pipeline.
|
|
2042
|
+
* This is useful for calling other routes internally and supports streaming responses.
|
|
1799
2043
|
* @param options The request options.
|
|
1800
|
-
* @returns The
|
|
2044
|
+
* @returns The raw Response object.
|
|
1801
2045
|
*/
|
|
1802
|
-
async
|
|
2046
|
+
async internalRequest(arg) {
|
|
1803
2047
|
const options = typeof arg === "string" ? { path: arg } : arg;
|
|
1804
2048
|
const store = asyncContext.getStore();
|
|
1805
2049
|
store?.get("req");
|
|
@@ -1818,9 +2062,10 @@ class ShokupanRouter {
|
|
|
1818
2062
|
return this.root[$dispatch](req);
|
|
1819
2063
|
}
|
|
1820
2064
|
/**
|
|
1821
|
-
* Processes a request
|
|
2065
|
+
* Processes a request for testing purposes.
|
|
2066
|
+
* Returns a simplified { status, headers, data } object instead of a Response.
|
|
1822
2067
|
*/
|
|
1823
|
-
async
|
|
2068
|
+
async testRequest(options) {
|
|
1824
2069
|
let url = options.url || options.path || "/";
|
|
1825
2070
|
if (!url.startsWith("http")) {
|
|
1826
2071
|
const base = `http://${this.rootConfig?.hostname || "localhost"}:${this.rootConfig?.port || 3e3}`;
|
|
@@ -1829,7 +2074,9 @@ class ShokupanRouter {
|
|
|
1829
2074
|
}
|
|
1830
2075
|
if (options.query) {
|
|
1831
2076
|
const u = new URL(url);
|
|
1832
|
-
|
|
2077
|
+
const entries = Object.entries(options.query);
|
|
2078
|
+
for (let i = 0; i < entries.length; i++) {
|
|
2079
|
+
const [k, v] = entries[i];
|
|
1833
2080
|
u.searchParams.set(k, v);
|
|
1834
2081
|
}
|
|
1835
2082
|
url = u.toString();
|
|
@@ -1874,28 +2121,17 @@ class ShokupanRouter {
|
|
|
1874
2121
|
data: result
|
|
1875
2122
|
};
|
|
1876
2123
|
}
|
|
1877
|
-
|
|
1878
|
-
if (!this.
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
}
|
|
1885
|
-
wrapWithHooks(handler, hooks) {
|
|
1886
|
-
const hookList = Array.isArray(hooks) ? hooks : [hooks];
|
|
1887
|
-
const hasStart = hookList.some((h) => !!h.onRequestStart);
|
|
1888
|
-
const hasEnd = hookList.some((h) => !!h.onRequestEnd);
|
|
1889
|
-
const hasError = hookList.some((h) => !!h.onError);
|
|
2124
|
+
wrapWithHooks(handler) {
|
|
2125
|
+
if (!this.hooksInitialized) {
|
|
2126
|
+
this.ensureHooksInitialized();
|
|
2127
|
+
}
|
|
2128
|
+
const hasStart = this.hookCache.get("onRequestStart")?.length > 0;
|
|
2129
|
+
const hasEnd = this.hookCache.get("onRequestEnd")?.length > 0;
|
|
2130
|
+
const hasError = this.hookCache.get("onError")?.length > 0;
|
|
1890
2131
|
if (!hasStart && !hasEnd && !hasError) return handler;
|
|
1891
2132
|
const originalHandler = handler;
|
|
1892
2133
|
const wrapped = async (ctx) => {
|
|
1893
|
-
|
|
1894
|
-
for (let i = 0; i < hookList.length; i++) {
|
|
1895
|
-
const h = hookList[i];
|
|
1896
|
-
if (typeof h.onRequestStart === "function") await h.onRequestStart(ctx);
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
2134
|
+
await this.runHooks("onRequestStart", ctx);
|
|
1899
2135
|
const debug = ctx._debug;
|
|
1900
2136
|
let debugId;
|
|
1901
2137
|
let previousNode;
|
|
@@ -1909,17 +2145,11 @@ class ShokupanRouter {
|
|
|
1909
2145
|
try {
|
|
1910
2146
|
const res = await originalHandler(ctx);
|
|
1911
2147
|
debug?.trackStep(debugId, "handler", performance.now() - start, "success");
|
|
1912
|
-
|
|
1913
|
-
const h = hookList[i];
|
|
1914
|
-
if (typeof h.onRequestEnd === "function") await h.onRequestEnd(ctx);
|
|
1915
|
-
}
|
|
2148
|
+
await this.runHooks("onRequestEnd", ctx);
|
|
1916
2149
|
return res;
|
|
1917
2150
|
} catch (err) {
|
|
1918
2151
|
debug?.trackStep(debugId, "handler", performance.now() - start, "error", err);
|
|
1919
|
-
|
|
1920
|
-
const h = hookList[i];
|
|
1921
|
-
if (typeof h.onError === "function") await h.onError(err, ctx);
|
|
1922
|
-
}
|
|
2152
|
+
await this.runHooks("onError", ctx, err);
|
|
1923
2153
|
throw err;
|
|
1924
2154
|
} finally {
|
|
1925
2155
|
if (debug && previousNode) debug.setNode(previousNode);
|
|
@@ -1941,18 +2171,19 @@ class ShokupanRouter {
|
|
|
1941
2171
|
result = this.trie.search("GET", path);
|
|
1942
2172
|
if (result) return result;
|
|
1943
2173
|
}
|
|
1944
|
-
for (
|
|
2174
|
+
for (let i = 0; i < this[$childRouters].length; i++) {
|
|
2175
|
+
const child = this[$childRouters][i];
|
|
1945
2176
|
const prefix = child[$mountPath];
|
|
1946
2177
|
if (path === prefix || path.startsWith(prefix + "/")) {
|
|
1947
2178
|
const subPath = path.slice(prefix.length) || "/";
|
|
1948
2179
|
const match = child.find(method, subPath);
|
|
1949
|
-
if (match) return
|
|
2180
|
+
if (match) return match;
|
|
1950
2181
|
}
|
|
1951
2182
|
if (prefix.endsWith("/")) {
|
|
1952
2183
|
if (path.startsWith(prefix)) {
|
|
1953
2184
|
const subPath = path.slice(prefix.length) || "/";
|
|
1954
2185
|
const match = child.find(method, subPath);
|
|
1955
|
-
if (match) return
|
|
2186
|
+
if (match) return match;
|
|
1956
2187
|
}
|
|
1957
2188
|
}
|
|
1958
2189
|
}
|
|
@@ -1960,10 +2191,13 @@ class ShokupanRouter {
|
|
|
1960
2191
|
}
|
|
1961
2192
|
parsePath(path) {
|
|
1962
2193
|
const keys = [];
|
|
2194
|
+
if (path.length > 2048) {
|
|
2195
|
+
throw new Error("Path too long");
|
|
2196
|
+
}
|
|
1963
2197
|
const pattern = path.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => {
|
|
1964
2198
|
keys.push(key);
|
|
1965
|
-
return "([^/]
|
|
1966
|
-
}).replace(/\*\*/g, "
|
|
2199
|
+
return "([^/]{1,255})";
|
|
2200
|
+
}).replace(/\*\*/g, ".{0,1000}").replace(/\*/g, "[^/]{1,255}");
|
|
1967
2201
|
return {
|
|
1968
2202
|
regex: new RegExp(`^${pattern}$`),
|
|
1969
2203
|
keys
|
|
@@ -1974,17 +2208,23 @@ class ShokupanRouter {
|
|
|
1974
2208
|
/**
|
|
1975
2209
|
* Adds a route to the router.
|
|
1976
2210
|
*
|
|
1977
|
-
* @param
|
|
1978
|
-
* @param
|
|
1979
|
-
* @param
|
|
1980
|
-
* @param
|
|
1981
|
-
* @param
|
|
2211
|
+
* @param arg - Route configuration object
|
|
2212
|
+
* @param arg.method - HTTP method
|
|
2213
|
+
* @param arg.path - URL path
|
|
2214
|
+
* @param arg.spec - OpenAPI specification for the route
|
|
2215
|
+
* @param arg.handler - Route handler function
|
|
2216
|
+
* @param arg.regex - Custom regex for path matching
|
|
2217
|
+
* @param arg.group - Group for the route
|
|
2218
|
+
* @param arg.requestTimeout - Timeout for this route in milliseconds
|
|
2219
|
+
* @param arg.renderer - JSX renderer for the route
|
|
2220
|
+
* @param arg.controller - Controller for the route
|
|
1982
2221
|
*/
|
|
1983
2222
|
add({ method, path, spec, handler, regex: customRegex, group, requestTimeout, renderer, controller }) {
|
|
1984
2223
|
const { regex, keys } = customRegex ? { regex: customRegex, keys: [] } : this.parsePath(path);
|
|
1985
2224
|
if (this.currentGuards.length > 0) {
|
|
1986
2225
|
spec = spec || {};
|
|
1987
|
-
for (
|
|
2226
|
+
for (let i = 0; i < this.currentGuards.length; i++) {
|
|
2227
|
+
const guard = this.currentGuards[i];
|
|
1988
2228
|
if (guard.spec) {
|
|
1989
2229
|
if (guard.spec.responses) {
|
|
1990
2230
|
spec.responses = spec.responses || {};
|
|
@@ -2013,7 +2253,8 @@ class ShokupanRouter {
|
|
|
2013
2253
|
if (routeGuards.length > 0) {
|
|
2014
2254
|
const innerHandler = wrappedHandler;
|
|
2015
2255
|
wrappedHandler = async (ctx) => {
|
|
2016
|
-
for (
|
|
2256
|
+
for (let i = 0; i < routeGuards.length; i++) {
|
|
2257
|
+
const guard = routeGuards[i];
|
|
2017
2258
|
let guardPassed = false;
|
|
2018
2259
|
let nextCalled = false;
|
|
2019
2260
|
const next = () => {
|
|
@@ -2107,7 +2348,7 @@ class ShokupanRouter {
|
|
|
2107
2348
|
wrappedHandler.originalHandler = trackingHandler.originalHandler || trackingHandler;
|
|
2108
2349
|
let bakedHandler = wrappedHandler;
|
|
2109
2350
|
if (this.config?.hooks) {
|
|
2110
|
-
bakedHandler = this.wrapWithHooks(wrappedHandler
|
|
2351
|
+
bakedHandler = this.wrapWithHooks(wrappedHandler);
|
|
2111
2352
|
}
|
|
2112
2353
|
this[$routes].push({
|
|
2113
2354
|
method,
|
|
@@ -2264,6 +2505,67 @@ class ShokupanRouter {
|
|
|
2264
2505
|
generateApiSpec(options = {}) {
|
|
2265
2506
|
return generateOpenApi(this, options);
|
|
2266
2507
|
}
|
|
2508
|
+
ensureHooksInitialized() {
|
|
2509
|
+
const hooks = this.config?.hooks;
|
|
2510
|
+
if (hooks) {
|
|
2511
|
+
const hookList = Array.isArray(hooks) ? hooks : [hooks];
|
|
2512
|
+
const hookTypes = [
|
|
2513
|
+
"onRequestStart",
|
|
2514
|
+
"onRequestEnd",
|
|
2515
|
+
"onResponseStart",
|
|
2516
|
+
"onResponseEnd",
|
|
2517
|
+
"onError",
|
|
2518
|
+
"beforeValidate",
|
|
2519
|
+
"afterValidate",
|
|
2520
|
+
"onRequestTimeout",
|
|
2521
|
+
"onReadTimeout",
|
|
2522
|
+
"onWriteTimeout"
|
|
2523
|
+
];
|
|
2524
|
+
for (let i = 0; i < hookTypes.length; i++) {
|
|
2525
|
+
const type = hookTypes[i];
|
|
2526
|
+
const fns = [];
|
|
2527
|
+
for (let j = 0; j < hookList.length; j++) {
|
|
2528
|
+
const h = hookList[j];
|
|
2529
|
+
if (h[type]) fns.push(h[type]);
|
|
2530
|
+
}
|
|
2531
|
+
if (fns.length > 0) {
|
|
2532
|
+
this.hookCache.set(type, fns);
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
this.hooksInitialized = true;
|
|
2537
|
+
}
|
|
2538
|
+
async runHooks(name, ...args) {
|
|
2539
|
+
if (!this.hooksInitialized) {
|
|
2540
|
+
this.ensureHooksInitialized();
|
|
2541
|
+
}
|
|
2542
|
+
const fns = this.hookCache.get(name);
|
|
2543
|
+
if (!fns) return;
|
|
2544
|
+
const ctx = args?.[0] instanceof ShokupanContext ? args[0] : void 0;
|
|
2545
|
+
const debug = ctx?._debug;
|
|
2546
|
+
if (debug) {
|
|
2547
|
+
await Promise.all(fns.map(async (fn, index) => {
|
|
2548
|
+
const hookId = `hook_${name}_${fn.name || index}`;
|
|
2549
|
+
const previousNode = debug.getCurrentNode();
|
|
2550
|
+
debug.trackEdge(previousNode, hookId);
|
|
2551
|
+
debug.setNode(hookId);
|
|
2552
|
+
const start = performance.now();
|
|
2553
|
+
try {
|
|
2554
|
+
await fn(...args);
|
|
2555
|
+
const duration = performance.now() - start;
|
|
2556
|
+
debug.trackStep(hookId, "hook", duration, "success");
|
|
2557
|
+
} catch (error) {
|
|
2558
|
+
const duration = performance.now() - start;
|
|
2559
|
+
debug.trackStep(hookId, "hook", duration, "error", error);
|
|
2560
|
+
throw error;
|
|
2561
|
+
} finally {
|
|
2562
|
+
if (previousNode) debug.setNode(previousNode);
|
|
2563
|
+
}
|
|
2564
|
+
}));
|
|
2565
|
+
} else {
|
|
2566
|
+
await Promise.all(fns.map((fn) => fn(...args)));
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2267
2569
|
}
|
|
2268
2570
|
class SystemCpuMonitor {
|
|
2269
2571
|
constructor(intervalMs = 1e3) {
|
|
@@ -2321,15 +2623,13 @@ class Shokupan extends ShokupanRouter {
|
|
|
2321
2623
|
openApiSpec;
|
|
2322
2624
|
composedMiddleware;
|
|
2323
2625
|
cpuMonitor;
|
|
2324
|
-
hookCache = /* @__PURE__ */ new Map();
|
|
2325
|
-
hooksInitialized = false;
|
|
2326
2626
|
get logger() {
|
|
2327
2627
|
return this.applicationConfig.logger;
|
|
2328
2628
|
}
|
|
2329
2629
|
constructor(applicationConfig = {}) {
|
|
2330
2630
|
const config = Object.assign({}, defaults, applicationConfig);
|
|
2331
2631
|
const { hooks, ...routerConfig } = config;
|
|
2332
|
-
super(routerConfig);
|
|
2632
|
+
super({ ...routerConfig, hooks });
|
|
2333
2633
|
this[$isApplication] = true;
|
|
2334
2634
|
this[$appRoot] = this;
|
|
2335
2635
|
this.applicationConfig = config;
|
|
@@ -2344,7 +2644,6 @@ class Shokupan extends ShokupanRouter {
|
|
|
2344
2644
|
* Adds middleware to the application.
|
|
2345
2645
|
*/
|
|
2346
2646
|
use(middleware) {
|
|
2347
|
-
let trackedMiddleware = middleware;
|
|
2348
2647
|
const { file, line } = getCallerInfo();
|
|
2349
2648
|
if (!middleware.metadata) {
|
|
2350
2649
|
middleware.metadata = {
|
|
@@ -2355,32 +2654,36 @@ class Shokupan extends ShokupanRouter {
|
|
|
2355
2654
|
pluginName: middleware.pluginName
|
|
2356
2655
|
};
|
|
2357
2656
|
}
|
|
2358
|
-
|
|
2359
|
-
const
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2657
|
+
if (this.applicationConfig.enableMiddlewareTracking) {
|
|
2658
|
+
const trackedMiddleware = async (ctx, next) => {
|
|
2659
|
+
const c = ctx;
|
|
2660
|
+
if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
|
|
2661
|
+
const metadata = middleware.metadata || {};
|
|
2662
|
+
const start = performance.now();
|
|
2663
|
+
const item = {
|
|
2664
|
+
name: metadata.pluginName ? `${metadata.pluginName} (${metadata.name})` : metadata.name || middleware.name || "middleware",
|
|
2665
|
+
file: metadata.file || file,
|
|
2666
|
+
line: metadata.line || line,
|
|
2667
|
+
isBuiltin: metadata.isBuiltin,
|
|
2668
|
+
startTime: start,
|
|
2669
|
+
duration: -1
|
|
2670
|
+
};
|
|
2671
|
+
c.handlerStack.push(item);
|
|
2672
|
+
try {
|
|
2673
|
+
return await middleware(ctx, next);
|
|
2674
|
+
} finally {
|
|
2675
|
+
item.duration = performance.now() - start;
|
|
2676
|
+
}
|
|
2376
2677
|
}
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2678
|
+
return middleware(ctx, next);
|
|
2679
|
+
};
|
|
2680
|
+
trackedMiddleware.metadata = middleware.metadata;
|
|
2681
|
+
Object.defineProperty(trackedMiddleware, "name", { value: middleware.name || "middleware" });
|
|
2682
|
+
trackedMiddleware.order = this.middleware.length;
|
|
2683
|
+
this.middleware.push(trackedMiddleware);
|
|
2684
|
+
} else {
|
|
2685
|
+
this.middleware.push(middleware);
|
|
2686
|
+
}
|
|
2384
2687
|
return this;
|
|
2385
2688
|
}
|
|
2386
2689
|
startupHooks = [];
|
|
@@ -2411,17 +2714,13 @@ class Shokupan extends ShokupanRouter {
|
|
|
2411
2714
|
if (finalPort < 0 || finalPort > 65535) {
|
|
2412
2715
|
throw new Error("Invalid port number");
|
|
2413
2716
|
}
|
|
2414
|
-
|
|
2415
|
-
await hook();
|
|
2416
|
-
}
|
|
2717
|
+
await Promise.all(this.startupHooks.map((hook) => hook()));
|
|
2417
2718
|
if (this.applicationConfig.enableOpenApiGen) {
|
|
2418
2719
|
this.openApiSpec = await generateOpenApi(this);
|
|
2419
|
-
|
|
2420
|
-
await hook(this.openApiSpec);
|
|
2421
|
-
}
|
|
2720
|
+
await Promise.all(this.specAvailableHooks.map((hook) => hook(this.openApiSpec)));
|
|
2422
2721
|
}
|
|
2423
2722
|
if (port === 0 && process.platform === "linux") ;
|
|
2424
|
-
if (this.applicationConfig.autoBackpressureFeedback) {
|
|
2723
|
+
if (this.applicationConfig.autoBackpressureFeedback === true) {
|
|
2425
2724
|
this.cpuMonitor = new SystemCpuMonitor();
|
|
2426
2725
|
this.cpuMonitor.start();
|
|
2427
2726
|
}
|
|
@@ -2449,7 +2748,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
2449
2748
|
};
|
|
2450
2749
|
let factory = this.applicationConfig.serverFactory;
|
|
2451
2750
|
if (!factory && typeof Bun === "undefined") {
|
|
2452
|
-
const { createHttpServer } = await import("./server-adapter-
|
|
2751
|
+
const { createHttpServer } = await import("./server-adapter-0xH174zz.js");
|
|
2453
2752
|
factory = createHttpServer();
|
|
2454
2753
|
}
|
|
2455
2754
|
const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
|
|
@@ -2462,7 +2761,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
2462
2761
|
/**
|
|
2463
2762
|
* Processes a request by wrapping the standard fetch method.
|
|
2464
2763
|
*/
|
|
2465
|
-
async
|
|
2764
|
+
async testRequest(options) {
|
|
2466
2765
|
let url = options.url || options.path || "/";
|
|
2467
2766
|
if (!url.startsWith("http")) {
|
|
2468
2767
|
const base = `http://${this.applicationConfig.hostname || "localhost"}:${this.applicationConfig.port || 3e3}`;
|
|
@@ -2471,7 +2770,9 @@ class Shokupan extends ShokupanRouter {
|
|
|
2471
2770
|
}
|
|
2472
2771
|
if (options.query) {
|
|
2473
2772
|
const u = new URL(url);
|
|
2474
|
-
|
|
2773
|
+
const entries = Object.entries(options.query);
|
|
2774
|
+
for (let i = 0; i < entries.length; i++) {
|
|
2775
|
+
const [k, v] = entries[i];
|
|
2475
2776
|
u.searchParams.set(k, v);
|
|
2476
2777
|
}
|
|
2477
2778
|
url = u.toString();
|
|
@@ -2540,18 +2841,18 @@ class Shokupan extends ShokupanRouter {
|
|
|
2540
2841
|
if (this.cpuMonitor && this.cpuMonitor.getUsage() > (this.applicationConfig.autoBackpressureLevel ?? 60)) {
|
|
2541
2842
|
const msg = "Too Many Requests (CPU Backpressure)";
|
|
2542
2843
|
const res = ctx.text(msg, 429);
|
|
2543
|
-
await this.
|
|
2844
|
+
await this.runHooks("onResponseEnd", ctx, res);
|
|
2544
2845
|
return res;
|
|
2545
2846
|
}
|
|
2546
2847
|
try {
|
|
2547
|
-
|
|
2548
|
-
await this.executeHook("onRequestStart", ctx);
|
|
2549
|
-
}
|
|
2848
|
+
await this.runHooks("onRequestStart", ctx);
|
|
2550
2849
|
const fn = this.composedMiddleware ??= compose(this.middleware);
|
|
2551
2850
|
const result = await fn(ctx, async () => {
|
|
2851
|
+
const bodyParsing = ["POST", "PUT", "PATCH", "DELETE"].includes(req.method) ? ctx.parseBody() : Promise.resolve();
|
|
2552
2852
|
const match = this.find(req.method, ctx.path);
|
|
2553
2853
|
if (match) {
|
|
2554
2854
|
ctx.params = match.params;
|
|
2855
|
+
await bodyParsing;
|
|
2555
2856
|
return match.handler(ctx);
|
|
2556
2857
|
}
|
|
2557
2858
|
return null;
|
|
@@ -2574,12 +2875,8 @@ class Shokupan extends ShokupanRouter {
|
|
|
2574
2875
|
} else {
|
|
2575
2876
|
response = ctx.text(String(result));
|
|
2576
2877
|
}
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
}
|
|
2580
|
-
if (this.hasHook("onResponseStart")) {
|
|
2581
|
-
await this.executeHook("onResponseStart", ctx, response);
|
|
2582
|
-
}
|
|
2878
|
+
await this.runHooks("onRequestEnd", ctx);
|
|
2879
|
+
await this.runHooks("onResponseStart", ctx, response);
|
|
2583
2880
|
return response;
|
|
2584
2881
|
} catch (err) {
|
|
2585
2882
|
console.error(err);
|
|
@@ -2588,9 +2885,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
2588
2885
|
const status = err.status || err.statusCode || 500;
|
|
2589
2886
|
const body = { error: err.message || "Internal Server Error" };
|
|
2590
2887
|
if (err.errors) body.errors = err.errors;
|
|
2591
|
-
|
|
2592
|
-
await this.executeHook("onError", err, ctx);
|
|
2593
|
-
}
|
|
2888
|
+
await this.runHooks("onError", ctx, err);
|
|
2594
2889
|
return ctx.json(body, status);
|
|
2595
2890
|
}
|
|
2596
2891
|
};
|
|
@@ -2601,9 +2896,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
2601
2896
|
const timeoutPromise = new Promise((_, reject) => {
|
|
2602
2897
|
timeoutId = setTimeout(async () => {
|
|
2603
2898
|
controller.abort();
|
|
2604
|
-
|
|
2605
|
-
await this.executeHook("onRequestTimeout", ctx);
|
|
2606
|
-
}
|
|
2899
|
+
await this.runHooks("onRequestTimeout", ctx);
|
|
2607
2900
|
reject(new Error("Request Timeout"));
|
|
2608
2901
|
}, timeoutMs);
|
|
2609
2902
|
});
|
|
@@ -2616,56 +2909,10 @@ class Shokupan extends ShokupanRouter {
|
|
|
2616
2909
|
console.error("Unexpected error in request execution:", err);
|
|
2617
2910
|
return ctx.text("Internal Server Error", 500);
|
|
2618
2911
|
}).then(async (res) => {
|
|
2619
|
-
|
|
2620
|
-
await this.executeHook("onResponseEnd", ctx, res);
|
|
2621
|
-
}
|
|
2912
|
+
await this.runHooks("onResponseEnd", ctx, res);
|
|
2622
2913
|
return res;
|
|
2623
2914
|
});
|
|
2624
2915
|
}
|
|
2625
|
-
ensureHooksInitialized() {
|
|
2626
|
-
const hooks = this.applicationConfig.hooks;
|
|
2627
|
-
if (hooks) {
|
|
2628
|
-
const hookList = Array.isArray(hooks) ? hooks : [hooks];
|
|
2629
|
-
const hookTypes = [
|
|
2630
|
-
"onRequestStart",
|
|
2631
|
-
"onRequestEnd",
|
|
2632
|
-
"onResponseStart",
|
|
2633
|
-
"onResponseEnd",
|
|
2634
|
-
"onError",
|
|
2635
|
-
"beforeValidate",
|
|
2636
|
-
"afterValidate",
|
|
2637
|
-
"onRequestTimeout",
|
|
2638
|
-
"onReadTimeout",
|
|
2639
|
-
"onWriteTimeout"
|
|
2640
|
-
];
|
|
2641
|
-
for (const type of hookTypes) {
|
|
2642
|
-
const fns = [];
|
|
2643
|
-
for (const h of hookList) {
|
|
2644
|
-
if (h[type]) fns.push(h[type]);
|
|
2645
|
-
}
|
|
2646
|
-
if (fns.length > 0) {
|
|
2647
|
-
this.hookCache.set(type, fns);
|
|
2648
|
-
}
|
|
2649
|
-
}
|
|
2650
|
-
}
|
|
2651
|
-
this.hooksInitialized = true;
|
|
2652
|
-
}
|
|
2653
|
-
async executeHook(name, ...args) {
|
|
2654
|
-
if (!this.hooksInitialized) {
|
|
2655
|
-
this.ensureHooksInitialized();
|
|
2656
|
-
}
|
|
2657
|
-
const fns = this.hookCache.get(name);
|
|
2658
|
-
if (!fns) return;
|
|
2659
|
-
for (const fn of fns) {
|
|
2660
|
-
await fn(...args);
|
|
2661
|
-
}
|
|
2662
|
-
}
|
|
2663
|
-
hasHook(name) {
|
|
2664
|
-
if (!this.hooksInitialized) {
|
|
2665
|
-
this.ensureHooksInitialized();
|
|
2666
|
-
}
|
|
2667
|
-
return this.hookCache.has(name);
|
|
2668
|
-
}
|
|
2669
2916
|
}
|
|
2670
2917
|
class AuthPlugin extends ShokupanRouter {
|
|
2671
2918
|
constructor(authConfig) {
|
|
@@ -2713,7 +2960,9 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
2713
2960
|
return jwt;
|
|
2714
2961
|
}
|
|
2715
2962
|
init() {
|
|
2716
|
-
|
|
2963
|
+
const providerEntries = Object.entries(this.authConfig.providers);
|
|
2964
|
+
for (let i = 0; i < providerEntries.length; i++) {
|
|
2965
|
+
const [providerName, providerConfig] = providerEntries[i];
|
|
2717
2966
|
if (!providerConfig) continue;
|
|
2718
2967
|
const provider = this.getProviderInstance(providerName, providerConfig);
|
|
2719
2968
|
if (!provider) {
|
|
@@ -2736,9 +2985,10 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
2736
2985
|
} else {
|
|
2737
2986
|
return ctx.text("Provider config error", 500);
|
|
2738
2987
|
}
|
|
2739
|
-
|
|
2988
|
+
const isSecure = ctx.secure;
|
|
2989
|
+
ctx.res.headers.set("Set-Cookie", `oauth_state=${state}; Path=/; HttpOnly; SameSite=Lax${isSecure ? "; Secure" : ""}; Max-Age=600`);
|
|
2740
2990
|
if (codeVerifier) {
|
|
2741
|
-
ctx.res.headers.append("Set-Cookie", `oauth_verifier=${codeVerifier}; Path=/; HttpOnly; Max-Age=600`);
|
|
2991
|
+
ctx.res.headers.append("Set-Cookie", `oauth_verifier=${codeVerifier}; Path=/; HttpOnly; SameSite=Lax${isSecure ? "; Secure" : ""}; Max-Age=600`);
|
|
2742
2992
|
}
|
|
2743
2993
|
return ctx.redirect(url.toString());
|
|
2744
2994
|
});
|
|
@@ -2779,7 +3029,7 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
2779
3029
|
return ctx.json({ token: jwt, user });
|
|
2780
3030
|
} catch (e) {
|
|
2781
3031
|
console.error("Auth Error", e);
|
|
2782
|
-
return ctx.text("Authentication failed
|
|
3032
|
+
return ctx.text("Authentication failed. Please try again.", 500);
|
|
2783
3033
|
}
|
|
2784
3034
|
});
|
|
2785
3035
|
}
|
|
@@ -2990,14 +3240,21 @@ function Cors(options = {}) {
|
|
|
2990
3240
|
const origin = ctx.headers.get("origin");
|
|
2991
3241
|
const set = (k, v) => headers.set(k, v);
|
|
2992
3242
|
const append = (k, v) => headers.append(k, v);
|
|
3243
|
+
if (origin === "null" && opts.origin !== "null") {
|
|
3244
|
+
return next();
|
|
3245
|
+
}
|
|
2993
3246
|
if (opts.origin === "*") {
|
|
2994
3247
|
set("Access-Control-Allow-Origin", "*");
|
|
2995
3248
|
} else if (typeof opts.origin === "string") {
|
|
2996
3249
|
set("Access-Control-Allow-Origin", opts.origin);
|
|
2997
3250
|
} else if (Array.isArray(opts.origin)) {
|
|
2998
|
-
if (origin
|
|
2999
|
-
|
|
3000
|
-
|
|
3251
|
+
if (origin) {
|
|
3252
|
+
const normalizedOrigin = origin.toLowerCase();
|
|
3253
|
+
const normalizedAllowed = opts.origin.map((o) => o.toLowerCase());
|
|
3254
|
+
if (normalizedAllowed.includes(normalizedOrigin)) {
|
|
3255
|
+
set("Access-Control-Allow-Origin", origin);
|
|
3256
|
+
append("Vary", "Origin");
|
|
3257
|
+
}
|
|
3001
3258
|
}
|
|
3002
3259
|
} else if (typeof opts.origin === "function") {
|
|
3003
3260
|
const allowed = opts.origin(ctx);
|
|
@@ -3041,7 +3298,9 @@ function Cors(options = {}) {
|
|
|
3041
3298
|
}
|
|
3042
3299
|
const response = await next();
|
|
3043
3300
|
if (response instanceof Response) {
|
|
3044
|
-
|
|
3301
|
+
const headerEntries = Array.from(headers.entries());
|
|
3302
|
+
for (let i = 0; i < headerEntries.length; i++) {
|
|
3303
|
+
const [key, value] = headerEntries[i];
|
|
3045
3304
|
response.headers.set(key, value);
|
|
3046
3305
|
}
|
|
3047
3306
|
}
|
|
@@ -3111,6 +3370,8 @@ function useExpress(expressMiddleware) {
|
|
|
3111
3370
|
});
|
|
3112
3371
|
};
|
|
3113
3372
|
}
|
|
3373
|
+
let plainToInstance;
|
|
3374
|
+
let validateOrReject;
|
|
3114
3375
|
class ValidationError extends Error {
|
|
3115
3376
|
constructor(errors) {
|
|
3116
3377
|
super("Validation Error");
|
|
@@ -3175,6 +3436,18 @@ function isClass(schema) {
|
|
|
3175
3436
|
}
|
|
3176
3437
|
}
|
|
3177
3438
|
async function validateClassValidator(schema, data) {
|
|
3439
|
+
if (!plainToInstance || !validateOrReject) {
|
|
3440
|
+
try {
|
|
3441
|
+
const ct = await import("class-transformer");
|
|
3442
|
+
const cv = await import("class-validator");
|
|
3443
|
+
plainToInstance = ct.plainToInstance;
|
|
3444
|
+
validateOrReject = cv.validateOrReject;
|
|
3445
|
+
} catch (e) {
|
|
3446
|
+
throw new Error(
|
|
3447
|
+
"class-transformer and class-validator are required for class-based validation. Install them with: bun add class-transformer class-validator reflect-metadata"
|
|
3448
|
+
);
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3178
3451
|
const object = plainToInstance(schema, data);
|
|
3179
3452
|
try {
|
|
3180
3453
|
await validateOrReject(object);
|
|
@@ -3189,30 +3462,8 @@ async function validateClassValidator(schema, data) {
|
|
|
3189
3462
|
}
|
|
3190
3463
|
}
|
|
3191
3464
|
const safelyGetBody = async (ctx) => {
|
|
3192
|
-
const req = ctx.req;
|
|
3193
|
-
if (req._bodyParsed) {
|
|
3194
|
-
return req._bodyValue;
|
|
3195
|
-
}
|
|
3196
3465
|
try {
|
|
3197
|
-
|
|
3198
|
-
if (typeof req.json === "function") {
|
|
3199
|
-
data = await req.json();
|
|
3200
|
-
} else {
|
|
3201
|
-
data = req.body;
|
|
3202
|
-
if (typeof data === "string") {
|
|
3203
|
-
try {
|
|
3204
|
-
data = JSON.parse(data);
|
|
3205
|
-
} catch {
|
|
3206
|
-
}
|
|
3207
|
-
}
|
|
3208
|
-
}
|
|
3209
|
-
req._bodyParsed = true;
|
|
3210
|
-
req._bodyValue = data;
|
|
3211
|
-
Object.defineProperty(req, "json", {
|
|
3212
|
-
value: async () => req._bodyValue,
|
|
3213
|
-
configurable: true
|
|
3214
|
-
});
|
|
3215
|
-
return data;
|
|
3466
|
+
return await ctx.body();
|
|
3216
3467
|
} catch (e) {
|
|
3217
3468
|
return {};
|
|
3218
3469
|
}
|
|
@@ -3259,9 +3510,7 @@ function validate(config) {
|
|
|
3259
3510
|
body = await safelyGetBody(ctx);
|
|
3260
3511
|
dataToValidate.body = body;
|
|
3261
3512
|
}
|
|
3262
|
-
|
|
3263
|
-
await ctx.app.executeHook("beforeValidate", ctx, dataToValidate);
|
|
3264
|
-
}
|
|
3513
|
+
await ctx.app.runHooks("beforeValidate", ctx, dataToValidate);
|
|
3265
3514
|
if (validators.params) {
|
|
3266
3515
|
ctx.params = await validators.params(ctx.params);
|
|
3267
3516
|
}
|
|
@@ -3277,21 +3526,20 @@ function validate(config) {
|
|
|
3277
3526
|
if (validators.body) {
|
|
3278
3527
|
const b = body ?? await safelyGetBody(ctx);
|
|
3279
3528
|
validBody = await validators.body(b);
|
|
3529
|
+
ctx._cachedBody = validBody;
|
|
3280
3530
|
const req = ctx.req;
|
|
3281
|
-
req._bodyValue = validBody;
|
|
3282
3531
|
Object.defineProperty(req, "json", {
|
|
3283
3532
|
value: async () => validBody,
|
|
3533
|
+
writable: true,
|
|
3284
3534
|
configurable: true
|
|
3285
3535
|
});
|
|
3286
3536
|
ctx.body = validBody;
|
|
3287
3537
|
}
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
await ctx.app?.executeHook("afterValidate", ctx, validatedData);
|
|
3294
|
-
}
|
|
3538
|
+
const validatedData = { ...dataToValidate };
|
|
3539
|
+
if (config.params) validatedData.params = ctx.params;
|
|
3540
|
+
if (config.query) validatedData.query = validQuery;
|
|
3541
|
+
if (config.body) validatedData.body = validBody;
|
|
3542
|
+
await ctx.app.runHooks("afterValidate", ctx, validatedData);
|
|
3295
3543
|
return next();
|
|
3296
3544
|
};
|
|
3297
3545
|
}
|
|
@@ -3314,12 +3562,14 @@ function openApiValidator() {
|
|
|
3314
3562
|
if (cache.validators.has(ctx.path)) {
|
|
3315
3563
|
matchPath = ctx.path;
|
|
3316
3564
|
} else {
|
|
3317
|
-
|
|
3565
|
+
const pathEntries = Array.from(cache.paths.entries());
|
|
3566
|
+
for (let i = 0; i < pathEntries.length; i++) {
|
|
3567
|
+
const [path, { regex, paramNames }] = pathEntries[i];
|
|
3318
3568
|
const match = regex.exec(ctx.path);
|
|
3319
3569
|
if (match) {
|
|
3320
3570
|
matchPath = path;
|
|
3321
|
-
paramNames.forEach((name,
|
|
3322
|
-
matchParams[name] = match[
|
|
3571
|
+
paramNames.forEach((name, i2) => {
|
|
3572
|
+
matchParams[name] = match[i2 + 1];
|
|
3323
3573
|
});
|
|
3324
3574
|
break;
|
|
3325
3575
|
}
|
|
@@ -3376,7 +3626,9 @@ function openApiValidator() {
|
|
|
3376
3626
|
function compileValidators(spec) {
|
|
3377
3627
|
const validators = /* @__PURE__ */ new Map();
|
|
3378
3628
|
const paths = /* @__PURE__ */ new Map();
|
|
3379
|
-
|
|
3629
|
+
const pathEntries = Object.entries(spec.paths || {});
|
|
3630
|
+
for (let i = 0; i < pathEntries.length; i++) {
|
|
3631
|
+
const [path, pathItem] = pathEntries[i];
|
|
3380
3632
|
if (path.includes("{")) {
|
|
3381
3633
|
const paramNames = [];
|
|
3382
3634
|
const regexStr = "^" + path.replace(/{([^}]+)}/g, (_, name) => {
|
|
@@ -3389,7 +3641,9 @@ function compileValidators(spec) {
|
|
|
3389
3641
|
});
|
|
3390
3642
|
}
|
|
3391
3643
|
const pathValidators = {};
|
|
3392
|
-
|
|
3644
|
+
const methodEntries = Object.entries(pathItem);
|
|
3645
|
+
for (let k = 0; k < methodEntries.length; k++) {
|
|
3646
|
+
const [method, operation] = methodEntries[k];
|
|
3393
3647
|
if (method === "parameters" || method === "summary" || method === "description") continue;
|
|
3394
3648
|
const oper = operation;
|
|
3395
3649
|
const opValidators = {};
|
|
@@ -3403,7 +3657,8 @@ function compileValidators(spec) {
|
|
|
3403
3657
|
const queryRequired = [];
|
|
3404
3658
|
const pathRequired = [];
|
|
3405
3659
|
const headerRequired = [];
|
|
3406
|
-
for (
|
|
3660
|
+
for (let j = 0; j < parameters.length; j++) {
|
|
3661
|
+
const param = parameters[j];
|
|
3407
3662
|
if (param.in === "query") {
|
|
3408
3663
|
queryProps[param.name] = param.schema || {};
|
|
3409
3664
|
if (param.required) queryRequired.push(param.name);
|
|
@@ -3564,14 +3819,18 @@ function SecurityHeaders(options = {}) {
|
|
|
3564
3819
|
if (opt === void 0 || opt === true) {
|
|
3565
3820
|
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");
|
|
3566
3821
|
} else if (typeof opt === "object") {
|
|
3567
|
-
|
|
3822
|
+
const optEntries = Object.entries(opt);
|
|
3823
|
+
for (let i = 0; i < optEntries.length; i++) {
|
|
3824
|
+
const [key, val] = optEntries[i];
|
|
3568
3825
|
}
|
|
3569
3826
|
}
|
|
3570
3827
|
}
|
|
3571
3828
|
if (options.hidePoweredBy !== false) ;
|
|
3572
3829
|
const response = await next();
|
|
3573
3830
|
if (response instanceof Response) {
|
|
3574
|
-
|
|
3831
|
+
const headerEntries = Object.entries(headers);
|
|
3832
|
+
for (let i = 0; i < headerEntries.length; i++) {
|
|
3833
|
+
const [k, v] = headerEntries[i];
|
|
3575
3834
|
response.headers.set(k, v);
|
|
3576
3835
|
}
|
|
3577
3836
|
return response;
|
|
@@ -3657,7 +3916,9 @@ class MemoryStore extends EventEmitter {
|
|
|
3657
3916
|
}
|
|
3658
3917
|
all(cb) {
|
|
3659
3918
|
const result = {};
|
|
3660
|
-
|
|
3919
|
+
const sessionKeys = Object.keys(this.sessions);
|
|
3920
|
+
for (let i = 0; i < sessionKeys.length; i++) {
|
|
3921
|
+
const sid = sessionKeys[i];
|
|
3661
3922
|
try {
|
|
3662
3923
|
result[sid] = JSON.parse(this.sessions[sid]);
|
|
3663
3924
|
} catch {
|
|
@@ -3680,11 +3941,17 @@ function unsign(input, secret) {
|
|
|
3680
3941
|
if (typeof secret !== "string") throw new TypeError("Secret string must be provided.");
|
|
3681
3942
|
const tentValue = input.slice(0, input.lastIndexOf("."));
|
|
3682
3943
|
const expectedInput = sign(tentValue, secret);
|
|
3683
|
-
const
|
|
3684
|
-
const
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3944
|
+
const maxLength = Math.max(expectedInput.length, input.length);
|
|
3945
|
+
const paddedExpected = Buffer.alloc(maxLength);
|
|
3946
|
+
const paddedInput = Buffer.alloc(maxLength);
|
|
3947
|
+
Buffer.from(expectedInput).copy(paddedExpected);
|
|
3948
|
+
Buffer.from(input).copy(paddedInput);
|
|
3949
|
+
try {
|
|
3950
|
+
const valid = require("crypto").timingSafeEqual(paddedExpected, paddedInput);
|
|
3951
|
+
return valid ? tentValue : false;
|
|
3952
|
+
} catch {
|
|
3953
|
+
return false;
|
|
3954
|
+
}
|
|
3688
3955
|
}
|
|
3689
3956
|
function Session(options) {
|
|
3690
3957
|
const store = options.store || new MemoryStore();
|
|
@@ -3743,7 +4010,9 @@ function Session(options) {
|
|
|
3743
4010
|
sessObj.regenerate = (cb) => {
|
|
3744
4011
|
store.destroy(sessObj.id, (err) => {
|
|
3745
4012
|
sessionID = generateId(ctx);
|
|
3746
|
-
|
|
4013
|
+
const keys = Object.keys(sessObj);
|
|
4014
|
+
for (let i = 0; i < keys.length; i++) {
|
|
4015
|
+
const key = keys[i];
|
|
3747
4016
|
if (key !== "cookie" && key !== "id" && typeof sessObj[key] !== "function") {
|
|
3748
4017
|
delete sessObj[key];
|
|
3749
4018
|
}
|
|
@@ -3758,7 +4027,9 @@ function Session(options) {
|
|
|
3758
4027
|
store.get(sessObj.id, (err, sess2) => {
|
|
3759
4028
|
if (err) return cb(err);
|
|
3760
4029
|
if (!sess2) return cb(new Error("Session not found"));
|
|
3761
|
-
|
|
4030
|
+
const keys = Object.keys(sessObj);
|
|
4031
|
+
for (let i = 0; i < keys.length; i++) {
|
|
4032
|
+
const key = keys[i];
|
|
3762
4033
|
if (key !== "cookie" && key !== "id" && typeof sessObj[key] !== "function") {
|
|
3763
4034
|
delete sessObj[key];
|
|
3764
4035
|
}
|