@cullet/erp-core 1.4.0 → 1.5.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 (79) hide show
  1. package/KIT_CONTEXT.md +6 -0
  2. package/dist/app-error.cjs +14 -6
  3. package/dist/app-error.cjs.map +1 -1
  4. package/dist/app-error.js +14 -6
  5. package/dist/app-error.js.map +1 -1
  6. package/dist/condition-evaluator.cjs +14 -0
  7. package/dist/condition-evaluator.cjs.map +1 -1
  8. package/dist/condition-evaluator.js +14 -0
  9. package/dist/condition-evaluator.js.map +1 -1
  10. package/dist/entity.cjs +2 -6
  11. package/dist/entity.cjs.map +1 -1
  12. package/dist/entity.js +2 -6
  13. package/dist/entity.js.map +1 -1
  14. package/dist/errors/index.cjs +2 -1
  15. package/dist/errors/index.js +2 -1
  16. package/dist/hashing.cjs +2 -44
  17. package/dist/hashing.cjs.map +1 -1
  18. package/dist/hashing.js +2 -38
  19. package/dist/hashing.js.map +1 -1
  20. package/dist/index.cjs +3 -2
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +2 -2
  23. package/dist/index.d.ts +2 -2
  24. package/dist/index.js +3 -2
  25. package/dist/index.js.map +1 -1
  26. package/dist/policies/engines/v1/gate/index.d.cts +2 -0
  27. package/dist/policies/engines/v1/gate/index.d.ts +2 -0
  28. package/dist/policy-service.cjs +2 -1
  29. package/dist/policy-service.cjs.map +1 -1
  30. package/dist/policy-service.js +2 -1
  31. package/dist/policy-service.js.map +1 -1
  32. package/dist/requested-by.cjs +3 -3
  33. package/dist/requested-by.cjs.map +1 -1
  34. package/dist/requested-by.js +1 -1
  35. package/dist/requested-by.js.map +1 -1
  36. package/dist/stable-stringify.cjs +47 -0
  37. package/dist/stable-stringify.cjs.map +1 -0
  38. package/dist/stable-stringify.js +42 -0
  39. package/dist/stable-stringify.js.map +1 -0
  40. package/dist/temporal-use-case.cjs +5 -0
  41. package/dist/temporal-use-case.cjs.map +1 -1
  42. package/dist/temporal-use-case.d.cts +6 -1
  43. package/dist/temporal-use-case.d.ts +6 -1
  44. package/dist/temporal-use-case.js +5 -0
  45. package/dist/temporal-use-case.js.map +1 -1
  46. package/dist/use-case.cjs +2 -6
  47. package/dist/use-case.cjs.map +1 -1
  48. package/dist/use-case.js +2 -6
  49. package/dist/use-case.js.map +1 -1
  50. package/dist/uuid-identifier.cjs +2 -7
  51. package/dist/uuid-identifier.cjs.map +1 -1
  52. package/dist/uuid-identifier.js +1 -6
  53. package/dist/uuid-identifier.js.map +1 -1
  54. package/dist/uuid.cjs +18 -0
  55. package/dist/uuid.cjs.map +1 -0
  56. package/dist/uuid.js +13 -0
  57. package/dist/uuid.js.map +1 -0
  58. package/dist/validation-error.d.cts +10 -5
  59. package/dist/validation-error.d.ts +10 -5
  60. package/dist/value-object.cjs +12 -3
  61. package/dist/value-object.cjs.map +1 -1
  62. package/dist/value-object.d.cts +9 -1
  63. package/dist/value-object.d.ts +9 -1
  64. package/dist/value-object.js +12 -3
  65. package/dist/value-object.js.map +1 -1
  66. package/meta.json +4 -2
  67. package/package.json +1 -1
  68. package/src/core/application/commands/requested-by.ts +3 -4
  69. package/src/core/application/queries/query.ts +6 -1
  70. package/src/core/application/use-case.ts +1 -1
  71. package/src/core/domain/entity.ts +1 -1
  72. package/src/core/domain/uuid-identifier.ts +1 -8
  73. package/src/core/domain/value-object.ts +12 -3
  74. package/src/core/errors/utils/json-safe.ts +20 -11
  75. package/src/core/policies/engines/v1/condition-evaluator.ts +56 -1
  76. package/src/core/shared/hashing.ts +1 -55
  77. package/src/core/shared/stable-stringify.ts +57 -0
  78. package/src/core/shared/uuid.ts +11 -0
  79. package/src/version.ts +1 -1
@@ -54,11 +54,14 @@ declare function serializationErrorCode(direction: SerializationCodeDirection, c
54
54
  /**
55
55
  * Ensures metadata is JSON-serializable.
56
56
  *
57
- * **Allowed:** string, number, boolean, null, arrays, and plain objects.
57
+ * **Allowed:** finite number, string, boolean, null, arrays, and plain objects.
58
+ *
59
+ * **Dropped (mirroring `JSON.stringify`):** object properties whose value is
60
+ * `undefined` (the key is omitted); an `undefined` array item becomes `null`.
58
61
  *
59
62
  * **Converted to placeholder:** Date, BigInt, class instances, functions,
60
- * symbols, undefined, and circular references (which would otherwise make
61
- * `JSON.stringify` throw).
63
+ * symbols, non-finite numbers (`NaN`/`Infinity`), and circular references
64
+ * (which would otherwise make `JSON.stringify` throw).
62
65
  *
63
66
  * @throws {TypeError} If `input` is not a plain object at the root level.
64
67
  */
@@ -266,8 +269,7 @@ declare function translateUniqueViolationToDuplicate(input: {
266
269
  cause?: unknown;
267
270
  }): DuplicateError;
268
271
  //#endregion
269
- //#region src/core/shared/hashing.d.ts
270
- declare function sha256Hex(value: string): string;
272
+ //#region src/core/shared/stable-stringify.d.ts
271
273
  /**
272
274
  * Deterministic JSON string with object keys sorted recursively, so key order
273
275
  * does not affect the output.
@@ -282,6 +284,9 @@ declare function sha256Hex(value: string): string;
282
284
  * @throws {TypeError} On circular references (mirroring `JSON.stringify`).
283
285
  */
284
286
  declare function stableStringify(value: unknown): string;
287
+ //#endregion
288
+ //#region src/core/shared/hashing.d.ts
289
+ declare function sha256Hex(value: string): string;
285
290
  declare function payloadHash(value: unknown): string;
286
291
  //#endregion
287
292
  //#region src/core/errors/idempotency-error.d.ts
@@ -54,11 +54,14 @@ declare function serializationErrorCode(direction: SerializationCodeDirection, c
54
54
  /**
55
55
  * Ensures metadata is JSON-serializable.
56
56
  *
57
- * **Allowed:** string, number, boolean, null, arrays, and plain objects.
57
+ * **Allowed:** finite number, string, boolean, null, arrays, and plain objects.
58
+ *
59
+ * **Dropped (mirroring `JSON.stringify`):** object properties whose value is
60
+ * `undefined` (the key is omitted); an `undefined` array item becomes `null`.
58
61
  *
59
62
  * **Converted to placeholder:** Date, BigInt, class instances, functions,
60
- * symbols, undefined, and circular references (which would otherwise make
61
- * `JSON.stringify` throw).
63
+ * symbols, non-finite numbers (`NaN`/`Infinity`), and circular references
64
+ * (which would otherwise make `JSON.stringify` throw).
62
65
  *
63
66
  * @throws {TypeError} If `input` is not a plain object at the root level.
64
67
  */
@@ -266,8 +269,7 @@ declare function translateUniqueViolationToDuplicate(input: {
266
269
  cause?: unknown;
267
270
  }): DuplicateError;
268
271
  //#endregion
269
- //#region src/core/shared/hashing.d.ts
270
- declare function sha256Hex(value: string): string;
272
+ //#region src/core/shared/stable-stringify.d.ts
271
273
  /**
272
274
  * Deterministic JSON string with object keys sorted recursively, so key order
273
275
  * does not affect the output.
@@ -282,6 +284,9 @@ declare function sha256Hex(value: string): string;
282
284
  * @throws {TypeError} On circular references (mirroring `JSON.stringify`).
283
285
  */
284
286
  declare function stableStringify(value: unknown): string;
287
+ //#endregion
288
+ //#region src/core/shared/hashing.d.ts
289
+ declare function sha256Hex(value: string): string;
285
290
  declare function payloadHash(value: unknown): string;
286
291
  //#endregion
287
292
  //#region src/core/errors/idempotency-error.d.ts
@@ -1,3 +1,4 @@
1
+ const require_stable_stringify = require("./stable-stringify.cjs");
1
2
  const require_immutable = require("./immutable.cjs");
2
3
  const require_plugin = require("./plugin.cjs");
3
4
  //#region src/core/domain/value-object.ts
@@ -14,6 +15,12 @@ var _ValueObject;
14
15
  * any risk of a consumer mutating shared state. Subclasses seal the instance
15
16
  * itself with {@link finalize} once their own fields are set.
16
17
  *
18
+ * One exception: nested `Date` instances are cloned but NOT frozen —
19
+ * `Object.freeze` does not block `setTime`/`setFullYear`, so a consumer of
20
+ * `value` can still mutate an inner `Date` in place. Store instants as ISO
21
+ * strings or epoch timestamps in the wrapped state (converting to `Date` only
22
+ * in an accessor) when that guarantee matters.
23
+ *
17
24
  * @typeParam T - The shape of the wrapped data.
18
25
  * @typeParam P - The primitive form produced by {@link toPrimitive} / {@link toJSON}.
19
26
  */
@@ -37,7 +44,7 @@ let ValueObject = class ValueObject {
37
44
  * `@version` decorator — used to detect state persisted under an older shape.
38
45
  */
39
46
  get contractVersion() {
40
- return _ValueObject.CONTRACT_VERSION;
47
+ return this.constructor.CONTRACT_VERSION;
41
48
  }
42
49
  /**
43
50
  * Freezes the instance shell, blocking reassignment of any own field.
@@ -66,11 +73,13 @@ let ValueObject = class ValueObject {
66
73
  * same data are considered equal.
67
74
  *
68
75
  * Delegates to the registered equality {@link plugins}; with no plugin
69
- * registered it falls back to comparing the serialized wrapped `value`.
76
+ * registered it falls back to comparing the serialized wrapped `value`,
77
+ * with object keys sorted so insertion order (e.g. code-built vs
78
+ * JSON-rehydrated) never affects the result.
70
79
  * Subclasses may still override for a faster or domain-specific comparison.
71
80
  */
72
81
  equals(other) {
73
- return _ValueObject.plugins.invoke("equals", [this, other], { fallback: (a, b) => JSON.stringify(a.value) === JSON.stringify(b.value) });
82
+ return _ValueObject.plugins.invoke("equals", [this, other], { fallback: (a, b) => require_stable_stringify.stableStringify(a.value) === require_stable_stringify.stableStringify(b.value) });
74
83
  }
75
84
  };
76
85
  ValueObject = _ValueObject = require_immutable.__decorate([require_immutable.version("1.0")], ValueObject);
@@ -1 +1 @@
1
- {"version":3,"file":"value-object.cjs","names":["PluginManager","makeImmutable","version"],"sources":["../src/core/domain/value-object.ts"],"sourcesContent":["import { PluginManager } from \"../plugins/index.js\";\nimport { type DeepReadonly, makeImmutable } from \"../shared/immutable.js\";\nimport { type ContractVersion, version } from \"../versioning/version.js\";\n\n/**\n * Extension point for value-object equality. A plugin implementing this\n * contract overrides how any two value objects are compared, letting the host\n * application swap in a structural comparator (e.g. `lodash.isEqual` over the\n * wrapped `value`) without every value object having to implement `equals` by\n * hand.\n */\ntype ValueObjectPluginContract = {\n equals: (\n a: ValueObject<unknown, unknown>,\n b: ValueObject<unknown, unknown>,\n ) => boolean;\n};\n\n/**\n * Base class for value objects — domain concepts defined entirely by their\n * contents, with no identity of their own. Two value objects are\n * interchangeable when they hold equal data, which is the opposite of an\n * {@link Entity} (compared by id). Money, a CPF, a date range: replacing one\n * instance with an equal one changes nothing about the model.\n *\n * Immutability is the defining guarantee here. The wrapped `value` is\n * deep-frozen on construction, so a value object can be shared freely without\n * any risk of a consumer mutating shared state. Subclasses seal the instance\n * itself with {@link finalize} once their own fields are set.\n *\n * @typeParam T - The shape of the wrapped data.\n * @typeParam P - The primitive form produced by {@link toPrimitive} / {@link toJSON}.\n */\n@version(\"1.0\")\nabstract class ValueObject<T, P> {\n declare public static readonly CONTRACT_VERSION: ContractVersion;\n\n /**\n * Registry of equality plugins shared by every value object. Empty by\n * default — when nothing is registered, {@link equals} falls back to a\n * structural comparison of the wrapped `value`. Hosts register a plugin\n * (e.g. `lodash.isEqual`) once at startup to customise equality globally.\n */\n public static readonly plugins =\n new PluginManager<ValueObjectPluginContract>();\n\n /** The wrapped data, deep-frozen so it can never be mutated after construction. */\n public readonly value: DeepReadonly<T>;\n\n /**\n * Wraps `value`, deep-freezing it so the value object is immutable from the\n * moment it exists. Declared `protected` because value objects are built\n * through a validating subclass factory, never instantiated directly.\n */\n protected constructor(value: T) {\n this.value = makeImmutable(value);\n }\n\n /**\n * The schema/contract version stamped on this value-object type by the\n * `@version` decorator — used to detect state persisted under an older shape.\n */\n public get contractVersion(): ContractVersion {\n return ValueObject.CONTRACT_VERSION;\n }\n\n /**\n * Freezes the instance shell, blocking reassignment of any own field.\n *\n * `value` is already deep-frozen by the constructor, so the wrapped data is\n * immutable regardless. The instance itself is NOT frozen automatically\n * because a subclass may still need to assign its own fields after\n * `super(value)` runs. Call `finalize()` at the very end of the subclass\n * constructor (after all fields are set) to make the whole value object\n * immutable.\n */\n protected finalize(): void {\n Object.freeze(this);\n }\n\n /**\n * Hook for `JSON.stringify`. Delegates to {@link toPrimitive} so a value\n * object serializes to its primitive form rather than exposing the internal\n * `value` wrapper.\n */\n public toJSON(): P {\n return this.toPrimitive();\n }\n\n /**\n * Compares this value object with another by content. Because value objects\n * carry no identity, two independently constructed instances holding the\n * same data are considered equal.\n *\n * Delegates to the registered equality {@link plugins}; with no plugin\n * registered it falls back to comparing the serialized wrapped `value`.\n * Subclasses may still override for a faster or domain-specific comparison.\n */\n public equals(other: this): boolean {\n return ValueObject.plugins.invoke(\"equals\", [this, other], {\n fallback: (a, b) =>\n JSON.stringify(a.value) === JSON.stringify(b.value),\n });\n }\n\n /**\n * Projects the value object down to a plain, serializable primitive form —\n * the representation suitable for persistence, transport, or comparison.\n */\n public abstract toPrimitive(): P;\n}\n\nexport { type DeepReadonly, ValueObject, type ValueObjectPluginContract };\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAiCA,IAAA,cAAA,MACe,YAAkB;;;;;iBAUzB,IAAIA,eAAAA,cAAyC;;;;;;;CAUjD,YAAsB,OAAU;EAC5B,KAAK,QAAQC,kBAAAA,cAAc,KAAK;CACpC;;;;;CAMA,IAAW,kBAAmC;EAC1C,OAAA,aAAmB;CACvB;;;;;;;;;;;CAYA,WAA2B;EACvB,OAAO,OAAO,IAAI;CACtB;;;;;;CAOA,SAAmB;EACf,OAAO,KAAK,YAAY;CAC5B;;;;;;;;;;CAWA,OAAc,OAAsB;EAChC,OAAA,aAAmB,QAAQ,OAAO,UAAU,CAAC,MAAM,KAAK,GAAG,EACvD,WAAW,GAAG,MACV,KAAK,UAAU,EAAE,KAAK,MAAM,KAAK,UAAU,EAAE,KAAK,EAC1D,CAAC;CACL;AAOJ;2DA7ECC,kBAAAA,QAAQ,KAAK,CAAA,GAAA,WAAA"}
1
+ {"version":3,"file":"value-object.cjs","names":["PluginManager","makeImmutable","stableStringify","version"],"sources":["../src/core/domain/value-object.ts"],"sourcesContent":["import { PluginManager } from \"../plugins/index.js\";\nimport { type DeepReadonly, makeImmutable } from \"../shared/immutable.js\";\nimport { stableStringify } from \"../shared/stable-stringify.js\";\nimport { type ContractVersion, version } from \"../versioning/version.js\";\n\n/**\n * Extension point for value-object equality. A plugin implementing this\n * contract overrides how any two value objects are compared, letting the host\n * application swap in a structural comparator (e.g. `lodash.isEqual` over the\n * wrapped `value`) without every value object having to implement `equals` by\n * hand.\n */\ntype ValueObjectPluginContract = {\n equals: (\n a: ValueObject<unknown, unknown>,\n b: ValueObject<unknown, unknown>,\n ) => boolean;\n};\n\n/**\n * Base class for value objects — domain concepts defined entirely by their\n * contents, with no identity of their own. Two value objects are\n * interchangeable when they hold equal data, which is the opposite of an\n * {@link Entity} (compared by id). Money, a CPF, a date range: replacing one\n * instance with an equal one changes nothing about the model.\n *\n * Immutability is the defining guarantee here. The wrapped `value` is\n * deep-frozen on construction, so a value object can be shared freely without\n * any risk of a consumer mutating shared state. Subclasses seal the instance\n * itself with {@link finalize} once their own fields are set.\n *\n * One exception: nested `Date` instances are cloned but NOT frozen —\n * `Object.freeze` does not block `setTime`/`setFullYear`, so a consumer of\n * `value` can still mutate an inner `Date` in place. Store instants as ISO\n * strings or epoch timestamps in the wrapped state (converting to `Date` only\n * in an accessor) when that guarantee matters.\n *\n * @typeParam T - The shape of the wrapped data.\n * @typeParam P - The primitive form produced by {@link toPrimitive} / {@link toJSON}.\n */\n@version(\"1.0\")\nabstract class ValueObject<T, P> {\n declare public static readonly CONTRACT_VERSION: ContractVersion;\n\n /**\n * Registry of equality plugins shared by every value object. Empty by\n * default — when nothing is registered, {@link equals} falls back to a\n * structural comparison of the wrapped `value`. Hosts register a plugin\n * (e.g. `lodash.isEqual`) once at startup to customise equality globally.\n */\n public static readonly plugins =\n new PluginManager<ValueObjectPluginContract>();\n\n /** The wrapped data, deep-frozen so it can never be mutated after construction. */\n public readonly value: DeepReadonly<T>;\n\n /**\n * Wraps `value`, deep-freezing it so the value object is immutable from the\n * moment it exists. Declared `protected` because value objects are built\n * through a validating subclass factory, never instantiated directly.\n */\n protected constructor(value: T) {\n this.value = makeImmutable(value);\n }\n\n /**\n * The schema/contract version stamped on this value-object type by the\n * `@version` decorator — used to detect state persisted under an older shape.\n */\n public get contractVersion(): ContractVersion {\n return (this.constructor as typeof ValueObject).CONTRACT_VERSION;\n }\n\n /**\n * Freezes the instance shell, blocking reassignment of any own field.\n *\n * `value` is already deep-frozen by the constructor, so the wrapped data is\n * immutable regardless. The instance itself is NOT frozen automatically\n * because a subclass may still need to assign its own fields after\n * `super(value)` runs. Call `finalize()` at the very end of the subclass\n * constructor (after all fields are set) to make the whole value object\n * immutable.\n */\n protected finalize(): void {\n Object.freeze(this);\n }\n\n /**\n * Hook for `JSON.stringify`. Delegates to {@link toPrimitive} so a value\n * object serializes to its primitive form rather than exposing the internal\n * `value` wrapper.\n */\n public toJSON(): P {\n return this.toPrimitive();\n }\n\n /**\n * Compares this value object with another by content. Because value objects\n * carry no identity, two independently constructed instances holding the\n * same data are considered equal.\n *\n * Delegates to the registered equality {@link plugins}; with no plugin\n * registered it falls back to comparing the serialized wrapped `value`,\n * with object keys sorted so insertion order (e.g. code-built vs\n * JSON-rehydrated) never affects the result.\n * Subclasses may still override for a faster or domain-specific comparison.\n */\n public equals(other: this): boolean {\n return ValueObject.plugins.invoke(\"equals\", [this, other], {\n fallback: (a, b) =>\n stableStringify(a.value) === stableStringify(b.value),\n });\n }\n\n /**\n * Projects the value object down to a plain, serializable primitive form —\n * the representation suitable for persistence, transport, or comparison.\n */\n public abstract toPrimitive(): P;\n}\n\nexport { type DeepReadonly, ValueObject, type ValueObjectPluginContract };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,IAAA,cAAA,MACe,YAAkB;;;;;iBAUzB,IAAIA,eAAAA,cAAyC;;;;;;;CAUjD,YAAsB,OAAU;EAC5B,KAAK,QAAQC,kBAAAA,cAAc,KAAK;CACpC;;;;;CAMA,IAAW,kBAAmC;EAC1C,OAAQ,KAAK,YAAmC;CACpD;;;;;;;;;;;CAYA,WAA2B;EACvB,OAAO,OAAO,IAAI;CACtB;;;;;;CAOA,SAAmB;EACf,OAAO,KAAK,YAAY;CAC5B;;;;;;;;;;;;CAaA,OAAc,OAAsB;EAChC,OAAA,aAAmB,QAAQ,OAAO,UAAU,CAAC,MAAM,KAAK,GAAG,EACvD,WAAW,GAAG,MACVC,yBAAAA,gBAAgB,EAAE,KAAK,MAAMA,yBAAAA,gBAAgB,EAAE,KAAK,EAC5D,CAAC;CACL;AAOJ;2DA/ECC,kBAAAA,QAAQ,KAAK,CAAA,GAAA,WAAA"}
@@ -26,6 +26,12 @@ type ValueObjectPluginContract = {
26
26
  * any risk of a consumer mutating shared state. Subclasses seal the instance
27
27
  * itself with {@link finalize} once their own fields are set.
28
28
  *
29
+ * One exception: nested `Date` instances are cloned but NOT frozen —
30
+ * `Object.freeze` does not block `setTime`/`setFullYear`, so a consumer of
31
+ * `value` can still mutate an inner `Date` in place. Store instants as ISO
32
+ * strings or epoch timestamps in the wrapped state (converting to `Date` only
33
+ * in an accessor) when that guarantee matters.
34
+ *
29
35
  * @typeParam T - The shape of the wrapped data.
30
36
  * @typeParam P - The primitive form produced by {@link toPrimitive} / {@link toJSON}.
31
37
  */
@@ -74,7 +80,9 @@ declare abstract class ValueObject<T, P> {
74
80
  * same data are considered equal.
75
81
  *
76
82
  * Delegates to the registered equality {@link plugins}; with no plugin
77
- * registered it falls back to comparing the serialized wrapped `value`.
83
+ * registered it falls back to comparing the serialized wrapped `value`,
84
+ * with object keys sorted so insertion order (e.g. code-built vs
85
+ * JSON-rehydrated) never affects the result.
78
86
  * Subclasses may still override for a faster or domain-specific comparison.
79
87
  */
80
88
  equals(other: this): boolean;
@@ -26,6 +26,12 @@ type ValueObjectPluginContract = {
26
26
  * any risk of a consumer mutating shared state. Subclasses seal the instance
27
27
  * itself with {@link finalize} once their own fields are set.
28
28
  *
29
+ * One exception: nested `Date` instances are cloned but NOT frozen —
30
+ * `Object.freeze` does not block `setTime`/`setFullYear`, so a consumer of
31
+ * `value` can still mutate an inner `Date` in place. Store instants as ISO
32
+ * strings or epoch timestamps in the wrapped state (converting to `Date` only
33
+ * in an accessor) when that guarantee matters.
34
+ *
29
35
  * @typeParam T - The shape of the wrapped data.
30
36
  * @typeParam P - The primitive form produced by {@link toPrimitive} / {@link toJSON}.
31
37
  */
@@ -74,7 +80,9 @@ declare abstract class ValueObject<T, P> {
74
80
  * same data are considered equal.
75
81
  *
76
82
  * Delegates to the registered equality {@link plugins}; with no plugin
77
- * registered it falls back to comparing the serialized wrapped `value`.
83
+ * registered it falls back to comparing the serialized wrapped `value`,
84
+ * with object keys sorted so insertion order (e.g. code-built vs
85
+ * JSON-rehydrated) never affects the result.
78
86
  * Subclasses may still override for a faster or domain-specific comparison.
79
87
  */
80
88
  equals(other: this): boolean;
@@ -1,3 +1,4 @@
1
+ import { t as stableStringify } from "./stable-stringify.js";
1
2
  import { i as version, n as __decorate, t as makeImmutable } from "./immutable.js";
2
3
  import { t as PluginManager } from "./plugin.js";
3
4
  //#region src/core/domain/value-object.ts
@@ -14,6 +15,12 @@ var _ValueObject;
14
15
  * any risk of a consumer mutating shared state. Subclasses seal the instance
15
16
  * itself with {@link finalize} once their own fields are set.
16
17
  *
18
+ * One exception: nested `Date` instances are cloned but NOT frozen —
19
+ * `Object.freeze` does not block `setTime`/`setFullYear`, so a consumer of
20
+ * `value` can still mutate an inner `Date` in place. Store instants as ISO
21
+ * strings or epoch timestamps in the wrapped state (converting to `Date` only
22
+ * in an accessor) when that guarantee matters.
23
+ *
17
24
  * @typeParam T - The shape of the wrapped data.
18
25
  * @typeParam P - The primitive form produced by {@link toPrimitive} / {@link toJSON}.
19
26
  */
@@ -37,7 +44,7 @@ let ValueObject = class ValueObject {
37
44
  * `@version` decorator — used to detect state persisted under an older shape.
38
45
  */
39
46
  get contractVersion() {
40
- return _ValueObject.CONTRACT_VERSION;
47
+ return this.constructor.CONTRACT_VERSION;
41
48
  }
42
49
  /**
43
50
  * Freezes the instance shell, blocking reassignment of any own field.
@@ -66,11 +73,13 @@ let ValueObject = class ValueObject {
66
73
  * same data are considered equal.
67
74
  *
68
75
  * Delegates to the registered equality {@link plugins}; with no plugin
69
- * registered it falls back to comparing the serialized wrapped `value`.
76
+ * registered it falls back to comparing the serialized wrapped `value`,
77
+ * with object keys sorted so insertion order (e.g. code-built vs
78
+ * JSON-rehydrated) never affects the result.
70
79
  * Subclasses may still override for a faster or domain-specific comparison.
71
80
  */
72
81
  equals(other) {
73
- return _ValueObject.plugins.invoke("equals", [this, other], { fallback: (a, b) => JSON.stringify(a.value) === JSON.stringify(b.value) });
82
+ return _ValueObject.plugins.invoke("equals", [this, other], { fallback: (a, b) => stableStringify(a.value) === stableStringify(b.value) });
74
83
  }
75
84
  };
76
85
  ValueObject = _ValueObject = __decorate([version("1.0")], ValueObject);
@@ -1 +1 @@
1
- {"version":3,"file":"value-object.js","names":[],"sources":["../src/core/domain/value-object.ts"],"sourcesContent":["import { PluginManager } from \"../plugins/index.js\";\nimport { type DeepReadonly, makeImmutable } from \"../shared/immutable.js\";\nimport { type ContractVersion, version } from \"../versioning/version.js\";\n\n/**\n * Extension point for value-object equality. A plugin implementing this\n * contract overrides how any two value objects are compared, letting the host\n * application swap in a structural comparator (e.g. `lodash.isEqual` over the\n * wrapped `value`) without every value object having to implement `equals` by\n * hand.\n */\ntype ValueObjectPluginContract = {\n equals: (\n a: ValueObject<unknown, unknown>,\n b: ValueObject<unknown, unknown>,\n ) => boolean;\n};\n\n/**\n * Base class for value objects — domain concepts defined entirely by their\n * contents, with no identity of their own. Two value objects are\n * interchangeable when they hold equal data, which is the opposite of an\n * {@link Entity} (compared by id). Money, a CPF, a date range: replacing one\n * instance with an equal one changes nothing about the model.\n *\n * Immutability is the defining guarantee here. The wrapped `value` is\n * deep-frozen on construction, so a value object can be shared freely without\n * any risk of a consumer mutating shared state. Subclasses seal the instance\n * itself with {@link finalize} once their own fields are set.\n *\n * @typeParam T - The shape of the wrapped data.\n * @typeParam P - The primitive form produced by {@link toPrimitive} / {@link toJSON}.\n */\n@version(\"1.0\")\nabstract class ValueObject<T, P> {\n declare public static readonly CONTRACT_VERSION: ContractVersion;\n\n /**\n * Registry of equality plugins shared by every value object. Empty by\n * default — when nothing is registered, {@link equals} falls back to a\n * structural comparison of the wrapped `value`. Hosts register a plugin\n * (e.g. `lodash.isEqual`) once at startup to customise equality globally.\n */\n public static readonly plugins =\n new PluginManager<ValueObjectPluginContract>();\n\n /** The wrapped data, deep-frozen so it can never be mutated after construction. */\n public readonly value: DeepReadonly<T>;\n\n /**\n * Wraps `value`, deep-freezing it so the value object is immutable from the\n * moment it exists. Declared `protected` because value objects are built\n * through a validating subclass factory, never instantiated directly.\n */\n protected constructor(value: T) {\n this.value = makeImmutable(value);\n }\n\n /**\n * The schema/contract version stamped on this value-object type by the\n * `@version` decorator — used to detect state persisted under an older shape.\n */\n public get contractVersion(): ContractVersion {\n return ValueObject.CONTRACT_VERSION;\n }\n\n /**\n * Freezes the instance shell, blocking reassignment of any own field.\n *\n * `value` is already deep-frozen by the constructor, so the wrapped data is\n * immutable regardless. The instance itself is NOT frozen automatically\n * because a subclass may still need to assign its own fields after\n * `super(value)` runs. Call `finalize()` at the very end of the subclass\n * constructor (after all fields are set) to make the whole value object\n * immutable.\n */\n protected finalize(): void {\n Object.freeze(this);\n }\n\n /**\n * Hook for `JSON.stringify`. Delegates to {@link toPrimitive} so a value\n * object serializes to its primitive form rather than exposing the internal\n * `value` wrapper.\n */\n public toJSON(): P {\n return this.toPrimitive();\n }\n\n /**\n * Compares this value object with another by content. Because value objects\n * carry no identity, two independently constructed instances holding the\n * same data are considered equal.\n *\n * Delegates to the registered equality {@link plugins}; with no plugin\n * registered it falls back to comparing the serialized wrapped `value`.\n * Subclasses may still override for a faster or domain-specific comparison.\n */\n public equals(other: this): boolean {\n return ValueObject.plugins.invoke(\"equals\", [this, other], {\n fallback: (a, b) =>\n JSON.stringify(a.value) === JSON.stringify(b.value),\n });\n }\n\n /**\n * Projects the value object down to a plain, serializable primitive form —\n * the representation suitable for persistence, transport, or comparison.\n */\n public abstract toPrimitive(): P;\n}\n\nexport { type DeepReadonly, ValueObject, type ValueObjectPluginContract };\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAiCA,IAAA,cAAA,MACe,YAAkB;;;;;iBAUzB,IAAI,cAAyC;;;;;;;CAUjD,YAAsB,OAAU;EAC5B,KAAK,QAAQ,cAAc,KAAK;CACpC;;;;;CAMA,IAAW,kBAAmC;EAC1C,OAAA,aAAmB;CACvB;;;;;;;;;;;CAYA,WAA2B;EACvB,OAAO,OAAO,IAAI;CACtB;;;;;;CAOA,SAAmB;EACf,OAAO,KAAK,YAAY;CAC5B;;;;;;;;;;CAWA,OAAc,OAAsB;EAChC,OAAA,aAAmB,QAAQ,OAAO,UAAU,CAAC,MAAM,KAAK,GAAG,EACvD,WAAW,GAAG,MACV,KAAK,UAAU,EAAE,KAAK,MAAM,KAAK,UAAU,EAAE,KAAK,EAC1D,CAAC;CACL;AAOJ;yCA7EC,QAAQ,KAAK,CAAA,GAAA,WAAA"}
1
+ {"version":3,"file":"value-object.js","names":[],"sources":["../src/core/domain/value-object.ts"],"sourcesContent":["import { PluginManager } from \"../plugins/index.js\";\nimport { type DeepReadonly, makeImmutable } from \"../shared/immutable.js\";\nimport { stableStringify } from \"../shared/stable-stringify.js\";\nimport { type ContractVersion, version } from \"../versioning/version.js\";\n\n/**\n * Extension point for value-object equality. A plugin implementing this\n * contract overrides how any two value objects are compared, letting the host\n * application swap in a structural comparator (e.g. `lodash.isEqual` over the\n * wrapped `value`) without every value object having to implement `equals` by\n * hand.\n */\ntype ValueObjectPluginContract = {\n equals: (\n a: ValueObject<unknown, unknown>,\n b: ValueObject<unknown, unknown>,\n ) => boolean;\n};\n\n/**\n * Base class for value objects — domain concepts defined entirely by their\n * contents, with no identity of their own. Two value objects are\n * interchangeable when they hold equal data, which is the opposite of an\n * {@link Entity} (compared by id). Money, a CPF, a date range: replacing one\n * instance with an equal one changes nothing about the model.\n *\n * Immutability is the defining guarantee here. The wrapped `value` is\n * deep-frozen on construction, so a value object can be shared freely without\n * any risk of a consumer mutating shared state. Subclasses seal the instance\n * itself with {@link finalize} once their own fields are set.\n *\n * One exception: nested `Date` instances are cloned but NOT frozen —\n * `Object.freeze` does not block `setTime`/`setFullYear`, so a consumer of\n * `value` can still mutate an inner `Date` in place. Store instants as ISO\n * strings or epoch timestamps in the wrapped state (converting to `Date` only\n * in an accessor) when that guarantee matters.\n *\n * @typeParam T - The shape of the wrapped data.\n * @typeParam P - The primitive form produced by {@link toPrimitive} / {@link toJSON}.\n */\n@version(\"1.0\")\nabstract class ValueObject<T, P> {\n declare public static readonly CONTRACT_VERSION: ContractVersion;\n\n /**\n * Registry of equality plugins shared by every value object. Empty by\n * default — when nothing is registered, {@link equals} falls back to a\n * structural comparison of the wrapped `value`. Hosts register a plugin\n * (e.g. `lodash.isEqual`) once at startup to customise equality globally.\n */\n public static readonly plugins =\n new PluginManager<ValueObjectPluginContract>();\n\n /** The wrapped data, deep-frozen so it can never be mutated after construction. */\n public readonly value: DeepReadonly<T>;\n\n /**\n * Wraps `value`, deep-freezing it so the value object is immutable from the\n * moment it exists. Declared `protected` because value objects are built\n * through a validating subclass factory, never instantiated directly.\n */\n protected constructor(value: T) {\n this.value = makeImmutable(value);\n }\n\n /**\n * The schema/contract version stamped on this value-object type by the\n * `@version` decorator — used to detect state persisted under an older shape.\n */\n public get contractVersion(): ContractVersion {\n return (this.constructor as typeof ValueObject).CONTRACT_VERSION;\n }\n\n /**\n * Freezes the instance shell, blocking reassignment of any own field.\n *\n * `value` is already deep-frozen by the constructor, so the wrapped data is\n * immutable regardless. The instance itself is NOT frozen automatically\n * because a subclass may still need to assign its own fields after\n * `super(value)` runs. Call `finalize()` at the very end of the subclass\n * constructor (after all fields are set) to make the whole value object\n * immutable.\n */\n protected finalize(): void {\n Object.freeze(this);\n }\n\n /**\n * Hook for `JSON.stringify`. Delegates to {@link toPrimitive} so a value\n * object serializes to its primitive form rather than exposing the internal\n * `value` wrapper.\n */\n public toJSON(): P {\n return this.toPrimitive();\n }\n\n /**\n * Compares this value object with another by content. Because value objects\n * carry no identity, two independently constructed instances holding the\n * same data are considered equal.\n *\n * Delegates to the registered equality {@link plugins}; with no plugin\n * registered it falls back to comparing the serialized wrapped `value`,\n * with object keys sorted so insertion order (e.g. code-built vs\n * JSON-rehydrated) never affects the result.\n * Subclasses may still override for a faster or domain-specific comparison.\n */\n public equals(other: this): boolean {\n return ValueObject.plugins.invoke(\"equals\", [this, other], {\n fallback: (a, b) =>\n stableStringify(a.value) === stableStringify(b.value),\n });\n }\n\n /**\n * Projects the value object down to a plain, serializable primitive form —\n * the representation suitable for persistence, transport, or comparison.\n */\n public abstract toPrimitive(): P;\n}\n\nexport { type DeepReadonly, ValueObject, type ValueObjectPluginContract };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,IAAA,cAAA,MACe,YAAkB;;;;;iBAUzB,IAAI,cAAyC;;;;;;;CAUjD,YAAsB,OAAU;EAC5B,KAAK,QAAQ,cAAc,KAAK;CACpC;;;;;CAMA,IAAW,kBAAmC;EAC1C,OAAQ,KAAK,YAAmC;CACpD;;;;;;;;;;;CAYA,WAA2B;EACvB,OAAO,OAAO,IAAI;CACtB;;;;;;CAOA,SAAmB;EACf,OAAO,KAAK,YAAY;CAC5B;;;;;;;;;;;;CAaA,OAAc,OAAsB;EAChC,OAAA,aAAmB,QAAQ,OAAO,UAAU,CAAC,MAAM,KAAK,GAAG,EACvD,WAAW,GAAG,MACV,gBAAgB,EAAE,KAAK,MAAM,gBAAgB,EAAE,KAAK,EAC5D,CAAC;CACL;AAOJ;yCA/EC,QAAQ,KAAK,CAAA,GAAA,WAAA"}
package/meta.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": "2",
3
3
  "name": "erp-core",
4
- "version": "1.4.0",
4
+ "version": "1.5.0",
5
5
  "description": "Núcleo arquitetural para ERP em TypeScript — entidades, value objects, erros tipados e policies declaráveis sobre clean architecture.",
6
6
  "compatibility": {
7
7
  "engines": {
@@ -83,6 +83,7 @@
83
83
  "PermissionSet",
84
84
  "RbacAuthorizer",
85
85
  "AuthorizerPort",
86
+ "rbacContextFields",
86
87
  "AbacRule",
87
88
  "AbacPolicySet",
88
89
  "AbacAuthorizer",
@@ -105,7 +106,8 @@
105
106
  "1.1.0 - **New: `ResultRepository`, `UseCaseObservability`, and `./application` subpath**",
106
107
  "1.2.0 - Add zod-free subpaths for the stable core primitives and a value-object equality plugin system.",
107
108
  "1.3.0 - Add zod-free subpaths for the stable core primitives and a value-object equality plugin system.",
108
- "1.4.0 - Add a zod-free ABAC module and the `./abac` subpath."
109
+ "1.4.0 - Add a zod-free ABAC module and the `./abac` subpath.",
110
+ "1.5.0 - Add a zod-free ABAC module and the `./abac` subpath."
109
111
  ],
110
112
  "deprecated": false
111
113
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cullet/erp-core",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Núcleo arquitetural para ERP em TypeScript — entidades, value objects, erros tipados e policies declaráveis sobre clean architecture.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -1,10 +1,12 @@
1
1
  import { ValidationCode } from "../../exceptions/validation-code.js";
2
2
  import { InvalidValueException } from "../../exceptions/validation-exception.js";
3
3
  import { ValidationField } from "../../exceptions/validation-field.js";
4
+ import { UUID_PATTERN } from "../../shared/uuid.js";
4
5
 
5
6
  // Identifies who triggered a Command.
6
7
  // Two valid formats:
7
- // - Human user: UUID v4 (e.g.: "550e8400-e29b-41d4-a716-446655440000")
8
+ // - Human user: canonical RFC-4122 UUID, versions 1–5 — the same pattern
9
+ // `UuidIdentifier` enforces (e.g.: "550e8400-e29b-41d4-a716-446655440000")
8
10
  // - System identity: "system:<job>" where <job> is [a-z][a-z0-9]*(-[a-z0-9]+)*
9
11
  // (e.g.: "system:late-fee-job", "system:email-sender")
10
12
  //
@@ -15,9 +17,6 @@ type RequestedByKind = "user" | "system";
15
17
 
16
18
  const REQUESTED_BY_FIELD = ValidationField.of("requestedBy");
17
19
 
18
- const UUID_PATTERN =
19
- /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
20
-
21
20
  // system:<job> where <job> starts with a lowercase letter and may have hyphens between segments
22
21
  const SYSTEM_IDENTITY_PATTERN = /^system:[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
23
22
 
@@ -44,7 +44,12 @@ abstract class Query<
44
44
  Data extends QueryOutput = never,
45
45
  Failure = AppError,
46
46
  > extends UseCase<Input, Result<Data, Failure>> {
47
- protected cacheStrategy(): CacheStrategy {
47
+ /**
48
+ * Declares how infrastructure should cache this query's result. Public so
49
+ * a caching adapter can actually read it off the instance; overrides must
50
+ * stay public.
51
+ */
52
+ public cacheStrategy(): CacheStrategy {
48
53
  return { kind: "NO_CACHE" };
49
54
  }
50
55
  }
@@ -34,7 +34,7 @@ abstract class UseCase<
34
34
  declare public static readonly CONTRACT_VERSION: ContractVersion;
35
35
 
36
36
  public get contractVersion(): ContractVersion {
37
- return UseCase.CONTRACT_VERSION;
37
+ return (this.constructor as typeof UseCase).CONTRACT_VERSION;
38
38
  }
39
39
 
40
40
  /**
@@ -112,7 +112,7 @@ abstract class Entity<TIdentifier> {
112
112
  * an older shape.
113
113
  */
114
114
  public get contractVersion(): ContractVersion {
115
- return Entity.CONTRACT_VERSION;
115
+ return (this.constructor as typeof Entity).CONTRACT_VERSION;
116
116
  }
117
117
 
118
118
  /**
@@ -1,13 +1,6 @@
1
+ import { UUID_PATTERN } from "../shared/uuid.js";
1
2
  import { ValueObject } from "./value-object.js";
2
3
 
3
- /**
4
- * Canonical RFC-4122 UUID (versions 1–5), matched case-insensitively. Declared
5
- * once here so the dozens of typed identifiers a domain accumulates never have
6
- * to re-state the pattern.
7
- */
8
- const UUID_PATTERN =
9
- /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
10
-
11
4
  /**
12
5
  * Base class for UUID-backed identity value objects.
13
6
  *
@@ -1,5 +1,6 @@
1
1
  import { PluginManager } from "../plugins/index.js";
2
2
  import { type DeepReadonly, makeImmutable } from "../shared/immutable.js";
3
+ import { stableStringify } from "../shared/stable-stringify.js";
3
4
  import { type ContractVersion, version } from "../versioning/version.js";
4
5
 
5
6
  /**
@@ -28,6 +29,12 @@ type ValueObjectPluginContract = {
28
29
  * any risk of a consumer mutating shared state. Subclasses seal the instance
29
30
  * itself with {@link finalize} once their own fields are set.
30
31
  *
32
+ * One exception: nested `Date` instances are cloned but NOT frozen —
33
+ * `Object.freeze` does not block `setTime`/`setFullYear`, so a consumer of
34
+ * `value` can still mutate an inner `Date` in place. Store instants as ISO
35
+ * strings or epoch timestamps in the wrapped state (converting to `Date` only
36
+ * in an accessor) when that guarantee matters.
37
+ *
31
38
  * @typeParam T - The shape of the wrapped data.
32
39
  * @typeParam P - The primitive form produced by {@link toPrimitive} / {@link toJSON}.
33
40
  */
@@ -61,7 +68,7 @@ abstract class ValueObject<T, P> {
61
68
  * `@version` decorator — used to detect state persisted under an older shape.
62
69
  */
63
70
  public get contractVersion(): ContractVersion {
64
- return ValueObject.CONTRACT_VERSION;
71
+ return (this.constructor as typeof ValueObject).CONTRACT_VERSION;
65
72
  }
66
73
 
67
74
  /**
@@ -93,13 +100,15 @@ abstract class ValueObject<T, P> {
93
100
  * same data are considered equal.
94
101
  *
95
102
  * Delegates to the registered equality {@link plugins}; with no plugin
96
- * registered it falls back to comparing the serialized wrapped `value`.
103
+ * registered it falls back to comparing the serialized wrapped `value`,
104
+ * with object keys sorted so insertion order (e.g. code-built vs
105
+ * JSON-rehydrated) never affects the result.
97
106
  * Subclasses may still override for a faster or domain-specific comparison.
98
107
  */
99
108
  public equals(other: this): boolean {
100
109
  return ValueObject.plugins.invoke("equals", [this, other], {
101
110
  fallback: (a, b) =>
102
- JSON.stringify(a.value) === JSON.stringify(b.value),
111
+ stableStringify(a.value) === stableStringify(b.value),
103
112
  });
104
113
  }
105
114
 
@@ -30,18 +30,23 @@ function sanitizeValue(value: unknown, seen: WeakSet<object>): JsonSafeValue {
30
30
 
31
31
  const type = typeof value;
32
32
 
33
- // Primitives
34
- if (type === "string" || type === "number" || type === "boolean") {
33
+ // Primitives (non-finite numbers are not JSON-representable — placeholder)
34
+ if (type === "number") {
35
+ return Number.isFinite(value)
36
+ ? (value as number)
37
+ : NON_SERIALIZABLE_PLACEHOLDER;
38
+ }
39
+ if (type === "string" || type === "boolean") {
35
40
  return value as JsonSafeValue;
36
41
  }
37
42
 
43
+ // `undefined` only reaches here as an array item: object properties with
44
+ // undefined values are omitted by the plain-object branch below. Both
45
+ // mirror `JSON.stringify` semantics.
46
+ if (value === undefined) return null;
47
+
38
48
  // Non-serializable primitives
39
- if (
40
- type === "bigint" ||
41
- type === "function" ||
42
- type === "symbol" ||
43
- value === undefined
44
- ) {
49
+ if (type === "bigint" || type === "function" || type === "symbol") {
45
50
  return NON_SERIALIZABLE_PLACEHOLDER;
46
51
  }
47
52
 
@@ -69,6 +74,7 @@ function sanitizeValue(value: unknown, seen: WeakSet<object>): JsonSafeValue {
69
74
  seen.add(value);
70
75
  const result: Record<string, JsonSafeValue> = {};
71
76
  for (const [key, val] of Object.entries(value)) {
77
+ if (val === undefined) continue;
72
78
  result[key] = sanitizeValue(val, seen);
73
79
  }
74
80
  seen.delete(value);
@@ -82,11 +88,14 @@ function sanitizeValue(value: unknown, seen: WeakSet<object>): JsonSafeValue {
82
88
  /**
83
89
  * Ensures metadata is JSON-serializable.
84
90
  *
85
- * **Allowed:** string, number, boolean, null, arrays, and plain objects.
91
+ * **Allowed:** finite number, string, boolean, null, arrays, and plain objects.
92
+ *
93
+ * **Dropped (mirroring `JSON.stringify`):** object properties whose value is
94
+ * `undefined` (the key is omitted); an `undefined` array item becomes `null`.
86
95
  *
87
96
  * **Converted to placeholder:** Date, BigInt, class instances, functions,
88
- * symbols, undefined, and circular references (which would otherwise make
89
- * `JSON.stringify` throw).
97
+ * symbols, non-finite numbers (`NaN`/`Infinity`), and circular references
98
+ * (which would otherwise make `JSON.stringify` throw).
90
99
  *
91
100
  * @throws {TypeError} If `input` is not a plain object at the root level.
92
101
  */
@@ -1,5 +1,8 @@
1
1
  import { PolicyContextPath } from "../../context/index.js";
2
- import { PolicyDateUtils } from "../../utils/index.js";
2
+ // Import the date util directly (not the utils barrel): the barrel re-exports
3
+ // PolicyHashing, whose node:crypto import would needlessly drag a Node-only
4
+ // API into the zod-free ./abac subpath.
5
+ import { PolicyDateUtils } from "../../utils/date.js";
3
6
  import { Result } from "../../../result/result.js";
4
7
 
5
8
  import type {
@@ -213,6 +216,32 @@ export class ConditionEvaluatorV1 {
213
216
  return Result.err(message);
214
217
  }
215
218
 
219
+ private static containsInvalidDate(value: unknown): boolean {
220
+ if (value instanceof Date) {
221
+ return !PolicyDateUtils.isValid(value);
222
+ }
223
+ if (Array.isArray(value)) {
224
+ return value.some((item) =>
225
+ ConditionEvaluatorV1.containsInvalidDate(item),
226
+ );
227
+ }
228
+ return false;
229
+ }
230
+
231
+ // Valid Dates become their ISO string so equality/set operators compare
232
+ // instants instead of references; every other value passes through.
233
+ private static normalizeDateOperand(value: unknown): unknown {
234
+ if (value instanceof Date) {
235
+ return value.toISOString();
236
+ }
237
+ if (Array.isArray(value)) {
238
+ return value.map((item) =>
239
+ ConditionEvaluatorV1.normalizeDateOperand(item),
240
+ );
241
+ }
242
+ return value;
243
+ }
244
+
216
245
  private reportDateOperandError(
217
246
  node: ConditionLeafNode,
218
247
  actual: unknown,
@@ -527,6 +556,32 @@ export class ConditionEvaluatorV1 {
527
556
  return this.reportInvalidSetOperand(node, actual);
528
557
  }
529
558
 
559
+ if (
560
+ node.op === "eq" ||
561
+ node.op === "neq" ||
562
+ node.op === "in" ||
563
+ node.op === "notIn"
564
+ ) {
565
+ // A Date operand would otherwise compare by reference and
566
+ // silently never match. Normalize valid Dates (either side,
567
+ // including set items) to their ISO string; an invalid Date is
568
+ // a context/configuration error, like the relational path.
569
+ if (
570
+ ConditionEvaluatorV1.containsInvalidDate(actual) ||
571
+ ConditionEvaluatorV1.containsInvalidDate(node.value)
572
+ ) {
573
+ return this.reportDateOperandError(node, actual);
574
+ }
575
+
576
+ return Result.ok(
577
+ this.evaluateOperator(
578
+ ConditionEvaluatorV1.normalizeDateOperand(actual),
579
+ node.op,
580
+ ConditionEvaluatorV1.normalizeDateOperand(node.value),
581
+ ),
582
+ );
583
+ }
584
+
530
585
  return Result.ok(
531
586
  this.evaluateOperator(actual, node.op, node.value),
532
587
  );
@@ -1,65 +1,11 @@
1
1
  import { createHash } from "node:crypto";
2
2
 
3
- const CIRCULAR_STRUCTURE_MESSAGE =
4
- "Cannot stably stringify a circular structure";
3
+ import { stableStringify } from "./stable-stringify.js";
5
4
 
6
5
  function sha256Hex(value: string): string {
7
6
  return createHash("sha256").update(value, "utf8").digest("hex");
8
7
  }
9
8
 
10
- // `seen` holds the current traversal branch so true cycles are rejected while
11
- // repeated (acyclic) references are still serialized — matching JSON.stringify.
12
- function sortKeysDeep(value: unknown, seen: WeakSet<object>): unknown {
13
- if (Array.isArray(value)) {
14
- if (seen.has(value)) {
15
- throw new TypeError(CIRCULAR_STRUCTURE_MESSAGE);
16
- }
17
- seen.add(value);
18
- const mapped = value.map((item) => sortKeysDeep(item, seen));
19
- seen.delete(value);
20
- return mapped;
21
- }
22
-
23
- if (value instanceof Date) {
24
- return value;
25
- }
26
-
27
- if (value && typeof value === "object") {
28
- if (seen.has(value)) {
29
- throw new TypeError(CIRCULAR_STRUCTURE_MESSAGE);
30
- }
31
- seen.add(value);
32
- const record = value as Record<string, unknown>;
33
- const ordered = Object.create(null) as Record<string, unknown>;
34
-
35
- for (const key of Object.keys(record).sort()) {
36
- ordered[key] = sortKeysDeep(record[key], seen);
37
- }
38
- seen.delete(value);
39
-
40
- return ordered;
41
- }
42
-
43
- return value;
44
- }
45
-
46
- /**
47
- * Deterministic JSON string with object keys sorted recursively, so key order
48
- * does not affect the output.
49
- *
50
- * Follows `JSON.stringify` semantics for the values it serializes: `undefined`,
51
- * functions and symbols are dropped, non-finite numbers become `null`, `bigint`
52
- * throws, and `Map`/`Set` serialize as `{}`. Distinct payloads can therefore
53
- * collapse to the same string — for collision-resistant hashing use
54
- * `PolicyHashing.canonicalJson` (policies/utils), which rejects those values
55
- * up front.
56
- *
57
- * @throws {TypeError} On circular references (mirroring `JSON.stringify`).
58
- */
59
- function stableStringify(value: unknown): string {
60
- return JSON.stringify(sortKeysDeep(value, new WeakSet<object>()));
61
- }
62
-
63
9
  function payloadHash(value: unknown): string {
64
10
  return sha256Hex(stableStringify(value));
65
11
  }