@bymax-one/nest-core 1.5.2 → 1.5.3
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/CHANGELOG.md +97 -1
- package/README.md +125 -2
- package/dist/index.cjs +6 -3
- package/dist/index.d.cts +6 -3
- package/dist/index.d.ts +6 -3
- package/dist/index.mjs +6 -3
- package/dist/openapi/index.cjs +41 -17
- package/dist/openapi/index.mjs +41 -17
- package/dist/pagination/index.cjs +11 -1
- package/dist/pagination/index.d.cts +32 -3
- package/dist/pagination/index.d.ts +32 -3
- package/dist/pagination/index.mjs +11 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,101 @@ heading here.
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [1.5.3] - 2026-08-18
|
|
15
|
+
|
|
16
|
+
Three findings from a functional and security audit of a derived backend
|
|
17
|
+
running against real Postgres, Redis and MinIO, plus the corrections that
|
|
18
|
+
review found inside those fixes.
|
|
19
|
+
|
|
20
|
+
The one that reaches a running deployment: an unprotected `/metrics` was
|
|
21
|
+
documented as **requiring a credential**. The endpoint answers anyone when no
|
|
22
|
+
`metrics.authToken` is set — the documented "protected at the edge"
|
|
23
|
+
arrangement — and it inherited the document-level default instead of declaring
|
|
24
|
+
itself public. That is the opposite of what 1.5.0 fixed and the more dangerous
|
|
25
|
+
direction: documenting a guarded route as open fails loudly at the first
|
|
26
|
+
generated client that omits the credential, while documenting an open route as
|
|
27
|
+
guarded fails nowhere and hands the wrong answer to whoever opened the document
|
|
28
|
+
to ask what is exposed.
|
|
29
|
+
|
|
30
|
+
**Apply to a derived backend:** bump the dependency. Nothing to change in code.
|
|
31
|
+
If you serve a document and leave the scrape endpoint unprotected, re-render it
|
|
32
|
+
and confirm `/metrics` now carries `security: []`. If your page indexes reach
|
|
33
|
+
SQL, `maxOffset` is now available and is opt-in.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- **An unprotected `/metrics` was documented as requiring a credential.** This
|
|
38
|
+
package writes an explicit `security: []` on its health probes so they do not
|
|
39
|
+
inherit a document-level default, and did not do the same for the scrape
|
|
40
|
+
endpoint. With `metrics.authToken` unset — the documented "protected at the
|
|
41
|
+
edge" arrangement, where the endpoint answers anyone — `GET /metrics` fell
|
|
42
|
+
through and inherited the default, so a document served by any backend with a
|
|
43
|
+
default claimed a credential was required for an endpoint serving process
|
|
44
|
+
metrics to whoever asked.
|
|
45
|
+
|
|
46
|
+
Measured on a running derived backend, not reasoned about: no credential →
|
|
47
|
+
`200` with the full Prometheus body, while the served document said
|
|
48
|
+
`security: [{ bymaxAuthAccessCookie: [] }]`.
|
|
49
|
+
|
|
50
|
+
**One half of the fix is covered by unit tests only, and that is worth saying
|
|
51
|
+
rather than leaving it to look field-verified.** The reported symptom reaches
|
|
52
|
+
a deployment through `openapi.security`, and that path was measured. Review
|
|
53
|
+
then found the same hole on the other path — a document that arrives carrying
|
|
54
|
+
its own default, whose `openapi.security` is therefore empty — and it is fixed
|
|
55
|
+
by reading the effective default from the document that will be served. No
|
|
56
|
+
consumer known to this project reaches that state today, so the only coverage
|
|
57
|
+
that can be pointed at is this repository's tests — which is a statement about
|
|
58
|
+
what is known, not a guarantee that nothing else exercises it. The health probes carried the same defect on
|
|
59
|
+
that path and are fixed by the same change.
|
|
60
|
+
|
|
61
|
+
This is the more dangerous of the two ways to describe a route wrongly, and
|
|
62
|
+
the opposite of what 1.5.0 fixed. Documenting a **guarded** route as open
|
|
63
|
+
fails loudly — a generated client omits the credential and gets a `401`.
|
|
64
|
+
Documenting an **open** route as guarded fails nowhere, and hands the wrong
|
|
65
|
+
answer to whoever opened the document to ask what is exposed.
|
|
66
|
+
|
|
67
|
+
### Added
|
|
68
|
+
|
|
69
|
+
- **`maxOffset`, a bound on how far into a dataset a request may start.**
|
|
70
|
+
`normalizePageQuery` capped the page size through `maxLimit` and bounded the
|
|
71
|
+
page index only for arithmetic safety, so `?page=1000000000&limit=20` resolved
|
|
72
|
+
to `OFFSET 19999999980`. Harmless against an in-memory repository and paid in
|
|
73
|
+
full by an offset-paginated database: twenty bytes of query for a table scan.
|
|
74
|
+
|
|
75
|
+
It is **absent by default and deliberately so** — legitimate deep paging
|
|
76
|
+
exists, and a silent ceiling would change the rows a working query returns.
|
|
77
|
+
Set it wherever the page index reaches SQL. `0` is a valid bound meaning "the
|
|
78
|
+
first page only"; any value that is not a non-negative safe integer reads as
|
|
79
|
+
absent rather than as an invented cap. Clamping matches how `maxLimit` already
|
|
80
|
+
behaves, and the resolved values come back in `meta`.
|
|
81
|
+
|
|
82
|
+
### Documentation
|
|
83
|
+
|
|
84
|
+
- **What the error filter classifies from, and what it cannot.** An error raised
|
|
85
|
+
before any handler ran becomes a clean `4xx` because the filter recognizes it
|
|
86
|
+
by **shape** — `expose: true` with a `4xx` status, the convention Node's body
|
|
87
|
+
pipeline follows — not by class. An error carrying no such marking is a `500`
|
|
88
|
+
even when a client caused it: a few kilobytes nested thousands of levels deep
|
|
89
|
+
overflows the stack during validation and surfaces as `RangeError`, well under
|
|
90
|
+
any size limit.
|
|
91
|
+
|
|
92
|
+
That is deliberate. Mapping `RangeError` to a `4xx` would make the filter
|
|
93
|
+
infer causation from an error class and would be wrong where it matters most —
|
|
94
|
+
a genuine stack overflow in application code is a `500` that should page
|
|
95
|
+
someone. Body-shape limits are the application's floor, applied in the one
|
|
96
|
+
window where the body exists and nothing has walked it yet.
|
|
97
|
+
|
|
98
|
+
**Apply to a derived backend:** cap nesting depth **after the body parser and
|
|
99
|
+
before validation**, so a hostile body is rejected as the `400` it is instead
|
|
100
|
+
of becoming a `5xx` that pollutes your error rate and writes a stack per
|
|
101
|
+
request. On Express that means module middleware, not `app.use()` during
|
|
102
|
+
bootstrap — measured, a middleware registered there runs ahead of Nest's own
|
|
103
|
+
parser and sees `req.body` as `undefined`, so the guard inspects nothing and
|
|
104
|
+
protects nothing while reading as present. Walk the parsed body iteratively; a
|
|
105
|
+
recursive depth check on a hostile payload overflows the stack it exists to
|
|
106
|
+
protect. The README carries the per-adapter table and a test that proves the
|
|
107
|
+
floor by behaviour rather than by where it is registered.
|
|
108
|
+
|
|
14
109
|
## [1.5.2] - 2026-08-15
|
|
15
110
|
|
|
16
111
|
The production guard read `NODE_ENV` and nothing else, and treated an unset
|
|
@@ -779,4 +874,5 @@ have regressed from. They are kept because the reasoning is worth having.
|
|
|
779
874
|
[1.5.0]: https://github.com/bymaxone/nest-core/compare/v1.4.0...v1.5.0
|
|
780
875
|
[1.5.1]: https://github.com/bymaxone/nest-core/compare/v1.5.0...v1.5.1
|
|
781
876
|
[1.5.2]: https://github.com/bymaxone/nest-core/compare/v1.5.1...v1.5.2
|
|
782
|
-
[
|
|
877
|
+
[1.5.3]: https://github.com/bymaxone/nest-core/compare/v1.5.2...v1.5.3
|
|
878
|
+
[Unreleased]: https://github.com/bymaxone/nest-core/compare/v1.5.3...HEAD
|
package/README.md
CHANGED
|
@@ -576,6 +576,96 @@ import { BadRequestException } from '@nestjs/common'
|
|
|
576
576
|
throw new BadRequestException({ code: 'INVOICE_OVERDUE', message: 'Invoice is overdue' })
|
|
577
577
|
```
|
|
578
578
|
|
|
579
|
+
### What the filter classifies from, and what it cannot
|
|
580
|
+
|
|
581
|
+
An error raised before any handler ran — a malformed JSON body, a payload over
|
|
582
|
+
the size limit — still becomes a clean `4xx` envelope rather than a 500. The
|
|
583
|
+
filter recognizes those by **shape**, not by class: it honours an error that
|
|
584
|
+
marks itself `expose: true` with a `4xx` status, which is the `http-errors`
|
|
585
|
+
convention Node's body pipeline follows. Nothing in the application failed, so
|
|
586
|
+
it is not routed through the unexpected-error seam either.
|
|
587
|
+
|
|
588
|
+
**An error that carries no such marking is a 500, including when a client
|
|
589
|
+
caused it.** The clearest case is depth: a body of a few kilobytes nested
|
|
590
|
+
thousands of levels deep overflows the stack during validation and surfaces as
|
|
591
|
+
`RangeError: Maximum call stack size exceeded` — well under any size limit, and
|
|
592
|
+
answered `500 BYMAX_INTERNAL_ERROR`.
|
|
593
|
+
|
|
594
|
+
That is deliberate, and the alternative is worse. Mapping `RangeError` to a
|
|
595
|
+
`4xx` would make the filter infer causation from an error class, and it would be
|
|
596
|
+
wrong exactly where it matters: a genuine stack overflow in your own code is a
|
|
597
|
+
`500` that should page someone, and relabelling it as a client error would hide
|
|
598
|
+
the failure the `500` exists to surface. By the time the filter sees the error,
|
|
599
|
+
the body that caused it is gone.
|
|
600
|
+
|
|
601
|
+
**So body-shape limits are the application's floor, not the filter's.** Cap
|
|
602
|
+
nesting depth and the request is rejected as the `400` it is, instead of
|
|
603
|
+
becoming a `5xx` that pollutes your error rate and writes a stack per request.
|
|
604
|
+
|
|
605
|
+
**Position it after the body parser and before validation** — that window is the
|
|
606
|
+
only place the body exists in a form you can measure and nothing has walked it
|
|
607
|
+
yet. Earlier there is nothing to inspect; later the overflow has already
|
|
608
|
+
happened, which is the failure you are trying to prevent.
|
|
609
|
+
|
|
610
|
+
**Where that window is depends on the adapter, and the obvious answer is wrong
|
|
611
|
+
on Express.** Measured against a real Nest application:
|
|
612
|
+
|
|
613
|
+
| Registration point | `req.body` when it runs |
|
|
614
|
+
| ---------------------------------------- | ----------------------- |
|
|
615
|
+
| `app.use(...)` in `bootstrap.ts` | **`undefined`** |
|
|
616
|
+
| Module middleware, `configure(consumer)` | the parsed body |
|
|
617
|
+
|
|
618
|
+
Nest registers its own parser during `app.init()`, so a middleware added with
|
|
619
|
+
`app.use()` before that is mounted _ahead_ of it — a depth guard there inspects
|
|
620
|
+
nothing and silently protects nothing. Register it as module middleware
|
|
621
|
+
instead. On Fastify the ordering differs again, since Nest middleware runs
|
|
622
|
+
through `@fastify/middie` ahead of body parsing; a `preValidation` hook is the
|
|
623
|
+
place to look, and it is worth measuring rather than assuming.
|
|
624
|
+
|
|
625
|
+
**Registering it in the right place is not enough — the route pattern silently
|
|
626
|
+
skips paths too.** This package hit the same trap with its own timing
|
|
627
|
+
middleware, and the measured behaviour is in `core.module.ts`:
|
|
628
|
+
|
|
629
|
+
| `forRoutes(...)` | Express | Fastify |
|
|
630
|
+
| ---------------- | -------------- | ---------------- |
|
|
631
|
+
| `'*splat'` | skips the root | — |
|
|
632
|
+
| `'{*splat}'` | skips `/api` | every path |
|
|
633
|
+
| `'/'` | every path | matches `/` only |
|
|
634
|
+
|
|
635
|
+
So the named-wildcard form every migration guide reaches for leaves `POST /`
|
|
636
|
+
unguarded on Express. A consumer measured exactly that: with `'*path'`, a
|
|
637
|
+
2000-level body to the root returned `404` because the middleware never ran;
|
|
638
|
+
with `'{*path}'`, `400`. If your application mounts nothing at the root, both
|
|
639
|
+
forms answer `4xx` and the status alone cannot tell you which one you have.
|
|
640
|
+
|
|
641
|
+
**Verify by behaviour, not by wiring — this is the part worth insisting on.**
|
|
642
|
+
Checking where the middleware is registered is what the consumer above did; it
|
|
643
|
+
looked correct, they confirmed it to us, and the guard was inert. Send a body
|
|
644
|
+
nested past your ceiling and require your own rejection:
|
|
645
|
+
|
|
646
|
+
```ts
|
|
647
|
+
it('refuses a body nested past the ceiling', async () => {
|
|
648
|
+
const deep = JSON.parse(`${'['.repeat(2000)}${']'.repeat(2000)}`)
|
|
649
|
+
|
|
650
|
+
const res = await request(app.getHttpServer()).post('/anything').send({ name: deep })
|
|
651
|
+
|
|
652
|
+
// Match your guard's own message, not the status: a validation pipe rejects
|
|
653
|
+
// this shape with a 400 as well, so a status assertion passes with the floor
|
|
654
|
+
// removed and proves nothing.
|
|
655
|
+
expect(res.body.message).toBe('Request body is nested too deeply.')
|
|
656
|
+
})
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
Use a depth that actually overflows, and assert the guard's own message. A test
|
|
660
|
+
at a depth your DTO validation already rejects passes identically with the guard
|
|
661
|
+
deleted — which is a check that cannot produce a negative result, and the reason
|
|
662
|
+
this defect survived a green suite.
|
|
663
|
+
|
|
664
|
+
A depth ceiling well above anything a legitimate payload nests and well below
|
|
665
|
+
what exhausts the stack leaves a wide margin: one consumer runs `32`, against
|
|
666
|
+
the ~2000 levels that overflow. Walk the parsed body iteratively — a recursive
|
|
667
|
+
depth check on a hostile payload overflows the stack it was written to protect.
|
|
668
|
+
|
|
579
669
|
## ⏱️ Request Timing
|
|
580
670
|
|
|
581
671
|
One `RequestTimingSample` is delivered to whatever implements `ITimingSink` for
|
|
@@ -689,13 +779,36 @@ export class InvoiceController {
|
|
|
689
779
|
|
|
690
780
|
@Get()
|
|
691
781
|
async list(@Query() raw: Record<string, unknown>): Promise<PageResult<Invoice>> {
|
|
692
|
-
const query = normalizePageQuery(raw, { maxLimit: 50 })
|
|
782
|
+
const query = normalizePageQuery(raw, { maxLimit: 50, maxOffset: 100_000 })
|
|
693
783
|
const { rows, total } = await this.invoices.findPage(query)
|
|
694
784
|
return buildPageResult(rows, total, query)
|
|
695
785
|
}
|
|
696
786
|
}
|
|
697
787
|
```
|
|
698
788
|
|
|
789
|
+
#### Bound the offset, not just the page size
|
|
790
|
+
|
|
791
|
+
`maxLimit` caps how many rows a request reads. `maxOffset` caps how far in it
|
|
792
|
+
starts — and on an offset-paginated database that is the half that costs:
|
|
793
|
+
|
|
794
|
+
```
|
|
795
|
+
GET /invoices?page=1000000000&limit=20 → OFFSET 19999999980
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
Twenty bytes of query, and Postgres walks the table to reach a page that does
|
|
799
|
+
not exist. The page index has a floor of `1` and an arithmetic guard that keeps
|
|
800
|
+
`(page - 1) * limit` an exact integer, but nothing bounds the product itself
|
|
801
|
+
unless you say so.
|
|
802
|
+
|
|
803
|
+
`maxOffset` is **absent by default and deliberately so**: legitimate deep paging
|
|
804
|
+
exists, and a silent ceiling would change the rows a working query returns. Set
|
|
805
|
+
it wherever the page index reaches SQL and your dataset has a knowable ceiling.
|
|
806
|
+
`0` is a valid bound and means "the first page only".
|
|
807
|
+
|
|
808
|
+
Clamping matches how `maxLimit` already behaves — the resolved values come back
|
|
809
|
+
in `meta`, so a caller that cares can compare what it asked for against what it
|
|
810
|
+
got.
|
|
811
|
+
|
|
699
812
|
### Cursor pagination
|
|
700
813
|
|
|
701
814
|
```typescript
|
|
@@ -1212,13 +1325,23 @@ being asked:
|
|
|
1212
1325
|
| Route | Documented as |
|
|
1213
1326
|
| ----------------------------------- | ------------------------------------------------------- |
|
|
1214
1327
|
| `GET /health/live`, `/health/ready` | Public (`security: []`), when a document default exists |
|
|
1215
|
-
| `GET /metrics
|
|
1328
|
+
| `GET /metrics`, token set | Bearer-protected |
|
|
1329
|
+
| `GET /metrics`, no token | Public (`security: []`), when a document default exists |
|
|
1216
1330
|
|
|
1217
1331
|
The probes are polled by an orchestrator holding no credential, and the scrape
|
|
1218
1332
|
endpoint is protected exactly when you configured a token — this package owns
|
|
1219
1333
|
both the route and the option, so you should not have to restate either. Your
|
|
1220
1334
|
own `operationSecurity` entry still wins.
|
|
1221
1335
|
|
|
1336
|
+
The last row matters more than it looks. Without a token the scrape endpoint
|
|
1337
|
+
answers anyone, which is the deliberate "protected at the edge" arrangement — and
|
|
1338
|
+
an open route must **say** it is open rather than inherit your document default.
|
|
1339
|
+
Of the two ways to describe a route wrongly, this is the direction that hides:
|
|
1340
|
+
documenting a guarded route as open fails loudly at the first generated client
|
|
1341
|
+
that omits the credential and gets a `401`, while documenting an open route as
|
|
1342
|
+
guarded fails nowhere at all, and hands the wrong answer to whoever opened the
|
|
1343
|
+
document to ask what is exposed.
|
|
1344
|
+
|
|
1222
1345
|
## 🧵 Trace correlation
|
|
1223
1346
|
|
|
1224
1347
|
Off by default. Enabled, it reads the span your instrumentation already opened
|
package/dist/index.cjs
CHANGED
|
@@ -1397,9 +1397,12 @@ exports.BymaxCoreModule = class BymaxCoreModule extends BymaxCoreModuleBase {
|
|
|
1397
1397
|
* it is scoped to, while `'{*splat}'` — the form Nest 11's migration guide
|
|
1398
1398
|
* prescribes for "all routes" — stops matching the prefixed root once an
|
|
1399
1399
|
* application calls `setGlobalPrefix`. That was reported as nest#14520 and
|
|
1400
|
-
* fixed by nest#14522, whose regression test covers Fastify; on
|
|
1401
|
-
*
|
|
1402
|
-
*
|
|
1400
|
+
* fixed by nest#14522, whose regression test covers Fastify; on the Express
|
|
1401
|
+
* adapter the prefixed root still reaches no middleware while resolving to
|
|
1402
|
+
* `200`. Measured on `@nestjs/core` 11.1.28 and re-measured unchanged on
|
|
1403
|
+
* 11.2.1 — a minor release is exactly where this would plausibly have been
|
|
1404
|
+
* fixed, so the version this was last confirmed against is part of the
|
|
1405
|
+
* claim rather than a footnote to it.
|
|
1403
1406
|
*
|
|
1404
1407
|
* On Fastify the same `'/'` is an exact match rather than a mount — one of
|
|
1405
1408
|
* three requests reached the middleware — so the wildcard is the only form
|
package/dist/index.d.cts
CHANGED
|
@@ -430,9 +430,12 @@ declare class BymaxCoreModule extends BymaxCoreModuleBase implements NestModule
|
|
|
430
430
|
* it is scoped to, while `'{*splat}'` — the form Nest 11's migration guide
|
|
431
431
|
* prescribes for "all routes" — stops matching the prefixed root once an
|
|
432
432
|
* application calls `setGlobalPrefix`. That was reported as nest#14520 and
|
|
433
|
-
* fixed by nest#14522, whose regression test covers Fastify; on
|
|
434
|
-
*
|
|
435
|
-
*
|
|
433
|
+
* fixed by nest#14522, whose regression test covers Fastify; on the Express
|
|
434
|
+
* adapter the prefixed root still reaches no middleware while resolving to
|
|
435
|
+
* `200`. Measured on `@nestjs/core` 11.1.28 and re-measured unchanged on
|
|
436
|
+
* 11.2.1 — a minor release is exactly where this would plausibly have been
|
|
437
|
+
* fixed, so the version this was last confirmed against is part of the
|
|
438
|
+
* claim rather than a footnote to it.
|
|
436
439
|
*
|
|
437
440
|
* On Fastify the same `'/'` is an exact match rather than a mount — one of
|
|
438
441
|
* three requests reached the middleware — so the wildcard is the only form
|
package/dist/index.d.ts
CHANGED
|
@@ -430,9 +430,12 @@ declare class BymaxCoreModule extends BymaxCoreModuleBase implements NestModule
|
|
|
430
430
|
* it is scoped to, while `'{*splat}'` — the form Nest 11's migration guide
|
|
431
431
|
* prescribes for "all routes" — stops matching the prefixed root once an
|
|
432
432
|
* application calls `setGlobalPrefix`. That was reported as nest#14520 and
|
|
433
|
-
* fixed by nest#14522, whose regression test covers Fastify; on
|
|
434
|
-
*
|
|
435
|
-
*
|
|
433
|
+
* fixed by nest#14522, whose regression test covers Fastify; on the Express
|
|
434
|
+
* adapter the prefixed root still reaches no middleware while resolving to
|
|
435
|
+
* `200`. Measured on `@nestjs/core` 11.1.28 and re-measured unchanged on
|
|
436
|
+
* 11.2.1 — a minor release is exactly where this would plausibly have been
|
|
437
|
+
* fixed, so the version this was last confirmed against is part of the
|
|
438
|
+
* claim rather than a footnote to it.
|
|
436
439
|
*
|
|
437
440
|
* On Fastify the same `'/'` is an exact match rather than a mount — one of
|
|
438
441
|
* three requests reached the middleware — so the wildcard is the only form
|
package/dist/index.mjs
CHANGED
|
@@ -1395,9 +1395,12 @@ var BymaxCoreModule = class extends BymaxCoreModuleBase {
|
|
|
1395
1395
|
* it is scoped to, while `'{*splat}'` — the form Nest 11's migration guide
|
|
1396
1396
|
* prescribes for "all routes" — stops matching the prefixed root once an
|
|
1397
1397
|
* application calls `setGlobalPrefix`. That was reported as nest#14520 and
|
|
1398
|
-
* fixed by nest#14522, whose regression test covers Fastify; on
|
|
1399
|
-
*
|
|
1400
|
-
*
|
|
1398
|
+
* fixed by nest#14522, whose regression test covers Fastify; on the Express
|
|
1399
|
+
* adapter the prefixed root still reaches no middleware while resolving to
|
|
1400
|
+
* `200`. Measured on `@nestjs/core` 11.1.28 and re-measured unchanged on
|
|
1401
|
+
* 11.2.1 — a minor release is exactly where this would plausibly have been
|
|
1402
|
+
* fixed, so the version this was last confirmed against is part of the
|
|
1403
|
+
* claim rather than a footnote to it.
|
|
1401
1404
|
*
|
|
1402
1405
|
* On Fastify the same `'/'` is an exact match rather than a mount — one of
|
|
1403
1406
|
* three requests reached the middleware — so the wildcard is the only form
|
package/dist/openapi/index.cjs
CHANGED
|
@@ -326,7 +326,7 @@ var CORE_PARAMETERS = {
|
|
|
326
326
|
}
|
|
327
327
|
};
|
|
328
328
|
|
|
329
|
-
// src/openapi/openapi.
|
|
329
|
+
// src/openapi/openapi.shape.ts
|
|
330
330
|
var OPERATION_METHODS = [
|
|
331
331
|
"get",
|
|
332
332
|
"post",
|
|
@@ -337,21 +337,26 @@ var OPERATION_METHODS = [
|
|
|
337
337
|
"options",
|
|
338
338
|
"trace"
|
|
339
339
|
];
|
|
340
|
-
var METRICS_SCHEME_NAME = "BymaxMetricsAuth";
|
|
341
|
-
var ERROR_ENVELOPE_SCHEMA = "BymaxErrorEnvelope";
|
|
342
|
-
var HEALTH_RESPONSE_SCHEMA = "BymaxHealthResponse";
|
|
343
340
|
function asRecord(value) {
|
|
344
341
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
345
342
|
return {};
|
|
346
343
|
}
|
|
347
344
|
return value;
|
|
348
345
|
}
|
|
349
|
-
function mergeAbsent(existing, additions) {
|
|
350
|
-
return { ...additions, ...existing };
|
|
351
|
-
}
|
|
352
346
|
function operationsOf(item) {
|
|
353
347
|
return Object.entries(asRecord(item)).filter(([key]) => OPERATION_METHODS.includes(key));
|
|
354
348
|
}
|
|
349
|
+
function operationKey(method, path) {
|
|
350
|
+
return `${method.toUpperCase()} ${path}`;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/openapi/openapi.document.ts
|
|
354
|
+
var METRICS_SCHEME_NAME = "BymaxMetricsAuth";
|
|
355
|
+
var ERROR_ENVELOPE_SCHEMA = "BymaxErrorEnvelope";
|
|
356
|
+
var HEALTH_RESPONSE_SCHEMA = "BymaxHealthResponse";
|
|
357
|
+
function mergeAbsent(existing, additions) {
|
|
358
|
+
return { ...additions, ...existing };
|
|
359
|
+
}
|
|
355
360
|
function mergeResponses(existing, additions) {
|
|
356
361
|
const merged = new Map(Object.entries(existing));
|
|
357
362
|
for (const [status, value] of Object.entries(additions)) {
|
|
@@ -377,21 +382,24 @@ function withoutDisabledRoutes(paths, options, routes) {
|
|
|
377
382
|
});
|
|
378
383
|
return Object.fromEntries(kept.filter(([, item]) => item !== void 0));
|
|
379
384
|
}
|
|
380
|
-
function
|
|
385
|
+
function inheritsRequirement(document, openapi) {
|
|
386
|
+
if (document.security === void 0) {
|
|
387
|
+
return openapi.security.length > 0;
|
|
388
|
+
}
|
|
389
|
+
return Array.isArray(document.security) && document.security.length > 0;
|
|
390
|
+
}
|
|
391
|
+
function ownRouteSecurity(path, method, options, routes, inherits) {
|
|
381
392
|
if (method !== "get") {
|
|
382
393
|
return void 0;
|
|
383
394
|
}
|
|
384
395
|
if (options.metrics.authToken !== void 0 && routes.isMetrics(path)) {
|
|
385
396
|
return [{ [METRICS_SCHEME_NAME]: [] }];
|
|
386
397
|
}
|
|
387
|
-
if (
|
|
398
|
+
if (inherits && (routes.isHealth(path) || routes.isMetrics(path))) {
|
|
388
399
|
return [];
|
|
389
400
|
}
|
|
390
401
|
return void 0;
|
|
391
402
|
}
|
|
392
|
-
function operationKey(method, path) {
|
|
393
|
-
return `${method.toUpperCase()} ${path}`;
|
|
394
|
-
}
|
|
395
403
|
function coreResponses(path, options, routes) {
|
|
396
404
|
const responses = {};
|
|
397
405
|
if (options.envelope.enabled) {
|
|
@@ -425,7 +433,7 @@ function mergeFragment(operation, fragment) {
|
|
|
425
433
|
}
|
|
426
434
|
return merged;
|
|
427
435
|
}
|
|
428
|
-
function augmentOperation(operation, path, method, options, routes, contributions) {
|
|
436
|
+
function augmentOperation(operation, path, method, options, routes, contributions, inherits) {
|
|
429
437
|
const declaredByDocument = operation["security"] !== void 0;
|
|
430
438
|
let result = { ...operation };
|
|
431
439
|
for (const fragment of fragmentsFor(result["operationId"], contributions)) {
|
|
@@ -434,7 +442,7 @@ function augmentOperation(operation, path, method, options, routes, contribution
|
|
|
434
442
|
if (!declaredByDocument) {
|
|
435
443
|
const override = options.openapi.operationSecurity[operationKey(method, path)];
|
|
436
444
|
const describedByLibrary = result["security"] !== void 0;
|
|
437
|
-
const security = override ?? (describedByLibrary ? void 0 : ownRouteSecurity(path, method, options, routes));
|
|
445
|
+
const security = override ?? (describedByLibrary ? void 0 : ownRouteSecurity(path, method, options, routes, inherits));
|
|
438
446
|
if (security !== void 0) {
|
|
439
447
|
result["security"] = security;
|
|
440
448
|
}
|
|
@@ -509,12 +517,20 @@ function assertOverridesMatch(paths, openapi) {
|
|
|
509
517
|
`[BymaxCoreModule] openapi.operationSecurity addresses ${unmatched.length} operation(s) that the document does not contain: ${unmatched.join(", ")}. Keys are "<METHOD> <path>" with the path exactly as documented, including any global prefix. The document contains: ${documented.length === 0 ? "(none)" : documented.join(", ")}.`
|
|
510
518
|
);
|
|
511
519
|
}
|
|
512
|
-
function augmentPaths(paths, options, routes, contributions) {
|
|
520
|
+
function augmentPaths(paths, options, routes, contributions, inherits) {
|
|
513
521
|
return Object.fromEntries(
|
|
514
522
|
Object.entries(paths).map(([path, item]) => {
|
|
515
523
|
const augmented = operationsOf(item).map(([method, operation]) => [
|
|
516
524
|
method,
|
|
517
|
-
augmentOperation(
|
|
525
|
+
augmentOperation(
|
|
526
|
+
asRecord(operation),
|
|
527
|
+
path,
|
|
528
|
+
method,
|
|
529
|
+
options,
|
|
530
|
+
routes,
|
|
531
|
+
contributions,
|
|
532
|
+
inherits
|
|
533
|
+
)
|
|
518
534
|
]);
|
|
519
535
|
return [path, { ...asRecord(item), ...Object.fromEntries(augmented) }];
|
|
520
536
|
})
|
|
@@ -552,7 +568,15 @@ function augmentDocument(document, options, pathPrefixes = [""], contributions =
|
|
|
552
568
|
const routes = indexOwnRoutes(options, pathPrefixes);
|
|
553
569
|
const served = withoutDisabledRoutes(asRecord(document.paths), options, routes);
|
|
554
570
|
assertOverridesMatch(served, openapi);
|
|
555
|
-
const paths = document.paths === void 0 ? {} : {
|
|
571
|
+
const paths = document.paths === void 0 ? {} : {
|
|
572
|
+
paths: augmentPaths(
|
|
573
|
+
served,
|
|
574
|
+
options,
|
|
575
|
+
routes,
|
|
576
|
+
contributions,
|
|
577
|
+
inheritsRequirement(document, openapi)
|
|
578
|
+
)
|
|
579
|
+
};
|
|
556
580
|
const security = openapi.security.length > 0 && document.security === void 0 ? { security: openapi.security } : {};
|
|
557
581
|
return { ...document, components: Object.fromEntries(merged), ...paths, ...security };
|
|
558
582
|
}
|
package/dist/openapi/index.mjs
CHANGED
|
@@ -324,7 +324,7 @@ var CORE_PARAMETERS = {
|
|
|
324
324
|
}
|
|
325
325
|
};
|
|
326
326
|
|
|
327
|
-
// src/openapi/openapi.
|
|
327
|
+
// src/openapi/openapi.shape.ts
|
|
328
328
|
var OPERATION_METHODS = [
|
|
329
329
|
"get",
|
|
330
330
|
"post",
|
|
@@ -335,21 +335,26 @@ var OPERATION_METHODS = [
|
|
|
335
335
|
"options",
|
|
336
336
|
"trace"
|
|
337
337
|
];
|
|
338
|
-
var METRICS_SCHEME_NAME = "BymaxMetricsAuth";
|
|
339
|
-
var ERROR_ENVELOPE_SCHEMA = "BymaxErrorEnvelope";
|
|
340
|
-
var HEALTH_RESPONSE_SCHEMA = "BymaxHealthResponse";
|
|
341
338
|
function asRecord(value) {
|
|
342
339
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
343
340
|
return {};
|
|
344
341
|
}
|
|
345
342
|
return value;
|
|
346
343
|
}
|
|
347
|
-
function mergeAbsent(existing, additions) {
|
|
348
|
-
return { ...additions, ...existing };
|
|
349
|
-
}
|
|
350
344
|
function operationsOf(item) {
|
|
351
345
|
return Object.entries(asRecord(item)).filter(([key]) => OPERATION_METHODS.includes(key));
|
|
352
346
|
}
|
|
347
|
+
function operationKey(method, path) {
|
|
348
|
+
return `${method.toUpperCase()} ${path}`;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/openapi/openapi.document.ts
|
|
352
|
+
var METRICS_SCHEME_NAME = "BymaxMetricsAuth";
|
|
353
|
+
var ERROR_ENVELOPE_SCHEMA = "BymaxErrorEnvelope";
|
|
354
|
+
var HEALTH_RESPONSE_SCHEMA = "BymaxHealthResponse";
|
|
355
|
+
function mergeAbsent(existing, additions) {
|
|
356
|
+
return { ...additions, ...existing };
|
|
357
|
+
}
|
|
353
358
|
function mergeResponses(existing, additions) {
|
|
354
359
|
const merged = new Map(Object.entries(existing));
|
|
355
360
|
for (const [status, value] of Object.entries(additions)) {
|
|
@@ -375,21 +380,24 @@ function withoutDisabledRoutes(paths, options, routes) {
|
|
|
375
380
|
});
|
|
376
381
|
return Object.fromEntries(kept.filter(([, item]) => item !== void 0));
|
|
377
382
|
}
|
|
378
|
-
function
|
|
383
|
+
function inheritsRequirement(document, openapi) {
|
|
384
|
+
if (document.security === void 0) {
|
|
385
|
+
return openapi.security.length > 0;
|
|
386
|
+
}
|
|
387
|
+
return Array.isArray(document.security) && document.security.length > 0;
|
|
388
|
+
}
|
|
389
|
+
function ownRouteSecurity(path, method, options, routes, inherits) {
|
|
379
390
|
if (method !== "get") {
|
|
380
391
|
return void 0;
|
|
381
392
|
}
|
|
382
393
|
if (options.metrics.authToken !== void 0 && routes.isMetrics(path)) {
|
|
383
394
|
return [{ [METRICS_SCHEME_NAME]: [] }];
|
|
384
395
|
}
|
|
385
|
-
if (
|
|
396
|
+
if (inherits && (routes.isHealth(path) || routes.isMetrics(path))) {
|
|
386
397
|
return [];
|
|
387
398
|
}
|
|
388
399
|
return void 0;
|
|
389
400
|
}
|
|
390
|
-
function operationKey(method, path) {
|
|
391
|
-
return `${method.toUpperCase()} ${path}`;
|
|
392
|
-
}
|
|
393
401
|
function coreResponses(path, options, routes) {
|
|
394
402
|
const responses = {};
|
|
395
403
|
if (options.envelope.enabled) {
|
|
@@ -423,7 +431,7 @@ function mergeFragment(operation, fragment) {
|
|
|
423
431
|
}
|
|
424
432
|
return merged;
|
|
425
433
|
}
|
|
426
|
-
function augmentOperation(operation, path, method, options, routes, contributions) {
|
|
434
|
+
function augmentOperation(operation, path, method, options, routes, contributions, inherits) {
|
|
427
435
|
const declaredByDocument = operation["security"] !== void 0;
|
|
428
436
|
let result = { ...operation };
|
|
429
437
|
for (const fragment of fragmentsFor(result["operationId"], contributions)) {
|
|
@@ -432,7 +440,7 @@ function augmentOperation(operation, path, method, options, routes, contribution
|
|
|
432
440
|
if (!declaredByDocument) {
|
|
433
441
|
const override = options.openapi.operationSecurity[operationKey(method, path)];
|
|
434
442
|
const describedByLibrary = result["security"] !== void 0;
|
|
435
|
-
const security = override ?? (describedByLibrary ? void 0 : ownRouteSecurity(path, method, options, routes));
|
|
443
|
+
const security = override ?? (describedByLibrary ? void 0 : ownRouteSecurity(path, method, options, routes, inherits));
|
|
436
444
|
if (security !== void 0) {
|
|
437
445
|
result["security"] = security;
|
|
438
446
|
}
|
|
@@ -507,12 +515,20 @@ function assertOverridesMatch(paths, openapi) {
|
|
|
507
515
|
`[BymaxCoreModule] openapi.operationSecurity addresses ${unmatched.length} operation(s) that the document does not contain: ${unmatched.join(", ")}. Keys are "<METHOD> <path>" with the path exactly as documented, including any global prefix. The document contains: ${documented.length === 0 ? "(none)" : documented.join(", ")}.`
|
|
508
516
|
);
|
|
509
517
|
}
|
|
510
|
-
function augmentPaths(paths, options, routes, contributions) {
|
|
518
|
+
function augmentPaths(paths, options, routes, contributions, inherits) {
|
|
511
519
|
return Object.fromEntries(
|
|
512
520
|
Object.entries(paths).map(([path, item]) => {
|
|
513
521
|
const augmented = operationsOf(item).map(([method, operation]) => [
|
|
514
522
|
method,
|
|
515
|
-
augmentOperation(
|
|
523
|
+
augmentOperation(
|
|
524
|
+
asRecord(operation),
|
|
525
|
+
path,
|
|
526
|
+
method,
|
|
527
|
+
options,
|
|
528
|
+
routes,
|
|
529
|
+
contributions,
|
|
530
|
+
inherits
|
|
531
|
+
)
|
|
516
532
|
]);
|
|
517
533
|
return [path, { ...asRecord(item), ...Object.fromEntries(augmented) }];
|
|
518
534
|
})
|
|
@@ -550,7 +566,15 @@ function augmentDocument(document, options, pathPrefixes = [""], contributions =
|
|
|
550
566
|
const routes = indexOwnRoutes(options, pathPrefixes);
|
|
551
567
|
const served = withoutDisabledRoutes(asRecord(document.paths), options, routes);
|
|
552
568
|
assertOverridesMatch(served, openapi);
|
|
553
|
-
const paths = document.paths === void 0 ? {} : {
|
|
569
|
+
const paths = document.paths === void 0 ? {} : {
|
|
570
|
+
paths: augmentPaths(
|
|
571
|
+
served,
|
|
572
|
+
options,
|
|
573
|
+
routes,
|
|
574
|
+
contributions,
|
|
575
|
+
inheritsRequirement(document, openapi)
|
|
576
|
+
)
|
|
577
|
+
};
|
|
554
578
|
const security = openapi.security.length > 0 && document.security === void 0 ? { security: openapi.security } : {};
|
|
555
579
|
return { ...document, components: Object.fromEntries(merged), ...paths, ...security };
|
|
556
580
|
}
|
|
@@ -19,6 +19,12 @@ function coercePositiveInt(value, fallback) {
|
|
|
19
19
|
function clampPageToLimit(page, limit) {
|
|
20
20
|
return Math.min(page, Math.floor(Number.MAX_SAFE_INTEGER / limit) + 1);
|
|
21
21
|
}
|
|
22
|
+
function clampPageToOffset(page, limit, maxOffset) {
|
|
23
|
+
if (maxOffset === void 0 || !Number.isSafeInteger(maxOffset) || maxOffset < 0) {
|
|
24
|
+
return page;
|
|
25
|
+
}
|
|
26
|
+
return Math.min(page, Math.floor(maxOffset / limit) + 1);
|
|
27
|
+
}
|
|
22
28
|
function clampLimit(rawLimit, options) {
|
|
23
29
|
const defaultLimit = coercePositiveInt(options?.defaultLimit, DEFAULT_LIMIT);
|
|
24
30
|
const maxLimit = coercePositiveInt(options?.maxLimit, DEFAULT_MAX_LIMIT);
|
|
@@ -29,7 +35,11 @@ function clampLimit(rawLimit, options) {
|
|
|
29
35
|
function normalizePageQuery(raw, options) {
|
|
30
36
|
const limit = clampLimit(raw.limit, options);
|
|
31
37
|
return {
|
|
32
|
-
page:
|
|
38
|
+
page: clampPageToOffset(
|
|
39
|
+
clampPageToLimit(coercePositiveInt(raw.page, MINIMUM), limit),
|
|
40
|
+
limit,
|
|
41
|
+
options?.maxOffset
|
|
42
|
+
),
|
|
33
43
|
limit
|
|
34
44
|
};
|
|
35
45
|
}
|
|
@@ -51,14 +51,43 @@ interface PageResult<T> {
|
|
|
51
51
|
* Options are per-call and never retained between calls.
|
|
52
52
|
*
|
|
53
53
|
* @param raw - The untrusted page and limit values from the request.
|
|
54
|
-
* @param options - Per-call `defaultLimit` (default `20`)
|
|
55
|
-
*
|
|
54
|
+
* @param options - Per-call `defaultLimit` (default `20`), `maxLimit` (default
|
|
55
|
+
* `100`) and `maxOffset` (absent by default) overrides. `maxLimit` bounds how
|
|
56
|
+
* many rows a request reads; `maxOffset` bounds how far in it starts, which is
|
|
57
|
+
* the half an offset-paginated database pays for.
|
|
56
58
|
* @returns A clamped, safe query ready to hand to a repository.
|
|
57
59
|
*/
|
|
60
|
+
/**
|
|
61
|
+
* Options for {@link normalizePageQuery}: the shared limit bounds, plus the one
|
|
62
|
+
* that only means anything to offset pagination.
|
|
63
|
+
*
|
|
64
|
+
* Declared here rather than beside the shared bounds so it cannot reach the
|
|
65
|
+
* cursor normalizer, which takes {@link PaginationLimitOptions} and has no
|
|
66
|
+
* offset to bound. An option that type-checks on a function that ignores it is
|
|
67
|
+
* worse than a missing one — it reads as configured and does nothing.
|
|
68
|
+
*/
|
|
69
|
+
interface PageQueryOptions extends PaginationLimitOptions {
|
|
70
|
+
/**
|
|
71
|
+
* Hard cap applied to the repository offset the query drives,
|
|
72
|
+
* `(page - 1) * limit`. Absent by default, which bounds nothing beyond
|
|
73
|
+
* arithmetic safety.
|
|
74
|
+
*
|
|
75
|
+
* `maxLimit` bounds how many rows a request reads; this bounds how far in it
|
|
76
|
+
* starts, which is the half that costs on an offset-paginated database — a
|
|
77
|
+
* `SELECT … OFFSET 20000000000` is a twenty-byte request that scans a table.
|
|
78
|
+
* Set it wherever the page index reaches SQL and the dataset has a knowable
|
|
79
|
+
* ceiling. There is deliberately no default: legitimate deep paging exists,
|
|
80
|
+
* and a silent cap would change the rows a working query returns.
|
|
81
|
+
*
|
|
82
|
+
* `0` is meaningful and means "the first page only". Any other value that is
|
|
83
|
+
* not a non-negative safe integer is read as absent.
|
|
84
|
+
*/
|
|
85
|
+
maxOffset?: number;
|
|
86
|
+
}
|
|
58
87
|
declare function normalizePageQuery(raw: {
|
|
59
88
|
page?: unknown;
|
|
60
89
|
limit?: unknown;
|
|
61
|
-
}, options?:
|
|
90
|
+
}, options?: PageQueryOptions): PageQuery;
|
|
62
91
|
/**
|
|
63
92
|
* Assemble a {@link PageResult} from a page of items and the total count.
|
|
64
93
|
*
|
|
@@ -51,14 +51,43 @@ interface PageResult<T> {
|
|
|
51
51
|
* Options are per-call and never retained between calls.
|
|
52
52
|
*
|
|
53
53
|
* @param raw - The untrusted page and limit values from the request.
|
|
54
|
-
* @param options - Per-call `defaultLimit` (default `20`)
|
|
55
|
-
*
|
|
54
|
+
* @param options - Per-call `defaultLimit` (default `20`), `maxLimit` (default
|
|
55
|
+
* `100`) and `maxOffset` (absent by default) overrides. `maxLimit` bounds how
|
|
56
|
+
* many rows a request reads; `maxOffset` bounds how far in it starts, which is
|
|
57
|
+
* the half an offset-paginated database pays for.
|
|
56
58
|
* @returns A clamped, safe query ready to hand to a repository.
|
|
57
59
|
*/
|
|
60
|
+
/**
|
|
61
|
+
* Options for {@link normalizePageQuery}: the shared limit bounds, plus the one
|
|
62
|
+
* that only means anything to offset pagination.
|
|
63
|
+
*
|
|
64
|
+
* Declared here rather than beside the shared bounds so it cannot reach the
|
|
65
|
+
* cursor normalizer, which takes {@link PaginationLimitOptions} and has no
|
|
66
|
+
* offset to bound. An option that type-checks on a function that ignores it is
|
|
67
|
+
* worse than a missing one — it reads as configured and does nothing.
|
|
68
|
+
*/
|
|
69
|
+
interface PageQueryOptions extends PaginationLimitOptions {
|
|
70
|
+
/**
|
|
71
|
+
* Hard cap applied to the repository offset the query drives,
|
|
72
|
+
* `(page - 1) * limit`. Absent by default, which bounds nothing beyond
|
|
73
|
+
* arithmetic safety.
|
|
74
|
+
*
|
|
75
|
+
* `maxLimit` bounds how many rows a request reads; this bounds how far in it
|
|
76
|
+
* starts, which is the half that costs on an offset-paginated database — a
|
|
77
|
+
* `SELECT … OFFSET 20000000000` is a twenty-byte request that scans a table.
|
|
78
|
+
* Set it wherever the page index reaches SQL and the dataset has a knowable
|
|
79
|
+
* ceiling. There is deliberately no default: legitimate deep paging exists,
|
|
80
|
+
* and a silent cap would change the rows a working query returns.
|
|
81
|
+
*
|
|
82
|
+
* `0` is meaningful and means "the first page only". Any other value that is
|
|
83
|
+
* not a non-negative safe integer is read as absent.
|
|
84
|
+
*/
|
|
85
|
+
maxOffset?: number;
|
|
86
|
+
}
|
|
58
87
|
declare function normalizePageQuery(raw: {
|
|
59
88
|
page?: unknown;
|
|
60
89
|
limit?: unknown;
|
|
61
|
-
}, options?:
|
|
90
|
+
}, options?: PageQueryOptions): PageQuery;
|
|
62
91
|
/**
|
|
63
92
|
* Assemble a {@link PageResult} from a page of items and the total count.
|
|
64
93
|
*
|
|
@@ -17,6 +17,12 @@ function coercePositiveInt(value, fallback) {
|
|
|
17
17
|
function clampPageToLimit(page, limit) {
|
|
18
18
|
return Math.min(page, Math.floor(Number.MAX_SAFE_INTEGER / limit) + 1);
|
|
19
19
|
}
|
|
20
|
+
function clampPageToOffset(page, limit, maxOffset) {
|
|
21
|
+
if (maxOffset === void 0 || !Number.isSafeInteger(maxOffset) || maxOffset < 0) {
|
|
22
|
+
return page;
|
|
23
|
+
}
|
|
24
|
+
return Math.min(page, Math.floor(maxOffset / limit) + 1);
|
|
25
|
+
}
|
|
20
26
|
function clampLimit(rawLimit, options) {
|
|
21
27
|
const defaultLimit = coercePositiveInt(options?.defaultLimit, DEFAULT_LIMIT);
|
|
22
28
|
const maxLimit = coercePositiveInt(options?.maxLimit, DEFAULT_MAX_LIMIT);
|
|
@@ -27,7 +33,11 @@ function clampLimit(rawLimit, options) {
|
|
|
27
33
|
function normalizePageQuery(raw, options) {
|
|
28
34
|
const limit = clampLimit(raw.limit, options);
|
|
29
35
|
return {
|
|
30
|
-
page:
|
|
36
|
+
page: clampPageToOffset(
|
|
37
|
+
clampPageToLimit(coercePositiveInt(raw.page, MINIMUM), limit),
|
|
38
|
+
limit,
|
|
39
|
+
options?.maxOffset
|
|
40
|
+
),
|
|
31
41
|
limit
|
|
32
42
|
};
|
|
33
43
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bymax-one/nest-core",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.3",
|
|
4
4
|
"description": "Zero-dependency NestJS 11 application foundation kit: error-envelope exception filter, request-timing interceptor, pagination helpers, health endpoints with indicator discovery, an optional Prometheus metrics endpoint with a contribution contract, OpenAPI documents in development, and OpenTelemetry trace correlation.",
|
|
5
5
|
"author": "Bymax One <support@bymax.one>",
|
|
6
6
|
"license": "MIT",
|