@c9up/ream 0.1.34 → 0.2.1
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/dist/Application.d.ts +68 -0
- package/dist/Application.d.ts.map +1 -1
- package/dist/Application.js +142 -6
- package/dist/Application.js.map +1 -1
- package/dist/HealthCheck.d.ts +14 -7
- package/dist/HealthCheck.d.ts.map +1 -1
- package/dist/HealthCheck.js +21 -12
- package/dist/HealthCheck.js.map +1 -1
- package/dist/HttpKernel.d.ts +12 -0
- package/dist/HttpKernel.d.ts.map +1 -1
- package/dist/HttpKernel.js +15 -4
- package/dist/HttpKernel.js.map +1 -1
- package/dist/Ignitor.d.ts +8 -8
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +11 -0
- package/dist/Ignitor.js.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.d.ts +13 -2
- package/dist/bodyparser/BodyParserMiddleware.d.ts.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.js +50 -30
- package/dist/bodyparser/BodyParserMiddleware.js.map +1 -1
- package/dist/bodyparser/MultipartFile.d.ts +75 -6
- package/dist/bodyparser/MultipartFile.d.ts.map +1 -1
- package/dist/bodyparser/MultipartFile.js +128 -8
- package/dist/bodyparser/MultipartFile.js.map +1 -1
- package/dist/console/BaseCommand.d.ts +8 -1
- package/dist/console/BaseCommand.d.ts.map +1 -1
- package/dist/console/BaseCommand.js +1 -1
- package/dist/console/BaseCommand.js.map +1 -1
- package/dist/console/Kernel.d.ts +15 -0
- package/dist/console/Kernel.d.ts.map +1 -1
- package/dist/console/Kernel.js +17 -0
- package/dist/console/Kernel.js.map +1 -1
- package/dist/directories.d.ts +52 -0
- package/dist/directories.d.ts.map +1 -0
- package/dist/directories.js +38 -0
- package/dist/directories.js.map +1 -0
- package/dist/events/Emitter.d.ts +49 -6
- package/dist/events/Emitter.d.ts.map +1 -1
- package/dist/events/Emitter.js +117 -7
- package/dist/events/Emitter.js.map +1 -1
- package/dist/events/EventsBuffer.d.ts +47 -0
- package/dist/events/EventsBuffer.d.ts.map +1 -0
- package/dist/events/EventsBuffer.js +107 -0
- package/dist/events/EventsBuffer.js.map +1 -0
- package/dist/events/index.d.ts +2 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +1 -0
- package/dist/events/index.js.map +1 -1
- package/dist/health/BaseCheck.d.ts +19 -0
- package/dist/health/BaseCheck.d.ts.map +1 -0
- package/dist/health/BaseCheck.js +24 -0
- package/dist/health/BaseCheck.js.map +1 -0
- package/dist/health/HealthChecks.d.ts +15 -0
- package/dist/health/HealthChecks.d.ts.map +1 -0
- package/dist/health/HealthChecks.js +69 -0
- package/dist/health/HealthChecks.js.map +1 -0
- package/dist/health/Result.d.ts +27 -0
- package/dist/health/Result.d.ts.map +1 -0
- package/dist/health/Result.js +55 -0
- package/dist/health/Result.js.map +1 -0
- package/dist/health/checks/DiskSpaceCheck.d.ts +29 -0
- package/dist/health/checks/DiskSpaceCheck.d.ts.map +1 -0
- package/dist/health/checks/DiskSpaceCheck.js +58 -0
- package/dist/health/checks/DiskSpaceCheck.js.map +1 -0
- package/dist/health/checks/MemoryHeapCheck.d.ts +21 -0
- package/dist/health/checks/MemoryHeapCheck.d.ts.map +1 -0
- package/dist/health/checks/MemoryHeapCheck.js +55 -0
- package/dist/health/checks/MemoryHeapCheck.js.map +1 -0
- package/dist/health/checks/MemoryRSSCheck.d.ts +21 -0
- package/dist/health/checks/MemoryRSSCheck.d.ts.map +1 -0
- package/dist/health/checks/MemoryRSSCheck.js +55 -0
- package/dist/health/checks/MemoryRSSCheck.js.map +1 -0
- package/dist/health/checks/memory.d.ts +24 -0
- package/dist/health/checks/memory.d.ts.map +1 -0
- package/dist/health/checks/memory.js +88 -0
- package/dist/health/checks/memory.js.map +1 -0
- package/dist/health/index.d.ts +9 -0
- package/dist/health/index.d.ts.map +1 -0
- package/dist/health/index.js +8 -0
- package/dist/health/index.js.map +1 -0
- package/dist/health/tracing.d.ts +12 -0
- package/dist/health/tracing.d.ts.map +1 -0
- package/dist/health/tracing.js +11 -0
- package/dist/health/tracing.js.map +1 -0
- package/dist/health/types.d.ts +51 -0
- package/dist/health/types.d.ts.map +1 -0
- package/dist/health/types.js +5 -0
- package/dist/health/types.js.map +1 -0
- package/dist/helpers/bytes.d.ts +28 -0
- package/dist/helpers/bytes.d.ts.map +1 -0
- package/dist/helpers/bytes.js +63 -0
- package/dist/helpers/bytes.js.map +1 -0
- package/dist/helpers/string.d.ts +4 -0
- package/dist/helpers/string.d.ts.map +1 -1
- package/dist/helpers/string.js +2 -0
- package/dist/helpers/string.js.map +1 -1
- package/dist/http/HttpContext.d.ts +30 -2
- package/dist/http/HttpContext.d.ts.map +1 -1
- package/dist/http/HttpContext.js +73 -3
- package/dist/http/HttpContext.js.map +1 -1
- package/dist/http/Request.d.ts +42 -3
- package/dist/http/Request.d.ts.map +1 -1
- package/dist/http/Request.js +51 -7
- package/dist/http/Request.js.map +1 -1
- package/dist/http/Response.d.ts +82 -2
- package/dist/http/Response.d.ts.map +1 -1
- package/dist/http/Response.js +240 -13
- package/dist/http/Response.js.map +1 -1
- package/dist/http/SseStream.d.ts +10 -0
- package/dist/http/SseStream.d.ts.map +1 -1
- package/dist/http/SseStream.js.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/migrations/MigrationRegistry.d.ts +28 -0
- package/dist/migrations/MigrationRegistry.d.ts.map +1 -0
- package/dist/migrations/MigrationRegistry.js +41 -0
- package/dist/migrations/MigrationRegistry.js.map +1 -0
- package/dist/migrations/types.d.ts +64 -0
- package/dist/migrations/types.d.ts.map +1 -0
- package/dist/migrations/types.js +15 -0
- package/dist/migrations/types.js.map +1 -0
- package/dist/router/Router.d.ts +112 -9
- package/dist/router/Router.d.ts.map +1 -1
- package/dist/router/Router.js +206 -28
- package/dist/router/Router.js.map +1 -1
- package/dist/security/CookieSigner.d.ts +43 -4
- package/dist/security/CookieSigner.d.ts.map +1 -1
- package/dist/security/CookieSigner.js +68 -10
- package/dist/security/CookieSigner.js.map +1 -1
- package/dist/session/Session.d.ts +46 -1
- package/dist/session/Session.d.ts.map +1 -1
- package/dist/session/Session.js +57 -4
- package/dist/session/Session.js.map +1 -1
- package/dist/session/SessionMiddleware.d.ts.map +1 -1
- package/dist/session/SessionMiddleware.js +19 -9
- package/dist/session/SessionMiddleware.js.map +1 -1
- package/dist/session/drivers/CookieDriver.d.ts +1 -1
- package/dist/session/drivers/CookieDriver.d.ts.map +1 -1
- package/dist/session/drivers/CookieDriver.js +3 -3
- package/dist/session/drivers/CookieDriver.js.map +1 -1
- package/dist/session/drivers/DatabaseDriver.d.ts +1 -1
- package/dist/session/drivers/DatabaseDriver.d.ts.map +1 -1
- package/dist/session/drivers/DatabaseDriver.js +2 -2
- package/dist/session/drivers/DatabaseDriver.js.map +1 -1
- package/dist/session/drivers/FileDriver.d.ts +1 -1
- package/dist/session/drivers/FileDriver.d.ts.map +1 -1
- package/dist/session/drivers/FileDriver.js +7 -5
- package/dist/session/drivers/FileDriver.js.map +1 -1
- package/dist/session/drivers/MemoryDriver.d.ts +1 -1
- package/dist/session/drivers/MemoryDriver.d.ts.map +1 -1
- package/dist/session/drivers/MemoryDriver.js +1 -1
- package/dist/session/drivers/MemoryDriver.js.map +1 -1
- package/dist/session/drivers/RedisDriver.d.ts +1 -1
- package/dist/session/drivers/RedisDriver.d.ts.map +1 -1
- package/dist/session/drivers/RedisDriver.js +11 -4
- package/dist/session/drivers/RedisDriver.js.map +1 -1
- package/events.win32-x64-msvc.node +0 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +48 -31
- package/scheduler.win32-x64-msvc.node +0 -0
- package/src/Application.ts +169 -6
- package/src/HealthCheck.ts +27 -14
- package/src/HttpKernel.ts +27 -4
- package/src/Ignitor.ts +17 -8
- package/src/bodyparser/BodyParserMiddleware.ts +70 -36
- package/src/bodyparser/MultipartFile.ts +153 -8
- package/src/console/BaseCommand.ts +8 -1
- package/src/console/Kernel.ts +18 -0
- package/src/directories.ts +81 -0
- package/src/events/Emitter.ts +129 -13
- package/src/events/EventsBuffer.ts +131 -0
- package/src/events/index.ts +7 -0
- package/src/health/BaseCheck.ts +30 -0
- package/src/health/HealthChecks.ts +83 -0
- package/src/health/Result.ts +69 -0
- package/src/health/checks/DiskSpaceCheck.ts +69 -0
- package/src/health/checks/MemoryHeapCheck.ts +68 -0
- package/src/health/checks/MemoryRSSCheck.ts +68 -0
- package/src/health/checks/memory.ts +115 -0
- package/src/health/index.ts +13 -0
- package/src/health/tracing.ts +15 -0
- package/src/health/types.ts +51 -0
- package/src/helpers/bytes.ts +71 -0
- package/src/helpers/string.ts +2 -0
- package/src/http/HttpContext.ts +78 -4
- package/src/http/Request.ts +62 -10
- package/src/http/Response.ts +283 -14
- package/src/http/SseStream.ts +10 -0
- package/src/index.ts +27 -1
- package/src/migrations/MigrationRegistry.ts +49 -0
- package/src/migrations/types.ts +68 -0
- package/src/router/Router.ts +241 -32
- package/src/security/CookieSigner.ts +92 -18
- package/src/session/Session.ts +71 -5
- package/src/session/SessionMiddleware.ts +19 -9
- package/src/session/drivers/CookieDriver.ts +4 -4
- package/src/session/drivers/DatabaseDriver.ts +3 -3
- package/src/session/drivers/FileDriver.ts +8 -6
- package/src/session/drivers/MemoryDriver.ts +2 -2
- package/src/session/drivers/RedisDriver.ts +12 -5
- package/tsconfig.app.json +13 -13
- package/tsconfig.base.json +19 -0
- package/tsconfig.client.json +16 -0
- package/tsconfig.package.json +15 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import v8 from 'node:v8'
|
|
2
|
+
import { BaseCheck } from '../BaseCheck.js'
|
|
3
|
+
import type { HealthCheckResult } from '../types.js'
|
|
4
|
+
import { assertPercentage, parseByteThreshold, runMemoryCheck } from './memory.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Watches the V8 heap (AdonisJS `MemoryHeapCheck`).
|
|
8
|
+
*/
|
|
9
|
+
export class MemoryHeapCheck extends BaseCheck {
|
|
10
|
+
#warnThresholdBytes: number | null = 250 * 1024 ** 2
|
|
11
|
+
#failThresholdBytes: number | null = 300 * 1024 ** 2
|
|
12
|
+
#warnThresholdPercentage: number | null = null
|
|
13
|
+
#failThresholdPercentage: number | null = null
|
|
14
|
+
#computeFn: () => NodeJS.MemoryUsage = () => process.memoryUsage()
|
|
15
|
+
|
|
16
|
+
name = 'Memory heap check'
|
|
17
|
+
|
|
18
|
+
/** Warn above this size — `'300 mb'` or a byte count. Clears any percentage. */
|
|
19
|
+
warnWhenExceeds(value: string | number): this {
|
|
20
|
+
this.#warnThresholdBytes = parseByteThreshold(value, 'warnWhenExceeds')
|
|
21
|
+
this.#warnThresholdPercentage = null
|
|
22
|
+
return this
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Fail above this size. Clears any percentage. */
|
|
26
|
+
failWhenExceeds(value: string | number): this {
|
|
27
|
+
this.#failThresholdBytes = parseByteThreshold(value, 'failWhenExceeds')
|
|
28
|
+
this.#failThresholdPercentage = null
|
|
29
|
+
return this
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Warn above this share of the heap ceiling. Clears any byte threshold. */
|
|
33
|
+
warnWhenExceedsPercentage(valueInPercentage: number): this {
|
|
34
|
+
assertPercentage(valueInPercentage, 'Warn')
|
|
35
|
+
this.#warnThresholdPercentage = valueInPercentage
|
|
36
|
+
this.#warnThresholdBytes = null
|
|
37
|
+
return this
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Fail above this share of the heap ceiling. Clears any byte threshold. */
|
|
41
|
+
failWhenExceedsPercentage(valueInPercentage: number): this {
|
|
42
|
+
assertPercentage(valueInPercentage, 'Fail')
|
|
43
|
+
this.#failThresholdPercentage = valueInPercentage
|
|
44
|
+
this.#failThresholdBytes = null
|
|
45
|
+
return this
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Replace how the reading is taken, so a test can drive the thresholds. */
|
|
49
|
+
compute(callback: () => NodeJS.MemoryUsage): this {
|
|
50
|
+
this.#computeFn = callback
|
|
51
|
+
return this
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async run(): Promise<HealthCheckResult> {
|
|
55
|
+
const { heapUsed } = this.#computeFn()
|
|
56
|
+
return runMemoryCheck(
|
|
57
|
+
heapUsed,
|
|
58
|
+
{
|
|
59
|
+
warnBytes: this.#warnThresholdBytes,
|
|
60
|
+
failBytes: this.#failThresholdBytes,
|
|
61
|
+
warnPercentage: this.#warnThresholdPercentage,
|
|
62
|
+
failPercentage: this.#failThresholdPercentage,
|
|
63
|
+
},
|
|
64
|
+
{ subject: 'Heap', ceilingKey: 'heapInBytes', ceilingName: 'maxHeapSize' },
|
|
65
|
+
() => v8.getHeapStatistics().heap_size_limit,
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import os from 'node:os'
|
|
2
|
+
import { BaseCheck } from '../BaseCheck.js'
|
|
3
|
+
import type { HealthCheckResult } from '../types.js'
|
|
4
|
+
import { assertPercentage, parseByteThreshold, runMemoryCheck } from './memory.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Watches the process' resident set size (AdonisJS `MemoryRSSCheck`).
|
|
8
|
+
*/
|
|
9
|
+
export class MemoryRSSCheck extends BaseCheck {
|
|
10
|
+
#warnThresholdBytes: number | null = 320 * 1024 ** 2
|
|
11
|
+
#failThresholdBytes: number | null = 350 * 1024 ** 2
|
|
12
|
+
#warnThresholdPercentage: number | null = null
|
|
13
|
+
#failThresholdPercentage: number | null = null
|
|
14
|
+
#computeFn: () => NodeJS.MemoryUsage = () => process.memoryUsage()
|
|
15
|
+
|
|
16
|
+
name = 'Memory RSS check'
|
|
17
|
+
|
|
18
|
+
/** Warn above this size — `'300 mb'` or a byte count. Clears any percentage. */
|
|
19
|
+
warnWhenExceeds(value: string | number): this {
|
|
20
|
+
this.#warnThresholdBytes = parseByteThreshold(value, 'warnWhenExceeds')
|
|
21
|
+
this.#warnThresholdPercentage = null
|
|
22
|
+
return this
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Fail above this size. Clears any percentage. */
|
|
26
|
+
failWhenExceeds(value: string | number): this {
|
|
27
|
+
this.#failThresholdBytes = parseByteThreshold(value, 'failWhenExceeds')
|
|
28
|
+
this.#failThresholdPercentage = null
|
|
29
|
+
return this
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Warn above this share of total system memory. Clears any byte threshold. */
|
|
33
|
+
warnWhenExceedsPercentage(valueInPercentage: number): this {
|
|
34
|
+
assertPercentage(valueInPercentage, 'Warn')
|
|
35
|
+
this.#warnThresholdPercentage = valueInPercentage
|
|
36
|
+
this.#warnThresholdBytes = null
|
|
37
|
+
return this
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Fail above this share of total system memory. Clears any byte threshold. */
|
|
41
|
+
failWhenExceedsPercentage(valueInPercentage: number): this {
|
|
42
|
+
assertPercentage(valueInPercentage, 'Fail')
|
|
43
|
+
this.#failThresholdPercentage = valueInPercentage
|
|
44
|
+
this.#failThresholdBytes = null
|
|
45
|
+
return this
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Replace how the reading is taken, so a test can drive the thresholds. */
|
|
49
|
+
compute(callback: () => NodeJS.MemoryUsage): this {
|
|
50
|
+
this.#computeFn = callback
|
|
51
|
+
return this
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async run(): Promise<HealthCheckResult> {
|
|
55
|
+
const { rss } = this.#computeFn()
|
|
56
|
+
return runMemoryCheck(
|
|
57
|
+
rss,
|
|
58
|
+
{
|
|
59
|
+
warnBytes: this.#warnThresholdBytes,
|
|
60
|
+
failBytes: this.#failThresholdBytes,
|
|
61
|
+
warnPercentage: this.#warnThresholdPercentage,
|
|
62
|
+
failPercentage: this.#failThresholdPercentage,
|
|
63
|
+
},
|
|
64
|
+
{ subject: 'RSS', ceilingKey: 'memoryInBytes', ceilingName: 'totalSystemMemory' },
|
|
65
|
+
() => os.totalmem(),
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import bytes from '../../helpers/bytes.js'
|
|
2
|
+
import { Result } from '../Result.js'
|
|
3
|
+
import type { HealthCheckResult } from '../types.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The shared body of the two memory checks.
|
|
7
|
+
*
|
|
8
|
+
* Upstream spells this logic out twice, once in `MemoryRSSCheck` and once in
|
|
9
|
+
* `MemoryHeapCheck`; only the reading, the ceiling it is compared against, and
|
|
10
|
+
* two strings differ. The public surface of both classes is unchanged — this is
|
|
11
|
+
* an internal detail, not a deviation.
|
|
12
|
+
*/
|
|
13
|
+
export type MemoryThresholds = {
|
|
14
|
+
warnBytes: number | null
|
|
15
|
+
failBytes: number | null
|
|
16
|
+
warnPercentage: number | null
|
|
17
|
+
failPercentage: number | null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function runMemoryCheck(
|
|
21
|
+
used: number,
|
|
22
|
+
thresholds: MemoryThresholds,
|
|
23
|
+
labels: { subject: string; ceilingKey: string; ceilingName: string },
|
|
24
|
+
readCeiling: () => number,
|
|
25
|
+
): HealthCheckResult {
|
|
26
|
+
const compareAsPercentage =
|
|
27
|
+
thresholds.warnPercentage !== null || thresholds.failPercentage !== null
|
|
28
|
+
|
|
29
|
+
let warnThreshold: number
|
|
30
|
+
let failThreshold: number
|
|
31
|
+
let valueToCompare: number
|
|
32
|
+
let ceiling: number | null = null
|
|
33
|
+
let usedPercentage: number | null = null
|
|
34
|
+
|
|
35
|
+
if (compareAsPercentage) {
|
|
36
|
+
ceiling = readCeiling()
|
|
37
|
+
if (ceiling === 0) {
|
|
38
|
+
return Result.failed(`Cannot determine ${labels.ceilingName} (0 bytes).`).mergeMetaData({
|
|
39
|
+
[labels.ceilingKey]: { used, [labels.ceilingName]: ceiling },
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
usedPercentage = Math.floor((used / ceiling) * 100)
|
|
43
|
+
valueToCompare = usedPercentage
|
|
44
|
+
|
|
45
|
+
if (thresholds.warnPercentage !== null) warnThreshold = thresholds.warnPercentage
|
|
46
|
+
else if (thresholds.warnBytes !== null)
|
|
47
|
+
warnThreshold = Math.floor((thresholds.warnBytes / ceiling) * 100)
|
|
48
|
+
else return Result.failed('Warning threshold for percentage comparison is missing.')
|
|
49
|
+
|
|
50
|
+
if (thresholds.failPercentage !== null) failThreshold = thresholds.failPercentage
|
|
51
|
+
else if (thresholds.failBytes !== null)
|
|
52
|
+
failThreshold = Math.floor((thresholds.failBytes / ceiling) * 100)
|
|
53
|
+
else return Result.failed('Failure threshold for percentage comparison is missing.')
|
|
54
|
+
} else {
|
|
55
|
+
valueToCompare = used
|
|
56
|
+
if (thresholds.warnBytes === null) return Result.failed('Warning threshold (bytes) is missing.')
|
|
57
|
+
warnThreshold = thresholds.warnBytes
|
|
58
|
+
if (thresholds.failBytes === null) return Result.failed('Failure threshold (bytes) is missing.')
|
|
59
|
+
failThreshold = thresholds.failBytes
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const metaData: Record<string, unknown> = {}
|
|
63
|
+
if (compareAsPercentage) {
|
|
64
|
+
metaData.sizeInPercentage = {
|
|
65
|
+
used: usedPercentage,
|
|
66
|
+
failureThreshold: failThreshold,
|
|
67
|
+
warningThreshold: warnThreshold,
|
|
68
|
+
}
|
|
69
|
+
if (ceiling !== null) {
|
|
70
|
+
metaData[labels.ceilingKey] = {
|
|
71
|
+
used,
|
|
72
|
+
[labels.ceilingName]: ceiling,
|
|
73
|
+
failureThreshold: Math.floor((failThreshold / 100) * ceiling),
|
|
74
|
+
warningThreshold: Math.floor((warnThreshold / 100) * ceiling),
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
metaData.memoryInBytes = {
|
|
79
|
+
used,
|
|
80
|
+
failureThreshold: failThreshold,
|
|
81
|
+
warningThreshold: warnThreshold,
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const show = (value: number, threshold: number) =>
|
|
86
|
+
compareAsPercentage
|
|
87
|
+
? { used: `${value}%`, limit: `${threshold}%` }
|
|
88
|
+
: { used: bytes.format(used), limit: bytes.format(threshold) }
|
|
89
|
+
|
|
90
|
+
if (valueToCompare >= failThreshold) {
|
|
91
|
+
const { used: u, limit } = show(valueToCompare, failThreshold)
|
|
92
|
+
return Result.failed(
|
|
93
|
+
`${labels.subject} usage is ${u}, which is above the threshold of ${limit}`,
|
|
94
|
+
).mergeMetaData(metaData)
|
|
95
|
+
}
|
|
96
|
+
if (valueToCompare >= warnThreshold) {
|
|
97
|
+
const { used: u, limit } = show(valueToCompare, warnThreshold)
|
|
98
|
+
return Result.warning(
|
|
99
|
+
`${labels.subject} usage is ${u}, which is above the threshold of ${limit}`,
|
|
100
|
+
).mergeMetaData(metaData)
|
|
101
|
+
}
|
|
102
|
+
return Result.ok(`${labels.subject} usage is under defined thresholds`).mergeMetaData(metaData)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The threshold setters both memory checks expose, shared verbatim. */
|
|
106
|
+
export function parseByteThreshold(value: string | number, method: string): number {
|
|
107
|
+
const parsed = bytes.parse(value)
|
|
108
|
+
if (parsed === null) throw new Error(`Invalid byte value for ${method}: ${value}`)
|
|
109
|
+
return parsed
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function assertPercentage(value: number, label: string): void {
|
|
113
|
+
if (value < 0 || value > 100)
|
|
114
|
+
throw new Error(`${label} threshold percentage must be between 0 and 100.`)
|
|
115
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { BaseCheck } from './BaseCheck.js'
|
|
2
|
+
export { DiskSpaceCheck } from './checks/DiskSpaceCheck.js'
|
|
3
|
+
export { MemoryHeapCheck } from './checks/MemoryHeapCheck.js'
|
|
4
|
+
export { MemoryRSSCheck } from './checks/MemoryRSSCheck.js'
|
|
5
|
+
export { HealthChecks } from './HealthChecks.js'
|
|
6
|
+
export { Result } from './Result.js'
|
|
7
|
+
export * as tracingChannels from './tracing.js'
|
|
8
|
+
export type {
|
|
9
|
+
HealthCheckContract,
|
|
10
|
+
HealthCheckReport,
|
|
11
|
+
HealthCheckResult,
|
|
12
|
+
HealthCheckTracingData,
|
|
13
|
+
} from './types.js'
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import diagnostics_channel from 'node:diagnostics_channel'
|
|
2
|
+
import type { HealthCheckTracingData } from './types.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A diagnostics channel carrying every check run, so an APM can time checks
|
|
6
|
+
* without the app wiring anything.
|
|
7
|
+
*
|
|
8
|
+
* NAMED DEVIATION — the channel is `ream.health.check`, not upstream's
|
|
9
|
+
* `adonisjs.health.check`. A channel name is product identity, and a subscriber
|
|
10
|
+
* listening for both frameworks in one process must be able to tell them apart.
|
|
11
|
+
*/
|
|
12
|
+
export const healthCheck = diagnostics_channel.tracingChannel<
|
|
13
|
+
'ream.health.check',
|
|
14
|
+
HealthCheckTracingData
|
|
15
|
+
>('ream.health.check')
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health-check contracts, mirroring `@adonisjs/core/health`.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** The outcome of running one check. */
|
|
6
|
+
export type HealthCheckResult = {
|
|
7
|
+
/** A summary of the check result. */
|
|
8
|
+
message: string
|
|
9
|
+
/** The status of the check. */
|
|
10
|
+
status: 'ok' | 'warning' | 'error'
|
|
11
|
+
/** When this check finished. */
|
|
12
|
+
finishedAt: Date
|
|
13
|
+
/** Free-form data the check wants to expose alongside its verdict. */
|
|
14
|
+
meta?: Record<string, unknown>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The report produced by running every registered check. */
|
|
18
|
+
export type HealthCheckReport = {
|
|
19
|
+
/** False as soon as one check reports `error`. A warning stays healthy. */
|
|
20
|
+
isHealthy: boolean
|
|
21
|
+
/** `error` beats `warning` beats `ok`. */
|
|
22
|
+
status: 'ok' | 'warning' | 'error'
|
|
23
|
+
/** When the whole report was computed. */
|
|
24
|
+
finishedAt: Date
|
|
25
|
+
/** Identifying data for the process that produced the report. */
|
|
26
|
+
debugInfo: {
|
|
27
|
+
pid: number
|
|
28
|
+
ppid?: number
|
|
29
|
+
/** Seconds the process has been running. */
|
|
30
|
+
uptime: number
|
|
31
|
+
version: string
|
|
32
|
+
platform: string
|
|
33
|
+
}
|
|
34
|
+
/** Each check's result, plus whether it came from the cache. */
|
|
35
|
+
checks: ({ isCached: boolean; name: string } & HealthCheckResult)[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** What every check must implement. */
|
|
39
|
+
export interface HealthCheckContract {
|
|
40
|
+
/** A unique name for the check. */
|
|
41
|
+
name: string
|
|
42
|
+
/** Seconds to reuse a previous result for. Undefined means never cache. */
|
|
43
|
+
cacheDuration?: number
|
|
44
|
+
/** Perform the check. */
|
|
45
|
+
run(): Promise<HealthCheckResult>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** What a tracing subscriber receives. */
|
|
49
|
+
export type HealthCheckTracingData = {
|
|
50
|
+
check: HealthCheckContract
|
|
51
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Byte-size parsing and formatting, as `string.bytes` in AdonisJS.
|
|
3
|
+
*
|
|
4
|
+
* Upstream reaches for `@poppinss/string`, which wraps the `bytes` package.
|
|
5
|
+
* Ream carries the ~40 lines instead of the dependency, matching that package's
|
|
6
|
+
* semantics exactly: binary units (1 kb = 1024 b), a bare number string read as
|
|
7
|
+
* bytes, and `null` — never a throw — when the input cannot be read.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const UNITS = {
|
|
11
|
+
b: 1,
|
|
12
|
+
kb: 1024,
|
|
13
|
+
mb: 1024 ** 2,
|
|
14
|
+
gb: 1024 ** 3,
|
|
15
|
+
tb: 1024 ** 4,
|
|
16
|
+
pb: 1024 ** 5,
|
|
17
|
+
} as const
|
|
18
|
+
|
|
19
|
+
type Unit = keyof typeof UNITS
|
|
20
|
+
|
|
21
|
+
const PARSE = /^((?:-|\+)?(?:\d+(?:\.\d+)?)) *(kb|mb|gb|tb|pb)$/i
|
|
22
|
+
/** Trailing `.00`, or the zeros trailing a real decimal. */
|
|
23
|
+
const TRAILING_ZEROS = /(?:\.0*|(\.[^0]+)0+)$/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Read a human-readable size into a number of bytes.
|
|
27
|
+
*
|
|
28
|
+
* A number passes through untouched. A string with no unit is read as bytes,
|
|
29
|
+
* so `'1024'` and `1024` agree. Returns `null` when there is no number to find.
|
|
30
|
+
*/
|
|
31
|
+
export function parse(value: string | number): number | null {
|
|
32
|
+
if (typeof value === 'number') return Number.isNaN(value) ? null : value
|
|
33
|
+
if (typeof value !== 'string') return null
|
|
34
|
+
|
|
35
|
+
const matched = PARSE.exec(value)
|
|
36
|
+
const amount = matched ? Number.parseFloat(matched[1]) : Number.parseInt(value, 10)
|
|
37
|
+
if (Number.isNaN(amount)) return null
|
|
38
|
+
|
|
39
|
+
const unit = (matched ? matched[2].toLowerCase() : 'b') as Unit
|
|
40
|
+
return Math.floor(UNITS[unit] * amount)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Render a number of bytes for a human, picking the largest unit that leaves a
|
|
45
|
+
* value at or above 1. Returns `null` for a non-finite input.
|
|
46
|
+
*/
|
|
47
|
+
export function format(valueInBytes: number, options?: { decimalPlaces?: number }): string | null {
|
|
48
|
+
if (!Number.isFinite(valueInBytes)) return null
|
|
49
|
+
|
|
50
|
+
const magnitude = Math.abs(valueInBytes)
|
|
51
|
+
const unit: Unit =
|
|
52
|
+
magnitude >= UNITS.pb
|
|
53
|
+
? 'pb'
|
|
54
|
+
: magnitude >= UNITS.tb
|
|
55
|
+
? 'tb'
|
|
56
|
+
: magnitude >= UNITS.gb
|
|
57
|
+
? 'gb'
|
|
58
|
+
: magnitude >= UNITS.mb
|
|
59
|
+
? 'mb'
|
|
60
|
+
: magnitude >= UNITS.kb
|
|
61
|
+
? 'kb'
|
|
62
|
+
: 'b'
|
|
63
|
+
|
|
64
|
+
const rendered = (valueInBytes / UNITS[unit])
|
|
65
|
+
.toFixed(options?.decimalPlaces ?? 2)
|
|
66
|
+
.replace(TRAILING_ZEROS, '$1')
|
|
67
|
+
|
|
68
|
+
return `${rendered}${unit.toUpperCase()}`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default { parse, format }
|
package/src/helpers/string.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto'
|
|
2
|
+
import bytes from './bytes.js'
|
|
2
3
|
|
|
3
4
|
// ---------------------------------------------------------------------------
|
|
4
5
|
// Internal helpers
|
|
@@ -441,6 +442,7 @@ export function interpolate(template: string, data: Record<string, unknown>): st
|
|
|
441
442
|
// ---------------------------------------------------------------------------
|
|
442
443
|
|
|
443
444
|
const string = {
|
|
445
|
+
bytes,
|
|
444
446
|
camelCase,
|
|
445
447
|
snakeCase,
|
|
446
448
|
pascalCase,
|
package/src/http/HttpContext.ts
CHANGED
|
@@ -168,9 +168,31 @@ export interface RouteInfo {
|
|
|
168
168
|
action?: string
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
/**
|
|
172
|
+
* The `'ControllerName.method'` identity of a route, or undefined for an inline
|
|
173
|
+
* handler. This is what `request.matchesRoute()` matches on besides the name and
|
|
174
|
+
* the pattern, the way AdonisJS matches `route.handler.reference`.
|
|
175
|
+
*/
|
|
176
|
+
function controllerReference(route: RouteInfo): string | undefined {
|
|
177
|
+
const controller = route.controller as { name?: string } | undefined
|
|
178
|
+
if (!controller?.name || !route.action) return undefined
|
|
179
|
+
return `${controller.name}.${route.action}`
|
|
180
|
+
}
|
|
181
|
+
|
|
171
182
|
/** Ambient per-request context store (AdonisJS `HttpContext` ALS accessor). */
|
|
172
183
|
const httpContextStorage = new AsyncLocalStorage<HttpContext>()
|
|
173
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Whether the ambient context is being tracked.
|
|
187
|
+
*
|
|
188
|
+
* NAMED DEVIATION — on by default, where AdonisJS makes it opt-in through
|
|
189
|
+
* `useAsyncLocalStorage` in `config/app.ts`. Ream's own middleware and service
|
|
190
|
+
* accessors read the ambient context, so defaulting it off would break the
|
|
191
|
+
* framework's own wiring rather than just an app's. Turn it off with
|
|
192
|
+
* {@link HttpContext.useAsyncLocalStorage} to reclaim the tracking cost.
|
|
193
|
+
*/
|
|
194
|
+
let asyncLocalStorageEnabled = true
|
|
195
|
+
|
|
174
196
|
export class HttpContext extends Macroable {
|
|
175
197
|
/**
|
|
176
198
|
* @internal Run `fn` with `ctx` as the ambient HttpContext, so code deep in
|
|
@@ -179,16 +201,50 @@ export class HttpContext extends Macroable {
|
|
|
179
201
|
* request pipeline in this.
|
|
180
202
|
*/
|
|
181
203
|
static run<T>(ctx: HttpContext, fn: () => T): T {
|
|
204
|
+
if (!asyncLocalStorageEnabled) return fn()
|
|
182
205
|
return httpContextStorage.run(ctx, fn)
|
|
183
206
|
}
|
|
184
207
|
|
|
185
|
-
/**
|
|
186
|
-
|
|
187
|
-
|
|
208
|
+
/**
|
|
209
|
+
* Whether the ambient context is being tracked (AdonisJS
|
|
210
|
+
* `HttpContext.usingAsyncLocalStorage`). When false, {@link get} always
|
|
211
|
+
* answers `null` and {@link getOrFail} always throws.
|
|
212
|
+
*/
|
|
213
|
+
static get usingAsyncLocalStorage(): boolean {
|
|
214
|
+
return asyncLocalStorageEnabled
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Turn ambient-context tracking on or off.
|
|
219
|
+
*
|
|
220
|
+
* AdonisJS decides this from `useAsyncLocalStorage` in `config/app.ts`; ream
|
|
221
|
+
* has it on by default (see the note on the storage above) and exposes the
|
|
222
|
+
* switch instead of a config key, so a benchmark or a worker process can
|
|
223
|
+
* drop the tracking without a config file.
|
|
224
|
+
*/
|
|
225
|
+
static useAsyncLocalStorage(enabled: boolean): void {
|
|
226
|
+
asyncLocalStorageEnabled = enabled
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* The ambient HttpContext, or `null` outside a request (AdonisJS
|
|
231
|
+
* `HttpContext.get()`).
|
|
232
|
+
*
|
|
233
|
+
* `null`, not `undefined`: upstream's contract is `HttpContext | null`, and a
|
|
234
|
+
* migrated `=== null` check has to keep working.
|
|
235
|
+
*/
|
|
236
|
+
static get(): HttpContext | null {
|
|
237
|
+
if (!asyncLocalStorageEnabled) return null
|
|
238
|
+
return httpContextStorage.getStore() ?? null
|
|
188
239
|
}
|
|
189
240
|
|
|
190
241
|
/** The ambient HttpContext, or throw outside a request (AdonisJS `HttpContext.getOrFail()`). */
|
|
191
242
|
static getOrFail(): HttpContext {
|
|
243
|
+
if (!asyncLocalStorageEnabled) {
|
|
244
|
+
throw new Error(
|
|
245
|
+
'[E_HTTP_CONTEXT_NOT_FOUND] HttpContext.getOrFail() called while ambient-context tracking is off. Re-enable it with HttpContext.useAsyncLocalStorage(true), or pass ctx explicitly.',
|
|
246
|
+
)
|
|
247
|
+
}
|
|
192
248
|
const ctx = httpContextStorage.getStore()
|
|
193
249
|
if (!ctx) {
|
|
194
250
|
throw new Error(
|
|
@@ -207,6 +263,17 @@ export class HttpContext extends Macroable {
|
|
|
207
263
|
return httpContextStorage.exit(fn)
|
|
208
264
|
}
|
|
209
265
|
|
|
266
|
+
/**
|
|
267
|
+
* A one-line summary of the request (AdonisJS `inspect`).
|
|
268
|
+
*
|
|
269
|
+
* What a log line or a debugger label wants: the verb, the path, and which
|
|
270
|
+
* route answered.
|
|
271
|
+
*/
|
|
272
|
+
inspect(): string {
|
|
273
|
+
const route = this.route.name ?? this.route.pattern
|
|
274
|
+
return `${this.request.method()} ${this.request.url()} (${route})`
|
|
275
|
+
}
|
|
276
|
+
|
|
210
277
|
/** Unique request/correlation ID. */
|
|
211
278
|
readonly id: string
|
|
212
279
|
|
|
@@ -343,7 +410,14 @@ export class HttpContext extends Macroable {
|
|
|
343
410
|
// And the reverse, so `request.fresh()`/`stale()` delegate to it, plus the
|
|
344
411
|
// matched-route info for `request.matchesRoute()`.
|
|
345
412
|
this.request.setResponse(this.response)
|
|
346
|
-
|
|
413
|
+
// The back-references AdonisJS exposes as `request.ctx` / `response.ctx`.
|
|
414
|
+
this.request.ctx = this
|
|
415
|
+
this.response.ctx = this
|
|
416
|
+
this.request.setRouteInfo({
|
|
417
|
+
name: route.name,
|
|
418
|
+
pattern: route.pattern,
|
|
419
|
+
reference: controllerReference(route),
|
|
420
|
+
})
|
|
347
421
|
// APP_KEY-backed encryption / signed-URL services + the base logger are
|
|
348
422
|
// resolved asynchronously by HttpKernel and injected via the setters above
|
|
349
423
|
// (the container is async now, so a constructor can't resolve them itself).
|
package/src/http/Request.ts
CHANGED
|
@@ -66,7 +66,7 @@ export class Request extends Macroable {
|
|
|
66
66
|
#signedUrl?: SignedUrl
|
|
67
67
|
#allowMethodSpoofing = false
|
|
68
68
|
#trustProxy = false
|
|
69
|
-
#routeInfo?: { name?: string; pattern: string }
|
|
69
|
+
#routeInfo?: { name?: string; pattern: string; reference?: string }
|
|
70
70
|
#response?: { fresh(): boolean }
|
|
71
71
|
#parsedBody: Dict<unknown> | undefined
|
|
72
72
|
#parsedQs: Dict<unknown> | undefined
|
|
@@ -122,13 +122,32 @@ export class Request extends Macroable {
|
|
|
122
122
|
return this.#raw.method
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/**
|
|
126
|
+
* The context this request belongs to (AdonisJS `request.ctx`).
|
|
127
|
+
*
|
|
128
|
+
* Wired by HttpContext. Optional because a Request built by hand — a test,
|
|
129
|
+
* a fixture — has no context around it.
|
|
130
|
+
*/
|
|
131
|
+
ctx?: object
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Seat the parsed body (AdonisJS `setInitialBody`).
|
|
135
|
+
*
|
|
136
|
+
* What a body parser calls once it has decoded the payload. An alias of
|
|
137
|
+
* {@link setParsedBody}, which is the name ream used first — both are here so
|
|
138
|
+
* a middleware written against either resolves.
|
|
139
|
+
*/
|
|
140
|
+
setInitialBody(body: Dict<unknown>): void {
|
|
141
|
+
this.setParsedBody(body)
|
|
142
|
+
}
|
|
143
|
+
|
|
125
144
|
/** @internal Give the request access to its response — wired by HttpContext (for `fresh()`). */
|
|
126
145
|
setResponse(response: { fresh(): boolean }): void {
|
|
127
146
|
this.#response = response
|
|
128
147
|
}
|
|
129
148
|
|
|
130
149
|
/** @internal Record the matched route — wired by HttpContext (for `matchesRoute()`). */
|
|
131
|
-
setRouteInfo(info: { name?: string; pattern: string }): void {
|
|
150
|
+
setRouteInfo(info: { name?: string; pattern: string; reference?: string }): void {
|
|
132
151
|
this.#routeInfo = info
|
|
133
152
|
}
|
|
134
153
|
|
|
@@ -146,10 +165,26 @@ export class Request extends Macroable {
|
|
|
146
165
|
return !this.fresh()
|
|
147
166
|
}
|
|
148
167
|
|
|
149
|
-
/**
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
168
|
+
/**
|
|
169
|
+
* True when the matched route's name, pattern, or controller reference is one
|
|
170
|
+
* of `identifier` (AdonisJS `matchesRoute`).
|
|
171
|
+
*
|
|
172
|
+
* Takes a list as well as a single value, as upstream does — the usual call
|
|
173
|
+
* is "am I on any of these routes", and a caller with several had to write
|
|
174
|
+
* the `.some()` themselves.
|
|
175
|
+
*
|
|
176
|
+
* The reference is the `'ControllerName.method'` form the router already
|
|
177
|
+
* accepts as a handler, which is the identity AdonisJS matches on through
|
|
178
|
+
* `route.handler.reference`. Absent for an inline handler, which has no name
|
|
179
|
+
* to be addressed by.
|
|
180
|
+
*/
|
|
181
|
+
matchesRoute(identifier: string | string[]): boolean {
|
|
182
|
+
const info = this.#routeInfo
|
|
183
|
+
if (!info) return false
|
|
184
|
+
const candidates = Array.isArray(identifier) ? identifier : [identifier]
|
|
185
|
+
return candidates.some(
|
|
186
|
+
(one) => info.name === one || info.pattern === one || info.reference === one,
|
|
187
|
+
)
|
|
153
188
|
}
|
|
154
189
|
|
|
155
190
|
/**
|
|
@@ -161,8 +196,15 @@ export class Request extends Macroable {
|
|
|
161
196
|
this.#allowMethodSpoofing = enabled
|
|
162
197
|
}
|
|
163
198
|
|
|
164
|
-
/**
|
|
165
|
-
|
|
199
|
+
/**
|
|
200
|
+
* The request URL — the PATHNAME by default, as in AdonisJS
|
|
201
|
+
* (`url(includeQueryString)`, false unless asked). Ream defaulted to
|
|
202
|
+
* including the query string, so a value used as a cache key, a log line or
|
|
203
|
+
* a route comparison silently carried the query with it.
|
|
204
|
+
*
|
|
205
|
+
* Pass `true` for path + query; {@link completeUrl} adds the origin.
|
|
206
|
+
*/
|
|
207
|
+
url(includeQs = false): string {
|
|
166
208
|
if (includeQs && this.#raw.query) {
|
|
167
209
|
return `${this.#raw.path}?${this.#raw.query}`
|
|
168
210
|
}
|
|
@@ -330,6 +372,10 @@ export class Request extends Macroable {
|
|
|
330
372
|
* Signed cookie value, verified with APP_KEY (AdonisJS default). Returns
|
|
331
373
|
* `defaultValue` (or null) when absent OR when the signature is invalid
|
|
332
374
|
* (tampered / not signed).
|
|
375
|
+
*
|
|
376
|
+
* NAMED DEVIATION — the fallback is `??`, where AdonisJS writes `||`. A
|
|
377
|
+
* cookie legitimately holding `0`, `false` or `""` is a value, not an
|
|
378
|
+
* absence, and upstream hands back the default for all three. Kept.
|
|
333
379
|
*/
|
|
334
380
|
cookie(name: string): string | null
|
|
335
381
|
cookie(name: string, defaultValue: string): string
|
|
@@ -398,7 +444,9 @@ export class Request extends Macroable {
|
|
|
398
444
|
*/
|
|
399
445
|
hasValidSignature(purpose?: string): boolean {
|
|
400
446
|
if (!this.#signedUrl) return false
|
|
401
|
-
|
|
447
|
+
// `url(true)`: the signature covers the path AND the query string —
|
|
448
|
+
// the expiry and the signature itself live in the query.
|
|
449
|
+
return this.#signedUrl.verify(this.url(true), purpose)
|
|
402
450
|
}
|
|
403
451
|
|
|
404
452
|
/** Get the raw body as a string (decoded from base64 if binary). */
|
|
@@ -683,7 +731,11 @@ export class Request extends Macroable {
|
|
|
683
731
|
all(): Dict<unknown> {
|
|
684
732
|
if (!this.#merged) {
|
|
685
733
|
this.#ensureParsedBody()
|
|
686
|
-
|
|
734
|
+
// `{ ...body, ...qs }` — the QUERY STRING wins, as in AdonisJS
|
|
735
|
+
// (`#requestData = { ...#requestBody, ...#requestQs }`). Ream had it the
|
|
736
|
+
// other way round, so `?id=1` with a body `{ id: 2 }` read 2 here and 1
|
|
737
|
+
// there — silently, on the request field an app trusts most.
|
|
738
|
+
this.#merged = { ...this.#parsedBody, ...this.qs() }
|
|
687
739
|
// Snapshot the first-seen input as the immutable "original" (flash old-input).
|
|
688
740
|
if (this.#original === undefined) this.#original = { ...this.#merged }
|
|
689
741
|
}
|