schema.ts300 lines · main
| 1 | /** |
| 2 | * Doltgres schema for briven-engine (all Auth product state). |
| 3 | * Applied at boot via ensure + bootstrap — no stock Postgres. |
| 4 | */ |
| 5 | |
| 6 | import { getEnginePool } from './db.js'; |
| 7 | import { log } from '../../lib/logger.js'; |
| 8 | |
| 9 | const STATEMENTS = [ |
| 10 | `CREATE TABLE IF NOT EXISTS be_tenants ( |
| 11 | tenant_id TEXT PRIMARY KEY, |
| 12 | project_id TEXT NOT NULL, |
| 13 | created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() |
| 14 | )`, |
| 15 | `CREATE TABLE IF NOT EXISTS be_users ( |
| 16 | id TEXT PRIMARY KEY, |
| 17 | tenant_id TEXT NOT NULL DEFAULT 'public', |
| 18 | email TEXT, |
| 19 | phone TEXT, |
| 20 | email_verified BOOLEAN NOT NULL DEFAULT FALSE, |
| 21 | time_joined TIMESTAMPTZ NOT NULL DEFAULT NOW(), |
| 22 | metadata_json TEXT NOT NULL DEFAULT '{}' |
| 23 | )`, |
| 24 | `CREATE INDEX IF NOT EXISTS be_users_tenant_email_idx ON be_users (tenant_id, email)`, |
| 25 | `CREATE INDEX IF NOT EXISTS be_users_tenant_phone_idx ON be_users (tenant_id, phone)`, |
| 26 | `CREATE TABLE IF NOT EXISTS be_password_hashes ( |
| 27 | user_id TEXT PRIMARY KEY, |
| 28 | password_hash TEXT NOT NULL, |
| 29 | updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() |
| 30 | )`, |
| 31 | `CREATE TABLE IF NOT EXISTS be_sessions ( |
| 32 | session_handle TEXT PRIMARY KEY, |
| 33 | user_id TEXT NOT NULL, |
| 34 | tenant_id TEXT NOT NULL DEFAULT 'public', |
| 35 | refresh_token_hash TEXT NOT NULL, |
| 36 | access_payload_json TEXT NOT NULL DEFAULT '{}', |
| 37 | expires_at TIMESTAMPTZ NOT NULL, |
| 38 | created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() |
| 39 | )`, |
| 40 | `CREATE INDEX IF NOT EXISTS be_sessions_user_idx ON be_sessions (user_id)`, |
| 41 | `CREATE TABLE IF NOT EXISTS be_passwordless_codes ( |
| 42 | pre_auth_session_id TEXT PRIMARY KEY, |
| 43 | tenant_id TEXT NOT NULL DEFAULT 'public', |
| 44 | email TEXT, |
| 45 | phone TEXT, |
| 46 | code_hash TEXT NOT NULL, |
| 47 | device_id TEXT NOT NULL, |
| 48 | expires_at TIMESTAMPTZ NOT NULL, |
| 49 | created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() |
| 50 | )`, |
| 51 | `CREATE TABLE IF NOT EXISTS be_third_party_links ( |
| 52 | id TEXT PRIMARY KEY, |
| 53 | user_id TEXT NOT NULL, |
| 54 | tenant_id TEXT NOT NULL DEFAULT 'public', |
| 55 | third_party_id TEXT NOT NULL, |
| 56 | third_party_user_id TEXT NOT NULL, |
| 57 | created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() |
| 58 | )`, |
| 59 | `CREATE UNIQUE INDEX IF NOT EXISTS be_tp_unique |
| 60 | ON be_third_party_links (tenant_id, third_party_id, third_party_user_id)`, |
| 61 | // MFA TOTP devices |
| 62 | `CREATE TABLE IF NOT EXISTS be_totp_devices ( |
| 63 | id TEXT PRIMARY KEY, |
| 64 | user_id TEXT NOT NULL, |
| 65 | tenant_id TEXT NOT NULL DEFAULT 'public', |
| 66 | device_name TEXT NOT NULL, |
| 67 | secret_base32 TEXT NOT NULL, |
| 68 | verified BOOLEAN NOT NULL DEFAULT FALSE, |
| 69 | created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() |
| 70 | )`, |
| 71 | `CREATE INDEX IF NOT EXISTS be_totp_user_idx ON be_totp_devices (user_id)`, |
| 72 | // Passkeys / WebAuthn |
| 73 | `CREATE TABLE IF NOT EXISTS be_webauthn_challenges ( |
| 74 | challenge_id TEXT PRIMARY KEY, |
| 75 | tenant_id TEXT NOT NULL DEFAULT 'public', |
| 76 | user_id TEXT, |
| 77 | challenge TEXT NOT NULL, |
| 78 | type TEXT NOT NULL, |
| 79 | expires_at TIMESTAMPTZ NOT NULL, |
| 80 | created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() |
| 81 | )`, |
| 82 | `CREATE TABLE IF NOT EXISTS be_webauthn_credentials ( |
| 83 | id TEXT PRIMARY KEY, |
| 84 | user_id TEXT NOT NULL, |
| 85 | tenant_id TEXT NOT NULL DEFAULT 'public', |
| 86 | credential_id TEXT NOT NULL, |
| 87 | public_key TEXT NOT NULL, |
| 88 | counter BIGINT NOT NULL DEFAULT 0, |
| 89 | device_type TEXT, |
| 90 | backed_up BOOLEAN NOT NULL DEFAULT FALSE, |
| 91 | transports TEXT, |
| 92 | created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() |
| 93 | )`, |
| 94 | `CREATE UNIQUE INDEX IF NOT EXISTS be_webauthn_cred_unique |
| 95 | ON be_webauthn_credentials (tenant_id, credential_id)`, |
| 96 | `CREATE INDEX IF NOT EXISTS be_webauthn_user_idx ON be_webauthn_credentials (user_id)`, |
| 97 | // Roles |
| 98 | `CREATE TABLE IF NOT EXISTS be_roles ( |
| 99 | tenant_id TEXT NOT NULL, |
| 100 | role_name TEXT NOT NULL, |
| 101 | permissions_json TEXT NOT NULL DEFAULT '[]', |
| 102 | created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), |
| 103 | PRIMARY KEY (tenant_id, role_name) |
| 104 | )`, |
| 105 | `CREATE TABLE IF NOT EXISTS be_user_roles ( |
| 106 | tenant_id TEXT NOT NULL, |
| 107 | user_id TEXT NOT NULL, |
| 108 | role_name TEXT NOT NULL, |
| 109 | created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), |
| 110 | PRIMARY KEY (tenant_id, user_id, role_name) |
| 111 | )`, |
| 112 | // Abuse counters (fallback when Redis unavailable) |
| 113 | `CREATE TABLE IF NOT EXISTS be_rate_limits ( |
| 114 | bucket_key TEXT PRIMARY KEY, |
| 115 | hit_count INT NOT NULL DEFAULT 0, |
| 116 | window_start TIMESTAMPTZ NOT NULL DEFAULT NOW() |
| 117 | )`, |
| 118 | // Enterprise SSO (SAML + OIDC) — briven-engine native |
| 119 | `CREATE TABLE IF NOT EXISTS be_sso_connections ( |
| 120 | id TEXT PRIMARY KEY, |
| 121 | project_id TEXT NOT NULL, |
| 122 | tenant_id TEXT NOT NULL, |
| 123 | name TEXT NOT NULL, |
| 124 | provider_type TEXT NOT NULL, |
| 125 | domains_json TEXT NOT NULL DEFAULT '[]', |
| 126 | config_json TEXT NOT NULL DEFAULT '{}', |
| 127 | jit_enabled BOOLEAN NOT NULL DEFAULT TRUE, |
| 128 | deactivated_at TIMESTAMPTZ, |
| 129 | created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() |
| 130 | )`, |
| 131 | `CREATE INDEX IF NOT EXISTS be_sso_conn_project_idx ON be_sso_connections (project_id)`, |
| 132 | `CREATE TABLE IF NOT EXISTS be_sso_states ( |
| 133 | state_id TEXT PRIMARY KEY, |
| 134 | connection_id TEXT NOT NULL, |
| 135 | project_id TEXT NOT NULL, |
| 136 | provider_type TEXT NOT NULL, |
| 137 | code_verifier TEXT, |
| 138 | redirect_uri TEXT, |
| 139 | expires_at TIMESTAMPTZ NOT NULL, |
| 140 | created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() |
| 141 | )`, |
| 142 | // Security audit trail (SuperTokens-class event log; no raw IPs) |
| 143 | `CREATE TABLE IF NOT EXISTS be_audit_events ( |
| 144 | id TEXT PRIMARY KEY, |
| 145 | tenant_id TEXT NOT NULL DEFAULT 'public', |
| 146 | project_id TEXT, |
| 147 | user_id TEXT, |
| 148 | action TEXT NOT NULL, |
| 149 | ip_hash_hint TEXT, |
| 150 | user_agent TEXT, |
| 151 | metadata_json TEXT NOT NULL DEFAULT '{}', |
| 152 | occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW() |
| 153 | )`, |
| 154 | `CREATE INDEX IF NOT EXISTS be_audit_tenant_time_idx |
| 155 | ON be_audit_events (tenant_id, occurred_at DESC)`, |
| 156 | `CREATE INDEX IF NOT EXISTS be_audit_project_time_idx |
| 157 | ON be_audit_events (project_id, occurred_at DESC)`, |
| 158 | `CREATE INDEX IF NOT EXISTS be_audit_action_time_idx |
| 159 | ON be_audit_events (action, occurred_at DESC)`, |
| 160 | // M2M OAuth2 client credentials (machine clients → short-lived tokens) |
| 161 | `CREATE TABLE IF NOT EXISTS be_m2m_clients ( |
| 162 | id TEXT PRIMARY KEY, |
| 163 | client_id TEXT NOT NULL UNIQUE, |
| 164 | project_id TEXT NOT NULL, |
| 165 | tenant_id TEXT NOT NULL DEFAULT 'public', |
| 166 | name TEXT NOT NULL, |
| 167 | secret_hash TEXT NOT NULL, |
| 168 | secret_suffix TEXT NOT NULL, |
| 169 | role TEXT NOT NULL DEFAULT 'developer', |
| 170 | revoked_at TIMESTAMPTZ, |
| 171 | last_used_at TIMESTAMPTZ, |
| 172 | created_by TEXT, |
| 173 | created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() |
| 174 | )`, |
| 175 | `CREATE INDEX IF NOT EXISTS be_m2m_project_idx |
| 176 | ON be_m2m_clients (project_id)`, |
| 177 | // ─── OIDC / OAuth2 IdP (Briven as SuperTokens-class provider) ─── |
| 178 | `CREATE TABLE IF NOT EXISTS be_oidc_signing_keys ( |
| 179 | kid TEXT PRIMARY KEY, |
| 180 | private_pem TEXT NOT NULL, |
| 181 | public_jwk_json TEXT NOT NULL, |
| 182 | active BOOLEAN NOT NULL DEFAULT TRUE, |
| 183 | created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() |
| 184 | )`, |
| 185 | `CREATE TABLE IF NOT EXISTS be_oidc_clients ( |
| 186 | id TEXT PRIMARY KEY, |
| 187 | client_id TEXT NOT NULL UNIQUE, |
| 188 | project_id TEXT NOT NULL, |
| 189 | tenant_id TEXT NOT NULL DEFAULT 'public', |
| 190 | name TEXT NOT NULL, |
| 191 | logo_url TEXT, |
| 192 | client_secret_hash TEXT, |
| 193 | client_secret_suffix TEXT, |
| 194 | is_public BOOLEAN NOT NULL DEFAULT FALSE, |
| 195 | redirect_uris_json TEXT NOT NULL DEFAULT '[]', |
| 196 | post_logout_uris_json TEXT NOT NULL DEFAULT '[]', |
| 197 | grant_types_json TEXT NOT NULL DEFAULT '["authorization_code","refresh_token"]', |
| 198 | scopes_json TEXT NOT NULL DEFAULT '["openid","profile","email","offline_access"]', |
| 199 | token_endpoint_auth_method TEXT NOT NULL DEFAULT 'client_secret_post', |
| 200 | created_by TEXT, |
| 201 | revoked_at TIMESTAMPTZ, |
| 202 | created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() |
| 203 | )`, |
| 204 | `CREATE INDEX IF NOT EXISTS be_oidc_clients_project_idx |
| 205 | ON be_oidc_clients (project_id)`, |
| 206 | `CREATE TABLE IF NOT EXISTS be_oidc_auth_requests ( |
| 207 | id TEXT PRIMARY KEY, |
| 208 | client_id TEXT NOT NULL, |
| 209 | project_id TEXT NOT NULL, |
| 210 | redirect_uri TEXT NOT NULL, |
| 211 | scope TEXT NOT NULL, |
| 212 | state TEXT, |
| 213 | nonce TEXT, |
| 214 | code_challenge TEXT, |
| 215 | code_challenge_method TEXT, |
| 216 | response_type TEXT NOT NULL DEFAULT 'code', |
| 217 | user_id TEXT, |
| 218 | consented_at TIMESTAMPTZ, |
| 219 | expires_at TIMESTAMPTZ NOT NULL, |
| 220 | created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() |
| 221 | )`, |
| 222 | `CREATE INDEX IF NOT EXISTS be_oidc_auth_req_exp_idx |
| 223 | ON be_oidc_auth_requests (expires_at)`, |
| 224 | `CREATE TABLE IF NOT EXISTS be_oidc_auth_codes ( |
| 225 | code_hash TEXT PRIMARY KEY, |
| 226 | client_id TEXT NOT NULL, |
| 227 | project_id TEXT NOT NULL, |
| 228 | user_id TEXT NOT NULL, |
| 229 | redirect_uri TEXT NOT NULL, |
| 230 | scope TEXT NOT NULL, |
| 231 | nonce TEXT, |
| 232 | code_challenge TEXT, |
| 233 | code_challenge_method TEXT, |
| 234 | expires_at TIMESTAMPTZ NOT NULL, |
| 235 | used_at TIMESTAMPTZ, |
| 236 | created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() |
| 237 | )`, |
| 238 | `CREATE TABLE IF NOT EXISTS be_oidc_refresh_tokens ( |
| 239 | token_hash TEXT PRIMARY KEY, |
| 240 | client_id TEXT NOT NULL, |
| 241 | project_id TEXT NOT NULL, |
| 242 | user_id TEXT NOT NULL, |
| 243 | scope TEXT NOT NULL, |
| 244 | expires_at TIMESTAMPTZ NOT NULL, |
| 245 | revoked_at TIMESTAMPTZ, |
| 246 | created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() |
| 247 | )`, |
| 248 | `CREATE INDEX IF NOT EXISTS be_oidc_refresh_client_idx |
| 249 | ON be_oidc_refresh_tokens (client_id)`, |
| 250 | `CREATE TABLE IF NOT EXISTS be_oidc_consents ( |
| 251 | user_id TEXT NOT NULL, |
| 252 | client_id TEXT NOT NULL, |
| 253 | scope TEXT NOT NULL, |
| 254 | granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), |
| 255 | PRIMARY KEY (user_id, client_id) |
| 256 | )`, |
| 257 | ]; |
| 258 | |
| 259 | export async function bootstrapBrivenEngineSchema(): Promise<void> { |
| 260 | const pool = getEnginePool(); |
| 261 | for (const sql of STATEMENTS) { |
| 262 | try { |
| 263 | await pool.query(sql); |
| 264 | } catch (err) { |
| 265 | // Doltgres may reject IF NOT EXISTS on some objects — retry without, or log. |
| 266 | const message = err instanceof Error ? err.message : String(err); |
| 267 | if (/already exists/i.test(message)) continue; |
| 268 | log.warn('briven_engine_schema_stmt', { message, sql: sql.slice(0, 80) }); |
| 269 | throw err; |
| 270 | } |
| 271 | } |
| 272 | log.info('briven_engine_schema_ready', { |
| 273 | engine: 'briven-engine', |
| 274 | storage: 'doltgres', |
| 275 | tables: [ |
| 276 | 'be_tenants', |
| 277 | 'be_users', |
| 278 | 'be_password_hashes', |
| 279 | 'be_sessions', |
| 280 | 'be_passwordless_codes', |
| 281 | 'be_third_party_links', |
| 282 | 'be_totp_devices', |
| 283 | 'be_webauthn_challenges', |
| 284 | 'be_webauthn_credentials', |
| 285 | 'be_roles', |
| 286 | 'be_user_roles', |
| 287 | 'be_rate_limits', |
| 288 | 'be_sso_connections', |
| 289 | 'be_sso_states', |
| 290 | 'be_audit_events', |
| 291 | 'be_m2m_clients', |
| 292 | 'be_oidc_signing_keys', |
| 293 | 'be_oidc_clients', |
| 294 | 'be_oidc_auth_requests', |
| 295 | 'be_oidc_auth_codes', |
| 296 | 'be_oidc_refresh_tokens', |
| 297 | 'be_oidc_consents', |
| 298 | ], |
| 299 | }); |
| 300 | } |