Multi-tenant reverse-tunnel service ("ngrok for Claude Code" with E2E): a
host-agent dials OUT to an operator-run relay; external devices reach the host
THROUGH the relay, routed by per-tenant subdomain, forwarding ciphertext only
(the relay never sees plaintext). Lets a customer reach their own self-hosted
web-terminal from anywhere with zero networking setup.
Packages — all tsc-strict + vitest green (656 tests), cross-package integration verified:
- relay-contracts: frozen shared contracts (mux frame codec, data model,
capability token, E2E envelope, pairing) — the src/types.ts analog
- term-relay: native WS mux + stateless data plane (subdomain routing, ciphertext forward)
- agent: host-agent (pairing, per-host Ed25519 + mTLS dial-out, forwards to 127.0.0.1:3000)
- control-plane: accounts/hosts registry, pairing-code flow, routing table, provisioning
- relay-e2e: browser<->agent E2E (X25519 ECDH through relay, AEAD, anti-replay, recoverable replay key)
- relay-auth: Passkey/WebAuthn, capability tokens, per-host certs, deny-by-default tenant isolation
- relay-web: browser login + Web Crypto E2E + client-side preview rendering
Security invariants INV1-15 enforced; cross-tenant isolation CI tripwire live
(.github/workflows/relay-tripwire.yml). Design + implementation-level plans in
docs/PLAN_RELAY_*.md and docs/EXPLORE_RELAY_SERVICE.md.
NOTE: generated autonomously per the reviewed plans. The security-critical
packages (relay-e2e, relay-auth) REQUIRE expert security audit before any real
deployment — passing tests prove self-consistency, not resistance to attackers.
Base app (src/, public/) unchanged; concurrent desktop work left uncommitted.
106 lines
5.8 KiB
SQL
106 lines
5.8 KiB
SQL
-- T2 — control-plane schema. Transcribes INDEX §4.2 (accounts/hosts/sessions/pairing_codes)
|
|
-- VERBATIM, plus P3-owned tables (relay_nodes, metering_samples, audit_log) and the INV8 companion
|
|
-- status-version tables + current-pointer columns (OQ6 — pending INDEX promotion of §4.2).
|
|
--
|
|
-- Discipline: no bare UPDATE of a lifecycle/business field without a matching *_status_versions
|
|
-- insert in the SAME transaction. Liveness columns (last_seen, last_attach_at) are advisory
|
|
-- caches (INV7 — live truth is Redis) and are exempt from versioning.
|
|
|
|
CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- gen_random_uuid()
|
|
|
|
-- ---- accounts (§4.2) --------------------------------------------------------------------------
|
|
CREATE TABLE IF NOT EXISTS accounts (
|
|
account_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- unguessable, never recycled
|
|
plan text NOT NULL, -- 'free'|'personal'|'pro'|'team'
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
status text NOT NULL DEFAULT 'active', -- 'active'|'suspended' (current pointer)
|
|
status_version int NOT NULL DEFAULT 1, -- INV8 monotonic pointer
|
|
CONSTRAINT accounts_plan_chk CHECK (plan IN ('free','personal','pro','team')),
|
|
CONSTRAINT accounts_status_chk CHECK (status IN ('active','suspended'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS account_status_versions (
|
|
account_id uuid NOT NULL REFERENCES accounts(account_id),
|
|
version int NOT NULL,
|
|
status text NOT NULL,
|
|
changed_at timestamptz NOT NULL DEFAULT now(),
|
|
changed_by text NOT NULL,
|
|
PRIMARY KEY (account_id, version)
|
|
);
|
|
|
|
-- ---- hosts (§4.2) — ownership source of truth --------------------------------------------------
|
|
CREATE TABLE IF NOT EXISTS hosts (
|
|
host_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- unguessable, NEVER recycled (INV1)
|
|
account_id uuid NOT NULL REFERENCES accounts(account_id),
|
|
subdomain text NOT NULL UNIQUE, -- stable across reconnects
|
|
agent_pubkey bytea NOT NULL, -- Ed25519 PUBLIC key only (INV4)
|
|
enroll_fpr text NOT NULL, -- fingerprint of agent_pubkey (E2E TOFU)
|
|
status text NOT NULL DEFAULT 'offline', -- 'online'|'offline'|'draining'|'revoked'
|
|
last_seen timestamptz NOT NULL DEFAULT now(), -- advisory cache (NOT versioned, INV7)
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
revoked_at timestamptz NULL, -- set on revocation (INV12)
|
|
status_version int NOT NULL DEFAULT 1,
|
|
CONSTRAINT hosts_status_chk CHECK (status IN ('online','offline','draining','revoked'))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS hosts_account_idx ON hosts(account_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS host_status_versions (
|
|
host_id uuid NOT NULL REFERENCES hosts(host_id),
|
|
version int NOT NULL,
|
|
status text NOT NULL,
|
|
revoked_at timestamptz NULL,
|
|
changed_at timestamptz NOT NULL DEFAULT now(),
|
|
changed_by text NOT NULL,
|
|
PRIMARY KEY (host_id, version)
|
|
);
|
|
|
|
-- ---- sessions (§4.2) --------------------------------------------------------------------------
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
session_id uuid PRIMARY KEY, -- base-app sessionId, opaque to relay
|
|
host_id uuid NOT NULL REFERENCES hosts(host_id),
|
|
account_id uuid NOT NULL, -- DENORMALIZED for O(1) authz (INV3/INV6)
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
last_attach_at timestamptz NOT NULL DEFAULT now() -- advisory cache (NOT versioned)
|
|
);
|
|
|
|
-- ---- pairing_codes (§4.2/§4.5) ----------------------------------------------------------------
|
|
CREATE TABLE IF NOT EXISTS pairing_codes (
|
|
code_hash text PRIMARY KEY, -- hash of single-use code; raw NEVER stored (INV5)
|
|
account_id uuid NOT NULL REFERENCES accounts(account_id),
|
|
expires_at timestamptz NOT NULL, -- short TTL
|
|
redeemed_at timestamptz NULL, -- single-use: non-null ⇒ spent
|
|
redeem_attempts int NOT NULL DEFAULT 0 -- code-scoped brute-force lockout (T8)
|
|
);
|
|
|
|
-- ---- relay_nodes (P3-owned) -------------------------------------------------------------------
|
|
CREATE TABLE IF NOT EXISTS relay_nodes (
|
|
node_id text PRIMARY KEY, -- SPIFFE-style id from mTLS cert subject
|
|
addr text NOT NULL,
|
|
status text NOT NULL DEFAULT 'active', -- 'active'|'draining'
|
|
last_seen timestamptz NOT NULL DEFAULT now(),
|
|
CONSTRAINT relay_nodes_status_chk CHECK (status IN ('active','draining'))
|
|
);
|
|
|
|
-- ---- metering_samples (P3-owned, append-only INV8) --------------------------------------------
|
|
CREATE TABLE IF NOT EXISTS metering_samples (
|
|
id bigserial PRIMARY KEY,
|
|
host_id uuid NOT NULL REFERENCES hosts(host_id),
|
|
account_id uuid NOT NULL, -- derived server-side from ownership (INV1)
|
|
node_id text NOT NULL, -- authenticated mTLS identity (INV3-analog)
|
|
concurrent_viewers int NOT NULL,
|
|
sampled_at timestamptz NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS metering_account_time_idx ON metering_samples(account_id, sampled_at);
|
|
|
|
-- ---- audit_log (P3-owned, append-only zero-payload INV10) -------------------------------------
|
|
CREATE TABLE IF NOT EXISTS audit_log (
|
|
id bigserial PRIMARY KEY,
|
|
action text NOT NULL,
|
|
principal_id text NOT NULL,
|
|
account_id text NOT NULL,
|
|
host_id text NULL,
|
|
ts timestamptz NOT NULL DEFAULT now(),
|
|
meta jsonb NOT NULL DEFAULT '{}'::jsonb -- metadata only; NO terminal payload (INV10)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS audit_account_time_idx ON audit_log(account_id, ts);
|