@govplane/runtime-sdk
v0.5.0
Published
Govplane Runtime SDK (Node/TS)
Downloads
108
Readme
@govplane/runtime-sdk
Official Govplane Runtime SDK for Node.js.
The Govplane Runtime SDK is a secure, production-grade client and policy engine designed to consume precompiled runtime governance bundles and evaluate decisions locally, with zero PII, no exposed endpoints, and minimal operational risk.
It is intended for backend services, workers, gateways, and critical paths that must react to policy changes in near real-time without delegating decisions to a remote service.
Architecture
┌─────────────────────────────────────────────────────────┐
│ Your Application │
│ │
│ client.evaluate({ target, context }) │
│ │ │
│ ▼ │
│ ┌─────────────────┐ bundle cache ┌───────────────┐ │
│ │ PolicyEngine │◄─────────────────│ RuntimeClient │ │
│ │ (in-process) │ │ (polling) │ │
│ └────────┬────────┘ └──────┬────────┘ │
│ │ Decision │ HTTP │
│ ▼ ▼ │
│ allow / deny / Govplane Control Plane │
│ throttle / kill_switch / (bundle endpoint only) │
│ custom │
└─────────────────────────────────────────────────────────┘| Property | Details | |---|---| | Decision latency | < 1 ms (in-process evaluation) | | Network dependency | Bundle fetch only — evaluation is offline-safe | | PII in traces | None — context is never included in trace events | | Fail-safe | Deny-by-default when bundle is missing or invalid | | Bundle delivery | Server-compiled JSON bundle, not evaluated server-side |
Design Principles
- No exposed endpoints – no middleware, no inbound surface
- Local-first evaluation – decisions are made in-process
- Zero PII by design – strict context validation and heuristic PII key blocking
- Precompiled policies – no DSL or dynamic code at runtime
- Deterministic outcomes – same input, same decision
- Cheap polling – HEAD + ETag + conditional GET
- Failure-safe – backoff, degraded mode, deny-by-default
Key Capabilities
Runtime Client
- Efficient HEAD-first polling using
ETagandIf-None-Match - In-memory bundle cache
warmStart()to block until first bundle is ready- Configurable polling interval
- Burst mode for incident response
- Exponential backoff with jitter
- Automatic degraded state with
nextRetryAtreporting - Status subscriptions via
onStatus()and bundle update subscriptions viaonUpdate()
Policy Engine
- Five effect types:
allow,deny,kill_switch,throttle,custom - Deny-by-default – no bundle or no match always denies
- Fixed precedence:
kill_switch > deny > throttle > allow > custom > deny-by-default - Throttle selects the most restrictive matching rule
thenEffect/elseEffectfor conditional branching within a single rule- Policy-level
defaultsas a fallback when no rule matches - Deterministic rule ordering by
priority, thenpolicyKey, thenruleId - Precompiled
whenAST evaluation — no dynamic code execution - Optional automatic JSON parsing of
customeffect values (parseCustomEffect)
Security & Context Safety
- Explicit context
allowedKeysallowlist — unknown keys throw immediately - Heuristic PII key blocking (
blockLikelyPiiKeys) - Configurable limits:
maxStringLen,maxArrayLen - Context policy is fully configurable per engine instance
- Disable validation only in controlled test environments
Decision Tracing
evaluate()for plain decisions;evaluateWithTrace()for traced decisions- Four trace levels:
off,errors,sampled,full - Sampling rate and per-window budget controls
force: trueto bypass sampling for debug sessions- Compact or full format (full includes per-rule breakdown)
- Synchronous and async trace sinks with bounded queues and drop strategies
flushTraces()for graceful shutdown
Requirements
- Node.js ≥ 18
- A valid Govplane Runtime Key (
rk_live_…orrk_test_…)
Installation
npm install @govplane/runtime-sdkyarn add @govplane/runtime-sdk
# or
pnpm add @govplane/runtime-sdkQuick Start
import { RuntimeClient } from "@govplane/runtime-sdk";
const client = new RuntimeClient({
baseUrl: "https://123456.runtime.govplane.com/",
runtimeKey: process.env.GP_RUNTIME_KEY!,
});
await client.warmStart(); // block until the first bundle is cached
client.start(); // begin background polling (every 5 s by default)
const result = client.evaluate({
target: { service: "payments", resource: "checkout", action: "create" },
context: { plan: "pro", country: "ES", isAuthenticated: true },
});
console.log(result.decision); // "allow" | "deny" | "throttle" | "kill_switch" | "custom"Effect Types
The engine returns one of five decision types, applied in this fixed precedence order:
kill_switch > deny > throttle > allow > custom > deny-by-default| decision | When it fires |
|---------------|---------------|
| allow | An allow rule matched, or the policy default is allow. |
| deny | A deny rule matched, no rule matched (deny-by-default), or the policy default is deny. |
| kill_switch | A kill_switch rule or policy default is active. Always wins. |
| throttle | A throttle rule or policy default matched. Most restrictive wins. |
| custom | A custom rule or policy default matched — carries an arbitrary string value. |
Decision Shape
evaluate() always returns a Decision object discriminated by decision:
type Decision =
| { decision: "allow";
reason: "default" | "rule"; policyKey?: string; ruleId?: string }
| { decision: "deny";
reason: "default" | "rule"; policyKey?: string; ruleId?: string }
| { decision: "kill_switch";
reason: "default" | "rule"; policyKey?: string; ruleId?: string;
killSwitch: { service: string; reason?: string } }
| { decision: "throttle";
reason: "default" | "rule"; policyKey?: string; ruleId?: string;
throttle: { limit: number; windowSeconds: number; key: string } }
| { decision: "custom";
reason: "default" | "rule"; policyKey?: string; ruleId?: string;
value: string; parsedValue?: unknown };Handling every decision type
const result = client.evaluate({ target, context });
if (result.decision === "allow") {
return next();
}
if (result.decision === "deny") {
return reply.status(403).send({ error: "Forbidden", policy: result.policyKey });
}
if (result.decision === "throttle") {
const allowed = await rateLimiter.check(
result.throttle.key === "tenant" ? ctx.tenantId : ctx.userId,
result.throttle.limit,
result.throttle.windowSeconds,
);
if (!allowed) return reply.status(429).header("Retry-After", String(result.throttle.windowSeconds)).send();
return next();
}
if (result.decision === "kill_switch") {
return reply.status(503).send({ error: "Service Unavailable", reason: result.killSwitch.reason });
}
if (result.decision === "custom") {
const payload = result.parsedValue ?? JSON.parse(result.value);
return reply.send(payload);
}Note: The SDK does not maintain rate-limit counters. It signals the throttle parameters; your infrastructure (Redis, middleware, etc.) is responsible for enforcement.
Custom Effects
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.
Enable automatic JSON parsing with engine.parseCustomEffect:
const client = new RuntimeClient({
baseUrl: "https://123456.runtime.govplane.com/",
runtimeKey: process.env.GP_RUNTIME_KEY!,
engine: {
parseCustomEffect: true, // JSON.parse() applied automatically
},
});
const result = client.evaluate({ target, context });
if (result.decision === "custom") {
const flags = result.parsedValue as { enabled: boolean; variant: string };
if (flags?.enabled) renderNewCheckout();
}Non-JSON strings leave parsedValue as undefined; the raw value string is always present as a fallback.
Conditional Rules (when / thenEffect / elseEffect)
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.
when absent → always apply effect
when == true → apply thenEffect (fallback: effect)
when == false → apply elseEffect (fallback: skip rule)Supported operators: eq, neq, gt, gte, lt, lte, in, exists, and, or, not.
The ctx. prefix on paths is optional and stripped automatically (ctx.plan ≡ plan).
Policy Defaults
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.
{
"policyKey": "strict-rbac",
"defaults": { "effect": "deny" },
"rules": [...]
}All five effect types are supported as defaults: allow, deny, throttle, kill_switch, custom.
Runtime Bundle Model
The SDK retrieves compiled runtime bundles generated by the Govplane control plane.
type RuntimeBundleV1 = {
schemaVersion: 1;
orgId: string;
projectId: string;
env: string;
generatedAt: string; // ISO timestamp
bundleVersion?: number;
checksum?: string; // e.g. "sha256:..."
policies: RuntimePolicy[];
};The bundle is treated as immutable and read-only. See Types & Interfaces for the full type tree.
Bundle Lifecycle & Polling
All evaluate() calls read from an in-memory cache — there is no network hop at decision time.
// Block until first bundle is ready, then start background polling
await client.warmStart({ timeoutMs: 10_000 });
client.start();Polling strategy per cycle:
HEADthe bundle endpoint to check the currentETag- If
ETagunchanged → update metadata, skip theGET - If
ETagchanged →GETthe full bundle, parse, update cache - Notify
onUpdatelisteners
| Phase | Interval |
|---|---|
| Normal | pollMs (default 5 000 ms) |
| Burst mode | burstPollMs (default 500 ms) for burstDurationMs (default 30 000 ms) |
| Backoff | Exponential, capped at backoffMaxMs |
Bundle update subscription
client.onUpdate((result) => {
logger.info("Bundle updated", {
etag: result.meta.etag,
bundleVersion: result.meta.bundleVersion,
updatedAt: result.meta.updatedAt,
});
});Backoff & Degraded Mode
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.
const client = new RuntimeClient({
...
backoffBaseMs: 500,
backoffMaxMs: 30_000,
backoffJitter: 0.2,
degradeAfterFailures: 3,
});
client.onStatus((status) => {
if (status.state === "degraded") {
alerting.trigger("govplane_sdk_degraded", {
failures: status.consecutiveFailures,
lastError: status.lastError.message,
nextRetryAt: status.nextRetryAt,
});
}
});Backoff formula: delay = clamp(base × 2^(failures-1), 0, backoffMaxMs) ± jitter%
Context Policy & PII Safety
The context object is validated synchronously before every evaluation. A violation throws immediately, before any rule is tested.
const client = new RuntimeClient({
...
engine: {
contextPolicy: {
allowedKeys: ["plan", "role", "country", "isAuthenticated", "requestTier"],
maxStringLen: 64,
maxArrayLen: 10,
blockLikelyPiiKeys: true, // blocks email, phone, name, ip, ssn, etc.
},
},
});Default allowed keys: plan, country, requestTier, feature, amount, isAuthenticated, role.
object values are not permitted at the top level. Permitted value types: string, number, boolean, null/undefined, string[].
Disable validation (
engine: { validateContext: false }) only in controlled test environments.
Decision Tracing
Use evaluateWithTrace() to attach a DecisionTrace to the result for observability and debugging. Traces contain only structural metadata — no context values or PII.
Production (sampled)
const client = new RuntimeClient({
...
trace: {
defaults: {
level: "sampled",
sampling: 0.05, // 5% of evaluations
budget: { maxTraces: 60, windowMs: 60_000 },
},
onDecisionTraceAsync: async (evt) => {
await analyticsClient.ingest("govplane.trace", evt);
},
queueMax: 1000,
dropPolicy: "drop_new",
},
});
const result = client.evaluateWithTrace({ target, context });
if (result.trace) {
console.log(result.trace.summary);
console.log(result.trace.winner);
}Debug (forced)
const result = client.evaluateWithTrace(
{ target, context },
{ level: "full", force: true }, // bypass sampling and budget
);| Level | Behaviour |
|---|---|
| "off" | No trace computed. Equivalent to evaluate(). |
| "errors" | Trace attached only on deny or kill_switch. Compact format. |
| "sampled" | Trace attached probabilistically by sampling rate and budget. Compact format. |
| "full" | Always attempted (budget permitting). Includes complete per-rule list. |
Trace sinks
Both synchronous (onDecisionTrace) and async (onDecisionTraceAsync) sinks are supported. Async evaluations are buffered internally and never block evaluation. Drain on shutdown with client.flushTraces().
Trace guarantees
- No rule bodies
- No context values
- No PII
- Deterministic structure
Runtime Incident Controls
Govplane provides passive incident controls that allow operators to respond to incidents without exposing endpoints, handling PII, or recompiling application code.
| Mechanism | Restart Required | Hot | Recommended |
|---|---|---|---|
| Environment Variable (GP_RUNTIME_INCIDENT=1) | Usually yes | No | Yes |
| File-based hot reload (incidentFilePath) | No | Yes | Primary |
| POSIX Signal (SIGUSR1) | No | Yes | Optional |
const client = new RuntimeClient({
...
incidentFilePath: "/etc/govplane/incident.json",
incidentFilePollMs: 1000,
incidentSignal: "SIGUSR1",
});Incident file format:
{
"burst": true,
"burstDurationMs": 60000,
"burstPollMs": 200,
"refreshNow": true
}See Incident Playbook and Incident Controls Reference for step-by-step procedures.
Using createPolicyEngine Directly
If you manage the bundle yourself (loaded from a file, injected from config), you can use the engine without RuntimeClient:
import { createPolicyEngine } from "@govplane/runtime-sdk";
import { readFileSync } from "node:fs";
const bundle = JSON.parse(readFileSync("bundle.json", "utf8"));
const engine = createPolicyEngine({
getBundle: () => bundle,
parseCustomEffect: true,
contextPolicy: {
allowedKeys: ["plan", "role"],
maxStringLen: 64,
maxArrayLen: 10,
blockLikelyPiiKeys: true,
},
});
const result = engine.evaluate({ target, context });What This SDK Does NOT Do
- No HTTP middleware
- No inbound endpoints
- No request interception
- No PII handling or storage
- No policy authoring
- No dynamic code execution
- No rate-limit counter maintenance
Security Notes
- Runtime keys are read-only and scoped by org / project / env
- Bundles are immutable — the SDK cannot modify control-plane state
- Context validation and PII heuristic blocking are on by default
- Safe to embed in critical paths
See Govplane Runtime SDK Threat Model for a full security analysis.
Documentation
| Topic | Link | |---|---| | Installation & Quick Start | docs/installation/GettingStarted.md | | Evaluating Decisions | docs/usage/Evaluate.md | | Effect Types | docs/usage/Effects.md | | Custom Effects | docs/usage/CustomEffect.md | | Conditional Rules | docs/usage/ConditionalRules.md | | Policy Defaults | docs/usage/PolicyDefaults.md | | Context Policy & PII Safety | docs/usage/ContextPolicy.md | | Bundle Lifecycle | docs/usage/BundleLifecycle.md | | Decision Tracing | docs/usage/DecisionTrace.md | | Configuration Reference | docs/reference/Configuration.md | | Types & Interfaces | docs/reference/TypesAndInterfaces.md | | Incident Playbook | docs/operations/Govplane_Incident_Playbook.md | | Incident Controls Reference | docs/operations/Govplane_Runtime_Incident_Controls.md | | Threat Model | docs/security/Govplane_Threat_Model.md |
License
MIT © Govplane
