import { PUBLIC_BETA_GATE_READY_SQL } from "./launch-gates.ts"; /** * Reserve a pairing slot in one SQLite statement. * * Parameters: * ?1 request id, ?2 account id, ?3 requested name, ?4 OS, * ?5 code hash, ?6 expiry, ?7 current UTC timestamp. * * Keeping the capacity and pending-limit predicates inside the INSERT makes * concurrent D1 writes serialize around the actual reservation instead of * trusting an earlier read that may already be stale. */ export const RESERVE_PAIRING_SQL = ` WITH active_beta AS ( SELECT capacity_slots FROM beta_programs WHERE state = 'active' AND starts_at <= ?7 AND (ends_at IS NULL OR ends_at > ?7) AND ${PUBLIC_BETA_GATE_READY_SQL} ORDER BY created_at DESC, id DESC LIMIT 1 ) INSERT INTO pairing_requests (id, account_id, requested_name, os, code_hash, status, expires_at, created_at) SELECT ?1, ?2, ?3, ?4, ?5, 'waiting', ?6, ?7 WHERE ( SELECT COUNT(*) FROM pairing_requests WHERE account_id = ?2 AND status = 'waiting' AND expires_at > ?7 ) < 5 AND ( EXISTS ( SELECT 1 FROM active_beta WHERE capacity_slots IS NULL ) OR EXISTS ( SELECT 1 FROM entitlement_grants WHERE account_id = ?2 AND state = 'active' AND starts_at <= ?7 AND (ends_at IS NULL OR ends_at > ?7) AND revoked_at IS NULL AND capacity_slots IS NULL ) OR ( COALESCE(( SELECT SUM(capacity_slots) FROM active_beta WHERE capacity_slots IS NOT NULL ), 0) + COALESCE(( SELECT SUM(capacity_slots) FROM entitlement_grants WHERE account_id = ?2 AND state = 'active' AND starts_at <= ?7 AND (ends_at IS NULL OR ends_at > ?7) AND revoked_at IS NULL AND capacity_slots IS NOT NULL ), 0) > ( SELECT COUNT(*) FROM hosts WHERE account_id = ?2 AND lifecycle = 'active' AND slot_state = 'active' ) + ( SELECT COUNT(*) FROM pairing_requests WHERE account_id = ?2 AND status = 'waiting' AND expires_at > ?7 ) ) ) `; export const CONSUME_CLAIM_RATE_SQL = ` INSERT INTO pairing_claim_rate_limits (source_hash, window_start, attempts, updated_at) VALUES (?1, ?2, 1, ?3) ON CONFLICT(source_hash, window_start) DO UPDATE SET attempts = pairing_claim_rate_limits.attempts + 1, updated_at = excluded.updated_at WHERE pairing_claim_rate_limits.attempts < 20 `; export const RECORD_FAILED_CODE_SQL = ` UPDATE pairing_requests SET failed_attempts = failed_attempts + 1, last_attempt_at = ?1, locked_at = CASE WHEN failed_attempts + 1 >= 5 THEN ?1 ELSE locked_at END, status = CASE WHEN failed_attempts + 1 >= 5 THEN 'locked' ELSE status END WHERE id = ?2 AND status = 'waiting' AND locked_at IS NULL AND expires_at > ?1 AND failed_attempts < 5 `; /** * Invalidate an unclaimed pairing request owned by one account. * * Parameters: * ?1 request id, ?2 account id, ?3 replacement code hash. * * Replacing the hash makes an accidentally reverted status insufficient to * revive the original bearer code. */ export const CANCEL_PAIRING_SQL = ` UPDATE pairing_requests SET status = 'cancelled', code_hash = ?3 WHERE id = ?1 AND account_id = ?2 AND status = 'waiting' `; export const OWNED_PAIRING_PROGRESS_SQL = ` SELECT pairing_requests.id, pairing_requests.status, pairing_requests.expires_at, pairing_requests.claimed_host_id, pairing_requests.claimed_at, (SELECT MAX(attempts.created_at) FROM pairing_claim_attempts AS attempts WHERE attempts.pairing_request_id = pairing_requests.id) AS last_claim_attempt_at FROM pairing_requests WHERE pairing_requests.id = ? AND pairing_requests.account_id = ? `; export type PairingProgressRow = { id: string; status: string; expires_at: string; claimed_host_id: string | null; claimed_at: string | null; last_claim_attempt_at: string | null; }; export type PairingProgress = { id: string; status: "waiting" | "claimed" | "expired" | "locked" | "cancelled"; expiresAt: string; claimedHostId: string | null; claimedAt: string | null; claimAttemptState: "not_seen" | "seen" | "invalid"; lastClaimAttemptAt: string | null; }; export function derivePairingProgress( row: PairingProgressRow, nowInput: string, ): PairingProgress { const now = Date.parse(nowInput); const expiresAt = Date.parse(row.expires_at); if (!Number.isFinite(now) || !Number.isFinite(expiresAt)) { throw new RangeError("invalid_pairing_progress_time"); } const claimAttemptAt = row.last_claim_attempt_at ? Date.parse(row.last_claim_attempt_at) : null; const claimAttemptState = claimAttemptAt === null ? "not_seen" : !Number.isFinite(claimAttemptAt) || claimAttemptAt > now + 5 * 60_000 ? "invalid" : "seen"; const lastClaimAttemptAt = claimAttemptState === "seen" ? row.last_claim_attempt_at : null; if (row.status === "waiting") { return { id: row.id, status: expiresAt <= now ? "expired" : "waiting", expiresAt: row.expires_at, claimedHostId: null, claimedAt: null, claimAttemptState, lastClaimAttemptAt, }; } if (row.status === "claimed") { if (!row.claimed_host_id || !row.claimed_at) { throw new RangeError("incomplete_claimed_pairing_progress"); } return { id: row.id, status: "claimed", expiresAt: row.expires_at, claimedHostId: row.claimed_host_id, claimedAt: row.claimed_at, claimAttemptState, lastClaimAttemptAt, }; } if (row.status === "locked" || row.status === "cancelled") { return { id: row.id, status: row.status, expiresAt: row.expires_at, claimedHostId: null, claimedAt: null, claimAttemptState, lastClaimAttemptAt, }; } throw new RangeError("unknown_pairing_progress_status"); } const CURRENT_PAIRING_ACCESS = ` ( EXISTS ( SELECT 1 FROM beta_programs WHERE state = 'active' AND starts_at <= ? AND (ends_at IS NULL OR ends_at > ?) AND ${PUBLIC_BETA_GATE_READY_SQL} ) OR EXISTS ( SELECT 1 FROM entitlement_grants WHERE account_id = pairing_requests.account_id AND state = 'active' AND starts_at <= ? AND (ends_at IS NULL OR ends_at > ?) AND revoked_at IS NULL ) )`; const VALID_CLAIM = ` id = ? AND code_hash = ? AND status = 'waiting' AND expires_at > ? AND locked_at IS NULL AND failed_attempts < 5 AND os = ? AND ${CURRENT_PAIRING_ACCESS} `; /** * Claim-time capacity check for CLAIM_HOST_SQL. * * The numbered parameters intentionally reuse that statement's current-time * bindings: ?11/?12 for the latest public beta and ?13/?14 for grants. A * waiting request reserves capacity while it is waiting, but claim converts * it into an active host. Therefore the atomic claim boundary compares the * current finite entitlement with active hosts only. If capacity was reduced * after several pairing codes were issued, the first claims up to the new * limit may succeed and later claims fail without touching existing hosts. */ const HOST_CLAIM_CAPACITY = ` ( EXISTS ( SELECT 1 FROM current_beta WHERE capacity_slots IS NULL ) OR EXISTS ( SELECT 1 FROM entitlement_grants WHERE account_id = pairing_requests.account_id AND state = 'active' AND starts_at <= ?13 AND (ends_at IS NULL OR ends_at > ?14) AND revoked_at IS NULL AND capacity_slots IS NULL ) OR ( COALESCE((SELECT capacity_slots FROM current_beta), 0) + COALESCE(( SELECT SUM(capacity_slots) FROM entitlement_grants WHERE account_id = pairing_requests.account_id AND state = 'active' AND starts_at <= ?13 AND (ends_at IS NULL OR ends_at > ?14) AND revoked_at IS NULL AND capacity_slots IS NOT NULL ), 0) > ( SELECT COUNT(*) FROM hosts WHERE account_id = pairing_requests.account_id AND lifecycle = 'active' AND slot_state = 'active' ) ) )`; export const CLAIM_HOST_SQL = ` WITH current_beta AS ( SELECT capacity_slots FROM beta_programs WHERE state = 'active' AND starts_at <= ?11 AND (ends_at IS NULL OR ends_at > ?12) AND ${PUBLIC_BETA_GATE_READY_SQL} ORDER BY created_at DESC, id DESC LIMIT 1 ) INSERT INTO hosts (id, account_id, name, os, lifecycle, slot_state, connection_state, daemon_version, ed25519_public, x25519_public, identity_fingerprint, claim_request_id, claimed_at, created_at) SELECT ?1, account_id, requested_name, os, 'active', 'active', 'offline', ?15, ?2, ?3, ?4, id, ?5, ?6 FROM pairing_requests WHERE id = ?7 AND code_hash = ?8 AND status = 'waiting' AND expires_at > ?9 AND locked_at IS NULL AND failed_attempts < 5 AND os = ?10 AND ( EXISTS (SELECT 1 FROM current_beta) OR EXISTS ( SELECT 1 FROM entitlement_grants WHERE account_id = pairing_requests.account_id AND state = 'active' AND starts_at <= ?13 AND (ends_at IS NULL OR ends_at > ?14) AND revoked_at IS NULL ) ) AND ${HOST_CLAIM_CAPACITY} ON CONFLICT(identity_fingerprint) DO UPDATE SET name = excluded.name, os = excluded.os, lifecycle = 'active', slot_state = 'active', connection_state = 'offline', daemon_version = COALESCE(excluded.daemon_version, hosts.daemon_version), ed25519_public = excluded.ed25519_public, x25519_public = excluded.x25519_public, claim_request_id = excluded.claim_request_id, claimed_at = excluded.claimed_at, deactivated_at = NULL WHERE hosts.account_id = excluded.account_id AND hosts.lifecycle = 'deactivated' AND hosts.slot_state = 'released' `; export const CLAIM_CREDENTIAL_SQL = ` INSERT INTO device_credentials (id, host_id, token_hash, status, issued_at, created_at) SELECT ?, ?, ?, 'active', ?, ? FROM pairing_requests WHERE ${VALID_CLAIM} AND EXISTS ( SELECT 1 FROM hosts WHERE id = ? AND identity_fingerprint = ? AND claim_request_id = pairing_requests.id ) `; export const COMMIT_PAIRING_CLAIM_SQL = ` UPDATE pairing_requests SET status = 'claimed', claimed_host_id = ?, claimed_at = ?, last_attempt_at = ?, code_hash = ? WHERE ${VALID_CLAIM} AND EXISTS ( SELECT 1 FROM device_credentials WHERE id = ? AND host_id = ? AND status = 'active' ) `; /** * Publish a successfully claimed device set to the tenant reconciler. * * Parameters: * ?1 current UTC timestamp, ?2 account id, ?3 pairing id, ?4 host id, * ?5 claimed timestamp, ?6 credential id, ?7 device token hash. * * The positive pairing and credential predicates keep this update in the * same fail-closed D1 batch as the claim. A failed or partial claim therefore * cannot create provisioning work. */ export const ADVANCE_TENANT_CREDENTIAL_AFTER_CLAIM_SQL = ` UPDATE tenant_instances SET credential_revision = credential_revision + 1, lifecycle = CASE WHEN lifecycle = 'ready' THEN 'degraded' ELSE lifecycle END, relay_ready = 0, updated_at = ? WHERE account_id = ? AND EXISTS ( SELECT 1 FROM pairing_requests WHERE id = ? AND status = 'claimed' AND claimed_host_id = ? AND claimed_at = ? ) AND EXISTS ( SELECT 1 FROM device_credentials WHERE id = ? AND host_id = ? AND token_hash = ? AND status = 'active' ) `;