@highflame/policy 2.1.0 → 2.1.1

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/dist/explain.d.ts CHANGED
@@ -46,6 +46,18 @@ export interface PolicyExplanation {
46
46
  /** Raw Cedar condition text if the policy uses rawCondition instead of structured conditions */
47
47
  raw_condition?: string;
48
48
  }
49
+ /**
50
+ * Identifies the detector that produced a context value.
51
+ * Used for provenance tracking — linking policy conditions back to their source detector.
52
+ */
53
+ export interface EvidenceSource {
54
+ /** Detector name (e.g., "injection", "secrets", "tool_validator") */
55
+ detector: string;
56
+ /** Detector execution latency in milliseconds */
57
+ latency_ms?: number;
58
+ /** Producer-specific metadata (e.g., "tier", "phase", "priority"). Treated as opaque key-value pairs. */
59
+ labels?: Record<string, string>;
60
+ }
49
61
  /**
50
62
  * Result of evaluating a single condition against the request context.
51
63
  */
@@ -60,6 +72,15 @@ export interface ConditionResult {
60
72
  actual?: unknown;
61
73
  /** Whether this condition matched */
62
74
  matched: boolean;
75
+ /** Source detector that produced this context value (when provenance is available) */
76
+ source?: EvidenceSource;
77
+ }
78
+ /**
79
+ * Options for explainDecision.
80
+ */
81
+ export interface ExplainOptions {
82
+ /** Maps context field names to the detector that produced them */
83
+ provenance?: Record<string, EvidenceSource>;
63
84
  }
64
85
  /** Evaluated comparison: context.field <op> value */
65
86
  export interface EvaluatedComparison {
@@ -69,6 +90,8 @@ export interface EvaluatedComparison {
69
90
  expected: string | number | boolean | string[];
70
91
  actual: unknown;
71
92
  matched: boolean;
93
+ /** Source detector that produced this context value (when provenance is available) */
94
+ source?: EvidenceSource;
72
95
  }
73
96
  /** Evaluated contains: context.field.contains(value) */
74
97
  export interface EvaluatedContains {
@@ -77,6 +100,8 @@ export interface EvaluatedContains {
77
100
  expected: string | number | boolean;
78
101
  actual: unknown;
79
102
  matched: boolean;
103
+ /** Source detector that produced this context value (when provenance is available) */
104
+ source?: EvidenceSource;
80
105
  }
81
106
  /** Evaluated like: context.field like "pattern" */
82
107
  export interface EvaluatedLike {
@@ -85,6 +110,8 @@ export interface EvaluatedLike {
85
110
  pattern: string;
86
111
  actual: unknown;
87
112
  matched: boolean;
113
+ /** Source detector that produced this context value (when provenance is available) */
114
+ source?: EvidenceSource;
88
115
  }
89
116
  /** Evaluated has: context has field */
90
117
  export interface EvaluatedHas {
@@ -142,9 +169,10 @@ export type EvaluatedExpression = EvaluatedComparison | EvaluatedContains | Eval
142
169
  * }
143
170
  * ```
144
171
  */
145
- export declare function explainDecision(decision: DecisionInput, rules: PolicyRule[], context: Record<string, unknown>): ExplainedDecision;
172
+ export declare function explainDecision(decision: DecisionInput, rules: PolicyRule[], context: Record<string, unknown>, options?: ExplainOptions): ExplainedDecision;
146
173
  /**
147
174
  * Recursively evaluate a ConditionExpression tree against a context map.
148
175
  * Returns an EvaluatedExpression tree with `matched` booleans and `actual` values.
176
+ * The optional provenance map links context field names to their source detectors.
149
177
  */
150
- export declare function evaluateExpression(expr: ConditionExpression, context: Record<string, unknown>): EvaluatedExpression;
178
+ export declare function evaluateExpression(expr: ConditionExpression, context: Record<string, unknown>, provenance?: Record<string, EvidenceSource>): EvaluatedExpression;
package/dist/explain.js CHANGED
@@ -29,7 +29,8 @@
29
29
  * }
30
30
  * ```
31
31
  */
32
- export function explainDecision(decision, rules, context) {
32
+ export function explainDecision(decision, rules, context, options) {
33
+ const provenance = options?.provenance;
33
34
  // Build lookup map: annotations.id → PolicyRule
34
35
  const ruleMap = new Map();
35
36
  for (const rule of rules) {
@@ -49,7 +50,7 @@ export function explainDecision(decision, rules, context) {
49
50
  // Recursive condition evaluation
50
51
  let evaluatedExpression;
51
52
  if (rule.conditionExpression) {
52
- evaluatedExpression = evaluateExpression(rule.conditionExpression, context);
53
+ evaluatedExpression = evaluateExpression(rule.conditionExpression, context, provenance);
53
54
  }
54
55
  // Populate condition_results: prefer leaf extraction from expression tree,
55
56
  // fall back to flat conditions[] for backward compat with simple policies
@@ -62,7 +63,11 @@ export function explainDecision(decision, rules, context) {
62
63
  conditionResults = conditions.map(cond => {
63
64
  const actual = context[cond.field];
64
65
  const matched = evaluateCondition(cond.operator, actual, cond.value);
65
- return { field: cond.field, operator: cond.operator, expected: cond.value, actual, matched };
66
+ const result = { field: cond.field, operator: cond.operator, expected: cond.value, actual, matched };
67
+ if (provenance?.[cond.field]) {
68
+ result.source = provenance[cond.field];
69
+ }
70
+ return result;
66
71
  });
67
72
  }
68
73
  // Surface rawCondition when there are no structured conditions and no expression tree
@@ -93,19 +98,20 @@ export function explainDecision(decision, rules, context) {
93
98
  /**
94
99
  * Recursively evaluate a ConditionExpression tree against a context map.
95
100
  * Returns an EvaluatedExpression tree with `matched` booleans and `actual` values.
101
+ * The optional provenance map links context field names to their source detectors.
96
102
  */
97
- export function evaluateExpression(expr, context) {
103
+ export function evaluateExpression(expr, context, provenance) {
98
104
  switch (expr.kind) {
99
105
  case 'and': {
100
- const children = expr.children.map(c => evaluateExpression(c, context));
106
+ const children = expr.children.map(c => evaluateExpression(c, context, provenance));
101
107
  return { kind: 'and', children, matched: children.every(c => c.matched) };
102
108
  }
103
109
  case 'or': {
104
- const children = expr.children.map(c => evaluateExpression(c, context));
110
+ const children = expr.children.map(c => evaluateExpression(c, context, provenance));
105
111
  return { kind: 'or', children, matched: children.some(c => c.matched) };
106
112
  }
107
113
  case 'not': {
108
- const child = evaluateExpression(expr.child, context);
114
+ const child = evaluateExpression(expr.child, context, provenance);
109
115
  return { kind: 'not', child, matched: !child.matched };
110
116
  }
111
117
  case 'has': {
@@ -115,17 +121,26 @@ export function evaluateExpression(expr, context) {
115
121
  case 'comparison': {
116
122
  const actual = context[expr.field];
117
123
  const matched = evaluateCondition(expr.operator, actual, expr.value);
118
- return { kind: 'comparison', field: expr.field, operator: expr.operator, expected: expr.value, actual, matched };
124
+ const result = { kind: 'comparison', field: expr.field, operator: expr.operator, expected: expr.value, actual, matched };
125
+ if (provenance?.[expr.field])
126
+ result.source = provenance[expr.field];
127
+ return result;
119
128
  }
120
129
  case 'contains': {
121
130
  const actual = context[expr.field];
122
131
  const matched = evaluateContains(actual, expr.value);
123
- return { kind: 'contains', field: expr.field, expected: expr.value, actual, matched };
132
+ const result = { kind: 'contains', field: expr.field, expected: expr.value, actual, matched };
133
+ if (provenance?.[expr.field])
134
+ result.source = provenance[expr.field];
135
+ return result;
124
136
  }
125
137
  case 'like': {
126
138
  const actual = context[expr.field];
127
139
  const matched = evaluateLike(actual, expr.pattern);
128
- return { kind: 'like', field: expr.field, pattern: expr.pattern, actual, matched };
140
+ const result = { kind: 'like', field: expr.field, pattern: expr.pattern, actual, matched };
141
+ if (provenance?.[expr.field])
142
+ result.source = provenance[expr.field];
143
+ return result;
129
144
  }
130
145
  case 'raw':
131
146
  return { kind: 'raw', text: expr.text, matched: false };
@@ -296,12 +311,24 @@ function collectLeafResults(expr) {
296
311
  case 'has':
297
312
  case 'raw':
298
313
  return [];
299
- case 'comparison':
300
- return [{ field: expr.field, operator: expr.operator, expected: expr.expected, actual: expr.actual, matched: expr.matched }];
301
- case 'contains':
302
- return [{ field: expr.field, operator: 'contains', expected: expr.expected, actual: expr.actual, matched: expr.matched }];
303
- case 'like':
304
- return [{ field: expr.field, operator: 'like', expected: expr.pattern, actual: expr.actual, matched: expr.matched }];
314
+ case 'comparison': {
315
+ const result = { field: expr.field, operator: expr.operator, expected: expr.expected, actual: expr.actual, matched: expr.matched };
316
+ if (expr.source)
317
+ result.source = expr.source;
318
+ return [result];
319
+ }
320
+ case 'contains': {
321
+ const result = { field: expr.field, operator: 'contains', expected: expr.expected, actual: expr.actual, matched: expr.matched };
322
+ if (expr.source)
323
+ result.source = expr.source;
324
+ return [result];
325
+ }
326
+ case 'like': {
327
+ const result = { field: expr.field, operator: 'like', expected: expr.pattern, actual: expr.actual, matched: expr.matched };
328
+ if (expr.source)
329
+ result.source = expr.source;
330
+ return [result];
331
+ }
305
332
  }
306
333
  }
307
334
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@highflame/policy",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "Highflame Cedar policy types and engine wrapper",
5
5
  "readme": "README.md",
6
6
  "main": "dist/index.js",