@mandujs/core 0.53.2 → 0.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +654 -654
- package/package.json +2 -2
- package/src/a11y/__tests__/run-audit.test.ts +333 -333
- package/src/a11y/fix-hints.ts +76 -76
- package/src/a11y/index.ts +18 -18
- package/src/a11y/run-audit.ts +394 -394
- package/src/a11y/types.ts +125 -125
- package/src/auth/__tests__/login.test.ts +1 -1
- package/src/auth/__tests__/password.test.ts +122 -122
- package/src/auth/__tests__/tokens.test.ts +274 -274
- package/src/auth/__tests__/verification.test.ts +274 -274
- package/src/auth/index.ts +76 -76
- package/src/auth/login.ts +225 -225
- package/src/auth/password.ts +120 -120
- package/src/auth/reset.ts +243 -243
- package/src/auth/tokens.ts +612 -612
- package/src/auth/verification.ts +253 -253
- package/src/brain/__tests__/redactor.test.ts +94 -94
- package/src/brain/adapters/__tests__/_helpers.ts +83 -83
- package/src/brain/adapters/__tests__/anthropic-oauth.test.ts +196 -196
- package/src/brain/adapters/__tests__/chatgpt-auth.test.ts +193 -193
- package/src/brain/adapters/__tests__/openai-oauth.test.ts +209 -209
- package/src/brain/adapters/__tests__/resolver.test.ts +143 -143
- package/src/brain/adapters/anthropic-oauth.ts +1 -1
- package/src/brain/adapters/chatgpt-auth.ts +300 -300
- package/src/brain/adapters/index.ts +319 -319
- package/src/brain/adapters/oauth-flow.ts +439 -439
- package/src/brain/consent.ts +240 -240
- package/src/brain/credentials.ts +396 -396
- package/src/bundler/__tests__/build-runner.ts +113 -113
- package/src/bundler/__tests__/dev-reliability.test.ts +619 -619
- package/src/bundler/__tests__/extended-watch.test.ts +711 -711
- package/src/bundler/__tests__/fast-refresh.test.ts +10 -10
- package/src/bundler/__tests__/hdr.test.ts +353 -353
- package/src/bundler/__tests__/hmr-client.test.ts +532 -532
- package/src/bundler/__tests__/manifest-schema.test.ts +266 -266
- package/src/bundler/__tests__/prod-smoke.test.ts +138 -138
- package/src/bundler/__tests__/reverse-import-graph.test.ts +519 -519
- package/src/bundler/__tests__/slot-dispatch.test.ts +573 -573
- package/src/bundler/__tests__/url-cap-and-slot-regex.test.ts +286 -286
- package/src/bundler/__tests__/vendor-cache.test.ts +455 -455
- package/src/bundler/budget.ts +404 -404
- package/src/bundler/build.test.ts +179 -179
- package/src/bundler/build.ts +55 -55
- package/src/bundler/dev.ts +42 -42
- package/src/bundler/manifest-schema.ts +301 -301
- package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +263 -263
- package/src/bundler/plugins/__tests__/react-compiler-config.test.ts +83 -83
- package/src/bundler/plugins/__tests__/react-compiler-lint.test.ts +110 -110
- package/src/bundler/plugins/block-generated-imports.ts +155 -155
- package/src/bundler/plugins/index.ts +83 -83
- package/src/bundler/plugins/react-compiler-config.ts +108 -108
- package/src/bundler/plugins/react-compiler-lint.ts +253 -253
- package/src/bundler/plugins/react-compiler.ts +162 -162
- package/src/bundler/reverse-import-graph.ts +339 -339
- package/src/bundler/safe-build.test.ts +201 -201
- package/src/bundler/safe-build.ts +103 -103
- package/src/bundler/scenario-matrix.ts +229 -229
- package/src/bundler/types.ts +10 -10
- package/src/bundler/vendor-cache-types.ts +130 -130
- package/src/bundler/vendor-cache.ts +526 -526
- package/src/change/snapshot.ts +18 -18
- package/src/client/Form.tsx +105 -105
- package/src/client/Link.tsx +9 -9
- package/src/client/__tests__/use-sse.test.ts +153 -153
- package/src/client/globals.ts +1 -1
- package/src/client/hooks.ts +362 -362
- package/src/client/hydrate.ts +340 -340
- package/src/client/prefetch-helper.ts +55 -55
- package/src/client/router.ts +11 -11
- package/src/client/runtime.ts +47 -47
- package/src/client/serialize.ts +404 -404
- package/src/client/use-fetch.ts +6 -6
- package/src/client/use-head.ts +197 -197
- package/src/client/use-sse.ts +378 -378
- package/src/client/window-state.ts +101 -101
- package/src/components/Image.tsx +162 -162
- package/src/config/validate.ts +3 -3
- package/src/config/watcher.ts +311 -311
- package/src/constants.ts +40 -40
- package/src/content/collection.ts +8 -8
- package/src/content/content-layer.ts +7 -7
- package/src/content/data-store.ts +245 -245
- package/src/content/frontmatter.ts +189 -189
- package/src/content/loader-context.ts +171 -171
- package/src/content/loaders/api.ts +216 -216
- package/src/content/loaders/file.ts +172 -172
- package/src/content/loaders/glob.ts +253 -253
- package/src/content/loaders/index.ts +34 -34
- package/src/content/loaders/types.ts +137 -137
- package/src/content/meta-store.ts +209 -209
- package/src/content/prebuild.test.ts +571 -571
- package/src/content/prebuild.ts +636 -636
- package/src/content/schema.ts +20 -20
- package/src/content/sidebar.ts +630 -630
- package/src/content/slug.ts +110 -110
- package/src/content/types.ts +282 -282
- package/src/content/watcher.ts +135 -135
- package/src/contract/client-safe.test.ts +42 -42
- package/src/contract/client-safe.ts +114 -114
- package/src/contract/define.ts +11 -11
- package/src/contract/index.ts +1 -1
- package/src/contract/normalize.test.ts +276 -276
- package/src/contract/normalize.ts +410 -410
- package/src/contract/registry.test.ts +206 -206
- package/src/contract/route-helpers.ts +1 -1
- package/src/contract/rpc.ts +443 -443
- package/src/contract/schema.ts +48 -48
- package/src/contract/types.ts +58 -58
- package/src/db/__tests__/db.test.ts +482 -482
- package/src/db/index.ts +138 -138
- package/src/db/migrations/history-table.ts +345 -345
- package/src/db/migrations/index.ts +3 -3
- package/src/db/migrations/lock.ts +324 -324
- package/src/db/migrations/runner.ts +650 -650
- package/src/deploy/cache.ts +140 -140
- package/src/deploy/compile/vercel.ts +344 -344
- package/src/deploy/index.ts +87 -87
- package/src/deploy/inference/brain.ts +268 -268
- package/src/deploy/inference/context.ts +182 -182
- package/src/deploy/inference/filling-extract.ts +245 -245
- package/src/deploy/inference/heuristic.ts +182 -182
- package/src/deploy/intent.ts +173 -173
- package/src/deploy/plan.ts +178 -178
- package/src/design/__tests__/agents-link.test.ts +109 -109
- package/src/design/__tests__/extract-patch-diff.test.ts +265 -265
- package/src/design/__tests__/lint.test.ts +110 -110
- package/src/design/__tests__/parser.test.ts +195 -195
- package/src/design/__tests__/tailwind-theme.test.ts +229 -229
- package/src/design/agents-link.ts +165 -165
- package/src/design/diff.ts +138 -138
- package/src/design/extract.ts +285 -285
- package/src/design/index.ts +102 -102
- package/src/design/lint.ts +209 -209
- package/src/design/parser.ts +555 -555
- package/src/design/patch.ts +241 -241
- package/src/design/scaffold.ts +147 -147
- package/src/design/tailwind-theme.ts +441 -441
- package/src/design/types.ts +210 -210
- package/src/desktop/__tests__/webview-fallback.test.ts +254 -254
- package/src/desktop/__tests__/window.test.ts +248 -248
- package/src/desktop/__tests__/worker.test.ts +266 -266
- package/src/desktop/index.ts +43 -43
- package/src/desktop/types.ts +158 -158
- package/src/desktop/worker.ts +180 -180
- package/src/dev-error-overlay/__tests__/overlay-injector.test.ts +241 -241
- package/src/dev-error-overlay/index.ts +30 -30
- package/src/dev-error-overlay/overlay-injector.ts +243 -243
- package/src/dev-error-overlay/overlay-styles.ts +52 -52
- package/src/dev-error-overlay/types.ts +66 -66
- package/src/devtools/ai/context-builder.ts +375 -375
- package/src/devtools/ai/index.ts +25 -25
- package/src/devtools/ai/mcp-connector.ts +25 -25
- package/src/devtools/client/catchers/error-catcher.ts +344 -344
- package/src/devtools/client/catchers/index.ts +18 -18
- package/src/devtools/client/components/index.ts +39 -39
- package/src/devtools/client/components/mandu-character.tsx +331 -331
- package/src/devtools/client/components/overlay.tsx +368 -368
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
- package/src/devtools/client/components/panel/guard-panel.tsx +30 -30
- package/src/devtools/client/components/panel/islands-panel.tsx +320 -320
- package/src/devtools/client/components/panel/network-panel.tsx +291 -291
- package/src/devtools/client/components/panel/panel-container.tsx +500 -500
- package/src/devtools/client/components/panel/preview-panel.tsx +46 -46
- package/src/devtools/client/filters/context-filters.ts +282 -282
- package/src/devtools/client/filters/index.ts +16 -16
- package/src/devtools/client/index.ts +63 -63
- package/src/devtools/client/persistence.ts +335 -335
- package/src/devtools/hook/create-hook.ts +207 -207
- package/src/devtools/hook/index.ts +13 -13
- package/src/devtools/index.ts +439 -439
- package/src/devtools/init.ts +265 -265
- package/src/devtools/protocol.ts +237 -237
- package/src/devtools/server/index.ts +17 -17
- package/src/devtools/server/source-context.ts +450 -450
- package/src/devtools/types.ts +35 -35
- package/src/devtools/worker/index.ts +25 -25
- package/src/devtools/worker/redaction-worker.ts +233 -233
- package/src/devtools/worker/worker-manager.ts +410 -410
- package/src/diagnose/__tests__/checks.test.ts +451 -380
- package/src/diagnose/checks.ts +832 -720
- package/src/diagnose/index.ts +17 -16
- package/src/diagnose/run.ts +93 -91
- package/src/diagnose/types.ts +53 -53
- package/src/email/__tests__/email.test.ts +355 -355
- package/src/email/index.ts +282 -282
- package/src/email/smtp.ts +64 -64
- package/src/error/domains.ts +265 -265
- package/src/error/result.ts +60 -60
- package/src/error/types.ts +6 -6
- package/src/errors/extractor.ts +409 -409
- package/src/errors/index.ts +19 -19
- package/src/filling/__tests__/session-sqlite.test.ts +454 -454
- package/src/filling/auth.ts +308 -308
- package/src/filling/cookie-codec.ts +299 -299
- package/src/filling/deps.ts +265 -265
- package/src/filling/session-sqlite.ts +617 -617
- package/src/filling/sse.ts +5 -5
- package/src/filling/ws.ts +78 -78
- package/src/generator/index.ts +3 -3
- package/src/guard/__tests__/design-inline-class.test.ts +219 -219
- package/src/guard/__tests__/tsgolint-bridge.test.ts +347 -347
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/auto-correct.ts +1 -1
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/define-rule.ts +243 -243
- package/src/guard/design-inline-class.ts +393 -393
- package/src/guard/file-type.test.ts +24 -24
- package/src/guard/graph.ts +1 -1
- package/src/guard/healing.ts +36 -36
- package/src/guard/presets/atomic.ts +70 -70
- package/src/guard/presets/clean.ts +77 -77
- package/src/guard/presets/fsd.ts +79 -79
- package/src/guard/presets/hexagonal.ts +68 -68
- package/src/guard/reporter.ts +442 -442
- package/src/guard/rule-presets.ts +379 -379
- package/src/guard/semantic-slots.ts +1 -1
- package/src/guard/suggestions.ts +358 -358
- package/src/guard/tsgolint-bridge.ts +512 -512
- package/src/guard/types.ts +348 -348
- package/src/guard/watcher.ts +405 -405
- package/src/i18n/define.ts +126 -126
- package/src/i18n/index.ts +52 -52
- package/src/i18n/message-registry.ts +173 -173
- package/src/i18n/types.ts +112 -112
- package/src/id/index.ts +105 -105
- package/src/index.ts +2 -2
- package/src/kitchen/api/agent-devtools-api.ts +779 -544
- package/src/kitchen/api/errors-grouping.ts +126 -0
- package/src/kitchen/kitchen-handler.ts +192 -63
- package/src/kitchen/kitchen-ui.ts +842 -464
- package/src/logging/index.ts +22 -22
- package/src/logging/transports.ts +365 -365
- package/src/middleware/bridge.ts +147 -147
- package/src/middleware/compose.ts +134 -134
- package/src/middleware/compress.ts +62 -62
- package/src/middleware/cors.ts +47 -47
- package/src/middleware/csrf.ts +328 -328
- package/src/middleware/define.ts +132 -132
- package/src/middleware/jwt.ts +134 -134
- package/src/middleware/logger.ts +58 -58
- package/src/middleware/oauth/__tests__/oauth.test.ts +1 -1
- package/src/middleware/oauth/index.ts +505 -505
- package/src/middleware/oauth/providers.ts +115 -115
- package/src/middleware/rate-limit/index.ts +522 -522
- package/src/middleware/rate-limit/sqlite-store.ts +382 -382
- package/src/middleware/scheduler-cron.ts +96 -96
- package/src/middleware/secure/__tests__/secure.test.ts +360 -360
- package/src/middleware/secure/csp.ts +193 -193
- package/src/middleware/session.ts +174 -174
- package/src/middleware/timeout.ts +55 -55
- package/src/observability/logger-adapter.ts +36 -36
- package/src/observability/sqlite-store.ts +254 -254
- package/src/openapi/generator.ts +1 -1
- package/src/openapi/openapi.test.ts +43 -43
- package/src/perf/__tests__/user-marks.test.ts +354 -354
- package/src/perf/index.ts +133 -133
- package/src/plugins/__tests__/lifecycle-integration.test.ts +272 -272
- package/src/plugins/__tests__/runner.test.ts +409 -409
- package/src/plugins/define.ts +124 -124
- package/src/plugins/examples/dep-check-plugin.ts +80 -80
- package/src/plugins/examples/prerender-cache-plugin.ts +111 -111
- package/src/plugins/examples/sitemap-plugin.ts +65 -65
- package/src/plugins/runner.ts +361 -361
- package/src/plugins/types.ts +368 -368
- package/src/report/index.ts +1 -1
- package/src/resource/__tests__/generator.test.ts +32 -32
- package/src/resource/ddl/__tests__/diff.test.ts +639 -639
- package/src/resource/ddl/__tests__/emit.test.ts +823 -823
- package/src/resource/ddl/__tests__/snapshot.test.ts +499 -499
- package/src/resource/ddl/emit.ts +559 -559
- package/src/resource/ddl/persistence-types.ts +218 -218
- package/src/resource/ddl/type-map.ts +223 -223
- package/src/resource/ddl/types.ts +232 -232
- package/src/resource/generator-repo.ts +630 -630
- package/src/resource/schema.ts +1 -1
- package/src/router/fs-patterns.test.ts +96 -96
- package/src/router/fs-routes.ts +6 -6
- package/src/routes/index.ts +74 -74
- package/src/routes/metadata-routes.ts +427 -427
- package/src/routes/types.ts +341 -341
- package/src/runtime/__tests__/error-boundary-redaction.test.ts +141 -141
- package/src/runtime/__tests__/hdr-client.test.ts +223 -223
- package/src/runtime/__tests__/http-errors.test.ts +117 -117
- package/src/runtime/__tests__/not-found.test.ts +152 -152
- package/src/runtime/adapter.ts +47 -47
- package/src/runtime/boundary.tsx +252 -252
- package/src/runtime/cache.ts +494 -494
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/fast-refresh-runtime.ts +322 -322
- package/src/runtime/handler.ts +65 -65
- package/src/runtime/handlers.ts +300 -300
- package/src/runtime/http-errors.ts +113 -113
- package/src/runtime/image-handler.ts +1 -1
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/logger.test.ts +345 -345
- package/src/runtime/middleware.ts +264 -264
- package/src/runtime/not-found.ts +93 -93
- package/src/runtime/openapi-endpoint.ts +236 -236
- package/src/runtime/ppr.ts +74 -74
- package/src/runtime/registry.ts +171 -171
- package/src/runtime/router.ts +105 -105
- package/src/runtime/server.ts +67 -67
- package/src/runtime/shims.ts +48 -48
- package/src/runtime/ssr.ts +24 -31
- package/src/runtime/streaming-ssr.ts +32 -37
- package/src/runtime/trace.ts +144 -144
- package/src/scheduler/validate.ts +169 -169
- package/src/seo/index.ts +219 -219
- package/src/seo/integration/ssr.ts +306 -306
- package/src/seo/render/basic.ts +435 -435
- package/src/seo/render/index.ts +143 -143
- package/src/seo/render/jsonld.ts +539 -539
- package/src/seo/render/opengraph.ts +197 -197
- package/src/seo/render/robots.ts +116 -116
- package/src/seo/render/sitemap.ts +137 -137
- package/src/seo/render/twitter.ts +127 -127
- package/src/seo/resolve/opengraph.ts +143 -143
- package/src/seo/resolve/robots.ts +73 -73
- package/src/seo/resolve/title.ts +94 -94
- package/src/seo/resolve/twitter.ts +73 -73
- package/src/seo/resolve/url.ts +104 -104
- package/src/seo/routes/index.ts +290 -290
- package/src/seo/types.ts +588 -588
- package/src/slot/validator.ts +39 -39
- package/src/storage/s3/__tests__/s3.test.ts +479 -479
- package/src/storage/s3/index.ts +412 -412
- package/src/testing/__tests__/assertions.test.ts +632 -632
- package/src/testing/__tests__/reporter.test.ts +454 -454
- package/src/testing/assertions.ts +986 -986
- package/src/testing/db.ts +157 -157
- package/src/testing/mocks.ts +203 -203
- package/src/testing/session.ts +190 -190
- package/src/types/branded.ts +56 -56
- package/src/types/index.ts +1 -1
- package/src/utils/safe-io.ts +188 -188
- package/src/utils/string-safe.ts +298 -298
- package/src/watcher/watcher.ts +18 -18
package/src/middleware/define.ts
CHANGED
|
@@ -1,132 +1,132 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Canonical Middleware Composition API — Phase 18.ε
|
|
3
|
-
*
|
|
4
|
-
* This module defines Mandu's request-level middleware contract. It is the
|
|
5
|
-
* runtime analogue of Next.js `middleware.ts` and SvelteKit's `hooks.server.ts`
|
|
6
|
-
* `handle` sequence: each middleware is a thin onion layer that wraps the
|
|
7
|
-
* final route handler, can short-circuit by returning a Response without
|
|
8
|
-
* calling `next()`, and executes in declaration order (outermost first).
|
|
9
|
-
*
|
|
10
|
-
* This API is intentionally **request-level**, not context-level:
|
|
11
|
-
*
|
|
12
|
-
* - `filling().use(...)` / `MiddlewarePlugin` — operates on a per-route
|
|
13
|
-
* `ManduContext`. Good for inline concerns like `csrf()` / `session()`
|
|
14
|
-
* / `secure()` that a single filling chain wants to compose.
|
|
15
|
-
*
|
|
16
|
-
* - `Middleware` / `compose()` (this module) — operates on the raw
|
|
17
|
-
* `Request` BEFORE route dispatch. Good for app-wide policies:
|
|
18
|
-
* auth gates, tenant resolution, rate limiting, request logging,
|
|
19
|
-
* rewrites, redirects.
|
|
20
|
-
*
|
|
21
|
-
* Both APIs co-exist. Bridge wrappers live alongside each individual
|
|
22
|
-
* middleware module (e.g. `csrf.ts` exports `csrfMiddleware(...)`) so users
|
|
23
|
-
* who want the canonical composition API for an existing middleware can
|
|
24
|
-
* plug it in without boilerplate.
|
|
25
|
-
*
|
|
26
|
-
* @example
|
|
27
|
-
* ```ts
|
|
28
|
-
* import { defineMiddleware, compose } from "@mandujs/core/middleware";
|
|
29
|
-
*
|
|
30
|
-
* const requestId = defineMiddleware({
|
|
31
|
-
* name: "request-id",
|
|
32
|
-
* async handler(req, next) {
|
|
33
|
-
* const id = req.headers.get("x-request-id") ?? crypto.randomUUID();
|
|
34
|
-
* const res = await next();
|
|
35
|
-
* res.headers.set("x-request-id", id);
|
|
36
|
-
* return res;
|
|
37
|
-
* },
|
|
38
|
-
* });
|
|
39
|
-
*
|
|
40
|
-
* const authGate = defineMiddleware({
|
|
41
|
-
* name: "auth-gate",
|
|
42
|
-
* match: (req) => new URL(req.url).pathname.startsWith("/admin"),
|
|
43
|
-
* async handler(req, next) {
|
|
44
|
-
* if (!req.headers.get("authorization")) {
|
|
45
|
-
* return new Response("Unauthorized", { status: 401 });
|
|
46
|
-
* }
|
|
47
|
-
* return next();
|
|
48
|
-
* },
|
|
49
|
-
* });
|
|
50
|
-
*
|
|
51
|
-
* // In mandu.config.ts:
|
|
52
|
-
* export default {
|
|
53
|
-
* middleware: [requestId, authGate],
|
|
54
|
-
* } satisfies ManduConfig;
|
|
55
|
-
* ```
|
|
56
|
-
*/
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* A request-level middleware. Runs BEFORE route dispatch in declaration order.
|
|
60
|
-
*
|
|
61
|
-
* - `name` identifies the middleware in error messages and diagnostic traces.
|
|
62
|
-
* - `match?` is an optional filter: middleware whose `match(req)` returns
|
|
63
|
-
* `false` are skipped (the chain proceeds to the next middleware). Absent
|
|
64
|
-
* means "always match".
|
|
65
|
-
* - `handler(req, next)` does the actual work. Return a Response directly
|
|
66
|
-
* to short-circuit (downstream middleware and the route handler are
|
|
67
|
-
* skipped). Call `next()` to invoke the rest of the chain and receive
|
|
68
|
-
* its Response — at which point you may mutate headers, re-wrap the
|
|
69
|
-
* body, log, etc.
|
|
70
|
-
*
|
|
71
|
-
* The `req` argument is the Request the current layer sees; a middleware
|
|
72
|
-
* may pass a modified Request to `next()` by calling it with a Request
|
|
73
|
-
* argument (rewrite pattern). When called with no argument, `next()` uses
|
|
74
|
-
* the Request passed into the handler.
|
|
75
|
-
*/
|
|
76
|
-
export interface Middleware {
|
|
77
|
-
/** Display name used in diagnostics. Must be non-empty. */
|
|
78
|
-
name: string;
|
|
79
|
-
/**
|
|
80
|
-
* Optional route filter. When present and returns `false`, the chain
|
|
81
|
-
* skips this middleware entirely (proceeds to the next layer / final
|
|
82
|
-
* handler). When absent the middleware matches all requests.
|
|
83
|
-
*
|
|
84
|
-
* Must be synchronous and side-effect-free — `match` runs once per
|
|
85
|
-
* request per middleware and any throw short-circuits the chain with
|
|
86
|
-
* a 500 response.
|
|
87
|
-
*/
|
|
88
|
-
match?: (req: Request) => boolean;
|
|
89
|
-
/**
|
|
90
|
-
* Middleware handler. Receives the current Request and a `next()` thunk
|
|
91
|
-
* that invokes the rest of the chain (or the final route handler, if
|
|
92
|
-
* this is the innermost middleware). Must return a Response.
|
|
93
|
-
*
|
|
94
|
-
* - Call `next()` to continue the chain with the same request.
|
|
95
|
-
* - Call `next(modifiedReq)` to continue with a rewritten request
|
|
96
|
-
* (downstream middleware and the final handler see `modifiedReq`).
|
|
97
|
-
* - Return a Response directly WITHOUT calling `next()` to short-circuit.
|
|
98
|
-
*/
|
|
99
|
-
handler: (
|
|
100
|
-
req: Request,
|
|
101
|
-
next: (req?: Request) => Promise<Response>
|
|
102
|
-
) => Promise<Response>;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Ergonomic helper — passes the middleware object through unchanged but
|
|
107
|
-
* preserves full type inference and documents intent at the call site.
|
|
108
|
-
* Mirrors Next.js `defineMiddleware()` and SvelteKit `Handle` exports.
|
|
109
|
-
*
|
|
110
|
-
* @throws {TypeError} when `m` is missing `name` or `handler`, or when
|
|
111
|
-
* `name` is empty. Fail-fast at definition time — we do NOT want a
|
|
112
|
-
* silent no-op layer corrupting a composition chain.
|
|
113
|
-
*/
|
|
114
|
-
export function defineMiddleware(m: Middleware): Middleware {
|
|
115
|
-
if (!m || typeof m !== "object") {
|
|
116
|
-
throw new TypeError("[Mandu Middleware] defineMiddleware requires an object");
|
|
117
|
-
}
|
|
118
|
-
if (typeof m.name !== "string" || m.name.length === 0) {
|
|
119
|
-
throw new TypeError("[Mandu Middleware] defineMiddleware requires a non-empty `name`");
|
|
120
|
-
}
|
|
121
|
-
if (typeof m.handler !== "function") {
|
|
122
|
-
throw new TypeError(
|
|
123
|
-
`[Mandu Middleware] defineMiddleware requires a \`handler\` function (middleware "${m.name}")`
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
if (m.match !== undefined && typeof m.match !== "function") {
|
|
127
|
-
throw new TypeError(
|
|
128
|
-
`[Mandu Middleware] \`match\` must be a function when provided (middleware "${m.name}")`
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
return m;
|
|
132
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Canonical Middleware Composition API — Phase 18.ε
|
|
3
|
+
*
|
|
4
|
+
* This module defines Mandu's request-level middleware contract. It is the
|
|
5
|
+
* runtime analogue of Next.js `middleware.ts` and SvelteKit's `hooks.server.ts`
|
|
6
|
+
* `handle` sequence: each middleware is a thin onion layer that wraps the
|
|
7
|
+
* final route handler, can short-circuit by returning a Response without
|
|
8
|
+
* calling `next()`, and executes in declaration order (outermost first).
|
|
9
|
+
*
|
|
10
|
+
* This API is intentionally **request-level**, not context-level:
|
|
11
|
+
*
|
|
12
|
+
* - `filling().use(...)` / `MiddlewarePlugin` — operates on a per-route
|
|
13
|
+
* `ManduContext`. Good for inline concerns like `csrf()` / `session()`
|
|
14
|
+
* / `secure()` that a single filling chain wants to compose.
|
|
15
|
+
*
|
|
16
|
+
* - `Middleware` / `compose()` (this module) — operates on the raw
|
|
17
|
+
* `Request` BEFORE route dispatch. Good for app-wide policies:
|
|
18
|
+
* auth gates, tenant resolution, rate limiting, request logging,
|
|
19
|
+
* rewrites, redirects.
|
|
20
|
+
*
|
|
21
|
+
* Both APIs co-exist. Bridge wrappers live alongside each individual
|
|
22
|
+
* middleware module (e.g. `csrf.ts` exports `csrfMiddleware(...)`) so users
|
|
23
|
+
* who want the canonical composition API for an existing middleware can
|
|
24
|
+
* plug it in without boilerplate.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* import { defineMiddleware, compose } from "@mandujs/core/middleware";
|
|
29
|
+
*
|
|
30
|
+
* const requestId = defineMiddleware({
|
|
31
|
+
* name: "request-id",
|
|
32
|
+
* async handler(req, next) {
|
|
33
|
+
* const id = req.headers.get("x-request-id") ?? crypto.randomUUID();
|
|
34
|
+
* const res = await next();
|
|
35
|
+
* res.headers.set("x-request-id", id);
|
|
36
|
+
* return res;
|
|
37
|
+
* },
|
|
38
|
+
* });
|
|
39
|
+
*
|
|
40
|
+
* const authGate = defineMiddleware({
|
|
41
|
+
* name: "auth-gate",
|
|
42
|
+
* match: (req) => new URL(req.url).pathname.startsWith("/admin"),
|
|
43
|
+
* async handler(req, next) {
|
|
44
|
+
* if (!req.headers.get("authorization")) {
|
|
45
|
+
* return new Response("Unauthorized", { status: 401 });
|
|
46
|
+
* }
|
|
47
|
+
* return next();
|
|
48
|
+
* },
|
|
49
|
+
* });
|
|
50
|
+
*
|
|
51
|
+
* // In mandu.config.ts:
|
|
52
|
+
* export default {
|
|
53
|
+
* middleware: [requestId, authGate],
|
|
54
|
+
* } satisfies ManduConfig;
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A request-level middleware. Runs BEFORE route dispatch in declaration order.
|
|
60
|
+
*
|
|
61
|
+
* - `name` identifies the middleware in error messages and diagnostic traces.
|
|
62
|
+
* - `match?` is an optional filter: middleware whose `match(req)` returns
|
|
63
|
+
* `false` are skipped (the chain proceeds to the next middleware). Absent
|
|
64
|
+
* means "always match".
|
|
65
|
+
* - `handler(req, next)` does the actual work. Return a Response directly
|
|
66
|
+
* to short-circuit (downstream middleware and the route handler are
|
|
67
|
+
* skipped). Call `next()` to invoke the rest of the chain and receive
|
|
68
|
+
* its Response — at which point you may mutate headers, re-wrap the
|
|
69
|
+
* body, log, etc.
|
|
70
|
+
*
|
|
71
|
+
* The `req` argument is the Request the current layer sees; a middleware
|
|
72
|
+
* may pass a modified Request to `next()` by calling it with a Request
|
|
73
|
+
* argument (rewrite pattern). When called with no argument, `next()` uses
|
|
74
|
+
* the Request passed into the handler.
|
|
75
|
+
*/
|
|
76
|
+
export interface Middleware {
|
|
77
|
+
/** Display name used in diagnostics. Must be non-empty. */
|
|
78
|
+
name: string;
|
|
79
|
+
/**
|
|
80
|
+
* Optional route filter. When present and returns `false`, the chain
|
|
81
|
+
* skips this middleware entirely (proceeds to the next layer / final
|
|
82
|
+
* handler). When absent the middleware matches all requests.
|
|
83
|
+
*
|
|
84
|
+
* Must be synchronous and side-effect-free — `match` runs once per
|
|
85
|
+
* request per middleware and any throw short-circuits the chain with
|
|
86
|
+
* a 500 response.
|
|
87
|
+
*/
|
|
88
|
+
match?: (req: Request) => boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Middleware handler. Receives the current Request and a `next()` thunk
|
|
91
|
+
* that invokes the rest of the chain (or the final route handler, if
|
|
92
|
+
* this is the innermost middleware). Must return a Response.
|
|
93
|
+
*
|
|
94
|
+
* - Call `next()` to continue the chain with the same request.
|
|
95
|
+
* - Call `next(modifiedReq)` to continue with a rewritten request
|
|
96
|
+
* (downstream middleware and the final handler see `modifiedReq`).
|
|
97
|
+
* - Return a Response directly WITHOUT calling `next()` to short-circuit.
|
|
98
|
+
*/
|
|
99
|
+
handler: (
|
|
100
|
+
req: Request,
|
|
101
|
+
next: (req?: Request) => Promise<Response>
|
|
102
|
+
) => Promise<Response>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Ergonomic helper — passes the middleware object through unchanged but
|
|
107
|
+
* preserves full type inference and documents intent at the call site.
|
|
108
|
+
* Mirrors Next.js `defineMiddleware()` and SvelteKit `Handle` exports.
|
|
109
|
+
*
|
|
110
|
+
* @throws {TypeError} when `m` is missing `name` or `handler`, or when
|
|
111
|
+
* `name` is empty. Fail-fast at definition time — we do NOT want a
|
|
112
|
+
* silent no-op layer corrupting a composition chain.
|
|
113
|
+
*/
|
|
114
|
+
export function defineMiddleware(m: Middleware): Middleware {
|
|
115
|
+
if (!m || typeof m !== "object") {
|
|
116
|
+
throw new TypeError("[Mandu Middleware] defineMiddleware requires an object");
|
|
117
|
+
}
|
|
118
|
+
if (typeof m.name !== "string" || m.name.length === 0) {
|
|
119
|
+
throw new TypeError("[Mandu Middleware] defineMiddleware requires a non-empty `name`");
|
|
120
|
+
}
|
|
121
|
+
if (typeof m.handler !== "function") {
|
|
122
|
+
throw new TypeError(
|
|
123
|
+
`[Mandu Middleware] defineMiddleware requires a \`handler\` function (middleware "${m.name}")`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (m.match !== undefined && typeof m.match !== "function") {
|
|
127
|
+
throw new TypeError(
|
|
128
|
+
`[Mandu Middleware] \`match\` must be a function when provided (middleware "${m.name}")`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
return m;
|
|
132
|
+
}
|
package/src/middleware/jwt.ts
CHANGED
|
@@ -1,134 +1,134 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* JWT Middleware Plugin
|
|
3
|
-
* filling.use(jwt({ secret: process.env.JWT_SECRET }))
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { ManduContext } from "../filling/context";
|
|
7
|
-
|
|
8
|
-
export interface JwtMiddlewareOptions {
|
|
9
|
-
/** JWT 시크릿 키 */
|
|
10
|
-
secret: string;
|
|
11
|
-
/** 허용할 알고리즘 (기본: ["HS256"]) */
|
|
12
|
-
algorithms?: ("HS256" | "HS384" | "HS512")[];
|
|
13
|
-
/** 토큰 추출 위치 (기본: Authorization 헤더) */
|
|
14
|
-
extractFrom?: "header" | "cookie";
|
|
15
|
-
/** 쿠키 이름 (extractFrom: "cookie" 일 때) */
|
|
16
|
-
cookieName?: string;
|
|
17
|
-
/** ctx.set()에 저장할 키 (기본: "user") */
|
|
18
|
-
storeAs?: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* JWT 인증 미들웨어
|
|
23
|
-
*
|
|
24
|
-
* @example
|
|
25
|
-
* ```typescript
|
|
26
|
-
* import { jwt } from "@mandujs/core/middleware";
|
|
27
|
-
*
|
|
28
|
-
* export default Mandu.filling()
|
|
29
|
-
* .use(jwt({ secret: process.env.JWT_SECRET! }))
|
|
30
|
-
* .get((ctx) => {
|
|
31
|
-
* const user = ctx.get("user");
|
|
32
|
-
* return ctx.ok({ user });
|
|
33
|
-
* });
|
|
34
|
-
* ```
|
|
35
|
-
*/
|
|
36
|
-
export function jwt(options: JwtMiddlewareOptions) {
|
|
37
|
-
const { secret, algorithms = ["HS256"], extractFrom = "header", cookieName = "token", storeAs = "user" } = options;
|
|
38
|
-
|
|
39
|
-
return async (ctx: ManduContext): Promise<Response | void> => {
|
|
40
|
-
let token: string | null = null;
|
|
41
|
-
|
|
42
|
-
if (extractFrom === "header") {
|
|
43
|
-
const auth = ctx.headers.get("Authorization");
|
|
44
|
-
if (auth?.startsWith("Bearer ")) {
|
|
45
|
-
token = auth.slice(7);
|
|
46
|
-
}
|
|
47
|
-
} else {
|
|
48
|
-
token = ctx.cookies.get(cookieName) ?? null;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (!token) {
|
|
52
|
-
return ctx.unauthorized("Missing authentication token");
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// 토큰 크기 제한 (메모리 소모 공격 방지)
|
|
56
|
-
if (token.length > 8192) {
|
|
57
|
-
return ctx.unauthorized("Token too large");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
// Bun native JWT verification
|
|
62
|
-
const payload = await verifyJwtToken(token, secret, algorithms);
|
|
63
|
-
ctx.set(storeAs, payload);
|
|
64
|
-
} catch {
|
|
65
|
-
return ctx.unauthorized("Invalid or expired token");
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const ALG_MAP: Record<string, string> = {
|
|
71
|
-
HS256: "SHA-256",
|
|
72
|
-
HS384: "SHA-384",
|
|
73
|
-
HS512: "SHA-512",
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
/** JWT 검증 (Bun crypto 기반, HS256/HS384/HS512 지원) */
|
|
77
|
-
async function verifyJwtToken(
|
|
78
|
-
token: string,
|
|
79
|
-
secret: string,
|
|
80
|
-
allowedAlgorithms: string[]
|
|
81
|
-
): Promise<Record<string, unknown>> {
|
|
82
|
-
const parts = token.split(".");
|
|
83
|
-
if (parts.length !== 3) throw new Error("Invalid JWT format");
|
|
84
|
-
|
|
85
|
-
const [headerB64, payloadB64, signatureB64] = parts;
|
|
86
|
-
|
|
87
|
-
// 헤더에서 알고리즘 확인
|
|
88
|
-
const header = JSON.parse(atob(headerB64.replace(/-/g, "+").replace(/_/g, "/")));
|
|
89
|
-
const alg = header.alg as string;
|
|
90
|
-
if (!allowedAlgorithms.includes(alg)) {
|
|
91
|
-
throw new Error(`Algorithm "${alg}" not allowed. Allowed: ${allowedAlgorithms.join(", ")}`);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const hash = ALG_MAP[alg];
|
|
95
|
-
if (!hash) throw new Error(`Unsupported algorithm: ${alg}`);
|
|
96
|
-
|
|
97
|
-
// 서명 검증
|
|
98
|
-
const encoder = new TextEncoder();
|
|
99
|
-
const key = await crypto.subtle.importKey(
|
|
100
|
-
"raw",
|
|
101
|
-
encoder.encode(secret),
|
|
102
|
-
{ name: "HMAC", hash },
|
|
103
|
-
false,
|
|
104
|
-
["verify"]
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
const data = encoder.encode(`${headerB64}.${payloadB64}`);
|
|
108
|
-
const signature = base64UrlDecode(signatureB64);
|
|
109
|
-
|
|
110
|
-
const valid = await crypto.subtle.verify("HMAC", key, signature as BufferSource, data);
|
|
111
|
-
if (!valid) throw new Error("Invalid signature");
|
|
112
|
-
|
|
113
|
-
// 페이로드 디코딩
|
|
114
|
-
const payload = JSON.parse(atob(payloadB64.replace(/-/g, "+").replace(/_/g, "/")));
|
|
115
|
-
|
|
116
|
-
// 만료 확인
|
|
117
|
-
if (payload.exp && payload.exp * 1000 < Date.now()) {
|
|
118
|
-
throw new Error("Token expired");
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// nbf (not-before) 확인 — 아직 유효하지 않은 토큰 거부
|
|
122
|
-
if (payload.nbf && payload.nbf * 1000 > Date.now()) {
|
|
123
|
-
throw new Error("Token not yet valid");
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return payload;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function base64UrlDecode(str: string): Uint8Array {
|
|
130
|
-
const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
131
|
-
const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
|
|
132
|
-
const binary = atob(padded);
|
|
133
|
-
return Uint8Array.from(binary, c => c.charCodeAt(0));
|
|
134
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* JWT Middleware Plugin
|
|
3
|
+
* filling.use(jwt({ secret: process.env.JWT_SECRET }))
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ManduContext } from "../filling/context";
|
|
7
|
+
|
|
8
|
+
export interface JwtMiddlewareOptions {
|
|
9
|
+
/** JWT 시크릿 키 */
|
|
10
|
+
secret: string;
|
|
11
|
+
/** 허용할 알고리즘 (기본: ["HS256"]) */
|
|
12
|
+
algorithms?: ("HS256" | "HS384" | "HS512")[];
|
|
13
|
+
/** 토큰 추출 위치 (기본: Authorization 헤더) */
|
|
14
|
+
extractFrom?: "header" | "cookie";
|
|
15
|
+
/** 쿠키 이름 (extractFrom: "cookie" 일 때) */
|
|
16
|
+
cookieName?: string;
|
|
17
|
+
/** ctx.set()에 저장할 키 (기본: "user") */
|
|
18
|
+
storeAs?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* JWT 인증 미들웨어
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* import { jwt } from "@mandujs/core/middleware";
|
|
27
|
+
*
|
|
28
|
+
* export default Mandu.filling()
|
|
29
|
+
* .use(jwt({ secret: process.env.JWT_SECRET! }))
|
|
30
|
+
* .get((ctx) => {
|
|
31
|
+
* const user = ctx.get("user");
|
|
32
|
+
* return ctx.ok({ user });
|
|
33
|
+
* });
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function jwt(options: JwtMiddlewareOptions) {
|
|
37
|
+
const { secret, algorithms = ["HS256"], extractFrom = "header", cookieName = "token", storeAs = "user" } = options;
|
|
38
|
+
|
|
39
|
+
return async (ctx: ManduContext): Promise<Response | void> => {
|
|
40
|
+
let token: string | null = null;
|
|
41
|
+
|
|
42
|
+
if (extractFrom === "header") {
|
|
43
|
+
const auth = ctx.headers.get("Authorization");
|
|
44
|
+
if (auth?.startsWith("Bearer ")) {
|
|
45
|
+
token = auth.slice(7);
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
token = ctx.cookies.get(cookieName) ?? null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!token) {
|
|
52
|
+
return ctx.unauthorized("Missing authentication token");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 토큰 크기 제한 (메모리 소모 공격 방지)
|
|
56
|
+
if (token.length > 8192) {
|
|
57
|
+
return ctx.unauthorized("Token too large");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
// Bun native JWT verification
|
|
62
|
+
const payload = await verifyJwtToken(token, secret, algorithms);
|
|
63
|
+
ctx.set(storeAs, payload);
|
|
64
|
+
} catch {
|
|
65
|
+
return ctx.unauthorized("Invalid or expired token");
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const ALG_MAP: Record<string, string> = {
|
|
71
|
+
HS256: "SHA-256",
|
|
72
|
+
HS384: "SHA-384",
|
|
73
|
+
HS512: "SHA-512",
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** JWT 검증 (Bun crypto 기반, HS256/HS384/HS512 지원) */
|
|
77
|
+
async function verifyJwtToken(
|
|
78
|
+
token: string,
|
|
79
|
+
secret: string,
|
|
80
|
+
allowedAlgorithms: string[]
|
|
81
|
+
): Promise<Record<string, unknown>> {
|
|
82
|
+
const parts = token.split(".");
|
|
83
|
+
if (parts.length !== 3) throw new Error("Invalid JWT format");
|
|
84
|
+
|
|
85
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
86
|
+
|
|
87
|
+
// 헤더에서 알고리즘 확인
|
|
88
|
+
const header = JSON.parse(atob(headerB64.replace(/-/g, "+").replace(/_/g, "/")));
|
|
89
|
+
const alg = header.alg as string;
|
|
90
|
+
if (!allowedAlgorithms.includes(alg)) {
|
|
91
|
+
throw new Error(`Algorithm "${alg}" not allowed. Allowed: ${allowedAlgorithms.join(", ")}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const hash = ALG_MAP[alg];
|
|
95
|
+
if (!hash) throw new Error(`Unsupported algorithm: ${alg}`);
|
|
96
|
+
|
|
97
|
+
// 서명 검증
|
|
98
|
+
const encoder = new TextEncoder();
|
|
99
|
+
const key = await crypto.subtle.importKey(
|
|
100
|
+
"raw",
|
|
101
|
+
encoder.encode(secret),
|
|
102
|
+
{ name: "HMAC", hash },
|
|
103
|
+
false,
|
|
104
|
+
["verify"]
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const data = encoder.encode(`${headerB64}.${payloadB64}`);
|
|
108
|
+
const signature = base64UrlDecode(signatureB64);
|
|
109
|
+
|
|
110
|
+
const valid = await crypto.subtle.verify("HMAC", key, signature as BufferSource, data);
|
|
111
|
+
if (!valid) throw new Error("Invalid signature");
|
|
112
|
+
|
|
113
|
+
// 페이로드 디코딩
|
|
114
|
+
const payload = JSON.parse(atob(payloadB64.replace(/-/g, "+").replace(/_/g, "/")));
|
|
115
|
+
|
|
116
|
+
// 만료 확인
|
|
117
|
+
if (payload.exp && payload.exp * 1000 < Date.now()) {
|
|
118
|
+
throw new Error("Token expired");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// nbf (not-before) 확인 — 아직 유효하지 않은 토큰 거부
|
|
122
|
+
if (payload.nbf && payload.nbf * 1000 > Date.now()) {
|
|
123
|
+
throw new Error("Token not yet valid");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return payload;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function base64UrlDecode(str: string): Uint8Array {
|
|
130
|
+
const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
131
|
+
const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
|
|
132
|
+
const binary = atob(padded);
|
|
133
|
+
return Uint8Array.from(binary, c => c.charCodeAt(0));
|
|
134
|
+
}
|
package/src/middleware/logger.ts
CHANGED
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Logger Middleware Plugin
|
|
3
|
-
* filling.use(logger())
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { MiddlewarePlugin } from "../filling/filling";
|
|
7
|
-
import type { ManduContext } from "../filling/context";
|
|
8
|
-
|
|
9
|
-
export interface LoggerMiddlewareOptions {
|
|
10
|
-
/** 로그 포맷 (기본: "short") */
|
|
11
|
-
format?: "short" | "detailed";
|
|
12
|
-
/** 커스텀 로거 (기본: console.log) */
|
|
13
|
-
log?: (message: string) => void;
|
|
14
|
-
/** 느린 요청 경고 임계값 (ms, 기본: 3000) */
|
|
15
|
-
slowThreshold?: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* 요청/응답 로깅 미들웨어
|
|
20
|
-
*
|
|
21
|
-
* @example
|
|
22
|
-
* ```typescript
|
|
23
|
-
* import { logger } from "@mandujs/core/middleware";
|
|
24
|
-
*
|
|
25
|
-
* export default Mandu.filling()
|
|
26
|
-
* .use(logger({ format: "detailed", slowThreshold: 1000 }))
|
|
27
|
-
* .get((ctx) => ctx.ok({ data: "logged" }));
|
|
28
|
-
* ```
|
|
29
|
-
*/
|
|
30
|
-
export function logger(options?: LoggerMiddlewareOptions): MiddlewarePlugin {
|
|
31
|
-
const format = options?.format ?? "short";
|
|
32
|
-
const log = options?.log ?? console.log;
|
|
33
|
-
const slowThreshold = options?.slowThreshold ?? 3000;
|
|
34
|
-
|
|
35
|
-
return {
|
|
36
|
-
beforeHandle: async (ctx: ManduContext): Promise<void> => {
|
|
37
|
-
ctx.set("_logger_start", Date.now());
|
|
38
|
-
},
|
|
39
|
-
afterHandle: async (ctx: ManduContext, response: Response): Promise<Response> => {
|
|
40
|
-
const start = ctx.get<number>("_logger_start");
|
|
41
|
-
if (start === undefined) return response;
|
|
42
|
-
|
|
43
|
-
const duration = Date.now() - start;
|
|
44
|
-
const method = ctx.request.method;
|
|
45
|
-
const pathname = new URL(ctx.request.url).pathname;
|
|
46
|
-
const status = response.status;
|
|
47
|
-
const slow = duration > slowThreshold ? " ⚠️ SLOW" : "";
|
|
48
|
-
|
|
49
|
-
if (format === "detailed") {
|
|
50
|
-
log(`${method} ${pathname} → ${status} (${duration}ms)${slow}`);
|
|
51
|
-
} else {
|
|
52
|
-
log(`${method} ${pathname} ${status} ${duration}ms${slow}`);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return response;
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Logger Middleware Plugin
|
|
3
|
+
* filling.use(logger())
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { MiddlewarePlugin } from "../filling/filling";
|
|
7
|
+
import type { ManduContext } from "../filling/context";
|
|
8
|
+
|
|
9
|
+
export interface LoggerMiddlewareOptions {
|
|
10
|
+
/** 로그 포맷 (기본: "short") */
|
|
11
|
+
format?: "short" | "detailed";
|
|
12
|
+
/** 커스텀 로거 (기본: console.log) */
|
|
13
|
+
log?: (message: string) => void;
|
|
14
|
+
/** 느린 요청 경고 임계값 (ms, 기본: 3000) */
|
|
15
|
+
slowThreshold?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 요청/응답 로깅 미들웨어
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* import { logger } from "@mandujs/core/middleware";
|
|
24
|
+
*
|
|
25
|
+
* export default Mandu.filling()
|
|
26
|
+
* .use(logger({ format: "detailed", slowThreshold: 1000 }))
|
|
27
|
+
* .get((ctx) => ctx.ok({ data: "logged" }));
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function logger(options?: LoggerMiddlewareOptions): MiddlewarePlugin {
|
|
31
|
+
const format = options?.format ?? "short";
|
|
32
|
+
const log = options?.log ?? console.log;
|
|
33
|
+
const slowThreshold = options?.slowThreshold ?? 3000;
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
beforeHandle: async (ctx: ManduContext): Promise<void> => {
|
|
37
|
+
ctx.set("_logger_start", Date.now());
|
|
38
|
+
},
|
|
39
|
+
afterHandle: async (ctx: ManduContext, response: Response): Promise<Response> => {
|
|
40
|
+
const start = ctx.get<number>("_logger_start");
|
|
41
|
+
if (start === undefined) return response;
|
|
42
|
+
|
|
43
|
+
const duration = Date.now() - start;
|
|
44
|
+
const method = ctx.request.method;
|
|
45
|
+
const pathname = new URL(ctx.request.url).pathname;
|
|
46
|
+
const status = response.status;
|
|
47
|
+
const slow = duration > slowThreshold ? " ⚠️ SLOW" : "";
|
|
48
|
+
|
|
49
|
+
if (format === "detailed") {
|
|
50
|
+
log(`${method} ${pathname} → ${status} (${duration}ms)${slow}`);
|
|
51
|
+
} else {
|
|
52
|
+
log(`${method} ${pathname} ${status} ${duration}ms${slow}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return response;
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
} from "../index";
|
|
22
22
|
import { session, saveSession } from "../../session";
|
|
23
23
|
import type {
|
|
24
|
-
Session} from "../../../filling/session";
|
|
24
|
+
Session} from "../../../filling/session";
|
|
25
25
|
import {
|
|
26
26
|
createCookieSessionStorage,
|
|
27
27
|
type SessionStorage,
|