@c9up/ream 0.2.4 → 0.2.6
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 +3 -9
- package/dist/Application.d.ts +85 -5
- package/dist/Application.d.ts.map +1 -1
- package/dist/Application.js +171 -21
- package/dist/Application.js.map +1 -1
- package/dist/ErrorBoundary.d.ts +1 -7
- package/dist/ErrorBoundary.d.ts.map +1 -1
- package/dist/ErrorBoundary.js +31 -31
- package/dist/ErrorBoundary.js.map +1 -1
- package/dist/HttpKernel.js +2 -2
- package/dist/HttpKernel.js.map +1 -1
- package/dist/Ignitor.d.ts +18 -49
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +211 -173
- package/dist/Ignitor.js.map +1 -1
- package/dist/env/Env.d.ts +6 -0
- package/dist/env/Env.d.ts.map +1 -1
- package/dist/env/Env.js +6 -0
- package/dist/env/Env.js.map +1 -1
- package/dist/events/Emitter.d.ts +0 -6
- package/dist/events/Emitter.d.ts.map +1 -1
- package/dist/events/Emitter.js +42 -42
- package/dist/events/Emitter.js.map +1 -1
- package/dist/events/native.d.ts +13 -11
- package/dist/events/native.d.ts.map +1 -1
- package/dist/events/native.js.map +1 -1
- package/dist/helpers/compose.d.ts +30 -0
- package/dist/helpers/compose.d.ts.map +1 -0
- package/dist/helpers/compose.js +27 -0
- package/dist/helpers/compose.js.map +1 -0
- package/dist/helpers/index.d.ts +1 -0
- package/dist/helpers/index.d.ts.map +1 -1
- package/dist/helpers/index.js +1 -0
- package/dist/helpers/index.js.map +1 -1
- package/dist/helpers/is.d.ts +69 -16
- package/dist/helpers/is.d.ts.map +1 -1
- package/dist/helpers/is.js +79 -16
- package/dist/helpers/is.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/dist/middleware/Pipeline.d.ts +9 -3
- package/dist/middleware/Pipeline.d.ts.map +1 -1
- package/dist/middleware/Pipeline.js +10 -4
- package/dist/middleware/Pipeline.js.map +1 -1
- package/dist/native/generated.d.ts +243 -0
- package/dist/native/generated.d.ts.map +1 -0
- package/dist/native/generated.js +7 -0
- package/dist/native/generated.js.map +1 -0
- package/dist/rpc/RpcRouter.js +2 -2
- package/dist/rpc/RpcRouter.js.map +1 -1
- package/dist/services/app.d.ts +6 -0
- package/dist/services/app.d.ts.map +1 -1
- package/dist/services/app.js +8 -0
- package/dist/services/app.js.map +1 -1
- package/dist/services/config.d.ts +15 -0
- package/dist/services/config.d.ts.map +1 -0
- package/dist/services/config.js +18 -0
- package/dist/services/config.js.map +1 -0
- package/dist/services/encryption.d.ts +23 -0
- package/dist/services/encryption.d.ts.map +1 -0
- package/dist/services/encryption.js +33 -0
- package/dist/services/encryption.js.map +1 -0
- package/dist/services/urlBuilder.d.ts +21 -0
- package/dist/services/urlBuilder.d.ts.map +1 -0
- package/dist/services/urlBuilder.js +34 -0
- package/dist/services/urlBuilder.js.map +1 -0
- package/dist/session/drivers/CookieDriver.d.ts +1 -1
- package/dist/session/drivers/CookieDriver.d.ts.map +1 -1
- package/dist/session/drivers/CookieDriver.js +4 -4
- package/dist/session/drivers/CookieDriver.js.map +1 -1
- package/dist/testing/ApiResponse.d.ts +44 -44
- package/dist/testing/ApiResponse.d.ts.map +1 -1
- package/dist/testing/ApiResponse.js +48 -48
- package/dist/testing/ApiResponse.js.map +1 -1
- package/dist/testing/RequestBuilder.d.ts +39 -39
- package/dist/testing/RequestBuilder.d.ts.map +1 -1
- package/dist/testing/RequestBuilder.js +49 -49
- package/dist/testing/RequestBuilder.js.map +1 -1
- package/dist/testing/TestClient.d.ts +5 -5
- package/dist/testing/TestClient.d.ts.map +1 -1
- package/dist/testing/TestClient.js +7 -7
- package/dist/testing/TestClient.js.map +1 -1
- package/events.win32-x64-msvc.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +15 -2
- package/scheduler.win32-x64-msvc.node +0 -0
- package/scripts/build-napi-types.mjs +68 -0
- package/scripts/generate-napi-types.mjs +171 -0
- package/src/Application.ts +221 -21
- package/src/ErrorBoundary.ts +31 -31
- package/src/HttpKernel.ts +2 -2
- package/src/Ignitor.ts +228 -176
- package/src/env/Env.ts +6 -0
- package/src/events/Emitter.ts +42 -42
- package/src/events/native.ts +21 -14
- package/src/helpers/compose.ts +84 -0
- package/src/helpers/index.ts +1 -0
- package/src/helpers/is.ts +87 -16
- package/src/index.ts +3 -3
- package/src/middleware/Pipeline.ts +10 -4
- package/src/native/generated.ts +291 -0
- package/src/rpc/RpcRouter.ts +2 -2
- package/src/services/app.ts +9 -0
- package/src/services/config.ts +24 -0
- package/src/services/encryption.ts +42 -0
- package/src/services/urlBuilder.ts +41 -0
- package/src/session/drivers/CookieDriver.ts +4 -4
- package/src/testing/ApiResponse.ts +52 -52
- package/src/testing/RequestBuilder.ts +55 -55
- package/src/testing/TestClient.ts +8 -8
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// GENERATED FROM THE RUST — do not edit.
|
|
2
|
+
//
|
|
3
|
+
// Produced by scripts/generate-napi-types.mjs from napi-derive's type-def
|
|
4
|
+
// output. Editing this file by hand puts it back where it started: a
|
|
5
|
+
// description that can disagree with the code it describes.
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Typed response object crossing the TS→NAPI→Rust boundary.
|
|
9
|
+
*
|
|
10
|
+
* Using `#[napi(object)]` (instead of `String` carrying JSON) means napi-rs
|
|
11
|
+
* walks the JS object tree directly into Rust fields — zero JSON parse overhead.
|
|
12
|
+
*
|
|
13
|
+
* When `stream_id` is set, `body` is ignored and the hyper response is fed
|
|
14
|
+
* from the matching entry in the shared `StreamRegistry`. The handler must
|
|
15
|
+
* have called [`HyperServer::register_stream`] with that id BEFORE
|
|
16
|
+
* returning, otherwise the response collapses to a 500 (E_STREAM_UNKNOWN).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export interface NapiResponse {
|
|
20
|
+
status: number
|
|
21
|
+
headers: Record<string, string>
|
|
22
|
+
body: string
|
|
23
|
+
streamId?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Configuration for the wire-level shield filter. Mirrors the Rust
|
|
28
|
+
* `ShieldConfig` shape; the JS layer constructs it from `ShieldMiddleware`
|
|
29
|
+
* options at boot and hands it off to `HyperServer.configureShield`.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
export interface NapiShieldConfig {
|
|
33
|
+
pathTraversal: boolean
|
|
34
|
+
paramPollution: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Configuration for the wire-level rate limiter. `windowSecs` is the size
|
|
39
|
+
* of the fixed window in seconds; `max` is the request budget per key
|
|
40
|
+
* (resolved client IP) per window.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
export interface NapiRateLimitConfig {
|
|
44
|
+
max: number
|
|
45
|
+
windowSecs: number
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* NAPI-exposed event bus.
|
|
50
|
+
* Uses shared Tokio runtime (no per-instance runtime).
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
export declare class EventBus {
|
|
54
|
+
constructor(requestHandlerTimeoutMs?: number | undefined | null)
|
|
55
|
+
/** Emit an event (async — does NOT block Node.js thread). */
|
|
56
|
+
emit(name: string, data: string): Promise<string>
|
|
57
|
+
/**
|
|
58
|
+
* Subscribe to events matching a pattern. Callback receives event JSON string.
|
|
59
|
+
* Returns subscription ID.
|
|
60
|
+
*
|
|
61
|
+
* NOTE: This is `pub fn` (sync) — napi-rs cannot make `async fn` capture
|
|
62
|
+
* `JsFunction` because JsFunction isn't Send. The block_on here runs on the
|
|
63
|
+
* JS thread (no async context above), so it doesn't deadlock the runtime.
|
|
64
|
+
*/
|
|
65
|
+
subscribe(pattern: string, callback: (eventJson: string) => void): number
|
|
66
|
+
/** Unsubscribe by subscription ID. */
|
|
67
|
+
unsubscribe(subId: number): Promise<void>
|
|
68
|
+
/** Register a request handler. */
|
|
69
|
+
onRequest(
|
|
70
|
+
name: string,
|
|
71
|
+
callback: (eventJson: string, reply: (response: string) => void) => void,
|
|
72
|
+
): void
|
|
73
|
+
/** Send a request and get a response (async with timeout). */
|
|
74
|
+
request(name: string, data: string, timeoutMs?: number | undefined | null): Promise<string>
|
|
75
|
+
/** Check if a pattern matches an event name (wildcard matching via Rust). */
|
|
76
|
+
matchesWildcard(pattern: string, eventName: string): boolean
|
|
77
|
+
/** Get subscription count. */
|
|
78
|
+
subscriptionCount(): Promise<number>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** NAPI-exposed Hyper HTTP server. */
|
|
82
|
+
|
|
83
|
+
export declare class HyperServer {
|
|
84
|
+
/**
|
|
85
|
+
* `host` is the bind address: an IPv4 literal (`0.0.0.0` to accept from
|
|
86
|
+
* every interface) or the `localhost` alias. Omitted → loopback only,
|
|
87
|
+
* the safe default for a dev machine.
|
|
88
|
+
*/
|
|
89
|
+
constructor(port?: number | undefined | null, host?: string | undefined | null)
|
|
90
|
+
/** Register the request handler. Callback receives JSON request string, must return JSON response string. */
|
|
91
|
+
onRequest(callback: (request: string) => Promise<string> | string): void
|
|
92
|
+
/**
|
|
93
|
+
* Configure the trusted-proxy CIDR list used by `request.ip` resolution.
|
|
94
|
+
* Must be called BEFORE `listen()`. Empty list = legacy permissive mode
|
|
95
|
+
* (any peer can populate `X-Forwarded-For`).
|
|
96
|
+
*/
|
|
97
|
+
configureTrustedProxies(cidrs: Array<string>): void
|
|
98
|
+
/**
|
|
99
|
+
* Install (or replace) the wire-level rate limiter. Pass `None` to
|
|
100
|
+
* disable. Must be called BEFORE `listen()`.
|
|
101
|
+
*/
|
|
102
|
+
configureRateLimit(config?: NapiRateLimitConfig | undefined | null): void
|
|
103
|
+
/**
|
|
104
|
+
* Configure the wire-level security shield. Must be called BEFORE
|
|
105
|
+
* `listen()` — the configuration is captured into the security filter
|
|
106
|
+
* when the server starts. Calling after `listen()` has no effect.
|
|
107
|
+
*/
|
|
108
|
+
configureShield(config: NapiShieldConfig): void
|
|
109
|
+
/** Start the HTTP server. */
|
|
110
|
+
listen(): Promise<void>
|
|
111
|
+
/** Get the actual bound port. */
|
|
112
|
+
port(): Promise<number>
|
|
113
|
+
/**
|
|
114
|
+
* Shut down the server and release the handles that keep the host's event
|
|
115
|
+
* loop alive.
|
|
116
|
+
*
|
|
117
|
+
* `listen()` registers the JS callback as a `ThreadsafeFunction` (which
|
|
118
|
+
* holds a libuv ref so `ream start` stays alive). Three `Arc` clones of the
|
|
119
|
+
* request handler capture it: this struct's `self.handler`, the inner
|
|
120
|
+
* `ReamServer.handler`, and the accept-loop task's clone. ALL three must
|
|
121
|
+
* drop for napi-rs to release the tsfn on `Drop` (refcount → 0). The old
|
|
122
|
+
* `close()` only signalled shutdown, leaving every clone alive — so an
|
|
123
|
+
* in-process host (test harness) leaked the handle and never drained.
|
|
124
|
+
*/
|
|
125
|
+
close(): Promise<void>
|
|
126
|
+
/**
|
|
127
|
+
* Reserve a slot in the stream registry. Returns `false` if the id
|
|
128
|
+
* already exists (caller picked a colliding UUID). Always returns
|
|
129
|
+
* before `listen()` need have run.
|
|
130
|
+
*/
|
|
131
|
+
registerStream(streamId: string): Promise<boolean>
|
|
132
|
+
/**
|
|
133
|
+
* Push a chunk onto a registered stream. Returns `false` when the
|
|
134
|
+
* stream has already finished (receiver dropped — client gone or
|
|
135
|
+
* `closeStream` called) so the caller can bail out of its push
|
|
136
|
+
* loop. Backpressure is handled silently: a full buffer drops the
|
|
137
|
+
* frame rather than awaiting — SSE is fire-and-forget by design.
|
|
138
|
+
*/
|
|
139
|
+
writeStream(streamId: string, chunk: string): Promise<boolean>
|
|
140
|
+
/**
|
|
141
|
+
* Push a BINARY chunk onto a registered stream, waiting for room.
|
|
142
|
+
*
|
|
143
|
+
* The counterpart of [`write_stream`](Self::write_stream) for a body the
|
|
144
|
+
* client reassembles — a file, an export, an archive. Two differences,
|
|
145
|
+
* both required there and both wrong for SSE:
|
|
146
|
+
*
|
|
147
|
+
* * it takes bytes, not a `String`, so a payload that is not valid UTF-8
|
|
148
|
+
* survives the crossing intact;
|
|
149
|
+
* * it AWAITS when the channel is full instead of dropping the frame, so
|
|
150
|
+
* a slow client slows the producer rather than silently receiving a
|
|
151
|
+
* truncated file.
|
|
152
|
+
*
|
|
153
|
+
* Returns `false` when the receiver is gone (client disconnected or the
|
|
154
|
+
* stream was closed), so the caller can stop reading its source.
|
|
155
|
+
*/
|
|
156
|
+
writeStreamBytes(streamId: string, chunk: Uint8Array): Promise<boolean>
|
|
157
|
+
/**
|
|
158
|
+
* Close a stream from the server side. The matching response body
|
|
159
|
+
* finishes cleanly (hyper writes the final chunk + `0
|
|
160
|
+
|
|
161
|
+
` for
|
|
162
|
+
* HTTP/1.1 chunked encoding). Idempotent.
|
|
163
|
+
*/
|
|
164
|
+
closeStream(streamId: string): Promise<boolean>
|
|
165
|
+
/**
|
|
166
|
+
* Install a one-shot callback fired the moment the matching stream
|
|
167
|
+
* receiver is dropped — i.e. the client disconnected or
|
|
168
|
+
* `closeStream` was called. Used by the JS SDK to drop subscription
|
|
169
|
+
* bookkeeping when an SSE viewer leaves.
|
|
170
|
+
*
|
|
171
|
+
* The callback runs on the libuv loop via the threadsafe-function
|
|
172
|
+
* machinery, so JS code in the callback uses regular await /
|
|
173
|
+
* reentrant container calls without crossing-thread surprises.
|
|
174
|
+
*/
|
|
175
|
+
onStreamDisconnect(streamId: string, callback: (streamId: string) => void): void
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Scheduler instance exposed to TypeScript.
|
|
180
|
+
*
|
|
181
|
+
* Usage (from TS):
|
|
182
|
+
* ```ignore
|
|
183
|
+
* const scheduler = new RustScheduler();
|
|
184
|
+
* scheduler.register('cleanup', '0 *\/5 * * *', (payload) => { ... });
|
|
185
|
+
* scheduler.start();
|
|
186
|
+
* // later
|
|
187
|
+
* scheduler.stop();
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
|
|
191
|
+
export declare class RustScheduler {
|
|
192
|
+
constructor()
|
|
193
|
+
/**
|
|
194
|
+
* Register a task. `cronExpr` is a standard 5-field cron expression
|
|
195
|
+
* evaluated in UTC.
|
|
196
|
+
*
|
|
197
|
+
* Throws with `DUPLICATE_TASK` if `name` already exists, or
|
|
198
|
+
* `INVALID_CRON` if the expression is malformed.
|
|
199
|
+
*/
|
|
200
|
+
register(
|
|
201
|
+
name: string,
|
|
202
|
+
cronExpr: string,
|
|
203
|
+
callback: (invocation: { taskName: string; scheduledForMs: number }) => void,
|
|
204
|
+
): void
|
|
205
|
+
/** Remove a task. Idempotent — unknown names are not an error. */
|
|
206
|
+
unregister(name: string): void
|
|
207
|
+
/** Launch the tick loop on the shared Tokio runtime. Idempotent. */
|
|
208
|
+
start(): void
|
|
209
|
+
/** Cancel the tick loop. Safe to call even if not running. */
|
|
210
|
+
stop(): void
|
|
211
|
+
/** Return the next fire time in ms epoch, or `null` if the task is unknown. */
|
|
212
|
+
nextRun(name: string): number | null
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Sign a JWT payload with HMAC-SHA256 (Rust-native).
|
|
217
|
+
* Returns the complete JWT token string.
|
|
218
|
+
*/
|
|
219
|
+
|
|
220
|
+
export declare function jwtSign(payload: string, secret: string): string
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Verify a JWT token and return the decoded payload JSON string.
|
|
224
|
+
* Validates signature (constant-time), exp, and nbf claims.
|
|
225
|
+
*/
|
|
226
|
+
|
|
227
|
+
export declare function jwtVerify(token: string, secret: string): string
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Hash a password with Argon2id (Rust-native).
|
|
231
|
+
* Returns the PHC-formatted hash string.
|
|
232
|
+
*/
|
|
233
|
+
|
|
234
|
+
export declare function argon2Hash(password: string): string
|
|
235
|
+
|
|
236
|
+
/** Verify a password against an Argon2id hash. */
|
|
237
|
+
|
|
238
|
+
export declare function argon2Verify(password: string, hash: string): boolean
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Hash a password with bcrypt (Rust-native).
|
|
242
|
+
* `rounds` defaults to 12 when not supplied — matches the previous
|
|
243
|
+
* warden-engine default and the OWASP-recommended cost floor enforced
|
|
244
|
+
* by `sigil-engine`.
|
|
245
|
+
*/
|
|
246
|
+
|
|
247
|
+
export declare function bcryptHash(password: string, rounds?: number | undefined | null): string
|
|
248
|
+
|
|
249
|
+
/** Verify a password against a bcrypt hash. */
|
|
250
|
+
|
|
251
|
+
export declare function bcryptVerify(password: string, hash: string): boolean
|
|
252
|
+
|
|
253
|
+
/** Constant-time string comparison. */
|
|
254
|
+
|
|
255
|
+
export declare function constantTimeEq(a: string, b: string): boolean
|
|
256
|
+
|
|
257
|
+
/** HMAC-SHA256 sign. Returns base64url signature. */
|
|
258
|
+
|
|
259
|
+
export declare function hmacSign(data: string, secret: string): string
|
|
260
|
+
|
|
261
|
+
/** HMAC-SHA256 verify (constant-time). */
|
|
262
|
+
|
|
263
|
+
export declare function hmacVerify(data: string, signature: string, secret: string): boolean
|
|
264
|
+
|
|
265
|
+
/** Generate cryptographically secure random bytes as base64url. */
|
|
266
|
+
|
|
267
|
+
export declare function randomBytesBase64(len: number): string
|
|
268
|
+
|
|
269
|
+
/** Generate cryptographically secure random bytes as hex. */
|
|
270
|
+
|
|
271
|
+
export declare function randomHex(len: number): string
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Parse a GraphQL query using the Rust graphql-parser crate.
|
|
275
|
+
* Returns JSON: { operationType, operationName, fields: [...], errors: [...] }
|
|
276
|
+
* Invalid queries are rejected here — they never reach the TypeScript resolver layer.
|
|
277
|
+
*/
|
|
278
|
+
|
|
279
|
+
export declare function graphqlParse(query: string): string
|
|
280
|
+
|
|
281
|
+
/** Validate a GraphQL query string. Returns JSON array of error strings (empty = valid). */
|
|
282
|
+
|
|
283
|
+
export declare function graphqlValidate(query: string): string
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Extract argument scalar types from a GraphQL SDL schema.
|
|
287
|
+
* Returns JSON: `{ "Type.field": { "argName": "ScalarType" } }` for argument
|
|
288
|
+
* coercion in the TypeScript engine.
|
|
289
|
+
*/
|
|
290
|
+
|
|
291
|
+
export declare function graphqlSchemaArgTypes(sdl: string): string
|
package/src/rpc/RpcRouter.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
} from '@c9up/comet'
|
|
21
21
|
import type { HttpContext } from '../http/HttpContext.js'
|
|
22
22
|
import type { MiddlewareRegistry, RuntimeValidator } from '../middleware/Pipeline.js'
|
|
23
|
-
import {
|
|
23
|
+
import { composeMiddleware, runValidator } from '../middleware/Pipeline.js'
|
|
24
24
|
|
|
25
25
|
export type RpcHandler = (ctx: HttpContext, params: unknown) => Promise<unknown> | unknown
|
|
26
26
|
|
|
@@ -252,7 +252,7 @@ export class RpcRouter {
|
|
|
252
252
|
}
|
|
253
253
|
return mw
|
|
254
254
|
})
|
|
255
|
-
await
|
|
255
|
+
await composeMiddleware(fns)(ctx, async () => {})
|
|
256
256
|
}
|
|
257
257
|
|
|
258
258
|
async #processOne(ctx: HttpContext, request: unknown): Promise<unknown> {
|
package/src/services/app.ts
CHANGED
|
@@ -26,6 +26,15 @@ export function clearApp(app: Application): void {
|
|
|
26
26
|
if (instance === app) instance = undefined
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* @internal Get the app instance directly. Used by the accessors that stand in
|
|
31
|
+
* for something the app OWNS (`services/config` is `app.config`), so they read
|
|
32
|
+
* through here instead of keeping a second copy that a re-boot could desync.
|
|
33
|
+
*/
|
|
34
|
+
export function getApp(): Application | undefined {
|
|
35
|
+
return instance
|
|
36
|
+
}
|
|
37
|
+
|
|
29
38
|
const app: Application = createServiceProxy<Application>(
|
|
30
39
|
() => instance,
|
|
31
40
|
'Application accessed before initialization. ' +
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config store singleton service.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import config from '@c9up/ream/services/config'
|
|
6
|
+
* const port = config.get<number>('app.port', 3333)
|
|
7
|
+
*
|
|
8
|
+
* Unlike the other accessors this one keeps no instance of its own: the store
|
|
9
|
+
* IS `app.config`, so it reads through the app locator. One source of truth
|
|
10
|
+
* means the two can never point at different stores after a re-boot.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ConfigStore } from '../ConfigLoader.js'
|
|
14
|
+
import { getApp } from './app.js'
|
|
15
|
+
import { createServiceProxy } from './createServiceProxy.js'
|
|
16
|
+
|
|
17
|
+
const config: ConfigStore = createServiceProxy<ConfigStore>(
|
|
18
|
+
() => getApp()?.config,
|
|
19
|
+
'Config accessed before initialization. ' +
|
|
20
|
+
'The store is built during the boot phase — read it from inside a provider, ' +
|
|
21
|
+
'a preload, or a request, not at module top level.',
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
export default config
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cookie/value signing singleton service.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import encryption from '@c9up/ream/services/encryption'
|
|
6
|
+
* const signed = encryption.sign('payload')
|
|
7
|
+
*
|
|
8
|
+
* Registered only when `APP_KEY` is set — there is nothing to sign with
|
|
9
|
+
* otherwise — so the not-ready message names that rather than the boot phase.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { CookieSigner } from '../security/CookieSigner.js'
|
|
13
|
+
import { createServiceProxy } from './createServiceProxy.js'
|
|
14
|
+
|
|
15
|
+
let instance: CookieSigner | undefined
|
|
16
|
+
|
|
17
|
+
/** @internal Set the signer (called by Ignitor once APP_KEY is validated). */
|
|
18
|
+
export function setEncryption(signer: CookieSigner): void {
|
|
19
|
+
instance = signer
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @internal Unset the locator IF it still points at `signer` (called by
|
|
24
|
+
* Ignitor.stop()). Ownership-guarded — see services/app.ts.
|
|
25
|
+
*/
|
|
26
|
+
export function clearEncryption(signer: CookieSigner): void {
|
|
27
|
+
if (instance === signer) instance = undefined
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** @internal Get the signer directly. */
|
|
31
|
+
export function getEncryption(): CookieSigner | undefined {
|
|
32
|
+
return instance
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const encryption: CookieSigner = createServiceProxy<CookieSigner>(
|
|
36
|
+
() => instance,
|
|
37
|
+
'[E_MISSING_APP_KEY] Encryption accessed before it was registered. ' +
|
|
38
|
+
'It exists only when APP_KEY is set (>= 16 characters) — check the ' +
|
|
39
|
+
'environment before reaching for it, or you are reading it before boot.',
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
export default encryption
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Named-route URL helpers, as free functions.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { urlFor, signedUrlFor } from '@c9up/ream/services/urlBuilder'
|
|
6
|
+
* const href = urlFor('users.show', { id: '42' })
|
|
7
|
+
* const link = signedUrlFor('email.verify', { id: '42' }, { expiresIn: '1h' })
|
|
8
|
+
*
|
|
9
|
+
* Both delegate to the router — the same code `router.urlFor` /
|
|
10
|
+
* `router.makeSignedUrl` run — so a route renamed in one place cannot mean two
|
|
11
|
+
* things. They exist as functions rather than as another proxy object because
|
|
12
|
+
* that is all there is to the URL builder: two calls, no state.
|
|
13
|
+
*
|
|
14
|
+
* `signedUrlFor` needs an APP_KEY-backed signer and says so if there is none.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { SignedUrlOptions } from '../router/Router.js'
|
|
18
|
+
import { getRouter } from './router.js'
|
|
19
|
+
|
|
20
|
+
const NOT_READY =
|
|
21
|
+
'URL builder used before initialization. ' +
|
|
22
|
+
'Routes are registered during the boot phase — build URLs from inside a ' +
|
|
23
|
+
'controller, a view, or a preload, not at module top level.'
|
|
24
|
+
|
|
25
|
+
/** Generate a URL from a named route, filling `:param` placeholders. */
|
|
26
|
+
export function urlFor(name: string, params?: Record<string, string>): string {
|
|
27
|
+
const router = getRouter()
|
|
28
|
+
if (!router) throw new Error(NOT_READY)
|
|
29
|
+
return router.urlFor(name, params)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Generate a tamper-proof signed URL for a named route. */
|
|
33
|
+
export function signedUrlFor(
|
|
34
|
+
name: string,
|
|
35
|
+
params?: Record<string, string>,
|
|
36
|
+
options?: SignedUrlOptions,
|
|
37
|
+
): string {
|
|
38
|
+
const router = getRouter()
|
|
39
|
+
if (!router) throw new Error(NOT_READY)
|
|
40
|
+
return router.makeSignedUrl(name, params, options)
|
|
41
|
+
}
|
|
@@ -7,7 +7,7 @@ import type { SessionDriver } from '../Session.js'
|
|
|
7
7
|
* Requires a secret for encryption.
|
|
8
8
|
*/
|
|
9
9
|
export class CookieDriver implements SessionDriver {
|
|
10
|
-
|
|
10
|
+
#signer: CookieSigner
|
|
11
11
|
|
|
12
12
|
constructor(secret: string) {
|
|
13
13
|
if (!secret) {
|
|
@@ -15,12 +15,12 @@ export class CookieDriver implements SessionDriver {
|
|
|
15
15
|
'CookieDriver requires a secret for session encryption. Set session.secret in your config.',
|
|
16
16
|
)
|
|
17
17
|
}
|
|
18
|
-
this
|
|
18
|
+
this.#signer = new CookieSigner(secret)
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
async read(sessionId: string): Promise<Record<string, unknown> | null> {
|
|
22
22
|
if (!sessionId) return null
|
|
23
|
-
const decrypted = this
|
|
23
|
+
const decrypted = this.#signer.decrypt(sessionId)
|
|
24
24
|
if (!decrypted) return null
|
|
25
25
|
try {
|
|
26
26
|
return JSON.parse(decrypted)
|
|
@@ -43,6 +43,6 @@ export class CookieDriver implements SessionDriver {
|
|
|
43
43
|
|
|
44
44
|
/** Encode session data for cookie storage (encrypted). */
|
|
45
45
|
encode(data: Record<string, unknown>): string {
|
|
46
|
-
return this
|
|
46
|
+
return this.#signer.encrypt(JSON.stringify(data))
|
|
47
47
|
}
|
|
48
48
|
}
|