@nifrajs/middleware 3.3.0 → 4.0.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 +11 -6
- package/dist/_ip.d.ts +17 -0
- package/dist/_ip.d.ts.map +1 -0
- package/dist/_ip.js +91 -0
- package/dist/_ip.js.map +1 -0
- package/dist/_utils.d.ts +16 -0
- package/dist/_utils.d.ts.map +1 -1
- package/dist/_utils.js +33 -0
- package/dist/_utils.js.map +1 -1
- package/dist/basic-auth.d.ts.map +1 -1
- package/dist/basic-auth.js +2 -2
- package/dist/basic-auth.js.map +1 -1
- package/dist/body-limit.js +2 -2
- package/dist/body-limit.js.map +1 -1
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +110 -22
- package/dist/cache.js.map +1 -1
- package/dist/compression.d.ts.map +1 -1
- package/dist/compression.js +13 -2
- package/dist/compression.js.map +1 -1
- package/dist/cors.d.ts.map +1 -1
- package/dist/cors.js +50 -5
- package/dist/cors.js.map +1 -1
- package/dist/csrf.d.ts +21 -2
- package/dist/csrf.d.ts.map +1 -1
- package/dist/csrf.js +99 -10
- package/dist/csrf.js.map +1 -1
- package/dist/etag.d.ts.map +1 -1
- package/dist/etag.js +7 -1
- package/dist/etag.js.map +1 -1
- package/dist/idempotency.d.ts +16 -7
- package/dist/idempotency.d.ts.map +1 -1
- package/dist/idempotency.js +51 -22
- package/dist/idempotency.js.map +1 -1
- package/dist/ip-restriction.d.ts +12 -7
- package/dist/ip-restriction.d.ts.map +1 -1
- package/dist/ip-restriction.js +25 -72
- package/dist/ip-restriction.js.map +1 -1
- package/dist/jwt.d.ts.map +1 -1
- package/dist/jwt.js +3 -3
- package/dist/jwt.js.map +1 -1
- package/dist/language.d.ts.map +1 -1
- package/dist/language.js +4 -1
- package/dist/language.js.map +1 -1
- package/dist/multipart.d.ts.map +1 -1
- package/dist/multipart.js +6 -1
- package/dist/multipart.js.map +1 -1
- package/dist/pretty-json.d.ts.map +1 -1
- package/dist/pretty-json.js +63 -7
- package/dist/pretty-json.js.map +1 -1
- package/dist/problem-details.d.ts.map +1 -1
- package/dist/problem-details.js +8 -1
- package/dist/problem-details.js.map +1 -1
- package/dist/range.d.ts.map +1 -1
- package/dist/range.js +5 -2
- package/dist/range.js.map +1 -1
- package/dist/rate-limit.d.ts +11 -10
- package/dist/rate-limit.d.ts.map +1 -1
- package/dist/rate-limit.js +29 -19
- package/dist/rate-limit.js.map +1 -1
- package/dist/request-id.d.ts +8 -1
- package/dist/request-id.d.ts.map +1 -1
- package/dist/request-id.js +6 -3
- package/dist/request-id.js.map +1 -1
- package/dist/security-headers.d.ts +4 -0
- package/dist/security-headers.d.ts.map +1 -1
- package/dist/security-headers.js +7 -1
- package/dist/security-headers.js.map +1 -1
- package/dist/token-auth.d.ts.map +1 -1
- package/dist/token-auth.js +3 -5
- package/dist/token-auth.js.map +1 -1
- package/dist/trailing-slash.d.ts.map +1 -1
- package/dist/trailing-slash.js +12 -1
- package/dist/trailing-slash.js.map +1 -1
- package/package.json +6 -3
- package/src/_ip.ts +87 -0
- package/src/_utils.ts +37 -0
- package/src/basic-auth.ts +2 -1
- package/src/body-limit.ts +2 -2
- package/src/cache.ts +119 -21
- package/src/compression.ts +13 -2
- package/src/cors.ts +51 -7
- package/src/csrf.ts +115 -10
- package/src/etag.ts +11 -1
- package/src/idempotency.ts +76 -28
- package/src/ip-restriction.ts +38 -75
- package/src/jwt.ts +4 -2
- package/src/language.ts +5 -1
- package/src/multipart.ts +6 -1
- package/src/pretty-json.ts +56 -7
- package/src/problem-details.ts +8 -1
- package/src/range.ts +6 -3
- package/src/rate-limit.ts +48 -31
- package/src/request-id.ts +16 -4
- package/src/security-headers.ts +7 -1
- package/src/token-auth.ts +3 -5
- package/src/trailing-slash.ts +13 -1
|
@@ -33,6 +33,10 @@ export interface SecurityHeadersOptions {
|
|
|
33
33
|
* Every value is fixed at construction, so these are declared statically rather than written by a
|
|
34
34
|
* response hook: an app whose response middleware is only this keeps the fused native lanes. A route
|
|
35
35
|
* that sets one of these names itself keeps its own value.
|
|
36
|
+
*
|
|
37
|
+
* A group can apply its own configuration over the app's: `group.use(securityHeaders({ ... }))`
|
|
38
|
+
* replaces the app's value of each header it sets, on the group's routes only. Applying the same
|
|
39
|
+
* configuration twice is a no-op.
|
|
36
40
|
*/
|
|
37
41
|
export declare function securityHeaders(options?: SecurityHeadersOptions): Middleware;
|
|
38
42
|
//# sourceMappingURL=security-headers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"security-headers.d.ts","sourceRoot":"","sources":["../src/security-headers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAEtD,MAAM,WAAW,sBAAsB;IACrC,+FAA+F;IAC/F,QAAQ,CAAC,IAAI,CAAC,EAAE;QACd,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;QACvB,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;QACpC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAC3B,CAAA;IACD,sEAAsE;IACtE,QAAQ,CAAC,qBAAqB,CAAC,EAAE,MAAM,CAAA;IACvC,2CAA2C;IAC3C,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,YAAY,CAAA;IAC7C,kDAAkD;IAClD,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;IAChC;sFACkF;IAClF,QAAQ,CAAC,uBAAuB,CAAC,EAAE,aAAa,GAAG,0BAA0B,GAAG,aAAa,CAAA;IAC7F;sFACkF;IAClF,QAAQ,CAAC,yBAAyB,CAAC,EAAE,cAAc,GAAG,gBAAgB,GAAG,aAAa,CAAA;IACtF;uCACmC;IACnC,QAAQ,CAAC,yBAAyB,CAAC,EAAE,aAAa,GAAG,WAAW,GAAG,cAAc,CAAA;IACjF,qGAAqG;IACrG,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;CACpC;AAED
|
|
1
|
+
{"version":3,"file":"security-headers.d.ts","sourceRoot":"","sources":["../src/security-headers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAEtD,MAAM,WAAW,sBAAsB;IACrC,+FAA+F;IAC/F,QAAQ,CAAC,IAAI,CAAC,EAAE;QACd,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;QACvB,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;QACpC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAC3B,CAAA;IACD,sEAAsE;IACtE,QAAQ,CAAC,qBAAqB,CAAC,EAAE,MAAM,CAAA;IACvC,2CAA2C;IAC3C,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,YAAY,CAAA;IAC7C,kDAAkD;IAClD,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;IAChC;sFACkF;IAClF,QAAQ,CAAC,uBAAuB,CAAC,EAAE,aAAa,GAAG,0BAA0B,GAAG,aAAa,CAAA;IAC7F;sFACkF;IAClF,QAAQ,CAAC,yBAAyB,CAAC,EAAE,cAAc,GAAG,gBAAgB,GAAG,aAAa,CAAA;IACtF;uCACmC;IACnC,QAAQ,CAAC,yBAAyB,CAAC,EAAE,aAAa,GAAG,WAAW,GAAG,cAAc,CAAA;IACjF,qGAAqG;IACrG,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;CACpC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,OAAO,GAAE,sBAA2B,GAAG,UAAU,CA8ChF"}
|
package/dist/security-headers.js
CHANGED
|
@@ -8,6 +8,10 @@ import { NIFRA_ASSURANCE, withRouteAssurance } from "@nifrajs/core/assurance";
|
|
|
8
8
|
* Every value is fixed at construction, so these are declared statically rather than written by a
|
|
9
9
|
* response hook: an app whose response middleware is only this keeps the fused native lanes. A route
|
|
10
10
|
* that sets one of these names itself keeps its own value.
|
|
11
|
+
*
|
|
12
|
+
* A group can apply its own configuration over the app's: `group.use(securityHeaders({ ... }))`
|
|
13
|
+
* replaces the app's value of each header it sets, on the group's routes only. Applying the same
|
|
14
|
+
* configuration twice is a no-op.
|
|
11
15
|
*/
|
|
12
16
|
export function securityHeaders(options = {}) {
|
|
13
17
|
const frameOptions = options.frameOptions ?? "DENY";
|
|
@@ -44,7 +48,9 @@ export function securityHeaders(options = {}) {
|
|
|
44
48
|
declared["permissions-policy"] = options.permissionsPolicy;
|
|
45
49
|
}
|
|
46
50
|
return withRouteAssurance({
|
|
47
|
-
name:
|
|
51
|
+
// The configuration is part of the name: the same one applied twice dedupes, while a group's
|
|
52
|
+
// different configuration applies over its parent's instead of being skipped as a repeat.
|
|
53
|
+
name: `security-headers:${JSON.stringify(declared)}`,
|
|
48
54
|
responseHeaders: declared,
|
|
49
55
|
}, {
|
|
50
56
|
id: NIFRA_ASSURANCE.SECURITY_HEADERS,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"security-headers.js","sourceRoot":"","sources":["../src/security-headers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AA6B7E
|
|
1
|
+
{"version":3,"file":"security-headers.js","sourceRoot":"","sources":["../src/security-headers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AA6B7E;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,eAAe,CAAC,UAAkC,EAAE;IAClE,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,MAAM,CAAA;IACnD,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,IAAI,aAAa,CAAA;IAC9D,MAAM,GAAG,GAAG,OAAO,CAAC,qBAAqB,CAAA;IAEzC,IAAI,SAA6B,CAAA;IACjC,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC/B,MAAM,KAAK,GAAG,CAAC,WAAW,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAA;QAChD,IAAI,OAAO,CAAC,IAAI,CAAC,iBAAiB;YAAE,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAA;QACnE,IAAI,OAAO,CAAC,IAAI,CAAC,OAAO;YAAE,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QAC/C,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC9B,CAAC;IAED,MAAM,QAAQ,GAA2B;QACvC,wBAAwB,EAAE,SAAS;QACnC,iBAAiB,EAAE,YAAY;QAC/B,iBAAiB,EAAE,cAAc;KAClC,CAAA;IACD,IAAI,SAAS,KAAK,SAAS;QAAE,QAAQ,CAAC,2BAA2B,CAAC,GAAG,SAAS,CAAA;IAC9E,IAAI,GAAG,KAAK,SAAS;QAAE,QAAQ,CAAC,yBAAyB,CAAC,GAAG,GAAG,CAAA;IAChE,IAAI,OAAO,CAAC,uBAAuB,KAAK,SAAS,EAAE,CAAC;QAClD,QAAQ,CAAC,4BAA4B,CAAC,GAAG,OAAO,CAAC,uBAAuB,CAAA;IAC1E,CAAC;IACD,IAAI,OAAO,CAAC,yBAAyB,KAAK,SAAS,EAAE,CAAC;QACpD,QAAQ,CAAC,8BAA8B,CAAC,GAAG,OAAO,CAAC,yBAAyB,CAAA;IAC9E,CAAC;IACD,IAAI,OAAO,CAAC,yBAAyB,KAAK,SAAS,EAAE,CAAC;QACpD,QAAQ,CAAC,8BAA8B,CAAC,GAAG,OAAO,CAAC,yBAAyB,CAAA;IAC9E,CAAC;IACD,IAAI,OAAO,CAAC,iBAAiB,KAAK,SAAS,EAAE,CAAC;QAC5C,QAAQ,CAAC,oBAAoB,CAAC,GAAG,OAAO,CAAC,iBAAiB,CAAA;IAC5D,CAAC;IAED,OAAO,kBAAkB,CACvB;QACE,6FAA6F;QAC7F,0FAA0F;QAC1F,IAAI,EAAE,oBAAoB,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE;QACpD,eAAe,EAAE,QAAQ;KAC1B,EACD;QACE,EAAE,EAAE,eAAe,CAAC,gBAAgB;QACpC,MAAM,EAAE,kBAAkB;QAC1B,KAAK,EAAE,QAAQ;KAChB,CACF,CAAA;AACH,CAAC"}
|
package/dist/token-auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-auth.d.ts","sourceRoot":"","sources":["../src/token-auth.ts"],"names":[],"mappings":"AACA,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"token-auth.d.ts","sourceRoot":"","sources":["../src/token-auth.ts"],"names":[],"mappings":"AACA,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAGhF,KAAK,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;AAErC;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI,cAAc,GAAG;IAC3C,gGAAgG;IAChG,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,CAAC,GAAG,IAAI,CAAA;IACrC,4EAA4E;IAC5E,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,CAAC,CAAA;CACtC,CAAA;AAqDD,MAAM,WAAW,aAAa,CAAC,CAAC;IAC9B;uGACmG;IACnG,QAAQ,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,YAAY,CAAC,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC,CAAA;IACtE,yGAAyG;IACzG,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAC3B,mFAAmF;IACnF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CACxB;AAED;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CASlE;AAED,MAAM,WAAW,mBAAmB,CAAC,CAAC;IACpC,sFAAsF;IACtF,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,YAAY,CAAC,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC,CAAA;IACpE,sDAAsD;IACtD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC;0EACsE;IACtE,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAC5B;AAoCD;;;;;;;GAOG;AACH,wBAAgB,MAAM,CAAC,OAAO,EAAE,mBAAmB,GAAG,UAAU,CAAC,MAAM,CAAC,CAAA;AACxE,wBAAgB,MAAM,CAAC,CAAC,EAAE,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAA"}
|
package/dist/token-auth.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NIFRA_ASSURANCE, withRouteAssurance } from "@nifrajs/core/assurance";
|
|
2
2
|
import { defineIdentityPlugin } from "@nifrajs/core/server";
|
|
3
|
+
import { bearerToken, guardName } from "./_utils.js";
|
|
3
4
|
function createTokenAuth(config) {
|
|
4
5
|
// Per-request principal cache: verify runs once (in beforeHandle), reads are free + can't desync.
|
|
5
6
|
const store = new WeakMap();
|
|
@@ -7,7 +8,7 @@ function createTokenAuth(config) {
|
|
|
7
8
|
status: 401,
|
|
8
9
|
headers: config.challenge !== undefined ? { "www-authenticate": config.challenge } : {},
|
|
9
10
|
});
|
|
10
|
-
const plugin = defineIdentityPlugin(config.name, (app) => app.beforeHandle(async (c) => {
|
|
11
|
+
const plugin = defineIdentityPlugin(guardName(config.name), (app) => app.beforeHandle(async (c) => {
|
|
11
12
|
const token = config.extract(c.req);
|
|
12
13
|
// Empty string is treated as "no credential" - never passed to verify.
|
|
13
14
|
const principal = token !== undefined && token !== "" ? await config.verify(token) : null;
|
|
@@ -43,10 +44,7 @@ export function bearer(options) {
|
|
|
43
44
|
const realm = options.realm ?? "api";
|
|
44
45
|
return createTokenAuth({
|
|
45
46
|
name: "bearer",
|
|
46
|
-
extract: (request) =>
|
|
47
|
-
const header = request.headers.get("authorization");
|
|
48
|
-
return header?.startsWith("Bearer ") === true ? header.slice(7).trim() : undefined;
|
|
49
|
-
},
|
|
47
|
+
extract: (request) => bearerToken(request.headers.get("authorization")) ?? undefined,
|
|
50
48
|
verify: options.verify,
|
|
51
49
|
optional: options.optional === true,
|
|
52
50
|
challenge: `Bearer realm="${realm}"`,
|
package/dist/token-auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-auth.js","sourceRoot":"","sources":["../src/token-auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAC7E,OAAO,EAAE,oBAAoB,EAAuB,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"token-auth.js","sourceRoot":"","sources":["../src/token-auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAC7E,OAAO,EAAE,oBAAoB,EAAuB,MAAM,sBAAsB,CAAA;AAChF,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAsCpD,SAAS,eAAe,CAAI,MAA0B;IACpD,kGAAkG;IAClG,MAAM,KAAK,GAAG,IAAI,OAAO,EAAc,CAAA;IACvC,MAAM,MAAM,GAAG,GAAa,EAAE,CAC5B,QAAQ,CAAC,IAAI,CACX,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,EACpC;QACE,MAAM,EAAE,GAAG;QACX,OAAO,EAAE,MAAM,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,kBAAkB,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE;KACxF,CACF,CAAA;IACH,MAAM,MAAM,GAAG,oBAAoB,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAClE,GAAG,CAAC,YAAY,CAAC,KAAK,EAAE,CAA4B,EAAE,EAAE;QACtD,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;QACnC,uEAAuE;QACvE,MAAM,SAAS,GAAG,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,MAAM,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;QACzF,IAAI,SAAS,KAAK,IAAI,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAClD,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,SAAS,CAAC,CAAA;YAC3B,OAAO,SAAS,CAAA,CAAC,uCAAuC;QAC1D,CAAC;QACD,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,EAAE,CAAA,CAAC,yCAAyC;IACzF,CAAC,CAAC,CACH,CAAA;IACD,MAAM,YAAY,GAAG,MAAM,CAAC,QAAQ;QAClC,CAAC,CAAC,MAAM;QACR,CAAC,CAAC,kBAAkB,CAAC,MAAM,EAAE;YACzB,EAAE,EAAE,eAAe,CAAC,aAAa;YACjC,MAAM,EAAE,MAAM,CAAC,IAAI;YACnB,KAAK,EAAE,YAAY;SACpB,CAAC,CAAA;IACN,OAAO,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE;QACjC,SAAS,EAAE,CAAC,OAAgB,EAAY,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI;QACrE,gBAAgB,EAAE,CAAC,OAAgB,EAAK,EAAE;YACxC,MAAM,SAAS,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;YACpC,IAAI,SAAS,KAAK,SAAS;gBAAE,MAAM,MAAM,EAAE,CAAA;YAC3C,OAAO,SAAS,CAAA;QAClB,CAAC;KACF,CAAkB,CAAA;AACrB,CAAC;AAYD;;;;GAIG;AACH,MAAM,UAAU,MAAM,CAAI,OAAyB;IACjD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,KAAK,CAAA;IACpC,OAAO,eAAe,CAAC;QACrB,IAAI,EAAE,QAAQ;QACd,OAAO,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,IAAI,SAAS;QACpF,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,QAAQ,EAAE,OAAO,CAAC,QAAQ,KAAK,IAAI;QACnC,SAAS,EAAE,iBAAiB,KAAK,GAAG;KACrC,CAAC,CAAA;AACJ,CAAC;AAkBD,MAAM,WAAW,GAAG,IAAI,WAAW,EAAE,CAAA;AAErC,KAAK,UAAU,MAAM,CAAC,KAAa;IACjC,OAAO,IAAI,UAAU,CAAC,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;AACzF,CAAC;AAED;oEACoE;AACpE,SAAS,oBAAoB,CAAC,CAAa,EAAE,CAAa;IACxD,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IACvC,IAAI,IAAI,GAAG,CAAC,CAAA;IACZ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;IACpE,OAAO,IAAI,KAAK,CAAC,CAAA;AACnB,CAAC;AAED;;;;;GAKG;AACH,SAAS,eAAe,CAAC,IAAuB;IAC9C,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;IAChE,OAAO,KAAK,EAAE,SAAS,EAAE,EAAE;QACzB,MAAM,eAAe,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,CAAA;QAC/C,IAAI,OAAO,GAAG,KAAK,CAAA;QACnB,KAAK,MAAM,MAAM,IAAI,MAAM,YAAY,EAAE,CAAC;YACxC,kGAAkG;YAClG,IAAI,oBAAoB,CAAC,eAAe,EAAE,MAAM,CAAC;gBAAE,OAAO,GAAG,IAAI,CAAA;QACnE,CAAC;QACD,OAAO,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAA;IACnC,CAAC,CAAA;AACH,CAAC;AAYD,MAAM,UAAU,MAAM,CACpB,OAAqD;IAErD,MAAM,MAAM,GAAG,CAAC,OAAO,CAAC,MAAM,IAAI,WAAW,CAAC,CAAC,WAAW,EAAE,CAAA;IAC5D,MAAM,MAAM,GAAG,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAA;IACjF,OAAO,eAAe,CAAa;QACjC,IAAI,EAAE,QAAQ;QACd,OAAO,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,SAAS;QAC9D,MAAM;QACN,QAAQ,EAAE,OAAO,CAAC,QAAQ,KAAK,IAAI;KACpC,CAAC,CAAA;AACJ,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trailing-slash.d.ts","sourceRoot":"","sources":["../src/trailing-slash.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,oBAAoB;IACnC,mEAAmE;IACnE,QAAQ,CAAC,IAAI,CAAC,EAAE,UAAU,GAAG,SAAS,CAAA;IACtC,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,CAAC,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAA;IACvC,uGAAuG;IACvG,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC,oGAAoG;IACpG,QAAQ,CAAC,oBAAoB,CAAC,EAAE,OAAO,CAAA;IACvC,mCAAmC;IACnC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,OAAO,CAAA;CAC9D;
|
|
1
|
+
{"version":3,"file":"trailing-slash.d.ts","sourceRoot":"","sources":["../src/trailing-slash.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,oBAAoB;IACnC,mEAAmE;IACnE,QAAQ,CAAC,IAAI,CAAC,EAAE,UAAU,GAAG,SAAS,CAAA;IACtC,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,CAAC,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAA;IACvC,uGAAuG;IACvG,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC,oGAAoG;IACpG,QAAQ,CAAC,oBAAoB,CAAC,EAAE,OAAO,CAAA;IACvC,mCAAmC;IACnC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,OAAO,CAAA;CAC9D;AAkDD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,oBAAyB,iDAenE;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,oBAAyB,iDAiBrE"}
|
package/dist/trailing-slash.js
CHANGED
|
@@ -16,10 +16,21 @@ function withPath(req, path) {
|
|
|
16
16
|
url.pathname = path;
|
|
17
17
|
return url;
|
|
18
18
|
}
|
|
19
|
+
const relativeLocation = (url) => {
|
|
20
|
+
// A path beginning with `//` is parsed as a network-path reference and can still redirect to a
|
|
21
|
+
// different host. Prefix it with `/.` so URL resolution keeps the request authority.
|
|
22
|
+
const pathname = url.pathname.startsWith("//") ? `/.${url.pathname}` : url.pathname;
|
|
23
|
+
return `${pathname}${url.search}`;
|
|
24
|
+
};
|
|
19
25
|
function route(req, url, mode, status) {
|
|
20
26
|
return mode === "rewrite"
|
|
21
27
|
? new Request(url.toString(), req)
|
|
22
|
-
:
|
|
28
|
+
: // Redirects need no authority. Using the request's Host here would reflect an untrusted Host
|
|
29
|
+
// header into an absolute open redirect before the adapter's host policy has a chance to run.
|
|
30
|
+
new Response(null, {
|
|
31
|
+
status,
|
|
32
|
+
headers: { location: relativeLocation(url) },
|
|
33
|
+
});
|
|
23
34
|
}
|
|
24
35
|
function methodsOf(methods) {
|
|
25
36
|
return new Set((methods ?? DEFAULT_METHODS).map((m) => m.toUpperCase()));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trailing-slash.js","sourceRoot":"","sources":["../src/trailing-slash.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAezD,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,MAAM,CAAU,CAAA;AAChD,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAA;AAEvD,SAAS,cAAc,CAAC,MAAc;IACpC,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAA;IACxE,CAAC;AACH,CAAC;AAED,SAAS,gBAAgB,CAAC,IAAY;IACpC,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAA;IACnC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAA;IACrC,OAAO,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAA;AAC9B,CAAC;AAED,SAAS,QAAQ,CAAC,GAAY,EAAE,IAAY;IAC1C,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAC5B,GAAG,CAAC,QAAQ,GAAG,IAAI,CAAA;IACnB,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,SAAS,KAAK,CACZ,GAAY,EACZ,GAAQ,EACR,IAA4B,EAC5B,MAA6B;IAE7B,OAAO,IAAI,KAAK,SAAS;QACvB,CAAC,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,GAAG,CAAC;QAClC,CAAC,CAAC,IAAI,QAAQ,CAAC,IAAI,EAAE,
|
|
1
|
+
{"version":3,"file":"trailing-slash.js","sourceRoot":"","sources":["../src/trailing-slash.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/middleware",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Composable middleware for nifra - auth, CSRF, JWT/JWKS, IP restriction, CORS, body limits, response cache, timing, and operational helpers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
"security-headers"
|
|
19
19
|
],
|
|
20
20
|
"type": "module",
|
|
21
|
+
"nifra": {
|
|
22
|
+
"environment": "library"
|
|
23
|
+
},
|
|
21
24
|
"main": "./dist/index.js",
|
|
22
25
|
"module": "./dist/index.js",
|
|
23
26
|
"types": "./dist/index.d.ts",
|
|
@@ -46,10 +49,10 @@
|
|
|
46
49
|
"access": "public"
|
|
47
50
|
},
|
|
48
51
|
"dependencies": {
|
|
49
|
-
"@nifrajs/schema": "
|
|
52
|
+
"@nifrajs/schema": "4.0.0"
|
|
50
53
|
},
|
|
51
54
|
"peerDependencies": {
|
|
52
|
-
"@nifrajs/core": "^
|
|
55
|
+
"@nifrajs/core": "^4.0.0"
|
|
53
56
|
},
|
|
54
57
|
"devDependencies": {
|
|
55
58
|
"@nifrajs/core": "workspace:*",
|
package/src/_ip.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export interface ParsedIp {
|
|
2
|
+
readonly version: 4 | 6
|
|
3
|
+
readonly value: bigint
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function parseIPv4(input: string): bigint | null {
|
|
7
|
+
const parts = input.split(".")
|
|
8
|
+
if (parts.length !== 4) return null
|
|
9
|
+
let out = 0n
|
|
10
|
+
for (const part of parts) {
|
|
11
|
+
if (!/^\d{1,3}$/.test(part)) return null
|
|
12
|
+
const n = Number(part)
|
|
13
|
+
if (n > 255) return null
|
|
14
|
+
out = (out << 8n) | BigInt(n)
|
|
15
|
+
}
|
|
16
|
+
return out
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseIPv6(input: string): bigint | null {
|
|
20
|
+
if (input.includes("%")) return null
|
|
21
|
+
let source = input.toLowerCase()
|
|
22
|
+
if (source.includes(".")) {
|
|
23
|
+
const lastColon = source.lastIndexOf(":")
|
|
24
|
+
if (lastColon < 0) return null
|
|
25
|
+
const ipv4 = parseIPv4(source.slice(lastColon + 1))
|
|
26
|
+
if (ipv4 === null) return null
|
|
27
|
+
const hi = Number((ipv4 >> 16n) & 0xffffn).toString(16)
|
|
28
|
+
const lo = Number(ipv4 & 0xffffn).toString(16)
|
|
29
|
+
source = `${source.slice(0, lastColon)}:${hi}:${lo}`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const halves = source.split("::")
|
|
33
|
+
if (halves.length > 2) return null
|
|
34
|
+
const left = halves[0] === "" ? [] : halves[0]!.split(":")
|
|
35
|
+
const right = halves.length === 1 || halves[1] === "" ? [] : halves[1]!.split(":")
|
|
36
|
+
const groups = [...left, ...right]
|
|
37
|
+
if (groups.some((g) => !/^[0-9a-f]{1,4}$/.test(g))) return null
|
|
38
|
+
|
|
39
|
+
const missing = 8 - groups.length
|
|
40
|
+
if (halves.length === 1) {
|
|
41
|
+
if (missing !== 0) return null
|
|
42
|
+
} else if (missing < 1) return null
|
|
43
|
+
|
|
44
|
+
const expanded =
|
|
45
|
+
halves.length === 1
|
|
46
|
+
? groups
|
|
47
|
+
: [...left, ...Array.from({ length: missing }, () => "0"), ...right]
|
|
48
|
+
let out = 0n
|
|
49
|
+
for (const group of expanded) out = (out << 16n) | BigInt(Number.parseInt(group, 16))
|
|
50
|
+
return out
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* An IPv4 or IPv6 address, bracketed or not. An IPv4-mapped IPv6 address (`::ffff:a.b.c.d`) is the
|
|
55
|
+
* IPv4 address it carries: Bun and Node report every IPv4 peer that way on their default dual-stack
|
|
56
|
+
* listener, and IPv4 rules must still match it.
|
|
57
|
+
*/
|
|
58
|
+
export function parseIp(input: string): ParsedIp | null {
|
|
59
|
+
const trimmed = input.trim()
|
|
60
|
+
const unbracketed =
|
|
61
|
+
trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.slice(1, -1) : trimmed
|
|
62
|
+
const v4 = parseIPv4(unbracketed)
|
|
63
|
+
if (v4 !== null) return { version: 4, value: v4 }
|
|
64
|
+
const v6 = parseIPv6(unbracketed)
|
|
65
|
+
if (v6 === null) return null
|
|
66
|
+
return v6 >> 32n === 0xffffn ? { version: 4, value: v6 & 0xffffffffn } : { version: 6, value: v6 }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The rate-limit bucket for a client address: an IPv4 address as itself (unmapped), an IPv6 address
|
|
71
|
+
* as its /64. One subscriber is routinely assigned a whole /64 and can send every request from a new
|
|
72
|
+
* address inside it, so a per-address bucket never fills. Anything that is not an address is kept.
|
|
73
|
+
*/
|
|
74
|
+
export function ipBucket(ip: string): string {
|
|
75
|
+
if (!ip.includes(":")) return ip
|
|
76
|
+
// Every IPv4 peer of a default Bun or Node listener: skip the full parse on that per-request path.
|
|
77
|
+
if (ip.startsWith("::ffff:") && ip.includes(".", 7) && ip.indexOf(":", 7) === -1)
|
|
78
|
+
return ip.slice(7)
|
|
79
|
+
const parsed = parseIp(ip)
|
|
80
|
+
if (parsed === null) return ip
|
|
81
|
+
const value = parsed.value
|
|
82
|
+
if (parsed.version === 4) {
|
|
83
|
+
return `${(value >> 24n) & 255n}.${(value >> 16n) & 255n}.${(value >> 8n) & 255n}.${value & 255n}`
|
|
84
|
+
}
|
|
85
|
+
const prefix = (value >> 64n).toString(16).padStart(16, "0")
|
|
86
|
+
return `${prefix.slice(0, 4)}:${prefix.slice(4, 8)}:${prefix.slice(8, 12)}:${prefix.slice(12)}::/64`
|
|
87
|
+
}
|
package/src/_utils.ts
CHANGED
|
@@ -18,9 +18,32 @@ export function jsonError(
|
|
|
18
18
|
)
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
let guardInstances = 0
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A guard's plugin name, unique to each instance. `use()` skips a name it already applied, so one
|
|
25
|
+
* shared name would silently drop a second, stricter guard (another `bearer()` before admin routes,
|
|
26
|
+
* a tighter `rateLimit()` inside a `group()`). The same instance applied twice still dedupes.
|
|
27
|
+
*/
|
|
28
|
+
export function guardName(base: string): string {
|
|
29
|
+
guardInstances += 1
|
|
30
|
+
return `${base}#${guardInstances}`
|
|
31
|
+
}
|
|
32
|
+
|
|
21
33
|
/** Shared with core so auth/CSRF middleware gets the allocation-light cookie scanner too. */
|
|
22
34
|
export const parseCookies = parseCoreCookies
|
|
23
35
|
|
|
36
|
+
/**
|
|
37
|
+
* The credential of an `Authorization: Bearer <token>` header, or `null`. The auth-scheme is
|
|
38
|
+
* case-insensitive (RFC 9110 section 11.1), so `bearer x` and `BEARER x` carry a token too.
|
|
39
|
+
*/
|
|
40
|
+
export function bearerToken(header: string | null | undefined): string | null {
|
|
41
|
+
if (header == null || header.length < 7 || header.slice(0, 7).toLowerCase() !== "bearer ") {
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
return header.slice(7).trim() || null
|
|
45
|
+
}
|
|
46
|
+
|
|
24
47
|
export function quotedHeaderValue(value: string): string {
|
|
25
48
|
let out = ""
|
|
26
49
|
for (let i = 0; i < value.length; i++) {
|
|
@@ -68,8 +91,22 @@ export function base64UrlEncode(bytes: ArrayBuffer | Uint8Array): string {
|
|
|
68
91
|
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")
|
|
69
92
|
}
|
|
70
93
|
|
|
94
|
+
const BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Decode unpadded base64url, rejecting non-canonical spellings. `atob` ignores the unused low bits of
|
|
98
|
+
* a final 2- or 3-character group, so without this check a JWT or CSRF signature has several textual
|
|
99
|
+
* forms that verify identically - the same rule core applies to signed cookies.
|
|
100
|
+
*/
|
|
71
101
|
export function base64UrlDecode(input: string): Uint8Array<ArrayBuffer> | null {
|
|
72
102
|
if (!/^[A-Za-z0-9_-]*$/.test(input) || input.length % 4 === 1) return null
|
|
103
|
+
const remainder = input.length % 4
|
|
104
|
+
if (remainder !== 0) {
|
|
105
|
+
const last = BASE64URL_ALPHABET.indexOf(input[input.length - 1]!)
|
|
106
|
+
if ((remainder === 2 && (last & 0x0f) !== 0) || (remainder === 3 && (last & 0x03) !== 0)) {
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
}
|
|
73
110
|
const padded = input
|
|
74
111
|
.replace(/-/g, "+")
|
|
75
112
|
.replace(/_/g, "/")
|
package/src/basic-auth.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { NIFRA_ASSURANCE, withRouteAssurance } from "@nifrajs/core/assurance"
|
|
|
2
2
|
import { defineIdentityPlugin, type IdentityPlugin } from "@nifrajs/core/server"
|
|
3
3
|
import {
|
|
4
4
|
decodeBase64,
|
|
5
|
+
guardName,
|
|
5
6
|
jsonError,
|
|
6
7
|
type MaybePromise,
|
|
7
8
|
quotedHeaderValue,
|
|
@@ -87,7 +88,7 @@ export function basicAuth<P>(
|
|
|
87
88
|
? Promise.resolve(options.verify)
|
|
88
89
|
: staticVerifier(options.username, options.password, options.principal ?? options.username)
|
|
89
90
|
|
|
90
|
-
const plugin = defineIdentityPlugin("basicAuth", (app) =>
|
|
91
|
+
const plugin = defineIdentityPlugin(guardName("basicAuth"), (app) =>
|
|
91
92
|
app.beforeHandle(async (c: { readonly req: Request }) => {
|
|
92
93
|
const parsed = credentials(c.req)
|
|
93
94
|
const principal =
|
package/src/body-limit.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NIFRA_ASSURANCE, withRouteAssurance } from "@nifrajs/core/assurance"
|
|
2
2
|
import { METHODS, type Middleware } from "@nifrajs/core/server"
|
|
3
|
-
import { jsonError, SAFE_METHODS } from "./_utils.ts"
|
|
3
|
+
import { guardName, jsonError, SAFE_METHODS } from "./_utils.ts"
|
|
4
4
|
|
|
5
5
|
export interface BodyLimitOptions {
|
|
6
6
|
/** Maximum raw request-body bytes accepted. */
|
|
@@ -37,7 +37,7 @@ export function bodyLimit(options: BodyLimitOptions): Middleware {
|
|
|
37
37
|
const allowLengthless = options.allowLengthless === true
|
|
38
38
|
|
|
39
39
|
const middleware: Middleware = {
|
|
40
|
-
name: "body-limit",
|
|
40
|
+
name: guardName("body-limit"),
|
|
41
41
|
onRequest(req) {
|
|
42
42
|
if (methods !== undefined ? !methods.has(req.method) : SAFE_METHODS.has(req.method)) {
|
|
43
43
|
return undefined
|
package/src/cache.ts
CHANGED
|
@@ -140,36 +140,119 @@ function defaultKey(req: Request, vary: readonly string[]): string {
|
|
|
140
140
|
return key
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
143
|
+
type ReadBytes =
|
|
144
|
+
| { readonly bytes: Uint8Array; readonly response: Response }
|
|
145
|
+
| { readonly response: Response }
|
|
146
|
+
|
|
147
|
+
interface ChunkReader {
|
|
148
|
+
read(): Promise<{ readonly done: boolean; readonly value?: Uint8Array }>
|
|
149
|
+
cancel(reason?: unknown): Promise<void>
|
|
150
|
+
releaseLock(): void
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const concatBytes = (chunks: readonly Uint8Array[], total: number): Uint8Array => {
|
|
154
|
+
const out = new Uint8Array(total)
|
|
155
|
+
let offset = 0
|
|
156
|
+
for (const chunk of chunks) {
|
|
157
|
+
out.set(chunk, offset)
|
|
158
|
+
offset += chunk.byteLength
|
|
159
|
+
}
|
|
160
|
+
return out
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function replayFrom(res: Response, chunks: readonly Uint8Array[], reader: ChunkReader): Response {
|
|
164
|
+
let released = false
|
|
165
|
+
const release = (): void => {
|
|
166
|
+
if (!released) {
|
|
167
|
+
released = true
|
|
168
|
+
reader.releaseLock()
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
172
|
+
start(controller) {
|
|
173
|
+
for (const chunk of chunks) controller.enqueue(chunk)
|
|
174
|
+
},
|
|
175
|
+
async pull(controller) {
|
|
176
|
+
try {
|
|
177
|
+
const { done, value } = await reader.read()
|
|
178
|
+
if (done || value === undefined) {
|
|
179
|
+
release()
|
|
180
|
+
controller.close()
|
|
181
|
+
} else {
|
|
182
|
+
controller.enqueue(value)
|
|
183
|
+
}
|
|
184
|
+
} catch (error) {
|
|
185
|
+
release()
|
|
186
|
+
controller.error(error)
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
cancel: (reason) => reader.cancel(reason).finally(release),
|
|
190
|
+
})
|
|
191
|
+
return new Response(stream, {
|
|
192
|
+
status: res.status,
|
|
193
|
+
statusText: res.statusText,
|
|
194
|
+
headers: res.headers,
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function readBytesCapped(res: Response, maxBytes: number): Promise<ReadBytes> {
|
|
199
|
+
const body = res.body
|
|
200
|
+
if (body === null) {
|
|
201
|
+
return {
|
|
202
|
+
bytes: new Uint8Array(),
|
|
203
|
+
response: new Response(null, {
|
|
204
|
+
status: res.status,
|
|
205
|
+
statusText: res.statusText,
|
|
206
|
+
headers: res.headers,
|
|
207
|
+
}),
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const reader = body.getReader() as unknown as ChunkReader
|
|
147
211
|
const chunks: Uint8Array[] = []
|
|
148
212
|
let total = 0
|
|
149
213
|
try {
|
|
150
214
|
while (true) {
|
|
151
215
|
const { done, value } = await reader.read()
|
|
152
216
|
if (done) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
217
|
+
reader.releaseLock()
|
|
218
|
+
const bytes = concatBytes(chunks, total)
|
|
219
|
+
return {
|
|
220
|
+
bytes,
|
|
221
|
+
response: new Response(bytes.byteLength === 0 ? null : bytes.slice(), {
|
|
222
|
+
status: res.status,
|
|
223
|
+
statusText: res.statusText,
|
|
224
|
+
headers: res.headers,
|
|
225
|
+
}),
|
|
158
226
|
}
|
|
159
|
-
return out
|
|
160
227
|
}
|
|
228
|
+
if (value === undefined) return { response: replayFrom(res, chunks, reader) }
|
|
161
229
|
total += value.byteLength
|
|
162
230
|
if (total > maxBytes) {
|
|
163
|
-
|
|
164
|
-
return null
|
|
231
|
+
return { response: replayFrom(res, chunks.concat([value]), reader) }
|
|
165
232
|
}
|
|
166
233
|
chunks.push(value)
|
|
167
234
|
}
|
|
168
235
|
} catch {
|
|
169
|
-
return
|
|
236
|
+
return { response: replayFrom(res, chunks, reader) }
|
|
170
237
|
}
|
|
171
238
|
}
|
|
172
239
|
|
|
240
|
+
function responseVary(headers: Headers): readonly string[] {
|
|
241
|
+
const value = headers.get("vary")
|
|
242
|
+
if (value === null) return []
|
|
243
|
+
return value
|
|
244
|
+
.split(",")
|
|
245
|
+
.map((part) => part.trim().toLowerCase())
|
|
246
|
+
.filter((part) => part !== "")
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function responseVaryMatchesKey(headers: Headers, configured: readonly string[]): boolean {
|
|
250
|
+
const declared = responseVary(headers)
|
|
251
|
+
if (declared.includes("*")) return false
|
|
252
|
+
const configuredSet = new Set(configured.map((header) => header.toLowerCase()))
|
|
253
|
+
return declared.every((header) => configuredSet.has(header))
|
|
254
|
+
}
|
|
255
|
+
|
|
173
256
|
function withStatusHeader(
|
|
174
257
|
res: Response,
|
|
175
258
|
header: string | false,
|
|
@@ -210,6 +293,7 @@ export function cache(options: CacheOptions): Middleware {
|
|
|
210
293
|
throw new Error("cache: cacheStatusHeader is empty")
|
|
211
294
|
}
|
|
212
295
|
const hits = new WeakSet<Request>()
|
|
296
|
+
const requestKeys = new WeakMap<Request, string>()
|
|
213
297
|
const hasAuthentication = (req: Request): boolean =>
|
|
214
298
|
authenticatedHeaders.some((header) => req.headers.has(header))
|
|
215
299
|
|
|
@@ -221,6 +305,7 @@ export function cache(options: CacheOptions): Middleware {
|
|
|
221
305
|
return undefined
|
|
222
306
|
}
|
|
223
307
|
const key = keyOf(req)
|
|
308
|
+
requestKeys.set(req, key)
|
|
224
309
|
const entry = await store.get(key)
|
|
225
310
|
if (entry === undefined) return undefined
|
|
226
311
|
// Never replay a personalized entry before route-scoped authentication runs. Public entries
|
|
@@ -252,6 +337,7 @@ export function cache(options: CacheOptions): Middleware {
|
|
|
252
337
|
async onResponse(res, req) {
|
|
253
338
|
if (hits.has(req)) {
|
|
254
339
|
hits.delete(req)
|
|
340
|
+
requestKeys.delete(req)
|
|
255
341
|
return res
|
|
256
342
|
}
|
|
257
343
|
if (!methods.has(req.method.toUpperCase())) return res
|
|
@@ -265,6 +351,12 @@ export function cache(options: CacheOptions): Middleware {
|
|
|
265
351
|
if (!cacheSetCookie && res.headers.has("set-cookie")) {
|
|
266
352
|
return withStatusHeader(res, cacheStatusHeader, "BYPASS")
|
|
267
353
|
}
|
|
354
|
+
// A response-declared Vary must be represented in the lookup key. If the caller did not opt
|
|
355
|
+
// those request headers into `vary`, bypass storage instead of replaying one representation to
|
|
356
|
+
// another request. `Vary: *` is never cacheable by a shared cache.
|
|
357
|
+
if (!responseVaryMatchesKey(res.headers, vary)) {
|
|
358
|
+
return withStatusHeader(res, cacheStatusHeader, "BYPASS")
|
|
359
|
+
}
|
|
268
360
|
// Shared-cache safety (RFC 9111 §3.5): never store a response to an authenticated request
|
|
269
361
|
// (Authorization or Cookie) unless it's explicitly public - otherwise a personalized 200 leaks
|
|
270
362
|
// across users. Opt in with `cacheAuthenticated: true` for a route that's the same for everyone.
|
|
@@ -279,15 +371,21 @@ export function cache(options: CacheOptions): Middleware {
|
|
|
279
371
|
if (declared !== undefined && declared > maxBytes) {
|
|
280
372
|
return withStatusHeader(res, cacheStatusHeader, "BYPASS")
|
|
281
373
|
}
|
|
282
|
-
const
|
|
283
|
-
if (
|
|
374
|
+
const captured = await readBytesCapped(res, maxBytes)
|
|
375
|
+
if (!("bytes" in captured)) {
|
|
376
|
+
return withStatusHeader(captured.response, cacheStatusHeader, "BYPASS")
|
|
377
|
+
}
|
|
378
|
+
const body = captured.bytes
|
|
379
|
+
const capturedResponse = captured.response
|
|
284
380
|
|
|
285
|
-
const headers = new Headers(
|
|
381
|
+
const headers = new Headers(capturedResponse.headers)
|
|
286
382
|
headers.delete("age")
|
|
287
383
|
if (cacheStatusHeader !== false) headers.delete(cacheStatusHeader)
|
|
288
384
|
appendVary(headers, vary)
|
|
289
385
|
const now = Date.now()
|
|
290
|
-
|
|
386
|
+
const key = requestKeys.get(req) ?? keyOf(req)
|
|
387
|
+
requestKeys.delete(req)
|
|
388
|
+
await store.set(key, {
|
|
291
389
|
status: res.status,
|
|
292
390
|
statusText: res.statusText,
|
|
293
391
|
headers: [...headers.entries()],
|
|
@@ -295,12 +393,12 @@ export function cache(options: CacheOptions): Middleware {
|
|
|
295
393
|
expiresAt: now + ttlMs,
|
|
296
394
|
storedAt: now,
|
|
297
395
|
})
|
|
298
|
-
const outgoing = new Headers(
|
|
396
|
+
const outgoing = new Headers(capturedResponse.headers)
|
|
299
397
|
appendVary(outgoing, vary)
|
|
300
398
|
if (cacheStatusHeader !== false) outgoing.set(cacheStatusHeader, "MISS")
|
|
301
|
-
return new Response(
|
|
302
|
-
status:
|
|
303
|
-
statusText:
|
|
399
|
+
return new Response(capturedResponse.body, {
|
|
400
|
+
status: capturedResponse.status,
|
|
401
|
+
statusText: capturedResponse.statusText,
|
|
304
402
|
headers: outgoing,
|
|
305
403
|
})
|
|
306
404
|
},
|
package/src/compression.ts
CHANGED
|
@@ -11,8 +11,9 @@ export interface CompressionOptions {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
// Text-like payloads benefit from gzip; binary media (images/video/archives) is already compressed.
|
|
14
|
+
// Not an event stream: gzip holds each event until its window fills, so none would arrive on time.
|
|
14
15
|
const COMPRESSIBLE =
|
|
15
|
-
/^(?:text
|
|
16
|
+
/^(?:text\/(?!event-stream\b)|application\/(?:json|[\w.-]+\+json|javascript|xml|[\w.-]+\+xml|wasm|x-ndjson)|image\/svg\+xml)/i
|
|
16
17
|
|
|
17
18
|
const defaultCompressible = (contentType: string): boolean => COMPRESSIBLE.test(contentType)
|
|
18
19
|
|
|
@@ -56,9 +57,18 @@ const gzipHeaders = (source: Headers): Headers => {
|
|
|
56
57
|
headers.set("content-encoding", "gzip")
|
|
57
58
|
headers.delete("content-length")
|
|
58
59
|
addGzipVary(headers)
|
|
60
|
+
weakenEtag(headers)
|
|
59
61
|
return headers
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
/** A strong ETag names the identity bytes; the gzip bytes are another representation, so the tag is
|
|
65
|
+
* weakened (as nginx does) rather than shared by both - a cache or range request trusting it to
|
|
66
|
+
* mean byte-identical would otherwise splice one representation into the other. */
|
|
67
|
+
function weakenEtag(headers: ResponseHeadersView): void {
|
|
68
|
+
const etag = headers.get("etag")
|
|
69
|
+
if (etag !== null && !etag.startsWith("W/")) headers.set("etag", `W/${etag}`)
|
|
70
|
+
}
|
|
71
|
+
|
|
62
72
|
function addGzipVary(headers: ResponseHeadersView): void {
|
|
63
73
|
const vary = headers.get("vary")
|
|
64
74
|
if (vary === null) {
|
|
@@ -140,7 +150,7 @@ async function compressRawBody(
|
|
|
140
150
|
while (total < threshold) {
|
|
141
151
|
const { done, value } = await reader.read()
|
|
142
152
|
if (done) {
|
|
143
|
-
return new Response(concat(buffered, total), {
|
|
153
|
+
return new Response(concat(buffered, total).slice(), {
|
|
144
154
|
status: response.status,
|
|
145
155
|
statusText: response.statusText,
|
|
146
156
|
headers: response.headers,
|
|
@@ -250,6 +260,7 @@ export function compression(options: CompressionOptions = {}) {
|
|
|
250
260
|
headers.set("content-encoding", "gzip")
|
|
251
261
|
headers.delete("content-length")
|
|
252
262
|
addGzipVary(headers)
|
|
263
|
+
weakenEtag(headers)
|
|
253
264
|
return compressed
|
|
254
265
|
})
|
|
255
266
|
},
|