@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.
Files changed (97) hide show
  1. package/README.md +11 -6
  2. package/dist/_ip.d.ts +17 -0
  3. package/dist/_ip.d.ts.map +1 -0
  4. package/dist/_ip.js +91 -0
  5. package/dist/_ip.js.map +1 -0
  6. package/dist/_utils.d.ts +16 -0
  7. package/dist/_utils.d.ts.map +1 -1
  8. package/dist/_utils.js +33 -0
  9. package/dist/_utils.js.map +1 -1
  10. package/dist/basic-auth.d.ts.map +1 -1
  11. package/dist/basic-auth.js +2 -2
  12. package/dist/basic-auth.js.map +1 -1
  13. package/dist/body-limit.js +2 -2
  14. package/dist/body-limit.js.map +1 -1
  15. package/dist/cache.d.ts.map +1 -1
  16. package/dist/cache.js +110 -22
  17. package/dist/cache.js.map +1 -1
  18. package/dist/compression.d.ts.map +1 -1
  19. package/dist/compression.js +13 -2
  20. package/dist/compression.js.map +1 -1
  21. package/dist/cors.d.ts.map +1 -1
  22. package/dist/cors.js +50 -5
  23. package/dist/cors.js.map +1 -1
  24. package/dist/csrf.d.ts +21 -2
  25. package/dist/csrf.d.ts.map +1 -1
  26. package/dist/csrf.js +99 -10
  27. package/dist/csrf.js.map +1 -1
  28. package/dist/etag.d.ts.map +1 -1
  29. package/dist/etag.js +7 -1
  30. package/dist/etag.js.map +1 -1
  31. package/dist/idempotency.d.ts +16 -7
  32. package/dist/idempotency.d.ts.map +1 -1
  33. package/dist/idempotency.js +51 -22
  34. package/dist/idempotency.js.map +1 -1
  35. package/dist/ip-restriction.d.ts +12 -7
  36. package/dist/ip-restriction.d.ts.map +1 -1
  37. package/dist/ip-restriction.js +25 -72
  38. package/dist/ip-restriction.js.map +1 -1
  39. package/dist/jwt.d.ts.map +1 -1
  40. package/dist/jwt.js +3 -3
  41. package/dist/jwt.js.map +1 -1
  42. package/dist/language.d.ts.map +1 -1
  43. package/dist/language.js +4 -1
  44. package/dist/language.js.map +1 -1
  45. package/dist/multipart.d.ts.map +1 -1
  46. package/dist/multipart.js +6 -1
  47. package/dist/multipart.js.map +1 -1
  48. package/dist/pretty-json.d.ts.map +1 -1
  49. package/dist/pretty-json.js +63 -7
  50. package/dist/pretty-json.js.map +1 -1
  51. package/dist/problem-details.d.ts.map +1 -1
  52. package/dist/problem-details.js +8 -1
  53. package/dist/problem-details.js.map +1 -1
  54. package/dist/range.d.ts.map +1 -1
  55. package/dist/range.js +5 -2
  56. package/dist/range.js.map +1 -1
  57. package/dist/rate-limit.d.ts +11 -10
  58. package/dist/rate-limit.d.ts.map +1 -1
  59. package/dist/rate-limit.js +29 -19
  60. package/dist/rate-limit.js.map +1 -1
  61. package/dist/request-id.d.ts +8 -1
  62. package/dist/request-id.d.ts.map +1 -1
  63. package/dist/request-id.js +6 -3
  64. package/dist/request-id.js.map +1 -1
  65. package/dist/security-headers.d.ts +4 -0
  66. package/dist/security-headers.d.ts.map +1 -1
  67. package/dist/security-headers.js +7 -1
  68. package/dist/security-headers.js.map +1 -1
  69. package/dist/token-auth.d.ts.map +1 -1
  70. package/dist/token-auth.js +3 -5
  71. package/dist/token-auth.js.map +1 -1
  72. package/dist/trailing-slash.d.ts.map +1 -1
  73. package/dist/trailing-slash.js +12 -1
  74. package/dist/trailing-slash.js.map +1 -1
  75. package/package.json +6 -3
  76. package/src/_ip.ts +87 -0
  77. package/src/_utils.ts +37 -0
  78. package/src/basic-auth.ts +2 -1
  79. package/src/body-limit.ts +2 -2
  80. package/src/cache.ts +119 -21
  81. package/src/compression.ts +13 -2
  82. package/src/cors.ts +51 -7
  83. package/src/csrf.ts +115 -10
  84. package/src/etag.ts +11 -1
  85. package/src/idempotency.ts +76 -28
  86. package/src/ip-restriction.ts +38 -75
  87. package/src/jwt.ts +4 -2
  88. package/src/language.ts +5 -1
  89. package/src/multipart.ts +6 -1
  90. package/src/pretty-json.ts +56 -7
  91. package/src/problem-details.ts +8 -1
  92. package/src/range.ts +6 -3
  93. package/src/rate-limit.ts +48 -31
  94. package/src/request-id.ts +16 -4
  95. package/src/security-headers.ts +7 -1
  96. package/src/token-auth.ts +3 -5
  97. 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;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,OAAO,GAAE,sBAA2B,GAAG,UAAU,CA4ChF"}
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"}
@@ -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: "security-headers",
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;;;;;;;;;GASG;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,IAAI,EAAE,kBAAkB;QACxB,eAAe,EAAE,QAAQ;KAC1B,EACD;QACE,EAAE,EAAE,eAAe,CAAC,gBAAgB;QACpC,MAAM,EAAE,kBAAkB;QAC1B,KAAK,EAAE,QAAQ;KAChB,CACF,CAAA;AACH,CAAC"}
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"}
@@ -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;AAEhF,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,CAYlE;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"}
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"}
@@ -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}"`,
@@ -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;AAsChF,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,MAAM,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,EAAE,CACvD,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;YACnB,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAA;YACnD,OAAO,MAAM,EAAE,UAAU,CAAC,SAAS,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;QACpF,CAAC;QACD,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
+ {"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;AAsCD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,oBAAyB,iDAenE;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,oBAAyB,iDAiBrE"}
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"}
@@ -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
- : new Response(null, { status, headers: { location: url.toString() } });
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":"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"}
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.3.0",
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": "3.3.0"
52
+ "@nifrajs/schema": "4.0.0"
50
53
  },
51
54
  "peerDependencies": {
52
- "@nifrajs/core": "^3.3.0"
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
- async function readBytesCapped(res: Response, maxBytes: number): Promise<Uint8Array | null> {
144
- const body = res.clone().body
145
- if (body === null) return new Uint8Array()
146
- const reader = body.getReader()
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
- const out = new Uint8Array(total)
154
- let offset = 0
155
- for (const chunk of chunks) {
156
- out.set(chunk, offset)
157
- offset += chunk.byteLength
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
- await reader.cancel()
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 null
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 body = await readBytesCapped(res, maxBytes)
283
- if (body === null) return withStatusHeader(res, cacheStatusHeader, "BYPASS")
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(res.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
- await store.set(keyOf(req), {
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(res.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(res.body, {
302
- status: res.status,
303
- statusText: res.statusText,
399
+ return new Response(capturedResponse.body, {
400
+ status: capturedResponse.status,
401
+ statusText: capturedResponse.statusText,
304
402
  headers: outgoing,
305
403
  })
306
404
  },
@@ -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\/|application\/(?:json|[\w.-]+\+json|javascript|xml|[\w.-]+\+xml|wasm|x-ndjson)|image\/svg\+xml)/i
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
  },