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