@c9up/ream 0.2.27 → 0.2.29
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 +1 -0
- package/dist/Codemods.d.ts +16 -0
- package/dist/Codemods.d.ts.map +1 -1
- package/dist/Codemods.js +182 -0
- package/dist/Codemods.js.map +1 -1
- package/dist/Ignitor.d.ts +11 -1
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +27 -21
- package/dist/Ignitor.js.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.d.ts.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.js +3 -0
- package/dist/bodyparser/BodyParserMiddleware.js.map +1 -1
- package/dist/bodyparser/qsParse.d.ts +10 -1
- package/dist/bodyparser/qsParse.d.ts.map +1 -1
- package/dist/bodyparser/qsParse.js +53 -15
- package/dist/bodyparser/qsParse.js.map +1 -1
- package/dist/codemods/source.d.ts +57 -0
- package/dist/codemods/source.d.ts.map +1 -0
- package/dist/codemods/source.js +282 -0
- package/dist/codemods/source.js.map +1 -0
- package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
- package/dist/graphql/GraphQLEngine.js +3 -2
- package/dist/graphql/GraphQLEngine.js.map +1 -1
- package/dist/http/Exception.d.ts +14 -1
- package/dist/http/Exception.d.ts.map +1 -1
- package/dist/http/Exception.js +10 -2
- package/dist/http/Exception.js.map +1 -1
- package/dist/http/HttpContext.d.ts +11 -0
- package/dist/http/HttpContext.d.ts.map +1 -1
- package/dist/http/HttpContext.js +2 -1
- package/dist/http/HttpContext.js.map +1 -1
- package/dist/http/RedirectBuilder.d.ts +9 -3
- package/dist/http/RedirectBuilder.d.ts.map +1 -1
- package/dist/http/RedirectBuilder.js +39 -37
- package/dist/http/RedirectBuilder.js.map +1 -1
- package/dist/http/Request.d.ts +42 -17
- package/dist/http/Request.d.ts.map +1 -1
- package/dist/http/Request.js +65 -29
- package/dist/http/Request.js.map +1 -1
- package/dist/http/SseStream.js +6 -7
- package/dist/http/SseStream.js.map +1 -1
- package/dist/http/permissions.d.ts +19 -0
- package/dist/http/permissions.d.ts.map +1 -0
- package/dist/http/permissions.js +28 -0
- package/dist/http/permissions.js.map +1 -0
- package/dist/http-client/HttpClient.d.ts +111 -0
- package/dist/http-client/HttpClient.d.ts.map +1 -0
- package/dist/http-client/HttpClient.js +386 -0
- package/dist/http-client/HttpClient.js.map +1 -0
- package/dist/http-client/addresses.d.ts +14 -0
- package/dist/http-client/addresses.d.ts.map +1 -0
- package/dist/http-client/addresses.js +57 -0
- package/dist/http-client/addresses.js.map +1 -0
- package/dist/http-client/fake.d.ts +50 -0
- package/dist/http-client/fake.d.ts.map +1 -0
- package/dist/http-client/fake.js +95 -0
- package/dist/http-client/fake.js.map +1 -0
- package/dist/http-client/index.d.ts +7 -0
- package/dist/http-client/index.d.ts.map +1 -0
- package/dist/http-client/index.js +7 -0
- package/dist/http-client/index.js.map +1 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/middleware/Pipeline.d.ts.map +1 -1
- package/dist/middleware/Pipeline.js +6 -3
- package/dist/middleware/Pipeline.js.map +1 -1
- package/dist/native/generated.d.ts +108 -3
- package/dist/native/generated.d.ts.map +1 -1
- package/dist/rpc/RpcProvider.d.ts +6 -0
- package/dist/rpc/RpcProvider.d.ts.map +1 -1
- package/dist/rpc/RpcProvider.js +4 -1
- package/dist/rpc/RpcProvider.js.map +1 -1
- package/dist/rpc/RpcRouter.d.ts +27 -14
- package/dist/rpc/RpcRouter.d.ts.map +1 -1
- package/dist/rpc/RpcRouter.js +95 -38
- package/dist/rpc/RpcRouter.js.map +1 -1
- package/dist/scheduler/locks/RedisLockBackend.d.ts.map +1 -1
- package/dist/scheduler/locks/RedisLockBackend.js +13 -2
- package/dist/scheduler/locks/RedisLockBackend.js.map +1 -1
- package/dist/security/CookieSigner.d.ts.map +1 -1
- package/dist/security/CookieSigner.js +17 -3
- package/dist/security/CookieSigner.js.map +1 -1
- package/dist/security/SignedUrl.d.ts.map +1 -1
- package/dist/security/SignedUrl.js +6 -1
- package/dist/security/SignedUrl.js.map +1 -1
- package/dist/server/Server.d.ts +75 -9
- package/dist/server/Server.d.ts.map +1 -1
- package/dist/server/Server.js +60 -9
- package/dist/server/Server.js.map +1 -1
- package/dist/server/nativeGrpc.d.ts +27 -0
- package/dist/server/nativeGrpc.d.ts.map +1 -0
- package/dist/server/nativeGrpc.js +47 -0
- package/dist/server/nativeGrpc.js.map +1 -0
- package/dist/server/nativeLimits.d.ts +49 -0
- package/dist/server/nativeLimits.d.ts.map +1 -0
- package/dist/server/nativeLimits.js +67 -0
- package/dist/server/nativeLimits.js.map +1 -0
- package/dist/session/Session.d.ts +45 -0
- package/dist/session/Session.d.ts.map +1 -1
- package/dist/session/Session.js +31 -2
- package/dist/session/Session.js.map +1 -1
- package/dist/session/SessionMiddleware.d.ts.map +1 -1
- package/dist/session/SessionMiddleware.js +52 -13
- package/dist/session/SessionMiddleware.js.map +1 -1
- package/dist/session/config.d.ts +5 -0
- package/dist/session/config.d.ts.map +1 -1
- package/dist/session/config.js +8 -0
- package/dist/session/config.js.map +1 -1
- package/dist/session/drivers/CookieDriver.d.ts +12 -3
- package/dist/session/drivers/CookieDriver.d.ts.map +1 -1
- package/dist/session/drivers/CookieDriver.js +20 -6
- package/dist/session/drivers/CookieDriver.js.map +1 -1
- package/dist/session/drivers/DatabaseDriver.d.ts +2 -0
- package/dist/session/drivers/DatabaseDriver.d.ts.map +1 -1
- package/dist/session/drivers/DatabaseDriver.js +6 -0
- package/dist/session/drivers/DatabaseDriver.js.map +1 -1
- package/dist/session/drivers/FileDriver.d.ts +8 -0
- package/dist/session/drivers/FileDriver.d.ts.map +1 -1
- package/dist/session/drivers/FileDriver.js +85 -11
- package/dist/session/drivers/FileDriver.js.map +1 -1
- package/dist/session/drivers/MemoryDriver.d.ts +2 -0
- package/dist/session/drivers/MemoryDriver.d.ts.map +1 -1
- package/dist/session/drivers/MemoryDriver.js +21 -5
- package/dist/session/drivers/MemoryDriver.js.map +1 -1
- package/dist/session/drivers/RedisDriver.d.ts +2 -0
- package/dist/session/drivers/RedisDriver.d.ts.map +1 -1
- package/dist/session/drivers/RedisDriver.js +6 -0
- package/dist/session/drivers/RedisDriver.js.map +1 -1
- package/dist/session/errors.d.ts +27 -0
- package/dist/session/errors.d.ts.map +1 -1
- package/dist/session/errors.js +33 -0
- package/dist/session/errors.js.map +1 -1
- package/dist/session/templateTags.d.ts +14 -0
- package/dist/session/templateTags.d.ts.map +1 -1
- package/dist/session/templateTags.js +45 -17
- package/dist/session/templateTags.js.map +1 -1
- package/dist/storage/StaticMiddleware.d.ts.map +1 -1
- package/dist/storage/StaticMiddleware.js +29 -18
- package/dist/storage/StaticMiddleware.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 +11 -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 +265 -0
- package/src/Ignitor.ts +48 -20
- package/src/bodyparser/BodyParserMiddleware.ts +3 -0
- package/src/bodyparser/qsParse.ts +69 -14
- package/src/codemods/source.ts +303 -0
- package/src/graphql/GraphQLEngine.ts +3 -2
- package/src/http/Exception.ts +17 -2
- package/src/http/HttpContext.ts +14 -1
- package/src/http/RedirectBuilder.ts +48 -37
- package/src/http/Request.ts +87 -29
- package/src/http/SseStream.ts +6 -7
- package/src/http/permissions.ts +31 -0
- package/src/http-client/HttpClient.ts +505 -0
- package/src/http-client/addresses.ts +58 -0
- package/src/http-client/fake.ts +141 -0
- package/src/http-client/index.ts +21 -0
- package/src/index.ts +9 -1
- package/src/middleware/Pipeline.ts +7 -3
- package/src/native/generated.ts +123 -3
- package/src/rpc/RpcProvider.ts +10 -1
- package/src/rpc/RpcRouter.ts +126 -42
- package/src/scheduler/locks/RedisLockBackend.ts +12 -4
- package/src/security/CookieSigner.ts +17 -3
- package/src/security/SignedUrl.ts +6 -1
- package/src/server/Server.ts +103 -9
- package/src/server/nativeGrpc.ts +59 -0
- package/src/server/nativeLimits.ts +94 -0
- package/src/session/Session.ts +72 -2
- package/src/session/SessionMiddleware.ts +53 -14
- package/src/session/config.ts +7 -0
- package/src/session/drivers/CookieDriver.ts +21 -6
- package/src/session/drivers/DatabaseDriver.ts +10 -0
- package/src/session/drivers/FileDriver.ts +91 -11
- package/src/session/drivers/MemoryDriver.ts +20 -5
- package/src/session/drivers/RedisDriver.ts +7 -0
- package/src/session/errors.ts +48 -0
- package/src/session/templateTags.ts +57 -0
- package/src/storage/StaticMiddleware.ts +28 -18
package/src/Codemods.ts
CHANGED
|
@@ -1,9 +1,36 @@
|
|
|
1
1
|
import * as fs from 'node:fs'
|
|
2
2
|
import * as path from 'node:path'
|
|
3
|
+
import {
|
|
4
|
+
appendToLiteral,
|
|
5
|
+
findCall,
|
|
6
|
+
findExportDefaultObject,
|
|
7
|
+
isSingleExpression,
|
|
8
|
+
matchClose,
|
|
9
|
+
properties,
|
|
10
|
+
segments,
|
|
11
|
+
} from './codemods/source.js'
|
|
3
12
|
import { render as renderTemplate, type StubState } from './stubs/template.js'
|
|
4
13
|
|
|
5
14
|
export type { StubState }
|
|
6
15
|
|
|
16
|
+
/** The environments a preload (or provider) entry can be scoped to. */
|
|
17
|
+
export type PreloadEnvironment = 'web' | 'console' | 'test' | 'repl'
|
|
18
|
+
|
|
19
|
+
const PRELOAD_ENVIRONMENTS: readonly PreloadEnvironment[] = ['web', 'console', 'test', 'repl']
|
|
20
|
+
|
|
21
|
+
/** Validation rules a configure hook adds to `start/env.ts`. */
|
|
22
|
+
export interface EnvValidationNode {
|
|
23
|
+
/** Written once, as a banner comment above the rules this call adds. */
|
|
24
|
+
leadingComment?: string
|
|
25
|
+
/** Variable name → rule source, e.g. `{ REDIS_PORT: 'Env.schema.number()' }`. */
|
|
26
|
+
variables: Record<string, string>
|
|
27
|
+
/**
|
|
28
|
+
* Replace a rule the file already has. Off by default, so a rule the user
|
|
29
|
+
* edited survives a re-run; configure hooks pass their `--force` flag.
|
|
30
|
+
*/
|
|
31
|
+
overwrite?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
7
34
|
export interface Codemods {
|
|
8
35
|
addProvider(importPath: string): Promise<void>
|
|
9
36
|
addMetaFile(pattern: string, reloadServer?: boolean): Promise<void>
|
|
@@ -17,6 +44,8 @@ export interface Codemods {
|
|
|
17
44
|
): Promise<GeneratedStub>
|
|
18
45
|
registerCommand(importPath: string): Promise<void>
|
|
19
46
|
registerMiddleware(importPath: string, options?: { tier?: 'server' | 'router' }): Promise<void>
|
|
47
|
+
defineEnvValidations(definition: EnvValidationNode): Promise<string[]>
|
|
48
|
+
addPreloadFile(modulePath: string, environments?: PreloadEnvironment[]): Promise<void>
|
|
20
49
|
}
|
|
21
50
|
|
|
22
51
|
/** What a stub may interpolate. Scalars only — a stub is text, not a program. */
|
|
@@ -128,6 +157,76 @@ function assertCanonicallyInside(root: string, filePath: string, displayName: st
|
|
|
128
157
|
}
|
|
129
158
|
}
|
|
130
159
|
|
|
160
|
+
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* A module path goes between the quotes of an `import('...')`; anything that
|
|
164
|
+
* could close them, escape or end the line is refused rather than escaped.
|
|
165
|
+
*/
|
|
166
|
+
const SAFE_MODULE_PATH = /^[^'"`\\\r\n\u2028\u2029]+$/
|
|
167
|
+
|
|
168
|
+
const BANNER_RULE = '|----------------------------------------------------------'
|
|
169
|
+
|
|
170
|
+
/** The rules of `definition`, refused before anything is written when unsafe. */
|
|
171
|
+
function assertEnvValidations(definition: EnvValidationNode): void {
|
|
172
|
+
const comment = definition.leadingComment
|
|
173
|
+
if (comment !== undefined && (/[\r\n\u2028\u2029]/.test(comment) || comment.includes('*/'))) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
'[configure] defineEnvValidations: the leading comment must be one line without `*/`',
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
for (const [name, rule] of Object.entries(definition.variables)) {
|
|
179
|
+
if (!ENV_NAME.test(name)) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`[configure] defineEnvValidations: '${name}' is not an environment variable name`,
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
if (typeof rule !== 'string' || !isSingleExpression(rule)) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
`[configure] defineEnvValidations: the rule for ${name} is not one complete expression: ${String(rule)}`,
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The `{` and `}` of the schema object passed to `Env.create`. */
|
|
193
|
+
function locateEnvSchema(content: string): { open: number; close: number } {
|
|
194
|
+
const paren = findCall(content, 'Env.create')
|
|
195
|
+
const parenClose = paren === -1 ? -1 : matchClose(content, paren)
|
|
196
|
+
if (paren === -1 || parenClose === -1) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
'[configure] Could not find an Env.create(...) call in start/env.ts — add the validations by hand.',
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
const open = segments(content, paren + 1, parenClose)[1]?.codeStart ?? -1
|
|
202
|
+
const close = content[open] === '{' ? matchClose(content, open) : -1
|
|
203
|
+
if (close === -1) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
'[configure] The second argument of Env.create in start/env.ts is not an object literal — add the validations by hand.',
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
return { open, close }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** The `{` and `}` of the object `reamrc.ts` exports (through `defineConfig` or bare). */
|
|
212
|
+
function locateRcObject(content: string): { open: number; close: number } | null {
|
|
213
|
+
const paren = findCall(content, 'defineConfig')
|
|
214
|
+
let open = -1
|
|
215
|
+
if (paren !== -1) {
|
|
216
|
+
const parenClose = matchClose(content, paren)
|
|
217
|
+
if (parenClose !== -1) open = segments(content, paren + 1, parenClose)[0]?.codeStart ?? -1
|
|
218
|
+
} else {
|
|
219
|
+
open = findExportDefaultObject(content)
|
|
220
|
+
}
|
|
221
|
+
const close = open !== -1 && content[open] === '{' ? matchClose(content, open) : -1
|
|
222
|
+
return close === -1 ? null : { open, close }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** The module an rc array element lazily imports. */
|
|
226
|
+
function importedModule(element: string): string | undefined {
|
|
227
|
+
return /import\s*\(\s*(['"])([^'"]*)\1\s*\)/.exec(element)?.[2]
|
|
228
|
+
}
|
|
229
|
+
|
|
131
230
|
export function createCodemods(options?: { force?: boolean; cwd?: string }): Codemods {
|
|
132
231
|
const force = options?.force ?? false
|
|
133
232
|
const root = path.resolve(options?.cwd ?? process.cwd())
|
|
@@ -516,5 +615,171 @@ export function createCodemods(options?: { force?: boolean; cwd?: string }): Cod
|
|
|
516
615
|
content = `${content.slice(0, targetBlock.start)}${eol}${entry}${content.slice(targetBlock.start)}`
|
|
517
616
|
fs.writeFileSync(kernelPath, content)
|
|
518
617
|
},
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Add validation rules to the schema `start/env.ts` passes to
|
|
621
|
+
* `Env.create`, so the variables a package writes to `.env` are checked
|
|
622
|
+
* at boot like the app's own.
|
|
623
|
+
*
|
|
624
|
+
* A rule already in the file is left alone, unless `overwrite` is set:
|
|
625
|
+
* the user may have tightened it. Returns the variables kept that way
|
|
626
|
+
* with a rule different from the one asked for, so the caller can say
|
|
627
|
+
* so. The banner comment is written only when the first new rule comes
|
|
628
|
+
* before any variable the file already had.
|
|
629
|
+
*/
|
|
630
|
+
async defineEnvValidations(definition: EnvValidationNode): Promise<string[]> {
|
|
631
|
+
const envPath = path.join(root, 'start', 'env.ts')
|
|
632
|
+
if (!fs.existsSync(envPath)) {
|
|
633
|
+
throw new Error(
|
|
634
|
+
`[configure] start/env.ts not found — cannot add the validations for ${Object.keys(definition.variables).join(', ')}.`,
|
|
635
|
+
)
|
|
636
|
+
}
|
|
637
|
+
assertCanonicallyInside(root, envPath, 'start/env.ts')
|
|
638
|
+
assertEnvValidations(definition)
|
|
639
|
+
|
|
640
|
+
const original = fs.readFileSync(envPath, 'utf8')
|
|
641
|
+
const eol = detectLineEnding(original)
|
|
642
|
+
let content = original
|
|
643
|
+
const schema = locateEnvSchema(content)
|
|
644
|
+
const existing = new Map(
|
|
645
|
+
properties(content, schema.open, schema.close).map((property) => [property.key, property]),
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
const kept: string[] = []
|
|
649
|
+
const replacements: Array<{ start: number; end: number; text: string }> = []
|
|
650
|
+
const additions: Array<[string, string]> = []
|
|
651
|
+
let shouldAddComment = true
|
|
652
|
+
let decorate = false
|
|
653
|
+
for (const [name, rule] of Object.entries(definition.variables)) {
|
|
654
|
+
const property = existing.get(name)
|
|
655
|
+
if (property === undefined) {
|
|
656
|
+
if (additions.length === 0) decorate = shouldAddComment
|
|
657
|
+
shouldAddComment = false
|
|
658
|
+
additions.push([name, rule])
|
|
659
|
+
continue
|
|
660
|
+
}
|
|
661
|
+
shouldAddComment = false
|
|
662
|
+
const current = content.slice(property.valueStart, property.valueEnd)
|
|
663
|
+
if (definition.overwrite) {
|
|
664
|
+
replacements.push({ start: property.valueStart, end: property.valueEnd, text: rule })
|
|
665
|
+
} else if (current.replace(/\s+/g, '') !== rule.replace(/\s+/g, '')) {
|
|
666
|
+
kept.push(name)
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
for (const { start, end, text } of replacements.sort((a, b) => b.start - a.start)) {
|
|
671
|
+
content = `${content.slice(0, start)}${text}${content.slice(end)}`
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (additions.length > 0) {
|
|
675
|
+
const { open, close } = locateEnvSchema(content)
|
|
676
|
+
const comment = definition.leadingComment
|
|
677
|
+
content = appendToLiteral(
|
|
678
|
+
content,
|
|
679
|
+
open,
|
|
680
|
+
close,
|
|
681
|
+
eol,
|
|
682
|
+
(indent) => [
|
|
683
|
+
...(decorate && comment !== undefined
|
|
684
|
+
? [
|
|
685
|
+
`${indent}/*`,
|
|
686
|
+
`${indent}${BANNER_RULE}`,
|
|
687
|
+
`${indent}| ${comment}`,
|
|
688
|
+
`${indent}${BANNER_RULE}`,
|
|
689
|
+
`${indent}*/`,
|
|
690
|
+
]
|
|
691
|
+
: []),
|
|
692
|
+
...additions.map(([name, rule]) => `${indent}${name}: ${rule},`),
|
|
693
|
+
],
|
|
694
|
+
decorate,
|
|
695
|
+
)
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (content !== original) fs.writeFileSync(envPath, content)
|
|
699
|
+
return kept
|
|
700
|
+
},
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Register a preload file in `reamrc.ts`, appended to `preloads` (the
|
|
704
|
+
* array is created when absent). Listing only some environments writes
|
|
705
|
+
* a `{ file, environment }` entry; none, or all of them, a plain lazy
|
|
706
|
+
* import. A module already in `preloads` is left as it is.
|
|
707
|
+
*/
|
|
708
|
+
async addPreloadFile(modulePath: string, environments?: PreloadEnvironment[]): Promise<void> {
|
|
709
|
+
if (!SAFE_MODULE_PATH.test(modulePath)) {
|
|
710
|
+
throw new Error(`[configure] Unsafe preload module path: ${JSON.stringify(modulePath)}`)
|
|
711
|
+
}
|
|
712
|
+
for (const environment of environments ?? []) {
|
|
713
|
+
if (!PRELOAD_ENVIRONMENTS.includes(environment)) {
|
|
714
|
+
throw new Error(
|
|
715
|
+
`[configure] Unknown environment '${String(environment)}' for preload ${modulePath}. Expected one of ${PRELOAD_ENVIRONMENTS.join(', ')}.`,
|
|
716
|
+
)
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const rcPath = path.join(root, 'reamrc.ts')
|
|
721
|
+
if (!fs.existsSync(rcPath)) {
|
|
722
|
+
throw new Error(
|
|
723
|
+
`[configure] reamrc.ts not found — cannot register preload ${modulePath}. Run 'ream new' first.`,
|
|
724
|
+
)
|
|
725
|
+
}
|
|
726
|
+
assertCanonicallyInside(root, rcPath, 'reamrc.ts')
|
|
727
|
+
|
|
728
|
+
const content = fs.readFileSync(rcPath, 'utf8')
|
|
729
|
+
const eol = detectLineEnding(content)
|
|
730
|
+
const config = locateRcObject(content)
|
|
731
|
+
if (config === null) {
|
|
732
|
+
throw new Error(
|
|
733
|
+
`[configure] Could not find 'defineConfig({ ... })' in reamrc.ts — preload ${modulePath} not added.`,
|
|
734
|
+
)
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Scoped only when it actually leaves an environment out.
|
|
738
|
+
const scope = PRELOAD_ENVIRONMENTS.some(
|
|
739
|
+
(environment) => environments !== undefined && !environments.includes(environment),
|
|
740
|
+
)
|
|
741
|
+
? environments
|
|
742
|
+
: undefined
|
|
743
|
+
const entry = (indent: string): string[] =>
|
|
744
|
+
scope
|
|
745
|
+
? [
|
|
746
|
+
`${indent}{`,
|
|
747
|
+
`${indent} file: () => import('${modulePath}'),`,
|
|
748
|
+
`${indent} environment: [${scope.map((e) => `'${e}'`).join(', ')}],`,
|
|
749
|
+
`${indent}},`,
|
|
750
|
+
]
|
|
751
|
+
: [`${indent}() => import('${modulePath}'),`]
|
|
752
|
+
|
|
753
|
+
const preloads = properties(content, config.open, config.close).find(
|
|
754
|
+
(property) => property.key === 'preloads',
|
|
755
|
+
)
|
|
756
|
+
if (preloads === undefined) {
|
|
757
|
+
fs.writeFileSync(
|
|
758
|
+
rcPath,
|
|
759
|
+
appendToLiteral(content, config.open, config.close, eol, (indent) => [
|
|
760
|
+
`${indent}preloads: [`,
|
|
761
|
+
...entry(`${indent} `),
|
|
762
|
+
`${indent}],`,
|
|
763
|
+
]),
|
|
764
|
+
)
|
|
765
|
+
return
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const arrayClose =
|
|
769
|
+
content[preloads.valueStart] === '[' ? matchClose(content, preloads.valueStart) : -1
|
|
770
|
+
if (arrayClose === -1) {
|
|
771
|
+
throw new Error(
|
|
772
|
+
`[configure] reamrc.ts declares preloads as something other than an array literal — add ${modulePath} by hand.`,
|
|
773
|
+
)
|
|
774
|
+
}
|
|
775
|
+
const registered = segments(content, preloads.valueStart + 1, arrayClose).some(
|
|
776
|
+
(element) => importedModule(content.slice(element.codeStart, element.end)) === modulePath,
|
|
777
|
+
)
|
|
778
|
+
if (registered) return
|
|
779
|
+
fs.writeFileSync(
|
|
780
|
+
rcPath,
|
|
781
|
+
appendToLiteral(content, preloads.valueStart, arrayClose, eol, entry),
|
|
782
|
+
)
|
|
783
|
+
},
|
|
519
784
|
}
|
|
520
785
|
}
|
package/src/Ignitor.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { Application } from './Application.js'
|
|
17
|
+
import type { BodyParserConfig } from './bodyparser/BodyParserMiddleware.js'
|
|
17
18
|
import type { Console } from './console/Console.js'
|
|
18
19
|
import type { CommandLoader, Kernel as ConsoleKernelInstance } from './console/Kernel.js'
|
|
19
20
|
import { type CommandClass, isCommandClass } from './console/types.js'
|
|
@@ -51,13 +52,20 @@ import {
|
|
|
51
52
|
import { Router } from './router/Router.js'
|
|
52
53
|
import { CookieSigner } from './security/CookieSigner.js'
|
|
53
54
|
import { SignedUrl } from './security/SignedUrl.js'
|
|
54
|
-
import {
|
|
55
|
+
import { cancellableGrpc, type NativeGrpcHandler } from './server/nativeGrpc.js'
|
|
56
|
+
import {
|
|
57
|
+
type NativeBodyLimits,
|
|
58
|
+
type NativeConnectionConfig,
|
|
59
|
+
nativeBodyLimits,
|
|
60
|
+
nativeConnectionConfig,
|
|
61
|
+
} from './server/nativeLimits.js'
|
|
62
|
+
import { type GrpcLimits, Server } from './server/Server.js'
|
|
55
63
|
import { clearApp, setApp } from './services/app.js'
|
|
56
64
|
import { clearEncryption, setEncryption } from './services/encryption.js'
|
|
57
65
|
import { clearLogger, setLogger } from './services/logger.js'
|
|
58
66
|
import { clearRouter, setRouter } from './services/router.js'
|
|
59
67
|
import { clearServer, setServer } from './services/server.js'
|
|
60
|
-
import {
|
|
68
|
+
import { installSessionTemplateTags } from './session/templateTags.js'
|
|
61
69
|
|
|
62
70
|
/** Application environment. */
|
|
63
71
|
/**
|
|
@@ -268,6 +276,10 @@ export function defineConfig(config: ReamrcConfig): ReamrcConfig {
|
|
|
268
276
|
/** Minimal interface for the HTTP server (NAPI or mock). */
|
|
269
277
|
export interface HyperServerLike {
|
|
270
278
|
onRequest(callback: (request: HttpKernelRequest) => Promise<HttpKernelResponse>): void
|
|
279
|
+
/** Optional — present on the real `HyperServer`; required once `server.grpc()` is used. */
|
|
280
|
+
onGrpc?(callback: NativeGrpcHandler, limits?: GrpcLimits): void
|
|
281
|
+
/** Optional — reports the calls a client gave up on; without it no call's signal aborts. */
|
|
282
|
+
onGrpcCancel?(callback: (id: number) => void): void
|
|
271
283
|
/**
|
|
272
284
|
* Optional — present on the real `HyperServer` (Rust-backed), absent on
|
|
273
285
|
* trivial test mocks. Ignitor only calls it when a shield config is set.
|
|
@@ -277,6 +289,10 @@ export interface HyperServerLike {
|
|
|
277
289
|
configureTrustedProxies?(cidrs: string[]): void
|
|
278
290
|
/** Optional — same lifecycle as `configureShield`. */
|
|
279
291
|
configureRateLimit?(config: { max: number; windowSecs: number } | null): void
|
|
292
|
+
/** Optional — same lifecycle as `configureShield`. */
|
|
293
|
+
configureBodyLimits?(config: NativeBodyLimits): void
|
|
294
|
+
/** Optional — same lifecycle as `configureShield`. */
|
|
295
|
+
configureConnections?(config: NativeConnectionConfig): void
|
|
280
296
|
listen(): Promise<void>
|
|
281
297
|
port(): Promise<number>
|
|
282
298
|
close(): Promise<void>
|
|
@@ -682,26 +698,12 @@ export class Ignitor {
|
|
|
682
698
|
|
|
683
699
|
/**
|
|
684
700
|
* Give the template engine `@error` / `@errors` / `@inputError` /
|
|
685
|
-
* `@flashMessage`, as AdonisJS's session plugin does
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
* on it, and an app with no template layer simply skips this.
|
|
701
|
+
* `@flashMessage`, as AdonisJS's session plugin does — through the engine
|
|
702
|
+
* package's module singleton, guarded by `usingInker` (see
|
|
703
|
+
* `installSessionTemplateTags`). An app with no template layer skips this.
|
|
689
704
|
*/
|
|
690
705
|
async #registerSessionTemplateTags(): Promise<void> {
|
|
691
|
-
|
|
692
|
-
if (!this.#app.container.has(token)) continue
|
|
693
|
-
const binding: unknown = await this.#app.container.resolve(token)
|
|
694
|
-
for (const candidate of [binding, Reflect.get(Object(binding), '_templates')]) {
|
|
695
|
-
const register = Reflect.get(Object(candidate), 'registerTag')
|
|
696
|
-
if (typeof register !== 'function') continue
|
|
697
|
-
registerSessionTemplateTags({
|
|
698
|
-
registerTag: (tag) => {
|
|
699
|
-
register.call(candidate, tag)
|
|
700
|
-
},
|
|
701
|
-
})
|
|
702
|
-
return
|
|
703
|
-
}
|
|
704
|
-
}
|
|
706
|
+
await installSessionTemplateTags(this.#app.usingInker === true)
|
|
705
707
|
}
|
|
706
708
|
|
|
707
709
|
async #phaseRegister(): Promise<void> {
|
|
@@ -903,6 +905,19 @@ export class Ignitor {
|
|
|
903
905
|
this.#config.host ?? process.env.HOST ?? (this.#app.inProduction ? '0.0.0.0' : 'localhost')
|
|
904
906
|
this.#_httpServer = this.#config.serverFactory(availablePort, this.#host)
|
|
905
907
|
this.#_httpServer.onRequest(kernel)
|
|
908
|
+
const grpc = this.#server.getGrpcHandler()
|
|
909
|
+
if (grpc) {
|
|
910
|
+
if (typeof this.#_httpServer.onGrpc !== 'function') {
|
|
911
|
+
throw new ReamError(
|
|
912
|
+
'E_IGNITOR_GRPC_UNSUPPORTED',
|
|
913
|
+
'A gRPC handler is registered, but this server cannot serve gRPC.',
|
|
914
|
+
{ hint: 'Use the native HyperServer, or a test server that implements onGrpc().' },
|
|
915
|
+
)
|
|
916
|
+
}
|
|
917
|
+
const calls = cancellableGrpc(grpc)
|
|
918
|
+
this.#_httpServer.onGrpcCancel?.(calls.onCancel)
|
|
919
|
+
this.#_httpServer.onGrpc(calls.onCall, this.#server.getGrpcLimits())
|
|
920
|
+
}
|
|
906
921
|
// Pre-resolve client IPs in Rust from the trusted-proxy CIDRs before
|
|
907
922
|
// listen. Security filtering itself lives in @c9up/blackhole.
|
|
908
923
|
const trustedProxies = this.#server.getTrustedProxies()
|
|
@@ -912,6 +927,19 @@ export class Ignitor {
|
|
|
912
927
|
) {
|
|
913
928
|
this.#_httpServer.configureTrustedProxies([...trustedProxies])
|
|
914
929
|
}
|
|
930
|
+
// Body limits from `config/bodyparser.ts`, enforced while the native
|
|
931
|
+
// server reads a body: nothing past them is ever buffered.
|
|
932
|
+
const bodyparser = this.#app.config.get<BodyParserConfig>('bodyparser')
|
|
933
|
+
if (bodyparser && typeof this.#_httpServer.configureBodyLimits === 'function') {
|
|
934
|
+
this.#_httpServer.configureBodyLimits(nativeBodyLimits(bodyparser))
|
|
935
|
+
}
|
|
936
|
+
// AdonisJS `config/app.ts` → `http: { headersTimeout, requestTimeout,
|
|
937
|
+
// keepAliveTimeout }`, plus `maxConnections`.
|
|
938
|
+
if (typeof this.#_httpServer.configureConnections === 'function') {
|
|
939
|
+
this.#_httpServer.configureConnections(
|
|
940
|
+
nativeConnectionConfig(this.#app.config.get<unknown>('app.http')),
|
|
941
|
+
)
|
|
942
|
+
}
|
|
915
943
|
await this.#_httpServer.listen()
|
|
916
944
|
|
|
917
945
|
// Whoever opened the socket closes it. `app.terminate()` runs the
|
|
@@ -136,6 +136,9 @@ export default class BodyParserMiddleware {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
const contentType = ctx.request.header('content-type') ?? ''
|
|
139
|
+
// A body over its native limit (`bodyTooLarge`) was never kept: `raw()`
|
|
140
|
+
// throws the same 413 below instead of handing back an empty string. The
|
|
141
|
+
// route is already matched here, so an unknown URL answered 404 first.
|
|
139
142
|
const rawBody = ctx.request.raw()
|
|
140
143
|
|
|
141
144
|
// Size check. Thrown rather than written: the app's exception handler
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* dependency it can write in a screenful, and the limits below are easier to
|
|
12
12
|
* defend when they are visible.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* Four guards, all of which `qs` also applies:
|
|
15
15
|
*
|
|
16
16
|
* - **Prototype keys are dropped.** `__proto__[x]=1` in a form body is the
|
|
17
17
|
* classic prototype-pollution vector; the key never reaches an object.
|
|
@@ -19,11 +19,14 @@
|
|
|
19
19
|
* as a literal key rather than allocating unbounded nesting.
|
|
20
20
|
* - **Parameter count is capped** (`qs` defaults to 1000), so a body of a
|
|
21
21
|
* million `a[0]`, `a[1]`… cannot be turned into a million-entry object.
|
|
22
|
+
* - **Explicit array indices are capped** (`qs` `arrayLimit`, default 20).
|
|
23
|
+
* A larger index is an object key, so `ids[4294967294]=1` stays one entry.
|
|
22
24
|
*/
|
|
23
25
|
|
|
24
26
|
/** `qs` defaults, reproduced so the behaviour is the same out of the box. */
|
|
25
27
|
export const DEFAULT_DEPTH = 5
|
|
26
28
|
export const DEFAULT_PARAMETER_LIMIT = 1000
|
|
29
|
+
export const DEFAULT_ARRAY_LIMIT = 20
|
|
27
30
|
|
|
28
31
|
export interface QsParseOptions {
|
|
29
32
|
/**
|
|
@@ -34,6 +37,12 @@ export interface QsParseOptions {
|
|
|
34
37
|
allowDots?: boolean
|
|
35
38
|
depth?: number
|
|
36
39
|
parameterLimit?: number
|
|
40
|
+
/**
|
|
41
|
+
* Highest explicit index + 1 that still builds an array (`qs` defaults to
|
|
42
|
+
* 20). `ids[25]=x` becomes `{ ids: { '25': 'x' } }`, so a 16-byte request
|
|
43
|
+
* cannot allocate a 100-million-slot array.
|
|
44
|
+
*/
|
|
45
|
+
arrayLimit?: number
|
|
37
46
|
/** Turn `''` into `null`, as AdonisJS' `convertEmptyStringsToNull` does. */
|
|
38
47
|
convertEmptyStringsToNull?: boolean
|
|
39
48
|
/** Trim surrounding whitespace, as AdonisJS' `trimWhitespaces` does. */
|
|
@@ -77,15 +86,53 @@ function isIndex(segment: string): boolean {
|
|
|
77
86
|
return /^\d+$/.test(segment)
|
|
78
87
|
}
|
|
79
88
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
89
|
+
type Container = Record<string, unknown> | unknown[]
|
|
90
|
+
|
|
91
|
+
function isContainer(value: unknown): value is Container {
|
|
92
|
+
return typeof value === 'object' && value !== null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** `{ '0': a, '1': b }` from `[a, b]`, holes left out — what `qs` produces on overflow. */
|
|
96
|
+
function arrayToObject(array: unknown[]): Record<string, unknown> {
|
|
97
|
+
const object: Record<string, unknown> = {}
|
|
98
|
+
array.forEach((item, index) => {
|
|
99
|
+
object[String(index)] = item
|
|
100
|
+
})
|
|
101
|
+
return object
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Assign `value` at `segments` inside `target`, creating containers as needed.
|
|
106
|
+
*
|
|
107
|
+
* An explicit index at or above `arrayLimit` never addresses an array: the
|
|
108
|
+
* container becomes (or is converted into) an object keyed by that index,
|
|
109
|
+
* as `qs` does. Without it, `ids[100000000]=1` allocates a sparse array whose
|
|
110
|
+
* iteration or serialisation blocks the event loop for seconds. Appends
|
|
111
|
+
* (`tags[]`, a repeated key) stay arrays — each one costs a parameter, so
|
|
112
|
+
* `parameterLimit` already bounds them, and a long checkbox group keeps the
|
|
113
|
+
* shape its handler expects (`qs` turns those into objects past the limit).
|
|
114
|
+
*/
|
|
115
|
+
function assign(
|
|
116
|
+
target: Record<string, unknown>,
|
|
117
|
+
segments: string[],
|
|
118
|
+
value: unknown,
|
|
119
|
+
arrayLimit: number,
|
|
120
|
+
): void {
|
|
121
|
+
let cursor: Container = target
|
|
122
|
+
// Swaps `cursor` inside its parent when an array has to become an object.
|
|
123
|
+
let replace: (next: Container) => void = () => {}
|
|
83
124
|
for (let i = 0; i < segments.length; i++) {
|
|
84
125
|
const segment = segments[i] ?? ''
|
|
85
126
|
const last = i === segments.length - 1
|
|
86
127
|
|
|
87
128
|
if (FORBIDDEN_KEYS.has(segment)) return
|
|
88
129
|
|
|
130
|
+
if (Array.isArray(cursor) && isIndex(segment) && Number(segment) >= arrayLimit) {
|
|
131
|
+
const converted = arrayToObject(cursor)
|
|
132
|
+
replace(converted)
|
|
133
|
+
cursor = converted
|
|
134
|
+
}
|
|
135
|
+
|
|
89
136
|
if (last) {
|
|
90
137
|
if (segment === '') {
|
|
91
138
|
// `tags[]` — append.
|
|
@@ -109,24 +156,31 @@ function assign(target: Record<string, unknown>, segments: string[], value: unkn
|
|
|
109
156
|
}
|
|
110
157
|
|
|
111
158
|
const nextSegment = segments[i + 1] ?? ''
|
|
112
|
-
const wantsArray =
|
|
159
|
+
const wantsArray =
|
|
160
|
+
nextSegment === '' || (isIndex(nextSegment) && Number(nextSegment) < arrayLimit)
|
|
113
161
|
|
|
114
162
|
if (Array.isArray(cursor)) {
|
|
115
163
|
if (!isIndex(segment)) return
|
|
164
|
+
const array: unknown[] = cursor
|
|
116
165
|
const index = Number(segment)
|
|
117
|
-
const existing =
|
|
118
|
-
|
|
119
|
-
|
|
166
|
+
const existing: unknown = array[index]
|
|
167
|
+
const child: Container = isContainer(existing) ? existing : wantsArray ? [] : {}
|
|
168
|
+
array[index] = child
|
|
169
|
+
replace = (next) => {
|
|
170
|
+
array[index] = next
|
|
120
171
|
}
|
|
121
|
-
cursor =
|
|
172
|
+
cursor = child
|
|
122
173
|
continue
|
|
123
174
|
}
|
|
124
175
|
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
176
|
+
const object: Record<string, unknown> = cursor
|
|
177
|
+
const existing: unknown = object[segment]
|
|
178
|
+
const child: Container = isContainer(existing) ? existing : wantsArray ? [] : {}
|
|
179
|
+
object[segment] = child
|
|
180
|
+
replace = (next) => {
|
|
181
|
+
object[segment] = next
|
|
128
182
|
}
|
|
129
|
-
cursor =
|
|
183
|
+
cursor = child
|
|
130
184
|
}
|
|
131
185
|
}
|
|
132
186
|
|
|
@@ -155,6 +209,7 @@ export function parseQueryString(
|
|
|
155
209
|
): Record<string, unknown> {
|
|
156
210
|
const depth = options.depth ?? DEFAULT_DEPTH
|
|
157
211
|
const parameterLimit = options.parameterLimit ?? DEFAULT_PARAMETER_LIMIT
|
|
212
|
+
const arrayLimit = options.arrayLimit ?? DEFAULT_ARRAY_LIMIT
|
|
158
213
|
const result: Record<string, unknown> = {}
|
|
159
214
|
if (!body) return result
|
|
160
215
|
|
|
@@ -173,7 +228,7 @@ export function parseQueryString(
|
|
|
173
228
|
if (options.trimWhitespaces && typeof value === 'string') value = value.trim()
|
|
174
229
|
if (options.convertEmptyStringsToNull && value === '') value = null
|
|
175
230
|
|
|
176
|
-
assign(result, splitKey(key, depth, options.allowDots ?? true), value)
|
|
231
|
+
assign(result, splitKey(key, depth, options.allowDots ?? true), value, arrayLimit)
|
|
177
232
|
}
|
|
178
233
|
return result
|
|
179
234
|
}
|