@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
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: >-
|
|
3
|
+
Install the Govplane Runtime SDK and make your first policy evaluation in
|
|
4
|
+
under five minutes.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Installation & Quick Start
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
* **Node.js** 18 or later
|
|
12
|
+
* **TypeScript** 4.9+ (if using TypeScript)
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
{% tabs %}
|
|
17
|
+
{% tab title="npm" %}
|
|
18
|
+
```bash
|
|
19
|
+
npm install @govplane/runtime-sdk
|
|
20
|
+
```
|
|
21
|
+
{% endtab %}
|
|
22
|
+
|
|
23
|
+
{% tab title="yarn" %}
|
|
24
|
+
```bash
|
|
25
|
+
yarn add @govplane/runtime-sdk
|
|
26
|
+
```
|
|
27
|
+
{% endtab %}
|
|
28
|
+
|
|
29
|
+
{% tab title="pnpm" %}
|
|
30
|
+
```bash
|
|
31
|
+
pnpm add @govplane/runtime-sdk
|
|
32
|
+
```
|
|
33
|
+
{% endtab %}
|
|
34
|
+
{% endtabs %}
|
|
35
|
+
|
|
36
|
+
{% hint style="info" %}
|
|
37
|
+
`undici` is bundled in Node 18+. If you are targeting Node 16 install it explicitly: `npm install undici`.
|
|
38
|
+
{% endhint %}
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Quick start
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { RuntimeClient } from "@govplane/runtime-sdk";
|
|
46
|
+
|
|
47
|
+
const client = new RuntimeClient({
|
|
48
|
+
baseUrl: "https://runtime.govplane.io",
|
|
49
|
+
runtimeKey: "rk_live_••••••••",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await client.warmStart(); // block until the first bundle is cached
|
|
53
|
+
client.start(); // start background polling (every 5 s by default)
|
|
54
|
+
|
|
55
|
+
const result = client.evaluate({
|
|
56
|
+
target: { service: "api", resource: "documents", action: "read" },
|
|
57
|
+
context: { user: { role: "viewer" } },
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
console.log(result.decision); // "allow" | "deny" | "throttle" | "kill_switch" | "custom"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Effect types
|
|
66
|
+
|
|
67
|
+
The engine returns one of five decision types.
|
|
68
|
+
|
|
69
|
+
| `decision` | When it fires |
|
|
70
|
+
|----------------|--------------------------------------------------------------------|
|
|
71
|
+
| `allow` | An `allow` rule matched, or the policy default is `allow`. |
|
|
72
|
+
| `deny` | A `deny` rule matched, or no rule matched (deny-by-default). |
|
|
73
|
+
| `kill_switch` | A `kill_switch` rule or policy default is active. |
|
|
74
|
+
| `throttle` | A `throttle` rule or policy default matched. |
|
|
75
|
+
| `custom` | A `custom` rule or policy default matched (carries a string value).|
|
|
76
|
+
|
|
77
|
+
Precedence (highest → lowest): `kill_switch > deny > throttle > allow > custom > deny-by-default`
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Custom effects
|
|
82
|
+
|
|
83
|
+
A `custom` effect lets the bundle carry an arbitrary string — including a
|
|
84
|
+
JSON-encoded object — back to the caller. This is useful for feature-flag
|
|
85
|
+
responses, A/B test variants, or any structured metadata that does not fit the
|
|
86
|
+
standard allow/deny/throttle shape.
|
|
87
|
+
|
|
88
|
+
### Bundle shape
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"id": "rule-feature-flags",
|
|
93
|
+
"status": "active",
|
|
94
|
+
"priority": 10,
|
|
95
|
+
"target": { "service": "app", "resource": "feature/checkout-v2", "action": "read" },
|
|
96
|
+
"effect": {
|
|
97
|
+
"type": "custom",
|
|
98
|
+
"value": "{\"enabled\": true, \"variant\": \"B\"}"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Evaluating a custom effect (raw string)
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
const result = client.evaluate({
|
|
107
|
+
target: { service: "app", resource: "feature/checkout-v2", action: "read" },
|
|
108
|
+
context: { user: { id: "u_123" } },
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (result.decision === "custom") {
|
|
112
|
+
console.log(result.value); // '{"enabled": true, "variant": "B"}'
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Automatically parsing the JSON value
|
|
117
|
+
|
|
118
|
+
Pass `engine.parseCustomEffect: true` to have the SDK call `JSON.parse()`
|
|
119
|
+
on the custom value and attach the result as `parsedValue`. Non-JSON strings
|
|
120
|
+
are silently left as `undefined` in `parsedValue` so the raw `value` is
|
|
121
|
+
always available as a fallback.
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
const client = new RuntimeClient({
|
|
125
|
+
baseUrl: "https://runtime.govplane.io",
|
|
126
|
+
runtimeKey: "rk_live_••••••••",
|
|
127
|
+
engine: {
|
|
128
|
+
parseCustomEffect: true, // ← enable JSON parsing
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const result = client.evaluate({
|
|
133
|
+
target: { service: "app", resource: "feature/checkout-v2", action: "read" },
|
|
134
|
+
context: { user: { id: "u_123" } },
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (result.decision === "custom") {
|
|
138
|
+
const flags = result.parsedValue as { enabled: boolean; variant: string };
|
|
139
|
+
console.log(flags.enabled, flags.variant); // true "B"
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Using `createPolicyEngine` directly
|
|
144
|
+
|
|
145
|
+
If you manage the bundle yourself (e.g. loaded from a file), you can create
|
|
146
|
+
the engine without `RuntimeClient`:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import { createPolicyEngine } from "@govplane/runtime-sdk";
|
|
150
|
+
|
|
151
|
+
const bundle = JSON.parse(await fs.readFile("bundle.json", "utf8"));
|
|
152
|
+
|
|
153
|
+
const engine = createPolicyEngine({
|
|
154
|
+
getBundle: () => bundle,
|
|
155
|
+
parseCustomEffect: true,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const result = engine.evaluate({
|
|
159
|
+
target: { service: "api", resource: "public", action: "read" },
|
|
160
|
+
context: { user: { role: "editor" }, tenant: { flags: { premium: true } } },
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Policy-level default effects
|
|
167
|
+
|
|
168
|
+
Every policy can declare a `defaults` object that fires when **no rule in that
|
|
169
|
+
policy produces a match** for the requested target + context. Defaults support
|
|
170
|
+
all five effect types.
|
|
171
|
+
|
|
172
|
+
### `custom` default
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
{
|
|
176
|
+
"policyKey": "feature-flags",
|
|
177
|
+
"activeVersion": 7,
|
|
178
|
+
"defaults": {
|
|
179
|
+
"effect": "custom",
|
|
180
|
+
"customEffect": "{\"test\": true, \"allow\": true}"
|
|
181
|
+
},
|
|
182
|
+
"rules": []
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
When evaluated with `engine.parseCustomEffect: true` the decision is:
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
{
|
|
190
|
+
decision: "custom",
|
|
191
|
+
reason: "default",
|
|
192
|
+
policyKey: "feature-flags",
|
|
193
|
+
value: '{"test": true, "allow": true}',
|
|
194
|
+
parsedValue: { test: true, allow: true }
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### `allow` / `deny` defaults
|
|
199
|
+
|
|
200
|
+
```json
|
|
201
|
+
{
|
|
202
|
+
"policyKey": "open-api",
|
|
203
|
+
"defaults": { "effect": "allow" },
|
|
204
|
+
"rules": [
|
|
205
|
+
{
|
|
206
|
+
"id": "block-admin",
|
|
207
|
+
"status": "active",
|
|
208
|
+
"priority": 1,
|
|
209
|
+
"target": { "service": "api", "resource": "admin", "action": "*" },
|
|
210
|
+
"effect": { "type": "deny" }
|
|
211
|
+
}
|
|
212
|
+
]
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The `admin` resource is denied by the rule; everything else falls through to
|
|
217
|
+
the `allow` default.
|
|
218
|
+
|
|
219
|
+
### `throttle` default
|
|
220
|
+
|
|
221
|
+
```json
|
|
222
|
+
{
|
|
223
|
+
"policyKey": "rate-limited-api",
|
|
224
|
+
"defaults": {
|
|
225
|
+
"effect": "throttle",
|
|
226
|
+
"throttle": { "limit": 100, "windowSeconds": 60, "key": "tenant" }
|
|
227
|
+
},
|
|
228
|
+
"rules": []
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### `kill_switch` default
|
|
233
|
+
|
|
234
|
+
```json
|
|
235
|
+
{
|
|
236
|
+
"policyKey": "payments",
|
|
237
|
+
"defaults": {
|
|
238
|
+
"effect": "kill_switch",
|
|
239
|
+
"killSwitch": { "service": "payments", "reason": "Scheduled maintenance" }
|
|
240
|
+
},
|
|
241
|
+
"rules": []
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Conditional rules (`when` / `thenEffect` / `elseEffect`)
|
|
248
|
+
|
|
249
|
+
Rules can carry a `when` clause, which is a condition AST evaluated
|
|
250
|
+
client-side against the `context` you pass at call time.
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
const result = client.evaluate({
|
|
254
|
+
target: { service: "api", resource: "export", action: "create" },
|
|
255
|
+
context: {
|
|
256
|
+
user: { role: "editor", emailVerified: true },
|
|
257
|
+
tenant: { plan: "enterprise" },
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### `thenEffect` / `elseEffect`
|
|
263
|
+
|
|
264
|
+
These optional fields allow different effects depending on whether `when`
|
|
265
|
+
evaluates to `true` or `false`, without needing two separate rules.
|
|
266
|
+
|
|
267
|
+
```json
|
|
268
|
+
{
|
|
269
|
+
"id": "rule-beta-toggle",
|
|
270
|
+
"status": "active",
|
|
271
|
+
"priority": 10,
|
|
272
|
+
"target": { "service": "app", "resource": "feature/beta", "action": "read" },
|
|
273
|
+
|
|
274
|
+
"when": { "op": "eq", "path": "user.beta_tester", "value": true },
|
|
275
|
+
"thenEffect": { "type": "allow" },
|
|
276
|
+
"elseEffect": { "type": "deny" },
|
|
277
|
+
|
|
278
|
+
"effect": { "type": "deny" }
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
| `when` | Effect used |
|
|
283
|
+
|----------|---------------------------|
|
|
284
|
+
| `true` | `thenEffect` → `effect` |
|
|
285
|
+
| `false` | `elseEffect` → skip rule |
|
|
286
|
+
| absent | `effect` always |
|
|
287
|
+
|
|
288
|
+
### Condition operators
|
|
289
|
+
|
|
290
|
+
| `op` | Description | Example |
|
|
291
|
+
|----------|-----------------------------------------------|---------------------------------------------------------|
|
|
292
|
+
| `eq` | Equal | `{"op":"eq","path":"user.role","value":"admin"}` |
|
|
293
|
+
| `neq` | Not equal | `{"op":"neq","path":"tenant.plan","value":"free"}` |
|
|
294
|
+
| `gt/gte` | Greater than (or equal) | `{"op":"gte","path":"user.level","value":3}` |
|
|
295
|
+
| `lt/lte` | Less than (or equal) | `{"op":"lte","path":"request.size","value":1048576}` |
|
|
296
|
+
| `in` | Value is one of a list | `{"op":"in","path":"user.role","values":["admin","moderator"]}` |
|
|
297
|
+
| `exists` | Path is present and not null/undefined | `{"op":"exists","path":"tenant.flags.premium"}` |
|
|
298
|
+
| `and` | All child conditions must be true | see below |
|
|
299
|
+
| `or` | At least one child condition must be true | see below |
|
|
300
|
+
| `not` | Inverts a single child condition | `{"op":"not","condition":{"op":"eq","path":"user.status","value":"banned"}}` |
|
|
301
|
+
|
|
302
|
+
```json
|
|
303
|
+
{
|
|
304
|
+
"op": "and",
|
|
305
|
+
"conditions": [
|
|
306
|
+
{ "op": "eq", "path": "user.emailVerified", "value": true },
|
|
307
|
+
{
|
|
308
|
+
"op": "or",
|
|
309
|
+
"conditions": [
|
|
310
|
+
{ "op": "eq", "path": "user.role", "value": "admin" },
|
|
311
|
+
{ "op": "eq", "path": "tenant.plan", "value": "enterprise" }
|
|
312
|
+
]
|
|
313
|
+
}
|
|
314
|
+
]
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Real-world examples
|
|
321
|
+
|
|
322
|
+
### 1 — Feature flags with a JSON custom effect
|
|
323
|
+
|
|
324
|
+
The bundle encodes feature-flag state for the whole application in a single
|
|
325
|
+
`custom` policy. The SDK caller reads the parsed object and activates
|
|
326
|
+
features accordingly.
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
// Evaluate feature flags for the current user
|
|
330
|
+
const result = client.evaluate({
|
|
331
|
+
target: { service: "app", resource: "features", action: "read" },
|
|
332
|
+
context: { user: { id: "u_abc", plan: "pro" } },
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
if (result.decision === "custom" && result.parsedValue) {
|
|
336
|
+
const flags = result.parsedValue as Record<string, boolean>;
|
|
337
|
+
if (flags["new-checkout"]) enableNewCheckout();
|
|
338
|
+
if (flags["ai-suggestions"]) enableAI();
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Corresponding bundle rule:
|
|
343
|
+
|
|
344
|
+
```json
|
|
345
|
+
{
|
|
346
|
+
"id": "rule-feature-flags-pro",
|
|
347
|
+
"status": "active",
|
|
348
|
+
"priority": 20,
|
|
349
|
+
"target": { "service": "app", "resource": "features", "action": "read" },
|
|
350
|
+
"when": { "op": "eq", "path": "user.plan", "value": "pro" },
|
|
351
|
+
"thenEffect": {
|
|
352
|
+
"type": "custom",
|
|
353
|
+
"value": "{\"new-checkout\": true, \"ai-suggestions\": true}"
|
|
354
|
+
},
|
|
355
|
+
"elseEffect": {
|
|
356
|
+
"type": "custom",
|
|
357
|
+
"value": "{\"new-checkout\": false, \"ai-suggestions\": false}"
|
|
358
|
+
},
|
|
359
|
+
"effect": { "type": "deny" }
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### 2 — RBAC with policy default deny
|
|
364
|
+
|
|
365
|
+
Only admin/superuser roles can write settings. Everything else is denied by
|
|
366
|
+
the policy default, so no explicit deny rules are needed for other resources.
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
const result = client.evaluate({
|
|
370
|
+
target: { service: "control", resource: "settings", action: "write" },
|
|
371
|
+
context: { user: { role: "viewer" } },
|
|
372
|
+
});
|
|
373
|
+
// → { decision: "deny", reason: "default", policyKey: "rbac-settings" }
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Bundle:
|
|
377
|
+
|
|
378
|
+
```json
|
|
379
|
+
{
|
|
380
|
+
"policyKey": "rbac-settings",
|
|
381
|
+
"defaults": { "effect": "deny" },
|
|
382
|
+
"rules": [
|
|
383
|
+
{
|
|
384
|
+
"id": "allow-admins",
|
|
385
|
+
"status": "active",
|
|
386
|
+
"priority": 10,
|
|
387
|
+
"target": { "service": "control", "resource": "settings", "action": "write" },
|
|
388
|
+
"when": {
|
|
389
|
+
"op": "in",
|
|
390
|
+
"path": "user.role",
|
|
391
|
+
"values": ["admin", "superuser"]
|
|
392
|
+
},
|
|
393
|
+
"thenEffect": { "type": "allow" },
|
|
394
|
+
"effect": { "type": "deny" }
|
|
395
|
+
}
|
|
396
|
+
]
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### 3 — Plan-gated throttling with a custom default
|
|
401
|
+
|
|
402
|
+
Free-plan users are throttled. Paid users get a `custom` response carrying
|
|
403
|
+
their effective rate-limit metadata for the UI to display.
|
|
404
|
+
|
|
405
|
+
```typescript
|
|
406
|
+
const result = client.evaluate({
|
|
407
|
+
target: { service: "api", resource: "export", action: "create" },
|
|
408
|
+
context: { tenant: { plan: "free" } },
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
if (result.decision === "throttle") {
|
|
412
|
+
reply.status(429).send({ retryAfter: result.throttle.windowSeconds });
|
|
413
|
+
}
|
|
414
|
+
if (result.decision === "custom" && result.parsedValue) {
|
|
415
|
+
// pass metadata through to the client
|
|
416
|
+
reply.header("X-Rate-Info", JSON.stringify(result.parsedValue));
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### 4 — Kill switch via policy default
|
|
421
|
+
|
|
422
|
+
Immediately stop all traffic to the payments service by pushing a new bundle
|
|
423
|
+
with a `kill_switch` default. No code deployment required.
|
|
424
|
+
|
|
425
|
+
```json
|
|
426
|
+
{
|
|
427
|
+
"policyKey": "payments-circuit-breaker",
|
|
428
|
+
"defaults": {
|
|
429
|
+
"effect": "kill_switch",
|
|
430
|
+
"killSwitch": { "service": "payments", "reason": "Database degradation detected" }
|
|
431
|
+
},
|
|
432
|
+
"rules": []
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
```typescript
|
|
437
|
+
const result = client.evaluate({
|
|
438
|
+
target: { service: "payments", resource: "checkout", action: "create" },
|
|
439
|
+
context: { user: { id: "u_xyz" } },
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
if (result.decision === "kill_switch") {
|
|
443
|
+
throw new ServiceUnavailableError(result.killSwitch.reason);
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## Configuration reference
|
|
450
|
+
|
|
451
|
+
```typescript
|
|
452
|
+
new RuntimeClient({
|
|
453
|
+
// Required
|
|
454
|
+
baseUrl: "https://runtime.govplane.io",
|
|
455
|
+
runtimeKey: "rk_live_••••••••",
|
|
456
|
+
|
|
457
|
+
// Polling
|
|
458
|
+
pollMs: 5000, // default poll interval
|
|
459
|
+
burstPollMs: 500, // burst poll interval
|
|
460
|
+
burstDurationMs: 30000, // how long burst lasts
|
|
461
|
+
|
|
462
|
+
// Engine
|
|
463
|
+
engine: {
|
|
464
|
+
validateContext: true, // validate context shape (default true)
|
|
465
|
+
parseCustomEffect: true, // JSON-parse custom effect values (default false)
|
|
466
|
+
},
|
|
467
|
+
|
|
468
|
+
// Decision tracing (zero PII)
|
|
469
|
+
trace: {
|
|
470
|
+
defaults: { level: "sampled", sampling: 0.05 },
|
|
471
|
+
onDecisionTrace: (evt) => logger.info(evt),
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
```
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: >-
|
|
3
|
+
Complete reference for every RuntimeClientConfig field, with types, defaults,
|
|
4
|
+
and guidance on when to change them.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Configuration Reference
|
|
8
|
+
|
|
9
|
+
## `RuntimeClientConfig`
|
|
10
|
+
|
|
11
|
+
Passed to the `RuntimeClient` constructor.
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { RuntimeClient } from "@govplane/runtime-sdk";
|
|
15
|
+
|
|
16
|
+
const client = new RuntimeClient({ ...config });
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Required fields
|
|
22
|
+
|
|
23
|
+
| Field | Type | Description |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `baseUrl` | `string` | Base URL of the Govplane bundle endpoint. |
|
|
26
|
+
| `runtimeKey` | `string` | Runtime API key (`rk_live_…` or `rk_test_…`). |
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Polling
|
|
31
|
+
|
|
32
|
+
| Field | Type | Default | Description |
|
|
33
|
+
|---|---|---|---|
|
|
34
|
+
| `pollMs` | `number` | `5000` | Normal polling interval in milliseconds. |
|
|
35
|
+
| `burstPollMs` | `number` | `500` | Burst polling interval. Used during burst mode and incident activation. |
|
|
36
|
+
| `burstDurationMs` | `number` | `30000` | How long burst mode stays active after triggering (ms). |
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## HTTP
|
|
41
|
+
|
|
42
|
+
| Field | Type | Default | Description |
|
|
43
|
+
|---|---|---|---|
|
|
44
|
+
| `timeoutMs` | `number` | `5000` | HTTP request timeout in milliseconds (applies to each HEAD and GET). |
|
|
45
|
+
| `userAgent` | `string` | `govplane-runtime-sdk/0.x` | Custom `User-Agent` header value. |
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Backoff & degraded mode
|
|
50
|
+
|
|
51
|
+
| Field | Type | Default | Description |
|
|
52
|
+
|---|---|---|---|
|
|
53
|
+
| `backoffBaseMs` | `number` | `500` | Initial backoff delay in ms. The delay doubles with each consecutive failure. |
|
|
54
|
+
| `backoffMaxMs` | `number` | `30000` | Maximum backoff cap in ms. |
|
|
55
|
+
| `backoffJitter` | `number` | `0.2` | Jitter factor (`0..1`). The computed delay is varied by ±`backoffJitter × delay`. |
|
|
56
|
+
| `degradeAfterFailures` | `number` | `3` | Number of consecutive failures before the client reports `state: "degraded"`. |
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Engine
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
engine?: {
|
|
64
|
+
validateContext?: boolean; // default true
|
|
65
|
+
contextPolicy?: ContextPolicy;
|
|
66
|
+
parseCustomEffect?: boolean; // default false
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
| Field | Default | Description |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| `validateContext` | `true` | Validate the `context` object against `contextPolicy` before evaluation. Set to `false` only in controlled test environments. |
|
|
73
|
+
| `contextPolicy` | `DEFAULT_CONTEXT_POLICY` | Allowed keys, max string/array lengths, and PII heuristic. See [Context Policy](../usage/ContextPolicy.md). |
|
|
74
|
+
| `parseCustomEffect` | `false` | When `true`, automatically `JSON.parse()` the `value` of any `custom` decision and attach the result as `parsedValue`. Parse errors are silently swallowed. |
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Tracing
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
trace?: {
|
|
82
|
+
defaults?: TraceOptions;
|
|
83
|
+
onDecisionTrace?: (evt: StructuredTraceEvent) => void;
|
|
84
|
+
onDecisionTraceAsync?: (evt: StructuredTraceEvent) => Promise<void>;
|
|
85
|
+
queueMax?: number;
|
|
86
|
+
dropPolicy?: "drop_new" | "drop_old";
|
|
87
|
+
onTraceError?: (err: unknown) => void;
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
| Field | Default | Description |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| `defaults` | — | Default `TraceOptions` applied to every `evaluateWithTrace()` call. Can be overridden per call. |
|
|
94
|
+
| `onDecisionTrace` | — | Synchronous trace sink. Called immediately after each traced evaluation. |
|
|
95
|
+
| `onDecisionTraceAsync` | — | Async trace sink. Buffered internally; does not block evaluation. |
|
|
96
|
+
| `queueMax` | `1000` | Maximum events in the async queue before dropping begins. |
|
|
97
|
+
| `dropPolicy` | `"drop_new"` | How to handle overflow: `"drop_new"` discards the incoming event; `"drop_old"` evicts the oldest. |
|
|
98
|
+
| `onTraceError` | — | Called when either sink throws an error. |
|
|
99
|
+
|
|
100
|
+
### `TraceOptions`
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
type TraceOptions = {
|
|
104
|
+
level?: "off" | "errors" | "sampled" | "full";
|
|
105
|
+
sampling?: number; // 0..1, default 0.01
|
|
106
|
+
force?: boolean; // bypass sampling + budget
|
|
107
|
+
budget?: {
|
|
108
|
+
maxTraces: number;
|
|
109
|
+
windowMs: number;
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
| Field | Default | Description |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| `level` | `"sampled"` | Which evaluations produce a trace. |
|
|
117
|
+
| `sampling` | `0.01` | Fraction of evaluations traced when `level === "sampled"`. `1` = 100%. |
|
|
118
|
+
| `force` | `false` | Always produce a trace, bypassing sampling and budget. |
|
|
119
|
+
| `budget.maxTraces` | `60` | Maximum traces allowed per `budget.windowMs`. |
|
|
120
|
+
| `budget.windowMs` | `60000` | Sliding window for the trace budget (ms). |
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Incident controls
|
|
125
|
+
|
|
126
|
+
| Field | Type | Default | Description |
|
|
127
|
+
|---|---|---|---|
|
|
128
|
+
| `incidentEnvFlag` | `string` | `"GP_RUNTIME_INCIDENT"` | Environment variable name. Any truthy value activates burst polling. |
|
|
129
|
+
| `incidentFilePath` | `string` | — | Path to a JSON file polled for incident directives. |
|
|
130
|
+
| `incidentFilePollMs` | `number` | `1000` | How often the incident file is checked (ms). |
|
|
131
|
+
| `incidentSignal` | `"SIGUSR1" \| false` | `"SIGUSR1"` | Process signal that triggers an immediate burst refresh. Set to `false` to disable. |
|
|
132
|
+
|
|
133
|
+
### Incident file format
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"burst": true,
|
|
138
|
+
"burstDurationMs": 60000,
|
|
139
|
+
"burstPollMs": 200,
|
|
140
|
+
"refreshNow": true
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
See the [Incident Playbook](../operations/Govplane_Incident_Playbook.md) for step-by-step procedures.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Full example
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import { RuntimeClient } from "@govplane/runtime-sdk";
|
|
152
|
+
|
|
153
|
+
const client = new RuntimeClient({
|
|
154
|
+
baseUrl: "https://runtime.govplane.io",
|
|
155
|
+
runtimeKey: process.env.GP_RUNTIME_KEY!,
|
|
156
|
+
|
|
157
|
+
pollMs: 10_000,
|
|
158
|
+
burstPollMs: 300,
|
|
159
|
+
burstDurationMs: 60_000,
|
|
160
|
+
timeoutMs: 8_000,
|
|
161
|
+
|
|
162
|
+
backoffBaseMs: 1_000,
|
|
163
|
+
backoffMaxMs: 60_000,
|
|
164
|
+
backoffJitter: 0.3,
|
|
165
|
+
degradeAfterFailures: 5,
|
|
166
|
+
|
|
167
|
+
engine: {
|
|
168
|
+
validateContext: true,
|
|
169
|
+
parseCustomEffect: true,
|
|
170
|
+
contextPolicy: {
|
|
171
|
+
allowedKeys: ["plan", "role", "country", "isAuthenticated", "requestTier"],
|
|
172
|
+
maxStringLen: 64,
|
|
173
|
+
maxArrayLen: 10,
|
|
174
|
+
blockLikelyPiiKeys: true,
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
trace: {
|
|
179
|
+
defaults: {
|
|
180
|
+
level: "sampled",
|
|
181
|
+
sampling: 0.1,
|
|
182
|
+
budget: { maxTraces: 120, windowMs: 60_000 },
|
|
183
|
+
},
|
|
184
|
+
onDecisionTraceAsync: async (evt) => {
|
|
185
|
+
await logger.shipTrace(evt);
|
|
186
|
+
},
|
|
187
|
+
queueMax: 2000,
|
|
188
|
+
dropPolicy: "drop_old",
|
|
189
|
+
onTraceError: (err) => logger.error("trace error", err),
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
incidentFilePath: "/etc/govplane/incident.json",
|
|
193
|
+
incidentSignal: "SIGUSR1",
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
await client.warmStart({ timeoutMs: 15_000 });
|
|
197
|
+
client.start();
|
|
198
|
+
```
|