@cullet/erp-core 1.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 +39 -0
- package/README.md +110 -0
- package/dist/errors/index.d.ts +2 -0
- package/dist/errors/index.js +3 -0
- package/dist/exceptions/validation-field.d.ts +2 -0
- package/dist/exceptions/validation-field.js +2 -0
- package/dist/gate-engine-registry.js +260 -0
- package/dist/gate-engine-registry.js.map +1 -0
- package/dist/gate-types.d.ts +307 -0
- package/dist/gate-v1-payload.schema.js +677 -0
- package/dist/gate-v1-payload.schema.js.map +1 -0
- package/dist/index.d.ts +88 -0
- package/dist/index.js +153 -0
- package/dist/index.js.map +1 -0
- package/dist/outcome.js +92 -0
- package/dist/outcome.js.map +1 -0
- package/dist/parse-gate-payload.d.ts +136 -0
- package/dist/policies/engines/index.d.ts +3 -0
- package/dist/policies/engines/index.js +2 -0
- package/dist/policies/engines/v1/gate/index.d.ts +115 -0
- package/dist/policies/engines/v1/gate/index.js +84 -0
- package/dist/policies/engines/v1/gate/index.js.map +1 -0
- package/dist/policies/index.d.ts +4 -0
- package/dist/policies/index.js +5 -0
- package/dist/policy-service.d.ts +541 -0
- package/dist/policy-service.js +992 -0
- package/dist/policy-service.js.map +1 -0
- package/dist/validation-code.js +210 -0
- package/dist/validation-code.js.map +1 -0
- package/dist/validation-error.d.ts +686 -0
- package/dist/validation-error.js +757 -0
- package/dist/validation-error.js.map +1 -0
- package/dist/validation-field.d.ts +12 -0
- package/dist/validation-field.js +23 -0
- package/dist/validation-field.js.map +1 -0
- package/meta.json +66 -0
- package/package.json +59 -0
- package/src/core/application/commands/command.ts +28 -0
- package/src/core/application/commands/index.ts +2 -0
- package/src/core/application/commands/requested-by.ts +97 -0
- package/src/core/application/index.ts +31 -0
- package/src/core/application/policy-error-mapper.ts +60 -0
- package/src/core/application/ports/index.ts +14 -0
- package/src/core/application/ports/logger.port.ts +10 -0
- package/src/core/application/ports/metrics.port.ts +9 -0
- package/src/core/application/ports/policy-port.ts +20 -0
- package/src/core/application/ports/repository.port.ts +7 -0
- package/src/core/application/ports/temporal-repository.port.ts +14 -0
- package/src/core/application/ports/tracer.port.ts +16 -0
- package/src/core/application/queries/index.ts +2 -0
- package/src/core/application/queries/query.ts +53 -0
- package/src/core/application/temporal/index.ts +11 -0
- package/src/core/application/temporal/temporal-context.ts +41 -0
- package/src/core/application/temporal/temporal-use-case.ts +41 -0
- package/src/core/application/use-case.ts +24 -0
- package/src/core/config/core-config.instance.ts +14 -0
- package/src/core/config/core-config.ts +87 -0
- package/src/core/config/index.ts +12 -0
- package/src/core/config/policy-reporter.ts +60 -0
- package/src/core/config/silent-policy-reporter.ts +5 -0
- package/src/core/domain/entity.ts +69 -0
- package/src/core/domain/rulesets/entity-ruleset.contracts.ts +16 -0
- package/src/core/domain/rulesets/ruleset-registry.ts +71 -0
- package/src/core/domain/rulesets/ruleset.contracts.ts +15 -0
- package/src/core/domain/rulesets/value-object-ruleset.contracts.ts +13 -0
- package/src/core/domain/temporal/index.ts +18 -0
- package/src/core/domain/temporal/temporal-range.ts +53 -0
- package/src/core/domain/temporal/temporal-snapshot.ts +47 -0
- package/src/core/domain/temporal/transaction-time.ts +60 -0
- package/src/core/domain/temporal/valid-time.ts +57 -0
- package/src/core/domain/value-object.ts +30 -0
- package/src/core/errors/app-error.ts +85 -0
- package/src/core/errors/authentication-error.ts +171 -0
- package/src/core/errors/authorization-error.ts +204 -0
- package/src/core/errors/business-rule-violation-error.ts +28 -0
- package/src/core/errors/conflict-error.ts +246 -0
- package/src/core/errors/error-codes.ts +60 -0
- package/src/core/errors/idempotency-error.ts +372 -0
- package/src/core/errors/index.ts +99 -0
- package/src/core/errors/integration-error.ts +157 -0
- package/src/core/errors/legacy-incompatible-error.ts +26 -0
- package/src/core/errors/not-found-error.ts +27 -0
- package/src/core/errors/serialization-error.ts +233 -0
- package/src/core/errors/temporal-error.ts +145 -0
- package/src/core/errors/types.ts +74 -0
- package/src/core/errors/unexpected-error.ts +22 -0
- package/src/core/errors/utils/index.ts +4 -0
- package/src/core/errors/utils/json-safe.ts +93 -0
- package/src/core/errors/validation-error.ts +34 -0
- package/src/core/exceptions/business-rule-violation-exception.ts +12 -0
- package/src/core/exceptions/domain-exception.ts +9 -0
- package/src/core/exceptions/entity-not-found-exception.ts +12 -0
- package/src/core/exceptions/invalid-state-transition-exception.ts +15 -0
- package/src/core/exceptions/invariant-violation-exception.ts +9 -0
- package/src/core/exceptions/validation-code.ts +39 -0
- package/src/core/exceptions/validation-exception.ts +39 -0
- package/src/core/exceptions/validation-field.ts +32 -0
- package/src/core/index.ts +15 -0
- package/src/core/policies/asof/asof.ts +68 -0
- package/src/core/policies/asof/index.ts +2 -0
- package/src/core/policies/catalog/catalog-types.ts +12 -0
- package/src/core/policies/catalog/catalog.instance.ts +21 -0
- package/src/core/policies/catalog/index.ts +11 -0
- package/src/core/policies/catalog/policy-catalog-entry.ts +220 -0
- package/src/core/policies/catalog/policy-catalog.ts +124 -0
- package/src/core/policies/catalog/policy-key.ts +78 -0
- package/src/core/policies/context/context-builder.ts +391 -0
- package/src/core/policies/context/context-registry.instance.ts +26 -0
- package/src/core/policies/context/context-registry.ts +95 -0
- package/src/core/policies/context/context-resolver.ts +31 -0
- package/src/core/policies/context/context-seed.ts +32 -0
- package/src/core/policies/context/index.ts +21 -0
- package/src/core/policies/context/path.ts +152 -0
- package/src/core/policies/defs/in-memory-policy-definition-repo.ts +73 -0
- package/src/core/policies/defs/index.ts +20 -0
- package/src/core/policies/defs/policy-definition-repository.ts +8 -0
- package/src/core/policies/defs/policy-definition.ts +192 -0
- package/src/core/policies/defs/policy-payload.contracts.ts +13 -0
- package/src/core/policies/defs/policy-scope.ts +53 -0
- package/src/core/policies/defs/policy-semver.ts +83 -0
- package/src/core/policies/engines/compute-engine-registry.ts +57 -0
- package/src/core/policies/engines/compute-evaluator-registry.ts +28 -0
- package/src/core/policies/engines/compute-payload.ts +5 -0
- package/src/core/policies/engines/compute-registry.ts +67 -0
- package/src/core/policies/engines/compute-types.ts +51 -0
- package/src/core/policies/engines/condition-evaluator-reporter.ts +34 -0
- package/src/core/policies/engines/gate-engine-registry.ts +38 -0
- package/src/core/policies/engines/gate-payload.ts +5 -0
- package/src/core/policies/engines/gate-types.ts +63 -0
- package/src/core/policies/engines/index.ts +48 -0
- package/src/core/policies/engines/parse-compute-payload.ts +71 -0
- package/src/core/policies/engines/parse-gate-payload.ts +68 -0
- package/src/core/policies/engines/v1/compute/compute-engine-v1.ts +55 -0
- package/src/core/policies/engines/v1/compute/compute-payload.schema.ts +75 -0
- package/src/core/policies/engines/v1/compute/index.ts +12 -0
- package/src/core/policies/engines/v1/compute/resolve-decision-table-v1.ts +54 -0
- package/src/core/policies/engines/v1/condition-evaluator.ts +592 -0
- package/src/core/policies/engines/v1/condition-schema.ts +50 -0
- package/src/core/policies/engines/v1/condition-types.ts +41 -0
- package/src/core/policies/engines/v1/gate/gate-engine-v1.ts +157 -0
- package/src/core/policies/engines/v1/gate/gate-types-v1.ts +12 -0
- package/src/core/policies/engines/v1/gate/gate-v1-payload.schema.ts +46 -0
- package/src/core/policies/engines/v1/gate/index.ts +9 -0
- package/src/core/policies/engines/v1/index.ts +13 -0
- package/src/core/policies/index.ts +132 -0
- package/src/core/policies/package/policy-package.ts +17 -0
- package/src/core/policies/policy-ids.ts +60 -0
- package/src/core/policies/resolver/index.ts +1 -0
- package/src/core/policies/resolver/policy-resolver.ts +57 -0
- package/src/core/policies/service/index.ts +10 -0
- package/src/core/policies/service/policy-evaluation-error.ts +148 -0
- package/src/core/policies/service/policy-service.ts +493 -0
- package/src/core/policies/utils/date.ts +7 -0
- package/src/core/policies/utils/hash.ts +90 -0
- package/src/core/policies/utils/index.ts +3 -0
- package/src/core/policies/utils/result.ts +4 -0
- package/src/core/result/outcome.ts +158 -0
- package/src/core/result/result.ts +153 -0
- package/src/core/shared/aggregate-version.ts +11 -0
- package/src/core/shared/hashing.ts +38 -0
- package/src/core/shared/immutable.ts +46 -0
- package/src/core/shared/temporal-guards.ts +22 -0
- package/src/core/versioning/domain-event-contracts.ts +73 -0
- package/src/core/versioning/version.ts +34 -0
- package/src/errors/index.ts +1 -0
- package/src/examples/rulesets/entity/order-creation-rules-v1.ts +31 -0
- package/src/examples/rulesets/entity/order-invariants-v1.ts +48 -0
- package/src/examples/rulesets/entity/order-invariants-v2.ts +28 -0
- package/src/examples/rulesets/entity/order.ts +162 -0
- package/src/examples/rulesets/value-object/cpf-rules-v1.ts +52 -0
- package/src/examples/rulesets/value-object/cpf-rules-v2.ts +27 -0
- package/src/examples/rulesets/value-object/cpf.ts +37 -0
- package/src/examples/rulesets/value-object/customer.ts +67 -0
- package/src/examples/rulesets/value-object/person-name-rules-v1.ts +37 -0
- package/src/examples/rulesets/value-object/person-name-rules-v2.ts +27 -0
- package/src/examples/rulesets/value-object/person-name.ts +39 -0
- package/src/exceptions/validation-field.ts +1 -0
- package/src/index.ts +11 -0
- package/src/policies/engines/index.ts +1 -0
- package/src/policies/engines/v1/gate/index.ts +1 -0
- package/src/policies/index.ts +1 -0
- package/src/version.ts +7 -0
|
@@ -0,0 +1,992 @@
|
|
|
1
|
+
import { a as stableStringify, i as sha256Hex, n as UnexpectedError, t as ValidationCode } from "./validation-code.js";
|
|
2
|
+
import { t as ValidationField } from "./validation-field.js";
|
|
3
|
+
import { _ as DomainException, f as SilentPolicyReporter, g as InvariantViolationException, h as isValidDate, l as Result, o as PolicyContextPath } from "./gate-v1-payload.schema.js";
|
|
4
|
+
//#region src/core/policies/utils/hash.ts
|
|
5
|
+
/**
|
|
6
|
+
* Produces a deterministic SHA-256 hex digest of a canonical JSON representation.
|
|
7
|
+
* Object keys are sorted recursively so key order does not affect the hash.
|
|
8
|
+
* Array item order is preserved on purpose; callers that treat arrays as sets
|
|
9
|
+
* must normalize or sort them before hashing.
|
|
10
|
+
*
|
|
11
|
+
* Values that JSON.stringify would silently drop or coerce (undefined,
|
|
12
|
+
* non-finite numbers, functions, symbols, bigint) are rejected up-front to
|
|
13
|
+
* prevent semantically different payloads from collapsing to the same hash.
|
|
14
|
+
*/
|
|
15
|
+
var PolicyHashing = class PolicyHashing {
|
|
16
|
+
static sha256(input) {
|
|
17
|
+
return sha256Hex(input);
|
|
18
|
+
}
|
|
19
|
+
static assertHashable(value, path) {
|
|
20
|
+
if (value === null) return;
|
|
21
|
+
const type = typeof value;
|
|
22
|
+
if (type === "undefined") throw new TypeError(`canonicalJson does not accept undefined values (at ${path})`);
|
|
23
|
+
if (type === "number" && !Number.isFinite(value)) throw new TypeError(`canonicalJson does not accept non-finite numbers (at ${path}, value: ${String(value)})`);
|
|
24
|
+
if (type === "bigint" || type === "function" || type === "symbol") throw new TypeError(`canonicalJson does not accept ${type} values (at ${path})`);
|
|
25
|
+
if (type !== "object") return;
|
|
26
|
+
if (value instanceof Date) {
|
|
27
|
+
if (!isValidDate(value)) throw new TypeError(`canonicalJson does not accept Invalid Date (at ${path})`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (Array.isArray(value)) {
|
|
31
|
+
value.forEach((item, index) => {
|
|
32
|
+
PolicyHashing.assertHashable(item, `${path}[${index}]`);
|
|
33
|
+
});
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
for (const [key, nested] of Object.entries(value)) PolicyHashing.assertHashable(nested, `${path}.${key}`);
|
|
37
|
+
}
|
|
38
|
+
static canonicalJson(value) {
|
|
39
|
+
PolicyHashing.assertHashable(value, "$");
|
|
40
|
+
return stableStringify(value);
|
|
41
|
+
}
|
|
42
|
+
static computePayloadHash(payload, policyKey, policyVersion) {
|
|
43
|
+
const canonical = PolicyHashing.canonicalJson({
|
|
44
|
+
policyKey,
|
|
45
|
+
policyVersion,
|
|
46
|
+
payload
|
|
47
|
+
});
|
|
48
|
+
return PolicyHashing.sha256(canonical);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/core/exceptions/validation-exception.ts
|
|
53
|
+
var ValidationException = class extends DomainException {
|
|
54
|
+
constructor(field, code, message) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.field = field;
|
|
57
|
+
this.code = code;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
var InvalidValueException = class extends ValidationException {
|
|
61
|
+
constructor(field, code, message) {
|
|
62
|
+
super(field, code, message);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/core/policies/policy-ids.ts
|
|
67
|
+
const POLICY_DEFINITION_ID_FIELD = ValidationField.of("policyDefinitionId");
|
|
68
|
+
const POLICY_DECISION_ID_FIELD = ValidationField.of("policyDecisionId");
|
|
69
|
+
const TENANT_ID_FIELD = ValidationField.of("tenantId");
|
|
70
|
+
const SCHOOL_ID_FIELD = ValidationField.of("schoolId");
|
|
71
|
+
function assertIdString(value, field) {
|
|
72
|
+
if (typeof value !== "string") throw new InvalidValueException(field, ValidationCode.INVALID_FORMAT, `${field.value} must be a string, got ${typeof value}`);
|
|
73
|
+
if (value.trim().length === 0) throw new InvalidValueException(field, ValidationCode.BLANK, `${field.value} must be a non-empty string`);
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
function brandId(value, field) {
|
|
77
|
+
return assertIdString(value, field);
|
|
78
|
+
}
|
|
79
|
+
function asPolicyDefinitionId(value) {
|
|
80
|
+
return brandId(value, POLICY_DEFINITION_ID_FIELD);
|
|
81
|
+
}
|
|
82
|
+
function asPolicyDecisionId(value) {
|
|
83
|
+
return brandId(value, POLICY_DECISION_ID_FIELD);
|
|
84
|
+
}
|
|
85
|
+
function asTenantId(value) {
|
|
86
|
+
return brandId(value, TENANT_ID_FIELD);
|
|
87
|
+
}
|
|
88
|
+
function asSchoolId(value) {
|
|
89
|
+
return brandId(value, SCHOOL_ID_FIELD);
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/core/policies/catalog/policy-catalog-entry.ts
|
|
93
|
+
/**
|
|
94
|
+
* Represents a single entry in the Policy Catalog.
|
|
95
|
+
* Encapsulates metadata and validation logic for a policy.
|
|
96
|
+
*/
|
|
97
|
+
var PolicyCatalogEntry = class PolicyCatalogEntry {
|
|
98
|
+
constructor(props) {
|
|
99
|
+
this.key = props.key;
|
|
100
|
+
this.kind = props.kind;
|
|
101
|
+
this.gateEngineVersion = props.gateEngineVersion;
|
|
102
|
+
this.computeEngineVersion = props.computeEngineVersion;
|
|
103
|
+
this.payloadSchemaVersion = props.payloadSchemaVersion;
|
|
104
|
+
this.owner = props.owner;
|
|
105
|
+
this.allowedScopes = [...props.allowedScopes];
|
|
106
|
+
this.asOfSource = props.asOfSource;
|
|
107
|
+
this.contextRequirements = [...props.contextRequirements];
|
|
108
|
+
this.tags = [...props.tags ?? []];
|
|
109
|
+
this.description = props.description;
|
|
110
|
+
}
|
|
111
|
+
static assertFamilyConsistency(entries) {
|
|
112
|
+
if (entries.length === 0) throw new UnexpectedError("Cannot validate an empty PolicyCatalogEntry family");
|
|
113
|
+
const [first, ...rest] = entries;
|
|
114
|
+
for (const entry of rest) {
|
|
115
|
+
if (!first.key.equals(entry.key)) throw new UnexpectedError("Cannot merge PolicyCatalog entries with different keys");
|
|
116
|
+
if (first.kind !== entry.kind) throw new UnexpectedError(`PolicyCatalog entries for key "${first.key.toString()}" must share the same kind`);
|
|
117
|
+
if (first.owner !== entry.owner) throw new UnexpectedError(`PolicyCatalog entries for key "${first.key.toString()}" must share the same owner`);
|
|
118
|
+
if (first.asOfSource !== entry.asOfSource) throw new UnexpectedError(`PolicyCatalog entries for key "${first.key.toString()}" must share the same asOfSource`);
|
|
119
|
+
if (first.description !== entry.description) throw new UnexpectedError(`PolicyCatalog entries for key "${first.key.toString()}" must share the same description`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
static from(entry) {
|
|
123
|
+
return entry instanceof PolicyCatalogEntry ? entry : new PolicyCatalogEntry(entry);
|
|
124
|
+
}
|
|
125
|
+
isGate() {
|
|
126
|
+
return this.kind === "GATE";
|
|
127
|
+
}
|
|
128
|
+
isCompute() {
|
|
129
|
+
return this.kind === "COMPUTE";
|
|
130
|
+
}
|
|
131
|
+
allowsScope(scope) {
|
|
132
|
+
return this.allowedScopes.includes(scope);
|
|
133
|
+
}
|
|
134
|
+
hasExplicitVersionSelector() {
|
|
135
|
+
return this.gateEngineVersion !== void 0 || this.computeEngineVersion !== void 0 || this.payloadSchemaVersion !== void 0;
|
|
136
|
+
}
|
|
137
|
+
matchesVersion(params) {
|
|
138
|
+
if (this.kind !== params.kind) return false;
|
|
139
|
+
if (this.gateEngineVersion !== void 0 && this.gateEngineVersion !== params.gateEngineVersion) return false;
|
|
140
|
+
if (this.computeEngineVersion !== void 0 && this.computeEngineVersion !== params.computeEngineVersion) return false;
|
|
141
|
+
if (this.payloadSchemaVersion !== void 0 && this.payloadSchemaVersion !== params.payloadSchemaVersion) return false;
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
toVariantKey() {
|
|
145
|
+
return [
|
|
146
|
+
this.key.toString(),
|
|
147
|
+
this.kind,
|
|
148
|
+
this.gateEngineVersion ?? "gate:any",
|
|
149
|
+
this.computeEngineVersion ?? "compute:any",
|
|
150
|
+
this.payloadSchemaVersion ?? "schema:any"
|
|
151
|
+
].join("::");
|
|
152
|
+
}
|
|
153
|
+
usesNowAsOf() {
|
|
154
|
+
return this.asOfSource === "NOW";
|
|
155
|
+
}
|
|
156
|
+
usesCallerProvidedAsOf() {
|
|
157
|
+
return this.asOfSource === "CALLER_PROVIDED";
|
|
158
|
+
}
|
|
159
|
+
requiresContext(path) {
|
|
160
|
+
return this.contextRequirements.includes(path);
|
|
161
|
+
}
|
|
162
|
+
requiresAllContexts(paths) {
|
|
163
|
+
return paths.every((path) => this.requiresContext(path));
|
|
164
|
+
}
|
|
165
|
+
hasTag(tag) {
|
|
166
|
+
return this.tags.includes(tag);
|
|
167
|
+
}
|
|
168
|
+
equals(other) {
|
|
169
|
+
return this.key.equals(other.key);
|
|
170
|
+
}
|
|
171
|
+
toJSON() {
|
|
172
|
+
return {
|
|
173
|
+
key: this.key,
|
|
174
|
+
kind: this.kind,
|
|
175
|
+
...this.gateEngineVersion !== void 0 ? { gateEngineVersion: this.gateEngineVersion } : {},
|
|
176
|
+
...this.computeEngineVersion !== void 0 ? { computeEngineVersion: this.computeEngineVersion } : {},
|
|
177
|
+
...this.payloadSchemaVersion !== void 0 ? { payloadSchemaVersion: this.payloadSchemaVersion } : {},
|
|
178
|
+
owner: this.owner,
|
|
179
|
+
allowedScopes: [...this.allowedScopes],
|
|
180
|
+
asOfSource: this.asOfSource,
|
|
181
|
+
contextRequirements: [...this.contextRequirements],
|
|
182
|
+
tags: this.tags.length > 0 ? [...this.tags] : void 0,
|
|
183
|
+
description: this.description
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
//#endregion
|
|
188
|
+
//#region src/core/policies/catalog/policy-catalog.ts
|
|
189
|
+
/**
|
|
190
|
+
* In-code catalog of all known policies.
|
|
191
|
+
* The catalog defines the universe of policies; the database cannot invent new ones.
|
|
192
|
+
*/
|
|
193
|
+
var PolicyCatalog = class {
|
|
194
|
+
constructor(entries) {
|
|
195
|
+
const families = /* @__PURE__ */ new Map();
|
|
196
|
+
const versionedMap = /* @__PURE__ */ new Map();
|
|
197
|
+
for (const rawEntry of entries) {
|
|
198
|
+
const entry = PolicyCatalogEntry.from(rawEntry);
|
|
199
|
+
const key = entry.key.toString();
|
|
200
|
+
const variantKey = entry.toVariantKey();
|
|
201
|
+
if (versionedMap.has(variantKey)) throw new UnexpectedError(`Duplicate PolicyCatalog entry for key "${key}" and variant "${variantKey}"`);
|
|
202
|
+
const family = families.get(key) ?? [];
|
|
203
|
+
family.push(entry);
|
|
204
|
+
families.set(key, family);
|
|
205
|
+
versionedMap.set(variantKey, entry);
|
|
206
|
+
}
|
|
207
|
+
for (const family of families.values()) PolicyCatalogEntry.assertFamilyConsistency(family);
|
|
208
|
+
this.versionedEntries = versionedMap;
|
|
209
|
+
this.entriesByKey = families;
|
|
210
|
+
}
|
|
211
|
+
get(key) {
|
|
212
|
+
const familyResult = this.getFamily(key);
|
|
213
|
+
if (familyResult.isErr()) return Result.err(familyResult.errorOrNull());
|
|
214
|
+
const family = familyResult.getOrNull();
|
|
215
|
+
if (family.length > 1) return Result.err(`Policy key "${key}" has multiple catalog variants; use getVersioned(...) or getFamily(...)`);
|
|
216
|
+
const [entry] = family;
|
|
217
|
+
if (!entry) return Result.err(`Policy not found in catalog: "${key}"`);
|
|
218
|
+
return Result.ok(entry);
|
|
219
|
+
}
|
|
220
|
+
getFamily(key) {
|
|
221
|
+
const family = this.entriesByKey.get(key);
|
|
222
|
+
if (!family) return Result.err(`Policy not found in catalog: "${key}"`);
|
|
223
|
+
return Result.ok(family);
|
|
224
|
+
}
|
|
225
|
+
getVersioned(params) {
|
|
226
|
+
const family = this.entriesByKey.get(params.key);
|
|
227
|
+
if (!family || family.length === 0) return Result.err(`Policy not found in catalog: "${params.key}"`);
|
|
228
|
+
const exactMatch = family.find((entry) => entry.matchesVersion(params));
|
|
229
|
+
if (exactMatch) return Result.ok(exactMatch);
|
|
230
|
+
if (family.length === 1 && !family[0].hasExplicitVersionSelector()) return Result.ok(family[0]);
|
|
231
|
+
const versionDetails = [
|
|
232
|
+
params.kind,
|
|
233
|
+
params.gateEngineVersion !== void 0 ? `gateEngineVersion=${params.gateEngineVersion}` : null,
|
|
234
|
+
params.computeEngineVersion !== void 0 ? `computeEngineVersion=${params.computeEngineVersion}` : null,
|
|
235
|
+
params.payloadSchemaVersion !== void 0 ? `payloadSchemaVersion=${params.payloadSchemaVersion}` : null
|
|
236
|
+
].filter((part) => part !== null).join(", ");
|
|
237
|
+
return Result.err(`Policy variant not found in catalog: "${params.key}" (${versionDetails})`);
|
|
238
|
+
}
|
|
239
|
+
list() {
|
|
240
|
+
return Array.from(this.versionedEntries.values());
|
|
241
|
+
}
|
|
242
|
+
has(key) {
|
|
243
|
+
return this.entriesByKey.has(key);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
//#endregion
|
|
247
|
+
//#region src/core/policies/catalog/catalog.instance.ts
|
|
248
|
+
/**
|
|
249
|
+
* Aggregates catalog entries contributed by each module package into a single
|
|
250
|
+
* PolicyCatalog. The core knows nothing about concrete modules — callers
|
|
251
|
+
* are responsible for passing all relevant packages at composition time.
|
|
252
|
+
*/
|
|
253
|
+
var PolicyCatalogFactory = class {
|
|
254
|
+
static build(packages) {
|
|
255
|
+
return new PolicyCatalog(packages.flatMap((policyPackage) => Array.from(policyPackage.catalogEntries).map(PolicyCatalogEntry.from)));
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
//#endregion
|
|
259
|
+
//#region src/core/policies/catalog/policy-key.ts
|
|
260
|
+
/**
|
|
261
|
+
* PolicyKey value object.
|
|
262
|
+
*
|
|
263
|
+
* Format: `module.area.policy_name` (exactly 3 dot-separated segments).
|
|
264
|
+
* - module: structural segment (lowercase alphanumeric + underscore, starting with a letter)
|
|
265
|
+
* - area: structural segment (lowercase alphanumeric + underscore, starting with a letter)
|
|
266
|
+
* - name: specific policy identifier (lowercase alphanumeric + underscore, starting with a letter)
|
|
267
|
+
*/
|
|
268
|
+
const SEGMENT_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
269
|
+
const MAX_POLICY_KEY_LENGTH = 255;
|
|
270
|
+
var PolicyKey = class PolicyKey {
|
|
271
|
+
constructor(module, area, name) {
|
|
272
|
+
this.module = module;
|
|
273
|
+
this.area = area;
|
|
274
|
+
this.name = name;
|
|
275
|
+
Object.freeze(this);
|
|
276
|
+
}
|
|
277
|
+
static parse(raw) {
|
|
278
|
+
if (!raw || typeof raw !== "string") return Result.err("PolicyKey cannot be empty");
|
|
279
|
+
if (raw.length > MAX_POLICY_KEY_LENGTH) return Result.err(`PolicyKey cannot exceed ${MAX_POLICY_KEY_LENGTH} characters, got ${raw.length}`);
|
|
280
|
+
const segments = raw.split(".");
|
|
281
|
+
if (segments.length !== 3) return Result.err(`PolicyKey must have exactly 3 segments (module.area.name), got ${segments.length}: "${raw}"`);
|
|
282
|
+
const [rawModule, rawArea, name] = segments;
|
|
283
|
+
if (!SEGMENT_PATTERN.test(rawModule)) return Result.err(`PolicyKey segment "module" is invalid: "${rawModule}". Must start with a lowercase letter and contain only [a-z0-9_].`);
|
|
284
|
+
if (!SEGMENT_PATTERN.test(rawArea)) return Result.err(`PolicyKey segment "area" is invalid: "${rawArea}". Must start with a lowercase letter and contain only [a-z0-9_].`);
|
|
285
|
+
if (!SEGMENT_PATTERN.test(name)) return Result.err(`PolicyKey segment "name" is invalid: "${name}". Must start with a lowercase letter and contain only [a-z0-9_].`);
|
|
286
|
+
return Result.ok(new PolicyKey(rawModule, rawArea, name));
|
|
287
|
+
}
|
|
288
|
+
toString() {
|
|
289
|
+
return `${this.module}.${this.area}.${this.name}`;
|
|
290
|
+
}
|
|
291
|
+
equals(other) {
|
|
292
|
+
return this.toString() === other.toString();
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
//#endregion
|
|
296
|
+
//#region src/core/policies/defs/policy-scope.ts
|
|
297
|
+
/**
|
|
298
|
+
* Numeric weight for scope specificity (higher = more specific).
|
|
299
|
+
*/
|
|
300
|
+
const SCOPE_WEIGHT = {
|
|
301
|
+
SCHOOL: 3,
|
|
302
|
+
TENANT: 2,
|
|
303
|
+
GLOBAL: 1
|
|
304
|
+
};
|
|
305
|
+
var PolicyScopeMatcher = class PolicyScopeMatcher {
|
|
306
|
+
static weight(level) {
|
|
307
|
+
return SCOPE_WEIGHT[level];
|
|
308
|
+
}
|
|
309
|
+
static matchesChain(defScope, chain) {
|
|
310
|
+
return chain.some((scope) => PolicyScopeMatcher.equals(defScope, scope));
|
|
311
|
+
}
|
|
312
|
+
static equals(a, b) {
|
|
313
|
+
return a.level === b.level && a.tenantId === b.tenantId && a.schoolId === b.schoolId;
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
//#endregion
|
|
317
|
+
//#region src/core/policies/defs/in-memory-policy-definition-repo.ts
|
|
318
|
+
var InMemoryPolicyDefinitionRepository = class {
|
|
319
|
+
constructor(definitions) {
|
|
320
|
+
this.definitions = definitions;
|
|
321
|
+
this.definitionsByPolicyKey = definitions.reduce((index, definition) => {
|
|
322
|
+
const existingDefinitions = index.get(definition.policyKey) ?? [];
|
|
323
|
+
existingDefinitions.push(definition);
|
|
324
|
+
index.set(definition.policyKey, existingDefinitions);
|
|
325
|
+
return index;
|
|
326
|
+
}, /* @__PURE__ */ new Map());
|
|
327
|
+
}
|
|
328
|
+
findCandidates(params) {
|
|
329
|
+
const { policyKey, kind, payloadSchemaVersion, asOf, contextVersion, scopeChain } = params;
|
|
330
|
+
return (this.definitionsByPolicyKey.get(policyKey) ?? []).filter((def) => {
|
|
331
|
+
if (def.kind !== kind) return false;
|
|
332
|
+
if (payloadSchemaVersion !== void 0 && def.payloadSchemaVersion !== payloadSchemaVersion) return false;
|
|
333
|
+
if (!def.enabled) return false;
|
|
334
|
+
if (def.status !== "PUBLISHED") return false;
|
|
335
|
+
if (def.effectiveFrom > asOf) return false;
|
|
336
|
+
if (def.effectiveTo !== null && def.effectiveTo <= asOf) return false;
|
|
337
|
+
if (contextVersion < def.contextVersionMin) return false;
|
|
338
|
+
if (contextVersion > def.contextVersionMax) return false;
|
|
339
|
+
if (!PolicyScopeMatcher.matchesChain(def.scope, scopeChain)) return false;
|
|
340
|
+
return true;
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
//#endregion
|
|
345
|
+
//#region src/core/policies/defs/policy-semver.ts
|
|
346
|
+
const POLICY_SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/;
|
|
347
|
+
function parseSemver(version) {
|
|
348
|
+
const [core, preRelease = null] = version.split("+")[0].split("-");
|
|
349
|
+
const [major, minor, patch] = core.split(".").map(Number);
|
|
350
|
+
return {
|
|
351
|
+
major,
|
|
352
|
+
minor,
|
|
353
|
+
patch,
|
|
354
|
+
preRelease: preRelease ?? null
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
function comparePreRelease(a, b) {
|
|
358
|
+
const aIds = a.split(".");
|
|
359
|
+
const bIds = b.split(".");
|
|
360
|
+
const len = Math.max(aIds.length, bIds.length);
|
|
361
|
+
for (let i = 0; i < len; i++) {
|
|
362
|
+
const ai = aIds[i];
|
|
363
|
+
const bi = bIds[i];
|
|
364
|
+
if (ai === void 0) return -1;
|
|
365
|
+
if (bi === void 0) return 1;
|
|
366
|
+
const aNum = Number(ai);
|
|
367
|
+
const bNum = Number(bi);
|
|
368
|
+
const aIsNum = !Number.isNaN(aNum);
|
|
369
|
+
const bIsNum = !Number.isNaN(bNum);
|
|
370
|
+
if (aIsNum && !bIsNum) return -1;
|
|
371
|
+
if (!aIsNum && bIsNum) return 1;
|
|
372
|
+
if (aIsNum && bIsNum) {
|
|
373
|
+
if (aNum !== bNum) return aNum - bNum;
|
|
374
|
+
} else {
|
|
375
|
+
if (ai < bi) return -1;
|
|
376
|
+
if (ai > bi) return 1;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return 0;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Compares two valid semver strings.
|
|
383
|
+
* Returns a negative number if a < b, 0 if a === b, positive if a > b.
|
|
384
|
+
*
|
|
385
|
+
* Callers are expected to pass only strings that already passed
|
|
386
|
+
* `POLICY_SEMVER_PATTERN` validation (i.e. values stored in PolicyDefinition).
|
|
387
|
+
*/
|
|
388
|
+
function comparePolicySemver(a, b) {
|
|
389
|
+
const av = parseSemver(a);
|
|
390
|
+
const bv = parseSemver(b);
|
|
391
|
+
if (av.major !== bv.major) return av.major - bv.major;
|
|
392
|
+
if (av.minor !== bv.minor) return av.minor - bv.minor;
|
|
393
|
+
if (av.patch !== bv.patch) return av.patch - bv.patch;
|
|
394
|
+
if (av.preRelease === null && bv.preRelease !== null) return 1;
|
|
395
|
+
if (av.preRelease !== null && bv.preRelease === null) return -1;
|
|
396
|
+
if (av.preRelease !== null && bv.preRelease !== null) return comparePreRelease(av.preRelease, bv.preRelease);
|
|
397
|
+
return 0;
|
|
398
|
+
}
|
|
399
|
+
//#endregion
|
|
400
|
+
//#region src/core/policies/defs/policy-definition.ts
|
|
401
|
+
var PolicyDefinition = class PolicyDefinition {
|
|
402
|
+
static assertPolicyVersionIsSemver(policyVersion) {
|
|
403
|
+
if (!POLICY_SEMVER_PATTERN.test(policyVersion)) throw new InvariantViolationException(`PolicyDefinition.policyVersion must be a valid semver. Received: "${policyVersion}"`);
|
|
404
|
+
}
|
|
405
|
+
constructor(props) {
|
|
406
|
+
this.id = props.id;
|
|
407
|
+
this.policyKey = props.policyKey;
|
|
408
|
+
PolicyDefinition.assertPolicyVersionIsSemver(props.policyVersion);
|
|
409
|
+
this.policyVersion = props.policyVersion;
|
|
410
|
+
this.payloadSchemaVersion = props.payloadSchemaVersion;
|
|
411
|
+
this.contextVersionMin = props.contextVersionMin;
|
|
412
|
+
this.contextVersionMax = props.contextVersionMax;
|
|
413
|
+
this.enabled = props.enabled;
|
|
414
|
+
this.status = props.status;
|
|
415
|
+
this.scope = props.scope;
|
|
416
|
+
this.effectiveFrom = props.effectiveFrom;
|
|
417
|
+
this.effectiveTo = props.effectiveTo;
|
|
418
|
+
this.priority = props.priority;
|
|
419
|
+
this.payloadJson = props.payloadJson;
|
|
420
|
+
this.payloadHash = props.payloadHash;
|
|
421
|
+
this.createdAt = props.createdAt;
|
|
422
|
+
this.publishedAt = props.publishedAt;
|
|
423
|
+
this.kind = props.kind;
|
|
424
|
+
if (props.kind === "GATE") {
|
|
425
|
+
if (props.gateEngineVersion === void 0) throw new InvariantViolationException(`PolicyDefinition of kind 'GATE' must have a 'gateEngineVersion'. Policy key: ${props.policyKey}`);
|
|
426
|
+
this.gateEngineVersion = props.gateEngineVersion;
|
|
427
|
+
} else {
|
|
428
|
+
if (props.computeEngineVersion === void 0) throw new InvariantViolationException(`PolicyDefinition of kind 'COMPUTE' must have a 'computeEngineVersion'. Policy key: ${props.policyKey}`);
|
|
429
|
+
this.computeEngineVersion = props.computeEngineVersion;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
static gate(props) {
|
|
433
|
+
return new PolicyDefinition({
|
|
434
|
+
...props,
|
|
435
|
+
enabled: props.enabled ?? true,
|
|
436
|
+
kind: "GATE"
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
static compute(props) {
|
|
440
|
+
return new PolicyDefinition({
|
|
441
|
+
...props,
|
|
442
|
+
enabled: props.enabled ?? true,
|
|
443
|
+
kind: "COMPUTE"
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
isGate() {
|
|
447
|
+
return this.kind === "GATE";
|
|
448
|
+
}
|
|
449
|
+
isCompute() {
|
|
450
|
+
return this.kind === "COMPUTE";
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
//#endregion
|
|
454
|
+
//#region src/core/policies/context/context-builder.ts
|
|
455
|
+
const DEFAULT_RESOLVER_RETRY_OPTIONS = {
|
|
456
|
+
maxAttempts: 1,
|
|
457
|
+
initialDelayMs: 25,
|
|
458
|
+
maxDelayMs: 1e3,
|
|
459
|
+
backoffMultiplier: 2
|
|
460
|
+
};
|
|
461
|
+
/**
|
|
462
|
+
* Builds a context object by resolving required paths from a seed.
|
|
463
|
+
*/
|
|
464
|
+
var PolicyContextBuilder = class PolicyContextBuilder {
|
|
465
|
+
constructor(registry, options = {}) {
|
|
466
|
+
this.registry = registry;
|
|
467
|
+
this.circuitStates = /* @__PURE__ */ new Map();
|
|
468
|
+
this.defaultResolverResilience = options.defaultResolverResilience;
|
|
469
|
+
this.now = options.now ?? Date.now;
|
|
470
|
+
this.sleep = options.sleep ?? (async (delayMs) => {
|
|
471
|
+
if (delayMs <= 0) return;
|
|
472
|
+
await new Promise((resolve) => {
|
|
473
|
+
setTimeout(resolve, delayMs);
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
static clampPositiveInteger(value, fallback) {
|
|
478
|
+
if (value === void 0 || !Number.isFinite(value)) return fallback;
|
|
479
|
+
return Math.max(1, Math.trunc(value));
|
|
480
|
+
}
|
|
481
|
+
static clampNonNegativeInteger(value, fallback) {
|
|
482
|
+
if (value === void 0 || !Number.isFinite(value)) return fallback;
|
|
483
|
+
return Math.max(0, Math.trunc(value));
|
|
484
|
+
}
|
|
485
|
+
static normalizeTimeoutMs(value) {
|
|
486
|
+
if (value === void 0 || !Number.isFinite(value) || value <= 0) return null;
|
|
487
|
+
return Math.trunc(value);
|
|
488
|
+
}
|
|
489
|
+
static mergeRetryOptions(defaults, overrides) {
|
|
490
|
+
if (overrides === false) return false;
|
|
491
|
+
if (overrides !== void 0) return {
|
|
492
|
+
...defaults === false || defaults === void 0 ? {} : defaults,
|
|
493
|
+
...overrides
|
|
494
|
+
};
|
|
495
|
+
return defaults;
|
|
496
|
+
}
|
|
497
|
+
static mergeCircuitBreakerOptions(defaults, overrides) {
|
|
498
|
+
if (overrides === false) return false;
|
|
499
|
+
if (overrides !== void 0) return {
|
|
500
|
+
...defaults === false || defaults === void 0 ? {} : defaults,
|
|
501
|
+
...overrides
|
|
502
|
+
};
|
|
503
|
+
return defaults;
|
|
504
|
+
}
|
|
505
|
+
resolveResilienceOptions(resolver) {
|
|
506
|
+
const defaults = this.defaultResolverResilience;
|
|
507
|
+
const override = resolver.resilience;
|
|
508
|
+
const retry = PolicyContextBuilder.mergeRetryOptions(defaults?.retry, override?.retry);
|
|
509
|
+
const circuitBreaker = PolicyContextBuilder.mergeCircuitBreakerOptions(defaults?.circuitBreaker, override?.circuitBreaker);
|
|
510
|
+
const retryOptions = retry === false ? void 0 : retry;
|
|
511
|
+
const circuitBreakerOptions = circuitBreaker === false ? void 0 : circuitBreaker;
|
|
512
|
+
return {
|
|
513
|
+
timeoutMs: PolicyContextBuilder.normalizeTimeoutMs(override?.timeoutMs ?? defaults?.timeoutMs),
|
|
514
|
+
retry: {
|
|
515
|
+
maxAttempts: PolicyContextBuilder.clampPositiveInteger(retryOptions?.maxAttempts, DEFAULT_RESOLVER_RETRY_OPTIONS.maxAttempts),
|
|
516
|
+
initialDelayMs: PolicyContextBuilder.clampNonNegativeInteger(retryOptions?.initialDelayMs, DEFAULT_RESOLVER_RETRY_OPTIONS.initialDelayMs),
|
|
517
|
+
maxDelayMs: PolicyContextBuilder.clampNonNegativeInteger(retryOptions?.maxDelayMs, DEFAULT_RESOLVER_RETRY_OPTIONS.maxDelayMs),
|
|
518
|
+
backoffMultiplier: PolicyContextBuilder.clampPositiveInteger(retryOptions?.backoffMultiplier, DEFAULT_RESOLVER_RETRY_OPTIONS.backoffMultiplier)
|
|
519
|
+
},
|
|
520
|
+
circuitBreaker: circuitBreakerOptions === void 0 ? null : {
|
|
521
|
+
failureThreshold: PolicyContextBuilder.clampPositiveInteger(circuitBreakerOptions.failureThreshold, 5),
|
|
522
|
+
cooldownMs: PolicyContextBuilder.clampPositiveInteger(circuitBreakerOptions.cooldownMs, 3e4)
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
getRetryDelayMs(attempt, retry) {
|
|
527
|
+
return Math.min(retry.initialDelayMs * retry.backoffMultiplier ** (attempt - 1), retry.maxDelayMs);
|
|
528
|
+
}
|
|
529
|
+
resetCircuit(path) {
|
|
530
|
+
this.circuitStates.delete(path);
|
|
531
|
+
}
|
|
532
|
+
isCircuitOpen(path, circuitBreaker) {
|
|
533
|
+
if (circuitBreaker === null) return false;
|
|
534
|
+
const state = this.circuitStates.get(path);
|
|
535
|
+
if (state === void 0 || state.openUntilMs === null) return false;
|
|
536
|
+
if (state.openUntilMs <= this.now()) {
|
|
537
|
+
this.resetCircuit(path);
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
542
|
+
recordResolverFailure(path, circuitBreaker) {
|
|
543
|
+
if (circuitBreaker === null) return;
|
|
544
|
+
const nextFailureCount = (this.circuitStates.get(path)?.failureCount ?? 0) + 1;
|
|
545
|
+
if (nextFailureCount >= circuitBreaker.failureThreshold) {
|
|
546
|
+
this.circuitStates.set(path, {
|
|
547
|
+
failureCount: 0,
|
|
548
|
+
openUntilMs: this.now() + circuitBreaker.cooldownMs
|
|
549
|
+
});
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
this.circuitStates.set(path, {
|
|
553
|
+
failureCount: nextFailureCount,
|
|
554
|
+
openUntilMs: null
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
static describeThrownResolverError(error) {
|
|
558
|
+
if (error instanceof Error) return `threw ${error.name}: ${error.message}`;
|
|
559
|
+
return `threw ${String(error)}`;
|
|
560
|
+
}
|
|
561
|
+
async resolveOnce(resolver, seed, timeoutMs) {
|
|
562
|
+
const execute = async () => {
|
|
563
|
+
try {
|
|
564
|
+
return await resolver.resolve(seed);
|
|
565
|
+
} catch (error) {
|
|
566
|
+
return Result.err(PolicyContextBuilder.describeThrownResolverError(error));
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
if (timeoutMs === null) return execute();
|
|
570
|
+
let timeoutId;
|
|
571
|
+
try {
|
|
572
|
+
return await Promise.race([execute(), new Promise((resolve) => {
|
|
573
|
+
timeoutId = setTimeout(() => {
|
|
574
|
+
resolve(Result.err(`timed out after ${timeoutMs}ms`));
|
|
575
|
+
}, timeoutMs);
|
|
576
|
+
})]);
|
|
577
|
+
} finally {
|
|
578
|
+
if (timeoutId !== void 0) clearTimeout(timeoutId);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
async resolveWithResilience(path, resolver, seed) {
|
|
582
|
+
const resilience = this.resolveResilienceOptions(resolver);
|
|
583
|
+
if (this.isCircuitOpen(path, resilience.circuitBreaker)) return Result.err("circuit breaker is open");
|
|
584
|
+
let lastError = "resolver failed";
|
|
585
|
+
for (let attempt = 1; attempt <= resilience.retry.maxAttempts; attempt++) {
|
|
586
|
+
const result = await this.resolveOnce(resolver, seed, resilience.timeoutMs);
|
|
587
|
+
if (result.isOk()) {
|
|
588
|
+
this.resetCircuit(path);
|
|
589
|
+
return result;
|
|
590
|
+
}
|
|
591
|
+
lastError = result.errorOrNull() ?? lastError;
|
|
592
|
+
if (attempt < resilience.retry.maxAttempts) await this.sleep(this.getRetryDelayMs(attempt, resilience.retry));
|
|
593
|
+
}
|
|
594
|
+
this.recordResolverFailure(path, resilience.circuitBreaker);
|
|
595
|
+
return Result.err(lastError);
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Resolves all required context paths and returns a flat context object.
|
|
599
|
+
* If any required path cannot be resolved, returns an error.
|
|
600
|
+
*/
|
|
601
|
+
async build(requirements, seed) {
|
|
602
|
+
const context = {};
|
|
603
|
+
for (const path of requirements) {
|
|
604
|
+
const resolver = this.registry.get(path);
|
|
605
|
+
if (!resolver) return Result.err(`Missing resolver for path "${path}"`);
|
|
606
|
+
const result = await this.resolveWithResilience(path, resolver, seed);
|
|
607
|
+
if (result.isErr()) return Result.err(`Resolver error for path "${path}": ${result.errorOrNull()}`);
|
|
608
|
+
const setPathResult = PolicyContextPath.set(context, path, result.getOrNull());
|
|
609
|
+
if (setPathResult.isErr()) return Result.err(setPathResult.errorOrNull());
|
|
610
|
+
}
|
|
611
|
+
const missing = requirements.filter((path) => {
|
|
612
|
+
const result = PolicyContextPath.getOrAbsent(context, path);
|
|
613
|
+
return result.isErr() || result.getOrNull() === void 0;
|
|
614
|
+
});
|
|
615
|
+
if (missing.length > 0) return Result.err(`Context missing required paths after resolution: ${missing.join(", ")}`);
|
|
616
|
+
return Result.ok(context);
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
//#endregion
|
|
620
|
+
//#region src/core/policies/context/context-registry.ts
|
|
621
|
+
const DEFAULT_CONTEXT_RESOLVER_NAMESPACE = "default";
|
|
622
|
+
function normalizeNamespace(namespace) {
|
|
623
|
+
const normalized = namespace?.trim();
|
|
624
|
+
if (normalized === void 0 || normalized.length === 0) return DEFAULT_CONTEXT_RESOLVER_NAMESPACE;
|
|
625
|
+
return normalized;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Registry of context value resolvers, keyed by path.
|
|
629
|
+
*/
|
|
630
|
+
var ContextResolverRegistry = class {
|
|
631
|
+
constructor() {
|
|
632
|
+
this.resolvers = /* @__PURE__ */ new Map();
|
|
633
|
+
}
|
|
634
|
+
register(resolver, options = {}) {
|
|
635
|
+
const namespace = normalizeNamespace(options.namespace);
|
|
636
|
+
const existing = this.resolvers.get(resolver.path);
|
|
637
|
+
if (existing !== void 0 && existing.namespace !== namespace) return Result.err(`Resolver path "${resolver.path}" is already registered by namespace "${existing.namespace}" and cannot be replaced by "${namespace}".`);
|
|
638
|
+
this.resolvers.set(resolver.path, {
|
|
639
|
+
namespace,
|
|
640
|
+
resolver
|
|
641
|
+
});
|
|
642
|
+
return Result.ok(void 0);
|
|
643
|
+
}
|
|
644
|
+
registerAll(resolvers, options = {}) {
|
|
645
|
+
for (const r of resolvers) {
|
|
646
|
+
const registrationResult = this.register(r, options);
|
|
647
|
+
if (registrationResult.isErr()) return registrationResult;
|
|
648
|
+
}
|
|
649
|
+
return Result.ok(void 0);
|
|
650
|
+
}
|
|
651
|
+
get(path) {
|
|
652
|
+
return this.resolvers.get(path)?.resolver;
|
|
653
|
+
}
|
|
654
|
+
has(path) {
|
|
655
|
+
return this.resolvers.has(path);
|
|
656
|
+
}
|
|
657
|
+
getNamespace(path) {
|
|
658
|
+
return this.resolvers.get(path)?.namespace;
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
function registerNamespacedContextResolversIn(registry, namespace, resolvers) {
|
|
662
|
+
const registrationResult = registry.registerAll(resolvers, { namespace });
|
|
663
|
+
if (registrationResult.isErr()) return Result.err(registrationResult.errorOrNull());
|
|
664
|
+
return Result.ok(registry);
|
|
665
|
+
}
|
|
666
|
+
//#endregion
|
|
667
|
+
//#region src/core/policies/context/context-registry.instance.ts
|
|
668
|
+
/**
|
|
669
|
+
* Official instance of the ContextResolverRegistry.
|
|
670
|
+
* Use `new ContextResolverRegistry()` plus `registerNamespacedContextResolversIn`
|
|
671
|
+
* when each runtime/composition root needs its own isolated resolver graph.
|
|
672
|
+
*/
|
|
673
|
+
const contextResolverRegistry = new ContextResolverRegistry();
|
|
674
|
+
function registerNamespacedContextResolvers(namespace, resolvers) {
|
|
675
|
+
return registerNamespacedContextResolversIn(contextResolverRegistry, namespace, resolvers);
|
|
676
|
+
}
|
|
677
|
+
//#endregion
|
|
678
|
+
//#region src/core/policies/context/context-seed.ts
|
|
679
|
+
var ContextSeedValidator = class ContextSeedValidator {
|
|
680
|
+
static isBlankSeedId(value) {
|
|
681
|
+
return value.trim().length === 0;
|
|
682
|
+
}
|
|
683
|
+
static validate(seed) {
|
|
684
|
+
if (ContextSeedValidator.isBlankSeedId(seed.tenantId)) return Result.err("ContextSeed tenantId must be a non-empty string");
|
|
685
|
+
if (ContextSeedValidator.isBlankSeedId(seed.schoolId)) return Result.err("ContextSeed schoolId must be a non-empty string");
|
|
686
|
+
return Result.ok(seed);
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
//#endregion
|
|
690
|
+
//#region src/core/policies/asof/asof.ts
|
|
691
|
+
const DEFAULT_MAX_AS_OF_FUTURE_YEARS = 10;
|
|
692
|
+
var PolicyAsOfResolver = class PolicyAsOfResolver {
|
|
693
|
+
static resolveMaxFutureYears(options) {
|
|
694
|
+
const maxFutureYears = options.maxFutureYears ?? DEFAULT_MAX_AS_OF_FUTURE_YEARS;
|
|
695
|
+
if (!Number.isInteger(maxFutureYears) || maxFutureYears < 0) return Result.err(`deriveAsOf maxFutureYears must be a non-negative integer. Received: ${maxFutureYears}`);
|
|
696
|
+
return Result.ok(maxFutureYears);
|
|
697
|
+
}
|
|
698
|
+
static derive(source, seed, now, options = {}) {
|
|
699
|
+
const maxFutureYearsResult = PolicyAsOfResolver.resolveMaxFutureYears(options);
|
|
700
|
+
if (maxFutureYearsResult.isErr()) return Result.err(maxFutureYearsResult.errorOrNull());
|
|
701
|
+
const maxFutureYears = maxFutureYearsResult.getOrNull();
|
|
702
|
+
switch (source) {
|
|
703
|
+
case "NOW": return Result.ok(now);
|
|
704
|
+
case "CALLER_PROVIDED": {
|
|
705
|
+
const asOf = seed.fields["asOf"];
|
|
706
|
+
if (!isValidDate(asOf)) return Result.err("asOfSource \"CALLER_PROVIDED\" requires seed.fields.asOf to be a Date");
|
|
707
|
+
const maxFuture = new Date(now);
|
|
708
|
+
maxFuture.setFullYear(maxFuture.getFullYear() + maxFutureYears);
|
|
709
|
+
if (asOf > maxFuture) return Result.err(`asOf date is more than ${maxFutureYears} years in the future (received ${asOf.toISOString()})`);
|
|
710
|
+
return Result.ok(asOf);
|
|
711
|
+
}
|
|
712
|
+
default: return Result.err(`Unknown asOfSource: "${source}"`);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
//#endregion
|
|
717
|
+
//#region src/core/policies/resolver/policy-resolver.ts
|
|
718
|
+
/**
|
|
719
|
+
* Selects the best policy definition from a list of already-filtered candidates.
|
|
720
|
+
*
|
|
721
|
+
* Ordering criteria (highest priority first):
|
|
722
|
+
* 1. Scope specificity: SCHOOL (3) > TENANT (2) > GLOBAL (1)
|
|
723
|
+
* 2. Priority: higher number wins
|
|
724
|
+
* 3. publishedAt descending (fallback to createdAt)
|
|
725
|
+
* 4. Engine version descending (prefer-latest-engine-version — deterministic tiebreaker)
|
|
726
|
+
* 5. policyVersion descending (semver — final deterministic tiebreaker)
|
|
727
|
+
*/
|
|
728
|
+
var PolicyResolver = class {
|
|
729
|
+
resolveBest(candidates) {
|
|
730
|
+
if (candidates.length === 0) return Result.err("No matching policy definition found (candidates list is empty)");
|
|
731
|
+
const sorted = [...candidates].sort((a, b) => {
|
|
732
|
+
const scopeDiff = PolicyScopeMatcher.weight(b.scope.level) - PolicyScopeMatcher.weight(a.scope.level);
|
|
733
|
+
if (scopeDiff !== 0) return scopeDiff;
|
|
734
|
+
const priorityDiff = b.priority - a.priority;
|
|
735
|
+
if (priorityDiff !== 0) return priorityDiff;
|
|
736
|
+
const aDate = a.publishedAt ?? a.createdAt;
|
|
737
|
+
const dateDiff = (b.publishedAt ?? b.createdAt).getTime() - aDate.getTime();
|
|
738
|
+
if (dateDiff !== 0) return dateDiff;
|
|
739
|
+
const aEngine = a.gateEngineVersion ?? a.computeEngineVersion ?? 0;
|
|
740
|
+
const engineDiff = (b.gateEngineVersion ?? b.computeEngineVersion ?? 0) - aEngine;
|
|
741
|
+
if (engineDiff !== 0) return engineDiff;
|
|
742
|
+
return comparePolicySemver(b.policyVersion, a.policyVersion);
|
|
743
|
+
});
|
|
744
|
+
return Result.ok(sorted[0]);
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
//#endregion
|
|
748
|
+
//#region src/core/policies/service/policy-evaluation-error.ts
|
|
749
|
+
var PolicyEvaluationErrors = class {
|
|
750
|
+
static invalidContext(stage, policyKey, cause) {
|
|
751
|
+
return {
|
|
752
|
+
kind: "INVALID_CONTEXT",
|
|
753
|
+
stage,
|
|
754
|
+
policyKey,
|
|
755
|
+
message: "Policy evaluation context is invalid",
|
|
756
|
+
cause
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
static invalidPolicyKey(rawPolicyKey, cause) {
|
|
760
|
+
return {
|
|
761
|
+
kind: "INVALID_POLICY_KEY",
|
|
762
|
+
rawPolicyKey,
|
|
763
|
+
message: "Policy key is invalid",
|
|
764
|
+
cause
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
static policyNotFound(policyKey, cause) {
|
|
768
|
+
return {
|
|
769
|
+
kind: "POLICY_NOT_FOUND",
|
|
770
|
+
policyKey,
|
|
771
|
+
message: "Policy is not registered in the catalog",
|
|
772
|
+
cause
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
static policyDefinitionNotFound(policyKey, asOf, contextVersion, cause) {
|
|
776
|
+
return {
|
|
777
|
+
kind: "POLICY_DEFINITION_NOT_FOUND",
|
|
778
|
+
policyKey,
|
|
779
|
+
contextVersion,
|
|
780
|
+
asOf,
|
|
781
|
+
message: "No published policy definition matched the evaluation inputs",
|
|
782
|
+
cause
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
static policyVariantNotFound(params) {
|
|
786
|
+
return {
|
|
787
|
+
kind: "POLICY_VARIANT_NOT_FOUND",
|
|
788
|
+
policyKey: params.policyKey,
|
|
789
|
+
policyKind: params.policyKind,
|
|
790
|
+
payloadSchemaVersion: params.payloadSchemaVersion,
|
|
791
|
+
...params.gateEngineVersion !== void 0 ? { gateEngineVersion: params.gateEngineVersion } : {},
|
|
792
|
+
...params.computeEngineVersion !== void 0 ? { computeEngineVersion: params.computeEngineVersion } : {},
|
|
793
|
+
message: "No catalog variant matched the selected policy definition",
|
|
794
|
+
cause: params.cause
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
static engineFailure(policyKey, engine, engineVersion, cause) {
|
|
798
|
+
return {
|
|
799
|
+
kind: "ENGINE_FAILURE",
|
|
800
|
+
policyKey,
|
|
801
|
+
engine,
|
|
802
|
+
engineVersion,
|
|
803
|
+
message: "Policy engine evaluation failed",
|
|
804
|
+
cause
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
//#endregion
|
|
809
|
+
//#region src/core/policies/service/policy-service.ts
|
|
810
|
+
var PolicyService = class {
|
|
811
|
+
#reporter;
|
|
812
|
+
constructor(params) {
|
|
813
|
+
this.catalog = params.catalog;
|
|
814
|
+
this.contextBuilder = params.contextBuilder;
|
|
815
|
+
this.defRepo = params.defRepo;
|
|
816
|
+
this.resolver = params.resolver;
|
|
817
|
+
this.gateEngines = params.gateEngines;
|
|
818
|
+
this.computeRegistry = params.computeRegistry;
|
|
819
|
+
this.options = params.options ?? {};
|
|
820
|
+
this.#reporter = this.options.reporter ?? new SilentPolicyReporter();
|
|
821
|
+
}
|
|
822
|
+
#reportSafely(event) {
|
|
823
|
+
try {
|
|
824
|
+
this.#reporter.report(event);
|
|
825
|
+
} catch {}
|
|
826
|
+
}
|
|
827
|
+
resolveEvaluationNow(seed) {
|
|
828
|
+
const candidate = seed.fields["now"];
|
|
829
|
+
if (candidate === void 0) return Result.ok(/* @__PURE__ */ new Date());
|
|
830
|
+
if (!isValidDate(candidate)) return Result.err("seed.fields.now must be a valid Date when provided");
|
|
831
|
+
return Result.ok(candidate);
|
|
832
|
+
}
|
|
833
|
+
async evaluate(input) {
|
|
834
|
+
const result = await this.#evaluate(input);
|
|
835
|
+
if (result.isErr()) {
|
|
836
|
+
const error = result.errorOrNull();
|
|
837
|
+
this.#reportSafely({
|
|
838
|
+
kind: "policy-evaluation-failed",
|
|
839
|
+
level: "error",
|
|
840
|
+
policyKey: "policyKey" in error ? error.policyKey : void 0,
|
|
841
|
+
errorKind: error.kind,
|
|
842
|
+
message: error.message,
|
|
843
|
+
cause: error.cause,
|
|
844
|
+
occurredAt: /* @__PURE__ */ new Date()
|
|
845
|
+
});
|
|
846
|
+
} else {
|
|
847
|
+
const ok = result.getOrNull();
|
|
848
|
+
this.#reportSafely({
|
|
849
|
+
kind: "policy-evaluation-completed",
|
|
850
|
+
level: "info",
|
|
851
|
+
policyKey: ok.ref.policyKey,
|
|
852
|
+
decisionId: ok.decisionId,
|
|
853
|
+
engineKind: ok.kind,
|
|
854
|
+
occurredAt: ok.evaluatedAt
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
return result;
|
|
858
|
+
}
|
|
859
|
+
async #evaluate(input) {
|
|
860
|
+
const seedResult = ContextSeedValidator.validate(input.seed);
|
|
861
|
+
if (seedResult.isErr()) return Result.err(PolicyEvaluationErrors.invalidContext("SEED_VALIDATION", input.policyKey, seedResult.errorOrNull()));
|
|
862
|
+
const seed = seedResult.getOrNull();
|
|
863
|
+
const catalogFamilyResult = this.#resolveCatalogFamily(input.policyKey);
|
|
864
|
+
if (catalogFamilyResult.isErr()) return Result.err(catalogFamilyResult.errorOrNull());
|
|
865
|
+
const { key: policyKey, family } = catalogFamilyResult.getOrNull();
|
|
866
|
+
const familyEntry = family[0];
|
|
867
|
+
const nowResult = this.resolveEvaluationNow(seed);
|
|
868
|
+
if (nowResult.isErr()) return Result.err(PolicyEvaluationErrors.invalidContext("AS_OF_DERIVATION", policyKey, nowResult.errorOrNull()));
|
|
869
|
+
const now = nowResult.getOrNull();
|
|
870
|
+
const asOfResult = PolicyAsOfResolver.derive(familyEntry.asOfSource, seed, now, this.options.asOf);
|
|
871
|
+
if (asOfResult.isErr()) return Result.err(PolicyEvaluationErrors.invalidContext("AS_OF_DERIVATION", policyKey, asOfResult.errorOrNull()));
|
|
872
|
+
const asOf = asOfResult.getOrNull();
|
|
873
|
+
const definitionResult = this.#findCandidates(policyKey, familyEntry.kind, asOf, input);
|
|
874
|
+
if (definitionResult.isErr()) return Result.err(definitionResult.errorOrNull());
|
|
875
|
+
const { definition: policyDefinition, catalogEntry: versionedCatalogEntry } = definitionResult.getOrNull();
|
|
876
|
+
this.#reportSafely({
|
|
877
|
+
kind: "policy-resolution",
|
|
878
|
+
level: "info",
|
|
879
|
+
policyKey,
|
|
880
|
+
definitionId: policyDefinition.id,
|
|
881
|
+
policyVersion: policyDefinition.policyVersion,
|
|
882
|
+
engineKind: policyDefinition.isGate() ? "GATE" : "COMPUTE",
|
|
883
|
+
engineVersion: policyDefinition.isGate() ? policyDefinition.gateEngineVersion : policyDefinition.computeEngineVersion,
|
|
884
|
+
occurredAt: now
|
|
885
|
+
});
|
|
886
|
+
const ctxResult = await this.contextBuilder.build(versionedCatalogEntry.contextRequirements, seed);
|
|
887
|
+
if (ctxResult.isErr()) return Result.err(PolicyEvaluationErrors.invalidContext("CONTEXT_BUILD", policyKey, ctxResult.errorOrNull()));
|
|
888
|
+
const context = ctxResult.getOrNull();
|
|
889
|
+
const decisionResult = this.#executeEngine(policyKey, policyDefinition, context);
|
|
890
|
+
if (decisionResult.isErr()) return Result.err(decisionResult.errorOrNull());
|
|
891
|
+
const decision = decisionResult.getOrNull();
|
|
892
|
+
const result = {
|
|
893
|
+
decisionId: input.decisionId,
|
|
894
|
+
ref: {
|
|
895
|
+
definitionId: policyDefinition.id,
|
|
896
|
+
policyKey: policyDefinition.policyKey,
|
|
897
|
+
policyVersion: policyDefinition.policyVersion,
|
|
898
|
+
payloadHash: policyDefinition.payloadHash,
|
|
899
|
+
...policyDefinition.isGate() ? { gateEngineVersion: policyDefinition.gateEngineVersion } : { computeEngineVersion: policyDefinition.computeEngineVersion }
|
|
900
|
+
},
|
|
901
|
+
kind: policyDefinition.kind,
|
|
902
|
+
asOf,
|
|
903
|
+
evaluatedAt: now,
|
|
904
|
+
contextVersion: input.contextVersion,
|
|
905
|
+
decision
|
|
906
|
+
};
|
|
907
|
+
return Result.ok(result);
|
|
908
|
+
}
|
|
909
|
+
#resolveCatalogVariant(definition) {
|
|
910
|
+
return this.catalog.getVersioned({
|
|
911
|
+
key: definition.policyKey,
|
|
912
|
+
kind: definition.kind,
|
|
913
|
+
...definition.isGate() ? { gateEngineVersion: definition.gateEngineVersion } : { computeEngineVersion: definition.computeEngineVersion },
|
|
914
|
+
payloadSchemaVersion: definition.payloadSchemaVersion
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
#resolveCatalogFamily(rawKey) {
|
|
918
|
+
const keyResult = PolicyKey.parse(rawKey);
|
|
919
|
+
if (keyResult.isErr()) return Result.err(PolicyEvaluationErrors.invalidPolicyKey(rawKey, keyResult.errorOrNull()));
|
|
920
|
+
const policyKey = keyResult.getOrNull().toString();
|
|
921
|
+
const catalogFamilyResult = this.catalog.getFamily(policyKey);
|
|
922
|
+
if (catalogFamilyResult.isErr()) return Result.err(PolicyEvaluationErrors.policyNotFound(policyKey, catalogFamilyResult.errorOrNull()));
|
|
923
|
+
return Result.ok({
|
|
924
|
+
key: policyKey,
|
|
925
|
+
family: catalogFamilyResult.getOrNull()
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
#findCandidates(policyKey, policyKind, asOf, input) {
|
|
929
|
+
const candidates = this.defRepo.findCandidates({
|
|
930
|
+
policyKey,
|
|
931
|
+
kind: policyKind,
|
|
932
|
+
asOf,
|
|
933
|
+
contextVersion: input.contextVersion,
|
|
934
|
+
scopeChain: input.scopeChain
|
|
935
|
+
});
|
|
936
|
+
const compatibleCandidates = [];
|
|
937
|
+
const compatibleCandidatesByDefinitionId = /* @__PURE__ */ new Map();
|
|
938
|
+
const incompatibleVariantErrorsByDefinitionId = /* @__PURE__ */ new Map();
|
|
939
|
+
for (const candidate of candidates) {
|
|
940
|
+
const compatibilityResult = this.#resolveCatalogVariant(candidate);
|
|
941
|
+
if (compatibilityResult.isErr()) {
|
|
942
|
+
incompatibleVariantErrorsByDefinitionId.set(candidate.id, compatibilityResult.errorOrNull());
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
945
|
+
const resolvedCandidate = {
|
|
946
|
+
definition: candidate,
|
|
947
|
+
catalogEntry: compatibilityResult.getOrNull()
|
|
948
|
+
};
|
|
949
|
+
compatibleCandidates.push(resolvedCandidate);
|
|
950
|
+
compatibleCandidatesByDefinitionId.set(candidate.id, resolvedCandidate);
|
|
951
|
+
}
|
|
952
|
+
if (compatibleCandidates.length === 0 && candidates.length > 0) {
|
|
953
|
+
const selectedCandidateResult = this.resolver.resolveBest(candidates);
|
|
954
|
+
if (selectedCandidateResult.isOk()) {
|
|
955
|
+
const selectedCandidate = selectedCandidateResult.getOrNull();
|
|
956
|
+
return Result.err(PolicyEvaluationErrors.policyVariantNotFound({
|
|
957
|
+
policyKey,
|
|
958
|
+
policyKind: selectedCandidate.kind,
|
|
959
|
+
payloadSchemaVersion: selectedCandidate.payloadSchemaVersion,
|
|
960
|
+
...selectedCandidate.isGate() ? { gateEngineVersion: selectedCandidate.gateEngineVersion } : { computeEngineVersion: selectedCandidate.computeEngineVersion },
|
|
961
|
+
cause: incompatibleVariantErrorsByDefinitionId.get(selectedCandidate.id) ?? "No compatible catalog variant matched the selected policy definition"
|
|
962
|
+
}));
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
const bestResult = this.resolver.resolveBest(compatibleCandidates.map((candidate) => candidate.definition));
|
|
966
|
+
if (bestResult.isErr()) return Result.err(PolicyEvaluationErrors.policyDefinitionNotFound(policyKey, asOf, input.contextVersion, bestResult.errorOrNull()));
|
|
967
|
+
const bestDefinition = bestResult.getOrNull();
|
|
968
|
+
const resolvedCandidate = compatibleCandidatesByDefinitionId.get(bestDefinition.id);
|
|
969
|
+
if (resolvedCandidate === void 0) return Result.err(PolicyEvaluationErrors.policyDefinitionNotFound(policyKey, asOf, input.contextVersion, `Resolved definition "${bestDefinition.id}" is not present among compatible catalog variants`));
|
|
970
|
+
return Result.ok(resolvedCandidate);
|
|
971
|
+
}
|
|
972
|
+
#executeEngine(policyKey, definition, context) {
|
|
973
|
+
if (definition.isGate()) {
|
|
974
|
+
const gateResult = this.gateEngines.evaluate(definition.gateEngineVersion, definition.payloadJson, context);
|
|
975
|
+
if (gateResult.isErr()) return Result.err(PolicyEvaluationErrors.engineFailure(policyKey, "GATE", definition.gateEngineVersion, gateResult.errorOrNull()));
|
|
976
|
+
return Result.ok(gateResult.getOrNull());
|
|
977
|
+
}
|
|
978
|
+
if (!definition.isCompute()) return Result.err(PolicyEvaluationErrors.policyVariantNotFound({
|
|
979
|
+
policyKey,
|
|
980
|
+
policyKind: definition.kind,
|
|
981
|
+
payloadSchemaVersion: definition.payloadSchemaVersion,
|
|
982
|
+
cause: `Unsupported policy kind "${definition.kind}"`
|
|
983
|
+
}));
|
|
984
|
+
const computeResult = this.computeRegistry.evaluate(policyKey, definition.computeEngineVersion, definition.payloadSchemaVersion, definition.payloadJson, context);
|
|
985
|
+
if (computeResult.isErr()) return Result.err(PolicyEvaluationErrors.engineFailure(policyKey, "COMPUTE", definition.computeEngineVersion, computeResult.errorOrNull()));
|
|
986
|
+
return Result.ok(computeResult.getOrNull());
|
|
987
|
+
}
|
|
988
|
+
};
|
|
989
|
+
//#endregion
|
|
990
|
+
export { asPolicyDecisionId as _, ContextSeedValidator as a, asTenantId as b, ContextResolverRegistry as c, PolicyDefinition as d, InMemoryPolicyDefinitionRepository as f, PolicyCatalog as g, PolicyCatalogFactory as h, PolicyAsOfResolver as i, registerNamespacedContextResolversIn as l, PolicyKey as m, PolicyEvaluationErrors as n, contextResolverRegistry as o, PolicyScopeMatcher as p, PolicyResolver as r, registerNamespacedContextResolvers as s, PolicyService as t, PolicyContextBuilder as u, asPolicyDefinitionId as v, PolicyHashing as x, asSchoolId as y };
|
|
991
|
+
|
|
992
|
+
//# sourceMappingURL=policy-service.js.map
|