@stonyx/rest-server 0.2.1-beta.94 → 0.2.1-beta.96

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.
@@ -1 +1 @@
1
- {"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAE,KAAK,OAAO,IAAI,cAAc,EAAE,KAAK,QAAQ,IAAI,eAAe,EAAqB,KAAK,OAAO,EAAE,MAAM,SAAS,CAAC;AAOrI,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AACnD,MAAM,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AACtG,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,MAAM,GAAG,SAAS,CAAC;AAC3F,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,cAAc,EAAE,CAAC,CAAC,CAAC;AAE9F,MAAM,CAAC,OAAO,OAAO,OAAO;IAC1B,MAAM,CAAC,SAAS,SAAmB;IAEnC,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,YAAY;IASlD,MAAM,CAAC,kBAAkB,CAAC,GAAG,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAWrE,eAAe,EAAE,OAAO,CAAC;IACzB,QAAQ,EAAG,aAAa,CAAC;IACjB,IAAI,CAAC,EAAE,WAAW,CAAC;;IA4B3B,aAAa,IAAI,IAAI;CAoItB"}
1
+ {"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAE,KAAK,OAAO,IAAI,cAAc,EAAE,KAAK,QAAQ,IAAI,eAAe,EAAE,KAAK,OAAO,EAAE,MAAM,SAAS,CAAC;AAMlH,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AACnD,MAAM,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AACtG,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,MAAM,GAAG,SAAS,CAAC;AAC3F,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,cAAc,EAAE,CAAC,CAAC,CAAC;AAE9F,MAAM,CAAC,OAAO,OAAO,OAAO;IAC1B,MAAM,CAAC,SAAS,SAAmB;IAEnC,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,YAAY;IASlD,MAAM,CAAC,kBAAkB,CAAC,GAAG,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAWrE,eAAe,EAAE,OAAO,CAAC;IACzB,QAAQ,EAAG,aAAa,CAAC;IACjB,IAAI,CAAC,EAAE,WAAW,CAAC;;IAS3B,aAAa,IAAI,IAAI;CAqDtB"}
package/dist/request.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import express from 'express';
2
2
  import config from 'stonyx/config';
3
3
  import { makeArray } from '@stonyx/utils/object';
4
- import applyRouteMatching, { shouldRejectEncoding, shouldRejectTarget } from './route-matching.js';
5
4
  const METHODS = new Set(['get', 'post', 'put', 'delete', 'patch']);
6
5
  export default class Request {
7
6
  static stateProp = '__stonyxState';
@@ -28,24 +27,6 @@ export default class Request {
28
27
  constructor() {
29
28
  const api = express();
30
29
  api.disable('x-powered-by');
31
- // Applies BOTH route-matching SETTINGS: case sensitive routing
32
- // (abofs/stonyx-rest-server#47) and strict routing (#50). For #47 this
33
- // call closes sub-paths (/public/SUCCESS) and the parent's call closes the
34
- // mount segment; for #50 THIS call closes the entire trailing-slash
35
- // authorization bypass on its own, and the parent's call has no security
36
- // role. Must stay in the constructor: registerCalls() materializes this
37
- // router, and a set applied afterwards has no effect. The parent app's
38
- // setting does not reach here -- see src/route-matching.ts.
39
- //
40
- // The third route-matching control, `canonicalRoutes` (#54), is NOT applied
41
- // here and must not be moved here. It is not an express setting, and its
42
- // timing contract is the opposite of these two: it is read PER REQUEST
43
- // inside the handler closure in registerCalls() below, via
44
- // shouldRejectTarget(). These two are constructor-timed because a late
45
- // `set` is silently ineffective; that hazard does not exist for #54, and
46
- // reading it per request is what lets the unit AC flip the flag between
47
- // probes.
48
- applyRouteMatching(api);
49
30
  this.expressInstance = api;
50
31
  }
51
32
  registerCalls() {
@@ -57,86 +38,7 @@ export default class Request {
57
38
  continue;
58
39
  }
59
40
  for (const [route, handler] of Object.entries(handlers)) {
60
- expressInstance[method](route, async (req, res, next) => {
61
- // abofs/stonyx-rest-server#54 -- reject a request whose RAW target is
62
- // not the canonical path express matched, closing two authorization
63
- // bypasses against hooks that authorize on `req.originalUrl`: the
64
- // mount-root trailing slash and the absolute-form request target.
65
- //
66
- // Three properties of this line are load-bearing. Each is named with
67
- // the ONE assertion that turns red when it is removed -- assertion
68
- // numbers in `test/integration/rest-server-test.ts` AC1, so the claim
69
- // is checkable rather than a promise that coverage exists somewhere:
70
- //
71
- // 1. It runs OUTSIDE `if (this.auth)`. Gating it on the hook would
72
- // leave `GET /public/` at 200 and make a security control depend
73
- // on an unrelated consumer choice. Killed by AC1.6, which probes
74
- // a route class with no hook.
75
- // 2. It runs BEFORE that block, not merely outside it. This is a
76
- // SEPARATE property from 1 and needs its own probe: AC1.6 is on
77
- // a hookless class, so it stays green if this line is merely
78
- // moved BELOW the block. Measured with it moved: the suite was
79
- // 34 pass / 0 fail while `GET http://HOST/private/failure`
80
- // answered 505 -- the consumer's hook status, which is the same
81
- // oracle class as 3, and the hook itself ran on a request this
82
- // module was about to reject. Killed by AC1.11.
83
- // 3. It rejects with `next('router')`, NOT sendStatusResponse() or
84
- // res.sendStatus(404). Measured: sendStatus returns
85
- // `text/plain "Not Found"` while a genuine miss returns
86
- // `text/html <pre>Cannot GET ...</pre>` -- a working ORACLE
87
- // telling an attacker the route exists but was spelled wrong.
88
- // next('router') exits this sub-app's router into finalhandler
89
- // and is shape-identical to a real miss (same status, same
90
- // Content-Type, same CSP header). Routing it through
91
- // sendStatusResponse() would additionally re-introduce the
92
- // oracle for any consumer who sets a 404 in `statusMap`.
93
- // Killed by AC1.5.
94
- //
95
- // Properties 1 and 2 were previously stated here as a single item
96
- // asserted to have "its own red-able assertion". It did not: only
97
- // half of it was covered. Do not re-merge them.
98
- if (shouldRejectTarget(req))
99
- return next('router');
100
- // abofs/stonyx-rest-server#56 -- reject a request whose RAW target
101
- // spells an UNRESERVED character (RFC 3986 2.3) as a percent-triplet,
102
- // closing the authorization bypass against a hook that compares
103
- // `req.path` OR `req.originalUrl` on any route class with a `:param`
104
- // segment. Express decodes only `req.params`, so both raw fields see
105
- // `%73ecret` while the handler is given `secret`.
106
- //
107
- // A SEPARATE line and a SEPARATE predicate from #54's above, and it
108
- // must stay that way. Extending shouldRejectTarget()'s comparison
109
- // cannot reach this: `target === canonical` for EVERY one of these
110
- // spellings, because both sides carry the same encoded string.
111
- // Reading #54's key here is worse than useless -- measured, gating
112
- // this rule on `canonicalRoutes` returns `GET /enc/%73ecret` to 200
113
- // under `REST_CANONICAL_ROUTES=false`, which is exactly the flag an
114
- // absolute-form-proxy consumer must set. Killed by
115
- // `test/unit/request-test.ts` AC5.
116
- //
117
- // The same three load-bearing properties as the line above apply
118
- // verbatim -- outside `if (this.auth)`, BEFORE it, and rejecting with
119
- // `next('router')` rather than a sendStatus that answers `text/plain`
120
- // and becomes an oracle. They are killed here by
121
- // `test/integration/rest-server-test.ts` #56 AC1.4 (the shape
122
- // deep-equal against a genuine miss) and AC1.6
123
- // (`/private/restricte%64` -> 404 rather than the hook's own 403),
124
- // on a class WITH a hook.
125
- //
126
- // AC1.6, NOT AC1.5, and the difference is measured rather than
127
- // reasoned. This comment named AC1.5 (`/private/%66ailure`) until
128
- // #58's fix round; that assertion CANNOT fail for the relocation.
129
- // For that request `private.ts`'s hook sees `req.path === '/%66ailure'`
130
- // (no 505) and `req.params.id === 'failure'` (no 403), so it returns
131
- // undefined and the relocated check still fires. Only
132
- // `/private/restricte%64` trips a hook clause -- `req.params.id` is
133
- // DECODED by express, so it equals `restricted` and answers 403 --
134
- // which is what makes AC1.6 the assertion that reds. Measured: move
135
- // this line below the `if (this.auth)` block, rebuild, and the suite
136
- // reports 40 pass / 1 fail, the single failure being #56's AC1 with
137
- // both of assertion 6's messages red and assertion 5 GREEN.
138
- if (shouldRejectEncoding(req))
139
- return next('router');
41
+ expressInstance[method](route, async (req, res) => {
140
42
  // Run auth after route matching so request.params is populated
141
43
  if (this.auth) {
142
44
  const status = this.auth(req, getState(req));
@@ -1 +1 @@
1
- {"version":3,"file":"request.js","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"AAAA,OAAO,OAA8G,MAAM,SAAS,CAAC;AACrI,OAAO,MAAM,MAAM,eAAe,CAAC;AACnC,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,kBAAkB,EAAE,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAEnG,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;AAOnE,MAAM,CAAC,OAAO,OAAO,OAAO;IAC1B,MAAM,CAAC,SAAS,GAAG,eAAe,CAAC;IAEnC,MAAM,CAAC,QAAQ,CAAC,GAAmB;QACjC,MAAM,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC;QAC9B,MAAM,MAAM,GAAG,GAAyC,CAAC;QACzD,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,SAAS;YAAE,OAAO,MAAM,CAAC,SAAS,CAAiB,CAAC;QAE9E,MAAM,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC;QACvB,OAAO,MAAM,CAAC,SAAS,CAAiB,CAAC;IAC3C,CAAC;IAED,MAAM,CAAC,kBAAkB,CAAC,GAAoB,EAAE,MAAc;QAC5D,MAAM,SAAS,GAAG,MAAM,CAAC,UAAU,EAAE,SAAS,IAAI,EAAE,CAAC;QACrD,MAAM,OAAO,GAAG,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QAExC,IAAI,OAAO,EAAE,CAAC;YACZ,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACnC,CAAC;aAAM,CAAC;YACN,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QACzB,CAAC;IACH,CAAC;IAED,eAAe,CAAU;IACzB,QAAQ,CAAiB;IAGzB;QACE,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;QACtB,GAAG,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;QAE5B,+DAA+D;QAC/D,uEAAuE;QACvE,2EAA2E;QAC3E,oEAAoE;QACpE,yEAAyE;QACzE,wEAAwE;QACxE,uEAAuE;QACvE,4DAA4D;QAC5D,EAAE;QACF,4EAA4E;QAC5E,yEAAyE;QACzE,uEAAuE;QACvE,2DAA2D;QAC3D,uEAAuE;QACvE,yEAAyE;QACzE,wEAAwE;QACxE,UAAU;QACV,kBAAkB,CAAC,GAAG,CAAC,CAAC;QAExB,IAAI,CAAC,eAAe,GAAG,GAAG,CAAC;IAC7B,CAAC;IAED,aAAa;QACX,MAAM,EAAE,eAAe,EAAE,GAAG,IAAI,CAAC;QACjC,MAAM,EAAE,QAAQ,EAAE,kBAAkB,EAAE,GAAG,OAAO,CAAC;QAEjD,KAAK,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC/D,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;gBACzB,OAAO,CAAC,IAAI,CAAC,WAAW,MAAM,2CAA2C,CAAC,CAAC;gBAC3E,SAAS;YACX,CAAC;YAED,KAAK,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACvD,eAAiK,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,GAAmB,EAAE,GAAoB,EAAE,IAAkB,EAAE,EAAE;oBACxP,sEAAsE;oBACtE,oEAAoE;oBACpE,kEAAkE;oBAClE,kEAAkE;oBAClE,EAAE;oBACF,qEAAqE;oBACrE,mEAAmE;oBACnE,sEAAsE;oBACtE,qEAAqE;oBACrE,EAAE;oBACF,qEAAqE;oBACrE,sEAAsE;oBACtE,sEAAsE;oBACtE,mCAAmC;oBACnC,mEAAmE;oBACnE,qEAAqE;oBACrE,kEAAkE;oBAClE,oEAAoE;oBACpE,gEAAgE;oBAChE,qEAAqE;oBACrE,oEAAoE;oBACpE,qDAAqD;oBACrD,qEAAqE;oBACrE,yDAAyD;oBACzD,6DAA6D;oBAC7D,iEAAiE;oBACjE,mEAAmE;oBACnE,oEAAoE;oBACpE,gEAAgE;oBAChE,0DAA0D;oBAC1D,gEAAgE;oBAChE,8DAA8D;oBAC9D,wBAAwB;oBACxB,EAAE;oBACF,kEAAkE;oBAClE,kEAAkE;oBAClE,gDAAgD;oBAChD,IAAI,kBAAkB,CAAC,GAAG,CAAC;wBAAE,OAAO,IAAI,CAAC,QAAQ,CAAC,CAAC;oBAEnD,mEAAmE;oBACnE,sEAAsE;oBACtE,gEAAgE;oBAChE,qEAAqE;oBACrE,qEAAqE;oBACrE,kDAAkD;oBAClD,EAAE;oBACF,oEAAoE;oBACpE,kEAAkE;oBAClE,mEAAmE;oBACnE,+DAA+D;oBAC/D,mEAAmE;oBACnE,oEAAoE;oBACpE,oEAAoE;oBACpE,mDAAmD;oBACnD,mCAAmC;oBACnC,EAAE;oBACF,iEAAiE;oBACjE,sEAAsE;oBACtE,sEAAsE;oBACtE,iDAAiD;oBACjD,8DAA8D;oBAC9D,+CAA+C;oBAC/C,mEAAmE;oBACnE,0BAA0B;oBAC1B,EAAE;oBACF,+DAA+D;oBAC/D,kEAAkE;oBAClE,kEAAkE;oBAClE,wEAAwE;oBACxE,qEAAqE;oBACrE,sDAAsD;oBACtD,oEAAoE;oBACpE,mEAAmE;oBACnE,oEAAoE;oBACpE,qEAAqE;oBACrE,oEAAoE;oBACpE,4DAA4D;oBAC5D,IAAI,oBAAoB,CAAC,GAAG,CAAC;wBAAE,OAAO,IAAI,CAAC,QAAQ,CAAC,CAAC;oBAErD,+DAA+D;oBAC/D,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;wBACd,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;wBAC7C,IAAI,MAAM;4BAAE,OAAO,kBAAkB,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;oBACrD,CAAC;oBAED,MAAM,SAAS,GAAG,CAAC,GAAG,SAAS,CAAC,OAAO,CAAC,CAAqB,CAAC;oBAC9D,MAAM,QAAQ,GAAG,SAAS,CAAC,GAAG,EAAG,CAAC;oBAClC,IAAI,QAAiB,CAAC;oBAEtB,iBAAiB;oBACjB,OAAM,SAAS,CAAC,MAAM,EAAE,CAAC;wBACvB,QAAQ,GAAG,MAAM,SAAS,CAAC,KAAK,EAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;wBACnE,IAAI,QAAQ,KAAK,SAAS;4BAAE,MAAM;oBACpC,CAAC;oBAED,IAAI,QAAQ,KAAK,SAAS;wBAAE,QAAQ,GAAG,MAAM,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;oBAC1E,IAAI,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC;wBAAE,OAAO,kBAAkB,CAAC,GAAG,EAAE,QAAkB,CAAC,CAAC;oBAEnF,+CAA+C;oBAC/C,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;oBAC5B,MAAM,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;oBAC3B,IAAI,QAAQ;wBAAE,OAAO,GAAG,CAAC,QAAQ,CAAC,QAAkB,CAAC,CAAC;oBAEtD,2CAA2C;oBAC3C,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC;oBACvB,IAAI,IAAI,EAAE,CAAC;wBACT,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAA0F,CAAC;wBAEvH,IAAI,OAAO;4BAAE,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC;gCAAE,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;wBACrF,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;oBAC1B,CAAC;oBAED,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;wBAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;wBAAC,OAAO;oBAAC,CAAC;oBAC5D,IAAI,OAAO,QAAQ,KAAK,QAAQ;wBAAE,OAAO,kBAAkB,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;oBAEtE,GAAG,CAAC,IAAI,CAAC,QAAmC,CAAC,CAAC;gBAChD,CAAC,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC"}
1
+ {"version":3,"file":"request.js","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"AAAA,OAAO,OAA2F,MAAM,SAAS,CAAC;AAClH,OAAO,MAAM,MAAM,eAAe,CAAC;AACnC,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEjD,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;AAOnE,MAAM,CAAC,OAAO,OAAO,OAAO;IAC1B,MAAM,CAAC,SAAS,GAAG,eAAe,CAAC;IAEnC,MAAM,CAAC,QAAQ,CAAC,GAAmB;QACjC,MAAM,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC;QAC9B,MAAM,MAAM,GAAG,GAAyC,CAAC;QACzD,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,SAAS;YAAE,OAAO,MAAM,CAAC,SAAS,CAAiB,CAAC;QAE9E,MAAM,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC;QACvB,OAAO,MAAM,CAAC,SAAS,CAAiB,CAAC;IAC3C,CAAC;IAED,MAAM,CAAC,kBAAkB,CAAC,GAAoB,EAAE,MAAc;QAC5D,MAAM,SAAS,GAAG,MAAM,CAAC,UAAU,EAAE,SAAS,IAAI,EAAE,CAAC;QACrD,MAAM,OAAO,GAAG,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QAExC,IAAI,OAAO,EAAE,CAAC;YACZ,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACnC,CAAC;aAAM,CAAC;YACN,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QACzB,CAAC;IACH,CAAC;IAED,eAAe,CAAU;IACzB,QAAQ,CAAiB;IAGzB;QACE,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;QACtB,GAAG,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;QAE5B,IAAI,CAAC,eAAe,GAAG,GAAG,CAAC;IAC7B,CAAC;IAED,aAAa;QACX,MAAM,EAAE,eAAe,EAAE,GAAG,IAAI,CAAC;QACjC,MAAM,EAAE,QAAQ,EAAE,kBAAkB,EAAE,GAAG,OAAO,CAAC;QAEjD,KAAK,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC/D,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;gBACzB,OAAO,CAAC,IAAI,CAAC,WAAW,MAAM,2CAA2C,CAAC,CAAC;gBAC3E,SAAS;YACX,CAAC;YAED,KAAK,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACvD,eAA6I,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,GAAmB,EAAE,GAAoB,EAAE,EAAE;oBAChN,+DAA+D;oBAC/D,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;wBACd,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;wBAC7C,IAAI,MAAM;4BAAE,OAAO,kBAAkB,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;oBACrD,CAAC;oBAED,MAAM,SAAS,GAAG,CAAC,GAAG,SAAS,CAAC,OAAO,CAAC,CAAqB,CAAC;oBAC9D,MAAM,QAAQ,GAAG,SAAS,CAAC,GAAG,EAAG,CAAC;oBAClC,IAAI,QAAiB,CAAC;oBAEtB,iBAAiB;oBACjB,OAAM,SAAS,CAAC,MAAM,EAAE,CAAC;wBACvB,QAAQ,GAAG,MAAM,SAAS,CAAC,KAAK,EAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;wBACnE,IAAI,QAAQ,KAAK,SAAS;4BAAE,MAAM;oBACpC,CAAC;oBAED,IAAI,QAAQ,KAAK,SAAS;wBAAE,QAAQ,GAAG,MAAM,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;oBAC1E,IAAI,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC;wBAAE,OAAO,kBAAkB,CAAC,GAAG,EAAE,QAAkB,CAAC,CAAC;oBAEnF,+CAA+C;oBAC/C,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;oBAC5B,MAAM,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;oBAC3B,IAAI,QAAQ;wBAAE,OAAO,GAAG,CAAC,QAAQ,CAAC,QAAkB,CAAC,CAAC;oBAEtD,2CAA2C;oBAC3C,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC;oBACvB,IAAI,IAAI,EAAE,CAAC;wBACT,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAA0F,CAAC;wBAEvH,IAAI,OAAO;4BAAE,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC;gCAAE,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;wBACrF,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;oBAC1B,CAAC;oBAED,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;wBAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;wBAAC,OAAO;oBAAC,CAAC;oBAC5D,IAAI,OAAO,QAAQ,KAAK,QAAQ;wBAAE,OAAO,kBAAkB,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;oBAEtE,GAAG,CAAC,IAAI,CAAC,QAAmC,CAAC,CAAC;gBAChD,CAAC,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC"}
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "stonyx-async",
5
5
  "stonyx-module"
6
6
  ],
7
- "version": "0.2.1-beta.94",
7
+ "version": "0.2.1-beta.96",
8
8
  "description": "Rest Server Module for Stonyx Framework",
9
9
  "repository": {
10
10
  "type": "git",
@@ -36,7 +36,7 @@
36
36
  "dependencies": {
37
37
  "cors": "^2.8.5",
38
38
  "express": "^5.1.0",
39
- "stonyx": "0.2.3-beta.78"
39
+ "stonyx": "0.2.3-beta.79"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@stonyx/utils": "0.2.3-beta.26",
@@ -1,257 +0,0 @@
1
- import type { Express, Request as ExpressRequest } from 'express';
2
- /**
3
- * Applies this module's route-matching settings to an express app.
4
- *
5
- * Called from BOTH express construction sites (abofs/stonyx-rest-server#47):
6
- * `RestServer`'s constructor closes the mount segment (`/PUBLIC/...`), and
7
- * `Request`'s constructor closes sub-paths (`/public/SUCCESS`). Neither alone
8
- * is sufficient -- settings are inherited on mount, but `mountRoute()` calls
9
- * `registerCalls()` before `api.use()`, so each child router is already built
10
- * by the time the parent's setting could reach it.
11
- *
12
- * Both callers invoke this from a constructor, and must keep doing so: express
13
- * materializes a router lazily on first route registration, and a setting
14
- * applied afterwards is silently ineffective -- no throw, no warning.
15
- *
16
- * The guard is `!== false`, not a plain truthy check, and that polarity is
17
- * load-bearing. `trustProxy` and `enableHealthCheck` default to the falsy
18
- * direction, so a missing key fails safe for them. This flag defaults to the
19
- * truthy direction, so `if (config.restServer?.caseSensitiveRoutes)` would
20
- * silently fail OPEN for a consumer whose shipped config predates the key.
21
- *
22
- * It lives here, in one place, rather than being written out at each call
23
- * site, so that a single test can anchor it. The invariant is duplicated the
24
- * moment the expression is: `test/unit/request-test.ts` AC6 reaches this
25
- * function through `Request`, which means the same assertion now also covers
26
- * the `RestServer` half. Two copies of the predicate left the parent's copy
27
- * free to drift -- inverting it, or dropping the condition entirely, kept the
28
- * suite green.
29
- *
30
- * Note `express({ caseSensitive: true })` does NOT work: express 5's
31
- * `createApplication()` takes zero arguments and forwards nothing. The app
32
- * setting is the only mechanism. The same is true of `strict`.
33
- *
34
- * ---
35
- *
36
- * `strict routing` (abofs/stonyx-rest-server#50) closes the same class of
37
- * authorization bypass for a TRAILING SLASH: `GET /private/failure` was denied
38
- * by a consumer's auth hook while `GET /private/failure/` reached the guarded
39
- * handler, because the hook compares `req.path` against `/failure`.
40
- *
41
- * It is a SEPARATE key from `caseSensitiveRoutes`, not a rename and not a
42
- * reuse. Coupling them would force any consumer who legitimately needs
43
- * trailing-slash tolerance -- a load balancer probing `/health/`, a
44
- * slash-normalizing proxy -- to re-open #47's case bypass to get it. Case
45
- * insensitivity is almost never intentional; slash tolerance frequently is.
46
- *
47
- * The two settings do NOT share the #47 split above, and this is the one thing
48
- * not to carry across from that fix. For `strict routing`:
49
- *
50
- * - The CHILD site (Request's constructor) closes the entire security
51
- * defect on its own. The parent site does nothing for it.
52
- * - The PARENT site (RestServer's constructor) closes exactly one thing in
53
- * this repo: `/health/`, the only route registered directly on the parent
54
- * app. It has no security role here; do not describe it as having one.
55
- *
56
- * The cause is concrete: `Router.prototype.use` hardcodes `strict: false` (and
57
- * `end: false`), so mount segments are structurally strict-immune. That is the
58
- * OPPOSITE of `sensitive`, which `use()` DOES forward, and which is why #47's
59
- * parent site closed `/PUBLIC/...`. The version-pinned file-and-line citation
60
- * for that upstream behaviour is deliberately kept in ONE place --
61
- * `docs/project-structure.md`, section "Strict routing (#50)" -- so a router
62
- * upgrade invalidates one line rather than five. Both sites still get both
63
- * settings (they share this function), but the justification differs and the
64
- * tests are built on the measured split, not on the analogy.
65
- *
66
- * Consequence worth stating so nobody expects this SETTING to cover it: no
67
- * express setting rejects the mount-segment trailing slash (`/public/`). For
68
- * both `/public` and `/public/` the mounted sub-app receives `req.path === '/'`,
69
- * so a `req.path` auth hook sees no difference and there is nothing for the
70
- * setting to reject. That statement is still true, and it is still the reason
71
- * `applyRouteMatching()` is not the fix site for that edge.
72
- *
73
- * The edge itself is CLOSED, by `shouldRejectTarget()` below rather than by a
74
- * setting (abofs/stonyx-rest-server#54). `req.originalUrl` does differ between
75
- * the two spellings, and a hook authorizing on it was bypassed by one
76
- * character -- measured: `GET /admin` -> 401, `GET /admin/` -> 200 with the
77
- * guarded handler running unauthenticated. `GET /public/` now returns 404, and
78
- * the integration AC asserts exactly that.
79
- *
80
- * All four guards in this file are `!== false` for the same reason: these flags
81
- * default to the
82
- * truthy direction, so a truthy check fails OPEN for a consumer whose shipped
83
- * config predates the key. All are also asserted at the unit tier for BOTH
84
- * failure shapes -- key present-and-`undefined` and key absent as an own
85
- * property -- in `test/unit/request-test.ts` (#47's AC6, #50's AC3, #54's AC2,
86
- * #56's AC6). The integration tier cannot see any of them: with the shipped
87
- * default `true`, a fail-open guard leaves every integration assertion green.
88
- */
89
- export default function applyRouteMatching(api: Express): void;
90
- /**
91
- * Decides whether a request must be rejected because its RAW request target is
92
- * not the canonical path express matched (abofs/stonyx-rest-server#54).
93
- *
94
- * Closes two live authorization bypasses against a consumer hook that
95
- * authorizes on `req.originalUrl` -- the field express does NOT normalize:
96
- *
97
- * 1. mount-root trailing slash GET /admin/ -> was 200
98
- * 2. absolute-form request target GET http://host/admin -> was 200
99
- * (RFC 9112 3.2.2; hits EVERY route, not just the mount root)
100
- *
101
- * Both were measured reaching the guarded handler unauthenticated while
102
- * `GET /admin` was denied 401.
103
- *
104
- * COMPARE THE RAW TARGET; DO NOT PARSE IT. Any implementation reaching for
105
- * `new URL(req.originalUrl, base).pathname` to "get the path" re-opens vector 2
106
- * BY CONSTRUCTION: parsing normalizes the exact string the consumer's hook is
107
- * exposed to, so the check would compare a laundered value while the hook still
108
- * sees the raw one. Measured: the narrow `endsWith('/')` form closes 1 and
109
- * leaves 2 at 200.
110
- *
111
- * `req.baseUrl + req.path` is likewise NOT usable as the left-hand side. It is
112
- * `/admin/` for BOTH spellings of vector 1 -- `originalUrl` is the only field
113
- * that differs, which is precisely why the bypass exists. A check built on
114
- * `baseUrl + path` cannot see its own defect.
115
- *
116
- * TIMING CONTRACT -- DELIBERATELY DIFFERENT FROM ITS TWO SIBLINGS. This lives
117
- * beside `applyRouteMatching()` for the same "one place anchors it" reason, but
118
- * deliberately OUTSIDE it: that function's contract is *apply express settings
119
- * to an app*, it is called from two constructors, and this is neither a setting
120
- * nor constructor-timed. `caseSensitiveRoutes`/`strictRoutes` are read once in a
121
- * constructor and are silently ineffective if applied late; this flag is read
122
- * PER REQUEST, inside the handler closure. There is no lazy-materialisation
123
- * hazard here, so do not carry that constraint across.
124
- *
125
- * The caller must reject with `next('router')`, NOT `res.sendStatus(404)`, and
126
- * must run this BEFORE `this.auth` as well as outside `if (this.auth)` -- those
127
- * are two separate properties with two separate assertions (AC1.11 and AC1.6);
128
- * see src/request.ts.
129
- *
130
- * Guard polarity is `!== false`, matching its three siblings, for the same measured
131
- * reason: the secure value is the TRUTHY one, so a plain truthy check fails
132
- * OPEN for any consumer whose shipped `restServer` block predates the key --
133
- * the state every existing consumer is in, and reachable in practice because
134
- * the stonyx loader only merges a module's `config/environment.js` for modules
135
- * in devDependencies. `trustProxy` deliberately differs (`=== 'true'`): its safe
136
- * default is FALSY, so a truthy check already fails closed for it. The rule is
137
- * "the guard must fail toward the safe value", not "all guards look alike" --
138
- * preserve the asymmetry.
139
- *
140
- * The integration tier cannot see a fail-open guard here: with the shipped
141
- * default `true`, `=== true` leaves every integration assertion green. Only
142
- * `test/unit/request-test.ts` AC2 can, and it probes BOTH failure shapes --
143
- * key present-and-`undefined` and key absent as an own property.
144
- */
145
- export declare function shouldRejectTarget(req: ExpressRequest): boolean;
146
- /**
147
- * Decides whether a request must be rejected because its RAW request target
148
- * spells an unreserved character as a percent-triplet
149
- * (abofs/stonyx-rest-server#56).
150
- *
151
- * Closes a live authorization bypass on any route class carrying a `:param`
152
- * segment. Express decodes `req.params` and NOTHING else -- `req.path` and
153
- * `req.originalUrl` both stay percent-encoded -- so a consumer hook comparing
154
- * either of those raw fields was walked past by re-spelling the id:
155
- *
156
- * GET /enc/secret -> 401 (hook fires)
157
- * GET /enc/%73ecret -> 200 guarded handler, unauthenticated, id "secret"
158
- *
159
- * Both hook shapes are affected and neither is safer than the other; there is
160
- * no spelling that defeats one and not the same-id comparison in the other.
161
- * A third shape is worse still: a LITERAL guarded route co-registered with a
162
- * sibling `/:id` (this repo's own `test/sample/requests/private.ts`) has the
163
- * encoded spelling miss the literal layer and be ABSORBED by the param route,
164
- * so the guard is walked past without the guarded handler ever running --
165
- * measured `GET /private/failure` -> 505 vs `GET /private/%66ailure` -> 200.
166
- *
167
- * THE RULE IS AN UNRESERVED-OCTET SCAN, NOT A DECODE-AND-COMPARE. Two wrong
168
- * implementations were built and measured, and each breaks a legitimate
169
- * request:
170
- *
171
- * 1. `decodeURIComponent(target) !== target` -- rejects `/enc/sec%2fret`
172
- * (404), which names the DISTINCT id `sec/ret`. The router SPLITS then
173
- * DECODES; a whole-target decode decodes then splits, and the two
174
- * disagree about `%2f` by construction. Killed by AC3.
175
- * 2. decode until stable -- rejects `/enc/%2573ecret` (404), which names the
176
- * legitimate id `%73ecret`. Express decodes EXACTLY ONCE, so `%2561` is
177
- * not a bypass and a loop invents a false deny. Killed by AC4.
178
- *
179
- * WHY THIS IS NOT PART OF `shouldRejectTarget()` (#54), and why extending that
180
- * comparison cannot work: for `GET /enc/%73ecret`, `originalUrl` is
181
- * `/enc/%73ecret`, `baseUrl` is `/enc` and `path` is `/%73ecret`, so
182
- * `target === canonical` -- both sides carry the SAME encoded string. The
183
- * comparison is structurally blind to this axis and no change to it can see it.
184
- *
185
- * WHY IT IS A FOURTH KEY AND NOT A REUSE OF `canonicalRoutes`. Measured with
186
- * the rule implemented correctly but gated on #54's key:
187
- * `REST_CANONICAL_ROUTES=false` returns `GET /enc/%73ecret` to 200. That flag
188
- * is exactly what a consumer behind an absolute-form-emitting forward proxy
189
- * must set, so folding the two would hand precisely those consumers the
190
- * encoding bypass as the price of staying up. Same argument the block above
191
- * makes for why #50 is not a rename of #47. Pinned by
192
- * `test/unit/request-test.ts` AC5, which also asserts -- in that same state --
193
- * that #54's own vector IS re-opened, so an implementation that simply ignores
194
- * `canonicalRoutes` cannot pass it vacuously.
195
- *
196
- * TIMING CONTRACT: identical to `shouldRejectTarget()` and NOT to the two
197
- * settings above. Read per request, inside the handler closure in
198
- * `Request.registerCalls()`; there is no lazy-materialisation hazard, so do not
199
- * move it into `applyRouteMatching()`. The caller must reject with
200
- * `next('router')`, and must run this BEFORE `this.auth` as well as outside
201
- * `if (this.auth)` -- see src/request.ts.
202
- *
203
- * Guard polarity is `!== false`, matching all three siblings, for the same
204
- * measured reason: the secure value is the TRUTHY one, so `=== true` fails OPEN
205
- * for any consumer whose shipped `restServer` block predates the key. The
206
- * integration tier CANNOT see that mutation -- with the shipped default `true`
207
- * every integration assertion stays green -- so it is `test/unit/request-test.ts`
208
- * AC6 that kills it, probing the key present-and-`undefined` and absent as an
209
- * own property separately.
210
- *
211
- * WHAT THIS DOES NOT CLOSE, stated here rather than left implied. It cannot
212
- * give each decoded id exactly one accepted spelling, because everything the
213
- * allowlist above does NOT cover must remain encodable: `/enc/a+b` and
214
- * `/enc/a%2Bb` both name the id `a+b`, and `/enc/sec%2fret` and
215
- * `/enc/sec%2Fret` both name `sec/ret`.
216
- *
217
- * THE RESIDUAL IS WIDER THAN "RESERVED CHARACTERS" AND MUST NOT BE WRITTEN
218
- * DOWN THAT WAY. An octet outside `[A-Za-z0-9-._~]` keeps more than one
219
- * accepted spelling when its hex carries a letter digit (upper- and lower-case
220
- * hex) or when a client may also send it literally. That is every reserved
221
- * character, every non-ASCII byte AND every control octet whose hex carries a
222
- * letter digit. Measured through a real listener against this predicate, on a
223
- * deny list holding NO reserved character at all:
224
- *
225
- * GET /i18n/caf%C3%A9 -> 401 GET /i18n/caf%c3%a9 -> 200, id "café"
226
- * GET /i18n/%E5%8C%97%E4%BA%AC -> 401 GET /i18n/%e5%8c%97%e4%ba%ac -> 200, id "北京"
227
- * GET /i18n/a%0Db -> 401 GET /i18n/a%0db -> 200, id "a\rb"
228
- *
229
- * A consumer whose ids are i18n text reads "any id containing a reserved
230
- * character" as not applying to them. It does. The three docs that carried the
231
- * narrow wording (`README.md`, `docs/project-structure.md`,
232
- * `docs/agents/security-reviewer.md`) were widened to this scope rather than
233
- * this comment being narrowed to theirs.
234
- *
235
- * NOT the whole complement of the unreserved set, and this qualifier is load-
236
- * bearing rather than pedantry -- the sentence above is stated as measured, so
237
- * it must not over-warn either. Measured counterexamples: `%21` and `%40` are
238
- * reserved and carry no letter hex digit, so they alias literal-versus-encoded
239
- * rather than by hex case; `%00` and `%09` have exactly one accepted spelling
240
- * and do not alias at all; `%90` is a 400 (invalid UTF-8), not an alias.
241
- *
242
- * So a hook comparing a raw path string REMAINS UNSOUND for any id carrying an
243
- * octet outside `[A-Za-z0-9-._~]` that keeps more than one accepted spelling,
244
- * and `req.params` -- which express decodes, and which is populated before
245
- * `auth()` runs -- is the sound idiom. That
246
- * residual is the consumer's comparison to own; the module cannot close it
247
- * without 404ing encodings clients are required to emit.
248
- *
249
- * SCOPE LIMIT, separate from the residual above: this predicate is called from
250
- * `Request.registerCalls()`, so it covers the routes mounted from request
251
- * classes and nothing else. A route registered directly on the public
252
- * `RestServer.instance.api` gets none of it -- measured,
253
- * `GET /direct/%73ecret` -> 200 with `id "secret"` while `GET /enc/%73ecret`
254
- * -> 404. Same registration-site limit `canonicalRoutes` (#54) has.
255
- */
256
- export declare function shouldRejectEncoding(req: ExpressRequest): boolean;
257
- //# sourceMappingURL=route-matching.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"route-matching.d.ts","sourceRoot":"","sources":["../src/route-matching.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,SAAS,CAAC;AAGlE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsFG;AACH,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAG7D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAuB/D;AA2BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6GG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAkBjE"}