@cullet/erp-core 1.4.0 → 2.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.
- package/KIT_CONTEXT.md +7 -1
- package/dist/abac/index.d.cts +2 -2
- package/dist/abac/index.d.ts +2 -2
- package/dist/aggregate-version.cjs +14 -0
- package/dist/aggregate-version.cjs.map +1 -0
- package/dist/aggregate-version.js +9 -0
- package/dist/aggregate-version.js.map +1 -0
- package/dist/app-error.cjs +21 -6
- package/dist/app-error.cjs.map +1 -1
- package/dist/app-error.js +21 -6
- package/dist/app-error.js.map +1 -1
- package/dist/application/index.cjs +8 -4
- package/dist/application/index.d.cts +2 -2
- package/dist/application/index.d.ts +2 -2
- package/dist/application/index.js +1 -2
- package/dist/authorization-error.cjs +67 -17
- package/dist/authorization-error.cjs.map +1 -1
- package/dist/authorization-error.d.cts +6 -2
- package/dist/authorization-error.d.ts +6 -2
- package/dist/authorization-error.js +50 -18
- package/dist/authorization-error.js.map +1 -1
- package/dist/authorizer.port.d.cts +18 -3
- package/dist/authorizer.port.d.ts +18 -3
- package/dist/composite-authorizer.d.cts +63 -10
- package/dist/composite-authorizer.d.ts +63 -10
- package/dist/condition-evaluator.cjs +117 -172
- package/dist/condition-evaluator.cjs.map +1 -1
- package/dist/condition-evaluator.js +118 -167
- package/dist/condition-evaluator.js.map +1 -1
- package/dist/core-config.d.cts +53 -6
- package/dist/core-config.d.ts +53 -6
- package/dist/decorate.cjs +14 -0
- package/dist/decorate.js +9 -0
- package/dist/domain/index.cjs +107 -2
- package/dist/domain/index.cjs.map +1 -0
- package/dist/domain/index.d.cts +25 -1
- package/dist/domain/index.d.ts +25 -1
- package/dist/domain/index.js +99 -3
- package/dist/domain/index.js.map +1 -0
- package/dist/domain-event-contracts.cjs +12 -8
- package/dist/domain-event-contracts.cjs.map +1 -1
- package/dist/domain-event-contracts.d.cts +29 -4
- package/dist/domain-event-contracts.d.ts +29 -4
- package/dist/domain-event-contracts.js +11 -7
- package/dist/domain-event-contracts.js.map +1 -1
- package/dist/domain-exception.cjs +2 -1
- package/dist/domain-exception.cjs.map +1 -1
- package/dist/domain-exception.js +2 -1
- package/dist/domain-exception.js.map +1 -1
- package/dist/errors/index.cjs +3 -2
- package/dist/errors/index.d.cts +1 -1
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +3 -2
- package/dist/exceptions/index.cjs +2 -2
- package/dist/exceptions/index.d.cts +1 -2
- package/dist/exceptions/index.d.ts +1 -2
- package/dist/exceptions/index.js +2 -2
- package/dist/gate-engine-registry.cjs.map +1 -1
- package/dist/gate-engine-registry.d.cts +1 -1
- package/dist/gate-engine-registry.d.ts +1 -1
- package/dist/gate-engine-registry.js.map +1 -1
- package/dist/gate-v1-payload.schema.cjs +11 -6
- package/dist/gate-v1-payload.schema.cjs.map +1 -1
- package/dist/gate-v1-payload.schema.js +11 -6
- package/dist/gate-v1-payload.schema.js.map +1 -1
- package/dist/hashing.cjs +2 -44
- package/dist/hashing.cjs.map +1 -1
- package/dist/hashing.js +3 -39
- package/dist/hashing.js.map +1 -1
- package/dist/immutable.cjs +25 -12
- package/dist/immutable.cjs.map +1 -1
- package/dist/immutable.js +24 -11
- package/dist/immutable.js.map +1 -1
- package/dist/index.cjs +15 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -10
- package/dist/index.d.ts +9 -10
- package/dist/index.js +8 -9
- package/dist/index.js.map +1 -1
- package/dist/invalid-state-transition-exception.cjs +6 -6
- package/dist/invalid-state-transition-exception.cjs.map +1 -1
- package/dist/invalid-state-transition-exception.js +6 -6
- package/dist/invalid-state-transition-exception.js.map +1 -1
- package/dist/invariant-violation-exception.cjs +2 -2
- package/dist/invariant-violation-exception.cjs.map +1 -1
- package/dist/invariant-violation-exception.js +2 -2
- package/dist/invariant-violation-exception.js.map +1 -1
- package/dist/not-found-error.cjs +6 -4
- package/dist/not-found-error.cjs.map +1 -1
- package/dist/not-found-error.js +6 -4
- package/dist/not-found-error.js.map +1 -1
- package/dist/outcome.cjs +5 -5
- package/dist/outcome.cjs.map +1 -1
- package/dist/outcome.js +5 -5
- package/dist/outcome.js.map +1 -1
- package/dist/parse-gate-payload.d.cts +1 -1
- package/dist/parse-gate-payload.d.ts +1 -1
- package/dist/path.d.cts +2 -2
- package/dist/path.d.ts +2 -2
- package/dist/plugin.cjs +23 -13
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.d.cts +22 -8
- package/dist/plugin.d.ts +22 -8
- package/dist/plugin.js +23 -13
- package/dist/plugin.js.map +1 -1
- package/dist/policies/engines/index.d.cts +1 -1
- package/dist/policies/engines/index.d.ts +1 -1
- package/dist/policies/engines/v1/gate/index.d.cts +3 -13
- package/dist/policies/engines/v1/gate/index.d.ts +3 -13
- package/dist/policies/index.d.cts +1 -1
- package/dist/policies/index.d.ts +1 -1
- package/dist/policy-bridge.cjs +30 -2
- package/dist/policy-bridge.cjs.map +1 -1
- package/dist/policy-bridge.d.cts +18 -0
- package/dist/policy-bridge.d.ts +18 -0
- package/dist/policy-bridge.js +30 -2
- package/dist/policy-bridge.js.map +1 -1
- package/dist/policy-service.cjs +41 -46
- package/dist/policy-service.cjs.map +1 -1
- package/dist/policy-service.d.cts +72 -9
- package/dist/policy-service.d.ts +72 -9
- package/dist/policy-service.js +43 -48
- package/dist/policy-service.js.map +1 -1
- package/dist/requested-by.cjs +4 -4
- package/dist/requested-by.cjs.map +1 -1
- package/dist/requested-by.js +2 -2
- package/dist/requested-by.js.map +1 -1
- package/dist/result.cjs +29 -1
- package/dist/result.cjs.map +1 -1
- package/dist/result.d.cts +12 -0
- package/dist/result.d.ts +12 -0
- package/dist/result.js +29 -1
- package/dist/result.js.map +1 -1
- package/dist/rule.cjs +94 -24
- package/dist/rule.cjs.map +1 -1
- package/dist/rule.js +94 -24
- package/dist/rule.js.map +1 -1
- package/dist/ruleset-registry.cjs +19 -0
- package/dist/ruleset-registry.cjs.map +1 -1
- package/dist/ruleset-registry.js +19 -0
- package/dist/ruleset-registry.js.map +1 -1
- package/dist/stable-stringify.cjs +87 -0
- package/dist/stable-stringify.cjs.map +1 -0
- package/dist/stable-stringify.js +76 -0
- package/dist/stable-stringify.js.map +1 -0
- package/dist/temporal-snapshot.d.cts +87 -0
- package/dist/temporal-snapshot.d.ts +87 -0
- package/dist/temporal-use-case.cjs +174 -16
- package/dist/temporal-use-case.cjs.map +1 -1
- package/dist/temporal-use-case.d.cts +82 -44
- package/dist/temporal-use-case.d.ts +82 -44
- package/dist/temporal-use-case.js +167 -15
- package/dist/temporal-use-case.js.map +1 -1
- package/dist/unexpected-error.cjs +4 -2
- package/dist/unexpected-error.cjs.map +1 -1
- package/dist/unexpected-error.js +4 -2
- package/dist/unexpected-error.js.map +1 -1
- package/dist/uuid-identifier.cjs +141 -8
- package/dist/uuid-identifier.cjs.map +1 -1
- package/dist/uuid-identifier.d.cts +26 -7
- package/dist/uuid-identifier.d.ts +26 -7
- package/dist/uuid-identifier.js +135 -8
- package/dist/uuid-identifier.js.map +1 -1
- package/dist/uuid.cjs +23 -0
- package/dist/uuid.cjs.map +1 -0
- package/dist/uuid.js +18 -0
- package/dist/uuid.js.map +1 -0
- package/dist/validation-code.cjs +9 -0
- package/dist/validation-code.cjs.map +1 -1
- package/dist/validation-code.d.cts +3 -0
- package/dist/validation-code.d.ts +3 -0
- package/dist/validation-code.js +9 -0
- package/dist/validation-code.js.map +1 -1
- package/dist/validation-error.cjs +42 -67
- package/dist/validation-error.cjs.map +1 -1
- package/dist/validation-error.d.cts +29 -8
- package/dist/validation-error.d.ts +29 -8
- package/dist/validation-error.js +41 -66
- package/dist/validation-error.js.map +1 -1
- package/dist/validation-exception.cjs +17 -6
- package/dist/validation-exception.cjs.map +1 -1
- package/dist/validation-exception.d.cts +33 -9
- package/dist/validation-exception.d.ts +33 -9
- package/dist/validation-exception.js +17 -6
- package/dist/validation-exception.js.map +1 -1
- package/dist/validation-field.cjs +3 -0
- package/dist/validation-field.cjs.map +1 -1
- package/dist/validation-field.d.cts +1 -0
- package/dist/validation-field.d.ts +1 -0
- package/dist/validation-field.js +3 -0
- package/dist/validation-field.js.map +1 -1
- package/dist/value-object-ruleset.contracts.d.cts +10 -0
- package/dist/value-object-ruleset.contracts.d.ts +10 -0
- package/dist/value-object.cjs +23 -4
- package/dist/value-object.cjs.map +1 -1
- package/dist/value-object.d.cts +25 -1
- package/dist/value-object.d.ts +25 -1
- package/dist/value-object.js +23 -4
- package/dist/value-object.js.map +1 -1
- package/dist/version.d.cts +27 -0
- package/dist/version.d.ts +27 -0
- package/dist/versioning/index.d.cts +2 -2
- package/dist/versioning/index.d.ts +2 -2
- package/meta.json +5 -2
- package/package.json +1 -1
- package/src/core/abac/authorizer.ts +60 -10
- package/src/core/abac/domain/policy-set.ts +52 -5
- package/src/core/abac/domain/rule.ts +28 -16
- package/src/core/abac/index.ts +7 -1
- package/src/core/application/commands/command.ts +14 -1
- package/src/core/application/commands/requested-by.ts +13 -5
- package/src/core/application/index.ts +2 -1
- package/src/core/application/policy-error-mapper.ts +6 -0
- package/src/core/application/ports/index.ts +7 -0
- package/src/core/application/ports/temporal-repository.port.ts +35 -2
- package/src/core/application/queries/index.ts +1 -1
- package/src/core/application/queries/query.ts +25 -3
- package/src/core/application/temporal/temporal-use-case.ts +31 -4
- package/src/core/application/use-case.ts +45 -8
- package/src/core/config/core-config.ts +46 -25
- package/src/core/config/index.ts +1 -0
- package/src/core/config/policy-reporter.ts +32 -1
- package/src/core/domain/entity.ts +51 -8
- package/src/core/domain/rulesets/entity-ruleset.contracts.ts +0 -2
- package/src/core/domain/rulesets/ruleset-registry.ts +24 -0
- package/src/core/domain/rulesets/value-object-ruleset.contracts.ts +1 -7
- package/src/core/domain/temporal/half-open-interval.ts +34 -0
- package/src/core/domain/temporal/temporal-snapshot.ts +13 -2
- package/src/core/domain/temporal/transaction-time.ts +19 -15
- package/src/core/domain/temporal/valid-time.ts +20 -15
- package/src/core/domain/uuid-identifier.ts +6 -11
- package/src/core/domain/value-object.ts +29 -3
- package/src/core/errors/authentication-error.ts +5 -31
- package/src/core/errors/authorization-error.ts +12 -20
- package/src/core/errors/business-rule-violation-error.ts +4 -1
- package/src/core/errors/idempotency-error.ts +5 -2
- package/src/core/errors/index.ts +4 -0
- package/src/core/errors/integration-error.ts +4 -24
- package/src/core/errors/legacy-incompatible-error.ts +1 -0
- package/src/core/errors/not-found-error.ts +4 -1
- package/src/core/errors/temporal-error.ts +22 -20
- package/src/core/errors/unexpected-error.ts +5 -1
- package/src/core/errors/utils/factory-helpers.ts +70 -0
- package/src/core/errors/utils/index.ts +5 -0
- package/src/core/errors/utils/json-safe.ts +35 -12
- package/src/core/errors/validation-error.ts +4 -1
- package/src/core/exceptions/business-rule-violation-exception.ts +2 -1
- package/src/core/exceptions/domain-exception.ts +9 -1
- package/src/core/exceptions/entity-not-found-exception.ts +5 -1
- package/src/core/exceptions/invalid-state-transition-exception.ts +5 -2
- package/src/core/exceptions/invariant-violation-exception.ts +2 -2
- package/src/core/exceptions/validation-code.ts +14 -0
- package/src/core/exceptions/validation-exception.ts +44 -5
- package/src/core/exceptions/validation-field.ts +11 -1
- package/src/core/plugins/plugin.ts +25 -15
- package/src/core/plugins/types.ts +4 -2
- package/src/core/policies/asof/asof.ts +12 -0
- package/src/core/policies/catalog/policy-catalog-entry.ts +3 -1
- package/src/core/policies/catalog/policy-catalog.ts +5 -1
- package/src/core/policies/context/context-builder.ts +20 -0
- package/src/core/policies/context/context-resolver.ts +5 -0
- package/src/core/policies/context/context-seed.ts +11 -0
- package/src/core/policies/defs/in-memory-policy-definition-repo.ts +0 -2
- package/src/core/policies/engines/parse-gate-payload.ts +9 -0
- package/src/core/policies/engines/v1/condition-date-operands.ts +112 -0
- package/src/core/policies/engines/v1/condition-evaluator.ts +120 -291
- package/src/core/policies/engines/v1/condition-schema.ts +23 -19
- package/src/core/policies/engines/v1/condition-types.ts +18 -11
- package/src/core/policies/index.ts +4 -6
- package/src/core/policies/service/policy-service.ts +46 -35
- package/src/core/policies/utils/hash.ts +5 -69
- package/src/core/policies/utils/result.ts +1 -1
- package/src/core/rbac/access-request.ts +12 -2
- package/src/core/rbac/authorizer.ts +8 -1
- package/src/core/rbac/domain/role.ts +20 -0
- package/src/core/rbac/domain/scope.ts +6 -1
- package/src/core/result/index.ts +5 -0
- package/src/core/result/outcome.ts +5 -7
- package/src/core/result/result.ts +34 -1
- package/src/core/shared/hashing.ts +6 -57
- package/src/core/shared/immutable.ts +22 -4
- package/src/core/shared/stable-stringify.ts +144 -0
- package/src/core/shared/uuid.ts +16 -0
- package/src/core/versioning/domain-event-contracts.ts +43 -15
- package/src/core/versioning/index.ts +1 -0
- package/src/core/versioning/version.ts +30 -1
- package/src/domain/index.ts +5 -0
- package/src/examples/rulesets/entity/order-creation-rules-v1.ts +1 -1
- package/src/examples/rulesets/entity/order-invariants-v1.ts +2 -4
- package/src/examples/rulesets/entity/order-invariants-v2.ts +2 -4
- package/src/examples/rulesets/value-object/cpf-rules-v1.ts +2 -4
- package/src/examples/rulesets/value-object/cpf-rules-v2.ts +2 -4
- package/src/examples/rulesets/value-object/person-name-rules-v1.ts +2 -4
- package/src/examples/rulesets/value-object/person-name-rules-v2.ts +2 -4
- package/src/result/index.ts +7 -2
- package/src/version.ts +1 -1
- package/dist/domain-exception.d.cts +0 -7
- package/dist/domain-exception.d.ts +0 -7
- package/dist/entity.cjs +0 -126
- package/dist/entity.cjs.map +0 -1
- package/dist/entity.js +0 -115
- package/dist/entity.js.map +0 -1
- package/dist/use-case.cjs +0 -96
- package/dist/use-case.cjs.map +0 -1
- package/dist/use-case.js +0 -91
- package/dist/use-case.js.map +0 -1
|
@@ -23,11 +23,7 @@ import { PolicyResolver } from "../resolver/index.js";
|
|
|
23
23
|
import { Result } from "../../result/result.js";
|
|
24
24
|
import { isValidDate } from "../../shared/temporal-guards.js";
|
|
25
25
|
|
|
26
|
-
import type
|
|
27
|
-
PolicyEvent,
|
|
28
|
-
PolicyReporter,
|
|
29
|
-
} from "../../config/policy-reporter.js";
|
|
30
|
-
import { SilentPolicyReporter } from "../../config/silent-policy-reporter.js";
|
|
26
|
+
import { type CoreConfig, coreConfig } from "../../config/index.js";
|
|
31
27
|
|
|
32
28
|
import {
|
|
33
29
|
PolicyEvaluationErrors,
|
|
@@ -42,6 +38,21 @@ import {
|
|
|
42
38
|
* `contextVersion` pins which context schema applies, and `seed` carries the
|
|
43
39
|
* raw facts the context builder expands. `decisionId` is the caller's
|
|
44
40
|
* idempotency/audit handle, echoed back on the result.
|
|
41
|
+
*
|
|
42
|
+
* `contextVersion` is a caller assertion, not a proof: it filters which
|
|
43
|
+
* definitions are compatible, but nothing checks that the context actually
|
|
44
|
+
* built matches the declared version — supplying the right number is the
|
|
45
|
+
* caller's contract.
|
|
46
|
+
*
|
|
47
|
+
* Two `seed.fields` keys are reserved and change what "now" means for this
|
|
48
|
+
* evaluation:
|
|
49
|
+
* - `now` (a `Date`) pins the evaluation clock — it feeds `evaluatedAt`, the
|
|
50
|
+
* `asOf` derivation, and therefore which effective-dated definitions are in
|
|
51
|
+
* force. Leave it unset to use wall-clock time.
|
|
52
|
+
* - `asOf` (a `Date`) is consumed when the policy's `asOfSource` is
|
|
53
|
+
* `CALLER_PROVIDED` (see {@link PolicyAsOfResolver}).
|
|
54
|
+
* Whoever assembles `fields` from untrusted input owns the consequence: a
|
|
55
|
+
* caller who can set these keys can shift which policy version is selected.
|
|
45
56
|
*/
|
|
46
57
|
export interface EvaluateInput {
|
|
47
58
|
readonly decisionId: PolicyDecisionId;
|
|
@@ -52,14 +63,13 @@ export interface EvaluateInput {
|
|
|
52
63
|
}
|
|
53
64
|
|
|
54
65
|
/**
|
|
55
|
-
* Cross-cutting knobs for a {@link PolicyService}: how the as-of
|
|
56
|
-
* derived ({@link asOf})
|
|
57
|
-
*
|
|
58
|
-
*
|
|
66
|
+
* Cross-cutting knobs for a {@link PolicyService}: today just how the as-of
|
|
67
|
+
* instant is derived ({@link asOf}). Telemetry is not configured here — it
|
|
68
|
+
* flows through the injected {@link CoreConfig}, the core's single
|
|
69
|
+
* observability seam, so a host wires reporting in one place.
|
|
59
70
|
*/
|
|
60
71
|
export interface PolicyServiceOptions {
|
|
61
72
|
readonly asOf?: DeriveAsOfOptions;
|
|
62
|
-
readonly reporter?: PolicyReporter;
|
|
63
73
|
}
|
|
64
74
|
|
|
65
75
|
/**
|
|
@@ -75,6 +85,12 @@ export interface PolicyServiceParams {
|
|
|
75
85
|
readonly resolver: PolicyResolver;
|
|
76
86
|
readonly gateEngines: GateEngineRegistry;
|
|
77
87
|
readonly computeRegistry: ComputeRegistry;
|
|
88
|
+
/**
|
|
89
|
+
* The observability seam telemetry is emitted through. Defaults to the
|
|
90
|
+
* shared {@link coreConfig} singleton; inject an isolated instance for
|
|
91
|
+
* tests or multi-tenant hosts.
|
|
92
|
+
*/
|
|
93
|
+
readonly coreConfig?: CoreConfig;
|
|
78
94
|
readonly options?: PolicyServiceOptions;
|
|
79
95
|
}
|
|
80
96
|
|
|
@@ -89,8 +105,11 @@ export type { PolicyEvaluationError } from "./policy-evaluation-error.js";
|
|
|
89
105
|
* The full record of one successful evaluation. Beyond the business
|
|
90
106
|
* {@link decision}, it pins the exact definition that produced it — id, key,
|
|
91
107
|
* version, payload hash, and engine version — together with the `asOf` instant
|
|
92
|
-
* the policy was selected for and the
|
|
93
|
-
* is
|
|
108
|
+
* the policy was selected for and `evaluatedAt`, the instant evaluation ran at.
|
|
109
|
+
* `evaluatedAt` is wall-clock time unless the caller pinned it via
|
|
110
|
+
* `seed.fields.now` (see {@link EvaluateInput}), in which case it echoes that.
|
|
111
|
+
* That provenance is what makes a decision reproducible and auditable after the
|
|
112
|
+
* fact.
|
|
94
113
|
*/
|
|
95
114
|
export interface PolicyEvaluationResult {
|
|
96
115
|
readonly decisionId: PolicyDecisionId;
|
|
@@ -130,7 +149,7 @@ interface ResolvedPolicyCandidate {
|
|
|
130
149
|
* evaluation — which keeps observability strictly side-band.
|
|
131
150
|
*/
|
|
132
151
|
export class PolicyService {
|
|
133
|
-
readonly
|
|
152
|
+
private readonly coreConfig: CoreConfig;
|
|
134
153
|
private readonly catalog: PolicyCatalog;
|
|
135
154
|
private readonly contextBuilder: PolicyContextBuilder;
|
|
136
155
|
private readonly defRepo: PolicyDefinitionRepository;
|
|
@@ -147,15 +166,7 @@ export class PolicyService {
|
|
|
147
166
|
this.gateEngines = params.gateEngines;
|
|
148
167
|
this.computeRegistry = params.computeRegistry;
|
|
149
168
|
this.options = params.options ?? {};
|
|
150
|
-
this
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
#reportSafely(event: PolicyEvent): void {
|
|
154
|
-
try {
|
|
155
|
-
this.#reporter.report(event);
|
|
156
|
-
} catch {
|
|
157
|
-
// Falhas de telemetria nao podem interromper a avaliacao de politicas.
|
|
158
|
-
}
|
|
169
|
+
this.coreConfig = params.coreConfig ?? coreConfig;
|
|
159
170
|
}
|
|
160
171
|
|
|
161
172
|
private resolveEvaluationNow(seed: ContextSeed): Result<Date, string> {
|
|
@@ -192,10 +203,10 @@ export class PolicyService {
|
|
|
192
203
|
async evaluate(
|
|
193
204
|
input: EvaluateInput,
|
|
194
205
|
): Promise<Result<PolicyEvaluationResult, PolicyEvaluationError>> {
|
|
195
|
-
const result = await this
|
|
206
|
+
const result = await this.runEvaluation(input);
|
|
196
207
|
if (result.isErr()) {
|
|
197
208
|
const error = result.errorOrNull()!;
|
|
198
|
-
this
|
|
209
|
+
this.coreConfig.reportSafely({
|
|
199
210
|
kind: "policy-evaluation-failed",
|
|
200
211
|
level: "error",
|
|
201
212
|
policyKey: "policyKey" in error ? error.policyKey : undefined,
|
|
@@ -206,7 +217,7 @@ export class PolicyService {
|
|
|
206
217
|
});
|
|
207
218
|
} else {
|
|
208
219
|
const ok = result.getOrNull()!;
|
|
209
|
-
this
|
|
220
|
+
this.coreConfig.reportSafely({
|
|
210
221
|
kind: "policy-evaluation-completed",
|
|
211
222
|
level: "info",
|
|
212
223
|
policyKey: ok.ref.policyKey,
|
|
@@ -218,7 +229,7 @@ export class PolicyService {
|
|
|
218
229
|
return result;
|
|
219
230
|
}
|
|
220
231
|
|
|
221
|
-
async
|
|
232
|
+
private async runEvaluation(
|
|
222
233
|
input: EvaluateInput,
|
|
223
234
|
): Promise<Result<PolicyEvaluationResult, PolicyEvaluationError>> {
|
|
224
235
|
const seedResult = ContextSeedValidator.validate(input.seed);
|
|
@@ -233,7 +244,7 @@ export class PolicyService {
|
|
|
233
244
|
}
|
|
234
245
|
const seed = seedResult.getOrNull()!;
|
|
235
246
|
|
|
236
|
-
const catalogFamilyResult = this
|
|
247
|
+
const catalogFamilyResult = this.resolveCatalogFamily(input.policyKey);
|
|
237
248
|
if (catalogFamilyResult.isErr()) {
|
|
238
249
|
return Result.err(catalogFamilyResult.errorOrNull()!);
|
|
239
250
|
}
|
|
@@ -269,7 +280,7 @@ export class PolicyService {
|
|
|
269
280
|
}
|
|
270
281
|
const asOf = asOfResult.getOrNull()!;
|
|
271
282
|
|
|
272
|
-
const definitionResult = this
|
|
283
|
+
const definitionResult = this.findCandidates(
|
|
273
284
|
policyKey,
|
|
274
285
|
familyEntry.kind,
|
|
275
286
|
asOf,
|
|
@@ -283,7 +294,7 @@ export class PolicyService {
|
|
|
283
294
|
catalogEntry: versionedCatalogEntry,
|
|
284
295
|
} = definitionResult.getOrNull()!;
|
|
285
296
|
|
|
286
|
-
this
|
|
297
|
+
this.coreConfig.reportSafely({
|
|
287
298
|
kind: "policy-resolution",
|
|
288
299
|
level: "info",
|
|
289
300
|
policyKey,
|
|
@@ -311,7 +322,7 @@ export class PolicyService {
|
|
|
311
322
|
}
|
|
312
323
|
const context = ctxResult.getOrNull()!;
|
|
313
324
|
|
|
314
|
-
const decisionResult = this
|
|
325
|
+
const decisionResult = this.executeEngine(
|
|
315
326
|
policyKey,
|
|
316
327
|
policyDefinition,
|
|
317
328
|
context,
|
|
@@ -347,7 +358,7 @@ export class PolicyService {
|
|
|
347
358
|
return Result.ok(result);
|
|
348
359
|
}
|
|
349
360
|
|
|
350
|
-
|
|
361
|
+
private resolveCatalogVariant(
|
|
351
362
|
definition: PolicyDefinition,
|
|
352
363
|
): Result<PolicyCatalogEntry, string> {
|
|
353
364
|
return this.catalog.getVersioned({
|
|
@@ -362,7 +373,7 @@ export class PolicyService {
|
|
|
362
373
|
});
|
|
363
374
|
}
|
|
364
375
|
|
|
365
|
-
|
|
376
|
+
private resolveCatalogFamily(
|
|
366
377
|
rawKey: string,
|
|
367
378
|
): Result<
|
|
368
379
|
{ key: string; family: readonly PolicyCatalogEntry[] },
|
|
@@ -395,7 +406,7 @@ export class PolicyService {
|
|
|
395
406
|
});
|
|
396
407
|
}
|
|
397
408
|
|
|
398
|
-
|
|
409
|
+
private findCandidates(
|
|
399
410
|
policyKey: string,
|
|
400
411
|
policyKind: PolicyCatalogEntry["kind"],
|
|
401
412
|
asOf: Date,
|
|
@@ -420,7 +431,7 @@ export class PolicyService {
|
|
|
420
431
|
>();
|
|
421
432
|
|
|
422
433
|
for (const candidate of candidates) {
|
|
423
|
-
const compatibilityResult = this
|
|
434
|
+
const compatibilityResult = this.resolveCatalogVariant(candidate);
|
|
424
435
|
if (compatibilityResult.isErr()) {
|
|
425
436
|
incompatibleVariantErrorsByDefinitionId.set(
|
|
426
437
|
candidate.id,
|
|
@@ -503,7 +514,7 @@ export class PolicyService {
|
|
|
503
514
|
return Result.ok(resolvedCandidate);
|
|
504
515
|
}
|
|
505
516
|
|
|
506
|
-
|
|
517
|
+
private executeEngine(
|
|
507
518
|
policyKey: string,
|
|
508
519
|
definition: PolicyDefinition,
|
|
509
520
|
context: PolicyContext,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { sha256Hex
|
|
2
|
-
import {
|
|
1
|
+
import { sha256Hex } from "../../shared/hashing.js";
|
|
2
|
+
import { canonicalStringify } from "../../shared/stable-stringify.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Produces a deterministic SHA-256 hex digest of a canonical JSON representation.
|
|
@@ -7,6 +7,8 @@ import { isValidDate } from "../../shared/temporal-guards.js";
|
|
|
7
7
|
* Array item order is preserved on purpose; callers that treat arrays as sets
|
|
8
8
|
* must normalize or sort them before hashing.
|
|
9
9
|
*
|
|
10
|
+
* The strict canonical form is owned by `shared/stable-stringify`
|
|
11
|
+
* ({@link canonicalStringify}); this class just composes it with SHA-256.
|
|
10
12
|
* Values that JSON.stringify would silently drop or coerce (undefined,
|
|
11
13
|
* non-finite numbers, functions, symbols, bigint) are rejected up-front to
|
|
12
14
|
* prevent semantically different payloads from collapsing to the same hash.
|
|
@@ -16,74 +18,8 @@ export class PolicyHashing {
|
|
|
16
18
|
return sha256Hex(input);
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
private static assertHashable(
|
|
20
|
-
value: unknown,
|
|
21
|
-
path: string,
|
|
22
|
-
seen: WeakSet<object>,
|
|
23
|
-
): void {
|
|
24
|
-
if (value === null) {
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const type = typeof value;
|
|
29
|
-
|
|
30
|
-
if (type === "undefined") {
|
|
31
|
-
throw new TypeError(
|
|
32
|
-
`canonicalJson does not accept undefined values (at ${path})`,
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (type === "number" && !Number.isFinite(value)) {
|
|
37
|
-
throw new TypeError(
|
|
38
|
-
`canonicalJson does not accept non-finite numbers (at ${path}, value: ${String(value)})`,
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (type === "bigint" || type === "function" || type === "symbol") {
|
|
43
|
-
throw new TypeError(
|
|
44
|
-
`canonicalJson does not accept ${type} values (at ${path})`,
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (type !== "object") {
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (value instanceof Date) {
|
|
53
|
-
if (!isValidDate(value)) {
|
|
54
|
-
throw new TypeError(
|
|
55
|
-
`canonicalJson does not accept Invalid Date (at ${path})`,
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (seen.has(value as object)) {
|
|
62
|
-
throw new TypeError(
|
|
63
|
-
`canonicalJson does not accept circular references (at ${path})`,
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
seen.add(value as object);
|
|
67
|
-
|
|
68
|
-
if (Array.isArray(value)) {
|
|
69
|
-
value.forEach((item, index) => {
|
|
70
|
-
PolicyHashing.assertHashable(item, `${path}[${index}]`, seen);
|
|
71
|
-
});
|
|
72
|
-
} else {
|
|
73
|
-
for (const [key, nested] of Object.entries(
|
|
74
|
-
value as Record<string, unknown>,
|
|
75
|
-
)) {
|
|
76
|
-
PolicyHashing.assertHashable(nested, `${path}.${key}`, seen);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
seen.delete(value as object);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
21
|
static canonicalJson(value: unknown): string {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return stableStringify(value);
|
|
22
|
+
return canonicalStringify(value);
|
|
87
23
|
}
|
|
88
24
|
|
|
89
25
|
static computePayloadHash(
|
|
@@ -22,10 +22,20 @@ interface AccessRequest {
|
|
|
22
22
|
*/
|
|
23
23
|
readonly required: Permission;
|
|
24
24
|
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* The target resource, identified without leaking its payload. Carried for
|
|
27
|
+
* audit metadata only — the RBAC decisor does **not** check it. Per-resource
|
|
28
|
+
* ownership (this actor may cancel *this* order) is an ABAC/policy concern,
|
|
29
|
+
* not a role/permission one.
|
|
30
|
+
*/
|
|
26
31
|
readonly resource?: { readonly type: string; readonly id?: string };
|
|
27
32
|
|
|
28
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* The scope the resource lives in, e.g. `Scope.of("school:123")`. Asking in
|
|
35
|
+
* the global scope (`Scope.global()`) means "may I do this *everywhere*?" and
|
|
36
|
+
* so requires a globally-scoped grant — it is **not** "in any scope I happen
|
|
37
|
+
* to hold". Ask about a concrete scope to check a bounded grant.
|
|
38
|
+
*/
|
|
29
39
|
readonly scope: Scope;
|
|
30
40
|
}
|
|
31
41
|
|
|
@@ -36,6 +36,13 @@ class RbacAuthorizer {
|
|
|
36
36
|
* 3. grants the permission but not in scope → `out_of_scope`;
|
|
37
37
|
* 4. otherwise → ALLOW (`Result.ok`).
|
|
38
38
|
*
|
|
39
|
+
* ALLOW is intentionally silent: `Result.ok(undefined)` carries no trace of
|
|
40
|
+
* *which* grant or role decided it. Denials are rich (see the metadata
|
|
41
|
+
* below) because they need to explain themselves; an allow does not. When an
|
|
42
|
+
* ERP needs a "who allowed this, and why" trail, that is the adapter's job —
|
|
43
|
+
* reconstruct it from the grants it loaded, or gate on `PermissionSet` before
|
|
44
|
+
* calling. Keeping the decisor's allow-path pure is the deliberate trade.
|
|
45
|
+
*
|
|
39
46
|
* @throws {@link InvalidValueException} when `request.required` is a wildcard
|
|
40
47
|
* permission (only *granted* permissions may wildcard, never the *required*
|
|
41
48
|
* one).
|
|
@@ -59,7 +66,7 @@ class RbacAuthorizer {
|
|
|
59
66
|
const metadata = {
|
|
60
67
|
action: request.action,
|
|
61
68
|
resource: request.resource,
|
|
62
|
-
actor: {
|
|
69
|
+
actor: { actorId: request.actor.raw },
|
|
63
70
|
required,
|
|
64
71
|
};
|
|
65
72
|
|
|
@@ -83,6 +83,26 @@ class Role extends ValueObject<RoleProps, RoleProps> {
|
|
|
83
83
|
return this._permissions;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Two roles are equal when they share a name and the same *set* of
|
|
88
|
+
* permissions. Order-insensitive on purpose: a role is a bundle, not a
|
|
89
|
+
* sequence, so `Role.of("cashier", [read, cancel])` equals
|
|
90
|
+
* `Role.of("cashier", [cancel, read])`. Overrides the base
|
|
91
|
+
* {@link ValueObject.equals}, whose stable-stringify keeps array order and
|
|
92
|
+
* would call those two roles different. The stored order (and thus
|
|
93
|
+
* {@link permissions}/{@link toPrimitive}) is still first-seen; only the
|
|
94
|
+
* comparison sorts.
|
|
95
|
+
*/
|
|
96
|
+
override equals(other: this): boolean {
|
|
97
|
+
if (this.value.name !== other.value.name) return false;
|
|
98
|
+
const mine = [...this.value.permissions].sort();
|
|
99
|
+
const theirs = [...other.value.permissions].sort();
|
|
100
|
+
return (
|
|
101
|
+
mine.length === theirs.length &&
|
|
102
|
+
mine.every((permission, index) => permission === theirs[index])
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
86
106
|
/** Whether any permission in this role {@link Permission.implies | implies} `required`. */
|
|
87
107
|
grants(required: Permission): boolean {
|
|
88
108
|
return this._permissions.some((permission) =>
|
|
@@ -29,7 +29,12 @@ class Scope extends ValueObject<ScopeProps, string> {
|
|
|
29
29
|
this.finalize();
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Validates `"type:id"` or the global `"*"`, throwing on anything else.
|
|
34
|
+
* `Scope.of("*")` is accepted and is equivalent to {@link Scope.global} —
|
|
35
|
+
* prefer `global()` at call-sites for intent, `of("*")` when rehydrating a
|
|
36
|
+
* stored string.
|
|
37
|
+
*/
|
|
33
38
|
static of(raw: string): Scope {
|
|
34
39
|
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
35
40
|
throw new InvalidValueException(
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// core/domain/outcome.ts
|
|
2
|
-
|
|
3
1
|
import { InvariantViolationException } from "../exceptions/invariant-violation-exception.js";
|
|
4
2
|
|
|
5
3
|
/**
|
|
@@ -59,32 +57,32 @@ export class Outcome<S extends string, D = undefined> {
|
|
|
59
57
|
data: D,
|
|
60
58
|
reason?: string,
|
|
61
59
|
): Outcome<"APPROVED", D> {
|
|
62
|
-
return
|
|
60
|
+
return Outcome.of("APPROVED", data, reason);
|
|
63
61
|
}
|
|
64
62
|
|
|
65
63
|
static rejected<D = undefined>(
|
|
66
64
|
data: D,
|
|
67
65
|
reason: string,
|
|
68
66
|
): Outcome<"REJECTED", D> {
|
|
69
|
-
return
|
|
67
|
+
return Outcome.of("REJECTED", data, reason);
|
|
70
68
|
}
|
|
71
69
|
|
|
72
70
|
static noOp<D = undefined>(data: D, reason?: string): Outcome<"NO_OP", D> {
|
|
73
|
-
return
|
|
71
|
+
return Outcome.of("NO_OP", data, reason);
|
|
74
72
|
}
|
|
75
73
|
|
|
76
74
|
static deferred<D = undefined>(
|
|
77
75
|
data: D,
|
|
78
76
|
reason: string,
|
|
79
77
|
): Outcome<"DEFERRED", D> {
|
|
80
|
-
return
|
|
78
|
+
return Outcome.of("DEFERRED", data, reason);
|
|
81
79
|
}
|
|
82
80
|
|
|
83
81
|
static requiresReview<D = undefined>(
|
|
84
82
|
data: D,
|
|
85
83
|
reason: string,
|
|
86
84
|
): Outcome<"REQUIRES_REVIEW", D> {
|
|
87
|
-
return
|
|
85
|
+
return Outcome.of("REQUIRES_REVIEW", data, reason);
|
|
88
86
|
}
|
|
89
87
|
|
|
90
88
|
// ─── Type narrowing ────────────────────────────────────────────────
|
|
@@ -41,6 +41,9 @@ export abstract class Result<T, E> {
|
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* Returns the value when this is success, or null when this is an error.
|
|
44
|
+
*
|
|
45
|
+
* Note: an `Ok(null)` also returns `null`, indistinguishable from an `Err`.
|
|
46
|
+
* Use `match`/`isOk` when that distinction matters.
|
|
44
47
|
*/
|
|
45
48
|
getOrNull(): T | null {
|
|
46
49
|
return this.match({
|
|
@@ -66,13 +69,15 @@ export abstract class Result<T, E> {
|
|
|
66
69
|
return this.match({
|
|
67
70
|
ok: (value) => value,
|
|
68
71
|
err: (error) => {
|
|
69
|
-
throw
|
|
72
|
+
throw toError(error);
|
|
70
73
|
},
|
|
71
74
|
});
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
/**
|
|
75
78
|
* Transforms the success value using the provided function. Keeps the error when this is a failure.
|
|
79
|
+
*
|
|
80
|
+
* Exceptions thrown by `transform` propagate; they are not captured into an `Err`.
|
|
76
81
|
*/
|
|
77
82
|
map<R>(transform: (value: T) => R): Result<R, E> {
|
|
78
83
|
return this.match<Result<R, E>>({
|
|
@@ -85,6 +90,8 @@ export abstract class Result<T, E> {
|
|
|
85
90
|
|
|
86
91
|
/**
|
|
87
92
|
* Transforms the error using the provided function. Keeps the value when this is a success.
|
|
93
|
+
*
|
|
94
|
+
* Exceptions thrown by `transform` propagate; they are not captured into an `Err`.
|
|
88
95
|
*/
|
|
89
96
|
mapError<F>(transform: (error: E) => F): Result<T, F> {
|
|
90
97
|
return this.match<Result<T, F>>({
|
|
@@ -96,6 +103,8 @@ export abstract class Result<T, E> {
|
|
|
96
103
|
|
|
97
104
|
/**
|
|
98
105
|
* Chains another operation that returns a Result when this result is a success.
|
|
106
|
+
*
|
|
107
|
+
* Exceptions thrown by `transform` propagate; they are not captured into an `Err`.
|
|
99
108
|
*/
|
|
100
109
|
flatMap<R>(transform: (value: T) => Result<R, E>): Result<R, E> {
|
|
101
110
|
return this.match<Result<R, E>>({
|
|
@@ -107,6 +116,9 @@ export abstract class Result<T, E> {
|
|
|
107
116
|
|
|
108
117
|
/**
|
|
109
118
|
* Success variant of Result.
|
|
119
|
+
*
|
|
120
|
+
* The instance is frozen, but the freeze is shallow: a contained mutable
|
|
121
|
+
* object is not frozen.
|
|
110
122
|
*/
|
|
111
123
|
export class Ok<T> extends Result<T, never> {
|
|
112
124
|
public readonly value: T;
|
|
@@ -154,3 +166,24 @@ export class Err<E> extends Result<never, E> {
|
|
|
154
166
|
return handlers.err(this.error);
|
|
155
167
|
}
|
|
156
168
|
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Coerces an arbitrary error value into an Error for `getOrThrow`, preserving
|
|
172
|
+
* the original structured payload as `cause` so nothing is lost when `E` is a
|
|
173
|
+
* plain object/DTO rather than an Error.
|
|
174
|
+
*/
|
|
175
|
+
function toError(error: unknown): Error {
|
|
176
|
+
if (error instanceof Error) return error;
|
|
177
|
+
if (typeof error === "string") return new Error(error);
|
|
178
|
+
let message: string | undefined;
|
|
179
|
+
try {
|
|
180
|
+
message = JSON.stringify(error);
|
|
181
|
+
} catch {
|
|
182
|
+
// circular / BigInt / etc.
|
|
183
|
+
}
|
|
184
|
+
const wrapped = new Error(message ?? String(error));
|
|
185
|
+
// Attach the original payload as `cause` (assigned, not passed to the
|
|
186
|
+
// ES2022 constructor option, to stay within the ES2020 lib target).
|
|
187
|
+
(wrapped as Error & { cause?: unknown }).cause = error;
|
|
188
|
+
return wrapped;
|
|
189
|
+
}
|
|
@@ -1,67 +1,16 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
"Cannot stably stringify a circular structure";
|
|
3
|
+
import { canonicalStringify } 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
|
-
//
|
|
11
|
-
//
|
|
12
|
-
|
|
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
|
-
|
|
9
|
+
// Strict by design: hashes the collision-resistant canonical form, so payloads
|
|
10
|
+
// that differ only in values `JSON.stringify` would drop (undefined, NaN, …)
|
|
11
|
+
// produce distinct digests instead of silently colliding.
|
|
63
12
|
function payloadHash(value: unknown): string {
|
|
64
|
-
return sha256Hex(
|
|
13
|
+
return sha256Hex(canonicalStringify(value));
|
|
65
14
|
}
|
|
66
15
|
|
|
67
|
-
export { payloadHash, sha256Hex
|
|
16
|
+
export { payloadHash, sha256Hex };
|
|
@@ -26,6 +26,12 @@ function deepFreezeInternal<T>(value: T, seen: WeakSet<object>): T {
|
|
|
26
26
|
// property — freezing it is a no-op against mutation. `makeImmutable`
|
|
27
27
|
// already hands us a `structuredClone` copy, so the caller's original Date
|
|
28
28
|
// is never aliased; deep-freezing of nested Dates is deliberately skipped.
|
|
29
|
+
//
|
|
30
|
+
// The same no-op caveat applies to `Map`/`Set`: `Object.freeze` locks the
|
|
31
|
+
// wrapper's own properties but not `.set`/`.delete`/`.clear`, which mutate
|
|
32
|
+
// internal slots. A frozen Map is still mutable through its methods. We do
|
|
33
|
+
// not neuter those methods here (no value object wraps a Map today); if one
|
|
34
|
+
// ever does, its immutability guarantee would be false — handle it there.
|
|
29
35
|
if (typeof value !== "object" || value === null || value instanceof Date) {
|
|
30
36
|
return value;
|
|
31
37
|
}
|
|
@@ -48,7 +54,17 @@ function deepFreezeInternal<T>(value: T, seen: WeakSet<object>): T {
|
|
|
48
54
|
const objectValue = value as Record<PropertyKey, object | PrimitiveValue>;
|
|
49
55
|
|
|
50
56
|
for (const propertyKey of Reflect.ownKeys(objectValue)) {
|
|
51
|
-
|
|
57
|
+
// Read through the descriptor rather than `objectValue[propertyKey]` so
|
|
58
|
+
// accessor properties are not invoked — a getter could have side effects
|
|
59
|
+
// or throw, and its computed result can't be frozen anyway. Only data
|
|
60
|
+
// properties hold a nested value worth recursing into.
|
|
61
|
+
const descriptor = Object.getOwnPropertyDescriptor(
|
|
62
|
+
objectValue,
|
|
63
|
+
propertyKey,
|
|
64
|
+
);
|
|
65
|
+
if (descriptor && "value" in descriptor) {
|
|
66
|
+
deepFreezeInternal(descriptor.value, seen);
|
|
67
|
+
}
|
|
52
68
|
}
|
|
53
69
|
|
|
54
70
|
return Object.freeze(value);
|
|
@@ -63,9 +79,11 @@ function makeImmutable<T>(value: T): DeepReadonly<T> {
|
|
|
63
79
|
try {
|
|
64
80
|
snapshot = structuredClone(value);
|
|
65
81
|
} catch (error) {
|
|
66
|
-
// `structuredClone` throws `DataCloneError` for functions
|
|
67
|
-
//
|
|
68
|
-
//
|
|
82
|
+
// `structuredClone` throws `DataCloneError` for functions and symbols.
|
|
83
|
+
// (Class instances do NOT throw — they are silently flattened to plain
|
|
84
|
+
// objects, prototype lost.) Surface the error as a domain invariant
|
|
85
|
+
// instead of leaking a host-specific runtime error to callers building
|
|
86
|
+
// value objects.
|
|
69
87
|
throw new InvariantViolationException(
|
|
70
88
|
`makeImmutable requires a structured-cloneable value: ${
|
|
71
89
|
error instanceof Error ? error.message : String(error)
|