@c9up/ream 0.2.29 → 0.2.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -6
- package/dist/Codemods.d.ts +8 -0
- package/dist/Codemods.d.ts.map +1 -1
- package/dist/Codemods.js +29 -1
- package/dist/Codemods.js.map +1 -1
- package/dist/HttpKernel.d.ts.map +1 -1
- package/dist/HttpKernel.js +17 -5
- package/dist/HttpKernel.js.map +1 -1
- package/dist/Ignitor.d.ts +5 -0
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +12 -1
- package/dist/Ignitor.js.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.d.ts +4 -4
- package/dist/bodyparser/BodyParserMiddleware.d.ts.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.js +38 -11
- package/dist/bodyparser/BodyParserMiddleware.js.map +1 -1
- package/dist/bodyparser/MultipartFile.d.ts +21 -15
- package/dist/bodyparser/MultipartFile.d.ts.map +1 -1
- package/dist/bodyparser/MultipartFile.js +40 -58
- package/dist/bodyparser/MultipartFile.js.map +1 -1
- package/dist/bodyparser/parseSize.d.ts +7 -4
- package/dist/bodyparser/parseSize.d.ts.map +1 -1
- package/dist/bodyparser/parseSize.js +12 -17
- package/dist/bodyparser/parseSize.js.map +1 -1
- package/dist/bodyparser/qsParse.d.ts +10 -0
- package/dist/bodyparser/qsParse.d.ts.map +1 -1
- package/dist/bodyparser/qsParse.js +27 -12
- package/dist/bodyparser/qsParse.js.map +1 -1
- package/dist/console/prompts.d.ts.map +1 -1
- package/dist/console/prompts.js +3 -1
- package/dist/console/prompts.js.map +1 -1
- package/dist/container/Container.d.ts.map +1 -1
- package/dist/container/Container.js +61 -71
- package/dist/container/Container.js.map +1 -1
- package/dist/events/Emitter.d.ts +12 -0
- package/dist/events/Emitter.d.ts.map +1 -1
- package/dist/events/Emitter.js +36 -9
- package/dist/events/Emitter.js.map +1 -1
- package/dist/events/helix/helpers.d.ts.map +1 -1
- package/dist/events/helix/helpers.js +19 -9
- package/dist/events/helix/helpers.js.map +1 -1
- package/dist/graphql/GraphQLEngine.d.ts +8 -0
- package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
- package/dist/graphql/GraphQLEngine.js +77 -11
- package/dist/graphql/GraphQLEngine.js.map +1 -1
- package/dist/helpers/MessageBuilder.d.ts +4 -2
- package/dist/helpers/MessageBuilder.d.ts.map +1 -1
- package/dist/helpers/MessageBuilder.js +4 -2
- package/dist/helpers/MessageBuilder.js.map +1 -1
- package/dist/helpers/errors.d.ts +11 -0
- package/dist/helpers/errors.d.ts.map +1 -0
- package/dist/helpers/errors.js +17 -0
- package/dist/helpers/errors.js.map +1 -0
- package/dist/helpers/napi-loader.d.ts.map +1 -1
- package/dist/helpers/napi-loader.js +3 -2
- package/dist/helpers/napi-loader.js.map +1 -1
- package/dist/http/HttpContext.d.ts +20 -2
- package/dist/http/HttpContext.d.ts.map +1 -1
- package/dist/http/HttpContext.js +10 -4
- package/dist/http/HttpContext.js.map +1 -1
- package/dist/http/RedirectBuilder.d.ts.map +1 -1
- package/dist/http/RedirectBuilder.js +4 -0
- package/dist/http/RedirectBuilder.js.map +1 -1
- package/dist/http/Request.d.ts.map +1 -1
- package/dist/http/Request.js +15 -29
- package/dist/http/Request.js.map +1 -1
- package/dist/http/Response.d.ts +7 -4
- package/dist/http/Response.d.ts.map +1 -1
- package/dist/http/Response.js +14 -6
- package/dist/http/Response.js.map +1 -1
- package/dist/http/permissions.d.ts +14 -8
- package/dist/http/permissions.d.ts.map +1 -1
- package/dist/http/permissions.js +26 -16
- package/dist/http/permissions.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/Pipeline.js +1 -1
- package/dist/middleware/Pipeline.js.map +1 -1
- package/dist/native/generated.d.ts +16 -30
- package/dist/native/generated.d.ts.map +1 -1
- package/dist/profiler/Profiler.d.ts.map +1 -1
- package/dist/profiler/Profiler.js +4 -1
- package/dist/profiler/Profiler.js.map +1 -1
- package/dist/profiler/client.generated.d.ts +5 -0
- package/dist/profiler/client.generated.d.ts.map +1 -0
- package/dist/profiler/client.generated.js +6 -0
- package/dist/profiler/client.generated.js.map +1 -0
- package/dist/profiler/collectors.d.ts +92 -13
- package/dist/profiler/collectors.d.ts.map +1 -1
- package/dist/profiler/collectors.js +106 -19
- package/dist/profiler/collectors.js.map +1 -1
- package/dist/profiler/endpoint.d.ts +2 -1
- package/dist/profiler/endpoint.d.ts.map +1 -1
- package/dist/profiler/endpoint.js +25 -1
- package/dist/profiler/endpoint.js.map +1 -1
- package/dist/profiler/index.d.ts +8 -4
- package/dist/profiler/index.d.ts.map +1 -1
- package/dist/profiler/index.js +11 -4
- package/dist/profiler/index.js.map +1 -1
- package/dist/profiler/middleware.d.ts.map +1 -1
- package/dist/profiler/middleware.js +76 -10
- package/dist/profiler/middleware.js.map +1 -1
- package/dist/profiler/preview.d.ts +17 -0
- package/dist/profiler/preview.d.ts.map +1 -0
- package/dist/profiler/preview.js +100 -0
- package/dist/profiler/preview.js.map +1 -0
- package/dist/profiler/toolbar.d.ts +15 -23
- package/dist/profiler/toolbar.d.ts.map +1 -1
- package/dist/profiler/toolbar.js +25 -91
- package/dist/profiler/toolbar.js.map +1 -1
- package/dist/profiler/types.d.ts +6 -1
- package/dist/profiler/types.d.ts.map +1 -1
- package/dist/router/Router.d.ts +6 -0
- package/dist/router/Router.d.ts.map +1 -1
- package/dist/router/Router.js +115 -12
- package/dist/router/Router.js.map +1 -1
- package/dist/rpc/RpcRouter.js +3 -3
- package/dist/rpc/RpcRouter.js.map +1 -1
- package/dist/scheduler/Scheduler.js +1 -1
- package/dist/scheduler/Scheduler.js.map +1 -1
- package/dist/security/CookieSigner.d.ts.map +1 -1
- package/dist/security/CookieSigner.js +15 -5
- package/dist/security/CookieSigner.js.map +1 -1
- package/dist/security/SignedUrl.d.ts.map +1 -1
- package/dist/security/SignedUrl.js +10 -18
- package/dist/security/SignedUrl.js.map +1 -1
- package/dist/server/Server.d.ts +4 -1
- package/dist/server/Server.d.ts.map +1 -1
- package/dist/server/Server.js.map +1 -1
- package/dist/server/nativeLimits.d.ts +9 -1
- package/dist/server/nativeLimits.d.ts.map +1 -1
- package/dist/server/nativeLimits.js +8 -0
- package/dist/server/nativeLimits.js.map +1 -1
- package/dist/session/Session.d.ts.map +1 -1
- package/dist/session/Session.js +13 -4
- package/dist/session/Session.js.map +1 -1
- package/dist/session/drivers/MemoryDriver.d.ts.map +1 -1
- package/dist/session/drivers/MemoryDriver.js +14 -2
- package/dist/session/drivers/MemoryDriver.js.map +1 -1
- package/dist/storage/StaticMiddleware.d.ts.map +1 -1
- package/dist/storage/StaticMiddleware.js +20 -9
- package/dist/storage/StaticMiddleware.js.map +1 -1
- package/dist/testing/ApiResponse.d.ts.map +1 -1
- package/dist/testing/ApiResponse.js +7 -1
- package/dist/testing/ApiResponse.js.map +1 -1
- package/dist/testing/TestClient.d.ts.map +1 -1
- package/dist/testing/TestClient.js +16 -4
- package/dist/testing/TestClient.js.map +1 -1
- package/dist/utils/Macroable.d.ts +1 -1
- package/dist/utils/Macroable.js +2 -2
- package/dist/utils/Macroable.js.map +1 -1
- package/events.darwin-arm64.node +0 -0
- package/events.darwin-x64.node +0 -0
- package/events.linux-arm64-gnu.node +0 -0
- package/events.linux-x64-gnu.node +0 -0
- 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 +1 -2
- package/scheduler.darwin-arm64.node +0 -0
- package/scheduler.darwin-x64.node +0 -0
- package/scheduler.linux-arm64-gnu.node +0 -0
- package/scheduler.linux-x64-gnu.node +0 -0
- package/scheduler.win32-x64-msvc.node +0 -0
- package/src/Codemods.ts +49 -3
- package/src/HttpKernel.ts +18 -7
- package/src/Ignitor.ts +21 -4
- package/src/bodyparser/BodyParserMiddleware.ts +42 -16
- package/src/bodyparser/MultipartFile.ts +46 -67
- package/src/bodyparser/parseSize.ts +17 -17
- package/src/bodyparser/qsParse.ts +28 -13
- package/src/console/prompts.ts +3 -1
- package/src/container/Container.ts +97 -81
- package/src/events/Emitter.ts +37 -10
- package/src/events/helix/helpers.ts +24 -19
- package/src/graphql/GraphQLEngine.ts +89 -13
- package/src/helpers/MessageBuilder.ts +4 -2
- package/src/helpers/errors.ts +16 -0
- package/src/helpers/napi-loader.ts +3 -2
- package/src/http/HttpContext.ts +20 -4
- package/src/http/RedirectBuilder.ts +3 -0
- package/src/http/Request.ts +15 -29
- package/src/http/Response.ts +15 -7
- package/src/http/permissions.ts +31 -15
- package/src/index.ts +2 -1
- package/src/middleware/Pipeline.ts +1 -1
- package/src/native/generated.ts +18 -42
- package/src/profiler/Profiler.ts +4 -1
- package/src/profiler/client/toolbar.css +595 -0
- package/src/profiler/client/toolbar.js +1299 -0
- package/src/profiler/client.generated.ts +7 -0
- package/src/profiler/collectors.ts +156 -19
- package/src/profiler/endpoint.ts +27 -1
- package/src/profiler/index.ts +17 -3
- package/src/profiler/middleware.ts +74 -9
- package/src/profiler/preview.ts +104 -0
- package/src/profiler/toolbar.ts +28 -91
- package/src/profiler/types.ts +6 -1
- package/src/router/Router.ts +136 -13
- package/src/rpc/RpcRouter.ts +4 -4
- package/src/scheduler/Scheduler.ts +1 -1
- package/src/security/CookieSigner.ts +13 -5
- package/src/security/SignedUrl.ts +12 -16
- package/src/server/Server.ts +4 -1
- package/src/server/nativeLimits.ts +18 -1
- package/src/session/Session.ts +13 -4
- package/src/session/drivers/MemoryDriver.ts +15 -2
- package/src/storage/StaticMiddleware.ts +18 -9
- package/src/testing/ApiResponse.ts +7 -1
- package/src/testing/TestClient.ts +14 -5
- package/src/utils/Macroable.ts +2 -2
- package/scripts/build-napi-types.mjs +0 -69
- package/scripts/copy-napi.mjs +0 -121
- package/scripts/generate-mime.mjs +0 -119
- package/scripts/generate-napi-types.mjs +0 -187
|
@@ -51,18 +51,27 @@ function isMimeType(value: string): value is MimeType {
|
|
|
51
51
|
*
|
|
52
52
|
* Like AdonisJS MultipartFile:
|
|
53
53
|
* const avatar = request.file('avatar', { size: '2mb', extnames: ['jpg', 'png'] })
|
|
54
|
-
* await avatar.
|
|
54
|
+
* await avatar.move(app.makePath('uploads'))
|
|
55
|
+
*
|
|
56
|
+
* Macroable: a package extends every upload at boot, the way a storage
|
|
57
|
+
* package adds `moveToDisk(key, disk?, options?)` to write it to a configured
|
|
58
|
+
* disk. `moveToDisk` is deliberately not defined here, so that macro owns it.
|
|
55
59
|
*/
|
|
56
60
|
|
|
57
61
|
import { randomBytes } from 'node:crypto'
|
|
58
62
|
import { Readable } from 'node:stream'
|
|
63
|
+
import { errorCode } from '../helpers/errors.js'
|
|
64
|
+
import { Macroable } from '../utils/Macroable.js'
|
|
65
|
+
import { parseSize } from './parseSize.js'
|
|
66
|
+
import { parseFormFields } from './qsParse.js'
|
|
59
67
|
|
|
60
68
|
export interface FileValidationOptions {
|
|
61
|
-
size
|
|
69
|
+
/** Largest accepted size: bytes, or a value such as `'2mb'` or `'1.5 mb'`. */
|
|
70
|
+
size?: string | number
|
|
62
71
|
extnames?: string[]
|
|
63
72
|
}
|
|
64
73
|
|
|
65
|
-
export class MultipartFile {
|
|
74
|
+
export class MultipartFile extends Macroable {
|
|
66
75
|
/** Original filename from the client. */
|
|
67
76
|
readonly clientName: string
|
|
68
77
|
|
|
@@ -153,6 +162,12 @@ export class MultipartFile {
|
|
|
153
162
|
/** The name it was written under, once it has been moved. */
|
|
154
163
|
fileName?: string
|
|
155
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Free-form data about the file, for whoever moves it: a storage macro
|
|
167
|
+
* records the URL of the stored object here.
|
|
168
|
+
*/
|
|
169
|
+
meta: Record<string, unknown> = {}
|
|
170
|
+
|
|
156
171
|
/** File size in bytes. */
|
|
157
172
|
readonly size: number
|
|
158
173
|
|
|
@@ -188,6 +203,7 @@ export class MultipartFile {
|
|
|
188
203
|
type: string
|
|
189
204
|
content: Buffer
|
|
190
205
|
}) {
|
|
206
|
+
super()
|
|
191
207
|
this.fieldName = options.fieldName
|
|
192
208
|
this.clientName = options.clientName
|
|
193
209
|
this.headers = { 'content-type': options.type }
|
|
@@ -272,8 +288,8 @@ export class MultipartFile {
|
|
|
272
288
|
* spells it.
|
|
273
289
|
*/
|
|
274
290
|
validate(options: FileValidationOptions = this.#validationOptions): boolean {
|
|
275
|
-
if (options.size) {
|
|
276
|
-
const maxBytes =
|
|
291
|
+
if (options.size !== undefined) {
|
|
292
|
+
const maxBytes = parseSize(options.size)
|
|
277
293
|
if (this.size > maxBytes) {
|
|
278
294
|
this.errors.push(`File size ${this.size} exceeds limit ${options.size}`)
|
|
279
295
|
}
|
|
@@ -318,40 +334,6 @@ export class MultipartFile {
|
|
|
318
334
|
return this.errors.length === 0
|
|
319
335
|
}
|
|
320
336
|
|
|
321
|
-
/**
|
|
322
|
-
* Move file to a directory on disk. When `name` is supplied it must be
|
|
323
|
-
* a plain filename — no path separators, no `.`/`..` segments. The
|
|
324
|
-
* common footgun is passing the client-supplied `clientName` directly
|
|
325
|
-
* (`await file.moveToDisk('uploads', file.clientName)`); a malicious
|
|
326
|
-
* upload could otherwise traverse out of `directory` via `../../etc/...`.
|
|
327
|
-
*/
|
|
328
|
-
async moveToDisk(directory: string, name?: string): Promise<string> {
|
|
329
|
-
const { mkdirSync, writeFileSync } = await import('node:fs')
|
|
330
|
-
const { join } = await import('node:path')
|
|
331
|
-
|
|
332
|
-
if (name !== undefined && !isSafeFilename(name)) {
|
|
333
|
-
throw new Error(
|
|
334
|
-
`MultipartFile.moveToDisk: 'name' must be a plain filename (no path separators, '.', or '..'). Got: ${JSON.stringify(name)}`,
|
|
335
|
-
)
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
mkdirSync(directory, { recursive: true })
|
|
339
|
-
const fileName = name ?? `${randomBytes(16).toString('hex')}.${this.extname}`
|
|
340
|
-
// Defense-in-depth: the default branch interpolates `extname` (derived
|
|
341
|
-
// from attacker-controlled `clientName`) into the filename, bypassing
|
|
342
|
-
// the `name` guard above. Re-validate the synthesized fileName before
|
|
343
|
-
// join to reject a hostile extname (separators, NUL).
|
|
344
|
-
if (!isSafeFilename(fileName)) {
|
|
345
|
-
throw new Error(
|
|
346
|
-
`MultipartFile.moveToDisk: derived filename '${fileName}' is unsafe (separator or invalid char in clientName-derived extname).`,
|
|
347
|
-
)
|
|
348
|
-
}
|
|
349
|
-
const filePath = join(directory, fileName)
|
|
350
|
-
writeFileSync(filePath, this.content)
|
|
351
|
-
this.markAsMoved(fileName, filePath)
|
|
352
|
-
return filePath
|
|
353
|
-
}
|
|
354
|
-
|
|
355
337
|
/**
|
|
356
338
|
* Write the file into `location` (AdonisJS `move`).
|
|
357
339
|
*
|
|
@@ -360,19 +342,31 @@ export class MultipartFile {
|
|
|
360
342
|
* construction), so this writes the buffer. There is therefore no
|
|
361
343
|
* `E_MISSING_FILE_TMP_PATH`: there is no temporary file to be missing.
|
|
362
344
|
*
|
|
363
|
-
* `overwrite` defaults to true, as upstream.
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
345
|
+
* `overwrite` defaults to true, as upstream.
|
|
346
|
+
*
|
|
347
|
+
* NAMED DEVIATION — without `name`, upstream writes under `clientName`, a
|
|
348
|
+
* value the client chose; Ream generates a random name carrying the
|
|
349
|
+
* detected extension, so an upload neither picks its own path nor
|
|
350
|
+
* overwrites another one by sharing its name.
|
|
351
|
+
*
|
|
352
|
+
* A caller-supplied `name` must be a plain filename (no separator, no `.`
|
|
353
|
+
* or `..`): the common footgun is passing `file.clientName` straight
|
|
354
|
+
* through. The generated name is checked too, since its extension comes
|
|
355
|
+
* from the client's filename when the bytes are not recognised.
|
|
367
356
|
*/
|
|
368
357
|
async move(location: string, options?: { name?: string; overwrite?: boolean }): Promise<void> {
|
|
369
358
|
const { mkdir, writeFile } = await import('node:fs/promises')
|
|
370
359
|
const { join } = await import('node:path')
|
|
371
360
|
|
|
361
|
+
if (options?.name !== undefined && !isSafeFilename(options.name)) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
`MultipartFile.move: 'name' must be a plain filename (no path separators, '.', or '..'). Got: ${JSON.stringify(options.name)}`,
|
|
364
|
+
)
|
|
365
|
+
}
|
|
372
366
|
const name = options?.name ?? `${randomBytes(16).toString('hex')}.${this.extname || 'unknown'}`
|
|
373
367
|
if (!isSafeFilename(name)) {
|
|
374
368
|
throw new Error(
|
|
375
|
-
`MultipartFile.move:
|
|
369
|
+
`MultipartFile.move: the derived file name ${JSON.stringify(name)} is unsafe (a separator or NUL in the extension of the client's filename).`,
|
|
376
370
|
)
|
|
377
371
|
}
|
|
378
372
|
|
|
@@ -392,7 +386,7 @@ export class MultipartFile {
|
|
|
392
386
|
try {
|
|
393
387
|
await writeFile(filePath, this.content, options?.overwrite === false ? { flag: 'wx' } : {})
|
|
394
388
|
} catch (error) {
|
|
395
|
-
if ((error
|
|
389
|
+
if (errorCode(error) === 'EEXIST') {
|
|
396
390
|
throw new Error(
|
|
397
391
|
`"${name}" already exists at "${location}". Set "overwrite = true" to overwrite it`,
|
|
398
392
|
)
|
|
@@ -432,23 +426,6 @@ function isSafeFilename(name: string): boolean {
|
|
|
432
426
|
return true
|
|
433
427
|
}
|
|
434
428
|
|
|
435
|
-
function parseFileSize(size: string): number {
|
|
436
|
-
const match = size.match(/^(\d+)(kb|mb|gb)?$/i)
|
|
437
|
-
if (!match) return 1024 * 1024
|
|
438
|
-
const [, digits, unit] = match
|
|
439
|
-
const num = parseInt(digits ?? '0', 10)
|
|
440
|
-
switch (unit?.toLowerCase()) {
|
|
441
|
-
case 'kb':
|
|
442
|
-
return num * 1024
|
|
443
|
-
case 'mb':
|
|
444
|
-
return num * 1024 * 1024
|
|
445
|
-
case 'gb':
|
|
446
|
-
return num * 1024 * 1024 * 1024
|
|
447
|
-
default:
|
|
448
|
-
return num
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
429
|
/**
|
|
453
430
|
* Hydrate `MultipartFile` instances from the typed payload the Rust
|
|
454
431
|
* HyperServer attaches to the request. The wire-level parser (multer crate,
|
|
@@ -464,11 +441,13 @@ export function hydrateMultipartPayload(payload: {
|
|
|
464
441
|
size: number
|
|
465
442
|
contentB64: string
|
|
466
443
|
}>
|
|
467
|
-
}): { fields: Record<string,
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
444
|
+
}): { fields: Record<string, unknown>; files: MultipartFile[] } {
|
|
445
|
+
// Nested and collected exactly like a urlencoded body: `tags[]` repeated is
|
|
446
|
+
// an array, `user[name]` an object. Flat, a repeated name kept only its
|
|
447
|
+
// last value once a file input was added to the form.
|
|
448
|
+
// The multipart part count is already bounded natively (`maxFields`), so
|
|
449
|
+
// no parameter cap applies on top of it here.
|
|
450
|
+
const fields = parseFormFields(payload.fields, { parameterLimit: Number.POSITIVE_INFINITY })
|
|
472
451
|
const files = payload.files.map(
|
|
473
452
|
(f) =>
|
|
474
453
|
new MultipartFile({
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
+
import { ReamError } from '../errors/ReamError.js'
|
|
2
|
+
import { parse } from '../helpers/bytes.js'
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
+
* Read a size limit (`'1mb'`, `'50 mb'`, `'1.5kb'`, `1024`) into bytes, with
|
|
6
|
+
* the `bytes` grammar every size option of the body parser accepts.
|
|
7
|
+
*
|
|
8
|
+
* Throws on anything it cannot read, or on a negative size. A silent fallback
|
|
9
|
+
* is what turned `limit: '50 mb'` into a 1 MB cap with no word: a limit the
|
|
10
|
+
* application did not set is worse than a boot that refuses to start.
|
|
5
11
|
*/
|
|
6
|
-
export function parseSize(size: string): number {
|
|
7
|
-
const
|
|
8
|
-
if (!
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return num * 1024
|
|
14
|
-
case 'mb':
|
|
15
|
-
return num * 1024 * 1024
|
|
16
|
-
case 'gb':
|
|
17
|
-
return num * 1024 * 1024 * 1024
|
|
18
|
-
default:
|
|
19
|
-
return num
|
|
12
|
+
export function parseSize(size: string | number): number {
|
|
13
|
+
const bytes = parse(size)
|
|
14
|
+
if (bytes === null || !Number.isFinite(bytes) || bytes < 0) {
|
|
15
|
+
throw new ReamError(
|
|
16
|
+
'E_BODYPARSER_INVALID_SIZE',
|
|
17
|
+
`Cannot read the size ${JSON.stringify(size)}: expected a number of bytes or a value such as '512kb', '1.5mb' or '2 gb'.`,
|
|
18
|
+
)
|
|
20
19
|
}
|
|
20
|
+
return bytes
|
|
21
21
|
}
|
|
@@ -206,29 +206,44 @@ export function decodeFormComponent(value: string): string {
|
|
|
206
206
|
export function parseQueryString(
|
|
207
207
|
body: string,
|
|
208
208
|
options: QsParseOptions = {},
|
|
209
|
+
): Record<string, unknown> {
|
|
210
|
+
if (!body) return {}
|
|
211
|
+
const parameterLimit = options.parameterLimit ?? DEFAULT_PARAMETER_LIMIT
|
|
212
|
+
const fields: Array<{ name: string; value: string }> = []
|
|
213
|
+
for (const pair of body.split('&')) {
|
|
214
|
+
if (!pair) continue
|
|
215
|
+
// Nothing past the limit is even decoded.
|
|
216
|
+
if (fields.length >= parameterLimit) break
|
|
217
|
+
const eqIdx = pair.indexOf('=')
|
|
218
|
+
const rawKey = eqIdx === -1 ? pair : pair.slice(0, eqIdx)
|
|
219
|
+
const rawValue = eqIdx === -1 ? '' : pair.slice(eqIdx + 1)
|
|
220
|
+
fields.push({ name: decodeFormComponent(rawKey), value: decodeFormComponent(rawValue) })
|
|
221
|
+
}
|
|
222
|
+
return parseFormFields(fields, options)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Nest form fields that arrive as decoded name/value pairs — a multipart
|
|
227
|
+
* body's text parts — with the key grammar and the guards of a urlencoded
|
|
228
|
+
* body. A `<select multiple name="tags[]">` keeps every value, and
|
|
229
|
+
* `user[name]` nests, whether or not the form also carries a file input.
|
|
230
|
+
*/
|
|
231
|
+
export function parseFormFields(
|
|
232
|
+
fields: Iterable<{ name: string; value: string }>,
|
|
233
|
+
options: QsParseOptions = {},
|
|
209
234
|
): Record<string, unknown> {
|
|
210
235
|
const depth = options.depth ?? DEFAULT_DEPTH
|
|
211
236
|
const parameterLimit = options.parameterLimit ?? DEFAULT_PARAMETER_LIMIT
|
|
212
237
|
const arrayLimit = options.arrayLimit ?? DEFAULT_ARRAY_LIMIT
|
|
213
238
|
const result: Record<string, unknown> = {}
|
|
214
|
-
if (!body) return result
|
|
215
239
|
|
|
216
|
-
const pairs = body.split('&')
|
|
217
240
|
let seen = 0
|
|
218
|
-
for (const
|
|
219
|
-
if (!pair) continue
|
|
241
|
+
for (const field of fields) {
|
|
220
242
|
if (++seen > parameterLimit) break
|
|
221
|
-
|
|
222
|
-
const eqIdx = pair.indexOf('=')
|
|
223
|
-
const rawKey = eqIdx === -1 ? pair : pair.slice(0, eqIdx)
|
|
224
|
-
const rawValue = eqIdx === -1 ? '' : pair.slice(eqIdx + 1)
|
|
225
|
-
|
|
226
|
-
const key = decodeFormComponent(rawKey)
|
|
227
|
-
let value: unknown = decodeFormComponent(rawValue)
|
|
243
|
+
let value: unknown = field.value
|
|
228
244
|
if (options.trimWhitespaces && typeof value === 'string') value = value.trim()
|
|
229
245
|
if (options.convertEmptyStringsToNull && value === '') value = null
|
|
230
|
-
|
|
231
|
-
assign(result, splitKey(key, depth, options.allowDots ?? true), value, arrayLimit)
|
|
246
|
+
assign(result, splitKey(field.name, depth, options.allowDots ?? true), value, arrayLimit)
|
|
232
247
|
}
|
|
233
248
|
return result
|
|
234
249
|
}
|
package/src/console/prompts.ts
CHANGED
|
@@ -513,7 +513,9 @@ export class Prompt {
|
|
|
513
513
|
rl.question(query, (answer) => {
|
|
514
514
|
if (masked) stdout.write('\n')
|
|
515
515
|
rl.close()
|
|
516
|
-
|
|
516
|
+
// A secret is returned as typed: a password may begin or end with a
|
|
517
|
+
// space, and trimming it would store or compare a different one.
|
|
518
|
+
resolve(masked ? answer : answer.trim())
|
|
517
519
|
})
|
|
518
520
|
})
|
|
519
521
|
}
|
|
@@ -30,10 +30,23 @@ import type { Binding, ServiceFactory, ServiceToken } from './types.js'
|
|
|
30
30
|
*/
|
|
31
31
|
const RESERVED_TOKEN_NAMES = new Set(['__proto__', 'constructor', 'prototype'])
|
|
32
32
|
|
|
33
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Where one resolution stands: the tokens above it (a list for the message, a
|
|
35
|
+
* set for O(1) membership), and what it carries down.
|
|
36
|
+
*
|
|
37
|
+
* **Never mutated once in the async context.** Every resolution, and every
|
|
38
|
+
* construction that changes the parent, runs on its OWN copy
|
|
39
|
+
* (`#chain.run({ ...chain, ... })`). The chain used to be one object pushed to
|
|
40
|
+
* and popped from in place, and a chain opened around a whole body
|
|
41
|
+
* (`container.call()` around a command's `run()`, `resolveFor`,
|
|
42
|
+
* `runWithScopedValues`) was then shared by every concurrent task inside it:
|
|
43
|
+
* two tasks asking for the same singleton at once read as `catalog → catalog`,
|
|
44
|
+
* and one task's class could stand as another's parent for a contextual
|
|
45
|
+
* binding. A copy per step costs an array and a set the depth of the chain.
|
|
46
|
+
*/
|
|
34
47
|
interface ResolutionChain {
|
|
35
|
-
stack: ServiceToken[]
|
|
36
|
-
set:
|
|
48
|
+
readonly stack: readonly ServiceToken[]
|
|
49
|
+
readonly set: ReadonlySet<ServiceToken>
|
|
37
50
|
/**
|
|
38
51
|
* Values bound on the {@link ContainerResolver} that opened this chain.
|
|
39
52
|
*
|
|
@@ -51,8 +64,13 @@ interface ResolutionChain {
|
|
|
51
64
|
* `HttpContext`. Comparing this counter before and after a build says whether
|
|
52
65
|
* it actually read one, so a singleton that touched no request state still
|
|
53
66
|
* caches exactly as before.
|
|
67
|
+
*
|
|
68
|
+
* A box, the one mutable part, so the copies a chain is walked on all count
|
|
69
|
+
* into it: a read three dependencies down is a read by the singleton being
|
|
70
|
+
* built at the top. Concurrent siblings count into it too, which can only
|
|
71
|
+
* keep a singleton out of the cache, never put a request's into it.
|
|
54
72
|
*/
|
|
55
|
-
scopedReads
|
|
73
|
+
readonly scopedReads: { count: number }
|
|
56
74
|
/**
|
|
57
75
|
* The class whose dependencies are being resolved right now.
|
|
58
76
|
*
|
|
@@ -60,14 +78,19 @@ interface ResolutionChain {
|
|
|
60
78
|
* this", so the lookup needs the direct dependent. Carried on the chain for
|
|
61
79
|
* the same reason the scoped values are: `#autoConstruct` knows it, and
|
|
62
80
|
* every method between there and the lookup would otherwise have to pass it
|
|
63
|
-
* down.
|
|
81
|
+
* down. Each construction runs on a copy that names it, so a dependency's own
|
|
64
82
|
* dependencies see IT as their parent, not its parent.
|
|
65
83
|
*/
|
|
66
|
-
parent?: abstract new (
|
|
84
|
+
readonly parent?: abstract new (
|
|
67
85
|
...args: never[]
|
|
68
86
|
) => unknown
|
|
69
87
|
}
|
|
70
88
|
|
|
89
|
+
/** A chain with nothing in it yet. */
|
|
90
|
+
function openChain(extra: Partial<ResolutionChain> = {}): ResolutionChain {
|
|
91
|
+
return { stack: [], set: new Set(), scopedReads: { count: 0 }, ...extra }
|
|
92
|
+
}
|
|
93
|
+
|
|
71
94
|
export class Container {
|
|
72
95
|
#bindings: Map<ServiceToken, Binding> = new Map()
|
|
73
96
|
#singletons: Map<ServiceToken, unknown> = new Map()
|
|
@@ -320,9 +343,7 @@ export class Container {
|
|
|
320
343
|
// it and nothing outside it does.
|
|
321
344
|
const chain = this.#chain.getStore()
|
|
322
345
|
if (chain === undefined) {
|
|
323
|
-
return this.#chain.run(
|
|
324
|
-
this.resolve<T>(token, runtimeValues),
|
|
325
|
-
)
|
|
346
|
+
return this.#chain.run(openChain(), () => this.resolve<T>(token, runtimeValues))
|
|
326
347
|
}
|
|
327
348
|
|
|
328
349
|
if (chain.set.has(key)) {
|
|
@@ -332,15 +353,14 @@ export class Container {
|
|
|
332
353
|
context: { chain: cycle },
|
|
333
354
|
})
|
|
334
355
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
chain.stack.pop()
|
|
342
|
-
chain.set.delete(key)
|
|
356
|
+
// On a copy: see `ResolutionChain`. What this resolution triggers sees the
|
|
357
|
+
// key above it; a sibling running at the same time does not.
|
|
358
|
+
const own: ResolutionChain = {
|
|
359
|
+
...chain,
|
|
360
|
+
stack: [...chain.stack, key],
|
|
361
|
+
set: new Set(chain.set).add(key),
|
|
343
362
|
}
|
|
363
|
+
return this.#chain.run(own, () => this.#resolveInner<T>(key, token, runtimeValues))
|
|
344
364
|
}
|
|
345
365
|
|
|
346
366
|
/**
|
|
@@ -364,52 +384,17 @@ export class Container {
|
|
|
364
384
|
// its own with nobody in it.
|
|
365
385
|
const chain = this.#chain.getStore()
|
|
366
386
|
if (chain === undefined) {
|
|
367
|
-
return this.#chain.run({
|
|
387
|
+
return this.#chain.run(openChain({ parent: target }), () =>
|
|
368
388
|
this.call(instance, method, runtimeValues),
|
|
369
389
|
)
|
|
370
390
|
}
|
|
371
|
-
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
const args:
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const paramTypes: unknown[] =
|
|
379
|
-
Reflect.getMetadata('design:paramtypes', target.prototype, method) ?? []
|
|
380
|
-
// `@Inject('token')` on a method parameter — a named binding the reflected
|
|
381
|
-
// type cannot express (an interface, or a class registered under a name).
|
|
382
|
-
const injectTokens = getMethodInjectTokens(target.prototype, method)
|
|
383
|
-
|
|
384
|
-
// `@Inject` counts towards the parameter span too: a method may carry named
|
|
385
|
-
// tokens without `design:paramtypes` being emitted for it, and computing the
|
|
386
|
-
// span from the reflected types alone left the loop at zero iterations —
|
|
387
|
-
// the tokens were read and then never used.
|
|
388
|
-
const injectSpan = injectTokens.size === 0 ? 0 : Math.max(...injectTokens.keys()) + 1
|
|
389
|
-
const len = Math.max(paramTypes.length, runtimeValues?.length ?? 0, injectSpan)
|
|
390
|
-
// Sequential resolution keeps the shared cycle-detection stack consistent.
|
|
391
|
-
for (let index = 0; index < len; index += 1) {
|
|
392
|
-
// Runtime values take precedence (and fill slots beyond paramTypes).
|
|
393
|
-
if (runtimeValues && index < runtimeValues.length) {
|
|
394
|
-
args.push(runtimeValues[index])
|
|
395
|
-
continue
|
|
396
|
-
}
|
|
397
|
-
const named = injectTokens.get(index)
|
|
398
|
-
if (named !== undefined) {
|
|
399
|
-
args.push(await this.resolve(named))
|
|
400
|
-
continue
|
|
401
|
-
}
|
|
402
|
-
const type = paramTypes[index]
|
|
403
|
-
args.push(isInjectableClass(type) ? await this.resolve(type) : undefined)
|
|
404
|
-
}
|
|
405
|
-
} finally {
|
|
406
|
-
// RESTORED whatever happened. Without the `finally`, a parameter that
|
|
407
|
-
// failed to resolve left the called class installed as the parent, so a
|
|
408
|
-
// caller that caught the error saw its contextual bindings applied to
|
|
409
|
-
// every later resolution — a binding meant for one handler leaking into
|
|
410
|
-
// the whole application.
|
|
411
|
-
chain.parent = outerParent
|
|
412
|
-
}
|
|
391
|
+
// The parameters are resolved with `target` as the parent, on a copy of
|
|
392
|
+
// the chain (see `ResolutionChain`), so nothing outside them sees it: not
|
|
393
|
+
// the caller once they are resolved, whatever happened, and not a task
|
|
394
|
+
// running beside them. The method itself runs on the caller's chain.
|
|
395
|
+
const args = await this.#chain.run({ ...chain, parent: target }, () =>
|
|
396
|
+
this.#methodArgs(target, method, runtimeValues),
|
|
397
|
+
)
|
|
413
398
|
|
|
414
399
|
const member: unknown = instance[method]
|
|
415
400
|
if (!isCallable(member)) {
|
|
@@ -418,6 +403,44 @@ export class Container {
|
|
|
418
403
|
return member.apply(instance, args)
|
|
419
404
|
}
|
|
420
405
|
|
|
406
|
+
/** The arguments `call()` passes to `target.prototype[method]`. */
|
|
407
|
+
async #methodArgs(
|
|
408
|
+
target: new (...args: never[]) => unknown,
|
|
409
|
+
method: string,
|
|
410
|
+
runtimeValues?: unknown[],
|
|
411
|
+
): Promise<unknown[]> {
|
|
412
|
+
const args: unknown[] = []
|
|
413
|
+
const paramTypes: unknown[] =
|
|
414
|
+
Reflect.getMetadata('design:paramtypes', target.prototype, method) ?? []
|
|
415
|
+
// `@Inject('token')` on a method parameter — a named binding the reflected
|
|
416
|
+
// type cannot express (an interface, or a class registered under a name).
|
|
417
|
+
const injectTokens = getMethodInjectTokens(target.prototype, method)
|
|
418
|
+
|
|
419
|
+
// `@Inject` counts towards the parameter span too: a method may carry named
|
|
420
|
+
// tokens without `design:paramtypes` being emitted for it, and computing the
|
|
421
|
+
// span from the reflected types alone left the loop at zero iterations —
|
|
422
|
+
// the tokens were read and then never used.
|
|
423
|
+
const injectSpan = injectTokens.size === 0 ? 0 : Math.max(...injectTokens.keys()) + 1
|
|
424
|
+
const len = Math.max(paramTypes.length, runtimeValues?.length ?? 0, injectSpan)
|
|
425
|
+
// One at a time, in parameter order: a factory with side effects runs
|
|
426
|
+
// in the order the signature reads.
|
|
427
|
+
for (let index = 0; index < len; index += 1) {
|
|
428
|
+
// Runtime values take precedence (and fill slots beyond paramTypes).
|
|
429
|
+
if (runtimeValues && index < runtimeValues.length) {
|
|
430
|
+
args.push(runtimeValues[index])
|
|
431
|
+
continue
|
|
432
|
+
}
|
|
433
|
+
const named = injectTokens.get(index)
|
|
434
|
+
if (named !== undefined) {
|
|
435
|
+
args.push(await this.resolve(named))
|
|
436
|
+
continue
|
|
437
|
+
}
|
|
438
|
+
const type = paramTypes[index]
|
|
439
|
+
args.push(isInjectableClass(type) ? await this.resolve(type) : undefined)
|
|
440
|
+
}
|
|
441
|
+
return args
|
|
442
|
+
}
|
|
443
|
+
|
|
421
444
|
/**
|
|
422
445
|
* A resolver whose {@link ContainerResolver.bindValue} values are isolated
|
|
423
446
|
* from this container (AdonisJS `container.createResolver()`).
|
|
@@ -441,10 +464,7 @@ export class Container {
|
|
|
441
464
|
fn: () => Promise<T>,
|
|
442
465
|
): Promise<T> {
|
|
443
466
|
const chain = this.#chain.getStore()
|
|
444
|
-
return this.#chain.run(
|
|
445
|
-
chain === undefined ? { stack: [], set: new Set(), values } : { ...chain, values },
|
|
446
|
-
fn,
|
|
447
|
-
)
|
|
467
|
+
return this.#chain.run(chain === undefined ? openChain({ values }) : { ...chain, values }, fn)
|
|
448
468
|
}
|
|
449
469
|
|
|
450
470
|
/** @internal Validate a token and hand back the value the container keys on. */
|
|
@@ -464,9 +484,8 @@ export class Container {
|
|
|
464
484
|
): Promise<T> {
|
|
465
485
|
if (parent === null) return this.make<T>(token, runtimeValues)
|
|
466
486
|
const chain = this.#chain.getStore()
|
|
467
|
-
return this.#chain.run(
|
|
468
|
-
|
|
469
|
-
() => this.make<T>(token, runtimeValues),
|
|
487
|
+
return this.#chain.run(chain === undefined ? openChain({ parent }) : { ...chain, parent }, () =>
|
|
488
|
+
this.make<T>(token, runtimeValues),
|
|
470
489
|
)
|
|
471
490
|
}
|
|
472
491
|
|
|
@@ -588,7 +607,7 @@ export class Container {
|
|
|
588
607
|
// resolves in (`resolveFor`: swaps, resolver values, container values).
|
|
589
608
|
// No `resolving` hooks: the value was not constructed here.
|
|
590
609
|
if (store?.values?.has(key)) {
|
|
591
|
-
store.scopedReads
|
|
610
|
+
store.scopedReads.count += 1
|
|
592
611
|
return store.values.get(key) as T
|
|
593
612
|
}
|
|
594
613
|
|
|
@@ -619,7 +638,7 @@ export class Container {
|
|
|
619
638
|
if (this.#wasPublished(key, shared)) return shared as T
|
|
620
639
|
}
|
|
621
640
|
|
|
622
|
-
const readsBefore = store?.scopedReads ?? 0
|
|
641
|
+
const readsBefore = store?.scopedReads.count ?? 0
|
|
623
642
|
const building = (async (): Promise<unknown> => {
|
|
624
643
|
const instance = binding.factory ? await binding.factory(this) : undefined
|
|
625
644
|
// Only cache while this is still the binding that started the build.
|
|
@@ -731,7 +750,7 @@ export class Container {
|
|
|
731
750
|
|
|
732
751
|
// Same rule as the explicit bindings: see `ResolutionChain.scopedReads`.
|
|
733
752
|
const store = this.#chain.getStore()
|
|
734
|
-
const readsBefore = store?.scopedReads ?? 0
|
|
753
|
+
const readsBefore = store?.scopedReads.count ?? 0
|
|
735
754
|
const building = (async (): Promise<unknown> => {
|
|
736
755
|
const instance = await this.#autoConstruct(target, runtimeValues)
|
|
737
756
|
await this.#runResolvingHooks(key, instance)
|
|
@@ -754,16 +773,13 @@ export class Container {
|
|
|
754
773
|
runtimeValues?: unknown[],
|
|
755
774
|
): Promise<unknown> {
|
|
756
775
|
// Everything resolved inside is a dependency OF `target`, which is what a
|
|
757
|
-
// contextual binding keys on.
|
|
758
|
-
//
|
|
759
|
-
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
} finally {
|
|
765
|
-
if (store !== undefined) store.parent = outerParent
|
|
766
|
-
}
|
|
776
|
+
// contextual binding keys on. On a copy of the chain (see
|
|
777
|
+
// `ResolutionChain`): nothing to restore on any of the construction's
|
|
778
|
+
// exits, and a construction running beside this one keeps its own parent.
|
|
779
|
+
const store = this.#chain.getStore() ?? openChain()
|
|
780
|
+
return this.#chain.run({ ...store, parent: target }, () =>
|
|
781
|
+
this.#constructWithDeps(target, runtimeValues),
|
|
782
|
+
)
|
|
767
783
|
}
|
|
768
784
|
|
|
769
785
|
async #constructWithDeps(
|
|
@@ -902,7 +918,7 @@ export class Container {
|
|
|
902
918
|
readsBefore: number,
|
|
903
919
|
): void {
|
|
904
920
|
if (scope !== 'singleton') return
|
|
905
|
-
if ((store?.scopedReads ?? 0) !== readsBefore) return
|
|
921
|
+
if ((store?.scopedReads.count ?? 0) !== readsBefore) return
|
|
906
922
|
this.#singletons.set(key, instance)
|
|
907
923
|
}
|
|
908
924
|
|