@fabricorg/policy-opa 0.1.0 → 0.2.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/CHANGELOG.md +17 -0
- package/README.md +46 -10
- package/dist/index.cjs +138 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +174 -11
- package/dist/index.d.ts +174 -11
- package/dist/index.js +136 -15
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -25,6 +25,58 @@ interface OpaPolicyBinding {
|
|
|
25
25
|
* keep the package name aligned with the decision path.
|
|
26
26
|
*/
|
|
27
27
|
regoPackage?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Optional selector for `bundleRevision` extraction when OPA's
|
|
30
|
+
* `provenance.bundles` carries multiple bundles. If absent and OPA reports
|
|
31
|
+
* a single bundle, the adapter uses that revision unambiguously. If absent
|
|
32
|
+
* and multiple bundles are present, the adapter marks the evidence with
|
|
33
|
+
* `bundleRevisionStatus: "ambiguous"` so audit dashboards can flag policies
|
|
34
|
+
* that need explicit selectors.
|
|
35
|
+
*
|
|
36
|
+
* Example: `"lending"` for a bundle named `lending` served by OPA.
|
|
37
|
+
*/
|
|
38
|
+
bundleName?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Result of one OPA evaluation, normalized for the adapter. Returned by
|
|
42
|
+
* implementations of {@link OpaPolicyExecutor}. The adapter accepts either a
|
|
43
|
+
* client (with `evaluate`) or an executor (with `execute`); the executor path
|
|
44
|
+
* preserves provenance OPA exposes via `?provenance=true` — primarily
|
|
45
|
+
* `decision_id` and `provenance.bundles[*].revision`.
|
|
46
|
+
*/
|
|
47
|
+
interface OpaPolicyExecution {
|
|
48
|
+
/** The raw decision body — the value of `result` in OPA's response. */
|
|
49
|
+
decision: unknown;
|
|
50
|
+
/** OPA's per-request decision ID (response field `decision_id`). */
|
|
51
|
+
decisionId?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Pre-selected bundle revision, when the executor can determine it
|
|
54
|
+
* unambiguously without binding context (e.g. a transport that only ever
|
|
55
|
+
* sees a single bundle). The factory may override this from
|
|
56
|
+
* `provenance.bundles` + `binding.bundleName`.
|
|
57
|
+
*/
|
|
58
|
+
bundleRevision?: string;
|
|
59
|
+
/** OPA server version (response field `provenance.version`). */
|
|
60
|
+
opaVersion?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Raw `provenance` block from the OPA response. Carries
|
|
63
|
+
* `bundles: { [name]: { revision } }`, build info, etc. The factory uses
|
|
64
|
+
* this together with `binding.bundleName` to select `bundleRevision`.
|
|
65
|
+
*/
|
|
66
|
+
provenance?: Record<string, unknown>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Rich execution interface. Implementations are responsible for talking to
|
|
70
|
+
* OPA and surfacing provenance. Two official implementations ship in this
|
|
71
|
+
* package: {@link executorFromOpaClient} (wraps the high-level SDK; limited
|
|
72
|
+
* provenance) and {@link executorFromOpaHttp} (calls OPA REST directly with
|
|
73
|
+
* `?provenance=true`; full provenance).
|
|
74
|
+
*
|
|
75
|
+
* Hosts wire one of these into `ctx.services[serviceKey]` as an alternative
|
|
76
|
+
* to {@link OpaPolicyClient}.
|
|
77
|
+
*/
|
|
78
|
+
interface OpaPolicyExecutor {
|
|
79
|
+
execute(path: string, input: unknown): Promise<OpaPolicyExecution>;
|
|
28
80
|
}
|
|
29
81
|
/**
|
|
30
82
|
* Structured Rego decision shape — the "house style" the adapter expects from
|
|
@@ -60,7 +112,18 @@ type OpaPolicyErrorMode = "block" | "throw";
|
|
|
60
112
|
* something we couldn't parse" from "no client was injected at runtime".
|
|
61
113
|
*/
|
|
62
114
|
type OpaPolicyFailureKind = "client_unavailable" | "input_build_failed" | "evaluation_failed" | "invalid_response";
|
|
63
|
-
|
|
115
|
+
/**
|
|
116
|
+
* Maps a validated `OpaDecision` to the fields of a `PolicyOutcome` the
|
|
117
|
+
* adapter consumes (`result`, `reason`, optional extra `metadata`). The
|
|
118
|
+
* optional third argument, `execution`, exposes OPA-side provenance
|
|
119
|
+
* (`decisionId`, `bundleRevision`, raw `provenance`) so a mapper can lift
|
|
120
|
+
* specific fields into `outcome.metadata` if the host wants them queryable
|
|
121
|
+
* outside `dispatchEvidence.engine`.
|
|
122
|
+
*
|
|
123
|
+
* Existing two-argument mappers (`(decision, ctx) => …`) remain valid —
|
|
124
|
+
* TypeScript permits implementations with fewer parameters.
|
|
125
|
+
*/
|
|
126
|
+
type OpaPolicyDecisionMapper<TDb = unknown> = (decision: OpaDecision, ctx: PolicyContext<TDb>, execution: OpaPolicyExecution) => Partial<PolicyOutcome> & {
|
|
64
127
|
metadata?: Record<string, unknown>;
|
|
65
128
|
};
|
|
66
129
|
interface CreateOpaPolicyEvaluatorOptions<TDb = unknown> {
|
|
@@ -111,20 +174,39 @@ interface CreateOpaPolicyEvaluatorOptions<TDb = unknown> {
|
|
|
111
174
|
|
|
112
175
|
/**
|
|
113
176
|
* Build a Fabric `PolicyEvaluator` that delegates decision-making to an OPA
|
|
114
|
-
* (Open Policy Agent) deployment.
|
|
177
|
+
* (Open Policy Agent) deployment.
|
|
178
|
+
*
|
|
179
|
+
* The adapter accepts either of two service shapes under
|
|
180
|
+
* `ctx.services[serviceKey]`:
|
|
115
181
|
*
|
|
116
|
-
*
|
|
182
|
+
* - `OpaPolicyExecutor` (preferred) — `execute(path, input)` returning a rich
|
|
183
|
+
* `OpaPolicyExecution` (decision plus `decisionId`, `bundleRevision`,
|
|
184
|
+
* `opaVersion`, `provenance`). Ship one with {@link executorFromOpaHttp} or
|
|
185
|
+
* {@link executorFromOpaClient}.
|
|
186
|
+
* - `OpaPolicyClient` — `evaluate(path, input)` returning the raw decision
|
|
187
|
+
* only. Compatible with the high-level `@open-policy-agent/opa` SDK at the
|
|
188
|
+
* cost of OPA-side provenance.
|
|
189
|
+
*
|
|
190
|
+
* If both methods are present, `execute` is preferred.
|
|
191
|
+
*
|
|
192
|
+
* The evaluator:
|
|
193
|
+
*
|
|
194
|
+
* 1. Resolves the service from `ctx.services[serviceKey]`.
|
|
117
195
|
* 2. Builds a JSON input snapshot via `buildInput(ctx)` — domain code that
|
|
118
|
-
* snapshots facts the engine needs.
|
|
119
|
-
*
|
|
120
|
-
* 3.
|
|
196
|
+
* snapshots facts the engine needs. OPA never reaches into the database
|
|
197
|
+
* directly; all facts flow through this function.
|
|
198
|
+
* 3. Calls OPA at `binding.decisionPath`.
|
|
121
199
|
* 4. Validates the response against the house-style shape (`OpaDecision`).
|
|
122
200
|
* 5. Maps the decision to a `PolicyOutcome` and attaches engine provenance
|
|
123
|
-
*
|
|
201
|
+
* under `dispatchEvidence.engine` — `name`, optional `version`, and
|
|
202
|
+
* metadata covering `decisionPath`, `regoPackage`, `inputHash`, plus (when
|
|
203
|
+
* the executor surfaces them) `decisionId`, `bundleName`, `bundleRevision`,
|
|
204
|
+
* `bundleRevisionStatus`, and the raw `provenance` block.
|
|
124
205
|
*
|
|
125
|
-
* Failures (missing client, build failure, network failure, malformed
|
|
126
|
-
* are governed by `onError`. Default `"block"` honors Fabric's
|
|
127
|
-
* posture and records the failure category under
|
|
206
|
+
* Failures (missing client, build failure, network failure, malformed
|
|
207
|
+
* response) are governed by `onError`. Default `"block"` honors Fabric's
|
|
208
|
+
* default-deny posture and records the failure category under
|
|
209
|
+
* `engine.metadata.error`.
|
|
128
210
|
*/
|
|
129
211
|
declare function createOpaPolicyEvaluator<TDb = unknown>(options: CreateOpaPolicyEvaluatorOptions<TDb>): PolicyEvaluator<TDb>;
|
|
130
212
|
|
|
@@ -160,4 +242,85 @@ declare function stableStringify(value: unknown): string;
|
|
|
160
242
|
*/
|
|
161
243
|
declare function defaultHashInput(input: Record<string, unknown>): string;
|
|
162
244
|
|
|
163
|
-
|
|
245
|
+
/**
|
|
246
|
+
* Wrap an `OpaPolicyClient` (the high-level `@open-policy-agent/opa` SDK
|
|
247
|
+
* shape) as an {@link OpaPolicyExecutor}. The resulting executor carries no
|
|
248
|
+
* OPA-side provenance — the high-level SDK discards `decisionId` and
|
|
249
|
+
* `provenance` from the response — but does centralize the
|
|
250
|
+
* `(path, input) => execution` plumbing so hosts can choose at startup whether
|
|
251
|
+
* to wire a client or a full executor.
|
|
252
|
+
*
|
|
253
|
+
* For full provenance (decisionId, bundleRevision, opaVersion), prefer
|
|
254
|
+
* {@link executorFromOpaHttp}.
|
|
255
|
+
*/
|
|
256
|
+
declare function executorFromOpaClient(client: OpaPolicyClient): OpaPolicyExecutor;
|
|
257
|
+
|
|
258
|
+
interface OpaHttpExecutorOptions {
|
|
259
|
+
/**
|
|
260
|
+
* Base URL of the OPA server, e.g. `"http://localhost:8181"` or
|
|
261
|
+
* `"https://opa.internal.example.com"`. Trailing slash is permitted; the
|
|
262
|
+
* executor normalizes it.
|
|
263
|
+
*/
|
|
264
|
+
baseUrl: string;
|
|
265
|
+
/**
|
|
266
|
+
* Override for `globalThis.fetch`. Lets the host inject a fetch instance
|
|
267
|
+
* with retries, mTLS, OpenTelemetry instrumentation, etc. Defaults to
|
|
268
|
+
* `globalThis.fetch`.
|
|
269
|
+
*/
|
|
270
|
+
fetch?: typeof globalThis.fetch;
|
|
271
|
+
/** Extra request headers (e.g. `Authorization`). */
|
|
272
|
+
headers?: Record<string, string>;
|
|
273
|
+
/**
|
|
274
|
+
* Request OPA-side provenance (`decision_id`, `provenance.version`,
|
|
275
|
+
* `provenance.bundles[*].revision`) via `?provenance=true`. Defaults to
|
|
276
|
+
* `true` — turning it off forfeits OPA's contribution to audit evidence
|
|
277
|
+
* and is rarely what you want.
|
|
278
|
+
*/
|
|
279
|
+
provenance?: boolean;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Build an {@link OpaPolicyExecutor} that talks to OPA's REST API directly.
|
|
283
|
+
*
|
|
284
|
+
* POSTs to `{baseUrl}/v1/data/{path}?provenance=true` with body
|
|
285
|
+
* `{ "input": ... }`. Parses the snake_case response and lifts:
|
|
286
|
+
*
|
|
287
|
+
* - `result` → `execution.decision`
|
|
288
|
+
* - `decision_id` → `execution.decisionId`
|
|
289
|
+
* - `provenance.version` → `execution.opaVersion`
|
|
290
|
+
* - `provenance` (whole block) → `execution.provenance`
|
|
291
|
+
*
|
|
292
|
+
* Bundle revision selection happens in the factory, not the executor, because
|
|
293
|
+
* it depends on `binding.bundleName`.
|
|
294
|
+
*
|
|
295
|
+
* Calling this in environments without `globalThis.fetch` (older Node) throws
|
|
296
|
+
* at call time; pass `fetch: nodeFetch` or `fetch: undici-fetch` etc. via the
|
|
297
|
+
* options to support those runtimes explicitly.
|
|
298
|
+
*/
|
|
299
|
+
declare function executorFromOpaHttp(opts: OpaHttpExecutorOptions): OpaPolicyExecutor;
|
|
300
|
+
|
|
301
|
+
interface BundleSelection {
|
|
302
|
+
bundleRevision?: string;
|
|
303
|
+
/**
|
|
304
|
+
* Set to `"ambiguous"` when OPA reports multiple bundles and the binding
|
|
305
|
+
* did not pick one with `bundleName`. The adapter records this on the
|
|
306
|
+
* evidence so dashboards can flag policies that need explicit selectors.
|
|
307
|
+
* `"missing"` is set when provenance was expected but absent.
|
|
308
|
+
*/
|
|
309
|
+
bundleRevisionStatus?: "ambiguous" | "missing";
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Determine which OPA bundle revision applies to this decision.
|
|
313
|
+
*
|
|
314
|
+
* Rules (in order):
|
|
315
|
+
* 1. If the executor already populated `execution.bundleRevision`, use it.
|
|
316
|
+
* 2. If `binding.bundleName` is set, look it up in `provenance.bundles`.
|
|
317
|
+
* 3. If exactly one bundle is reported, use its revision unambiguously.
|
|
318
|
+
* 4. If multiple bundles are reported and no selector exists, mark
|
|
319
|
+
* `bundleRevisionStatus: "ambiguous"`.
|
|
320
|
+
* 5. If no provenance is reported at all, return `{}` — the caller decides
|
|
321
|
+
* whether absence is meaningful (it is not, for executors that don't
|
|
322
|
+
* carry provenance).
|
|
323
|
+
*/
|
|
324
|
+
declare function selectBundleRevision(execution: OpaPolicyExecution, binding: OpaPolicyBinding): BundleSelection;
|
|
325
|
+
|
|
326
|
+
export { type BundleSelection, type CreateOpaPolicyEvaluatorOptions, type OpaDecision, type OpaDecisionParseResult, type OpaHttpExecutorOptions, type OpaPolicyBinding, type OpaPolicyClient, type OpaPolicyDecisionMapper, type OpaPolicyErrorMode, type OpaPolicyExecution, type OpaPolicyExecutor, type OpaPolicyFailureKind, createOpaPolicyEvaluator, defaultHashInput, executorFromOpaClient, executorFromOpaHttp, parseOpaDecision, selectBundleRevision, stableStringify };
|
package/dist/index.d.ts
CHANGED
|
@@ -25,6 +25,58 @@ interface OpaPolicyBinding {
|
|
|
25
25
|
* keep the package name aligned with the decision path.
|
|
26
26
|
*/
|
|
27
27
|
regoPackage?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Optional selector for `bundleRevision` extraction when OPA's
|
|
30
|
+
* `provenance.bundles` carries multiple bundles. If absent and OPA reports
|
|
31
|
+
* a single bundle, the adapter uses that revision unambiguously. If absent
|
|
32
|
+
* and multiple bundles are present, the adapter marks the evidence with
|
|
33
|
+
* `bundleRevisionStatus: "ambiguous"` so audit dashboards can flag policies
|
|
34
|
+
* that need explicit selectors.
|
|
35
|
+
*
|
|
36
|
+
* Example: `"lending"` for a bundle named `lending` served by OPA.
|
|
37
|
+
*/
|
|
38
|
+
bundleName?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Result of one OPA evaluation, normalized for the adapter. Returned by
|
|
42
|
+
* implementations of {@link OpaPolicyExecutor}. The adapter accepts either a
|
|
43
|
+
* client (with `evaluate`) or an executor (with `execute`); the executor path
|
|
44
|
+
* preserves provenance OPA exposes via `?provenance=true` — primarily
|
|
45
|
+
* `decision_id` and `provenance.bundles[*].revision`.
|
|
46
|
+
*/
|
|
47
|
+
interface OpaPolicyExecution {
|
|
48
|
+
/** The raw decision body — the value of `result` in OPA's response. */
|
|
49
|
+
decision: unknown;
|
|
50
|
+
/** OPA's per-request decision ID (response field `decision_id`). */
|
|
51
|
+
decisionId?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Pre-selected bundle revision, when the executor can determine it
|
|
54
|
+
* unambiguously without binding context (e.g. a transport that only ever
|
|
55
|
+
* sees a single bundle). The factory may override this from
|
|
56
|
+
* `provenance.bundles` + `binding.bundleName`.
|
|
57
|
+
*/
|
|
58
|
+
bundleRevision?: string;
|
|
59
|
+
/** OPA server version (response field `provenance.version`). */
|
|
60
|
+
opaVersion?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Raw `provenance` block from the OPA response. Carries
|
|
63
|
+
* `bundles: { [name]: { revision } }`, build info, etc. The factory uses
|
|
64
|
+
* this together with `binding.bundleName` to select `bundleRevision`.
|
|
65
|
+
*/
|
|
66
|
+
provenance?: Record<string, unknown>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Rich execution interface. Implementations are responsible for talking to
|
|
70
|
+
* OPA and surfacing provenance. Two official implementations ship in this
|
|
71
|
+
* package: {@link executorFromOpaClient} (wraps the high-level SDK; limited
|
|
72
|
+
* provenance) and {@link executorFromOpaHttp} (calls OPA REST directly with
|
|
73
|
+
* `?provenance=true`; full provenance).
|
|
74
|
+
*
|
|
75
|
+
* Hosts wire one of these into `ctx.services[serviceKey]` as an alternative
|
|
76
|
+
* to {@link OpaPolicyClient}.
|
|
77
|
+
*/
|
|
78
|
+
interface OpaPolicyExecutor {
|
|
79
|
+
execute(path: string, input: unknown): Promise<OpaPolicyExecution>;
|
|
28
80
|
}
|
|
29
81
|
/**
|
|
30
82
|
* Structured Rego decision shape — the "house style" the adapter expects from
|
|
@@ -60,7 +112,18 @@ type OpaPolicyErrorMode = "block" | "throw";
|
|
|
60
112
|
* something we couldn't parse" from "no client was injected at runtime".
|
|
61
113
|
*/
|
|
62
114
|
type OpaPolicyFailureKind = "client_unavailable" | "input_build_failed" | "evaluation_failed" | "invalid_response";
|
|
63
|
-
|
|
115
|
+
/**
|
|
116
|
+
* Maps a validated `OpaDecision` to the fields of a `PolicyOutcome` the
|
|
117
|
+
* adapter consumes (`result`, `reason`, optional extra `metadata`). The
|
|
118
|
+
* optional third argument, `execution`, exposes OPA-side provenance
|
|
119
|
+
* (`decisionId`, `bundleRevision`, raw `provenance`) so a mapper can lift
|
|
120
|
+
* specific fields into `outcome.metadata` if the host wants them queryable
|
|
121
|
+
* outside `dispatchEvidence.engine`.
|
|
122
|
+
*
|
|
123
|
+
* Existing two-argument mappers (`(decision, ctx) => …`) remain valid —
|
|
124
|
+
* TypeScript permits implementations with fewer parameters.
|
|
125
|
+
*/
|
|
126
|
+
type OpaPolicyDecisionMapper<TDb = unknown> = (decision: OpaDecision, ctx: PolicyContext<TDb>, execution: OpaPolicyExecution) => Partial<PolicyOutcome> & {
|
|
64
127
|
metadata?: Record<string, unknown>;
|
|
65
128
|
};
|
|
66
129
|
interface CreateOpaPolicyEvaluatorOptions<TDb = unknown> {
|
|
@@ -111,20 +174,39 @@ interface CreateOpaPolicyEvaluatorOptions<TDb = unknown> {
|
|
|
111
174
|
|
|
112
175
|
/**
|
|
113
176
|
* Build a Fabric `PolicyEvaluator` that delegates decision-making to an OPA
|
|
114
|
-
* (Open Policy Agent) deployment.
|
|
177
|
+
* (Open Policy Agent) deployment.
|
|
178
|
+
*
|
|
179
|
+
* The adapter accepts either of two service shapes under
|
|
180
|
+
* `ctx.services[serviceKey]`:
|
|
115
181
|
*
|
|
116
|
-
*
|
|
182
|
+
* - `OpaPolicyExecutor` (preferred) — `execute(path, input)` returning a rich
|
|
183
|
+
* `OpaPolicyExecution` (decision plus `decisionId`, `bundleRevision`,
|
|
184
|
+
* `opaVersion`, `provenance`). Ship one with {@link executorFromOpaHttp} or
|
|
185
|
+
* {@link executorFromOpaClient}.
|
|
186
|
+
* - `OpaPolicyClient` — `evaluate(path, input)` returning the raw decision
|
|
187
|
+
* only. Compatible with the high-level `@open-policy-agent/opa` SDK at the
|
|
188
|
+
* cost of OPA-side provenance.
|
|
189
|
+
*
|
|
190
|
+
* If both methods are present, `execute` is preferred.
|
|
191
|
+
*
|
|
192
|
+
* The evaluator:
|
|
193
|
+
*
|
|
194
|
+
* 1. Resolves the service from `ctx.services[serviceKey]`.
|
|
117
195
|
* 2. Builds a JSON input snapshot via `buildInput(ctx)` — domain code that
|
|
118
|
-
* snapshots facts the engine needs.
|
|
119
|
-
*
|
|
120
|
-
* 3.
|
|
196
|
+
* snapshots facts the engine needs. OPA never reaches into the database
|
|
197
|
+
* directly; all facts flow through this function.
|
|
198
|
+
* 3. Calls OPA at `binding.decisionPath`.
|
|
121
199
|
* 4. Validates the response against the house-style shape (`OpaDecision`).
|
|
122
200
|
* 5. Maps the decision to a `PolicyOutcome` and attaches engine provenance
|
|
123
|
-
*
|
|
201
|
+
* under `dispatchEvidence.engine` — `name`, optional `version`, and
|
|
202
|
+
* metadata covering `decisionPath`, `regoPackage`, `inputHash`, plus (when
|
|
203
|
+
* the executor surfaces them) `decisionId`, `bundleName`, `bundleRevision`,
|
|
204
|
+
* `bundleRevisionStatus`, and the raw `provenance` block.
|
|
124
205
|
*
|
|
125
|
-
* Failures (missing client, build failure, network failure, malformed
|
|
126
|
-
* are governed by `onError`. Default `"block"` honors Fabric's
|
|
127
|
-
* posture and records the failure category under
|
|
206
|
+
* Failures (missing client, build failure, network failure, malformed
|
|
207
|
+
* response) are governed by `onError`. Default `"block"` honors Fabric's
|
|
208
|
+
* default-deny posture and records the failure category under
|
|
209
|
+
* `engine.metadata.error`.
|
|
128
210
|
*/
|
|
129
211
|
declare function createOpaPolicyEvaluator<TDb = unknown>(options: CreateOpaPolicyEvaluatorOptions<TDb>): PolicyEvaluator<TDb>;
|
|
130
212
|
|
|
@@ -160,4 +242,85 @@ declare function stableStringify(value: unknown): string;
|
|
|
160
242
|
*/
|
|
161
243
|
declare function defaultHashInput(input: Record<string, unknown>): string;
|
|
162
244
|
|
|
163
|
-
|
|
245
|
+
/**
|
|
246
|
+
* Wrap an `OpaPolicyClient` (the high-level `@open-policy-agent/opa` SDK
|
|
247
|
+
* shape) as an {@link OpaPolicyExecutor}. The resulting executor carries no
|
|
248
|
+
* OPA-side provenance — the high-level SDK discards `decisionId` and
|
|
249
|
+
* `provenance` from the response — but does centralize the
|
|
250
|
+
* `(path, input) => execution` plumbing so hosts can choose at startup whether
|
|
251
|
+
* to wire a client or a full executor.
|
|
252
|
+
*
|
|
253
|
+
* For full provenance (decisionId, bundleRevision, opaVersion), prefer
|
|
254
|
+
* {@link executorFromOpaHttp}.
|
|
255
|
+
*/
|
|
256
|
+
declare function executorFromOpaClient(client: OpaPolicyClient): OpaPolicyExecutor;
|
|
257
|
+
|
|
258
|
+
interface OpaHttpExecutorOptions {
|
|
259
|
+
/**
|
|
260
|
+
* Base URL of the OPA server, e.g. `"http://localhost:8181"` or
|
|
261
|
+
* `"https://opa.internal.example.com"`. Trailing slash is permitted; the
|
|
262
|
+
* executor normalizes it.
|
|
263
|
+
*/
|
|
264
|
+
baseUrl: string;
|
|
265
|
+
/**
|
|
266
|
+
* Override for `globalThis.fetch`. Lets the host inject a fetch instance
|
|
267
|
+
* with retries, mTLS, OpenTelemetry instrumentation, etc. Defaults to
|
|
268
|
+
* `globalThis.fetch`.
|
|
269
|
+
*/
|
|
270
|
+
fetch?: typeof globalThis.fetch;
|
|
271
|
+
/** Extra request headers (e.g. `Authorization`). */
|
|
272
|
+
headers?: Record<string, string>;
|
|
273
|
+
/**
|
|
274
|
+
* Request OPA-side provenance (`decision_id`, `provenance.version`,
|
|
275
|
+
* `provenance.bundles[*].revision`) via `?provenance=true`. Defaults to
|
|
276
|
+
* `true` — turning it off forfeits OPA's contribution to audit evidence
|
|
277
|
+
* and is rarely what you want.
|
|
278
|
+
*/
|
|
279
|
+
provenance?: boolean;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Build an {@link OpaPolicyExecutor} that talks to OPA's REST API directly.
|
|
283
|
+
*
|
|
284
|
+
* POSTs to `{baseUrl}/v1/data/{path}?provenance=true` with body
|
|
285
|
+
* `{ "input": ... }`. Parses the snake_case response and lifts:
|
|
286
|
+
*
|
|
287
|
+
* - `result` → `execution.decision`
|
|
288
|
+
* - `decision_id` → `execution.decisionId`
|
|
289
|
+
* - `provenance.version` → `execution.opaVersion`
|
|
290
|
+
* - `provenance` (whole block) → `execution.provenance`
|
|
291
|
+
*
|
|
292
|
+
* Bundle revision selection happens in the factory, not the executor, because
|
|
293
|
+
* it depends on `binding.bundleName`.
|
|
294
|
+
*
|
|
295
|
+
* Calling this in environments without `globalThis.fetch` (older Node) throws
|
|
296
|
+
* at call time; pass `fetch: nodeFetch` or `fetch: undici-fetch` etc. via the
|
|
297
|
+
* options to support those runtimes explicitly.
|
|
298
|
+
*/
|
|
299
|
+
declare function executorFromOpaHttp(opts: OpaHttpExecutorOptions): OpaPolicyExecutor;
|
|
300
|
+
|
|
301
|
+
interface BundleSelection {
|
|
302
|
+
bundleRevision?: string;
|
|
303
|
+
/**
|
|
304
|
+
* Set to `"ambiguous"` when OPA reports multiple bundles and the binding
|
|
305
|
+
* did not pick one with `bundleName`. The adapter records this on the
|
|
306
|
+
* evidence so dashboards can flag policies that need explicit selectors.
|
|
307
|
+
* `"missing"` is set when provenance was expected but absent.
|
|
308
|
+
*/
|
|
309
|
+
bundleRevisionStatus?: "ambiguous" | "missing";
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Determine which OPA bundle revision applies to this decision.
|
|
313
|
+
*
|
|
314
|
+
* Rules (in order):
|
|
315
|
+
* 1. If the executor already populated `execution.bundleRevision`, use it.
|
|
316
|
+
* 2. If `binding.bundleName` is set, look it up in `provenance.bundles`.
|
|
317
|
+
* 3. If exactly one bundle is reported, use its revision unambiguously.
|
|
318
|
+
* 4. If multiple bundles are reported and no selector exists, mark
|
|
319
|
+
* `bundleRevisionStatus: "ambiguous"`.
|
|
320
|
+
* 5. If no provenance is reported at all, return `{}` — the caller decides
|
|
321
|
+
* whether absence is meaningful (it is not, for executors that don't
|
|
322
|
+
* carry provenance).
|
|
323
|
+
*/
|
|
324
|
+
declare function selectBundleRevision(execution: OpaPolicyExecution, binding: OpaPolicyBinding): BundleSelection;
|
|
325
|
+
|
|
326
|
+
export { type BundleSelection, type CreateOpaPolicyEvaluatorOptions, type OpaDecision, type OpaDecisionParseResult, type OpaHttpExecutorOptions, type OpaPolicyBinding, type OpaPolicyClient, type OpaPolicyDecisionMapper, type OpaPolicyErrorMode, type OpaPolicyExecution, type OpaPolicyExecutor, type OpaPolicyFailureKind, createOpaPolicyEvaluator, defaultHashInput, executorFromOpaClient, executorFromOpaHttp, parseOpaDecision, selectBundleRevision, stableStringify };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
2
|
|
|
3
|
+
// src/bundle-selector.ts
|
|
4
|
+
function selectBundleRevision(execution, binding) {
|
|
5
|
+
if (typeof execution.bundleRevision === "string" && execution.bundleRevision.length > 0) {
|
|
6
|
+
return { bundleRevision: execution.bundleRevision };
|
|
7
|
+
}
|
|
8
|
+
const bundles = readBundles(execution.provenance);
|
|
9
|
+
if (!bundles) return {};
|
|
10
|
+
const names = Object.keys(bundles);
|
|
11
|
+
if (names.length === 0) return {};
|
|
12
|
+
if (binding.bundleName) {
|
|
13
|
+
const revision = bundles[binding.bundleName]?.revision;
|
|
14
|
+
return typeof revision === "string" && revision.length > 0 ? { bundleRevision: revision } : {};
|
|
15
|
+
}
|
|
16
|
+
if (names.length === 1) {
|
|
17
|
+
const only = names[0];
|
|
18
|
+
const revision = only !== void 0 ? bundles[only]?.revision : void 0;
|
|
19
|
+
return typeof revision === "string" && revision.length > 0 ? { bundleRevision: revision } : {};
|
|
20
|
+
}
|
|
21
|
+
return { bundleRevisionStatus: "ambiguous" };
|
|
22
|
+
}
|
|
23
|
+
function readBundles(provenance) {
|
|
24
|
+
if (!provenance || typeof provenance !== "object") return void 0;
|
|
25
|
+
const b = provenance.bundles;
|
|
26
|
+
if (!b || typeof b !== "object" || Array.isArray(b)) return void 0;
|
|
27
|
+
return b;
|
|
28
|
+
}
|
|
29
|
+
|
|
3
30
|
// src/decision-schema.ts
|
|
4
31
|
var POLICY_RESULTS = ["pass", "warn", "block"];
|
|
5
32
|
function parseOpaDecision(raw) {
|
|
@@ -89,13 +116,14 @@ function createOpaPolicyEvaluator(options) {
|
|
|
89
116
|
version: binding.version,
|
|
90
117
|
previewSafe,
|
|
91
118
|
evaluate: async (ctx) => {
|
|
92
|
-
const
|
|
93
|
-
|
|
119
|
+
const service = ctx.services?.[serviceKey];
|
|
120
|
+
const transport = resolveTransport(service);
|
|
121
|
+
if (!transport) {
|
|
94
122
|
return makeFailureOutcome(
|
|
95
123
|
binding,
|
|
96
124
|
engineVersion,
|
|
97
125
|
"client_unavailable",
|
|
98
|
-
`No OPA client at ctx.services["${serviceKey}"]`,
|
|
126
|
+
`No OPA client/executor at ctx.services["${serviceKey}"]`,
|
|
99
127
|
onError
|
|
100
128
|
);
|
|
101
129
|
}
|
|
@@ -116,9 +144,9 @@ function createOpaPolicyEvaluator(options) {
|
|
|
116
144
|
inputHash = hashInput(input);
|
|
117
145
|
} catch {
|
|
118
146
|
}
|
|
119
|
-
let
|
|
147
|
+
let execution;
|
|
120
148
|
try {
|
|
121
|
-
|
|
149
|
+
execution = await transport(binding.decisionPath, input);
|
|
122
150
|
} catch (err) {
|
|
123
151
|
return makeFailureOutcome(
|
|
124
152
|
binding,
|
|
@@ -129,7 +157,7 @@ function createOpaPolicyEvaluator(options) {
|
|
|
129
157
|
inputHash
|
|
130
158
|
);
|
|
131
159
|
}
|
|
132
|
-
const parsed = parseOpaDecision(
|
|
160
|
+
const parsed = parseOpaDecision(execution.decision);
|
|
133
161
|
if (!parsed.ok) {
|
|
134
162
|
return makeFailureOutcome(
|
|
135
163
|
binding,
|
|
@@ -137,13 +165,15 @@ function createOpaPolicyEvaluator(options) {
|
|
|
137
165
|
"invalid_response",
|
|
138
166
|
`OPA returned malformed decision: ${parsed.error}`,
|
|
139
167
|
onError,
|
|
140
|
-
inputHash
|
|
168
|
+
inputHash,
|
|
169
|
+
execution
|
|
141
170
|
);
|
|
142
171
|
}
|
|
143
172
|
return buildSuccessOutcome(
|
|
144
173
|
binding,
|
|
145
174
|
engineVersion,
|
|
146
175
|
parsed.decision,
|
|
176
|
+
execution,
|
|
147
177
|
inputHash,
|
|
148
178
|
ctx,
|
|
149
179
|
mapDecision
|
|
@@ -151,13 +181,28 @@ function createOpaPolicyEvaluator(options) {
|
|
|
151
181
|
}
|
|
152
182
|
};
|
|
153
183
|
}
|
|
154
|
-
function
|
|
155
|
-
|
|
184
|
+
function resolveTransport(service) {
|
|
185
|
+
if (!service || typeof service !== "object") return null;
|
|
186
|
+
const exec = service.execute;
|
|
187
|
+
if (typeof exec === "function") {
|
|
188
|
+
return (path, input) => service.execute(path, input);
|
|
189
|
+
}
|
|
190
|
+
const eval_ = service.evaluate;
|
|
191
|
+
if (typeof eval_ === "function") {
|
|
192
|
+
return async (path, input) => ({
|
|
193
|
+
decision: await service.evaluate(path, input)
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
function buildSuccessOutcome(binding, engineVersion, decision, execution, inputHash, ctx, mapDecision) {
|
|
199
|
+
const selection = selectBundleRevision(execution, binding);
|
|
200
|
+
const mapped = mapDecision ? mapDecision(decision, ctx, execution) : { result: decision.result, reason: decision.reason };
|
|
156
201
|
return {
|
|
157
202
|
policyId: binding.policyId,
|
|
158
203
|
policyVersion: binding.version,
|
|
159
|
-
result: mapped.result,
|
|
160
|
-
reason: mapped.reason,
|
|
204
|
+
result: mapped.result ?? decision.result,
|
|
205
|
+
reason: mapped.reason ?? decision.reason,
|
|
161
206
|
metadata: {
|
|
162
207
|
...mapped.metadata ?? {},
|
|
163
208
|
opaDecision: decision
|
|
@@ -169,11 +214,16 @@ function buildSuccessOutcome(binding, engineVersion, decision, inputHash, ctx, m
|
|
|
169
214
|
dispatchPath: ["code"],
|
|
170
215
|
engine: {
|
|
171
216
|
name: ENGINE_NAME,
|
|
172
|
-
version: engineVersion,
|
|
217
|
+
version: execution.opaVersion ?? engineVersion,
|
|
173
218
|
metadata: stripUndefined({
|
|
174
219
|
decisionPath: binding.decisionPath,
|
|
175
220
|
regoPackage: binding.regoPackage,
|
|
176
221
|
inputHash,
|
|
222
|
+
decisionId: execution.decisionId,
|
|
223
|
+
bundleName: binding.bundleName,
|
|
224
|
+
bundleRevision: selection.bundleRevision,
|
|
225
|
+
bundleRevisionStatus: selection.bundleRevisionStatus,
|
|
226
|
+
provenance: execution.provenance,
|
|
177
227
|
conditionResults: decision.conditionResults,
|
|
178
228
|
factsUsed: decision.factsUsed,
|
|
179
229
|
obligations: decision.obligations
|
|
@@ -182,12 +232,13 @@ function buildSuccessOutcome(binding, engineVersion, decision, inputHash, ctx, m
|
|
|
182
232
|
}
|
|
183
233
|
};
|
|
184
234
|
}
|
|
185
|
-
function makeFailureOutcome(binding, engineVersion, error, reason, onError, inputHash) {
|
|
235
|
+
function makeFailureOutcome(binding, engineVersion, error, reason, onError, inputHash, execution) {
|
|
186
236
|
if (onError === "throw") {
|
|
187
237
|
const err = new Error(reason);
|
|
188
238
|
err.kind = error;
|
|
189
239
|
throw err;
|
|
190
240
|
}
|
|
241
|
+
const selection = execution ? selectBundleRevision(execution, binding) : {};
|
|
191
242
|
return {
|
|
192
243
|
policyId: binding.policyId,
|
|
193
244
|
policyVersion: binding.version,
|
|
@@ -203,11 +254,16 @@ function makeFailureOutcome(binding, engineVersion, error, reason, onError, inpu
|
|
|
203
254
|
dispatchPath: ["code"],
|
|
204
255
|
engine: {
|
|
205
256
|
name: ENGINE_NAME,
|
|
206
|
-
version: engineVersion,
|
|
257
|
+
version: execution?.opaVersion ?? engineVersion,
|
|
207
258
|
metadata: stripUndefined({
|
|
208
259
|
decisionPath: binding.decisionPath,
|
|
209
260
|
regoPackage: binding.regoPackage,
|
|
210
261
|
inputHash,
|
|
262
|
+
decisionId: execution?.decisionId,
|
|
263
|
+
bundleName: binding.bundleName,
|
|
264
|
+
bundleRevision: selection.bundleRevision,
|
|
265
|
+
bundleRevisionStatus: selection.bundleRevisionStatus,
|
|
266
|
+
provenance: execution?.provenance,
|
|
211
267
|
error,
|
|
212
268
|
errorReason: reason
|
|
213
269
|
})
|
|
@@ -232,6 +288,71 @@ function formatError(err) {
|
|
|
232
288
|
}
|
|
233
289
|
}
|
|
234
290
|
|
|
235
|
-
|
|
291
|
+
// src/executor-client.ts
|
|
292
|
+
function executorFromOpaClient(client) {
|
|
293
|
+
return {
|
|
294
|
+
async execute(path, input) {
|
|
295
|
+
const decision = await client.evaluate(path, input);
|
|
296
|
+
return { decision };
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/executor-http.ts
|
|
302
|
+
function executorFromOpaHttp(opts) {
|
|
303
|
+
const baseUrl = stripTrailingSlash(opts.baseUrl);
|
|
304
|
+
const provenance = opts.provenance ?? true;
|
|
305
|
+
const customFetch = opts.fetch;
|
|
306
|
+
const headers = {
|
|
307
|
+
"content-type": "application/json",
|
|
308
|
+
accept: "application/json",
|
|
309
|
+
...opts.headers ?? {}
|
|
310
|
+
};
|
|
311
|
+
return {
|
|
312
|
+
async execute(path, input) {
|
|
313
|
+
const fetchImpl = customFetch ?? globalThis.fetch;
|
|
314
|
+
if (typeof fetchImpl !== "function") {
|
|
315
|
+
throw new Error(
|
|
316
|
+
"executorFromOpaHttp: no fetch implementation available. Pass `fetch` in options or run on a runtime with `globalThis.fetch`."
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
const url = `${baseUrl}/v1/data/${stripLeadingSlash(path)}${provenance ? "?provenance=true" : ""}`;
|
|
320
|
+
const response = await fetchImpl(url, {
|
|
321
|
+
method: "POST",
|
|
322
|
+
headers,
|
|
323
|
+
body: JSON.stringify({ input })
|
|
324
|
+
});
|
|
325
|
+
if (!response.ok) {
|
|
326
|
+
const text = await safeReadText(response);
|
|
327
|
+
throw new Error(`OPA HTTP ${response.status} at ${url}: ${truncate(text, 500)}`);
|
|
328
|
+
}
|
|
329
|
+
const body = await response.json();
|
|
330
|
+
return {
|
|
331
|
+
decision: body.result,
|
|
332
|
+
decisionId: body.decision_id,
|
|
333
|
+
opaVersion: body.provenance?.version,
|
|
334
|
+
provenance: body.provenance
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
function stripTrailingSlash(s) {
|
|
340
|
+
return s.replace(/\/+$/, "");
|
|
341
|
+
}
|
|
342
|
+
function stripLeadingSlash(s) {
|
|
343
|
+
return s.replace(/^\/+/, "");
|
|
344
|
+
}
|
|
345
|
+
async function safeReadText(response) {
|
|
346
|
+
try {
|
|
347
|
+
return await response.text();
|
|
348
|
+
} catch {
|
|
349
|
+
return "<unreadable body>";
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function truncate(s, max) {
|
|
353
|
+
return s.length <= max ? s : `${s.slice(0, max)}\u2026`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export { createOpaPolicyEvaluator, defaultHashInput, executorFromOpaClient, executorFromOpaHttp, parseOpaDecision, selectBundleRevision, stableStringify };
|
|
236
357
|
//# sourceMappingURL=index.js.map
|
|
237
358
|
//# sourceMappingURL=index.js.map
|