@warlock.js/core 5.11.0 → 5.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +92 -54
- package/esm/application/app.d.mts +3 -3
- package/esm/application/app.mjs +6 -6
- package/esm/application/app.mjs.map +1 -1
- package/esm/cli/cli-commands.utils.mjs +7 -3
- package/esm/cli/cli-commands.utils.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/health.check.mjs +1 -1
- package/esm/cli/commands/doctor/checks/health.check.mjs.map +1 -1
- package/esm/connectors/socket-connector.mjs +13 -16
- package/esm/connectors/socket-connector.mjs.map +1 -1
- package/esm/container/index.d.mts +20 -7
- package/esm/container/index.d.mts.map +1 -1
- package/esm/container/index.mjs +8 -5
- package/esm/container/index.mjs.map +1 -1
- package/esm/database/utils.d.mts +5 -1
- package/esm/database/utils.d.mts.map +1 -1
- package/esm/database/utils.mjs +7 -3
- package/esm/database/utils.mjs.map +1 -1
- package/esm/dev-server/dev-logger.mjs +12 -1
- package/esm/dev-server/dev-logger.mjs.map +1 -1
- package/esm/dev-server/development-server.mjs +1 -1
- package/esm/dev-server/development-server.mjs.map +1 -1
- package/esm/dev-server/file-event-handler.mjs +72 -13
- package/esm/dev-server/file-event-handler.mjs.map +1 -1
- package/esm/dev-server/file-manager.d.mts +8 -0
- package/esm/dev-server/file-manager.d.mts.map +1 -1
- package/esm/dev-server/file-manager.mjs +26 -4
- package/esm/dev-server/file-manager.mjs.map +1 -1
- package/esm/dev-server/files-orchestrator.mjs +3 -3
- package/esm/dev-server/files-orchestrator.mjs.map +1 -1
- package/esm/dev-server/files-watcher.mjs +16 -2
- package/esm/dev-server/files-watcher.mjs.map +1 -1
- package/esm/dev-server/flags.mjs +16 -1
- package/esm/dev-server/flags.mjs.map +1 -1
- package/esm/dev-server/layer-executor.mjs +19 -2
- package/esm/dev-server/layer-executor.mjs.map +1 -1
- package/esm/dev-server/translation-type-generator.mjs +28 -0
- package/esm/dev-server/translation-type-generator.mjs.map +1 -0
- package/esm/dev-server/tsconfig-manager.mjs +1 -0
- package/esm/dev-server/tsconfig-manager.mjs.map +1 -1
- package/esm/dev-server/type-generator.mjs +41 -5
- package/esm/dev-server/type-generator.mjs.map +1 -1
- package/esm/encryption/index.mjs +1 -1
- package/esm/errors/container-key-missing-error.mjs +18 -6
- package/esm/errors/container-key-missing-error.mjs.map +1 -1
- package/esm/generations/features/auth-google.feature.mjs +18 -0
- package/esm/generations/features/auth-google.feature.mjs.map +1 -0
- package/esm/generations/features/auth-passkeys.feature.mjs +19 -0
- package/esm/generations/features/auth-passkeys.feature.mjs.map +1 -0
- package/esm/generations/features/index.mjs +6 -0
- package/esm/generations/features/index.mjs.map +1 -1
- package/esm/generations/features/queue.feature.mjs +67 -0
- package/esm/generations/features/queue.feature.mjs.map +1 -0
- package/esm/generations/stubs.mjs +10 -10
- package/esm/generations/stubs.mjs.map +1 -1
- package/esm/http/context/request-context.d.mts +14 -11
- package/esm/http/context/request-context.d.mts.map +1 -1
- package/esm/http/context/request-context.mjs +13 -9
- package/esm/http/context/request-context.mjs.map +1 -1
- package/esm/http/createHttpApplication.d.mts.map +1 -1
- package/esm/http/createHttpApplication.mjs +2 -0
- package/esm/http/createHttpApplication.mjs.map +1 -1
- package/esm/http/csp.d.mts +139 -0
- package/esm/http/csp.d.mts.map +1 -0
- package/esm/http/csp.mjs +134 -0
- package/esm/http/csp.mjs.map +1 -0
- package/esm/http/errors/errors.d.mts +17 -1
- package/esm/http/errors/errors.d.mts.map +1 -1
- package/esm/http/errors/errors.mjs +20 -1
- package/esm/http/errors/errors.mjs.map +1 -1
- package/esm/http/index.d.mts +8 -3
- package/esm/http/index.mjs +6 -1
- package/esm/http/middleware/concurrency-limit.middleware.d.mts +2 -2
- package/esm/http/middleware/concurrency-limit.middleware.mjs +1 -1
- package/esm/http/middleware/concurrency-limit.middleware.mjs.map +1 -1
- package/esm/http/middleware/inject-request-context.d.mts +1 -1
- package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs +10 -0
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/middleware/rate-limit.middleware.d.mts +1 -1
- package/esm/http/middleware/rate-limit.middleware.mjs.map +1 -1
- package/esm/http/middleware/utils/idempotency-key.d.mts.map +1 -1
- package/esm/http/middleware/utils/idempotency-key.mjs +13 -13
- package/esm/http/middleware/utils/idempotency-key.mjs.map +1 -1
- package/esm/http/request-controller.d.mts +1 -1
- package/esm/http/request.d.mts +32 -44
- package/esm/http/request.d.mts.map +1 -1
- package/esm/http/request.mjs +52 -44
- package/esm/http/request.mjs.map +1 -1
- package/esm/http/response.d.mts +18 -0
- package/esm/http/response.d.mts.map +1 -1
- package/esm/http/response.mjs +31 -0
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/stream-react-response.d.mts +65 -0
- package/esm/http/stream-react-response.d.mts.map +1 -0
- package/esm/http/stream-react-response.mjs +46 -0
- package/esm/http/stream-react-response.mjs.map +1 -0
- package/esm/http/tracing/index.mjs +4 -0
- package/esm/http/tracing/trace-id.d.mts +14 -0
- package/esm/http/tracing/trace-id.d.mts.map +1 -0
- package/esm/http/tracing/trace-id.mjs +37 -0
- package/esm/http/tracing/trace-id.mjs.map +1 -0
- package/esm/http/tracing/tracing-dispatcher.d.mts +42 -0
- package/esm/http/tracing/tracing-dispatcher.d.mts.map +1 -0
- package/esm/http/tracing/tracing-dispatcher.mjs +94 -0
- package/esm/http/tracing/tracing-dispatcher.mjs.map +1 -0
- package/esm/http/tracing/tracing.type.d.mts +66 -0
- package/esm/http/tracing/tracing.type.d.mts.map +1 -0
- package/esm/http/types.d.mts +27 -21
- package/esm/http/types.d.mts.map +1 -1
- package/esm/index.d.mts +9 -4
- package/esm/index.mjs +7 -3
- package/esm/production/esbuild-preflight.mjs +47 -0
- package/esm/production/esbuild-preflight.mjs.map +1 -0
- package/esm/production/production-builder.mjs +19 -15
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/restful/restful.d.mts +1 -1
- package/esm/router/log-request-lifecycle.mjs +12 -2
- package/esm/router/log-request-lifecycle.mjs.map +1 -1
- package/esm/router/router.d.mts.map +1 -1
- package/esm/router/router.mjs +10 -0
- package/esm/router/router.mjs.map +1 -1
- package/esm/router/types.d.mts +1 -1
- package/esm/socket/utils.mjs +1 -2
- package/esm/socket/utils.mjs.map +1 -1
- package/esm/storage/drivers/cloud-driver.d.mts.map +1 -1
- package/esm/storage/drivers/cloud-driver.mjs +2 -5
- package/esm/storage/drivers/cloud-driver.mjs.map +1 -1
- package/esm/warlock-config/types.d.mts +12 -0
- package/esm/warlock-config/types.d.mts.map +1 -1
- package/llms-full.txt +314 -46
- package/llms.txt +1 -0
- package/package.json +11 -12
- package/skills/add-connector/SKILL.md +1 -1
- package/skills/build-restful/SKILL.md +2 -2
- package/skills/configure-app/SKILL.md +31 -0
- package/skills/create-controller/SKILL.md +4 -4
- package/skills/request-tracing/SKILL.md +208 -0
- package/skills/send-response/SKILL.md +16 -0
- package/skills/store-file/SKILL.md +1 -1
- package/skills/upload-file/SKILL.md +2 -2
- package/skills/use-app-context/SKILL.md +2 -2
- package/skills/use-localization/SKILL.md +24 -21
- package/skills/use-middleware/SKILL.md +2 -2
- package/skills/use-repository/SKILL.md +1 -1
- package/skills/use-request-locals/SKILL.md +1 -1
- package/skills/wire-socket/SKILL.md +3 -3
- package/skills/write-middleware/SKILL.md +11 -5
- package/skills/write-use-case/SKILL.md +1 -1
package/llms-full.txt
CHANGED
|
@@ -233,7 +233,7 @@ See the [Registering a connector](#registering-a-connector) section above for th
|
|
|
233
233
|
|
|
234
234
|
- **Set `this.active = true` only on success.** If `start()` throws partway, leaving `active` true means `shutdown()` thinks it has work to do and may double-close half-initialized resources.
|
|
235
235
|
- **`shutdown()` must be idempotent.** SIGINT can fire twice on Windows. The manager guards re-entry with its own flag, but individual connectors get called once per shutdown loop — guard with `if (!this.active) return`.
|
|
236
|
-
- **Don't reach across connector boundaries in `start()`.** The manager's `start()` loop runs all `boot()`s first, then all `start()`s — wiring across connectors goes through the `container` (`container.get("http.server")`
|
|
236
|
+
- **Don't reach across connector boundaries in `start()`.** The manager's `start()` loop runs all `boot()`s first, then all `start()`s — wiring across connectors goes through the `container`, not through imports. Use `container.tryGet("http.server")` when the other connector is genuinely optional (e.g. socket falls back to its own raw server when HTTP isn't configured); use `container.get("http.server")` when its absence would be a bug — it throws a named `ContainerKeyMissingError` instead of handing back `undefined`.
|
|
237
237
|
- **Production build still needs config registration.** Placing the connector under `src/connectors/<name>.ts` does not auto-register it. Put the same instance in `warlock.config.ts > connectors`; that array is what build-time contribution discovery and runtime boot share.
|
|
238
238
|
- **`watchedFiles` is restart-trigger, not dependency.** It says "I want to restart when this file changes." It does *not* mean the framework reloads that file first — that's the file orchestrator's job.
|
|
239
239
|
|
|
@@ -751,8 +751,8 @@ class OrdersRestful extends Restful<Order> {
|
|
|
751
751
|
protected recordName = "order";
|
|
752
752
|
|
|
753
753
|
protected async beforeCreate(request, response, order) {
|
|
754
|
-
order.set("organization_id", request.user.organizationId);
|
|
755
|
-
order.set("created_by", request.user.id);
|
|
754
|
+
order.set("organization_id", request.locals.user.organizationId);
|
|
755
|
+
order.set("created_by", request.locals.user.id);
|
|
756
756
|
}
|
|
757
757
|
}
|
|
758
758
|
|
|
@@ -1023,11 +1023,42 @@ const httpConfigurations: HttpConfigurations = {
|
|
|
1023
1023
|
path: "/",
|
|
1024
1024
|
},
|
|
1025
1025
|
},
|
|
1026
|
+
csp: {
|
|
1027
|
+
enabled: true,
|
|
1028
|
+
directives: {
|
|
1029
|
+
"img-src": ["'self'", "data:", "https://cdn.example.com"],
|
|
1030
|
+
},
|
|
1031
|
+
},
|
|
1026
1032
|
};
|
|
1027
1033
|
|
|
1028
1034
|
export default httpConfigurations;
|
|
1029
1035
|
```
|
|
1030
1036
|
|
|
1037
|
+
### `csp` — Content-Security-Policy (5.12.0)
|
|
1038
|
+
|
|
1039
|
+
Opt-in (`enabled: true`) `Content-Security-Policy` header, built per-request
|
|
1040
|
+
from a documented default policy plus your `directives`, with the
|
|
1041
|
+
framework's own per-request nonce always added to `script-src`:
|
|
1042
|
+
|
|
1043
|
+
```
|
|
1044
|
+
default-src 'self'; script-src 'self' 'nonce-<request nonce>'; style-src 'self';
|
|
1045
|
+
img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'
|
|
1046
|
+
```
|
|
1047
|
+
|
|
1048
|
+
A directive you name under `directives` **replaces** the default list for
|
|
1049
|
+
that directive (never merged element-wise) — declare the full value list you
|
|
1050
|
+
want. Set `reportOnly: true` to emit `Content-Security-Policy-Report-Only`
|
|
1051
|
+
instead while you observe violations without blocking anything. Absent or
|
|
1052
|
+
`enabled: false` (the default) emits no header at all — no behaviour change
|
|
1053
|
+
for apps that don't opt in.
|
|
1054
|
+
|
|
1055
|
+
A malformed directive value (containing `;`, or with an unbalanced `'`
|
|
1056
|
+
count) throws `InvalidCspDirectiveError` at boot, not silently — fix the
|
|
1057
|
+
value in `src/config/http.ts` rather than expecting the framework to repair
|
|
1058
|
+
it. See [`send-response/SKILL.md`](../send-response/SKILL.md) for how the
|
|
1059
|
+
header lands on the response, and `@warlock.js/web`'s docs for how the
|
|
1060
|
+
same nonce reaches the page's `<script>` tags.
|
|
1061
|
+
|
|
1031
1062
|
## Which layer holds what
|
|
1032
1063
|
|
|
1033
1064
|
| Setting | Layer |
|
|
@@ -1244,7 +1275,7 @@ Scaffold with: `npx warlock generate.controller <module>/<action>` (add `--with-
|
|
|
1244
1275
|
| `request.input("key", default?)` | one field | reading a single param/body field by name |
|
|
1245
1276
|
| `request.all()` | full input object | passing the whole input straight to a service |
|
|
1246
1277
|
| `request.validated()` | schema-typed object (only after schema attached) | controllers with a schema — preferred over `.all()` |
|
|
1247
|
-
| `request.user`
|
|
1278
|
+
| `request.locals.user` | authenticated user | guarded routes (see "Typing a guarded handler") |
|
|
1248
1279
|
| `request.file("key")` | `UploadedFile` | multipart uploads |
|
|
1249
1280
|
| `request.header("X-Foo")` | header value | reading request metadata |
|
|
1250
1281
|
| `request.ip`, `request.userAgent` | strings | logging, device info |
|
|
@@ -1300,7 +1331,7 @@ If validation fails, the framework returns a 400 with an `errors` payload and yo
|
|
|
1300
1331
|
|
|
1301
1332
|
### Typing a guarded handler
|
|
1302
1333
|
|
|
1303
|
-
Routes behind `authMiddleware` need `request.user` typed. Project conventions add a `GuardedRequest<TSchema>` (
|
|
1334
|
+
Routes behind `authMiddleware` need `request.locals.user` typed. Project conventions add a `GuardedRequest<TSchema>` (overriding `locals` to `RequestLocals & { user: User }`) and a paired `GuardedRequestHandler<TSchema>` alias in `app/auth/requests/guarded.request`:
|
|
1304
1335
|
|
|
1305
1336
|
```ts
|
|
1306
1337
|
import { type GuardedRequestHandler } from "app/auth/requests/guarded.request";
|
|
@@ -1310,7 +1341,7 @@ export const createProductController: GuardedRequestHandler<CreateProductSchema>
|
|
|
1310
1341
|
request,
|
|
1311
1342
|
response,
|
|
1312
1343
|
}) => {
|
|
1313
|
-
// request.user is typed
|
|
1344
|
+
// request.locals.user is typed
|
|
1314
1345
|
const product = await createProductService(request.validated());
|
|
1315
1346
|
return response.successCreate({ product });
|
|
1316
1347
|
};
|
|
@@ -1359,7 +1390,7 @@ import { listProductsService } from "../services/list-products.service";
|
|
|
1359
1390
|
export const listProductsController: RequestHandler = async ({ request, response }) => {
|
|
1360
1391
|
const { data: products, pagination } = await listProductsService({
|
|
1361
1392
|
...request.all(),
|
|
1362
|
-
organization_id: request.user.organizationId,
|
|
1393
|
+
organization_id: request.locals.user.organizationId,
|
|
1363
1394
|
});
|
|
1364
1395
|
|
|
1365
1396
|
return response.success({ products, pagination });
|
|
@@ -2986,6 +3017,218 @@ Reusing one key for different result types is a caller bug: TypeScript cannot co
|
|
|
2986
3017
|
- [`create-controller/SKILL.md`](../create-controller/SKILL.md) — consuming request-scoped values from controllers.
|
|
2987
3018
|
|
|
2988
3019
|
|
|
3020
|
+
## request-tracing `@warlock.js/core/request-tracing/SKILL.md`
|
|
3021
|
+
|
|
3022
|
+
---
|
|
3023
|
+
name: request-tracing
|
|
3024
|
+
description: 'Vendor-neutral request tracing hooks (`http.tracing`) — observe request start/end and named phase spans (`route.match`, `middleware`, `validation`, `handler`, `response.write`) without adopting an OTel/vendor dependency in `core`. Covers the `TracingHooks` shape, trace id derivation from an inbound W3C `traceparent` vs `request.id`, that a throwing hook is caught and reported once (never breaks the request), and zero overhead when disabled. Triggers: `http.tracing`, `TracingHooks`, `onRequestStart`, `onRequestEnd`, `onPhase`, `traceparent`, `traceId`, `dispatchPhase`, "instrument requests", "trace a request", "slow phase logging", "APM / OTel bridge for Warlock". Skip: request-id header echo/inheritance (`X-Request-Id`) — `@warlock.js/core/use-middleware/SKILL.md`; app-level structured logging — `@warlock.js/logger/logger-basics/SKILL.md`; competing libs `@opentelemetry/api` direct instrumentation, `express-request-id`, hand-rolled `X-Trace-Id` middleware.'
|
|
3025
|
+
---
|
|
3026
|
+
|
|
3027
|
+
# Warlock — request tracing hooks
|
|
3028
|
+
|
|
3029
|
+
`http.tracing` gives every request a set of named phase spans and start/end
|
|
3030
|
+
events, delivered to plain callbacks you register in config — no
|
|
3031
|
+
`@opentelemetry/api` dependency in `core`, in this release or later (an OTel
|
|
3032
|
+
bridge is a separate, optional package that subscribes to these hooks; it
|
|
3033
|
+
does not exist yet). OFF by default, and a disabled app pays exactly one
|
|
3034
|
+
cached boolean check per call site — no context object is built, no
|
|
3035
|
+
`performance.now()` is called, nothing is allocated.
|
|
3036
|
+
|
|
3037
|
+
## Enabling it
|
|
3038
|
+
|
|
3039
|
+
```ts
|
|
3040
|
+
// src/config/http.ts
|
|
3041
|
+
export default {
|
|
3042
|
+
tracing: {
|
|
3043
|
+
enabled: true, // default false
|
|
3044
|
+
hooks: [
|
|
3045
|
+
{
|
|
3046
|
+
onRequestStart(ctx) {
|
|
3047
|
+
/* ctx: { traceId, requestId, method, route, path } */
|
|
3048
|
+
},
|
|
3049
|
+
onPhase(ctx, phase) {
|
|
3050
|
+
/* phase: { name, durationMs, attrs? } */
|
|
3051
|
+
},
|
|
3052
|
+
onRequestEnd(ctx, result) {
|
|
3053
|
+
/* result: { status?, durationMs, error? } */
|
|
3054
|
+
},
|
|
3055
|
+
},
|
|
3056
|
+
],
|
|
3057
|
+
},
|
|
3058
|
+
};
|
|
3059
|
+
```
|
|
3060
|
+
|
|
3061
|
+
`enabled` is resolved once at first read and cached for the process — flipping
|
|
3062
|
+
it requires a restart, the same trade-off as `http.maintenance.enabled` and
|
|
3063
|
+
`http.requestId.enabled`.
|
|
3064
|
+
|
|
3065
|
+
## The `TracingHooks` shape
|
|
3066
|
+
|
|
3067
|
+
```ts
|
|
3068
|
+
type TracingContext = {
|
|
3069
|
+
traceId: string; // see "Trace id derivation" below
|
|
3070
|
+
requestId: string; // request.id
|
|
3071
|
+
method: string;
|
|
3072
|
+
route?: string; // matched pattern, e.g. "/users/:id" — undefined before routing
|
|
3073
|
+
path: string;
|
|
3074
|
+
};
|
|
3075
|
+
|
|
3076
|
+
type TracingPhaseInfo = {
|
|
3077
|
+
name: string;
|
|
3078
|
+
durationMs: number;
|
|
3079
|
+
attrs?: Record<string, unknown>;
|
|
3080
|
+
};
|
|
3081
|
+
|
|
3082
|
+
type TracingRequestEndInfo = {
|
|
3083
|
+
status?: number;
|
|
3084
|
+
durationMs: number;
|
|
3085
|
+
error?: unknown;
|
|
3086
|
+
};
|
|
3087
|
+
|
|
3088
|
+
type TracingHooks = {
|
|
3089
|
+
onRequestStart?(ctx: TracingContext): void;
|
|
3090
|
+
onRequestEnd?(ctx: TracingContext, result: TracingRequestEndInfo): void;
|
|
3091
|
+
onPhase?(ctx: TracingContext, phase: TracingPhaseInfo): void;
|
|
3092
|
+
};
|
|
3093
|
+
```
|
|
3094
|
+
|
|
3095
|
+
Every verb is optional — a hook that only wants phase spans need not implement
|
|
3096
|
+
`onRequestStart`/`onRequestEnd`. Register as many hooks as you like via
|
|
3097
|
+
`hooks: TracingHooks[]`; each fires independently.
|
|
3098
|
+
|
|
3099
|
+
## Phase names
|
|
3100
|
+
|
|
3101
|
+
`core` wires five phases into the request lifecycle, in this order, for every
|
|
3102
|
+
HTTP request:
|
|
3103
|
+
|
|
3104
|
+
| Phase | Fires around |
|
|
3105
|
+
| --- | --- |
|
|
3106
|
+
| `route.match` | Resolving the incoming path/method to a registered route |
|
|
3107
|
+
| `middleware` | Each middleware in the route's chain — one `onPhase` call per middleware, with `attrs: { name, index }` |
|
|
3108
|
+
| `validation` | The route's input validation (`v.object(...)` / RESTful resource validation) |
|
|
3109
|
+
| `handler` | The route handler itself |
|
|
3110
|
+
| `response.write` | The overall request span, closed once the response has settled (success or thrown error) — this is also where `onRequestEnd` fires |
|
|
3111
|
+
|
|
3112
|
+
`@warlock.js/web` page requests report through this same `onPhase` surface
|
|
3113
|
+
instead of adding a separate hook API. They add three phases:
|
|
3114
|
+
|
|
3115
|
+
| Phase | Fires around |
|
|
3116
|
+
| --- | --- |
|
|
3117
|
+
| `loader` | Each loader level, once per app/layout/page, with `attrs: { level, layoutPath? }` |
|
|
3118
|
+
| `render.shell` | Time from render start until React's shell is ready to stream |
|
|
3119
|
+
| `stream.end` | The whole streamed response, including every `defer()` value settling |
|
|
3120
|
+
|
|
3121
|
+
## Trace id derivation
|
|
3122
|
+
|
|
3123
|
+
`ctx.traceId` is derived once per request:
|
|
3124
|
+
|
|
3125
|
+
1. If the inbound `traceparent` header is a valid W3C version-`00` header
|
|
3126
|
+
(`00-<32 hex>-<16 hex>-<2 hex>`, trace id not all-zero), `traceId` is that
|
|
3127
|
+
header's trace id — so a request already inside someone else's distributed
|
|
3128
|
+
trace keeps the same id through Warlock.
|
|
3129
|
+
2. Otherwise `traceId` falls back to `request.id` (the framework's own
|
|
3130
|
+
per-request correlation id — see
|
|
3131
|
+
[`use-middleware/SKILL.md`](../use-middleware/SKILL.md#request-id-correlation)
|
|
3132
|
+
for how that id is generated/inherited/echoed).
|
|
3133
|
+
|
|
3134
|
+
`ctx.requestId` is always `request.id`, regardless of which branch produced
|
|
3135
|
+
`traceId` — so a hook can always join back to the same id the framework logs
|
|
3136
|
+
and echoes on `X-Request-Id`, even when `traceId` came from an inbound header.
|
|
3137
|
+
|
|
3138
|
+
```ts
|
|
3139
|
+
import { deriveTraceId, parseTraceparentTraceId } from "@warlock.js/core";
|
|
3140
|
+
|
|
3141
|
+
parseTraceparentTraceId("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01");
|
|
3142
|
+
// -> "4bf92f3577b34da6a3ce929d0e0e4736"
|
|
3143
|
+
|
|
3144
|
+
deriveTraceId(undefined, "req-abc123");
|
|
3145
|
+
// -> "req-abc123" (no traceparent, falls back to requestId)
|
|
3146
|
+
```
|
|
3147
|
+
|
|
3148
|
+
## Response header: no new one
|
|
3149
|
+
|
|
3150
|
+
Tracing does **not** add a response header of its own. `core` already echoes
|
|
3151
|
+
`request.id` back as `X-Request-Id` on every response (see
|
|
3152
|
+
[`use-middleware/SKILL.md`](../use-middleware/SKILL.md#request-id-correlation)) —
|
|
3153
|
+
apps correlate through that existing header. When a valid inbound
|
|
3154
|
+
`traceparent` is present, `ctx.traceId` inside your hooks carries that trace
|
|
3155
|
+
id even though the response header still reflects `request.id`; if you need
|
|
3156
|
+
the resolved `traceId` on the wire (e.g. to hand back to a caller that sent
|
|
3157
|
+
`traceparent`), read it from your own `onRequestStart`/`onPhase` hook and set
|
|
3158
|
+
it yourself — `core` deliberately doesn't duplicate it into a second header.
|
|
3159
|
+
|
|
3160
|
+
## A throwing hook never breaks a request
|
|
3161
|
+
|
|
3162
|
+
Every hook call is wrapped: if a hook throws, the dispatcher catches it,
|
|
3163
|
+
reports it once per `(hook, verb)` pair per process to the error sink, and
|
|
3164
|
+
continues with the next hook. A hook that throws on every request does not
|
|
3165
|
+
flood your logs and never turns an observability bug into a 500 for real
|
|
3166
|
+
traffic.
|
|
3167
|
+
|
|
3168
|
+
## Zero overhead when disabled
|
|
3169
|
+
|
|
3170
|
+
`http.tracing.enabled` is resolved once (lazily, on first read) and cached —
|
|
3171
|
+
never re-read per request. Every instrumented call site checks that cached
|
|
3172
|
+
boolean **before** building a context object or calling `performance.now()`,
|
|
3173
|
+
so a disabled app pays exactly one boolean check per phase and allocates
|
|
3174
|
+
nothing extra. Don't wrap `dispatchPhase`/`buildTracingContext` calls in your
|
|
3175
|
+
own extra guard — the check is already there.
|
|
3176
|
+
|
|
3177
|
+
## Example: log phases slower than a threshold
|
|
3178
|
+
|
|
3179
|
+
```ts
|
|
3180
|
+
// src/config/http.ts
|
|
3181
|
+
import { log } from "@warlock.js/logger";
|
|
3182
|
+
|
|
3183
|
+
const SLOW_MS = 200;
|
|
3184
|
+
|
|
3185
|
+
export default {
|
|
3186
|
+
tracing: {
|
|
3187
|
+
enabled: env("TRACING_ENABLED") === "true",
|
|
3188
|
+
hooks: [
|
|
3189
|
+
{
|
|
3190
|
+
onPhase(ctx, phase) {
|
|
3191
|
+
if (phase.durationMs < SLOW_MS) return;
|
|
3192
|
+
|
|
3193
|
+
log.warn("http", "slow-phase", {
|
|
3194
|
+
traceId: ctx.traceId,
|
|
3195
|
+
requestId: ctx.requestId,
|
|
3196
|
+
route: ctx.route,
|
|
3197
|
+
phase: phase.name,
|
|
3198
|
+
durationMs: phase.durationMs,
|
|
3199
|
+
attrs: phase.attrs,
|
|
3200
|
+
});
|
|
3201
|
+
},
|
|
3202
|
+
},
|
|
3203
|
+
],
|
|
3204
|
+
},
|
|
3205
|
+
};
|
|
3206
|
+
```
|
|
3207
|
+
|
|
3208
|
+
## Gotchas
|
|
3209
|
+
|
|
3210
|
+
- **No OTel dependency, and none planned for `core`.** An OTel (or other
|
|
3211
|
+
vendor) bridge is a separate, optional package that subscribes to these
|
|
3212
|
+
hooks — never add `@opentelemetry/api` to `core` itself.
|
|
3213
|
+
- **`web`'s phases aren't live yet.** Don't register a hook expecting
|
|
3214
|
+
`loader`/`render.shell`/`stream.end` calls today; only the five `core`
|
|
3215
|
+
phases fire in this release.
|
|
3216
|
+
- **`route` is `undefined` until routing has matched.** There's no path where
|
|
3217
|
+
a hook fires before that, but code branching on `ctx.route` for an
|
|
3218
|
+
early-failing request (e.g. a 404 with no match) must handle `undefined`.
|
|
3219
|
+
- **`traceId` is not a second correlation id to store separately by default.**
|
|
3220
|
+
It equals `requestId` unless the caller sent a valid `traceparent` — don't
|
|
3221
|
+
assume it's always a 32-hex OTel-shaped value.
|
|
3222
|
+
- **Toggling `enabled` needs a restart.** It's resolved once per process, the
|
|
3223
|
+
same trade-off as `http.maintenance.enabled`.
|
|
3224
|
+
|
|
3225
|
+
## See also
|
|
3226
|
+
|
|
3227
|
+
- [`use-middleware/SKILL.md`](../use-middleware/SKILL.md#request-id-correlation) — `X-Request-Id` inheritance/echo, the header tracing correlates through.
|
|
3228
|
+
- [`use-app-context/SKILL.md`](../use-app-context/SKILL.md) — request-scoped ALS context (`request`/`response`) tracing hooks run inside.
|
|
3229
|
+
- [`@warlock.js/logger/logger-basics/SKILL.md`](../../../logger/skills/logger-basics/SKILL.md) — structured logging; tracing hooks are the place to bridge phase timing into your log channel.
|
|
3230
|
+
|
|
3231
|
+
|
|
2989
3232
|
## resolve-path `@warlock.js/core/resolve-path/SKILL.md`
|
|
2990
3233
|
|
|
2991
3234
|
---
|
|
@@ -4337,6 +4580,22 @@ These are the flags whose absence never fails a test and is fatal in production:
|
|
|
4337
4580
|
|
|
4338
4581
|
These mutate the response in place; chain or call before the final `return response.<helper>()`.
|
|
4339
4582
|
|
|
4583
|
+
### `Content-Security-Policy` (5.12.0)
|
|
4584
|
+
|
|
4585
|
+
Opt in via `http.csp.enabled` in `src/config/http.ts` (see
|
|
4586
|
+
[`configure-app/SKILL.md`](../configure-app/SKILL.md) for the full shape).
|
|
4587
|
+
When enabled, every response through the shared request funnel
|
|
4588
|
+
(`http/middleware/inject-request-context.ts`, the same seam that stamps
|
|
4589
|
+
`X-Request-Id`) gets a `Content-Security-Policy` header — `-Report-Only`
|
|
4590
|
+
instead when `reportOnly: true` — built from the framework's default policy
|
|
4591
|
+
merged with your `directives`, with the current request's CSP nonce always
|
|
4592
|
+
appended to `script-src`. It is a no-op, and adds no header, while `http.csp`
|
|
4593
|
+
is unset — every existing app is unaffected until it opts in.
|
|
4594
|
+
|
|
4595
|
+
A route can still override it — call `response.header("Content-Security-Policy", ...)`
|
|
4596
|
+
from a controller or middleware that runs after the funnel, and your value
|
|
4597
|
+
wins, same as any other header.
|
|
4598
|
+
|
|
4340
4599
|
## Common patterns
|
|
4341
4600
|
|
|
4342
4601
|
### Localized error
|
|
@@ -4768,7 +5027,7 @@ export const uploadAvatarController: GuardedRequestHandler = async ({ request, r
|
|
|
4768
5027
|
return response.badRequest({ error: "missing file" });
|
|
4769
5028
|
}
|
|
4770
5029
|
|
|
4771
|
-
const file = await storage.put(upload, `avatars/${request.user.id}/${upload.name}`);
|
|
5030
|
+
const file = await storage.put(upload, `avatars/${request.locals.user.id}/${upload.name}`);
|
|
4772
5031
|
|
|
4773
5032
|
return response.successCreate({ url: file.url, hash: file.hash });
|
|
4774
5033
|
};
|
|
@@ -5711,7 +5970,7 @@ export const uploadAvatarController: GuardedRequestHandler<UploadAvatarSchema> =
|
|
|
5711
5970
|
.resize(400, 400)
|
|
5712
5971
|
.format("webp")
|
|
5713
5972
|
.quality(85)
|
|
5714
|
-
.save(`avatars/${request.user.id}`);
|
|
5973
|
+
.save(`avatars/${request.locals.user.id}`);
|
|
5715
5974
|
|
|
5716
5975
|
return response.successCreate({ path: file.path, url: file.url });
|
|
5717
5976
|
};
|
|
@@ -5929,7 +6188,7 @@ export const createUploadController: RequestHandler = async ({ request, response
|
|
|
5929
6188
|
|
|
5930
6189
|
const saved = await Promise.all(
|
|
5931
6190
|
files.map((file) =>
|
|
5932
|
-
file.save(`uploads/${request.user.organizationId}`, {
|
|
6191
|
+
file.save(`uploads/${request.locals.user.organizationId}`, {
|
|
5933
6192
|
prefix: { as: "directory", format: "DD-MM-YYYY" },
|
|
5934
6193
|
}),
|
|
5935
6194
|
),
|
|
@@ -6170,7 +6429,7 @@ app.router // the Router singleton
|
|
|
6170
6429
|
app.database // Cascade's DataSource
|
|
6171
6430
|
```
|
|
6172
6431
|
|
|
6173
|
-
Each property is a getter backed by the framework's DI container (`container.
|
|
6432
|
+
Each property is a getter backed by the framework's DI container (`container.tryGet("http.server")` etc. — `router` uses `container.get(...)`, which throws instead, since it is never legitimately absent). The container is populated by connectors during their `boot()`/`start()` phase — read these accessors only after the relevant connector has run. `http`, `socket`, and `database` are thin `container.tryGet(...)` reads, so before the connector boots they return `undefined` (they do **not** throw); chaining off an `undefined` accessor is what blows up. `http` and `socket` are *late*-phase connectors — they boot **after** app code is imported, so these accessors are not populated at the top level of a module's `main.ts`. From inside controllers, services, use-cases, or any code that runs while a request is in flight, every accessor is safe.
|
|
6174
6433
|
|
|
6175
6434
|
Typical uses (note: from runtime code, after bootstrap — not at module-import time):
|
|
6176
6435
|
|
|
@@ -6277,7 +6536,7 @@ cors: {
|
|
|
6277
6536
|
|
|
6278
6537
|
- **`version` is `null` until the first `await`.** The version loader is async (it reads `package.json`). On a cold start before any framework code has run `getWarlockVersion()`, `Application.version` returns `null`. The framework does load it during bootstrap, so anywhere downstream of bootstrap is fine — controllers, services, connectors after `start()`. CLI commands without `preload.bootstrap` may see `null`.
|
|
6279
6538
|
- **`Application` is static, not a DI registration.** Don't try to inject it. There's nothing to inject — it's a class with only static members.
|
|
6280
|
-
- **`app.*` accessors return `undefined` before their connector boots — they don't throw.** `app.socket` / `app.database` / `app.http` are populated by their respective connectors during boot; until then each getter returns `undefined` (a bare `container.
|
|
6539
|
+
- **`app.*` accessors return `undefined` before their connector boots — they don't throw.** `app.socket` / `app.database` / `app.http` are populated by their respective connectors during boot; until then each getter returns `undefined` (a bare `container.tryGet(...)`). Reading them earlier (eager module-load code, the top level of a `main.ts` for the late-phase `http`/`socket`, certain CLI commands without the right `preload.connectors`) hands you `undefined`, and chaining off it throws. Safe everywhere downstream of bootstrap — including inside `Application.onceBooted(...)`. `app.router` is the exception: it reads via `container.get(...)`, which throws a named `ContainerKeyMissingError` instead of handing back `undefined`, because the router is never a legitimate absence.
|
|
6281
6540
|
- **`onceBooted` is a latch, not `events.on`.** A callback registered *after* boot completed still runs (next microtask) instead of silently missing the signal — so register it wherever it reads best, including module top-level in `main.ts`. A listener that throws is caught and logged; it can't break boot or the other listeners.
|
|
6282
6541
|
- **`onShutdown` runs before connectors close, not after.** That ordering is deliberate so cleanup can still use db/cache/http — but it means a hook that hangs delays connector teardown (bounded only by your process manager's kill timeout). Keep teardown fast; for HTTP draining the framework already bounds it via `http.gracefulShutdown.timeout`.
|
|
6283
6542
|
|
|
@@ -6313,7 +6572,7 @@ import { groupedTranslations } from "@mongez/localization";
|
|
|
6313
6572
|
|
|
6314
6573
|
groupedTranslations("products", {
|
|
6315
6574
|
notFound: { en: "Product not found", ar: "المنتج غير موجود" },
|
|
6316
|
-
created:
|
|
6575
|
+
created: { en: "Product created", ar: "تم إنشاء المنتج" },
|
|
6317
6576
|
});
|
|
6318
6577
|
|
|
6319
6578
|
// 2. Look up in a controller / service
|
|
@@ -6338,11 +6597,11 @@ Every module owns its translation namespace under `src/app/<module>/utils/locale
|
|
|
6338
6597
|
import { groupedTranslations } from "@mongez/localization";
|
|
6339
6598
|
|
|
6340
6599
|
groupedTranslations("products", {
|
|
6341
|
-
notFound:
|
|
6342
|
-
outOfStock:
|
|
6343
|
-
created:
|
|
6344
|
-
updated:
|
|
6345
|
-
deleted:
|
|
6600
|
+
notFound: { en: "Product not found", ar: "المنتج غير موجود" },
|
|
6601
|
+
outOfStock: { en: "Product out of stock", ar: "المنتج غير متوفر" },
|
|
6602
|
+
created: { en: "Product created", ar: "تم إنشاء المنتج" },
|
|
6603
|
+
updated: { en: "Product updated", ar: "تم تحديث المنتج" },
|
|
6604
|
+
deleted: { en: "Product deleted", ar: "تم حذف المنتج" },
|
|
6346
6605
|
});
|
|
6347
6606
|
```
|
|
6348
6607
|
|
|
@@ -6389,6 +6648,10 @@ request.trans("products.notFound");
|
|
|
6389
6648
|
|
|
6390
6649
|
All three lookups go through `@mongez/localization`'s `trans()` under the hood, with the locale pulled from the request context (or the global default).
|
|
6391
6650
|
|
|
6651
|
+
### Web `useTrans()` key checking
|
|
6652
|
+
|
|
6653
|
+
When an app uses `@warlock.js/web`, `warlock dev` writes `.warlock/typings/translations.d.ts` from literal `groupedTranslations("group", { key: ... })` registrations. It augments web's `TranslationKeyRegistry`, so `useTrans()("products.notFound")` is checked against registered keys and a typo fails TypeScript. Before the generated file exists, `useTrans()` accepts `string` for a non-breaking first boot. Dynamic groups/keys and placeholders are not inferred.
|
|
6654
|
+
|
|
6392
6655
|
### Locale on a specific lookup
|
|
6393
6656
|
|
|
6394
6657
|
```ts
|
|
@@ -6416,7 +6679,7 @@ Configure the default:
|
|
|
6416
6679
|
|
|
6417
6680
|
```ts title="src/config/app.ts"
|
|
6418
6681
|
export default {
|
|
6419
|
-
localeCode: "en",
|
|
6682
|
+
localeCode: "en", // app-wide default
|
|
6420
6683
|
// ...
|
|
6421
6684
|
};
|
|
6422
6685
|
```
|
|
@@ -6435,20 +6698,19 @@ When a column stores per-locale values as an array:
|
|
|
6435
6698
|
|
|
6436
6699
|
```ts
|
|
6437
6700
|
// Schema (Seal):
|
|
6438
|
-
name_translations: v.array(
|
|
6701
|
+
name_translations: (v.array(
|
|
6439
6702
|
v.object({
|
|
6440
|
-
localeCode: v.string(),
|
|
6703
|
+
localeCode: v.string(), // "en", "ar", ...
|
|
6441
6704
|
value: v.string(),
|
|
6442
|
-
})
|
|
6705
|
+
}),
|
|
6443
6706
|
),
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
}
|
|
6707
|
+
// Stored row (DB):
|
|
6708
|
+
{
|
|
6709
|
+
name_translations: [
|
|
6710
|
+
{ localeCode: "en", value: "Hello World" },
|
|
6711
|
+
{ localeCode: "ar", value: "مرحبا" },
|
|
6712
|
+
],
|
|
6713
|
+
});
|
|
6452
6714
|
```
|
|
6453
6715
|
|
|
6454
6716
|
Pick the right one for the current request:
|
|
@@ -6472,12 +6734,12 @@ getLocalized(
|
|
|
6472
6734
|
```
|
|
6473
6735
|
|
|
6474
6736
|
- **`values`** — the localized-array column.
|
|
6475
|
-
- **`localeCode`**
|
|
6476
|
-
- **`key`**
|
|
6737
|
+
- **`localeCode`** _(optional)_ — pin to a specific locale. Defaults to the current request's locale (reads via `useRequestStore()`).
|
|
6738
|
+
- **`key`** _(default `"value"`)_ — which property of the matched entry to return. Use a different key if your localized objects store the value under a different name.
|
|
6477
6739
|
|
|
6478
6740
|
```ts
|
|
6479
6741
|
const slug = getLocalized(product.get("slug_translations"), undefined, "value");
|
|
6480
|
-
const tagline = getLocalized(product.get("name_translations"), "fr");
|
|
6742
|
+
const tagline = getLocalized(product.get("name_translations"), "fr"); // force French
|
|
6481
6743
|
```
|
|
6482
6744
|
|
|
6483
6745
|
### Use inside a resource for clean per-locale responses
|
|
@@ -6651,7 +6913,7 @@ router.post("/ai/summarize", summarizeController, {
|
|
|
6651
6913
|
|
|
6652
6914
|
## `idempotency` — must run after auth
|
|
6653
6915
|
|
|
6654
|
-
The cache key is `idem:{userType}:{userId|ip}:{key}` so user A can't replay user B's key. That requires `request.user` to be populated, so order it **after** `authMiddleware`:
|
|
6916
|
+
The cache key is `idem:{userType}:{userId|ip}:{key}` so user A can't replay user B's key. That requires `request.locals.user` to be populated, so order it **after** `authMiddleware`:
|
|
6655
6917
|
|
|
6656
6918
|
```ts
|
|
6657
6919
|
import { authMiddleware } from "@warlock.js/auth";
|
|
@@ -6827,7 +7089,7 @@ export default {
|
|
|
6827
7089
|
## Gotchas
|
|
6828
7090
|
|
|
6829
7091
|
- **Bare factory names are not exported.** Always reach for them via `middleware` (`middleware.rateLimit`, not `rateLimitMiddleware`). The internal `*Middleware`-suffixed names are an in-package code-organization detail.
|
|
6830
|
-
- **Idempotency must run after auth.** The cache key includes `request.user` for scope-isolation. Putting it before auth silently falls back to IP-scope for every request.
|
|
7092
|
+
- **Idempotency must run after auth.** The cache key includes `request.locals.user` for scope-isolation. Putting it before auth silently falls back to IP-scope for every request.
|
|
6831
7093
|
- **In-process counters lose state on restart.** `middleware.rateLimit` and `middleware.concurrencyLimit` use module-scoped `Map`s. A redeploy resets every window/counter. For globally-shared limits, use `@fastify/rate-limit` with a Redis store.
|
|
6832
7094
|
- **Idempotency clients must reuse the key across retries.** If your client generates a new UUID on every attempt, idempotency is a no-op. Generate once at "intent" time.
|
|
6833
7095
|
- **`ipFilter` fail-closed.** Empty / unparseable IP = denied. Internal callers (Unix sockets, local processes) need explicit allowlisting.
|
|
@@ -7352,7 +7614,7 @@ import { listFaqsService } from "../services/list-faqs.service";
|
|
|
7352
7614
|
export const listFaqsController: GuardedRequestHandler = async ({ request, response }) => {
|
|
7353
7615
|
const { data, pagination } = await listFaqsService({
|
|
7354
7616
|
...request.all(),
|
|
7355
|
-
organization_id: request.user.organizationId,
|
|
7617
|
+
organization_id: request.locals.user.organizationId,
|
|
7356
7618
|
});
|
|
7357
7619
|
|
|
7358
7620
|
return response.success({ data, pagination });
|
|
@@ -7654,7 +7916,7 @@ Writing `request.locals.organization` does not affect `request.all()`, `request.
|
|
|
7654
7916
|
- Use `request.locals` for per-request data written by middleware and read downstream.
|
|
7655
7917
|
- Use `requestMemo(key, fn)` for lazily computed, single-flight work scoped to the active request.
|
|
7656
7918
|
- Augment `Request` itself only for a genuine new typed member that also has a runtime implementation.
|
|
7657
|
-
-
|
|
7919
|
+
- The authenticated user itself already lives at `request.locals.user` — `@warlock.js/auth` augments `RequestLocals` with a `user?: RequestUser` key and its middleware writes it. Core's own `request.user` was removed in 5.12.0; do not re-add a separate `user` local outside that key.
|
|
7658
7920
|
|
|
7659
7921
|
## Gotchas
|
|
7660
7922
|
|
|
@@ -8468,7 +8730,7 @@ const socketOptions: SocketOptions = {
|
|
|
8468
8730
|
Two ways to get the `socket.io` `Server` instance:
|
|
8469
8731
|
|
|
8470
8732
|
```ts
|
|
8471
|
-
// 1. Via the app runtime accessor — reads container.
|
|
8733
|
+
// 1. Via the app runtime accessor — reads container.tryGet("socket")
|
|
8472
8734
|
import { app } from "@warlock.js/core";
|
|
8473
8735
|
|
|
8474
8736
|
const io = app.socket; // → Server, or undefined before the socket connector boots
|
|
@@ -8479,7 +8741,7 @@ import { getSocketServer } from "@warlock.js/core";
|
|
|
8479
8741
|
const io = getSocketServer(); // → Server | null
|
|
8480
8742
|
```
|
|
8481
8743
|
|
|
8482
|
-
Both read the same DI container slot. `app.socket` is a getter that returns `container.
|
|
8744
|
+
Both read the same DI container slot. `app.socket` is a getter that returns `container.tryGet("socket")` — that's `undefined` (it does **not** throw) until the socket connector has booted. `getSocketServer()` reads the same `container.tryGet("socket")` and returns `null` if absent. Prefer `getSocketServer()` plus a null-guard at any call site that *might* run before the connector boots (module-load code, `main.ts` top level, scripts that skip bootstrap). From a controller, a service, a job — anything downstream of a completed bootstrap — `app.socket` is populated and safe to read directly.
|
|
8483
8745
|
|
|
8484
8746
|
## Wiring `connection` handlers
|
|
8485
8747
|
|
|
@@ -8524,7 +8786,7 @@ import type { GuardedRequestHandler } from "app/auth/requests/guarded.request";
|
|
|
8524
8786
|
import { notifyUserService } from "../services/notify-user.service";
|
|
8525
8787
|
|
|
8526
8788
|
export const sendNotificationController: GuardedRequestHandler = async ({ request, response }) => {
|
|
8527
|
-
await notifyUserService(request.user, request.input("payload"));
|
|
8789
|
+
await notifyUserService(request.locals.user, request.input("payload"));
|
|
8528
8790
|
return response.success({ delivered: true });
|
|
8529
8791
|
};
|
|
8530
8792
|
```
|
|
@@ -9057,7 +9319,8 @@ If you short-circuit, the controller never runs. The response helper you pick (`
|
|
|
9057
9319
|
You can attach arbitrary fields to `request` from a middleware, and they survive into the controller. The cleanest pattern is to extend `Request` via module augmentation in a `.d.ts` and assign in the middleware:
|
|
9058
9320
|
|
|
9059
9321
|
```ts title="src/app/feature-flags/middleware/load-feature-flag.middleware.ts"
|
|
9060
|
-
import type { Middleware, Request
|
|
9322
|
+
import type { Middleware, Request } from "@warlock.js/core";
|
|
9323
|
+
import type { RequestUser } from "@warlock.js/auth";
|
|
9061
9324
|
import { FeatureFlag } from "../models/feature-flag";
|
|
9062
9325
|
|
|
9063
9326
|
declare module "@warlock.js/core" {
|
|
@@ -9066,12 +9329,17 @@ declare module "@warlock.js/core" {
|
|
|
9066
9329
|
}
|
|
9067
9330
|
}
|
|
9068
9331
|
|
|
9069
|
-
export const loadFeatureFlag: Middleware<Request & { user: RequestUser }> = async ({
|
|
9070
|
-
request
|
|
9332
|
+
export const loadFeatureFlag: Middleware<Request & { locals: { user: RequestUser } }> = async ({
|
|
9333
|
+
request,
|
|
9334
|
+
}) => {
|
|
9335
|
+
request.featureFlag = await FeatureFlag.findBy(
|
|
9336
|
+
"organization_id",
|
|
9337
|
+
request.locals.user.organizationId,
|
|
9338
|
+
);
|
|
9071
9339
|
};
|
|
9072
9340
|
```
|
|
9073
9341
|
|
|
9074
|
-
After this middleware runs, `request.featureFlag` is typed inside any downstream middleware or controller. The same pattern is how `@warlock.js/auth`'s `authMiddleware` attaches `request.user` and `request.decodedAccessToken`.
|
|
9342
|
+
After this middleware runs, `request.featureFlag` is typed inside any downstream middleware or controller. The same pattern is how `@warlock.js/auth`'s `authMiddleware` attaches `request.locals.user` and `request.decodedAccessToken`.
|
|
9075
9343
|
|
|
9076
9344
|
## Registration — three scopes
|
|
9077
9345
|
|
|
@@ -9212,7 +9480,7 @@ export const optionalAuth: Middleware = async ({ request, response }) => {
|
|
|
9212
9480
|
|
|
9213
9481
|
- [`use-middleware/SKILL.md`](../use-middleware/SKILL.md) — the built-in middleware catalog (`rateLimit`, `idempotency`, `maxBodySize`, etc.) + request-id correlation.
|
|
9214
9482
|
- [`register-route/SKILL.md`](../register-route/SKILL.md) — where middleware attaches: `router.group` and route-options.
|
|
9215
|
-
- [`create-controller/SKILL.md`](../create-controller/SKILL.md) — how the controller picks up `request.user`, `request.validated()`, etc., set by upstream middleware.
|
|
9483
|
+
- [`create-controller/SKILL.md`](../create-controller/SKILL.md) — how the controller picks up `request.locals.user`, `request.validated()`, etc., set by upstream middleware.
|
|
9216
9484
|
- [`send-response/SKILL.md`](../send-response/SKILL.md) — the response helpers used to short-circuit.
|
|
9217
9485
|
- [`warlock-conventions/SKILL.md`](../warlock-conventions/SKILL.md) — the `guarded()` / `guardedAdmin()` / `publicRoutes()` convention.
|
|
9218
9486
|
|
|
@@ -9794,7 +10062,7 @@ Populate `ctx` from a controller via runtime options:
|
|
|
9794
10062
|
```ts
|
|
9795
10063
|
await cancelOrderUseCase(
|
|
9796
10064
|
{ orderId: request.input("id") },
|
|
9797
|
-
{ ctx: { currentUser: request.user } },
|
|
10065
|
+
{ ctx: { currentUser: request.locals.user } },
|
|
9798
10066
|
);
|
|
9799
10067
|
```
|
|
9800
10068
|
|
package/llms.txt
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
- [process-image](@warlock.js/core/process-image/SKILL.md): Transform images with the `Image` class — resize, crop, rotate, format, quality, watermark, blur, etc. — using a deferred pipeline that runs only at `save()` / `toBuffer()` / `toBase64()` / `toDataUrl()` time. Requires sharp via `warlock add image`. Triggers: `Image`, `Image.fromFile`, `Image.fromBuffer`, `Image.fromUrl`, `.resize`, `.crop`, `.watermark`, `.toBuffer`, `.toDataUrl`, `.apply`; "resize an image", "generate a thumbnail", "watermark a product photo", "build an image pipeline"; typical import `import { Image } from "@warlock.js/core"`. Skip: multipart upload entry — `@warlock.js/core/upload-file/SKILL.md`; storage persistence — `@warlock.js/core/store-file/SKILL.md`; competing libs `sharp` direct, `jimp`, `imagemagick`, `gm`.
|
|
22
22
|
- [register-route](@warlock.js/core/register-route/SKILL.md): Register HTTP routes via @warlock.js/core's router — single routes, prefix groups, middleware-guarded blocks, and RESTful resource chains. Routes always live in `src/app/<module>/routes.ts`. Triggers: `router.get`, `router.post`, `router.prefix`, `router.group`, `router.route`, `guarded`; "add a route", "wire a controller to a URL", "group routes by prefix", "register a RESTful resource"; typical import `import { router } from "@warlock.js/core"`. Skip: handler shape — `@warlock.js/core/create-controller/SKILL.md`; CRUD chain details — `@warlock.js/core/build-restful/SKILL.md`; middleware authoring — `@warlock.js/core/write-middleware/SKILL.md`; competing libs `express`, `fastify`, `koa`, `@nestjs/common`.
|
|
23
23
|
- [request-memo](@warlock.js/core/request-memo/SKILL.md): Memoize async work for one HTTP request with `requestMemo<T>(key, fn)` — the v5 replacement for removed `fromRequest`, with single-flight promise sharing, settled-success reuse, rejection eviction, and no cross-request fallback. Triggers: `requestMemo`, `fromRequest`, `fromRequest removed`, `request-scoped cache`, `single-flight`, `current request memo`; "migrate off fromRequest", "load this once per request", "deduplicate concurrent loaders", "cache a repository lookup during one request"; typical import `import { requestMemo } from "@warlock.js/core"`. Skip: middleware-written request state — `@warlock.js/core/use-request-locals/SKILL.md`; process-wide or cross-request caching — `@warlock.js/core/use-middleware/SKILL.md`; competing patterns: dynamic `request[key]` properties, module-global `Map`, payload `request.get()` / `request.set()`.
|
|
24
|
+
- [request-tracing](@warlock.js/core/request-tracing/SKILL.md): Vendor-neutral request tracing hooks (`http.tracing`) — observe request start/end and named phase spans (`route.match`, `middleware`, `validation`, `handler`, `response.write`) without adopting an OTel/vendor dependency in `core`. Covers the `TracingHooks` shape, trace id derivation from an inbound W3C `traceparent` vs `request.id`, that a throwing hook is caught and reported once (never breaks the request), and zero overhead when disabled. Triggers: `http.tracing`, `TracingHooks`, `onRequestStart`, `onRequestEnd`, `onPhase`, `traceparent`, `traceId`, `dispatchPhase`, "instrument requests", "trace a request", "slow phase logging", "APM / OTel bridge for Warlock". Skip: request-id header echo/inheritance (`X-Request-Id`) — `@warlock.js/core/use-middleware/SKILL.md`; app-level structured logging — `@warlock.js/logger/logger-basics/SKILL.md`; competing libs `@opentelemetry/api` direct instrumentation, `express-request-id`, hand-rolled `X-Trace-Id` middleware.
|
|
24
25
|
- [resolve-path](@warlock.js/core/resolve-path/SKILL.md): Path helpers anchored at `process.cwd()` — `rootPath`, `srcPath`, `appPath`, `configPath`, `publicPath`, `storagePath`, `uploadsPath`, `cachePath`, `logsPath`, `tempPath`, `warlockPath`, `sanitizePath`. Optional `uploads.root` config overrides the uploads anchor. Triggers: `appPath`, `configPath`, `uploadsPath`, `storagePath`, `publicPath`, `cachePath`, `logsPath`, `tempPath`, `sanitizePath`, `paths`; "resolve a path inside src/app", "absolute upload destination", "sanitize a user filename", "ship uploads to a mounted volume"; typical import `import { appPath, uploadsPath } from "@warlock.js/core"`. Skip: HTTP URL helpers — `@warlock.js/core/build-url/SKILL.md`; app metadata — `@warlock.js/core/use-app-context/SKILL.md`; storage abstraction — `@warlock.js/core/store-file/SKILL.md`; competing patterns: `path.join(process.cwd(), ...)`, hand-rolled directory constants.
|
|
25
26
|
- [retry-operation](@warlock.js/core/retry-operation/SKILL.md): Wrap a flaky operation with `retry(fn, options)` — now provided by `@mongez/reinforcements` (not `@warlock.js/core`). `attempts` total tries, `delay` + `backoff` (linear/exponential/fn), `maxDelay`, `jitter`, `shouldRetry` to bail on permanent errors, `signal` to cancel, plus `retryable()` to pre-bind options. Triggers: `retry`, `retryable`, `RetryOptions`, `attempts`, `backoff`, `jitter`, `maxDelay`, `shouldRetry`, `signal`; "retry a flaky API call", "handle transient errors", "exponential backoff with jitter", "wrap an external request"; typical import `import { retry } from "@mongez/reinforcements"`. Skip: timing the retried op — `@warlock.js/core/benchmark-code/SKILL.md`; use-case-level `retry` option — `@warlock.js/core/write-use-case/SKILL.md`; competing libs `p-retry`, `async-retry`, `cockatiel`.
|
|
26
27
|
- [run-app](@warlock.js/core/run-app/SKILL.md): Three operational commands — `warlock dev` (HMR + type-gen + health checks), `warlock build` (esbuild bundle), `warlock start` (spawn the production bundle). All flags, all `warlock.config.ts` knobs that shape them. Triggers: `warlock dev`, `warlock build`, `warlock start`, `devServer`, `--fresh`, `--skip-typings`, `--skip-health`, `outdir`, `outFile`, `sourcemap`, `PortInUseError`, `assertPortIsAvailable`, `EADDRINUSE`; "start the dev server", "build for production", "run the bundle", "skip type generation", "tune watch globs", "dev server keyboard shortcuts", "press r to restart", "press q to quit", "restart the dev server", "port already in use"; typical config `warlock.config.ts > devServer / build`. Skip: writing a custom CLI — `@warlock.js/core/write-cli-command/SKILL.md`; config shape — `@warlock.js/core/configure-app/SKILL.md`; competing tooling `nodemon`, `tsx`, `ts-node-dev`, `esbuild` direct.
|
package/package.json
CHANGED
|
@@ -25,13 +25,12 @@
|
|
|
25
25
|
"@mongez/slug": "^1.0.7",
|
|
26
26
|
"@mongez/supportive-is": "^2.1.4",
|
|
27
27
|
"@mongez/time-wizard": "^1.0.6",
|
|
28
|
-
"@warlock.js/
|
|
29
|
-
"@warlock.js/
|
|
30
|
-
"@warlock.js/
|
|
31
|
-
"@warlock.js/
|
|
32
|
-
"@warlock.js/
|
|
33
|
-
"@warlock.js/
|
|
34
|
-
"@warlock.js/fs": "5.11.0",
|
|
28
|
+
"@warlock.js/cache": "5.13.0",
|
|
29
|
+
"@warlock.js/cascade": "5.13.0",
|
|
30
|
+
"@warlock.js/context": "5.13.0",
|
|
31
|
+
"@warlock.js/logger": "5.13.0",
|
|
32
|
+
"@warlock.js/seal": "5.13.0",
|
|
33
|
+
"@warlock.js/fs": "5.13.0",
|
|
35
34
|
"chokidar": "^5.0.0",
|
|
36
35
|
"dayjs": "^1.11.19",
|
|
37
36
|
"es-module-lexer": "^2.0.0",
|
|
@@ -57,10 +56,10 @@
|
|
|
57
56
|
"react": "^19.2.3",
|
|
58
57
|
"react-dom": "^19.2.3",
|
|
59
58
|
"@react-email/render": "^2.0.5",
|
|
60
|
-
"@warlock.js/herald": "5.
|
|
61
|
-
"@warlock.js/ai": "5.
|
|
62
|
-
"@warlock.js/access": "5.
|
|
63
|
-
"@warlock.js/notifications": "5.
|
|
59
|
+
"@warlock.js/herald": "5.13.0",
|
|
60
|
+
"@warlock.js/ai": "5.13.0",
|
|
61
|
+
"@warlock.js/access": "5.13.0",
|
|
62
|
+
"@warlock.js/notifications": "5.13.0"
|
|
64
63
|
},
|
|
65
64
|
"peerDependenciesMeta": {
|
|
66
65
|
"sharp": {
|
|
@@ -123,7 +122,7 @@
|
|
|
123
122
|
],
|
|
124
123
|
"author": "hassanzohdy",
|
|
125
124
|
"license": "MIT",
|
|
126
|
-
"version": "5.
|
|
125
|
+
"version": "5.13.0",
|
|
127
126
|
"type": "module",
|
|
128
127
|
"main": "./esm/index.mjs",
|
|
129
128
|
"module": "./esm/index.mjs",
|