352 lines
11 KiB
TypeScript
352 lines
11 KiB
TypeScript
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'
|
|
)
|
|
`;
|