@fabricorg/policy-opa 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/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # @fabricorg/policy-opa
2
+
3
+ ## 0.1.0 — 2026-05-16
4
+
5
+ ### Initial release
6
+
7
+ The official Open Policy Agent (OPA) adapter for `@fabricorg/platform`. Wraps a Rego decision behind Fabric's `PolicyEvaluator` contract so a tenant or vertical can centralize compliance rules in Rego without giving up Fabric's canonical audit identity.
8
+
9
+ - **`createOpaPolicyEvaluator({ binding, buildInput, serviceKey?, onError?, previewSafe?, engineVersion?, mapDecision?, hashInput? })`** — returns a `PolicyEvaluator` ready to `registerPolicy()`.
10
+ - **`OpaPolicyBinding`** — typed pair of `{ policyId, version, decisionPath, regoPackage? }` for the vertical manifest. Keeps Fabric platform identity and OPA artifact paths aligned in one place.
11
+ - **Decision shape (`OpaDecision`)** — Rego "house style" the adapter validates: `result` (required, tri-state), `reason`, `conditionResults[]`, `factsUsed`, `obligations`. Per-condition shape preserves the same evidence granularity Fabric's data policies provide.
12
+ - **Engine evidence** — populates `PolicyDispatchEvidence.engine` with `name: "opa"`, optional `version`, and `metadata.{ decisionPath, regoPackage, inputHash, conditionResults, factsUsed, obligations }`. Fabric's `policyId.vN` / `policyVersion` remain canonical for audit.
13
+ - **Default-deny failure handling.** `onError: "block"` (default) returns `result: "block"` with structured `engine.metadata.error` (`client_unavailable`, `input_build_failed`, `evaluation_failed`, `invalid_response`). `onError: "throw"` propagates errors instead.
14
+ - **`previewSafe: false` by default.** Network-backed evaluators are skipped in `preview` mode by Fabric's runtime; consumers don't need extra wiring.
15
+ - **Structurally-typed OPA client** (`{ evaluate(path, input): Promise<unknown> }`). The high-level `@open-policy-agent/opa` SDK works out of the box; hand-rolled wrappers around the low-level `OpaApiClient` work too.
16
+ - **Zero runtime dependencies.** Dual ESM + CJS distribution. Hand-rolled decision validator (no zod runtime dep). Default `hashInput` uses `node:crypto`; override via the `hashInput` option for FIPS environments or non-Node runtimes.
17
+ - **Peer-deps:** `@fabricorg/platform ^0.3.2`. No hard dep on `@open-policy-agent/opa` — the OPA SDK is consumer-supplied.
18
+ - **25 unit tests** covering: happy path with engine evidence, custom `serviceKey`, custom `mapDecision`, all four failure-mode categories under both `block` and `throw` modes, decision schema validation, and hash determinism.
19
+
20
+ ### Known limitations
21
+
22
+ - The adapter does not yet capture `bundleRevision` / `decisionId` from OPA responses. The high-level `OPAClient.evaluate(...)` API discards them; a richer client interface that uses the low-level `OpaApiClient` (with `provenance: true`) will land in a future minor.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fabric
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,180 @@
1
+ # @fabricorg/policy-opa
2
+
3
+ Open Policy Agent (OPA) adapter for [`@fabricorg/platform`](https://www.npmjs.com/package/@fabricorg/platform). Lets a Rego decision back a Fabric `code` policy while preserving Fabric's canonical audit identity (`policyId.vN`, `policyVersion`) and capturing OPA-side provenance (`decisionPath`, `inputHash`, optional `version`) under `PolicyDispatchEvidence.engine`.
4
+
5
+ The platform stays zero-dependency and vendor-neutral; the OPA-specific glue lives here.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @fabricorg/platform @fabricorg/policy-opa
11
+ # Bring your own OPA client. The high-level SDK works out of the box:
12
+ pnpm add @open-policy-agent/opa
13
+ ```
14
+
15
+ `@fabricorg/policy-opa` has **zero runtime dependencies**. The OPA client is structurally typed (`{ evaluate(path, input): Promise<unknown> }`) so the official `@open-policy-agent/opa` SDK works, and so do hand-rolled wrappers that capture more provenance from the low-level API.
16
+
17
+ ## Usage — the four pieces
18
+
19
+ A complete OPA-backed evaluator has four pieces, three of which the vertical owns.
20
+
21
+ ### 1. The vertical policy manifest
22
+
23
+ Keep Fabric policy IDs, versions, decision paths, and Rego package names in one place. Don't inline strings at call sites.
24
+
25
+ ```ts
26
+ import type { OpaPolicyBinding } from "@fabricorg/policy-opa";
27
+
28
+ export const LENDING_POLICY_BINDINGS = {
29
+ tcpaContactWindow: {
30
+ policyId: "lending.tcpa_contact_window.v1",
31
+ version: 1,
32
+ decisionPath: "fabric/lending/tcpa/contact_window/decision",
33
+ regoPackage: "fabric.lending.tcpa.contact_window",
34
+ },
35
+ } satisfies Record<string, OpaPolicyBinding>;
36
+ ```
37
+
38
+ ### 2. The fact builder (TypeScript)
39
+
40
+ Snapshot the facts OPA needs from your database. OPA never touches your DB directly — your TypeScript host owns reads, tenant scoping, and redaction.
41
+
42
+ ```ts
43
+ import type { PolicyContext } from "@fabricorg/platform/policies";
44
+
45
+ async function buildContactWindowInput(ctx: PolicyContext) {
46
+ const consent = await ctx.db.tcpaConsent.findUnique({
47
+ where: { partyId: ctx.parameters.partyId },
48
+ });
49
+ return {
50
+ tenantId: ctx.tenantId,
51
+ actionId: ctx.actionId,
52
+ now: (ctx.now ?? new Date()).toISOString(),
53
+ consent: consent
54
+ ? { active: consent.status === "active", scope: consent.scope }
55
+ : null,
56
+ contactWindow: { startHour: 8, endHour: 21, timezone: ctx.parameters.timezone },
57
+ };
58
+ }
59
+ ```
60
+
61
+ ### 3. The Rego policy (house style)
62
+
63
+ Have Rego return a structured decision object, not a bare boolean. The adapter validates against this shape.
64
+
65
+ ```rego
66
+ package fabric.lending.tcpa.contact_window
67
+ import rego.v1
68
+
69
+ decision := {
70
+ "result": result,
71
+ "reason": reason,
72
+ "conditionResults": condition_results,
73
+ "factsUsed": facts_used,
74
+ }
75
+
76
+ result := "block" if not consent_active
77
+ result := "block" if not within_window
78
+ default result := "pass"
79
+
80
+ # ...rules computing reason, condition_results, facts_used...
81
+ ```
82
+
83
+ The validator accepts the following fields (TypeScript types: `OpaDecision`):
84
+
85
+ ```ts
86
+ {
87
+ result: "pass" | "warn" | "block"; // required
88
+ reason?: string;
89
+ conditionResults?: Array<{
90
+ conditionId: string;
91
+ passed: boolean;
92
+ result: "pass" | "warn" | "block";
93
+ reason?: string;
94
+ }>;
95
+ factsUsed?: Record<string, unknown>;
96
+ obligations?: Record<string, unknown>;
97
+ }
98
+ ```
99
+
100
+ ### 4. The evaluator factory (this package)
101
+
102
+ ```ts
103
+ import { createOpaPolicyEvaluator } from "@fabricorg/policy-opa";
104
+ import { registerPolicy } from "@fabricorg/platform/policies";
105
+
106
+ import { LENDING_POLICY_BINDINGS } from "../policies/manifest";
107
+
108
+ const tcpaPolicy = createOpaPolicyEvaluator({
109
+ binding: LENDING_POLICY_BINDINGS.tcpaContactWindow,
110
+ buildInput: buildContactWindowInput,
111
+ serviceKey: "opa", // default; ctx.services.opa must hold an OpaPolicyClient
112
+ onError: "block", // default; honors Fabric's default-deny posture
113
+ });
114
+
115
+ registerPolicy(tcpaPolicy);
116
+ ```
117
+
118
+ ### Host wiring
119
+
120
+ The runtime host injects the OPA client into `PolicyContext.services` when it builds a policy context. With the high-level SDK:
121
+
122
+ ```ts
123
+ import { OPAClient } from "@open-policy-agent/opa";
124
+
125
+ const opa = new OPAClient(process.env.OPA_URL ?? "http://localhost:8181");
126
+
127
+ // when constructing PolicyContext for evaluatePolicyDefinitions:
128
+ const policyCtx = {
129
+ tenantId, spaceId, actionInvocationId, actionId, parameters,
130
+ db,
131
+ mode: "execute",
132
+ services: { opa },
133
+ };
134
+ ```
135
+
136
+ ## What lands in audit evidence
137
+
138
+ For a successful OPA evaluation, `PolicyDispatchEvidence.engine` looks like:
139
+
140
+ ```json
141
+ {
142
+ "name": "opa",
143
+ "version": "0.65.0",
144
+ "metadata": {
145
+ "decisionPath": "fabric/lending/tcpa/contact_window/decision",
146
+ "regoPackage": "fabric.lending.tcpa.contact_window",
147
+ "inputHash": "sha256:8a3c…",
148
+ "conditionResults": [{ "conditionId": "consent_active", "passed": true, "result": "pass" }],
149
+ "factsUsed": { "consent": { "active": true } }
150
+ }
151
+ }
152
+ ```
153
+
154
+ Fabric's `policyId` / `policyVersion` remain canonical (`"what was evaluated"`). `engine.metadata` answers `"which artifact produced the answer"`. Both ride on the `PolicyEvaluation` row Fabric persists per outcome.
155
+
156
+ ## Failure modes
157
+
158
+ | Situation | Result | `engine.metadata.error` |
159
+ |---|---|---|
160
+ | `ctx.services[serviceKey]` missing or not an `OpaPolicyClient` | `block` | `client_unavailable` |
161
+ | `buildInput(ctx)` throws | `block` | `input_build_failed` |
162
+ | OPA call throws (network, timeout, 5xx) | `block` | `evaluation_failed` |
163
+ | OPA returns a shape that doesn't match `OpaDecision` | `block` | `invalid_response` |
164
+
165
+ Default behavior is `onError: "block"` — Fabric's default-deny posture. Pass `onError: "throw"` to propagate the error and let Fabric surface it as a `failed` invocation instead.
166
+
167
+ ## Preview mode
168
+
169
+ OPA evaluation is a network call, so the adapter defaults `previewSafe: false`. Fabric's runtime will skip the evaluator in `preview` mode and yield a `warn` outcome with structured skip evidence — you don't need to opt out of preview manually. Pass `previewSafe: true` only if your OPA topology guarantees deterministic, side-effect-free evaluation appropriate for preview.
170
+
171
+ ## What this package does NOT do
172
+
173
+ - It doesn't talk to your database. `buildInput` is yours.
174
+ - It doesn't run an OPA server. You bring an OPA sidecar (or remote service) and inject a client.
175
+ - It doesn't ship Rego policies. You author and bundle those.
176
+ - It doesn't (yet) capture `bundleRevision` / `decisionId`. The high-level OPA SDK discards those fields; capturing them requires using the SDK's low-level `OpaApiClient` directly. A future minor release will add a richer client interface for hosts that want that provenance.
177
+
178
+ ## License
179
+
180
+ MIT — see [LICENSE](./LICENSE).
package/dist/index.cjs ADDED
@@ -0,0 +1,242 @@
1
+ 'use strict';
2
+
3
+ var crypto = require('crypto');
4
+
5
+ // src/decision-schema.ts
6
+ var POLICY_RESULTS = ["pass", "warn", "block"];
7
+ function parseOpaDecision(raw) {
8
+ if (raw === null || typeof raw !== "object") {
9
+ return { ok: false, error: "decision must be an object" };
10
+ }
11
+ const d = raw;
12
+ if (typeof d.result !== "string" || !POLICY_RESULTS.includes(d.result)) {
13
+ return {
14
+ ok: false,
15
+ error: `result must be "pass" | "warn" | "block" (got ${JSON.stringify(d.result)})`
16
+ };
17
+ }
18
+ if (d.reason !== void 0 && typeof d.reason !== "string") {
19
+ return { ok: false, error: "reason must be a string when present" };
20
+ }
21
+ if (d.conditionResults !== void 0) {
22
+ if (!Array.isArray(d.conditionResults)) {
23
+ return { ok: false, error: "conditionResults must be an array when present" };
24
+ }
25
+ for (let i = 0; i < d.conditionResults.length; i += 1) {
26
+ const err = validateConditionResult(d.conditionResults[i], i);
27
+ if (err) return { ok: false, error: err };
28
+ }
29
+ }
30
+ if (d.factsUsed !== void 0 && !isPlainObject(d.factsUsed)) {
31
+ return { ok: false, error: "factsUsed must be an object when present" };
32
+ }
33
+ if (d.obligations !== void 0 && !isPlainObject(d.obligations)) {
34
+ return { ok: false, error: "obligations must be an object when present" };
35
+ }
36
+ return { ok: true, decision: d };
37
+ }
38
+ function validateConditionResult(value, index) {
39
+ if (!isPlainObject(value)) {
40
+ return `conditionResults[${index}] must be an object`;
41
+ }
42
+ const c = value;
43
+ if (typeof c.conditionId !== "string" || c.conditionId.length === 0) {
44
+ return `conditionResults[${index}].conditionId must be a non-empty string`;
45
+ }
46
+ if (typeof c.passed !== "boolean") {
47
+ return `conditionResults[${index}].passed must be a boolean`;
48
+ }
49
+ if (typeof c.result !== "string" || !POLICY_RESULTS.includes(c.result)) {
50
+ return `conditionResults[${index}].result must be "pass" | "warn" | "block"`;
51
+ }
52
+ if (c.reason !== void 0 && typeof c.reason !== "string") {
53
+ return `conditionResults[${index}].reason must be a string when present`;
54
+ }
55
+ return null;
56
+ }
57
+ function isPlainObject(value) {
58
+ return value !== null && typeof value === "object" && !Array.isArray(value);
59
+ }
60
+ function stableStringify(value) {
61
+ if (value === void 0) return "null";
62
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
63
+ if (Array.isArray(value)) {
64
+ return `[${value.map(stableStringify).join(",")}]`;
65
+ }
66
+ const obj = value;
67
+ const keys = Object.keys(obj).sort();
68
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
69
+ }
70
+ function defaultHashInput(input) {
71
+ const hex = crypto.createHash("sha256").update(stableStringify(input)).digest("hex");
72
+ return `sha256:${hex}`;
73
+ }
74
+
75
+ // src/factory.ts
76
+ var DEFAULT_SERVICE_KEY = "opa";
77
+ var ENGINE_NAME = "opa";
78
+ function createOpaPolicyEvaluator(options) {
79
+ const {
80
+ binding,
81
+ buildInput,
82
+ serviceKey = DEFAULT_SERVICE_KEY,
83
+ onError = "block",
84
+ previewSafe = false,
85
+ engineVersion,
86
+ mapDecision,
87
+ hashInput = defaultHashInput
88
+ } = options;
89
+ return {
90
+ policyId: binding.policyId,
91
+ version: binding.version,
92
+ previewSafe,
93
+ evaluate: async (ctx) => {
94
+ const client = ctx.services?.[serviceKey];
95
+ if (!client || typeof client.evaluate !== "function") {
96
+ return makeFailureOutcome(
97
+ binding,
98
+ engineVersion,
99
+ "client_unavailable",
100
+ `No OPA client at ctx.services["${serviceKey}"]`,
101
+ onError
102
+ );
103
+ }
104
+ let input;
105
+ try {
106
+ input = await buildInput(ctx);
107
+ } catch (err) {
108
+ return makeFailureOutcome(
109
+ binding,
110
+ engineVersion,
111
+ "input_build_failed",
112
+ `buildInput threw: ${formatError(err)}`,
113
+ onError
114
+ );
115
+ }
116
+ let inputHash;
117
+ try {
118
+ inputHash = hashInput(input);
119
+ } catch {
120
+ }
121
+ let rawResult;
122
+ try {
123
+ rawResult = await client.evaluate(binding.decisionPath, input);
124
+ } catch (err) {
125
+ return makeFailureOutcome(
126
+ binding,
127
+ engineVersion,
128
+ "evaluation_failed",
129
+ `OPA evaluation failed: ${formatError(err)}`,
130
+ onError,
131
+ inputHash
132
+ );
133
+ }
134
+ const parsed = parseOpaDecision(rawResult);
135
+ if (!parsed.ok) {
136
+ return makeFailureOutcome(
137
+ binding,
138
+ engineVersion,
139
+ "invalid_response",
140
+ `OPA returned malformed decision: ${parsed.error}`,
141
+ onError,
142
+ inputHash
143
+ );
144
+ }
145
+ return buildSuccessOutcome(
146
+ binding,
147
+ engineVersion,
148
+ parsed.decision,
149
+ inputHash,
150
+ ctx,
151
+ mapDecision
152
+ );
153
+ }
154
+ };
155
+ }
156
+ function buildSuccessOutcome(binding, engineVersion, decision, inputHash, ctx, mapDecision) {
157
+ const mapped = mapDecision ? mapDecision(decision, ctx) : { result: decision.result, reason: decision.reason };
158
+ return {
159
+ policyId: binding.policyId,
160
+ policyVersion: binding.version,
161
+ result: mapped.result,
162
+ reason: mapped.reason,
163
+ metadata: {
164
+ ...mapped.metadata ?? {},
165
+ opaDecision: decision
166
+ },
167
+ dispatchEvidence: {
168
+ policyKind: "code",
169
+ policyId: binding.policyId,
170
+ policyVersion: binding.version,
171
+ dispatchPath: ["code"],
172
+ engine: {
173
+ name: ENGINE_NAME,
174
+ version: engineVersion,
175
+ metadata: stripUndefined({
176
+ decisionPath: binding.decisionPath,
177
+ regoPackage: binding.regoPackage,
178
+ inputHash,
179
+ conditionResults: decision.conditionResults,
180
+ factsUsed: decision.factsUsed,
181
+ obligations: decision.obligations
182
+ })
183
+ }
184
+ }
185
+ };
186
+ }
187
+ function makeFailureOutcome(binding, engineVersion, error, reason, onError, inputHash) {
188
+ if (onError === "throw") {
189
+ const err = new Error(reason);
190
+ err.kind = error;
191
+ throw err;
192
+ }
193
+ return {
194
+ policyId: binding.policyId,
195
+ policyVersion: binding.version,
196
+ result: "block",
197
+ reason,
198
+ metadata: {
199
+ opaError: error
200
+ },
201
+ dispatchEvidence: {
202
+ policyKind: "code",
203
+ policyId: binding.policyId,
204
+ policyVersion: binding.version,
205
+ dispatchPath: ["code"],
206
+ engine: {
207
+ name: ENGINE_NAME,
208
+ version: engineVersion,
209
+ metadata: stripUndefined({
210
+ decisionPath: binding.decisionPath,
211
+ regoPackage: binding.regoPackage,
212
+ inputHash,
213
+ error,
214
+ errorReason: reason
215
+ })
216
+ }
217
+ }
218
+ };
219
+ }
220
+ function stripUndefined(obj) {
221
+ const out = {};
222
+ for (const [k, v] of Object.entries(obj)) {
223
+ if (v !== void 0) out[k] = v;
224
+ }
225
+ return out;
226
+ }
227
+ function formatError(err) {
228
+ if (err instanceof Error) return err.message;
229
+ if (typeof err === "string") return err;
230
+ try {
231
+ return JSON.stringify(err);
232
+ } catch {
233
+ return String(err);
234
+ }
235
+ }
236
+
237
+ exports.createOpaPolicyEvaluator = createOpaPolicyEvaluator;
238
+ exports.defaultHashInput = defaultHashInput;
239
+ exports.parseOpaDecision = parseOpaDecision;
240
+ exports.stableStringify = stableStringify;
241
+ //# sourceMappingURL=index.cjs.map
242
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/decision-schema.ts","../src/hash.ts","../src/factory.ts"],"names":["createHash"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { PolicyEvaluationResult } from \"@fabricorg/platform/policies\";\nimport type { OpaDecision } from \"./types\";\n\nconst POLICY_RESULTS: readonly PolicyEvaluationResult[] = [\"pass\", \"warn\", \"block\"];\n\nexport type OpaDecisionParseResult =\n\t| { ok: true; decision: OpaDecision }\n\t| { ok: false; error: string };\n\n/**\n * Hand-rolled validator for the Rego \"house style\" decision shape. Avoids a\n * runtime zod dependency to keep the adapter zero-dep.\n *\n * Accepts a raw OPA result and either returns the parsed `OpaDecision` or a\n * single human-readable error string describing the first violation. We don't\n * accumulate every error: when audit evidence has to record \"OPA returned\n * something invalid\", one precise reason is more useful than a list.\n */\nexport function parseOpaDecision(raw: unknown): OpaDecisionParseResult {\n\tif (raw === null || typeof raw !== \"object\") {\n\t\treturn { ok: false, error: \"decision must be an object\" };\n\t}\n\n\tconst d = raw as Record<string, unknown>;\n\n\tif (typeof d.result !== \"string\" || !POLICY_RESULTS.includes(d.result as PolicyEvaluationResult)) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\terror: `result must be \"pass\" | \"warn\" | \"block\" (got ${JSON.stringify(d.result)})`,\n\t\t};\n\t}\n\n\tif (d.reason !== undefined && typeof d.reason !== \"string\") {\n\t\treturn { ok: false, error: \"reason must be a string when present\" };\n\t}\n\n\tif (d.conditionResults !== undefined) {\n\t\tif (!Array.isArray(d.conditionResults)) {\n\t\t\treturn { ok: false, error: \"conditionResults must be an array when present\" };\n\t\t}\n\t\tfor (let i = 0; i < d.conditionResults.length; i += 1) {\n\t\t\tconst err = validateConditionResult(d.conditionResults[i], i);\n\t\t\tif (err) return { ok: false, error: err };\n\t\t}\n\t}\n\n\tif (d.factsUsed !== undefined && !isPlainObject(d.factsUsed)) {\n\t\treturn { ok: false, error: \"factsUsed must be an object when present\" };\n\t}\n\n\tif (d.obligations !== undefined && !isPlainObject(d.obligations)) {\n\t\treturn { ok: false, error: \"obligations must be an object when present\" };\n\t}\n\n\treturn { ok: true, decision: d as unknown as OpaDecision };\n}\n\nfunction validateConditionResult(value: unknown, index: number): string | null {\n\tif (!isPlainObject(value)) {\n\t\treturn `conditionResults[${index}] must be an object`;\n\t}\n\tconst c = value as Record<string, unknown>;\n\tif (typeof c.conditionId !== \"string\" || c.conditionId.length === 0) {\n\t\treturn `conditionResults[${index}].conditionId must be a non-empty string`;\n\t}\n\tif (typeof c.passed !== \"boolean\") {\n\t\treturn `conditionResults[${index}].passed must be a boolean`;\n\t}\n\tif (typeof c.result !== \"string\" || !POLICY_RESULTS.includes(c.result as PolicyEvaluationResult)) {\n\t\treturn `conditionResults[${index}].result must be \"pass\" | \"warn\" | \"block\"`;\n\t}\n\tif (c.reason !== undefined && typeof c.reason !== \"string\") {\n\t\treturn `conditionResults[${index}].reason must be a string when present`;\n\t}\n\treturn null;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\treturn value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n","import { createHash } from \"node:crypto\";\n\n/**\n * Stable JSON stringify — sorts object keys recursively so two\n * structurally-equal inputs produce the same string regardless of property\n * insertion order. Required for hash determinism across builds of the same\n * `buildInput` function.\n */\nexport function stableStringify(value: unknown): string {\n\tif (value === undefined) return \"null\";\n\tif (value === null || typeof value !== \"object\") return JSON.stringify(value);\n\tif (Array.isArray(value)) {\n\t\treturn `[${value.map(stableStringify).join(\",\")}]`;\n\t}\n\tconst obj = value as Record<string, unknown>;\n\tconst keys = Object.keys(obj).sort();\n\treturn `{${keys\n\t\t.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`)\n\t\t.join(\",\")}}`;\n}\n\n/**\n * Default input hasher: SHA-256 over a stable stringification.\n * Format: `sha256:<hex>` — namespaced so future migrations to a different\n * algorithm can be detected by a prefix check in evidence dashboards.\n */\nexport function defaultHashInput(input: Record<string, unknown>): string {\n\tconst hex = createHash(\"sha256\").update(stableStringify(input)).digest(\"hex\");\n\treturn `sha256:${hex}`;\n}\n","import type {\n\tPolicyContext,\n\tPolicyEvaluator,\n\tPolicyOutcome,\n} from \"@fabricorg/platform/policies\";\n\nimport { parseOpaDecision } from \"./decision-schema\";\nimport { defaultHashInput } from \"./hash\";\nimport type {\n\tCreateOpaPolicyEvaluatorOptions,\n\tOpaDecision,\n\tOpaPolicyClient,\n\tOpaPolicyFailureKind,\n} from \"./types\";\n\nconst DEFAULT_SERVICE_KEY = \"opa\";\nconst ENGINE_NAME = \"opa\";\n\n/**\n * Build a Fabric `PolicyEvaluator` that delegates decision-making to an OPA\n * (Open Policy Agent) deployment. The evaluator:\n *\n * 1. Reads the host-injected OPA client from `ctx.services[serviceKey]`.\n * 2. Builds a JSON input snapshot via `buildInput(ctx)` — domain code that\n * snapshots facts the engine needs. Fabric platform never lets OPA reach\n * into the database directly; all facts flow through this function.\n * 3. Evaluates the Rego decision at `binding.decisionPath`.\n * 4. Validates the response against the house-style shape (`OpaDecision`).\n * 5. Maps the decision to a `PolicyOutcome` and attaches engine provenance\n * (`engine.name = \"opa\"`, `decisionPath`, `inputHash`) for audit.\n *\n * Failures (missing client, build failure, network failure, malformed response)\n * are governed by `onError`. Default `\"block\"` honors Fabric's default-deny\n * posture and records the failure category under `engine.metadata.error`.\n */\nexport function createOpaPolicyEvaluator<TDb = unknown>(\n\toptions: CreateOpaPolicyEvaluatorOptions<TDb>,\n): PolicyEvaluator<TDb> {\n\tconst {\n\t\tbinding,\n\t\tbuildInput,\n\t\tserviceKey = DEFAULT_SERVICE_KEY,\n\t\tonError = \"block\",\n\t\tpreviewSafe = false,\n\t\tengineVersion,\n\t\tmapDecision,\n\t\thashInput = defaultHashInput,\n\t} = options;\n\n\treturn {\n\t\tpolicyId: binding.policyId,\n\t\tversion: binding.version,\n\t\tpreviewSafe,\n\t\tevaluate: async (ctx) => {\n\t\t\tconst client = ctx.services?.[serviceKey] as OpaPolicyClient | undefined;\n\t\t\tif (!client || typeof client.evaluate !== \"function\") {\n\t\t\t\treturn makeFailureOutcome(\n\t\t\t\t\tbinding,\n\t\t\t\t\tengineVersion,\n\t\t\t\t\t\"client_unavailable\",\n\t\t\t\t\t`No OPA client at ctx.services[\"${serviceKey}\"]`,\n\t\t\t\t\tonError,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tlet input: Record<string, unknown>;\n\t\t\ttry {\n\t\t\t\tinput = await buildInput(ctx);\n\t\t\t} catch (err) {\n\t\t\t\treturn makeFailureOutcome(\n\t\t\t\t\tbinding,\n\t\t\t\t\tengineVersion,\n\t\t\t\t\t\"input_build_failed\",\n\t\t\t\t\t`buildInput threw: ${formatError(err)}`,\n\t\t\t\t\tonError,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tlet inputHash: string | undefined;\n\t\t\ttry {\n\t\t\t\tinputHash = hashInput(input);\n\t\t\t} catch {\n\t\t\t\t// hashing is best-effort — if a custom hasher throws, omit the\n\t\t\t\t// field rather than fail the policy.\n\t\t\t}\n\n\t\t\tlet rawResult: unknown;\n\t\t\ttry {\n\t\t\t\trawResult = await client.evaluate(binding.decisionPath, input);\n\t\t\t} catch (err) {\n\t\t\t\treturn makeFailureOutcome(\n\t\t\t\t\tbinding,\n\t\t\t\t\tengineVersion,\n\t\t\t\t\t\"evaluation_failed\",\n\t\t\t\t\t`OPA evaluation failed: ${formatError(err)}`,\n\t\t\t\t\tonError,\n\t\t\t\t\tinputHash,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst parsed = parseOpaDecision(rawResult);\n\t\t\tif (!parsed.ok) {\n\t\t\t\treturn makeFailureOutcome(\n\t\t\t\t\tbinding,\n\t\t\t\t\tengineVersion,\n\t\t\t\t\t\"invalid_response\",\n\t\t\t\t\t`OPA returned malformed decision: ${parsed.error}`,\n\t\t\t\t\tonError,\n\t\t\t\t\tinputHash,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn buildSuccessOutcome(\n\t\t\t\tbinding,\n\t\t\t\tengineVersion,\n\t\t\t\tparsed.decision,\n\t\t\t\tinputHash,\n\t\t\t\tctx,\n\t\t\t\tmapDecision,\n\t\t\t);\n\t\t},\n\t};\n}\n\nfunction buildSuccessOutcome<TDb>(\n\tbinding: CreateOpaPolicyEvaluatorOptions<TDb>[\"binding\"],\n\tengineVersion: string | undefined,\n\tdecision: OpaDecision,\n\tinputHash: string | undefined,\n\tctx: PolicyContext<TDb>,\n\tmapDecision: CreateOpaPolicyEvaluatorOptions<TDb>[\"mapDecision\"],\n): PolicyOutcome {\n\tconst mapped = mapDecision\n\t\t? mapDecision(decision, ctx)\n\t\t: { result: decision.result, reason: decision.reason };\n\n\treturn {\n\t\tpolicyId: binding.policyId,\n\t\tpolicyVersion: binding.version,\n\t\tresult: mapped.result,\n\t\treason: mapped.reason,\n\t\tmetadata: {\n\t\t\t...(mapped.metadata ?? {}),\n\t\t\topaDecision: decision,\n\t\t},\n\t\tdispatchEvidence: {\n\t\t\tpolicyKind: \"code\",\n\t\t\tpolicyId: binding.policyId,\n\t\t\tpolicyVersion: binding.version,\n\t\t\tdispatchPath: [\"code\"],\n\t\t\tengine: {\n\t\t\t\tname: ENGINE_NAME,\n\t\t\t\tversion: engineVersion,\n\t\t\t\tmetadata: stripUndefined({\n\t\t\t\t\tdecisionPath: binding.decisionPath,\n\t\t\t\t\tregoPackage: binding.regoPackage,\n\t\t\t\t\tinputHash,\n\t\t\t\t\tconditionResults: decision.conditionResults,\n\t\t\t\t\tfactsUsed: decision.factsUsed,\n\t\t\t\t\tobligations: decision.obligations,\n\t\t\t\t}),\n\t\t\t},\n\t\t},\n\t};\n}\n\nfunction makeFailureOutcome(\n\tbinding: CreateOpaPolicyEvaluatorOptions[\"binding\"],\n\tengineVersion: string | undefined,\n\terror: OpaPolicyFailureKind,\n\treason: string,\n\tonError: \"block\" | \"throw\",\n\tinputHash?: string,\n): PolicyOutcome {\n\tif (onError === \"throw\") {\n\t\tconst err = new Error(reason);\n\t\t(err as Error & { kind?: OpaPolicyFailureKind }).kind = error;\n\t\tthrow err;\n\t}\n\n\treturn {\n\t\tpolicyId: binding.policyId,\n\t\tpolicyVersion: binding.version,\n\t\tresult: \"block\",\n\t\treason,\n\t\tmetadata: {\n\t\t\topaError: error,\n\t\t},\n\t\tdispatchEvidence: {\n\t\t\tpolicyKind: \"code\",\n\t\t\tpolicyId: binding.policyId,\n\t\t\tpolicyVersion: binding.version,\n\t\t\tdispatchPath: [\"code\"],\n\t\t\tengine: {\n\t\t\t\tname: ENGINE_NAME,\n\t\t\t\tversion: engineVersion,\n\t\t\t\tmetadata: stripUndefined({\n\t\t\t\t\tdecisionPath: binding.decisionPath,\n\t\t\t\t\tregoPackage: binding.regoPackage,\n\t\t\t\t\tinputHash,\n\t\t\t\t\terror,\n\t\t\t\t\terrorReason: reason,\n\t\t\t\t}),\n\t\t\t},\n\t\t},\n\t};\n}\n\nfunction stripUndefined(obj: Record<string, unknown>): Record<string, unknown> {\n\tconst out: Record<string, unknown> = {};\n\tfor (const [k, v] of Object.entries(obj)) {\n\t\tif (v !== undefined) out[k] = v;\n\t}\n\treturn out;\n}\n\nfunction formatError(err: unknown): string {\n\tif (err instanceof Error) return err.message;\n\tif (typeof err === \"string\") return err;\n\ttry {\n\t\treturn JSON.stringify(err);\n\t} catch {\n\t\treturn String(err);\n\t}\n}\n"]}
@@ -0,0 +1,163 @@
1
+ import { PolicyId, PolicyContext, PolicyEvaluationResult, PolicyOutcome, PolicyEvaluator } from '@fabricorg/platform/policies';
2
+
3
+ /**
4
+ * Binding between a Fabric platform policy and an OPA decision. Verticals
5
+ * declare these in a manifest (e.g. `LENDING_POLICY_BINDINGS`) so the same
6
+ * IDs, version, and decision path are referenced consistently by the TypeScript
7
+ * registration, the Rego package, and audit evidence.
8
+ */
9
+ interface OpaPolicyBinding {
10
+ policyId: PolicyId;
11
+ version: number;
12
+ /**
13
+ * Path to the Rego decision rule, without `/v1/data/` prefix.
14
+ *
15
+ * Example: `"fabric/lending/tcpa/contact_window/decision"`.
16
+ *
17
+ * Stored verbatim in `PolicyDispatchEvidence.engine.metadata.decisionPath`
18
+ * so auditors can trace a decision back to its Rego artifact.
19
+ */
20
+ decisionPath: string;
21
+ /**
22
+ * Documentation-only: the Rego package name, e.g.
23
+ * `"fabric.lending.tcpa.contact_window"`. The adapter does not consume this
24
+ * — it is part of the manifest contract to help Rego authors and reviewers
25
+ * keep the package name aligned with the decision path.
26
+ */
27
+ regoPackage?: string;
28
+ }
29
+ /**
30
+ * Structured Rego decision shape — the "house style" the adapter expects from
31
+ * any Rego policy. Wrap policy logic so it returns a typed object rather than
32
+ * a bare boolean: the per-condition shape preserves the same evidence
33
+ * granularity Fabric's built-in data policies provide.
34
+ */
35
+ interface OpaDecision {
36
+ result: PolicyEvaluationResult;
37
+ reason?: string;
38
+ conditionResults?: Array<{
39
+ conditionId: string;
40
+ passed: boolean;
41
+ result: PolicyEvaluationResult;
42
+ reason?: string;
43
+ }>;
44
+ factsUsed?: Record<string, unknown>;
45
+ obligations?: Record<string, unknown>;
46
+ }
47
+ /**
48
+ * Minimal structurally-typed OPA client. Compatible with `OPAClient` from
49
+ * `@open-policy-agent/opa` (whose `evaluate(path, input, opts?)` signature
50
+ * widens to this interface); advanced users can supply their own wrapper that
51
+ * captures `decisionId` / `bundleRevision` from the low-level SDK.
52
+ */
53
+ interface OpaPolicyClient {
54
+ evaluate<T = unknown>(path: string, input: unknown): Promise<T>;
55
+ }
56
+ type OpaPolicyErrorMode = "block" | "throw";
57
+ /**
58
+ * Failure category surfaced in `engine.metadata.error` when `onError: "block"`
59
+ * is used. Lets dashboards distinguish "OPA is down" from "OPA returned
60
+ * something we couldn't parse" from "no client was injected at runtime".
61
+ */
62
+ 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"> & {
64
+ metadata?: Record<string, unknown>;
65
+ };
66
+ interface CreateOpaPolicyEvaluatorOptions<TDb = unknown> {
67
+ binding: OpaPolicyBinding;
68
+ buildInput: (ctx: PolicyContext<TDb>) => Promise<Record<string, unknown>> | Record<string, unknown>;
69
+ /**
70
+ * Key under `ctx.services` where the host has placed an `OpaPolicyClient`.
71
+ * Defaults to `"opa"`. The adapter reads it as
72
+ * `ctx.services?.[serviceKey]`; if absent, the configured `onError` policy
73
+ * applies.
74
+ */
75
+ serviceKey?: string;
76
+ /**
77
+ * What to do when the OPA call fails, the client is missing, or the
78
+ * response is malformed. Defaults to `"block"` — matches Fabric's
79
+ * default-deny posture. With `"throw"`, the error propagates and Fabric's
80
+ * runtime surfaces it as a `failed` invocation (not `blocked_by_policy`).
81
+ */
82
+ onError?: OpaPolicyErrorMode;
83
+ /**
84
+ * Whether this evaluator is safe to run in `preview` mode. Defaults to
85
+ * `false` because OPA evaluation is a network call and not preview-safe by
86
+ * default. Set `true` only if the host's OPA topology guarantees
87
+ * deterministic, side-effect-free evaluation suitable for preview.
88
+ */
89
+ previewSafe?: boolean;
90
+ /**
91
+ * Optional override for `engine.version` in dispatch evidence. The
92
+ * high-level OPA SDK does not surface OPA's server version; if the host
93
+ * knows it (e.g. from sidecar metadata), it can be passed here so audit
94
+ * evidence captures "which OPA server made this decision".
95
+ */
96
+ engineVersion?: string;
97
+ /**
98
+ * Custom mapper from a validated `OpaDecision` to a `PolicyOutcome`-shaped
99
+ * partial. Defaults to identity (`result` and `reason` copied through).
100
+ * Use this to translate Rego idioms into Fabric's tri-state or to enrich
101
+ * `metadata`.
102
+ */
103
+ mapDecision?: OpaPolicyDecisionMapper<TDb>;
104
+ /**
105
+ * Custom input hasher. Defaults to SHA-256 over a stable JSON
106
+ * stringification (sorted keys). Override for FIPS environments or to use
107
+ * Web Crypto on runtimes without `node:crypto`.
108
+ */
109
+ hashInput?: (input: Record<string, unknown>) => string;
110
+ }
111
+
112
+ /**
113
+ * Build a Fabric `PolicyEvaluator` that delegates decision-making to an OPA
114
+ * (Open Policy Agent) deployment. The evaluator:
115
+ *
116
+ * 1. Reads the host-injected OPA client from `ctx.services[serviceKey]`.
117
+ * 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`.
121
+ * 4. Validates the response against the house-style shape (`OpaDecision`).
122
+ * 5. Maps the decision to a `PolicyOutcome` and attaches engine provenance
123
+ * (`engine.name = "opa"`, `decisionPath`, `inputHash`) for audit.
124
+ *
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`.
128
+ */
129
+ declare function createOpaPolicyEvaluator<TDb = unknown>(options: CreateOpaPolicyEvaluatorOptions<TDb>): PolicyEvaluator<TDb>;
130
+
131
+ type OpaDecisionParseResult = {
132
+ ok: true;
133
+ decision: OpaDecision;
134
+ } | {
135
+ ok: false;
136
+ error: string;
137
+ };
138
+ /**
139
+ * Hand-rolled validator for the Rego "house style" decision shape. Avoids a
140
+ * runtime zod dependency to keep the adapter zero-dep.
141
+ *
142
+ * Accepts a raw OPA result and either returns the parsed `OpaDecision` or a
143
+ * single human-readable error string describing the first violation. We don't
144
+ * accumulate every error: when audit evidence has to record "OPA returned
145
+ * something invalid", one precise reason is more useful than a list.
146
+ */
147
+ declare function parseOpaDecision(raw: unknown): OpaDecisionParseResult;
148
+
149
+ /**
150
+ * Stable JSON stringify — sorts object keys recursively so two
151
+ * structurally-equal inputs produce the same string regardless of property
152
+ * insertion order. Required for hash determinism across builds of the same
153
+ * `buildInput` function.
154
+ */
155
+ declare function stableStringify(value: unknown): string;
156
+ /**
157
+ * Default input hasher: SHA-256 over a stable stringification.
158
+ * Format: `sha256:<hex>` — namespaced so future migrations to a different
159
+ * algorithm can be detected by a prefix check in evidence dashboards.
160
+ */
161
+ declare function defaultHashInput(input: Record<string, unknown>): string;
162
+
163
+ export { type CreateOpaPolicyEvaluatorOptions, type OpaDecision, type OpaDecisionParseResult, type OpaPolicyBinding, type OpaPolicyClient, type OpaPolicyDecisionMapper, type OpaPolicyErrorMode, type OpaPolicyFailureKind, createOpaPolicyEvaluator, defaultHashInput, parseOpaDecision, stableStringify };
@@ -0,0 +1,163 @@
1
+ import { PolicyId, PolicyContext, PolicyEvaluationResult, PolicyOutcome, PolicyEvaluator } from '@fabricorg/platform/policies';
2
+
3
+ /**
4
+ * Binding between a Fabric platform policy and an OPA decision. Verticals
5
+ * declare these in a manifest (e.g. `LENDING_POLICY_BINDINGS`) so the same
6
+ * IDs, version, and decision path are referenced consistently by the TypeScript
7
+ * registration, the Rego package, and audit evidence.
8
+ */
9
+ interface OpaPolicyBinding {
10
+ policyId: PolicyId;
11
+ version: number;
12
+ /**
13
+ * Path to the Rego decision rule, without `/v1/data/` prefix.
14
+ *
15
+ * Example: `"fabric/lending/tcpa/contact_window/decision"`.
16
+ *
17
+ * Stored verbatim in `PolicyDispatchEvidence.engine.metadata.decisionPath`
18
+ * so auditors can trace a decision back to its Rego artifact.
19
+ */
20
+ decisionPath: string;
21
+ /**
22
+ * Documentation-only: the Rego package name, e.g.
23
+ * `"fabric.lending.tcpa.contact_window"`. The adapter does not consume this
24
+ * — it is part of the manifest contract to help Rego authors and reviewers
25
+ * keep the package name aligned with the decision path.
26
+ */
27
+ regoPackage?: string;
28
+ }
29
+ /**
30
+ * Structured Rego decision shape — the "house style" the adapter expects from
31
+ * any Rego policy. Wrap policy logic so it returns a typed object rather than
32
+ * a bare boolean: the per-condition shape preserves the same evidence
33
+ * granularity Fabric's built-in data policies provide.
34
+ */
35
+ interface OpaDecision {
36
+ result: PolicyEvaluationResult;
37
+ reason?: string;
38
+ conditionResults?: Array<{
39
+ conditionId: string;
40
+ passed: boolean;
41
+ result: PolicyEvaluationResult;
42
+ reason?: string;
43
+ }>;
44
+ factsUsed?: Record<string, unknown>;
45
+ obligations?: Record<string, unknown>;
46
+ }
47
+ /**
48
+ * Minimal structurally-typed OPA client. Compatible with `OPAClient` from
49
+ * `@open-policy-agent/opa` (whose `evaluate(path, input, opts?)` signature
50
+ * widens to this interface); advanced users can supply their own wrapper that
51
+ * captures `decisionId` / `bundleRevision` from the low-level SDK.
52
+ */
53
+ interface OpaPolicyClient {
54
+ evaluate<T = unknown>(path: string, input: unknown): Promise<T>;
55
+ }
56
+ type OpaPolicyErrorMode = "block" | "throw";
57
+ /**
58
+ * Failure category surfaced in `engine.metadata.error` when `onError: "block"`
59
+ * is used. Lets dashboards distinguish "OPA is down" from "OPA returned
60
+ * something we couldn't parse" from "no client was injected at runtime".
61
+ */
62
+ 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"> & {
64
+ metadata?: Record<string, unknown>;
65
+ };
66
+ interface CreateOpaPolicyEvaluatorOptions<TDb = unknown> {
67
+ binding: OpaPolicyBinding;
68
+ buildInput: (ctx: PolicyContext<TDb>) => Promise<Record<string, unknown>> | Record<string, unknown>;
69
+ /**
70
+ * Key under `ctx.services` where the host has placed an `OpaPolicyClient`.
71
+ * Defaults to `"opa"`. The adapter reads it as
72
+ * `ctx.services?.[serviceKey]`; if absent, the configured `onError` policy
73
+ * applies.
74
+ */
75
+ serviceKey?: string;
76
+ /**
77
+ * What to do when the OPA call fails, the client is missing, or the
78
+ * response is malformed. Defaults to `"block"` — matches Fabric's
79
+ * default-deny posture. With `"throw"`, the error propagates and Fabric's
80
+ * runtime surfaces it as a `failed` invocation (not `blocked_by_policy`).
81
+ */
82
+ onError?: OpaPolicyErrorMode;
83
+ /**
84
+ * Whether this evaluator is safe to run in `preview` mode. Defaults to
85
+ * `false` because OPA evaluation is a network call and not preview-safe by
86
+ * default. Set `true` only if the host's OPA topology guarantees
87
+ * deterministic, side-effect-free evaluation suitable for preview.
88
+ */
89
+ previewSafe?: boolean;
90
+ /**
91
+ * Optional override for `engine.version` in dispatch evidence. The
92
+ * high-level OPA SDK does not surface OPA's server version; if the host
93
+ * knows it (e.g. from sidecar metadata), it can be passed here so audit
94
+ * evidence captures "which OPA server made this decision".
95
+ */
96
+ engineVersion?: string;
97
+ /**
98
+ * Custom mapper from a validated `OpaDecision` to a `PolicyOutcome`-shaped
99
+ * partial. Defaults to identity (`result` and `reason` copied through).
100
+ * Use this to translate Rego idioms into Fabric's tri-state or to enrich
101
+ * `metadata`.
102
+ */
103
+ mapDecision?: OpaPolicyDecisionMapper<TDb>;
104
+ /**
105
+ * Custom input hasher. Defaults to SHA-256 over a stable JSON
106
+ * stringification (sorted keys). Override for FIPS environments or to use
107
+ * Web Crypto on runtimes without `node:crypto`.
108
+ */
109
+ hashInput?: (input: Record<string, unknown>) => string;
110
+ }
111
+
112
+ /**
113
+ * Build a Fabric `PolicyEvaluator` that delegates decision-making to an OPA
114
+ * (Open Policy Agent) deployment. The evaluator:
115
+ *
116
+ * 1. Reads the host-injected OPA client from `ctx.services[serviceKey]`.
117
+ * 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`.
121
+ * 4. Validates the response against the house-style shape (`OpaDecision`).
122
+ * 5. Maps the decision to a `PolicyOutcome` and attaches engine provenance
123
+ * (`engine.name = "opa"`, `decisionPath`, `inputHash`) for audit.
124
+ *
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`.
128
+ */
129
+ declare function createOpaPolicyEvaluator<TDb = unknown>(options: CreateOpaPolicyEvaluatorOptions<TDb>): PolicyEvaluator<TDb>;
130
+
131
+ type OpaDecisionParseResult = {
132
+ ok: true;
133
+ decision: OpaDecision;
134
+ } | {
135
+ ok: false;
136
+ error: string;
137
+ };
138
+ /**
139
+ * Hand-rolled validator for the Rego "house style" decision shape. Avoids a
140
+ * runtime zod dependency to keep the adapter zero-dep.
141
+ *
142
+ * Accepts a raw OPA result and either returns the parsed `OpaDecision` or a
143
+ * single human-readable error string describing the first violation. We don't
144
+ * accumulate every error: when audit evidence has to record "OPA returned
145
+ * something invalid", one precise reason is more useful than a list.
146
+ */
147
+ declare function parseOpaDecision(raw: unknown): OpaDecisionParseResult;
148
+
149
+ /**
150
+ * Stable JSON stringify — sorts object keys recursively so two
151
+ * structurally-equal inputs produce the same string regardless of property
152
+ * insertion order. Required for hash determinism across builds of the same
153
+ * `buildInput` function.
154
+ */
155
+ declare function stableStringify(value: unknown): string;
156
+ /**
157
+ * Default input hasher: SHA-256 over a stable stringification.
158
+ * Format: `sha256:<hex>` — namespaced so future migrations to a different
159
+ * algorithm can be detected by a prefix check in evidence dashboards.
160
+ */
161
+ declare function defaultHashInput(input: Record<string, unknown>): string;
162
+
163
+ export { type CreateOpaPolicyEvaluatorOptions, type OpaDecision, type OpaDecisionParseResult, type OpaPolicyBinding, type OpaPolicyClient, type OpaPolicyDecisionMapper, type OpaPolicyErrorMode, type OpaPolicyFailureKind, createOpaPolicyEvaluator, defaultHashInput, parseOpaDecision, stableStringify };
package/dist/index.js ADDED
@@ -0,0 +1,237 @@
1
+ import { createHash } from 'crypto';
2
+
3
+ // src/decision-schema.ts
4
+ var POLICY_RESULTS = ["pass", "warn", "block"];
5
+ function parseOpaDecision(raw) {
6
+ if (raw === null || typeof raw !== "object") {
7
+ return { ok: false, error: "decision must be an object" };
8
+ }
9
+ const d = raw;
10
+ if (typeof d.result !== "string" || !POLICY_RESULTS.includes(d.result)) {
11
+ return {
12
+ ok: false,
13
+ error: `result must be "pass" | "warn" | "block" (got ${JSON.stringify(d.result)})`
14
+ };
15
+ }
16
+ if (d.reason !== void 0 && typeof d.reason !== "string") {
17
+ return { ok: false, error: "reason must be a string when present" };
18
+ }
19
+ if (d.conditionResults !== void 0) {
20
+ if (!Array.isArray(d.conditionResults)) {
21
+ return { ok: false, error: "conditionResults must be an array when present" };
22
+ }
23
+ for (let i = 0; i < d.conditionResults.length; i += 1) {
24
+ const err = validateConditionResult(d.conditionResults[i], i);
25
+ if (err) return { ok: false, error: err };
26
+ }
27
+ }
28
+ if (d.factsUsed !== void 0 && !isPlainObject(d.factsUsed)) {
29
+ return { ok: false, error: "factsUsed must be an object when present" };
30
+ }
31
+ if (d.obligations !== void 0 && !isPlainObject(d.obligations)) {
32
+ return { ok: false, error: "obligations must be an object when present" };
33
+ }
34
+ return { ok: true, decision: d };
35
+ }
36
+ function validateConditionResult(value, index) {
37
+ if (!isPlainObject(value)) {
38
+ return `conditionResults[${index}] must be an object`;
39
+ }
40
+ const c = value;
41
+ if (typeof c.conditionId !== "string" || c.conditionId.length === 0) {
42
+ return `conditionResults[${index}].conditionId must be a non-empty string`;
43
+ }
44
+ if (typeof c.passed !== "boolean") {
45
+ return `conditionResults[${index}].passed must be a boolean`;
46
+ }
47
+ if (typeof c.result !== "string" || !POLICY_RESULTS.includes(c.result)) {
48
+ return `conditionResults[${index}].result must be "pass" | "warn" | "block"`;
49
+ }
50
+ if (c.reason !== void 0 && typeof c.reason !== "string") {
51
+ return `conditionResults[${index}].reason must be a string when present`;
52
+ }
53
+ return null;
54
+ }
55
+ function isPlainObject(value) {
56
+ return value !== null && typeof value === "object" && !Array.isArray(value);
57
+ }
58
+ function stableStringify(value) {
59
+ if (value === void 0) return "null";
60
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
61
+ if (Array.isArray(value)) {
62
+ return `[${value.map(stableStringify).join(",")}]`;
63
+ }
64
+ const obj = value;
65
+ const keys = Object.keys(obj).sort();
66
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
67
+ }
68
+ function defaultHashInput(input) {
69
+ const hex = createHash("sha256").update(stableStringify(input)).digest("hex");
70
+ return `sha256:${hex}`;
71
+ }
72
+
73
+ // src/factory.ts
74
+ var DEFAULT_SERVICE_KEY = "opa";
75
+ var ENGINE_NAME = "opa";
76
+ function createOpaPolicyEvaluator(options) {
77
+ const {
78
+ binding,
79
+ buildInput,
80
+ serviceKey = DEFAULT_SERVICE_KEY,
81
+ onError = "block",
82
+ previewSafe = false,
83
+ engineVersion,
84
+ mapDecision,
85
+ hashInput = defaultHashInput
86
+ } = options;
87
+ return {
88
+ policyId: binding.policyId,
89
+ version: binding.version,
90
+ previewSafe,
91
+ evaluate: async (ctx) => {
92
+ const client = ctx.services?.[serviceKey];
93
+ if (!client || typeof client.evaluate !== "function") {
94
+ return makeFailureOutcome(
95
+ binding,
96
+ engineVersion,
97
+ "client_unavailable",
98
+ `No OPA client at ctx.services["${serviceKey}"]`,
99
+ onError
100
+ );
101
+ }
102
+ let input;
103
+ try {
104
+ input = await buildInput(ctx);
105
+ } catch (err) {
106
+ return makeFailureOutcome(
107
+ binding,
108
+ engineVersion,
109
+ "input_build_failed",
110
+ `buildInput threw: ${formatError(err)}`,
111
+ onError
112
+ );
113
+ }
114
+ let inputHash;
115
+ try {
116
+ inputHash = hashInput(input);
117
+ } catch {
118
+ }
119
+ let rawResult;
120
+ try {
121
+ rawResult = await client.evaluate(binding.decisionPath, input);
122
+ } catch (err) {
123
+ return makeFailureOutcome(
124
+ binding,
125
+ engineVersion,
126
+ "evaluation_failed",
127
+ `OPA evaluation failed: ${formatError(err)}`,
128
+ onError,
129
+ inputHash
130
+ );
131
+ }
132
+ const parsed = parseOpaDecision(rawResult);
133
+ if (!parsed.ok) {
134
+ return makeFailureOutcome(
135
+ binding,
136
+ engineVersion,
137
+ "invalid_response",
138
+ `OPA returned malformed decision: ${parsed.error}`,
139
+ onError,
140
+ inputHash
141
+ );
142
+ }
143
+ return buildSuccessOutcome(
144
+ binding,
145
+ engineVersion,
146
+ parsed.decision,
147
+ inputHash,
148
+ ctx,
149
+ mapDecision
150
+ );
151
+ }
152
+ };
153
+ }
154
+ function buildSuccessOutcome(binding, engineVersion, decision, inputHash, ctx, mapDecision) {
155
+ const mapped = mapDecision ? mapDecision(decision, ctx) : { result: decision.result, reason: decision.reason };
156
+ return {
157
+ policyId: binding.policyId,
158
+ policyVersion: binding.version,
159
+ result: mapped.result,
160
+ reason: mapped.reason,
161
+ metadata: {
162
+ ...mapped.metadata ?? {},
163
+ opaDecision: decision
164
+ },
165
+ dispatchEvidence: {
166
+ policyKind: "code",
167
+ policyId: binding.policyId,
168
+ policyVersion: binding.version,
169
+ dispatchPath: ["code"],
170
+ engine: {
171
+ name: ENGINE_NAME,
172
+ version: engineVersion,
173
+ metadata: stripUndefined({
174
+ decisionPath: binding.decisionPath,
175
+ regoPackage: binding.regoPackage,
176
+ inputHash,
177
+ conditionResults: decision.conditionResults,
178
+ factsUsed: decision.factsUsed,
179
+ obligations: decision.obligations
180
+ })
181
+ }
182
+ }
183
+ };
184
+ }
185
+ function makeFailureOutcome(binding, engineVersion, error, reason, onError, inputHash) {
186
+ if (onError === "throw") {
187
+ const err = new Error(reason);
188
+ err.kind = error;
189
+ throw err;
190
+ }
191
+ return {
192
+ policyId: binding.policyId,
193
+ policyVersion: binding.version,
194
+ result: "block",
195
+ reason,
196
+ metadata: {
197
+ opaError: error
198
+ },
199
+ dispatchEvidence: {
200
+ policyKind: "code",
201
+ policyId: binding.policyId,
202
+ policyVersion: binding.version,
203
+ dispatchPath: ["code"],
204
+ engine: {
205
+ name: ENGINE_NAME,
206
+ version: engineVersion,
207
+ metadata: stripUndefined({
208
+ decisionPath: binding.decisionPath,
209
+ regoPackage: binding.regoPackage,
210
+ inputHash,
211
+ error,
212
+ errorReason: reason
213
+ })
214
+ }
215
+ }
216
+ };
217
+ }
218
+ function stripUndefined(obj) {
219
+ const out = {};
220
+ for (const [k, v] of Object.entries(obj)) {
221
+ if (v !== void 0) out[k] = v;
222
+ }
223
+ return out;
224
+ }
225
+ function formatError(err) {
226
+ if (err instanceof Error) return err.message;
227
+ if (typeof err === "string") return err;
228
+ try {
229
+ return JSON.stringify(err);
230
+ } catch {
231
+ return String(err);
232
+ }
233
+ }
234
+
235
+ export { createOpaPolicyEvaluator, defaultHashInput, parseOpaDecision, stableStringify };
236
+ //# sourceMappingURL=index.js.map
237
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/decision-schema.ts","../src/hash.ts","../src/factory.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { PolicyEvaluationResult } from \"@fabricorg/platform/policies\";\nimport type { OpaDecision } from \"./types\";\n\nconst POLICY_RESULTS: readonly PolicyEvaluationResult[] = [\"pass\", \"warn\", \"block\"];\n\nexport type OpaDecisionParseResult =\n\t| { ok: true; decision: OpaDecision }\n\t| { ok: false; error: string };\n\n/**\n * Hand-rolled validator for the Rego \"house style\" decision shape. Avoids a\n * runtime zod dependency to keep the adapter zero-dep.\n *\n * Accepts a raw OPA result and either returns the parsed `OpaDecision` or a\n * single human-readable error string describing the first violation. We don't\n * accumulate every error: when audit evidence has to record \"OPA returned\n * something invalid\", one precise reason is more useful than a list.\n */\nexport function parseOpaDecision(raw: unknown): OpaDecisionParseResult {\n\tif (raw === null || typeof raw !== \"object\") {\n\t\treturn { ok: false, error: \"decision must be an object\" };\n\t}\n\n\tconst d = raw as Record<string, unknown>;\n\n\tif (typeof d.result !== \"string\" || !POLICY_RESULTS.includes(d.result as PolicyEvaluationResult)) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\terror: `result must be \"pass\" | \"warn\" | \"block\" (got ${JSON.stringify(d.result)})`,\n\t\t};\n\t}\n\n\tif (d.reason !== undefined && typeof d.reason !== \"string\") {\n\t\treturn { ok: false, error: \"reason must be a string when present\" };\n\t}\n\n\tif (d.conditionResults !== undefined) {\n\t\tif (!Array.isArray(d.conditionResults)) {\n\t\t\treturn { ok: false, error: \"conditionResults must be an array when present\" };\n\t\t}\n\t\tfor (let i = 0; i < d.conditionResults.length; i += 1) {\n\t\t\tconst err = validateConditionResult(d.conditionResults[i], i);\n\t\t\tif (err) return { ok: false, error: err };\n\t\t}\n\t}\n\n\tif (d.factsUsed !== undefined && !isPlainObject(d.factsUsed)) {\n\t\treturn { ok: false, error: \"factsUsed must be an object when present\" };\n\t}\n\n\tif (d.obligations !== undefined && !isPlainObject(d.obligations)) {\n\t\treturn { ok: false, error: \"obligations must be an object when present\" };\n\t}\n\n\treturn { ok: true, decision: d as unknown as OpaDecision };\n}\n\nfunction validateConditionResult(value: unknown, index: number): string | null {\n\tif (!isPlainObject(value)) {\n\t\treturn `conditionResults[${index}] must be an object`;\n\t}\n\tconst c = value as Record<string, unknown>;\n\tif (typeof c.conditionId !== \"string\" || c.conditionId.length === 0) {\n\t\treturn `conditionResults[${index}].conditionId must be a non-empty string`;\n\t}\n\tif (typeof c.passed !== \"boolean\") {\n\t\treturn `conditionResults[${index}].passed must be a boolean`;\n\t}\n\tif (typeof c.result !== \"string\" || !POLICY_RESULTS.includes(c.result as PolicyEvaluationResult)) {\n\t\treturn `conditionResults[${index}].result must be \"pass\" | \"warn\" | \"block\"`;\n\t}\n\tif (c.reason !== undefined && typeof c.reason !== \"string\") {\n\t\treturn `conditionResults[${index}].reason must be a string when present`;\n\t}\n\treturn null;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\treturn value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n","import { createHash } from \"node:crypto\";\n\n/**\n * Stable JSON stringify — sorts object keys recursively so two\n * structurally-equal inputs produce the same string regardless of property\n * insertion order. Required for hash determinism across builds of the same\n * `buildInput` function.\n */\nexport function stableStringify(value: unknown): string {\n\tif (value === undefined) return \"null\";\n\tif (value === null || typeof value !== \"object\") return JSON.stringify(value);\n\tif (Array.isArray(value)) {\n\t\treturn `[${value.map(stableStringify).join(\",\")}]`;\n\t}\n\tconst obj = value as Record<string, unknown>;\n\tconst keys = Object.keys(obj).sort();\n\treturn `{${keys\n\t\t.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`)\n\t\t.join(\",\")}}`;\n}\n\n/**\n * Default input hasher: SHA-256 over a stable stringification.\n * Format: `sha256:<hex>` — namespaced so future migrations to a different\n * algorithm can be detected by a prefix check in evidence dashboards.\n */\nexport function defaultHashInput(input: Record<string, unknown>): string {\n\tconst hex = createHash(\"sha256\").update(stableStringify(input)).digest(\"hex\");\n\treturn `sha256:${hex}`;\n}\n","import type {\n\tPolicyContext,\n\tPolicyEvaluator,\n\tPolicyOutcome,\n} from \"@fabricorg/platform/policies\";\n\nimport { parseOpaDecision } from \"./decision-schema\";\nimport { defaultHashInput } from \"./hash\";\nimport type {\n\tCreateOpaPolicyEvaluatorOptions,\n\tOpaDecision,\n\tOpaPolicyClient,\n\tOpaPolicyFailureKind,\n} from \"./types\";\n\nconst DEFAULT_SERVICE_KEY = \"opa\";\nconst ENGINE_NAME = \"opa\";\n\n/**\n * Build a Fabric `PolicyEvaluator` that delegates decision-making to an OPA\n * (Open Policy Agent) deployment. The evaluator:\n *\n * 1. Reads the host-injected OPA client from `ctx.services[serviceKey]`.\n * 2. Builds a JSON input snapshot via `buildInput(ctx)` — domain code that\n * snapshots facts the engine needs. Fabric platform never lets OPA reach\n * into the database directly; all facts flow through this function.\n * 3. Evaluates the Rego decision at `binding.decisionPath`.\n * 4. Validates the response against the house-style shape (`OpaDecision`).\n * 5. Maps the decision to a `PolicyOutcome` and attaches engine provenance\n * (`engine.name = \"opa\"`, `decisionPath`, `inputHash`) for audit.\n *\n * Failures (missing client, build failure, network failure, malformed response)\n * are governed by `onError`. Default `\"block\"` honors Fabric's default-deny\n * posture and records the failure category under `engine.metadata.error`.\n */\nexport function createOpaPolicyEvaluator<TDb = unknown>(\n\toptions: CreateOpaPolicyEvaluatorOptions<TDb>,\n): PolicyEvaluator<TDb> {\n\tconst {\n\t\tbinding,\n\t\tbuildInput,\n\t\tserviceKey = DEFAULT_SERVICE_KEY,\n\t\tonError = \"block\",\n\t\tpreviewSafe = false,\n\t\tengineVersion,\n\t\tmapDecision,\n\t\thashInput = defaultHashInput,\n\t} = options;\n\n\treturn {\n\t\tpolicyId: binding.policyId,\n\t\tversion: binding.version,\n\t\tpreviewSafe,\n\t\tevaluate: async (ctx) => {\n\t\t\tconst client = ctx.services?.[serviceKey] as OpaPolicyClient | undefined;\n\t\t\tif (!client || typeof client.evaluate !== \"function\") {\n\t\t\t\treturn makeFailureOutcome(\n\t\t\t\t\tbinding,\n\t\t\t\t\tengineVersion,\n\t\t\t\t\t\"client_unavailable\",\n\t\t\t\t\t`No OPA client at ctx.services[\"${serviceKey}\"]`,\n\t\t\t\t\tonError,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tlet input: Record<string, unknown>;\n\t\t\ttry {\n\t\t\t\tinput = await buildInput(ctx);\n\t\t\t} catch (err) {\n\t\t\t\treturn makeFailureOutcome(\n\t\t\t\t\tbinding,\n\t\t\t\t\tengineVersion,\n\t\t\t\t\t\"input_build_failed\",\n\t\t\t\t\t`buildInput threw: ${formatError(err)}`,\n\t\t\t\t\tonError,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tlet inputHash: string | undefined;\n\t\t\ttry {\n\t\t\t\tinputHash = hashInput(input);\n\t\t\t} catch {\n\t\t\t\t// hashing is best-effort — if a custom hasher throws, omit the\n\t\t\t\t// field rather than fail the policy.\n\t\t\t}\n\n\t\t\tlet rawResult: unknown;\n\t\t\ttry {\n\t\t\t\trawResult = await client.evaluate(binding.decisionPath, input);\n\t\t\t} catch (err) {\n\t\t\t\treturn makeFailureOutcome(\n\t\t\t\t\tbinding,\n\t\t\t\t\tengineVersion,\n\t\t\t\t\t\"evaluation_failed\",\n\t\t\t\t\t`OPA evaluation failed: ${formatError(err)}`,\n\t\t\t\t\tonError,\n\t\t\t\t\tinputHash,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst parsed = parseOpaDecision(rawResult);\n\t\t\tif (!parsed.ok) {\n\t\t\t\treturn makeFailureOutcome(\n\t\t\t\t\tbinding,\n\t\t\t\t\tengineVersion,\n\t\t\t\t\t\"invalid_response\",\n\t\t\t\t\t`OPA returned malformed decision: ${parsed.error}`,\n\t\t\t\t\tonError,\n\t\t\t\t\tinputHash,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn buildSuccessOutcome(\n\t\t\t\tbinding,\n\t\t\t\tengineVersion,\n\t\t\t\tparsed.decision,\n\t\t\t\tinputHash,\n\t\t\t\tctx,\n\t\t\t\tmapDecision,\n\t\t\t);\n\t\t},\n\t};\n}\n\nfunction buildSuccessOutcome<TDb>(\n\tbinding: CreateOpaPolicyEvaluatorOptions<TDb>[\"binding\"],\n\tengineVersion: string | undefined,\n\tdecision: OpaDecision,\n\tinputHash: string | undefined,\n\tctx: PolicyContext<TDb>,\n\tmapDecision: CreateOpaPolicyEvaluatorOptions<TDb>[\"mapDecision\"],\n): PolicyOutcome {\n\tconst mapped = mapDecision\n\t\t? mapDecision(decision, ctx)\n\t\t: { result: decision.result, reason: decision.reason };\n\n\treturn {\n\t\tpolicyId: binding.policyId,\n\t\tpolicyVersion: binding.version,\n\t\tresult: mapped.result,\n\t\treason: mapped.reason,\n\t\tmetadata: {\n\t\t\t...(mapped.metadata ?? {}),\n\t\t\topaDecision: decision,\n\t\t},\n\t\tdispatchEvidence: {\n\t\t\tpolicyKind: \"code\",\n\t\t\tpolicyId: binding.policyId,\n\t\t\tpolicyVersion: binding.version,\n\t\t\tdispatchPath: [\"code\"],\n\t\t\tengine: {\n\t\t\t\tname: ENGINE_NAME,\n\t\t\t\tversion: engineVersion,\n\t\t\t\tmetadata: stripUndefined({\n\t\t\t\t\tdecisionPath: binding.decisionPath,\n\t\t\t\t\tregoPackage: binding.regoPackage,\n\t\t\t\t\tinputHash,\n\t\t\t\t\tconditionResults: decision.conditionResults,\n\t\t\t\t\tfactsUsed: decision.factsUsed,\n\t\t\t\t\tobligations: decision.obligations,\n\t\t\t\t}),\n\t\t\t},\n\t\t},\n\t};\n}\n\nfunction makeFailureOutcome(\n\tbinding: CreateOpaPolicyEvaluatorOptions[\"binding\"],\n\tengineVersion: string | undefined,\n\terror: OpaPolicyFailureKind,\n\treason: string,\n\tonError: \"block\" | \"throw\",\n\tinputHash?: string,\n): PolicyOutcome {\n\tif (onError === \"throw\") {\n\t\tconst err = new Error(reason);\n\t\t(err as Error & { kind?: OpaPolicyFailureKind }).kind = error;\n\t\tthrow err;\n\t}\n\n\treturn {\n\t\tpolicyId: binding.policyId,\n\t\tpolicyVersion: binding.version,\n\t\tresult: \"block\",\n\t\treason,\n\t\tmetadata: {\n\t\t\topaError: error,\n\t\t},\n\t\tdispatchEvidence: {\n\t\t\tpolicyKind: \"code\",\n\t\t\tpolicyId: binding.policyId,\n\t\t\tpolicyVersion: binding.version,\n\t\t\tdispatchPath: [\"code\"],\n\t\t\tengine: {\n\t\t\t\tname: ENGINE_NAME,\n\t\t\t\tversion: engineVersion,\n\t\t\t\tmetadata: stripUndefined({\n\t\t\t\t\tdecisionPath: binding.decisionPath,\n\t\t\t\t\tregoPackage: binding.regoPackage,\n\t\t\t\t\tinputHash,\n\t\t\t\t\terror,\n\t\t\t\t\terrorReason: reason,\n\t\t\t\t}),\n\t\t\t},\n\t\t},\n\t};\n}\n\nfunction stripUndefined(obj: Record<string, unknown>): Record<string, unknown> {\n\tconst out: Record<string, unknown> = {};\n\tfor (const [k, v] of Object.entries(obj)) {\n\t\tif (v !== undefined) out[k] = v;\n\t}\n\treturn out;\n}\n\nfunction formatError(err: unknown): string {\n\tif (err instanceof Error) return err.message;\n\tif (typeof err === \"string\") return err;\n\ttry {\n\t\treturn JSON.stringify(err);\n\t} catch {\n\t\treturn String(err);\n\t}\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@fabricorg/policy-opa",
3
+ "version": "0.1.0",
4
+ "description": "Open Policy Agent (OPA) adapter for @fabricorg/platform. Wraps a Rego decision behind Fabric's PolicyEvaluator contract — Fabric keeps policyId.vN canonical for audit; engine evidence carries OPA provenance (decision path, input hash, bundle revision). Zero coupling to a specific OPA SDK; structurally typed client.",
5
+ "keywords": [
6
+ "fabric",
7
+ "fabricorg",
8
+ "policy",
9
+ "opa",
10
+ "open-policy-agent",
11
+ "rego",
12
+ "authorization",
13
+ "policy-engine"
14
+ ],
15
+ "license": "MIT",
16
+ "author": "Fabric",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/Fabric-Pro/fabric-platform.git",
20
+ "directory": "packages/policy-opa"
21
+ },
22
+ "homepage": "https://github.com/Fabric-Pro/fabric-platform#readme",
23
+ "bugs": {
24
+ "url": "https://github.com/Fabric-Pro/fabric-platform/issues"
25
+ },
26
+ "type": "module",
27
+ "sideEffects": false,
28
+ "main": "./dist/index.cjs",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "import": {
33
+ "types": "./dist/index.d.ts",
34
+ "default": "./dist/index.js"
35
+ },
36
+ "require": {
37
+ "types": "./dist/index.d.cts",
38
+ "default": "./dist/index.cjs"
39
+ }
40
+ }
41
+ },
42
+ "files": [
43
+ "dist",
44
+ "README.md",
45
+ "CHANGELOG.md",
46
+ "LICENSE"
47
+ ],
48
+ "peerDependencies": {
49
+ "@fabricorg/platform": "^0.3.2"
50
+ },
51
+ "dependencies": {},
52
+ "devDependencies": {
53
+ "@types/node": "^22.10.0",
54
+ "tsup": "^8.5.0",
55
+ "typescript": "^5.7.3",
56
+ "vitest": "^4.1.5",
57
+ "@fabricorg/platform": "0.3.2"
58
+ },
59
+ "engines": {
60
+ "node": ">=20"
61
+ },
62
+ "publishConfig": {
63
+ "access": "public"
64
+ },
65
+ "scripts": {
66
+ "clean": "rm -rf dist tsconfig.tsbuildinfo",
67
+ "build": "tsup",
68
+ "type-check": "tsc --noEmit",
69
+ "test": "vitest run --passWithNoTests"
70
+ }
71
+ }