Files
Shade/packages/shade-storage-postgres/src/ensure-tables.ts
Sterister e6fdf31b49
Some checks failed
Test / test (push) Has been cancelled
Cross-platform vectors / TypeScript vectors (bun) (push) Has been cancelled
Cross-platform vectors / Kotlin vectors (gradle) (push) Has been cancelled
Docker build and publish / docker (push) Has been cancelled
Publish / publish (push) Has been cancelled
release(v4.0.0): Shade GA — V3.x consolidation + audit prep
V3.1 → V3.12 consolidated and tagged for the first GA release. Wire
format unchanged from 0.4.x — 4.0 peers interoperate with 0.4.x peers
byte-for-byte. The version bump is semantic: audit-cycle complete,
opt-in surface fully exposed, threat model refreshed for every new
surface.

Highlights:
- All 24 @shade/* packages bumped to 4.0.0 in lockstep.
- CHANGELOG 4.0.0 section is the canonical manifest of what landed.
- THREAT-MODEL extended (§10 fingerprint gates, §11 WebRTC P2P, §12
  Web-Worker boundary) + residual-risks table refreshed.
- OpenAPI now covers all 27 routes: prekey, transfer, KT, inbox,
  bridge, observer, /metrics, /healthz, /ready.
- MIGRATION 0.3.x → 4.0 documented + smoke-tested against
  shade migrate-storage on a real SQLite DB.
- docs/audit/REVIEW-BUNDLE.md + SCOPE.md ready for external reviewer.
- scripts/soak.ts harness for the GA-stable 2-week soak window.
- All V*.md plans archived under docs/archive/ with Status: Done.
- Voice/Video carved out into V5.0; 4.0 audit focuses on the frozen
  non-realtime stack.

Tests: TS 1000/1000 + Kotlin 11/11 cross-platform vectors green.
Docker: gt.zyon.no/stian/shade-prekey:4.0.0 builds and reports
  version 4.0.0 on /health.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:35:35 +02:00

242 lines
7.1 KiB
TypeScript

import type { Sql } from 'postgres';
/**
* Auto-create all Shade tables if they don't exist.
*
* Called on PostgresStorage / PostgresPrekeyStore construction.
* Uses raw SQL (not Drizzle migrations) for zero-config deployment.
*
* All tables prefixed with `shade_` to avoid collisions when sharing
* a PostgreSQL instance with another project.
*/
export async function ensureClientTables(sql: Sql): Promise<void> {
await sql`
CREATE TABLE IF NOT EXISTS shade_identity (
id INTEGER PRIMARY KEY CHECK (id = 1),
signing_public_key TEXT NOT NULL,
signing_private_key TEXT NOT NULL,
dh_public_key TEXT NOT NULL,
dh_private_key TEXT NOT NULL
)
`;
await sql`
CREATE TABLE IF NOT EXISTS shade_config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)
`;
await sql`
CREATE TABLE IF NOT EXISTS shade_signed_prekeys (
key_id INTEGER PRIMARY KEY,
data_json TEXT NOT NULL
)
`;
await sql`
CREATE TABLE IF NOT EXISTS shade_one_time_prekeys (
key_id INTEGER PRIMARY KEY,
data_json TEXT NOT NULL
)
`;
await sql`
CREATE TABLE IF NOT EXISTS shade_sessions (
address TEXT PRIMARY KEY,
state_json TEXT NOT NULL
)
`;
await sql`
CREATE TABLE IF NOT EXISTS shade_trusted_identities (
address TEXT PRIMARY KEY,
identity_key TEXT NOT NULL
)
`;
await sql`
CREATE TABLE IF NOT EXISTS shade_retired_identities (
id SERIAL PRIMARY KEY,
data_json TEXT NOT NULL,
retired_at BIGINT NOT NULL
)
`;
await sql`
CREATE INDEX IF NOT EXISTS shade_retired_at_idx ON shade_retired_identities(retired_at)
`;
await sql`
CREATE TABLE IF NOT EXISTS shade_stream_state (
stream_id TEXT PRIMARY KEY,
direction TEXT NOT NULL CHECK (direction IN ('send','receive')),
peer_address TEXT NOT NULL,
status TEXT NOT NULL CHECK (status IN ('active','paused','finished','aborted')),
metadata_json TEXT NOT NULL,
partition_json TEXT NOT NULL,
lane_state_json TEXT NOT NULL,
io_descriptor_json TEXT NOT NULL,
secret_enc BYTEA NOT NULL,
secret_nonce BYTEA NOT NULL,
overall_hash_state TEXT,
created_at BIGINT NOT NULL,
updated_at BIGINT NOT NULL
)
`;
await sql`
CREATE INDEX IF NOT EXISTS shade_stream_state_peer_idx
ON shade_stream_state(peer_address)
`;
await sql`
CREATE INDEX IF NOT EXISTS shade_stream_state_updated_idx
ON shade_stream_state(updated_at)
`;
await sql`
CREATE INDEX IF NOT EXISTS shade_stream_state_status_idx
ON shade_stream_state(status, direction)
`;
await sql`
CREATE TABLE IF NOT EXISTS shade_peer_verifications (
peer_address TEXT PRIMARY KEY,
fingerprint TEXT NOT NULL,
verified_at BIGINT NOT NULL,
verified_by TEXT NOT NULL,
identity_version BIGINT NOT NULL
)
`;
await sql`
CREATE TABLE IF NOT EXISTS shade_peer_identity_versions (
peer_address TEXT PRIMARY KEY,
version BIGINT NOT NULL
)
`;
}
export async function ensurePrekeyServerTables(sql: Sql): Promise<void> {
await sql`
CREATE TABLE IF NOT EXISTS shade_server_identities (
address TEXT PRIMARY KEY,
identity_signing_key TEXT NOT NULL,
identity_dh_key TEXT NOT NULL,
last_activity_at BIGINT NOT NULL DEFAULT 0
)
`;
// Migrate existing deployments (no-op if column exists)
await sql`
ALTER TABLE shade_server_identities
ADD COLUMN IF NOT EXISTS last_activity_at BIGINT NOT NULL DEFAULT 0
`;
await sql`
CREATE INDEX IF NOT EXISTS shade_server_identities_activity_idx
ON shade_server_identities(last_activity_at)
`;
await sql`
CREATE TABLE IF NOT EXISTS shade_server_signed_prekeys (
address TEXT PRIMARY KEY,
key_id INTEGER NOT NULL,
public_key TEXT NOT NULL,
signature TEXT NOT NULL
)
`;
await sql`
CREATE TABLE IF NOT EXISTS shade_server_one_time_prekeys (
id SERIAL PRIMARY KEY,
address TEXT NOT NULL,
key_id INTEGER NOT NULL,
public_key TEXT NOT NULL
)
`;
await sql`
CREATE INDEX IF NOT EXISTS shade_server_otp_address_idx ON shade_server_one_time_prekeys(address)
`;
}
/**
* Tables for the Key-Transparency log (V3.12).
*
* Append-only invariant for `shade_kt_leaves`:
* - Application code never UPDATEs or DELETEs leaves.
* - A trigger guards against accidental mutation in misconfigured ops:
* even a misbehaving DBA query is rejected, which protects the log
* from silent re-writes.
*/
export async function ensureKTLogTables(sql: Sql): Promise<void> {
await sql`
CREATE TABLE IF NOT EXISTS shade_kt_leaves (
leaf_index BIGINT PRIMARY KEY,
leaf_hash TEXT NOT NULL,
timestamp_ms BIGINT NOT NULL,
operation SMALLINT NOT NULL,
address TEXT NOT NULL,
bundle_hash TEXT NOT NULL
)
`;
await sql`
CREATE INDEX IF NOT EXISTS shade_kt_leaves_address_idx
ON shade_kt_leaves(address)
`;
await sql`
CREATE OR REPLACE FUNCTION shade_kt_block_mutations()
RETURNS trigger AS $$
BEGIN
RAISE EXCEPTION 'shade_kt_leaves is append-only: % rejected', TG_OP;
END;
$$ LANGUAGE plpgsql
`;
await sql`DROP TRIGGER IF EXISTS shade_kt_leaves_no_update ON shade_kt_leaves`;
await sql`
CREATE TRIGGER shade_kt_leaves_no_update
BEFORE UPDATE OR DELETE OR TRUNCATE ON shade_kt_leaves
FOR EACH STATEMENT
EXECUTE FUNCTION shade_kt_block_mutations()
`;
await sql`
CREATE TABLE IF NOT EXISTS shade_kt_index (
address TEXT PRIMARY KEY,
latest_leaf_index BIGINT NOT NULL,
bundle_hash TEXT NOT NULL,
deleted BOOLEAN NOT NULL DEFAULT FALSE
)
`;
await sql`
CREATE TABLE IF NOT EXISTS shade_kt_sths (
tree_size BIGINT NOT NULL,
timestamp_ms BIGINT NOT NULL,
root_hash TEXT NOT NULL,
index_root TEXT NOT NULL,
log_id TEXT NOT NULL,
signature TEXT NOT NULL,
PRIMARY KEY (tree_size, timestamp_ms, signature)
)
`;
await sql`
CREATE INDEX IF NOT EXISTS shade_kt_sths_timestamp_idx
ON shade_kt_sths(timestamp_ms DESC)
`;
}
export async function ensureInboxServerTables(sql: Sql): Promise<void> {
await sql`
CREATE TABLE IF NOT EXISTS shade_inbox_owners (
address TEXT PRIMARY KEY,
signing_key TEXT NOT NULL
)
`;
await sql`CREATE SEQUENCE IF NOT EXISTS shade_inbox_seq`;
await sql`
CREATE TABLE IF NOT EXISTS shade_inbox_blobs (
address TEXT NOT NULL,
msg_id TEXT NOT NULL,
ciphertext TEXT NOT NULL,
received_at BIGINT NOT NULL,
expires_at BIGINT NOT NULL,
PRIMARY KEY (address, msg_id)
)
`;
await sql`
CREATE INDEX IF NOT EXISTS shade_inbox_addr_expires_idx
ON shade_inbox_blobs(address, expires_at)
`;
await sql`
CREATE INDEX IF NOT EXISTS shade_inbox_addr_received_idx
ON shade_inbox_blobs(address, received_at)
`;
await sql`
CREATE INDEX IF NOT EXISTS shade_inbox_expires_idx
ON shade_inbox_blobs(expires_at)
`;
}