@warlock.js/core 5.20.0 → 5.21.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 +1124 -1086
- package/esm/benchmark/benchmark.mjs +34 -25
- package/esm/benchmark/benchmark.mjs.map +1 -1
- package/esm/benchmark/profiler.d.mts.map +1 -1
- package/esm/benchmark/profiler.mjs +8 -3
- package/esm/benchmark/profiler.mjs.map +1 -1
- package/esm/bootstrap.d.mts.map +1 -1
- package/esm/bootstrap.mjs +3 -0
- package/esm/bootstrap.mjs.map +1 -1
- package/esm/cache/database-cache-driver.d.mts +2 -2
- package/esm/cache/database-cache-driver.d.mts.map +1 -1
- package/esm/cache/database-cache-driver.mjs +3 -2
- package/esm/cache/database-cache-driver.mjs.map +1 -1
- package/esm/cli/cli-commands.manager.mjs +16 -14
- package/esm/cli/cli-commands.manager.mjs.map +1 -1
- package/esm/cli/commands/add.command.mjs +1 -1
- package/esm/cli/commands/add.command.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/optional-peers.check.mjs +3 -2
- package/esm/cli/commands/doctor/checks/optional-peers.check.mjs.map +1 -1
- package/esm/cli/commands/generate/generate.command.mjs +20 -0
- package/esm/cli/commands/generate/generate.command.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/column-dsl-parser.mjs +4 -2
- package/esm/cli/commands/generate/generators/column-dsl-parser.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/controller.generator.mjs +1 -1
- package/esm/cli/commands/generate/generators/controller.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/migration.generator.mjs +22 -6
- package/esm/cli/commands/generate/generators/migration.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/model.generator.mjs +22 -7
- package/esm/cli/commands/generate/generators/model.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/module.generator.mjs +8 -6
- package/esm/cli/commands/generate/generators/module.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/repository.generator.mjs +1 -1
- package/esm/cli/commands/generate/generators/repository.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/resource.generator.mjs +1 -1
- package/esm/cli/commands/generate/generators/resource.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/service.generator.mjs +1 -1
- package/esm/cli/commands/generate/generators/service.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/templates/stubs.mjs +7 -7
- package/esm/cli/commands/generate/templates/stubs.mjs.map +1 -1
- package/esm/cli/commands/generate/utils/writer.mjs +25 -1
- package/esm/cli/commands/generate/utils/writer.mjs.map +1 -1
- package/esm/cli/commands/routes/routes-command.action.mjs +5 -1
- package/esm/cli/commands/routes/routes-command.action.mjs.map +1 -1
- package/esm/cli/commands/routes/with-console-on-stderr.mjs +24 -0
- package/esm/cli/commands/routes/with-console-on-stderr.mjs.map +1 -0
- package/esm/cli/commands/routes.command.mjs +3 -4
- package/esm/cli/commands/routes.command.mjs.map +1 -1
- package/esm/cli/commands/start-production.command.mjs +5 -5
- package/esm/cli/commands/start-production.command.mjs.map +1 -1
- package/esm/cli/commands/storage-put.action.mjs +2 -1
- package/esm/cli/commands/storage-put.action.mjs.map +1 -1
- package/esm/cli/commands/update.command.mjs +1 -0
- package/esm/cli/commands/update.command.mjs.map +1 -1
- package/esm/cli/parse-cli-args.mjs +13 -1
- package/esm/cli/parse-cli-args.mjs.map +1 -1
- package/esm/commands/cli-command.d.mts.map +1 -1
- package/esm/commands/cli-command.mjs +4 -2
- package/esm/commands/cli-command.mjs.map +1 -1
- package/esm/commands/index.mjs +3 -0
- package/esm/config/config-key-from-path.mjs +20 -0
- package/esm/config/config-key-from-path.mjs.map +1 -0
- package/esm/config/config-loader.d.mts.map +1 -1
- package/esm/config/config-loader.mjs +2 -3
- package/esm/config/config-loader.mjs.map +1 -1
- package/esm/connectors/connectors-manager.d.mts +4 -0
- package/esm/connectors/connectors-manager.d.mts.map +1 -1
- package/esm/connectors/connectors-manager.mjs +30 -2
- package/esm/connectors/connectors-manager.mjs.map +1 -1
- package/esm/connectors/socket-connector.d.mts +29 -0
- package/esm/connectors/socket-connector.d.mts.map +1 -1
- package/esm/connectors/socket-connector.mjs +73 -9
- package/esm/connectors/socket-connector.mjs.map +1 -1
- package/esm/connectors/types.d.mts +1 -1
- package/esm/connectors/types.mjs.map +1 -1
- package/esm/database/models/database-log/database-log.d.mts +47 -24
- package/esm/database/models/database-log/database-log.d.mts.map +1 -1
- package/esm/database/models/database-log/database-log.mjs +36 -4
- package/esm/database/models/database-log/database-log.mjs.map +1 -1
- package/esm/database/seed-command-action.mjs +10 -2
- package/esm/database/seed-command-action.mjs.map +1 -1
- package/esm/dev-server/development-server.mjs +11 -4
- package/esm/dev-server/development-server.mjs.map +1 -1
- package/esm/dev-server/file-event-handler.mjs +26 -6
- package/esm/dev-server/file-event-handler.mjs.map +1 -1
- package/esm/dev-server/file-manager.d.mts.map +1 -1
- package/esm/dev-server/file-manager.mjs +2 -2
- package/esm/dev-server/file-manager.mjs.map +1 -1
- package/esm/dev-server/files-orchestrator.mjs +3 -1
- package/esm/dev-server/files-orchestrator.mjs.map +1 -1
- package/esm/dev-server/files-watcher.mjs +64 -5
- package/esm/dev-server/files-watcher.mjs.map +1 -1
- package/esm/dev-server/health-checker/files-healthcare.manager.mjs +31 -9
- package/esm/dev-server/health-checker/files-healthcare.manager.mjs.map +1 -1
- package/esm/dev-server/health-checker/workers/eslint-health.worker.mjs +11 -7
- package/esm/dev-server/health-checker/workers/eslint-health.worker.mjs.map +1 -1
- package/esm/dev-server/loader/load-hook.mjs +1 -1
- package/esm/dev-server/loader/register-loader.mjs +1 -1
- package/esm/dev-server/manifest-manager.d.mts.map +1 -1
- package/esm/dev-server/manifest-manager.mjs +12 -6
- package/esm/dev-server/manifest-manager.mjs.map +1 -1
- package/esm/dev-server/parse-imports.mjs +9 -0
- package/esm/dev-server/parse-imports.mjs.map +1 -1
- package/esm/dev-server/register-dev-shortcuts.mjs +2 -1
- package/esm/dev-server/register-dev-shortcuts.mjs.map +1 -1
- package/esm/dev-server/shortcuts.mjs +2 -2
- package/esm/dev-server/shortcuts.mjs.map +1 -1
- package/esm/dev-server/special-file-patterns.mjs +18 -0
- package/esm/dev-server/special-file-patterns.mjs.map +1 -0
- package/esm/dev-server/special-files-collector.d.mts.map +1 -1
- package/esm/dev-server/special-files-collector.mjs +2 -5
- package/esm/dev-server/special-files-collector.mjs.map +1 -1
- package/esm/dev-server/start-development-server.mjs +1 -2
- package/esm/dev-server/start-development-server.mjs.map +1 -1
- package/esm/dev-server/supervisor.mjs +23 -3
- package/esm/dev-server/supervisor.mjs.map +1 -1
- package/esm/dev-server/tsconfig-manager.mjs +22 -12
- package/esm/dev-server/tsconfig-manager.mjs.map +1 -1
- package/esm/dev-server/type-generator.mjs +10 -9
- package/esm/dev-server/type-generator.mjs.map +1 -1
- package/esm/encryption/encrypt.d.mts.map +1 -1
- package/esm/encryption/encrypt.mjs +16 -5
- package/esm/encryption/encrypt.mjs.map +1 -1
- package/esm/encryption/hash.d.mts.map +1 -1
- package/esm/encryption/hash.mjs +2 -0
- package/esm/encryption/hash.mjs.map +1 -1
- package/esm/encryption/password.d.mts.map +1 -1
- package/esm/encryption/password.mjs +3 -1
- package/esm/encryption/password.mjs.map +1 -1
- package/esm/generations/features/index.mjs +1 -1
- package/esm/generations/features/mysql.feature.mjs +9 -2
- package/esm/generations/features/mysql.feature.mjs.map +1 -1
- package/esm/generations/features/test.feature.mjs +2 -1
- package/esm/generations/features/test.feature.mjs.map +1 -1
- package/esm/generations/features/web.feature.mjs +4 -2
- package/esm/generations/features/web.feature.mjs.map +1 -1
- package/esm/http/csrf-origin-policy.d.mts.map +1 -1
- package/esm/http/csrf-origin-policy.mjs +5 -2
- package/esm/http/csrf-origin-policy.mjs.map +1 -1
- package/esm/http/flush-pending-cookies.mjs +52 -0
- package/esm/http/flush-pending-cookies.mjs.map +1 -0
- package/esm/http/middleware/cache-response-middleware.d.mts.map +1 -1
- package/esm/http/middleware/cache-response-middleware.mjs +4 -2
- package/esm/http/middleware/cache-response-middleware.mjs.map +1 -1
- package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs +14 -3
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/middleware/utils/cidr-match.d.mts +1 -1
- package/esm/http/middleware/utils/cidr-match.d.mts.map +1 -1
- package/esm/http/middleware/utils/cidr-match.mjs +11 -2
- package/esm/http/middleware/utils/cidr-match.mjs.map +1 -1
- package/esm/http/middleware/utils/idempotency-key.mjs +1 -1
- package/esm/http/middleware/utils/idempotency-key.mjs.map +1 -1
- package/esm/http/plugins.d.mts.map +1 -1
- package/esm/http/plugins.mjs +10 -1
- package/esm/http/plugins.mjs.map +1 -1
- package/esm/http/request.d.mts +12 -1
- package/esm/http/request.d.mts.map +1 -1
- package/esm/http/request.mjs +35 -7
- package/esm/http/request.mjs.map +1 -1
- package/esm/http/response.d.mts +16 -0
- package/esm/http/response.d.mts.map +1 -1
- package/esm/http/response.mjs +75 -37
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/types.d.mts +24 -0
- package/esm/http/types.d.mts.map +1 -1
- package/esm/http/uploads/uploaded-file.controller.mjs +1 -1
- package/esm/http/uploads-config.mjs +2 -1
- package/esm/http/uploads-config.mjs.map +1 -1
- package/esm/http/uploads-types.d.mts +3 -3
- package/esm/image/image.d.mts +1 -1
- package/esm/image/image.d.mts.map +1 -1
- package/esm/image/image.mjs +25 -15
- package/esm/image/image.mjs.map +1 -1
- package/esm/index.d.mts +4 -3
- package/esm/index.mjs +5 -3
- package/esm/logger/logger.mjs +6 -1
- package/esm/logger/logger.mjs.map +1 -1
- package/esm/mail/config.d.mts.map +1 -1
- package/esm/mail/config.mjs +31 -21
- package/esm/mail/config.mjs.map +1 -1
- package/esm/mail/react-mail.mjs +6 -3
- package/esm/mail/react-mail.mjs.map +1 -1
- package/esm/mail/send-mail.d.mts.map +1 -1
- package/esm/mail/send-mail.mjs +40 -19
- package/esm/mail/send-mail.mjs.map +1 -1
- package/esm/production/build-contributions.mjs +3 -0
- package/esm/production/build-contributions.mjs.map +1 -1
- package/esm/production/esbuild-plugins.mjs +3 -2
- package/esm/production/esbuild-plugins.mjs.map +1 -1
- package/esm/production/production-builder.mjs +32 -36
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/production/production-supervisor.mjs +12 -5
- package/esm/production/production-supervisor.mjs.map +1 -1
- package/esm/repositories/adapters/cascade/cascade-adapter.d.mts +3 -1
- package/esm/repositories/adapters/cascade/cascade-adapter.d.mts.map +1 -1
- package/esm/repositories/adapters/cascade/cascade-adapter.mjs +17 -5
- package/esm/repositories/adapters/cascade/cascade-adapter.mjs.map +1 -1
- package/esm/repositories/adapters/cascade/cascade-query-builder.d.mts +3 -3
- package/esm/repositories/adapters/cascade/cascade-query-builder.mjs.map +1 -1
- package/esm/repositories/adapters/cascade/filter-applicator.mjs +88 -61
- package/esm/repositories/adapters/cascade/filter-applicator.mjs.map +1 -1
- package/esm/repositories/adapters/cascade/index.d.mts +1 -1
- package/esm/repositories/adapters/cascade/index.mjs +1 -1
- package/esm/repositories/contracts/query-builder.contract.d.mts +3 -3
- package/esm/repositories/contracts/types.d.mts +5 -0
- package/esm/repositories/contracts/types.d.mts.map +1 -1
- package/esm/repositories/index.d.mts +1 -1
- package/esm/repositories/index.mjs +1 -1
- package/esm/repositories/repository.manager.d.mts +65 -3
- package/esm/repositories/repository.manager.d.mts.map +1 -1
- package/esm/repositories/repository.manager.mjs +146 -57
- package/esm/repositories/repository.manager.mjs.map +1 -1
- package/esm/resource/resource-field-builder.d.mts.map +1 -1
- package/esm/resource/resource-field-builder.mjs +1 -0
- package/esm/resource/resource-field-builder.mjs.map +1 -1
- package/esm/resource/resource.d.mts +10 -3
- package/esm/resource/resource.d.mts.map +1 -1
- package/esm/resource/resource.mjs +19 -5
- package/esm/resource/resource.mjs.map +1 -1
- package/esm/restful/restful.d.mts +12 -5
- package/esm/restful/restful.d.mts.map +1 -1
- package/esm/restful/restful.mjs +47 -16
- package/esm/restful/restful.mjs.map +1 -1
- package/esm/router/build-route-rate-limit.mjs +22 -0
- package/esm/router/build-route-rate-limit.mjs.map +1 -0
- package/esm/router/dev-dispatch.mjs +60 -0
- package/esm/router/dev-dispatch.mjs.map +1 -0
- package/esm/router/route-builder.mjs +4 -1
- package/esm/router/route-builder.mjs.map +1 -1
- package/esm/router/route-registry.d.mts.map +1 -1
- package/esm/router/route-registry.mjs +2 -2
- package/esm/router/route-registry.mjs.map +1 -1
- package/esm/router/router.d.mts +1 -1
- package/esm/router/router.d.mts.map +1 -1
- package/esm/router/router.mjs +57 -39
- package/esm/router/router.mjs.map +1 -1
- package/esm/socket/types.d.mts +9 -2
- package/esm/socket/types.d.mts.map +1 -1
- package/esm/storage/drivers/cloud-driver.d.mts +17 -0
- package/esm/storage/drivers/cloud-driver.d.mts.map +1 -1
- package/esm/storage/drivers/cloud-driver.mjs +53 -17
- package/esm/storage/drivers/cloud-driver.mjs.map +1 -1
- package/esm/storage/drivers/local-driver.d.mts +7 -5
- package/esm/storage/drivers/local-driver.d.mts.map +1 -1
- package/esm/storage/drivers/local-driver.mjs +31 -26
- package/esm/storage/drivers/local-driver.mjs.map +1 -1
- package/esm/storage/index.d.mts +2 -1
- package/esm/storage/index.mjs +2 -1
- package/esm/storage/scoped-storage.d.mts +13 -1
- package/esm/storage/scoped-storage.d.mts.map +1 -1
- package/esm/storage/scoped-storage.mjs +18 -8
- package/esm/storage/scoped-storage.mjs.map +1 -1
- package/esm/storage/storage.d.mts +0 -5
- package/esm/storage/storage.d.mts.map +1 -1
- package/esm/storage/storage.mjs +7 -16
- package/esm/storage/storage.mjs.map +1 -1
- package/esm/storage/types.d.mts +22 -4
- package/esm/storage/types.d.mts.map +1 -1
- package/esm/storage/utils/classify-ip-address.d.mts +13 -0
- package/esm/storage/utils/classify-ip-address.d.mts.map +1 -0
- package/esm/storage/utils/classify-ip-address.mjs +98 -0
- package/esm/storage/utils/classify-ip-address.mjs.map +1 -0
- package/esm/storage/utils/safe-fetch.d.mts +3 -12
- package/esm/storage/utils/safe-fetch.d.mts.map +1 -1
- package/esm/storage/utils/safe-fetch.mjs +3 -48
- package/esm/storage/utils/safe-fetch.mjs.map +1 -1
- package/esm/updater/update-warlock-packages.mjs +11 -1
- package/esm/updater/update-warlock-packages.mjs.map +1 -1
- package/esm/use-cases/use-case-broadcast.mjs +7 -1
- package/esm/use-cases/use-case-broadcast.mjs.map +1 -1
- package/esm/use-cases/use-cases-registry.mjs +21 -2
- package/esm/use-cases/use-cases-registry.mjs.map +1 -1
- package/esm/utils/database-log.mjs +0 -3
- package/esm/utils/database-log.mjs.map +1 -1
- package/esm/utils/queue.d.mts +6 -4
- package/esm/utils/queue.d.mts.map +1 -1
- package/esm/utils/queue.mjs +20 -9
- package/esm/utils/queue.mjs.map +1 -1
- package/esm/validation/database/exists-except-current-id.mjs +6 -2
- package/esm/validation/database/exists-except-current-id.mjs.map +1 -1
- package/esm/validation/database/unique-except-current-id.mjs +6 -2
- package/esm/validation/database/unique-except-current-id.mjs.map +1 -1
- package/esm/validation/init.mjs +6 -2
- package/esm/validation/init.mjs.map +1 -1
- package/esm/validation/validateAll.mjs +3 -2
- package/esm/validation/validateAll.mjs.map +1 -1
- package/esm/warlock-config/types.d.mts +14 -1
- package/esm/warlock-config/types.d.mts.map +1 -1
- package/esm/warlock-config/warlock-config.manager.d.mts.map +1 -1
- package/esm/warlock-config/warlock-config.manager.mjs +3 -1
- package/esm/warlock-config/warlock-config.manager.mjs.map +1 -1
- package/esm/warlock-config/warn-deprecated-config.mjs +25 -0
- package/esm/warlock-config/warn-deprecated-config.mjs.map +1 -0
- package/package.json +11 -11
- package/esm/_virtual/_rolldown/runtime.mjs +0 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,49 @@
|
|
|
1
|
-
# Changelog — @warlock.js/core
|
|
2
|
-
|
|
3
|
-
All notable changes to `@warlock.js/core` are documented in this file.
|
|
4
|
-
|
|
5
|
-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
|
|
6
|
-
|
|
7
|
-
> ⚠ **Versioning: `@warlock.js/*` does not follow SemVer strictly — breaking changes may ship in a minor.** This is a deliberate decision, not an oversight: the framework is pre-adoption and the cost of a major per behaviour fix currently outweighs the benefit. **Pin an exact version or a tilde range (`~4.13.0`) if you need to opt into changes rather than receive them.** Every breaking change is marked **BREAKING** in its entry and summarised in an _Upgrading_ section at the top of the release. **This policy will change once the framework has consumers beyond its author.**
|
|
8
|
-
|
|
1
|
+
# Changelog — @warlock.js/core
|
|
2
|
+
|
|
3
|
+
All notable changes to `@warlock.js/core` are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
|
|
6
|
+
|
|
7
|
+
> ⚠ **Versioning: `@warlock.js/*` does not follow SemVer strictly — breaking changes may ship in a minor.** This is a deliberate decision, not an oversight: the framework is pre-adoption and the cost of a major per behaviour fix currently outweighs the benefit. **Pin an exact version or a tilde range (`~4.13.0`) if you need to opt into changes rather than receive them.** Every breaking change is marked **BREAKING** in its entry and summarised in an _Upgrading_ section at the top of the release. **This policy will change once the framework has consumers beyond its author.**
|
|
8
|
+
|
|
9
|
+
## 5.21.0 - 2026-09-25
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `app.shutdownTimeout` (default 10s) — an overall shutdown budget. HTTP stops accepting and drains first, then the other connectors stop in reverse priority; if the budget runs out, the hung connector is logged and the process exits with code 1. In shared mode, closing socket.io no longer closes the Fastify server.
|
|
14
|
+
- `putFromPath(localPath, storagePath)` on storage — uploads a local file by path.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **BREAKING:** `put(string)` now always stores the string as **content**. To upload a local file, use `putFromPath()`.
|
|
19
|
+
- **BREAKING:** multipart limit violations now respond `413`.
|
|
20
|
+
- **BREAKING:** `hmacKey` is validated — a non-hex key throws instead of silently hashing with an empty key. Set a hex `encryption.hmacKey`.
|
|
21
|
+
- **BREAKING:** `DatabaseLogModel` fields are now `module`/`action`/`content`/`stack`/`date`; legacy `message`/`trace` are still read. Update any code that queries the old fields directly.
|
|
22
|
+
- **BREAKING:** SMTP `secure` now defaults from the port (465 → implicit TLS) when not set explicitly.
|
|
23
|
+
- **BREAKING:** the page cache keeps the request path's case, and the HTML variant is not cached under a CSP. Middleware (app, layout, page) now runs before a page-cache hit is served.
|
|
24
|
+
- **BREAKING:** `list(request.all())` honours only a whitelist of control keys, and `exists()` defaults to the primary key. Pass an explicit column if you relied on another one.
|
|
25
|
+
- **BREAKING:** `Restful` saves validated data only — fields missing from the validation schema are dropped. Add them to the schema if they must be persisted.
|
|
26
|
+
- **BREAKING:** mail mode defaults to `"development"` (log/preview, no real send) outside production, unless `mail.sendInDevelopment === true` or `setMailMode` was called.
|
|
27
|
+
- **BREAKING:** unhandled request errors no longer print via `console.error`; they reach configured logger channels only. Configure a channel to keep that visibility.
|
|
28
|
+
- Logger `enabled: false` (top-level or per-environment) now silences the channel, and the `test` environment block is honoured.
|
|
29
|
+
- `generate.model` migration naming is corrected, `--with-resource` also creates the resource file, and `gen.migration` refuses to run when the target model file is missing.
|
|
30
|
+
- `warlock <cmd> -v` reaches the command instead of printing the version; `--help` always rescans plugin and project commands. Resolved CLI `option.name` is always camelCase.
|
|
31
|
+
- `image.fromUrl()` throws `StorageError` for private/reserved hosts, disallowed schemes and oversized (>50MiB) or slow (>30s) bodies.
|
|
32
|
+
- Routes with `rateLimit.errorMessage` return that message on `429`. The upload default prefix format is now `DD-MM-YYYY-HH-mm-ss`.
|
|
33
|
+
- `flushPendingCookies` throws instead of silently dropping cookies when the parked-cookie symbol is missing. Later `bootstrap()` calls no longer re-register the unhandled-rejection listener.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- The dev health checker now lints projects using `eslint.config.ts`/`.mts`/`.cts`.
|
|
38
|
+
- An explicit `HEAD` route now takes precedence over Fastify's automatic HEAD
|
|
39
|
+
registration for a `GET` route at the same path. GET-only routes retain
|
|
40
|
+
Fastify's implicit HEAD behavior.
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- Lockstep release maintenance and dependency refresh.
|
|
46
|
+
|
|
9
47
|
## 5.20.0 - 2026-09-24
|
|
10
48
|
|
|
11
49
|
### Added
|
|
@@ -14,1085 +52,1085 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
14
52
|
`ConnectorBuildContext.namedApiRoutes` snapshot when Web is configured. It
|
|
15
53
|
contains only named API `name`, `path`, and `method` records, never handlers,
|
|
16
54
|
middleware, schemas, or source paths.
|
|
17
|
-
|
|
18
|
-
- `storage.putIfAbsent(file, location, options?)` and `storage.supportsPutIfAbsent()` — an atomic create-only write. Returns the `StorageFile`, or `null` when something already exists at `location`. Unlike `put`, a string argument is **content**, not a path. Local driver writes a temp file then hard-links it; S3/R2 send `If-None-Match: *`. DigitalOcean Spaces does not expose it. Drivers without it throw `StorageCapabilityError`.
|
|
19
|
-
- `http.rateLimit` now passes `@fastify/rate-limit` options through (`redis` for a store shared across servers, `nameSpace`, `keyGenerator`, `allowList`, ...). `http.rateLimit.enabled: false` turns the global limiter off. The per-route `middleware.rateLimit()` stays in-process.
|
|
20
|
-
- `socket.adapter` — an adapter factory (e.g. `@socket.io/redis-adapter`) applied via `io.adapter()` for broadcasting across servers. Polling clients need sticky sessions.
|
|
21
|
-
- Production single-server warnings, once at boot, when the default cache driver is in-memory, the default storage driver is local, or sockets have no `socket.adapter`. Silence with `cache.silenceSingleServerWarning`, `storage.silenceSingleServerWarning`, `socket.silenceSingleServerWarning`.
|
|
22
|
-
- `middleware.idempotency({ reservationTtl })` (also `http.idempotency.reservationTtl`, default `60` seconds) — how long the in-flight reservation lives.
|
|
23
|
-
- Docs: "Running on multiple servers" guide.
|
|
24
|
-
|
|
55
|
+
|
|
56
|
+
- `storage.putIfAbsent(file, location, options?)` and `storage.supportsPutIfAbsent()` — an atomic create-only write. Returns the `StorageFile`, or `null` when something already exists at `location`. Unlike `put`, a string argument is **content**, not a path. Local driver writes a temp file then hard-links it; S3/R2 send `If-None-Match: *`. DigitalOcean Spaces does not expose it. Drivers without it throw `StorageCapabilityError`.
|
|
57
|
+
- `http.rateLimit` now passes `@fastify/rate-limit` options through (`redis` for a store shared across servers, `nameSpace`, `keyGenerator`, `allowList`, ...). `http.rateLimit.enabled: false` turns the global limiter off. The per-route `middleware.rateLimit()` stays in-process.
|
|
58
|
+
- `socket.adapter` — an adapter factory (e.g. `@socket.io/redis-adapter`) applied via `io.adapter()` for broadcasting across servers. Polling clients need sticky sessions.
|
|
59
|
+
- Production single-server warnings, once at boot, when the default cache driver is in-memory, the default storage driver is local, or sockets have no `socket.adapter`. Silence with `cache.silenceSingleServerWarning`, `storage.silenceSingleServerWarning`, `socket.silenceSingleServerWarning`.
|
|
60
|
+
- `middleware.idempotency({ reservationTtl })` (also `http.idempotency.reservationTtl`, default `60` seconds) — how long the in-flight reservation lives.
|
|
61
|
+
- Docs: "Running on multiple servers" guide.
|
|
62
|
+
|
|
25
63
|
### Changed
|
|
26
64
|
|
|
27
65
|
- Refined connector build guidance and grouped skill discovery.
|
|
28
66
|
- `middleware.idempotency()` reserves the key (create-only) before the handler runs. A concurrent duplicate now gets **409 + `Retry-After`** instead of running twice. A 5xx response frees the key so the client can retry. If the cache is down the middleware fails open.
|
|
29
|
-
|
|
30
|
-
### Fixed
|
|
31
|
-
|
|
32
|
-
- The repository cache is also cleared after the DB transaction commits (cascade `afterCommit`), fixing stale reads when a concurrent read re-cached the old row between the model event and `COMMIT`.
|
|
33
|
-
|
|
34
|
-
## 5.19.1 - 2026-09-23
|
|
35
|
-
|
|
36
|
-
### Changed
|
|
37
|
-
|
|
38
|
-
- Lockstep patch release aligning `@warlock.js/core` with the 5.19.1 family; no Core runtime API change.
|
|
39
|
-
|
|
40
|
-
## 5.19.0 - 2026-09-23
|
|
41
|
-
|
|
42
|
-
### Added
|
|
43
|
-
|
|
44
|
-
- `router.getNamedApiRoutes()` returns fresh frozen browser-safe snapshots of
|
|
45
|
-
registered named API routes (`name`, `path`, `method`). It excludes pages,
|
|
46
|
-
handlers, middleware, schemas, and source paths; `all` remains visible as
|
|
47
|
-
metadata so a browser consumer can require an explicit request method.
|
|
48
|
-
|
|
49
|
-
### Changed
|
|
50
|
-
|
|
51
|
-
- Refined package skill-discovery descriptions and regenerated the llms projections.
|
|
52
|
-
|
|
53
|
-
## 5.18.0 - 2026-09-22
|
|
54
|
-
|
|
55
|
-
### Fixed
|
|
56
|
-
|
|
57
|
-
- Development model loading now shares model ownership and reload notifications with Web's SSR adapter, allowing pages to reuse the same model constructors as Core instead of registering a second copy.
|
|
58
|
-
|
|
59
|
-
## 5.17.1 - 2026-09-22
|
|
60
|
-
|
|
61
|
-
### Fixed
|
|
62
|
-
|
|
63
|
-
- `@warlock.js/web` fixes page projection to resolve import references by lexical binding after server configuration is removed. A component-local `t = useTrans()` no longer retains an unrelated metadata-only Core import in the client view.
|
|
64
|
-
- Development startup now completes initial typings generation before it starts health checking, so the checker receives the generated declaration roots. Health checking remains background work and does not delay connector readiness.
|
|
65
|
-
- The health checker preserves declaration roots already included by the project's tsconfig.
|
|
66
|
-
|
|
67
|
-
## 5.17.0 - 2026-09-21
|
|
68
|
-
|
|
69
|
-
### Security
|
|
70
|
-
|
|
71
|
-
- `uploadedFileController` no longer serves an upload original inline unless its bytes sniff as jpeg, png, webp or avif AND its extension names that same image type (`.jpg`/`.jpeg` for jpeg, `.png` for png, `.webp` for webp, `.avif` for avif) — never sniffed bytes alone, and never the extension alone. An inline response always carries the SNIFFED format's `Content-Type` set explicitly, not an extension-derived guess from `sendFile`. This closes two stored-XSS holes, sniffed-bytes vs. extension mismatched in each direction: an svg (or any other non-raster file) named `.png`/`.jpg` still downloads instead of rendering as its extension's type, and raster bytes named with a non-image or different-image extension — e.g. jpeg magic bytes saved as `x.html` — also download instead of being served under that extension's advertised type (`text/html`, previously inherited from `sendFile`). Every other original — svg, html, xml, text, pdf, unknown, any raster/extension mismatch either way — is sent with `Content-Disposition: attachment`, `Content-Security-Policy: sandbox`, and its extension-derived `Content-Type`, except the svg/html/xml family, which is downgraded to `application/octet-stream`. Every uploads response — originals and variants — now also carries `X-Content-Type-Options: nosniff`.
|
|
72
|
-
- A default CSRF-Origin guard (card 8a752ab2) now runs at the earliest HTTP seam, before route middleware and any app handler, for every unsafe-method (`POST`/`PUT`/`PATCH`/`DELETE`) request that carries a `Cookie` header naming anything other than the framework locale cookie or a validated locale-preference cookie (a malformed `Cookie` header counts as carrying one — fails closed). It requires `Origin` — or, absent that, `Referer` — to name the request's own origin or an entry in `auth.csrf.allowedOrigins`, rejecting with `403` otherwise. Before this, that check only ran inside `authMiddleware("cookie:*")`, so a cookie-authenticated write reached through any other path (e.g. an app-owned optional-auth pattern reading its own `token` cookie) was never checked. A header-only API request (no `Cookie` header, e.g. `Authorization: Bearer`) is unaffected, and a route can opt out with `{ csrf: false }` (`RouteOptions.csrf`) — documented as dangerous, for third-party callbacks and machine-to-machine routes only.
|
|
73
|
-
|
|
74
|
-
### Added
|
|
75
|
-
|
|
76
|
-
- Standalone `warlock generate.typings` includes route `locales.json` keys, honoring `$group` and existing literal translation registrations. The optional Web build helper is resolved from the application's installed package using ESM export conditions, without executing application configuration or the Web root entry.
|
|
77
|
-
- `uploadedFileController`, a ready-made handler for `router.get("/uploads/*", uploadedFileController)` that serves local uploads safely. The request path must resolve inside the storage root, symlinks included, and never into the variant cache. Anything else gets the same 404 as a missing file, so the route cannot be used to probe for files. Originals are sent with a one-year cache.
|
|
78
|
-
- `generateImageVariants(relativePath, options?)`, an opt-in ingest-time counterpart to `uploadedFileController`: call it right after an upload is saved to render every configured `uploads.images` variant (optionally `options.variants` to limit which ones) up front. It shares the route's normalized config, cache key and derivative path, so `GET /uploads/<path>?variant=<name>` is served from cache on its first request, and its source guards and error statuses match the route. It returns a plain, serializable descriptor (`{ src, width, height, variants, formats? }`) matching web's `ImageDescriptor`.
|
|
79
|
-
- Bounded on-demand image variants, configured under `uploads.images`: `?variant=<name>` renders a variant named in `uploads.images.variants` (width and height from 1 to 8192, quality from 1 to 100, `fit` of `cover`, `contain` or `inside`), and `&format=` picks an output format from the `formats` allowlist (`webp`, `avif`). Any other query key, a repeated key, an unknown variant or a format that is not allowed returns 400. Only jpeg, png, webp and avif sources are resized, detected by their magic bytes, never their extension. gif and svg return 415. `maxSourceBytes` (default 25 MB) and `maxSourcePixels` (default 40,000,000) cap the source, and a larger one returns 413. Each derivative is cached on disk under a sha256 of the source path, size, mtime, variant and format, and it is written atomically. Concurrent requests for the same derivative generate it once. It is served with `Cache-Control: public, max-age=31536000, immutable` and an ETag that answers 304. Rewriting the source changes the key, so the next request renders a new derivative. An invalid `uploads.images` config throws `ImageVariantsConfigError` the first time a variant is requested. The variant path needs `sharp`, but originals are served without it.
|
|
80
|
-
|
|
81
|
-
### Fixed
|
|
82
|
-
|
|
83
|
-
- Request translation helpers follow the request's current locale; Web can supply its immutable route-scoped resolver for `request.t()`, `request.trans()`, and explicit `request.transFrom(locale, key, placeholders?)`. Without that resolver, all retain their global-registry behavior.
|
|
84
|
-
- Locale preference resolution validates the host-only preference before falling through to the legacy cookie, header, and default. `response.setLocale()` clears the preference before writing its authoritative locale cookie. Provisional Web navigation requests do not persist a locale before the browser commits a successful current navigation.
|
|
85
|
-
- Image variants (`uploadedFileController` and `generateImageVariants`) no longer enlarge a source smaller than the variant's target width or height. A variant is now rendered with sharp's `withoutEnlargement: true` by default, so a 100px source requested at `{ width: 320 }` stays 100px wide instead of being upscaled. Opt in per variant with `enlarge: true` in `ImageVariantDefinition` (a non-boolean value throws `ImageVariantsConfigError`); the effective flag is part of the normalized variant and its cache key, so flipping it regenerates the derivative. `generateImageVariants`' descriptor now reports the actual rendered width and height, read from the derivative it wrote, not the requested size.
|
|
86
|
-
- `generateImageVariants` now throws `ImageVariantsConfigError` — not a plain `Error` (no storage root) or `HttpError(400)` (`uploads.images` not configured) — for both server misconfiguration states, matching how a bad `uploads.images` value is already reported; input-derived failures (traversal, missing file, bad format, size limits, unknown variant name) still throw `HttpError`. `uploadedFileController`'s own responses for these requests are unchanged.
|
|
87
|
-
- `warlock seed` (and `warlock migrate`, for data migrations) now preload the `storage` connector, and `Storage`/`ScopedStorage` throw a named `StorageNotInitializedError` — pointing at `src/config/storage.ts` and a custom command's `preload.connectors` — instead of a bare `TypeError: Cannot read properties of null (reading 'name')` when `activeDriver` is read before storage has been initialized in the process.
|
|
88
|
-
|
|
89
|
-
## 5.16.0 - 2026-09-18
|
|
90
|
-
|
|
91
|
-
### Added
|
|
92
|
-
|
|
93
|
-
- Core now parses `application/x-www-form-urlencoded` request bodies, so plain HTML forms and OAuth `form_post` callbacks (Apple) reach their routes instead of failing with `FST_ERR_CTP_INVALID_MEDIA_TYPE`. These bodies have the same `http.bodyLimit` as JSON. A key sent more than once becomes an array.
|
|
94
|
-
- `response.xml()` accepts a raw string or any value with a `toXML(): string` method (such as a `@warlock.js/sitemap` `Sitemap`), and sends `application/xml`.
|
|
95
|
-
|
|
96
|
-
### Changed
|
|
97
|
-
|
|
98
|
-
- `warlock add sitemap` now merges a disabled `sitemap` section into `src/config/web.ts`, and creates that file when it is missing. It no longer writes `src/config/sitemap.ts` or registers `sitemapConnector()`. The merge edits only a `sitemap` key directly on the exported config object, and the result is re-parsed before it is written.
|
|
99
|
-
|
|
100
|
-
## 5.15.0 - 2026-09-18
|
|
101
|
-
|
|
102
|
-
### Added
|
|
103
|
-
|
|
104
|
-
- `warlock add sitemap` installs `@warlock.js/sitemap`, writes `src/config/sitemap.ts`, and registers `sitemapConnector()` in `warlock.config.ts`. The generated config ships **disabled**: a sitemap needs the application public origin and a generated app cannot know it, so the block explains the two steps to turn it on rather than producing an app that refuses to boot.
|
|
105
|
-
- `app.publicUrl` config key, with a `PUBLIC_APP_URL` environment fallback: the one absolute-URL source for every consumer that needs an origin. It never falls back to a request-derived host — an absolute URL built from the wrong host is worse than a boot that refuses to start, because nothing downstream reports it.
|
|
106
|
-
|
|
107
|
-
### Changed
|
|
108
|
-
|
|
109
|
-
- **BREAKING (fail-loud):** `request.cookie(name)` and `request.hasCookie(name)` now throw `CookieJarUnavailableError` when `@fastify/cookie` is not registered on the Fastify instance, instead of returning `undefined` / `false`. An unregistered plugin is a configuration fault, and it was previously indistinguishable from "the caller sent no such cookie" — under `authMiddleware([], "cookie:token")` it surfaced as a permanent, unexplained 401. Apps built on `createHttpApplication` are unaffected: core registers the plugin before anything mounts. The exposure is a host that mounts a guarded surface on its own Fastify instance. `request.cookies` stays lenient and still returns `{}`, so the framework's own opportunistic reads — locale resolution among them — are unchanged.
|
|
110
|
-
|
|
111
|
-
## 5.14.0 - 2026-09-17
|
|
112
|
-
|
|
113
|
-
### Added
|
|
114
|
-
|
|
115
|
-
- `warlock add bull-board` installs the Bull Board packages and writes a `dashboard` block to `src/config/queue.ts`; it adds the queue feature first when it is missing. The generated block enables the dashboard outside production only, since an unguarded dashboard refuses to mount in production.
|
|
116
|
-
- `middleware.cache({ tags })`: cached API responses can be tagged and are evicted by `cache.tags([...]).invalidate()`, like cached pages.
|
|
117
|
-
|
|
118
|
-
### Fixed
|
|
119
|
-
|
|
120
|
-
- `warlock dev` checks esbuild's native binary before starting and fails with `EsbuildBinaryMissingError` naming the fix (`warlock build` already did).
|
|
121
|
-
- `devServer.timings`: the `watcher` phase always reported 0ms on Windows and Linux; it now measures the real settle time.
|
|
122
|
-
- `warlock add` writes connector arrays formatted like Prettier (`[queueConnector(), webConnector()]`), including empty and multi-line arrays.
|
|
123
|
-
|
|
124
|
-
## 5.13.0 - 2026-09-17
|
|
125
|
-
|
|
126
|
-
### Added
|
|
127
|
-
|
|
128
|
-
- `warlock add queue` installs `@warlock.js/queue`, generates Redis-backed `src/config/queue.ts`, and registers `queueConnector()` in `warlock.config.ts`.
|
|
129
|
-
- `warlock add auth-google` and `warlock add auth-passkeys` install `@warlock.js/auth`'s Google sign-in (`jose`) and passkey (`@simplewebauthn/server`) login methods.
|
|
130
|
-
- `warlock dev` generates `.warlock/typings/translations.d.ts` from literal `groupedTranslations` dictionaries, augmenting web's typed translation-key registry.
|
|
131
|
-
|
|
132
|
-
### Changed
|
|
133
|
-
|
|
134
|
-
- `warlock dev` now uses a 12ms quiet window for isolated file saves while extending multi-file bursts up to a 60ms maximum, reducing routine HMR latency without splitting formatter or checkout reloads.
|
|
135
|
-
|
|
136
|
-
### Fixed
|
|
137
|
-
|
|
138
|
-
- Safely return no alias resolution when a configured alias has no first target.
|
|
139
|
-
- `@warlock.js/core` no longer imports `@warlock.js/auth`; `useHashedPassword` now calls core's own `hashPassword` directly instead of an auth-service delegate that only called back into core, and `@warlock.js/auth` moves to a dev-only dependency. A guard spec now fails the build if any `core/src` module imports `@warlock.js/auth`.
|
|
140
|
-
|
|
141
|
-
## 5.12.0 - 2026-09-16
|
|
142
|
-
|
|
143
|
-
### Added
|
|
144
|
-
|
|
145
|
-
- Opt-in request tracing (`http.tracing`): vendor-neutral `onRequestStart` / `onRequestEnd` / `onPhase` hooks fired around `route.match`, `middleware`, `validation`, `handler`, and `response.write`, with the trace id derived from an inbound W3C `traceparent` header (falling back to `request.id`). Off by default and zero-overhead when disabled; a throwing hook is caught and reported once, never breaking the request. No new response header — apps still correlate through the existing `X-Request-Id` echo. See the `request-tracing` skill.
|
|
146
|
-
- Opt-in `Content-Security-Policy` header (`http.csp`), using the per-request nonce the framework already generates; report-only mode supported.
|
|
147
|
-
- `Response.streamReact()` and the underlying `streamReactResponse()` helper (`@warlock.js/core`'s Stage 1 streaming SSR seam): pipe a React server stream (`renderToPipeableStream`) onto the raw response after writing the already-committed status and headers, aborting the render if the client disconnects. `@warlock.js/web` uses this exclusively to stream a page document — it never touches the raw response itself.
|
|
148
|
-
|
|
149
|
-
### Fixed
|
|
150
|
-
|
|
151
|
-
- Renaming or moving a file under `warlock dev` no longer prints a false `ENOENT` failure before the route is rewired.
|
|
152
|
-
- `warlock routes --json` now prints only JSON on stdout. The `› Running <command>...` header moved to stderr, like the completion banner.
|
|
153
|
-
- Production build contribution hooks (`generate` / `emit`) saw `outFile`, `entryPath`, `singleBundle`, `esmShim` and `banner` as `undefined`, because bundling deleted them from the shared build options. The bundler now works on its own copy.
|
|
154
|
-
|
|
155
|
-
### Changed
|
|
156
|
-
|
|
157
|
-
- **`container.get(key)` now throws a named error when the key is not registered**, instead of returning `undefined` while typed as present. Use `container.tryGet(key)` where the value is genuinely optional.
|
|
158
|
-
- **BREAKING:** `request.user` and `clearCurrentUser()` removed from the HTTP request; `RequestUser` moved to `@warlock.js/auth`. The authenticated user now lives at `request.locals.user`, a key `@warlock.js/auth` declares via module augmentation on `RequestLocals` and writes from its middleware. Reading `request.user` in development throws a new `RequestUserMovedError` naming `request.locals.user` (kept for one release as a migration diagnostic; removal is documented, not silent). `decodedAccessToken` and its cache-mark behavior (`request.locals.authDerived`) are unchanged. `useCurrentUser()` / `requestContext.getUser()` now read `request.locals.user` and return `unknown`/the caller's generic instead of the removed `RequestUser` type — see `@warlock.js/auth`'s `currentUser()` for a typed wrapper.
|
|
159
|
-
|
|
160
|
-
## 5.11.0 - 2026-09-14
|
|
161
|
-
|
|
162
|
-
### Added
|
|
163
|
-
|
|
164
|
-
- Tests and documentation for optional file fields: `v.file().optional()` skips an absent upload and reports a present non-file value as a normal validation error.
|
|
165
|
-
|
|
166
|
-
### Fixed
|
|
167
|
-
|
|
168
|
-
- `warlock dev` printed `hmr update` for a backend file before the new code was live, so a request made right after the line could still get the old response. The line now prints once the reload has finished and shows how long it took.
|
|
169
|
-
|
|
170
|
-
### Changed
|
|
171
|
-
|
|
172
|
-
- Backend file changes are picked up sooner in `warlock dev` (event debounce 150ms → 50ms).
|
|
173
|
-
|
|
174
|
-
## 5.10.0 - 2026-09-14
|
|
175
|
-
|
|
176
|
-
_Released in lockstep with the `@warlock.js/*` family; no package-specific changes in 5.10.0._
|
|
177
|
-
|
|
178
|
-
## 5.9.0 - 2026-09-13
|
|
179
|
-
|
|
180
|
-
_Released in lockstep with the `@warlock.js/*` family; no package-specific changes in 5.9.0._
|
|
181
|
-
|
|
182
|
-
## 5.8.0 - 2026-09-13
|
|
183
|
-
|
|
184
|
-
### Added
|
|
185
|
-
|
|
186
|
-
- `doctor` gains a `jwt-secret` check: it fails when `auth.userType` is configured but no JWT signing secret is set — the pre-flight form of the silent first-login 500 the lazy secret resolution would otherwise throw.
|
|
187
|
-
|
|
188
|
-
### Fixed
|
|
189
|
-
|
|
190
|
-
- CLI command modules now resolve the `app/*` path alias and `.ts` siblings the way controllers and pages do. The ESM loader hook is registered before a command module is imported, so a `warlock <command>` file that imports `app/*` no longer dies with `ERR_MODULE_NOT_FOUND`.
|
|
191
|
-
- `warlock generate.*` error hints point at `generate`, not the removed `create.*` command.
|
|
192
|
-
- A generated CRUD repository imports every `@warlock.js/core` type it references, and a generated seed stub ships disabled so an unfilled stub cannot abort the whole `warlock seed` run.
|
|
193
|
-
- The HTTP-port preflight runs only for a boot that actually starts the http connector, so a scoped data command (`warlock seed`, `migrate`) no longer probes — and collides with — a port it never binds.
|
|
194
|
-
- A port-in-use error now names the `HTTP_PORT` environment variable and tells you to unset it when an ambient value is the cause, instead of advising a `src/config/http.ts` edit that the environment variable would just override.
|
|
195
|
-
|
|
196
|
-
### Changed
|
|
197
|
-
|
|
198
|
-
- The emitted generator and starter templates no longer carry a redundant `.required()` — `@warlock.js/seal` fields are required by default, so the call was a no-op that taught the opposite of the truth. `.required()` still exists for setting a custom message.
|
|
199
|
-
|
|
200
|
-
## 5.7.0 - 2026-09-11
|
|
201
|
-
|
|
202
|
-
### Security
|
|
203
|
-
|
|
204
|
-
- **The SSRF guard now fails closed on an address it cannot classify.** An unclassifiable IP was previously treated as public, so an outbound request could reach an internal address the guard exists to block.
|
|
205
|
-
|
|
206
|
-
### Fixed
|
|
207
|
-
|
|
208
|
-
- **The dev server no longer says it is watching for changes before it is serving.** On a slow boot that line could arrive minutes before the port was bound.
|
|
209
|
-
- The dev-server error formatter can no longer throw while formatting an error, which replaced the developer's real error with its own crash.
|
|
210
|
-
- `warlock add` with an unknown feature now names it, instead of dying with `Cannot read properties of undefined`.
|
|
211
|
-
- Bracket-notation request bodies (`items[0][name]`) no longer silently drop values.
|
|
212
|
-
- `warlock routes` and the circular-import report degrade to unaligned output instead of throwing on an unexpected shape.
|
|
213
|
-
|
|
214
|
-
### Changed
|
|
215
|
-
|
|
216
|
-
- Internal type-safety hardening across the CLI, dev server and request handling; no other behaviour change.
|
|
217
|
-
|
|
218
|
-
## 5.6.0 - 2026-09-08
|
|
219
|
-
|
|
220
|
-
### Fixed
|
|
221
|
-
|
|
222
|
-
- **A production build asked the APP to resolve packages only the framework declares, so a built app could not boot under a strict pnpm tree.** Every bare specifier was left external, including the `@fastify/*`, `find-my-way`, `fast-jwt` and `@mongez/*` imports that reach the bundle through the framework's own code — none of which an app has any reason to declare. Under npm/yarn hoisting they resolved by accident; under pnpm the app died with `ERR_MODULE_NOT_FOUND` at startup, after a build that reported success. Externality is now decided per import edge: a bare specifier stays external unless the importer is not the app's own code AND the specifier names a package that importer's own `dependencies` declare.
|
|
223
|
-
- **An optional peer still stays external, and now by construction rather than by a list.** `nodemailer`, `socket.io`, `mongodb`, `vite`, `redis`, `pg`, the AI SDKs and `@aws-sdk/*` are declared as peer dependencies, never dependencies, so the rule above leaves every one of them alone without anyone having to maintain a list of their names.
|
|
224
|
-
- **Wildcard `paths` in `tsconfig.json` were resolved by nothing.** `app/*` and `web/*` — the aliases an app uses to refer to its own source — were skipped because esbuild's `alias` option cannot express a wildcard, and nothing took over. `import { User } from "app/users/models/user.model"` survived into the artifact as a bare specifier and `warlock start` failed with `Cannot find package 'app'`. They now resolve, and a wildcard that matches a declared alias but resolves to no file is a build error rather than a silent external.
|
|
225
|
-
- `response.setLocale()` wrote the locale cookie under a name spelled independently of the one `request` read it back under. One constant now owns that name at both ends, with a test that reads back whatever the writer emitted rather than naming the cookie itself.
|
|
226
|
-
- A container lookup that missed reported only that the key was not bound. When more than one copy of `@warlock.js/core` is loaded — which a source checkout or a mixed install can produce — that message described a real condition as if it were a wiring mistake. The failure now names the duplicate-instance condition and how many copies it found, on the failure path only.
|
|
227
|
-
|
|
228
|
-
### Changed
|
|
229
|
-
|
|
230
|
-
- `response.clearCookies()` documents what it cannot do, in its first sentence: a cookie set on one path is not cleared by a call made from another. The behaviour is unchanged — the promise it appeared to make was never one it could keep.
|
|
231
|
-
- The router folds its own route prefixes through the same normaliser the rest of the framework uses, so a prefix cannot be normalised two different ways.
|
|
232
|
-
|
|
233
|
-
## 5.5.0 - 2026-09-07
|
|
234
|
-
|
|
235
|
-
### Fixed
|
|
236
|
-
|
|
237
|
-
- `warlock add web` silently overwrote an existing `src/app/contact` module. It guarded `src/web/root.tsx` against clobbering a human's work but wrote the contact route and controller unconditionally, destroying them without a word. Each file is now guarded on its own existence, and a skip reports the consequence — that the contact form's `POST /api/contact` endpoint is missing and the form will 404 until you wire it.
|
|
238
|
-
- Documentation shipped in `skills/` told users to run `pnpm`-specific commands — including `pnpm warlock routes --json`, which cannot work under npm at all, since `pnpm <binary>` has no npm equivalent. Commands are now package-manager neutral.
|
|
239
|
-
|
|
240
|
-
## 5.4.0 - 2026-09-07
|
|
241
|
-
|
|
242
|
-
### Fixed
|
|
243
|
-
|
|
244
|
-
- `warlock routes --json` could not be parsed. The command's success banner shared stdout with the JSON payload, so the documented machine seam — "emit the routes as JSON for piping into scripts/CI" — produced output no consumer could read, and always had.
|
|
245
|
-
- `warlock add react-email` failed on every freshly scaffolded app. It read the project `tsconfig.json` with `JSON.parse`, and the scaffold's own tsconfig carries `//` comments, so the command aborted pointing at the developer's file.
|
|
246
|
-
- `warlock add web` produced an app whose homepage returned HTTP 500. `GET /` was registered twice — by the scaffold's own home route and by the generated page — and Fastify refused the duplicate. Affected both `warlock dev` and `warlock start`.
|
|
247
|
-
- `warlock add web` generated code that failed the scaffold's own lint gate: twelve `prettier/prettier` errors in files the developer had not written.
|
|
248
|
-
- `warlock add notifications` generated a controller that did not compile — seven `TS2345` errors from passing `request.user` where a `Notifiable | Id` was required.
|
|
249
|
-
- Every `warlock` command opened a stdin handle at import time, through a module-level singleton whose constructor defaulted to `process.stdin`. Only `warlock dev` has any use for stdin.
|
|
250
|
-
|
|
251
|
-
### Changed
|
|
252
|
-
|
|
253
|
-
- **Command success and failure banners now write to stderr, not stdout.** stdout carries a command's output; status chrome carries no data. A script that captured only stdout to grep for `✔ … completed successfully` must now read stderr. Nothing could have depended on the previous behaviour for `--json`, whose payload was unparseable precisely because of it.
|
|
254
|
-
- Feature generators that patch `tsconfig.json` now edit its text instead of parsing and rewriting it, so the file's comments survive.
|
|
255
|
-
- `warlock add web` locates an existing `GET /` by scanning `src/app/**/routes.ts` rather than assuming one hardcoded path.
|
|
256
|
-
|
|
257
|
-
## 5.3.2 - 2026-09-05
|
|
258
|
-
|
|
259
|
-
### Fixed
|
|
260
|
-
|
|
261
|
-
- An `HTTP_PORT` that does not round-trip through `Number()` — `03999`, `" 3999"`, `+3999`, `1e3` — reached the HTTP boot path as a string. `1e3` bound port 1000 with no diagnostic anywhere, the port published to the ready signal was the raw configured value, and `PortInUseError`'s suggestion string-concatenated into `port: 039991`. A configured port is now resolved to a canonical integer before anything binds, logs or reports it, and a value that cannot become one fails naming it.
|
|
262
|
-
- An ambient environment variable that overrode the app's own `.env` did so silently. The precedence is unchanged and deliberate — a checked-in `.env` is a default, an exported variable is the situational override — but the app now prints one line naming the variable, the value in effect, and that it came from the process environment. Keys that look like secrets are named with their values redacted.
|
|
263
|
-
- The port reported to a supervisor, to `WARLOCK_TEST_SERVER_PORT` and to the ready report was the **configured** port rather than the one actually bound. It is now read back from the address `listen()` resolves with, so the two can no longer diverge — including under `http.port: 0`, where the configured value carries no information at all.
|
|
264
|
-
- `warlock dev` restart-looped forever on a startup precondition that could never clear itself, reprinting its own diagnostic every few seconds and then scrolling it away with the restart banner. A failed precondition now stops, prints once, and exits.
|
|
265
|
-
- The port preflight ran after the database connected, so a busy port took 7–13 seconds to report on the `warlock dev` path. It now runs before the early-phase connectors, as it already did for a production build.
|
|
266
|
-
- 57 interfaces were imported as values across the package, each one crashing `warlock dev`'s from-source boot the moment its file reached the per-file transpiler — which has no type information and so cannot elide the import.
|
|
267
|
-
- `localized()` lost its `StandardSchemaV1` typing in the published 5.3.0 and 5.3.1 tarballs. The typing is restored.
|
|
268
|
-
|
|
269
|
-
## 5.3.1 - 2026-09-04
|
|
270
|
-
|
|
271
|
-
### Fixed
|
|
272
|
-
|
|
273
|
-
- Republished the complete family so a clean install resolves. The 5.3.0 publish left the family's reciprocal exact peer requirements unsatisfiable from a fresh registry install; 5.3.1 is the same code, published as one complete set.
|
|
274
|
-
|
|
275
|
-
## 5.3.0 - 2026-09-03
|
|
276
|
-
|
|
277
|
-
### Fixed
|
|
278
|
-
|
|
279
|
-
- Every error response was cacheable. The `no-store` floor is now set at the single error funnel, so an error can no longer be served from a cache to a second request.
|
|
280
|
-
- `warlock gen` emitted controllers with v4 handler signatures — every generated controller failed to compile against the v5 contract. The generator now emits the v5 `ctx` object signature.
|
|
281
|
-
- A validated file field was inferred as `unknown`; the sweep that found it found a second occurrence, and both are fixed.
|
|
282
|
-
|
|
283
|
-
### Changed
|
|
284
|
-
|
|
285
|
-
- A request is marked auth-derived the moment `user` or `decodedAccessToken` is assigned, so cacheability is decided by what the request actually read rather than by a separate declaration.
|
|
286
|
-
|
|
287
|
-
### Removed
|
|
288
|
-
|
|
289
|
-
- Five unreachable error branches that described a response the framework never sent.
|
|
290
|
-
|
|
291
|
-
## 5.2.4 - 2026-09-02
|
|
292
|
-
|
|
293
|
-
### Fixed
|
|
294
|
-
|
|
295
|
-
- `warlock add web` now writes universal page setup to `src/web/index.register.ts` and re-exports `register()` from the page, preserving component state across React Fast Refresh edits without changing SSR or hydration registration.
|
|
296
|
-
|
|
297
|
-
## 5.2.3 - 2026-09-02
|
|
298
|
-
|
|
299
|
-
### Fixed
|
|
300
|
-
|
|
301
|
-
- `warlock add` now preserves exact lockstep versions for added Warlock family packages while retaining declared ranges for third-party dependencies.
|
|
302
|
-
- `warlock add web` now emits a projection-safe `register()` hook and one `index` route identity for SSR and hydration, with deterministic form and favicon markup for a clean browser console.
|
|
303
|
-
|
|
304
|
-
## 5.2.2
|
|
305
|
-
|
|
306
|
-
### Fixed
|
|
307
|
-
|
|
308
|
-
- Restored exact 5.2.2 pins across the complete Warlock family. The partial
|
|
309
|
-
5.2.0 and 5.2.1 releases could not satisfy the family's reciprocal exact
|
|
310
|
-
peer requirements in a clean install.
|
|
311
|
-
|
|
312
|
-
## 5.2.1
|
|
313
|
-
|
|
314
|
-
### Fixed
|
|
315
|
-
|
|
316
|
-
- Rebuilt the published package with the complete Warlock family registry and
|
|
317
|
-
installable npm ranges for every internal dependency. The 5.2.0 registry
|
|
318
|
-
manifest leaked six `workspace:` dependency specifications and could not be
|
|
319
|
-
installed outside the monorepo.
|
|
320
|
-
|
|
321
|
-
## 5.2.0
|
|
322
|
-
|
|
323
|
-
### Added
|
|
324
|
-
|
|
325
|
-
- **Connectors can contribute to `warlock build` through an optional static
|
|
326
|
-
`build` object.** Configured connectors may define ordered, awaited
|
|
327
|
-
`generate(context)` and `emit(context)` hooks without being booted or started.
|
|
328
|
-
`generate` can add generated entry imports and a narrow esbuild patch; `emit`
|
|
329
|
-
can produce non-esbuild artifacts after the server bundle. Unknown contribution
|
|
330
|
-
keys, duplicate/reserved connector names, generated unresolved imports, and hook
|
|
331
|
-
failures stop the build instead of producing a partial artifact.
|
|
332
|
-
- **`warlock routes:diff`** — compares the live dev-server page-route surface against
|
|
333
|
-
the last successful `warlock build`'s snapshot (`page-routes.manifest.json` in the
|
|
334
|
-
build `outdir`). Boots diagnostically (same fail-loud boot as `warlock routes` /
|
|
335
|
-
`warlock doctor` — no connectors started), then reports `changed` / `removed` /
|
|
336
|
-
`added` page routes and exits non-zero on drift; exits `0` with "Page routes match"
|
|
337
|
-
when the two agree. Refuses to run (with an instruction to run `warlock build`
|
|
338
|
-
first) when no snapshot exists yet, or when an existing one is malformed. A route
|
|
339
|
-
whose `path`/`name` moved but whose `source` file didn't is reported as one
|
|
340
|
-
`changed` line instead of a `removed` + `added` pair.
|
|
341
|
-
|
|
342
|
-
**A clean baseline is reported immediately after a successful build.** The manifest
|
|
343
|
-
is written through the router's own path normalizer, so the catch-all page route is
|
|
344
|
-
recorded the way the router registers it (`/*`) rather than the way discovery derives
|
|
345
|
-
it (`*`) — an untouched checkout right after a green `warlock build` reports
|
|
346
|
-
`Page routes match`, where an earlier draft of this command reported drift on the
|
|
347
|
-
first run every time.
|
|
348
|
-
|
|
349
|
-
⚠ **What a clean diff does not prove.** The manifest records what each page's route
|
|
350
|
-
name was _derived_ as, not what it was _registered_ as: `warlock build` boots no
|
|
351
|
-
connectors, so it cannot see the API routes a page name may collide with, and the
|
|
352
|
-
router appends a `.<method>` suffix to a name another method already claimed —
|
|
353
|
-
which the comparison accepts rather than reporting. `Page routes match` therefore
|
|
354
|
-
means the page surface has not moved since the build; it does **not** mean no page
|
|
355
|
-
route name collided at registration. The clean-run output says so in a trailing note,
|
|
356
|
-
and `warlock routes` shows the names as actually registered
|
|
357
|
-
|
|
358
|
-
### Changed
|
|
359
|
-
|
|
360
|
-
- **Incoming routes now match with or without one trailing slash in both
|
|
361
|
-
development and production.** `/about` and `/about/` dispatch to the same
|
|
362
|
-
route; `/` remains `/`, query strings are preserved, and case matching is
|
|
363
|
-
unchanged. Both router paths call the same request-path normalizer before
|
|
364
|
-
matching instead of relying on different Fastify/find-my-way defaults.
|
|
365
|
-
|
|
366
|
-
- ⚠ **BREAKING — `http.trustProxy` is validated at boot, and a number is refused.** The
|
|
367
|
-
accepted shapes are exactly `boolean`, a non-empty string (a single IP/CIDR or several
|
|
368
|
-
comma-separated), a non-empty `string[]`, and an `(address, hop) => boolean` predicate;
|
|
369
|
-
a missing or nullish value means `false`. Anything else — a number, `""`, `[]`, an array
|
|
370
|
-
with a non-string entry, a plain object — now throws a `TypeError` while the HTTP server
|
|
371
|
-
is being constructed, before Fastify is instantiated, instead of being handed through
|
|
372
|
-
and silently coerced
|
|
373
|
-
|
|
374
|
-
```
|
|
375
|
-
Invalid http.trustProxy configuration: expected a boolean, a non-empty IP/CIDR string,
|
|
376
|
-
a non-empty string array, or a predicate function; received number.
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
⚠ **The hop-count shape 4.16.0 introduced and recommended (`http.trustProxy: 2`) no
|
|
380
|
-
longer boots.** The Fastify version this release builds against evaluates a numeric
|
|
381
|
-
`trustProxy` as always-false at runtime, so a hop count never walked past your proxies —
|
|
382
|
-
it silently degraded to "trust nothing" while reading like a configured proxy chain, and
|
|
383
|
-
`detectIp()` returned the socket peer for every request. Refusing it loudly is the only
|
|
384
|
-
honest option: **migrate a hop count to a trusted-proxy list (`"10.0.0.0/8"`) or a
|
|
385
|
-
predicate before upgrading.** `true`, a list, and a predicate are unaffected
|
|
386
|
-
|
|
387
|
-
- **`warlock build` writes into a temporary directory and promotes it only on success.**
|
|
388
|
-
The build no longer writes into `outdir` as it goes. It builds into a hidden sibling
|
|
389
|
-
directory (`.<outdir-basename>.build-<hex>`, same volume so the promotion is a rename),
|
|
390
|
-
writes a `.warlock-build.json` success marker (`{ status, builtAt }`) as its last step,
|
|
391
|
-
then swaps the directory into place. **A successful build therefore leaves no stale
|
|
392
|
-
files** — `outdir` is replaced wholesale rather than merged over, so an artifact a
|
|
393
|
-
previous build emitted and this one did not is gone. **A failed build leaves no usable
|
|
394
|
-
`dist`** — the temp directory is removed and the error rethrown without `outdir` ever
|
|
395
|
-
being touched, so a previous good build survives intact and no half-written one takes
|
|
396
|
-
its place
|
|
397
|
-
|
|
398
|
-
- **`warlock start` refuses a `dist` that was not produced by a successful build**, and
|
|
399
|
-
names that as the reason. It checks for the `.warlock-build.json` marker in `outdir`
|
|
400
|
-
before spawning anything; missing, unreadable, malformed, or `status !== "success"` all
|
|
401
|
-
exit `1` on stderr with
|
|
402
|
-
|
|
403
|
-
```
|
|
404
|
-
✖ "dist" was not produced by a successful `warlock build` run (no build-success marker found). Run `warlock build` before `warlock start`.
|
|
405
|
-
```
|
|
406
|
-
|
|
407
|
-
Never built, a build that failed before promotion, and a hand-assembled directory are
|
|
408
|
-
deliberately not distinguished — the fix for all three is the same command. Previously
|
|
409
|
-
`start` spawned whatever happened to be sitting in `outdir` and failed somewhere inside
|
|
410
|
-
the boot instead. ⚠ Ship `outdir` whole: an artifact pipeline that prunes dotfiles
|
|
411
|
-
strips the marker and makes the build unstartable on the target host
|
|
412
|
-
|
|
413
|
-
- **`warlock start` surfaces the child process's real output on a failed boot.** The
|
|
414
|
-
production supervisor forwards every stdout/stderr chunk from the spawned bundle
|
|
415
|
-
verbatim and live, and its failure summary now reports whether a cause actually arrived:
|
|
416
|
-
`the cause is printed above, in the application's own output` when output was seen, and
|
|
417
|
-
`no output was captured from the application process — its cause did not reach this
|
|
418
|
-
terminal` when none was. It previously pointed at "above" unconditionally, which on a
|
|
419
|
-
silent child meant pointing at an empty terminal
|
|
420
|
-
|
|
421
|
-
- **The HTTP connector now preflights its port before binding.** `warlock dev` and
|
|
422
|
-
`warlock start` both go through `HttpConnector.start()`, which now calls
|
|
423
|
-
`assertPortIsAvailable(port, host)` immediately before `listen()`. A collision now
|
|
424
|
-
surfaces as `EADDRINUSE: Port <port> is already in use on <host>. Stop the dev server
|
|
425
|
-
(or whatever else is listening on port <port>) and run again...` — the code and the
|
|
426
|
-
port named in the same sentence — instead of a bare `EADDRINUSE` thrown from inside
|
|
427
|
-
Fastify with no indication of which port it meant. `EACCES` on the port is treated
|
|
428
|
-
the same way, since "cannot bind" is one problem from the operator's side. The test
|
|
429
|
-
server (`startHttpTestServer`)
|
|
430
|
-
already preflighted its port before this release; this brings `dev`/`start` to the
|
|
431
|
-
same behavior.
|
|
432
|
-
- **`startHttpTestServer()` now runs `Application.runStartupValidators()`** — the
|
|
433
|
-
same slot `warlock dev` and the generated production `app.ts` already ran it in —
|
|
434
|
-
after application modules load and before the late-phase connectors (http, socket)
|
|
435
|
-
bind. A validator registered via `Application.onValidateBoot(...)` that rejects now
|
|
436
|
-
aborts the test server's boot exactly as it aborts `dev`/`start`, instead of only
|
|
437
|
-
being enforced outside of tests.
|
|
438
|
-
- **`warlock add web` scaffolds a real, validated API endpoint**, not just a static
|
|
439
|
-
page. It now also writes `src/app/contact/routes.ts` and
|
|
440
|
-
`src/app/contact/controllers/contact.controller.ts` (a `POST /api/contact` route
|
|
441
|
-
validated with `@warlock.js/seal`), and `src/web/home.page.tsx` ships an
|
|
442
|
-
interactive, localized (en/ar) contact form wired to that route via `@mongez/http`
|
|
443
|
-
- `@mongez/react-form` + `@mongez/react-localization`. The `web` feature now also
|
|
444
|
-
installs those three packages as dependencies.
|
|
445
|
-
|
|
446
|
-
### Fixed
|
|
447
|
-
|
|
448
|
-
- **Feature definitions no longer carry stale `~4.0.0` defaults for
|
|
449
|
-
`@warlock.js/*`.** They now use an explicit internal placeholder that
|
|
450
|
-
`warlock add` must resolve from the installed Core version before invoking a
|
|
451
|
-
package manager or writing `package.json`. A forgotten resolution therefore
|
|
452
|
-
fails loudly instead of silently selecting an old framework major.
|
|
453
|
-
- **`startHttpTestServer()` fails loudly instead of silently skipping the preflight**
|
|
454
|
-
when `http.port` doesn't round-trip through `Number()` (e.g. `HTTP_PORT=03999`,
|
|
455
|
-
`+3999`, `1e3`, or a value with stray whitespace) — previously it returned early
|
|
456
|
-
and let the connector reach `listen({ port })` with the unvalidated value and no
|
|
457
|
-
published port for test workers to resolve.
|
|
458
|
-
- **A race in the mail SES driver** where `getSesMailer()` could read the
|
|
459
|
-
eagerly-loaded `nodemailer` module before its load promise had settled. It now
|
|
460
|
-
awaits the in-flight load first (throwing the "nodemailer is not installed"
|
|
461
|
-
install-instructions error if the load ultimately failed), matching the guard the
|
|
462
|
-
SMTP path already had.
|
|
463
|
-
|
|
464
|
-
## 5.1.0
|
|
465
|
-
|
|
466
|
-
> **If you use `@warlock.js/web`, upgrade.** React did not execute at all in published
|
|
467
|
-
> installs of 5.0.0 through 5.0.2 — the defect and its fix are in that package's
|
|
468
|
-
> changelog. `core` ships the CLI-side half of this release.
|
|
469
|
-
|
|
470
|
-
### Added
|
|
471
|
-
|
|
472
|
-
- **`warlock add tailwind`** — installs and wires Tailwind CSS v4 through PostCSS.
|
|
473
|
-
- **`warlock add shadcn`** — sets up the prerequisites shadcn/ui expects. It is _not_ a
|
|
474
|
-
wrapper around the shadcn CLI: you still run that yourself to add components, this
|
|
475
|
-
only makes the project ready for it.
|
|
476
|
-
|
|
477
|
-
### Changed
|
|
478
|
-
|
|
479
|
-
- **`warlock dev` prints one status block per run.** The banner is printed exactly once,
|
|
480
|
-
and the URL it prints is never a raw `[::1]` address. The single-boot guard behind
|
|
481
|
-
this is **new in this release — it was never present in any published version**, so
|
|
482
|
-
duplicate boots on 5.0.x were real, not a display artefact.
|
|
483
|
-
- **`warlock doctor` now reports the same route count as `warlock dev`.** The two walked
|
|
484
|
-
routes differently and disagreed. `doctor` also emits zero warnings on a healthy
|
|
485
|
-
project — so a warning now means something — and fails loudly on a route module that
|
|
486
|
-
genuinely fails to load, instead of counting it as fine.
|
|
487
|
-
|
|
488
|
-
### Removed
|
|
489
|
-
|
|
490
|
-
- **A false comment that the `warlock add web` scaffold emitted in 5.0.0 through 5.0.2**,
|
|
491
|
-
claiming a page's route is derived from its file location. No such derivation has ever
|
|
492
|
-
existed. The stub no longer emits it; **apps already scaffolded on those versions still
|
|
493
|
-
carry the comment in their own source** and must delete it by hand.
|
|
494
|
-
|
|
495
|
-
### Internal
|
|
496
|
-
|
|
497
|
-
- The `add` feature registry was split into one module per feature, replacing the single
|
|
498
|
-
growing switch.
|
|
499
|
-
- Fixed 7 mojibake checkmarks in CLI output.
|
|
500
|
-
|
|
501
|
-
## 5.0.2 - 2026-08-25
|
|
502
|
-
|
|
503
|
-
No changes to `@warlock.js/core`. Released in lockstep with the `@warlock.js/web` SSR
|
|
504
|
-
fix (`ssr.noExternal`) — see that package's changelog.
|
|
505
|
-
|
|
506
|
-
## 5.0.1 - 2026-08-25
|
|
507
|
-
|
|
508
|
-
No changes to `@warlock.js/core`. Released in lockstep with the `create-warlock` vite
|
|
509
|
-
resolution pin and the `@warlock.js/web` peer narrowing — see those packages' changelogs.
|
|
510
|
-
|
|
511
|
-
## 5.0.0 - 2026-08-25
|
|
512
|
-
|
|
513
|
-
### Upgrading
|
|
514
|
-
|
|
515
|
-
- **BREAKING — `Request` no longer has a `[key: string]: any` index signature.** Attaching arbitrary properties (`request.post = post`) no longer compiles. Migrate per attachment: per-request data written by middleware → `request.locals` (augment `RequestLocals`); per-request memoized computation → `requestMemo(key, fn)`; new typed members → module augmentation of `Request` itself
|
|
516
|
-
- **BREAKING — `fromRequest(key, callback)` is removed.** It cached values as dynamic `Request` properties, which only compiled because of the deleted index signature. `requestMemo(key, fn)` is the drop-in successor: same per-request lifetime, single-flight (concurrent callers share one promise), and it never touches the `Request` object
|
|
517
|
-
- **BREAKING — client-supplied locales are validated against `app.localeCodes`.** A `locale`/`translation-locale-code` header or `locale` query value outside the configured list is treated as absent and falls through to the default locale — it previously won as-is, steering every translated string and every serialized Resource. Apps that declare no `app.localeCodes` keep the pass-through behavior
|
|
518
|
-
- **`request.trans()` / `request.t()` now resolve the locale at call time.** Previously the translator captured the locale once during request construction (before routing), so a locale set later — a path locale, `setLocaleCode()` — changed `request.locale` but not the language of translated strings
|
|
519
|
-
|
|
520
|
-
### Added
|
|
521
|
-
|
|
522
|
-
- **`request.requireUser()`** — returns the authenticated user non-optionally, or throws `UnAuthorizedError` when no user is attached. For handlers behind an auth guard, where an absent user is a misconfigured route rather than a normal state; replaces `request.user!` assertions
|
|
523
|
-
|
|
524
|
-
## 4.16.0 - 2026-08-18
|
|
525
|
-
|
|
526
|
-
### Security
|
|
527
|
-
|
|
528
|
-
- **`request.detectIp()` no longer trusts `X-Real-IP` / `X-Forwarded-For` unless `http.trustProxy` is set.** Both headers are client-settable, and `detectIp()` honoured them unconditionally — bypassing the `trustProxy` opt-in the Fastify server itself is configured with. Any client could therefore spoof its IP to everything keyed on `detectIp()`: `ipFilter` allowlists/denylists, the default rate-limit bucket key, and anonymous idempotency scoping. Without the opt-in, `detectIp()` (and its `realIp` alias) now returns `baseRequest.ip` — the socket peer address, which cannot be forged
|
|
529
|
-
|
|
530
|
-
⚠ **If your app runs behind a proxy and relied on `detectIp()` reading the forwarding headers without setting `http.trustProxy`, set `http.trustProxy: true`** (or a Fastify `trustProxy` value matching your edge). With `true` set, behaviour is unchanged: `X-Real-IP` first, then the leftmost `X-Forwarded-For` hop, then the peer address. Only enable `true` when your edge overwrites those headers — it trusts them wholesale
|
|
531
|
-
|
|
532
|
-
- **`http.trustProxy` now accepts a hop count or a trusted-proxy list, and `detectIp()` honours them.** `true` is the wrong shape for the common topology: an edge that _appends_ to `X-Forwarded-For` leaves whatever the client prepended as the leftmost entry, so "trust the leftmost hop" hands the client its own IP back. The config value is passed to Fastify untouched, and `detectIp()` now reads the resolved client off `request.ip` instead of re-parsing the header — so both agree, and every Fastify shape works:
|
|
533
|
-
|
|
534
|
-
| `http.trustProxy` | Client IP |
|
|
535
|
-
| ---------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
536
|
-
| `false` _(default)_ | Socket peer address; forwarding headers ignored |
|
|
537
|
-
| `true` | Leftmost `X-Forwarded-For` entry (whole chain trusted) |
|
|
538
|
-
| `2` | Walks past the 2 rightmost hops — for an edge that appends |
|
|
539
|
-
| `"10.0.0.0/8"`, `"loopback, 10.0.0.0/8"`, `["10.0.0.0/8", "192.168.0.0/16"]` | Walks left while each hop is a listed proxy, stops at the first that isn't |
|
|
540
|
-
| `(address, hop) => boolean` | Your predicate |
|
|
541
|
-
|
|
542
|
-
Prefer the narrowest shape your topology allows: with `true`, any client that can reach the process directly picks its own IP, and an `ipFilter` allowlist in front of it is decorative
|
|
543
|
-
|
|
544
|
-
⚠ **`X-Real-IP` is now honoured only under `trustProxy: true`.** It carries no chain, so there is nothing to check a hop count or proxy list against, and a trusted edge that forwards the client's own `X-Real-IP` verbatim would otherwise let any client escape the bound. Under a bounded `trustProxy` the value comes from the `X-Forwarded-For` chain instead — if your edge sets only `X-Real-IP`, have it set `X-Forwarded-For` as well
|
|
545
|
-
|
|
546
|
-
### Dependencies
|
|
547
|
-
|
|
548
|
-
- Bumped `@mongez/*` deps to their 2026-08-17 security release specs: `concat-route` ^1.2.0, `config` ^1.2.1, `dotenv` ^1.3.2, `events` ^2.2.7, `http` ^3.5.0, `localization` ^3.4.7, `reinforcements` ^4.0.1, `supportive-is` ^2.1.4
|
|
549
|
-
- ⚠ **`@mongez/reinforcements` 4.0.1 is a major bump: `Random` is now CSPRNG-backed (WebCrypto) and `Random.seed()` was removed** — seeded/reproducible `Random.string/nanoid/id/token/uuid` calls now throw. Audited `core`'s `Random.string(...)` call sites (`use-case.ts`, `http/request.ts`, `dev-server/files-watcher.ts`, `http/uploaded-file.ts`) and its test suite: none rely on seeding or reproducible output, so no code changes were required
|
|
550
|
-
- `@mongez/encryption` 2.0.1 (async `encrypt`/`decrypt`, throws on failure) does not apply to this package — `core` is not a consumer; `src/encryption/encrypt.ts` uses Node's built-in `crypto` module directly and is unaffected
|
|
551
|
-
|
|
552
|
-
## 4.14.0 - 2026-08-16
|
|
553
|
-
|
|
554
|
-
### ⚠ Upgrading from 4.13.0 — read this first
|
|
555
|
-
|
|
556
|
-
**Three behaviour changes and one documentation correction. All four touch the test lifecycle; none touch application runtime.**
|
|
557
|
-
|
|
558
|
-
⛔ **Every existing project must replace its `src/test-setup.ts`.** Three things are wrong with the file 4.13.0 generated:
|
|
559
|
-
|
|
560
|
-
```ts
|
|
561
|
-
/**
|
|
562
|
-
* Test Setup
|
|
563
|
-
* Runs before EACH test file — not once per worker.
|
|
564
|
-
*/
|
|
565
|
-
import { afterAll } from "vitest";
|
|
566
|
-
import { setupTest, teardownTest } from "@warlock.js/core/tests";
|
|
567
|
-
|
|
568
|
-
await setupTest(); // ← was setupTest({ connectors: true })
|
|
569
|
-
afterAll(teardownTest); // ← is new
|
|
570
|
-
```
|
|
571
|
-
|
|
572
|
-
1. **`{ connectors: true }` must become a bare `setupTest()`.** Under the new precedence it is an **explicit** value, so it now overrides your `src/config/tests.ts` where it previously deferred to it.
|
|
573
|
-
2. **`afterAll(teardownTest)` is new and is not optional** — without it nothing ever closes the framework your tests started.
|
|
574
|
-
3. **The `Per-Worker Test Setup` comment is false.** It always was.
|
|
575
|
-
|
|
576
|
-
**This is the migration step nobody can skip.** `warlock add test` emits the corrected file for new projects.
|
|
577
|
-
|
|
578
|
-
| What changes | How you'll see it | What to do |
|
|
579
|
-
| -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
580
|
-
| **`setupTest({ connectors })` now beats `tests.connectors` config** — the precedence flipped | a test file that passes `connectors` explicitly starts a **different connector set** than it did in 4.13.0 | grep for `setupTest({` — a call passing `connectors` was previously **ignored** and is now honoured. **Including the one in your generated setup file** |
|
|
581
|
-
| **A second `setupTest` call with different options now REJECTS** | an error naming the active and the requested selection, where 4.13.0 silently did nothing | call `teardownTest()` first, or don't call `setupTest` again at all |
|
|
582
|
-
| **The generated setup file now registers `afterAll(teardownTest)`** | your test files tear the framework down when they finish, instead of leaving it running | **add it to your existing `src/test-setup.ts`** — see below |
|
|
583
|
-
| **Docs corrected: `setupTest` is called per TEST FILE, not per worker** | no runtime effect on its own — the _invocation_ always worked this way | fix the comment in `src/test-setup.ts` as above |
|
|
584
|
-
|
|
585
|
-
### Added
|
|
586
|
-
|
|
587
|
-
- **`teardownTest()` — the other half of the pair.** `setupTest` has shipped without a counterpart since it was introduced: there was no supported way to close the framework a test file brought up, and the only "reset" available was a module flag that proved nothing about whether ports, sockets, pools or timers had actually closed
|
|
588
|
-
|
|
589
|
-
`teardownTest()` is idempotent when idle, shares one shutdown between concurrent callers, waits for an in-flight setup to settle before closing, and **always clears local state in a `finally`** so a failed shutdown cannot leave the lifecycle claiming to be ready
|
|
590
|
-
|
|
591
|
-
⚠ **A shutdown failure poisons the lifecycle rather than pretending to recover.** If the shutdown layer reports a rejection, later `setupTest` calls refuse until the Vitest worker is recycled or a retried teardown fully succeeds. **We cannot promise a clean restart after a reported close failure, so we don't.** ⚠ **What it cannot see:** `connectorsManager.shutdown()` catches and logs individual connector failures internally — those never reach this lifecycle and never poison it. Manager-wide error policy is a separate piece of work
|
|
592
|
-
|
|
593
|
-
### Changed
|
|
594
|
-
|
|
595
|
-
- **BREAKING — an explicit `setupTest({ connectors })` now wins over `tests.connectors` config.** The order was `config > parameter > true`; it is now **`explicit parameter > config > true`**
|
|
596
|
-
|
|
597
|
-
4.13.0's changelog said this question was open, not settled: _"a per-call override is a contract decision for a later release."_ This is that decision. **Call-site intent should beat a project default** — a caller who names a connector set is being specific on purpose, and silently overruling them was the wrong behaviour
|
|
598
|
-
|
|
599
|
-
**"Explicit" means a non-`undefined` value.** `setupTest()`, `setupTest({})` and `setupTest({ connectors: undefined })` **all fall through to config, then to `true`.** The `undefined` rule is deliberate: an optional variable that happens to be `undefined` must not silently erase project config
|
|
600
|
-
|
|
601
|
-
⚠ **The generated `src/test-setup.ts` now calls `setupTest()` with no argument**, where it previously passed `{ connectors: true }`. Under the new order, passing `true` explicitly would erase the `tests.connectors` layer for the entire project. **If you edit your setup file, leave the call bare**
|
|
602
|
-
|
|
603
|
-
⚠ **This is user-visible and it is why the change is marked BREAKING:** an application that sets `tests.connectors` _and_ passes `connectors` from any test file will start a different connector set after upgrading
|
|
604
|
-
|
|
605
|
-
- **BREAKING — a conflicting `setupTest` call rejects instead of being ignored.** While a setup is starting or ready, a call with _different_ effective options now rejects with an error naming both the active and the requested selection. The same options remain a no-op, and concurrent identical calls share one startup
|
|
606
|
-
|
|
607
|
-
Through 4.13.0 this was a silent early-return on an `isSetupComplete` flag — so `setupTest({ connectors: false })` in a file whose `src/test-setup.ts` had already run **did nothing at all, reported nothing, and started every connector anyway.** Connector arrays are compared as **sets** after deduplication, so caller order never counts as a conflict
|
|
608
|
-
|
|
609
|
-
- **Lifecycle state is now scoped to the worker runtime instead of the module.** `isSetupComplete` was a module-level variable, and **Vitest rebuilds the setup module's registry between test files while the worker process or thread keeps running** — so the flag reset in exactly the situation where live DB connections, pools and timers survive
|
|
610
|
-
|
|
611
|
-
Scope is per **process** under `pool: "forks"` and per **thread** under `pool: "threads"`; it deliberately does not cross thread workers, because `globalThis` is per realm and the resources are per worker too. **The guard's scope now matches the leak's scope in all four `pool` × `isolate` combinations**
|
|
612
|
-
|
|
613
|
-
### Fixed
|
|
614
|
-
|
|
615
|
-
- **A stranded setup no longer exhausts the heap.** A lifecycle left in the `starting` state sent `teardownTest`'s wait-then-re-enter path into unbounded recursion — **`FATAL ERROR: JavaScript heap out of memory` at 4 GB, killing the worker with 26 tests in that run never executed.** It was found while proving the state machine, not reported by a user, and it would have shipped
|
|
616
|
-
|
|
617
|
-
The setup attempt is now bounded by **`tests.setupTimeout`, defaulting to `120000` ms**, and expiry **poisons** the lifecycle rather than returning it to `idle` — the attempt may have started connectors nobody can now account for. The message names the state, the bound and the remedy:
|
|
618
|
-
|
|
619
|
-
```
|
|
620
|
-
setupTest() did not finish within 120000ms and is stuck in the "starting" state. The
|
|
621
|
-
lifecycle is now poisoned: whatever that attempt had already started is not known to be
|
|
622
|
-
closed, so later setupTest() calls refuse until the Vitest worker is recycled. If your
|
|
623
|
-
cold start is legitimately slower than this, raise the bound with `tests.setupTimeout`
|
|
624
|
-
in `src/config/tests.ts` — milliseconds, default 120000.
|
|
625
|
-
```
|
|
626
|
-
|
|
627
|
-
**It bounds the setup attempt, not teardown separately** — `teardownTest()` awaits the same attempt and inherits the bound. **A second teardown-side deadline was tried and rejected during implementation**: it expired instead of the setup's, was swallowed on settle, re-entered and armed a third, and left the stuck setup unbounded after all — reproducing the exact recursion the guard exists to remove
|
|
628
|
-
|
|
629
|
-
⚠ **An invalid `tests.setupTimeout` throws, naming the value.** Zero, negative and non-numeric fail loudly instead of falling back to the default; a silent fallback hides a typo behind a working suite
|
|
630
|
-
|
|
631
|
-
⚠ **A stranded lifecycle must fail with a message, not a dead process** — a crash mid-file is indistinguishable from an infrastructure flake, which is the worst way for a framework to report its own bug
|
|
632
|
-
|
|
633
|
-
⚠ **Scope of the proof, stated because a green here is easy to over-read:** all nine guards were seen to fail under their own mutation, **but every spec injects its scheduler** — the default _value_ is tested while the production timer, and whether its `unref` releases the worker, is not. **No spec observes a real hang**; the stuck attempt is a mock gate, not a socket that never returns
|
|
634
|
-
|
|
635
|
-
### Documentation
|
|
636
|
-
|
|
637
|
-
- **Corrected: `setupTest` is CALLED once per TEST FILE, not once per worker.** Every version of the `test-service` and `test-http` skills, both generated LLM projections, the generator's comments and `setupTest`'s own JSDoc described a per-worker lifetime. **Vitest runs `setupFiles` before each test file and their exports are ignored** — measured across all four `pool` × `isolate` combinations, not inferred
|
|
638
|
-
|
|
639
|
-
⛔ **If your project was generated before 4.14.0, replace the whole file** — see the migration block above. It is not a comment-only change
|
|
640
|
-
|
|
641
|
-
**The lifetime this release commits to is FILE-SCOPED:** the setup file bootstraps the framework and its `afterAll(teardownTest)` closes it, once per test file. **One owner, one pairing, correct under every pool, every isolation setting, and watch mode**
|
|
642
|
-
|
|
643
|
-
⚠ **This deliberately declines a faster option.** Holding lifecycle state in the worker runtime makes a worker-scoped lifetime _possible_ — bootstrap once, reuse across every file in that worker — and an earlier draft of this release simply left the framework running to get it. **We are not shipping that**, for two reasons neither of which is performance:
|
|
644
|
-
|
|
645
|
-
1. **Under `pool: "threads"` we cannot honestly claim the runner cleans up.** Vitest tears the thread down while the process lives, and whether Node reclaims that thread's sockets and pools is **unmeasured** — so "the runner owns cleanup by termination" would be a promise we cannot observe being kept
|
|
646
|
-
2. **In watch mode Vitest reuses workers between reruns**, so there is no recycle and therefore **no cleanup owner at all** between reruns. Declaring watch mode unsupported was the alternative, and a test framework whose lifecycle is undefined in the mode people use all day does not have a lifecycle
|
|
647
|
-
|
|
648
|
-
**The cost is a framework bootstrap per test file — which is exactly what 4.13.0 already paid**, since its module-level flag died with the module registry between files. **Nothing gets slower; an unearned speed-up is simply not being claimed.** A worker-scoped lifetime remains open, and gets taken when the real per-file cost has been measured on a real application and the runner integration is chosen deliberately rather than inherited from whatever the wiring happened to do
|
|
649
|
-
|
|
650
|
-
## 4.13.0 - 2026-08-12
|
|
651
|
-
|
|
652
|
-
### ⚠ Upgrading from 4.12.0 — read this first
|
|
653
|
-
|
|
654
|
-
**Four breaking changes. Every one of them fails visibly, and every one is fixed by a single line or a single config key.** Three are security defaults that were wrong; the fourth is an import path.
|
|
655
|
-
|
|
656
|
-
| What breaks | How you'll see it | The fix |
|
|
657
|
-
| ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
658
|
-
| **`http.cors` now actually applies** — it never had any effect in any release through 4.12.0 | requests from origins you never allow-listed start being rejected | check `http.cors` before upgrading; it now means what it says |
|
|
659
|
-
| **`http.bodyLimit` defaults to Fastify's 1 MB**, not 200 GB | large uploads that used to be accepted answer `413` | set `http.bodyLimit` explicitly if you need more |
|
|
660
|
-
| **`http.trustProxy` defaults to `false`** | `request.ip` becomes the socket address instead of `X-Forwarded-For` | set `http.trustProxy: true` **only if** you are genuinely behind a proxy that strips the header |
|
|
661
|
-
| **The package entry no longer re-exports the CLI, dev server, test helpers or Vite integration** | build fails with `has no exported member` | test helpers move to `@warlock.js/core/tests`, `lowerStage3Decorators` to `@warlock.js/core/vite` — **one line per import.** The CLI and dev-server internals are **not** public and have no replacement specifier |
|
|
662
|
-
|
|
663
|
-
**If your app configures none of the three HTTP keys, the first three changes make it strictly safer with no action from you.** The `trustProxy` default in particular meant per-IP rate limiting was bypassable by anyone sending their own `X-Forwarded-For`.
|
|
664
|
-
|
|
665
|
-
Details for each are in the entries below.
|
|
666
|
-
|
|
667
|
-
### Added
|
|
668
|
-
|
|
669
|
-
- **`build.singleBundle` — one file you can run with `node dist/app.js`.** The default build keeps dependencies as real `import` specifiers resolved from `node_modules`, which is right when you deploy the folder. Producing a single self-contained file previously meant knowing to set `packages: "bundle"` **and** `splitting: false`, and it still did not work
|
|
670
|
-
|
|
671
|
-
It sets both as **defaults you can override** — an explicit `splitting: true` in your config still wins. Phase ordering is unaffected: it comes from the generated entry using dynamic `await import(...)`, which the bundler defers to the call site whether or not splitting is on. A comment in the builder claiming ordering "requires `splitting: true`" was wrong and has been corrected
|
|
672
|
-
|
|
673
|
-
⚠ Native `.node` addons cannot be inlined by any bundler and are still emitted alongside the file, so "single bundle" means one JavaScript file plus any native addons — the flag does not promise otherwise
|
|
674
|
-
|
|
675
|
-
**Verified end to end on a real application**, not in isolation: the build emits exactly one file, the interop prelude is present, and the result boots and serves a request. This only became possible once dev-only tooling left the production module graph — see the entry below, which is what made `singleBundle` build a real application rather than only a synthetic one
|
|
676
|
-
|
|
677
|
-
- **`@warlock.js/core/tests` and `@warlock.js/core/vite` are real subpaths**, with their own build entries and `exports` keys — the first version in which those helpers are addressable at all. `/tests` carries the 13 documented test helpers; `/vite` carries `lowerStage3Decorators`
|
|
678
|
-
|
|
679
|
-
**The test server's port channel — `publishTestServerPort`, `withdrawTestServerPort`, `TEST_SERVER_PORT_ENV_KEY` — is deliberately NOT part of that surface.** It is how `startHttpTestServer` hands the resolved port to the worker processes that later call `testGet`; it is plumbing, and nothing outside the framework needs it. **Decided before the subpath shipped rather than after, because removing an export once people depend on it is a breaking change**
|
|
680
|
-
|
|
681
|
-
### Fixed
|
|
682
|
-
|
|
683
|
-
- **`setupTest()` no longer crashes in a project that has no `src/config/tests.ts`.** `config.get("tests")` resolves an absent key to `null`, and the result was dereferenced — so the very path `warlock add test` generates threw `Cannot read properties of null (reading 'connectors')` before running a single test. **`setupTest()` with no arguments at all** threw one step earlier still, on a destructured parameter with no default
|
|
684
|
-
|
|
685
|
-
**`connectors: false` now starts no connectors, as its type and the skill have always said.** The selection read `testConfig.connectors || connectors`, so a configured `false` was discarded, and every non-array value then took the "start all but http" branch — **`false` started everything except http, from config and from the caller alike.** It is now `??` with an explicit `false` check
|
|
686
|
-
|
|
687
|
-
⚠ **Config still beats the parameter:** if `tests.connectors` is set, `setupTest({ connectors })` cannot override it. That is the existing contract and this release does not change it — one line of `skills/test-service` claimed otherwise and has been corrected. **A per-call override is a contract decision for a later release**
|
|
688
|
-
|
|
689
|
-
- **The request helpers send falsy JSON bodies.** `testPost`, `testPut` and `testPatch` used `body ? JSON.stringify(body) : undefined`, so **`false`, `0`, `""` and `null` — all legal JSON documents — were sent as no body at all.** Only an omitted argument now means "no body"
|
|
690
|
-
|
|
691
|
-
**Headers are accepted in every shape `RequestInit` allows.** They were merged by object spread, which is correct only for a plain record — a `Headers` instance or a `[name, value]` tuple list was silently turned into an object with numeric keys and the header was lost with no error. They are now normalised through `new Headers(...)`
|
|
692
|
-
|
|
693
|
-
**`Content-Type: application/json` is set only when the helper serialized the body**, and never over a content type the caller set. It was previously forced onto every request, including `FormData` — replacing the multipart boundary the runtime generates and producing a request no server can parse
|
|
694
|
-
|
|
695
|
-
- **Shutdown survives a throwing log channel.** A connector whose `shutdown()` failed was reported through `log.error(...)` **from inside the catch block** — and `Logger.log()` hands each entry to `channel.log()` with no isolation, so a channel that throws synchronously (a misconfigured transport, an unserialisable payload) made that report reject. The rejection escaped `shutdown()` entirely, and the consequences went well past a missing log line: **`log.flush()` never ran, so every buffered entry from the whole run was lost; the remaining connectors were never torn down; and `process.exit(0)` — the line `gracefulShutdown` runs once `shutdown()` resolves — was never reached, leaving the process alive on the handles those connectors still held**
|
|
696
|
-
|
|
697
|
-
⚠ **This hardens the shutdown path, not the logger.** `Logger.log()` still aborts its fan-out on the first throwing channel, so the other channels never receive that entry, and an _asynchronously_ rejecting channel is not covered at all — `channel.log()` is never awaited. **Logger-wide isolation is a separate fix in a later release**
|
|
698
|
-
|
|
699
|
-
- **A test server that fails to start no longer leaves half of itself running.** `startHttpTestServer()` publishes the resolved port before the late connector phase and sets `isServerRunning` only on its last line, so a failure in between left **live early-phase connectors and a published port pointing at a server that never came up** — while `stopHttpTestServer()` in `globalTeardown` reported _"No server to stop"_ and walked away from them. Startup now unwinds what it started, always withdraws the port and resets its state. ⚠ **The error you get back is unchanged — it always was.** Startup had no `catch` at all, so the original failure already propagated correctly; what was missing was the cleanup, and the new `catch` exists only to run it. A failure _during_ that cleanup is reported and never substituted for the cause, which is the one propagation guarantee the wrapper had to be careful not to break
|
|
700
|
-
|
|
701
|
-
**`stopHttpTestServer()` withdraws the port and resets state in a `finally`.** They previously ran after the `await`, so a shutdown that threw left the published port behind and the next run in the same process inherited it
|
|
702
|
-
|
|
703
|
-
- **`startHttpTestServer({ port: 0 })` is refused with an instruction instead of half-working.** `0` is the OS's "pick a free one" idiom, and the test server cannot honour it: the preflight would bind some unrelated ephemeral port and pass without proving anything, and nothing publishable exists afterwards — `HttpConnector.start()` records the port it **asked for**, not the one Fastify bound. Accepting it silently meant `getTestServerUrl()` resolved `0` through its own config fallback and **every worker request went to `http://host:0`**, where nothing listens. The error names the fix: pass an explicit port, or set `http.port`
|
|
704
|
-
|
|
705
|
-
- ⚠ **BREAKING — the package entry no longer re-exports the CLI, the dev server, the test helpers or the Vite integration.** Five `export *` lines are gone from `@warlock.js/core`'s root: `./cli`, `./dev-server/files-orchestrator`, `./dev-server/health-checker`, `./tests` and `./vite`
|
|
706
|
-
|
|
707
|
-
**Why it had to change:** those five put dev-only tooling into the **static module graph of every application that imports the framework** — 39 files, reaching ESLint and, through it, ESLint's optional `jiti` import. It cost nothing while the builder kept `packages: "external"`, because esbuild never walked into the framework. **Anything that bundles walks it, and the build fails.** That is why `singleBundle` could not build a single real application
|
|
708
|
-
|
|
709
|
-
⚠ **Making those imports lazy does not help and should not be attempted.** esbuild resolves `import()` at build time; a dynamic import defers _evaluation_, not resolution. Measured: `await import("jiti")` in an otherwise empty file still fails with `Could not resolve "jiti"`. **Only unreachability from the entry removes a module from the graph**
|
|
710
|
-
|
|
711
|
-
**The two public halves are now reachable from a subpath — and in 4.12.0 and earlier they were reachable from nowhere at all.** `./tests` and `./vite` are **real build entries with their own emitted files and `exports` entries**, not reachable from the root barrel, which is the entire point:
|
|
712
|
-
|
|
713
|
-
```ts
|
|
714
|
-
// before // after
|
|
715
|
-
import { setupTest } from "@warlock.js/core";
|
|
716
|
-
import { setupTest } from "@warlock.js/core/tests";
|
|
717
|
-
import { startHttpTestServer } from "@warlock.js/core";
|
|
718
|
-
import { startHttpTestServer } from "@warlock.js/core/tests";
|
|
719
|
-
import { lowerStage3Decorators } from "@warlock.js/core";
|
|
720
|
-
import { lowerStage3Decorators } from "@warlock.js/core/vite";
|
|
721
|
-
```
|
|
722
|
-
|
|
723
|
-
**The CLI and dev-server internals are different — they were never a public API and have no replacement specifier.** If you were importing from those, you were reaching into framework internals; open an issue describing what you needed.
|
|
724
|
-
|
|
725
|
-
⚠ **If you were importing these symbols in 4.12.0, they were already not in the published package.** `esm/tests/` and `esm/vite/` did not exist in the 4.12.0 artifact, and none of those symbols appear in `esm/index.d.mts` — **the root barrel re-exported source the build never emitted.** So this is the release that makes them installable, not the one that removed them.
|
|
726
|
-
|
|
727
|
-
`Path` is unaffected — it moved to a genuine utility module and remains exported
|
|
728
|
-
|
|
729
|
-
**Removing the five lines was necessary but not sufficient.** Three production modules — `connectors/http-connector`, `connectors/connectors-manager` and `warlock-config/warlock-config.manager` — imported the dev server's console formatter directly. In `http-connector` the dev-console call was simply **deleted**: the same error was already routed through `log.fatal` on the following line. `connectors-manager` now logs a connector's shutdown failure through `@warlock.js/logger`, **awaited and then flushed** — `process.exit(0)` follows immediately, so an un-awaited log is a log that never happens. `warlock-config.manager`'s _"`warlock.config.ts` is missing"_ warning writes **straight to the console instead**: it runs during CLI bootstrap, before the logger has a single channel configured, so routing it through the logger would drop it in every application
|
|
730
|
-
|
|
731
|
-
`tests/unit/meta/production-entry-graph.test.ts` enforces this from now on. It keys on **our own directory names** rather than a denylist of third-party packages, because a denylist rots the moment a dependency changes its imports and can only catch names someone thought of
|
|
732
|
-
|
|
733
|
-
- ⚠ **BREAKING — the CORS allow-list in `http.cors` now actually applies.** The framework's defaults were spread **after** your configuration, so `{ origin: "*", methods: "*" }` overwrote whatever you set. **`http.cors` has never had any effect**, in any release up to 4.12.0 — an app that configured an allow-list still answered every origin. Your configuration now wins
|
|
734
|
-
|
|
735
|
-
**This is breaking in the direction you want, but it is breaking:** an app that has been relying on the accidental `origin: "*"` while believing it was restricted will now restrict. Check your `http.cors` before upgrading
|
|
736
|
-
|
|
737
|
-
Worth knowing why the old behaviour was also broken for the legitimate case: `Access-Control-Allow-Origin: *` together with `Access-Control-Allow-Credentials: true` is a pair **browsers reject**, so a cross-origin `fetch(…, { credentials: "include" })` failed outright. The default was simultaneously too open for unauthenticated reads and unusable for authenticated ones
|
|
738
|
-
|
|
739
|
-
- ⚠ **BREAKING — `http.bodyLimit` no longer defaults to 200 GB.** An app that configures nothing now gets **Fastify's own 1 MB limit**. The previous default did not merely allow large bodies, it **replaced a protection Fastify provides**: an unauthenticated endpoint accepted a 5 MB body and ran application logic on it where bare Fastify would have answered `413`
|
|
740
|
-
|
|
741
|
-
**If you accept uploads, set `http.bodyLimit` before upgrading** — otherwise those requests start failing with `413`. The failure is immediate and one config key fixes it
|
|
742
|
-
|
|
743
|
-
The old comment recommended `middleware.maxBodySize()` for per-route caps. **That middleware cannot do this job** and its documentation has been corrected: framework middleware runs inside the route handler, after `@fastify/multipart` has already parsed the body, so it can report `Content-Length` but cannot refuse the read. For a real per-route cap use `serverOptions.bodyLimit`, which Fastify enforces while reading
|
|
744
|
-
|
|
745
|
-
- ⚠ **BREAKING — `http.trustProxy` now defaults to `false`.** `request.ip` was derived from the client-supplied `X-Forwarded-For` header by default, and `@fastify/rate-limit` keys its buckets on `request.ip` — so **a client sending a different `X-Forwarded-For` on each request got a fresh rate-limit bucket every time.** Any deployment not behind a proxy that strips the header had bypassable rate limiting, and the same applied to per-IP lockouts and audit logs
|
|
746
|
-
|
|
747
|
-
**If your app is behind a proxy, set `http.trustProxy = true`.** Until you do, `request.ip` reports the proxy's address rather than the client's — a visible, diagnosable change, unlike the previous default, which could not be detected from inside the application
|
|
748
|
-
|
|
749
|
-
- **Per-route `serverOptions` are no longer discarded by the dev server.** `scanDevServer` registers wildcard routes and dispatches per request, so it had no per-route registration slot and dropped `serverOptions` entirely. A route declaring `serverOptions.onRequest` — the documented way to run **before body parsing** — worked in production and **silently never ran in dev**, which is the only mode most teams run. `route.rateLimit` was dropped the same way, since it rides in the same options object
|
|
750
|
-
|
|
751
|
-
Matching now happens in Fastify's own `onRequest` phase and the matched route's hooks run there, so `onRequest`, `preValidation` and `preHandler` are forwarded with their pre-parse ordering intact
|
|
752
|
-
|
|
753
|
-
⚠ **Two options still cannot be forwarded in dev, and `serverOptions`' documentation now says so rather than implying otherwise:** `bodyLimit`, because Fastify reads it at registration time and no hook can bound a body already being parsed; and `preParsing`, because it must return the payload stream. For a pre-parse guarantee that holds in both modes, register a server-level hook via `router.beforeScanning` — it must be idempotent per request, since a connector restart builds a new server and runs the callback again
|
|
754
|
-
|
|
755
|
-
- **The dev server no longer rebuilds its entire route registry on every request.** A comment claimed the registry was initialised "once" and pointed at a `rebuildRouteRegistry` function that **does not exist**; the code sat inside the per-request handler, re-registering every route on every hit — and `router.any()` routes expand into seven registrations each. It is now built once and rebuilt when the route table changes
|
|
756
|
-
|
|
757
|
-
- **The dev dispatcher logs through the framework logger instead of `console.log(error)`,** with the request method and url attached
|
|
758
|
-
|
|
759
|
-
- **A bundled production build no longer succeeds and then dies at startup.** Setting `packages: "bundle"` produced a clean build whose process failed immediately with `Error: Dynamic require of "node:assert" is not supported`. Warlock's output is an ES module; bundled CommonJS dependencies call `require(...)` and read `__dirname` to locate their own assets, and neither exists in an ES module, so the bundler substituted a stub that throws. The only fix available to an application author was to hand-write an esbuild `banner` recreating `require` via `createRequire(import.meta.url)` — esbuild internals no app should need to know
|
|
760
|
-
|
|
761
|
-
The interop prelude is now injected automatically for **any** ESM build (`build.esmShim`, default `true`), not only under `singleBundle` — so a hand-written `packages: "bundle"` works too. It defines `__filename` and `__dirname` as well as `require`: a dependency resolving an asset path with a missing `__dirname` gets `undefined` and fails later, further from the cause
|
|
762
|
-
|
|
763
|
-
**An existing hand-written `banner` is preserved.** esbuild's `banner` is an object and every merge on the way to it replaces rather than merges, so previously whichever was applied second silently deleted the other. The shim is prepended to your banner instead
|
|
764
|
-
|
|
765
|
-
## 4.12.0
|
|
766
|
-
|
|
767
|
-
### Added
|
|
768
|
-
|
|
769
|
-
- **`warlock migrate --pending` — what will run next, in the order it will run.** `migrate` could report what had already run (`--list`) and what files existed on disk (`--all`), but not the one thing an operator asks before a schema change against a live database. The pending set was already computed on every migrate run; it simply had no read-only exit
|
|
770
|
-
|
|
771
|
-
The gap forced a workaround that is **unsound in the dangerous direction**. `--all` globs `src/app` only, so it cannot see migrations a _package_ registers through `database.migrations` — `@warlock.js/auth` alone contributes two. `--list` reads the migrations table, which does contain them. Differencing the two counts subtracts populations that do not overlap, and it under-counts pending by roughly the number of package migrations installed — reporting "nothing else is pending" when something is
|
|
772
|
-
|
|
773
|
-
**`migrate --list` now prints both sections**, executed and pending, so the question can be answered without knowing a second flag exists. The executed section prints **first and unconditionally**: it is a table read that cannot fail because of a broken file on disk, and `--list` is the command reached for while something is already wrong. `--list` always exits `0` — it is a report
|
|
774
|
-
|
|
775
|
-
**`--pending` is the gate**, and its exit code is its entire API: **`0`** computed and nothing pending, **`1`** computed and N pending, **`2`** could not be computed. Two codes would fold "three migrations are waiting" into "I could not work out what is waiting", and those demand opposite responses — the first is _run them_, the second is _stop_. `migrate --pending && deploy` behaves correctly under all three
|
|
776
|
-
|
|
777
|
-
**A failure to read the migrations never reports `0`.** Computing pending requires loading project code, and a single migration file missing its default export throws. That degrades to an explicit `Pending: unavailable — <reason>` line with the executed listing intact above it, and `--pending` exits `2`. An empty pending set means _nothing is pending_, and nothing else
|
|
778
|
-
|
|
779
|
-
`--all` deliberately does **not** gain a migration name beside each path. The only identifier available without loading is the one derived from the filename, and that derivation is a _fallback_ used when a migration does not set `migrationName` — so any migration that names itself (`auth`'s do) would be listed under a name that does not exist. A wrong identifier in a listing whose purpose is cross-referencing is worse than no identifier, and `--list`'s two sections answer the comparison directly
|
|
780
|
-
|
|
781
|
-
Proven against a real Postgres: an executed package migration and a pending local one land in the correct sections, a fully-migrated database reports an empty pending list rather than an absent one, and the reporter's `files − executed` arithmetic is pinned as a test that fails if it is ever reintroduced
|
|
782
|
-
|
|
783
|
-
### Changed
|
|
784
|
-
|
|
785
|
-
- `migrate`'s preload block no longer declares `env: true`. The flag has done nothing since env began loading for every command that declares a preload block; it was decoration, and the test suite now asserts its **absence** so it is not re-added by someone reading the still-deprecated type
|
|
786
|
-
|
|
787
|
-
- **The package now declares its own test runner and a `test` script.** `@warlock.js/core` shipped a maintained `vitest.config.ts` — aliasing eight sibling packages to their sources — with **no `devDependencies` key at all** and no way to invoke it. Its suite was reachable only by knowing to type `npx vitest`, which resolves whatever happens to exist in the tree rather than anything the manifest asked for. The runner is pinned to an **exact** version, not a range: it moved from 4.1.8 to 4.1.10 mid-development on an unrelated install, silently, and a suite whose runner can change underneath it proves less than it appears to
|
|
788
|
-
|
|
789
|
-
### Fixed
|
|
790
|
-
|
|
791
|
-
- **A build artifact that names an entry point it does not contain is now refused before it can be packed.** An interrupted build leaves a directory that looks finished — `package.json`, `README`, `CHANGELOG`, `bin/`, `skills/` — and holds no compiled code at all. Nineteen existed in this tree at once, and nothing in the release path noticed: the only related guard compares **modification times**, so a hollow directory with a freshly written manifest is _newer than source_ and passes, and it runs solely on the artifact-reuse path, which is not how the hollow directories were produced
|
|
792
|
-
|
|
793
|
-
Each artifact is now verified immediately before `npm pack`, on the normal build path and the reuse path alike. **The manifest is the specification:** `main`, `module` and the typings field name the exact files the package promises to ship, so they are resolved against the artifact and must exist. Fields a manifest does not declare are skipped — `core` and `auth` point `main` at `esm/` while `cascade`, `ai` and `seal` point it at `cjs/`, and any check that assumed one build shape would have raised a false failure on packages that are entirely correct. A manifest declaring no entry point at all is also a failure: a published package nothing can import is not a package
|
|
794
|
-
|
|
795
|
-
- **The production acceptance gate no longer inherits the environment it is supposed to be testing.** `run-pnpm-acceptance.mjs` spawned every child with `env: { ...process.env }` and set no `NODE_ENV`. It exercised the production path only because the shell it was written in happened to carry `NODE_ENV=production`; on a clean checkout, a new contributor's machine, or CI, the same gate boots the app in **development** — and does not fail, it passes while testing something other than the thing it is named after. That is the worst outcome available to a gate, and it sat underneath the proof for 4.11.0's headline fix
|
|
796
|
-
|
|
797
|
-
`NODE_ENV=production` is now set explicitly on every spawn, and — more importantly — **asserted from inside the running app**: `/acceptance` reports the environment it actually booted in, and the run fails if it is anything else. Setting a variable and never checking it arrived is how the original defect survived. The remaining `{ ...process.env }` is documented as a deliberate inheritance of `PATH` and the package-manager store paths, with everything the _verdict_ depends on set after it
|
|
798
|
-
|
|
799
|
-
Consequence for the roadmap, recorded because the ordering matters: **CI wiring for this gate is now blocked on this fix, not parallel to it.** Wiring it up first would have produced a green from CI — which carries more weight than a local one — for a run that never touched the production path
|
|
800
|
-
|
|
801
|
-
- **`warlock migrate --rollback=false` no longer drops every table.** CLI options were parsed as raw strings and nothing ever coerced them: `--rollback=false` reached the action as the string `"false"`, `if (rollback)` saw a truthy value, and the run rolled back _everything_. The declared `type: "boolean"` on the option was decorative — used only to render help. The same shape existed on every boolean option, including `warlock drop.tables --force=false`, where it turned a confirmation prompt into an unattended drop
|
|
802
|
-
|
|
803
|
-
Its twin was worse. A bare `--flag` swallowed the following token as its value, so `warlock migrate --rollback 2024_users.ts` produced `rollback: "2024_users.ts"` — the filename was never read as a path, and every table went down while the operator believed they had named one file. **A declared boolean now never consumes the next positional**: `--rollback 2024_users.ts` is `rollback: true` plus the positional `2024_users.ts`
|
|
804
|
-
|
|
805
|
-
Coercion is **type-aware, driven by the command's own declared options**, not a blanket rule in the parser: `--flag` → `true`, `--flag=true|1|yes` → `true`, `--flag=false|0|no` → `false` (case-insensitive), and short aliases (`-r=false`) behave identically. A string-typed option whose value is genuinely the word `false` — `--name=false` — still arrives as the string `"false"`. Options a command does not declare are untouched
|
|
806
|
-
|
|
807
|
-
**An unreadable value is an error, not a guess.** `--rollback=maybe` prints what was invalid and what is accepted, and exits 1. Guessing is what produced this defect; a flag that gates a destructive action must refuse input it cannot read rather than pick a side
|
|
808
|
-
|
|
809
|
-
`parseCliArgs` now takes an optional schema and runs twice: once bare to discover the command name (behaviour unchanged — there is no command to consult yet), then again against the resolved command's options. Re-reading argv is what makes the swallowed positional recoverable; by the time the first pass returns, a swallowed argument is indistinguishable from a value
|
|
810
|
-
|
|
811
|
-
- **`warlock generate.module users --force=false` no longer overwrites your files.** The coercion above is opt-in by design — it applies only to options a command declares `type: "boolean"`, so a string option whose value is genuinely the word `false` survives. The generate family and `add` never carried that declaration, so the fix reached none of them and both faces of the defect stayed live on the commands most likely to be run against existing source
|
|
812
|
-
|
|
813
|
-
`--force=false` arrived at every generator as the truthy string `"false"` and the overwrite guard (`if (exists && !force)`) let it through — a flag written to _prevent_ clobbering did the clobbering. Its twin ate the target: `warlock generate.module --force users` parsed `users` as the value of `--force`, so the module name was lost entirely and the generator ran with no name
|
|
814
|
-
|
|
815
|
-
Twenty-three option declarations are now typed: `--force, -f` and `--dry-run` on all eight `generate.*` commands, plus `--minimal, -m`, `--with-validation, -v`, `--with-resource, -rs`, and both `--timestamps [bool]` declarations, and `--list, -l` / `--no-install` on `add`. Options that carry real data are deliberately untouched and still take a value — `--table`, `--add`, `--drop`, `--rename` on the generators, `--package-manager` on `add`, and `seed --drop="Seed Name"`, whose value scopes which seeder is undone
|
|
816
|
-
|
|
817
|
-
`add --no-install` no longer has to be passed last. That instruction was in its help text only because the bare flag used to swallow the feature that followed it; `warlock add --no-install auth` now records `auth` as the feature and skips the install, and the wording is gone
|
|
818
|
-
|
|
819
|
-
The guard drives the real command objects through the manager's own resolution path (`tests/unit/cli/generate-flag-options.test.ts`) and asserts what the action is handed. Asserting the declaration object instead would pass against a fixture while the CLI stayed broken
|
|
820
|
-
|
|
821
|
-
- **`new Image(...)` no longer fails depending on how soon you call it.** The `Image` module fired `import("sharp")` at load time without awaiting it, and the constructor only checked whether that import had _failed_ — never whether it was still in flight. Constructing an image in the first tick after importing the package therefore ran with an undefined sharp function and died with `TypeError: sharpFn is not a function`; the exact same code passed if something had awaited a timer first. Anything that builds an image during boot — a startup thumbnail job, a module-level warm-up — hit it, and it presented as a mysterious "works locally, breaks in prod" timing bug rather than as a missing dependency
|
|
822
|
-
|
|
823
|
-
Sharp is now resolved **synchronously on the first construction that needs it**, via `createRequire`, and the outcome is cached for the process. There is no longer a window in which the constructor can proceed without a real sharp function: it either has the module or throws. A missing sharp still throws the same install-hint error, at the same point (construction), with the same wording
|
|
824
|
-
|
|
825
|
-
Resolution stays **lazy** — importing `@warlock.js/core` still does not load sharp's native binary, so apps that never touch images pay nothing — and constructing an `Image` from an existing sharp instance short-circuits before any module load
|
|
826
|
-
|
|
827
|
-
The guard for this is a spawned fresh Node process that imports and constructs with nothing in between (`tests/unit/image/image-sharp-resolution.test.ts`). A same-process test cannot catch it: importing at collection time and constructing later _is_ the delay that hides the bug
|
|
828
|
-
|
|
829
|
-
- **A sharp that is installed but will not load no longer reports itself as "not installed".** The resolution above swallowed every failure into a single outcome, so the most common real-world sharp problem — the package present but its native binary built for another platform — arrived as `sharp is not installed.` plus instructions to run `npm install sharp`, which cannot fix it. sharp throws its own long, actionable error naming the runtime, the failing `.node` file and the exact install flags to use; that text was discarded and replaced with a different, wrong cause
|
|
830
|
-
|
|
831
|
-
Only **genuine absence** now produces the install hint: a `MODULE_NOT_FOUND` whose message names the specifier `'sharp'` exactly. Matching on the code alone is not sufficient — a dependency missing _inside_ sharp raises the very same code (`Cannot find module 'color'`), and would have been reported as sharp itself being absent. Any other failure surfaces sharp's own message, inlined as `Failed to load "sharp": …` **and** chained as `cause`, so a terminal that never prints `cause` still shows the text that helps. The absent-sharp path is unchanged, wording included
|
|
832
|
-
|
|
833
|
-
**The failure reason is cached, not just the fact of failure.** The resolution attempt runs once per process; a second `new Image(...)` skips the load entirely, so caching only "there is no sharp function" would have re-told the same lie one call later. The guard therefore constructs **twice** in each spawned process and asserts the second error matches the first (`tests/unit/image/image-sharp-load-failure.test.ts`) — a one-shot test passes even with that bug present
|
|
834
|
-
|
|
835
|
-
The `MODULE_NOT_FOUND` shapes the guard feeds in are produced by asking Node for a module that genuinely is not installed, rather than hand-written, so the matcher is tested against Node's real message text
|
|
836
|
-
|
|
837
|
-
- **`renderReact()` no longer renders against modules that have not loaded yet.** The same defect as the two above, in a second module, found by looking for the pattern rather than by a bug report. `react/index.ts` fired `import("react")` and `import("react-dom/server")` at load time without awaiting either, and tracked them with a three-state flag that the guard only tested for one state: `if (moduleExists === false)`. While the imports were in flight the flag was `null`, which is not `false`, so the guard passed and the synchronous `renderReact` read `createElement` off `undefined`. With two sequential dynamic imports the window is wider than the image module's, and it is open during exactly the work a server does at boot — rendering a page or an email template from a module-level warm-up
|
|
838
|
-
|
|
839
|
-
The window swallowed the diagnostics as well as the render. A genuinely missing react did **not** produce the install hint during that window: it produced `TypeError: Cannot read properties of undefined (reading 'createElement')`, because the flag was still `null` rather than `false`. The install instructions only appeared for callers late enough to have missed the race — the callers who least needed telling
|
|
840
|
-
|
|
841
|
-
Both modules are now resolved **synchronously on the first `renderReact()` that needs them**, via `createRequire`, with the outcome cached for the process. Resolution stays **lazy**: importing `@warlock.js/core` still does not pull react into apps that never render. A genuinely absent react throws the same `react is not installed.` message with the same instructions, unchanged
|
|
842
|
-
|
|
843
|
-
- **A broken `react-dom/server` no longer reports itself as `react is not installed`.** The two packages were loaded in one `try` and collapsed into one flag, so any failure of either was attributed to react. The specifiers are now resolved and reported **separately**, and the message names the one that actually failed — `Failed to load "react-dom/server": …` — because sending an operator to reinstall react when react is fine costs them the debugging session. Absence is distinguished from breakage the same way as for sharp: `MODULE_NOT_FOUND` **and** a message naming the specifier exactly, quoted, which is also what stops `'react-dom'` from satisfying a check for `'react'`. Everything else surfaces the original error, inlined and chained as `cause`. A `react-dom` whose `./server` subpath is missing from `exports` raises `ERR_PACKAGE_PATH_NOT_EXPORTED`, so it correctly reports as an incompatible install rather than an absent one
|
|
844
|
-
|
|
845
|
-
As with sharp, the failure **reason** is cached rather than only the fact of failure, so the second call cannot fall through to the "not installed" branch and re-tell a lie the first call got right
|
|
846
|
-
|
|
847
|
-
The guard is a spawned fresh Node process (`tests/unit/react/react-module-resolution.test.ts`); a same-process test cannot catch this, which is why the pre-existing suite was green against it. Against the live bug all five cases failed, and — the detail that shows how much the race hid — all five failed with the _same_ `Cannot read properties of undefined` symptom, including the case that only asserts the install message. The present-but-broken states are staged by copying the module source next to a fixture `node_modules`, since resolution anchors to the importing file; the real workspace is not mutated
|
|
848
|
-
|
|
849
|
-
## 4.11.0
|
|
850
|
-
|
|
851
|
-
### Added
|
|
852
|
-
|
|
853
|
-
- `startHttpTestServer({ port })` — run an integration suite on an explicit port, honoured over `HTTP_PORT` in `.env`, which the internal bootstrap re-reads and no caller could previously override
|
|
854
|
-
- the test server preflights its port and fails with "stop the dev server" naming the port, instead of a raw `EADDRINUSE` from inside Fastify
|
|
855
|
-
- `Application.setServedPort()` and a `port` field on the readiness signal, so a supervisor learns the bound http port from the app rather than re-deriving it from config it may not be able to read
|
|
856
|
-
- `setConfig(name, value)` — the write side of the config store, exported separately from the read-only `config` accessor so registering configuration stays a deliberate boot-time act
|
|
857
|
-
|
|
858
|
-
### Changed
|
|
859
|
-
|
|
860
|
-
- `@mongez/dotenv` is now required at `^1.3.1` (was `^1.2.4`). Under the old range a fresh install resolved to 1.3.x while an existing lockfile could stay on 1.2.x, so we could not say which behaviour a given consumer actually had. 1.3.x only changes cases that were previously wrong: `env()` now consults `process.env` instead of returning a default for a key the environment defines, `${VAR}` interpolation throws naming the key instead of baking the string `"undefined"` into a value, and numeric coercion no longer corrupts values like `0123456789` or IDs beyond 2^53. Precedence between `.env` files and injected variables is unchanged
|
|
861
|
-
|
|
862
|
-
Core keeps its own guard for a missing `.env` regardless of which version resolves — an application can pin its own transitive dependencies, so the installed version is never something core can assume
|
|
863
|
-
|
|
864
|
-
### Deprecated
|
|
865
|
-
|
|
866
|
-
- the `env` preloader flag on a CLI command is no longer read — env is loaded for every command that declares a preload block. Setting it is harmless and does nothing; remove it. Dropped at 5.0
|
|
867
|
-
|
|
868
|
-
### Fixed
|
|
869
|
-
|
|
870
|
-
- **A production bundle no longer imports a package your app does not declare.** `warlock build`'s generated config loader emitted `import config from "@mongez/config"` — one of _core's_ dependencies, never the app's. npm and yarn hoist flat so it resolved by accident; under pnpm's strict layout the shipped bundle died at boot with `ERR_MODULE_NOT_FOUND` for a package the app had no reason to install. The generator now emits `setConfig` from `@warlock.js/core`, which the app does declare, and Node resolves `@mongez/config` from core's own install — correct under pnpm, and portable, unlike baking absolute paths into an artifact meant to be copied between machines
|
|
871
|
-
|
|
872
|
-
**The rule is now enforced, not just followed.** `warlock build` fails if any specifier written into generated code is missing from the app's `dependencies`, listing every violation at once. Rewriting the one bad import fixes today's bundle; the check is what stops the next change to the generator from reintroducing it invisibly — under npm and yarn the mistake never surfaces
|
|
873
|
-
|
|
874
|
-
Two scaffolding sites had the same defect and are fixed with it: `warlock generate.module` emitted `groupedTranslations` from `@mongez/localization`, and the communicators config stub emitted `env` from `@mongez/dotenv`. Both now come from `@warlock.js/core`, which already re-exports them
|
|
875
|
-
|
|
876
|
-
- **`env()` inside `warlock.config.ts` no longer always returns its default.** The config module was evaluated _before_ any `.env` file was read, so a project following the documented `build: { outdir: env("BUILD_OUT", "dist") }` recipe silently got `dist` no matter what the environment said — under every command, `dev` included, and under `build` and `start` env was never loaded at all. Env files are now loaded before `warlock.config.ts` is evaluated, for every command
|
|
877
|
-
|
|
878
|
-
Loading is guarded: a project with no `.env` is legitimate and must not start failing `warlock build` now that env loads everywhere. `NODE_ENV` remains authoritative for which file is chosen — no command forces the environment, so a deliberate `NODE_ENV=staging` build still reads `.env.staging`. With `NODE_ENV` unset, plain `.env` is read
|
|
879
|
-
|
|
880
|
-
- **An application without `src/config/storage.ts` can boot again.** The storage connector starts unconditionally, on the documented grounds that `storage.init()` falls back to a built-in `local` driver so file storage works out of the box. That fallback was never implemented: `init()` resolved the default driver _name_ and then found nothing registered under it, so any app without a storage config died at boot with `Storage driver "local" is not configured`. A built-in `local` driver rooted at `uploadsPath()` is now registered before configured drivers — so an app defining its own `local` still overrides it, and naming a driver that genuinely does not exist still fails loudly
|
|
881
|
-
|
|
882
|
-
Only scaffolded apps hid this, because `create-warlock` always ships a storage config
|
|
883
|
-
|
|
884
|
-
- `startHttpTestServer` no longer breaks a suite that configures `http.port: 0`. `0` is the OS's "pick a free port for me" idiom, but the guard only checked `typeof port !== "number"`, so `0` fell through: the preflight bound an unrelated ephemeral port and passed without proving anything, and `0` was then published as the bound port, pointing every request in the suite at `http://host:0`. An explicit `0` now takes the same path as no configured port — no preflight, nothing published, Fastify picks the port
|
|
885
|
-
|
|
886
|
-
- **`warlock start` no longer claims success before the app has booted.** The startup banner printed in `preAction` — before the child process was even spawned — and the failure that followed went only to stderr. Any CI gate or process supervisor that watches stdout for the banner read a 🔴 boot failure as a healthy start, which is how a production app that never booted was recorded as running. The banner now prints only when the application reports a completed boot, and a child that dies before reporting is a failed start: the message goes to **both** stdout and stderr, and the exit code is forced non-zero even when the process itself exited `0`
|
|
887
|
-
|
|
888
|
-
Readiness is signalled, never assumed. `Application.markBooted()` sends a versioned `warlock:ready` message — `{ type, version, pid, at, environment, runtimeStrategy, bootDurationMs?, port? }` — over the IPC channel `warlock start` opens, and closes that channel immediately so an open handle can't keep a wedged process looking alive. The signal is gated on a `WARLOCK_BOOT_SIGNAL` handshake the CLI sets on the child it spawns, so an app running under pm2 or any other supervisor never writes into a channel it does not own
|
|
889
|
-
|
|
890
|
-
A bundle built by an older Warlock has no readiness signal. It still starts normally and draws a note on **stderr only** telling you to re-run `warlock build` — an absent signal is never an error, never fails a run, and never kills a slow boot
|
|
891
|
-
|
|
892
|
-
**Upgrading:** re-run `warlock build` so the bundle can report readiness; until you do, `warlock start` runs your app but prints no started banner. Anything parsing `warlock start` output should note that progress lines now go to stderr — stdout carries the started banner and start failures, nothing else
|
|
893
|
-
|
|
894
|
-
## 4.10.0
|
|
895
|
-
|
|
896
|
-
### Changed
|
|
897
|
-
|
|
898
|
-
- **`response.cookie` is now secure by default** — `httpOnly: true`, `sameSite: "lax"`, and `secure: true` outside development are applied unless overridden. Previously nothing set them: a cookie was readable by any injected script, sent in cleartext, and attached to cross-site requests unless the developer knew to pass three flags on every call. Nothing failed when they were missing, so the app worked and was simply insecure. Precedence is framework defaults → `http.cookies.options` → the per-call argument, so opting out stays possible and explicit. `secure` is relaxed only in development, because browsers drop a `Secure` cookie over plain http
|
|
899
|
-
|
|
900
|
-
**Upgrading:** a cookie your client-side JavaScript reads now needs `{ httpOnly: false }` stated explicitly. Anywhere you wrote `secure: Application.isProduction` by hand can be deleted — the default already tracks the environment, and the hand-written form wrongly disables `secure` in test and staging
|
|
901
|
-
|
|
902
|
-
## 4.9.2
|
|
903
|
-
|
|
904
|
-
### Fixed
|
|
905
|
-
|
|
906
|
-
- the dev server's generated `.warlock/loader-hook.mjs` no longer ships bare `esbuild` / `get-tsconfig` imports. That file is written into the **consuming app's** directory, so a bare specifier resolves from the app — but both packages are core's own dependencies. npm and yarn hoist flat so it worked by accident; under pnpm's strict layout the dev server died with `ERR_MODULE_NOT_FOUND` for a package the app never imported. Each npm specifier is now rewritten at generation time to an absolute path resolved from core's own install, so no consumer has to declare a phantom dependency
|
|
907
|
-
|
|
908
|
-
## 4.9.0 - 2026-08-06
|
|
909
|
-
|
|
910
|
-
### Added
|
|
911
|
-
|
|
912
|
-
- `warlock dev` keyboard shortcuts — `r` restart, `c` clear, `q` quit, `h` help — armed once the server is ready and listed by `h`. TTY-gated, and `Ctrl+C` keeps working while raw mode is held
|
|
913
|
-
- press `u` on the `warlock dev` update notice to update every `@warlock.js/*` dependency, install, and restart the server in place — no Ctrl+C round-trip. Falls back to the printed `npx warlock update` command when the terminal can't deliver keypresses (CI, piped stdin, supervisors)
|
|
914
|
-
- `warlock dev` now runs as a supervised pair — a thin parent that owns the terminal and a disposable worker — so restarting replaces the worker instead of stacking a process per restart, and the supervisor never loads config or connectors
|
|
915
|
-
- `warlock dev` restarts automatically when `warlock.config.ts` or any `.env*` changes, since neither can be hot-reloaded; opt out with `devServer.restartOnConfigChange: false` for the previous warning
|
|
916
|
-
- Bun support in `warlock update` and `warlock add` — `bun.lock` / `bun.lockb` are detected and drive `bun install` / `bun add`
|
|
917
|
-
- `warlock dev` recovers from a crash: a worker that dies after running healthily for 5s is replaced automatically, while one that dies during boot is left alone so its error isn't buried under a reprint. Capped at 3 crashes per minute
|
|
918
|
-
- `warlock update --dry-run` reports what would change without touching anything, and `--check` does the same but exits `1` when a package is behind — a CI gate for staying current
|
|
919
|
-
|
|
920
|
-
### Changed
|
|
921
|
-
|
|
922
|
-
- the dev-server update check remembers npm's answer for 24h in `.warlock/update-check.json`, so a day of restarts costs one lookup instead of one per boot; failed lookups are never cached, and the entry is dropped once an update is applied
|
|
923
|
-
|
|
924
|
-
### Fixed
|
|
925
|
-
|
|
926
|
-
- `warlock start` spawns `process.execPath` instead of a bare `node`, which failed with `ENOENT` wherever `node` is not on `PATH` (systemd units, cron, slim containers) and could otherwise pick a different Node version than the one running the CLI
|
|
927
|
-
- `warlock add` no longer carries its own package-manager detection that silently produced an undefined install command when the project had no recognised lockfile — it shares the updater's detection
|
|
928
|
-
- `build.outDirectory` — the name the docs have used for several releases — is now actually read. Only `build.outdir` ever was, so a config written from the documentation was silently ignored and the bundle still went to `dist/`. Both names now work (`outdir` wins if you set both) and the docs lead with `outdir`
|
|
929
|
-
- `warlock update` no longer reports "All @warlock.js packages are already up to date" when it never reached the npm registry — an offline run now says so and changes nothing
|
|
930
|
-
- a failed package-manager install during `warlock update` no longer loses the rewritten `package.json`; the CLI still exits non-zero
|
|
931
|
-
- the dev server's update check now uses a 5s abort budget instead of 30s, so a hanging network can't leave a pending request behind a running server
|
|
932
|
-
|
|
933
|
-
## 4.8.1 - 2026-07-21
|
|
934
|
-
|
|
935
|
-
### Fixed
|
|
936
|
-
|
|
937
|
-
- `@warlock.js/ai`, `@warlock.js/access`, and `@warlock.js/notifications` declared as optional `peerDependencies` (matching the existing `@warlock.js/herald` pattern) so pkgist's bundler leaves them external instead of vendoring their source into core's own build — a vendored `@warlock.js/ai` copy was a disconnected module instance whose config listeners (e.g. `ai-panoptic`'s dashboard wiring) never received `ai.config(...)` calls routed through the real, separately-installed package
|
|
938
|
-
|
|
939
|
-
## 4.6.1
|
|
940
|
-
|
|
941
|
-
### Fixed
|
|
942
|
-
|
|
943
|
-
- a fatal `uncaughtException` at production boot (e.g. a config file that throws) is no longer swallowed into a silent `exit 0` — bootstrap now wires the crash handler to exit non-zero in production so `warlock start` surfaces the failure; the dev server still logs-and-continues for HMR
|
|
944
|
-
|
|
945
|
-
## 4.6.0
|
|
946
|
-
|
|
947
|
-
### Added
|
|
948
|
-
|
|
949
|
-
- release-hygiene tests: version↔changelog invariant + generator-stub import check
|
|
950
|
-
- `router.routeCount()` exposes the number of registered routes as a boot/readiness signal
|
|
951
|
-
- `health.addRoutesRegisteredCheck(getRouteCount)` registers a readiness check that reports not-ready when a booted HTTP app has zero routes
|
|
952
|
-
- seeders now receive a `{ track }` context — `track(model)`, `track(models[])`, and `track(table, id)` register created records (each call returns its argument so it can be chained inline); `recordsCreated` is auto-derived from the track count
|
|
953
|
-
- `seed_records` table (created via the new `SeedRecordsTableMigration`) records every tracked seed reference within the same transaction the seed runs in; only the last run's refs are kept per seeder
|
|
954
|
-
- `warlock seed --drop [name]` undoes a seed: deletes its tracked records in reverse run/insertion order inside a transaction, then resets the matching seeds-log rows so `once: true` seeds re-run; scope to one seeder with `--drop=<name>`
|
|
955
|
-
- `Seeder.dependsOn` is now resolved — seeders are topologically sorted so dependencies run before dependents, layered over the numeric `order` tie-break; throws `UnknownSeederDependencyError` for a missing dependency and `SeederDependencyCycleError` for a cycle
|
|
956
|
-
- seeders receive an injectable clock and a meaningful batch size — `run({ track, now, batchSize })`; `now()` (default `() => new Date()`) drives both seed data and the seeds-log timestamps so historical/back-fill runs are deterministic, and `batchSize` surfaces the seeder's own `batchSize` for `Model.createMany(rows, { batchSize })`
|
|
957
|
-
- repository-level aggregation — `aggregate()`, `sum()`, `avg()`, `min()`, `max()`, and `groupBy()` on `RepositoryManager`, each reusing `filterBy` (and its operator-injection guard), `where`, and scopes before the aggregate, exactly like `count()`
|
|
958
|
-
- `warlock doctor` — a read-only diagnostics command that runs routes / config / connectors / optional-peers / health / release-hygiene checks and prints a pass/warn/fail report (exits non-zero on any failure, never opens a DB/cache/socket connection)
|
|
959
|
-
- `warlock routes` — a read-only command that lists the registered HTTP routes as a verb-colored table (method / path / name / action / middleware-count / source); filter with `--method` / `--path` / `--name`, or emit the normalized rows as JSON with `--json`. Boots app code to register routes but starts no connectors
|
|
960
|
-
|
|
961
|
-
### Changed
|
|
962
|
-
|
|
963
|
-
- `Seeder.run` now receives a `SeedContext` (`run(ctx)`) — backward compatible, an existing zero-arg `run()` keeps working unchanged
|
|
964
|
-
|
|
965
|
-
### Fixed
|
|
966
|
-
|
|
967
|
-
- route-module load/registration failures are no longer swallowed: a route file that throws on import or registration now surfaces loudly instead of silently 404'ing the whole surface
|
|
968
|
-
- `ModuleLoader.loadModule` rethrows after logging (wrapped in a new `ModuleLoadError` carrying the failing file + cause), so a broken module aborts boot and is caught loudly by the HMR batch-reload handler in dev
|
|
969
|
-
- `ModuleLoader.loadAll` aggregates per-file failures and throws an `AggregateError` at the end, so one broken module no longer hides the others
|
|
970
|
-
- `router.withSourceFile` rethrows the callback error after logging instead of consuming it with a bare `console.log` (the `try/finally` source-file stack cleanup is preserved)
|
|
971
|
-
|
|
972
|
-
## 4.5.0
|
|
973
|
-
|
|
974
|
-
### Changed
|
|
975
|
-
|
|
976
|
-
- dev-server update notice now fires immediately on `warlock dev` — the check is spawned in parallel with server startup instead of awaiting it, so the notice surfaces as soon as npm responds
|
|
977
|
-
- raised the update check's npm registry timeout from 2.5s to 30s, so a slow connection no longer drops the notice
|
|
978
|
-
- repository lifecycle hooks (`onCreating` / `onCreate` / `onUpdating` / `onSaving` / `onDeleting` / …) now run on `create` / `update` / `delete` — they were defined but never invoked
|
|
979
|
-
- `repository.list()` / `all()` now honor the `sortBy`, `sortDirection`, and `purgeCache` options — previously accepted but silently ignored
|
|
980
|
-
|
|
981
|
-
### Removed
|
|
982
|
-
|
|
983
|
-
- presigned-upload `maxSize` option — a presigned PUT URL cannot enforce a size cap, so the option was a false guarantee
|
|
984
|
-
|
|
985
|
-
### Fixed
|
|
986
|
-
|
|
987
|
-
- `response.sendFile({ filename })` and `response.download()` no longer 500 on non-ASCII file names — the `Content-Disposition` header is now RFC 6266-encoded (a sanitized ASCII `filename` fallback plus an RFC 5987 `filename*=UTF-8''…`), so an Arabic / emoji / UTF-8 download name streams correctly instead of throwing Node's `ERR_INVALID_CHAR`
|
|
988
|
-
- local storage paths are contained to their disk root — `../` traversal segments and absolute paths can no longer escape the configured directory
|
|
989
|
-
- `storage.putFromUrl` adds SSRF guards — private / loopback / link-local hosts are rejected and the fetched body is size-capped
|
|
990
|
-
- S3 / R2 / DigitalOcean Spaces `url()` no longer produces a malformed double-host URL when `urlPrefix` is set
|
|
991
|
-
- cloud `deleteDirectory` paginates via the list continuation cursor instead of re-listing the first page
|
|
992
|
-
- local-storage metadata cache is invalidated on write / delete — it was serving a stale size / modified-time
|
|
993
|
-
- the cloud driver no longer reports a misleading "SDK not installed" error when the AWS SDK is in fact present (driver load race)
|
|
994
|
-
- repository `countCached` / `countActiveCached` now cache and return correctly — a `null` cache miss was being returned as the count
|
|
995
|
-
- repository `firstCached` / `lastCached` no longer fetch and cache the entire table to return a single row
|
|
996
|
-
- repository boolean filters no longer coerce `false` / `0` to `true`
|
|
997
|
-
- repository cache keys are now order-independent (stable key serialization)
|
|
998
|
-
- router groups restore prefix / name / middleware state via `try/finally` even when the group callback throws
|
|
999
|
-
- `router.any()` / `all` routes now match every HTTP verb under the dev server — they previously matched only GET and POST, diverging from production
|
|
1000
|
-
- the HTTP concurrency limiter releases its slot on every response path (`noContent`, redirect, file, buffer) — a throwing or non-`send` handler no longer leaks a permit and permanently 429s the route
|
|
1001
|
-
- the cached-response middleware replays a hit through `response.replay()` instead of re-sending an already-sent reply, preserving status and content-type
|
|
1002
|
-
- `onSent` cache writes in the idempotency and cache middleware are error-handled — a cache-backend failure no longer surfaces as an unhandled rejection
|
|
1003
|
-
- `X-Forwarded-For` is parsed to its first hop, so IP-filter / rate-limit / idempotency scoping cannot be spoofed with extra header hops
|
|
1004
|
-
- the `maintenance` middleware allowlist matches request paths that carry a query string
|
|
1005
|
-
- use-cases run their `after` middleware and broadcast for a `void` handler, and a failed history write no longer fails an otherwise-successful call
|
|
1006
|
-
- the use-case retry counter reports the correct count on total failure
|
|
1007
|
-
- the socket connector no longer double-closes the shared HTTP server during graceful shutdown
|
|
1008
|
-
- the cache connector disconnects its drivers on shutdown — an open Redis connection was left dangling
|
|
1009
|
-
- generator stubs import `v` / `Infer` from `@warlock.js/seal` (core never re-exported them), so generated models compile and run
|
|
1010
|
-
- `warlock dev` hot-reloads when a file is emptied or saved with no trailing newline — a stale no-op-change check was silently dropping those saves before they reached HMR
|
|
1011
|
-
|
|
1012
|
-
## 4.4.0 - 2026-06-21
|
|
1013
|
-
|
|
1014
|
-
### Added
|
|
1015
|
-
|
|
1016
|
-
- `Application.onceBooted(cb)` — run a callback once the app is fully booted (fires immediately if already booted)
|
|
1017
|
-
- `Application.whenBooted()` — promise that resolves with the boot context when the app is fully booted
|
|
1018
|
-
- `Application.isBooted` — whether the app has finished booting
|
|
1019
|
-
- `Application.onShutdown(cb)` — run teardown once on shutdown, before connectors stop (mirror of `onceBooted`)
|
|
1020
|
-
- `Application.isShuttingDown` — whether shutdown has begun
|
|
1021
|
-
- built-in `/health` (liveness) and `/ready` (readiness) endpoints with a `health` check registry (`health.addCheck`)
|
|
1022
|
-
- graceful HTTP shutdown — drains in-flight requests on shutdown, bounded by `http.gracefulShutdown.timeout`
|
|
1023
|
-
- `http.health.*` config to toggle or rename the health endpoints
|
|
1024
|
-
|
|
1025
|
-
### Fixed
|
|
1026
|
-
|
|
1027
|
-
- connector shutdown no longer reverses the connector list in place (could corrupt order on a repeated shutdown)
|
|
1028
|
-
|
|
1029
|
-
## 4.3.0 - 2026-06-21
|
|
1030
|
-
|
|
1031
|
-
### Added
|
|
1032
|
-
|
|
1033
|
-
- `warlock update` — update every `@warlock.js/*` package in package.json to its latest version (operator preserved), then run the detected package manager's install
|
|
1034
|
-
- dev-server update notice — `warlock dev` checks npm on start and prints a one-line notice when a newer `@warlock.js/core` is published
|
|
1035
|
-
- `devServer.checkForUpdates` config flag (default `true`) to toggle the dev-server update notice
|
|
1036
|
-
- `fetchLatestVersion()` and `isNewerVersion()` registry/version utilities
|
|
1037
|
-
|
|
1038
|
-
### Fixed
|
|
1039
|
-
|
|
1040
|
-
- `warlock dev --skip-typings` and `--skip-health` long-form flags now work (were silently ignored)
|
|
1041
|
-
|
|
1042
|
-
## 4.2.11
|
|
1043
|
-
|
|
1044
|
-
### Added
|
|
1045
|
-
|
|
1046
|
-
- `lowerStage3Decorators()` — Vite/Vitest plugin that lowers TC39 Stage-3 decorators with esbuild before oxc / the SSR rewrite mangles them; drop it first in `plugins` so model-decorated files load under Vitest 4 / Vite 8.
|
|
1047
|
-
- `warlock add notifications` — installs `@warlock.js/notifications` (+ the `mail` feature), ejects `config/notifications.ts`, and scaffolds the app-owned `Notification` model + migration (idempotent).
|
|
1048
|
-
- Notifications connector — a built-in, config-gated connector that lazy-imports `@warlock.js/notifications`, so core keeps no hard dependency on it.
|
|
1049
|
-
|
|
1050
|
-
### Changed
|
|
1051
|
-
|
|
1052
|
-
- `warlock add test` now scaffolds a `vite.config.ts` that includes `lowerStage3Decorators()`, so a fresh project can test decorated models out of the box.
|
|
1053
|
-
- `warlock add test` `test` / `test:coverage` scripts now run one-shot (`vitest run`) instead of watch mode — CI-safe by default.
|
|
1054
|
-
- Bumped `@mongez/reinforcements` to 3.3.0
|
|
1055
|
-
|
|
1056
|
-
### Fixed
|
|
1057
|
-
|
|
1058
|
-
- `startHttpTestServer` now starts early-phase connectors (database, cache, logger, …) before app modules, then late-phase (http, socket) after — mirroring dev/prod boot order; fixes a `MissingDataSourceError` under the Vitest integration harness.
|
|
1059
|
-
|
|
1060
|
-
## 4.2.5
|
|
1061
|
-
|
|
1062
|
-
### Added
|
|
1063
|
-
|
|
1064
|
-
- `warlock add notifications` now scaffolds the in-app read/dismiss HTTP surface — `routes.ts` + a `notifications.controller.ts` (list / unread-count / mark-read / mark-all-read / clear / delete), gated by `authMiddleware` and recipient-scoped via `inApp`. Pulls `@warlock.js/auth`.
|
|
1065
|
-
|
|
1066
|
-
## 4.2.4
|
|
1067
|
-
|
|
1068
|
-
### Fixed
|
|
1069
|
-
|
|
1070
|
-
- Fix the worker-loader path in the build entry points — a wrong path in 4.2.3 left the worker entry broken (and blocked the 4.2.3 publish for some packages).
|
|
1071
|
-
|
|
1072
|
-
## 4.2.3
|
|
1073
|
-
|
|
1074
|
-
### Fixed
|
|
1075
|
-
|
|
1076
|
-
- Add the worker scripts as build entry points so they ship in the published package.
|
|
1077
|
-
|
|
1078
|
-
## 4.2.2
|
|
1079
|
-
|
|
1080
|
-
### Fixed
|
|
1081
|
-
|
|
1082
|
-
- Add `cli/start` to the build entry points so the `warlock` CLI entry ships in the published package.
|
|
1083
|
-
|
|
1084
|
-
## 4.2.1
|
|
1085
|
-
|
|
1086
|
-
### Fixed
|
|
1087
|
-
|
|
1088
|
-
- Ship the `bin` folder so the `warlock` CLI works from the published package — it was omitted from the 4.2.0 build.
|
|
1089
|
-
|
|
1090
|
-
## 4.2.0
|
|
1091
|
-
|
|
1092
|
-
### Changed
|
|
1093
|
-
|
|
1094
|
-
- `herald-connector` and `http-connector` now log a failed boot-time connection at `log.fatal` (was `log.error`) — an unrecoverable broker connection or HTTP port-bind failure makes "page on fatal only" alerting clean; the HTTP connector flushes logs before `process.exit(1)`. Disconnect / shutdown failures stay at `error`.
|
|
1095
|
-
|
|
1096
|
-
## 4.1.15
|
|
1097
|
-
|
|
1098
|
-
- Baseline — per-package changelog tracking starts at this version.
|
|
67
|
+
|
|
68
|
+
### Fixed
|
|
69
|
+
|
|
70
|
+
- The repository cache is also cleared after the DB transaction commits (cascade `afterCommit`), fixing stale reads when a concurrent read re-cached the old row between the model event and `COMMIT`.
|
|
71
|
+
|
|
72
|
+
## 5.19.1 - 2026-09-23
|
|
73
|
+
|
|
74
|
+
### Changed
|
|
75
|
+
|
|
76
|
+
- Lockstep patch release aligning `@warlock.js/core` with the 5.19.1 family; no Core runtime API change.
|
|
77
|
+
|
|
78
|
+
## 5.19.0 - 2026-09-23
|
|
79
|
+
|
|
80
|
+
### Added
|
|
81
|
+
|
|
82
|
+
- `router.getNamedApiRoutes()` returns fresh frozen browser-safe snapshots of
|
|
83
|
+
registered named API routes (`name`, `path`, `method`). It excludes pages,
|
|
84
|
+
handlers, middleware, schemas, and source paths; `all` remains visible as
|
|
85
|
+
metadata so a browser consumer can require an explicit request method.
|
|
86
|
+
|
|
87
|
+
### Changed
|
|
88
|
+
|
|
89
|
+
- Refined package skill-discovery descriptions and regenerated the llms projections.
|
|
90
|
+
|
|
91
|
+
## 5.18.0 - 2026-09-22
|
|
92
|
+
|
|
93
|
+
### Fixed
|
|
94
|
+
|
|
95
|
+
- Development model loading now shares model ownership and reload notifications with Web's SSR adapter, allowing pages to reuse the same model constructors as Core instead of registering a second copy.
|
|
96
|
+
|
|
97
|
+
## 5.17.1 - 2026-09-22
|
|
98
|
+
|
|
99
|
+
### Fixed
|
|
100
|
+
|
|
101
|
+
- `@warlock.js/web` fixes page projection to resolve import references by lexical binding after server configuration is removed. A component-local `t = useTrans()` no longer retains an unrelated metadata-only Core import in the client view.
|
|
102
|
+
- Development startup now completes initial typings generation before it starts health checking, so the checker receives the generated declaration roots. Health checking remains background work and does not delay connector readiness.
|
|
103
|
+
- The health checker preserves declaration roots already included by the project's tsconfig.
|
|
104
|
+
|
|
105
|
+
## 5.17.0 - 2026-09-21
|
|
106
|
+
|
|
107
|
+
### Security
|
|
108
|
+
|
|
109
|
+
- `uploadedFileController` no longer serves an upload original inline unless its bytes sniff as jpeg, png, webp or avif AND its extension names that same image type (`.jpg`/`.jpeg` for jpeg, `.png` for png, `.webp` for webp, `.avif` for avif) — never sniffed bytes alone, and never the extension alone. An inline response always carries the SNIFFED format's `Content-Type` set explicitly, not an extension-derived guess from `sendFile`. This closes two stored-XSS holes, sniffed-bytes vs. extension mismatched in each direction: an svg (or any other non-raster file) named `.png`/`.jpg` still downloads instead of rendering as its extension's type, and raster bytes named with a non-image or different-image extension — e.g. jpeg magic bytes saved as `x.html` — also download instead of being served under that extension's advertised type (`text/html`, previously inherited from `sendFile`). Every other original — svg, html, xml, text, pdf, unknown, any raster/extension mismatch either way — is sent with `Content-Disposition: attachment`, `Content-Security-Policy: sandbox`, and its extension-derived `Content-Type`, except the svg/html/xml family, which is downgraded to `application/octet-stream`. Every uploads response — originals and variants — now also carries `X-Content-Type-Options: nosniff`.
|
|
110
|
+
- A default CSRF-Origin guard (card 8a752ab2) now runs at the earliest HTTP seam, before route middleware and any app handler, for every unsafe-method (`POST`/`PUT`/`PATCH`/`DELETE`) request that carries a `Cookie` header naming anything other than the framework locale cookie or a validated locale-preference cookie (a malformed `Cookie` header counts as carrying one — fails closed). It requires `Origin` — or, absent that, `Referer` — to name the request's own origin or an entry in `auth.csrf.allowedOrigins`, rejecting with `403` otherwise. Before this, that check only ran inside `authMiddleware("cookie:*")`, so a cookie-authenticated write reached through any other path (e.g. an app-owned optional-auth pattern reading its own `token` cookie) was never checked. A header-only API request (no `Cookie` header, e.g. `Authorization: Bearer`) is unaffected, and a route can opt out with `{ csrf: false }` (`RouteOptions.csrf`) — documented as dangerous, for third-party callbacks and machine-to-machine routes only.
|
|
111
|
+
|
|
112
|
+
### Added
|
|
113
|
+
|
|
114
|
+
- Standalone `warlock generate.typings` includes route `locales.json` keys, honoring `$group` and existing literal translation registrations. The optional Web build helper is resolved from the application's installed package using ESM export conditions, without executing application configuration or the Web root entry.
|
|
115
|
+
- `uploadedFileController`, a ready-made handler for `router.get("/uploads/*", uploadedFileController)` that serves local uploads safely. The request path must resolve inside the storage root, symlinks included, and never into the variant cache. Anything else gets the same 404 as a missing file, so the route cannot be used to probe for files. Originals are sent with a one-year cache.
|
|
116
|
+
- `generateImageVariants(relativePath, options?)`, an opt-in ingest-time counterpart to `uploadedFileController`: call it right after an upload is saved to render every configured `uploads.images` variant (optionally `options.variants` to limit which ones) up front. It shares the route's normalized config, cache key and derivative path, so `GET /uploads/<path>?variant=<name>` is served from cache on its first request, and its source guards and error statuses match the route. It returns a plain, serializable descriptor (`{ src, width, height, variants, formats? }`) matching web's `ImageDescriptor`.
|
|
117
|
+
- Bounded on-demand image variants, configured under `uploads.images`: `?variant=<name>` renders a variant named in `uploads.images.variants` (width and height from 1 to 8192, quality from 1 to 100, `fit` of `cover`, `contain` or `inside`), and `&format=` picks an output format from the `formats` allowlist (`webp`, `avif`). Any other query key, a repeated key, an unknown variant or a format that is not allowed returns 400. Only jpeg, png, webp and avif sources are resized, detected by their magic bytes, never their extension. gif and svg return 415. `maxSourceBytes` (default 25 MB) and `maxSourcePixels` (default 40,000,000) cap the source, and a larger one returns 413. Each derivative is cached on disk under a sha256 of the source path, size, mtime, variant and format, and it is written atomically. Concurrent requests for the same derivative generate it once. It is served with `Cache-Control: public, max-age=31536000, immutable` and an ETag that answers 304. Rewriting the source changes the key, so the next request renders a new derivative. An invalid `uploads.images` config throws `ImageVariantsConfigError` the first time a variant is requested. The variant path needs `sharp`, but originals are served without it.
|
|
118
|
+
|
|
119
|
+
### Fixed
|
|
120
|
+
|
|
121
|
+
- Request translation helpers follow the request's current locale; Web can supply its immutable route-scoped resolver for `request.t()`, `request.trans()`, and explicit `request.transFrom(locale, key, placeholders?)`. Without that resolver, all retain their global-registry behavior.
|
|
122
|
+
- Locale preference resolution validates the host-only preference before falling through to the legacy cookie, header, and default. `response.setLocale()` clears the preference before writing its authoritative locale cookie. Provisional Web navigation requests do not persist a locale before the browser commits a successful current navigation.
|
|
123
|
+
- Image variants (`uploadedFileController` and `generateImageVariants`) no longer enlarge a source smaller than the variant's target width or height. A variant is now rendered with sharp's `withoutEnlargement: true` by default, so a 100px source requested at `{ width: 320 }` stays 100px wide instead of being upscaled. Opt in per variant with `enlarge: true` in `ImageVariantDefinition` (a non-boolean value throws `ImageVariantsConfigError`); the effective flag is part of the normalized variant and its cache key, so flipping it regenerates the derivative. `generateImageVariants`' descriptor now reports the actual rendered width and height, read from the derivative it wrote, not the requested size.
|
|
124
|
+
- `generateImageVariants` now throws `ImageVariantsConfigError` — not a plain `Error` (no storage root) or `HttpError(400)` (`uploads.images` not configured) — for both server misconfiguration states, matching how a bad `uploads.images` value is already reported; input-derived failures (traversal, missing file, bad format, size limits, unknown variant name) still throw `HttpError`. `uploadedFileController`'s own responses for these requests are unchanged.
|
|
125
|
+
- `warlock seed` (and `warlock migrate`, for data migrations) now preload the `storage` connector, and `Storage`/`ScopedStorage` throw a named `StorageNotInitializedError` — pointing at `src/config/storage.ts` and a custom command's `preload.connectors` — instead of a bare `TypeError: Cannot read properties of null (reading 'name')` when `activeDriver` is read before storage has been initialized in the process.
|
|
126
|
+
|
|
127
|
+
## 5.16.0 - 2026-09-18
|
|
128
|
+
|
|
129
|
+
### Added
|
|
130
|
+
|
|
131
|
+
- Core now parses `application/x-www-form-urlencoded` request bodies, so plain HTML forms and OAuth `form_post` callbacks (Apple) reach their routes instead of failing with `FST_ERR_CTP_INVALID_MEDIA_TYPE`. These bodies have the same `http.bodyLimit` as JSON. A key sent more than once becomes an array.
|
|
132
|
+
- `response.xml()` accepts a raw string or any value with a `toXML(): string` method (such as a `@warlock.js/sitemap` `Sitemap`), and sends `application/xml`.
|
|
133
|
+
|
|
134
|
+
### Changed
|
|
135
|
+
|
|
136
|
+
- `warlock add sitemap` now merges a disabled `sitemap` section into `src/config/web.ts`, and creates that file when it is missing. It no longer writes `src/config/sitemap.ts` or registers `sitemapConnector()`. The merge edits only a `sitemap` key directly on the exported config object, and the result is re-parsed before it is written.
|
|
137
|
+
|
|
138
|
+
## 5.15.0 - 2026-09-18
|
|
139
|
+
|
|
140
|
+
### Added
|
|
141
|
+
|
|
142
|
+
- `warlock add sitemap` installs `@warlock.js/sitemap`, writes `src/config/sitemap.ts`, and registers `sitemapConnector()` in `warlock.config.ts`. The generated config ships **disabled**: a sitemap needs the application public origin and a generated app cannot know it, so the block explains the two steps to turn it on rather than producing an app that refuses to boot.
|
|
143
|
+
- `app.publicUrl` config key, with a `PUBLIC_APP_URL` environment fallback: the one absolute-URL source for every consumer that needs an origin. It never falls back to a request-derived host — an absolute URL built from the wrong host is worse than a boot that refuses to start, because nothing downstream reports it.
|
|
144
|
+
|
|
145
|
+
### Changed
|
|
146
|
+
|
|
147
|
+
- **BREAKING (fail-loud):** `request.cookie(name)` and `request.hasCookie(name)` now throw `CookieJarUnavailableError` when `@fastify/cookie` is not registered on the Fastify instance, instead of returning `undefined` / `false`. An unregistered plugin is a configuration fault, and it was previously indistinguishable from "the caller sent no such cookie" — under `authMiddleware([], "cookie:token")` it surfaced as a permanent, unexplained 401. Apps built on `createHttpApplication` are unaffected: core registers the plugin before anything mounts. The exposure is a host that mounts a guarded surface on its own Fastify instance. `request.cookies` stays lenient and still returns `{}`, so the framework's own opportunistic reads — locale resolution among them — are unchanged.
|
|
148
|
+
|
|
149
|
+
## 5.14.0 - 2026-09-17
|
|
150
|
+
|
|
151
|
+
### Added
|
|
152
|
+
|
|
153
|
+
- `warlock add bull-board` installs the Bull Board packages and writes a `dashboard` block to `src/config/queue.ts`; it adds the queue feature first when it is missing. The generated block enables the dashboard outside production only, since an unguarded dashboard refuses to mount in production.
|
|
154
|
+
- `middleware.cache({ tags })`: cached API responses can be tagged and are evicted by `cache.tags([...]).invalidate()`, like cached pages.
|
|
155
|
+
|
|
156
|
+
### Fixed
|
|
157
|
+
|
|
158
|
+
- `warlock dev` checks esbuild's native binary before starting and fails with `EsbuildBinaryMissingError` naming the fix (`warlock build` already did).
|
|
159
|
+
- `devServer.timings`: the `watcher` phase always reported 0ms on Windows and Linux; it now measures the real settle time.
|
|
160
|
+
- `warlock add` writes connector arrays formatted like Prettier (`[queueConnector(), webConnector()]`), including empty and multi-line arrays.
|
|
161
|
+
|
|
162
|
+
## 5.13.0 - 2026-09-17
|
|
163
|
+
|
|
164
|
+
### Added
|
|
165
|
+
|
|
166
|
+
- `warlock add queue` installs `@warlock.js/queue`, generates Redis-backed `src/config/queue.ts`, and registers `queueConnector()` in `warlock.config.ts`.
|
|
167
|
+
- `warlock add auth-google` and `warlock add auth-passkeys` install `@warlock.js/auth`'s Google sign-in (`jose`) and passkey (`@simplewebauthn/server`) login methods.
|
|
168
|
+
- `warlock dev` generates `.warlock/typings/translations.d.ts` from literal `groupedTranslations` dictionaries, augmenting web's typed translation-key registry.
|
|
169
|
+
|
|
170
|
+
### Changed
|
|
171
|
+
|
|
172
|
+
- `warlock dev` now uses a 12ms quiet window for isolated file saves while extending multi-file bursts up to a 60ms maximum, reducing routine HMR latency without splitting formatter or checkout reloads.
|
|
173
|
+
|
|
174
|
+
### Fixed
|
|
175
|
+
|
|
176
|
+
- Safely return no alias resolution when a configured alias has no first target.
|
|
177
|
+
- `@warlock.js/core` no longer imports `@warlock.js/auth`; `useHashedPassword` now calls core's own `hashPassword` directly instead of an auth-service delegate that only called back into core, and `@warlock.js/auth` moves to a dev-only dependency. A guard spec now fails the build if any `core/src` module imports `@warlock.js/auth`.
|
|
178
|
+
|
|
179
|
+
## 5.12.0 - 2026-09-16
|
|
180
|
+
|
|
181
|
+
### Added
|
|
182
|
+
|
|
183
|
+
- Opt-in request tracing (`http.tracing`): vendor-neutral `onRequestStart` / `onRequestEnd` / `onPhase` hooks fired around `route.match`, `middleware`, `validation`, `handler`, and `response.write`, with the trace id derived from an inbound W3C `traceparent` header (falling back to `request.id`). Off by default and zero-overhead when disabled; a throwing hook is caught and reported once, never breaking the request. No new response header — apps still correlate through the existing `X-Request-Id` echo. See the `request-tracing` skill.
|
|
184
|
+
- Opt-in `Content-Security-Policy` header (`http.csp`), using the per-request nonce the framework already generates; report-only mode supported.
|
|
185
|
+
- `Response.streamReact()` and the underlying `streamReactResponse()` helper (`@warlock.js/core`'s Stage 1 streaming SSR seam): pipe a React server stream (`renderToPipeableStream`) onto the raw response after writing the already-committed status and headers, aborting the render if the client disconnects. `@warlock.js/web` uses this exclusively to stream a page document — it never touches the raw response itself.
|
|
186
|
+
|
|
187
|
+
### Fixed
|
|
188
|
+
|
|
189
|
+
- Renaming or moving a file under `warlock dev` no longer prints a false `ENOENT` failure before the route is rewired.
|
|
190
|
+
- `warlock routes --json` now prints only JSON on stdout. The `› Running <command>...` header moved to stderr, like the completion banner.
|
|
191
|
+
- Production build contribution hooks (`generate` / `emit`) saw `outFile`, `entryPath`, `singleBundle`, `esmShim` and `banner` as `undefined`, because bundling deleted them from the shared build options. The bundler now works on its own copy.
|
|
192
|
+
|
|
193
|
+
### Changed
|
|
194
|
+
|
|
195
|
+
- **`container.get(key)` now throws a named error when the key is not registered**, instead of returning `undefined` while typed as present. Use `container.tryGet(key)` where the value is genuinely optional.
|
|
196
|
+
- **BREAKING:** `request.user` and `clearCurrentUser()` removed from the HTTP request; `RequestUser` moved to `@warlock.js/auth`. The authenticated user now lives at `request.locals.user`, a key `@warlock.js/auth` declares via module augmentation on `RequestLocals` and writes from its middleware. Reading `request.user` in development throws a new `RequestUserMovedError` naming `request.locals.user` (kept for one release as a migration diagnostic; removal is documented, not silent). `decodedAccessToken` and its cache-mark behavior (`request.locals.authDerived`) are unchanged. `useCurrentUser()` / `requestContext.getUser()` now read `request.locals.user` and return `unknown`/the caller's generic instead of the removed `RequestUser` type — see `@warlock.js/auth`'s `currentUser()` for a typed wrapper.
|
|
197
|
+
|
|
198
|
+
## 5.11.0 - 2026-09-14
|
|
199
|
+
|
|
200
|
+
### Added
|
|
201
|
+
|
|
202
|
+
- Tests and documentation for optional file fields: `v.file().optional()` skips an absent upload and reports a present non-file value as a normal validation error.
|
|
203
|
+
|
|
204
|
+
### Fixed
|
|
205
|
+
|
|
206
|
+
- `warlock dev` printed `hmr update` for a backend file before the new code was live, so a request made right after the line could still get the old response. The line now prints once the reload has finished and shows how long it took.
|
|
207
|
+
|
|
208
|
+
### Changed
|
|
209
|
+
|
|
210
|
+
- Backend file changes are picked up sooner in `warlock dev` (event debounce 150ms → 50ms).
|
|
211
|
+
|
|
212
|
+
## 5.10.0 - 2026-09-14
|
|
213
|
+
|
|
214
|
+
_Released in lockstep with the `@warlock.js/*` family; no package-specific changes in 5.10.0._
|
|
215
|
+
|
|
216
|
+
## 5.9.0 - 2026-09-13
|
|
217
|
+
|
|
218
|
+
_Released in lockstep with the `@warlock.js/*` family; no package-specific changes in 5.9.0._
|
|
219
|
+
|
|
220
|
+
## 5.8.0 - 2026-09-13
|
|
221
|
+
|
|
222
|
+
### Added
|
|
223
|
+
|
|
224
|
+
- `doctor` gains a `jwt-secret` check: it fails when `auth.userType` is configured but no JWT signing secret is set — the pre-flight form of the silent first-login 500 the lazy secret resolution would otherwise throw.
|
|
225
|
+
|
|
226
|
+
### Fixed
|
|
227
|
+
|
|
228
|
+
- CLI command modules now resolve the `app/*` path alias and `.ts` siblings the way controllers and pages do. The ESM loader hook is registered before a command module is imported, so a `warlock <command>` file that imports `app/*` no longer dies with `ERR_MODULE_NOT_FOUND`.
|
|
229
|
+
- `warlock generate.*` error hints point at `generate`, not the removed `create.*` command.
|
|
230
|
+
- A generated CRUD repository imports every `@warlock.js/core` type it references, and a generated seed stub ships disabled so an unfilled stub cannot abort the whole `warlock seed` run.
|
|
231
|
+
- The HTTP-port preflight runs only for a boot that actually starts the http connector, so a scoped data command (`warlock seed`, `migrate`) no longer probes — and collides with — a port it never binds.
|
|
232
|
+
- A port-in-use error now names the `HTTP_PORT` environment variable and tells you to unset it when an ambient value is the cause, instead of advising a `src/config/http.ts` edit that the environment variable would just override.
|
|
233
|
+
|
|
234
|
+
### Changed
|
|
235
|
+
|
|
236
|
+
- The emitted generator and starter templates no longer carry a redundant `.required()` — `@warlock.js/seal` fields are required by default, so the call was a no-op that taught the opposite of the truth. `.required()` still exists for setting a custom message.
|
|
237
|
+
|
|
238
|
+
## 5.7.0 - 2026-09-11
|
|
239
|
+
|
|
240
|
+
### Security
|
|
241
|
+
|
|
242
|
+
- **The SSRF guard now fails closed on an address it cannot classify.** An unclassifiable IP was previously treated as public, so an outbound request could reach an internal address the guard exists to block.
|
|
243
|
+
|
|
244
|
+
### Fixed
|
|
245
|
+
|
|
246
|
+
- **The dev server no longer says it is watching for changes before it is serving.** On a slow boot that line could arrive minutes before the port was bound.
|
|
247
|
+
- The dev-server error formatter can no longer throw while formatting an error, which replaced the developer's real error with its own crash.
|
|
248
|
+
- `warlock add` with an unknown feature now names it, instead of dying with `Cannot read properties of undefined`.
|
|
249
|
+
- Bracket-notation request bodies (`items[0][name]`) no longer silently drop values.
|
|
250
|
+
- `warlock routes` and the circular-import report degrade to unaligned output instead of throwing on an unexpected shape.
|
|
251
|
+
|
|
252
|
+
### Changed
|
|
253
|
+
|
|
254
|
+
- Internal type-safety hardening across the CLI, dev server and request handling; no other behaviour change.
|
|
255
|
+
|
|
256
|
+
## 5.6.0 - 2026-09-08
|
|
257
|
+
|
|
258
|
+
### Fixed
|
|
259
|
+
|
|
260
|
+
- **A production build asked the APP to resolve packages only the framework declares, so a built app could not boot under a strict pnpm tree.** Every bare specifier was left external, including the `@fastify/*`, `find-my-way`, `fast-jwt` and `@mongez/*` imports that reach the bundle through the framework's own code — none of which an app has any reason to declare. Under npm/yarn hoisting they resolved by accident; under pnpm the app died with `ERR_MODULE_NOT_FOUND` at startup, after a build that reported success. Externality is now decided per import edge: a bare specifier stays external unless the importer is not the app's own code AND the specifier names a package that importer's own `dependencies` declare.
|
|
261
|
+
- **An optional peer still stays external, and now by construction rather than by a list.** `nodemailer`, `socket.io`, `mongodb`, `vite`, `redis`, `pg`, the AI SDKs and `@aws-sdk/*` are declared as peer dependencies, never dependencies, so the rule above leaves every one of them alone without anyone having to maintain a list of their names.
|
|
262
|
+
- **Wildcard `paths` in `tsconfig.json` were resolved by nothing.** `app/*` and `web/*` — the aliases an app uses to refer to its own source — were skipped because esbuild's `alias` option cannot express a wildcard, and nothing took over. `import { User } from "app/users/models/user.model"` survived into the artifact as a bare specifier and `warlock start` failed with `Cannot find package 'app'`. They now resolve, and a wildcard that matches a declared alias but resolves to no file is a build error rather than a silent external.
|
|
263
|
+
- `response.setLocale()` wrote the locale cookie under a name spelled independently of the one `request` read it back under. One constant now owns that name at both ends, with a test that reads back whatever the writer emitted rather than naming the cookie itself.
|
|
264
|
+
- A container lookup that missed reported only that the key was not bound. When more than one copy of `@warlock.js/core` is loaded — which a source checkout or a mixed install can produce — that message described a real condition as if it were a wiring mistake. The failure now names the duplicate-instance condition and how many copies it found, on the failure path only.
|
|
265
|
+
|
|
266
|
+
### Changed
|
|
267
|
+
|
|
268
|
+
- `response.clearCookies()` documents what it cannot do, in its first sentence: a cookie set on one path is not cleared by a call made from another. The behaviour is unchanged — the promise it appeared to make was never one it could keep.
|
|
269
|
+
- The router folds its own route prefixes through the same normaliser the rest of the framework uses, so a prefix cannot be normalised two different ways.
|
|
270
|
+
|
|
271
|
+
## 5.5.0 - 2026-09-07
|
|
272
|
+
|
|
273
|
+
### Fixed
|
|
274
|
+
|
|
275
|
+
- `warlock add web` silently overwrote an existing `src/app/contact` module. It guarded `src/web/root.tsx` against clobbering a human's work but wrote the contact route and controller unconditionally, destroying them without a word. Each file is now guarded on its own existence, and a skip reports the consequence — that the contact form's `POST /api/contact` endpoint is missing and the form will 404 until you wire it.
|
|
276
|
+
- Documentation shipped in `skills/` told users to run `pnpm`-specific commands — including `pnpm warlock routes --json`, which cannot work under npm at all, since `pnpm <binary>` has no npm equivalent. Commands are now package-manager neutral.
|
|
277
|
+
|
|
278
|
+
## 5.4.0 - 2026-09-07
|
|
279
|
+
|
|
280
|
+
### Fixed
|
|
281
|
+
|
|
282
|
+
- `warlock routes --json` could not be parsed. The command's success banner shared stdout with the JSON payload, so the documented machine seam — "emit the routes as JSON for piping into scripts/CI" — produced output no consumer could read, and always had.
|
|
283
|
+
- `warlock add react-email` failed on every freshly scaffolded app. It read the project `tsconfig.json` with `JSON.parse`, and the scaffold's own tsconfig carries `//` comments, so the command aborted pointing at the developer's file.
|
|
284
|
+
- `warlock add web` produced an app whose homepage returned HTTP 500. `GET /` was registered twice — by the scaffold's own home route and by the generated page — and Fastify refused the duplicate. Affected both `warlock dev` and `warlock start`.
|
|
285
|
+
- `warlock add web` generated code that failed the scaffold's own lint gate: twelve `prettier/prettier` errors in files the developer had not written.
|
|
286
|
+
- `warlock add notifications` generated a controller that did not compile — seven `TS2345` errors from passing `request.user` where a `Notifiable | Id` was required.
|
|
287
|
+
- Every `warlock` command opened a stdin handle at import time, through a module-level singleton whose constructor defaulted to `process.stdin`. Only `warlock dev` has any use for stdin.
|
|
288
|
+
|
|
289
|
+
### Changed
|
|
290
|
+
|
|
291
|
+
- **Command success and failure banners now write to stderr, not stdout.** stdout carries a command's output; status chrome carries no data. A script that captured only stdout to grep for `✔ … completed successfully` must now read stderr. Nothing could have depended on the previous behaviour for `--json`, whose payload was unparseable precisely because of it.
|
|
292
|
+
- Feature generators that patch `tsconfig.json` now edit its text instead of parsing and rewriting it, so the file's comments survive.
|
|
293
|
+
- `warlock add web` locates an existing `GET /` by scanning `src/app/**/routes.ts` rather than assuming one hardcoded path.
|
|
294
|
+
|
|
295
|
+
## 5.3.2 - 2026-09-05
|
|
296
|
+
|
|
297
|
+
### Fixed
|
|
298
|
+
|
|
299
|
+
- An `HTTP_PORT` that does not round-trip through `Number()` — `03999`, `" 3999"`, `+3999`, `1e3` — reached the HTTP boot path as a string. `1e3` bound port 1000 with no diagnostic anywhere, the port published to the ready signal was the raw configured value, and `PortInUseError`'s suggestion string-concatenated into `port: 039991`. A configured port is now resolved to a canonical integer before anything binds, logs or reports it, and a value that cannot become one fails naming it.
|
|
300
|
+
- An ambient environment variable that overrode the app's own `.env` did so silently. The precedence is unchanged and deliberate — a checked-in `.env` is a default, an exported variable is the situational override — but the app now prints one line naming the variable, the value in effect, and that it came from the process environment. Keys that look like secrets are named with their values redacted.
|
|
301
|
+
- The port reported to a supervisor, to `WARLOCK_TEST_SERVER_PORT` and to the ready report was the **configured** port rather than the one actually bound. It is now read back from the address `listen()` resolves with, so the two can no longer diverge — including under `http.port: 0`, where the configured value carries no information at all.
|
|
302
|
+
- `warlock dev` restart-looped forever on a startup precondition that could never clear itself, reprinting its own diagnostic every few seconds and then scrolling it away with the restart banner. A failed precondition now stops, prints once, and exits.
|
|
303
|
+
- The port preflight ran after the database connected, so a busy port took 7–13 seconds to report on the `warlock dev` path. It now runs before the early-phase connectors, as it already did for a production build.
|
|
304
|
+
- 57 interfaces were imported as values across the package, each one crashing `warlock dev`'s from-source boot the moment its file reached the per-file transpiler — which has no type information and so cannot elide the import.
|
|
305
|
+
- `localized()` lost its `StandardSchemaV1` typing in the published 5.3.0 and 5.3.1 tarballs. The typing is restored.
|
|
306
|
+
|
|
307
|
+
## 5.3.1 - 2026-09-04
|
|
308
|
+
|
|
309
|
+
### Fixed
|
|
310
|
+
|
|
311
|
+
- Republished the complete family so a clean install resolves. The 5.3.0 publish left the family's reciprocal exact peer requirements unsatisfiable from a fresh registry install; 5.3.1 is the same code, published as one complete set.
|
|
312
|
+
|
|
313
|
+
## 5.3.0 - 2026-09-03
|
|
314
|
+
|
|
315
|
+
### Fixed
|
|
316
|
+
|
|
317
|
+
- Every error response was cacheable. The `no-store` floor is now set at the single error funnel, so an error can no longer be served from a cache to a second request.
|
|
318
|
+
- `warlock gen` emitted controllers with v4 handler signatures — every generated controller failed to compile against the v5 contract. The generator now emits the v5 `ctx` object signature.
|
|
319
|
+
- A validated file field was inferred as `unknown`; the sweep that found it found a second occurrence, and both are fixed.
|
|
320
|
+
|
|
321
|
+
### Changed
|
|
322
|
+
|
|
323
|
+
- A request is marked auth-derived the moment `user` or `decodedAccessToken` is assigned, so cacheability is decided by what the request actually read rather than by a separate declaration.
|
|
324
|
+
|
|
325
|
+
### Removed
|
|
326
|
+
|
|
327
|
+
- Five unreachable error branches that described a response the framework never sent.
|
|
328
|
+
|
|
329
|
+
## 5.2.4 - 2026-09-02
|
|
330
|
+
|
|
331
|
+
### Fixed
|
|
332
|
+
|
|
333
|
+
- `warlock add web` now writes universal page setup to `src/web/index.register.ts` and re-exports `register()` from the page, preserving component state across React Fast Refresh edits without changing SSR or hydration registration.
|
|
334
|
+
|
|
335
|
+
## 5.2.3 - 2026-09-02
|
|
336
|
+
|
|
337
|
+
### Fixed
|
|
338
|
+
|
|
339
|
+
- `warlock add` now preserves exact lockstep versions for added Warlock family packages while retaining declared ranges for third-party dependencies.
|
|
340
|
+
- `warlock add web` now emits a projection-safe `register()` hook and one `index` route identity for SSR and hydration, with deterministic form and favicon markup for a clean browser console.
|
|
341
|
+
|
|
342
|
+
## 5.2.2
|
|
343
|
+
|
|
344
|
+
### Fixed
|
|
345
|
+
|
|
346
|
+
- Restored exact 5.2.2 pins across the complete Warlock family. The partial
|
|
347
|
+
5.2.0 and 5.2.1 releases could not satisfy the family's reciprocal exact
|
|
348
|
+
peer requirements in a clean install.
|
|
349
|
+
|
|
350
|
+
## 5.2.1
|
|
351
|
+
|
|
352
|
+
### Fixed
|
|
353
|
+
|
|
354
|
+
- Rebuilt the published package with the complete Warlock family registry and
|
|
355
|
+
installable npm ranges for every internal dependency. The 5.2.0 registry
|
|
356
|
+
manifest leaked six `workspace:` dependency specifications and could not be
|
|
357
|
+
installed outside the monorepo.
|
|
358
|
+
|
|
359
|
+
## 5.2.0
|
|
360
|
+
|
|
361
|
+
### Added
|
|
362
|
+
|
|
363
|
+
- **Connectors can contribute to `warlock build` through an optional static
|
|
364
|
+
`build` object.** Configured connectors may define ordered, awaited
|
|
365
|
+
`generate(context)` and `emit(context)` hooks without being booted or started.
|
|
366
|
+
`generate` can add generated entry imports and a narrow esbuild patch; `emit`
|
|
367
|
+
can produce non-esbuild artifacts after the server bundle. Unknown contribution
|
|
368
|
+
keys, duplicate/reserved connector names, generated unresolved imports, and hook
|
|
369
|
+
failures stop the build instead of producing a partial artifact.
|
|
370
|
+
- **`warlock routes:diff`** — compares the live dev-server page-route surface against
|
|
371
|
+
the last successful `warlock build`'s snapshot (`page-routes.manifest.json` in the
|
|
372
|
+
build `outdir`). Boots diagnostically (same fail-loud boot as `warlock routes` /
|
|
373
|
+
`warlock doctor` — no connectors started), then reports `changed` / `removed` /
|
|
374
|
+
`added` page routes and exits non-zero on drift; exits `0` with "Page routes match"
|
|
375
|
+
when the two agree. Refuses to run (with an instruction to run `warlock build`
|
|
376
|
+
first) when no snapshot exists yet, or when an existing one is malformed. A route
|
|
377
|
+
whose `path`/`name` moved but whose `source` file didn't is reported as one
|
|
378
|
+
`changed` line instead of a `removed` + `added` pair.
|
|
379
|
+
|
|
380
|
+
**A clean baseline is reported immediately after a successful build.** The manifest
|
|
381
|
+
is written through the router's own path normalizer, so the catch-all page route is
|
|
382
|
+
recorded the way the router registers it (`/*`) rather than the way discovery derives
|
|
383
|
+
it (`*`) — an untouched checkout right after a green `warlock build` reports
|
|
384
|
+
`Page routes match`, where an earlier draft of this command reported drift on the
|
|
385
|
+
first run every time.
|
|
386
|
+
|
|
387
|
+
⚠ **What a clean diff does not prove.** The manifest records what each page's route
|
|
388
|
+
name was _derived_ as, not what it was _registered_ as: `warlock build` boots no
|
|
389
|
+
connectors, so it cannot see the API routes a page name may collide with, and the
|
|
390
|
+
router appends a `.<method>` suffix to a name another method already claimed —
|
|
391
|
+
which the comparison accepts rather than reporting. `Page routes match` therefore
|
|
392
|
+
means the page surface has not moved since the build; it does **not** mean no page
|
|
393
|
+
route name collided at registration. The clean-run output says so in a trailing note,
|
|
394
|
+
and `warlock routes` shows the names as actually registered
|
|
395
|
+
|
|
396
|
+
### Changed
|
|
397
|
+
|
|
398
|
+
- **Incoming routes now match with or without one trailing slash in both
|
|
399
|
+
development and production.** `/about` and `/about/` dispatch to the same
|
|
400
|
+
route; `/` remains `/`, query strings are preserved, and case matching is
|
|
401
|
+
unchanged. Both router paths call the same request-path normalizer before
|
|
402
|
+
matching instead of relying on different Fastify/find-my-way defaults.
|
|
403
|
+
|
|
404
|
+
- ⚠ **BREAKING — `http.trustProxy` is validated at boot, and a number is refused.** The
|
|
405
|
+
accepted shapes are exactly `boolean`, a non-empty string (a single IP/CIDR or several
|
|
406
|
+
comma-separated), a non-empty `string[]`, and an `(address, hop) => boolean` predicate;
|
|
407
|
+
a missing or nullish value means `false`. Anything else — a number, `""`, `[]`, an array
|
|
408
|
+
with a non-string entry, a plain object — now throws a `TypeError` while the HTTP server
|
|
409
|
+
is being constructed, before Fastify is instantiated, instead of being handed through
|
|
410
|
+
and silently coerced
|
|
411
|
+
|
|
412
|
+
```
|
|
413
|
+
Invalid http.trustProxy configuration: expected a boolean, a non-empty IP/CIDR string,
|
|
414
|
+
a non-empty string array, or a predicate function; received number.
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
⚠ **The hop-count shape 4.16.0 introduced and recommended (`http.trustProxy: 2`) no
|
|
418
|
+
longer boots.** The Fastify version this release builds against evaluates a numeric
|
|
419
|
+
`trustProxy` as always-false at runtime, so a hop count never walked past your proxies —
|
|
420
|
+
it silently degraded to "trust nothing" while reading like a configured proxy chain, and
|
|
421
|
+
`detectIp()` returned the socket peer for every request. Refusing it loudly is the only
|
|
422
|
+
honest option: **migrate a hop count to a trusted-proxy list (`"10.0.0.0/8"`) or a
|
|
423
|
+
predicate before upgrading.** `true`, a list, and a predicate are unaffected
|
|
424
|
+
|
|
425
|
+
- **`warlock build` writes into a temporary directory and promotes it only on success.**
|
|
426
|
+
The build no longer writes into `outdir` as it goes. It builds into a hidden sibling
|
|
427
|
+
directory (`.<outdir-basename>.build-<hex>`, same volume so the promotion is a rename),
|
|
428
|
+
writes a `.warlock-build.json` success marker (`{ status, builtAt }`) as its last step,
|
|
429
|
+
then swaps the directory into place. **A successful build therefore leaves no stale
|
|
430
|
+
files** — `outdir` is replaced wholesale rather than merged over, so an artifact a
|
|
431
|
+
previous build emitted and this one did not is gone. **A failed build leaves no usable
|
|
432
|
+
`dist`** — the temp directory is removed and the error rethrown without `outdir` ever
|
|
433
|
+
being touched, so a previous good build survives intact and no half-written one takes
|
|
434
|
+
its place
|
|
435
|
+
|
|
436
|
+
- **`warlock start` refuses a `dist` that was not produced by a successful build**, and
|
|
437
|
+
names that as the reason. It checks for the `.warlock-build.json` marker in `outdir`
|
|
438
|
+
before spawning anything; missing, unreadable, malformed, or `status !== "success"` all
|
|
439
|
+
exit `1` on stderr with
|
|
440
|
+
|
|
441
|
+
```
|
|
442
|
+
✖ "dist" was not produced by a successful `warlock build` run (no build-success marker found). Run `warlock build` before `warlock start`.
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
Never built, a build that failed before promotion, and a hand-assembled directory are
|
|
446
|
+
deliberately not distinguished — the fix for all three is the same command. Previously
|
|
447
|
+
`start` spawned whatever happened to be sitting in `outdir` and failed somewhere inside
|
|
448
|
+
the boot instead. ⚠ Ship `outdir` whole: an artifact pipeline that prunes dotfiles
|
|
449
|
+
strips the marker and makes the build unstartable on the target host
|
|
450
|
+
|
|
451
|
+
- **`warlock start` surfaces the child process's real output on a failed boot.** The
|
|
452
|
+
production supervisor forwards every stdout/stderr chunk from the spawned bundle
|
|
453
|
+
verbatim and live, and its failure summary now reports whether a cause actually arrived:
|
|
454
|
+
`the cause is printed above, in the application's own output` when output was seen, and
|
|
455
|
+
`no output was captured from the application process — its cause did not reach this
|
|
456
|
+
terminal` when none was. It previously pointed at "above" unconditionally, which on a
|
|
457
|
+
silent child meant pointing at an empty terminal
|
|
458
|
+
|
|
459
|
+
- **The HTTP connector now preflights its port before binding.** `warlock dev` and
|
|
460
|
+
`warlock start` both go through `HttpConnector.start()`, which now calls
|
|
461
|
+
`assertPortIsAvailable(port, host)` immediately before `listen()`. A collision now
|
|
462
|
+
surfaces as `EADDRINUSE: Port <port> is already in use on <host>. Stop the dev server
|
|
463
|
+
(or whatever else is listening on port <port>) and run again...` — the code and the
|
|
464
|
+
port named in the same sentence — instead of a bare `EADDRINUSE` thrown from inside
|
|
465
|
+
Fastify with no indication of which port it meant. `EACCES` on the port is treated
|
|
466
|
+
the same way, since "cannot bind" is one problem from the operator's side. The test
|
|
467
|
+
server (`startHttpTestServer`)
|
|
468
|
+
already preflighted its port before this release; this brings `dev`/`start` to the
|
|
469
|
+
same behavior.
|
|
470
|
+
- **`startHttpTestServer()` now runs `Application.runStartupValidators()`** — the
|
|
471
|
+
same slot `warlock dev` and the generated production `app.ts` already ran it in —
|
|
472
|
+
after application modules load and before the late-phase connectors (http, socket)
|
|
473
|
+
bind. A validator registered via `Application.onValidateBoot(...)` that rejects now
|
|
474
|
+
aborts the test server's boot exactly as it aborts `dev`/`start`, instead of only
|
|
475
|
+
being enforced outside of tests.
|
|
476
|
+
- **`warlock add web` scaffolds a real, validated API endpoint**, not just a static
|
|
477
|
+
page. It now also writes `src/app/contact/routes.ts` and
|
|
478
|
+
`src/app/contact/controllers/contact.controller.ts` (a `POST /api/contact` route
|
|
479
|
+
validated with `@warlock.js/seal`), and `src/web/home.page.tsx` ships an
|
|
480
|
+
interactive, localized (en/ar) contact form wired to that route via `@mongez/http`
|
|
481
|
+
- `@mongez/react-form` + `@mongez/react-localization`. The `web` feature now also
|
|
482
|
+
installs those three packages as dependencies.
|
|
483
|
+
|
|
484
|
+
### Fixed
|
|
485
|
+
|
|
486
|
+
- **Feature definitions no longer carry stale `~4.0.0` defaults for
|
|
487
|
+
`@warlock.js/*`.** They now use an explicit internal placeholder that
|
|
488
|
+
`warlock add` must resolve from the installed Core version before invoking a
|
|
489
|
+
package manager or writing `package.json`. A forgotten resolution therefore
|
|
490
|
+
fails loudly instead of silently selecting an old framework major.
|
|
491
|
+
- **`startHttpTestServer()` fails loudly instead of silently skipping the preflight**
|
|
492
|
+
when `http.port` doesn't round-trip through `Number()` (e.g. `HTTP_PORT=03999`,
|
|
493
|
+
`+3999`, `1e3`, or a value with stray whitespace) — previously it returned early
|
|
494
|
+
and let the connector reach `listen({ port })` with the unvalidated value and no
|
|
495
|
+
published port for test workers to resolve.
|
|
496
|
+
- **A race in the mail SES driver** where `getSesMailer()` could read the
|
|
497
|
+
eagerly-loaded `nodemailer` module before its load promise had settled. It now
|
|
498
|
+
awaits the in-flight load first (throwing the "nodemailer is not installed"
|
|
499
|
+
install-instructions error if the load ultimately failed), matching the guard the
|
|
500
|
+
SMTP path already had.
|
|
501
|
+
|
|
502
|
+
## 5.1.0
|
|
503
|
+
|
|
504
|
+
> **If you use `@warlock.js/web`, upgrade.** React did not execute at all in published
|
|
505
|
+
> installs of 5.0.0 through 5.0.2 — the defect and its fix are in that package's
|
|
506
|
+
> changelog. `core` ships the CLI-side half of this release.
|
|
507
|
+
|
|
508
|
+
### Added
|
|
509
|
+
|
|
510
|
+
- **`warlock add tailwind`** — installs and wires Tailwind CSS v4 through PostCSS.
|
|
511
|
+
- **`warlock add shadcn`** — sets up the prerequisites shadcn/ui expects. It is _not_ a
|
|
512
|
+
wrapper around the shadcn CLI: you still run that yourself to add components, this
|
|
513
|
+
only makes the project ready for it.
|
|
514
|
+
|
|
515
|
+
### Changed
|
|
516
|
+
|
|
517
|
+
- **`warlock dev` prints one status block per run.** The banner is printed exactly once,
|
|
518
|
+
and the URL it prints is never a raw `[::1]` address. The single-boot guard behind
|
|
519
|
+
this is **new in this release — it was never present in any published version**, so
|
|
520
|
+
duplicate boots on 5.0.x were real, not a display artefact.
|
|
521
|
+
- **`warlock doctor` now reports the same route count as `warlock dev`.** The two walked
|
|
522
|
+
routes differently and disagreed. `doctor` also emits zero warnings on a healthy
|
|
523
|
+
project — so a warning now means something — and fails loudly on a route module that
|
|
524
|
+
genuinely fails to load, instead of counting it as fine.
|
|
525
|
+
|
|
526
|
+
### Removed
|
|
527
|
+
|
|
528
|
+
- **A false comment that the `warlock add web` scaffold emitted in 5.0.0 through 5.0.2**,
|
|
529
|
+
claiming a page's route is derived from its file location. No such derivation has ever
|
|
530
|
+
existed. The stub no longer emits it; **apps already scaffolded on those versions still
|
|
531
|
+
carry the comment in their own source** and must delete it by hand.
|
|
532
|
+
|
|
533
|
+
### Internal
|
|
534
|
+
|
|
535
|
+
- The `add` feature registry was split into one module per feature, replacing the single
|
|
536
|
+
growing switch.
|
|
537
|
+
- Fixed 7 mojibake checkmarks in CLI output.
|
|
538
|
+
|
|
539
|
+
## 5.0.2 - 2026-08-25
|
|
540
|
+
|
|
541
|
+
No changes to `@warlock.js/core`. Released in lockstep with the `@warlock.js/web` SSR
|
|
542
|
+
fix (`ssr.noExternal`) — see that package's changelog.
|
|
543
|
+
|
|
544
|
+
## 5.0.1 - 2026-08-25
|
|
545
|
+
|
|
546
|
+
No changes to `@warlock.js/core`. Released in lockstep with the `create-warlock` vite
|
|
547
|
+
resolution pin and the `@warlock.js/web` peer narrowing — see those packages' changelogs.
|
|
548
|
+
|
|
549
|
+
## 5.0.0 - 2026-08-25
|
|
550
|
+
|
|
551
|
+
### Upgrading
|
|
552
|
+
|
|
553
|
+
- **BREAKING — `Request` no longer has a `[key: string]: any` index signature.** Attaching arbitrary properties (`request.post = post`) no longer compiles. Migrate per attachment: per-request data written by middleware → `request.locals` (augment `RequestLocals`); per-request memoized computation → `requestMemo(key, fn)`; new typed members → module augmentation of `Request` itself
|
|
554
|
+
- **BREAKING — `fromRequest(key, callback)` is removed.** It cached values as dynamic `Request` properties, which only compiled because of the deleted index signature. `requestMemo(key, fn)` is the drop-in successor: same per-request lifetime, single-flight (concurrent callers share one promise), and it never touches the `Request` object
|
|
555
|
+
- **BREAKING — client-supplied locales are validated against `app.localeCodes`.** A `locale`/`translation-locale-code` header or `locale` query value outside the configured list is treated as absent and falls through to the default locale — it previously won as-is, steering every translated string and every serialized Resource. Apps that declare no `app.localeCodes` keep the pass-through behavior
|
|
556
|
+
- **`request.trans()` / `request.t()` now resolve the locale at call time.** Previously the translator captured the locale once during request construction (before routing), so a locale set later — a path locale, `setLocaleCode()` — changed `request.locale` but not the language of translated strings
|
|
557
|
+
|
|
558
|
+
### Added
|
|
559
|
+
|
|
560
|
+
- **`request.requireUser()`** — returns the authenticated user non-optionally, or throws `UnAuthorizedError` when no user is attached. For handlers behind an auth guard, where an absent user is a misconfigured route rather than a normal state; replaces `request.user!` assertions
|
|
561
|
+
|
|
562
|
+
## 4.16.0 - 2026-08-18
|
|
563
|
+
|
|
564
|
+
### Security
|
|
565
|
+
|
|
566
|
+
- **`request.detectIp()` no longer trusts `X-Real-IP` / `X-Forwarded-For` unless `http.trustProxy` is set.** Both headers are client-settable, and `detectIp()` honoured them unconditionally — bypassing the `trustProxy` opt-in the Fastify server itself is configured with. Any client could therefore spoof its IP to everything keyed on `detectIp()`: `ipFilter` allowlists/denylists, the default rate-limit bucket key, and anonymous idempotency scoping. Without the opt-in, `detectIp()` (and its `realIp` alias) now returns `baseRequest.ip` — the socket peer address, which cannot be forged
|
|
567
|
+
|
|
568
|
+
⚠ **If your app runs behind a proxy and relied on `detectIp()` reading the forwarding headers without setting `http.trustProxy`, set `http.trustProxy: true`** (or a Fastify `trustProxy` value matching your edge). With `true` set, behaviour is unchanged: `X-Real-IP` first, then the leftmost `X-Forwarded-For` hop, then the peer address. Only enable `true` when your edge overwrites those headers — it trusts them wholesale
|
|
569
|
+
|
|
570
|
+
- **`http.trustProxy` now accepts a hop count or a trusted-proxy list, and `detectIp()` honours them.** `true` is the wrong shape for the common topology: an edge that _appends_ to `X-Forwarded-For` leaves whatever the client prepended as the leftmost entry, so "trust the leftmost hop" hands the client its own IP back. The config value is passed to Fastify untouched, and `detectIp()` now reads the resolved client off `request.ip` instead of re-parsing the header — so both agree, and every Fastify shape works:
|
|
571
|
+
|
|
572
|
+
| `http.trustProxy` | Client IP |
|
|
573
|
+
| ---------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
574
|
+
| `false` _(default)_ | Socket peer address; forwarding headers ignored |
|
|
575
|
+
| `true` | Leftmost `X-Forwarded-For` entry (whole chain trusted) |
|
|
576
|
+
| `2` | Walks past the 2 rightmost hops — for an edge that appends |
|
|
577
|
+
| `"10.0.0.0/8"`, `"loopback, 10.0.0.0/8"`, `["10.0.0.0/8", "192.168.0.0/16"]` | Walks left while each hop is a listed proxy, stops at the first that isn't |
|
|
578
|
+
| `(address, hop) => boolean` | Your predicate |
|
|
579
|
+
|
|
580
|
+
Prefer the narrowest shape your topology allows: with `true`, any client that can reach the process directly picks its own IP, and an `ipFilter` allowlist in front of it is decorative
|
|
581
|
+
|
|
582
|
+
⚠ **`X-Real-IP` is now honoured only under `trustProxy: true`.** It carries no chain, so there is nothing to check a hop count or proxy list against, and a trusted edge that forwards the client's own `X-Real-IP` verbatim would otherwise let any client escape the bound. Under a bounded `trustProxy` the value comes from the `X-Forwarded-For` chain instead — if your edge sets only `X-Real-IP`, have it set `X-Forwarded-For` as well
|
|
583
|
+
|
|
584
|
+
### Dependencies
|
|
585
|
+
|
|
586
|
+
- Bumped `@mongez/*` deps to their 2026-08-17 security release specs: `concat-route` ^1.2.0, `config` ^1.2.1, `dotenv` ^1.3.2, `events` ^2.2.7, `http` ^3.5.0, `localization` ^3.4.7, `reinforcements` ^4.0.1, `supportive-is` ^2.1.4
|
|
587
|
+
- ⚠ **`@mongez/reinforcements` 4.0.1 is a major bump: `Random` is now CSPRNG-backed (WebCrypto) and `Random.seed()` was removed** — seeded/reproducible `Random.string/nanoid/id/token/uuid` calls now throw. Audited `core`'s `Random.string(...)` call sites (`use-case.ts`, `http/request.ts`, `dev-server/files-watcher.ts`, `http/uploaded-file.ts`) and its test suite: none rely on seeding or reproducible output, so no code changes were required
|
|
588
|
+
- `@mongez/encryption` 2.0.1 (async `encrypt`/`decrypt`, throws on failure) does not apply to this package — `core` is not a consumer; `src/encryption/encrypt.ts` uses Node's built-in `crypto` module directly and is unaffected
|
|
589
|
+
|
|
590
|
+
## 4.14.0 - 2026-08-16
|
|
591
|
+
|
|
592
|
+
### ⚠ Upgrading from 4.13.0 — read this first
|
|
593
|
+
|
|
594
|
+
**Three behaviour changes and one documentation correction. All four touch the test lifecycle; none touch application runtime.**
|
|
595
|
+
|
|
596
|
+
⛔ **Every existing project must replace its `src/test-setup.ts`.** Three things are wrong with the file 4.13.0 generated:
|
|
597
|
+
|
|
598
|
+
```ts
|
|
599
|
+
/**
|
|
600
|
+
* Test Setup
|
|
601
|
+
* Runs before EACH test file — not once per worker.
|
|
602
|
+
*/
|
|
603
|
+
import { afterAll } from "vitest";
|
|
604
|
+
import { setupTest, teardownTest } from "@warlock.js/core/tests";
|
|
605
|
+
|
|
606
|
+
await setupTest(); // ← was setupTest({ connectors: true })
|
|
607
|
+
afterAll(teardownTest); // ← is new
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
1. **`{ connectors: true }` must become a bare `setupTest()`.** Under the new precedence it is an **explicit** value, so it now overrides your `src/config/tests.ts` where it previously deferred to it.
|
|
611
|
+
2. **`afterAll(teardownTest)` is new and is not optional** — without it nothing ever closes the framework your tests started.
|
|
612
|
+
3. **The `Per-Worker Test Setup` comment is false.** It always was.
|
|
613
|
+
|
|
614
|
+
**This is the migration step nobody can skip.** `warlock add test` emits the corrected file for new projects.
|
|
615
|
+
|
|
616
|
+
| What changes | How you'll see it | What to do |
|
|
617
|
+
| -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
618
|
+
| **`setupTest({ connectors })` now beats `tests.connectors` config** — the precedence flipped | a test file that passes `connectors` explicitly starts a **different connector set** than it did in 4.13.0 | grep for `setupTest({` — a call passing `connectors` was previously **ignored** and is now honoured. **Including the one in your generated setup file** |
|
|
619
|
+
| **A second `setupTest` call with different options now REJECTS** | an error naming the active and the requested selection, where 4.13.0 silently did nothing | call `teardownTest()` first, or don't call `setupTest` again at all |
|
|
620
|
+
| **The generated setup file now registers `afterAll(teardownTest)`** | your test files tear the framework down when they finish, instead of leaving it running | **add it to your existing `src/test-setup.ts`** — see below |
|
|
621
|
+
| **Docs corrected: `setupTest` is called per TEST FILE, not per worker** | no runtime effect on its own — the _invocation_ always worked this way | fix the comment in `src/test-setup.ts` as above |
|
|
622
|
+
|
|
623
|
+
### Added
|
|
624
|
+
|
|
625
|
+
- **`teardownTest()` — the other half of the pair.** `setupTest` has shipped without a counterpart since it was introduced: there was no supported way to close the framework a test file brought up, and the only "reset" available was a module flag that proved nothing about whether ports, sockets, pools or timers had actually closed
|
|
626
|
+
|
|
627
|
+
`teardownTest()` is idempotent when idle, shares one shutdown between concurrent callers, waits for an in-flight setup to settle before closing, and **always clears local state in a `finally`** so a failed shutdown cannot leave the lifecycle claiming to be ready
|
|
628
|
+
|
|
629
|
+
⚠ **A shutdown failure poisons the lifecycle rather than pretending to recover.** If the shutdown layer reports a rejection, later `setupTest` calls refuse until the Vitest worker is recycled or a retried teardown fully succeeds. **We cannot promise a clean restart after a reported close failure, so we don't.** ⚠ **What it cannot see:** `connectorsManager.shutdown()` catches and logs individual connector failures internally — those never reach this lifecycle and never poison it. Manager-wide error policy is a separate piece of work
|
|
630
|
+
|
|
631
|
+
### Changed
|
|
632
|
+
|
|
633
|
+
- **BREAKING — an explicit `setupTest({ connectors })` now wins over `tests.connectors` config.** The order was `config > parameter > true`; it is now **`explicit parameter > config > true`**
|
|
634
|
+
|
|
635
|
+
4.13.0's changelog said this question was open, not settled: _"a per-call override is a contract decision for a later release."_ This is that decision. **Call-site intent should beat a project default** — a caller who names a connector set is being specific on purpose, and silently overruling them was the wrong behaviour
|
|
636
|
+
|
|
637
|
+
**"Explicit" means a non-`undefined` value.** `setupTest()`, `setupTest({})` and `setupTest({ connectors: undefined })` **all fall through to config, then to `true`.** The `undefined` rule is deliberate: an optional variable that happens to be `undefined` must not silently erase project config
|
|
638
|
+
|
|
639
|
+
⚠ **The generated `src/test-setup.ts` now calls `setupTest()` with no argument**, where it previously passed `{ connectors: true }`. Under the new order, passing `true` explicitly would erase the `tests.connectors` layer for the entire project. **If you edit your setup file, leave the call bare**
|
|
640
|
+
|
|
641
|
+
⚠ **This is user-visible and it is why the change is marked BREAKING:** an application that sets `tests.connectors` _and_ passes `connectors` from any test file will start a different connector set after upgrading
|
|
642
|
+
|
|
643
|
+
- **BREAKING — a conflicting `setupTest` call rejects instead of being ignored.** While a setup is starting or ready, a call with _different_ effective options now rejects with an error naming both the active and the requested selection. The same options remain a no-op, and concurrent identical calls share one startup
|
|
644
|
+
|
|
645
|
+
Through 4.13.0 this was a silent early-return on an `isSetupComplete` flag — so `setupTest({ connectors: false })` in a file whose `src/test-setup.ts` had already run **did nothing at all, reported nothing, and started every connector anyway.** Connector arrays are compared as **sets** after deduplication, so caller order never counts as a conflict
|
|
646
|
+
|
|
647
|
+
- **Lifecycle state is now scoped to the worker runtime instead of the module.** `isSetupComplete` was a module-level variable, and **Vitest rebuilds the setup module's registry between test files while the worker process or thread keeps running** — so the flag reset in exactly the situation where live DB connections, pools and timers survive
|
|
648
|
+
|
|
649
|
+
Scope is per **process** under `pool: "forks"` and per **thread** under `pool: "threads"`; it deliberately does not cross thread workers, because `globalThis` is per realm and the resources are per worker too. **The guard's scope now matches the leak's scope in all four `pool` × `isolate` combinations**
|
|
650
|
+
|
|
651
|
+
### Fixed
|
|
652
|
+
|
|
653
|
+
- **A stranded setup no longer exhausts the heap.** A lifecycle left in the `starting` state sent `teardownTest`'s wait-then-re-enter path into unbounded recursion — **`FATAL ERROR: JavaScript heap out of memory` at 4 GB, killing the worker with 26 tests in that run never executed.** It was found while proving the state machine, not reported by a user, and it would have shipped
|
|
654
|
+
|
|
655
|
+
The setup attempt is now bounded by **`tests.setupTimeout`, defaulting to `120000` ms**, and expiry **poisons** the lifecycle rather than returning it to `idle` — the attempt may have started connectors nobody can now account for. The message names the state, the bound and the remedy:
|
|
656
|
+
|
|
657
|
+
```
|
|
658
|
+
setupTest() did not finish within 120000ms and is stuck in the "starting" state. The
|
|
659
|
+
lifecycle is now poisoned: whatever that attempt had already started is not known to be
|
|
660
|
+
closed, so later setupTest() calls refuse until the Vitest worker is recycled. If your
|
|
661
|
+
cold start is legitimately slower than this, raise the bound with `tests.setupTimeout`
|
|
662
|
+
in `src/config/tests.ts` — milliseconds, default 120000.
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
**It bounds the setup attempt, not teardown separately** — `teardownTest()` awaits the same attempt and inherits the bound. **A second teardown-side deadline was tried and rejected during implementation**: it expired instead of the setup's, was swallowed on settle, re-entered and armed a third, and left the stuck setup unbounded after all — reproducing the exact recursion the guard exists to remove
|
|
666
|
+
|
|
667
|
+
⚠ **An invalid `tests.setupTimeout` throws, naming the value.** Zero, negative and non-numeric fail loudly instead of falling back to the default; a silent fallback hides a typo behind a working suite
|
|
668
|
+
|
|
669
|
+
⚠ **A stranded lifecycle must fail with a message, not a dead process** — a crash mid-file is indistinguishable from an infrastructure flake, which is the worst way for a framework to report its own bug
|
|
670
|
+
|
|
671
|
+
⚠ **Scope of the proof, stated because a green here is easy to over-read:** all nine guards were seen to fail under their own mutation, **but every spec injects its scheduler** — the default _value_ is tested while the production timer, and whether its `unref` releases the worker, is not. **No spec observes a real hang**; the stuck attempt is a mock gate, not a socket that never returns
|
|
672
|
+
|
|
673
|
+
### Documentation
|
|
674
|
+
|
|
675
|
+
- **Corrected: `setupTest` is CALLED once per TEST FILE, not once per worker.** Every version of the `test-service` and `test-http` skills, both generated LLM projections, the generator's comments and `setupTest`'s own JSDoc described a per-worker lifetime. **Vitest runs `setupFiles` before each test file and their exports are ignored** — measured across all four `pool` × `isolate` combinations, not inferred
|
|
676
|
+
|
|
677
|
+
⛔ **If your project was generated before 4.14.0, replace the whole file** — see the migration block above. It is not a comment-only change
|
|
678
|
+
|
|
679
|
+
**The lifetime this release commits to is FILE-SCOPED:** the setup file bootstraps the framework and its `afterAll(teardownTest)` closes it, once per test file. **One owner, one pairing, correct under every pool, every isolation setting, and watch mode**
|
|
680
|
+
|
|
681
|
+
⚠ **This deliberately declines a faster option.** Holding lifecycle state in the worker runtime makes a worker-scoped lifetime _possible_ — bootstrap once, reuse across every file in that worker — and an earlier draft of this release simply left the framework running to get it. **We are not shipping that**, for two reasons neither of which is performance:
|
|
682
|
+
|
|
683
|
+
1. **Under `pool: "threads"` we cannot honestly claim the runner cleans up.** Vitest tears the thread down while the process lives, and whether Node reclaims that thread's sockets and pools is **unmeasured** — so "the runner owns cleanup by termination" would be a promise we cannot observe being kept
|
|
684
|
+
2. **In watch mode Vitest reuses workers between reruns**, so there is no recycle and therefore **no cleanup owner at all** between reruns. Declaring watch mode unsupported was the alternative, and a test framework whose lifecycle is undefined in the mode people use all day does not have a lifecycle
|
|
685
|
+
|
|
686
|
+
**The cost is a framework bootstrap per test file — which is exactly what 4.13.0 already paid**, since its module-level flag died with the module registry between files. **Nothing gets slower; an unearned speed-up is simply not being claimed.** A worker-scoped lifetime remains open, and gets taken when the real per-file cost has been measured on a real application and the runner integration is chosen deliberately rather than inherited from whatever the wiring happened to do
|
|
687
|
+
|
|
688
|
+
## 4.13.0 - 2026-08-12
|
|
689
|
+
|
|
690
|
+
### ⚠ Upgrading from 4.12.0 — read this first
|
|
691
|
+
|
|
692
|
+
**Four breaking changes. Every one of them fails visibly, and every one is fixed by a single line or a single config key.** Three are security defaults that were wrong; the fourth is an import path.
|
|
693
|
+
|
|
694
|
+
| What breaks | How you'll see it | The fix |
|
|
695
|
+
| ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
696
|
+
| **`http.cors` now actually applies** — it never had any effect in any release through 4.12.0 | requests from origins you never allow-listed start being rejected | check `http.cors` before upgrading; it now means what it says |
|
|
697
|
+
| **`http.bodyLimit` defaults to Fastify's 1 MB**, not 200 GB | large uploads that used to be accepted answer `413` | set `http.bodyLimit` explicitly if you need more |
|
|
698
|
+
| **`http.trustProxy` defaults to `false`** | `request.ip` becomes the socket address instead of `X-Forwarded-For` | set `http.trustProxy: true` **only if** you are genuinely behind a proxy that strips the header |
|
|
699
|
+
| **The package entry no longer re-exports the CLI, dev server, test helpers or Vite integration** | build fails with `has no exported member` | test helpers move to `@warlock.js/core/tests`, `lowerStage3Decorators` to `@warlock.js/core/vite` — **one line per import.** The CLI and dev-server internals are **not** public and have no replacement specifier |
|
|
700
|
+
|
|
701
|
+
**If your app configures none of the three HTTP keys, the first three changes make it strictly safer with no action from you.** The `trustProxy` default in particular meant per-IP rate limiting was bypassable by anyone sending their own `X-Forwarded-For`.
|
|
702
|
+
|
|
703
|
+
Details for each are in the entries below.
|
|
704
|
+
|
|
705
|
+
### Added
|
|
706
|
+
|
|
707
|
+
- **`build.singleBundle` — one file you can run with `node dist/app.js`.** The default build keeps dependencies as real `import` specifiers resolved from `node_modules`, which is right when you deploy the folder. Producing a single self-contained file previously meant knowing to set `packages: "bundle"` **and** `splitting: false`, and it still did not work
|
|
708
|
+
|
|
709
|
+
It sets both as **defaults you can override** — an explicit `splitting: true` in your config still wins. Phase ordering is unaffected: it comes from the generated entry using dynamic `await import(...)`, which the bundler defers to the call site whether or not splitting is on. A comment in the builder claiming ordering "requires `splitting: true`" was wrong and has been corrected
|
|
710
|
+
|
|
711
|
+
⚠ Native `.node` addons cannot be inlined by any bundler and are still emitted alongside the file, so "single bundle" means one JavaScript file plus any native addons — the flag does not promise otherwise
|
|
712
|
+
|
|
713
|
+
**Verified end to end on a real application**, not in isolation: the build emits exactly one file, the interop prelude is present, and the result boots and serves a request. This only became possible once dev-only tooling left the production module graph — see the entry below, which is what made `singleBundle` build a real application rather than only a synthetic one
|
|
714
|
+
|
|
715
|
+
- **`@warlock.js/core/tests` and `@warlock.js/core/vite` are real subpaths**, with their own build entries and `exports` keys — the first version in which those helpers are addressable at all. `/tests` carries the 13 documented test helpers; `/vite` carries `lowerStage3Decorators`
|
|
716
|
+
|
|
717
|
+
**The test server's port channel — `publishTestServerPort`, `withdrawTestServerPort`, `TEST_SERVER_PORT_ENV_KEY` — is deliberately NOT part of that surface.** It is how `startHttpTestServer` hands the resolved port to the worker processes that later call `testGet`; it is plumbing, and nothing outside the framework needs it. **Decided before the subpath shipped rather than after, because removing an export once people depend on it is a breaking change**
|
|
718
|
+
|
|
719
|
+
### Fixed
|
|
720
|
+
|
|
721
|
+
- **`setupTest()` no longer crashes in a project that has no `src/config/tests.ts`.** `config.get("tests")` resolves an absent key to `null`, and the result was dereferenced — so the very path `warlock add test` generates threw `Cannot read properties of null (reading 'connectors')` before running a single test. **`setupTest()` with no arguments at all** threw one step earlier still, on a destructured parameter with no default
|
|
722
|
+
|
|
723
|
+
**`connectors: false` now starts no connectors, as its type and the skill have always said.** The selection read `testConfig.connectors || connectors`, so a configured `false` was discarded, and every non-array value then took the "start all but http" branch — **`false` started everything except http, from config and from the caller alike.** It is now `??` with an explicit `false` check
|
|
724
|
+
|
|
725
|
+
⚠ **Config still beats the parameter:** if `tests.connectors` is set, `setupTest({ connectors })` cannot override it. That is the existing contract and this release does not change it — one line of `skills/test-service` claimed otherwise and has been corrected. **A per-call override is a contract decision for a later release**
|
|
726
|
+
|
|
727
|
+
- **The request helpers send falsy JSON bodies.** `testPost`, `testPut` and `testPatch` used `body ? JSON.stringify(body) : undefined`, so **`false`, `0`, `""` and `null` — all legal JSON documents — were sent as no body at all.** Only an omitted argument now means "no body"
|
|
728
|
+
|
|
729
|
+
**Headers are accepted in every shape `RequestInit` allows.** They were merged by object spread, which is correct only for a plain record — a `Headers` instance or a `[name, value]` tuple list was silently turned into an object with numeric keys and the header was lost with no error. They are now normalised through `new Headers(...)`
|
|
730
|
+
|
|
731
|
+
**`Content-Type: application/json` is set only when the helper serialized the body**, and never over a content type the caller set. It was previously forced onto every request, including `FormData` — replacing the multipart boundary the runtime generates and producing a request no server can parse
|
|
732
|
+
|
|
733
|
+
- **Shutdown survives a throwing log channel.** A connector whose `shutdown()` failed was reported through `log.error(...)` **from inside the catch block** — and `Logger.log()` hands each entry to `channel.log()` with no isolation, so a channel that throws synchronously (a misconfigured transport, an unserialisable payload) made that report reject. The rejection escaped `shutdown()` entirely, and the consequences went well past a missing log line: **`log.flush()` never ran, so every buffered entry from the whole run was lost; the remaining connectors were never torn down; and `process.exit(0)` — the line `gracefulShutdown` runs once `shutdown()` resolves — was never reached, leaving the process alive on the handles those connectors still held**
|
|
734
|
+
|
|
735
|
+
⚠ **This hardens the shutdown path, not the logger.** `Logger.log()` still aborts its fan-out on the first throwing channel, so the other channels never receive that entry, and an _asynchronously_ rejecting channel is not covered at all — `channel.log()` is never awaited. **Logger-wide isolation is a separate fix in a later release**
|
|
736
|
+
|
|
737
|
+
- **A test server that fails to start no longer leaves half of itself running.** `startHttpTestServer()` publishes the resolved port before the late connector phase and sets `isServerRunning` only on its last line, so a failure in between left **live early-phase connectors and a published port pointing at a server that never came up** — while `stopHttpTestServer()` in `globalTeardown` reported _"No server to stop"_ and walked away from them. Startup now unwinds what it started, always withdraws the port and resets its state. ⚠ **The error you get back is unchanged — it always was.** Startup had no `catch` at all, so the original failure already propagated correctly; what was missing was the cleanup, and the new `catch` exists only to run it. A failure _during_ that cleanup is reported and never substituted for the cause, which is the one propagation guarantee the wrapper had to be careful not to break
|
|
738
|
+
|
|
739
|
+
**`stopHttpTestServer()` withdraws the port and resets state in a `finally`.** They previously ran after the `await`, so a shutdown that threw left the published port behind and the next run in the same process inherited it
|
|
740
|
+
|
|
741
|
+
- **`startHttpTestServer({ port: 0 })` is refused with an instruction instead of half-working.** `0` is the OS's "pick a free one" idiom, and the test server cannot honour it: the preflight would bind some unrelated ephemeral port and pass without proving anything, and nothing publishable exists afterwards — `HttpConnector.start()` records the port it **asked for**, not the one Fastify bound. Accepting it silently meant `getTestServerUrl()` resolved `0` through its own config fallback and **every worker request went to `http://host:0`**, where nothing listens. The error names the fix: pass an explicit port, or set `http.port`
|
|
742
|
+
|
|
743
|
+
- ⚠ **BREAKING — the package entry no longer re-exports the CLI, the dev server, the test helpers or the Vite integration.** Five `export *` lines are gone from `@warlock.js/core`'s root: `./cli`, `./dev-server/files-orchestrator`, `./dev-server/health-checker`, `./tests` and `./vite`
|
|
744
|
+
|
|
745
|
+
**Why it had to change:** those five put dev-only tooling into the **static module graph of every application that imports the framework** — 39 files, reaching ESLint and, through it, ESLint's optional `jiti` import. It cost nothing while the builder kept `packages: "external"`, because esbuild never walked into the framework. **Anything that bundles walks it, and the build fails.** That is why `singleBundle` could not build a single real application
|
|
746
|
+
|
|
747
|
+
⚠ **Making those imports lazy does not help and should not be attempted.** esbuild resolves `import()` at build time; a dynamic import defers _evaluation_, not resolution. Measured: `await import("jiti")` in an otherwise empty file still fails with `Could not resolve "jiti"`. **Only unreachability from the entry removes a module from the graph**
|
|
748
|
+
|
|
749
|
+
**The two public halves are now reachable from a subpath — and in 4.12.0 and earlier they were reachable from nowhere at all.** `./tests` and `./vite` are **real build entries with their own emitted files and `exports` entries**, not reachable from the root barrel, which is the entire point:
|
|
750
|
+
|
|
751
|
+
```ts
|
|
752
|
+
// before // after
|
|
753
|
+
import { setupTest } from "@warlock.js/core";
|
|
754
|
+
import { setupTest } from "@warlock.js/core/tests";
|
|
755
|
+
import { startHttpTestServer } from "@warlock.js/core";
|
|
756
|
+
import { startHttpTestServer } from "@warlock.js/core/tests";
|
|
757
|
+
import { lowerStage3Decorators } from "@warlock.js/core";
|
|
758
|
+
import { lowerStage3Decorators } from "@warlock.js/core/vite";
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
**The CLI and dev-server internals are different — they were never a public API and have no replacement specifier.** If you were importing from those, you were reaching into framework internals; open an issue describing what you needed.
|
|
762
|
+
|
|
763
|
+
⚠ **If you were importing these symbols in 4.12.0, they were already not in the published package.** `esm/tests/` and `esm/vite/` did not exist in the 4.12.0 artifact, and none of those symbols appear in `esm/index.d.mts` — **the root barrel re-exported source the build never emitted.** So this is the release that makes them installable, not the one that removed them.
|
|
764
|
+
|
|
765
|
+
`Path` is unaffected — it moved to a genuine utility module and remains exported
|
|
766
|
+
|
|
767
|
+
**Removing the five lines was necessary but not sufficient.** Three production modules — `connectors/http-connector`, `connectors/connectors-manager` and `warlock-config/warlock-config.manager` — imported the dev server's console formatter directly. In `http-connector` the dev-console call was simply **deleted**: the same error was already routed through `log.fatal` on the following line. `connectors-manager` now logs a connector's shutdown failure through `@warlock.js/logger`, **awaited and then flushed** — `process.exit(0)` follows immediately, so an un-awaited log is a log that never happens. `warlock-config.manager`'s _"`warlock.config.ts` is missing"_ warning writes **straight to the console instead**: it runs during CLI bootstrap, before the logger has a single channel configured, so routing it through the logger would drop it in every application
|
|
768
|
+
|
|
769
|
+
`tests/unit/meta/production-entry-graph.test.ts` enforces this from now on. It keys on **our own directory names** rather than a denylist of third-party packages, because a denylist rots the moment a dependency changes its imports and can only catch names someone thought of
|
|
770
|
+
|
|
771
|
+
- ⚠ **BREAKING — the CORS allow-list in `http.cors` now actually applies.** The framework's defaults were spread **after** your configuration, so `{ origin: "*", methods: "*" }` overwrote whatever you set. **`http.cors` has never had any effect**, in any release up to 4.12.0 — an app that configured an allow-list still answered every origin. Your configuration now wins
|
|
772
|
+
|
|
773
|
+
**This is breaking in the direction you want, but it is breaking:** an app that has been relying on the accidental `origin: "*"` while believing it was restricted will now restrict. Check your `http.cors` before upgrading
|
|
774
|
+
|
|
775
|
+
Worth knowing why the old behaviour was also broken for the legitimate case: `Access-Control-Allow-Origin: *` together with `Access-Control-Allow-Credentials: true` is a pair **browsers reject**, so a cross-origin `fetch(…, { credentials: "include" })` failed outright. The default was simultaneously too open for unauthenticated reads and unusable for authenticated ones
|
|
776
|
+
|
|
777
|
+
- ⚠ **BREAKING — `http.bodyLimit` no longer defaults to 200 GB.** An app that configures nothing now gets **Fastify's own 1 MB limit**. The previous default did not merely allow large bodies, it **replaced a protection Fastify provides**: an unauthenticated endpoint accepted a 5 MB body and ran application logic on it where bare Fastify would have answered `413`
|
|
778
|
+
|
|
779
|
+
**If you accept uploads, set `http.bodyLimit` before upgrading** — otherwise those requests start failing with `413`. The failure is immediate and one config key fixes it
|
|
780
|
+
|
|
781
|
+
The old comment recommended `middleware.maxBodySize()` for per-route caps. **That middleware cannot do this job** and its documentation has been corrected: framework middleware runs inside the route handler, after `@fastify/multipart` has already parsed the body, so it can report `Content-Length` but cannot refuse the read. For a real per-route cap use `serverOptions.bodyLimit`, which Fastify enforces while reading
|
|
782
|
+
|
|
783
|
+
- ⚠ **BREAKING — `http.trustProxy` now defaults to `false`.** `request.ip` was derived from the client-supplied `X-Forwarded-For` header by default, and `@fastify/rate-limit` keys its buckets on `request.ip` — so **a client sending a different `X-Forwarded-For` on each request got a fresh rate-limit bucket every time.** Any deployment not behind a proxy that strips the header had bypassable rate limiting, and the same applied to per-IP lockouts and audit logs
|
|
784
|
+
|
|
785
|
+
**If your app is behind a proxy, set `http.trustProxy = true`.** Until you do, `request.ip` reports the proxy's address rather than the client's — a visible, diagnosable change, unlike the previous default, which could not be detected from inside the application
|
|
786
|
+
|
|
787
|
+
- **Per-route `serverOptions` are no longer discarded by the dev server.** `scanDevServer` registers wildcard routes and dispatches per request, so it had no per-route registration slot and dropped `serverOptions` entirely. A route declaring `serverOptions.onRequest` — the documented way to run **before body parsing** — worked in production and **silently never ran in dev**, which is the only mode most teams run. `route.rateLimit` was dropped the same way, since it rides in the same options object
|
|
788
|
+
|
|
789
|
+
Matching now happens in Fastify's own `onRequest` phase and the matched route's hooks run there, so `onRequest`, `preValidation` and `preHandler` are forwarded with their pre-parse ordering intact
|
|
790
|
+
|
|
791
|
+
⚠ **Two options still cannot be forwarded in dev, and `serverOptions`' documentation now says so rather than implying otherwise:** `bodyLimit`, because Fastify reads it at registration time and no hook can bound a body already being parsed; and `preParsing`, because it must return the payload stream. For a pre-parse guarantee that holds in both modes, register a server-level hook via `router.beforeScanning` — it must be idempotent per request, since a connector restart builds a new server and runs the callback again
|
|
792
|
+
|
|
793
|
+
- **The dev server no longer rebuilds its entire route registry on every request.** A comment claimed the registry was initialised "once" and pointed at a `rebuildRouteRegistry` function that **does not exist**; the code sat inside the per-request handler, re-registering every route on every hit — and `router.any()` routes expand into seven registrations each. It is now built once and rebuilt when the route table changes
|
|
794
|
+
|
|
795
|
+
- **The dev dispatcher logs through the framework logger instead of `console.log(error)`,** with the request method and url attached
|
|
796
|
+
|
|
797
|
+
- **A bundled production build no longer succeeds and then dies at startup.** Setting `packages: "bundle"` produced a clean build whose process failed immediately with `Error: Dynamic require of "node:assert" is not supported`. Warlock's output is an ES module; bundled CommonJS dependencies call `require(...)` and read `__dirname` to locate their own assets, and neither exists in an ES module, so the bundler substituted a stub that throws. The only fix available to an application author was to hand-write an esbuild `banner` recreating `require` via `createRequire(import.meta.url)` — esbuild internals no app should need to know
|
|
798
|
+
|
|
799
|
+
The interop prelude is now injected automatically for **any** ESM build (`build.esmShim`, default `true`), not only under `singleBundle` — so a hand-written `packages: "bundle"` works too. It defines `__filename` and `__dirname` as well as `require`: a dependency resolving an asset path with a missing `__dirname` gets `undefined` and fails later, further from the cause
|
|
800
|
+
|
|
801
|
+
**An existing hand-written `banner` is preserved.** esbuild's `banner` is an object and every merge on the way to it replaces rather than merges, so previously whichever was applied second silently deleted the other. The shim is prepended to your banner instead
|
|
802
|
+
|
|
803
|
+
## 4.12.0
|
|
804
|
+
|
|
805
|
+
### Added
|
|
806
|
+
|
|
807
|
+
- **`warlock migrate --pending` — what will run next, in the order it will run.** `migrate` could report what had already run (`--list`) and what files existed on disk (`--all`), but not the one thing an operator asks before a schema change against a live database. The pending set was already computed on every migrate run; it simply had no read-only exit
|
|
808
|
+
|
|
809
|
+
The gap forced a workaround that is **unsound in the dangerous direction**. `--all` globs `src/app` only, so it cannot see migrations a _package_ registers through `database.migrations` — `@warlock.js/auth` alone contributes two. `--list` reads the migrations table, which does contain them. Differencing the two counts subtracts populations that do not overlap, and it under-counts pending by roughly the number of package migrations installed — reporting "nothing else is pending" when something is
|
|
810
|
+
|
|
811
|
+
**`migrate --list` now prints both sections**, executed and pending, so the question can be answered without knowing a second flag exists. The executed section prints **first and unconditionally**: it is a table read that cannot fail because of a broken file on disk, and `--list` is the command reached for while something is already wrong. `--list` always exits `0` — it is a report
|
|
812
|
+
|
|
813
|
+
**`--pending` is the gate**, and its exit code is its entire API: **`0`** computed and nothing pending, **`1`** computed and N pending, **`2`** could not be computed. Two codes would fold "three migrations are waiting" into "I could not work out what is waiting", and those demand opposite responses — the first is _run them_, the second is _stop_. `migrate --pending && deploy` behaves correctly under all three
|
|
814
|
+
|
|
815
|
+
**A failure to read the migrations never reports `0`.** Computing pending requires loading project code, and a single migration file missing its default export throws. That degrades to an explicit `Pending: unavailable — <reason>` line with the executed listing intact above it, and `--pending` exits `2`. An empty pending set means _nothing is pending_, and nothing else
|
|
816
|
+
|
|
817
|
+
`--all` deliberately does **not** gain a migration name beside each path. The only identifier available without loading is the one derived from the filename, and that derivation is a _fallback_ used when a migration does not set `migrationName` — so any migration that names itself (`auth`'s do) would be listed under a name that does not exist. A wrong identifier in a listing whose purpose is cross-referencing is worse than no identifier, and `--list`'s two sections answer the comparison directly
|
|
818
|
+
|
|
819
|
+
Proven against a real Postgres: an executed package migration and a pending local one land in the correct sections, a fully-migrated database reports an empty pending list rather than an absent one, and the reporter's `files − executed` arithmetic is pinned as a test that fails if it is ever reintroduced
|
|
820
|
+
|
|
821
|
+
### Changed
|
|
822
|
+
|
|
823
|
+
- `migrate`'s preload block no longer declares `env: true`. The flag has done nothing since env began loading for every command that declares a preload block; it was decoration, and the test suite now asserts its **absence** so it is not re-added by someone reading the still-deprecated type
|
|
824
|
+
|
|
825
|
+
- **The package now declares its own test runner and a `test` script.** `@warlock.js/core` shipped a maintained `vitest.config.ts` — aliasing eight sibling packages to their sources — with **no `devDependencies` key at all** and no way to invoke it. Its suite was reachable only by knowing to type `npx vitest`, which resolves whatever happens to exist in the tree rather than anything the manifest asked for. The runner is pinned to an **exact** version, not a range: it moved from 4.1.8 to 4.1.10 mid-development on an unrelated install, silently, and a suite whose runner can change underneath it proves less than it appears to
|
|
826
|
+
|
|
827
|
+
### Fixed
|
|
828
|
+
|
|
829
|
+
- **A build artifact that names an entry point it does not contain is now refused before it can be packed.** An interrupted build leaves a directory that looks finished — `package.json`, `README`, `CHANGELOG`, `bin/`, `skills/` — and holds no compiled code at all. Nineteen existed in this tree at once, and nothing in the release path noticed: the only related guard compares **modification times**, so a hollow directory with a freshly written manifest is _newer than source_ and passes, and it runs solely on the artifact-reuse path, which is not how the hollow directories were produced
|
|
830
|
+
|
|
831
|
+
Each artifact is now verified immediately before `npm pack`, on the normal build path and the reuse path alike. **The manifest is the specification:** `main`, `module` and the typings field name the exact files the package promises to ship, so they are resolved against the artifact and must exist. Fields a manifest does not declare are skipped — `core` and `auth` point `main` at `esm/` while `cascade`, `ai` and `seal` point it at `cjs/`, and any check that assumed one build shape would have raised a false failure on packages that are entirely correct. A manifest declaring no entry point at all is also a failure: a published package nothing can import is not a package
|
|
832
|
+
|
|
833
|
+
- **The production acceptance gate no longer inherits the environment it is supposed to be testing.** `run-pnpm-acceptance.mjs` spawned every child with `env: { ...process.env }` and set no `NODE_ENV`. It exercised the production path only because the shell it was written in happened to carry `NODE_ENV=production`; on a clean checkout, a new contributor's machine, or CI, the same gate boots the app in **development** — and does not fail, it passes while testing something other than the thing it is named after. That is the worst outcome available to a gate, and it sat underneath the proof for 4.11.0's headline fix
|
|
834
|
+
|
|
835
|
+
`NODE_ENV=production` is now set explicitly on every spawn, and — more importantly — **asserted from inside the running app**: `/acceptance` reports the environment it actually booted in, and the run fails if it is anything else. Setting a variable and never checking it arrived is how the original defect survived. The remaining `{ ...process.env }` is documented as a deliberate inheritance of `PATH` and the package-manager store paths, with everything the _verdict_ depends on set after it
|
|
836
|
+
|
|
837
|
+
Consequence for the roadmap, recorded because the ordering matters: **CI wiring for this gate is now blocked on this fix, not parallel to it.** Wiring it up first would have produced a green from CI — which carries more weight than a local one — for a run that never touched the production path
|
|
838
|
+
|
|
839
|
+
- **`warlock migrate --rollback=false` no longer drops every table.** CLI options were parsed as raw strings and nothing ever coerced them: `--rollback=false` reached the action as the string `"false"`, `if (rollback)` saw a truthy value, and the run rolled back _everything_. The declared `type: "boolean"` on the option was decorative — used only to render help. The same shape existed on every boolean option, including `warlock drop.tables --force=false`, where it turned a confirmation prompt into an unattended drop
|
|
840
|
+
|
|
841
|
+
Its twin was worse. A bare `--flag` swallowed the following token as its value, so `warlock migrate --rollback 2024_users.ts` produced `rollback: "2024_users.ts"` — the filename was never read as a path, and every table went down while the operator believed they had named one file. **A declared boolean now never consumes the next positional**: `--rollback 2024_users.ts` is `rollback: true` plus the positional `2024_users.ts`
|
|
842
|
+
|
|
843
|
+
Coercion is **type-aware, driven by the command's own declared options**, not a blanket rule in the parser: `--flag` → `true`, `--flag=true|1|yes` → `true`, `--flag=false|0|no` → `false` (case-insensitive), and short aliases (`-r=false`) behave identically. A string-typed option whose value is genuinely the word `false` — `--name=false` — still arrives as the string `"false"`. Options a command does not declare are untouched
|
|
844
|
+
|
|
845
|
+
**An unreadable value is an error, not a guess.** `--rollback=maybe` prints what was invalid and what is accepted, and exits 1. Guessing is what produced this defect; a flag that gates a destructive action must refuse input it cannot read rather than pick a side
|
|
846
|
+
|
|
847
|
+
`parseCliArgs` now takes an optional schema and runs twice: once bare to discover the command name (behaviour unchanged — there is no command to consult yet), then again against the resolved command's options. Re-reading argv is what makes the swallowed positional recoverable; by the time the first pass returns, a swallowed argument is indistinguishable from a value
|
|
848
|
+
|
|
849
|
+
- **`warlock generate.module users --force=false` no longer overwrites your files.** The coercion above is opt-in by design — it applies only to options a command declares `type: "boolean"`, so a string option whose value is genuinely the word `false` survives. The generate family and `add` never carried that declaration, so the fix reached none of them and both faces of the defect stayed live on the commands most likely to be run against existing source
|
|
850
|
+
|
|
851
|
+
`--force=false` arrived at every generator as the truthy string `"false"` and the overwrite guard (`if (exists && !force)`) let it through — a flag written to _prevent_ clobbering did the clobbering. Its twin ate the target: `warlock generate.module --force users` parsed `users` as the value of `--force`, so the module name was lost entirely and the generator ran with no name
|
|
852
|
+
|
|
853
|
+
Twenty-three option declarations are now typed: `--force, -f` and `--dry-run` on all eight `generate.*` commands, plus `--minimal, -m`, `--with-validation, -v`, `--with-resource, -rs`, and both `--timestamps [bool]` declarations, and `--list, -l` / `--no-install` on `add`. Options that carry real data are deliberately untouched and still take a value — `--table`, `--add`, `--drop`, `--rename` on the generators, `--package-manager` on `add`, and `seed --drop="Seed Name"`, whose value scopes which seeder is undone
|
|
854
|
+
|
|
855
|
+
`add --no-install` no longer has to be passed last. That instruction was in its help text only because the bare flag used to swallow the feature that followed it; `warlock add --no-install auth` now records `auth` as the feature and skips the install, and the wording is gone
|
|
856
|
+
|
|
857
|
+
The guard drives the real command objects through the manager's own resolution path (`tests/unit/cli/generate-flag-options.test.ts`) and asserts what the action is handed. Asserting the declaration object instead would pass against a fixture while the CLI stayed broken
|
|
858
|
+
|
|
859
|
+
- **`new Image(...)` no longer fails depending on how soon you call it.** The `Image` module fired `import("sharp")` at load time without awaiting it, and the constructor only checked whether that import had _failed_ — never whether it was still in flight. Constructing an image in the first tick after importing the package therefore ran with an undefined sharp function and died with `TypeError: sharpFn is not a function`; the exact same code passed if something had awaited a timer first. Anything that builds an image during boot — a startup thumbnail job, a module-level warm-up — hit it, and it presented as a mysterious "works locally, breaks in prod" timing bug rather than as a missing dependency
|
|
860
|
+
|
|
861
|
+
Sharp is now resolved **synchronously on the first construction that needs it**, via `createRequire`, and the outcome is cached for the process. There is no longer a window in which the constructor can proceed without a real sharp function: it either has the module or throws. A missing sharp still throws the same install-hint error, at the same point (construction), with the same wording
|
|
862
|
+
|
|
863
|
+
Resolution stays **lazy** — importing `@warlock.js/core` still does not load sharp's native binary, so apps that never touch images pay nothing — and constructing an `Image` from an existing sharp instance short-circuits before any module load
|
|
864
|
+
|
|
865
|
+
The guard for this is a spawned fresh Node process that imports and constructs with nothing in between (`tests/unit/image/image-sharp-resolution.test.ts`). A same-process test cannot catch it: importing at collection time and constructing later _is_ the delay that hides the bug
|
|
866
|
+
|
|
867
|
+
- **A sharp that is installed but will not load no longer reports itself as "not installed".** The resolution above swallowed every failure into a single outcome, so the most common real-world sharp problem — the package present but its native binary built for another platform — arrived as `sharp is not installed.` plus instructions to run `npm install sharp`, which cannot fix it. sharp throws its own long, actionable error naming the runtime, the failing `.node` file and the exact install flags to use; that text was discarded and replaced with a different, wrong cause
|
|
868
|
+
|
|
869
|
+
Only **genuine absence** now produces the install hint: a `MODULE_NOT_FOUND` whose message names the specifier `'sharp'` exactly. Matching on the code alone is not sufficient — a dependency missing _inside_ sharp raises the very same code (`Cannot find module 'color'`), and would have been reported as sharp itself being absent. Any other failure surfaces sharp's own message, inlined as `Failed to load "sharp": …` **and** chained as `cause`, so a terminal that never prints `cause` still shows the text that helps. The absent-sharp path is unchanged, wording included
|
|
870
|
+
|
|
871
|
+
**The failure reason is cached, not just the fact of failure.** The resolution attempt runs once per process; a second `new Image(...)` skips the load entirely, so caching only "there is no sharp function" would have re-told the same lie one call later. The guard therefore constructs **twice** in each spawned process and asserts the second error matches the first (`tests/unit/image/image-sharp-load-failure.test.ts`) — a one-shot test passes even with that bug present
|
|
872
|
+
|
|
873
|
+
The `MODULE_NOT_FOUND` shapes the guard feeds in are produced by asking Node for a module that genuinely is not installed, rather than hand-written, so the matcher is tested against Node's real message text
|
|
874
|
+
|
|
875
|
+
- **`renderReact()` no longer renders against modules that have not loaded yet.** The same defect as the two above, in a second module, found by looking for the pattern rather than by a bug report. `react/index.ts` fired `import("react")` and `import("react-dom/server")` at load time without awaiting either, and tracked them with a three-state flag that the guard only tested for one state: `if (moduleExists === false)`. While the imports were in flight the flag was `null`, which is not `false`, so the guard passed and the synchronous `renderReact` read `createElement` off `undefined`. With two sequential dynamic imports the window is wider than the image module's, and it is open during exactly the work a server does at boot — rendering a page or an email template from a module-level warm-up
|
|
876
|
+
|
|
877
|
+
The window swallowed the diagnostics as well as the render. A genuinely missing react did **not** produce the install hint during that window: it produced `TypeError: Cannot read properties of undefined (reading 'createElement')`, because the flag was still `null` rather than `false`. The install instructions only appeared for callers late enough to have missed the race — the callers who least needed telling
|
|
878
|
+
|
|
879
|
+
Both modules are now resolved **synchronously on the first `renderReact()` that needs them**, via `createRequire`, with the outcome cached for the process. Resolution stays **lazy**: importing `@warlock.js/core` still does not pull react into apps that never render. A genuinely absent react throws the same `react is not installed.` message with the same instructions, unchanged
|
|
880
|
+
|
|
881
|
+
- **A broken `react-dom/server` no longer reports itself as `react is not installed`.** The two packages were loaded in one `try` and collapsed into one flag, so any failure of either was attributed to react. The specifiers are now resolved and reported **separately**, and the message names the one that actually failed — `Failed to load "react-dom/server": …` — because sending an operator to reinstall react when react is fine costs them the debugging session. Absence is distinguished from breakage the same way as for sharp: `MODULE_NOT_FOUND` **and** a message naming the specifier exactly, quoted, which is also what stops `'react-dom'` from satisfying a check for `'react'`. Everything else surfaces the original error, inlined and chained as `cause`. A `react-dom` whose `./server` subpath is missing from `exports` raises `ERR_PACKAGE_PATH_NOT_EXPORTED`, so it correctly reports as an incompatible install rather than an absent one
|
|
882
|
+
|
|
883
|
+
As with sharp, the failure **reason** is cached rather than only the fact of failure, so the second call cannot fall through to the "not installed" branch and re-tell a lie the first call got right
|
|
884
|
+
|
|
885
|
+
The guard is a spawned fresh Node process (`tests/unit/react/react-module-resolution.test.ts`); a same-process test cannot catch this, which is why the pre-existing suite was green against it. Against the live bug all five cases failed, and — the detail that shows how much the race hid — all five failed with the _same_ `Cannot read properties of undefined` symptom, including the case that only asserts the install message. The present-but-broken states are staged by copying the module source next to a fixture `node_modules`, since resolution anchors to the importing file; the real workspace is not mutated
|
|
886
|
+
|
|
887
|
+
## 4.11.0
|
|
888
|
+
|
|
889
|
+
### Added
|
|
890
|
+
|
|
891
|
+
- `startHttpTestServer({ port })` — run an integration suite on an explicit port, honoured over `HTTP_PORT` in `.env`, which the internal bootstrap re-reads and no caller could previously override
|
|
892
|
+
- the test server preflights its port and fails with "stop the dev server" naming the port, instead of a raw `EADDRINUSE` from inside Fastify
|
|
893
|
+
- `Application.setServedPort()` and a `port` field on the readiness signal, so a supervisor learns the bound http port from the app rather than re-deriving it from config it may not be able to read
|
|
894
|
+
- `setConfig(name, value)` — the write side of the config store, exported separately from the read-only `config` accessor so registering configuration stays a deliberate boot-time act
|
|
895
|
+
|
|
896
|
+
### Changed
|
|
897
|
+
|
|
898
|
+
- `@mongez/dotenv` is now required at `^1.3.1` (was `^1.2.4`). Under the old range a fresh install resolved to 1.3.x while an existing lockfile could stay on 1.2.x, so we could not say which behaviour a given consumer actually had. 1.3.x only changes cases that were previously wrong: `env()` now consults `process.env` instead of returning a default for a key the environment defines, `${VAR}` interpolation throws naming the key instead of baking the string `"undefined"` into a value, and numeric coercion no longer corrupts values like `0123456789` or IDs beyond 2^53. Precedence between `.env` files and injected variables is unchanged
|
|
899
|
+
|
|
900
|
+
Core keeps its own guard for a missing `.env` regardless of which version resolves — an application can pin its own transitive dependencies, so the installed version is never something core can assume
|
|
901
|
+
|
|
902
|
+
### Deprecated
|
|
903
|
+
|
|
904
|
+
- the `env` preloader flag on a CLI command is no longer read — env is loaded for every command that declares a preload block. Setting it is harmless and does nothing; remove it. Dropped at 5.0
|
|
905
|
+
|
|
906
|
+
### Fixed
|
|
907
|
+
|
|
908
|
+
- **A production bundle no longer imports a package your app does not declare.** `warlock build`'s generated config loader emitted `import config from "@mongez/config"` — one of _core's_ dependencies, never the app's. npm and yarn hoist flat so it resolved by accident; under pnpm's strict layout the shipped bundle died at boot with `ERR_MODULE_NOT_FOUND` for a package the app had no reason to install. The generator now emits `setConfig` from `@warlock.js/core`, which the app does declare, and Node resolves `@mongez/config` from core's own install — correct under pnpm, and portable, unlike baking absolute paths into an artifact meant to be copied between machines
|
|
909
|
+
|
|
910
|
+
**The rule is now enforced, not just followed.** `warlock build` fails if any specifier written into generated code is missing from the app's `dependencies`, listing every violation at once. Rewriting the one bad import fixes today's bundle; the check is what stops the next change to the generator from reintroducing it invisibly — under npm and yarn the mistake never surfaces
|
|
911
|
+
|
|
912
|
+
Two scaffolding sites had the same defect and are fixed with it: `warlock generate.module` emitted `groupedTranslations` from `@mongez/localization`, and the communicators config stub emitted `env` from `@mongez/dotenv`. Both now come from `@warlock.js/core`, which already re-exports them
|
|
913
|
+
|
|
914
|
+
- **`env()` inside `warlock.config.ts` no longer always returns its default.** The config module was evaluated _before_ any `.env` file was read, so a project following the documented `build: { outdir: env("BUILD_OUT", "dist") }` recipe silently got `dist` no matter what the environment said — under every command, `dev` included, and under `build` and `start` env was never loaded at all. Env files are now loaded before `warlock.config.ts` is evaluated, for every command
|
|
915
|
+
|
|
916
|
+
Loading is guarded: a project with no `.env` is legitimate and must not start failing `warlock build` now that env loads everywhere. `NODE_ENV` remains authoritative for which file is chosen — no command forces the environment, so a deliberate `NODE_ENV=staging` build still reads `.env.staging`. With `NODE_ENV` unset, plain `.env` is read
|
|
917
|
+
|
|
918
|
+
- **An application without `src/config/storage.ts` can boot again.** The storage connector starts unconditionally, on the documented grounds that `storage.init()` falls back to a built-in `local` driver so file storage works out of the box. That fallback was never implemented: `init()` resolved the default driver _name_ and then found nothing registered under it, so any app without a storage config died at boot with `Storage driver "local" is not configured`. A built-in `local` driver rooted at `uploadsPath()` is now registered before configured drivers — so an app defining its own `local` still overrides it, and naming a driver that genuinely does not exist still fails loudly
|
|
919
|
+
|
|
920
|
+
Only scaffolded apps hid this, because `create-warlock` always ships a storage config
|
|
921
|
+
|
|
922
|
+
- `startHttpTestServer` no longer breaks a suite that configures `http.port: 0`. `0` is the OS's "pick a free port for me" idiom, but the guard only checked `typeof port !== "number"`, so `0` fell through: the preflight bound an unrelated ephemeral port and passed without proving anything, and `0` was then published as the bound port, pointing every request in the suite at `http://host:0`. An explicit `0` now takes the same path as no configured port — no preflight, nothing published, Fastify picks the port
|
|
923
|
+
|
|
924
|
+
- **`warlock start` no longer claims success before the app has booted.** The startup banner printed in `preAction` — before the child process was even spawned — and the failure that followed went only to stderr. Any CI gate or process supervisor that watches stdout for the banner read a 🔴 boot failure as a healthy start, which is how a production app that never booted was recorded as running. The banner now prints only when the application reports a completed boot, and a child that dies before reporting is a failed start: the message goes to **both** stdout and stderr, and the exit code is forced non-zero even when the process itself exited `0`
|
|
925
|
+
|
|
926
|
+
Readiness is signalled, never assumed. `Application.markBooted()` sends a versioned `warlock:ready` message — `{ type, version, pid, at, environment, runtimeStrategy, bootDurationMs?, port? }` — over the IPC channel `warlock start` opens, and closes that channel immediately so an open handle can't keep a wedged process looking alive. The signal is gated on a `WARLOCK_BOOT_SIGNAL` handshake the CLI sets on the child it spawns, so an app running under pm2 or any other supervisor never writes into a channel it does not own
|
|
927
|
+
|
|
928
|
+
A bundle built by an older Warlock has no readiness signal. It still starts normally and draws a note on **stderr only** telling you to re-run `warlock build` — an absent signal is never an error, never fails a run, and never kills a slow boot
|
|
929
|
+
|
|
930
|
+
**Upgrading:** re-run `warlock build` so the bundle can report readiness; until you do, `warlock start` runs your app but prints no started banner. Anything parsing `warlock start` output should note that progress lines now go to stderr — stdout carries the started banner and start failures, nothing else
|
|
931
|
+
|
|
932
|
+
## 4.10.0
|
|
933
|
+
|
|
934
|
+
### Changed
|
|
935
|
+
|
|
936
|
+
- **`response.cookie` is now secure by default** — `httpOnly: true`, `sameSite: "lax"`, and `secure: true` outside development are applied unless overridden. Previously nothing set them: a cookie was readable by any injected script, sent in cleartext, and attached to cross-site requests unless the developer knew to pass three flags on every call. Nothing failed when they were missing, so the app worked and was simply insecure. Precedence is framework defaults → `http.cookies.options` → the per-call argument, so opting out stays possible and explicit. `secure` is relaxed only in development, because browsers drop a `Secure` cookie over plain http
|
|
937
|
+
|
|
938
|
+
**Upgrading:** a cookie your client-side JavaScript reads now needs `{ httpOnly: false }` stated explicitly. Anywhere you wrote `secure: Application.isProduction` by hand can be deleted — the default already tracks the environment, and the hand-written form wrongly disables `secure` in test and staging
|
|
939
|
+
|
|
940
|
+
## 4.9.2
|
|
941
|
+
|
|
942
|
+
### Fixed
|
|
943
|
+
|
|
944
|
+
- the dev server's generated `.warlock/loader-hook.mjs` no longer ships bare `esbuild` / `get-tsconfig` imports. That file is written into the **consuming app's** directory, so a bare specifier resolves from the app — but both packages are core's own dependencies. npm and yarn hoist flat so it worked by accident; under pnpm's strict layout the dev server died with `ERR_MODULE_NOT_FOUND` for a package the app never imported. Each npm specifier is now rewritten at generation time to an absolute path resolved from core's own install, so no consumer has to declare a phantom dependency
|
|
945
|
+
|
|
946
|
+
## 4.9.0 - 2026-08-06
|
|
947
|
+
|
|
948
|
+
### Added
|
|
949
|
+
|
|
950
|
+
- `warlock dev` keyboard shortcuts — `r` restart, `c` clear, `q` quit, `h` help — armed once the server is ready and listed by `h`. TTY-gated, and `Ctrl+C` keeps working while raw mode is held
|
|
951
|
+
- press `u` on the `warlock dev` update notice to update every `@warlock.js/*` dependency, install, and restart the server in place — no Ctrl+C round-trip. Falls back to the printed `npx warlock update` command when the terminal can't deliver keypresses (CI, piped stdin, supervisors)
|
|
952
|
+
- `warlock dev` now runs as a supervised pair — a thin parent that owns the terminal and a disposable worker — so restarting replaces the worker instead of stacking a process per restart, and the supervisor never loads config or connectors
|
|
953
|
+
- `warlock dev` restarts automatically when `warlock.config.ts` or any `.env*` changes, since neither can be hot-reloaded; opt out with `devServer.restartOnConfigChange: false` for the previous warning
|
|
954
|
+
- Bun support in `warlock update` and `warlock add` — `bun.lock` / `bun.lockb` are detected and drive `bun install` / `bun add`
|
|
955
|
+
- `warlock dev` recovers from a crash: a worker that dies after running healthily for 5s is replaced automatically, while one that dies during boot is left alone so its error isn't buried under a reprint. Capped at 3 crashes per minute
|
|
956
|
+
- `warlock update --dry-run` reports what would change without touching anything, and `--check` does the same but exits `1` when a package is behind — a CI gate for staying current
|
|
957
|
+
|
|
958
|
+
### Changed
|
|
959
|
+
|
|
960
|
+
- the dev-server update check remembers npm's answer for 24h in `.warlock/update-check.json`, so a day of restarts costs one lookup instead of one per boot; failed lookups are never cached, and the entry is dropped once an update is applied
|
|
961
|
+
|
|
962
|
+
### Fixed
|
|
963
|
+
|
|
964
|
+
- `warlock start` spawns `process.execPath` instead of a bare `node`, which failed with `ENOENT` wherever `node` is not on `PATH` (systemd units, cron, slim containers) and could otherwise pick a different Node version than the one running the CLI
|
|
965
|
+
- `warlock add` no longer carries its own package-manager detection that silently produced an undefined install command when the project had no recognised lockfile — it shares the updater's detection
|
|
966
|
+
- `build.outDirectory` — the name the docs have used for several releases — is now actually read. Only `build.outdir` ever was, so a config written from the documentation was silently ignored and the bundle still went to `dist/`. Both names now work (`outdir` wins if you set both) and the docs lead with `outdir`
|
|
967
|
+
- `warlock update` no longer reports "All @warlock.js packages are already up to date" when it never reached the npm registry — an offline run now says so and changes nothing
|
|
968
|
+
- a failed package-manager install during `warlock update` no longer loses the rewritten `package.json`; the CLI still exits non-zero
|
|
969
|
+
- the dev server's update check now uses a 5s abort budget instead of 30s, so a hanging network can't leave a pending request behind a running server
|
|
970
|
+
|
|
971
|
+
## 4.8.1 - 2026-07-21
|
|
972
|
+
|
|
973
|
+
### Fixed
|
|
974
|
+
|
|
975
|
+
- `@warlock.js/ai`, `@warlock.js/access`, and `@warlock.js/notifications` declared as optional `peerDependencies` (matching the existing `@warlock.js/herald` pattern) so pkgist's bundler leaves them external instead of vendoring their source into core's own build — a vendored `@warlock.js/ai` copy was a disconnected module instance whose config listeners (e.g. `ai-panoptic`'s dashboard wiring) never received `ai.config(...)` calls routed through the real, separately-installed package
|
|
976
|
+
|
|
977
|
+
## 4.6.1
|
|
978
|
+
|
|
979
|
+
### Fixed
|
|
980
|
+
|
|
981
|
+
- a fatal `uncaughtException` at production boot (e.g. a config file that throws) is no longer swallowed into a silent `exit 0` — bootstrap now wires the crash handler to exit non-zero in production so `warlock start` surfaces the failure; the dev server still logs-and-continues for HMR
|
|
982
|
+
|
|
983
|
+
## 4.6.0
|
|
984
|
+
|
|
985
|
+
### Added
|
|
986
|
+
|
|
987
|
+
- release-hygiene tests: version↔changelog invariant + generator-stub import check
|
|
988
|
+
- `router.routeCount()` exposes the number of registered routes as a boot/readiness signal
|
|
989
|
+
- `health.addRoutesRegisteredCheck(getRouteCount)` registers a readiness check that reports not-ready when a booted HTTP app has zero routes
|
|
990
|
+
- seeders now receive a `{ track }` context — `track(model)`, `track(models[])`, and `track(table, id)` register created records (each call returns its argument so it can be chained inline); `recordsCreated` is auto-derived from the track count
|
|
991
|
+
- `seed_records` table (created via the new `SeedRecordsTableMigration`) records every tracked seed reference within the same transaction the seed runs in; only the last run's refs are kept per seeder
|
|
992
|
+
- `warlock seed --drop [name]` undoes a seed: deletes its tracked records in reverse run/insertion order inside a transaction, then resets the matching seeds-log rows so `once: true` seeds re-run; scope to one seeder with `--drop=<name>`
|
|
993
|
+
- `Seeder.dependsOn` is now resolved — seeders are topologically sorted so dependencies run before dependents, layered over the numeric `order` tie-break; throws `UnknownSeederDependencyError` for a missing dependency and `SeederDependencyCycleError` for a cycle
|
|
994
|
+
- seeders receive an injectable clock and a meaningful batch size — `run({ track, now, batchSize })`; `now()` (default `() => new Date()`) drives both seed data and the seeds-log timestamps so historical/back-fill runs are deterministic, and `batchSize` surfaces the seeder's own `batchSize` for `Model.createMany(rows, { batchSize })`
|
|
995
|
+
- repository-level aggregation — `aggregate()`, `sum()`, `avg()`, `min()`, `max()`, and `groupBy()` on `RepositoryManager`, each reusing `filterBy` (and its operator-injection guard), `where`, and scopes before the aggregate, exactly like `count()`
|
|
996
|
+
- `warlock doctor` — a read-only diagnostics command that runs routes / config / connectors / optional-peers / health / release-hygiene checks and prints a pass/warn/fail report (exits non-zero on any failure, never opens a DB/cache/socket connection)
|
|
997
|
+
- `warlock routes` — a read-only command that lists the registered HTTP routes as a verb-colored table (method / path / name / action / middleware-count / source); filter with `--method` / `--path` / `--name`, or emit the normalized rows as JSON with `--json`. Boots app code to register routes but starts no connectors
|
|
998
|
+
|
|
999
|
+
### Changed
|
|
1000
|
+
|
|
1001
|
+
- `Seeder.run` now receives a `SeedContext` (`run(ctx)`) — backward compatible, an existing zero-arg `run()` keeps working unchanged
|
|
1002
|
+
|
|
1003
|
+
### Fixed
|
|
1004
|
+
|
|
1005
|
+
- route-module load/registration failures are no longer swallowed: a route file that throws on import or registration now surfaces loudly instead of silently 404'ing the whole surface
|
|
1006
|
+
- `ModuleLoader.loadModule` rethrows after logging (wrapped in a new `ModuleLoadError` carrying the failing file + cause), so a broken module aborts boot and is caught loudly by the HMR batch-reload handler in dev
|
|
1007
|
+
- `ModuleLoader.loadAll` aggregates per-file failures and throws an `AggregateError` at the end, so one broken module no longer hides the others
|
|
1008
|
+
- `router.withSourceFile` rethrows the callback error after logging instead of consuming it with a bare `console.log` (the `try/finally` source-file stack cleanup is preserved)
|
|
1009
|
+
|
|
1010
|
+
## 4.5.0
|
|
1011
|
+
|
|
1012
|
+
### Changed
|
|
1013
|
+
|
|
1014
|
+
- dev-server update notice now fires immediately on `warlock dev` — the check is spawned in parallel with server startup instead of awaiting it, so the notice surfaces as soon as npm responds
|
|
1015
|
+
- raised the update check's npm registry timeout from 2.5s to 30s, so a slow connection no longer drops the notice
|
|
1016
|
+
- repository lifecycle hooks (`onCreating` / `onCreate` / `onUpdating` / `onSaving` / `onDeleting` / …) now run on `create` / `update` / `delete` — they were defined but never invoked
|
|
1017
|
+
- `repository.list()` / `all()` now honor the `sortBy`, `sortDirection`, and `purgeCache` options — previously accepted but silently ignored
|
|
1018
|
+
|
|
1019
|
+
### Removed
|
|
1020
|
+
|
|
1021
|
+
- presigned-upload `maxSize` option — a presigned PUT URL cannot enforce a size cap, so the option was a false guarantee
|
|
1022
|
+
|
|
1023
|
+
### Fixed
|
|
1024
|
+
|
|
1025
|
+
- `response.sendFile({ filename })` and `response.download()` no longer 500 on non-ASCII file names — the `Content-Disposition` header is now RFC 6266-encoded (a sanitized ASCII `filename` fallback plus an RFC 5987 `filename*=UTF-8''…`), so an Arabic / emoji / UTF-8 download name streams correctly instead of throwing Node's `ERR_INVALID_CHAR`
|
|
1026
|
+
- local storage paths are contained to their disk root — `../` traversal segments and absolute paths can no longer escape the configured directory
|
|
1027
|
+
- `storage.putFromUrl` adds SSRF guards — private / loopback / link-local hosts are rejected and the fetched body is size-capped
|
|
1028
|
+
- S3 / R2 / DigitalOcean Spaces `url()` no longer produces a malformed double-host URL when `urlPrefix` is set
|
|
1029
|
+
- cloud `deleteDirectory` paginates via the list continuation cursor instead of re-listing the first page
|
|
1030
|
+
- local-storage metadata cache is invalidated on write / delete — it was serving a stale size / modified-time
|
|
1031
|
+
- the cloud driver no longer reports a misleading "SDK not installed" error when the AWS SDK is in fact present (driver load race)
|
|
1032
|
+
- repository `countCached` / `countActiveCached` now cache and return correctly — a `null` cache miss was being returned as the count
|
|
1033
|
+
- repository `firstCached` / `lastCached` no longer fetch and cache the entire table to return a single row
|
|
1034
|
+
- repository boolean filters no longer coerce `false` / `0` to `true`
|
|
1035
|
+
- repository cache keys are now order-independent (stable key serialization)
|
|
1036
|
+
- router groups restore prefix / name / middleware state via `try/finally` even when the group callback throws
|
|
1037
|
+
- `router.any()` / `all` routes now match every HTTP verb under the dev server — they previously matched only GET and POST, diverging from production
|
|
1038
|
+
- the HTTP concurrency limiter releases its slot on every response path (`noContent`, redirect, file, buffer) — a throwing or non-`send` handler no longer leaks a permit and permanently 429s the route
|
|
1039
|
+
- the cached-response middleware replays a hit through `response.replay()` instead of re-sending an already-sent reply, preserving status and content-type
|
|
1040
|
+
- `onSent` cache writes in the idempotency and cache middleware are error-handled — a cache-backend failure no longer surfaces as an unhandled rejection
|
|
1041
|
+
- `X-Forwarded-For` is parsed to its first hop, so IP-filter / rate-limit / idempotency scoping cannot be spoofed with extra header hops
|
|
1042
|
+
- the `maintenance` middleware allowlist matches request paths that carry a query string
|
|
1043
|
+
- use-cases run their `after` middleware and broadcast for a `void` handler, and a failed history write no longer fails an otherwise-successful call
|
|
1044
|
+
- the use-case retry counter reports the correct count on total failure
|
|
1045
|
+
- the socket connector no longer double-closes the shared HTTP server during graceful shutdown
|
|
1046
|
+
- the cache connector disconnects its drivers on shutdown — an open Redis connection was left dangling
|
|
1047
|
+
- generator stubs import `v` / `Infer` from `@warlock.js/seal` (core never re-exported them), so generated models compile and run
|
|
1048
|
+
- `warlock dev` hot-reloads when a file is emptied or saved with no trailing newline — a stale no-op-change check was silently dropping those saves before they reached HMR
|
|
1049
|
+
|
|
1050
|
+
## 4.4.0 - 2026-06-21
|
|
1051
|
+
|
|
1052
|
+
### Added
|
|
1053
|
+
|
|
1054
|
+
- `Application.onceBooted(cb)` — run a callback once the app is fully booted (fires immediately if already booted)
|
|
1055
|
+
- `Application.whenBooted()` — promise that resolves with the boot context when the app is fully booted
|
|
1056
|
+
- `Application.isBooted` — whether the app has finished booting
|
|
1057
|
+
- `Application.onShutdown(cb)` — run teardown once on shutdown, before connectors stop (mirror of `onceBooted`)
|
|
1058
|
+
- `Application.isShuttingDown` — whether shutdown has begun
|
|
1059
|
+
- built-in `/health` (liveness) and `/ready` (readiness) endpoints with a `health` check registry (`health.addCheck`)
|
|
1060
|
+
- graceful HTTP shutdown — drains in-flight requests on shutdown, bounded by `http.gracefulShutdown.timeout`
|
|
1061
|
+
- `http.health.*` config to toggle or rename the health endpoints
|
|
1062
|
+
|
|
1063
|
+
### Fixed
|
|
1064
|
+
|
|
1065
|
+
- connector shutdown no longer reverses the connector list in place (could corrupt order on a repeated shutdown)
|
|
1066
|
+
|
|
1067
|
+
## 4.3.0 - 2026-06-21
|
|
1068
|
+
|
|
1069
|
+
### Added
|
|
1070
|
+
|
|
1071
|
+
- `warlock update` — update every `@warlock.js/*` package in package.json to its latest version (operator preserved), then run the detected package manager's install
|
|
1072
|
+
- dev-server update notice — `warlock dev` checks npm on start and prints a one-line notice when a newer `@warlock.js/core` is published
|
|
1073
|
+
- `devServer.checkForUpdates` config flag (default `true`) to toggle the dev-server update notice
|
|
1074
|
+
- `fetchLatestVersion()` and `isNewerVersion()` registry/version utilities
|
|
1075
|
+
|
|
1076
|
+
### Fixed
|
|
1077
|
+
|
|
1078
|
+
- `warlock dev --skip-typings` and `--skip-health` long-form flags now work (were silently ignored)
|
|
1079
|
+
|
|
1080
|
+
## 4.2.11
|
|
1081
|
+
|
|
1082
|
+
### Added
|
|
1083
|
+
|
|
1084
|
+
- `lowerStage3Decorators()` — Vite/Vitest plugin that lowers TC39 Stage-3 decorators with esbuild before oxc / the SSR rewrite mangles them; drop it first in `plugins` so model-decorated files load under Vitest 4 / Vite 8.
|
|
1085
|
+
- `warlock add notifications` — installs `@warlock.js/notifications` (+ the `mail` feature), ejects `config/notifications.ts`, and scaffolds the app-owned `Notification` model + migration (idempotent).
|
|
1086
|
+
- Notifications connector — a built-in, config-gated connector that lazy-imports `@warlock.js/notifications`, so core keeps no hard dependency on it.
|
|
1087
|
+
|
|
1088
|
+
### Changed
|
|
1089
|
+
|
|
1090
|
+
- `warlock add test` now scaffolds a `vite.config.ts` that includes `lowerStage3Decorators()`, so a fresh project can test decorated models out of the box.
|
|
1091
|
+
- `warlock add test` `test` / `test:coverage` scripts now run one-shot (`vitest run`) instead of watch mode — CI-safe by default.
|
|
1092
|
+
- Bumped `@mongez/reinforcements` to 3.3.0
|
|
1093
|
+
|
|
1094
|
+
### Fixed
|
|
1095
|
+
|
|
1096
|
+
- `startHttpTestServer` now starts early-phase connectors (database, cache, logger, …) before app modules, then late-phase (http, socket) after — mirroring dev/prod boot order; fixes a `MissingDataSourceError` under the Vitest integration harness.
|
|
1097
|
+
|
|
1098
|
+
## 4.2.5
|
|
1099
|
+
|
|
1100
|
+
### Added
|
|
1101
|
+
|
|
1102
|
+
- `warlock add notifications` now scaffolds the in-app read/dismiss HTTP surface — `routes.ts` + a `notifications.controller.ts` (list / unread-count / mark-read / mark-all-read / clear / delete), gated by `authMiddleware` and recipient-scoped via `inApp`. Pulls `@warlock.js/auth`.
|
|
1103
|
+
|
|
1104
|
+
## 4.2.4
|
|
1105
|
+
|
|
1106
|
+
### Fixed
|
|
1107
|
+
|
|
1108
|
+
- Fix the worker-loader path in the build entry points — a wrong path in 4.2.3 left the worker entry broken (and blocked the 4.2.3 publish for some packages).
|
|
1109
|
+
|
|
1110
|
+
## 4.2.3
|
|
1111
|
+
|
|
1112
|
+
### Fixed
|
|
1113
|
+
|
|
1114
|
+
- Add the worker scripts as build entry points so they ship in the published package.
|
|
1115
|
+
|
|
1116
|
+
## 4.2.2
|
|
1117
|
+
|
|
1118
|
+
### Fixed
|
|
1119
|
+
|
|
1120
|
+
- Add `cli/start` to the build entry points so the `warlock` CLI entry ships in the published package.
|
|
1121
|
+
|
|
1122
|
+
## 4.2.1
|
|
1123
|
+
|
|
1124
|
+
### Fixed
|
|
1125
|
+
|
|
1126
|
+
- Ship the `bin` folder so the `warlock` CLI works from the published package — it was omitted from the 4.2.0 build.
|
|
1127
|
+
|
|
1128
|
+
## 4.2.0
|
|
1129
|
+
|
|
1130
|
+
### Changed
|
|
1131
|
+
|
|
1132
|
+
- `herald-connector` and `http-connector` now log a failed boot-time connection at `log.fatal` (was `log.error`) — an unrecoverable broker connection or HTTP port-bind failure makes "page on fatal only" alerting clean; the HTTP connector flushes logs before `process.exit(1)`. Disconnect / shutdown failures stay at `error`.
|
|
1133
|
+
|
|
1134
|
+
## 4.1.15
|
|
1135
|
+
|
|
1136
|
+
- Baseline — per-package changelog tracking starts at this version.
|