@ainra/middleware 0.7.0 → 0.8.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 CHANGED
@@ -75,7 +75,7 @@ depends on the lenient path.
75
75
  ## Underneath
76
76
 
77
77
  Verification is [`@ainra/sdk`](https://www.npmjs.com/package/@ainra/sdk) — one of the three implementations that
78
- agree on all 1153 conformance vectors, on verdict and reason. This package adds only the request plumbing: header decoding, the 403, the
78
+ agree on all 1169 conformance vectors, on verdict and reason. This package adds only the request plumbing: header decoding, the 403, the
79
79
  verdict event, and `req.ainra`.
80
80
 
81
81
  Licensed Apache-2.0 OR MIT.
package/dist/index.js CHANGED
@@ -120,6 +120,22 @@ export function checkRequest(verifier, bundle, opts = {}) {
120
120
  }
121
121
  return { allow, reason, verdict, event: verdictEvent(parsed, verdict, now) };
122
122
  }
123
+ /** Does the request carry a body the gate was not given the raw bytes of? */
124
+ function bodyUnseen(req) {
125
+ if (req.rawBody !== undefined)
126
+ return false;
127
+ const h = (k) => { const v = req.headers[k]; return Array.isArray(v) ? v[0] : v; };
128
+ if (Number(h("content-length") ?? 0) > 0 || h("transfer-encoding") !== undefined || h("content-digest") !== undefined)
129
+ return true;
130
+ const b = req.body;
131
+ if (typeof b === "string")
132
+ return b.length > 0;
133
+ if (b && typeof b === "object") {
134
+ const keys = Object.keys(b).filter((k) => k !== "ainra_passport");
135
+ return keys.length > 0;
136
+ }
137
+ return false;
138
+ }
123
139
  /** Build the signed-over view of an Express/Node request. `authority` comes from the Host header, which is what
124
140
  * the client addressed and therefore what it signed. */
125
141
  function bindingFromReq(req) {
@@ -188,7 +204,10 @@ export function ainraGate(verifier, opts = {}) {
188
204
  return (req, res, next) => {
189
205
  const raw = req.headers[header] ?? req.body?.ainra_passport;
190
206
  const popRaw = req.headers[popHeader];
191
- const result = raw === undefined
207
+ // A signed request whose body the gate cannot see is refused, not checked as if it had none: a signature captured
208
+ // on a bodyless request would otherwise pass with any body attached (2026-11-07 review).
209
+ const blind = opts.requireSignature === true && bodyUnseen(req);
210
+ const result = raw === undefined || blind
192
211
  ? { allow: false, reason: "schema_violation", verdict: DENY_SCHEMA, event: verdictEvent({}, DENY_SCHEMA, (opts.now ?? (() => Math.floor(Date.now() / 1000)))()) }
193
212
  : checkRequest(verifier, Array.isArray(raw) ? raw[0] : raw, {
194
213
  ...opts,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainra/middleware",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "AINRA verifier middleware — a fail-closed gate (Connect/Express + framework-agnostic) that verifies an AINRA passport on a request, over @ainra/sdk. The verification wedge: local, offline, ~5-line integration.",
5
5
  "license": "Apache-2.0 OR MIT",
6
6
  "type": "module",
@@ -14,7 +14,7 @@
14
14
  "test": "node --test"
15
15
  },
16
16
  "dependencies": {
17
- "@ainra/sdk": "^0.7.0"
17
+ "@ainra/sdk": "^0.8.0"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@types/node": "^26.1.1",