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 { 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 { 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 { 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 { 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) `; }