@cullet/erp-core 1.3.0 → 1.4.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 +5 -3
- package/README.md +160 -0
- package/dist/abac/index.cjs +7 -0
- package/dist/abac/index.d.cts +2 -0
- package/dist/abac/index.d.ts +2 -0
- package/dist/abac/index.js +2 -0
- package/dist/app-error.cjs +193 -0
- package/dist/app-error.cjs.map +1 -0
- package/dist/app-error.js +170 -0
- package/dist/app-error.js.map +1 -0
- package/dist/application/index.cjs +2 -1
- package/dist/application/index.d.cts +2 -1
- package/dist/application/index.d.ts +2 -1
- package/dist/application/index.js +2 -1
- package/dist/authorization-error.cjs +157 -0
- package/dist/authorization-error.cjs.map +1 -0
- package/dist/authorization-error.d.cts +113 -0
- package/dist/authorization-error.d.ts +113 -0
- package/dist/authorization-error.js +152 -0
- package/dist/authorization-error.js.map +1 -0
- package/dist/authorizer.port.d.cts +130 -0
- package/dist/authorizer.port.d.ts +130 -0
- package/dist/composite-authorizer.d.cts +216 -0
- package/dist/composite-authorizer.d.ts +216 -0
- package/dist/condition-evaluator.cjs +565 -0
- package/dist/condition-evaluator.cjs.map +1 -0
- package/dist/condition-evaluator.js +536 -0
- package/dist/condition-evaluator.js.map +1 -0
- package/dist/{gate-types.d.ts → core-config.d.cts} +78 -77
- package/dist/{gate-types.d.cts → core-config.d.ts} +78 -77
- package/dist/domain/index.cjs +2 -1
- package/dist/domain/index.d.cts +2 -1
- package/dist/domain/index.d.ts +2 -1
- package/dist/domain/index.js +2 -1
- package/dist/domain-event-contracts.cjs +3 -2
- package/dist/domain-event-contracts.cjs.map +1 -1
- package/dist/domain-event-contracts.js +2 -1
- package/dist/domain-event-contracts.js.map +1 -1
- package/dist/entity.cjs +126 -0
- package/dist/entity.cjs.map +1 -0
- package/dist/entity.js +115 -0
- package/dist/entity.js.map +1 -0
- package/dist/errors/index.cjs +8 -6
- package/dist/errors/index.d.cts +2 -1
- package/dist/errors/index.d.ts +2 -1
- package/dist/errors/index.js +4 -2
- package/dist/gate-engine-registry.cjs +5 -4
- package/dist/gate-engine-registry.cjs.map +1 -1
- package/dist/gate-engine-registry.d.cts +3 -2
- package/dist/gate-engine-registry.d.ts +3 -2
- package/dist/gate-engine-registry.js +2 -1
- package/dist/gate-engine-registry.js.map +1 -1
- package/dist/gate-v1-payload.schema.cjs +0 -562
- package/dist/gate-v1-payload.schema.cjs.map +1 -1
- package/dist/gate-v1-payload.schema.js +1 -533
- package/dist/gate-v1-payload.schema.js.map +1 -1
- package/dist/index.cjs +34 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -12
- package/dist/index.d.ts +19 -12
- package/dist/index.js +15 -9
- package/dist/index.js.map +1 -1
- package/dist/not-found-error.cjs +5 -5
- package/dist/not-found-error.cjs.map +1 -1
- package/dist/not-found-error.js +1 -1
- package/dist/outcome.d.cts +1 -83
- package/dist/outcome.d.ts +1 -83
- package/dist/parse-gate-payload.d.cts +2 -2
- package/dist/parse-gate-payload.d.ts +2 -2
- package/dist/path.d.cts +2 -2
- package/dist/path.d.ts +2 -2
- package/dist/policies/engines/index.d.cts +1 -1
- package/dist/policies/engines/index.d.ts +1 -1
- package/dist/policies/engines/v1/gate/index.cjs +5 -4
- package/dist/policies/engines/v1/gate/index.cjs.map +1 -1
- package/dist/policies/engines/v1/gate/index.d.cts +2 -2
- package/dist/policies/engines/v1/gate/index.d.ts +2 -2
- 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 +5 -5
- package/dist/policies/index.d.cts +3 -2
- package/dist/policies/index.d.ts +3 -2
- package/dist/policies/index.js +2 -2
- package/dist/policy-bridge.cjs +437 -0
- package/dist/policy-bridge.cjs.map +1 -0
- package/dist/policy-bridge.d.cts +185 -0
- package/dist/policy-bridge.d.ts +185 -0
- package/dist/policy-bridge.js +396 -0
- package/dist/policy-bridge.js.map +1 -0
- package/dist/policy-service.cjs +239 -239
- package/dist/policy-service.cjs.map +1 -1
- package/dist/policy-service.d.cts +2 -2
- package/dist/policy-service.d.ts +2 -2
- package/dist/policy-service.js +239 -239
- package/dist/policy-service.js.map +1 -1
- package/dist/rbac/index.cjs +9 -0
- package/dist/rbac/index.d.cts +3 -0
- package/dist/rbac/index.d.ts +3 -0
- package/dist/rbac/index.js +2 -0
- package/dist/requested-by.cjs +54 -0
- package/dist/requested-by.cjs.map +1 -0
- package/dist/requested-by.d.cts +25 -0
- package/dist/requested-by.d.ts +25 -0
- package/dist/requested-by.js +49 -0
- package/dist/requested-by.js.map +1 -0
- package/dist/result/index.d.cts +2 -1
- package/dist/result/index.d.ts +2 -1
- package/dist/result.d.cts +84 -0
- package/dist/result.d.ts +84 -0
- package/dist/rule.cjs +327 -0
- package/dist/rule.cjs.map +1 -0
- package/dist/rule.js +298 -0
- package/dist/rule.js.map +1 -0
- package/dist/temporal-use-case.cjs +1 -53
- package/dist/temporal-use-case.cjs.map +1 -1
- package/dist/temporal-use-case.d.cts +5 -27
- package/dist/temporal-use-case.d.ts +5 -27
- package/dist/temporal-use-case.js +2 -48
- package/dist/temporal-use-case.js.map +1 -1
- package/dist/unexpected-error.cjs +3 -192
- package/dist/unexpected-error.cjs.map +1 -1
- package/dist/unexpected-error.js +2 -167
- package/dist/unexpected-error.js.map +1 -1
- package/dist/uuid-identifier.d.cts +2 -85
- package/dist/uuid-identifier.d.ts +2 -85
- package/dist/validation-error.cjs +33 -166
- package/dist/validation-error.cjs.map +1 -1
- package/dist/validation-error.d.cts +2 -98
- package/dist/validation-error.d.ts +2 -98
- package/dist/validation-error.js +7 -134
- package/dist/validation-error.js.map +1 -1
- package/dist/value-object.cjs +0 -123
- package/dist/value-object.cjs.map +1 -1
- package/dist/value-object.d.cts +89 -0
- package/dist/value-object.d.ts +89 -0
- package/dist/value-object.js +1 -112
- package/dist/value-object.js.map +1 -1
- package/meta.json +18 -4
- package/package.json +27 -1
- package/src/abac/index.ts +1 -0
- package/src/core/abac/abac-request.ts +29 -0
- package/src/core/abac/attributes.ts +39 -0
- package/src/core/abac/authorizer.ts +108 -0
- package/src/core/abac/combining.ts +63 -0
- package/src/core/abac/composite-authorizer.ts +45 -0
- package/src/core/abac/domain/policy-set.ts +52 -0
- package/src/core/abac/domain/rule.ts +197 -0
- package/src/core/abac/index.ts +21 -0
- package/src/core/application/ports/abac-authorizer.port.ts +20 -0
- package/src/core/application/ports/authorizer.port.ts +22 -0
- package/src/core/errors/authorization-error.ts +26 -0
- package/src/core/errors/error-codes.ts +1 -0
- package/src/core/index.ts +7 -0
- package/src/core/rbac/access-request.ts +32 -0
- package/src/core/rbac/authorizer.ts +91 -0
- package/src/core/rbac/domain/grant.ts +96 -0
- package/src/core/rbac/domain/permission-set.ts +67 -0
- package/src/core/rbac/domain/permission.ts +119 -0
- package/src/core/rbac/domain/role.ts +101 -0
- package/src/core/rbac/domain/scope.ts +84 -0
- package/src/core/rbac/index.ts +15 -0
- package/src/core/rbac/policy-bridge.ts +44 -0
- package/src/examples/application/authorize-cancel-order-abac.example.ts +107 -0
- package/src/examples/application/authorize-cancel-order.example.ts +101 -0
- package/src/rbac/index.ts +1 -0
- package/src/version.ts +1 -1
package/dist/policy-service.js
CHANGED
|
@@ -1,12 +1,248 @@
|
|
|
1
|
-
import { t as UnexpectedError } from "./unexpected-error.js";
|
|
2
1
|
import { n as sha256Hex, r as stableStringify } from "./hashing.js";
|
|
2
|
+
import { t as UnexpectedError } from "./unexpected-error.js";
|
|
3
3
|
import { t as ValidationCode } from "./validation-code.js";
|
|
4
4
|
import { t as ValidationField } from "./validation-field.js";
|
|
5
5
|
import { t as InvariantViolationException } from "./invariant-violation-exception.js";
|
|
6
6
|
import { t as InvalidValueException } from "./validation-exception.js";
|
|
7
7
|
import { r as isValidDate } from "./temporal-guards.js";
|
|
8
|
-
import { l as SilentPolicyReporter, o as PolicyContextPath } from "./gate-v1-payload.schema.js";
|
|
9
8
|
import { r as Result } from "./result.js";
|
|
9
|
+
import { a as SilentPolicyReporter, n as PolicyContextPath } from "./condition-evaluator.js";
|
|
10
|
+
//#region src/core/policies/context/context-builder.ts
|
|
11
|
+
const DEFAULT_RESOLVER_RETRY_OPTIONS = {
|
|
12
|
+
maxAttempts: 1,
|
|
13
|
+
initialDelayMs: 25,
|
|
14
|
+
maxDelayMs: 1e3,
|
|
15
|
+
backoffMultiplier: 2
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Builds a context object by resolving required paths from a seed.
|
|
19
|
+
*/
|
|
20
|
+
var PolicyContextBuilder = class PolicyContextBuilder {
|
|
21
|
+
constructor(registry, options = {}) {
|
|
22
|
+
this.registry = registry;
|
|
23
|
+
this.circuitStates = /* @__PURE__ */ new Map();
|
|
24
|
+
this.defaultResolverResilience = options.defaultResolverResilience;
|
|
25
|
+
this.now = options.now ?? Date.now;
|
|
26
|
+
this.sleep = options.sleep ?? (async (delayMs) => {
|
|
27
|
+
if (delayMs <= 0) return;
|
|
28
|
+
await new Promise((resolve) => {
|
|
29
|
+
setTimeout(resolve, delayMs);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
static clampPositiveInteger(value, fallback) {
|
|
34
|
+
if (value === void 0 || !Number.isFinite(value)) return fallback;
|
|
35
|
+
return Math.max(1, Math.trunc(value));
|
|
36
|
+
}
|
|
37
|
+
static clampNonNegativeInteger(value, fallback) {
|
|
38
|
+
if (value === void 0 || !Number.isFinite(value)) return fallback;
|
|
39
|
+
return Math.max(0, Math.trunc(value));
|
|
40
|
+
}
|
|
41
|
+
static normalizeTimeoutMs(value) {
|
|
42
|
+
if (value === void 0 || !Number.isFinite(value) || value <= 0) return null;
|
|
43
|
+
return Math.trunc(value);
|
|
44
|
+
}
|
|
45
|
+
static mergeRetryOptions(defaults, overrides) {
|
|
46
|
+
if (overrides === false) return false;
|
|
47
|
+
if (overrides !== void 0) return {
|
|
48
|
+
...defaults === false || defaults === void 0 ? {} : defaults,
|
|
49
|
+
...overrides
|
|
50
|
+
};
|
|
51
|
+
return defaults;
|
|
52
|
+
}
|
|
53
|
+
static mergeCircuitBreakerOptions(defaults, overrides) {
|
|
54
|
+
if (overrides === false) return false;
|
|
55
|
+
if (overrides !== void 0) return {
|
|
56
|
+
...defaults === false || defaults === void 0 ? {} : defaults,
|
|
57
|
+
...overrides
|
|
58
|
+
};
|
|
59
|
+
return defaults;
|
|
60
|
+
}
|
|
61
|
+
resolveResilienceOptions(resolver) {
|
|
62
|
+
const defaults = this.defaultResolverResilience;
|
|
63
|
+
const override = resolver.resilience;
|
|
64
|
+
const retry = PolicyContextBuilder.mergeRetryOptions(defaults?.retry, override?.retry);
|
|
65
|
+
const circuitBreaker = PolicyContextBuilder.mergeCircuitBreakerOptions(defaults?.circuitBreaker, override?.circuitBreaker);
|
|
66
|
+
const retryOptions = retry === false ? void 0 : retry;
|
|
67
|
+
const circuitBreakerOptions = circuitBreaker === false ? void 0 : circuitBreaker;
|
|
68
|
+
return {
|
|
69
|
+
timeoutMs: PolicyContextBuilder.normalizeTimeoutMs(override?.timeoutMs ?? defaults?.timeoutMs),
|
|
70
|
+
retry: {
|
|
71
|
+
maxAttempts: PolicyContextBuilder.clampPositiveInteger(retryOptions?.maxAttempts, DEFAULT_RESOLVER_RETRY_OPTIONS.maxAttempts),
|
|
72
|
+
initialDelayMs: PolicyContextBuilder.clampNonNegativeInteger(retryOptions?.initialDelayMs, DEFAULT_RESOLVER_RETRY_OPTIONS.initialDelayMs),
|
|
73
|
+
maxDelayMs: PolicyContextBuilder.clampNonNegativeInteger(retryOptions?.maxDelayMs, DEFAULT_RESOLVER_RETRY_OPTIONS.maxDelayMs),
|
|
74
|
+
backoffMultiplier: PolicyContextBuilder.clampPositiveInteger(retryOptions?.backoffMultiplier, DEFAULT_RESOLVER_RETRY_OPTIONS.backoffMultiplier)
|
|
75
|
+
},
|
|
76
|
+
circuitBreaker: circuitBreakerOptions === void 0 ? null : {
|
|
77
|
+
failureThreshold: PolicyContextBuilder.clampPositiveInteger(circuitBreakerOptions.failureThreshold, 5),
|
|
78
|
+
cooldownMs: PolicyContextBuilder.clampPositiveInteger(circuitBreakerOptions.cooldownMs, 3e4)
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
getRetryDelayMs(attempt, retry) {
|
|
83
|
+
return Math.min(retry.initialDelayMs * retry.backoffMultiplier ** (attempt - 1), retry.maxDelayMs);
|
|
84
|
+
}
|
|
85
|
+
resetCircuit(path) {
|
|
86
|
+
this.circuitStates.delete(path);
|
|
87
|
+
}
|
|
88
|
+
isCircuitOpen(path, circuitBreaker) {
|
|
89
|
+
if (circuitBreaker === null) return false;
|
|
90
|
+
const state = this.circuitStates.get(path);
|
|
91
|
+
if (state === void 0 || state.openUntilMs === null) return false;
|
|
92
|
+
if (state.openUntilMs <= this.now()) {
|
|
93
|
+
this.resetCircuit(path);
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
recordResolverFailure(path, circuitBreaker) {
|
|
99
|
+
if (circuitBreaker === null) return;
|
|
100
|
+
const nextFailureCount = (this.circuitStates.get(path)?.failureCount ?? 0) + 1;
|
|
101
|
+
if (nextFailureCount >= circuitBreaker.failureThreshold) {
|
|
102
|
+
this.circuitStates.set(path, {
|
|
103
|
+
failureCount: 0,
|
|
104
|
+
openUntilMs: this.now() + circuitBreaker.cooldownMs
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
this.circuitStates.set(path, {
|
|
109
|
+
failureCount: nextFailureCount,
|
|
110
|
+
openUntilMs: null
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
static describeThrownResolverError(error) {
|
|
114
|
+
if (error instanceof Error) return `threw ${error.name}: ${error.message}`;
|
|
115
|
+
return `threw ${String(error)}`;
|
|
116
|
+
}
|
|
117
|
+
async resolveOnce(resolver, seed, timeoutMs) {
|
|
118
|
+
const execute = async () => {
|
|
119
|
+
try {
|
|
120
|
+
return await resolver.resolve(seed);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
return Result.err(PolicyContextBuilder.describeThrownResolverError(error));
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
if (timeoutMs === null) return execute();
|
|
126
|
+
let timeoutId;
|
|
127
|
+
try {
|
|
128
|
+
return await Promise.race([execute(), new Promise((resolve) => {
|
|
129
|
+
timeoutId = setTimeout(() => {
|
|
130
|
+
resolve(Result.err(`timed out after ${timeoutMs}ms`));
|
|
131
|
+
}, timeoutMs);
|
|
132
|
+
})]);
|
|
133
|
+
} finally {
|
|
134
|
+
if (timeoutId !== void 0) clearTimeout(timeoutId);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async resolveWithResilience(path, resolver, seed) {
|
|
138
|
+
const resilience = this.resolveResilienceOptions(resolver);
|
|
139
|
+
if (this.isCircuitOpen(path, resilience.circuitBreaker)) return Result.err("circuit breaker is open");
|
|
140
|
+
let lastError = "resolver failed";
|
|
141
|
+
for (let attempt = 1; attempt <= resilience.retry.maxAttempts; attempt++) {
|
|
142
|
+
const result = await this.resolveOnce(resolver, seed, resilience.timeoutMs);
|
|
143
|
+
if (result.isOk()) {
|
|
144
|
+
this.resetCircuit(path);
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
lastError = result.errorOrNull() ?? lastError;
|
|
148
|
+
if (attempt < resilience.retry.maxAttempts) await this.sleep(this.getRetryDelayMs(attempt, resilience.retry));
|
|
149
|
+
}
|
|
150
|
+
this.recordResolverFailure(path, resilience.circuitBreaker);
|
|
151
|
+
return Result.err(lastError);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Resolves all required context paths and returns a flat context object.
|
|
155
|
+
* If any required path cannot be resolved, returns an error.
|
|
156
|
+
*/
|
|
157
|
+
async build(requirements, seed) {
|
|
158
|
+
const context = {};
|
|
159
|
+
for (const path of requirements) {
|
|
160
|
+
const resolver = this.registry.get(path);
|
|
161
|
+
if (!resolver) return Result.err(`Missing resolver for path "${path}"`);
|
|
162
|
+
const result = await this.resolveWithResilience(path, resolver, seed);
|
|
163
|
+
if (result.isErr()) return Result.err(`Resolver error for path "${path}": ${result.errorOrNull()}`);
|
|
164
|
+
const setPathResult = PolicyContextPath.set(context, path, result.getOrNull());
|
|
165
|
+
if (setPathResult.isErr()) return Result.err(setPathResult.errorOrNull());
|
|
166
|
+
}
|
|
167
|
+
const missing = requirements.filter((path) => {
|
|
168
|
+
const result = PolicyContextPath.getOrAbsent(context, path);
|
|
169
|
+
return result.isErr() || result.getOrNull() === void 0;
|
|
170
|
+
});
|
|
171
|
+
if (missing.length > 0) return Result.err(`Context missing required paths after resolution: ${missing.join(", ")}`);
|
|
172
|
+
return Result.ok(context);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
//#endregion
|
|
176
|
+
//#region src/core/policies/context/context-registry.ts
|
|
177
|
+
const DEFAULT_CONTEXT_RESOLVER_NAMESPACE = "default";
|
|
178
|
+
function normalizeNamespace(namespace) {
|
|
179
|
+
const normalized = namespace?.trim();
|
|
180
|
+
if (normalized === void 0 || normalized.length === 0) return DEFAULT_CONTEXT_RESOLVER_NAMESPACE;
|
|
181
|
+
return normalized;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Registry of context value resolvers, keyed by path.
|
|
185
|
+
*/
|
|
186
|
+
var ContextResolverRegistry = class {
|
|
187
|
+
constructor() {
|
|
188
|
+
this.resolvers = /* @__PURE__ */ new Map();
|
|
189
|
+
}
|
|
190
|
+
register(resolver, options = {}) {
|
|
191
|
+
const namespace = normalizeNamespace(options.namespace);
|
|
192
|
+
const existing = this.resolvers.get(resolver.path);
|
|
193
|
+
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}".`);
|
|
194
|
+
this.resolvers.set(resolver.path, {
|
|
195
|
+
namespace,
|
|
196
|
+
resolver
|
|
197
|
+
});
|
|
198
|
+
return Result.ok(void 0);
|
|
199
|
+
}
|
|
200
|
+
registerAll(resolvers, options = {}) {
|
|
201
|
+
for (const r of resolvers) {
|
|
202
|
+
const registrationResult = this.register(r, options);
|
|
203
|
+
if (registrationResult.isErr()) return registrationResult;
|
|
204
|
+
}
|
|
205
|
+
return Result.ok(void 0);
|
|
206
|
+
}
|
|
207
|
+
get(path) {
|
|
208
|
+
return this.resolvers.get(path)?.resolver;
|
|
209
|
+
}
|
|
210
|
+
has(path) {
|
|
211
|
+
return this.resolvers.has(path);
|
|
212
|
+
}
|
|
213
|
+
getNamespace(path) {
|
|
214
|
+
return this.resolvers.get(path)?.namespace;
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
function registerNamespacedContextResolversIn(registry, namespace, resolvers) {
|
|
218
|
+
const registrationResult = registry.registerAll(resolvers, { namespace });
|
|
219
|
+
if (registrationResult.isErr()) return Result.err(registrationResult.errorOrNull());
|
|
220
|
+
return Result.ok(registry);
|
|
221
|
+
}
|
|
222
|
+
//#endregion
|
|
223
|
+
//#region src/core/policies/context/context-registry.instance.ts
|
|
224
|
+
/**
|
|
225
|
+
* Official instance of the ContextResolverRegistry.
|
|
226
|
+
* Use `new ContextResolverRegistry()` plus `registerNamespacedContextResolversIn`
|
|
227
|
+
* when each runtime/composition root needs its own isolated resolver graph.
|
|
228
|
+
*/
|
|
229
|
+
const contextResolverRegistry = new ContextResolverRegistry();
|
|
230
|
+
function registerNamespacedContextResolvers(namespace, resolvers) {
|
|
231
|
+
return registerNamespacedContextResolversIn(contextResolverRegistry, namespace, resolvers);
|
|
232
|
+
}
|
|
233
|
+
//#endregion
|
|
234
|
+
//#region src/core/policies/context/context-seed.ts
|
|
235
|
+
var ContextSeedValidator = class ContextSeedValidator {
|
|
236
|
+
static isBlankSeedId(value) {
|
|
237
|
+
return value.trim().length === 0;
|
|
238
|
+
}
|
|
239
|
+
static validate(seed) {
|
|
240
|
+
if (ContextSeedValidator.isBlankSeedId(seed.tenantId)) return Result.err("ContextSeed tenantId must be a non-empty string");
|
|
241
|
+
if (ContextSeedValidator.isBlankSeedId(seed.schoolId)) return Result.err("ContextSeed schoolId must be a non-empty string");
|
|
242
|
+
return Result.ok(seed);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
//#endregion
|
|
10
246
|
//#region src/core/policies/utils/hash.ts
|
|
11
247
|
/**
|
|
12
248
|
* Produces a deterministic SHA-256 hex digest of a canonical JSON representation.
|
|
@@ -487,242 +723,6 @@ var PolicyDefinition = class PolicyDefinition {
|
|
|
487
723
|
}
|
|
488
724
|
};
|
|
489
725
|
//#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 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(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 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 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 Result.err(`Missing resolver for path "${path}"`);
|
|
642
|
-
const result = await this.resolveWithResilience(path, resolver, seed);
|
|
643
|
-
if (result.isErr()) return Result.err(`Resolver error for path "${path}": ${result.errorOrNull()}`);
|
|
644
|
-
const setPathResult = PolicyContextPath.set(context, path, result.getOrNull());
|
|
645
|
-
if (setPathResult.isErr()) return Result.err(setPathResult.errorOrNull());
|
|
646
|
-
}
|
|
647
|
-
const missing = requirements.filter((path) => {
|
|
648
|
-
const result = PolicyContextPath.getOrAbsent(context, path);
|
|
649
|
-
return result.isErr() || result.getOrNull() === void 0;
|
|
650
|
-
});
|
|
651
|
-
if (missing.length > 0) return Result.err(`Context missing required paths after resolution: ${missing.join(", ")}`);
|
|
652
|
-
return 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 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 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 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 Result.err(registrationResult.errorOrNull());
|
|
700
|
-
return 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 Result.err("ContextSeed tenantId must be a non-empty string");
|
|
721
|
-
if (ContextSeedValidator.isBlankSeedId(seed.schoolId)) return Result.err("ContextSeed schoolId must be a non-empty string");
|
|
722
|
-
return Result.ok(seed);
|
|
723
|
-
}
|
|
724
|
-
};
|
|
725
|
-
//#endregion
|
|
726
726
|
//#region src/core/policies/asof/asof.ts
|
|
727
727
|
const DEFAULT_MAX_AS_OF_FUTURE_YEARS = 10;
|
|
728
728
|
var PolicyAsOfResolver = class PolicyAsOfResolver {
|
|
@@ -1060,6 +1060,6 @@ var PolicyService = class {
|
|
|
1060
1060
|
}
|
|
1061
1061
|
};
|
|
1062
1062
|
//#endregion
|
|
1063
|
-
export {
|
|
1063
|
+
export { contextResolverRegistry as _, PolicyDefinition as a, registerNamespacedContextResolversIn as b, PolicyKey as c, asPolicyDecisionId as d, asPolicyDefinitionId as f, ContextSeedValidator as g, PolicyHashing as h, PolicyAsOfResolver as i, PolicyCatalogFactory as l, asTenantId as m, PolicyEvaluationErrors as n, InMemoryPolicyDefinitionRepository as o, asSchoolId as p, PolicyResolver as r, PolicyScopeMatcher as s, PolicyService as t, PolicyCatalog as u, registerNamespacedContextResolvers as v, PolicyContextBuilder as x, ContextResolverRegistry as y };
|
|
1064
1064
|
|
|
1065
1065
|
//# sourceMappingURL=policy-service.js.map
|