@gomagentic/verdict-control
v0.1.1
Published
Verdict control-plane services: policy lifecycle (draft/validate/simulate/publish/rollback), tenant bundle builder with Ed25519 signing, API key management.
Maintainers
Readme
@gomagentic/verdict-control
Control plane: policy lifecycle, signed bundle builder, accounts, API keys.
Part of Verdict — a serverless-first authorization engine. Policies (RBAC / ABAC / ReBAC) compile once and decide in microseconds, embedded in your app, behind a central PDP, or synced to the edge.
This is a server-side building block. Most applications consume the control plane through @gomagentic/verdict-server, which wraps these services in a REST + management API. Install this package directly only when you are building your own control plane.
Install
npm install @gomagentic/verdict-controlSigning bundles
A bundle is the unit of deployment: the tenant's published policies merged with ~global shared policies, compiled as one unit, content-hashed with SHA-256, and — when a signing key is configured — signed with Ed25519 over that content hash. @gomagentic/verdict-sync re-verifies both the hash and the signature at load, so a tampered or truncated bundle never reaches an engine.
Generate a key pair once and keep the private JWK in a secrets manager. Keys travel as JWK so they serialize cleanly; the keyId rides along in the signed envelope.
import {
BundleBuilder,
generateSigningKeyPair,
verifyBundleSignature,
} from "@gomagentic/verdict-control";
const keyPair = await generateSigningKeyPair(); // { keyId, publicKeyJwk, privateKeyJwk }
const builder = new BundleBuilder({
store, // a @gomagentic/verdict-store ControlPlaneStore
signing: { keyId: keyPair.keyId, privateKeyJwk: keyPair.privateKeyJwk },
});
const { record, envelope } = await builder.buildForTenant(tenantId, "deploy-bot");
// envelope.contentHash is SHA-256 over the sealed bundle;
// envelope.signature is Ed25519 over that hash, tagged with envelope.signingKeyId.
const ok = await verifyBundleSignature(envelope, keyPair.publicKeyJwk);buildForTenant() returns a BuildResult — { record, envelope }, where record is the persisted BundleRecord (also set as the tenant's latest) and envelope is the full signed BundleEnvelope. Large artifacts overflow to object storage; configure BundleBuilder's overflow option (R2/S3) when a tenant's bundle can exceed the inline limit.
For lower-level control, signContentHash(contentHash, privateKeyJwk) produces the raw base64 signature, and verifyContentHash / verifyBundleSignature (re-exported from @gomagentic/verdict-core) check it.
Policy lifecycle
PolicyService drives the full lifecycle over a @gomagentic/verdict-store ControlPlaneStore. Publishing compiles the merged tenant set, freezes an immutable version, and rebuilds the affected bundle(s) — every tenant's bundle when a ~global policy changes.
import { PolicyService } from "@gomagentic/verdict-control";
const policies = new PolicyService({
store,
signing: { keyId: keyPair.keyId, privateKeyJwk: keyPair.privateKeyJwk },
});Methods:
createPolicy(input)/updateDraft(policyId, input, expectedRevision)— draft authoring (optimistic concurrency on the revision).validate(policyId)— compile the draft against the merged set; returns{ valid, diagnostics }.simulate(policyId, request, opts)— dry-run aCheckRequestagainst an ephemeral engine, optionally with the draft substituted in; returns aDecision.publish(policyId, { changeNote, publishedBy })— freeze a new immutable version and rebuild bundles; returns{ version, build, rebuiltTenants }.rollback(policyId, toVersion, { publishedBy })— republish an older version's content as a new version (history is append-only; nothing is mutated or deleted).history(policyId)/rebuildBundle(tenantId, createdBy)— version listing and out-of-band rebuilds.
Set onBundleRebuilt to a callback and the PDP can hot-reload engines each time a tenant's bundle is rebuilt.
Accounts & API keys
AccountService(store, options?) handles console signup/login with PBKDF2-SHA256 password hashing (hashPassword, DEFAULT_PBKDF2_ITERATIONS = 210k) and opaque vs_… session tokens stored only as SHA-256 hashes; verifySession returns SessionInfo. On Cloudflare Workers, pass iterations: WORKERS_MAX_PBKDF2_ITERATIONS (100k) — Workers rejects PBKDF2 above that outright.
ApiKeyService(store, env?) issues machine credentials: issue() returns an IssuedApiKey whose full secret is shown exactly once (only its SHA-256 is stored), and verify() checks a presented key with constantTimeEqual.
Documentation
License
Apache-2.0
