@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 +22 -0
- package/LICENSE +21 -0
- package/README.md +180 -0
- package/dist/index.cjs +242 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +163 -0
- package/dist/index.d.ts +163 -0
- package/dist/index.js +237 -0
- package/dist/index.js.map +1 -0
- package/package.json +71 -0
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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|