@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/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
- type OpaPolicyDecisionMapper<TDb = unknown> = (decision: OpaDecision, ctx: PolicyContext<TDb>) => Pick<PolicyOutcome, "result" | "reason"> & {
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. The evaluator:
177
+ * (Open Policy Agent) deployment.
178
+ *
179
+ * The adapter accepts either of two service shapes under
180
+ * `ctx.services[serviceKey]`:
115
181
  *
116
- * 1. Reads the host-injected OPA client from `ctx.services[serviceKey]`.
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. Fabric platform never lets OPA reach
119
- * into the database directly; all facts flow through this function.
120
- * 3. Evaluates the Rego decision at `binding.decisionPath`.
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
- * (`engine.name = "opa"`, `decisionPath`, `inputHash`) for audit.
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 response)
126
- * are governed by `onError`. Default `"block"` honors Fabric's default-deny
127
- * posture and records the failure category under `engine.metadata.error`.
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
- export { type CreateOpaPolicyEvaluatorOptions, type OpaDecision, type OpaDecisionParseResult, type OpaPolicyBinding, type OpaPolicyClient, type OpaPolicyDecisionMapper, type OpaPolicyErrorMode, type OpaPolicyFailureKind, createOpaPolicyEvaluator, defaultHashInput, parseOpaDecision, stableStringify };
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
- type OpaPolicyDecisionMapper<TDb = unknown> = (decision: OpaDecision, ctx: PolicyContext<TDb>) => Pick<PolicyOutcome, "result" | "reason"> & {
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. The evaluator:
177
+ * (Open Policy Agent) deployment.
178
+ *
179
+ * The adapter accepts either of two service shapes under
180
+ * `ctx.services[serviceKey]`:
115
181
  *
116
- * 1. Reads the host-injected OPA client from `ctx.services[serviceKey]`.
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. Fabric platform never lets OPA reach
119
- * into the database directly; all facts flow through this function.
120
- * 3. Evaluates the Rego decision at `binding.decisionPath`.
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
- * (`engine.name = "opa"`, `decisionPath`, `inputHash`) for audit.
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 response)
126
- * are governed by `onError`. Default `"block"` honors Fabric's default-deny
127
- * posture and records the failure category under `engine.metadata.error`.
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
- export { type CreateOpaPolicyEvaluatorOptions, type OpaDecision, type OpaDecisionParseResult, type OpaPolicyBinding, type OpaPolicyClient, type OpaPolicyDecisionMapper, type OpaPolicyErrorMode, type OpaPolicyFailureKind, createOpaPolicyEvaluator, defaultHashInput, parseOpaDecision, stableStringify };
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 client = ctx.services?.[serviceKey];
93
- if (!client || typeof client.evaluate !== "function") {
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 rawResult;
147
+ let execution;
120
148
  try {
121
- rawResult = await client.evaluate(binding.decisionPath, input);
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(rawResult);
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 buildSuccessOutcome(binding, engineVersion, decision, inputHash, ctx, mapDecision) {
155
- const mapped = mapDecision ? mapDecision(decision, ctx) : { result: decision.result, reason: decision.reason };
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
- export { createOpaPolicyEvaluator, defaultHashInput, parseOpaDecision, stableStringify };
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