@govplane/runtime-sdk 0.2.4 → 0.5.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/README.md +400 -216
- package/dist/index.cjs +187 -91
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +65 -14
- package/dist/index.d.ts +65 -14
- package/dist/index.js +187 -91
- package/dist/index.js.map +1 -1
- package/docs/README.md +89 -0
- package/docs/SUMMARY.md +34 -0
- package/docs/installation/GettingStarted.md +474 -0
- package/docs/reference/Configuration.md +198 -0
- package/docs/reference/TypesAndInterfaces.md +373 -0
- package/docs/usage/BundleLifecycle.md +209 -0
- package/docs/usage/ConditionalRules.md +251 -0
- package/docs/usage/ContextPolicy.md +196 -0
- package/docs/usage/CustomEffect.md +217 -0
- package/docs/usage/DecisionTrace.md +289 -0
- package/docs/usage/Effects.md +213 -0
- package/docs/usage/Evaluate.md +164 -0
- package/docs/usage/PolicyDefaults.md +220 -0
- package/package.json +3 -5
package/README.md
CHANGED
|
@@ -8,15 +8,46 @@ It is intended for backend services, workers, gateways, and critical paths that
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
+
## Architecture
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
┌─────────────────────────────────────────────────────────┐
|
|
15
|
+
│ Your Application │
|
|
16
|
+
│ │
|
|
17
|
+
│ client.evaluate({ target, context }) │
|
|
18
|
+
│ │ │
|
|
19
|
+
│ ▼ │
|
|
20
|
+
│ ┌─────────────────┐ bundle cache ┌───────────────┐ │
|
|
21
|
+
│ │ PolicyEngine │◄─────────────────│ RuntimeClient │ │
|
|
22
|
+
│ │ (in-process) │ │ (polling) │ │
|
|
23
|
+
│ └────────┬────────┘ └──────┬────────┘ │
|
|
24
|
+
│ │ Decision │ HTTP │
|
|
25
|
+
│ ▼ ▼ │
|
|
26
|
+
│ allow / deny / Govplane Control Plane │
|
|
27
|
+
│ throttle / kill_switch / (bundle endpoint only) │
|
|
28
|
+
│ custom │
|
|
29
|
+
└─────────────────────────────────────────────────────────┘
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
| Property | Details |
|
|
33
|
+
|---|---|
|
|
34
|
+
| Decision latency | < 1 ms (in-process evaluation) |
|
|
35
|
+
| Network dependency | Bundle fetch only — evaluation is offline-safe |
|
|
36
|
+
| PII in traces | None — context is never included in trace events |
|
|
37
|
+
| Fail-safe | Deny-by-default when bundle is missing or invalid |
|
|
38
|
+
| Bundle delivery | Server-compiled JSON bundle, not evaluated server-side |
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
11
42
|
## Design Principles
|
|
12
43
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
44
|
+
- **No exposed endpoints** – no middleware, no inbound surface
|
|
45
|
+
- **Local-first evaluation** – decisions are made in-process
|
|
46
|
+
- **Zero PII by design** – strict context validation and heuristic PII key blocking
|
|
47
|
+
- **Precompiled policies** – no DSL or dynamic code at runtime
|
|
48
|
+
- **Deterministic outcomes** – same input, same decision
|
|
49
|
+
- **Cheap polling** – HEAD + ETag + conditional GET
|
|
50
|
+
- **Failure-safe** – backoff, degraded mode, deny-by-default
|
|
20
51
|
|
|
21
52
|
---
|
|
22
53
|
|
|
@@ -25,51 +56,46 @@ It is intended for backend services, workers, gateways, and critical paths that
|
|
|
25
56
|
### Runtime Client
|
|
26
57
|
- Efficient **HEAD-first polling** using `ETag` and `If-None-Match`
|
|
27
58
|
- In-memory bundle cache
|
|
28
|
-
-
|
|
59
|
+
- `warmStart()` to block until first bundle is ready
|
|
29
60
|
- Configurable polling interval
|
|
30
61
|
- **Burst mode** for incident response
|
|
31
62
|
- **Exponential backoff with jitter**
|
|
32
|
-
- Automatic **degraded state**
|
|
33
|
-
- Status subscriptions (`
|
|
34
|
-
|
|
35
|
-
### Policy Engine
|
|
36
|
-
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
- Precompiled `when` AST evaluation
|
|
46
|
-
- No dynamic evaluation or code execution
|
|
63
|
+
- Automatic **degraded state** with `nextRetryAt` reporting
|
|
64
|
+
- Status subscriptions via `onStatus()` and bundle update subscriptions via `onUpdate()`
|
|
65
|
+
|
|
66
|
+
### Policy Engine
|
|
67
|
+
- Five effect types: `allow`, `deny`, `kill_switch`, `throttle`, `custom`
|
|
68
|
+
- **Deny-by-default** – no bundle or no match always denies
|
|
69
|
+
- Fixed precedence: `kill_switch > deny > throttle > allow > custom > deny-by-default`
|
|
70
|
+
- Throttle selects the **most restrictive** matching rule
|
|
71
|
+
- `thenEffect` / `elseEffect` for conditional branching within a single rule
|
|
72
|
+
- Policy-level `defaults` as a fallback when no rule matches
|
|
73
|
+
- Deterministic rule ordering by `priority`, then `policyKey`, then `ruleId`
|
|
74
|
+
- Precompiled `when` AST evaluation — no dynamic code execution
|
|
75
|
+
- Optional automatic JSON parsing of `custom` effect values (`parseCustomEffect`)
|
|
47
76
|
|
|
48
77
|
### Security & Context Safety
|
|
49
|
-
- Explicit context allowlist
|
|
50
|
-
-
|
|
51
|
-
- Configurable limits:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
60
|
-
-
|
|
61
|
-
-
|
|
62
|
-
-
|
|
63
|
-
- **Async trace sinks** (fire-and-forget)
|
|
64
|
-
- Bounded queues with drop strategies
|
|
78
|
+
- Explicit context `allowedKeys` allowlist — unknown keys throw immediately
|
|
79
|
+
- Heuristic PII key blocking (`blockLikelyPiiKeys`)
|
|
80
|
+
- Configurable limits: `maxStringLen`, `maxArrayLen`
|
|
81
|
+
- Context policy is fully configurable per engine instance
|
|
82
|
+
- Disable validation only in controlled test environments
|
|
83
|
+
|
|
84
|
+
### Decision Tracing
|
|
85
|
+
- `evaluate()` for plain decisions; `evaluateWithTrace()` for traced decisions
|
|
86
|
+
- Four trace levels: `off`, `errors`, `sampled`, `full`
|
|
87
|
+
- Sampling rate and per-window budget controls
|
|
88
|
+
- `force: true` to bypass sampling for debug sessions
|
|
89
|
+
- Compact or full format (full includes per-rule breakdown)
|
|
90
|
+
- **Synchronous and async trace sinks** with bounded queues and drop strategies
|
|
91
|
+
- `flushTraces()` for graceful shutdown
|
|
65
92
|
|
|
66
93
|
---
|
|
67
94
|
|
|
68
95
|
## Requirements
|
|
69
96
|
|
|
70
97
|
- **Node.js ≥ 18**
|
|
71
|
-
- A valid **Govplane Runtime Key**
|
|
72
|
-
- Access to a Govplane Runtime API (`gp-runtime`)
|
|
98
|
+
- A valid **Govplane Runtime Key** (`rk_live_…` or `rk_test_…`)
|
|
73
99
|
|
|
74
100
|
---
|
|
75
101
|
|
|
@@ -79,214 +105,334 @@ It is intended for backend services, workers, gateways, and critical paths that
|
|
|
79
105
|
npm install @govplane/runtime-sdk
|
|
80
106
|
```
|
|
81
107
|
|
|
108
|
+
```bash
|
|
109
|
+
yarn add @govplane/runtime-sdk
|
|
110
|
+
# or
|
|
111
|
+
pnpm add @govplane/runtime-sdk
|
|
112
|
+
```
|
|
113
|
+
|
|
82
114
|
---
|
|
83
115
|
|
|
84
116
|
## Quick Start
|
|
85
117
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
```ts
|
|
118
|
+
```typescript
|
|
89
119
|
import { RuntimeClient } from "@govplane/runtime-sdk";
|
|
90
120
|
|
|
91
121
|
const client = new RuntimeClient({
|
|
92
|
-
baseUrl:
|
|
122
|
+
baseUrl: "https://123456.runtime.govplane.com/",
|
|
93
123
|
runtimeKey: process.env.GP_RUNTIME_KEY!,
|
|
94
|
-
orgId: "org_dev",
|
|
95
|
-
projectId: "prj_web",
|
|
96
|
-
env: "prod"
|
|
97
124
|
});
|
|
98
125
|
|
|
99
|
-
client.
|
|
126
|
+
await client.warmStart(); // block until the first bundle is cached
|
|
127
|
+
client.start(); // begin background polling (every 5 s by default)
|
|
128
|
+
|
|
129
|
+
const result = client.evaluate({
|
|
130
|
+
target: { service: "payments", resource: "checkout", action: "create" },
|
|
131
|
+
context: { plan: "pro", country: "ES", isAuthenticated: true },
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
console.log(result.decision); // "allow" | "deny" | "throttle" | "kill_switch" | "custom"
|
|
100
135
|
```
|
|
101
136
|
|
|
102
|
-
|
|
137
|
+
---
|
|
103
138
|
|
|
104
|
-
|
|
105
|
-
import { createPolicyEngine } from "@govplane/runtime-sdk";
|
|
139
|
+
## Effect Types
|
|
106
140
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
allowedKeys: [
|
|
112
|
-
"ctx.plan",
|
|
113
|
-
"ctx.country",
|
|
114
|
-
"ctx.amount",
|
|
115
|
-
"ctx.isAuthenticated"
|
|
116
|
-
],
|
|
117
|
-
maxStringLen: 64,
|
|
118
|
-
maxArrayLen: 10
|
|
119
|
-
}
|
|
120
|
-
});
|
|
141
|
+
The engine returns one of five decision types, applied in this fixed precedence order:
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
kill_switch > deny > throttle > allow > custom > deny-by-default
|
|
121
145
|
```
|
|
122
146
|
|
|
123
|
-
|
|
147
|
+
| `decision` | When it fires |
|
|
148
|
+
|---------------|---------------|
|
|
149
|
+
| `allow` | An `allow` rule matched, or the policy default is `allow`. |
|
|
150
|
+
| `deny` | A `deny` rule matched, no rule matched (deny-by-default), or the policy default is `deny`. |
|
|
151
|
+
| `kill_switch` | A `kill_switch` rule or policy default is active. Always wins. |
|
|
152
|
+
| `throttle` | A `throttle` rule or policy default matched. Most restrictive wins. |
|
|
153
|
+
| `custom` | A `custom` rule or policy default matched — carries an arbitrary string value. |
|
|
124
154
|
|
|
125
|
-
|
|
126
|
-
const result = engine.evaluate({
|
|
127
|
-
target: {
|
|
128
|
-
service: "payments",
|
|
129
|
-
resource: "checkout",
|
|
130
|
-
action: "create"
|
|
131
|
-
},
|
|
132
|
-
context: {
|
|
133
|
-
plan: "pro",
|
|
134
|
-
country: "ES",
|
|
135
|
-
amount: 42,
|
|
136
|
-
isAuthenticated: true
|
|
137
|
-
}
|
|
138
|
-
});
|
|
155
|
+
---
|
|
139
156
|
|
|
140
|
-
|
|
157
|
+
## Decision Shape
|
|
158
|
+
|
|
159
|
+
`evaluate()` always returns a `Decision` object discriminated by `decision`:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
type Decision =
|
|
163
|
+
| { decision: "allow";
|
|
164
|
+
reason: "default" | "rule"; policyKey?: string; ruleId?: string }
|
|
165
|
+
| { decision: "deny";
|
|
166
|
+
reason: "default" | "rule"; policyKey?: string; ruleId?: string }
|
|
167
|
+
| { decision: "kill_switch";
|
|
168
|
+
reason: "default" | "rule"; policyKey?: string; ruleId?: string;
|
|
169
|
+
killSwitch: { service: string; reason?: string } }
|
|
170
|
+
| { decision: "throttle";
|
|
171
|
+
reason: "default" | "rule"; policyKey?: string; ruleId?: string;
|
|
172
|
+
throttle: { limit: number; windowSeconds: number; key: string } }
|
|
173
|
+
| { decision: "custom";
|
|
174
|
+
reason: "default" | "rule"; policyKey?: string; ruleId?: string;
|
|
175
|
+
value: string; parsedValue?: unknown };
|
|
141
176
|
```
|
|
142
177
|
|
|
143
|
-
|
|
178
|
+
### Handling every decision type
|
|
144
179
|
|
|
145
|
-
|
|
180
|
+
```typescript
|
|
181
|
+
const result = client.evaluate({ target, context });
|
|
146
182
|
|
|
147
|
-
|
|
183
|
+
if (result.decision === "allow") {
|
|
184
|
+
return next();
|
|
185
|
+
}
|
|
148
186
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
187
|
+
if (result.decision === "deny") {
|
|
188
|
+
return reply.status(403).send({ error: "Forbidden", policy: result.policyKey });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (result.decision === "throttle") {
|
|
192
|
+
const allowed = await rateLimiter.check(
|
|
193
|
+
result.throttle.key === "tenant" ? ctx.tenantId : ctx.userId,
|
|
194
|
+
result.throttle.limit,
|
|
195
|
+
result.throttle.windowSeconds,
|
|
196
|
+
);
|
|
197
|
+
if (!allowed) return reply.status(429).header("Retry-After", String(result.throttle.windowSeconds)).send();
|
|
198
|
+
return next();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (result.decision === "kill_switch") {
|
|
202
|
+
return reply.status(503).send({ error: "Service Unavailable", reason: result.killSwitch.reason });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (result.decision === "custom") {
|
|
206
|
+
const payload = result.parsedValue ?? JSON.parse(result.value);
|
|
207
|
+
return reply.send(payload);
|
|
208
|
+
}
|
|
158
209
|
```
|
|
159
210
|
|
|
160
|
-
The
|
|
211
|
+
> **Note:** The SDK does not maintain rate-limit counters. It signals the throttle parameters; your infrastructure (Redis, middleware, etc.) is responsible for enforcement.
|
|
161
212
|
|
|
162
213
|
---
|
|
163
214
|
|
|
164
|
-
##
|
|
215
|
+
## Custom Effects
|
|
165
216
|
|
|
166
|
-
|
|
167
|
-
// Force immediate refresh
|
|
168
|
-
await client.refreshNow();
|
|
217
|
+
A `custom` effect carries an arbitrary string — including a JSON-encoded object — back to the caller. Common uses: feature flags, A/B variants, per-tenant configuration overlays.
|
|
169
218
|
|
|
170
|
-
|
|
171
|
-
|
|
219
|
+
Enable automatic JSON parsing with `engine.parseCustomEffect`:
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
const client = new RuntimeClient({
|
|
223
|
+
baseUrl: "https://123456.runtime.govplane.com/",
|
|
224
|
+
runtimeKey: process.env.GP_RUNTIME_KEY!,
|
|
225
|
+
engine: {
|
|
226
|
+
parseCustomEffect: true, // JSON.parse() applied automatically
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const result = client.evaluate({ target, context });
|
|
231
|
+
|
|
232
|
+
if (result.decision === "custom") {
|
|
233
|
+
const flags = result.parsedValue as { enabled: boolean; variant: string };
|
|
234
|
+
if (flags?.enabled) renderNewCheckout();
|
|
235
|
+
}
|
|
172
236
|
```
|
|
173
237
|
|
|
174
|
-
|
|
175
|
-
1. `HEAD /v1/runtime/bundle`
|
|
176
|
-
2. Compare `ETag`
|
|
177
|
-
3. If changed → `GET`
|
|
178
|
-
4. If unchanged → no download, no JSON parse
|
|
238
|
+
Non-JSON strings leave `parsedValue` as `undefined`; the raw `value` string is always present as a fallback.
|
|
179
239
|
|
|
180
240
|
---
|
|
181
241
|
|
|
182
|
-
##
|
|
242
|
+
## Conditional Rules (`when` / `thenEffect` / `elseEffect`)
|
|
183
243
|
|
|
184
|
-
|
|
185
|
-
- Applies exponential backoff with jitter
|
|
186
|
-
- Enters **degraded** state after N failures
|
|
187
|
-
- Emits status updates
|
|
244
|
+
Rules support an optional `when` condition AST evaluated against the call-time context. The AST is compiled by the control plane and distributed in the bundle — the SDK never executes dynamic code.
|
|
188
245
|
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
});
|
|
246
|
+
```
|
|
247
|
+
when absent → always apply effect
|
|
248
|
+
when == true → apply thenEffect (fallback: effect)
|
|
249
|
+
when == false → apply elseEffect (fallback: skip rule)
|
|
195
250
|
```
|
|
196
251
|
|
|
197
|
-
|
|
252
|
+
Supported operators: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `exists`, `and`, `or`, `not`.
|
|
253
|
+
|
|
254
|
+
The `ctx.` prefix on paths is optional and stripped automatically (`ctx.plan` ≡ `plan`).
|
|
198
255
|
|
|
199
256
|
---
|
|
200
257
|
|
|
201
|
-
##
|
|
258
|
+
## Policy Defaults
|
|
202
259
|
|
|
203
|
-
|
|
204
|
-
- Force faster policy propagation
|
|
205
|
-
- Trigger immediate policy refresh
|
|
206
|
-
- Temporarily increase polling frequency (“burst mode”)
|
|
260
|
+
Every policy can declare a `defaults` object — the fallback effect when no rule matches that target. Defaults are treated as a synthetic rule with `priority = -1`, so any explicit rule always wins.
|
|
207
261
|
|
|
208
|
-
|
|
262
|
+
```json
|
|
263
|
+
{
|
|
264
|
+
"policyKey": "strict-rbac",
|
|
265
|
+
"defaults": { "effect": "deny" },
|
|
266
|
+
"rules": [...]
|
|
267
|
+
}
|
|
268
|
+
```
|
|
209
269
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
- Safe under active attack
|
|
214
|
-
- Compatible with containerized and orchestrated environments
|
|
270
|
+
All five effect types are supported as defaults: `allow`, `deny`, `throttle`, `kill_switch`, `custom`.
|
|
271
|
+
|
|
272
|
+
---
|
|
215
273
|
|
|
216
|
-
|
|
274
|
+
## Runtime Bundle Model
|
|
217
275
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
276
|
+
The SDK retrieves **compiled runtime bundles** generated by the Govplane control plane.
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
type RuntimeBundleV1 = {
|
|
280
|
+
schemaVersion: 1;
|
|
281
|
+
orgId: string;
|
|
282
|
+
projectId: string;
|
|
283
|
+
env: string;
|
|
284
|
+
generatedAt: string; // ISO timestamp
|
|
285
|
+
bundleVersion?: number;
|
|
286
|
+
checksum?: string; // e.g. "sha256:..."
|
|
287
|
+
policies: RuntimePolicy[];
|
|
288
|
+
};
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
The bundle is treated as **immutable and read-only**. See [Types & Interfaces](docs/reference/TypesAndInterfaces.md) for the full type tree.
|
|
223
292
|
|
|
224
293
|
---
|
|
225
294
|
|
|
226
|
-
|
|
295
|
+
## Bundle Lifecycle & Polling
|
|
227
296
|
|
|
228
|
-
|
|
297
|
+
All `evaluate()` calls read from an in-memory cache — there is no network hop at decision time.
|
|
229
298
|
|
|
230
|
-
```
|
|
231
|
-
|
|
299
|
+
```typescript
|
|
300
|
+
// Block until first bundle is ready, then start background polling
|
|
301
|
+
await client.warmStart({ timeoutMs: 10_000 });
|
|
302
|
+
client.start();
|
|
232
303
|
```
|
|
233
304
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
305
|
+
Polling strategy per cycle:
|
|
306
|
+
1. `HEAD` the bundle endpoint to check the current `ETag`
|
|
307
|
+
2. If `ETag` unchanged → update metadata, skip the `GET`
|
|
308
|
+
3. If `ETag` changed → `GET` the full bundle, parse, update cache
|
|
309
|
+
4. Notify `onUpdate` listeners
|
|
310
|
+
|
|
311
|
+
| Phase | Interval |
|
|
312
|
+
|---|---|
|
|
313
|
+
| Normal | `pollMs` (default **5 000 ms**) |
|
|
314
|
+
| Burst mode | `burstPollMs` (default **500 ms**) for `burstDurationMs` (default **30 000 ms**) |
|
|
315
|
+
| Backoff | Exponential, capped at `backoffMaxMs` |
|
|
316
|
+
|
|
317
|
+
### Bundle update subscription
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
client.onUpdate((result) => {
|
|
321
|
+
logger.info("Bundle updated", {
|
|
322
|
+
etag: result.meta.etag,
|
|
323
|
+
bundleVersion: result.meta.bundleVersion,
|
|
324
|
+
updatedAt: result.meta.updatedAt,
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
---
|
|
238
330
|
|
|
239
|
-
|
|
240
|
-
• In most container platforms (ECS, Kubernetes), environment variables cannot be changed at runtime
|
|
241
|
-
• A rolling restart is usually required
|
|
242
|
-
• For zero-restart response, use File-based Hot Reload
|
|
331
|
+
## Backoff & Degraded Mode
|
|
243
332
|
|
|
333
|
+
On repeated failures the client applies exponential backoff with jitter and enters **degraded mode** after a configurable number of consecutive failures. The cached bundle remains active in degraded mode.
|
|
244
334
|
|
|
245
|
-
|
|
335
|
+
```typescript
|
|
336
|
+
const client = new RuntimeClient({
|
|
337
|
+
...
|
|
338
|
+
backoffBaseMs: 500,
|
|
339
|
+
backoffMaxMs: 30_000,
|
|
340
|
+
backoffJitter: 0.2,
|
|
341
|
+
degradeAfterFailures: 3,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
client.onStatus((status) => {
|
|
345
|
+
if (status.state === "degraded") {
|
|
346
|
+
alerting.trigger("govplane_sdk_degraded", {
|
|
347
|
+
failures: status.consecutiveFailures,
|
|
348
|
+
lastError: status.lastError.message,
|
|
349
|
+
nextRetryAt: status.nextRetryAt,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Backoff formula: `delay = clamp(base × 2^(failures-1), 0, backoffMaxMs) ± jitter%`
|
|
246
356
|
|
|
247
357
|
---
|
|
248
358
|
|
|
249
|
-
##
|
|
359
|
+
## Context Policy & PII Safety
|
|
250
360
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
361
|
+
The `context` object is validated synchronously before every evaluation. A violation throws immediately, before any rule is tested.
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
const client = new RuntimeClient({
|
|
365
|
+
...
|
|
366
|
+
engine: {
|
|
367
|
+
contextPolicy: {
|
|
368
|
+
allowedKeys: ["plan", "role", "country", "isAuthenticated", "requestTier"],
|
|
369
|
+
maxStringLen: 64,
|
|
370
|
+
maxArrayLen: 10,
|
|
371
|
+
blockLikelyPiiKeys: true, // blocks email, phone, name, ip, ssn, etc.
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
});
|
|
259
375
|
```
|
|
260
376
|
|
|
261
|
-
|
|
377
|
+
Default allowed keys: `plan`, `country`, `requestTier`, `feature`, `amount`, `isAuthenticated`, `role`.
|
|
378
|
+
|
|
379
|
+
`object` values are not permitted at the top level. Permitted value types: `string`, `number`, `boolean`, `null/undefined`, `string[]`.
|
|
380
|
+
|
|
381
|
+
> Disable validation (`engine: { validateContext: false }`) only in controlled test environments.
|
|
262
382
|
|
|
263
383
|
---
|
|
264
384
|
|
|
265
|
-
## Decision
|
|
385
|
+
## Decision Tracing
|
|
266
386
|
|
|
267
|
-
|
|
387
|
+
Use `evaluateWithTrace()` to attach a `DecisionTrace` to the result for observability and debugging. Traces contain only structural metadata — no context values or PII.
|
|
268
388
|
|
|
269
|
-
|
|
270
|
-
const out = engine.evaluateWithTrace(
|
|
271
|
-
{ target, context },
|
|
272
|
-
{ sampling: 0.01, mode: "compact" }
|
|
273
|
-
);
|
|
389
|
+
### Production (sampled)
|
|
274
390
|
|
|
275
|
-
|
|
276
|
-
|
|
391
|
+
```typescript
|
|
392
|
+
const client = new RuntimeClient({
|
|
393
|
+
...
|
|
394
|
+
trace: {
|
|
395
|
+
defaults: {
|
|
396
|
+
level: "sampled",
|
|
397
|
+
sampling: 0.05, // 5% of evaluations
|
|
398
|
+
budget: { maxTraces: 60, windowMs: 60_000 },
|
|
399
|
+
},
|
|
400
|
+
onDecisionTraceAsync: async (evt) => {
|
|
401
|
+
await analyticsClient.ingest("govplane.trace", evt);
|
|
402
|
+
},
|
|
403
|
+
queueMax: 1000,
|
|
404
|
+
dropPolicy: "drop_new",
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
const result = client.evaluateWithTrace({ target, context });
|
|
409
|
+
if (result.trace) {
|
|
410
|
+
console.log(result.trace.summary);
|
|
411
|
+
console.log(result.trace.winner);
|
|
277
412
|
}
|
|
278
413
|
```
|
|
279
414
|
|
|
280
|
-
### Debug (
|
|
415
|
+
### Debug (forced)
|
|
281
416
|
|
|
282
|
-
```
|
|
283
|
-
|
|
417
|
+
```typescript
|
|
418
|
+
const result = client.evaluateWithTrace(
|
|
284
419
|
{ target, context },
|
|
285
|
-
{ force: true,
|
|
420
|
+
{ level: "full", force: true }, // bypass sampling and budget
|
|
286
421
|
);
|
|
287
422
|
```
|
|
288
423
|
|
|
289
|
-
|
|
424
|
+
| Level | Behaviour |
|
|
425
|
+
|---|---|
|
|
426
|
+
| `"off"` | No trace computed. Equivalent to `evaluate()`. |
|
|
427
|
+
| `"errors"` | Trace attached only on `deny` or `kill_switch`. Compact format. |
|
|
428
|
+
| `"sampled"` | Trace attached probabilistically by `sampling` rate and budget. Compact format. |
|
|
429
|
+
| `"full"` | Always attempted (budget permitting). Includes complete per-rule list. |
|
|
430
|
+
|
|
431
|
+
### Trace sinks
|
|
432
|
+
|
|
433
|
+
Both synchronous (`onDecisionTrace`) and async (`onDecisionTraceAsync`) sinks are supported. Async evaluations are buffered internally and never block evaluation. Drain on shutdown with `client.flushTraces()`.
|
|
434
|
+
|
|
435
|
+
### Trace guarantees
|
|
290
436
|
|
|
291
437
|
- No rule bodies
|
|
292
438
|
- No context values
|
|
@@ -295,68 +441,106 @@ engine.evaluateWithTrace(
|
|
|
295
441
|
|
|
296
442
|
---
|
|
297
443
|
|
|
298
|
-
##
|
|
299
|
-
|
|
300
|
-
```ts
|
|
301
|
-
engine.setTraceSink(
|
|
302
|
-
createAsyncTraceSink({
|
|
303
|
-
maxQueue: 100,
|
|
304
|
-
drop: "drop_new",
|
|
305
|
-
sink: async (event) => {
|
|
306
|
-
await fetch("https://trace-endpoint", {
|
|
307
|
-
method: "POST",
|
|
308
|
-
body: JSON.stringify(event)
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
})
|
|
312
|
-
);
|
|
313
|
-
```
|
|
444
|
+
## Runtime Incident Controls
|
|
314
445
|
|
|
315
|
-
|
|
316
|
-
- Never blocks evaluation
|
|
317
|
-
- Never throws
|
|
318
|
-
- Drops safely under pressure
|
|
446
|
+
Govplane provides **passive incident controls** that allow operators to respond to incidents without exposing endpoints, handling PII, or recompiling application code.
|
|
319
447
|
|
|
320
|
-
|
|
448
|
+
| Mechanism | Restart Required | Hot | Recommended |
|
|
449
|
+
|---|---|---|---|
|
|
450
|
+
| Environment Variable (`GP_RUNTIME_INCIDENT=1`) | Usually yes | No | Yes |
|
|
451
|
+
| File-based hot reload (`incidentFilePath`) | No | Yes | **Primary** |
|
|
452
|
+
| POSIX Signal (`SIGUSR1`) | No | Yes | Optional |
|
|
321
453
|
|
|
322
|
-
|
|
454
|
+
```typescript
|
|
455
|
+
const client = new RuntimeClient({
|
|
456
|
+
...
|
|
457
|
+
incidentFilePath: "/etc/govplane/incident.json",
|
|
458
|
+
incidentFilePollMs: 1000,
|
|
459
|
+
incidentSignal: "SIGUSR1",
|
|
460
|
+
});
|
|
461
|
+
```
|
|
323
462
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
463
|
+
Incident file format:
|
|
464
|
+
```json
|
|
465
|
+
{
|
|
466
|
+
"burst": true,
|
|
467
|
+
"burstDurationMs": 60000,
|
|
468
|
+
"burstPollMs": 200,
|
|
469
|
+
"refreshNow": true
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
See [Incident Playbook](docs/operations/Govplane_Incident_Playbook.md) and [Incident Controls Reference](docs/operations/Govplane_Runtime_Incident_Controls.md) for step-by-step procedures.
|
|
330
474
|
|
|
331
475
|
---
|
|
332
476
|
|
|
333
|
-
##
|
|
477
|
+
## Using `createPolicyEngine` Directly
|
|
334
478
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
479
|
+
If you manage the bundle yourself (loaded from a file, injected from config), you can use the engine without `RuntimeClient`:
|
|
480
|
+
|
|
481
|
+
```typescript
|
|
482
|
+
import { createPolicyEngine } from "@govplane/runtime-sdk";
|
|
483
|
+
import { readFileSync } from "node:fs";
|
|
484
|
+
|
|
485
|
+
const bundle = JSON.parse(readFileSync("bundle.json", "utf8"));
|
|
340
486
|
|
|
341
|
-
|
|
487
|
+
const engine = createPolicyEngine({
|
|
488
|
+
getBundle: () => bundle,
|
|
489
|
+
parseCustomEffect: true,
|
|
490
|
+
contextPolicy: {
|
|
491
|
+
allowedKeys: ["plan", "role"],
|
|
492
|
+
maxStringLen: 64,
|
|
493
|
+
maxArrayLen: 10,
|
|
494
|
+
blockLikelyPiiKeys: true,
|
|
495
|
+
},
|
|
496
|
+
});
|
|
342
497
|
|
|
343
|
-
|
|
498
|
+
const result = engine.evaluate({ target, context });
|
|
499
|
+
```
|
|
344
500
|
|
|
345
501
|
---
|
|
346
502
|
|
|
347
|
-
##
|
|
503
|
+
## What This SDK Does NOT Do
|
|
348
504
|
|
|
349
|
-
-
|
|
350
|
-
-
|
|
351
|
-
-
|
|
505
|
+
- No HTTP middleware
|
|
506
|
+
- No inbound endpoints
|
|
507
|
+
- No request interception
|
|
508
|
+
- No PII handling or storage
|
|
509
|
+
- No policy authoring
|
|
510
|
+
- No dynamic code execution
|
|
511
|
+
- No rate-limit counter maintenance
|
|
352
512
|
|
|
353
513
|
---
|
|
354
514
|
|
|
355
|
-
##
|
|
515
|
+
## Security Notes
|
|
516
|
+
|
|
517
|
+
- Runtime keys are **read-only** and scoped by org / project / env
|
|
518
|
+
- Bundles are immutable — the SDK cannot modify control-plane state
|
|
519
|
+
- Context validation and PII heuristic blocking are on by default
|
|
520
|
+
- Safe to embed in critical paths
|
|
521
|
+
|
|
522
|
+
See [Govplane Runtime SDK Threat Model](docs/security/Govplane_Threat_Model.md) for a full security analysis.
|
|
523
|
+
|
|
524
|
+
---
|
|
356
525
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
526
|
+
## Documentation
|
|
527
|
+
|
|
528
|
+
| Topic | Link |
|
|
529
|
+
|---|---|
|
|
530
|
+
| Installation & Quick Start | [docs/installation/GettingStarted.md](docs/installation/GettingStarted.md) |
|
|
531
|
+
| Evaluating Decisions | [docs/usage/Evaluate.md](docs/usage/Evaluate.md) |
|
|
532
|
+
| Effect Types | [docs/usage/Effects.md](docs/usage/Effects.md) |
|
|
533
|
+
| Custom Effects | [docs/usage/CustomEffect.md](docs/usage/CustomEffect.md) |
|
|
534
|
+
| Conditional Rules | [docs/usage/ConditionalRules.md](docs/usage/ConditionalRules.md) |
|
|
535
|
+
| Policy Defaults | [docs/usage/PolicyDefaults.md](docs/usage/PolicyDefaults.md) |
|
|
536
|
+
| Context Policy & PII Safety | [docs/usage/ContextPolicy.md](docs/usage/ContextPolicy.md) |
|
|
537
|
+
| Bundle Lifecycle | [docs/usage/BundleLifecycle.md](docs/usage/BundleLifecycle.md) |
|
|
538
|
+
| Decision Tracing | [docs/usage/DecisionTrace.md](docs/usage/DecisionTrace.md) |
|
|
539
|
+
| Configuration Reference | [docs/reference/Configuration.md](docs/reference/Configuration.md) |
|
|
540
|
+
| Types & Interfaces | [docs/reference/TypesAndInterfaces.md](docs/reference/TypesAndInterfaces.md) |
|
|
541
|
+
| Incident Playbook | [docs/operations/Govplane_Incident_Playbook.md](docs/operations/Govplane_Incident_Playbook.md) |
|
|
542
|
+
| Incident Controls Reference | [docs/operations/Govplane_Runtime_Incident_Controls.md](docs/operations/Govplane_Runtime_Incident_Controls.md) |
|
|
543
|
+
| Threat Model | [docs/security/Govplane_Threat_Model.md](docs/security/Govplane_Threat_Model.md) |
|
|
360
544
|
|
|
361
545
|
---
|
|
362
546
|
|