@c9up/ream 0.1.33 → 0.2.0
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/ConfigLoader.d.ts +4 -0
- package/dist/ConfigLoader.d.ts.map +1 -1
- package/dist/ConfigLoader.js +0 -8
- package/dist/ConfigLoader.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 -0
- 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 +65 -6
- package/dist/bodyparser/MultipartFile.d.ts.map +1 -1
- package/dist/bodyparser/MultipartFile.js +100 -7
- 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 +38 -2
- package/dist/http/HttpContext.d.ts.map +1 -1
- package/dist/http/HttpContext.js +81 -3
- package/dist/http/HttpContext.js.map +1 -1
- package/dist/http/Request.d.ts +57 -3
- package/dist/http/Request.d.ts.map +1 -1
- package/dist/http/Request.js +51 -38
- 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 +5 -2
- package/scheduler.win32-x64-msvc.node +0 -0
- package/src/Application.ts +169 -6
- package/src/ConfigLoader.ts +6 -0
- package/src/HealthCheck.ts +27 -14
- package/src/HttpKernel.ts +27 -4
- package/src/Ignitor.ts +17 -0
- package/src/bodyparser/BodyParserMiddleware.ts +70 -36
- package/src/bodyparser/MultipartFile.ts +125 -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 +86 -4
- package/src/http/Request.ts +82 -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
|
@@ -6,12 +6,19 @@
|
|
|
6
6
|
* Configured via config/bodyparser.ts.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { Exception } from '../http/Exception.js'
|
|
9
10
|
import type { HttpContext } from '../http/HttpContext.js'
|
|
10
11
|
import { hydrateMultipartPayload } from './MultipartFile.js'
|
|
11
12
|
import { parseSize } from './parseSize.js'
|
|
12
13
|
import { parseQueryString, type QsParseOptions } from './qsParse.js'
|
|
13
14
|
|
|
14
15
|
export interface BodyParserConfig {
|
|
16
|
+
/**
|
|
17
|
+
* HTTP methods whose body is parsed at all. AdonisJS defaults to
|
|
18
|
+
* `['POST', 'PUT', 'PATCH', 'DELETE']` and returns early for anything else,
|
|
19
|
+
* so a GET that happens to carry a JSON content-type is left untouched.
|
|
20
|
+
*/
|
|
21
|
+
allowedMethods?: string[]
|
|
15
22
|
json?: {
|
|
16
23
|
limit?: string // e.g. '1mb'
|
|
17
24
|
types?: string[]
|
|
@@ -37,15 +44,21 @@ export interface BodyParserConfig {
|
|
|
37
44
|
multipart?: {
|
|
38
45
|
/** Total multipart body size cap (sum of all file sizes). Default: '20mb'. */
|
|
39
46
|
limit?: string
|
|
40
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* Maximum number of files per request. AdonisJS has no such option — it
|
|
49
|
+
* bounds a multipart body by total size and field count only, so ten
|
|
50
|
+
* thousand one-byte files pass. Kept as a DoS bound, at the same scale as
|
|
51
|
+
* `maxFields` so it never rejects an upload AdonisJS would have accepted.
|
|
52
|
+
*/
|
|
41
53
|
maxFiles?: number
|
|
54
|
+
/** Maximum number of form fields. AdonisJS default: 1000. */
|
|
42
55
|
maxFields?: number
|
|
43
|
-
tmpDir?: string
|
|
44
56
|
types?: string[]
|
|
45
57
|
}
|
|
46
58
|
}
|
|
47
59
|
|
|
48
60
|
interface ResolvedBodyParserConfig {
|
|
61
|
+
allowedMethods: string[]
|
|
49
62
|
json: Required<NonNullable<BodyParserConfig['json']>>
|
|
50
63
|
form: Required<NonNullable<BodyParserConfig['form']>>
|
|
51
64
|
raw: Required<NonNullable<BodyParserConfig['raw']>>
|
|
@@ -53,6 +66,9 @@ interface ResolvedBodyParserConfig {
|
|
|
53
66
|
}
|
|
54
67
|
|
|
55
68
|
const DEFAULT_CONFIG: ResolvedBodyParserConfig = {
|
|
69
|
+
// AdonisJS define_config: allowedMethods. A body is parsed only for methods
|
|
70
|
+
// that are supposed to carry one.
|
|
71
|
+
allowedMethods: ['POST', 'PUT', 'PATCH', 'DELETE'],
|
|
56
72
|
json: {
|
|
57
73
|
limit: '1mb',
|
|
58
74
|
types: ['application/json', 'application/vnd.api+json'],
|
|
@@ -73,9 +89,8 @@ const DEFAULT_CONFIG: ResolvedBodyParserConfig = {
|
|
|
73
89
|
},
|
|
74
90
|
multipart: {
|
|
75
91
|
limit: '20mb',
|
|
76
|
-
maxFiles:
|
|
77
|
-
maxFields:
|
|
78
|
-
tmpDir: '/tmp',
|
|
92
|
+
maxFiles: 1000,
|
|
93
|
+
maxFields: 1000,
|
|
79
94
|
types: ['multipart/form-data'],
|
|
80
95
|
},
|
|
81
96
|
}
|
|
@@ -86,6 +101,7 @@ export default class BodyParserMiddleware {
|
|
|
86
101
|
constructor(config?: BodyParserConfig) {
|
|
87
102
|
assertNoEnabledFlag(config)
|
|
88
103
|
this.#config = {
|
|
104
|
+
allowedMethods: config?.allowedMethods ?? DEFAULT_CONFIG.allowedMethods,
|
|
89
105
|
json: { ...DEFAULT_CONFIG.json, ...config?.json },
|
|
90
106
|
form: { ...DEFAULT_CONFIG.form, ...config?.form },
|
|
91
107
|
raw: { ...DEFAULT_CONFIG.raw, ...config?.raw },
|
|
@@ -94,15 +110,28 @@ export default class BodyParserMiddleware {
|
|
|
94
110
|
}
|
|
95
111
|
|
|
96
112
|
async handle(ctx: HttpContext, next: () => Promise<void>) {
|
|
113
|
+
// Only methods that are supposed to carry a body get one parsed
|
|
114
|
+
// (AdonisJS `allowedMethods`). Comparison is case-sensitive on the
|
|
115
|
+
// canonical upper-case verb, as AdonisJS' `includes()` is.
|
|
116
|
+
if (!this.#config.allowedMethods.includes(ctx.request.method())) {
|
|
117
|
+
return next()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Many clients send `content-length: 0` on a bodyless request, which the
|
|
121
|
+
// parsers below do not recognise as empty (AdonisJS returns early here for
|
|
122
|
+
// the same reason).
|
|
123
|
+
if (!ctx.request.hasBody()) {
|
|
124
|
+
return next()
|
|
125
|
+
}
|
|
126
|
+
|
|
97
127
|
const contentType = ctx.request.header('content-type') ?? ''
|
|
98
128
|
const rawBody = ctx.request.raw()
|
|
99
129
|
|
|
100
|
-
// Size check
|
|
130
|
+
// Size check. Thrown rather than written: the app's exception handler
|
|
131
|
+
// negotiates the response, so an HTML app gets an error page instead of a
|
|
132
|
+
// JSON envelope it never asked for — and can override the rendering.
|
|
101
133
|
if (Buffer.byteLength(rawBody, 'utf8') > parseSize(this.#getLimit(contentType))) {
|
|
102
|
-
|
|
103
|
-
error: { code: 'E_REQUEST_ENTITY_TOO_LARGE', message: 'Request body exceeds size limit' },
|
|
104
|
-
})
|
|
105
|
-
return
|
|
134
|
+
throw entityTooLarge('request entity too large')
|
|
106
135
|
}
|
|
107
136
|
|
|
108
137
|
// JSON
|
|
@@ -134,7 +163,7 @@ export default class BodyParserMiddleware {
|
|
|
134
163
|
if (matchesType(contentType, this.#config.multipart.types)) {
|
|
135
164
|
const payload = ctx.request.multipart()
|
|
136
165
|
if (payload) {
|
|
137
|
-
|
|
166
|
+
this.#assertMultipartWithinLimits(payload.files, payload.fields)
|
|
138
167
|
const { fields, files } = hydrateMultipartPayload(payload)
|
|
139
168
|
// Fingerprint each file's real type from its magic bytes BEFORE handlers
|
|
140
169
|
// run, so `request.file(field, { extnames })` validates against the
|
|
@@ -155,44 +184,36 @@ export default class BodyParserMiddleware {
|
|
|
155
184
|
return this.#config.raw.limit
|
|
156
185
|
}
|
|
157
186
|
|
|
158
|
-
/**
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
187
|
+
/**
|
|
188
|
+
* Throws when the multipart payload exceeds a configured limit.
|
|
189
|
+
*
|
|
190
|
+
* All three raise the same 413 `E_REQUEST_ENTITY_TOO_LARGE` AdonisJS raises
|
|
191
|
+
* for `maxFields` and for an oversized body — one code for "you sent too
|
|
192
|
+
* much", whichever dimension it was.
|
|
193
|
+
*/
|
|
194
|
+
#assertMultipartWithinLimits(files: Array<{ size: number }>, fields: Array<unknown>): void {
|
|
164
195
|
const cfg = this.#config.multipart
|
|
165
|
-
const maxBytes = parseSize(cfg.limit)
|
|
166
196
|
|
|
167
197
|
if (fields.length > cfg.maxFields) {
|
|
168
|
-
|
|
169
|
-
error: {
|
|
170
|
-
code: 'E_TOO_MANY_FIELDS',
|
|
171
|
-
message: `Upload exceeds maxFields (${cfg.maxFields})`,
|
|
172
|
-
},
|
|
173
|
-
})
|
|
174
|
-
return true
|
|
198
|
+
throw entityTooLarge('Fields length limit exceeded')
|
|
175
199
|
}
|
|
176
200
|
|
|
177
201
|
if (files.length > cfg.maxFiles) {
|
|
178
|
-
|
|
179
|
-
error: { code: 'E_TOO_MANY_FILES', message: `Upload exceeds maxFiles (${cfg.maxFiles})` },
|
|
180
|
-
})
|
|
181
|
-
return true
|
|
202
|
+
throw entityTooLarge('Files length limit exceeded')
|
|
182
203
|
}
|
|
183
204
|
|
|
184
205
|
const totalBytes = files.reduce((sum, f) => sum + f.size, 0)
|
|
185
|
-
if (totalBytes >
|
|
186
|
-
|
|
187
|
-
error: { code: 'E_REQUEST_ENTITY_TOO_LARGE', message: 'Upload exceeds size limit' },
|
|
188
|
-
})
|
|
189
|
-
return true
|
|
206
|
+
if (totalBytes > parseSize(cfg.limit)) {
|
|
207
|
+
throw entityTooLarge('request entity too large')
|
|
190
208
|
}
|
|
191
|
-
|
|
192
|
-
return false
|
|
193
209
|
}
|
|
194
210
|
}
|
|
195
211
|
|
|
212
|
+
/** The one exception every body-size limit raises (AdonisJS parity). */
|
|
213
|
+
function entityTooLarge(message: string): Exception {
|
|
214
|
+
return new Exception(message, { status: 413, code: 'E_REQUEST_ENTITY_TOO_LARGE' })
|
|
215
|
+
}
|
|
216
|
+
|
|
196
217
|
/**
|
|
197
218
|
* Whether a request's content-type is claimed by one of `types`.
|
|
198
219
|
*
|
|
@@ -233,4 +254,17 @@ function assertNoEnabledFlag(config?: BodyParserConfig): void {
|
|
|
233
254
|
)
|
|
234
255
|
}
|
|
235
256
|
}
|
|
257
|
+
|
|
258
|
+
// `multipart.tmpDir` was accepted and never read by anything: the Rust
|
|
259
|
+
// multipart parser hands the body over in memory, so no file is ever written
|
|
260
|
+
// to a temporary directory — an app pointing it at a volume was silently
|
|
261
|
+
// getting nothing. Saying so beats letting it keep believing.
|
|
262
|
+
const multipart: unknown = config.multipart
|
|
263
|
+
if (multipart !== null && typeof multipart === 'object' && 'tmpDir' in multipart) {
|
|
264
|
+
throw new Error(
|
|
265
|
+
'[E_BODYPARSER_CONFIG] `multipart.tmpDir` is not supported — uploaded files are held in ' +
|
|
266
|
+
'memory and never written to a temporary directory. Choose the destination when you ' +
|
|
267
|
+
'store the file: `await file.moveToDisk(directory)`.',
|
|
268
|
+
)
|
|
269
|
+
}
|
|
236
270
|
}
|
|
@@ -19,11 +19,43 @@ export class MultipartFile {
|
|
|
19
19
|
readonly clientName: string
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* The PRIMARY mime type from the Content-Type header — `image` for
|
|
23
|
+
* `image/png`. Pair it with {@link subtype}, as AdonisJS does.
|
|
24
|
+
*
|
|
25
|
+
* BREAKING as of this version: it used to hold the whole `image/png`. The
|
|
26
|
+
* change aligns the field with upstream, and TypeScript cannot catch a
|
|
27
|
+
* comparison against a full mime string — `file.type === 'image/png'` now
|
|
28
|
+
* simply never matches. Use `file.subtype` or {@link detectedType}.
|
|
29
|
+
*
|
|
30
|
+
* ATTACKER-CONTROLLED — never trust it for security decisions or as a
|
|
31
|
+
* response Content-Type. Prefer {@link detectedType}, which is fingerprinted
|
|
32
|
+
* from the file's magic bytes.
|
|
25
33
|
*/
|
|
26
|
-
readonly type
|
|
34
|
+
readonly type?: string
|
|
35
|
+
|
|
36
|
+
/** The mime SUBTYPE — `png` for `image/png`. Attacker-controlled, like {@link type}. */
|
|
37
|
+
readonly subtype?: string
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The part's headers.
|
|
41
|
+
*
|
|
42
|
+
* NAMED CONSTRAINT — Rust's multipart parser forwards only the part's
|
|
43
|
+
* content-type across the NAPI boundary today, so this carries that one
|
|
44
|
+
* header rather than every header the client sent.
|
|
45
|
+
*/
|
|
46
|
+
readonly headers: Record<string, string>
|
|
47
|
+
|
|
48
|
+
/** Always true. Lets a value be told apart from a plain form field. */
|
|
49
|
+
readonly isMultipartFile = true as const
|
|
50
|
+
|
|
51
|
+
/** Where the file is in its life: idle, consumed, or moved to disk. */
|
|
52
|
+
state: 'idle' | 'streaming' | 'consumed' | 'moved' = 'consumed'
|
|
53
|
+
|
|
54
|
+
/** Absolute path the file was moved to, once it has been. */
|
|
55
|
+
filePath?: string
|
|
56
|
+
|
|
57
|
+
/** The name it was written under, once it has been moved. */
|
|
58
|
+
fileName?: string
|
|
27
59
|
|
|
28
60
|
/** File size in bytes. */
|
|
29
61
|
readonly size: number
|
|
@@ -43,6 +75,9 @@ export class MultipartFile {
|
|
|
43
75
|
/** Extension derived from the (attacker-controlled) client filename. */
|
|
44
76
|
#clientExtname: string
|
|
45
77
|
|
|
78
|
+
/** Rules set through `sizeLimit` / `allowedExtensions`, used by a bare `validate()`. */
|
|
79
|
+
#validationOptions: FileValidationOptions = {}
|
|
80
|
+
|
|
46
81
|
/** Magic-byte fingerprint — set by {@link detectType}; absent for content `file-type` can't detect (text: txt/csv/svg/json). */
|
|
47
82
|
#detected?: { ext: string; mime: string }
|
|
48
83
|
|
|
@@ -54,7 +89,10 @@ export class MultipartFile {
|
|
|
54
89
|
}) {
|
|
55
90
|
this.fieldName = options.fieldName
|
|
56
91
|
this.clientName = options.clientName
|
|
57
|
-
this.
|
|
92
|
+
this.headers = { 'content-type': options.type }
|
|
93
|
+
const [type, subtype] = options.type.split(';')[0].trim().split('/')
|
|
94
|
+
this.type = type || undefined
|
|
95
|
+
this.subtype = subtype || undefined
|
|
58
96
|
this.content = options.content
|
|
59
97
|
this.size = options.content.length
|
|
60
98
|
const segments = options.clientName.split('.')
|
|
@@ -91,8 +129,37 @@ export class MultipartFile {
|
|
|
91
129
|
return this.#detected?.ext ?? this.#clientExtname
|
|
92
130
|
}
|
|
93
131
|
|
|
94
|
-
/**
|
|
95
|
-
|
|
132
|
+
/** The size ceiling set through {@link sizeLimit}, if any. */
|
|
133
|
+
get sizeLimit(): number | string | undefined {
|
|
134
|
+
return this.#validationOptions.size
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
set sizeLimit(limit: number | string | undefined) {
|
|
138
|
+
this.#validationOptions.size = typeof limit === 'number' ? String(limit) : limit
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The extension allowlist set through {@link allowedExtensions}, if any. */
|
|
142
|
+
get allowedExtensions(): string[] | undefined {
|
|
143
|
+
return this.#validationOptions.extnames
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
set allowedExtensions(extensions: string[] | undefined) {
|
|
147
|
+
this.#validationOptions.extnames = extensions
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Whether validation turned anything up. The inverse of {@link isValid}. */
|
|
151
|
+
get hasErrors(): boolean {
|
|
152
|
+
return this.errors.length > 0
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Validate the file against size and extension rules.
|
|
157
|
+
*
|
|
158
|
+
* The argument is optional: called bare it uses whatever was set through
|
|
159
|
+
* {@link sizeLimit} and {@link allowedExtensions}, which is how AdonisJS
|
|
160
|
+
* spells it.
|
|
161
|
+
*/
|
|
162
|
+
validate(options: FileValidationOptions = this.#validationOptions): boolean {
|
|
96
163
|
if (options.size) {
|
|
97
164
|
const maxBytes = parseFileSize(options.size)
|
|
98
165
|
if (this.size > maxBytes) {
|
|
@@ -145,10 +212,60 @@ export class MultipartFile {
|
|
|
145
212
|
}
|
|
146
213
|
const filePath = join(directory, fileName)
|
|
147
214
|
writeFileSync(filePath, this.content)
|
|
148
|
-
this
|
|
215
|
+
this.markAsMoved(fileName, filePath)
|
|
149
216
|
return filePath
|
|
150
217
|
}
|
|
151
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Write the file into `location` (AdonisJS `move`).
|
|
221
|
+
*
|
|
222
|
+
* NAMED DEVIATION — upstream renames a temporary file it streamed to disk;
|
|
223
|
+
* Ream holds the bytes in memory (`multipart.tmpDir` is refused at
|
|
224
|
+
* construction), so this writes the buffer. There is therefore no
|
|
225
|
+
* `E_MISSING_FILE_TMP_PATH`: there is no temporary file to be missing.
|
|
226
|
+
*
|
|
227
|
+
* `overwrite` defaults to true, as upstream. The generated name carries the
|
|
228
|
+
* detected extension, and both it and a caller-supplied `name` go through the
|
|
229
|
+
* same plain-filename guard {@link moveToDisk} uses — the common footgun is
|
|
230
|
+
* passing `file.clientName` straight through.
|
|
231
|
+
*/
|
|
232
|
+
async move(location: string, options?: { name?: string; overwrite?: boolean }): Promise<void> {
|
|
233
|
+
const { mkdir, writeFile, access } = await import('node:fs/promises')
|
|
234
|
+
const { join } = await import('node:path')
|
|
235
|
+
|
|
236
|
+
const name = options?.name ?? `${randomBytes(16).toString('hex')}.${this.extname || 'unknown'}`
|
|
237
|
+
if (!isSafeFilename(name)) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
`MultipartFile.move: 'name' must be a plain filename (no path separators, '.', or '..'). Got: ${JSON.stringify(name)}`,
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const filePath = join(location, name)
|
|
244
|
+
if (options?.overwrite === false) {
|
|
245
|
+
const exists = await access(filePath).then(
|
|
246
|
+
() => true,
|
|
247
|
+
() => false,
|
|
248
|
+
)
|
|
249
|
+
if (exists) {
|
|
250
|
+
throw new Error(
|
|
251
|
+
`"${name}" already exists at "${location}". Set "overwrite = true" to overwrite it`,
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
await mkdir(location, { recursive: true })
|
|
257
|
+
await writeFile(filePath, this.content)
|
|
258
|
+
this.markAsMoved(name, filePath)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Record that the file now lives on disk. */
|
|
262
|
+
markAsMoved(fileName: string, filePath: string): void {
|
|
263
|
+
this.fileName = fileName
|
|
264
|
+
this.filePath = filePath
|
|
265
|
+
this.state = 'moved'
|
|
266
|
+
this.#moved = true
|
|
267
|
+
}
|
|
268
|
+
|
|
152
269
|
get isMoved(): boolean {
|
|
153
270
|
return this.#moved
|
|
154
271
|
}
|
|
@@ -112,7 +112,7 @@ export abstract class BaseCommand {
|
|
|
112
112
|
|
|
113
113
|
/**
|
|
114
114
|
* The error thrown by `prepare` / `interact` / `run`, assigned before
|
|
115
|
-
* `completed()` runs so the hook can inspect it
|
|
115
|
+
* `completed()` runs so the hook can inspect it.
|
|
116
116
|
*/
|
|
117
117
|
error?: unknown
|
|
118
118
|
|
|
@@ -124,6 +124,13 @@ export abstract class BaseCommand {
|
|
|
124
124
|
* `run`, then `completed` — which runs even when one of the earlier stages
|
|
125
125
|
* threw. Returning `true` from `completed()` marks the error handled, so the
|
|
126
126
|
* kernel stops propagating it.
|
|
127
|
+
*
|
|
128
|
+
* A SUPERSET, not parity: AdonisJS carried these three through ace 13 and
|
|
129
|
+
* dropped them in ace 14, whose command lifecycle is `hydrate()` then
|
|
130
|
+
* `run()` — both of which {@link hydrate} and {@link run} match exactly.
|
|
131
|
+
* They are kept because a `completed()` that runs even after a failure is
|
|
132
|
+
* the only place a command can clean up after itself, and a command written
|
|
133
|
+
* against either lifecycle works here.
|
|
127
134
|
*/
|
|
128
135
|
// Parameters are allowed: the container injects them (`@inject()` on a
|
|
129
136
|
// lifecycle method), and a no-arg signature rejects those commands.
|
package/src/console/Kernel.ts
CHANGED
|
@@ -268,6 +268,24 @@ export class Kernel {
|
|
|
268
268
|
return this.#binaryName
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
/**
|
|
272
|
+
* A named `true`, for a flag listener that wants to stop the run (Console
|
|
273
|
+
* `kernel.shortcircuit`).
|
|
274
|
+
*
|
|
275
|
+
* A listener returning a bare `true` reads like a bug at the call site;
|
|
276
|
+
* `return kernel.shortcircuit()` says what the boolean means:
|
|
277
|
+
*
|
|
278
|
+
* ```ts
|
|
279
|
+
* kernel.on('version', (_c, kernel) => {
|
|
280
|
+
* console.log(version)
|
|
281
|
+
* return kernel.shortcircuit()
|
|
282
|
+
* })
|
|
283
|
+
* ```
|
|
284
|
+
*/
|
|
285
|
+
shortcircuit(): boolean {
|
|
286
|
+
return true
|
|
287
|
+
}
|
|
288
|
+
|
|
271
289
|
/**
|
|
272
290
|
* Declare a flag every command accepts (Console `kernel.defineFlag`).
|
|
273
291
|
*
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The conventional directory layout, as `@adonisjs/application` exports it.
|
|
3
|
+
*
|
|
4
|
+
* Every path helper on {@link Application} reads this map, so an app that moves
|
|
5
|
+
* a directory declares it once in `reamrc.ts` and every helper follows:
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* export default defineConfig({
|
|
9
|
+
* directories: { httpControllers: 'app/http/controllers' },
|
|
10
|
+
* })
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export interface DirectoriesNode {
|
|
14
|
+
[key: string]: string
|
|
15
|
+
config: string
|
|
16
|
+
commands: string
|
|
17
|
+
contracts: string
|
|
18
|
+
public: string
|
|
19
|
+
providers: string
|
|
20
|
+
languageFiles: string
|
|
21
|
+
migrations: string
|
|
22
|
+
seeders: string
|
|
23
|
+
factories: string
|
|
24
|
+
views: string
|
|
25
|
+
start: string
|
|
26
|
+
tmp: string
|
|
27
|
+
tests: string
|
|
28
|
+
httpControllers: string
|
|
29
|
+
models: string
|
|
30
|
+
services: string
|
|
31
|
+
exceptions: string
|
|
32
|
+
mailers: string
|
|
33
|
+
mails: string
|
|
34
|
+
middleware: string
|
|
35
|
+
policies: string
|
|
36
|
+
validators: string
|
|
37
|
+
events: string
|
|
38
|
+
listeners: string
|
|
39
|
+
transformers: string
|
|
40
|
+
stubs: string
|
|
41
|
+
generatedClient: string
|
|
42
|
+
generatedServer: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Defaults, matching AdonisJS directory for directory.
|
|
47
|
+
*
|
|
48
|
+
* NAMED DEVIATION — the two generated directories are `.ream/*` where upstream
|
|
49
|
+
* writes `.adonisjs/*`. A dot-directory in the user's project is product
|
|
50
|
+
* identity, not layout.
|
|
51
|
+
*/
|
|
52
|
+
export const directories: DirectoriesNode = {
|
|
53
|
+
config: 'config',
|
|
54
|
+
commands: 'commands',
|
|
55
|
+
contracts: 'contracts',
|
|
56
|
+
public: 'public',
|
|
57
|
+
providers: 'providers',
|
|
58
|
+
languageFiles: 'resources/lang',
|
|
59
|
+
migrations: 'database/migrations',
|
|
60
|
+
seeders: 'database/seeders',
|
|
61
|
+
factories: 'database/factories',
|
|
62
|
+
views: 'resources/views',
|
|
63
|
+
start: 'start',
|
|
64
|
+
tmp: 'tmp',
|
|
65
|
+
tests: 'tests',
|
|
66
|
+
httpControllers: 'app/controllers',
|
|
67
|
+
models: 'app/models',
|
|
68
|
+
services: 'app/services',
|
|
69
|
+
exceptions: 'app/exceptions',
|
|
70
|
+
mailers: 'app/mailers',
|
|
71
|
+
mails: 'app/mails',
|
|
72
|
+
middleware: 'app/middleware',
|
|
73
|
+
policies: 'app/policies',
|
|
74
|
+
validators: 'app/validators',
|
|
75
|
+
events: 'app/events',
|
|
76
|
+
listeners: 'app/listeners',
|
|
77
|
+
transformers: 'app/transformers',
|
|
78
|
+
stubs: 'stubs',
|
|
79
|
+
generatedClient: '.ream/client',
|
|
80
|
+
generatedServer: '.ream/server',
|
|
81
|
+
}
|