shokupan 0.0.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 +1669 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli.cjs +154 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.js +136 -0
- package/dist/cli.js.map +1 -0
- package/dist/context.d.ts +88 -0
- package/dist/decorators.d.ts +23 -0
- package/dist/di.d.ts +18 -0
- package/dist/index.cjs +2305 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +2288 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware.d.ts +7 -0
- package/dist/plugins/auth.d.ts +58 -0
- package/dist/plugins/compression.d.ts +5 -0
- package/dist/plugins/cors.d.ts +11 -0
- package/dist/plugins/express.d.ts +6 -0
- package/dist/plugins/rate-limit.d.ts +12 -0
- package/dist/plugins/scalar.d.ts +13 -0
- package/dist/plugins/security-headers.d.ts +36 -0
- package/dist/plugins/session.d.ts +87 -0
- package/dist/plugins/validation.d.ts +18 -0
- package/dist/request.d.ts +34 -0
- package/dist/response.d.ts +42 -0
- package/dist/router.d.ts +237 -0
- package/dist/shokupan.d.ts +41 -0
- package/dist/symbol.d.ts +13 -0
- package/dist/types.d.ts +142 -0
- package/dist/util/async-hooks.d.ts +3 -0
- package/dist/util/deep-merge.d.ts +12 -0
- package/dist/util/instrumentation.d.ts +9 -0
- package/package.json +82 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2305 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const api = require("@opentelemetry/api");
|
|
4
|
+
const exporterTraceOtlpProto = require("@opentelemetry/exporter-trace-otlp-proto");
|
|
5
|
+
const resources = require("@opentelemetry/resources");
|
|
6
|
+
const sdkTraceBase = require("@opentelemetry/sdk-trace-base");
|
|
7
|
+
const sdkTraceNode = require("@opentelemetry/sdk-trace-node");
|
|
8
|
+
const semanticConventions = require("@opentelemetry/semantic-conventions");
|
|
9
|
+
const eta$2 = require("eta");
|
|
10
|
+
const promises = require("fs/promises");
|
|
11
|
+
const path = require("path");
|
|
12
|
+
const node_async_hooks = require("node:async_hooks");
|
|
13
|
+
const arctic = require("arctic");
|
|
14
|
+
const jose = require("jose");
|
|
15
|
+
const crypto = require("crypto");
|
|
16
|
+
const events = require("events");
|
|
17
|
+
function _interopNamespaceDefault(e) {
|
|
18
|
+
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
19
|
+
if (e) {
|
|
20
|
+
for (const k in e) {
|
|
21
|
+
if (k !== "default") {
|
|
22
|
+
const d = Object.getOwnPropertyDescriptor(e, k);
|
|
23
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
24
|
+
enumerable: true,
|
|
25
|
+
get: () => e[k]
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
n.default = e;
|
|
31
|
+
return Object.freeze(n);
|
|
32
|
+
}
|
|
33
|
+
const jose__namespace = /* @__PURE__ */ _interopNamespaceDefault(jose);
|
|
34
|
+
class ShokupanResponse {
|
|
35
|
+
_headers = new Headers();
|
|
36
|
+
_status = 200;
|
|
37
|
+
/**
|
|
38
|
+
* Get the current headers
|
|
39
|
+
*/
|
|
40
|
+
get headers() {
|
|
41
|
+
return this._headers;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get the current status code
|
|
45
|
+
*/
|
|
46
|
+
get status() {
|
|
47
|
+
return this._status;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Set the status code
|
|
51
|
+
*/
|
|
52
|
+
set status(code) {
|
|
53
|
+
this._status = code;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Set a response header
|
|
57
|
+
* @param key Header name
|
|
58
|
+
* @param value Header value
|
|
59
|
+
*/
|
|
60
|
+
set(key, value) {
|
|
61
|
+
this._headers.set(key, value);
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Append to a response header
|
|
66
|
+
* @param key Header name
|
|
67
|
+
* @param value Header value
|
|
68
|
+
*/
|
|
69
|
+
append(key, value) {
|
|
70
|
+
this._headers.append(key, value);
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Get a response header value
|
|
75
|
+
* @param key Header name
|
|
76
|
+
*/
|
|
77
|
+
get(key) {
|
|
78
|
+
return this._headers.get(key);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Check if a header exists
|
|
82
|
+
* @param key Header name
|
|
83
|
+
*/
|
|
84
|
+
has(key) {
|
|
85
|
+
return this._headers.has(key);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
class ShokupanContext {
|
|
89
|
+
constructor(request, state) {
|
|
90
|
+
this.request = request;
|
|
91
|
+
this.url = new URL(request.url);
|
|
92
|
+
this.state = state || {};
|
|
93
|
+
this.response = new ShokupanResponse();
|
|
94
|
+
}
|
|
95
|
+
url;
|
|
96
|
+
params = {};
|
|
97
|
+
state;
|
|
98
|
+
response;
|
|
99
|
+
/**
|
|
100
|
+
* Base request
|
|
101
|
+
*/
|
|
102
|
+
get req() {
|
|
103
|
+
return this.request;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* HTTP method
|
|
107
|
+
*/
|
|
108
|
+
get method() {
|
|
109
|
+
return this.request.method;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Request path
|
|
113
|
+
*/
|
|
114
|
+
get path() {
|
|
115
|
+
return this.url.pathname;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Request query params
|
|
119
|
+
*/
|
|
120
|
+
get query() {
|
|
121
|
+
return Object.fromEntries(this.url.searchParams);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Request headers
|
|
125
|
+
*/
|
|
126
|
+
get headers() {
|
|
127
|
+
return this.request.headers;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Base response object
|
|
131
|
+
*/
|
|
132
|
+
get res() {
|
|
133
|
+
return this.response;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Helper to set a header on the response
|
|
137
|
+
*/
|
|
138
|
+
set(key, value) {
|
|
139
|
+
this.response.set(key, value);
|
|
140
|
+
return this;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Set a cookie
|
|
144
|
+
* @param name Cookie name
|
|
145
|
+
* @param value Cookie value
|
|
146
|
+
* @param options Cookie options
|
|
147
|
+
*/
|
|
148
|
+
setCookie(name, value, options = {}) {
|
|
149
|
+
let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
|
|
150
|
+
if (options.maxAge) cookie += `; Max-Age=${Math.floor(options.maxAge)}`;
|
|
151
|
+
if (options.expires) cookie += `; Expires=${options.expires.toUTCString()}`;
|
|
152
|
+
if (options.httpOnly) cookie += `; HttpOnly`;
|
|
153
|
+
if (options.secure) cookie += `; Secure`;
|
|
154
|
+
if (options.domain) cookie += `; Domain=${options.domain}`;
|
|
155
|
+
if (options.path) cookie += `; Path=${options.path || "/"}`;
|
|
156
|
+
if (options.sameSite) {
|
|
157
|
+
typeof options.sameSite === "string" ? options.sameSite.toLowerCase() : options.sameSite ? "strict" : "lax";
|
|
158
|
+
cookie += `; SameSite=${typeof options.sameSite === "boolean" ? "Strict" : options.sameSite.charAt(0).toUpperCase() + options.sameSite.slice(1)}`;
|
|
159
|
+
}
|
|
160
|
+
if (options.priority) {
|
|
161
|
+
cookie += `; Priority=${options.priority.charAt(0).toUpperCase() + options.priority.slice(1)}`;
|
|
162
|
+
}
|
|
163
|
+
this.response.append("Set-Cookie", cookie);
|
|
164
|
+
return this;
|
|
165
|
+
}
|
|
166
|
+
mergeHeaders(headers) {
|
|
167
|
+
const h = new Headers(this.response.headers);
|
|
168
|
+
if (headers) {
|
|
169
|
+
new Headers(headers).forEach((v, k) => h.set(k, v));
|
|
170
|
+
}
|
|
171
|
+
return h;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Send a response
|
|
175
|
+
* @param body Response body
|
|
176
|
+
* @param options Response options
|
|
177
|
+
* @returns Response
|
|
178
|
+
*/
|
|
179
|
+
send(body, options) {
|
|
180
|
+
const headers = this.mergeHeaders(options?.headers);
|
|
181
|
+
const status = options?.status ?? this.response.status;
|
|
182
|
+
return new Response(body, { status, headers });
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Read request body
|
|
186
|
+
*/
|
|
187
|
+
async body() {
|
|
188
|
+
const contentType = this.request.headers.get("content-type");
|
|
189
|
+
if (contentType?.includes("application/json")) {
|
|
190
|
+
return this.request.json();
|
|
191
|
+
}
|
|
192
|
+
if (contentType?.includes("multipart/form-data") || contentType?.includes("application/x-www-form-urlencoded")) {
|
|
193
|
+
return this.request.formData();
|
|
194
|
+
}
|
|
195
|
+
return this.request.text();
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Respond with a JSON object
|
|
199
|
+
*/
|
|
200
|
+
json(data, status, headers) {
|
|
201
|
+
const finalHeaders = this.mergeHeaders(headers);
|
|
202
|
+
finalHeaders.set("content-type", "application/json");
|
|
203
|
+
const finalStatus = status ?? this.response.status;
|
|
204
|
+
return new Response(JSON.stringify(data), { status: finalStatus, headers: finalHeaders });
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Respond with a text string
|
|
208
|
+
*/
|
|
209
|
+
text(data, status, headers) {
|
|
210
|
+
const finalHeaders = this.mergeHeaders(headers);
|
|
211
|
+
finalHeaders.set("content-type", "text/plain");
|
|
212
|
+
const finalStatus = status ?? this.response.status;
|
|
213
|
+
return new Response(data, { status: finalStatus, headers: finalHeaders });
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Respond with HTML content
|
|
217
|
+
*/
|
|
218
|
+
html(html, status, headers) {
|
|
219
|
+
const finalHeaders = this.mergeHeaders(headers);
|
|
220
|
+
finalHeaders.set("content-type", "text/html");
|
|
221
|
+
const finalStatus = status ?? this.response.status;
|
|
222
|
+
return new Response(html, { status: finalStatus, headers: finalHeaders });
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Respond with a redirect
|
|
226
|
+
*/
|
|
227
|
+
redirect(url, status = 302) {
|
|
228
|
+
const headers = this.mergeHeaders();
|
|
229
|
+
headers.set("Location", url);
|
|
230
|
+
return new Response(null, { status, headers });
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Respond with a status code
|
|
234
|
+
* DOES NOT CHAIN!
|
|
235
|
+
*/
|
|
236
|
+
status(status) {
|
|
237
|
+
const headers = this.mergeHeaders();
|
|
238
|
+
return new Response(null, { status, headers });
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Respond with a file
|
|
242
|
+
*/
|
|
243
|
+
file(path2, fileOptions, responseOptions) {
|
|
244
|
+
const headers = this.mergeHeaders(responseOptions?.headers);
|
|
245
|
+
const status = responseOptions?.status ?? this.response.status;
|
|
246
|
+
return new Response(Bun.file(path2, fileOptions), { status, headers });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
|
|
250
|
+
const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
|
|
251
|
+
const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
|
|
252
|
+
const $routeMethods = /* @__PURE__ */ Symbol("Shokupan.routeMethods");
|
|
253
|
+
const $routeArgs = /* @__PURE__ */ Symbol("Shokupan.routeArgs");
|
|
254
|
+
const $controllerPath = /* @__PURE__ */ Symbol("Shokupan.controllerPath");
|
|
255
|
+
const $middleware = /* @__PURE__ */ Symbol("Shokupan.middleware");
|
|
256
|
+
const $isRouter = /* @__PURE__ */ Symbol.for("Shokupan.router");
|
|
257
|
+
const $parent = /* @__PURE__ */ Symbol.for("Shokupan.parent");
|
|
258
|
+
const $childRouters = /* @__PURE__ */ Symbol.for("Shokupan.child-routers");
|
|
259
|
+
const $childControllers = /* @__PURE__ */ Symbol.for("Shokupan.child-controllers");
|
|
260
|
+
const $mountPath = /* @__PURE__ */ Symbol.for("Shokupan.mount-path");
|
|
261
|
+
const $dispatch = /* @__PURE__ */ Symbol.for("Shokupan.dispatch");
|
|
262
|
+
const HTTPMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "ALL"];
|
|
263
|
+
var RouteParamType = /* @__PURE__ */ ((RouteParamType2) => {
|
|
264
|
+
RouteParamType2["BODY"] = "BODY";
|
|
265
|
+
RouteParamType2["PARAM"] = "PARAM";
|
|
266
|
+
RouteParamType2["QUERY"] = "QUERY";
|
|
267
|
+
RouteParamType2["HEADER"] = "HEADER";
|
|
268
|
+
RouteParamType2["REQUEST"] = "REQUEST";
|
|
269
|
+
RouteParamType2["CONTEXT"] = "CONTEXT";
|
|
270
|
+
return RouteParamType2;
|
|
271
|
+
})(RouteParamType || {});
|
|
272
|
+
function Controller(path2 = "/") {
|
|
273
|
+
return (target) => {
|
|
274
|
+
target[$controllerPath] = path2;
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function Use(...middleware) {
|
|
278
|
+
return (target, propertyKey, descriptor) => {
|
|
279
|
+
if (!propertyKey) {
|
|
280
|
+
const existing = target[$middleware] || [];
|
|
281
|
+
target[$middleware] = [...existing, ...middleware];
|
|
282
|
+
} else {
|
|
283
|
+
if (!target[$middleware]) {
|
|
284
|
+
target[$middleware] = /* @__PURE__ */ new Map();
|
|
285
|
+
}
|
|
286
|
+
const existing = target[$middleware].get(propertyKey) || [];
|
|
287
|
+
target[$middleware].set(propertyKey, [...existing, ...middleware]);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function createParamDecorator(type) {
|
|
292
|
+
return (name) => {
|
|
293
|
+
return (target, propertyKey, parameterIndex) => {
|
|
294
|
+
if (!target[$routeArgs]) {
|
|
295
|
+
target[$routeArgs] = /* @__PURE__ */ new Map();
|
|
296
|
+
}
|
|
297
|
+
if (!target[$routeArgs].has(propertyKey)) {
|
|
298
|
+
target[$routeArgs].set(propertyKey, []);
|
|
299
|
+
}
|
|
300
|
+
target[$routeArgs].get(propertyKey).push({
|
|
301
|
+
index: parameterIndex,
|
|
302
|
+
type,
|
|
303
|
+
name
|
|
304
|
+
});
|
|
305
|
+
};
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
const Body = createParamDecorator(RouteParamType.BODY);
|
|
309
|
+
const Param = createParamDecorator(RouteParamType.PARAM);
|
|
310
|
+
const Query = createParamDecorator(RouteParamType.QUERY);
|
|
311
|
+
const Headers$1 = createParamDecorator(RouteParamType.HEADER);
|
|
312
|
+
const Req = createParamDecorator(RouteParamType.REQUEST);
|
|
313
|
+
const Ctx = createParamDecorator(RouteParamType.CONTEXT);
|
|
314
|
+
function createMethodDecorator(method) {
|
|
315
|
+
return (path2 = "/") => {
|
|
316
|
+
return (target, propertyKey, descriptor) => {
|
|
317
|
+
if (!target[$routeMethods]) {
|
|
318
|
+
target[$routeMethods] = /* @__PURE__ */ new Map();
|
|
319
|
+
}
|
|
320
|
+
target[$routeMethods].set(propertyKey, {
|
|
321
|
+
method,
|
|
322
|
+
path: path2
|
|
323
|
+
});
|
|
324
|
+
};
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
const Get = createMethodDecorator("GET");
|
|
328
|
+
const Post = createMethodDecorator("POST");
|
|
329
|
+
const Put = createMethodDecorator("PUT");
|
|
330
|
+
const Delete = createMethodDecorator("DELETE");
|
|
331
|
+
const Patch = createMethodDecorator("PATCH");
|
|
332
|
+
const Options = createMethodDecorator("OPTIONS");
|
|
333
|
+
const Head = createMethodDecorator("HEAD");
|
|
334
|
+
const All = createMethodDecorator("ALL");
|
|
335
|
+
class Container {
|
|
336
|
+
static services = /* @__PURE__ */ new Map();
|
|
337
|
+
static register(target, instance) {
|
|
338
|
+
this.services.set(target, instance);
|
|
339
|
+
}
|
|
340
|
+
static get(target) {
|
|
341
|
+
return this.services.get(target);
|
|
342
|
+
}
|
|
343
|
+
static has(target) {
|
|
344
|
+
return this.services.has(target);
|
|
345
|
+
}
|
|
346
|
+
static resolve(target) {
|
|
347
|
+
if (this.services.has(target)) {
|
|
348
|
+
return this.services.get(target);
|
|
349
|
+
}
|
|
350
|
+
const instance = new target();
|
|
351
|
+
this.services.set(target, instance);
|
|
352
|
+
return instance;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function Injectable() {
|
|
356
|
+
return (target) => {
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function Inject(token) {
|
|
360
|
+
return (target, key) => {
|
|
361
|
+
Object.defineProperty(target, key, {
|
|
362
|
+
get: () => Container.resolve(token),
|
|
363
|
+
enumerable: true,
|
|
364
|
+
configurable: true
|
|
365
|
+
});
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
const provider = new sdkTraceNode.NodeTracerProvider({
|
|
369
|
+
resource: resources.resourceFromAttributes({
|
|
370
|
+
[semanticConventions.ATTR_SERVICE_NAME]: "basic-service"
|
|
371
|
+
}),
|
|
372
|
+
spanProcessors: [
|
|
373
|
+
new sdkTraceBase.SimpleSpanProcessor(
|
|
374
|
+
new exporterTraceOtlpProto.OTLPTraceExporter({
|
|
375
|
+
url: "http://localhost:4318/v1/traces"
|
|
376
|
+
// Default OTLP port
|
|
377
|
+
})
|
|
378
|
+
)
|
|
379
|
+
]
|
|
380
|
+
});
|
|
381
|
+
provider.register();
|
|
382
|
+
const tracer = api.trace.getTracer("shokupan.middleware");
|
|
383
|
+
function traceMiddleware(fn, name) {
|
|
384
|
+
const middlewareName = fn.name || "anonymous middleware";
|
|
385
|
+
return async (ctx, next) => {
|
|
386
|
+
return tracer.startActiveSpan(`middleware - ${middlewareName}`, {
|
|
387
|
+
kind: api.SpanKind.INTERNAL,
|
|
388
|
+
attributes: {
|
|
389
|
+
"code.function": middlewareName,
|
|
390
|
+
"component": "shokupan.middleware"
|
|
391
|
+
}
|
|
392
|
+
}, async (span) => {
|
|
393
|
+
try {
|
|
394
|
+
const result = await fn(ctx, next);
|
|
395
|
+
return result;
|
|
396
|
+
} catch (err) {
|
|
397
|
+
span.recordException(err);
|
|
398
|
+
span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
|
|
399
|
+
throw err;
|
|
400
|
+
} finally {
|
|
401
|
+
span.end();
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
function traceHandler(fn, name) {
|
|
407
|
+
return async function(...args) {
|
|
408
|
+
return tracer.startActiveSpan(`route handler - ${name}`, {
|
|
409
|
+
kind: api.SpanKind.INTERNAL,
|
|
410
|
+
attributes: {
|
|
411
|
+
"http.route": name,
|
|
412
|
+
"component": "shokupan.route"
|
|
413
|
+
}
|
|
414
|
+
}, async (span) => {
|
|
415
|
+
try {
|
|
416
|
+
const result = await fn.apply(this, args);
|
|
417
|
+
return result;
|
|
418
|
+
} catch (err) {
|
|
419
|
+
span.recordException(err);
|
|
420
|
+
span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
|
|
421
|
+
throw err;
|
|
422
|
+
} finally {
|
|
423
|
+
span.end();
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
const compose = (middleware) => {
|
|
429
|
+
function fn(context, next) {
|
|
430
|
+
let runner = next || (async () => {
|
|
431
|
+
});
|
|
432
|
+
for (let i = middleware.length - 1; i >= 0; i--) {
|
|
433
|
+
const fn2 = traceMiddleware(middleware[i]);
|
|
434
|
+
const nextStep = runner;
|
|
435
|
+
let called = false;
|
|
436
|
+
runner = async () => {
|
|
437
|
+
if (called) throw new Error("next() called multiple times");
|
|
438
|
+
called = true;
|
|
439
|
+
return fn2(context, nextStep);
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
return runner();
|
|
443
|
+
}
|
|
444
|
+
return fn;
|
|
445
|
+
};
|
|
446
|
+
class ShokupanRequestBase {
|
|
447
|
+
method;
|
|
448
|
+
url;
|
|
449
|
+
headers;
|
|
450
|
+
body;
|
|
451
|
+
async json() {
|
|
452
|
+
return JSON.parse(this.body);
|
|
453
|
+
}
|
|
454
|
+
async text() {
|
|
455
|
+
return this.body;
|
|
456
|
+
}
|
|
457
|
+
async formData() {
|
|
458
|
+
if (this.body instanceof FormData) {
|
|
459
|
+
return this.body;
|
|
460
|
+
}
|
|
461
|
+
return new Response(this.body, { headers: this.headers }).formData();
|
|
462
|
+
}
|
|
463
|
+
constructor(props) {
|
|
464
|
+
Object.assign(this, props);
|
|
465
|
+
if (!(this.headers instanceof Headers)) {
|
|
466
|
+
this.headers = new Headers(this.headers);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const ShokupanRequest = ShokupanRequestBase;
|
|
471
|
+
const asyncContext = new node_async_hooks.AsyncLocalStorage();
|
|
472
|
+
function isObject(item) {
|
|
473
|
+
return item && typeof item === "object" && !Array.isArray(item);
|
|
474
|
+
}
|
|
475
|
+
function deepMerge(target, ...sources) {
|
|
476
|
+
if (!sources.length) return target;
|
|
477
|
+
const source = sources.shift();
|
|
478
|
+
if (isObject(target) && isObject(source)) {
|
|
479
|
+
for (const key in source) {
|
|
480
|
+
if (isObject(source[key])) {
|
|
481
|
+
if (!target[key]) Object.assign(target, { [key]: {} });
|
|
482
|
+
deepMerge(target[key], source[key]);
|
|
483
|
+
} else if (Array.isArray(source[key])) {
|
|
484
|
+
if (!target[key]) Object.assign(target, { [key]: [] });
|
|
485
|
+
target[key] = target[key].concat(source[key]);
|
|
486
|
+
} else {
|
|
487
|
+
Object.assign(target, { [key]: source[key] });
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return deepMerge(target, ...sources);
|
|
492
|
+
}
|
|
493
|
+
const eta$1 = new eta$2.Eta();
|
|
494
|
+
const RouterRegistry = /* @__PURE__ */ new Map();
|
|
495
|
+
const ShokupanApplicationTree = {};
|
|
496
|
+
class ShokupanRouter {
|
|
497
|
+
constructor(config) {
|
|
498
|
+
this.config = config;
|
|
499
|
+
}
|
|
500
|
+
// Internal marker to identify Router vs. Application
|
|
501
|
+
[$isApplication] = false;
|
|
502
|
+
[$isMounted] = false;
|
|
503
|
+
[$isRouter] = true;
|
|
504
|
+
[$appRoot];
|
|
505
|
+
[$mountPath] = "/";
|
|
506
|
+
[$parent] = null;
|
|
507
|
+
[$childRouters] = [];
|
|
508
|
+
[$childControllers] = [];
|
|
509
|
+
get rootConfig() {
|
|
510
|
+
return this[$appRoot]?.applicationConfig;
|
|
511
|
+
}
|
|
512
|
+
get root() {
|
|
513
|
+
return this[$appRoot];
|
|
514
|
+
}
|
|
515
|
+
routes = [];
|
|
516
|
+
currentGuards = [];
|
|
517
|
+
isRouterInstance(target) {
|
|
518
|
+
return typeof target === "object" && target !== null && $isRouter in target;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Mounts a controller instance to a path prefix.
|
|
522
|
+
*
|
|
523
|
+
* Controller can be a convection router or an arbitrary class.
|
|
524
|
+
*
|
|
525
|
+
* Routes are derived from method names:
|
|
526
|
+
* - get(ctx) -> GET /prefix/
|
|
527
|
+
* - getUsers(ctx) -> GET /prefix/users
|
|
528
|
+
* - postCreate(ctx) -> POST /prefix/create
|
|
529
|
+
*/
|
|
530
|
+
mount(prefix, controller) {
|
|
531
|
+
if (this.isRouterInstance(controller)) {
|
|
532
|
+
if (controller[$isMounted]) {
|
|
533
|
+
throw new Error("Router is already mounted");
|
|
534
|
+
}
|
|
535
|
+
controller[$mountPath] = prefix;
|
|
536
|
+
this[$childRouters].push(controller);
|
|
537
|
+
controller[$parent] = this;
|
|
538
|
+
const setRouterContext = (router) => {
|
|
539
|
+
router[$appRoot] = this.root;
|
|
540
|
+
router[$childRouters].forEach((child) => setRouterContext(child));
|
|
541
|
+
};
|
|
542
|
+
setRouterContext(controller);
|
|
543
|
+
if (this[$appRoot]) ;
|
|
544
|
+
controller[$appRoot] = this.root;
|
|
545
|
+
controller[$isMounted] = true;
|
|
546
|
+
} else {
|
|
547
|
+
let instance = controller;
|
|
548
|
+
if (typeof controller === "function") {
|
|
549
|
+
instance = Container.resolve(controller);
|
|
550
|
+
const controllerPath = controller[$controllerPath];
|
|
551
|
+
if (controllerPath) {
|
|
552
|
+
const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
553
|
+
const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
|
|
554
|
+
prefix = p1 + p2;
|
|
555
|
+
if (!prefix) prefix = "/";
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
instance[$mountPath] = prefix;
|
|
559
|
+
this[$childControllers].push(instance);
|
|
560
|
+
const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
|
|
561
|
+
const proto = Object.getPrototypeOf(instance);
|
|
562
|
+
const methods = /* @__PURE__ */ new Set();
|
|
563
|
+
let current = proto;
|
|
564
|
+
while (current && current !== Object.prototype) {
|
|
565
|
+
Object.getOwnPropertyNames(current).forEach((name) => methods.add(name));
|
|
566
|
+
current = Object.getPrototypeOf(current);
|
|
567
|
+
}
|
|
568
|
+
Object.getOwnPropertyNames(instance).forEach((name) => methods.add(name));
|
|
569
|
+
const decoratedRoutes = instance[$routeMethods] || proto && proto[$routeMethods];
|
|
570
|
+
const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
|
|
571
|
+
const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
|
|
572
|
+
let routesAttached = 0;
|
|
573
|
+
for (const name of methods) {
|
|
574
|
+
if (name === "constructor") continue;
|
|
575
|
+
if (["arguments", "caller", "callee"].includes(name)) continue;
|
|
576
|
+
const originalHandler = instance[name];
|
|
577
|
+
if (typeof originalHandler !== "function") continue;
|
|
578
|
+
let method;
|
|
579
|
+
let subPath = "";
|
|
580
|
+
if (decoratedRoutes && decoratedRoutes.has(name)) {
|
|
581
|
+
const config = decoratedRoutes.get(name);
|
|
582
|
+
method = config.method;
|
|
583
|
+
subPath = config.path;
|
|
584
|
+
} else {
|
|
585
|
+
for (const m of HTTPMethods) {
|
|
586
|
+
if (name.toUpperCase().startsWith(m)) {
|
|
587
|
+
method = m;
|
|
588
|
+
const rest = name.slice(m.length);
|
|
589
|
+
if (rest.length === 0) {
|
|
590
|
+
subPath = "/";
|
|
591
|
+
} else {
|
|
592
|
+
subPath = "";
|
|
593
|
+
let buffer = "";
|
|
594
|
+
const flush = () => {
|
|
595
|
+
if (buffer.length > 0) {
|
|
596
|
+
subPath += "/" + buffer.toLowerCase();
|
|
597
|
+
buffer = "";
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
for (let i = 0; i < rest.length; i++) {
|
|
601
|
+
const char = rest[i];
|
|
602
|
+
if (char === "$") {
|
|
603
|
+
flush();
|
|
604
|
+
subPath += "/:";
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
subPath = rest.replace(/\$/g, "/:").replace(/([a-z0-9])([A-Z])/g, "$1/$2").toLowerCase();
|
|
609
|
+
if (!subPath.startsWith("/")) {
|
|
610
|
+
subPath = "/" + subPath;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (method) {
|
|
618
|
+
routesAttached++;
|
|
619
|
+
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
620
|
+
const cleanSubPath = subPath === "/" ? "" : subPath;
|
|
621
|
+
let joined;
|
|
622
|
+
if (cleanSubPath.length === 0) {
|
|
623
|
+
joined = cleanPrefix;
|
|
624
|
+
} else if (cleanSubPath.startsWith("/")) {
|
|
625
|
+
joined = cleanPrefix + cleanSubPath;
|
|
626
|
+
} else {
|
|
627
|
+
joined = cleanPrefix + "/" + cleanSubPath;
|
|
628
|
+
}
|
|
629
|
+
const fullPath = joined || "/";
|
|
630
|
+
const normalizedPath = fullPath.replace(/\/+/g, "/");
|
|
631
|
+
const methodMw = methodMiddlewareMap instanceof Map ? methodMiddlewareMap.get(name) || [] : [];
|
|
632
|
+
const allMiddleware = [...controllerMiddleware, ...methodMw];
|
|
633
|
+
const routeArgs = decoratedArgs && decoratedArgs.get(name);
|
|
634
|
+
const wrappedHandler = async (ctx) => {
|
|
635
|
+
let args = [ctx];
|
|
636
|
+
if (routeArgs?.length > 0) {
|
|
637
|
+
args = [];
|
|
638
|
+
const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
|
|
639
|
+
for (const arg of sortedArgs) {
|
|
640
|
+
switch (arg.type) {
|
|
641
|
+
case RouteParamType.BODY:
|
|
642
|
+
args[arg.index] = await ctx.req.json().catch(() => ({}));
|
|
643
|
+
break;
|
|
644
|
+
case RouteParamType.PARAM:
|
|
645
|
+
args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
|
|
646
|
+
break;
|
|
647
|
+
case RouteParamType.QUERY: {
|
|
648
|
+
const url = new URL(ctx.req.url);
|
|
649
|
+
args[arg.index] = arg.name ? url.searchParams.get(arg.name) : Object.fromEntries(url.searchParams);
|
|
650
|
+
break;
|
|
651
|
+
}
|
|
652
|
+
case RouteParamType.HEADER:
|
|
653
|
+
args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
|
|
654
|
+
break;
|
|
655
|
+
case RouteParamType.REQUEST:
|
|
656
|
+
args[arg.index] = ctx.req;
|
|
657
|
+
break;
|
|
658
|
+
case RouteParamType.CONTEXT:
|
|
659
|
+
args[arg.index] = ctx;
|
|
660
|
+
break;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
const tracedOriginalHandler = traceHandler(originalHandler, normalizedPath);
|
|
665
|
+
return tracedOriginalHandler.apply(instance, args);
|
|
666
|
+
};
|
|
667
|
+
let finalHandler = wrappedHandler;
|
|
668
|
+
if (allMiddleware.length > 0) {
|
|
669
|
+
const composed = compose(allMiddleware);
|
|
670
|
+
finalHandler = async (ctx) => {
|
|
671
|
+
return composed(ctx, () => wrappedHandler(ctx));
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
const tagName = instance.constructor.name;
|
|
675
|
+
const spec = { tags: [tagName] };
|
|
676
|
+
this.add({ method, path: normalizedPath, handler: finalHandler, spec });
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (routesAttached === 0) {
|
|
680
|
+
console.warn(`No routes attached to controller ${instance.constructor.name}`);
|
|
681
|
+
}
|
|
682
|
+
instance[$isMounted] = true;
|
|
683
|
+
}
|
|
684
|
+
return this;
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Returns all routes attached to this router and its descendants.
|
|
688
|
+
*/
|
|
689
|
+
getRoutes() {
|
|
690
|
+
const routes = this.routes.map((r) => ({
|
|
691
|
+
method: r.method,
|
|
692
|
+
path: r.path,
|
|
693
|
+
handler: r.handler
|
|
694
|
+
}));
|
|
695
|
+
for (const child of this[$childRouters]) {
|
|
696
|
+
const childRoutes = child.getRoutes();
|
|
697
|
+
for (const route of childRoutes) {
|
|
698
|
+
const cleanPrefix = child[$mountPath].endsWith("/") ? child[$mountPath].slice(0, -1) : child[$mountPath];
|
|
699
|
+
const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
|
|
700
|
+
const fullPath = cleanPrefix + cleanPath || "/";
|
|
701
|
+
routes.push({
|
|
702
|
+
method: route.method,
|
|
703
|
+
path: fullPath,
|
|
704
|
+
handler: route.handler
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return routes;
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Makes a sub request to this router.
|
|
712
|
+
* This is useful for triggering other methods or route handlers.
|
|
713
|
+
* @param options The request options.
|
|
714
|
+
* @returns The response.
|
|
715
|
+
*/
|
|
716
|
+
async subRequest(arg) {
|
|
717
|
+
const options = typeof arg === "string" ? { path: arg } : arg;
|
|
718
|
+
const store = asyncContext.getStore();
|
|
719
|
+
store?.get("req");
|
|
720
|
+
let url = options.path;
|
|
721
|
+
if (!url.startsWith("http")) {
|
|
722
|
+
const base = `http://${this.rootConfig?.hostname || "localhost"}:${this.rootConfig.port || 3e3}`;
|
|
723
|
+
const path2 = url.startsWith("/") ? url : "/" + url;
|
|
724
|
+
url = base + path2;
|
|
725
|
+
}
|
|
726
|
+
const req = new ShokupanRequest({
|
|
727
|
+
method: options.method || "GET",
|
|
728
|
+
url,
|
|
729
|
+
headers: options.headers,
|
|
730
|
+
body: options.body ? JSON.stringify(options.body) : void 0
|
|
731
|
+
});
|
|
732
|
+
return this.root[$dispatch](req);
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Processes a request directly.
|
|
736
|
+
*/
|
|
737
|
+
async processRequest(options) {
|
|
738
|
+
let url = options.url || options.path || "/";
|
|
739
|
+
if (!url.startsWith("http")) {
|
|
740
|
+
const base = `http://${this.rootConfig?.hostname || "localhost"}:${this.rootConfig?.port || 3e3}`;
|
|
741
|
+
const path2 = url.startsWith("/") ? url : "/" + url;
|
|
742
|
+
url = base + path2;
|
|
743
|
+
}
|
|
744
|
+
if (options.query) {
|
|
745
|
+
const u = new URL(url);
|
|
746
|
+
for (const [k, v] of Object.entries(options.query)) {
|
|
747
|
+
u.searchParams.set(k, v);
|
|
748
|
+
}
|
|
749
|
+
url = u.toString();
|
|
750
|
+
}
|
|
751
|
+
const req = new ShokupanRequest({
|
|
752
|
+
method: options.method || "GET",
|
|
753
|
+
url,
|
|
754
|
+
headers: options.headers,
|
|
755
|
+
body: options.body && typeof options.body === "object" ? JSON.stringify(options.body) : options.body
|
|
756
|
+
});
|
|
757
|
+
const ctx = new ShokupanContext(req);
|
|
758
|
+
let result = null;
|
|
759
|
+
let status = 200;
|
|
760
|
+
const headers = {};
|
|
761
|
+
const match = this.find(req.method, ctx.path);
|
|
762
|
+
if (match) {
|
|
763
|
+
ctx.params = match.params;
|
|
764
|
+
try {
|
|
765
|
+
result = await match.handler(ctx);
|
|
766
|
+
} catch (err) {
|
|
767
|
+
console.error(err);
|
|
768
|
+
status = err.status || err.statusCode || 500;
|
|
769
|
+
result = { error: err.message || "Internal Server Error" };
|
|
770
|
+
if (err.errors) result.errors = err.errors;
|
|
771
|
+
}
|
|
772
|
+
} else {
|
|
773
|
+
status = 404;
|
|
774
|
+
result = "Not Found";
|
|
775
|
+
}
|
|
776
|
+
if (result instanceof Response) {
|
|
777
|
+
status = result.status;
|
|
778
|
+
result.headers.forEach((v, k) => headers[k] = v);
|
|
779
|
+
if (headers["content-type"]?.includes("application/json")) {
|
|
780
|
+
result = await result.json();
|
|
781
|
+
} else {
|
|
782
|
+
result = await result.text();
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return {
|
|
786
|
+
status,
|
|
787
|
+
headers,
|
|
788
|
+
data: result
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Find a route matching the given method and path.
|
|
793
|
+
* @param method HTTP method
|
|
794
|
+
* @param path Request path
|
|
795
|
+
* @returns Route handler and parameters if found, otherwise null
|
|
796
|
+
*/
|
|
797
|
+
find(method, path2) {
|
|
798
|
+
for (const route of this.routes) {
|
|
799
|
+
if (route.method !== "ALL" && route.method !== method) continue;
|
|
800
|
+
const match = route.regex.exec(path2);
|
|
801
|
+
if (match) {
|
|
802
|
+
const params = {};
|
|
803
|
+
route.keys.forEach((key, index) => {
|
|
804
|
+
params[key] = match[index + 1];
|
|
805
|
+
});
|
|
806
|
+
return { handler: route.handler, params };
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
for (const child of this[$childRouters]) {
|
|
810
|
+
const prefix = child[$mountPath];
|
|
811
|
+
if (path2 === prefix || path2.startsWith(prefix + "/")) {
|
|
812
|
+
const subPath = path2.slice(prefix.length) || "/";
|
|
813
|
+
const match = child.find(method, subPath);
|
|
814
|
+
if (match) return match;
|
|
815
|
+
}
|
|
816
|
+
if (prefix.endsWith("/")) {
|
|
817
|
+
if (path2.startsWith(prefix)) {
|
|
818
|
+
const subPath = path2.slice(prefix.length) || "/";
|
|
819
|
+
const match = child.find(method, subPath);
|
|
820
|
+
if (match) return match;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return null;
|
|
825
|
+
}
|
|
826
|
+
parsePath(path2) {
|
|
827
|
+
const keys = [];
|
|
828
|
+
const pattern = path2.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => {
|
|
829
|
+
keys.push(key);
|
|
830
|
+
return "([^/]+)";
|
|
831
|
+
}).replace(/\*/g, ".*");
|
|
832
|
+
return {
|
|
833
|
+
regex: new RegExp(`^${pattern}$`),
|
|
834
|
+
keys
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
// --- Functional Routing ---
|
|
838
|
+
/**
|
|
839
|
+
* Adds a route to the router.
|
|
840
|
+
*
|
|
841
|
+
* @param method - HTTP method
|
|
842
|
+
* @param path - URL path
|
|
843
|
+
* @param spec - OpenAPI specification for the route
|
|
844
|
+
* @param handler - Route handler function
|
|
845
|
+
*/
|
|
846
|
+
add({ method, path: path2, spec, handler, regex: customRegex, group }) {
|
|
847
|
+
const { regex, keys } = customRegex ? { regex: customRegex, keys: [] } : this.parsePath(path2);
|
|
848
|
+
let wrappedHandler = handler;
|
|
849
|
+
const routeGuards = [...this.currentGuards];
|
|
850
|
+
if (routeGuards.length > 0) {
|
|
851
|
+
wrappedHandler = async (ctx) => {
|
|
852
|
+
for (const guard of routeGuards) {
|
|
853
|
+
let guardPassed = false;
|
|
854
|
+
let nextCalled = false;
|
|
855
|
+
const next = () => {
|
|
856
|
+
nextCalled = true;
|
|
857
|
+
return Promise.resolve();
|
|
858
|
+
};
|
|
859
|
+
try {
|
|
860
|
+
const result = await guard.handler(ctx, next);
|
|
861
|
+
if (result === true || nextCalled) {
|
|
862
|
+
guardPassed = true;
|
|
863
|
+
} else if (result !== void 0 && result !== null && result !== false) {
|
|
864
|
+
return result;
|
|
865
|
+
} else {
|
|
866
|
+
return ctx.json({ error: "Forbidden" }, 403);
|
|
867
|
+
}
|
|
868
|
+
} catch (error) {
|
|
869
|
+
throw error;
|
|
870
|
+
}
|
|
871
|
+
if (!guardPassed) {
|
|
872
|
+
return ctx.json({ error: "Forbidden" }, 403);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
return handler(ctx);
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
this.routes.push({
|
|
879
|
+
method,
|
|
880
|
+
path: path2,
|
|
881
|
+
regex,
|
|
882
|
+
keys,
|
|
883
|
+
handler: wrappedHandler,
|
|
884
|
+
handlerSpec: spec,
|
|
885
|
+
group,
|
|
886
|
+
guards: routeGuards.length > 0 ? routeGuards : void 0
|
|
887
|
+
});
|
|
888
|
+
return this;
|
|
889
|
+
}
|
|
890
|
+
get(path2, ...args) {
|
|
891
|
+
this.attachVerb("GET", path2, ...args);
|
|
892
|
+
return this;
|
|
893
|
+
}
|
|
894
|
+
post(path2, ...args) {
|
|
895
|
+
this.attachVerb("POST", path2, ...args);
|
|
896
|
+
return this;
|
|
897
|
+
}
|
|
898
|
+
put(path2, ...args) {
|
|
899
|
+
this.attachVerb("PUT", path2, ...args);
|
|
900
|
+
return this;
|
|
901
|
+
}
|
|
902
|
+
delete(path2, ...args) {
|
|
903
|
+
this.attachVerb("DELETE", path2, ...args);
|
|
904
|
+
return this;
|
|
905
|
+
}
|
|
906
|
+
patch(path2, ...args) {
|
|
907
|
+
this.attachVerb("PATCH", path2, ...args);
|
|
908
|
+
return this;
|
|
909
|
+
}
|
|
910
|
+
options(path2, ...args) {
|
|
911
|
+
this.attachVerb("OPTIONS", path2, ...args);
|
|
912
|
+
return this;
|
|
913
|
+
}
|
|
914
|
+
head(path2, ...args) {
|
|
915
|
+
this.attachVerb("HEAD", path2, ...args);
|
|
916
|
+
return this;
|
|
917
|
+
}
|
|
918
|
+
guard(specOrHandler, handler) {
|
|
919
|
+
const spec = typeof specOrHandler === "function" ? void 0 : specOrHandler;
|
|
920
|
+
const guardHandler = typeof specOrHandler === "function" ? specOrHandler : handler;
|
|
921
|
+
this.currentGuards.push({ handler: guardHandler, spec });
|
|
922
|
+
return this;
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Statically serves a directory with standard options.
|
|
926
|
+
* @param uriPath URL path prefix
|
|
927
|
+
* @param options Configuration options or root directory string
|
|
928
|
+
*/
|
|
929
|
+
static(uriPath, options) {
|
|
930
|
+
const config = typeof options === "string" ? { root: options } : options;
|
|
931
|
+
const rootPath = path.resolve(config.root || ".");
|
|
932
|
+
const prefix = uriPath.startsWith("/") ? uriPath : "/" + uriPath;
|
|
933
|
+
const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
|
|
934
|
+
const handler = async (ctx) => {
|
|
935
|
+
let relative = ctx.path.slice(normalizedPrefix.length);
|
|
936
|
+
if (!relative.startsWith("/") && relative.length > 0) relative = "/" + relative;
|
|
937
|
+
if (relative.length === 0) relative = "/";
|
|
938
|
+
relative = decodeURIComponent(relative);
|
|
939
|
+
const requestPath = path.join(rootPath, relative);
|
|
940
|
+
if (!requestPath.startsWith(rootPath)) {
|
|
941
|
+
return ctx.json({ error: "Forbidden" }, 403);
|
|
942
|
+
}
|
|
943
|
+
if (requestPath.includes("\0")) {
|
|
944
|
+
return ctx.json({ error: "Forbidden" }, 403);
|
|
945
|
+
}
|
|
946
|
+
if (config.hooks?.onRequest) {
|
|
947
|
+
const res = await config.hooks.onRequest(ctx);
|
|
948
|
+
if (res) return res;
|
|
949
|
+
}
|
|
950
|
+
if (config.exclude) {
|
|
951
|
+
for (const pattern2 of config.exclude) {
|
|
952
|
+
if (pattern2 instanceof RegExp) {
|
|
953
|
+
if (pattern2.test(relative)) return ctx.json({ error: "Forbidden" }, 403);
|
|
954
|
+
} else if (typeof pattern2 === "string") {
|
|
955
|
+
if (relative.includes(pattern2)) return ctx.json({ error: "Forbidden" }, 403);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
if (path.basename(requestPath).startsWith(".")) {
|
|
960
|
+
const behavior = config.dotfiles || "ignore";
|
|
961
|
+
if (behavior === "deny") return ctx.json({ error: "Forbidden" }, 403);
|
|
962
|
+
if (behavior === "ignore") return ctx.json({ error: "Not Found" }, 404);
|
|
963
|
+
}
|
|
964
|
+
let finalPath = requestPath;
|
|
965
|
+
let stats;
|
|
966
|
+
try {
|
|
967
|
+
stats = await promises.stat(requestPath);
|
|
968
|
+
} catch (e) {
|
|
969
|
+
if (config.extensions) {
|
|
970
|
+
for (const ext of config.extensions) {
|
|
971
|
+
const p = requestPath + (ext.startsWith(".") ? ext : "." + ext);
|
|
972
|
+
try {
|
|
973
|
+
const s = await promises.stat(p);
|
|
974
|
+
if (s.isFile()) {
|
|
975
|
+
finalPath = p;
|
|
976
|
+
stats = s;
|
|
977
|
+
break;
|
|
978
|
+
}
|
|
979
|
+
} catch {
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
if (!stats) return ctx.json({ error: "Not Found" }, 404);
|
|
984
|
+
}
|
|
985
|
+
if (stats.isDirectory()) {
|
|
986
|
+
if (!ctx.path.endsWith("/")) {
|
|
987
|
+
const query = ctx.url.search;
|
|
988
|
+
return ctx.redirect(ctx.path + "/" + query, 302);
|
|
989
|
+
}
|
|
990
|
+
let indexes = [];
|
|
991
|
+
if (config.index === void 0) {
|
|
992
|
+
indexes = ["index.html", "index.htm"];
|
|
993
|
+
} else if (Array.isArray(config.index)) {
|
|
994
|
+
indexes = config.index;
|
|
995
|
+
} else if (config.index) {
|
|
996
|
+
indexes = [config.index];
|
|
997
|
+
}
|
|
998
|
+
let foundIndex = false;
|
|
999
|
+
for (const idx of indexes) {
|
|
1000
|
+
const idxPath = path.join(finalPath, idx);
|
|
1001
|
+
try {
|
|
1002
|
+
const idxStats = await promises.stat(idxPath);
|
|
1003
|
+
if (idxStats.isFile()) {
|
|
1004
|
+
finalPath = idxPath;
|
|
1005
|
+
foundIndex = true;
|
|
1006
|
+
break;
|
|
1007
|
+
}
|
|
1008
|
+
} catch {
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
if (!foundIndex) {
|
|
1012
|
+
if (config.listDirectory) {
|
|
1013
|
+
try {
|
|
1014
|
+
const files = await promises.readdir(requestPath);
|
|
1015
|
+
const listing = eta$1.renderString(`
|
|
1016
|
+
<!DOCTYPE html>
|
|
1017
|
+
<html>
|
|
1018
|
+
<head>
|
|
1019
|
+
<title>Index of <%= it.relative %></title>
|
|
1020
|
+
<style>
|
|
1021
|
+
body { font-family: system-ui, -apple-system, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
|
|
1022
|
+
ul { list-style: none; padding: 0; }
|
|
1023
|
+
li { padding: 0.5rem 0; border-bottom: 1px solid #eee; }
|
|
1024
|
+
a { text-decoration: none; color: #0066cc; }
|
|
1025
|
+
a:hover { text-decoration: underline; }
|
|
1026
|
+
h1 { font-size: 1.5rem; margin-bottom: 1rem; }
|
|
1027
|
+
</style>
|
|
1028
|
+
</head>
|
|
1029
|
+
<body>
|
|
1030
|
+
<h1>Index of <%= it.relative %></h1>
|
|
1031
|
+
<ul>
|
|
1032
|
+
<% if (it.relative !== '/') { %>
|
|
1033
|
+
<li><a href="../">../</a></li>
|
|
1034
|
+
<% } %>
|
|
1035
|
+
<% it.files.forEach(function(f) { %>
|
|
1036
|
+
<li><a href="<%= f %>"><%= f %></a></li>
|
|
1037
|
+
<% }) %>
|
|
1038
|
+
</ul>
|
|
1039
|
+
</body>
|
|
1040
|
+
</html>
|
|
1041
|
+
`, { relative, files, join: path.join });
|
|
1042
|
+
return new Response(listing, { headers: { "Content-Type": "text/html" } });
|
|
1043
|
+
} catch (e) {
|
|
1044
|
+
return ctx.json({ error: "Internal Server Error" }, 500);
|
|
1045
|
+
}
|
|
1046
|
+
} else {
|
|
1047
|
+
return ctx.json({ error: "Forbidden" }, 403);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
const file = Bun.file(finalPath);
|
|
1052
|
+
let response = new Response(file);
|
|
1053
|
+
if (config.hooks?.onResponse) {
|
|
1054
|
+
const hooked = await config.hooks.onResponse(ctx, response);
|
|
1055
|
+
if (hooked) response = hooked;
|
|
1056
|
+
}
|
|
1057
|
+
return response;
|
|
1058
|
+
};
|
|
1059
|
+
let groupName = "Static";
|
|
1060
|
+
const segments = normalizedPrefix.split("/").filter(Boolean);
|
|
1061
|
+
if (segments.length > 0) {
|
|
1062
|
+
const last = segments[segments.length - 1];
|
|
1063
|
+
groupName = last.charAt(0).toUpperCase() + last.slice(1);
|
|
1064
|
+
}
|
|
1065
|
+
const defaultSpec = {
|
|
1066
|
+
summary: "Static Content",
|
|
1067
|
+
description: "Serves static files from " + normalizedPrefix,
|
|
1068
|
+
tags: [groupName]
|
|
1069
|
+
};
|
|
1070
|
+
const spec = config.openapi ? config.openapi : defaultSpec;
|
|
1071
|
+
if (!spec.tags) spec.tags = [groupName];
|
|
1072
|
+
else if (!spec.tags.includes(groupName)) spec.tags.push(groupName);
|
|
1073
|
+
const pattern = `^${normalizedPrefix}(/.*)?$`;
|
|
1074
|
+
const regex = new RegExp(pattern);
|
|
1075
|
+
const displayPath = normalizedPrefix === "/" ? "/*" : normalizedPrefix + "/*";
|
|
1076
|
+
this.add({ method: "GET", path: displayPath, handler, spec, regex });
|
|
1077
|
+
this.add({ method: "HEAD", path: displayPath, handler, spec, regex });
|
|
1078
|
+
return this;
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Attach the verb routes with their overload signatures.
|
|
1082
|
+
* Use compose to handle multiple handlers (middleware).
|
|
1083
|
+
*/
|
|
1084
|
+
attachVerb(method, path2, ...args) {
|
|
1085
|
+
let spec;
|
|
1086
|
+
let handlers = [];
|
|
1087
|
+
if (args.length > 0) {
|
|
1088
|
+
if (typeof args[0] === "object" && args[0] !== null) {
|
|
1089
|
+
spec = args[0];
|
|
1090
|
+
handlers = args.slice(1);
|
|
1091
|
+
} else {
|
|
1092
|
+
handlers = args;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
if (handlers.length === 0) {
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
let finalHandler = handlers[handlers.length - 1];
|
|
1099
|
+
if (handlers.length > 1) {
|
|
1100
|
+
const fn = compose(handlers);
|
|
1101
|
+
finalHandler = (ctx) => fn(ctx);
|
|
1102
|
+
}
|
|
1103
|
+
this.add({
|
|
1104
|
+
method,
|
|
1105
|
+
path: path2,
|
|
1106
|
+
spec,
|
|
1107
|
+
handler: finalHandler
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Generates an OpenAPI 3.1 Document by recursing through the router and its descendants.
|
|
1112
|
+
*/
|
|
1113
|
+
generateApiSpec(options = {}) {
|
|
1114
|
+
const paths = {};
|
|
1115
|
+
const tagGroups = /* @__PURE__ */ new Map();
|
|
1116
|
+
const defaultTagGroup = options.defaultTagGroup || "General";
|
|
1117
|
+
const defaultTagName = options.defaultTag || "Application";
|
|
1118
|
+
const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName) => {
|
|
1119
|
+
let group = currentGroup;
|
|
1120
|
+
let tag = defaultTag;
|
|
1121
|
+
if (router.config?.group) {
|
|
1122
|
+
group = router.config.group;
|
|
1123
|
+
}
|
|
1124
|
+
if (router.config?.name) {
|
|
1125
|
+
tag = router.config.name;
|
|
1126
|
+
} else {
|
|
1127
|
+
const mountPath = router[$mountPath];
|
|
1128
|
+
if (mountPath && mountPath !== "/") {
|
|
1129
|
+
const segments = mountPath.split("/").filter(Boolean);
|
|
1130
|
+
if (segments.length > 0) {
|
|
1131
|
+
const lastSegment = segments[segments.length - 1];
|
|
1132
|
+
const humanized = lastSegment.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1133
|
+
tag = humanized;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
if (!tagGroups.has(group)) {
|
|
1138
|
+
tagGroups.set(group, /* @__PURE__ */ new Set());
|
|
1139
|
+
}
|
|
1140
|
+
for (const route of router.routes) {
|
|
1141
|
+
const routeGroup = route.group || group;
|
|
1142
|
+
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1143
|
+
const cleanSubPath = route.path.startsWith("/") ? route.path : "/" + route.path;
|
|
1144
|
+
let fullPath = cleanPrefix + cleanSubPath || "/";
|
|
1145
|
+
fullPath = fullPath.replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
|
|
1146
|
+
if (!paths[fullPath]) {
|
|
1147
|
+
paths[fullPath] = {};
|
|
1148
|
+
}
|
|
1149
|
+
const operation = {
|
|
1150
|
+
responses: {
|
|
1151
|
+
200: { description: "OK" }
|
|
1152
|
+
}
|
|
1153
|
+
};
|
|
1154
|
+
if (route.keys.length > 0) {
|
|
1155
|
+
operation.parameters = route.keys.map((key) => ({
|
|
1156
|
+
name: key,
|
|
1157
|
+
in: "path",
|
|
1158
|
+
required: true,
|
|
1159
|
+
schema: { type: "string" }
|
|
1160
|
+
}));
|
|
1161
|
+
}
|
|
1162
|
+
if (route.guards) {
|
|
1163
|
+
for (const guard of route.guards) {
|
|
1164
|
+
if (guard.spec) {
|
|
1165
|
+
deepMerge(operation, guard.spec);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
if (route.handlerSpec) {
|
|
1170
|
+
deepMerge(operation, route.handlerSpec);
|
|
1171
|
+
}
|
|
1172
|
+
if (!operation.tags || operation.tags.length === 0) {
|
|
1173
|
+
operation.tags = [tag];
|
|
1174
|
+
}
|
|
1175
|
+
if (operation.tags) {
|
|
1176
|
+
operation.tags = Array.from(new Set(operation.tags));
|
|
1177
|
+
for (const t of operation.tags) {
|
|
1178
|
+
if (!tagGroups.has(routeGroup)) {
|
|
1179
|
+
tagGroups.set(routeGroup, /* @__PURE__ */ new Set());
|
|
1180
|
+
}
|
|
1181
|
+
tagGroups.get(routeGroup)?.add(t);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
const methodLower = route.method.toLowerCase();
|
|
1185
|
+
if (methodLower === "all") {
|
|
1186
|
+
["get", "post", "put", "delete", "patch"].forEach((m) => {
|
|
1187
|
+
if (!paths[fullPath][m]) {
|
|
1188
|
+
paths[fullPath][m] = { ...operation };
|
|
1189
|
+
}
|
|
1190
|
+
});
|
|
1191
|
+
} else {
|
|
1192
|
+
paths[fullPath][methodLower] = operation;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
for (const controller of router[$childControllers]) {
|
|
1196
|
+
const mountPath = controller[$mountPath] || "";
|
|
1197
|
+
prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1198
|
+
mountPath.startsWith("/") ? mountPath : "/" + mountPath;
|
|
1199
|
+
const controllerName = controller.constructor.name || "UnknownController";
|
|
1200
|
+
tagGroups.get(group)?.add(controllerName);
|
|
1201
|
+
}
|
|
1202
|
+
for (const child of router[$childRouters]) {
|
|
1203
|
+
const mountPath = child[$mountPath];
|
|
1204
|
+
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1205
|
+
const cleanMount = mountPath.startsWith("/") ? mountPath : "/" + mountPath;
|
|
1206
|
+
const nextPrefix = cleanPrefix + cleanMount || "/";
|
|
1207
|
+
collect(child, nextPrefix, group, tag);
|
|
1208
|
+
}
|
|
1209
|
+
};
|
|
1210
|
+
collect(this);
|
|
1211
|
+
const xTagGroups = [];
|
|
1212
|
+
for (const [name, tags] of tagGroups) {
|
|
1213
|
+
xTagGroups.push({
|
|
1214
|
+
name,
|
|
1215
|
+
tags: Array.from(tags).sort()
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
return {
|
|
1219
|
+
openapi: "3.1.0",
|
|
1220
|
+
info: {
|
|
1221
|
+
title: "Shokupan API",
|
|
1222
|
+
version: "1.0.0",
|
|
1223
|
+
...options.info
|
|
1224
|
+
},
|
|
1225
|
+
paths,
|
|
1226
|
+
components: options.components,
|
|
1227
|
+
servers: options.servers,
|
|
1228
|
+
tags: options.tags,
|
|
1229
|
+
externalDocs: options.externalDocs,
|
|
1230
|
+
"x-tagGroups": xTagGroups
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
const defaults = {
|
|
1235
|
+
port: 3e3,
|
|
1236
|
+
hostname: "localhost",
|
|
1237
|
+
development: process.env.NODE_ENV !== "production",
|
|
1238
|
+
enableAsyncLocalStorage: false
|
|
1239
|
+
};
|
|
1240
|
+
api.trace.getTracer("shokupan.application");
|
|
1241
|
+
class Shokupan extends ShokupanRouter {
|
|
1242
|
+
applicationConfig = {};
|
|
1243
|
+
middleware = [];
|
|
1244
|
+
get logger() {
|
|
1245
|
+
return this.applicationConfig.logger;
|
|
1246
|
+
}
|
|
1247
|
+
constructor(applicationConfig = {}) {
|
|
1248
|
+
super();
|
|
1249
|
+
this[$isApplication] = true;
|
|
1250
|
+
this[$appRoot] = this;
|
|
1251
|
+
Object.assign(this.applicationConfig, defaults, applicationConfig);
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* Adds middleware to the application.
|
|
1255
|
+
*/
|
|
1256
|
+
use(middleware) {
|
|
1257
|
+
this.middleware.push(middleware);
|
|
1258
|
+
return this;
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Starts the application server.
|
|
1262
|
+
*
|
|
1263
|
+
* @param port - The port to listen on. If not specified, the port from the configuration is used. If that is not specified, port 3000 is used.
|
|
1264
|
+
* @returns The server instance.
|
|
1265
|
+
*/
|
|
1266
|
+
listen(port) {
|
|
1267
|
+
const finalPort = port ?? this.applicationConfig.port ?? 3e3;
|
|
1268
|
+
if (finalPort < 0 || finalPort > 65535) {
|
|
1269
|
+
throw new Error("Invalid port number");
|
|
1270
|
+
}
|
|
1271
|
+
if (port === 0 && process.platform === "linux") ;
|
|
1272
|
+
const server = Bun.serve({
|
|
1273
|
+
port: finalPort,
|
|
1274
|
+
hostname: this.applicationConfig.hostname,
|
|
1275
|
+
development: this.applicationConfig.development,
|
|
1276
|
+
fetch: this.fetch.bind(this)
|
|
1277
|
+
});
|
|
1278
|
+
console.log(`Shokupan server listening on http://${server.hostname}:${server.port}`);
|
|
1279
|
+
return server;
|
|
1280
|
+
}
|
|
1281
|
+
[$dispatch](req) {
|
|
1282
|
+
return this.fetch(req);
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Processes a request by wrapping the standard fetch method.
|
|
1286
|
+
*/
|
|
1287
|
+
async processRequest(options) {
|
|
1288
|
+
let url = options.url || options.path || "/";
|
|
1289
|
+
if (!url.startsWith("http")) {
|
|
1290
|
+
const base = `http://${this.applicationConfig.hostname || "localhost"}:${this.applicationConfig.port || 3e3}`;
|
|
1291
|
+
const path2 = url.startsWith("/") ? url : "/" + url;
|
|
1292
|
+
url = base + path2;
|
|
1293
|
+
}
|
|
1294
|
+
if (options.query) {
|
|
1295
|
+
const u = new URL(url);
|
|
1296
|
+
for (const [k, v] of Object.entries(options.query)) {
|
|
1297
|
+
u.searchParams.set(k, v);
|
|
1298
|
+
}
|
|
1299
|
+
url = u.toString();
|
|
1300
|
+
}
|
|
1301
|
+
const req = new ShokupanRequest({
|
|
1302
|
+
method: options.method || "GET",
|
|
1303
|
+
url,
|
|
1304
|
+
headers: options.headers,
|
|
1305
|
+
body: options.body && typeof options.body === "object" ? JSON.stringify(options.body) : options.body
|
|
1306
|
+
});
|
|
1307
|
+
const res = await this.fetch(req);
|
|
1308
|
+
const status = res.status;
|
|
1309
|
+
const headers = {};
|
|
1310
|
+
res.headers.forEach((v, k) => headers[k] = v);
|
|
1311
|
+
let data;
|
|
1312
|
+
if (headers["content-type"]?.includes("application/json")) {
|
|
1313
|
+
data = await res.json();
|
|
1314
|
+
} else {
|
|
1315
|
+
data = await res.text();
|
|
1316
|
+
}
|
|
1317
|
+
return {
|
|
1318
|
+
status,
|
|
1319
|
+
headers,
|
|
1320
|
+
data
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* Handles an incoming request (Bun.serve interface).
|
|
1325
|
+
* This logic contains the middleware chain and router dispatch.
|
|
1326
|
+
*
|
|
1327
|
+
* @param req - The request to handle.
|
|
1328
|
+
* @returns The response to send.
|
|
1329
|
+
*/
|
|
1330
|
+
async fetch(req) {
|
|
1331
|
+
const tracer2 = api.trace.getTracer("shokupan.application");
|
|
1332
|
+
const store = asyncContext.getStore();
|
|
1333
|
+
const attrs = {
|
|
1334
|
+
attributes: {
|
|
1335
|
+
"http.url": req.url,
|
|
1336
|
+
"http.method": req.method
|
|
1337
|
+
}
|
|
1338
|
+
};
|
|
1339
|
+
const parent = store?.get("span");
|
|
1340
|
+
const ctx = parent ? api.trace.setSpan(api.context.active(), parent) : void 0;
|
|
1341
|
+
return tracer2.startActiveSpan(`${req.method} ${new URL(req.url).pathname}`, attrs, ctx, (span) => {
|
|
1342
|
+
const ctxMap = /* @__PURE__ */ new Map();
|
|
1343
|
+
ctxMap.set("span", span);
|
|
1344
|
+
ctxMap.set("request", req);
|
|
1345
|
+
const runCallback = () => {
|
|
1346
|
+
const request = req;
|
|
1347
|
+
const handle = async () => {
|
|
1348
|
+
const ctx2 = new ShokupanContext(request);
|
|
1349
|
+
const fn = compose(this.middleware);
|
|
1350
|
+
try {
|
|
1351
|
+
const result = await fn(ctx2, async () => {
|
|
1352
|
+
const match = this.find(req.method, ctx2.path);
|
|
1353
|
+
if (match) {
|
|
1354
|
+
ctx2.params = match.params;
|
|
1355
|
+
return match.handler(ctx2);
|
|
1356
|
+
}
|
|
1357
|
+
return null;
|
|
1358
|
+
});
|
|
1359
|
+
if (result instanceof Response) {
|
|
1360
|
+
return result;
|
|
1361
|
+
}
|
|
1362
|
+
if (result === null || result === void 0) {
|
|
1363
|
+
span.setAttribute("http.status_code", 404);
|
|
1364
|
+
return ctx2.text("Not Found", 404);
|
|
1365
|
+
}
|
|
1366
|
+
if (typeof result === "object") {
|
|
1367
|
+
return ctx2.json(result);
|
|
1368
|
+
}
|
|
1369
|
+
return ctx2.text(String(result));
|
|
1370
|
+
} catch (err) {
|
|
1371
|
+
console.error(err);
|
|
1372
|
+
span.recordException(err);
|
|
1373
|
+
span.setStatus({ code: 2 });
|
|
1374
|
+
const status = err.status || err.statusCode || 500;
|
|
1375
|
+
const body = { error: err.message || "Internal Server Error" };
|
|
1376
|
+
if (err.errors) body.errors = err.errors;
|
|
1377
|
+
return ctx2.json(body, status);
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
return handle().finally(() => span.end());
|
|
1381
|
+
};
|
|
1382
|
+
if (this.applicationConfig.enableAsyncLocalStorage) {
|
|
1383
|
+
return asyncContext.run(ctxMap, runCallback);
|
|
1384
|
+
} else {
|
|
1385
|
+
return runCallback();
|
|
1386
|
+
}
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
class AuthPlugin extends ShokupanRouter {
|
|
1391
|
+
constructor(authConfig) {
|
|
1392
|
+
super();
|
|
1393
|
+
this.authConfig = authConfig;
|
|
1394
|
+
this.secret = typeof authConfig.jwtSecret === "string" ? new TextEncoder().encode(authConfig.jwtSecret) : authConfig.jwtSecret;
|
|
1395
|
+
this.init();
|
|
1396
|
+
}
|
|
1397
|
+
secret;
|
|
1398
|
+
getProviderInstance(name, p) {
|
|
1399
|
+
switch (name) {
|
|
1400
|
+
case "github":
|
|
1401
|
+
return new arctic.GitHub(p.clientId, p.clientSecret, p.redirectUri);
|
|
1402
|
+
case "google":
|
|
1403
|
+
return new arctic.Google(p.clientId, p.clientSecret, p.redirectUri);
|
|
1404
|
+
case "microsoft":
|
|
1405
|
+
return new arctic.MicrosoftEntraId(p.tenantId, p.clientId, p.clientSecret, p.redirectUri);
|
|
1406
|
+
case "apple":
|
|
1407
|
+
return new arctic.Apple(
|
|
1408
|
+
p.clientId,
|
|
1409
|
+
p.teamId,
|
|
1410
|
+
p.keyId,
|
|
1411
|
+
p.clientSecret,
|
|
1412
|
+
p.redirectUri
|
|
1413
|
+
);
|
|
1414
|
+
case "auth0":
|
|
1415
|
+
return new arctic.Auth0(p.domain, p.clientId, p.clientSecret, p.redirectUri);
|
|
1416
|
+
case "okta":
|
|
1417
|
+
return new arctic.Okta(p.domain, p.authUrl, p.clientId, p.clientSecret, p.redirectUri);
|
|
1418
|
+
case "oauth2":
|
|
1419
|
+
return new arctic.OAuth2Client(p.clientId, p.clientSecret, p.redirectUri);
|
|
1420
|
+
default:
|
|
1421
|
+
return null;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
async createSession(user, ctx) {
|
|
1425
|
+
const alg = "HS256";
|
|
1426
|
+
const jwt = await new jose__namespace.SignJWT({ ...user }).setProtectedHeader({ alg }).setIssuedAt().setExpirationTime(this.authConfig.jwtExpiration || "24h").sign(this.secret);
|
|
1427
|
+
const opts = this.authConfig.cookieOptions || {};
|
|
1428
|
+
let cookie = `auth_token=${jwt}; Path=${opts.path || "/"}; HttpOnly`;
|
|
1429
|
+
if (opts.secure) cookie += "; Secure";
|
|
1430
|
+
if (opts.sameSite) cookie += `; SameSite=${opts.sameSite}`;
|
|
1431
|
+
if (opts.maxAge) cookie += `; Max-Age=${opts.maxAge}`;
|
|
1432
|
+
ctx.set("Set-Cookie", cookie);
|
|
1433
|
+
return jwt;
|
|
1434
|
+
}
|
|
1435
|
+
init() {
|
|
1436
|
+
for (const [providerName, providerConfig] of Object.entries(this.authConfig.providers)) {
|
|
1437
|
+
if (!providerConfig) continue;
|
|
1438
|
+
const provider2 = this.getProviderInstance(providerName, providerConfig);
|
|
1439
|
+
if (!provider2) {
|
|
1440
|
+
continue;
|
|
1441
|
+
}
|
|
1442
|
+
this.get(`/auth/${providerName}/login`, async (ctx) => {
|
|
1443
|
+
const state = arctic.generateState();
|
|
1444
|
+
const codeVerifier = providerName === "google" || providerName === "microsoft" || providerName === "auth0" || providerName === "okta" ? arctic.generateCodeVerifier() : void 0;
|
|
1445
|
+
const scopes = providerConfig.scopes || [];
|
|
1446
|
+
let url;
|
|
1447
|
+
if (provider2 instanceof arctic.GitHub) {
|
|
1448
|
+
url = await provider2.createAuthorizationURL(state, scopes);
|
|
1449
|
+
} else if (provider2 instanceof arctic.Google || provider2 instanceof arctic.MicrosoftEntraId || provider2 instanceof arctic.Auth0 || provider2 instanceof arctic.Okta) {
|
|
1450
|
+
url = await provider2.createAuthorizationURL(state, codeVerifier, scopes);
|
|
1451
|
+
} else if (provider2 instanceof arctic.Apple) {
|
|
1452
|
+
url = await provider2.createAuthorizationURL(state, scopes);
|
|
1453
|
+
} else if (provider2 instanceof arctic.OAuth2Client) {
|
|
1454
|
+
if (!providerConfig.authUrl) return ctx.text("Config error: authUrl required for oauth2", 500);
|
|
1455
|
+
url = await provider2.createAuthorizationURL(providerConfig.authUrl, state, scopes);
|
|
1456
|
+
} else {
|
|
1457
|
+
return ctx.text("Provider config error", 500);
|
|
1458
|
+
}
|
|
1459
|
+
ctx.res.headers.set("Set-Cookie", `oauth_state=${state}; Path=/; HttpOnly; Max-Age=600`);
|
|
1460
|
+
if (codeVerifier) {
|
|
1461
|
+
ctx.res.headers.append("Set-Cookie", `oauth_verifier=${codeVerifier}; Path=/; HttpOnly; Max-Age=600`);
|
|
1462
|
+
}
|
|
1463
|
+
return ctx.redirect(url.toString());
|
|
1464
|
+
});
|
|
1465
|
+
this.get(`/auth/${providerName}/callback`, async (ctx) => {
|
|
1466
|
+
const url = new URL(ctx.req.url);
|
|
1467
|
+
const code = url.searchParams.get("code");
|
|
1468
|
+
const state = url.searchParams.get("state");
|
|
1469
|
+
const cookieHeader = ctx.req.headers.get("Cookie");
|
|
1470
|
+
const storedState = cookieHeader?.match(/oauth_state=([^;]+)/)?.[1];
|
|
1471
|
+
const storedVerifier = cookieHeader?.match(/oauth_verifier=([^;]+)/)?.[1];
|
|
1472
|
+
if (!code || !state || !storedState || state !== storedState) {
|
|
1473
|
+
return ctx.text("Invalid state or code", 400);
|
|
1474
|
+
}
|
|
1475
|
+
try {
|
|
1476
|
+
let tokens;
|
|
1477
|
+
let idToken;
|
|
1478
|
+
if (provider2 instanceof arctic.GitHub) {
|
|
1479
|
+
tokens = await provider2.validateAuthorizationCode(code);
|
|
1480
|
+
} else if (provider2 instanceof arctic.Google || provider2 instanceof arctic.MicrosoftEntraId) {
|
|
1481
|
+
if (!storedVerifier) return ctx.text("Missing verifier", 400);
|
|
1482
|
+
tokens = await provider2.validateAuthorizationCode(code, storedVerifier);
|
|
1483
|
+
} else if (provider2 instanceof arctic.Auth0 || provider2 instanceof arctic.Okta) {
|
|
1484
|
+
tokens = await provider2.validateAuthorizationCode(code, storedVerifier || "");
|
|
1485
|
+
} else if (provider2 instanceof arctic.Apple) {
|
|
1486
|
+
tokens = await provider2.validateAuthorizationCode(code);
|
|
1487
|
+
idToken = tokens.idToken;
|
|
1488
|
+
} else if (provider2 instanceof arctic.OAuth2Client) {
|
|
1489
|
+
if (!providerConfig.tokenUrl) return ctx.text("Config error: tokenUrl required for oauth2", 500);
|
|
1490
|
+
tokens = await provider2.validateAuthorizationCode(providerConfig.tokenUrl, code, null);
|
|
1491
|
+
}
|
|
1492
|
+
const accessToken = tokens.accessToken || tokens.access_token;
|
|
1493
|
+
const user = await this.fetchUser(providerName, accessToken, providerConfig, idToken);
|
|
1494
|
+
if (this.authConfig.onSuccess) {
|
|
1495
|
+
const res = await this.authConfig.onSuccess(user, ctx);
|
|
1496
|
+
if (res) return res;
|
|
1497
|
+
}
|
|
1498
|
+
const jwt = await this.createSession(user, ctx);
|
|
1499
|
+
return ctx.json({ token: jwt, user });
|
|
1500
|
+
} catch (e) {
|
|
1501
|
+
console.error("Auth Error", e);
|
|
1502
|
+
return ctx.text("Authentication failed: " + e.message + "\n" + e.stack, 500);
|
|
1503
|
+
}
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
async fetchUser(provider2, token, config, idToken) {
|
|
1508
|
+
let user = { id: "unknown", provider: provider2 };
|
|
1509
|
+
if (provider2 === "github") {
|
|
1510
|
+
const res = await fetch("https://api.github.com/user", {
|
|
1511
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1512
|
+
});
|
|
1513
|
+
const data = await res.json();
|
|
1514
|
+
user = {
|
|
1515
|
+
id: String(data.id),
|
|
1516
|
+
name: data.name || data.login,
|
|
1517
|
+
email: data.email,
|
|
1518
|
+
picture: data.avatar_url,
|
|
1519
|
+
provider: provider2,
|
|
1520
|
+
raw: data
|
|
1521
|
+
};
|
|
1522
|
+
} else if (provider2 === "google") {
|
|
1523
|
+
const res = await fetch("https://openidconnect.googleapis.com/v1/userinfo", {
|
|
1524
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1525
|
+
});
|
|
1526
|
+
const data = await res.json();
|
|
1527
|
+
user = {
|
|
1528
|
+
id: data.sub,
|
|
1529
|
+
name: data.name,
|
|
1530
|
+
email: data.email,
|
|
1531
|
+
picture: data.picture,
|
|
1532
|
+
provider: provider2,
|
|
1533
|
+
raw: data
|
|
1534
|
+
};
|
|
1535
|
+
} else if (provider2 === "microsoft") {
|
|
1536
|
+
const res = await fetch("https://graph.microsoft.com/v1.0/me", {
|
|
1537
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1538
|
+
});
|
|
1539
|
+
const data = await res.json();
|
|
1540
|
+
user = {
|
|
1541
|
+
id: data.id,
|
|
1542
|
+
name: data.displayName,
|
|
1543
|
+
email: data.mail || data.userPrincipalName,
|
|
1544
|
+
provider: provider2,
|
|
1545
|
+
raw: data
|
|
1546
|
+
};
|
|
1547
|
+
} else if (provider2 === "auth0" || provider2 === "okta") {
|
|
1548
|
+
const domain = config.domain.startsWith("http") ? config.domain : `https://${config.domain}`;
|
|
1549
|
+
const endpoint = provider2 === "auth0" ? `${domain}/userinfo` : `${domain}/oauth2/v1/userinfo`;
|
|
1550
|
+
const res = await fetch(endpoint, {
|
|
1551
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1552
|
+
});
|
|
1553
|
+
const data = await res.json();
|
|
1554
|
+
user = {
|
|
1555
|
+
id: data.sub,
|
|
1556
|
+
name: data.name,
|
|
1557
|
+
email: data.email,
|
|
1558
|
+
picture: data.picture,
|
|
1559
|
+
provider: provider2,
|
|
1560
|
+
raw: data
|
|
1561
|
+
};
|
|
1562
|
+
} else if (provider2 === "apple") {
|
|
1563
|
+
if (idToken) {
|
|
1564
|
+
const payload = jose__namespace.decodeJwt(idToken);
|
|
1565
|
+
user = {
|
|
1566
|
+
id: payload.sub,
|
|
1567
|
+
email: payload["email"],
|
|
1568
|
+
provider: provider2,
|
|
1569
|
+
raw: payload
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
} else if (provider2 === "oauth2") {
|
|
1573
|
+
if (config.userInfoUrl) {
|
|
1574
|
+
const res = await fetch(config.userInfoUrl, {
|
|
1575
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1576
|
+
});
|
|
1577
|
+
const data = await res.json();
|
|
1578
|
+
user = {
|
|
1579
|
+
id: data.id || data.sub || "unknown",
|
|
1580
|
+
name: data.name,
|
|
1581
|
+
email: data.email,
|
|
1582
|
+
picture: data.picture,
|
|
1583
|
+
provider: provider2,
|
|
1584
|
+
raw: data
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
return user;
|
|
1589
|
+
}
|
|
1590
|
+
/**
|
|
1591
|
+
* Middleware to verify JWT
|
|
1592
|
+
*/
|
|
1593
|
+
middleware() {
|
|
1594
|
+
return async (ctx, next) => {
|
|
1595
|
+
const authHeader = ctx.req.headers.get("Authorization");
|
|
1596
|
+
let token = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null;
|
|
1597
|
+
if (!token) {
|
|
1598
|
+
const cookieHeader = ctx.req.headers.get("Cookie");
|
|
1599
|
+
token = cookieHeader?.match(/auth_token=([^;]+)/)?.[1] || null;
|
|
1600
|
+
}
|
|
1601
|
+
if (token) {
|
|
1602
|
+
try {
|
|
1603
|
+
const { payload } = await jose__namespace.jwtVerify(token, this.secret);
|
|
1604
|
+
ctx.user = payload;
|
|
1605
|
+
} catch {
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
return next();
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
function Compression(options = {}) {
|
|
1613
|
+
const threshold = options.threshold ?? 1024;
|
|
1614
|
+
return async (ctx, next) => {
|
|
1615
|
+
const acceptEncoding = ctx.headers.get("accept-encoding") || "";
|
|
1616
|
+
let method = null;
|
|
1617
|
+
if (acceptEncoding.includes("br")) method = "br";
|
|
1618
|
+
else if (acceptEncoding.includes("gzip")) method = "gzip";
|
|
1619
|
+
else if (acceptEncoding.includes("deflate")) method = "deflate";
|
|
1620
|
+
if (!method) return next();
|
|
1621
|
+
const response = await next();
|
|
1622
|
+
if (response instanceof Response) {
|
|
1623
|
+
if (response.headers.has("Content-Encoding")) return response;
|
|
1624
|
+
const body = await response.arrayBuffer();
|
|
1625
|
+
if (body.byteLength < threshold) {
|
|
1626
|
+
return new Response(body, {
|
|
1627
|
+
status: response.status,
|
|
1628
|
+
statusText: response.statusText,
|
|
1629
|
+
headers: response.headers
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
let compressed;
|
|
1633
|
+
if (method === "br") {
|
|
1634
|
+
compressed = require("node:zlib").brotliCompressSync(body);
|
|
1635
|
+
} else if (method === "gzip") {
|
|
1636
|
+
compressed = Bun.gzipSync(body);
|
|
1637
|
+
} else {
|
|
1638
|
+
compressed = Bun.deflateSync(body);
|
|
1639
|
+
}
|
|
1640
|
+
const headers = new Headers(response.headers);
|
|
1641
|
+
headers.set("Content-Encoding", method);
|
|
1642
|
+
headers.set("Content-Length", String(compressed.length));
|
|
1643
|
+
headers.delete("Content-Length");
|
|
1644
|
+
return new Response(compressed, {
|
|
1645
|
+
status: response.status,
|
|
1646
|
+
statusText: response.statusText,
|
|
1647
|
+
headers
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
return response;
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
function Cors(options = {}) {
|
|
1654
|
+
const defaults2 = {
|
|
1655
|
+
origin: "*",
|
|
1656
|
+
methods: "GET,HEAD,PUT,PATCH,POST,DELETE",
|
|
1657
|
+
preflightContinue: false,
|
|
1658
|
+
optionsSuccessStatus: 204
|
|
1659
|
+
};
|
|
1660
|
+
const opts = { ...defaults2, ...options };
|
|
1661
|
+
return async (ctx, next) => {
|
|
1662
|
+
const headers = new Headers();
|
|
1663
|
+
const origin = ctx.headers.get("origin");
|
|
1664
|
+
const set = (k, v) => headers.set(k, v);
|
|
1665
|
+
const append = (k, v) => headers.append(k, v);
|
|
1666
|
+
if (opts.origin === "*") {
|
|
1667
|
+
set("Access-Control-Allow-Origin", "*");
|
|
1668
|
+
} else if (typeof opts.origin === "string") {
|
|
1669
|
+
set("Access-Control-Allow-Origin", opts.origin);
|
|
1670
|
+
} else if (Array.isArray(opts.origin)) {
|
|
1671
|
+
if (origin && opts.origin.includes(origin)) {
|
|
1672
|
+
set("Access-Control-Allow-Origin", origin);
|
|
1673
|
+
append("Vary", "Origin");
|
|
1674
|
+
}
|
|
1675
|
+
} else if (typeof opts.origin === "function") {
|
|
1676
|
+
const allowed = opts.origin(ctx);
|
|
1677
|
+
if (allowed === true && origin) {
|
|
1678
|
+
set("Access-Control-Allow-Origin", origin);
|
|
1679
|
+
append("Vary", "Origin");
|
|
1680
|
+
} else if (typeof allowed === "string") {
|
|
1681
|
+
set("Access-Control-Allow-Origin", allowed);
|
|
1682
|
+
append("Vary", "Origin");
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
if (opts.credentials) {
|
|
1686
|
+
set("Access-Control-Allow-Credentials", "true");
|
|
1687
|
+
}
|
|
1688
|
+
if (opts.exposedHeaders) {
|
|
1689
|
+
const exposed = Array.isArray(opts.exposedHeaders) ? opts.exposedHeaders.join(",") : opts.exposedHeaders;
|
|
1690
|
+
if (exposed) set("Access-Control-Expose-Headers", exposed);
|
|
1691
|
+
}
|
|
1692
|
+
if (ctx.method === "OPTIONS") {
|
|
1693
|
+
if (opts.methods) {
|
|
1694
|
+
const methods = Array.isArray(opts.methods) ? opts.methods.join(",") : opts.methods;
|
|
1695
|
+
set("Access-Control-Allow-Methods", methods);
|
|
1696
|
+
}
|
|
1697
|
+
if (opts.allowedHeaders) {
|
|
1698
|
+
const h = Array.isArray(opts.allowedHeaders) ? opts.allowedHeaders.join(",") : opts.allowedHeaders;
|
|
1699
|
+
set("Access-Control-Allow-Headers", h);
|
|
1700
|
+
} else {
|
|
1701
|
+
const reqHeaders = ctx.headers.get("access-control-request-headers");
|
|
1702
|
+
if (reqHeaders) {
|
|
1703
|
+
set("Access-Control-Allow-Headers", reqHeaders);
|
|
1704
|
+
append("Vary", "Access-Control-Request-Headers");
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
if (opts.maxAge) {
|
|
1708
|
+
set("Access-Control-Max-Age", String(opts.maxAge));
|
|
1709
|
+
}
|
|
1710
|
+
return new Response(null, {
|
|
1711
|
+
status: opts.optionsSuccessStatus || 204,
|
|
1712
|
+
headers
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
const response = await next();
|
|
1716
|
+
if (response instanceof Response) {
|
|
1717
|
+
for (const [key, value] of headers.entries()) {
|
|
1718
|
+
response.headers.set(key, value);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
return response;
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
function RateLimit(options = {}) {
|
|
1725
|
+
const windowMs = options.windowMs || 60 * 1e3;
|
|
1726
|
+
const max = options.max || 5;
|
|
1727
|
+
const message = options.message || "Too many requests, please try again later.";
|
|
1728
|
+
const statusCode = options.statusCode || 429;
|
|
1729
|
+
const headers = options.headers !== false;
|
|
1730
|
+
const keyGenerator = options.keyGenerator || ((ctx) => {
|
|
1731
|
+
return ctx.headers.get("x-forwarded-for") || ctx.url.hostname || "unknown";
|
|
1732
|
+
});
|
|
1733
|
+
const skip = options.skip || (() => false);
|
|
1734
|
+
const hits = /* @__PURE__ */ new Map();
|
|
1735
|
+
const interval = setInterval(() => {
|
|
1736
|
+
const now = Date.now();
|
|
1737
|
+
for (const [key, record] of hits.entries()) {
|
|
1738
|
+
if (record.resetTime <= now) {
|
|
1739
|
+
hits.delete(key);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
}, windowMs);
|
|
1743
|
+
if (interval.unref) interval.unref();
|
|
1744
|
+
return async (ctx, next) => {
|
|
1745
|
+
if (skip(ctx)) return next();
|
|
1746
|
+
const key = keyGenerator(ctx);
|
|
1747
|
+
const now = Date.now();
|
|
1748
|
+
let record = hits.get(key);
|
|
1749
|
+
if (!record || record.resetTime <= now) {
|
|
1750
|
+
record = {
|
|
1751
|
+
hits: 0,
|
|
1752
|
+
resetTime: now + windowMs
|
|
1753
|
+
};
|
|
1754
|
+
hits.set(key, record);
|
|
1755
|
+
}
|
|
1756
|
+
record.hits++;
|
|
1757
|
+
const remaining = Math.max(0, max - record.hits);
|
|
1758
|
+
const resetTime = Math.ceil(record.resetTime / 1e3);
|
|
1759
|
+
if (record.hits > max) {
|
|
1760
|
+
if (headers) {
|
|
1761
|
+
const res = typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
|
|
1762
|
+
res.headers.set("X-RateLimit-Limit", String(max));
|
|
1763
|
+
res.headers.set("X-RateLimit-Remaining", "0");
|
|
1764
|
+
res.headers.set("X-RateLimit-Reset", String(resetTime));
|
|
1765
|
+
return res;
|
|
1766
|
+
}
|
|
1767
|
+
return typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
|
|
1768
|
+
}
|
|
1769
|
+
const response = await next();
|
|
1770
|
+
if (response instanceof Response && headers) {
|
|
1771
|
+
response.headers.set("X-RateLimit-Limit", String(max));
|
|
1772
|
+
response.headers.set("X-RateLimit-Remaining", String(remaining));
|
|
1773
|
+
response.headers.set("X-RateLimit-Reset", String(resetTime));
|
|
1774
|
+
}
|
|
1775
|
+
return response;
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
const eta = new eta$2.Eta();
|
|
1779
|
+
class ScalarPlugin extends ShokupanRouter {
|
|
1780
|
+
constructor(pluginOptions) {
|
|
1781
|
+
super();
|
|
1782
|
+
this.pluginOptions = pluginOptions;
|
|
1783
|
+
this.init();
|
|
1784
|
+
}
|
|
1785
|
+
init() {
|
|
1786
|
+
this.get("/", (ctx) => {
|
|
1787
|
+
let path2 = ctx.url.toString();
|
|
1788
|
+
if (!path2.endsWith("/")) path2 += "/";
|
|
1789
|
+
return ctx.html(eta.renderString(`<!doctype html>
|
|
1790
|
+
<html>
|
|
1791
|
+
<head>
|
|
1792
|
+
<title>API Reference</title>
|
|
1793
|
+
<meta charset = "utf-8" />
|
|
1794
|
+
<meta name="viewport" content = "width=device-width, initial-scale=1" />
|
|
1795
|
+
</head>
|
|
1796
|
+
|
|
1797
|
+
<body>
|
|
1798
|
+
<div id="app"></div>
|
|
1799
|
+
|
|
1800
|
+
<script src="<%= it.path %>scalar.js"><\/script>
|
|
1801
|
+
<script>
|
|
1802
|
+
Scalar.createApiReference('#app', [{ ...<%~ JSON.stringify(it.config.baseDocument) %>,
|
|
1803
|
+
url: "<%= it.path %>openapi.json",
|
|
1804
|
+
}
|
|
1805
|
+
])
|
|
1806
|
+
<\/script>
|
|
1807
|
+
</body>
|
|
1808
|
+
|
|
1809
|
+
</html>`, { path: path2, config: this.pluginOptions }));
|
|
1810
|
+
});
|
|
1811
|
+
this.get("/scalar.js", (ctx) => {
|
|
1812
|
+
return ctx.file(__dirname + "/../../node_modules/@scalar/api-reference/dist/browser/standalone.js");
|
|
1813
|
+
});
|
|
1814
|
+
this.get("/openapi.json", (ctx) => {
|
|
1815
|
+
return (this.root || this).generateApiSpec();
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
function SecurityHeaders(options = {}) {
|
|
1820
|
+
return async (ctx, next) => {
|
|
1821
|
+
const headers = {};
|
|
1822
|
+
const set = (k, v) => headers[k] = v;
|
|
1823
|
+
if (options.dnsPrefetchControl !== false) {
|
|
1824
|
+
const allow = options.dnsPrefetchControl?.allow;
|
|
1825
|
+
set("X-DNS-Prefetch-Control", allow ? "on" : "off");
|
|
1826
|
+
}
|
|
1827
|
+
if (options.frameguard !== false) {
|
|
1828
|
+
const opt = options.frameguard || {};
|
|
1829
|
+
const action = opt.action || "sameorigin";
|
|
1830
|
+
if (action === "sameorigin") set("X-Frame-Options", "SAMEORIGIN");
|
|
1831
|
+
else if (action === "deny") set("X-Frame-Options", "DENY");
|
|
1832
|
+
}
|
|
1833
|
+
if (options.hsts !== false) {
|
|
1834
|
+
const opt = options.hsts || {};
|
|
1835
|
+
const maxAge = opt.maxAge || 15552e3;
|
|
1836
|
+
let header = `max-age=${maxAge}`;
|
|
1837
|
+
if (opt.includeSubDomains !== false) header += "; includeSubDomains";
|
|
1838
|
+
if (opt.preload) header += "; preload";
|
|
1839
|
+
set("Strict-Transport-Security", header);
|
|
1840
|
+
}
|
|
1841
|
+
if (options.ieNoOpen !== false) {
|
|
1842
|
+
set("X-Download-Options", "noopen");
|
|
1843
|
+
}
|
|
1844
|
+
if (options.noSniff !== false) {
|
|
1845
|
+
set("X-Content-Type-Options", "nosniff");
|
|
1846
|
+
}
|
|
1847
|
+
if (options.xssFilter !== false) {
|
|
1848
|
+
set("X-XSS-Protection", "0");
|
|
1849
|
+
}
|
|
1850
|
+
if (options.referrerPolicy !== false) {
|
|
1851
|
+
const opt = options.referrerPolicy || {};
|
|
1852
|
+
const policy = opt.policy || "no-referrer";
|
|
1853
|
+
set("Referrer-Policy", Array.isArray(policy) ? policy.join(",") : policy);
|
|
1854
|
+
}
|
|
1855
|
+
if (options.contentSecurityPolicy !== false) {
|
|
1856
|
+
const opt = options.contentSecurityPolicy;
|
|
1857
|
+
if (opt === void 0 || opt === true) {
|
|
1858
|
+
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");
|
|
1859
|
+
} else if (typeof opt === "object") {
|
|
1860
|
+
for (const [key, val] of Object.entries(opt)) {
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
if (options.hidePoweredBy !== false) ;
|
|
1865
|
+
const response = await next();
|
|
1866
|
+
if (response instanceof Response) {
|
|
1867
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
1868
|
+
response.headers.set(k, v);
|
|
1869
|
+
}
|
|
1870
|
+
return response;
|
|
1871
|
+
}
|
|
1872
|
+
return response;
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
class Cookie {
|
|
1876
|
+
maxAge;
|
|
1877
|
+
signed;
|
|
1878
|
+
expires;
|
|
1879
|
+
httpOnly;
|
|
1880
|
+
path;
|
|
1881
|
+
domain;
|
|
1882
|
+
secure;
|
|
1883
|
+
sameSite;
|
|
1884
|
+
originalMaxAge;
|
|
1885
|
+
constructor(options = {}) {
|
|
1886
|
+
this.path = options.path || "/";
|
|
1887
|
+
this.httpOnly = options.httpOnly !== void 0 ? options.httpOnly : true;
|
|
1888
|
+
this.secure = options.secure;
|
|
1889
|
+
this.maxAge = options.maxAge;
|
|
1890
|
+
this.sameSite = options.sameSite;
|
|
1891
|
+
this.domain = options.domain;
|
|
1892
|
+
this.expires = options.expires;
|
|
1893
|
+
if (this.maxAge !== void 0) {
|
|
1894
|
+
this.originalMaxAge = this.maxAge;
|
|
1895
|
+
this.expires = new Date(Date.now() + this.maxAge);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
serialize(name, val) {
|
|
1899
|
+
let str = `${name}=${encodeURIComponent(val)}`;
|
|
1900
|
+
if (this.maxAge) {
|
|
1901
|
+
const expires = new Date(Date.now() + this.maxAge);
|
|
1902
|
+
str += `; Expires=${expires.toUTCString()}`;
|
|
1903
|
+
str += `; Max-Age=${Math.floor(this.maxAge / 1e3)}`;
|
|
1904
|
+
} else if (this.expires) {
|
|
1905
|
+
str += `; Expires=${this.expires.toUTCString()}`;
|
|
1906
|
+
}
|
|
1907
|
+
if (this.domain) str += `; Domain=${this.domain}`;
|
|
1908
|
+
if (this.path) str += `; Path=${this.path}`;
|
|
1909
|
+
if (this.httpOnly) str += `; HttpOnly`;
|
|
1910
|
+
if (this.secure) str += `; Secure`;
|
|
1911
|
+
if (this.sameSite) {
|
|
1912
|
+
const sameSite = typeof this.sameSite === "string" ? this.sameSite.charAt(0).toUpperCase() + this.sameSite.slice(1) : "Strict";
|
|
1913
|
+
str += `; SameSite=${sameSite}`;
|
|
1914
|
+
}
|
|
1915
|
+
return str;
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
class MemoryStore extends events.EventEmitter {
|
|
1919
|
+
sessions = {};
|
|
1920
|
+
get(sid, cb) {
|
|
1921
|
+
const sess = this.sessions[sid];
|
|
1922
|
+
if (!sess) return cb(null, null);
|
|
1923
|
+
try {
|
|
1924
|
+
const data = JSON.parse(sess);
|
|
1925
|
+
if (data.cookie && data.cookie.expires) {
|
|
1926
|
+
data.cookie.expires = new Date(data.cookie.expires);
|
|
1927
|
+
}
|
|
1928
|
+
cb(null, data);
|
|
1929
|
+
} catch (e) {
|
|
1930
|
+
cb(e);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
set(sid, sess, cb) {
|
|
1934
|
+
this.sessions[sid] = JSON.stringify(sess);
|
|
1935
|
+
cb && cb();
|
|
1936
|
+
}
|
|
1937
|
+
destroy(sid, cb) {
|
|
1938
|
+
delete this.sessions[sid];
|
|
1939
|
+
cb && cb();
|
|
1940
|
+
}
|
|
1941
|
+
touch(sid, sess, cb) {
|
|
1942
|
+
const current = this.sessions[sid];
|
|
1943
|
+
if (current) {
|
|
1944
|
+
this.sessions[sid] = JSON.stringify(sess);
|
|
1945
|
+
}
|
|
1946
|
+
cb && cb();
|
|
1947
|
+
}
|
|
1948
|
+
all(cb) {
|
|
1949
|
+
const result = {};
|
|
1950
|
+
for (const sid in this.sessions) {
|
|
1951
|
+
try {
|
|
1952
|
+
result[sid] = JSON.parse(this.sessions[sid]);
|
|
1953
|
+
} catch {
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
cb(null, result);
|
|
1957
|
+
}
|
|
1958
|
+
clear(cb) {
|
|
1959
|
+
this.sessions = {};
|
|
1960
|
+
cb && cb();
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
function sign(val, secret) {
|
|
1964
|
+
if (typeof val !== "string") throw new TypeError("Cookie value must be provided as a string.");
|
|
1965
|
+
if (typeof secret !== "string") throw new TypeError("Secret string must be provided.");
|
|
1966
|
+
return val + "." + crypto.createHmac("sha256", secret).update(val).digest("base64").replace(/\=+$/, "");
|
|
1967
|
+
}
|
|
1968
|
+
function unsign(input, secret) {
|
|
1969
|
+
if (typeof input !== "string") throw new TypeError("Signed cookie string must be provided.");
|
|
1970
|
+
if (typeof secret !== "string") throw new TypeError("Secret string must be provided.");
|
|
1971
|
+
const tentValue = input.slice(0, input.lastIndexOf("."));
|
|
1972
|
+
const expectedInput = sign(tentValue, secret);
|
|
1973
|
+
const expectedBuffer = Buffer.from(expectedInput);
|
|
1974
|
+
const inputBuffer = Buffer.from(input);
|
|
1975
|
+
if (expectedBuffer.length !== inputBuffer.length) return false;
|
|
1976
|
+
const valid = require("crypto").timingSafeEqual(expectedBuffer, inputBuffer);
|
|
1977
|
+
return valid ? tentValue : false;
|
|
1978
|
+
}
|
|
1979
|
+
function Session(options) {
|
|
1980
|
+
const store = options.store || new MemoryStore();
|
|
1981
|
+
const name = options.name || "connect.sid";
|
|
1982
|
+
const secrets = Array.isArray(options.secret) ? options.secret : [options.secret];
|
|
1983
|
+
const generateId = options.genid || (() => crypto.randomUUID());
|
|
1984
|
+
const resave = options.resave === void 0 ? true : options.resave;
|
|
1985
|
+
const saveUninitialized = options.saveUninitialized === void 0 ? true : options.saveUninitialized;
|
|
1986
|
+
const rolling = options.rolling || false;
|
|
1987
|
+
return async (ctx, next) => {
|
|
1988
|
+
let reqSessionId = null;
|
|
1989
|
+
const cookieHeader = ctx.req.headers.get("cookie");
|
|
1990
|
+
const cookies = {};
|
|
1991
|
+
if (cookieHeader) {
|
|
1992
|
+
cookieHeader.split(";").forEach((c) => {
|
|
1993
|
+
const [k, v] = c.split("=").map((s) => s.trim());
|
|
1994
|
+
if (k && v) cookies[k] = decodeURIComponent(v);
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
const rawCookie = cookies[name];
|
|
1998
|
+
if (rawCookie) {
|
|
1999
|
+
if (rawCookie.substr(0, 2) === "s:") {
|
|
2000
|
+
const val = unsign(rawCookie.slice(2), secrets[0]);
|
|
2001
|
+
if (val) {
|
|
2002
|
+
reqSessionId = val;
|
|
2003
|
+
}
|
|
2004
|
+
} else {
|
|
2005
|
+
reqSessionId = rawCookie;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
let sessionID = reqSessionId;
|
|
2009
|
+
let isNew = false;
|
|
2010
|
+
if (!sessionID) {
|
|
2011
|
+
sessionID = generateId(ctx);
|
|
2012
|
+
isNew = true;
|
|
2013
|
+
}
|
|
2014
|
+
const createSessionObject = (data) => {
|
|
2015
|
+
const existing = data || { cookie: new Cookie(options.cookie) };
|
|
2016
|
+
if (!existing.cookie) existing.cookie = new Cookie(options.cookie);
|
|
2017
|
+
else {
|
|
2018
|
+
const c = new Cookie(options.cookie);
|
|
2019
|
+
Object.assign(c, existing.cookie);
|
|
2020
|
+
if (c.expires && typeof c.expires === "string") c.expires = new Date(c.expires);
|
|
2021
|
+
existing.cookie = c;
|
|
2022
|
+
}
|
|
2023
|
+
const sessObj = existing;
|
|
2024
|
+
Object.defineProperty(sessObj, "id", { value: sessionID, configurable: true });
|
|
2025
|
+
sessObj.save = (cb) => {
|
|
2026
|
+
store.set(sessObj.id, sessObj, cb);
|
|
2027
|
+
};
|
|
2028
|
+
sessObj.destroy = (cb) => {
|
|
2029
|
+
store.destroy(sessObj.id, (err) => {
|
|
2030
|
+
if (cb) cb(err);
|
|
2031
|
+
});
|
|
2032
|
+
};
|
|
2033
|
+
sessObj.regenerate = (cb) => {
|
|
2034
|
+
store.destroy(sessObj.id, (err) => {
|
|
2035
|
+
sessionID = generateId(ctx);
|
|
2036
|
+
for (const key in sessObj) {
|
|
2037
|
+
if (key !== "cookie" && key !== "id" && typeof sessObj[key] !== "function") {
|
|
2038
|
+
delete sessObj[key];
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
Object.defineProperty(sessObj, "id", { value: sessionID, configurable: true });
|
|
2042
|
+
if (cb) cb(err);
|
|
2043
|
+
});
|
|
2044
|
+
};
|
|
2045
|
+
sessObj.undefined = () => {
|
|
2046
|
+
};
|
|
2047
|
+
sessObj.reload = (cb) => {
|
|
2048
|
+
store.get(sessObj.id, (err, sess2) => {
|
|
2049
|
+
if (err) return cb(err);
|
|
2050
|
+
if (!sess2) return cb(new Error("Session not found"));
|
|
2051
|
+
for (const key in sessObj) {
|
|
2052
|
+
if (key !== "cookie" && key !== "id" && typeof sessObj[key] !== "function") {
|
|
2053
|
+
delete sessObj[key];
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
Object.assign(sessObj, sess2);
|
|
2057
|
+
cb(null);
|
|
2058
|
+
});
|
|
2059
|
+
};
|
|
2060
|
+
sessObj.touch = () => {
|
|
2061
|
+
sessObj.cookie.expires = new Date(Date.now() + (sessObj.cookie.maxAge || 0));
|
|
2062
|
+
if (store.touch) store.touch(sessObj.id, sessObj);
|
|
2063
|
+
};
|
|
2064
|
+
return sessObj;
|
|
2065
|
+
};
|
|
2066
|
+
let sessionData = null;
|
|
2067
|
+
if (!isNew && sessionID) {
|
|
2068
|
+
await new Promise((resolve) => {
|
|
2069
|
+
store.get(sessionID, (err, sess2) => {
|
|
2070
|
+
if (err) {
|
|
2071
|
+
sessionID = generateId(ctx);
|
|
2072
|
+
isNew = true;
|
|
2073
|
+
} else if (!sess2) {
|
|
2074
|
+
sessionID = generateId(ctx);
|
|
2075
|
+
isNew = true;
|
|
2076
|
+
} else {
|
|
2077
|
+
sessionData = sess2;
|
|
2078
|
+
}
|
|
2079
|
+
resolve();
|
|
2080
|
+
});
|
|
2081
|
+
});
|
|
2082
|
+
}
|
|
2083
|
+
const sess = createSessionObject(sessionData);
|
|
2084
|
+
ctx.session = sess;
|
|
2085
|
+
ctx.sessionID = sessionID;
|
|
2086
|
+
ctx.sessionStore = store;
|
|
2087
|
+
const originalHash = JSON.stringify(sess);
|
|
2088
|
+
const result = await next();
|
|
2089
|
+
const currentHash = JSON.stringify(sess);
|
|
2090
|
+
const isModified = originalHash !== currentHash;
|
|
2091
|
+
if (!sessionID) return result;
|
|
2092
|
+
let shouldSave = false;
|
|
2093
|
+
if (isModified) {
|
|
2094
|
+
shouldSave = true;
|
|
2095
|
+
} else if (isNew && saveUninitialized) {
|
|
2096
|
+
shouldSave = true;
|
|
2097
|
+
} else if (!isNew && resave) {
|
|
2098
|
+
shouldSave = true;
|
|
2099
|
+
}
|
|
2100
|
+
if (shouldSave) {
|
|
2101
|
+
await new Promise((resolve, reject) => {
|
|
2102
|
+
store.set(sessionID, sess, (err) => {
|
|
2103
|
+
if (err) console.error("Failed to save session", err);
|
|
2104
|
+
resolve();
|
|
2105
|
+
});
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
2108
|
+
if (rolling && sess.cookie.maxAge) {
|
|
2109
|
+
sess.cookie.expires = new Date(Date.now() + sess.cookie.maxAge);
|
|
2110
|
+
}
|
|
2111
|
+
const shouldSetCookie = shouldSave || !isNew && rolling;
|
|
2112
|
+
if (shouldSetCookie) {
|
|
2113
|
+
let val = sessionID;
|
|
2114
|
+
if (secrets.length > 0) {
|
|
2115
|
+
val = "s:" + sign(val, secrets[0]);
|
|
2116
|
+
}
|
|
2117
|
+
const options2 = sess.cookie;
|
|
2118
|
+
const str = options2.serialize(name, val);
|
|
2119
|
+
ctx.set("Set-Cookie", str);
|
|
2120
|
+
}
|
|
2121
|
+
return result;
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
class ValidationError extends Error {
|
|
2125
|
+
constructor(errors) {
|
|
2126
|
+
super("Validation Error");
|
|
2127
|
+
this.errors = errors;
|
|
2128
|
+
}
|
|
2129
|
+
status = 400;
|
|
2130
|
+
}
|
|
2131
|
+
function isZod(schema) {
|
|
2132
|
+
return typeof schema?.safeParse === "function";
|
|
2133
|
+
}
|
|
2134
|
+
async function validateZod(schema, data) {
|
|
2135
|
+
const result = await schema.safeParseAsync(data);
|
|
2136
|
+
if (!result.success) {
|
|
2137
|
+
throw new ValidationError(result.error.errors);
|
|
2138
|
+
}
|
|
2139
|
+
return result.data;
|
|
2140
|
+
}
|
|
2141
|
+
function isTypeBox(schema) {
|
|
2142
|
+
return typeof schema?.Check === "function" && typeof schema?.Errors === "function";
|
|
2143
|
+
}
|
|
2144
|
+
function validateTypeBox(schema, data) {
|
|
2145
|
+
if (!schema.Check(data)) {
|
|
2146
|
+
throw new ValidationError([...schema.Errors(data)]);
|
|
2147
|
+
}
|
|
2148
|
+
return data;
|
|
2149
|
+
}
|
|
2150
|
+
function isAjv(schema) {
|
|
2151
|
+
return typeof schema === "function" && "errors" in schema;
|
|
2152
|
+
}
|
|
2153
|
+
function validateAjv(schema, data) {
|
|
2154
|
+
const valid = schema(data);
|
|
2155
|
+
if (!valid) {
|
|
2156
|
+
throw new ValidationError(schema.errors);
|
|
2157
|
+
}
|
|
2158
|
+
return data;
|
|
2159
|
+
}
|
|
2160
|
+
const valibot = (schema, parser) => {
|
|
2161
|
+
return {
|
|
2162
|
+
_valibot: true,
|
|
2163
|
+
schema,
|
|
2164
|
+
parser
|
|
2165
|
+
};
|
|
2166
|
+
};
|
|
2167
|
+
function isValibotWrapper(schema) {
|
|
2168
|
+
return schema?._valibot === true;
|
|
2169
|
+
}
|
|
2170
|
+
async function validateValibotWrapper(wrapper, data) {
|
|
2171
|
+
const result = await wrapper.parser(wrapper.schema, data);
|
|
2172
|
+
if (!result.success) {
|
|
2173
|
+
throw new ValidationError(result.issues);
|
|
2174
|
+
}
|
|
2175
|
+
return result.output;
|
|
2176
|
+
}
|
|
2177
|
+
const safelyGetBody = async (ctx) => {
|
|
2178
|
+
const req = ctx.req;
|
|
2179
|
+
if (req._bodyParsed) {
|
|
2180
|
+
return req._bodyValue;
|
|
2181
|
+
}
|
|
2182
|
+
try {
|
|
2183
|
+
let data;
|
|
2184
|
+
if (typeof req.json === "function") {
|
|
2185
|
+
data = await req.json();
|
|
2186
|
+
} else {
|
|
2187
|
+
data = req.body;
|
|
2188
|
+
if (typeof data === "string") {
|
|
2189
|
+
try {
|
|
2190
|
+
data = JSON.parse(data);
|
|
2191
|
+
} catch {
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
req._bodyParsed = true;
|
|
2196
|
+
req._bodyValue = data;
|
|
2197
|
+
Object.defineProperty(req, "json", {
|
|
2198
|
+
value: async () => req._bodyValue,
|
|
2199
|
+
configurable: true
|
|
2200
|
+
});
|
|
2201
|
+
return data;
|
|
2202
|
+
} catch (e) {
|
|
2203
|
+
return {};
|
|
2204
|
+
}
|
|
2205
|
+
};
|
|
2206
|
+
function validate(config) {
|
|
2207
|
+
return async (ctx, next) => {
|
|
2208
|
+
if (config.params) {
|
|
2209
|
+
ctx.params = await runValidation(config.params, ctx.params);
|
|
2210
|
+
}
|
|
2211
|
+
if (config.query) {
|
|
2212
|
+
const url = new URL(ctx.req.url);
|
|
2213
|
+
const queryObj = Object.fromEntries(url.searchParams.entries());
|
|
2214
|
+
await runValidation(config.query, queryObj);
|
|
2215
|
+
}
|
|
2216
|
+
if (config.headers) {
|
|
2217
|
+
const headersObj = Object.fromEntries(ctx.req.headers.entries());
|
|
2218
|
+
await runValidation(config.headers, headersObj);
|
|
2219
|
+
}
|
|
2220
|
+
if (config.body) {
|
|
2221
|
+
const body = await safelyGetBody(ctx);
|
|
2222
|
+
const validBody = await runValidation(config.body, body);
|
|
2223
|
+
const req = ctx.req;
|
|
2224
|
+
req._bodyValue = validBody;
|
|
2225
|
+
Object.defineProperty(req, "json", {
|
|
2226
|
+
value: async () => validBody,
|
|
2227
|
+
configurable: true
|
|
2228
|
+
});
|
|
2229
|
+
ctx.body = validBody;
|
|
2230
|
+
}
|
|
2231
|
+
return next();
|
|
2232
|
+
};
|
|
2233
|
+
}
|
|
2234
|
+
async function runValidation(schema, data) {
|
|
2235
|
+
if (isZod(schema)) {
|
|
2236
|
+
return validateZod(schema, data);
|
|
2237
|
+
}
|
|
2238
|
+
if (isTypeBox(schema)) {
|
|
2239
|
+
return validateTypeBox(schema, data);
|
|
2240
|
+
}
|
|
2241
|
+
if (isAjv(schema)) {
|
|
2242
|
+
return validateAjv(schema, data);
|
|
2243
|
+
}
|
|
2244
|
+
if (isValibotWrapper(schema)) {
|
|
2245
|
+
return validateValibotWrapper(schema, data);
|
|
2246
|
+
}
|
|
2247
|
+
if (typeof schema === "function") {
|
|
2248
|
+
return schema(data);
|
|
2249
|
+
}
|
|
2250
|
+
throw new Error("Unknown validator type provided. Please use a supported library (Zod, Ajv, TypeBox) or a custom function.");
|
|
2251
|
+
}
|
|
2252
|
+
exports.$appRoot = $appRoot;
|
|
2253
|
+
exports.$childControllers = $childControllers;
|
|
2254
|
+
exports.$childRouters = $childRouters;
|
|
2255
|
+
exports.$controllerPath = $controllerPath;
|
|
2256
|
+
exports.$dispatch = $dispatch;
|
|
2257
|
+
exports.$isApplication = $isApplication;
|
|
2258
|
+
exports.$isMounted = $isMounted;
|
|
2259
|
+
exports.$isRouter = $isRouter;
|
|
2260
|
+
exports.$middleware = $middleware;
|
|
2261
|
+
exports.$mountPath = $mountPath;
|
|
2262
|
+
exports.$parent = $parent;
|
|
2263
|
+
exports.$routeArgs = $routeArgs;
|
|
2264
|
+
exports.$routeMethods = $routeMethods;
|
|
2265
|
+
exports.All = All;
|
|
2266
|
+
exports.AuthPlugin = AuthPlugin;
|
|
2267
|
+
exports.Body = Body;
|
|
2268
|
+
exports.Compression = Compression;
|
|
2269
|
+
exports.Container = Container;
|
|
2270
|
+
exports.Controller = Controller;
|
|
2271
|
+
exports.Cors = Cors;
|
|
2272
|
+
exports.Ctx = Ctx;
|
|
2273
|
+
exports.Delete = Delete;
|
|
2274
|
+
exports.Get = Get;
|
|
2275
|
+
exports.HTTPMethods = HTTPMethods;
|
|
2276
|
+
exports.Head = Head;
|
|
2277
|
+
exports.Headers = Headers$1;
|
|
2278
|
+
exports.Inject = Inject;
|
|
2279
|
+
exports.Injectable = Injectable;
|
|
2280
|
+
exports.MemoryStore = MemoryStore;
|
|
2281
|
+
exports.Options = Options;
|
|
2282
|
+
exports.Param = Param;
|
|
2283
|
+
exports.Patch = Patch;
|
|
2284
|
+
exports.Post = Post;
|
|
2285
|
+
exports.Put = Put;
|
|
2286
|
+
exports.Query = Query;
|
|
2287
|
+
exports.RateLimit = RateLimit;
|
|
2288
|
+
exports.Req = Req;
|
|
2289
|
+
exports.RouteParamType = RouteParamType;
|
|
2290
|
+
exports.RouterRegistry = RouterRegistry;
|
|
2291
|
+
exports.ScalarPlugin = ScalarPlugin;
|
|
2292
|
+
exports.SecurityHeaders = SecurityHeaders;
|
|
2293
|
+
exports.Session = Session;
|
|
2294
|
+
exports.Shokupan = Shokupan;
|
|
2295
|
+
exports.ShokupanApplicationTree = ShokupanApplicationTree;
|
|
2296
|
+
exports.ShokupanContext = ShokupanContext;
|
|
2297
|
+
exports.ShokupanRequest = ShokupanRequest;
|
|
2298
|
+
exports.ShokupanResponse = ShokupanResponse;
|
|
2299
|
+
exports.ShokupanRouter = ShokupanRouter;
|
|
2300
|
+
exports.Use = Use;
|
|
2301
|
+
exports.ValidationError = ValidationError;
|
|
2302
|
+
exports.compose = compose;
|
|
2303
|
+
exports.valibot = valibot;
|
|
2304
|
+
exports.validate = validate;
|
|
2305
|
+
//# sourceMappingURL=index.cjs.map
|