@erenthedeveloper0/zen-middleware 0.1.0-alpha.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/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eren Sümer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,85 @@
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/erenthedeveloper0/zen/main/.github/images/logo-with-text-white.png">
4
+ <img alt="zen.js" src="https://raw.githubusercontent.com/erenthedeveloper0/zen/main/.github/images/logo-with-text-black.png" width="220">
5
+ </picture>
6
+ </p>
7
+
8
+ # @erenthedeveloper0/zen-middleware
9
+
10
+ CORS, security headers, request ids and rate limiting for
11
+ [Zen](https://github.com/erenthedeveloper0/zen) — built so that they run on the
12
+ requests that matter most, which are the ones most middleware never sees.
13
+
14
+ > **Alpha.** Also re-exported by
15
+ > [`@erenthedeveloper0/zen`](https://www.npmjs.com/package/@erenthedeveloper0/zen).
16
+
17
+ ```bash
18
+ npm install @erenthedeveloper0/zen-middleware@alpha
19
+ ```
20
+
21
+ ```ts
22
+ import { zen } from '@erenthedeveloper0/zen'
23
+ import { cors, rateLimit, requestId, securityHeaders } from '@erenthedeveloper0/zen-middleware'
24
+
25
+ const app = zen({ trustProxy: 1 }) // one load balancer in front — see below
26
+
27
+ app.use(securityHeaders())
28
+ app.use(cors({ origin: ['https://app.example.com'], credentials: true }))
29
+ app.use(rateLimit({ limit: 120, window: '1m' }))
30
+ app.use(requestId())
31
+ ```
32
+
33
+ ## The one thing this package is about
34
+
35
+ **Route middleware does not run on a request that matched no route.** A browser
36
+ sends `OPTIONS /api/notes` before any cross-origin write; almost no application
37
+ declares an `OPTIONS` route; so a CORS middleware registered on routes never
38
+ sees the preflight, and the browser reports the failure on the *next* request,
39
+ in code that is correct. Counted over a matched `GET`, an unmatched path and a
40
+ preflight: route middleware ran **1 of 3**. For rate limiting the same gap is a
41
+ bypass — request a path that does not exist, and nothing counts it.
42
+
43
+ So each of these is a plugin that registers a **global `onRequest` hook**, which
44
+ Zen runs on every request, matched or not: **3 of 3**. And each *stages* its
45
+ headers rather than writing them, so they appear on the 404, the 429, the 422
46
+ and the 500 as well as the 200 — which is the difference between an API that
47
+ works in the browser and one that works only in Postman.
48
+
49
+ ## What each one decides
50
+
51
+ - **`cors()`** — the allowlist is required (the secure default is not
52
+ registering the plugin at all). `Vary: Origin` is sent on every response,
53
+ including those without an `Origin`, because a shared cache needs it.
54
+ `Access-Control-Allow-Methods` is read from the routes you actually declared.
55
+ `origin: '*'` with `credentials: true` is refused at boot. The allowlist can
56
+ come from configuration (`config.cors.origin`).
57
+ - **`securityHeaders()`** — `nosniff`, `X-Frame-Options: DENY`,
58
+ `Referrer-Policy: no-referrer`, conservative `Cross-Origin-*` policies. HSTS is
59
+ off until you configure it, because it cannot be taken back. A CORS allowlist
60
+ that contradicts `Cross-Origin-Resource-Policy` is a boot error naming both.
61
+ - **`requestId()`** — echoes the request's id. Adopting an inbound
62
+ `X-Request-Id` is off by default, and validated when on.
63
+ - **`rateLimit()`** — a fixed-window counter keyed by `ctx.ip`, refusing with an
64
+ ordinary 429 problem document *before* the body is read, with `RateLimit`
65
+ headers. The in-memory store evicts a whole window at once, so memory is
66
+ bounded even when the key is attacker-chosen; `Store` is the seam for Redis.
67
+
68
+ The pack orders itself: register them in any order and they run request id →
69
+ security headers → CORS → rate limit.
70
+
71
+ ## Behind a proxy
72
+
73
+ `ctx.ip` — and so the rate limiter — reads `X-Forwarded-For` only when the app
74
+ sets `trustProxy`. Set it to **the number of proxies** in front of the process:
75
+ `trustProxy: 1` for one load balancer. That reads the address your proxy saw,
76
+ which no client can forge. `trustProxy: true` reads the leftmost entry, which
77
+ the client writes itself when a proxy appends to the header — and then a client
78
+ rotating a fake address gets a fresh rate-limit budget on every request.
79
+
80
+ ## Documentation
81
+
82
+ [ARCHITECTURE.md §32](https://github.com/erenthedeveloper0/zen/blob/main/ARCHITECTURE.md#32-first-party-middleware) ·
83
+ [`examples/middleware`](https://github.com/erenthedeveloper0/zen/tree/main/examples/middleware).
84
+
85
+ [MIT](https://github.com/erenthedeveloper0/zen/blob/main/LICENSE) © [Eren Sümer](https://github.com/erenthedeveloper0) · [contributors](https://github.com/erenthedeveloper0/zen/blob/main/CONTRIBUTORS.md)
@@ -0,0 +1,30 @@
1
+ import type { CorsExports } from './cors.ts';
2
+ /**
3
+ * Cross-plugin consistency — the check §2.4 makes possible.
4
+ *
5
+ * `Cross-Origin-Resource-Policy: same-origin` tells the browser to refuse
6
+ * cross-origin reads of this response. `cors({ origin: [...] })` tells it to
7
+ * permit them. Together they are a configuration that says two opposite things,
8
+ * the CORP one wins, and the symptom is a CORS setup that "does not work" for
9
+ * reasons that appear nowhere in the CORS configuration.
10
+ *
11
+ * In a framework where middleware is a list of opaque functions there is
12
+ * nothing to check. Here both are plugins on one graph and `exportsOf` lets
13
+ * each read the other's decision, so it is a boot error naming both settings.
14
+ *
15
+ * ### Why this is its own module rather than a function in `security.ts`
16
+ *
17
+ * Because only the plugin that runs **second** can see both, and which one that
18
+ * is now depends on hook ordering rather than on this check. `securityHeaders`
19
+ * has to register its hook *first* — its headers must be staged before anything
20
+ * can short-circuit, or a preflight and a 429 go out without `nosniff` — so it
21
+ * can no longer be the one that reads `cors`'s exports.
22
+ *
23
+ * Rather than move the check to `cors.ts` and leave it there until the next
24
+ * ordering change moves it back, both plugins call this with whatever they can
25
+ * see. The one that ran first passes `undefined` for the other half and returns;
26
+ * the one that ran second has both and decides. Order-independent by
27
+ * construction, which is the property that was actually wanted.
28
+ */
29
+ export declare function assertCorsCorpConsistent(corp: string | false, cors: CorsExports | undefined, reporter: string): void;
30
+ //# sourceMappingURL=consistency.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consistency.d.ts","sourceRoot":"","sources":["../src/consistency.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAE5C;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,GAAG,KAAK,EACpB,IAAI,EAAE,WAAW,GAAG,SAAS,EAC7B,QAAQ,EAAE,MAAM,GACf,IAAI,CA2BN"}
@@ -0,0 +1,49 @@
1
+ import { Codes, ZenError } from '@erenthedeveloper0/zen-core';
2
+ /**
3
+ * Cross-plugin consistency — the check §2.4 makes possible.
4
+ *
5
+ * `Cross-Origin-Resource-Policy: same-origin` tells the browser to refuse
6
+ * cross-origin reads of this response. `cors({ origin: [...] })` tells it to
7
+ * permit them. Together they are a configuration that says two opposite things,
8
+ * the CORP one wins, and the symptom is a CORS setup that "does not work" for
9
+ * reasons that appear nowhere in the CORS configuration.
10
+ *
11
+ * In a framework where middleware is a list of opaque functions there is
12
+ * nothing to check. Here both are plugins on one graph and `exportsOf` lets
13
+ * each read the other's decision, so it is a boot error naming both settings.
14
+ *
15
+ * ### Why this is its own module rather than a function in `security.ts`
16
+ *
17
+ * Because only the plugin that runs **second** can see both, and which one that
18
+ * is now depends on hook ordering rather than on this check. `securityHeaders`
19
+ * has to register its hook *first* — its headers must be staged before anything
20
+ * can short-circuit, or a preflight and a 429 go out without `nosniff` — so it
21
+ * can no longer be the one that reads `cors`'s exports.
22
+ *
23
+ * Rather than move the check to `cors.ts` and leave it there until the next
24
+ * ordering change moves it back, both plugins call this with whatever they can
25
+ * see. The one that ran first passes `undefined` for the other half and returns;
26
+ * the one that ran second has both and decides. Order-independent by
27
+ * construction, which is the property that was actually wanted.
28
+ */
29
+ export function assertCorsCorpConsistent(corp, cors, reporter) {
30
+ if (cors === undefined || corp !== 'same-origin')
31
+ return;
32
+ const allowing = cors.origins === 'any' || cors.origins === 'dynamic' || cors.origins.length > 0;
33
+ if (!allowing)
34
+ return;
35
+ const described = cors.origins === 'any' ? "'*'"
36
+ : cors.origins === 'dynamic' ? 'origins chosen by a predicate'
37
+ : cors.origins.join(', ');
38
+ throw new ZenError(Codes.CONFIG_INVALID, `security-headers sets Cross-Origin-Resource-Policy: same-origin while cors allows ${described}. ` +
39
+ 'The two instruct the browser to do opposite things, and the CORP header wins.', {
40
+ status: 500,
41
+ expose: false,
42
+ hint: "Use crossOriginResource: 'same-site' (the default) or 'cross-origin' if these responses are " +
43
+ 'meant to be read cross-origin, or narrow the CORS allowlist if they are not.',
44
+ consequence: 'Left as configured, every allowed origin still fails in the browser — and it fails with a ' +
45
+ 'message naming CORS, so the search starts in the file that is correct. ' +
46
+ `Reported by ${reporter}, whichever of the two was registered second.`,
47
+ });
48
+ }
49
+ //# sourceMappingURL=consistency.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consistency.js","sourceRoot":"","sources":["../src/consistency.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAA;AAG7D;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,wBAAwB,CACtC,IAAoB,EACpB,IAA6B,EAC7B,QAAgB;IAEhB,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,aAAa;QAAE,OAAM;IAExD,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,KAAK,KAAK,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAA;IAChG,IAAI,CAAC,QAAQ;QAAE,OAAM;IAErB,MAAM,SAAS,GACb,IAAI,CAAC,OAAO,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK;QAC9B,CAAC,CAAC,IAAI,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,+BAA+B;YAC9D,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAE3B,MAAM,IAAI,QAAQ,CAChB,KAAK,CAAC,cAAc,EACpB,qFAAqF,SAAS,IAAI;QAChG,+EAA+E,EACjF;QACE,MAAM,EAAE,GAAG;QACX,MAAM,EAAE,KAAK;QACb,IAAI,EACF,8FAA8F;YAC9F,8EAA8E;QAChF,WAAW,EACT,4FAA4F;YAC5F,yEAAyE;YACzE,eAAe,QAAQ,+CAA+C;KACzE,CACF,CAAA;AACH,CAAC"}
package/dist/cors.d.ts ADDED
@@ -0,0 +1,110 @@
1
+ import { type Duration, type HttpMethod, type Plugin } from '@erenthedeveloper0/zen-core';
2
+ /**
3
+ * CORS — rfcs/0001 §19.2, §4.2 stage 5, §9.2.
4
+ *
5
+ * ### Why this is a hook and not middleware
6
+ *
7
+ * `app.use(corsMiddleware)` — the shape every other framework uses — is
8
+ * **bypassable**, and not in a subtle way. Phase middleware lives inside a
9
+ * route's compiled pipeline, so it runs only when a route matched. A browser's
10
+ * preflight is `OPTIONS /api/things`, and almost no application registers an
11
+ * `OPTIONS` route; the request matches nothing, the pipeline never exists, and
12
+ * the middleware never runs. Measured on this codebase before the design was
13
+ * settled: over a matched `GET`, an unmatched path and a preflight, a `.use()`
14
+ * middleware ran **1 of 3** times and a global `onRequest` hook ran **3 of 3**.
15
+ *
16
+ * §9.2 already says this in the paragraph that made global `onRequest` hooks
17
+ * run on unmatched requests — "`onRequest` is the documented home for rate
18
+ * limiting, CORS and auth, and a rate limiter that only sees matched routes is
19
+ * bypassed by requesting a path that does not exist". CORS is the same defect
20
+ * with a different symptom: not a bypass, but a preflight that never gets an
21
+ * answer, which the browser reports as a CORS failure on the *actual* request
22
+ * and which is therefore debugged in the wrong place.
23
+ *
24
+ * So every member of this pack is a global `onRequest` hook, and therefore a
25
+ * plugin: a plugin is the only registration surface that reaches global scope
26
+ * while still carrying a manifest, a version and a config namespace (§10.1).
27
+ *
28
+ * ### Why it stages headers instead of writing them
29
+ *
30
+ * The response half of CORS is `Access-Control-Allow-Origin` on the *actual*
31
+ * response — including the 404, the 429, the 422 and the 500. A middleware that
32
+ * stamps the reply on the way out misses all of them: §4.6 says the error path
33
+ * never re-enters user middleware, and an `after` middleware on a route that
34
+ * did not match never runs at all. The classic symptom is a service whose
35
+ * errors show up in the browser as CORS failures, so every error looks like a
36
+ * configuration problem and nobody sees the actual status.
37
+ *
38
+ * `ctx.res` already solves this. Staged metadata is applied by `prepareForWire`
39
+ * at egress (§13.6), which is downstream of *every* path — success, error,
40
+ * timeout, and unmatched. So this hook stages, and one invariant covers all of
41
+ * them: **CORS never writes a reply's headers.** The consequence is asserted in
42
+ * `cors.test.ts` against a 404, a 500 and a rate-limited 429.
43
+ *
44
+ * ### The default is the absence of this plugin
45
+ *
46
+ * §19.2 says CORS is "deny all until configured", and that default is what a
47
+ * Zen app already has: with no plugin registered, no `Access-Control-Allow-*`
48
+ * header is ever emitted and every browser denies. Registering the plugin
49
+ * *without* an allowlist is therefore not "the secure default" — it is a line
50
+ * of code that asks for cross-origin access and does not say from where, which
51
+ * is indistinguishable from a bug. It is a boot error naming the fix.
52
+ */
53
+ export type CorsOrigin = '*' | string | readonly string[] | RegExp | ((origin: string) => boolean);
54
+ export interface CorsOptions {
55
+ /**
56
+ * Who may read the response. **Required** — see the note above on why the
57
+ * secure default is not registering this plugin at all.
58
+ *
59
+ * A string or list is matched by exact, case-sensitive comparison against the
60
+ * whole `Origin` header (`https://app.acme.com`), because that is what the
61
+ * header contains: a scheme, a host and an optional port, with no path and no
62
+ * trailing slash. A trailing slash is the single most common way an allowlist
63
+ * silently matches nothing, so it is a boot error rather than a mystery.
64
+ */
65
+ readonly origin?: CorsOrigin | undefined;
66
+ /**
67
+ * Methods advertised on a preflight.
68
+ *
69
+ * Defaults to **the methods this application actually serves**, read off the
70
+ * frozen `AppGraph` at boot. Every other framework hardcodes the same six
71
+ * verbs, which advertises `DELETE` on a read-only API and `PUT` on a service
72
+ * that has never had one — a small thing, but it is free here and it is the
73
+ * kind of answer only a framework with a graph can give (§2.4).
74
+ */
75
+ readonly methods?: readonly HttpMethod[] | undefined;
76
+ /**
77
+ * Request headers a cross-origin caller may send. `'reflect'` (the default)
78
+ * echoes `Access-Control-Request-Headers`.
79
+ *
80
+ * Reflecting is safe and is not the same as trusting: the browser only asks
81
+ * for headers the page itself set, and the server still validates every one
82
+ * of them. What it costs is a `Vary`, which is why an explicit list is the
83
+ * better answer for a cacheable API and why the two differ in what they emit.
84
+ */
85
+ readonly allowedHeaders?: readonly string[] | 'reflect' | undefined;
86
+ /** Response headers JavaScript may read. `Content-Type` and the other CORS-safelisted ones are always readable. */
87
+ readonly exposedHeaders?: readonly string[] | undefined;
88
+ /** Allow cookies and `Authorization`. Cannot be combined with `origin: '*'`. */
89
+ readonly credentials?: boolean | undefined;
90
+ /**
91
+ * How long a browser may cache a preflight.
92
+ *
93
+ * Defaults to **10 minutes**, which is a deliberate non-zero. With no
94
+ * `Access-Control-Max-Age` a browser re-preflights every few seconds, and
95
+ * "my API is twice as slow from the browser" is the most common CORS
96
+ * complaint that is not a misconfiguration. Ten minutes bounds how long a
97
+ * revoked origin stays cached in one browser; Chrome caps the value at two
98
+ * hours regardless.
99
+ */
100
+ readonly maxAge?: Duration | undefined;
101
+ /** `204` by default. Some legacy XHR stacks require `200` with a body. */
102
+ readonly preflightStatus?: 200 | 204 | undefined;
103
+ }
104
+ /** What `cors` publishes for other plugins to read — see `security.ts`. */
105
+ export interface CorsExports {
106
+ readonly origins: readonly string[] | 'any' | 'dynamic';
107
+ readonly credentials: boolean;
108
+ }
109
+ export declare function cors(options?: CorsOptions): Plugin<void, {}>;
110
+ //# sourceMappingURL=cors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cors.d.ts","sourceRoot":"","sources":["../src/cors.ts"],"names":[],"mappings":"AAAA,OAAO,EAEU,KAAK,QAAQ,EAAE,KAAK,UAAU,EAAE,KAAK,MAAM,EAC3D,MAAM,6BAA6B,CAAA;AAIpC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAEH,MAAM,MAAM,UAAU,GAClB,GAAG,GACH,MAAM,GACN,SAAS,MAAM,EAAE,GACjB,MAAM,GACN,CAAC,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,CAAA;AAEjC,MAAM,WAAW,WAAW;IAC1B;;;;;;;;;OASG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,SAAS,CAAA;IACxC;;;;;;;;OAQG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,UAAU,EAAE,GAAG,SAAS,CAAA;IACpD;;;;;;;;OAQG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,GAAG,SAAS,CAAA;IACnE,mHAAmH;IACnH,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAA;IACvD,gFAAgF;IAChF,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAC1C;;;;;;;;;OASG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAA;IACtC,0EAA0E;IAC1E,QAAQ,CAAC,eAAe,CAAC,EAAE,GAAG,GAAG,GAAG,GAAG,SAAS,CAAA;CACjD;AAID,2EAA2E;AAC3E,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,KAAK,GAAG,SAAS,CAAA;IACvD,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAA;CAC9B;AA2BD,wBAAgB,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,CAqGhE"}
package/dist/cors.js ADDED
@@ -0,0 +1,276 @@
1
+ import { definePlugin, parseDuration, Codes, ZenError, } from '@erenthedeveloper0/zen-core';
2
+ import { assertCorsCorpConsistent } from "./consistency.js";
3
+ import { headerOf, isPreflight } from "./shared.js";
4
+ const DEFAULT_METHODS = ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE'];
5
+ export function cors(options = {}) {
6
+ return definePlugin({
7
+ name: 'cors',
8
+ version: '0.1.0',
9
+ // The pack orders itself (§10.5 step 4). A 429 or a 404 must carry
10
+ // `Access-Control-Allow-Origin` or the browser reports a rate limit as a
11
+ // CORS failure, so this hook has to be staged before the limiter can
12
+ // short-circuit — whichever order the two `use()` calls appear in.
13
+ before: ['rate-limit'],
14
+ config: { namespace: 'cors', env: ['CORS_ORIGINS'] },
15
+ setup(app) {
16
+ const compiled = compile(options, app.config, app.pluginName);
17
+ const exports = {
18
+ origins: compiled.origins,
19
+ credentials: compiled.credentials,
20
+ };
21
+ // The reciprocal half of the check in `consistency.ts`: whichever of the
22
+ // two plugins is registered second sees both sides. `securityHeaders`
23
+ // declares `before: ['cors']`, so in practice that is this one.
24
+ const security = app.exportsOf('security-headers');
25
+ if (security !== undefined) {
26
+ assertCorsCorpConsistent(security.crossOriginResource ?? 'same-site', exports, app.pluginName);
27
+ }
28
+ app.hook('onRequest', function cors(ctx) {
29
+ // `Vary: Origin` first, and **before** the early return.
30
+ //
31
+ // The tempting shape is to read `Origin`, bail when it is absent, and
32
+ // only then vary — every CORS library does it that way and it is
33
+ // wrong for caching. A request with no `Origin` produces a response
34
+ // with no `Access-Control-Allow-Origin`, which is a *different*
35
+ // response; a shared cache that stored it without `Vary` would replay
36
+ // it to a browser request that needed the header, and the browser
37
+ // would deny a caller that is on the allowlist. The failure only
38
+ // appears behind a CDN, only for some users, and never in a test.
39
+ //
40
+ // So the cost is one array push on every request in the application,
41
+ // paid for a correctness property, and §32.3 measures it rather than
42
+ // asserting it is small. The one configuration that skips it is a
43
+ // literal `*` with no credentials, where the answer genuinely does not
44
+ // depend on the origin.
45
+ if (!compiled.anyOrigin)
46
+ ctx.res.appendHeader('vary', 'origin');
47
+ // Read through `raw` rather than `ctx.headers`, which materialises a
48
+ // record of every header on first touch (§7.2). This hook runs on every
49
+ // request in the application and must not be the reason that exists.
50
+ const origin = headerOf(ctx, 'origin');
51
+ if (origin === undefined)
52
+ return undefined;
53
+ const preflight = isPreflight(ctx);
54
+ const allowed = compiled.matcher(origin);
55
+ if (allowed) {
56
+ ctx.res.header('access-control-allow-origin', compiled.anyOrigin ? '*' : origin);
57
+ if (compiled.credentials)
58
+ ctx.res.header('access-control-allow-credentials', 'true');
59
+ if (!preflight && compiled.exposeHeaders !== null) {
60
+ ctx.res.header('access-control-expose-headers', compiled.exposeHeaders);
61
+ }
62
+ }
63
+ if (!preflight)
64
+ return undefined;
65
+ // ── preflight ───────────────────────────────────────────────────────
66
+ // Answered here, before routing, body intake, validation and the
67
+ // handler — which is the point of §4.2 stage 5. A preflight that
68
+ // reached a route would have to be a route somebody wrote.
69
+ ctx.res.appendHeader('vary', 'access-control-request-method');
70
+ if (allowed) {
71
+ ctx.res.header('access-control-allow-methods', compiled.allowMethods);
72
+ if (compiled.allowHeaders === 'reflect') {
73
+ const asked = headerOf(ctx, 'access-control-request-headers');
74
+ ctx.res.appendHeader('vary', 'access-control-request-headers');
75
+ if (asked !== undefined)
76
+ ctx.res.header('access-control-allow-headers', asked);
77
+ }
78
+ else if (compiled.allowHeaders !== null) {
79
+ ctx.res.header('access-control-allow-headers', compiled.allowHeaders);
80
+ }
81
+ if (compiled.maxAge !== null)
82
+ ctx.res.header('access-control-max-age', compiled.maxAge);
83
+ }
84
+ // A disallowed origin still gets a well-formed 204 with no CORS
85
+ // headers, which is what makes the browser deny. A 403 would say
86
+ // "this origin is not on the list", and an allowlist that reports its
87
+ // own contents is an allowlist you can enumerate.
88
+ return ctx.empty(compiled.preflightStatus);
89
+ }, 'cors');
90
+ // §2.4 — the graph is the one structure everything reads. The advertised
91
+ // method set is a projection of it, computed once, so it cannot claim a
92
+ // verb the router would answer 405 for.
93
+ app.onBoot((graph) => {
94
+ if (compiled.methodsPinned)
95
+ return;
96
+ compiled.allowMethods = servedMethods(graph).join(', ');
97
+ });
98
+ // Spread rather than passed by reference: `PluginResult.exports` is a
99
+ // `Record<string, unknown>`, and a named interface has no index signature.
100
+ return { exports: { ...exports } };
101
+ },
102
+ });
103
+ }
104
+ /**
105
+ * The verbs this application answers, `OPTIONS` excluded.
106
+ *
107
+ * `HEAD` is added whenever any `GET` exists, because §4.2 gives every `GET`
108
+ * route a free `HEAD` that never appears as a `RouteRecord` — advertising the
109
+ * declared set alone would omit a method the server demonstrably serves.
110
+ */
111
+ function servedMethods(graph) {
112
+ const seen = new Set();
113
+ for (const route of graph.routes)
114
+ seen.add(route.method);
115
+ if (seen.has('GET'))
116
+ seen.add('HEAD');
117
+ seen.delete('OPTIONS');
118
+ const order = ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'TRACE'];
119
+ const out = order.filter((m) => seen.has(m));
120
+ return out.length === 0 ? [...DEFAULT_METHODS] : out;
121
+ }
122
+ /**
123
+ * Options → the decided form, with everything that can be wrong reported here.
124
+ *
125
+ * Thrown rather than collected, because §12.7's aggregation is the *app's*
126
+ * list and a plugin reaches it by throwing from `setup` — `ready()` wraps it
127
+ * into a `ZEN_PLUGIN_OPTIONS` diagnostic that names the plugin and everything
128
+ * that will now not load.
129
+ */
130
+ function compile(written, config, pluginName) {
131
+ const options = merge(written, config, pluginName);
132
+ const origin = options.origin;
133
+ if (origin === undefined) {
134
+ throw new ZenError(Codes.CONFIG_INVALID, `${pluginName} needs an origin allowlist, and refuses to guess one.`, {
135
+ status: 500,
136
+ expose: false,
137
+ hint: "Pass one — cors({ origin: ['https://app.example.com'] }) — or set `cors.origin` in " +
138
+ 'configuration so a deployment can supply it from CORS_ORIGINS.',
139
+ consequence: 'Not registering this plugin at all is the deny-everything default (§19.2): with no ' +
140
+ 'Access-Control-Allow-Origin header, every browser already denies. Registering it ' +
141
+ 'without a list is a request for cross-origin access that does not say from where.',
142
+ });
143
+ }
144
+ const anyOrigin = origin === '*';
145
+ const credentials = options.credentials === true;
146
+ if (anyOrigin && credentials) {
147
+ throw new ZenError(Codes.CONFIG_INVALID, `${pluginName} cannot combine origin: '*' with credentials: true.`, {
148
+ status: 500,
149
+ expose: false,
150
+ hint: 'Name the origins that may send credentials, or drop credentials.',
151
+ consequence: 'Browsers reject the pair outright. The usual library "fix" is to reflect whatever ' +
152
+ 'Origin arrives, which turns an allowlist into allow-everyone while still reading ' +
153
+ "like an allowlist — so this is refused rather than quietly repaired.",
154
+ });
155
+ }
156
+ const list = typeof origin === 'string' && !anyOrigin ? [origin]
157
+ : Array.isArray(origin) ? origin
158
+ : null;
159
+ if (list !== null) {
160
+ for (const entry of list) {
161
+ if (entry.endsWith('/') || entry.includes('/', 8)) {
162
+ throw new ZenError(Codes.CONFIG_INVALID, `${pluginName}: "${entry}" is not an origin — an Origin header carries a scheme, host and optional port, and no path.`, {
163
+ status: 500,
164
+ expose: false,
165
+ hint: `Use "${entry.replace(/\/+$/, '').replace(/^(\w+:\/\/[^/]+).*$/, '$1')}".`,
166
+ consequence: 'A trailing slash matches no origin, so the allowlist would silently allow nothing.',
167
+ });
168
+ }
169
+ }
170
+ }
171
+ const matcher = anyOrigin ? () => true
172
+ : list !== null
173
+ ? list.length === 1
174
+ // One origin is the common case and deserves a comparison rather than
175
+ // a Set lookup; more than one, and the Set wins from about four.
176
+ ? ((only) => (o) => o === only)(list[0])
177
+ : ((set) => (o) => set.has(o))(new Set(list))
178
+ : origin instanceof RegExp
179
+ // A fresh test each call: a global regex carries `lastIndex` between
180
+ // calls and would match every other request. §19.3 forbids unbounded
181
+ // backtracking in framework source; a user's own pattern is their
182
+ // choice, and it is named in `explainRoute` as `cors`.
183
+ ? ((re) => (o) => new RegExp(re.source, re.flags.replace('g', '')).test(o))(origin)
184
+ : origin;
185
+ return {
186
+ anyOrigin,
187
+ matcher,
188
+ credentials,
189
+ allowHeaders: options.allowedHeaders === undefined ? 'reflect'
190
+ : options.allowedHeaders === 'reflect' ? 'reflect'
191
+ : options.allowedHeaders.length === 0 ? null
192
+ : options.allowedHeaders.join(', '),
193
+ exposeHeaders: options.exposedHeaders === undefined || options.exposedHeaders.length === 0
194
+ ? null
195
+ : options.exposedHeaders.join(', '),
196
+ maxAge: maxAgeOf(options.maxAge),
197
+ preflightStatus: options.preflightStatus ?? 204,
198
+ origins: anyOrigin ? 'any'
199
+ : list !== null ? [...list]
200
+ : 'dynamic',
201
+ methodsPinned: options.methods !== undefined,
202
+ allowMethods: (options.methods ?? DEFAULT_METHODS).join(', '),
203
+ };
204
+ }
205
+ function maxAgeOf(value) {
206
+ const ms = parseDuration(value ?? '10m');
207
+ return ms <= 0 ? null : String(Math.floor(ms / 1000));
208
+ }
209
+ /**
210
+ * `config.cors.*` under what the call site wrote — §16.1's precedence, applied
211
+ * field by field.
212
+ *
213
+ * The first draft read only `origin` from configuration, and the example caught
214
+ * it immediately: `zen.config.ts` declared `cors.credentials: true`, `cors()`
215
+ * was called with no arguments, and the header was silently absent. Half a
216
+ * feature is worse than none here, because "configuration is ignored" is
217
+ * indistinguishable from "the browser is wrong" from the outside.
218
+ *
219
+ * Field by field rather than a spread for the same reason plugin defaults merge
220
+ * that way (§16.1 layer 2): `cors({ credentials: true })` next to a config that
221
+ * names the origins should end up with both, not with whichever object was
222
+ * spread last.
223
+ *
224
+ * A config value of the wrong *shape* is a boot error rather than something
225
+ * quietly dropped. `cors.origin: 42` is a mistake somebody made, and the two
226
+ * ways to not report it — ignore it, or coerce it — are how an allowlist ends
227
+ * up meaning something nobody wrote.
228
+ */
229
+ function merge(written, config, pluginName) {
230
+ const namespace = config['cors'];
231
+ if (namespace === undefined || namespace === null)
232
+ return written;
233
+ if (typeof namespace !== 'object' || Array.isArray(namespace)) {
234
+ throw new ZenError(Codes.CONFIG_INVALID, `${pluginName}: config.cors must be an object of options, and is ${describe(namespace)}.`, {
235
+ status: 500,
236
+ expose: false,
237
+ hint: 'Give it a namespace: `cors: { origin: env => env.CORS_ORIGINS.split(",") }`.',
238
+ consequence: 'Nothing under config.cors can be read while it is not an object.',
239
+ });
240
+ }
241
+ const from = namespace;
242
+ const take = (key, ok) => {
243
+ const value = from[key];
244
+ if (value === undefined)
245
+ return undefined;
246
+ if (!ok(value)) {
247
+ throw new ZenError(Codes.CONFIG_INVALID, `${pluginName}: config.cors.${key} is ${describe(value)}, which is not a valid ${key}.`, {
248
+ status: 500,
249
+ expose: false,
250
+ hint: `Fix the value in the configuration file, or pass ${key} to cors() directly.`,
251
+ consequence: 'Ignoring it would make the running configuration differ from the written one.',
252
+ });
253
+ }
254
+ return value;
255
+ };
256
+ const isStringList = (v) => Array.isArray(v) && v.every((e) => typeof e === 'string');
257
+ return {
258
+ origin: written.origin ?? take('origin', (v) => typeof v === 'string' || isStringList(v) || v instanceof RegExp || typeof v === 'function'),
259
+ methods: written.methods ?? take('methods', isStringList),
260
+ allowedHeaders: written.allowedHeaders ?? take('allowedHeaders', (v) => v === 'reflect' || isStringList(v)),
261
+ exposedHeaders: written.exposedHeaders ?? take('exposedHeaders', isStringList),
262
+ credentials: written.credentials ?? take('credentials', (v) => typeof v === 'boolean'),
263
+ maxAge: written.maxAge ?? take('maxAge', (v) => typeof v === 'number' || typeof v === 'string'),
264
+ preflightStatus: written.preflightStatus ?? take('preflightStatus', (v) => v === 200 || v === 204),
265
+ };
266
+ }
267
+ function describe(value) {
268
+ if (value === null)
269
+ return 'null';
270
+ if (Array.isArray(value))
271
+ return `an array (${JSON.stringify(value).slice(0, 40)})`;
272
+ if (typeof value === 'object')
273
+ return 'an object';
274
+ return `${typeof value} ${JSON.stringify(value)}`;
275
+ }
276
+ //# sourceMappingURL=cors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cors.js","sourceRoot":"","sources":["../src/cors.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * `@erenthedeveloper0/zen-middleware` — the first-party pack (rfcs/0001 §24.2, §25 M4, §32).
3
+ *
4
+ * Four plugins, one shape: **a global `onRequest` hook that stages response
5
+ * metadata**. Both halves of that sentence are load-bearing and both were
6
+ * decided by measurement rather than by taste — see §32.1 and §32.2, or the
7
+ * header comment on `cors.ts`, which carries the numbers.
8
+ *
9
+ * What is *not* here, named rather than left as a silent gap:
10
+ *
11
+ * - **`compression` and `static`.** Both need a platform: `node:zlib` and
12
+ * `node:fs`. §14.1 already puts compression on the adapter boundary as a
13
+ * capability (`compression: 'native' | 'library' | 'none'`), which is the
14
+ * right home for it — a middleware package that imported `node:zlib` would
15
+ * be a package the edge adapters cannot load. They belong to an
16
+ * adapter-coupled package and are recorded in §28.8.
17
+ * - **`timeout` and `body-limit`**, which §24.2 lists here. Both are already
18
+ * built into core as first-class route policy — §4.4's deadlines and §19.2's
19
+ * body limits — and a middleware wrapping them would be a second way to say
20
+ * the same thing, with its own precedence rules for the case where both are
21
+ * set. §24.2's row predates both features.
22
+ */
23
+ export { cors, type CorsOptions, type CorsOrigin, type CorsExports } from './cors.ts';
24
+ export { securityHeaders, type SecurityHeadersOptions, type ReferrerPolicy, } from './security.ts';
25
+ export { requestId, type RequestIdOptions } from './request-id.ts';
26
+ export { rateLimit, type RateLimitOptions, type RateLimitContext, } from './rate-limit.ts';
27
+ export { MemoryStore, ReferenceStore, type Store, type Tally, type MemoryStoreOptions, } from './store.ts';
28
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,IAAI,EAAE,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAA;AACrF,OAAO,EACL,eAAe,EAAE,KAAK,sBAAsB,EAAE,KAAK,cAAc,GAClE,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAClE,OAAO,EACL,SAAS,EAAE,KAAK,gBAAgB,EAAE,KAAK,gBAAgB,GACxD,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,WAAW,EAAE,cAAc,EAAE,KAAK,KAAK,EAAE,KAAK,KAAK,EAAE,KAAK,kBAAkB,GAC7E,MAAM,YAAY,CAAA"}