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
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>
242 lines
7.1 KiB
TypeScript
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)
|
|
`;
|
|
}
|