@stonyx/rest-server 0.2.1-alpha.20 → 0.2.1-alpha.22

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
@@ -79,59 +79,75 @@ Configuration is read from `stonyx/config` under `restServer`:
79
79
  | `origin` | **String \| Array** | `'*'` | CORS origin(s) allowed |
80
80
  | `methods` | **String** | `'GET,POST,PATCH,PUT,DELETE'` | CORS allowed methods |
81
81
  | `enableHealthCheck` | **Boolean** | `true` | Register `GET /health` endpoint (disable via `REST_HEALTH_CHECK_DISABLE=true`) |
82
- | `caseSensitiveRoutes` | **Boolean** | `true` | Match route paths case-sensitively. Disable via `REST_CASE_SENSITIVE_ROUTES=false`. See [Case-Sensitive Routing](#case-sensitive-routing) — **disabling this re-opens a security hole**. |
82
+ | `caseSensitiveRoutes` | **Boolean** | `true` | Match route paths case-sensitively. Disable via `REST_CASE_SENSITIVE_ROUTES=false`. See [Route Matching Strictness](#route-matching-strictness) — **disabling this re-opens a security hole**. |
83
+ | `strictRoutes` | **Boolean** | `true` | Match route paths strictly, so a trailing slash does not match a route registered without one. Disable via `REST_STRICT_ROUTES=false`. See [Route Matching Strictness](#route-matching-strictness) — **disabling this re-opens a security hole**, and note `GET /health/` now 404s. |
83
84
  | `trustProxy` | **Boolean** | `false` | Trust reverse proxy headers (e.g. `X-Forwarded-Proto`). Enable via `REST_TRUST_PROXY=true` when running behind a load balancer such as AWS ALB/ELB to ensure correct protocol detection. |
84
85
  | `statusMap` | **Object** | `{}` | Optional mapping of HTTP status codes to custom messages |
85
86
 
86
- ### Case-Sensitive Routing
87
+ ### Route Matching Strictness
87
88
 
88
- Routes match **case-sensitively by default**. `GET /users` reaches a route
89
- mounted at `/users`; `GET /Users` does not reach that mount, and
90
- `GET /users/Success` does not reach a `/success` handler registered inside it.
89
+ Routes match **case-sensitively and strictly by default**. Two settings, both
90
+ on, both applied at both express construction sites:
91
91
 
92
- Read [What this does not do](#what-this-does-not-do) before you rely on that
93
- sentence. Two things it does not say: "does not reach the handler" is not the
94
- same as "404", and casing is only one of the two ways express matches more
95
- loosely than the authorization predicates written against it.
92
+ | axis | setting | example that no longer matches |
93
+ |---|---|---|
94
+ | casing | `case sensitive routing` | `GET /users/Success` -> does not reach `/success` |
95
+ | trailing slash | `strict routing` | `GET /users/success/` -> does not reach `/success` |
96
96
 
97
- This is deliberate and security-relevant. Express matches case-insensitively by
98
- default, which means any authorization written against the request URL can be
99
- walked past by changing the case of the request:
97
+ Read [What this does not do](#what-this-does-not-do) and
98
+ [Upgrading](#upgrading-behaviour-changes) before you rely on that. Two things
99
+ the table does not say: "does not reach the handler" is not the same as "404",
100
+ and one edge of the trailing-slash axis is **not** closed and cannot be.
101
+
102
+ This is deliberate and security-relevant. Express matches both case-insensitively
103
+ and slash-insensitively by default, which means any authorization written
104
+ against the request URL can be walked past by changing the case of the request,
105
+ or by appending one character:
100
106
 
101
107
  ```
102
- GET /owners/angela -> 404 (correctly filtered)
103
- GET /OwNeRs/angela -> 200 (full record)
104
- DELETE /ANIMALS/22 -> 204 (record destroyed)
108
+ GET /owners/angela -> 404 (correctly filtered)
109
+ GET /OwNeRs/angela -> 200 (full record) <- closed by case sensitive routing
110
+ GET /owners/angela/ -> 200 (full record) <- closed by strict routing
111
+ DELETE /ANIMALS/22 -> 204 (record destroyed)
112
+ DELETE /animals/22/ -> 204 (record destroyed)
105
113
  ```
106
114
 
107
115
  The consumer's predicate is stricter than the router that dispatched the
108
116
  request, so the router hands the handler a request the predicate would have
109
- rejected. Case-sensitive matching closes the **casing** half of that asymmetry:
110
- the path a handler sees can only ever be the exact registered casing.
111
-
112
- It does not close the asymmetry itself. Express exposes `case sensitive
113
- routing` and `strict routing` as a pair of loose-by-default router settings and
114
- this change sets only the first, so the identical bypass is still reachable by
115
- appending a slash. Measured on this release against this repo's own fixture:
117
+ rejected. Measured against this repo's own fixture, before and after:
116
118
 
117
119
  ```
118
- GET /private/failure -> 505 (auth hook fires, request blocked)
119
- GET /private/failure/ -> 200 (auth hook never fires, handler runs)
120
+ before after
121
+ GET /private/failure 505 505 (auth hook fires, request blocked)
122
+ GET /private/failure/ 200 404 (auth hook never fired; now a miss)
123
+ GET /private/FAILURE 200 200 (absorbed by /:id — see below)
120
124
  ```
121
125
 
122
- That is the same defect, one character instead of a case shift translated to
123
- the example above, `DELETE /animals/22` is filtered and `DELETE /animals/22/`
124
- destroys the record. It is tracked as
125
- [#50](https://github.com/abofs/stonyx-rest-server/issues/50) and is not fixed
126
- here; it is a second consumer-visible behaviour change that needs its own flag
127
- and its own release note.
128
-
129
- **So do not drop a URL-normalizing defence you already have on the strength of
130
- this section.** If your authorization compares `req.path` or `req.originalUrl`,
131
- keep whatever normalization you have until #50 ships.
126
+ For a handler that authorizes on `req.path`, the path it sees can now only ever
127
+ be the exact registered spelling, in the exact registered casing, with no
128
+ trailing slash. That closes [#47](https://github.com/abofs/stonyx-rest-server/issues/47)
129
+ and [#50](https://github.com/abofs/stonyx-rest-server/issues/50).
132
130
 
133
131
  #### What this does not do
134
132
 
133
+ **It does not close the trailing slash on a mount root, and no setting can.**
134
+ This is the one edge that remains open, so do not read the section above as
135
+ closing the class outright:
136
+
137
+ ```
138
+ GET /public -> req.path '/' req.originalUrl '/public'
139
+ GET /public/ -> req.path '/' req.originalUrl '/public/'
140
+ ```
141
+
142
+ Express's router applies mount-prefix matching with `strict: false`
143
+ unconditionally (`router@2.2.0`, `index.js:400-401`), so both forms reach the
144
+ mounted route class and both arrive with `req.path === '/'`. A hook authorizing
145
+ on `req.path` cannot tell them apart, so for that hook there is no asymmetry to
146
+ exploit. **A hook comparing `req.originalUrl` still sees two different strings,
147
+ and `strictRoutes` does not change that.** If your authorization compares
148
+ `req.originalUrl` rather than `req.path`, keep whatever URL normalization you
149
+ have.
150
+
135
151
  **It does not normalize path *parameter values*.** If your `auth()` hook rejects
136
152
  `params.id === 'restricted'`, then `GET /private/RESTRICTED` still reaches the
137
153
  handler — the router matched the route correctly, and `restricted` and
@@ -148,36 +164,64 @@ repo's AC5 asserts exactly that. A class exposing `/orders/summary` alongside
148
164
  database lookup. The param route's own `auth()` hook still runs, so this is an
149
165
  expectation defect rather than a bypass — but plan for a reroute, not a 404.
150
166
 
151
- **It does not cover trailing slashes.** See
152
- [#50](https://github.com/abofs/stonyx-rest-server/issues/50) above.
167
+ Note the two axes differ here. A *trailing slash* is not absorbed by `/:id`,
168
+ because `/:id` is equally strict: `GET /private/failure/` misses `/failure` and
169
+ misses `/:id`, and is a true 404.
170
+
171
+ **It does not redirect or rewrite** mixed-case or trailing-slash requests to
172
+ their canonical form. Whether `/Users` is a typo to forgive or an attack to
173
+ reject is an application policy decision, and encoding it here would mint
174
+ another variant of the bug above.
153
175
 
154
- **It does not redirect or rewrite** mixed-case requests to their canonical
155
- casing. Whether `/Users` is a typo to forgive or an attack to reject is an
156
- application policy decision, and encoding it here would mint another variant of
157
- the bug above.
176
+ #### Upgrading: behaviour changes
177
+
178
+ Both settings change which requests match, so both are consumer-visible.
179
+
180
+ **`GET /health/` now returns 404.** `GET /health` is unaffected. This is the
181
+ change most likely to page someone, and it is an **availability** problem rather
182
+ than a 404 you will read about in a log: if a Kubernetes liveness probe, an ELB
183
+ target-group health check or an uptime monitor is pointed at the trailing-slash
184
+ form, it starts failing and the deployment gets marked unhealthy and cycled.
185
+ This module emits no request logging, so the only symptom is the probe going
186
+ red. **Check your probe URLs before upgrading.**
187
+
188
+ Also affected:
189
+
190
+ - **Param routes.** `/resource/:id/` no longer matches. Any client calling
191
+ `/private/restricted/` gets a 404 where it previously got the param route.
192
+ - **Trailing-slash-normalizing proxies.** nginx `try_files`/`rewrite`, Apache
193
+ `DirectorySlash On` and some CDN edge rules append a slash; behind one of
194
+ those, every route stops matching at once.
195
+ - **Mount paths from filenames.** With `camelCaseRoutes` truthy, `phone-number.ts`
196
+ mounts at `/phoneNumber`, so `GET /phonenumber` returns 404; with it falsy,
197
+ `Users.ts` mounts at `/Users`, so `GET /users` returns 404.
198
+
199
+ A request that stops matching returns express's default `404 Cannot GET /x` with
200
+ no log line and no stack, so it looks like a deploy that dropped a route.
158
201
 
159
202
  #### Opting out
160
203
 
204
+ Two separate flags, one per axis:
205
+
161
206
  ```bash
162
- REST_CASE_SENSITIVE_ROUTES=false
207
+ REST_CASE_SENSITIVE_ROUTES=false # restores case-insensitive matching (#47)
208
+ REST_STRICT_ROUTES=false # restores trailing-slash tolerance (#50)
163
209
  ```
164
210
 
165
- **This restores the vulnerability described above** any URL-based
166
- authorization in your application becomes bypassable by changing case. It
167
- exists as a one-line remediation for an existing deployment, not as a
168
- configuration to run on.
211
+ or equivalently `restServer: { caseSensitiveRoutes: false, strictRoutes: false }`.
169
212
 
170
- You need it if clients call your endpoints with casing that does not match the
171
- mount path. Mount paths come from filenames, so this is not hypothetical:
213
+ **They are deliberately separate keys, and neither implies the other.** Slash
214
+ tolerance is a legitimate need a health-check URL you cannot change today is
215
+ the common case. Casing tolerance almost never is. Folding them into one flag
216
+ would force anyone who needs the first to accept the second, which is why a
217
+ consumer who took the `#47` opt-out still has to set `REST_STRICT_ROUTES=false`
218
+ separately to keep trailing slashes working.
172
219
 
173
- - with `camelCaseRoutes` truthy, `phone-number.ts` mounts at `/phoneNumber`, and
174
- `GET /phonenumber` now returns 404
175
- - with `camelCaseRoutes` falsy, filenames are used verbatim, so
176
- `Users.ts` mounts at `/Users` and `GET /users` now returns 404
177
-
178
- A request that stops matching returns express's default `404 Cannot GET /x` with
179
- no log line and no stack, so it looks like a deploy that dropped a route. Set
180
- the flag to restore service, then fix the client's casing and remove the flag.
220
+ **Each flag restores the corresponding vulnerability described above** — the
221
+ URL-based authorization in your application becomes bypassable along that axis
222
+ again. They exist as one-line remediations for an existing deployment, not as a
223
+ configuration to run on. Set the flag to restore service, then fix the client
224
+ and remove the flag.
181
225
 
182
226
  ### Running Behind a Load Balancer
183
227
 
@@ -5,6 +5,7 @@ const {
5
5
  REST_HEALTH_CHECK_DISABLE,
6
6
  REST_PORT,
7
7
  REST_REQUEST_PATH,
8
+ REST_STRICT_ROUTES,
8
9
  REST_TRUST_PROXY
9
10
  } = process.env;
10
11
 
@@ -31,6 +32,37 @@ const config = {
31
32
  // the subprocess-based env isolation this repo does not yet have; any fix
32
33
  // must keep a live assertion on this default.
33
34
  caseSensitiveRoutes: REST_CASE_SENSITIVE_ROUTES !== 'false',
35
+
36
+ // Secure by default, same polarity and same reasoning as caseSensitiveRoutes
37
+ // above: routes match strictly, so a trailing slash cannot walk past a
38
+ // consumer's URL-based authorization (abofs/stonyx-rest-server#50).
39
+ //
40
+ // BEHAVIOUR CHANGE for consumers upgrading: `/health/` now returns 404, and
41
+ // param routes like `/resource/:id/` no longer match. Opt out with
42
+ // REST_STRICT_ROUTES=false only as a temporary remediation. It is a separate
43
+ // key from REST_CASE_SENSITIVE_ROUTES on purpose -- opting out of slash
44
+ // strictness must not silently re-open #47's case bypass.
45
+ //
46
+ // DELIBERATELY NOT PINNED in test/config/environment.ts -- do not "fix" this
47
+ // as part of abofs/stonyx-rest-server#43. Same trap as the key above, and now
48
+ // measured for both: pin `strictRoutes: true` in test/config/environment.ts
49
+ // AND invert this line to `=== 'true'`, and the suite reports 31 pass /
50
+ // 0 fail. A naive pin makes an insecure published default completely
51
+ // invisible to a green suite.
52
+ //
53
+ // Unpinned, inverting this line alone turns #50's AC1 and AC2 red (29/2).
54
+ // AC3 stays GREEN under that mutation, because AC3 sets `strictRoutes` on the
55
+ // config object directly and so guards src/route-matching.ts's READ rather
56
+ // than this default -- the two assertions cover different halves and neither
57
+ // subsumes the other.
58
+ //
59
+ // The cost is that the suite is ambient-sensitive here
60
+ // (`REST_STRICT_ROUTES=false pnpm test` => 29 pass / 2 fail), but it fails
61
+ // LOUDLY, so there is no false green. Closing #43 for either key needs
62
+ // subprocess-based env isolation this repo does not have; any fix must keep a
63
+ // live assertion on this default.
64
+ strictRoutes: REST_STRICT_ROUTES !== 'false',
65
+
34
66
  enableHealthCheck: REST_HEALTH_CHECK_DISABLE !== 'true',
35
67
  origin: REST_CORS_ORIGIN ?? '*',
36
68
  methods: REST_CORS_METHODS ?? 'GET,POST,PATCH,PUT,DELETE',
@@ -29,7 +29,49 @@ import type { Express } from 'express';
29
29
  *
30
30
  * Note `express({ caseSensitive: true })` does NOT work: express 5's
31
31
  * `createApplication()` takes zero arguments and forwards nothing. The app
32
- * setting is the only mechanism.
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@2.2.0 `index.js:400-401` hardcodes
57
+ * `strict: false, end: false` for `Router.prototype.use`, so mount segments
58
+ * are structurally strict-immune. That is the OPPOSITE of `sensitive`, which
59
+ * `use()` does forward (line 399) and which is why #47's parent site closed
60
+ * `/PUBLIC/...`. Both sites still get both settings -- they share this
61
+ * function -- but the justification differs and the tests are built on the
62
+ * measured split, not on the analogy.
63
+ *
64
+ * Consequence worth stating so nobody "fixes" it: the mount-segment trailing
65
+ * slash (`/public/`) can never be closed by this setting, and does not need to
66
+ * be. For both `/public` and `/public/` the mounted sub-app receives
67
+ * `req.path === '/'`, so a `req.path` auth hook sees no difference and there is
68
+ * no asymmetry. A test asserting `/public/` -> 404 could never pass. The
69
+ * residual is `req.originalUrl`, which DOES differ; that is disclosed in the
70
+ * README rather than silently closed over.
71
+ *
72
+ * Both guards are `!== false` for the same reason: these flags default to the
73
+ * truthy direction, so a truthy check fails OPEN for a consumer whose shipped
74
+ * config predates the key.
33
75
  */
34
76
  export default function applyRouteMatching(api: Express): void;
35
77
  //# sourceMappingURL=route-matching.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"route-matching.d.ts","sourceRoot":"","sources":["../src/route-matching.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAGvC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAE7D"}
1
+ {"version":3,"file":"route-matching.d.ts","sourceRoot":"","sources":["../src/route-matching.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAGvC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyEG;AACH,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAG7D"}
@@ -29,10 +29,54 @@ import config from 'stonyx/config';
29
29
  *
30
30
  * Note `express({ caseSensitive: true })` does NOT work: express 5's
31
31
  * `createApplication()` takes zero arguments and forwards nothing. The app
32
- * setting is the only mechanism.
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@2.2.0 `index.js:400-401` hardcodes
57
+ * `strict: false, end: false` for `Router.prototype.use`, so mount segments
58
+ * are structurally strict-immune. That is the OPPOSITE of `sensitive`, which
59
+ * `use()` does forward (line 399) and which is why #47's parent site closed
60
+ * `/PUBLIC/...`. Both sites still get both settings -- they share this
61
+ * function -- but the justification differs and the tests are built on the
62
+ * measured split, not on the analogy.
63
+ *
64
+ * Consequence worth stating so nobody "fixes" it: the mount-segment trailing
65
+ * slash (`/public/`) can never be closed by this setting, and does not need to
66
+ * be. For both `/public` and `/public/` the mounted sub-app receives
67
+ * `req.path === '/'`, so a `req.path` auth hook sees no difference and there is
68
+ * no asymmetry. A test asserting `/public/` -> 404 could never pass. The
69
+ * residual is `req.originalUrl`, which DOES differ; that is disclosed in the
70
+ * README rather than silently closed over.
71
+ *
72
+ * Both guards are `!== false` for the same reason: these flags default to the
73
+ * truthy direction, so a truthy check fails OPEN for a consumer whose shipped
74
+ * config predates the key.
33
75
  */
34
76
  export default function applyRouteMatching(api) {
35
77
  if (config.restServer?.caseSensitiveRoutes !== false)
36
78
  api.set('case sensitive routing', true);
79
+ if (config.restServer?.strictRoutes !== false)
80
+ api.set('strict routing', true);
37
81
  }
38
82
  //# sourceMappingURL=route-matching.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"route-matching.js","sourceRoot":"","sources":["../src/route-matching.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,MAAM,eAAe,CAAC;AAEnC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,GAAY;IACrD,IAAI,MAAM,CAAC,UAAU,EAAE,mBAAmB,KAAK,KAAK;QAAE,GAAG,CAAC,GAAG,CAAC,wBAAwB,EAAE,IAAI,CAAC,CAAC;AAChG,CAAC"}
1
+ {"version":3,"file":"route-matching.js","sourceRoot":"","sources":["../src/route-matching.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,MAAM,eAAe,CAAC;AAEnC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyEG;AACH,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,GAAY;IACrD,IAAI,MAAM,CAAC,UAAU,EAAE,mBAAmB,KAAK,KAAK;QAAE,GAAG,CAAC,GAAG,CAAC,wBAAwB,EAAE,IAAI,CAAC,CAAC;IAC9F,IAAI,MAAM,CAAC,UAAU,EAAE,YAAY,KAAK,KAAK;QAAE,GAAG,CAAC,GAAG,CAAC,gBAAgB,EAAE,IAAI,CAAC,CAAC;AACjF,CAAC"}
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "stonyx-async",
5
5
  "stonyx-module"
6
6
  ],
7
- "version": "0.2.1-alpha.20",
7
+ "version": "0.2.1-alpha.22",
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.77"
39
+ "stonyx": "0.2.3-beta.78"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@stonyx/utils": "0.2.3-beta.26",