@warlock.js/core 5.10.0 → 5.12.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.
Files changed (130) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/esm/application/app.d.mts +3 -3
  3. package/esm/application/app.mjs +6 -6
  4. package/esm/application/app.mjs.map +1 -1
  5. package/esm/cli/cli-commands.utils.mjs +7 -3
  6. package/esm/cli/cli-commands.utils.mjs.map +1 -1
  7. package/esm/cli/commands/doctor/checks/health.check.mjs +1 -1
  8. package/esm/cli/commands/doctor/checks/health.check.mjs.map +1 -1
  9. package/esm/connectors/socket-connector.mjs +13 -16
  10. package/esm/connectors/socket-connector.mjs.map +1 -1
  11. package/esm/container/index.d.mts +20 -7
  12. package/esm/container/index.d.mts.map +1 -1
  13. package/esm/container/index.mjs +8 -5
  14. package/esm/container/index.mjs.map +1 -1
  15. package/esm/dev-server/dev-logger.mjs +15 -3
  16. package/esm/dev-server/dev-logger.mjs.map +1 -1
  17. package/esm/dev-server/development-server.mjs +1 -1
  18. package/esm/dev-server/development-server.mjs.map +1 -1
  19. package/esm/dev-server/file-event-handler.mjs +50 -9
  20. package/esm/dev-server/file-event-handler.mjs.map +1 -1
  21. package/esm/dev-server/file-manager.d.mts +8 -0
  22. package/esm/dev-server/file-manager.d.mts.map +1 -1
  23. package/esm/dev-server/file-manager.mjs +26 -4
  24. package/esm/dev-server/file-manager.mjs.map +1 -1
  25. package/esm/dev-server/files-orchestrator.mjs +3 -3
  26. package/esm/dev-server/files-orchestrator.mjs.map +1 -1
  27. package/esm/dev-server/files-watcher.mjs +16 -2
  28. package/esm/dev-server/files-watcher.mjs.map +1 -1
  29. package/esm/dev-server/flags.mjs +16 -1
  30. package/esm/dev-server/flags.mjs.map +1 -1
  31. package/esm/dev-server/layer-executor.mjs +27 -3
  32. package/esm/dev-server/layer-executor.mjs.map +1 -1
  33. package/esm/errors/container-key-missing-error.mjs +18 -6
  34. package/esm/errors/container-key-missing-error.mjs.map +1 -1
  35. package/esm/generations/stubs.mjs +10 -10
  36. package/esm/generations/stubs.mjs.map +1 -1
  37. package/esm/http/context/request-context.d.mts +14 -11
  38. package/esm/http/context/request-context.d.mts.map +1 -1
  39. package/esm/http/context/request-context.mjs +13 -9
  40. package/esm/http/context/request-context.mjs.map +1 -1
  41. package/esm/http/createHttpApplication.d.mts.map +1 -1
  42. package/esm/http/createHttpApplication.mjs +2 -0
  43. package/esm/http/createHttpApplication.mjs.map +1 -1
  44. package/esm/http/csp.d.mts +139 -0
  45. package/esm/http/csp.d.mts.map +1 -0
  46. package/esm/http/csp.mjs +134 -0
  47. package/esm/http/csp.mjs.map +1 -0
  48. package/esm/http/errors/errors.d.mts +17 -1
  49. package/esm/http/errors/errors.d.mts.map +1 -1
  50. package/esm/http/errors/errors.mjs +20 -1
  51. package/esm/http/errors/errors.mjs.map +1 -1
  52. package/esm/http/index.d.mts +8 -3
  53. package/esm/http/index.mjs +6 -1
  54. package/esm/http/middleware/concurrency-limit.middleware.d.mts +2 -2
  55. package/esm/http/middleware/concurrency-limit.middleware.mjs +1 -1
  56. package/esm/http/middleware/concurrency-limit.middleware.mjs.map +1 -1
  57. package/esm/http/middleware/inject-request-context.d.mts +1 -1
  58. package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
  59. package/esm/http/middleware/inject-request-context.mjs +10 -0
  60. package/esm/http/middleware/inject-request-context.mjs.map +1 -1
  61. package/esm/http/middleware/rate-limit.middleware.d.mts +1 -1
  62. package/esm/http/middleware/rate-limit.middleware.mjs.map +1 -1
  63. package/esm/http/middleware/utils/idempotency-key.d.mts.map +1 -1
  64. package/esm/http/middleware/utils/idempotency-key.mjs +13 -13
  65. package/esm/http/middleware/utils/idempotency-key.mjs.map +1 -1
  66. package/esm/http/request-controller.d.mts +1 -1
  67. package/esm/http/request.d.mts +32 -44
  68. package/esm/http/request.d.mts.map +1 -1
  69. package/esm/http/request.mjs +52 -44
  70. package/esm/http/request.mjs.map +1 -1
  71. package/esm/http/response.d.mts +18 -0
  72. package/esm/http/response.d.mts.map +1 -1
  73. package/esm/http/response.mjs +31 -0
  74. package/esm/http/response.mjs.map +1 -1
  75. package/esm/http/stream-react-response.d.mts +65 -0
  76. package/esm/http/stream-react-response.d.mts.map +1 -0
  77. package/esm/http/stream-react-response.mjs +46 -0
  78. package/esm/http/stream-react-response.mjs.map +1 -0
  79. package/esm/http/tracing/index.mjs +4 -0
  80. package/esm/http/tracing/trace-id.d.mts +14 -0
  81. package/esm/http/tracing/trace-id.d.mts.map +1 -0
  82. package/esm/http/tracing/trace-id.mjs +37 -0
  83. package/esm/http/tracing/trace-id.mjs.map +1 -0
  84. package/esm/http/tracing/tracing-dispatcher.d.mts +42 -0
  85. package/esm/http/tracing/tracing-dispatcher.d.mts.map +1 -0
  86. package/esm/http/tracing/tracing-dispatcher.mjs +94 -0
  87. package/esm/http/tracing/tracing-dispatcher.mjs.map +1 -0
  88. package/esm/http/tracing/tracing.type.d.mts +66 -0
  89. package/esm/http/tracing/tracing.type.d.mts.map +1 -0
  90. package/esm/http/types.d.mts +27 -21
  91. package/esm/http/types.d.mts.map +1 -1
  92. package/esm/index.d.mts +9 -4
  93. package/esm/index.mjs +6 -2
  94. package/esm/production/esbuild-preflight.mjs +47 -0
  95. package/esm/production/esbuild-preflight.mjs.map +1 -0
  96. package/esm/production/production-builder.mjs +19 -15
  97. package/esm/production/production-builder.mjs.map +1 -1
  98. package/esm/restful/restful.d.mts +1 -1
  99. package/esm/router/log-request-lifecycle.mjs +12 -2
  100. package/esm/router/log-request-lifecycle.mjs.map +1 -1
  101. package/esm/router/router.d.mts.map +1 -1
  102. package/esm/router/router.mjs +10 -0
  103. package/esm/router/router.mjs.map +1 -1
  104. package/esm/router/types.d.mts +1 -1
  105. package/esm/socket/utils.mjs +1 -2
  106. package/esm/socket/utils.mjs.map +1 -1
  107. package/esm/storage/drivers/cloud-driver.d.mts.map +1 -1
  108. package/esm/storage/drivers/cloud-driver.mjs +2 -5
  109. package/esm/storage/drivers/cloud-driver.mjs.map +1 -1
  110. package/esm/warlock-config/types.d.mts +12 -0
  111. package/esm/warlock-config/types.d.mts.map +1 -1
  112. package/llms-full.txt +315 -25
  113. package/llms.txt +1 -0
  114. package/package.json +12 -12
  115. package/skills/add-connector/SKILL.md +1 -1
  116. package/skills/build-restful/SKILL.md +2 -2
  117. package/skills/configure-app/SKILL.md +31 -0
  118. package/skills/create-controller/SKILL.md +4 -4
  119. package/skills/request-tracing/SKILL.md +208 -0
  120. package/skills/send-response/SKILL.md +16 -0
  121. package/skills/store-file/SKILL.md +1 -1
  122. package/skills/upload-file/SKILL.md +14 -2
  123. package/skills/use-app-context/SKILL.md +2 -2
  124. package/skills/use-middleware/SKILL.md +2 -2
  125. package/skills/use-repository/SKILL.md +1 -1
  126. package/skills/use-request-locals/SKILL.md +1 -1
  127. package/skills/validate-input/SKILL.md +13 -0
  128. package/skills/wire-socket/SKILL.md +3 -3
  129. package/skills/write-middleware/SKILL.md +11 -5
  130. 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")`), not through imports.
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` | authenticated user | guarded routes (see "Typing a guarded handler") |
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>` (adding `user: User`) and a paired `GuardedRequestHandler<TSchema>` alias in `app/auth/requests/guarded.request`:
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
  };
@@ -5884,6 +6143,18 @@ v.file() // must be UploadedFile
5884
6143
 
5885
6144
  Size accepts either bytes (`.maxSize(5_242_880)`) or `{ unit, size }` (`{ unit: "MB", size: 5 }`). See [`validate-input`](../validate-input/SKILL.md) for the full validation pattern.
5886
6145
 
6146
+ ### Optional file field
6147
+
6148
+ Chain `.optional()` in front of the file rules for a field the caller may or may not send — no hand-rolled "was a file attached" check needed:
6149
+
6150
+ ```ts
6151
+ const updateAvatarSchema = v.object({
6152
+ avatar: v.file().optional().image(),
6153
+ });
6154
+ ```
6155
+
6156
+ An absent (or `null`) `avatar` key passes validation with `avatar` coming back `undefined`. A present `avatar` value that isn't a file fails with a normal, structured `avatar` error — the framework never throws for it.
6157
+
5887
6158
  For ad-hoc validation outside a schema:
5888
6159
 
5889
6160
  ```ts
@@ -5917,7 +6188,7 @@ export const createUploadController: RequestHandler = async ({ request, response
5917
6188
 
5918
6189
  const saved = await Promise.all(
5919
6190
  files.map((file) =>
5920
- file.save(`uploads/${request.user.organizationId}`, {
6191
+ file.save(`uploads/${request.locals.user.organizationId}`, {
5921
6192
  prefix: { as: "directory", format: "DD-MM-YYYY" },
5922
6193
  }),
5923
6194
  ),
@@ -6158,7 +6429,7 @@ app.router // the Router singleton
6158
6429
  app.database // Cascade's DataSource
6159
6430
  ```
6160
6431
 
6161
- Each property is a getter backed by the framework's DI container (`container.get("http.server")` etc.). The container is populated by connectors during their `boot()`/`start()` phase — read these accessors only after the relevant connector has run. Every getter is a thin `container.get(...)`, so before the connector boots it returns `undefined` (it does **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.
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.
6162
6433
 
6163
6434
  Typical uses (note: from runtime code, after bootstrap — not at module-import time):
6164
6435
 
@@ -6265,7 +6536,7 @@ cors: {
6265
6536
 
6266
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`.
6267
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.
6268
- - **`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.get(...)`). 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(...)`.
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.
6269
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.
6270
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`.
6271
6542
 
@@ -6639,7 +6910,7 @@ router.post("/ai/summarize", summarizeController, {
6639
6910
 
6640
6911
  ## `idempotency` — must run after auth
6641
6912
 
6642
- 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`:
6913
+ 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`:
6643
6914
 
6644
6915
  ```ts
6645
6916
  import { authMiddleware } from "@warlock.js/auth";
@@ -6815,7 +7086,7 @@ export default {
6815
7086
  ## Gotchas
6816
7087
 
6817
7088
  - **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.
6818
- - **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.
7089
+ - **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.
6819
7090
  - **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.
6820
7091
  - **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.
6821
7092
  - **`ipFilter` fail-closed.** Empty / unparseable IP = denied. Internal callers (Unix sockets, local processes) need explicit allowlisting.
@@ -7340,7 +7611,7 @@ import { listFaqsService } from "../services/list-faqs.service";
7340
7611
  export const listFaqsController: GuardedRequestHandler = async ({ request, response }) => {
7341
7612
  const { data, pagination } = await listFaqsService({
7342
7613
  ...request.all(),
7343
- organization_id: request.user.organizationId,
7614
+ organization_id: request.locals.user.organizationId,
7344
7615
  });
7345
7616
 
7346
7617
  return response.success({ data, pagination });
@@ -7642,7 +7913,7 @@ Writing `request.locals.organization` does not affect `request.all()`, `request.
7642
7913
  - Use `request.locals` for per-request data written by middleware and read downstream.
7643
7914
  - Use `requestMemo(key, fn)` for lazily computed, single-flight work scoped to the active request.
7644
7915
  - Augment `Request` itself only for a genuine new typed member that also has a runtime implementation.
7645
- - Augment `RequestUser` for the authenticated `request.user` shape; do not duplicate it under locals merely to work around typing.
7916
+ - 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.
7646
7917
 
7647
7918
  ## Gotchas
7648
7919
 
@@ -7833,6 +8104,19 @@ const uploadAvatarSchema = v.object({
7833
8104
 
7834
8105
  Full file chain: `.image()`, `.accept(extensions)`, `.mimeType(types)`, `.pdf()`, `.excel()`, `.word()`, `.minSize(n)`, `.maxSize(n)`, `.minWidth(px)`, `.maxWidth(px)`, `.minHeight(px)`, `.maxHeight(px)`. See [`upload-file`](../upload-file/SKILL.md) for the full upload flow.
7835
8106
 
8107
+ ### Optional file field
8108
+
8109
+ `.optional()` composes with `v.file()` the same as any other validator — no need to hand-roll an "if a file was sent" guard in the controller:
8110
+
8111
+ ```ts
8112
+ const updateAvatarSchema = v.object({
8113
+ avatar: v.file().optional().image(),
8114
+ });
8115
+ ```
8116
+
8117
+ - Key absent (or `null`) → valid, `avatar` comes back `undefined`.
8118
+ - Key present but not a file (e.g. a stray string) → invalid, with a normal `avatar` error in the response — it never throws.
8119
+
7836
8120
  ## What the framework sends on failure
7837
8121
 
7838
8122
  The framework calls `response.failedSchema(result)` which sends `400` with the shape configured under `validation.response` (defaults shown):
@@ -8443,7 +8727,7 @@ const socketOptions: SocketOptions = {
8443
8727
  Two ways to get the `socket.io` `Server` instance:
8444
8728
 
8445
8729
  ```ts
8446
- // 1. Via the app runtime accessor — reads container.get("socket")
8730
+ // 1. Via the app runtime accessor — reads container.tryGet("socket")
8447
8731
  import { app } from "@warlock.js/core";
8448
8732
 
8449
8733
  const io = app.socket; // → Server, or undefined before the socket connector boots
@@ -8454,7 +8738,7 @@ import { getSocketServer } from "@warlock.js/core";
8454
8738
  const io = getSocketServer(); // → Server | null
8455
8739
  ```
8456
8740
 
8457
- Both read the same DI container slot. `app.socket` is a getter that returns `container.get("socket")` — that's `undefined` (it does **not** throw) until the socket connector has booted. `getSocketServer()` checks `container.has("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.
8741
+ 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.
8458
8742
 
8459
8743
  ## Wiring `connection` handlers
8460
8744
 
@@ -8499,7 +8783,7 @@ import type { GuardedRequestHandler } from "app/auth/requests/guarded.request";
8499
8783
  import { notifyUserService } from "../services/notify-user.service";
8500
8784
 
8501
8785
  export const sendNotificationController: GuardedRequestHandler = async ({ request, response }) => {
8502
- await notifyUserService(request.user, request.input("payload"));
8786
+ await notifyUserService(request.locals.user, request.input("payload"));
8503
8787
  return response.success({ delivered: true });
8504
8788
  };
8505
8789
  ```
@@ -9032,7 +9316,8 @@ If you short-circuit, the controller never runs. The response helper you pick (`
9032
9316
  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:
9033
9317
 
9034
9318
  ```ts title="src/app/feature-flags/middleware/load-feature-flag.middleware.ts"
9035
- import type { Middleware, Request, RequestUser } from "@warlock.js/core";
9319
+ import type { Middleware, Request } from "@warlock.js/core";
9320
+ import type { RequestUser } from "@warlock.js/auth";
9036
9321
  import { FeatureFlag } from "../models/feature-flag";
9037
9322
 
9038
9323
  declare module "@warlock.js/core" {
@@ -9041,12 +9326,17 @@ declare module "@warlock.js/core" {
9041
9326
  }
9042
9327
  }
9043
9328
 
9044
- export const loadFeatureFlag: Middleware<Request & { user: RequestUser }> = async ({ request }) => {
9045
- request.featureFlag = await FeatureFlag.findBy("organization_id", request.user.organizationId);
9329
+ export const loadFeatureFlag: Middleware<Request & { locals: { user: RequestUser } }> = async ({
9330
+ request,
9331
+ }) => {
9332
+ request.featureFlag = await FeatureFlag.findBy(
9333
+ "organization_id",
9334
+ request.locals.user.organizationId,
9335
+ );
9046
9336
  };
9047
9337
  ```
9048
9338
 
9049
- 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`.
9339
+ 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`.
9050
9340
 
9051
9341
  ## Registration — three scopes
9052
9342
 
@@ -9187,7 +9477,7 @@ export const optionalAuth: Middleware = async ({ request, response }) => {
9187
9477
 
9188
9478
  - [`use-middleware/SKILL.md`](../use-middleware/SKILL.md) — the built-in middleware catalog (`rateLimit`, `idempotency`, `maxBodySize`, etc.) + request-id correlation.
9189
9479
  - [`register-route/SKILL.md`](../register-route/SKILL.md) — where middleware attaches: `router.group` and route-options.
9190
- - [`create-controller/SKILL.md`](../create-controller/SKILL.md) — how the controller picks up `request.user`, `request.validated()`, etc., set by upstream middleware.
9480
+ - [`create-controller/SKILL.md`](../create-controller/SKILL.md) — how the controller picks up `request.locals.user`, `request.validated()`, etc., set by upstream middleware.
9191
9481
  - [`send-response/SKILL.md`](../send-response/SKILL.md) — the response helpers used to short-circuit.
9192
9482
  - [`warlock-conventions/SKILL.md`](../warlock-conventions/SKILL.md) — the `guarded()` / `guardedAdmin()` / `publicRoutes()` convention.
9193
9483
 
@@ -9769,7 +10059,7 @@ Populate `ctx` from a controller via runtime options:
9769
10059
  ```ts
9770
10060
  await cancelOrderUseCase(
9771
10061
  { orderId: request.input("id") },
9772
- { ctx: { currentUser: request.user } },
10062
+ { ctx: { currentUser: request.locals.user } },
9773
10063
  );
9774
10064
  ```
9775
10065
 
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,13 @@
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/auth": "5.10.0",
29
- "@warlock.js/cache": "5.10.0",
30
- "@warlock.js/cascade": "5.10.0",
31
- "@warlock.js/context": "5.10.0",
32
- "@warlock.js/logger": "5.10.0",
33
- "@warlock.js/seal": "5.10.0",
34
- "@warlock.js/fs": "5.10.0",
28
+ "@warlock.js/auth": "5.12.0",
29
+ "@warlock.js/cache": "5.12.0",
30
+ "@warlock.js/cascade": "5.12.0",
31
+ "@warlock.js/context": "5.12.0",
32
+ "@warlock.js/logger": "5.12.0",
33
+ "@warlock.js/seal": "5.12.0",
34
+ "@warlock.js/fs": "5.12.0",
35
35
  "chokidar": "^5.0.0",
36
36
  "dayjs": "^1.11.19",
37
37
  "es-module-lexer": "^2.0.0",
@@ -57,10 +57,10 @@
57
57
  "react": "^19.2.3",
58
58
  "react-dom": "^19.2.3",
59
59
  "@react-email/render": "^2.0.5",
60
- "@warlock.js/herald": "5.10.0",
61
- "@warlock.js/ai": "5.10.0",
62
- "@warlock.js/access": "5.10.0",
63
- "@warlock.js/notifications": "5.10.0"
60
+ "@warlock.js/herald": "5.12.0",
61
+ "@warlock.js/ai": "5.12.0",
62
+ "@warlock.js/access": "5.12.0",
63
+ "@warlock.js/notifications": "5.12.0"
64
64
  },
65
65
  "peerDependenciesMeta": {
66
66
  "sharp": {
@@ -123,7 +123,7 @@
123
123
  ],
124
124
  "author": "hassanzohdy",
125
125
  "license": "MIT",
126
- "version": "5.10.0",
126
+ "version": "5.12.0",
127
127
  "type": "module",
128
128
  "main": "./esm/index.mjs",
129
129
  "module": "./esm/index.mjs",
@@ -225,7 +225,7 @@ See the [Registering a connector](#registering-a-connector) section above for th
225
225
 
226
226
  - **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.
227
227
  - **`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`.
228
- - **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")`), not through imports.
228
+ - **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`.
229
229
  - **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.
230
230
  - **`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.
231
231
 
@@ -243,8 +243,8 @@ class OrdersRestful extends Restful<Order> {
243
243
  protected recordName = "order";
244
244
 
245
245
  protected async beforeCreate(request, response, order) {
246
- order.set("organization_id", request.user.organizationId);
247
- order.set("created_by", request.user.id);
246
+ order.set("organization_id", request.locals.user.organizationId);
247
+ order.set("created_by", request.locals.user.id);
248
248
  }
249
249
  }
250
250