480 lines
20 KiB
SQL
480 lines
20 KiB
SQL
-- EncryptID PostgreSQL Schema
|
|
-- Run once to initialize the database
|
|
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id TEXT PRIMARY KEY,
|
|
username TEXT UNIQUE NOT NULL,
|
|
display_name TEXT,
|
|
did TEXT,
|
|
email TEXT,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
|
|
|
-- Profile columns (added for user profile management)
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS bio TEXT;
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar_url TEXT;
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS profile_email TEXT;
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS profile_email_is_recovery BOOLEAN DEFAULT FALSE;
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS wallet_address TEXT;
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW();
|
|
|
|
-- Email forwarding (Mailcow alias)
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS email_forward_enabled BOOLEAN DEFAULT FALSE;
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS email_forward_mailcow_id TEXT;
|
|
|
|
CREATE TABLE IF NOT EXISTS credentials (
|
|
credential_id TEXT PRIMARY KEY,
|
|
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
public_key TEXT NOT NULL,
|
|
counter INTEGER DEFAULT 0,
|
|
transports TEXT[],
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
last_used TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_credentials_user_id ON credentials(user_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS challenges (
|
|
challenge TEXT PRIMARY KEY,
|
|
user_id TEXT,
|
|
type TEXT NOT NULL CHECK (type IN ('registration', 'authentication', 'device_registration', 'wallet_link')),
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
expires_at TIMESTAMPTZ NOT NULL
|
|
);
|
|
|
|
-- Auto-clean expired challenges (run periodically or use pg_cron)
|
|
CREATE INDEX IF NOT EXISTS idx_challenges_expires_at ON challenges(expires_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS recovery_tokens (
|
|
token TEXT PRIMARY KEY,
|
|
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
type TEXT NOT NULL CHECK (type IN ('email_verify', 'account_recovery')),
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
used BOOLEAN DEFAULT FALSE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_recovery_tokens_user_id ON recovery_tokens(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_recovery_tokens_expires_at ON recovery_tokens(expires_at);
|
|
|
|
-- Space membership: source of truth for user roles across the r*.online ecosystem
|
|
CREATE TABLE IF NOT EXISTS space_members (
|
|
space_slug TEXT NOT NULL,
|
|
user_did TEXT NOT NULL,
|
|
role TEXT NOT NULL CHECK (role IN ('viewer', 'participant', 'moderator', 'admin')),
|
|
joined_at TIMESTAMPTZ DEFAULT NOW(),
|
|
granted_by TEXT,
|
|
PRIMARY KEY (space_slug, user_did)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_space_members_user_did ON space_members(user_did);
|
|
CREATE INDEX IF NOT EXISTS idx_space_members_space_slug ON space_members(space_slug);
|
|
|
|
-- ============================================================================
|
|
-- GUARDIAN RECOVERY (2-of-3 social recovery)
|
|
-- ============================================================================
|
|
|
|
-- Guardians: up to 3 trusted contacts per account
|
|
CREATE TABLE IF NOT EXISTS guardians (
|
|
id TEXT PRIMARY KEY,
|
|
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL, -- display name (e.g. "Mom", "Alex")
|
|
email TEXT, -- for sending invite/approval emails
|
|
guardian_user_id TEXT REFERENCES users(id), -- if they have an EncryptID account
|
|
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'accepted', 'revoked')),
|
|
invite_token TEXT UNIQUE, -- token for invite link
|
|
invite_expires_at TIMESTAMPTZ,
|
|
accepted_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_guardians_user_id ON guardians(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_guardians_guardian_user_id ON guardians(guardian_user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_guardians_invite_token ON guardians(invite_token);
|
|
|
|
-- Recovery requests: when a user needs to recover their account
|
|
CREATE TABLE IF NOT EXISTS recovery_requests (
|
|
id TEXT PRIMARY KEY,
|
|
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'cancelled', 'completed', 'expired')),
|
|
threshold INTEGER NOT NULL DEFAULT 2, -- approvals needed (2-of-3)
|
|
approval_count INTEGER NOT NULL DEFAULT 0,
|
|
initiated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
expires_at TIMESTAMPTZ NOT NULL, -- 7 days to collect approvals
|
|
completed_at TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_recovery_requests_user_id ON recovery_requests(user_id);
|
|
|
|
-- Recovery approvals: guardian votes on a recovery request
|
|
CREATE TABLE IF NOT EXISTS recovery_approvals (
|
|
request_id TEXT NOT NULL REFERENCES recovery_requests(id) ON DELETE CASCADE,
|
|
guardian_id TEXT NOT NULL REFERENCES guardians(id) ON DELETE CASCADE,
|
|
approval_token TEXT UNIQUE, -- token sent via email/link for one-click approve
|
|
approved_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
PRIMARY KEY (request_id, guardian_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_recovery_approvals_token ON recovery_approvals(approval_token);
|
|
|
|
-- Device link tokens: for linking a second device via QR or email
|
|
CREATE TABLE IF NOT EXISTS device_links (
|
|
token TEXT PRIMARY KEY,
|
|
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
expires_at TIMESTAMPTZ NOT NULL, -- 10 minutes
|
|
used BOOLEAN DEFAULT FALSE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_device_links_user_id ON device_links(user_id);
|
|
|
|
-- ============================================================================
|
|
-- ENCRYPTED POSTAL ADDRESSES (zero-knowledge address storage)
|
|
-- ============================================================================
|
|
|
|
-- Addresses are encrypted client-side with passkey-derived AES-256-GCM keys.
|
|
-- The server stores opaque ciphertext + cleartext metadata (label, isDefault) for UI listing.
|
|
CREATE TABLE IF NOT EXISTS encrypted_addresses (
|
|
id TEXT NOT NULL,
|
|
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
ciphertext TEXT NOT NULL,
|
|
iv TEXT NOT NULL,
|
|
label TEXT NOT NULL CHECK (label IN ('home', 'work', 'shipping', 'billing', 'other')),
|
|
label_custom TEXT,
|
|
is_default BOOLEAN DEFAULT FALSE,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
PRIMARY KEY (id, user_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_encrypted_addresses_user_id ON encrypted_addresses(user_id);
|
|
|
|
-- ============================================================================
|
|
-- ROLE RENAME: participant → member
|
|
-- ============================================================================
|
|
|
|
UPDATE space_members SET role = 'member' WHERE role = 'participant';
|
|
ALTER TABLE space_members DROP CONSTRAINT IF EXISTS space_members_role_check;
|
|
ALTER TABLE space_members ADD CONSTRAINT space_members_role_check
|
|
CHECK (role IN ('viewer', 'member', 'moderator', 'admin'));
|
|
|
|
-- ============================================================================
|
|
-- SPACE INVITES
|
|
-- ============================================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS space_invites (
|
|
id TEXT PRIMARY KEY,
|
|
space_slug TEXT NOT NULL,
|
|
email TEXT,
|
|
role TEXT NOT NULL DEFAULT 'member'
|
|
CHECK (role IN ('viewer', 'member', 'moderator', 'admin')),
|
|
token TEXT UNIQUE NOT NULL,
|
|
invited_by TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'pending'
|
|
CHECK (status IN ('pending', 'accepted', 'expired', 'revoked')),
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
accepted_at TIMESTAMPTZ,
|
|
accepted_by_did TEXT
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_space_invites_token ON space_invites(token);
|
|
CREATE INDEX IF NOT EXISTS idx_space_invites_space ON space_invites(space_slug);
|
|
|
|
-- ============================================================================
|
|
-- NOTIFICATIONS
|
|
-- ============================================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS notifications (
|
|
id TEXT PRIMARY KEY,
|
|
user_did TEXT NOT NULL,
|
|
category TEXT NOT NULL CHECK (category IN ('space', 'module', 'system', 'social')),
|
|
event_type TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
body TEXT,
|
|
space_slug TEXT,
|
|
module_id TEXT,
|
|
action_url TEXT,
|
|
actor_did TEXT,
|
|
actor_username TEXT,
|
|
metadata JSONB DEFAULT '{}',
|
|
read BOOLEAN DEFAULT FALSE,
|
|
dismissed BOOLEAN DEFAULT FALSE,
|
|
delivered_ws BOOLEAN DEFAULT FALSE,
|
|
delivered_email BOOLEAN DEFAULT FALSE,
|
|
delivered_push BOOLEAN DEFAULT FALSE,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
read_at TIMESTAMPTZ,
|
|
expires_at TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_notif_user_unread ON notifications(user_did, read)
|
|
WHERE NOT read AND NOT dismissed;
|
|
CREATE INDEX IF NOT EXISTS idx_notif_user_created ON notifications(user_did, created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_notif_expires ON notifications(expires_at)
|
|
WHERE expires_at IS NOT NULL;
|
|
|
|
CREATE TABLE IF NOT EXISTS notification_preferences (
|
|
user_did TEXT PRIMARY KEY,
|
|
email_enabled BOOLEAN DEFAULT TRUE,
|
|
push_enabled BOOLEAN DEFAULT TRUE,
|
|
quiet_hours_start TEXT,
|
|
quiet_hours_end TEXT,
|
|
muted_spaces TEXT[] DEFAULT '{}',
|
|
muted_categories TEXT[] DEFAULT '{}',
|
|
digest_frequency TEXT DEFAULT 'none'
|
|
CHECK (digest_frequency IN ('none', 'daily', 'weekly')),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- ============================================================================
|
|
-- FUND CLAIMS (on-ramp claim-via-EncryptID flow)
|
|
-- ============================================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS fund_claims (
|
|
id TEXT PRIMARY KEY,
|
|
token TEXT UNIQUE NOT NULL,
|
|
email_hash TEXT NOT NULL,
|
|
email TEXT, -- nulled after claim or 7 days
|
|
wallet_address TEXT NOT NULL,
|
|
openfort_player_id TEXT,
|
|
fiat_amount TEXT,
|
|
fiat_currency TEXT DEFAULT 'USD',
|
|
session_id TEXT,
|
|
provider TEXT, -- 'coinbase' | 'transak'
|
|
status TEXT NOT NULL DEFAULT 'pending'
|
|
CHECK (status IN ('pending', 'claimed', 'expired', 'resent')),
|
|
claimed_by_user_id TEXT REFERENCES users(id),
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
claimed_at TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_fund_claims_token ON fund_claims(token);
|
|
CREATE INDEX IF NOT EXISTS idx_fund_claims_email_hash ON fund_claims(email_hash);
|
|
CREATE INDEX IF NOT EXISTS idx_fund_claims_expires ON fund_claims(expires_at);
|
|
|
|
-- ============================================================================
|
|
-- OIDC PROVIDER (Authorization Code flow for Postiz, etc.)
|
|
-- ============================================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS oidc_clients (
|
|
client_id TEXT PRIMARY KEY,
|
|
client_secret TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
redirect_uris TEXT[] NOT NULL,
|
|
allowed_emails TEXT[] DEFAULT '{}', -- empty = unrestricted
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS oidc_auth_codes (
|
|
code TEXT PRIMARY KEY,
|
|
client_id TEXT NOT NULL REFERENCES oidc_clients(client_id),
|
|
user_id TEXT NOT NULL REFERENCES users(id),
|
|
redirect_uri TEXT NOT NULL,
|
|
scope TEXT DEFAULT 'openid profile email',
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
used BOOLEAN DEFAULT FALSE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_oidc_auth_codes_expires ON oidc_auth_codes(expires_at);
|
|
|
|
-- ============================================================================
|
|
-- IDENTITY INVITES ("Claim your rSpace" flow)
|
|
-- ============================================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS identity_invites (
|
|
id TEXT PRIMARY KEY,
|
|
token TEXT UNIQUE NOT NULL,
|
|
email TEXT NOT NULL,
|
|
invited_by_user_id TEXT NOT NULL REFERENCES users(id),
|
|
invited_by_username TEXT NOT NULL,
|
|
message TEXT, -- optional personal message
|
|
space_slug TEXT, -- auto-join this space on claim
|
|
space_role TEXT DEFAULT 'member'
|
|
CHECK (space_role IN ('viewer', 'member', 'moderator', 'admin')),
|
|
status TEXT NOT NULL DEFAULT 'pending'
|
|
CHECK (status IN ('pending', 'claimed', 'expired', 'revoked')),
|
|
claimed_by_user_id TEXT REFERENCES users(id),
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
claimed_at TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_identity_invites_token ON identity_invites(token);
|
|
CREATE INDEX IF NOT EXISTS idx_identity_invites_email ON identity_invites(email);
|
|
CREATE INDEX IF NOT EXISTS idx_identity_invites_invited_by ON identity_invites(invited_by_user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_identity_invites_expires ON identity_invites(expires_at);
|
|
|
|
-- OIDC client invite: optional link to an OIDC client for "Accept Your Role" flow
|
|
ALTER TABLE identity_invites ADD COLUMN IF NOT EXISTS client_id TEXT REFERENCES oidc_clients(client_id);
|
|
CREATE INDEX IF NOT EXISTS idx_identity_invites_client_id ON identity_invites(client_id);
|
|
|
|
-- ============================================================================
|
|
-- CHALLENGES: extend type constraint to include 'wallet_link'
|
|
-- ============================================================================
|
|
|
|
ALTER TABLE challenges DROP CONSTRAINT IF EXISTS challenges_type_check;
|
|
ALTER TABLE challenges ADD CONSTRAINT challenges_type_check
|
|
CHECK (type IN ('registration', 'authentication', 'device_registration', 'wallet_link', 'legacy_migration'));
|
|
|
|
-- ============================================================================
|
|
-- LINKED EXTERNAL WALLETS (SIWE-verified wallet associations)
|
|
-- ============================================================================
|
|
|
|
-- Server stores encrypted blobs + address hash for dedup.
|
|
-- Cleartext wallet data is never stored server-side.
|
|
CREATE TABLE IF NOT EXISTS linked_wallets (
|
|
id TEXT NOT NULL,
|
|
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
ciphertext TEXT NOT NULL,
|
|
iv TEXT NOT NULL,
|
|
address_hash TEXT NOT NULL,
|
|
source TEXT NOT NULL CHECK (source IN ('external-eoa', 'external-safe')),
|
|
verified BOOLEAN DEFAULT FALSE,
|
|
linked_at TIMESTAMPTZ DEFAULT NOW(),
|
|
PRIMARY KEY (id, user_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_linked_wallets_user_id ON linked_wallets(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_linked_wallets_address_hash ON linked_wallets(address_hash);
|
|
|
|
-- ============================================================================
|
|
-- PUSH SUBSCRIPTIONS (Web Push notifications)
|
|
-- ============================================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS push_subscriptions (
|
|
id TEXT PRIMARY KEY,
|
|
user_did TEXT NOT NULL,
|
|
endpoint TEXT NOT NULL,
|
|
key_p256dh TEXT NOT NULL,
|
|
key_auth TEXT NOT NULL,
|
|
user_agent TEXT,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
last_used TIMESTAMPTZ,
|
|
UNIQUE (user_did, endpoint)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_push_sub_user ON push_subscriptions(user_did);
|
|
|
|
-- Prevent duplicate wallet links per user (application-level check + DB enforcement)
|
|
DO $$ BEGIN
|
|
ALTER TABLE linked_wallets ADD CONSTRAINT linked_wallets_user_address_unique
|
|
UNIQUE (user_id, address_hash);
|
|
EXCEPTION WHEN duplicate_table THEN NULL;
|
|
END $$;
|
|
|
|
-- ============================================================================
|
|
-- DELEGATIVE TRUST (person-to-person liquid democracy)
|
|
-- ============================================================================
|
|
|
|
-- Delegations: person-to-person authority delegation within a space
|
|
CREATE TABLE IF NOT EXISTS delegations (
|
|
id TEXT PRIMARY KEY,
|
|
delegator_did TEXT NOT NULL,
|
|
delegate_did TEXT NOT NULL,
|
|
authority TEXT NOT NULL CHECK (authority IN ('gov-ops', 'fin-ops', 'dev-ops', 'custom')),
|
|
weight REAL NOT NULL CHECK (weight > 0 AND weight <= 1),
|
|
max_depth INTEGER NOT NULL DEFAULT 3,
|
|
retain_authority BOOLEAN NOT NULL DEFAULT TRUE,
|
|
space_slug TEXT NOT NULL,
|
|
state TEXT NOT NULL DEFAULT 'active' CHECK (state IN ('active', 'paused', 'revoked')),
|
|
custom_scope TEXT,
|
|
expires_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
UNIQUE (delegator_did, delegate_did, authority, space_slug),
|
|
CHECK (delegator_did != delegate_did)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_delegations_delegator ON delegations(delegator_did, space_slug);
|
|
CREATE INDEX IF NOT EXISTS idx_delegations_delegate ON delegations(delegate_did, space_slug);
|
|
CREATE INDEX IF NOT EXISTS idx_delegations_space ON delegations(space_slug, authority);
|
|
CREATE INDEX IF NOT EXISTS idx_delegations_expires ON delegations(expires_at) WHERE expires_at IS NOT NULL;
|
|
|
|
-- Trust events: append-only log of trust-relevant actions
|
|
CREATE TABLE IF NOT EXISTS trust_events (
|
|
id TEXT PRIMARY KEY,
|
|
source_did TEXT NOT NULL,
|
|
target_did TEXT NOT NULL,
|
|
event_type TEXT NOT NULL CHECK (event_type IN (
|
|
'delegation_created', 'delegation_increased', 'delegation_decreased',
|
|
'delegation_revoked', 'delegation_paused', 'delegation_resumed',
|
|
'endorsement', 'flag', 'collaboration', 'guardian_link'
|
|
)),
|
|
authority TEXT,
|
|
weight_delta REAL,
|
|
space_slug TEXT NOT NULL,
|
|
metadata JSONB DEFAULT '{}',
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_trust_events_source ON trust_events(source_did, created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_trust_events_target ON trust_events(target_did, created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_trust_events_space ON trust_events(space_slug, created_at DESC);
|
|
|
|
-- Trust scores: materialized aggregation of delegation + transitive trust
|
|
CREATE TABLE IF NOT EXISTS trust_scores (
|
|
source_did TEXT NOT NULL,
|
|
target_did TEXT NOT NULL,
|
|
authority TEXT NOT NULL,
|
|
space_slug TEXT NOT NULL,
|
|
score REAL NOT NULL DEFAULT 0,
|
|
direct_weight REAL NOT NULL DEFAULT 0,
|
|
transitive_weight REAL NOT NULL DEFAULT 0,
|
|
last_computed TIMESTAMPTZ DEFAULT NOW(),
|
|
PRIMARY KEY (source_did, target_did, authority, space_slug)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_trust_scores_target ON trust_scores(target_did, authority, space_slug);
|
|
CREATE INDEX IF NOT EXISTS idx_trust_scores_space ON trust_scores(space_slug, authority);
|
|
|
|
-- ============================================================================
|
|
-- LEGACY IDENTITY LINKS (CryptID → EncryptID migration)
|
|
-- ============================================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS legacy_identities (
|
|
id TEXT PRIMARY KEY,
|
|
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
provider TEXT NOT NULL CHECK (provider IN ('cryptid')),
|
|
legacy_username TEXT NOT NULL,
|
|
legacy_public_key TEXT NOT NULL,
|
|
legacy_public_key_hash TEXT NOT NULL,
|
|
verified BOOLEAN DEFAULT FALSE,
|
|
migrated_data BOOLEAN DEFAULT FALSE,
|
|
linked_at TIMESTAMPTZ DEFAULT NOW(),
|
|
verified_at TIMESTAMPTZ,
|
|
UNIQUE (provider, legacy_public_key_hash)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_legacy_identities_user ON legacy_identities(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_legacy_identities_pubkey ON legacy_identities(legacy_public_key_hash);
|
|
|
|
-- ============================================================================
|
|
-- MIGRATION: Rename authority verticals (voting/moderation/curation/treasury/membership → gov-ops/fin-ops/dev-ops)
|
|
-- ============================================================================
|
|
|
|
-- Drop old CHECK constraint first so UPDATEs can set new values
|
|
ALTER TABLE delegations DROP CONSTRAINT IF EXISTS delegations_authority_check;
|
|
|
|
-- Migrate existing delegation data
|
|
UPDATE delegations SET authority = 'gov-ops' WHERE authority IN ('voting', 'moderation');
|
|
UPDATE delegations SET authority = 'fin-ops' WHERE authority = 'treasury';
|
|
UPDATE delegations SET authority = 'dev-ops' WHERE authority IN ('curation', 'membership');
|
|
|
|
-- Migrate existing trust_scores data
|
|
UPDATE trust_scores SET authority = 'gov-ops' WHERE authority IN ('voting', 'moderation');
|
|
UPDATE trust_scores SET authority = 'fin-ops' WHERE authority = 'treasury';
|
|
UPDATE trust_scores SET authority = 'dev-ops' WHERE authority IN ('curation', 'membership');
|
|
|
|
-- Migrate existing trust_events data
|
|
UPDATE trust_events SET authority = 'gov-ops' WHERE authority IN ('voting', 'moderation');
|
|
UPDATE trust_events SET authority = 'fin-ops' WHERE authority = 'treasury';
|
|
UPDATE trust_events SET authority = 'dev-ops' WHERE authority IN ('curation', 'membership');
|
|
|
|
-- Add new CHECK constraint with updated values
|
|
ALTER TABLE delegations ADD CONSTRAINT delegations_authority_check CHECK (authority IN ('gov-ops', 'fin-ops', 'dev-ops', 'custom'));
|