Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c63e6a1
docs: add Plan 2 — control-plane TS foundation + data model
Jul 27, 2026
162c186
chore(control-plane): scaffold TypeScript project with deps and direc…
Jul 27, 2026
bf1e24b
feat(control-plane): add centralized config module
Jul 27, 2026
42612f3
feat(control-plane): add Postgres pool + withTransaction helper
Jul 27, 2026
f6a6088
feat(control-plane): add migration runner with advisory lock + checks…
Jul 27, 2026
c97e24b
feat(control-plane): migration 001 — principals, principal_keys, issuers
Jul 27, 2026
2481334
feat(control-plane): migration 002 — attestations, network_scores, ne…
Jul 27, 2026
ddf7e8e
feat(control-plane): migration 003 — sync_events, bootstrap_issuers
Jul 27, 2026
4b7ea19
feat(control-plane): migration 004 — tenants, users, tenant_membershi…
Jul 27, 2026
ac3593b
feat(control-plane): migration 005 — policies, edge_nodes, sync_cursors
Jul 27, 2026
d959272
feat(control-plane): migration 006 — subscriptions, decisions, audit_log
Jul 27, 2026
0a3b504
feat(control-plane): add AppError, response helpers, defineHandler, l…
Jul 27, 2026
6e2875c
feat(control-plane): add requestTracker, auth (OIDC+API key), rateLim…
Jul 27, 2026
669d5c7
feat(control-plane): add principal/issuer registry domain (repository…
Jul 27, 2026
c31b1ba
feat(control-plane): add attestation domain (submit, dedup, lazy subj…
Jul 27, 2026
4bfb7e2
feat(control-plane): add sync event domain (append, snapshot, event s…
Jul 27, 2026
821812b
feat(control-plane): add route handlers for principals, attestations,…
Jul 27, 2026
d9aef82
feat(control-plane): add Express app composition + server entry point
Jul 27, 2026
80ff1d9
chore(control-plane): add jose dependency for JWT verification
Jul 27, 2026
8b8feab
fix(control-plane): address all 29 CodeRabbit review comments
Jul 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions control-plane/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Database
DATABASE_URL=postgresql://verilink:verilink@localhost:5432/verilink

# Clerk OIDC
CLERK_ISSUER_URL=https://your-tenant.clerk.accounts.dev
CLERK_CLIENT_ID=
CLERK_CLIENT_SECRET=

# API key HMAC secret (generate: openssl rand -hex 32)
API_KEY_HMAC_SECRET=

# Trust engine gRPC
TRUST_ENGINE_ADDR=localhost:9091

# Server
PORT=3000
NODE_ENV=development
LOG_LEVEL=info

# Stripe
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
5 changes: 5 additions & 0 deletions control-plane/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules/
dist/
.env
Comment thread
coderabbitai[bot] marked this conversation as resolved.
.env.*
!.env.example
45 changes: 45 additions & 0 deletions control-plane/migrations/001_graph/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
-- control-plane/migrations/001_graph/migration.sql

-- Unified principals: agents and issuers share one namespace.
CREATE TABLE principals (
id TEXT PRIMARY KEY, -- vrl:p:<uuid>
entity_kind TEXT NOT NULL CHECK (entity_kind IN ('agent', 'issuer', 'both')),
name TEXT,
owner_tenant_id UUID, -- FK added in 004_tenancy
metadata JSONB DEFAULT '{}',
first_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'deactivated')),
deactivated_at TIMESTAMPTZ
);

-- assurance_level is DERIVED from principal_keys (has a non-revoked key with
-- control_verified_at set → verified_key; else unknown). Not a stored column.

-- Principal keys (rotation/history)
CREATE TABLE principal_keys (
principal_id TEXT NOT NULL REFERENCES principals(id),
key_id TEXT NOT NULL, -- e.g. k1
public_key_raw BYTEA NOT NULL, -- raw 32-byte Ed25519 public key
public_key_jwk JSONB NOT NULL, -- did:key verification method form
key_hash TEXT NOT NULL, -- sha256(public_key_raw); indexed for lookup
control_verified_at TIMESTAMPTZ, -- set when the principal proved control of this key
valid_from TIMESTAMPTZ NOT NULL DEFAULT now(),
valid_until TIMESTAMPTZ, -- null = current
revoked_at TIMESTAMPTZ,
revocation_reason TEXT,
PRIMARY KEY (principal_id, key_id)
);

-- A public key is globally unique by key_hash: one key belongs to at most
-- one principal, even across rotation/validity windows.
CREATE UNIQUE INDEX key_hash_unique ON principal_keys (key_hash);

-- Issuer attributes (a principal that can sign attestations)
CREATE TABLE issuers (
principal_id TEXT PRIMARY KEY REFERENCES principals(id),
trust_weight NUMERIC(3,2) NOT NULL DEFAULT 1.0 CHECK (trust_weight >= 0), -- issuer-quality knob; NOT touched by bootstrap de-emphasis
is_bootstrap BOOLEAN DEFAULT false, -- derived from bootstrap_issuers by the seeder
verified_at TIMESTAMPTZ, -- set after proof of key control + review
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
58 changes: 58 additions & 0 deletions control-plane/migrations/002_attestations/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
-- control-plane/migrations/002_attestations/migration.sql

-- Attestations: signed behavioral reports (global)
CREATE TABLE attestations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
issuer_id TEXT NOT NULL REFERENCES issuers(principal_id),
subject_id TEXT NOT NULL REFERENCES principals(id),
jws_token TEXT NOT NULL,
token_digest TEXT NOT NULL UNIQUE, -- sha256(jws_token); dedup
payload JSONB NOT NULL,
facts JSONB NOT NULL, -- shareable facts (public or participants)
facts_hash TEXT NOT NULL, -- sha256(RFC 8785 JCS(facts)); exact-content identity
visibility TEXT NOT NULL DEFAULT 'participants' CHECK (visibility IN ('participants', 'public')),
trust_delta INTEGER NOT NULL CHECK (trust_delta BETWEEN -100 AND 100),
attestation_type TEXT NOT NULL,
schema_version TEXT NOT NULL,
jti TEXT,
observation_id TEXT, -- for split-visibility pairing; null = no pairing
issued_at TIMESTAMPTZ NOT NULL,
expires_at TIMESTAMPTZ,
superseded_by UUID REFERENCES attestations(id),
sig_verified BOOLEAN NOT NULL DEFAULT true,
verified_key_id TEXT NOT NULL, -- which key verified (from VerifyResult)
received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK (
(attestation_type = 'negative_incident' AND trust_delta < 0)
OR (attestation_type <> 'negative_incident' AND trust_delta >= 0)
),
-- Composite FK: the verified key belongs to the issuer
FOREIGN KEY (issuer_id, verified_key_id) REFERENCES principal_keys(principal_id, key_id)
);

CREATE INDEX idx_attestations_subject ON attestations (subject_id);
CREATE INDEX idx_attestations_issuer ON attestations (issuer_id);
CREATE INDEX idx_attestations_issued_at ON attestations (issued_at);

-- Network scores: materialized VeriRank output (global)
CREATE TABLE network_scores (
principal_id TEXT NOT NULL REFERENCES principals(id) ON DELETE CASCADE,
entity_kind TEXT NOT NULL,
score INTEGER NOT NULL,
blacklisted BOOLEAN NOT NULL DEFAULT false,
score_reason TEXT NOT NULL CHECK (score_reason IN ('propagated', 'blacklisted')),
computed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
sync_version BIGINT NOT NULL,
PRIMARY KEY (principal_id)
);

-- Score history: one row per principal per score CHANGE
CREATE TABLE network_score_history (
principal_id TEXT NOT NULL REFERENCES principals(id) ON DELETE CASCADE,
score INTEGER NOT NULL,
blacklisted BOOLEAN NOT NULL,
score_reason TEXT NOT NULL,
computed_at TIMESTAMPTZ NOT NULL,
sync_version BIGINT NOT NULL,
PRIMARY KEY (principal_id, sync_version)
);
26 changes: 26 additions & 0 deletions control-plane/migrations/003_sync/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
-- control-plane/migrations/003_sync/migration.sql

-- Sync event log (unified, transactionally safe)
CREATE TABLE sync_events (
sync_version BIGINT PRIMARY KEY, -- allocated by the locked allocator, in-commit-order
event_type TEXT NOT NULL CHECK (event_type IN (
'score.upsert', 'score.delete', 'key.upsert', 'key.revoke', 'policy.replace'
)),
principal_id TEXT,
tenant_id UUID,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_sync_events_tenant ON sync_events (tenant_id) WHERE tenant_id IS NOT NULL;

-- Bootstrap registry (issuers only — roots are always issuers)
CREATE TABLE bootstrap_issuers (
principal_id TEXT PRIMARY KEY REFERENCES issuers(principal_id),
name TEXT NOT NULL,
current_weight NUMERIC(3,2) NOT NULL DEFAULT 1.0 CHECK (current_weight >= 0), -- written through to Root.weight
seeded_at TIMESTAMPTZ NOT NULL DEFAULT now(),
de_emphasized_at TIMESTAMPTZ,
de_emphasis_reason TEXT,
approved_by UUID
);
55 changes: 55 additions & 0 deletions control-plane/migrations/004_tenancy/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
-- control-plane/migrations/004_tenancy/migration.sql

CREATE EXTENSION IF NOT EXISTS citext;

-- Tenants
CREATE TABLE tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
slug TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
plan TEXT NOT NULL DEFAULT 'free',
status TEXT NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Global users
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email CITEXT UNIQUE NOT NULL,
oidc_issuer TEXT NOT NULL,
oidc_subject TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (oidc_issuer, oidc_subject)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
);

-- Tenant memberships
CREATE TABLE tenant_memberships (
user_id UUID NOT NULL REFERENCES users(id),
tenant_id UUID NOT NULL REFERENCES tenants(id),
role TEXT NOT NULL DEFAULT 'member',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (user_id, tenant_id)
);

-- API keys — HMAC-SHA256. Format: vrl_ + exactly 64 lowercase hex.
CREATE TABLE api_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
key_prefix TEXT NOT NULL,
key_hash_hmac TEXT NOT NULL,
scopes TEXT[] NOT NULL DEFAULT '{}',
last_used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
revoked_at TIMESTAMPTZ,
UNIQUE (tenant_id, id)
);

-- Now add the FK from principals to tenants
ALTER TABLE principals
ADD CONSTRAINT fk_principals_owner_tenant
FOREIGN KEY (owner_tenant_id) REFERENCES tenants(id);

-- Now add the FK from bootstrap_issuers to users
ALTER TABLE bootstrap_issuers
ADD CONSTRAINT fk_bootstrap_approved_by
FOREIGN KEY (approved_by) REFERENCES users(id);
48 changes: 48 additions & 0 deletions control-plane/migrations/005_policy/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
-- control-plane/migrations/005_policy/migration.sql

-- Policies: per-tenant threshold + actions
CREATE TABLE policies (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
name TEXT NOT NULL,
threshold INTEGER NOT NULL DEFAULT 50,
below_threshold_action TEXT NOT NULL DEFAULT 'deny' CHECK (below_threshold_action IN ('allow', 'deny')),
unsigned_action TEXT NOT NULL DEFAULT 'passthrough' CHECK (unsigned_action IN ('passthrough', 'deny')),
allow_fingerprints TEXT[] DEFAULT '{}',
deny_fingerprints TEXT[] DEFAULT '{}',
fail_open_expired BOOLEAN NOT NULL DEFAULT false,
no_drop_decisions BOOLEAN NOT NULL DEFAULT false,
max_snapshot_age_seconds INTEGER NOT NULL DEFAULT 300 CHECK (max_snapshot_age_seconds >= 0),
allow_sample_rate NUMERIC(4,3) NOT NULL DEFAULT 0.010 CHECK (allow_sample_rate >= 0 AND allow_sample_rate <= 1),
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (tenant_id, name)
);

-- One active policy per tenant (partial unique index)
CREATE UNIQUE INDEX active_policy_per_tenant ON policies (tenant_id) WHERE is_active;

-- Edge nodes
CREATE TABLE edge_nodes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
name TEXT NOT NULL,
api_key_id UUID,
last_seen_at TIMESTAMPTZ,
last_sync_version BIGINT,
status TEXT NOT NULL DEFAULT 'unknown',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (tenant_id, id),
FOREIGN KEY (tenant_id, api_key_id) REFERENCES api_keys(tenant_id, id)
);

-- Sync cursors
CREATE TABLE sync_cursors (
tenant_id UUID NOT NULL,
edge_node_id UUID NOT NULL,
last_cursor BIGINT NOT NULL DEFAULT 0,
last_sync_at TIMESTAMPTZ,
snapshot_hash TEXT,
PRIMARY KEY (tenant_id, edge_node_id),
FOREIGN KEY (tenant_id, edge_node_id) REFERENCES edge_nodes(tenant_id, id)
);
82 changes: 82 additions & 0 deletions control-plane/migrations/006_audit/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
-- control-plane/migrations/006_audit/migration.sql

-- Subscriptions (Stripe)
CREATE TABLE subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
stripe_customer_id TEXT NOT NULL,
stripe_subscription_id TEXT NOT NULL,
plan TEXT NOT NULL,
status TEXT NOT NULL,
current_period_end TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Stripe webhook event dedup (global)
CREATE TABLE stripe_webhook_events (
id TEXT PRIMARY KEY,
received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
processed_at TIMESTAMPTZ,
payload JSONB NOT NULL
);

-- Decision aggregates: per-minute rollup
CREATE TABLE decision_aggregates (
id BIGSERIAL PRIMARY KEY,
tenant_id UUID NOT NULL REFERENCES tenants(id),
edge_node_id UUID NOT NULL,
bucket_minute TIMESTAMPTZ NOT NULL,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
dimension_kind TEXT NOT NULL CHECK (dimension_kind IN ('all', 'principal', 'fingerprint')),
dimension_value TEXT NOT NULL, -- '' for 'all'; the principal_id or fingerprint otherwise
action TEXT NOT NULL CHECK (action IN ('allow', 'deny', 'passthrough')),
count INTEGER NOT NULL CHECK (count >= 0),
UNIQUE (tenant_id, edge_node_id, bucket_minute, dimension_kind, dimension_value, action),
FOREIGN KEY (tenant_id, edge_node_id) REFERENCES edge_nodes(tenant_id, id)
);

-- Decision samples: all denies + tunable % of allows/passthroughs
CREATE TABLE decision_samples (
id BIGSERIAL PRIMARY KEY,
tenant_id UUID NOT NULL REFERENCES tenants(id),
edge_node_id UUID NOT NULL,
wal_seq BIGINT NOT NULL,
fingerprint TEXT NOT NULL,
principal_id TEXT,
score INTEGER,
blacklisted BOOLEAN,
score_reason TEXT,
action TEXT NOT NULL CHECK (action IN ('allow', 'deny', 'passthrough')),
decided_at TIMESTAMPTZ NOT NULL,
FOREIGN KEY (tenant_id, edge_node_id) REFERENCES edge_nodes(tenant_id, id),
received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (edge_node_id, wal_seq)
);

-- Batch receipt (idempotent delivery)
CREATE TABLE decision_batches (
edge_node_id UUID NOT NULL,
batch_id UUID NOT NULL,
tenant_id UUID NOT NULL REFERENCES tenants(id),
first_wal_seq BIGINT NOT NULL,
last_wal_seq BIGINT NOT NULL,
payload_hash TEXT NOT NULL, -- sha256(batch payload)
received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (edge_node_id, batch_id),
CHECK (first_wal_seq <= last_wal_seq),
FOREIGN KEY (tenant_id, edge_node_id) REFERENCES edge_nodes(tenant_id, id)
);

-- Audit log: administrative/state-change events only (low volume)
CREATE TABLE audit_log (
id BIGSERIAL PRIMARY KEY,
tenant_id UUID NOT NULL REFERENCES tenants(id),
actor_type TEXT NOT NULL,
actor_id TEXT,
action TEXT NOT NULL,
resource TEXT NOT NULL,
resource_id TEXT,
metadata JSONB DEFAULT '{}',
ip TEXT,
user_agent TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
Loading