@fabricorg/platform 0.1.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/LICENSE +21 -0
- package/README.md +104 -0
- package/dist/actions/index.d.ts +114 -0
- package/dist/actions/index.js +59 -0
- package/dist/actions/index.js.map +1 -0
- package/dist/adapters/index.d.ts +83 -0
- package/dist/adapters/index.js +101 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/displays/index.d.ts +34 -0
- package/dist/displays/index.js +89 -0
- package/dist/displays/index.js.map +1 -0
- package/dist/events/index.d.ts +35 -0
- package/dist/events/index.js +55 -0
- package/dist/events/index.js.map +1 -0
- package/dist/evidence/index.d.ts +43 -0
- package/dist/evidence/index.js +3 -0
- package/dist/evidence/index.js.map +1 -0
- package/dist/ids/index.d.ts +19 -0
- package/dist/ids/index.js +55 -0
- package/dist/ids/index.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +1357 -0
- package/dist/index.js.map +1 -0
- package/dist/modules/index.d.ts +73 -0
- package/dist/modules/index.js +375 -0
- package/dist/modules/index.js.map +1 -0
- package/dist/objects/index.d.ts +7 -0
- package/dist/objects/index.js +30 -0
- package/dist/objects/index.js.map +1 -0
- package/dist/policies/index.d.ts +143 -0
- package/dist/policies/index.js +544 -0
- package/dist/policies/index.js.map +1 -0
- package/dist/privacy/index.d.ts +77 -0
- package/dist/privacy/index.js +6 -0
- package/dist/privacy/index.js.map +1 -0
- package/dist/projections/index.d.ts +52 -0
- package/dist/projections/index.js +181 -0
- package/dist/projections/index.js.map +1 -0
- package/dist/state-machines/index.d.ts +60 -0
- package/dist/state-machines/index.js +67 -0
- package/dist/state-machines/index.js.map +1 -0
- package/dist/testing/index.d.ts +19 -0
- package/dist/testing/index.js +58 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +113 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
type PolicyEvaluationResult = "pass" | "warn" | "block";
|
|
2
|
+
type PolicyKind = "code" | "data" | "hybrid";
|
|
3
|
+
type PolicyId = `${string}.v${number}`;
|
|
4
|
+
type PolicyEvaluationMode = "preview" | "execute";
|
|
5
|
+
declare function assertPolicyId(policyId: string): asserts policyId is PolicyId;
|
|
6
|
+
interface PolicyContext<TDb = unknown> {
|
|
7
|
+
tenantId: string;
|
|
8
|
+
spaceId: string;
|
|
9
|
+
actionInvocationId: string;
|
|
10
|
+
actionId: string;
|
|
11
|
+
parameters: unknown;
|
|
12
|
+
db: TDb;
|
|
13
|
+
now?: Date;
|
|
14
|
+
mode?: PolicyEvaluationMode;
|
|
15
|
+
}
|
|
16
|
+
interface PolicyOutcome {
|
|
17
|
+
policyId: PolicyId;
|
|
18
|
+
policyVersion: number;
|
|
19
|
+
policyKind?: PolicyKind;
|
|
20
|
+
result: PolicyEvaluationResult;
|
|
21
|
+
reason?: string;
|
|
22
|
+
metadata?: Record<string, unknown>;
|
|
23
|
+
dispatchEvidence?: PolicyDispatchEvidence;
|
|
24
|
+
}
|
|
25
|
+
interface PolicyEvaluator<TDb = unknown> {
|
|
26
|
+
policyId: PolicyId;
|
|
27
|
+
version: number;
|
|
28
|
+
kind?: Extract<PolicyKind, "code">;
|
|
29
|
+
previewSafe?: boolean;
|
|
30
|
+
evaluate: (ctx: PolicyContext<TDb>) => Promise<PolicyOutcome>;
|
|
31
|
+
}
|
|
32
|
+
type DataPolicySource = "parameters" | "context";
|
|
33
|
+
type DataPolicyLiteral = string | number | boolean | null;
|
|
34
|
+
type DataPolicyComparisonOperator = "exists" | "equals" | "notEquals" | "gt" | "gte" | "lt" | "lte";
|
|
35
|
+
interface DataPolicySourceReference {
|
|
36
|
+
source: DataPolicySource;
|
|
37
|
+
path: string;
|
|
38
|
+
}
|
|
39
|
+
type DataPolicyCondition = {
|
|
40
|
+
conditionId: string;
|
|
41
|
+
type: "always";
|
|
42
|
+
result: PolicyEvaluationResult;
|
|
43
|
+
reason?: string;
|
|
44
|
+
} | {
|
|
45
|
+
conditionId: string;
|
|
46
|
+
type: "parameter";
|
|
47
|
+
path: string;
|
|
48
|
+
operator: DataPolicyComparisonOperator;
|
|
49
|
+
value?: DataPolicyLiteral;
|
|
50
|
+
onPass?: PolicyEvaluationResult;
|
|
51
|
+
onFail?: PolicyEvaluationResult;
|
|
52
|
+
reason?: string;
|
|
53
|
+
} | {
|
|
54
|
+
conditionId: string;
|
|
55
|
+
type: "comparison";
|
|
56
|
+
left: DataPolicySourceReference;
|
|
57
|
+
operator: DataPolicyComparisonOperator;
|
|
58
|
+
value?: DataPolicyLiteral;
|
|
59
|
+
onPass?: PolicyEvaluationResult;
|
|
60
|
+
onFail?: PolicyEvaluationResult;
|
|
61
|
+
reason?: string;
|
|
62
|
+
} | {
|
|
63
|
+
conditionId: string;
|
|
64
|
+
type: "all" | "any";
|
|
65
|
+
conditions: DataPolicyCondition[];
|
|
66
|
+
onPass?: PolicyEvaluationResult;
|
|
67
|
+
onFail?: PolicyEvaluationResult;
|
|
68
|
+
reason?: string;
|
|
69
|
+
} | {
|
|
70
|
+
conditionId: string;
|
|
71
|
+
type: "not";
|
|
72
|
+
condition: DataPolicyCondition;
|
|
73
|
+
onPass?: PolicyEvaluationResult;
|
|
74
|
+
onFail?: PolicyEvaluationResult;
|
|
75
|
+
reason?: string;
|
|
76
|
+
};
|
|
77
|
+
interface DataPolicyDefinition {
|
|
78
|
+
conditions: DataPolicyCondition[];
|
|
79
|
+
defaultResult?: PolicyEvaluationResult;
|
|
80
|
+
reason?: string;
|
|
81
|
+
}
|
|
82
|
+
interface DataPolicyValidationResult {
|
|
83
|
+
valid: boolean;
|
|
84
|
+
errors: string[];
|
|
85
|
+
}
|
|
86
|
+
interface PolicyFallbackDefinition {
|
|
87
|
+
codeEvaluatorPolicyId: PolicyId;
|
|
88
|
+
onResults?: PolicyEvaluationResult[];
|
|
89
|
+
triggers?: PolicyFallbackTrigger[];
|
|
90
|
+
reason?: string;
|
|
91
|
+
}
|
|
92
|
+
type PolicyFallbackTrigger = "data_result" | "missing_data_definition" | "invalid_data_definition";
|
|
93
|
+
interface DataPolicyConditionResult {
|
|
94
|
+
conditionId: string;
|
|
95
|
+
passed: boolean;
|
|
96
|
+
result: PolicyEvaluationResult;
|
|
97
|
+
reason?: string;
|
|
98
|
+
}
|
|
99
|
+
interface RuntimePolicyDefinition {
|
|
100
|
+
policyId: PolicyId;
|
|
101
|
+
policyVersion: number;
|
|
102
|
+
kind: PolicyKind;
|
|
103
|
+
dataDefinition?: DataPolicyDefinition;
|
|
104
|
+
codeEvaluatorPolicyId?: PolicyId;
|
|
105
|
+
fallback?: PolicyFallbackDefinition;
|
|
106
|
+
}
|
|
107
|
+
interface PolicyDispatchEvidence {
|
|
108
|
+
policyKind: PolicyKind;
|
|
109
|
+
policyId?: PolicyId;
|
|
110
|
+
policyVersion?: number;
|
|
111
|
+
dispatchPath: Array<"data" | "code" | "fallback">;
|
|
112
|
+
data?: {
|
|
113
|
+
definitionVersion: number;
|
|
114
|
+
definitionStatus: "evaluated" | "missing" | "invalid";
|
|
115
|
+
conditionResults: DataPolicyConditionResult[];
|
|
116
|
+
validationErrors?: string[];
|
|
117
|
+
};
|
|
118
|
+
code?: {
|
|
119
|
+
requestedPolicyId?: PolicyId;
|
|
120
|
+
policyId: PolicyId;
|
|
121
|
+
version?: number;
|
|
122
|
+
registered: boolean;
|
|
123
|
+
};
|
|
124
|
+
fallback?: {
|
|
125
|
+
used: boolean;
|
|
126
|
+
trigger: PolicyFallbackTrigger | "not_triggered";
|
|
127
|
+
reason: string;
|
|
128
|
+
fromResult?: PolicyEvaluationResult;
|
|
129
|
+
definitionVersion: number;
|
|
130
|
+
codeEvaluatorPolicyId: PolicyId;
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
declare function registerPolicy(evaluator: PolicyEvaluator): void;
|
|
134
|
+
declare function registerPolicyIfMissing(evaluator: PolicyEvaluator): void;
|
|
135
|
+
declare function resolvePolicy(policyId: PolicyId): PolicyEvaluator | undefined;
|
|
136
|
+
declare function getRegisteredPolicyIds(): PolicyId[];
|
|
137
|
+
declare function evaluateRegisteredPolicies(policyIds: PolicyId[], ctx: PolicyContext): Promise<PolicyOutcome[]>;
|
|
138
|
+
declare function evaluatePolicyDefinitions(policies: RuntimePolicyDefinition[], ctx: PolicyContext): Promise<PolicyOutcome[]>;
|
|
139
|
+
declare function evaluatePolicyDefinition(policy: RuntimePolicyDefinition, ctx: PolicyContext): Promise<PolicyOutcome>;
|
|
140
|
+
declare function validateDataPolicyDefinition(definition: DataPolicyDefinition): DataPolicyValidationResult;
|
|
141
|
+
declare function aggregatePolicyOutcomes(outcomes: PolicyOutcome[]): PolicyOutcome | undefined;
|
|
142
|
+
|
|
143
|
+
export { type DataPolicyComparisonOperator, type DataPolicyCondition, type DataPolicyConditionResult, type DataPolicyDefinition, type DataPolicyLiteral, type DataPolicySource, type DataPolicySourceReference, type DataPolicyValidationResult, type PolicyContext, type PolicyDispatchEvidence, type PolicyEvaluationMode, type PolicyEvaluationResult, type PolicyEvaluator, type PolicyFallbackDefinition, type PolicyFallbackTrigger, type PolicyId, type PolicyKind, type PolicyOutcome, type RuntimePolicyDefinition, aggregatePolicyOutcomes, assertPolicyId, evaluatePolicyDefinition, evaluatePolicyDefinitions, evaluateRegisteredPolicies, getRegisteredPolicyIds, registerPolicy, registerPolicyIfMissing, resolvePolicy, validateDataPolicyDefinition };
|
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
// policies/index.ts
|
|
2
|
+
function assertPolicyId(policyId) {
|
|
3
|
+
if (!/^[a-z][a-z0-9_-]*(\.[a-z][a-z0-9_-]*)*\.v[0-9]+$/.test(policyId)) {
|
|
4
|
+
throw new Error(`Invalid policy ID: ${policyId}`);
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
var DATA_POLICY_MAX_DEPTH = 5;
|
|
8
|
+
var DATA_POLICY_MAX_CONDITIONS_PER_NODE = 20;
|
|
9
|
+
var DATA_POLICY_MAX_TOTAL_CONDITIONS = 100;
|
|
10
|
+
var DATA_POLICY_MAX_PATH_SEGMENTS = 12;
|
|
11
|
+
var DATA_POLICY_MAX_STRING_LENGTH = 500;
|
|
12
|
+
var DATA_POLICY_CONTEXT_PATHS = /* @__PURE__ */ new Set([
|
|
13
|
+
"tenantId",
|
|
14
|
+
"spaceId",
|
|
15
|
+
"actionInvocationId",
|
|
16
|
+
"actionId",
|
|
17
|
+
"mode"
|
|
18
|
+
]);
|
|
19
|
+
var DATA_POLICY_COMPARISON_OPERATORS = /* @__PURE__ */ new Set([
|
|
20
|
+
"exists",
|
|
21
|
+
"equals",
|
|
22
|
+
"notEquals",
|
|
23
|
+
"gt",
|
|
24
|
+
"gte",
|
|
25
|
+
"lt",
|
|
26
|
+
"lte"
|
|
27
|
+
]);
|
|
28
|
+
var POLICY_RESULTS = /* @__PURE__ */ new Set(["pass", "warn", "block"]);
|
|
29
|
+
var registry = /* @__PURE__ */ new Map();
|
|
30
|
+
function registerPolicy(evaluator) {
|
|
31
|
+
assertPolicyId(evaluator.policyId);
|
|
32
|
+
const existing = registry.get(evaluator.policyId);
|
|
33
|
+
if (existing && existing !== evaluator) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Conflicting policy registration for "${evaluator.policyId}". A different evaluator is already registered.`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
registry.set(evaluator.policyId, evaluator);
|
|
39
|
+
}
|
|
40
|
+
function registerPolicyIfMissing(evaluator) {
|
|
41
|
+
if (!registry.has(evaluator.policyId)) {
|
|
42
|
+
registerPolicy(evaluator);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const existing = registry.get(evaluator.policyId);
|
|
46
|
+
if (existing && existing !== evaluator) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Conflicting policy registration for "${evaluator.policyId}". A different evaluator is already registered.`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function resolvePolicy(policyId) {
|
|
53
|
+
return registry.get(policyId);
|
|
54
|
+
}
|
|
55
|
+
function getRegisteredPolicyIds() {
|
|
56
|
+
return Array.from(registry.keys());
|
|
57
|
+
}
|
|
58
|
+
async function evaluateRegisteredPolicies(policyIds, ctx) {
|
|
59
|
+
return evaluatePolicyDefinitions(
|
|
60
|
+
policyIds.map((policyId) => ({
|
|
61
|
+
policyId,
|
|
62
|
+
policyVersion: parsePolicyVersion(policyId),
|
|
63
|
+
kind: "code",
|
|
64
|
+
codeEvaluatorPolicyId: policyId
|
|
65
|
+
})),
|
|
66
|
+
ctx
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
async function evaluatePolicyDefinitions(policies, ctx) {
|
|
70
|
+
const outcomes = [];
|
|
71
|
+
for (const policy of policies) {
|
|
72
|
+
assertPolicyId(policy.policyId);
|
|
73
|
+
outcomes.push(await evaluatePolicyDefinition(policy, ctx));
|
|
74
|
+
}
|
|
75
|
+
return outcomes;
|
|
76
|
+
}
|
|
77
|
+
async function evaluatePolicyDefinition(policy, ctx) {
|
|
78
|
+
if (policy.kind === "data") {
|
|
79
|
+
return evaluateDataPolicy(policy, ctx);
|
|
80
|
+
}
|
|
81
|
+
if (policy.kind === "code") {
|
|
82
|
+
return evaluateCodePolicy(policy, ctx);
|
|
83
|
+
}
|
|
84
|
+
return evaluateHybridPolicy(policy, ctx);
|
|
85
|
+
}
|
|
86
|
+
async function evaluateCodePolicy(policy, ctx) {
|
|
87
|
+
const codeEvaluatorPolicyId = policy.codeEvaluatorPolicyId ?? policy.policyId;
|
|
88
|
+
const evaluator = registry.get(codeEvaluatorPolicyId);
|
|
89
|
+
if (!evaluator) {
|
|
90
|
+
return {
|
|
91
|
+
policyId: policy.policyId,
|
|
92
|
+
policyVersion: policy.policyVersion,
|
|
93
|
+
policyKind: policy.kind,
|
|
94
|
+
result: "block",
|
|
95
|
+
reason: `No evaluator registered for policy ${codeEvaluatorPolicyId}`,
|
|
96
|
+
metadata: {
|
|
97
|
+
missingCodeEvaluatorPolicyId: codeEvaluatorPolicyId
|
|
98
|
+
},
|
|
99
|
+
dispatchEvidence: {
|
|
100
|
+
policyKind: policy.kind,
|
|
101
|
+
policyId: policy.policyId,
|
|
102
|
+
policyVersion: policy.policyVersion,
|
|
103
|
+
dispatchPath: ["code"],
|
|
104
|
+
code: {
|
|
105
|
+
requestedPolicyId: codeEvaluatorPolicyId,
|
|
106
|
+
policyId: codeEvaluatorPolicyId,
|
|
107
|
+
registered: false
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const outcome = await evaluator.evaluate(ctx);
|
|
113
|
+
return normalizeOutcome(policy, outcome, {
|
|
114
|
+
policyKind: policy.kind,
|
|
115
|
+
policyId: policy.policyId,
|
|
116
|
+
policyVersion: policy.policyVersion,
|
|
117
|
+
dispatchPath: ["code"],
|
|
118
|
+
code: {
|
|
119
|
+
requestedPolicyId: codeEvaluatorPolicyId,
|
|
120
|
+
policyId: evaluator.policyId,
|
|
121
|
+
version: evaluator.version,
|
|
122
|
+
registered: true
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
async function evaluateHybridPolicy(policy, ctx) {
|
|
127
|
+
const dataOutcome = evaluateDataPolicy(policy, ctx);
|
|
128
|
+
const fallback = policy.fallback;
|
|
129
|
+
const fallbackDecision = fallback ? resolveFallbackDecision(policy, dataOutcome, fallback) : { shouldFallback: false, trigger: "not_triggered" };
|
|
130
|
+
if (!fallback || !fallbackDecision.shouldFallback) {
|
|
131
|
+
if (fallback && dataOutcome.dispatchEvidence) {
|
|
132
|
+
return {
|
|
133
|
+
...dataOutcome,
|
|
134
|
+
dispatchEvidence: {
|
|
135
|
+
...dataOutcome.dispatchEvidence,
|
|
136
|
+
fallback: {
|
|
137
|
+
used: false,
|
|
138
|
+
trigger: "not_triggered",
|
|
139
|
+
reason: "Data policy result did not trigger fallback",
|
|
140
|
+
fromResult: dataOutcome.result,
|
|
141
|
+
definitionVersion: policy.policyVersion,
|
|
142
|
+
codeEvaluatorPolicyId: fallback.codeEvaluatorPolicyId
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return dataOutcome;
|
|
148
|
+
}
|
|
149
|
+
const codePolicy = {
|
|
150
|
+
policyId: policy.policyId,
|
|
151
|
+
policyVersion: policy.policyVersion,
|
|
152
|
+
kind: "hybrid",
|
|
153
|
+
codeEvaluatorPolicyId: fallback.codeEvaluatorPolicyId
|
|
154
|
+
};
|
|
155
|
+
const codeOutcome = await evaluateCodePolicy(codePolicy, ctx);
|
|
156
|
+
return normalizeOutcome(policy, codeOutcome, {
|
|
157
|
+
...codeOutcome.dispatchEvidence ?? {
|
|
158
|
+
policyKind: "hybrid",
|
|
159
|
+
policyId: policy.policyId,
|
|
160
|
+
policyVersion: policy.policyVersion,
|
|
161
|
+
dispatchPath: ["code"]
|
|
162
|
+
},
|
|
163
|
+
policyKind: "hybrid",
|
|
164
|
+
policyId: policy.policyId,
|
|
165
|
+
policyVersion: policy.policyVersion,
|
|
166
|
+
dispatchPath: ["data", "fallback", "code"],
|
|
167
|
+
data: dataOutcome.dispatchEvidence?.data,
|
|
168
|
+
fallback: {
|
|
169
|
+
used: true,
|
|
170
|
+
trigger: fallbackDecision.trigger,
|
|
171
|
+
reason: fallback.reason ?? "Data policy result triggered code fallback",
|
|
172
|
+
fromResult: dataOutcome.result,
|
|
173
|
+
definitionVersion: policy.policyVersion,
|
|
174
|
+
codeEvaluatorPolicyId: fallback.codeEvaluatorPolicyId
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
function resolveFallbackDecision(policy, dataOutcome, fallback) {
|
|
179
|
+
const allowedTriggers = fallback.triggers ?? [
|
|
180
|
+
"data_result",
|
|
181
|
+
"missing_data_definition",
|
|
182
|
+
"invalid_data_definition"
|
|
183
|
+
];
|
|
184
|
+
const definitionStatus = dataOutcome.dispatchEvidence?.data?.definitionStatus;
|
|
185
|
+
if (definitionStatus === "missing") {
|
|
186
|
+
return {
|
|
187
|
+
shouldFallback: allowedTriggers.includes("missing_data_definition"),
|
|
188
|
+
trigger: "missing_data_definition"
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
if (definitionStatus === "invalid") {
|
|
192
|
+
return {
|
|
193
|
+
shouldFallback: allowedTriggers.includes("invalid_data_definition"),
|
|
194
|
+
trigger: "invalid_data_definition"
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
const fallbackResults = fallback.onResults ?? ["warn", "block"];
|
|
198
|
+
return {
|
|
199
|
+
shouldFallback: policy.kind === "hybrid" && allowedTriggers.includes("data_result") && fallbackResults.includes(dataOutcome.result),
|
|
200
|
+
trigger: "data_result"
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function evaluateDataPolicy(policy, ctx) {
|
|
204
|
+
const definition = policy.dataDefinition;
|
|
205
|
+
if (!definition) {
|
|
206
|
+
return {
|
|
207
|
+
policyId: policy.policyId,
|
|
208
|
+
policyVersion: policy.policyVersion,
|
|
209
|
+
policyKind: policy.kind,
|
|
210
|
+
result: "block",
|
|
211
|
+
reason: "Data policy definition is missing",
|
|
212
|
+
dispatchEvidence: {
|
|
213
|
+
policyKind: policy.kind,
|
|
214
|
+
policyId: policy.policyId,
|
|
215
|
+
policyVersion: policy.policyVersion,
|
|
216
|
+
dispatchPath: ["data"],
|
|
217
|
+
data: {
|
|
218
|
+
definitionVersion: policy.policyVersion,
|
|
219
|
+
definitionStatus: "missing",
|
|
220
|
+
conditionResults: []
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
const validation = validateDataPolicyDefinition(definition);
|
|
226
|
+
if (!validation.valid) {
|
|
227
|
+
return {
|
|
228
|
+
policyId: policy.policyId,
|
|
229
|
+
policyVersion: policy.policyVersion,
|
|
230
|
+
policyKind: policy.kind,
|
|
231
|
+
result: "block",
|
|
232
|
+
reason: `Invalid data policy definition: ${validation.errors.join("; ")}`,
|
|
233
|
+
metadata: {
|
|
234
|
+
validationErrors: validation.errors
|
|
235
|
+
},
|
|
236
|
+
dispatchEvidence: {
|
|
237
|
+
policyKind: policy.kind,
|
|
238
|
+
policyId: policy.policyId,
|
|
239
|
+
policyVersion: policy.policyVersion,
|
|
240
|
+
dispatchPath: ["data"],
|
|
241
|
+
data: {
|
|
242
|
+
definitionVersion: policy.policyVersion,
|
|
243
|
+
definitionStatus: "invalid",
|
|
244
|
+
conditionResults: [],
|
|
245
|
+
validationErrors: validation.errors
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const evaluations = definition.conditions.map((condition) => evaluateDataPolicyCondition(condition, ctx));
|
|
251
|
+
const conditionResults = evaluations.flatMap((evaluation) => evaluation.evidence);
|
|
252
|
+
const topLevelResults = evaluations.map((evaluation) => evaluation.result);
|
|
253
|
+
const aggregate = topLevelResults.find((result2) => result2.result === "block") ?? topLevelResults.find((result2) => result2.result === "warn");
|
|
254
|
+
const result = aggregate?.result ?? definition.defaultResult ?? "pass";
|
|
255
|
+
return {
|
|
256
|
+
policyId: policy.policyId,
|
|
257
|
+
policyVersion: policy.policyVersion,
|
|
258
|
+
policyKind: policy.kind,
|
|
259
|
+
result,
|
|
260
|
+
reason: aggregate?.reason ?? definition.reason,
|
|
261
|
+
metadata: {
|
|
262
|
+
conditionCount: definition.conditions.length,
|
|
263
|
+
failedConditionId: aggregate?.conditionId
|
|
264
|
+
},
|
|
265
|
+
dispatchEvidence: {
|
|
266
|
+
policyKind: policy.kind,
|
|
267
|
+
policyId: policy.policyId,
|
|
268
|
+
policyVersion: policy.policyVersion,
|
|
269
|
+
dispatchPath: ["data"],
|
|
270
|
+
data: {
|
|
271
|
+
definitionVersion: policy.policyVersion,
|
|
272
|
+
definitionStatus: "evaluated",
|
|
273
|
+
conditionResults
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function validateDataPolicyDefinition(definition) {
|
|
279
|
+
const errors = [];
|
|
280
|
+
if (!Array.isArray(definition.conditions) || definition.conditions.length === 0) {
|
|
281
|
+
errors.push("conditions must contain at least one condition");
|
|
282
|
+
} else if (definition.conditions.length > DATA_POLICY_MAX_CONDITIONS_PER_NODE) {
|
|
283
|
+
errors.push(
|
|
284
|
+
`conditions must contain at most ${DATA_POLICY_MAX_CONDITIONS_PER_NODE} top-level conditions`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
if (definition.defaultResult && !POLICY_RESULTS.has(definition.defaultResult)) {
|
|
288
|
+
errors.push("defaultResult must be pass, warn, or block");
|
|
289
|
+
}
|
|
290
|
+
let totalConditions = 0;
|
|
291
|
+
for (const condition of definition.conditions ?? []) {
|
|
292
|
+
validateDataPolicyCondition(condition, errors, 1, () => {
|
|
293
|
+
totalConditions += 1;
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
if (totalConditions > DATA_POLICY_MAX_TOTAL_CONDITIONS) {
|
|
297
|
+
errors.push(`policy must contain at most ${DATA_POLICY_MAX_TOTAL_CONDITIONS} total conditions`);
|
|
298
|
+
}
|
|
299
|
+
return { valid: errors.length === 0, errors };
|
|
300
|
+
}
|
|
301
|
+
function validateDataPolicyCondition(condition, errors, depth, count) {
|
|
302
|
+
count();
|
|
303
|
+
if (!condition || typeof condition !== "object") {
|
|
304
|
+
errors.push("condition must be an object");
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (depth > DATA_POLICY_MAX_DEPTH) {
|
|
308
|
+
errors.push(`condition depth must not exceed ${DATA_POLICY_MAX_DEPTH}`);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (!isNonEmptyBoundedString(condition.conditionId)) {
|
|
312
|
+
errors.push("conditionId must be a non-empty bounded string");
|
|
313
|
+
}
|
|
314
|
+
validateOptionalPolicyResult("onPass" in condition ? condition.onPass : void 0, "onPass", errors);
|
|
315
|
+
validateOptionalPolicyResult("onFail" in condition ? condition.onFail : void 0, "onFail", errors);
|
|
316
|
+
if (condition.reason !== void 0 && !isNonEmptyBoundedString(condition.reason)) {
|
|
317
|
+
errors.push(`condition ${condition.conditionId} reason must be a bounded string`);
|
|
318
|
+
}
|
|
319
|
+
if (condition.type === "always") {
|
|
320
|
+
if (!POLICY_RESULTS.has(condition.result)) {
|
|
321
|
+
errors.push(`condition ${condition.conditionId} result must be pass, warn, or block`);
|
|
322
|
+
}
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (condition.type === "parameter") {
|
|
326
|
+
validatePath(condition.path, `condition ${condition.conditionId} path`, errors);
|
|
327
|
+
validateComparison(condition.operator, condition.value, condition.conditionId, errors);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (condition.type === "comparison") {
|
|
331
|
+
validateSourceReference(condition.left, condition.conditionId, errors);
|
|
332
|
+
validateComparison(condition.operator, condition.value, condition.conditionId, errors);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (condition.type === "all" || condition.type === "any") {
|
|
336
|
+
if (!Array.isArray(condition.conditions) || condition.conditions.length === 0) {
|
|
337
|
+
errors.push(`condition ${condition.conditionId} must contain child conditions`);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (condition.conditions.length > DATA_POLICY_MAX_CONDITIONS_PER_NODE) {
|
|
341
|
+
errors.push(
|
|
342
|
+
`condition ${condition.conditionId} must contain at most ${DATA_POLICY_MAX_CONDITIONS_PER_NODE} child conditions`
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
for (const child of condition.conditions) {
|
|
346
|
+
validateDataPolicyCondition(child, errors, depth + 1, count);
|
|
347
|
+
}
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (condition.type === "not") {
|
|
351
|
+
validateDataPolicyCondition(condition.condition, errors, depth + 1, count);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
errors.push(`condition ${condition.conditionId} has unsupported type`);
|
|
355
|
+
}
|
|
356
|
+
function validateOptionalPolicyResult(result, fieldName, errors) {
|
|
357
|
+
if (result !== void 0 && !POLICY_RESULTS.has(result)) {
|
|
358
|
+
errors.push(`${fieldName} must be pass, warn, or block`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function validateSourceReference(reference, conditionId, errors) {
|
|
362
|
+
if (!reference || typeof reference !== "object") {
|
|
363
|
+
errors.push(`condition ${conditionId} left must be a source reference`);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (reference.source !== "parameters" && reference.source !== "context") {
|
|
367
|
+
errors.push(`condition ${conditionId} source must be parameters or context`);
|
|
368
|
+
}
|
|
369
|
+
validatePath(reference.path, `condition ${conditionId} left.path`, errors);
|
|
370
|
+
if (reference.source === "context" && !DATA_POLICY_CONTEXT_PATHS.has(reference.path)) {
|
|
371
|
+
errors.push(`condition ${conditionId} uses an unsupported context path`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function validateComparison(operator, value, conditionId, errors) {
|
|
375
|
+
if (!DATA_POLICY_COMPARISON_OPERATORS.has(operator)) {
|
|
376
|
+
errors.push(`condition ${conditionId} has unsupported operator`);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (operator === "exists") {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (!isDataPolicyLiteral(value)) {
|
|
383
|
+
errors.push(`condition ${conditionId} comparison value must be a bounded literal`);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (["gt", "gte", "lt", "lte"].includes(operator) && typeof value !== "number") {
|
|
387
|
+
errors.push(`condition ${conditionId} numeric comparison requires a number value`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function validatePath(path, fieldName, errors) {
|
|
391
|
+
if (!isNonEmptyBoundedString(path)) {
|
|
392
|
+
errors.push(`${fieldName} must be a non-empty bounded string`);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const segments = path.split(".");
|
|
396
|
+
if (segments.length > DATA_POLICY_MAX_PATH_SEGMENTS) {
|
|
397
|
+
errors.push(`${fieldName} must contain at most ${DATA_POLICY_MAX_PATH_SEGMENTS} segments`);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
for (const segment of segments) {
|
|
401
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(segment)) {
|
|
402
|
+
errors.push(`${fieldName} contains an invalid path segment`);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (["__proto__", "constructor", "prototype"].includes(segment)) {
|
|
406
|
+
errors.push(`${fieldName} contains a forbidden path segment`);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function isDataPolicyLiteral(value) {
|
|
412
|
+
if (value === null || typeof value === "number" || typeof value === "boolean") {
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
return typeof value === "string" && value.length <= DATA_POLICY_MAX_STRING_LENGTH;
|
|
416
|
+
}
|
|
417
|
+
function isNonEmptyBoundedString(value) {
|
|
418
|
+
return typeof value === "string" && value.length > 0 && value.length <= DATA_POLICY_MAX_STRING_LENGTH;
|
|
419
|
+
}
|
|
420
|
+
function evaluateDataPolicyCondition(condition, ctx) {
|
|
421
|
+
if (condition.type === "always") {
|
|
422
|
+
const result2 = {
|
|
423
|
+
conditionId: condition.conditionId,
|
|
424
|
+
passed: condition.result === "pass",
|
|
425
|
+
result: condition.result,
|
|
426
|
+
reason: condition.reason
|
|
427
|
+
};
|
|
428
|
+
return { result: result2, evidence: [result2] };
|
|
429
|
+
}
|
|
430
|
+
if (condition.type === "all" || condition.type === "any") {
|
|
431
|
+
const childEvaluations = condition.conditions.map((child) => evaluateDataPolicyCondition(child, ctx));
|
|
432
|
+
const passed2 = condition.type === "all" ? childEvaluations.every((evaluation) => evaluation.result.passed) : childEvaluations.some((evaluation) => evaluation.result.passed);
|
|
433
|
+
const result2 = {
|
|
434
|
+
conditionId: condition.conditionId,
|
|
435
|
+
passed: passed2,
|
|
436
|
+
result: passed2 ? condition.onPass ?? "pass" : condition.onFail ?? "block",
|
|
437
|
+
reason: passed2 ? void 0 : condition.reason
|
|
438
|
+
};
|
|
439
|
+
return {
|
|
440
|
+
result: result2,
|
|
441
|
+
evidence: [result2, ...childEvaluations.flatMap((evaluation) => evaluation.evidence)]
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
if (condition.type === "not") {
|
|
445
|
+
const childEvaluation = evaluateDataPolicyCondition(condition.condition, ctx);
|
|
446
|
+
const passed2 = !childEvaluation.result.passed;
|
|
447
|
+
const result2 = {
|
|
448
|
+
conditionId: condition.conditionId,
|
|
449
|
+
passed: passed2,
|
|
450
|
+
result: passed2 ? condition.onPass ?? "pass" : condition.onFail ?? "block",
|
|
451
|
+
reason: passed2 ? void 0 : condition.reason
|
|
452
|
+
};
|
|
453
|
+
return { result: result2, evidence: [result2, ...childEvaluation.evidence] };
|
|
454
|
+
}
|
|
455
|
+
if (condition.type !== "parameter" && condition.type !== "comparison") {
|
|
456
|
+
const result2 = {
|
|
457
|
+
conditionId: condition.conditionId,
|
|
458
|
+
passed: false,
|
|
459
|
+
result: "block",
|
|
460
|
+
reason: "Unsupported data policy condition type"
|
|
461
|
+
};
|
|
462
|
+
return { result: result2, evidence: [result2] };
|
|
463
|
+
}
|
|
464
|
+
const value = condition.type === "parameter" ? readPath(ctx.parameters, condition.path) : readDataPolicySource(ctx, condition.left);
|
|
465
|
+
const operator = condition.operator;
|
|
466
|
+
const expected = condition.value;
|
|
467
|
+
const passed = evaluateOperator(value, operator, expected);
|
|
468
|
+
const result = {
|
|
469
|
+
conditionId: condition.conditionId,
|
|
470
|
+
passed,
|
|
471
|
+
result: passed ? condition.onPass ?? "pass" : condition.onFail ?? "block",
|
|
472
|
+
reason: passed ? void 0 : condition.reason
|
|
473
|
+
};
|
|
474
|
+
return { result, evidence: [result] };
|
|
475
|
+
}
|
|
476
|
+
function evaluateOperator(actual, operator, expected) {
|
|
477
|
+
switch (operator) {
|
|
478
|
+
case "exists":
|
|
479
|
+
return actual !== void 0 && actual !== null && actual !== "";
|
|
480
|
+
case "equals":
|
|
481
|
+
return actual === expected;
|
|
482
|
+
case "notEquals":
|
|
483
|
+
return actual !== expected;
|
|
484
|
+
case "gt":
|
|
485
|
+
return typeof actual === "number" && typeof expected === "number" && actual > expected;
|
|
486
|
+
case "gte":
|
|
487
|
+
return typeof actual === "number" && typeof expected === "number" && actual >= expected;
|
|
488
|
+
case "lt":
|
|
489
|
+
return typeof actual === "number" && typeof expected === "number" && actual < expected;
|
|
490
|
+
case "lte":
|
|
491
|
+
return typeof actual === "number" && typeof expected === "number" && actual <= expected;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
function readDataPolicySource(ctx, reference) {
|
|
495
|
+
if (reference.source === "parameters") {
|
|
496
|
+
return readPath(ctx.parameters, reference.path);
|
|
497
|
+
}
|
|
498
|
+
return readPath(
|
|
499
|
+
{
|
|
500
|
+
tenantId: ctx.tenantId,
|
|
501
|
+
spaceId: ctx.spaceId,
|
|
502
|
+
actionInvocationId: ctx.actionInvocationId,
|
|
503
|
+
actionId: ctx.actionId,
|
|
504
|
+
mode: ctx.mode
|
|
505
|
+
},
|
|
506
|
+
reference.path
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
function readPath(source, path) {
|
|
510
|
+
if (!source || typeof source !== "object") {
|
|
511
|
+
return void 0;
|
|
512
|
+
}
|
|
513
|
+
return path.split(".").reduce((current, segment) => {
|
|
514
|
+
if (!current || typeof current !== "object") {
|
|
515
|
+
return void 0;
|
|
516
|
+
}
|
|
517
|
+
return current[segment];
|
|
518
|
+
}, source);
|
|
519
|
+
}
|
|
520
|
+
function normalizeOutcome(policy, outcome, dispatchEvidence) {
|
|
521
|
+
return {
|
|
522
|
+
...outcome,
|
|
523
|
+
policyId: policy.policyId,
|
|
524
|
+
policyVersion: policy.policyVersion,
|
|
525
|
+
policyKind: policy.kind,
|
|
526
|
+
metadata: {
|
|
527
|
+
...outcome.metadata ?? {},
|
|
528
|
+
codePolicyId: dispatchEvidence.code?.policyId ?? outcome.policyId,
|
|
529
|
+
codePolicyVersion: dispatchEvidence.code?.version ?? outcome.policyVersion
|
|
530
|
+
},
|
|
531
|
+
dispatchEvidence
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
function parsePolicyVersion(policyId) {
|
|
535
|
+
const segments = policyId.split(".v");
|
|
536
|
+
return Number(segments[segments.length - 1] ?? 1);
|
|
537
|
+
}
|
|
538
|
+
function aggregatePolicyOutcomes(outcomes) {
|
|
539
|
+
return outcomes.find((outcome) => outcome.result === "block") ?? outcomes.find((outcome) => outcome.result === "warn");
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export { aggregatePolicyOutcomes, assertPolicyId, evaluatePolicyDefinition, evaluatePolicyDefinitions, evaluateRegisteredPolicies, getRegisteredPolicyIds, registerPolicy, registerPolicyIfMissing, resolvePolicy, validateDataPolicyDefinition };
|
|
543
|
+
//# sourceMappingURL=index.js.map
|
|
544
|
+
//# sourceMappingURL=index.js.map
|