@c9up/ream 0.2.14 → 0.2.16
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/Application.d.ts.map +1 -1
- package/dist/Application.js +6 -1
- package/dist/Application.js.map +1 -1
- package/dist/Ignitor.d.ts +9 -0
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +29 -3
- package/dist/Ignitor.js.map +1 -1
- package/dist/Provider.d.ts +20 -2
- package/dist/Provider.d.ts.map +1 -1
- package/dist/Provider.js.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.d.ts +1 -1
- package/dist/bodyparser/BodyParserMiddleware.d.ts.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.js +12 -1
- package/dist/bodyparser/BodyParserMiddleware.js.map +1 -1
- package/dist/bodyparser/MultipartFile.d.ts.map +1 -1
- package/dist/bodyparser/MultipartFile.js +7 -3
- package/dist/bodyparser/MultipartFile.js.map +1 -1
- package/dist/bodyparser/config.d.ts +15 -0
- package/dist/bodyparser/config.d.ts.map +1 -0
- package/dist/bodyparser/config.js +16 -0
- package/dist/bodyparser/config.js.map +1 -0
- package/dist/bodyparser/parseSize.d.ts.map +1 -1
- package/dist/bodyparser/parseSize.js +3 -2
- package/dist/bodyparser/parseSize.js.map +1 -1
- package/dist/commands/MigrateBase.d.ts.map +1 -1
- package/dist/commands/MigrateBase.js +4 -1
- package/dist/commands/MigrateBase.js.map +1 -1
- package/dist/config/moduleConfig.d.ts +15 -0
- package/dist/config/moduleConfig.d.ts.map +1 -0
- package/dist/config/moduleConfig.js +24 -0
- package/dist/config/moduleConfig.js.map +1 -0
- package/dist/console/Kernel.d.ts.map +1 -1
- package/dist/console/Kernel.js +7 -0
- package/dist/console/Kernel.js.map +1 -1
- package/dist/console/cliui.d.ts +2 -0
- package/dist/console/cliui.d.ts.map +1 -1
- package/dist/console/cliui.js +2 -1
- package/dist/console/cliui.js.map +1 -1
- package/dist/console/types.d.ts +14 -0
- package/dist/console/types.d.ts.map +1 -1
- package/dist/console/types.js.map +1 -1
- package/dist/container/Container.d.ts.map +1 -1
- package/dist/container/Container.js +6 -1
- package/dist/container/Container.js.map +1 -1
- package/dist/container/types.d.ts +11 -1
- package/dist/container/types.d.ts.map +1 -1
- package/dist/decorators/Service.d.ts +1 -1
- package/dist/decorators/Service.d.ts.map +1 -1
- package/dist/env/Env.d.ts.map +1 -1
- package/dist/env/Env.js +4 -2
- package/dist/env/Env.js.map +1 -1
- package/dist/env/interpolate.d.ts.map +1 -1
- package/dist/env/interpolate.js +5 -3
- package/dist/env/interpolate.js.map +1 -1
- package/dist/errors/FuzzyMatcher.d.ts.map +1 -1
- package/dist/errors/FuzzyMatcher.js +30 -13
- package/dist/errors/FuzzyMatcher.js.map +1 -1
- package/dist/errors/PipelineStageError.d.ts.map +1 -1
- package/dist/errors/PipelineStageError.js +4 -2
- package/dist/errors/PipelineStageError.js.map +1 -1
- package/dist/events/Emitter.d.ts +21 -4
- package/dist/events/Emitter.d.ts.map +1 -1
- package/dist/events/Emitter.js +6 -2
- package/dist/events/Emitter.js.map +1 -1
- package/dist/events/testing/FakeBus.d.ts +1 -1
- package/dist/events/testing/FakeBus.d.ts.map +1 -1
- package/dist/events/testing/FakeBus.js.map +1 -1
- package/dist/graphql/GraphQLEngine.d.ts +2 -2
- package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
- package/dist/graphql/GraphQLEngine.js +4 -2
- package/dist/graphql/GraphQLEngine.js.map +1 -1
- package/dist/helpers/bytes.d.ts.map +1 -1
- package/dist/helpers/bytes.js +13 -2
- package/dist/helpers/bytes.js.map +1 -1
- package/dist/helpers/string.js +3 -3
- package/dist/helpers/string.js.map +1 -1
- package/dist/http/Exception.d.ts +8 -4
- package/dist/http/Exception.d.ts.map +1 -1
- package/dist/http/Exception.js +14 -8
- package/dist/http/Exception.js.map +1 -1
- package/dist/http/Request.d.ts.map +1 -1
- package/dist/http/Request.js +10 -7
- package/dist/http/Request.js.map +1 -1
- package/dist/http/mime.d.ts.map +1 -1
- package/dist/http/mime.js +2 -1
- package/dist/http/mime.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/native/generated.d.ts +12 -8
- package/dist/native/generated.d.ts.map +1 -1
- package/dist/openapi/OpenApiGenerator.d.ts.map +1 -1
- package/dist/openapi/OpenApiGenerator.js +6 -4
- package/dist/openapi/OpenApiGenerator.js.map +1 -1
- package/dist/router/Router.d.ts +10 -1
- package/dist/router/Router.d.ts.map +1 -1
- package/dist/router/Router.js +22 -10
- package/dist/router/Router.js.map +1 -1
- package/dist/rpc/RpcRouter.d.ts +16 -3
- package/dist/rpc/RpcRouter.d.ts.map +1 -1
- package/dist/rpc/RpcRouter.js +34 -3
- package/dist/rpc/RpcRouter.js.map +1 -1
- package/dist/scheduler/Schedule.d.ts +6 -1
- package/dist/scheduler/Schedule.d.ts.map +1 -1
- package/dist/scheduler/Schedule.js +5 -0
- package/dist/scheduler/Schedule.js.map +1 -1
- package/dist/scheduler/ScheduleProvider.d.ts +15 -11
- package/dist/scheduler/ScheduleProvider.d.ts.map +1 -1
- package/dist/scheduler/ScheduleProvider.js +21 -12
- package/dist/scheduler/ScheduleProvider.js.map +1 -1
- package/dist/security/CookieSigner.d.ts.map +1 -1
- package/dist/security/CookieSigner.js +12 -5
- package/dist/security/CookieSigner.js.map +1 -1
- package/dist/security/SignedUrl.js +3 -2
- package/dist/security/SignedUrl.js.map +1 -1
- package/dist/session/SessionMiddleware.d.ts +2 -4
- package/dist/session/SessionMiddleware.d.ts.map +1 -1
- package/dist/session/SessionMiddleware.js +13 -1
- package/dist/session/SessionMiddleware.js.map +1 -1
- package/dist/session/config.d.ts +25 -0
- package/dist/session/config.d.ts.map +1 -0
- package/dist/session/config.js +18 -0
- package/dist/session/config.js.map +1 -0
- package/dist/session/drivers/DatabaseDriver.d.ts +13 -1
- package/dist/session/drivers/DatabaseDriver.d.ts.map +1 -1
- package/dist/session/drivers/DatabaseDriver.js +23 -3
- package/dist/session/drivers/DatabaseDriver.js.map +1 -1
- package/dist/session/drivers/RedisDriver.d.ts.map +1 -1
- package/dist/session/drivers/RedisDriver.js +10 -2
- package/dist/session/drivers/RedisDriver.js.map +1 -1
- package/dist/testing/ApiResponse.d.ts.map +1 -1
- package/dist/testing/ApiResponse.js +10 -11
- package/dist/testing/ApiResponse.js.map +1 -1
- package/dist/testing/TestClient.js +6 -6
- package/dist/testing/TestClient.js.map +1 -1
- package/dist/utils/Macroable.d.ts +6 -2
- package/dist/utils/Macroable.d.ts.map +1 -1
- package/dist/utils/Macroable.js +4 -0
- package/dist/utils/Macroable.js.map +1 -1
- package/dist/utils/objectPath.d.ts.map +1 -1
- package/dist/utils/objectPath.js +14 -6
- package/dist/utils/objectPath.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 +13 -4
- 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/scripts/build-napi-types.mjs +4 -3
- package/scripts/generate-napi-types.mjs +28 -12
- package/src/Application.ts +6 -1
- package/src/Ignitor.ts +42 -3
- package/src/Provider.ts +21 -2
- package/src/bodyparser/BodyParserMiddleware.ts +12 -1
- package/src/bodyparser/MultipartFile.ts +7 -3
- package/src/bodyparser/config.ts +18 -0
- package/src/bodyparser/parseSize.ts +3 -2
- package/src/commands/MigrateBase.ts +4 -1
- package/src/config/moduleConfig.ts +24 -0
- package/src/console/BaseCommand.ts +1 -1
- package/src/console/Kernel.ts +8 -0
- package/src/console/cliui.ts +2 -1
- package/src/console/types.ts +15 -0
- package/src/container/Container.ts +15 -6
- package/src/container/types.ts +11 -1
- package/src/decorators/Service.ts +1 -1
- package/src/env/Env.ts +4 -2
- package/src/env/interpolate.ts +5 -3
- package/src/errors/FuzzyMatcher.ts +34 -13
- package/src/errors/PipelineStageError.ts +4 -2
- package/src/events/Emitter.ts +34 -8
- package/src/events/testing/FakeBus.ts +12 -5
- package/src/graphql/GraphQLEngine.ts +11 -7
- package/src/helpers/bytes.ts +13 -2
- package/src/helpers/string.ts +3 -3
- package/src/http/Exception.ts +14 -8
- package/src/http/Request.ts +8 -5
- package/src/http/mime.ts +2 -1
- package/src/index.ts +2 -2
- package/src/native/generated.ts +12 -8
- package/src/openapi/OpenApiGenerator.ts +6 -4
- package/src/router/Router.ts +31 -11
- package/src/rpc/RpcRouter.ts +38 -8
- package/src/scheduler/Schedule.ts +8 -1
- package/src/scheduler/ScheduleProvider.ts +22 -12
- package/src/security/CookieSigner.ts +15 -5
- package/src/security/SignedUrl.ts +3 -2
- package/src/session/SessionMiddleware.ts +15 -2
- package/src/session/config.ts +29 -0
- package/src/session/drivers/DatabaseDriver.ts +36 -8
- package/src/session/drivers/RedisDriver.ts +13 -5
- package/src/testing/ApiResponse.ts +9 -10
- package/src/testing/TestClient.ts +6 -6
- package/src/utils/Macroable.ts +6 -2
- package/src/utils/objectPath.ts +12 -6
package/src/rpc/RpcRouter.ts
CHANGED
|
@@ -30,7 +30,7 @@ export type RpcHandler = (ctx: HttpContext, params: unknown) => Promise<unknown>
|
|
|
30
30
|
* the shared `'middleware'` registry, and `validator:<name>` schemas.
|
|
31
31
|
*/
|
|
32
32
|
interface RpcContainer {
|
|
33
|
-
make<T>(target: new (...args:
|
|
33
|
+
make<T>(target: abstract new (...args: never[]) => T): Promise<T>
|
|
34
34
|
resolve<T>(token: string): Promise<T>
|
|
35
35
|
has(token: string): boolean
|
|
36
36
|
}
|
|
@@ -151,11 +151,21 @@ export class RpcRouter {
|
|
|
151
151
|
return new RpcMethodBuilder(def)
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
/**
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
154
|
+
/**
|
|
155
|
+
* Register methods from a controller — auto-registers all public methods.
|
|
156
|
+
*
|
|
157
|
+
* The parameter used to demand
|
|
158
|
+
* `new (...args: unknown[]) => Record<string, (...args: unknown[]) => unknown>`,
|
|
159
|
+
* which NO real controller satisfies: a class with any non-method property
|
|
160
|
+
* fails the `Record`, and a method declared `bump(): number` fails the
|
|
161
|
+
* `unknown[]` parameters, which are compared contravariantly. So the
|
|
162
|
+
* documented call — `rpc.namespace('task', TaskController)` — did not
|
|
163
|
+
* typecheck, and nothing noticed because the tests were outside `tsc`.
|
|
164
|
+
*
|
|
165
|
+
* Any class, then. The body already checks that what it found is callable
|
|
166
|
+
* and names the method when it is not.
|
|
167
|
+
*/
|
|
168
|
+
namespace(prefix: string, controller: abstract new (...args: never[]) => object): void {
|
|
159
169
|
const proto = controller.prototype
|
|
160
170
|
const methods = Object.getOwnPropertyNames(proto).filter(
|
|
161
171
|
(m) => m !== 'constructor' && typeof proto[m] === 'function',
|
|
@@ -164,8 +174,18 @@ export class RpcRouter {
|
|
|
164
174
|
this.method(`${prefix}.${methodName}`, async (ctx, params) => {
|
|
165
175
|
// Resolve through the container on every call (fresh DI per request,
|
|
166
176
|
// like GraphQLEngine), falling back to a bare `new` when unset.
|
|
167
|
-
|
|
168
|
-
|
|
177
|
+
// `Reflect.construct`, not `new controller()`: the token may be an
|
|
178
|
+
// abstract class, which the runtime builds happily and `new` refuses.
|
|
179
|
+
const instance: object = this.#container
|
|
180
|
+
? await this.#container.make(controller)
|
|
181
|
+
: Reflect.construct(controller, [])
|
|
182
|
+
const handler = Reflect.get(instance, methodName)
|
|
183
|
+
if (typeof handler !== 'function') {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`RPC method '${prefix}.${methodName}' is not a function on ${controller.name}`,
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
return handler.call(instance, ctx, params)
|
|
169
189
|
})
|
|
170
190
|
}
|
|
171
191
|
}
|
|
@@ -199,6 +219,16 @@ export class RpcRouter {
|
|
|
199
219
|
|
|
200
220
|
// Batch support (max 50 to prevent DoS)
|
|
201
221
|
if (Array.isArray(body)) {
|
|
222
|
+
// §6: "If the batch rpc call itself fails to be recognized as an valid
|
|
223
|
+
// JSON or as an Array with at least one value, the response from the
|
|
224
|
+
// Server MUST be a single Response object." An empty batch answered with
|
|
225
|
+
// 204 No Content, which is what this replies when every call in a batch
|
|
226
|
+
// was a notification — so a client that sent nothing was told its calls
|
|
227
|
+
// had run.
|
|
228
|
+
if (body.length === 0) {
|
|
229
|
+
ctx.response.status(200).json(rpcError(-32600, 'Invalid Request', null))
|
|
230
|
+
return
|
|
231
|
+
}
|
|
202
232
|
if (body.length > 50) {
|
|
203
233
|
ctx.response.status(400).json(rpcError(-32600, 'Batch too large (max 50)', null))
|
|
204
234
|
return
|
|
@@ -90,7 +90,14 @@ export function Schedule(cronExpr: string): MethodDecorator {
|
|
|
90
90
|
* none). Returns a shallow copy so callers cannot mutate the stored
|
|
91
91
|
* metadata by accident.
|
|
92
92
|
*/
|
|
93
|
-
|
|
93
|
+
/**
|
|
94
|
+
* `abstract new`, not `Constructor`: reading metadata off a class never
|
|
95
|
+
* constructs it, and the service registry holds abstract ones too since a
|
|
96
|
+
* contextual binding binds against an abstract base.
|
|
97
|
+
*/
|
|
98
|
+
export function getScheduleMetadata(
|
|
99
|
+
target: abstract new (...args: never[]) => unknown,
|
|
100
|
+
): ScheduleMetadata[] {
|
|
94
101
|
const stored: ScheduleMetadata[] = Reflect.getOwnMetadata(SCHEDULE_METADATA_KEY, target) ?? []
|
|
95
102
|
return [...stored]
|
|
96
103
|
}
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* throws, tasks 1..N-1 registered in the same boot are unregistered
|
|
11
11
|
* before the error propagates, so the scheduler is never left in a
|
|
12
12
|
* half-registered state.
|
|
13
|
-
* - `
|
|
13
|
+
* - `ready()` — discovers the tasks `app/modules/**` declared, then
|
|
14
|
+
* launches the Rust tick loop.
|
|
14
15
|
* - `shutdown()` — cancels the Rust tick loop.
|
|
15
16
|
*
|
|
16
17
|
* The registered callback resolves the service **at invocation time**
|
|
@@ -86,26 +87,35 @@ export class ScheduleProvider extends Provider {
|
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
/**
|
|
89
|
-
*
|
|
90
|
+
* The pass that matters runs at READY, and the ordering is the reason.
|
|
90
91
|
*
|
|
91
|
-
* `
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* registry
|
|
95
|
-
* and nothing said so
|
|
96
|
-
* normally and the task simply never fired.
|
|
92
|
+
* A `@Service()` carrying `@Schedule` naturally lives in `app/modules/**`,
|
|
93
|
+
* which the application auto-loads at the END of the start phase — after
|
|
94
|
+
* every provider's `start()`. Discovering in `start()` therefore read the
|
|
95
|
+
* service registry before the modules had put anything in it: the task was
|
|
96
|
+
* never registered, and nothing said so. No error, no warning, the
|
|
97
|
+
* application started normally and the task simply never fired. `ready()`
|
|
98
|
+
* runs after the autoload, which is what makes the second pass real.
|
|
97
99
|
*
|
|
98
|
-
*
|
|
99
|
-
* service declared
|
|
100
|
-
*
|
|
100
|
+
* The earlier passes are kept because they cost nothing and shorten the gap:
|
|
101
|
+
* a service declared by a provider or a preload is registered as soon as it
|
|
102
|
+
* is seen. `#discover()` is idempotent, so a task found twice is registered
|
|
103
|
+
* once.
|
|
101
104
|
*/
|
|
102
105
|
override async boot(): Promise<void> {
|
|
103
106
|
// Anything already in the registry — declared by a provider, or imported
|
|
104
|
-
// by one. The rest is caught
|
|
107
|
+
// by one. The rest is caught later.
|
|
105
108
|
this.#discover()
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
override async start(): Promise<void> {
|
|
112
|
+
// Preloads have not run yet either; this catches whatever the boot phase
|
|
113
|
+
// added. The ticker deliberately does NOT start here.
|
|
114
|
+
this.#discover()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
override async ready(): Promise<void> {
|
|
118
|
+
// The pass after `app/modules/**`.
|
|
109
119
|
this.#discover()
|
|
110
120
|
if (activeProvider !== undefined && activeProvider !== this) {
|
|
111
121
|
// A previous app's scheduler is still running — stop it before this one
|
|
@@ -225,12 +225,22 @@ export class CookieSigner {
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
decrypt<T = string>(encrypted: string, purpose?: string): T | null {
|
|
228
|
-
|
|
229
|
-
|
|
228
|
+
// Destructured rather than indexed, and `extra` is what keeps the old
|
|
229
|
+
// length check honest: three names bind on a four-part string too, so the
|
|
230
|
+
// fourth has to be looked at to reject it.
|
|
231
|
+
const [ivPart, dataPart, tagPart, extra] = encrypted.split('.')
|
|
232
|
+
if (
|
|
233
|
+
ivPart === undefined ||
|
|
234
|
+
dataPart === undefined ||
|
|
235
|
+
tagPart === undefined ||
|
|
236
|
+
extra !== undefined
|
|
237
|
+
) {
|
|
238
|
+
return null
|
|
239
|
+
}
|
|
230
240
|
try {
|
|
231
|
-
const iv = Buffer.from(
|
|
232
|
-
const data = Buffer.from(
|
|
233
|
-
const tag = Buffer.from(
|
|
241
|
+
const iv = Buffer.from(ivPart, 'base64url')
|
|
242
|
+
const data = Buffer.from(dataPart, 'base64url')
|
|
243
|
+
const tag = Buffer.from(tagPart, 'base64url')
|
|
234
244
|
const decipher = createDecipheriv('aes-256-gcm', this.#keyBuffer, iv)
|
|
235
245
|
decipher.setAuthTag(tag)
|
|
236
246
|
const raw = Buffer.concat([decipher.update(data), decipher.final()]).toString('utf8')
|
|
@@ -55,8 +55,9 @@ function parseExpiry(value: string | number): number {
|
|
|
55
55
|
if (typeof value === 'number') return value
|
|
56
56
|
const match = value.match(/^(\d+)(s|m|h|d)$/)
|
|
57
57
|
if (!match) return 3600
|
|
58
|
-
const
|
|
59
|
-
|
|
58
|
+
const [, digits, unit] = match
|
|
59
|
+
const num = parseInt(digits ?? '0', 10)
|
|
60
|
+
switch (unit) {
|
|
60
61
|
case 's':
|
|
61
62
|
return num
|
|
62
63
|
case 'm':
|
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { randomBytes } from 'node:crypto'
|
|
9
|
+
import { readModuleConfig } from '../config/moduleConfig.js'
|
|
9
10
|
import { currentNodeEnv } from '../env/nodeEnv.js'
|
|
10
11
|
import { durationToSeconds } from '../helpers/duration.js'
|
|
11
12
|
import type { HttpContext } from '../http/HttpContext.js'
|
|
13
|
+
import type { SessionConfigInput } from './config.js'
|
|
12
14
|
import { CookieDriver } from './drivers/CookieDriver.js'
|
|
13
15
|
import { DatabaseDriver, type SessionDbConnection } from './drivers/DatabaseDriver.js'
|
|
14
16
|
import { FileDriver } from './drivers/FileDriver.js'
|
|
@@ -16,7 +18,7 @@ import { MemoryDriver } from './drivers/MemoryDriver.js'
|
|
|
16
18
|
import { RedisDriver } from './drivers/RedisDriver.js'
|
|
17
19
|
import { quasarConnection } from './quasar.js'
|
|
18
20
|
import { ReadOnlyValuesStore, type ValuePath } from './ReadOnlyValuesStore.js'
|
|
19
|
-
import type {
|
|
21
|
+
import type { SessionDriver } from './Session.js'
|
|
20
22
|
import { Session } from './Session.js'
|
|
21
23
|
|
|
22
24
|
interface ResolvedSessionConfig {
|
|
@@ -42,7 +44,18 @@ export default class SessionMiddleware {
|
|
|
42
44
|
#cookieDriver: CookieDriver | null = null
|
|
43
45
|
#config: ResolvedSessionConfig
|
|
44
46
|
|
|
45
|
-
constructor(
|
|
47
|
+
constructor(explicitConfig: SessionConfigInput | undefined = undefined) {
|
|
48
|
+
// The default value is load-bearing: an OPTIONAL parameter still counts
|
|
49
|
+
// toward `Function.length`, and the container refuses to auto-construct a
|
|
50
|
+
// class that declares parameters it has no metadata for. A parameter with a
|
|
51
|
+
// default counts for nothing, which is what lets
|
|
52
|
+
// `router.use([() => import('…')])` build this middleware.
|
|
53
|
+
// Nothing is passed when the container builds this middleware from
|
|
54
|
+
// `router.use([() => import('@c9up/ream/session_middleware')])`, which is
|
|
55
|
+
// the registration shape the framework documents. `config/session.ts` is
|
|
56
|
+
// where the settings live in that case — the same file the config loader
|
|
57
|
+
// already reads into `config.get('session')`.
|
|
58
|
+
const config = explicitConfig ?? readModuleConfig<SessionConfigInput>('session')
|
|
46
59
|
// `store` + `stores` (AdonisJS) and `driver` (ream) name the same thing.
|
|
47
60
|
// A named store supplies the driver and its own options, so
|
|
48
61
|
// `{ store: 'redis', stores: { redis: { driver: 'redis' } } }` works.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `config/session.ts` — the session store's configuration, in the file the
|
|
3
|
+
* config loader reads (`config/<name>.ts` → `config.get('<name>')`).
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* // config/session.ts
|
|
7
|
+
* import env from '#start/env.js'
|
|
8
|
+
* import { defineConfig } from '@c9up/ream/session/config'
|
|
9
|
+
*
|
|
10
|
+
* export default defineConfig({
|
|
11
|
+
* store: 'cookie',
|
|
12
|
+
* secret: env.get('APP_KEY'),
|
|
13
|
+
* })
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { SessionConfig } from './Session.js'
|
|
17
|
+
|
|
18
|
+
/** What a session config file may declare. */
|
|
19
|
+
export type SessionConfigInput = SessionConfig & {
|
|
20
|
+
/**
|
|
21
|
+
* Signs the session cookie. Required by the cookie store, which carries the
|
|
22
|
+
* data itself and would otherwise hand the client something it could edit.
|
|
23
|
+
*/
|
|
24
|
+
secret?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function defineConfig(config: SessionConfigInput): SessionConfigInput {
|
|
28
|
+
return config
|
|
29
|
+
}
|
|
@@ -8,10 +8,28 @@ import type { SessionDriverWithTagging, TaggedSession } from '../Session.js'
|
|
|
8
8
|
* app's own wrapper.
|
|
9
9
|
*/
|
|
10
10
|
export interface SessionDbConnection {
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Rows, as rows: `Record<string, unknown>`, not a shape the caller names.
|
|
13
|
+
*
|
|
14
|
+
* `query<T>` let this driver ASSERT what a SELECT returned — a promise the
|
|
15
|
+
* database never made and the connection cannot keep. It also made the
|
|
16
|
+
* interface unimplementable by anything but a real driver: a generic method
|
|
17
|
+
* must work for every `T`, so no fake could satisfy it, and this file's tests
|
|
18
|
+
* sat outside the typecheck because of it.
|
|
19
|
+
*
|
|
20
|
+
* The columns are read through {@link textColumn} below instead, which is
|
|
21
|
+
* what a row from a database deserves.
|
|
22
|
+
*/
|
|
23
|
+
query(sql: string, params?: unknown[]): Promise<Array<Record<string, unknown>>>
|
|
12
24
|
execute(sql: string, params?: unknown[]): Promise<{ rowsAffected: number }>
|
|
13
25
|
}
|
|
14
26
|
|
|
27
|
+
/** A column that must be text to be usable, or `null`. */
|
|
28
|
+
function textColumn(row: Record<string, unknown>, name: string): string | null {
|
|
29
|
+
const value = row[name]
|
|
30
|
+
return typeof value === 'string' ? value : null
|
|
31
|
+
}
|
|
32
|
+
|
|
15
33
|
export interface DatabaseDriverOptions {
|
|
16
34
|
connection: SessionDbConnection
|
|
17
35
|
/** Table holding the sessions. Default `sessions`. */
|
|
@@ -71,17 +89,20 @@ export class DatabaseDriver implements SessionDriverWithTagging {
|
|
|
71
89
|
}
|
|
72
90
|
|
|
73
91
|
async read(sessionId: string): Promise<Record<string, unknown> | null> {
|
|
74
|
-
const rows = await this.#db.query
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
)
|
|
92
|
+
const rows = await this.#db.query(`SELECT data, expires_at FROM ${this.#table} WHERE id = ?`, [
|
|
93
|
+
sessionId,
|
|
94
|
+
])
|
|
78
95
|
const row = rows[0]
|
|
79
96
|
if (!row) return null
|
|
80
97
|
if (Number(row.expires_at) < Date.now()) {
|
|
81
98
|
await this.destroy(sessionId)
|
|
82
99
|
return null
|
|
83
100
|
}
|
|
84
|
-
|
|
101
|
+
const data = textColumn(row, 'data')
|
|
102
|
+
// A row whose payload is not text is not a session — the same outcome as a
|
|
103
|
+
// row that will not parse.
|
|
104
|
+
if (data === null) return null
|
|
105
|
+
return parseData(data)
|
|
85
106
|
}
|
|
86
107
|
|
|
87
108
|
async write(sessionId: string, data: Record<string, unknown>, ttl: number): Promise<void> {
|
|
@@ -157,11 +178,18 @@ export class DatabaseDriver implements SessionDriverWithTagging {
|
|
|
157
178
|
|
|
158
179
|
/** Every non-expired session this user holds — their active devices. */
|
|
159
180
|
async tagged(userId: string | number): Promise<TaggedSession[]> {
|
|
160
|
-
const rows = await this.#db.query
|
|
181
|
+
const rows = await this.#db.query(
|
|
161
182
|
`SELECT id, data FROM ${this.#table} WHERE user_id = ? AND expires_at >= ?`,
|
|
162
183
|
[String(userId), Date.now()],
|
|
163
184
|
)
|
|
164
|
-
|
|
185
|
+
const sessions: TaggedSession[] = []
|
|
186
|
+
for (const row of rows) {
|
|
187
|
+
const id = textColumn(row, 'id')
|
|
188
|
+
const data = textColumn(row, 'data')
|
|
189
|
+
if (id === null || data === null) continue
|
|
190
|
+
sessions.push({ id, data: parseData(data) })
|
|
191
|
+
}
|
|
192
|
+
return sessions
|
|
165
193
|
}
|
|
166
194
|
|
|
167
195
|
/** Delete every expired row — for a scheduled job, since nothing else does. */
|
|
@@ -67,11 +67,19 @@ export class RedisDriver implements SessionDriverWithTagging {
|
|
|
67
67
|
}
|
|
68
68
|
if (!this.#pending) {
|
|
69
69
|
const resolver = this.#source
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
// `finally`, not the success path: clearing it only on success left a
|
|
71
|
+
// REJECTED promise in the slot, and every later call returned that same
|
|
72
|
+
// rejection. One refused connection at boot — Redis still starting, a
|
|
73
|
+
// network blip — and the driver never tried again for the life of the
|
|
74
|
+
// process. Bay and echo already clear it this way.
|
|
75
|
+
this.#pending = Promise.resolve(resolver())
|
|
76
|
+
.then((client) => {
|
|
77
|
+
this.#resolved = client
|
|
78
|
+
return client
|
|
79
|
+
})
|
|
80
|
+
.finally(() => {
|
|
81
|
+
this.#pending = undefined
|
|
82
|
+
})
|
|
75
83
|
}
|
|
76
84
|
return this.#pending
|
|
77
85
|
}
|
|
@@ -149,15 +149,14 @@ function parseMultipartResponse(
|
|
|
149
149
|
// Trim the trailing CRLF before the next boundary.
|
|
150
150
|
let content = part.subarray(headerEnd + 4)
|
|
151
151
|
if (content.subarray(-2).toString('binary') === '\r\n') content = content.subarray(0, -2)
|
|
152
|
-
const
|
|
153
|
-
if (
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
if (fileMatch) {
|
|
152
|
+
const [, name] = headerText.match(/name="([^"]*)"/i) ?? []
|
|
153
|
+
if (name === undefined) continue
|
|
154
|
+
const [, filename] = headerText.match(/filename="([^"]*)"/i) ?? []
|
|
155
|
+
if (filename !== undefined) {
|
|
157
156
|
const typeMatch = headerText.match(/content-type:\s*([^\r\n]+)/i)
|
|
158
157
|
files[name] = {
|
|
159
158
|
fieldName: name,
|
|
160
|
-
filename
|
|
159
|
+
filename,
|
|
161
160
|
type: typeMatch?.[1]?.trim() ?? 'application/octet-stream',
|
|
162
161
|
size: content.length,
|
|
163
162
|
content,
|
|
@@ -355,8 +354,8 @@ export class ApiResponse {
|
|
|
355
354
|
/** Charset from `Content-Type`, if any — helix `charset()`. */
|
|
356
355
|
charset(): string | undefined {
|
|
357
356
|
const ct = this.#raw.headers['content-type'] ?? ''
|
|
358
|
-
const
|
|
359
|
-
return
|
|
357
|
+
const [, charset] = ct.match(/charset=([^;]+)/i) ?? []
|
|
358
|
+
return charset?.trim()
|
|
360
359
|
}
|
|
361
360
|
|
|
362
361
|
/** All response cookies, parsed from `Set-Cookie` — helix `cookies()`. */
|
|
@@ -380,8 +379,8 @@ export class ApiResponse {
|
|
|
380
379
|
if (!link) return {}
|
|
381
380
|
const out: Record<string, string> = {}
|
|
382
381
|
for (const part of link.split(',')) {
|
|
383
|
-
const
|
|
384
|
-
if (
|
|
382
|
+
const [, url, rel] = part.match(/<([^>]+)>\s*;\s*rel="?([^";]+)"?/) ?? []
|
|
383
|
+
if (url !== undefined && rel !== undefined) out[rel] = url
|
|
385
384
|
}
|
|
386
385
|
return out
|
|
387
386
|
}
|
|
@@ -368,15 +368,15 @@ export function parseHttpResponse(raw: string | Buffer): TestResponse {
|
|
|
368
368
|
|
|
369
369
|
const lines = headerSection.split('\r\n')
|
|
370
370
|
const statusLine = lines[0] ?? ''
|
|
371
|
-
const
|
|
372
|
-
const status =
|
|
371
|
+
const [, statusDigits] = statusLine.match(/HTTP\/\d+\.\d+ (\d+)/) ?? []
|
|
372
|
+
const status = statusDigits === undefined ? 0 : Number.parseInt(statusDigits, 10)
|
|
373
373
|
|
|
374
374
|
const headers: Dict = {}
|
|
375
|
-
for (
|
|
376
|
-
const colonIdx =
|
|
375
|
+
for (const line of lines.slice(1)) {
|
|
376
|
+
const colonIdx = line.indexOf(':')
|
|
377
377
|
if (colonIdx === -1) continue
|
|
378
|
-
const key =
|
|
379
|
-
const value =
|
|
378
|
+
const key = line.slice(0, colonIdx).trim().toLowerCase()
|
|
379
|
+
const value = line.slice(colonIdx + 1).trim()
|
|
380
380
|
headers[key] = value
|
|
381
381
|
}
|
|
382
382
|
|
package/src/utils/Macroable.ts
CHANGED
|
@@ -38,7 +38,11 @@ export class Macroable {
|
|
|
38
38
|
* `HttpContext`, and two subclasses declaring the same name clobbered each
|
|
39
39
|
* other silently.
|
|
40
40
|
*/
|
|
41
|
-
|
|
41
|
+
// `{ prototype: object }`, not `typeof Macroable`: a subclass whose
|
|
42
|
+
// constructor takes arguments — `Request`, say — has a different static side,
|
|
43
|
+
// so `Request.macro(...)` was "the 'this' context of type 'typeof Request' is
|
|
44
|
+
// not assignable". What this needs from `this` is the prototype it defines on.
|
|
45
|
+
static macro(this: { prototype: object }, name: string, fn: MacroFn): void {
|
|
42
46
|
Object.defineProperty(this.prototype, name, {
|
|
43
47
|
value: fn,
|
|
44
48
|
writable: true,
|
|
@@ -51,7 +55,7 @@ export class Macroable {
|
|
|
51
55
|
* `getter`). With `singleton`, the value is computed once per instance and
|
|
52
56
|
* cached.
|
|
53
57
|
*/
|
|
54
|
-
static getter(this:
|
|
58
|
+
static getter(this: { prototype: object }, name: string, fn: GetterFn, singleton = false): void {
|
|
55
59
|
if (!singleton) {
|
|
56
60
|
Object.defineProperty(this.prototype, name, { get: fn, configurable: true })
|
|
57
61
|
return
|
package/src/utils/objectPath.ts
CHANGED
|
@@ -77,9 +77,13 @@ export function hasPath(source: unknown, path: string): boolean {
|
|
|
77
77
|
*/
|
|
78
78
|
export function setPath(target: UnknownRecord, path: string, value: unknown): UnknownRecord {
|
|
79
79
|
const segments = toSegments(path)
|
|
80
|
+
// The last segment is the one written; everything before it is walked. Split
|
|
81
|
+
// off by name rather than by index, so neither has to be read back out of
|
|
82
|
+
// the array afterwards and re-checked for existence.
|
|
83
|
+
const last = segments.pop()
|
|
84
|
+
if (last === undefined) return target
|
|
80
85
|
let current: UnknownRecord = target
|
|
81
|
-
for (
|
|
82
|
-
const key = segments[i]
|
|
86
|
+
for (const key of segments) {
|
|
83
87
|
const next = current[key]
|
|
84
88
|
if (!isIndexable(next)) {
|
|
85
89
|
const created: UnknownRecord = {}
|
|
@@ -89,7 +93,7 @@ export function setPath(target: UnknownRecord, path: string, value: unknown): Un
|
|
|
89
93
|
current = next
|
|
90
94
|
}
|
|
91
95
|
}
|
|
92
|
-
current[
|
|
96
|
+
current[last] = value
|
|
93
97
|
return target
|
|
94
98
|
}
|
|
95
99
|
|
|
@@ -115,12 +119,14 @@ export function omitPaths(source: UnknownRecord, paths: string[]): UnknownRecord
|
|
|
115
119
|
const result = structuredClone(source)
|
|
116
120
|
for (const path of paths) {
|
|
117
121
|
const segments = toSegments(path)
|
|
122
|
+
const last = segments.pop()
|
|
123
|
+
if (last === undefined) continue
|
|
118
124
|
let current: unknown = result
|
|
119
|
-
for (
|
|
125
|
+
for (const key of segments) {
|
|
120
126
|
if (!isIndexable(current)) break
|
|
121
|
-
current = current[
|
|
127
|
+
current = current[key]
|
|
122
128
|
}
|
|
123
|
-
if (isIndexable(current)) delete current[
|
|
129
|
+
if (isIndexable(current)) delete current[last]
|
|
124
130
|
}
|
|
125
131
|
return result
|
|
126
132
|
}
|