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

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,382 +79,9 @@ 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 [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. |
84
- | `canonicalRoutes` | **Boolean** | `true` | Reject a request whose raw target is not the canonical path express matched, before your `auth` hook runs. Disable via `REST_CANONICAL_ROUTES=false`. See [Route Matching Strictness](#route-matching-strictness) — **disabling this re-opens a security hole**, and note `GET /route/` at a mount root and every absolute-form request target now 404 on the routes this module registers (see the scope limit under [Upgrading](#upgrading-behaviour-changes)). |
85
- | `canonicalEncoding` | **Boolean** | `true` | Reject a request whose raw target percent-encodes an RFC 3986 §2.3 *unreserved* character (`A-Z a-z 0-9 - . _ ~`), before your `auth` hook runs. Disable via `REST_CANONICAL_ENCODING=false`. See [Route Matching Strictness](#route-matching-strictness) — **disabling this re-opens a security hole**, and note that a client over-encoding an unreserved character in a path now gets 404 (see [Upgrading](#upgrading-behaviour-changes)). Like `canonicalRoutes` this is a **registration-site** control, not a global one: the check runs in the handlers mounted from your request classes, so a route registered directly on `RestServer.instance.api` gets none of it — measured, `GET /direct/%73ecret` → **200** with `id "secret"` while `GET /enc/%73ecret` → 404. |
86
82
  | `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. |
87
83
  | `statusMap` | **Object** | `{}` | Optional mapping of HTTP status codes to custom messages |
88
84
 
89
- ### Route Matching Strictness
90
-
91
- Routes match **case-sensitively, strictly, and only at their canonical target**
92
- by default, and a raw target that percent-encodes an RFC 3986 §2.3 *unreserved*
93
- character is rejected. Four controls, all on.
94
-
95
- Note what that does **not** say. It is not "one accepted spelling per id", and
96
- this module cannot give you that: reserved characters, non-ASCII bytes and
97
- control octets all stay encodable, and every one of them whose hex carries a
98
- letter digit aliases by hex-digit case. The residual is stated and measured
99
- below, under [the residual](#the-residual-stated-plainly).
100
-
101
- | axis | control | example that no longer matches |
102
- |---|---|---|
103
- | casing | `case sensitive routing` (setting) | `GET /users/Success` -> does not reach `/success` |
104
- | trailing slash | `strict routing` (setting) | `GET /users/success/` -> does not reach `/success` |
105
- | canonical target | `canonicalRoutes` (per-request check) | `GET /users/` and `GET http://host/users` -> do not reach the mounted `/users` class |
106
- | percent-encoding | `canonicalEncoding` (per-request check) | `GET /users/%73ecret` -> does not reach `/users/:id` with `id === "secret"` |
107
-
108
- The first two are express settings applied at both construction sites. The last
109
- two are **not settings** — no express setting can express either — they are
110
- per-request checks run ahead of your `auth` hook: one compares the raw request
111
- target against the path express matched, the other rejects a raw target that
112
- percent-encodes a character which never needs encoding. See
113
- [`src/route-matching.ts`](src/route-matching.ts).
114
-
115
- Read [What this does not do](#what-this-does-not-do) and
116
- [Upgrading](#upgrading-behaviour-changes) before you rely on that. One thing the
117
- table does not say: "does not reach the handler" is not the same as "404".
118
-
119
- This is deliberate and security-relevant. Express matches both case-insensitively
120
- and slash-insensitively by default, which means any authorization written
121
- against the request URL can be walked past by changing the case of the request,
122
- or by appending one character:
123
-
124
- ```
125
- GET /owners/angela -> 404 (correctly filtered)
126
- GET /OwNeRs/angela -> 200 (full record) <- closed by case sensitive routing
127
- GET /owners/angela/ -> 200 (full record) <- closed by strict routing
128
- DELETE /ANIMALS/22 -> 204 (record destroyed)
129
- DELETE /animals/22/ -> 204 (record destroyed)
130
- ```
131
-
132
- The consumer's predicate is stricter than the router that dispatched the
133
- request, so the router hands the handler a request the predicate would have
134
- rejected. Measured against this repo's own fixture, before and after:
135
-
136
- ```
137
- before after
138
- GET /private/failure 505 505 (auth hook fires, request blocked)
139
- GET /private/failure/ 200 404 (auth hook never fired; now a miss)
140
- GET /private/FAILURE 200 200 (absorbed by /:id — see below)
141
- ```
142
-
143
- For a handler that authorizes on `req.path`, the path it sees can now only ever
144
- be the exact registered spelling, in the exact registered casing, with no
145
- trailing slash. That closes [#47](https://github.com/abofs/stonyx-rest-server/issues/47)
146
- and [#50](https://github.com/abofs/stonyx-rest-server/issues/50).
147
-
148
- #### The canonical-target check (`canonicalRoutes`)
149
-
150
- **No express *setting* closes the trailing slash on a mount root**, and that has
151
- not changed:
152
-
153
- ```
154
- GET /public -> req.path '/' req.originalUrl '/public'
155
- GET /public/ -> req.path '/' req.originalUrl '/public/'
156
- ```
157
-
158
- Express's router applies mount-prefix matching with `strict: false`
159
- unconditionally (`router@2.2.0`; the file-and-line citation is in
160
- [`docs/project-structure.md`](docs/project-structure.md) § *Strict routing
161
- (#50)*), so both forms reach the mounted route class and both arrive with
162
- `req.path === '/'`. A hook authorizing on `req.path` cannot tell them apart, so
163
- for that hook there is no asymmetry to exploit — and there is nothing for
164
- `strict routing` to reject. **A hook comparing `req.originalUrl` sees two
165
- different strings**, and that was a live authorization bypass.
166
-
167
- `canonicalRoutes` closes it, as a per-request check rather than a setting
168
- ([#54](https://github.com/abofs/stonyx-rest-server/issues/54)). Before your
169
- `auth` hook runs, the raw request target is compared against the path express
170
- matched, and a mismatch is rejected as a plain 404. It closes **two** vectors
171
- against `req.originalUrl` — the field express does not normalize:
172
-
173
- ```
174
- before after
175
- GET /admin 401 401 (hook fires, request blocked)
176
- GET /admin/ 200 404 (hook never fired; now a miss)
177
- GET http://host/admin 200 404 (absolute-form; hook never fired)
178
- GET http://host/admin/settings 200 404 (absolute-form; every route from a request class)
179
- ```
180
-
181
- The second vector is the one to check first. [RFC 9112
182
- §3.2.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2.2) permits an
183
- **absolute-form** request target, express routes it, and it hands your hook the
184
- whole URI — `req.originalUrl === "http://host/admin"`. Unlike the mount-root
185
- slash, that affects **every route mounted from a request class**, not one edge.
186
- The qualifier is a registration-site limit, not a special case for one URL: the
187
- check runs inside the handlers this module registers, so anything you register
188
- directly on `RestServer.instance.api` is outside it. In this repo that is
189
- `/health` alone, and `GET http://host/health` still returns 200 — measured.
190
-
191
- The target is compared **raw**. It is not parsed, normalized or resolved first:
192
- normalizing it would launder exactly the string your hook is exposed to and
193
- re-open the absolute-form vector by construction. Only the query string is
194
- removed before comparison, so `GET /admin?x=1` still reaches your hook — **strip
195
- the query yourself** if your hook compares `req.originalUrl` against a fixed
196
- path, or it will not match (see [Consumer
197
- Contracts](#consumer-contracts)). Rejections are indistinguishable from a
198
- genuine miss by design; see [Upgrading](#upgrading-behaviour-changes).
199
-
200
- Routes registered *with* a literal trailing slash are unaffected — their
201
- canonical target carries the slash. So are index-mounted route classes and
202
- query strings on canonical paths.
203
-
204
- **Param routes: "unaffected" means "no regression".** `/resource/:id` keeps
205
- matching exactly as it did. `canonicalRoutes` is structurally blind to how a
206
- param value is *spelled* — express decodes only `req.params`, so `target` and
207
- `canonical` are both the same encoded string and this comparison passes the
208
- request through by construction. That axis has its own control, below.
209
-
210
- #### The percent-encoding check (`canonicalEncoding`)
211
-
212
- Express decodes **`req.params` and nothing else**. `req.path` and
213
- `req.originalUrl` both stay percent-encoded, so an `auth` hook comparing either
214
- of them against a fixed string was walked past by re-spelling the id:
215
-
216
- ```
217
- before after
218
- GET /enc/secret 401 401 (hook fires, request blocked)
219
- GET /enc/%73ecret 200 404 (hook never fired; handler got id "secret")
220
- GET /enc/%73%65%63%72%65%74 200 404
221
- GET /private/%66ailure 200 404 (guard missed; absorbed by a sibling /:id)
222
- ```
223
-
224
- `canonicalEncoding` closes it
225
- ([#56](https://github.com/abofs/stonyx-rest-server/issues/56)). Before your
226
- `auth` hook runs, the query-stripped raw target is rejected as a plain 404 if it
227
- contains a percent-triplet whose octet is an
228
- [RFC 3986 §2.3](https://www.rfc-editor.org/rfc/rfc3986#section-2.3)
229
- **unreserved** character — `A-Z`, `a-z`, `0-9`, `-`, `.`, `_`, `~`. Those are
230
- exactly the characters a URI generator must **not** encode and a normalizer
231
- **must** decode, so nothing a client is required to send is affected.
232
-
233
- **This is not a list of spellings, it is a family.** For an id of *n* bytes
234
- there are `∏(1 + vᵢ) − 1` non-canonical spellings, where a byte whose hex
235
- carries a letter digit has two (`m`, `%6d`, `%6D`). Measured against unfixed
236
- code: **63 of 63** spellings of `secret` returned 200, and **71 of 71** of
237
- `admin`. Enumerating them in your own hook is not a remedy.
238
-
239
- **Three things it deliberately does not reject:**
240
-
241
- ```
242
- GET /enc/sec%2fret -> 200 id "sec/ret" %2f is RESERVED — must stay encodable
243
- GET /enc/a%2Bb -> 200 id "a+b" %2B is RESERVED
244
- GET /enc/%2573ecret -> 200 id "%73ecret" express decodes exactly ONCE
245
- GET /enc/x?name=%61 -> 200 id "x" the query string is stripped, not scanned
246
- GET /enc/%zz -> 400 malformed escapes are the router's 400, unchanged
247
- ```
248
-
249
- If you were tempted to write `decodeURIComponent(req.path)` in your hook: the
250
- first line is why not. The router **splits then decodes**, so `sec%2fret` is one
251
- segment naming the id `sec/ret`; a hook that decodes then splits sees two
252
- segments and denies a request the router routed somewhere else entirely. And a
253
- hook that decodes *until stable* denies line three, which is a legitimately
254
- distinct id.
255
-
256
- #### The residual, stated plainly
257
-
258
- **This does not give each id one spelling.** The rule rejects an over-encoded
259
- *unreserved* octet, which means every octet **outside** `A-Za-z0-9-._~` stays
260
- encodable — and any such octet whose hex carries a letter digit therefore has
261
- two accepted spellings, upper- and lower-case hex. **That is every reserved
262
- character, every non-ASCII byte, and every control octet whose hex carries a
263
- letter digit — not only the reserved ones.** It is not the whole complement of
264
- `A-Za-z0-9-._~`: `%21` and `%40` carry no letter hex digit and alias
265
- literal-versus-encoded instead, `%00` and `%09` keep exactly one accepted
266
- spelling and do not alias at all, and `%90` is a 400. Two different raw targets
267
- can still name the same record:
268
-
269
- ```
270
- GET /enc/a+b -> 200 id "a+b"
271
- GET /enc/a%2Bb -> 200 id "a+b" <- two accepted spellings, one id
272
- GET /enc/sec%2fret -> 200 id "sec/ret"
273
- GET /enc/sec%2Fret -> 200 id "sec/ret" <- hex-digit case, same id again
274
-
275
- GET /i18n/caf%C3%A9 -> 401 hook denies this spelling
276
- GET /i18n/caf%c3%a9 -> 200 id "café" <- same id, hook walked past
277
- GET /i18n/%E5%8C%97%E4%BA%AC -> 401
278
- GET /i18n/%e5%8c%97%e4%ba%ac -> 200 id "北京"
279
- GET /i18n/a%0Db -> 401
280
- GET /i18n/a%0db -> 200 id "a\rb" <- a CONTROL octet, same aliasing
281
- ```
282
-
283
- The last six lines were measured against this module's shipped predicate on a
284
- deny list holding **no reserved character at all** — the three uppercase-hex
285
- spellings, nothing else. If your ids are i18n text, or anything else that is not
286
- pure `A-Za-z0-9-._~`, the residual applies to you. Reading it as "only affects
287
- ids with a `/` or a `+` in them" is the mistake this paragraph exists to
288
- prevent.
289
-
290
- **So a hook comparing a raw path string is still unsound for any id carrying an
291
- octet outside `A-Za-z0-9-._~` that keeps more than one accepted spelling —
292
- reserved characters, non-ASCII bytes and control octets whose hex carries a
293
- letter digit alike — and `req.params` is the sound comparison.** `req.params` is
294
- decoded by express and is populated *before* your `auth` hook runs, by design —
295
- compare that, and none of this applies to you. This module cannot close the
296
- residual for you without 404ing encodings clients are entitled to send; see
297
- [Consumer Contracts](#consumer-contracts).
298
-
299
- #### What this does not do
300
-
301
- **It does not normalize path *parameter values*.** If your `auth()` hook rejects
302
- `params.id === 'restricted'`, then `GET /private/RESTRICTED` still reaches the
303
- handler — the router matched the route correctly, and `restricted` and
304
- `RESTRICTED` are different values. Record ids are legitimately case-sensitive,
305
- so this is a comparison your application owns. Compare param values with the
306
- same case-handling you use when you look them up.
307
-
308
- **A sub-path that misses is not necessarily a 404.** If the route class also
309
- registers a param route such as `/:id`, a mis-cased sub-path is absorbed by it
310
- rather than rejected. `GET /private/FAILURE` misses `/failure` and is dispatched
311
- to `/:id` with `id="FAILURE"` — a different handler, at 200, not a miss; this
312
- repo's AC5 asserts exactly that. A class exposing `/orders/summary` alongside
313
- `/orders/:id` will send `GET /orders/SUMMARY` into the `/:id` handler and its
314
- database lookup. The param route's own `auth()` hook still runs, so this is an
315
- expectation defect rather than a bypass — but plan for a reroute, not a 404.
316
-
317
- Note the two axes differ here. A *trailing slash* is not absorbed by `/:id`,
318
- because `/:id` is equally strict: `GET /private/failure/` misses `/failure` and
319
- misses `/:id`, and is a true 404.
320
-
321
- **It does not redirect or rewrite** mixed-case or trailing-slash requests to
322
- their canonical form. Whether `/Users` is a typo to forgive or an attack to
323
- reject is an application policy decision, and encoding it here would mint
324
- another variant of the bug above.
325
-
326
- #### Upgrading: behaviour changes
327
-
328
- All four controls change which requests match, so all four are
329
- consumer-visible.
330
-
331
- **A client that over-encodes an unreserved character in a path now gets 404.**
332
- `GET /public/url-params/%61/b/c` returns **404** where it previously returned
333
- 200 — measured on this repo's own fixture. `%61` is `a`, and
334
- [RFC 3986 §2.3](https://www.rfc-editor.org/rfc/rfc3986#section-2.3) says a
335
- generator must not encode it, so no correct client emits this. Some do anyway:
336
- over-eager `encodeURIComponent` on an id that never needed it, a URL builder
337
- that percent-encodes everything, or a client library normalizing in the wrong
338
- direction. Reserved characters are **unaffected** — `%2f`, `%2B`, `%25` and
339
- every non-ASCII byte still route, and so does anything in the query string.
340
- Remediation is `REST_CANONICAL_ENCODING=false`, or fix the client. Like the two
341
- below, the rejection is **indistinguishable from a route that was never
342
- registered**, and this module emits no request logging.
343
-
344
- **Clients or forward proxies sending absolute-form request targets now get 404
345
- on every route mounted from a request class.** `GET http://host/admin HTTP/1.1`
346
- is a legal request target
347
- ([RFC 9112 §3.2.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2.2)), and
348
- express used to route it. It is now rejected on every route this module
349
- registers — for a client that emits it, this is a total outage, not a partial
350
- one, and it is the largest blast radius in this change. The one carve-out is a
351
- **registration site**, not a route: the check lives in the handlers mounted from
352
- your request classes, so anything registered directly on
353
- `RestServer.instance.api` never reaches it. `GET /health` is the only such route
354
- in this repo, and `GET http://host/health` still returns 200 — so do not use it
355
- to confirm the new rejection is live, and do not assume an authorized route you
356
- registered on `api` yourself is covered. Reverse proxies in normal use (nginx,
357
- HAProxy, AWS ALB) send origin-form and are unaffected; **forward** proxies and
358
- hand-rolled HTTP clients are the exposure. Remediation is
359
- `REST_CANONICAL_ROUTES=false`, or fix the client.
360
-
361
- **`GET /route/` at a mounted route class's root now returns 404.** Previously
362
- 200. If a client appends a trailing slash to a mount root, it stops working.
363
-
364
- Both rejections are **indistinguishable from a route that was never
365
- registered** — same status, same `Content-Type`, same headers — which is the
366
- intended security property: a distinguishable rejection is an oracle telling an
367
- attacker the route exists and was merely spelled wrong. Combined with this
368
- module emitting **no request logging**, a broken client shows up as a bare
369
- `Cannot GET …` with nothing at all on the server side. **Check this first if
370
- routes start 404ing after upgrade.**
371
-
372
- **`GET /health/` now returns 404.** `GET /health` is unaffected. This is the
373
- change most likely to page someone, and it is an **availability** problem rather
374
- than a 404 you will read about in a log: if a Kubernetes liveness probe, an ELB
375
- target-group health check or an uptime monitor is pointed at the trailing-slash
376
- form, it starts failing and the deployment gets marked unhealthy and cycled.
377
- This module emits no request logging, so the only symptom is the probe going
378
- red. **Check your probe URLs before upgrading.**
379
-
380
- Also affected:
381
-
382
- - **Param routes.** `/resource/:id/` no longer matches. Any client calling
383
- `/private/restricted/` gets a 404 where it previously got the param route.
384
- - **Trailing-slash-normalizing proxies.** nginx `try_files`/`rewrite`, Apache
385
- `DirectorySlash On` and some CDN edge rules append a slash; behind one of
386
- those, every route stops matching at once.
387
- - **Mount paths from filenames.** With `camelCaseRoutes` truthy, `phone-number.ts`
388
- mounts at `/phoneNumber`, so `GET /phonenumber` returns 404; with it falsy,
389
- `Users.ts` mounts at `/Users`, so `GET /users` returns 404.
390
-
391
- A request that stops matching returns express's default `404 Cannot GET /x` with
392
- no log line and no stack, so it looks like a deploy that dropped a route.
393
-
394
- #### Opting out
395
-
396
- Four separate flags, one per axis:
397
-
398
- ```bash
399
- REST_CASE_SENSITIVE_ROUTES=false # restores case-insensitive matching (#47)
400
- REST_STRICT_ROUTES=false # restores trailing-slash tolerance (#50)
401
- REST_CANONICAL_ROUTES=false # restores non-canonical request targets (#54)
402
- REST_CANONICAL_ENCODING=false # restores percent-encoded spellings (#56)
403
- ```
404
-
405
- or equivalently
406
- `restServer: { caseSensitiveRoutes: false, strictRoutes: false, canonicalRoutes: false, canonicalEncoding: false }`.
407
-
408
- **They are deliberately separate keys, and none implies the others.** Slash
409
- tolerance is a legitimate need — a health-check URL you cannot change today is
410
- the common case. Casing tolerance almost never is. Folding them into one flag
411
- would force anyone who needs the first to accept the second, which is why a
412
- consumer who took the `#47` opt-out still has to set `REST_STRICT_ROUTES=false`
413
- separately to keep trailing slashes working. `REST_CANONICAL_ROUTES=false` is
414
- separate for the same reason: a consumer who needs mount-root slash tolerance
415
- should not have to re-open `#50`'s sub-path bypass to get it.
416
-
417
- `REST_CANONICAL_ROUTES=false` is a **temporary remediation**, not a
418
- configuration to run on. It re-opens both `#54` vectors at once — the mount-root
419
- slash *and* the absolute-form target — against any hook authorizing on
420
- `req.originalUrl`. It is env-only, so restoring service does not need a
421
- redeploy; use it to stop the bleeding, then fix the client and remove it.
422
-
423
- **`REST_CANONICAL_ENCODING=false` re-opens the `#56` bypass, and it is the
424
- widest of the four.** With it set, `GET /users/%73ecret` reaches your `/:id`
425
- handler with `id === "secret"` while your hook compared `%73ecret` and did not
426
- match — and it does that against a hook comparing `req.path` **or**
427
- `req.originalUrl`, on every route class with a param segment. The other three
428
- flags each re-open one field's worth of exposure; this one re-opens both. It is
429
- also **independent** of `REST_CANONICAL_ROUTES`: if you have to set that one for
430
- an absolute-form-emitting forward proxy, you keep this one on, which is exactly
431
- why they are separate keys. If you must set it, the mitigation that costs you
432
- nothing is to compare `req.params` in your hook rather than a raw path string —
433
- `req.params` is decoded and was never exposed to this.
434
-
435
- **Each flag restores the corresponding vulnerability described above** — the
436
- URL-based authorization in your application becomes bypassable along that axis
437
- again. They exist as one-line remediations for an existing deployment, not as a
438
- configuration to run on. Set the flag to restore service, then fix the client
439
- and remove the flag.
440
-
441
- ### Consumer Contracts
442
-
443
- Three things this module deliberately does **not** do for you. Each is a state
444
- the framework permits, only your own discipline prevents, and that produces **no
445
- error and no log** when that discipline lapses — so they are collected here
446
- rather than left implied by the sections above.
447
-
448
- | you must | because | symptom if you don't |
449
- |---|---|---|
450
- | **Strip the query string** before comparing `req.originalUrl` to a fixed path in an `auth` hook | `canonicalRoutes` compares the query-*stripped* target — a query string is a legitimately variable part of a request target, and rejecting on it would 404 every `?`-carrying request | `GET /admin?x=1` reaches your guarded handler **unauthenticated**, 200, no error, no log. Measured identical before and after `canonicalRoutes` |
451
- | **Compare `req.params`, not a raw path string** | express decodes only `req.params`; `req.path` and `req.originalUrl` both stay percent-encoded. `canonicalEncoding` (#56) rejects an over-encoded *unreserved* character, but every octet outside `A-Za-z0-9-._~` stays encodable — **reserved characters, non-ASCII bytes and control octets alike** — and every one of them whose hex carries a letter digit aliases by hex-digit case, so one decoded id still has more than one accepted spelling: `GET /orders/a+b` and `GET /orders/a%2Bb` both run the handler with `id === "a+b"`, `sec%2fret` / `sec%2Fret` both give `sec/ret`, and `caf%C3%A9` / `caf%c3%a9` both give `café`. The class is **not** limited to reserved characters — see [the residual](#the-residual-stated-plainly) | your hook compares one spelling, the request arrives in another, and the handler runs **unauthenticated** — 200, no error, no log. Comparing `req.params.id` instead is immune by construction, and it is populated before `auth()` runs |
452
- | **Compare param values with the same casing you look them up with** | param *values* are never case-normalized, and record ids are legitimately case-sensitive | `GET /orders/SECRET` runs the handler with `id === "SECRET"` while your hook compared `secret`. Note that lower-casing the value is **not** the fix: it false-denies a genuinely distinct `SECRET` record and, measured in a sibling module, false-allowed an encoded spelling at the same time |
453
- | **Return `undefined` from `auth()` to mean "authorized"** — never `0` | any integer return is sent as the HTTP status | returning `0` sends a `0` status rather than allowing the request |
454
-
455
- `test/sample/requests/admin.ts` in this repo is the worked example of a
456
- correctly-written `originalUrl` hook for the first row.
457
-
458
85
  ### Running Behind a Load Balancer
459
86
 
460
87
  When your application runs behind a reverse proxy or load balancer (e.g. AWS ALB/ELB), the load balancer terminates SSL and forwards requests to your server over HTTP internally. This means Express sees `http` as the protocol even though the original client request used `https`.
@@ -1,205 +1,13 @@
1
1
  const {
2
- REST_CANONICAL_ENCODING,
3
- REST_CANONICAL_ROUTES,
4
- REST_CASE_SENSITIVE_ROUTES,
5
2
  REST_CORS_ORIGIN,
6
3
  REST_CORS_METHODS,
7
4
  REST_HEALTH_CHECK_DISABLE,
8
5
  REST_PORT,
9
6
  REST_REQUEST_PATH,
10
- REST_STRICT_ROUTES,
11
7
  REST_TRUST_PROXY
12
8
  } = process.env;
13
9
 
14
10
  const config = {
15
- // Secure by default: routes match case-sensitively so a consumer's
16
- // URL-based authorization cannot be walked past by changing case
17
- // (abofs/stonyx-rest-server#47). Opt out with REST_CASE_SENSITIVE_ROUTES=false
18
- // only as a temporary remediation for a client that relies on loose casing.
19
- //
20
- // DELIBERATELY NOT PINNED in test/config/environment.ts -- do not "fix" this
21
- // as part of abofs/stonyx-rest-server#43. This line is the only thing the
22
- // suite still checks about the SHIPPED default. Inverting it to
23
- // `=== 'true'` turns AC3, AC4 and AC5 red; AC6 stays GREEN, because AC6
24
- // stubs `caseSensitiveRoutes` to `undefined` and src/route-matching.ts reads
25
- // `!== false`, so AC6 guards the source's read and not this default.
26
- // Measured: pin `caseSensitiveRoutes: true` in test/config/environment.ts AND
27
- // invert this line, and the suite reports 34 pass / 0 fail. A naive pin makes
28
- // an insecure published default completely invisible to a green suite --
29
- // quieter and weaker, which is the outcome pinning was supposed to prevent.
30
- // Inverting this line ALONE, unpinned, reports 31 pass / 3 fail (#47's AC3,
31
- // AC4 and AC5; AC6 green).
32
- //
33
- // The cost of leaving it unpinned is that the suite is ambient-sensitive here
34
- // (`REST_CASE_SENSITIVE_ROUTES=false pnpm test` => 31 pass / 3 fail), but it
35
- // fails LOUDLY, so there is no false green.
36
- //
37
- // Every count in this block was re-measured against the 34-test suite at
38
- // #54's head. PASS totals here move whenever a test is ADDED anywhere in the
39
- // repo -- the FAIL counts are the load-bearing half. Re-measure, do not
40
- // adjust by arithmetic, when this block next looks stale. Closing #43 for this key needs
41
- // the subprocess-based env isolation this repo does not yet have; any fix
42
- // must keep a live assertion on this default.
43
- caseSensitiveRoutes: REST_CASE_SENSITIVE_ROUTES !== 'false',
44
-
45
- // Secure by default, same polarity and same reasoning as caseSensitiveRoutes
46
- // above: routes match strictly, so a trailing slash cannot walk past a
47
- // consumer's URL-based authorization (abofs/stonyx-rest-server#50).
48
- //
49
- // BEHAVIOUR CHANGE for consumers upgrading: `/health/` now returns 404, and
50
- // param routes like `/resource/:id/` no longer match. Opt out with
51
- // REST_STRICT_ROUTES=false only as a temporary remediation. It is a separate
52
- // key from REST_CASE_SENSITIVE_ROUTES on purpose -- opting out of slash
53
- // strictness must not silently re-open #47's case bypass.
54
- //
55
- // DELIBERATELY NOT PINNED in test/config/environment.ts -- do not "fix" this
56
- // as part of abofs/stonyx-rest-server#43. Same trap as the key above, and now
57
- // measured for both: pin `strictRoutes: true` in test/config/environment.ts
58
- // AND invert this line to `=== 'true'`, and the suite reports 34 pass /
59
- // 0 fail. A naive pin makes an insecure published default completely
60
- // invisible to a green suite.
61
- //
62
- // Unpinned, inverting this line alone turns #50's AC1 and AC2 red (32/2).
63
- // AC3 stays GREEN under that mutation, because AC3 sets `strictRoutes` on the
64
- // config object directly and so guards src/route-matching.ts's READ rather
65
- // than this default -- the two assertions cover different halves and neither
66
- // subsumes the other.
67
- //
68
- // The cost is that the suite is ambient-sensitive here
69
- // (`REST_STRICT_ROUTES=false pnpm test` => 32 pass / 2 fail), but it fails
70
- // LOUDLY, so there is no false green. All counts in this block re-measured
71
- // against the 34-test suite at #54's head; the FAIL count is the load-bearing
72
- // half, since PASS totals move whenever a test is added anywhere. Closing #43 for either key needs
73
- // subprocess-based env isolation this repo does not have; any fix must keep a
74
- // live assertion on this default.
75
- strictRoutes: REST_STRICT_ROUTES !== 'false',
76
-
77
- // Secure by default, same polarity and same reasoning as the two keys above:
78
- // a request whose RAW target is not the canonical path express matched is
79
- // rejected with a plain 404 before the consumer's `auth` hook runs
80
- // (abofs/stonyx-rest-server#54). This is NOT an express setting -- it is a
81
- // per-request check in src/route-matching.ts (`shouldRejectTarget`), called
82
- // from the handler closure in src/request.ts.
83
- //
84
- // BEHAVIOUR CHANGE for consumers upgrading, on TWO axes:
85
- // 1. `GET /route/` at a mounted route class's ROOT now returns 404.
86
- // 2. Clients or forward proxies emitting an ABSOLUTE-FORM request target
87
- // (`GET http://host/admin HTTP/1.1`, RFC 9112 3.2.2) now receive 404 on
88
- // every route registered through Request.registerCalls(). That is a
89
- // REGISTRATION-SITE limit, not a carve-out for one URL: /health is
90
- // registered directly on the parent app (src/main.ts) and still answers
91
- // 200 to an absolute-form target -- and so would any route a consumer
92
- // registers on the public RestServer.instance.api itself. This is the
93
- // larger blast radius: for such a client it is a total outage, not a
94
- // partial one. Reverse proxies in normal use
95
- // (nginx, HAProxy, ALB) send origin-form and are unaffected.
96
- // This module emits no request log, so both look like a dropped route.
97
- //
98
- // Opt out with REST_CANONICAL_ROUTES=false only as a temporary remediation --
99
- // it RE-OPENS the bypass. Separate key from REST_STRICT_ROUTES and
100
- // REST_CASE_SENSITIVE_ROUTES on purpose: coupling it to strictness would
101
- // force a consumer who needs mount-root slash tolerance to also re-open #50's
102
- // sub-path bypass.
103
- //
104
- // Note trustProxy below deliberately uses `=== 'true'` instead. That is not
105
- // an inconsistency to "fix": its safe default is FALSY, so a truthy check
106
- // already fails closed for it. The rule is "the guard must fail toward the
107
- // safe value", not "all guards look alike".
108
- //
109
- // DELIBERATELY NOT PINNED in test/config/environment.ts -- do not "fix" this
110
- // as part of abofs/stonyx-rest-server#43. Same trap as the two keys above,
111
- // and now measured for all three: pin `canonicalRoutes: true` in
112
- // test/config/environment.ts AND invert this line to `=== 'true'`, and the
113
- // suite reports 34 pass / 0 fail while an insecure default ships. The pin is
114
- // quieter AND weaker than no pin, which is the outcome pinning was supposed
115
- // to prevent.
116
- //
117
- // Unpinned, inverting this line alone reports 32 pass / 2 fail: #54's
118
- // integration AC1 and #50's AC2. AC2 (unit) stays GREEN under that mutation,
119
- // because it sets `canonicalRoutes` on the config object directly and so
120
- // guards src/route-matching.ts's READ rather than this default -- the two
121
- // assertions cover different halves and neither subsumes the other.
122
- // Conversely, weakening the READ to `=== true` reports 33 pass / 1 fail with
123
- // AC2 as the only failure and AC1 fully green. All four counts in this block
124
- // were re-measured on the #54 branch head after the AC1.11/AC1.12 assertions
125
- // were added; the suite is 34 tests, and the FAIL count is the load-bearing
126
- // half.
127
- //
128
- // The cost is that the suite is ambient-sensitive here
129
- // (`REST_CANONICAL_ROUTES=false pnpm test` => 32 pass / 2 fail), but it fails
130
- // LOUDLY, so there is no false green. Closing #43 for any of the three keys
131
- // needs subprocess-based env isolation this repo does not have; any fix must
132
- // keep a live assertion on this default.
133
- canonicalRoutes: REST_CANONICAL_ROUTES !== 'false',
134
-
135
- // Secure by default, same polarity and same reasoning as the three keys
136
- // above: a request whose RAW target percent-encodes an RFC 3986 2.3
137
- // UNRESERVED character (ALPHA / DIGIT / "-" / "." / "_" / "~") is rejected
138
- // with a plain 404 before the consumer's `auth` hook runs
139
- // (abofs/stonyx-rest-server#56). Like canonicalRoutes this is NOT an express
140
- // setting -- it is a per-request check in src/route-matching.ts
141
- // (`shouldRejectEncoding`), called from the handler closure in
142
- // src/request.ts.
143
- //
144
- // What it closes: express decodes `req.params` and NOTHING else, so a
145
- // consumer hook comparing `req.path` OR `req.originalUrl` was walked past by
146
- // re-spelling an id -- `GET /enc/secret` -> 401 while
147
- // `GET /enc/%73ecret` -> 200 with the guarded handler running
148
- // unauthenticated and `req.params.id === 'secret'`. The spelling family is
149
- // PROD(1 + v_i) - 1 per id, measured at 63 spellings for `secret` and 71 for
150
- // `admin`, ALL of them 200 before this key existed. It is EXCLUSIVE to route
151
- // classes carrying a `:param` segment; literal routes and mount segments
152
- // match raw and were never reachable this way.
153
- //
154
- // BEHAVIOUR CHANGE for consumers upgrading, on a FOURTH axis:
155
- // Any client that over-encodes an unreserved character in a path now gets
156
- // 404. Measured on this repo's own fixture:
157
- // `GET /public/url-params/%61/b/c` -> 404 (was 200). Over-encoding an
158
- // unreserved character is never required by RFC 3986 -- a normaliser MUST
159
- // decode these (6.2.2.2) -- so the blast radius is smaller than #54's,
160
- // whose absolute-form vector hits a real deployment shape. It is still a
161
- // breaking change and it is documented as one in the README.
162
- //
163
- // Opt out with REST_CANONICAL_ENCODING=false only as a temporary remediation
164
- // -- it RE-OPENS the bypass. SEPARATE key from REST_CANONICAL_ROUTES on
165
- // purpose, and this one is not a symmetry argument but a measurement: with
166
- // the rule gated on `canonicalRoutes` instead of its own key,
167
- // `REST_CANONICAL_ROUTES=false` returns `GET /enc/%73ecret` to 200 -- and
168
- // that flag is exactly what a consumer behind an absolute-form-emitting
169
- // forward proxy must set to stay up. Folding the two would hand precisely
170
- // those consumers the encoding bypass as the price. Killed by
171
- // test/unit/request-test.ts AC5.
172
- //
173
- // DELIBERATELY NOT PINNED in test/config/environment.ts -- do not "fix" this
174
- // as part of abofs/stonyx-rest-server#43. Both halves RE-MEASURED for this
175
- // key rather than inferred from the three above, against the 41-test suite
176
- // at #56's head:
177
- // (a) invert this line to `=== 'true'` ALONE, unpinned:
178
- // 36 pass / 5 fail -- #56's integration AC1 and AC2, unit AC5 and AC6,
179
- // and [Unit] Config AC7. It fails LOUDLY, so there is no false green.
180
- // (b) pin `canonicalEncoding: true` in test/config/environment.ts AND
181
- // invert this line: 40 pass / 1 fail, and the ONE failure is
182
- // [Unit] Config AC7 -- every behavioural assertion goes green because
183
- // the pin supplies the secure value the suite then observes. Measured
184
- // again with test/unit/config-test.ts removed: 40 pass / 0 fail, a
185
- // fully green suite shipping an insecure default. That is the trap, and
186
- // AC7 is the only thing standing between this key and it.
187
- //
188
- // Conversely, weakening the READ in src/route-matching.ts to `=== true`
189
- // reports 40 pass / 1 fail with unit AC6 as the only failure and every
190
- // integration assertion green -- the two guard different halves and neither
191
- // subsumes the other. Closing #43 for any of the four keys needs the
192
- // subprocess-based env isolation this repo does not have; any fix must keep a
193
- // live assertion on this default.
194
- //
195
- // FOUR security-relevant keys now, all defaulting on, all disable-able, none
196
- // pinned. Per docs/framework/testing.md the pinned set has to be evaluated as
197
- // a SET rather than key by key; [Unit] Config AC7 asserts all four together
198
- // for that reason. Whoever takes #43 inherits four keys and this paragraph as
199
- // the reason they are unpinned, rather than finding four and assuming
200
- // neglect.
201
- canonicalEncoding: REST_CANONICAL_ENCODING !== 'false',
202
-
203
11
  enableHealthCheck: REST_HEALTH_CHECK_DISABLE !== 'true',
204
12
  origin: REST_CORS_ORIGIN ?? '*',
205
13
  methods: REST_CORS_METHODS ?? 'GET,POST,PATCH,PUT,DELETE',
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAiBA,OAAgB,EAAE,KAAK,OAAO,EAAoE,MAAM,SAAS,CAAC;AAKlH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAEnC,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,cAAc,CAAC;AAElD,MAAM,CAAC,OAAO,OAAO,UAAU;IAC7B,MAAM,CAAC,QAAQ,EAAE,UAAU,CAAC;IAE5B,GAAG,EAAG,OAAO,CAAC;IACd,MAAM,EAAG,MAAM,CAAC;;IAwBhB,MAAM,CAAC,KAAK,IAAI,IAAI;IAQd,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAerB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAelC,qBAAqB,IAAI,IAAI;IAW7B,UAAU,CAAC,iBAAiB,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI;CAarG"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAiBA,OAAgB,EAAE,KAAK,OAAO,EAAoE,MAAM,SAAS,CAAC;AAIlH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAEnC,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,cAAc,CAAC;AAElD,MAAM,CAAC,OAAO,OAAO,UAAU;IAC7B,MAAM,CAAC,QAAQ,EAAE,UAAU,CAAC;IAE5B,GAAG,EAAG,OAAO,CAAC;IACd,MAAM,EAAG,MAAM,CAAC;;IAShB,MAAM,CAAC,KAAK,IAAI,IAAI;IAQd,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAerB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAelC,qBAAqB,IAAI,IAAI;IAW7B,UAAU,CAAC,iBAAiB,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI;CAarG"}
package/dist/main.js CHANGED
@@ -18,7 +18,6 @@ import express from 'express';
18
18
  import config from 'stonyx/config';
19
19
  import log from 'stonyx/log';
20
20
  import { forEachFileImport } from '@stonyx/utils/file';
21
- import applyRouteMatching from './route-matching.js';
22
21
  export { default as Request } from './request.js';
23
22
  export default class RestServer {
24
23
  static instance;
@@ -29,20 +28,6 @@ export default class RestServer {
29
28
  return RestServer.instance;
30
29
  RestServer.instance = this;
31
30
  this.api = express();
32
- // Applies BOTH route-matching settings: case sensitive routing
33
- // (abofs/stonyx-rest-server#47) and strict routing (#50). The two do not
34
- // have the same role at this site:
35
- // - #47: this call closes the mount segment (/PUBLIC/...). The matching
36
- // call in Request's constructor closes sub-paths; both are required.
37
- // - #50: this call closes exactly /health/, the only route registered
38
- // directly on this app. It has NO security role for #50 -- do not
39
- // describe it as having one. Router.prototype.use hardcodes
40
- // `strict: false`, so mount segments are strict-immune, and the call in
41
- // Request's constructor closes the trailing-slash bypass on its own.
42
- // Must stay in the constructor: the router is materialized lazily on first
43
- // route registration, so applying this after setupRouter() is silently
44
- // ineffective. See src/route-matching.ts for the per-site split.
45
- applyRouteMatching(this.api);
46
31
  }
47
32
  static close() {
48
33
  if (!RestServer.instance)
package/dist/main.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,OAA2F,MAAM,SAAS,CAAC;AAClH,OAAO,MAAM,MAAM,eAAe,CAAC;AACnC,OAAO,GAAG,MAAM,YAAY,CAAC;AAC7B,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,kBAAkB,MAAM,qBAAqB,CAAC;AAGrD,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,cAAc,CAAC;AAElD,MAAM,CAAC,OAAO,OAAO,UAAU;IAC7B,MAAM,CAAC,QAAQ,CAAa;IAE5B,GAAG,CAAW;IACd,MAAM,CAAU;IAEhB;QACE,IAAI,UAAU,CAAC,QAAQ;YAAE,OAAO,UAAU,CAAC,QAAQ,CAAC;QACpD,UAAU,CAAC,QAAQ,GAAG,IAAI,CAAC;QAE3B,IAAI,CAAC,GAAG,GAAG,OAAO,EAAE,CAAC;QAErB,+DAA+D;QAC/D,yEAAyE;QACzE,mCAAmC;QACnC,0EAA0E;QAC1E,yEAAyE;QACzE,wEAAwE;QACxE,sEAAsE;QACtE,gEAAgE;QAChE,4EAA4E;QAC5E,yEAAyE;QACzE,2EAA2E;QAC3E,uEAAuE;QACvE,iEAAiE;QACjE,kBAAkB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC/B,CAAC;IAED,MAAM,CAAC,KAAK;QACV,IAAI,CAAC,UAAU,CAAC,QAAQ;YAAE,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;QAErF,MAAM,EAAE,MAAM,EAAE,GAAG,UAAU,CAAC,QAAQ,CAAC;QACvC,MAAM,CAAC,mBAAmB,EAAE,CAAC;QAC7B,MAAM,CAAC,KAAK,EAAE,CAAC;IACjB,CAAC;IAED,KAAK,CAAC,IAAI;QACR,yEAAyE;QACzE,yEAAyE;QACzE,MAAM,EAAE,QAAQ,GAAG,QAAQ,EAAE,SAAS,GAAG,KAAK,EAAE,GAAG,MAAM,CAAC,UAAU,CAAC;QACrE,GAAG,CAAC,UAAU,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;QAEpC,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QAEzB,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,CAAC,UAAU,CAAC;QAEnC,oBAAoB;QACpB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACpC,GAAG,CAAC,KAAK,CAAC,mCAAmC,IAAI,EAAE,CAAC,CAAC;IACvD,CAAC;IAED,KAAK,CAAC,WAAW;QACf,MAAM,EAAE,eAAe,EAAE,GAAG,EAAE,iBAAiB,EAAE,GAAG,MAAM,CAAC,UAAU,CAAC;QACtE,IAAI,CAAC,qBAAqB,EAAE,CAAC;QAE7B,IAAI,CAAC;YACH,MAAM,iBAAiB,CAAC,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,OAAO,EAAE,CAAC,eAAe,EAAE,mBAAmB,EAAE,IAAI,EAAE,CAAC,CAAC;YAEnH,IAAI,iBAAiB;gBAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,IAAoB,EAAE,GAAoB,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;QACtH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,MAAM,CAAC,KAAK;gBAAE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACrC,GAAG,CAAC,KAAK,CAAC,wDAAwD,GAAG,EAAE,CAAC,CAAC;YACzE,MAAM,IAAI,KAAK,CAAC,wDAAwD,GAAG,EAAE,CAAC,CAAC;QACjF,CAAC;IACH,CAAC;IAED,qBAAqB;QACnB,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,MAAM,CAAC,UAAU,CAAC;QAE1D,IAAI,UAAU;YAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC;QAElD,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;YACX,IAAI,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;YACzB,OAAO,CAAC,IAAI,EAAE;SACf,CAAC,CAAC;IACL,CAAC;IAED,UAAU,CAAC,iBAA0B,EAAE,EAAE,IAAI,EAAE,OAAO,EAAuC;QAC3F,MAAM,UAAU,GAAG,iBAAmG,CAAC;QACvH,MAAM,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;QACrB,MAAM,aAAa,GAAG,IAAI,UAAU,CAAC,OAAO,CAAC,CAAC;QAC9C,MAAM,KAAK,GAAG,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;QAClD,MAAM,EAAE,eAAe,EAAE,GAAG,aAAa,CAAC;QAE1C,aAAa,CAAC,aAAa,EAAE,CAAC;QAC9B,eAAe,CAAC,SAAS,GAAG,KAAK,CAAC;QAElC,qCAAqC;QACrC,GAAG,CAAC,GAAG,CAAC,KAAK,EAAE,eAAe,CAAC,CAAC;IAClC,CAAC;CACF"}
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,OAA2F,MAAM,SAAS,CAAC;AAClH,OAAO,MAAM,MAAM,eAAe,CAAC;AACnC,OAAO,GAAG,MAAM,YAAY,CAAC;AAC7B,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAGvD,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,cAAc,CAAC;AAElD,MAAM,CAAC,OAAO,OAAO,UAAU;IAC7B,MAAM,CAAC,QAAQ,CAAa;IAE5B,GAAG,CAAW;IACd,MAAM,CAAU;IAEhB;QACE,IAAI,UAAU,CAAC,QAAQ;YAAE,OAAO,UAAU,CAAC,QAAQ,CAAC;QACpD,UAAU,CAAC,QAAQ,GAAG,IAAI,CAAC;QAE3B,IAAI,CAAC,GAAG,GAAG,OAAO,EAAE,CAAC;IACvB,CAAC;IAED,MAAM,CAAC,KAAK;QACV,IAAI,CAAC,UAAU,CAAC,QAAQ;YAAE,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;QAErF,MAAM,EAAE,MAAM,EAAE,GAAG,UAAU,CAAC,QAAQ,CAAC;QACvC,MAAM,CAAC,mBAAmB,EAAE,CAAC;QAC7B,MAAM,CAAC,KAAK,EAAE,CAAC;IACjB,CAAC;IAED,KAAK,CAAC,IAAI;QACR,yEAAyE;QACzE,yEAAyE;QACzE,MAAM,EAAE,QAAQ,GAAG,QAAQ,EAAE,SAAS,GAAG,KAAK,EAAE,GAAG,MAAM,CAAC,UAAU,CAAC;QACrE,GAAG,CAAC,UAAU,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;QAEpC,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QAEzB,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,CAAC,UAAU,CAAC;QAEnC,oBAAoB;QACpB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACpC,GAAG,CAAC,KAAK,CAAC,mCAAmC,IAAI,EAAE,CAAC,CAAC;IACvD,CAAC;IAED,KAAK,CAAC,WAAW;QACf,MAAM,EAAE,eAAe,EAAE,GAAG,EAAE,iBAAiB,EAAE,GAAG,MAAM,CAAC,UAAU,CAAC;QACtE,IAAI,CAAC,qBAAqB,EAAE,CAAC;QAE7B,IAAI,CAAC;YACH,MAAM,iBAAiB,CAAC,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,OAAO,EAAE,CAAC,eAAe,EAAE,mBAAmB,EAAE,IAAI,EAAE,CAAC,CAAC;YAEnH,IAAI,iBAAiB;gBAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,IAAoB,EAAE,GAAoB,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;QACtH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,MAAM,CAAC,KAAK;gBAAE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACrC,GAAG,CAAC,KAAK,CAAC,wDAAwD,GAAG,EAAE,CAAC,CAAC;YACzE,MAAM,IAAI,KAAK,CAAC,wDAAwD,GAAG,EAAE,CAAC,CAAC;QACjF,CAAC;IACH,CAAC;IAED,qBAAqB;QACnB,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,MAAM,CAAC,UAAU,CAAC;QAE1D,IAAI,UAAU;YAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC;QAElD,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;YACX,IAAI,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;YACzB,OAAO,CAAC,IAAI,EAAE;SACf,CAAC,CAAC;IACL,CAAC;IAED,UAAU,CAAC,iBAA0B,EAAE,EAAE,IAAI,EAAE,OAAO,EAAuC;QAC3F,MAAM,UAAU,GAAG,iBAAmG,CAAC;QACvH,MAAM,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;QACrB,MAAM,aAAa,GAAG,IAAI,UAAU,CAAC,OAAO,CAAC,CAAC;QAC9C,MAAM,KAAK,GAAG,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;QAClD,MAAM,EAAE,eAAe,EAAE,GAAG,aAAa,CAAC;QAE1C,aAAa,CAAC,aAAa,EAAE,CAAC;QAC9B,eAAe,CAAC,SAAS,GAAG,KAAK,CAAC;QAElC,qCAAqC;QACrC,GAAG,CAAC,GAAG,CAAC,KAAK,EAAE,eAAe,CAAC,CAAC;IAClC,CAAC;CACF"}