@warlock.js/cascade 4.15.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/cjs/index.cjs +494 -72
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/contracts/query-builder.contract.d.mts +7 -3
  5. package/esm/contracts/query-builder.contract.d.mts.map +1 -1
  6. package/esm/drivers/mongodb/mongodb-query-builder.d.mts.map +1 -1
  7. package/esm/drivers/mongodb/mongodb-query-builder.mjs +4 -3
  8. package/esm/drivers/mongodb/mongodb-query-builder.mjs.map +1 -1
  9. package/esm/drivers/mongodb/mongodb-query-parser.d.mts +21 -0
  10. package/esm/drivers/mongodb/mongodb-query-parser.d.mts.map +1 -1
  11. package/esm/drivers/mongodb/mongodb-query-parser.mjs +55 -22
  12. package/esm/drivers/mongodb/mongodb-query-parser.mjs.map +1 -1
  13. package/esm/errors/unsafe-filter.error.d.mts +33 -0
  14. package/esm/errors/unsafe-filter.error.d.mts.map +1 -0
  15. package/esm/errors/unsafe-filter.error.mjs +40 -0
  16. package/esm/errors/unsafe-filter.error.mjs.map +1 -0
  17. package/esm/errors/unsafe-raw-expression.error.d.mts +23 -0
  18. package/esm/errors/unsafe-raw-expression.error.d.mts.map +1 -0
  19. package/esm/errors/unsafe-raw-expression.error.mjs +28 -0
  20. package/esm/errors/unsafe-raw-expression.error.mjs.map +1 -0
  21. package/esm/index.d.mts +5 -1
  22. package/esm/index.mjs +5 -1
  23. package/esm/model/methods/accessor-methods.mjs +39 -1
  24. package/esm/model/methods/accessor-methods.mjs.map +1 -1
  25. package/esm/model/methods/delete-methods.mjs +3 -2
  26. package/esm/model/methods/delete-methods.mjs.map +1 -1
  27. package/esm/model/methods/query-methods.mjs +7 -4
  28. package/esm/model/methods/query-methods.mjs.map +1 -1
  29. package/esm/model/methods/serialization-methods.mjs +49 -4
  30. package/esm/model/methods/serialization-methods.mjs.map +1 -1
  31. package/esm/model/methods/write-methods.d.mts.map +1 -1
  32. package/esm/model/methods/write-methods.mjs +2 -4
  33. package/esm/model/methods/write-methods.mjs.map +1 -1
  34. package/esm/model/model.d.mts +48 -2
  35. package/esm/model/model.d.mts.map +1 -1
  36. package/esm/model/model.mjs +61 -2
  37. package/esm/model/model.mjs.map +1 -1
  38. package/esm/model/model.types.d.mts +1 -1
  39. package/esm/query-builder/query-builder.d.mts +13 -1
  40. package/esm/query-builder/query-builder.d.mts.map +1 -1
  41. package/esm/query-builder/query-builder.mjs +28 -11
  42. package/esm/query-builder/query-builder.mjs.map +1 -1
  43. package/esm/remover/database-remover.d.mts.map +1 -1
  44. package/esm/remover/database-remover.mjs +1 -1
  45. package/esm/remover/database-remover.mjs.map +1 -1
  46. package/esm/utils/database-writer.utils.d.mts +1 -0
  47. package/esm/utils/database-writer.utils.d.mts.map +1 -1
  48. package/esm/utils/escape-regex.d.mts +67 -0
  49. package/esm/utils/escape-regex.d.mts.map +1 -0
  50. package/esm/utils/escape-regex.mjs +76 -0
  51. package/esm/utils/escape-regex.mjs.map +1 -0
  52. package/esm/utils/sanitize-filter.d.mts +26 -0
  53. package/esm/utils/sanitize-filter.d.mts.map +1 -0
  54. package/esm/utils/sanitize-filter.mjs +76 -0
  55. package/esm/utils/sanitize-filter.mjs.map +1 -0
  56. package/esm/writer/database-writer.d.mts +12 -0
  57. package/esm/writer/database-writer.d.mts.map +1 -1
  58. package/esm/writer/database-writer.mjs +26 -6
  59. package/esm/writer/database-writer.mjs.map +1 -1
  60. package/llms-full.txt +59 -4
  61. package/llms.txt +3 -3
  62. package/package.json +8 -8
  63. package/skills/README.md +3 -3
  64. package/skills/define-model/SKILL.md +17 -1
  65. package/skills/perform-atomic-ops/SKILL.md +4 -1
  66. package/skills/query-data/SKILL.md +38 -2
package/CHANGELOG.md CHANGED
@@ -4,6 +4,25 @@ All notable changes to `@warlock.js/cascade` are documented in this file.
4
4
 
5
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
6
 
7
+ ## 5.0.0 - 2026-08-25
8
+
9
+ ### Changed
10
+
11
+ - This package is unchanged in 5.0.0; its version moved only because the Warlock family releases in lockstep.
12
+
13
+ ## 4.16.0 - 2026-08-18
14
+
15
+ ### Security
16
+
17
+ - **NoSQL operator injection via equality filters is now rejected.** `where({ field: value })`, `where(field, value)` and the filter-accepting model statics (`first`, `findFirst`, `findAll`, `count`, `paginate`, `deleteMany`, `deleteOne`, …) treated the value verbatim, so a request-controlled payload such as `{ password: { $ne: null } }` compiled into a MongoDB *operator* query instead of an equality match — the textbook auth-bypass primitive (`User.first({ email, password })` matched any user). Equality-position values (and top-level object-form keys) containing `$`-prefixed keys now throw `UnsafeFilterError`. Explicit operator APIs are unaffected: `where(field, operator, value)`, `whereIn`/`whereNull`/`whereBetween`/…, and the object form of `whereRaw`. Dotted paths (`"profile.name"`) and plain sub-document equality values remain valid. A `sanitizeFilter` / `sanitizeFilterValue` helper pair is exported for callers who forward request objects to other driver-level APIs
18
+ - **String-mode `whereRaw()` / `orWhereRaw()` no longer compiles to `$where` on the MongoDB driver.** Any string expression was wrapped as `{ $where: "<js>" }` — JavaScript executed *inside* `mongod` for every scanned document (an injection sink whenever any part of the string was request-influenced, and an unindexed full-scan DoS even when trusted), with `?`-bindings substituted by string concatenation rather than real parameterization. The MongoDB parser now throws `UnsafeRawExpressionError` for string expressions and directs callers to the object form (`whereRaw({ $expr: … })`), which keeps working. SQL drivers keep string mode with real bindings
19
+ - **`static hidden` — fields `toJSON()` can never emit.** With no `resource`/`toJsonColumns` configured (the quick-start model shape), `toJSON()` — invoked implicitly by `JSON.stringify(model)` / `res.json(model)` — returned the entire raw document, password hashes and tokens included. Models can now declare `static hidden = ["password", …]`; those top-level fields are ALWAYS stripped from `toJSON()` output — with the raw-document default, with `toJsonColumns` (hidden wins), and from the data handed to a `resource` class. Defaults to `[]`, so nothing changes until a model declares hidden fields — but because that default still fails open, cascade now logs a one-time `console.warn` per model whose schema declares a credential-shaped field (`password`/`passwordHash`/`secret`/`token`/`apiKey`/`api_key`, case-insensitive) that no `hidden`/`resource`/`toJsonColumns` covers
20
+ - **Atomic/find-and-modify statics now sanitize their filter.** `atomic()`, `findAndUpdate()`, `findOneAndUpdate()`, `findAndReplace()` and `findOneAndDelete()` forwarded their `filter` object straight to the driver — bypassing `where()` and therefore the operator-injection check above, so `{ role: { $ne: "admin" } }` from a request body was still a live operator query on these paths. The filter argument now runs through `sanitizeFilter` and throws `UnsafeFilterError` on `$`-prefixed keys. Update-operator semantics (`$set`/`$inc`/`$unset`/…) are untouched — only the FILTER is checked. Callers who legitimately need operator conditions must express them through the query API (`Model.query().where(…)`) instead of the raw filter argument
21
+ - **Residual injection paths closed.** The three-argument equality form `where(field, "=", value)` now sanitizes its value like the two-argument form (other operators are unaffected), and the object form of `whereRaw()`/`orWhereRaw()` rejects the server-side JavaScript operators `$where`, `$function` and `$accumulator` anywhere in the expression (throws `UnsafeRawExpressionError`); `$expr` and the other aggregation operators keep working
22
+ - **A `merge()`d primary key can no longer retarget a write at another document.** `performUpdate()` built its filter from `model.get(primaryKey)` *after* `merge()` had run, so the canonical update-my-profile shape — `model.merge(req.body); await model.save()` — let a body carrying `{ id: "<victim-id>", role: "admin" }` redirect the UPDATE (and the mass-assigned fields with it) onto somebody else's row. Two independent controls now stand between a payload and the write target: (1) an instance captures its primary key at the moment it becomes persisted (`isNew` flipping to `false` — hydration, or the writer after an insert), and `update`/`replace`/`destroy` build their filter from that captured value, exposed as `model.trustedPrimaryKey`; (2) `merge()` on an already-persisted model drops the identity columns (`id`, `_id`, and the configured primary key) instead of applying them — which also covers `save({ merge })`. Identity columns are additionally excluded from the update's `$set`/`$unset`, so an explicit `set("id", …)` on a loaded record no longer rewrites the key of the row it is pinned to (`_id` was never writable in MongoDB anyway); changing a primary key is now a deliberate operation through the atomic/raw APIs. **Creating** a record with an explicit id is unchanged — a new model accepts identity columns — and the writer still merges driver-returned values (generated `_id`, `RETURNING *`) back onto the instance through a framework-internal path that request data never reaches
23
+ - **`whereLike` / `whereSearch` string arguments are matched literally instead of compiled as regexes.** `whereLike`, `whereNotLike`, `whereStartsWith`/`whereEndsWith` (and their `Not` variants) and the `$regex` form of `whereSearch` interpolated their argument straight into a MongoDB `$regex`. Wired to a search box — `User.query().whereSearch("name", req.query.q)`, the intended use — that handed the caller the regex engine running inside `mongod`: metacharacters rewrote the match semantics (`^.*$` matches everything, `^a`/`^b` probes read a value back character by character), and a nested-quantifier pattern such as `(a+)+$` backtracked catastrophically against every scanned document. String arguments are now escaped and treated as literals; the SQL `LIKE` wildcard `%` still expands (to `.*`, with runs of `%` collapsed) and matching stays unanchored/substring as this driver documents. An explicit `RegExp` argument — which cannot arrive as JSON — is still used as a pattern, so raw regex remains available to developer-authored queries; never build that `RegExp` from user input. `escapeRegex` / `likePatternToRegexSource` are exported for callers compiling their own patterns. The Postgres path was already parameterized (`ILIKE $1`) and is unchanged
24
+ - **`@mongez/*` dependencies bumped to the 2026-08-17 security release**: `@mongez/reinforcements` `^3.3.0` → `^4.0.1`, `@mongez/dotenv` `^1.3.1` → `^1.3.2`, `@mongez/events` `^2.2.6` → `^2.2.7`, `@mongez/supportive-is` `^2.1.3` → `^2.1.4`. Reinforcements 4 adds a prototype-pollution guard to `set`/`merge`/`pick`/`defaults` — the utilities behind `model.set()` / `model.merge()`, i.e. exactly where request-shaped data enters a model — and a ReDoS fix in `repeatsOf`. Its breaking change (`Random` is CSPRNG-backed and no longer honors `Random.seed()`) does not affect cascade: the package imports no `Random` API and seeds nothing. Requires Node 20+, which cascade already targets
25
+
7
26
  ## 4.12.0
8
27
 
9
28
  ### Added