import { env } from "cloudflare:workers"; import { ensureDatabase, getD1 } from "./bootstrap"; import { sha256Hex, randomDeviceToken, validateDeviceIdentity } from "./device-claim"; import { DomainError } from "./repository"; import { type AuthorizedPhone, RELAY_AUTHORIZATION_MAX_TTL_SECONDS, RELAY_AUTHORIZATION_SNAPSHOT_VERSION, RELAY_SIGNING_KEY_FOR_SNAPSHOT_SQL, classifySnapshotPlacement, signRelayAuthorizationSnapshot, type AuthorizedDevice, type RelayAuthorizationSnapshotPayload, type SignedRelayAuthorizationSnapshot, } from "./relay-authorization"; import { ACTIVATE_PHONE_PRINCIPAL_SQL, ACTIVATE_PHONE_ROUTE_SQL, ADVANCE_AUTHORIZATION_AFTER_PHONE_ACTIVATION_SQL, AUTHORIZE_PHONE_ROUTE_SQL, ADVANCE_AUTHORIZATION_AFTER_PHONE_REVOKE_SQL, CLAIM_PHONE_HANDOFF_ACTIVATION_SQL, CONSUME_PHONE_HANDOFF_SQL, DELETE_SUPERSEDED_PENDING_PHONE_PRINCIPALS_SQL, DELETE_SUPERSEDED_PENDING_PHONE_ROUTES_SQL, FINALIZE_PHONE_HANDOFF_ACTIVATION_SQL, PHONE_FOR_NODE_REVOCATION_SQL, REVOKE_PHONE_PRINCIPAL_SQL, REVOKE_PHONE_ROUTES_SQL, } from "./relay-control-sql.ts"; import { AUTHENTICATE_RELAY_NODE_IDENTITY_SQL, RelayMtlsIdentityError, verifyTrustedRelayMtlsIdentity, } from "./relay-node-identity.ts"; import { resolveRelayPlacementRoute, type RelayPlacementRouteRecord, type RelayRouteResolution, } from "./relay-routing"; export type { RelayRouteResolution } from "./relay-routing"; export type RelayNodePrincipal = { nodeId: string; regionId: string; }; export type TenantConnectionState = { connectionState: "ready" | "provisioning"; retryAfterSeconds?: number; }; type RelaySigningKeyRecord = { kid: string; public_key_jwk: string; private_key_ref: string; }; type PlacementRecord = Omit & { tenant_status: "active" | "suspended"; }; const PHONE_HANDOFF_ACTIVATION_TTL_MS = 5 * 60_000; function newId(prefix: string): string { return `${prefix}_${crypto.randomUUID().replaceAll("-", "")}`; } function assertOrigin(value: string): string { let url: URL; try { url = new URL(value); } catch { throw new DomainError("invalid_pwa_origin", "PWA 地址无效"); } if ( url.origin !== value || !["https:", "http:"].includes(url.protocol) || (url.protocol === "http:" && !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)) ) { throw new DomainError("invalid_pwa_origin", "PWA 必须使用精确 HTTPS origin"); } return url.origin; } function privateJwkForKid(kid: string): JsonWebKey { const encoded = env.NEKONEST_CLOUD_RELAY_SIGNING_PRIVATE_JWKS?.trim() ?? ""; if (!encoded) { throw new DomainError( "relay_signing_key_unavailable", "Relay 授权签名密钥不可用", 503, true, 5, ); } let parsed: unknown; try { parsed = JSON.parse(encoded); } catch { throw new DomainError("relay_signing_key_unavailable", "Relay 授权签名密钥配置无效", 503, true, 5); } const candidates = Array.isArray(parsed) ? parsed : [parsed]; const match = candidates.find( (candidate): candidate is JsonWebKey & { kid: string } => typeof candidate === "object" && candidate !== null && (candidate as { kid?: unknown }).kid === kid, ); if (!match || match.kty !== "OKP" || match.crv !== "Ed25519" || !match.d) { throw new DomainError("relay_signing_key_unavailable", "Relay 授权签名密钥不匹配", 503, true, 5); } return match; } async function loadSigningKey(issuedAt: string, snapshotExpiresAt: string): Promise<{ record: RelaySigningKeyRecord; privateKey: CryptoKey; }> { const record = await getD1() .prepare(RELAY_SIGNING_KEY_FOR_SNAPSHOT_SQL) .bind(issuedAt, snapshotExpiresAt) .first(); if (!record) { throw new DomainError("relay_signing_key_unavailable", "Relay 授权签名公钥元数据不可用", 503, true, 5); } if (!/^env:NEKONEST_CLOUD_RELAY_SIGNING_PRIVATE_JWKS(?:#[A-Za-z0-9._:-]+)?$/u.test(record.private_key_ref)) { throw new DomainError("relay_signing_key_unavailable", "Relay 私钥引用不受支持", 503, true, 5); } const privateKey = await crypto.subtle.importKey( "jwk", privateJwkForKid(record.kid), { name: "Ed25519" }, false, ["sign"], ); return { record, privateKey }; } export async function tenantConnectionState(accountId: string): Promise { await ensureDatabase(); const row = await getD1() .prepare( `SELECT placements.state, nodes.status AS node_status FROM tenant_instances AS tenants LEFT JOIN tenant_placements AS placements ON placements.tenant_id = tenants.id LEFT JOIN relay_nodes AS nodes ON nodes.id = placements.relay_node_id WHERE tenants.account_id = ?`, ) .bind(accountId) .first<{ state: string | null; node_status: string | null }>(); if (["active", "draining"].includes(row?.state ?? "") && row?.node_status === "active") { return { connectionState: "ready" }; } return { connectionState: "provisioning", retryAfterSeconds: 5 }; } export async function authenticateRelayNode(request: Request): Promise { await ensureDatabase(); let mtlsIdentity; try { mtlsIdentity = await verifyTrustedRelayMtlsIdentity({ request, assertionSecret: env.NEKONEST_CLOUD_RELAY_INGRESS_ASSERTION_SECRET?.trim() ?? "", }); } catch (error) { if (error instanceof RelayMtlsIdentityError) { throw new DomainError(error.code, error.message, error.status, error.status >= 500, 5); } throw error; } const nodeId = mtlsIdentity.nodeId; const now = new Date().toISOString(); const db = getD1(); const result = await db .prepare(AUTHENTICATE_RELAY_NODE_IDENTITY_SQL) .bind( now, nodeId, mtlsIdentity.spiffeId, mtlsIdentity.certificateFingerprintSha256, ) .run(); if (Number(result.meta.changes ?? 0) !== 1) { throw new DomainError("relay_node_credential_invalid", "Relay 节点凭证无效", 401); } const node = await db .prepare("SELECT region_id FROM relay_nodes WHERE id = ?") .bind(nodeId) .first<{ region_id: string }>(); if (!node) throw new DomainError("relay_node_credential_invalid", "Relay 节点凭证无效", 401); return { nodeId, regionId: node.region_id }; } export async function heartbeatRelayNode(input: { principal: RelayNodePrincipal; generation: number; capacityTenants: number; }): Promise<{ accepted: true; checked_at: string }> { if (!Number.isSafeInteger(input.generation) || input.generation < 0) { throw new DomainError("invalid_heartbeat_generation", "节点 heartbeat generation 无效"); } if (!Number.isSafeInteger(input.capacityTenants) || input.capacityTenants < 0) { throw new DomainError("invalid_relay_capacity", "节点容量无效"); } const now = new Date().toISOString(); const result = await getD1() .prepare( `UPDATE relay_nodes SET last_heartbeat_at = ?1, capacity_tenants = ?2, heartbeat_generation = ?3, updated_at = ?1 WHERE id = ?4 AND region_id = ?5 AND heartbeat_generation <= ?3 AND status IN ('active', 'draining')`, ) .bind(now, input.capacityTenants, input.generation, input.principal.nodeId, input.principal.regionId) .run(); if (Number(result.meta.changes ?? 0) !== 1) { throw new DomainError("stale_relay_heartbeat", "Relay 节点 heartbeat 已过期", 409); } return { accepted: true, checked_at: now }; } async function placementForDevice(input: { principal: RelayNodePrincipal; deviceId: string; tokenHash: string; allowRemote?: boolean; }): Promise { if (!/^host_[0-9a-f]{32}$/u.test(input.deviceId) || !/^[0-9a-f]{64}$/u.test(input.tokenHash)) { throw new DomainError("device_credential_invalid", "设备凭证无效", 401); } const now = new Date().toISOString(); const row = await getD1() .prepare( `SELECT tenants.id AS tenant_id, authorizations.status AS tenant_status, regions.code AS home_region, placements.relay_node_id, placements.generation, placements.state AS placement_state, authorizations.revision AS authorization_revision, nodes.internal_endpoint_ref, nodes.status AS relay_node_status FROM device_credentials AS credentials INNER JOIN hosts ON hosts.id = credentials.host_id INNER JOIN tenant_instances AS tenants ON tenants.account_id = hosts.account_id INNER JOIN tenant_placements AS placements ON placements.tenant_id = tenants.id INNER JOIN relay_regions AS regions ON regions.id = placements.home_region_id INNER JOIN tenant_authorization_state AS authorizations ON authorizations.tenant_id = tenants.id LEFT JOIN relay_nodes AS nodes ON nodes.id = placements.relay_node_id WHERE hosts.id = ?1 AND credentials.token_hash = ?2 AND credentials.status = 'active' AND credentials.revoked_at IS NULL AND (credentials.expires_at IS NULL OR credentials.expires_at > ?3) AND hosts.lifecycle = 'active' AND hosts.slot_state = 'active'`, ) .bind(input.deviceId, input.tokenHash, now) .first(); if (!row) throw new DomainError("device_credential_invalid", "设备凭证无效", 401); if (row.tenant_status !== "active") { throw new DomainError("access_suspended", "租户访问已暂停", 403); } if (!["active", "draining"].includes(row.placement_state) || !row.relay_node_id) { throw new DomainError("service_provisioning", "租户 Relay 正在准备", 503, true, 5); } if (row.relay_node_status !== "active") { throw new DomainError("region_unavailable", "目标 Relay 节点当前不可用", 503, true, 5); } if (!input.allowRemote && row.relay_node_id !== input.principal.nodeId) { throw new DomainError("route_unavailable", "该节点不是租户当前写入节点", 503, true, 5); } return row; } export async function resolveDeviceRouteForRelay(input: { principal: RelayNodePrincipal; deviceId: string; tokenHash: string; }): Promise { await ensureDatabase(); return resolveRelayPlacementRoute( await placementForDevice({ ...input, allowRemote: true }), input.principal.nodeId, ); } export async function resolvePhoneRouteForRelay(input: { principal: RelayNodePrincipal; routeHandle: string; phoneTokenHash: string; }): Promise { await ensureDatabase(); const routeHash = await sha256Hex(input.routeHandle.trim()); const tokenHash = input.phoneTokenHash.trim().toLowerCase(); if (!/^[0-9a-f]{64}$/u.test(routeHash) || !/^[0-9a-f]{64}$/u.test(tokenHash)) { throw new DomainError("phone_credential_invalid", "手机路由凭据无效", 401); } const activationCutoff = new Date(Date.now() - PHONE_HANDOFF_ACTIVATION_TTL_MS).toISOString(); const placement = await getD1() .prepare( `SELECT handles.tenant_id, authorizations.status AS tenant_status, regions.code AS home_region, placements.relay_node_id, placements.generation, placements.state AS placement_state, authorizations.revision AS authorization_revision, nodes.internal_endpoint_ref, nodes.status AS relay_node_status FROM phone_route_handles AS handles INNER JOIN relay_phone_principals AS phones ON phones.phone_id = handles.phone_id INNER JOIN tenant_placements AS placements ON placements.tenant_id = handles.tenant_id INNER JOIN relay_regions AS regions ON regions.id = placements.home_region_id INNER JOIN tenant_authorization_state AS authorizations ON authorizations.tenant_id = handles.tenant_id LEFT JOIN relay_nodes AS nodes ON nodes.id = placements.relay_node_id WHERE handles.handle_hash = ?1 AND phones.token_hash = ?2 AND handles.revoked_at IS NULL AND phones.revoked_at IS NULL AND phones.tenant_id = handles.tenant_id AND ( (handles.status = 'active' AND phones.status = 'active') OR ( handles.status = 'pending' AND phones.status = 'pending' AND EXISTS ( SELECT 1 FROM phone_handoff_tickets AS tickets WHERE tickets.tenant_id = handles.tenant_id AND tickets.completed_phone_id = phones.phone_id AND tickets.completed_phone_token_hash = ?2 AND tickets.completed_route_handle_hash = ?1 AND tickets.completed_at > ?3 AND tickets.activation_nonce IS NULL ) ) )`, ) .bind(routeHash, tokenHash, activationCutoff) .first(); if (!placement) throw new DomainError("phone_credential_invalid", "手机路由凭据无效", 401); if (placement.tenant_status !== "active") { throw new DomainError("access_suspended", "租户访问已暂停", 403); } return resolveRelayPlacementRoute(placement, input.principal.nodeId); } export async function resolveTenantRouteForRelay(input: { principal: RelayNodePrincipal; tenantId: string; placementGeneration: number; }): Promise { await ensureDatabase(); if (!/^tenant_[0-9a-f]{32}$/u.test(input.tenantId) || !Number.isSafeInteger(input.placementGeneration) || input.placementGeneration < 1) { throw new DomainError("route_unavailable", "租户路由令牌无效", 404); } const placement = await getD1() .prepare( `SELECT placements.tenant_id, authorizations.status AS tenant_status, regions.code AS home_region, placements.relay_node_id, placements.generation, placements.state AS placement_state, authorizations.revision AS authorization_revision, nodes.internal_endpoint_ref, nodes.status AS relay_node_status FROM tenant_placements AS placements INNER JOIN relay_regions AS regions ON regions.id = placements.home_region_id INNER JOIN tenant_authorization_state AS authorizations ON authorizations.tenant_id = placements.tenant_id LEFT JOIN relay_nodes AS nodes ON nodes.id = placements.relay_node_id WHERE placements.tenant_id = ?1 AND placements.generation = ?2`, ) .bind(input.tenantId, input.placementGeneration) .first(); if (!placement) throw new DomainError("route_unavailable", "租户路由已过期", 404); if (placement.tenant_status !== "active") { throw new DomainError("access_suspended", "租户访问已暂停", 403); } return resolveRelayPlacementRoute(placement, input.principal.nodeId); } export async function resolveHandoffRouteForRelay(input: { principal: RelayNodePrincipal; ticket: string; pwaOrigin: string; }): Promise { await ensureDatabase(); const ticketHash = await sha256Hex(input.ticket.trim()); const origin = assertOrigin(input.pwaOrigin.trim()); const placement = await getD1() .prepare( `SELECT tickets.tenant_id, authorizations.status AS tenant_status, regions.code AS home_region, placements.relay_node_id, placements.generation, placements.state AS placement_state, authorizations.revision AS authorization_revision, nodes.internal_endpoint_ref, nodes.status AS relay_node_status FROM phone_handoff_tickets AS tickets INNER JOIN tenant_placements AS placements ON placements.tenant_id = tickets.tenant_id INNER JOIN relay_regions AS regions ON regions.id = placements.home_region_id INNER JOIN tenant_authorization_state AS authorizations ON authorizations.tenant_id = tickets.tenant_id LEFT JOIN relay_nodes AS nodes ON nodes.id = placements.relay_node_id WHERE tickets.ticket_hash = ?1 AND tickets.expected_origin = ?2 AND tickets.expires_at > ?3 AND ( tickets.consumed_at IS NULL OR tickets.consumed_by_node_id = placements.relay_node_id )`, ) .bind(ticketHash, origin, new Date().toISOString()) .first(); if (!placement) throw new DomainError("phone_credential_invalid", "手机交接凭证无效或已过期", 401); if (placement.tenant_status !== "active") { throw new DomainError("access_suspended", "租户访问已暂停", 403); } return resolveRelayPlacementRoute(placement, input.principal.nodeId); } export async function authorizeDeviceForRelay(input: { principal: RelayNodePrincipal; deviceId: string; tokenHash: string; }): Promise<{ snapshot: SignedRelayAuthorizationSnapshot; public_key_jwk: JsonWebKey }> { await ensureDatabase(); const placement = await placementForDevice(input); await getD1() .prepare( `UPDATE device_credentials SET last_used_at = ? WHERE host_id = ? AND token_hash = ? AND status = 'active' AND revoked_at IS NULL`, ) .bind(new Date().toISOString(), input.deviceId, input.tokenHash) .run(); return signedAuthorizationSnapshot(placement); } async function signedAuthorizationSnapshot( placement: PlacementRecord, ): Promise<{ snapshot: SignedRelayAuthorizationSnapshot; public_key_jwk: JsonWebKey }> { const devices = await getD1() .prepare( `SELECT hosts.id AS device_id, hosts.name, hosts.os, hosts.ed25519_public, hosts.x25519_public, credentials.token_hash AS credential_hash, hosts.identity_fingerprint FROM tenant_instances AS tenants INNER JOIN hosts ON hosts.account_id = tenants.account_id INNER JOIN device_credentials AS credentials ON credentials.host_id = hosts.id WHERE tenants.id = ? AND hosts.lifecycle = 'active' AND hosts.slot_state = 'active' AND credentials.status = 'active' AND credentials.revoked_at IS NULL ORDER BY hosts.id ASC, credentials.id ASC`, ) .bind(placement.tenant_id) .all(); const phones = await getD1() .prepare( `SELECT phone_id, name, token_hash AS credential_hash, ed25519_public, x25519_public, identity_fingerprint FROM relay_phone_principals WHERE tenant_id = ? AND status = 'active' AND revoked_at IS NULL ORDER BY phone_id ASC`, ) .bind(placement.tenant_id) .all(); const nowMs = Date.now(); const issuedAt = new Date(nowMs).toISOString(); const expiresAt = new Date( nowMs + RELAY_AUTHORIZATION_MAX_TTL_SECONDS * 1_000, ).toISOString(); const payload: RelayAuthorizationSnapshotPayload = { snapshot_version: RELAY_AUTHORIZATION_SNAPSHOT_VERSION, tenant_id: placement.tenant_id, tenant_status: placement.tenant_status, home_region: placement.home_region, relay_node_id: placement.relay_node_id!, placement_generation: placement.generation, authorization_revision: placement.authorization_revision, devices: devices.results ?? [], phones: phones.results ?? [], issued_at: issuedAt, expires_at: expiresAt, }; const signing = await loadSigningKey(issuedAt, expiresAt); let publicKey: JsonWebKey; try { publicKey = JSON.parse(signing.record.public_key_jwk) as JsonWebKey; } catch { throw new DomainError("relay_signing_key_unavailable", "Relay 公钥元数据无效", 503, true, 5); } return { snapshot: await signRelayAuthorizationSnapshot({ kid: signing.record.kid, privateKey: signing.privateKey, payload, }), public_key_jwk: publicKey, }; } export async function fullAuthorizationSnapshot(input: { principal: RelayNodePrincipal; tenantId: string; placementGeneration: number; }): Promise<{ snapshot: SignedRelayAuthorizationSnapshot; public_key_jwk: JsonWebKey }> { if (!/^tenant_[0-9a-f]{32}$/u.test(input.tenantId)) { throw new DomainError("invalid_tenant_id", "租户标识无效"); } if (!Number.isSafeInteger(input.placementGeneration) || input.placementGeneration < 1) { throw new DomainError("invalid_placement_generation", "placement generation 无效"); } const placement = await getD1() .prepare( `SELECT placements.tenant_id, authorizations.status AS tenant_status, regions.code AS home_region, placements.relay_node_id, placements.generation, placements.state AS placement_state, authorizations.revision AS authorization_revision FROM tenant_placements AS placements INNER JOIN relay_regions AS regions ON regions.id = placements.home_region_id INNER JOIN tenant_authorization_state AS authorizations ON authorizations.tenant_id = placements.tenant_id WHERE placements.tenant_id = ?`, ) .bind(input.tenantId) .first(); assertFullSnapshotPlacement({ placement, nodeId: input.principal.nodeId, placementGeneration: input.placementGeneration, }); return signedAuthorizationSnapshot(placement!); } export function assertFullSnapshotPlacement(input: { placement: PlacementRecord | null; nodeId: string; placementGeneration: number; }): void { const placement = input.placement; const admission = classifySnapshotPlacement({ placement, nodeId: input.nodeId, expectedGeneration: input.placementGeneration, }); if (admission === "wrong_node") { throw new DomainError("route_unavailable", "租户不属于当前 Relay 节点", 404); } if (admission === "stale_generation") { throw new DomainError("route_unavailable", "placement generation 已过期", 409, true, 1); } if (admission === "suspended") { throw new DomainError("access_suspended", "租户访问已暂停", 403); } if (admission === "provisioning") { throw new DomainError("service_provisioning", "租户 Relay 正在准备", 503, true, 5); } } export async function authorizationRevisionDelta(input: { principal: RelayNodePrincipal; tenantId: string; afterRevision: number; }): Promise<{ tenant_id: string; authorization_revision: number; tenant_status: string; changed: boolean; checked_at: string; }> { if (!/^tenant_[0-9a-f]{32}$/u.test(input.tenantId)) { throw new DomainError("invalid_tenant_id", "租户标识无效"); } if (!Number.isSafeInteger(input.afterRevision) || input.afterRevision < 0) { throw new DomainError("invalid_authorization_revision", "授权 revision 无效"); } const row = await getD1() .prepare( `SELECT authorizations.revision, authorizations.status FROM tenant_authorization_state AS authorizations INNER JOIN tenant_placements AS placements ON placements.tenant_id = authorizations.tenant_id WHERE authorizations.tenant_id = ? AND placements.relay_node_id = ?`, ) .bind(input.tenantId, input.principal.nodeId) .first<{ revision: number; status: string }>(); if (!row) throw new DomainError("route_unavailable", "租户不属于当前 Relay 节点", 404); return { tenant_id: input.tenantId, authorization_revision: row.revision, tenant_status: row.status, changed: row.revision !== input.afterRevision, checked_at: new Date().toISOString(), }; } export async function createPhoneHandoffTicket(input: { accountId: string; pwaOrigin: string; }): Promise<{ ticket: string; pwa_url: string }> { await ensureDatabase(); const origin = assertOrigin(input.pwaOrigin); const tenant = await getD1() .prepare( `SELECT tenants.id FROM tenant_instances AS tenants INNER JOIN accounts ON accounts.id = tenants.account_id WHERE tenants.account_id = ? AND accounts.status = 'active'`, ) .bind(input.accountId) .first<{ id: string }>(); if (!tenant) throw new DomainError("tenant_not_found", "租户不存在", 404); const ticket = randomDeviceToken(); const ticketHash = await sha256Hex(ticket); const nowMs = Date.now(); const now = new Date(nowMs).toISOString(); const expiresAt = new Date(nowMs + 60_000).toISOString(); await getD1() .prepare( `INSERT INTO phone_handoff_tickets (id, ticket_hash, account_id, tenant_id, expected_origin, expires_at, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, ) .bind(newId("handoff"), ticketHash, input.accountId, tenant.id, origin, expiresAt, now) .run(); return { ticket, pwa_url: `${origin}/#handoff=${encodeURIComponent(ticket)}`, }; } export async function consumePhoneHandoffForRelay(input: { principal: RelayNodePrincipal; ticket: string; pwaOrigin: string; name: string; phoneEd25519Public: string; phoneX25519Public: string; identityFingerprint: string; }): Promise<{ handoff_id: string; tenant_id: string; name: string; phone_ed25519_public: string; phone_x25519_public: string; identity_fingerprint: string; placement_generation: number; }> { await ensureDatabase(); const origin = assertOrigin(input.pwaOrigin); const ticket = input.ticket.trim().toLowerCase(); if (!/^[0-9a-f]{64}$/u.test(ticket)) { throw new DomainError("phone_handoff_invalid", "手机交接凭证无效或已过期", 401); } const name = input.name.trim(); if (name.length < 1 || name.length > 48) { throw new DomainError("invalid_phone_name", "手机名称需为 1 到 48 个字符"); } let identity; try { identity = await validateDeviceIdentity({ ed25519Public: input.phoneEd25519Public, x25519Public: input.phoneX25519Public, identityFingerprint: input.identityFingerprint, }); } catch { throw new DomainError("invalid_phone_identity", "手机 E2E 身份无效"); } const db = getD1(); const ticketHash = await sha256Hex(ticket); const selectionSql = `SELECT tickets.id, tickets.tenant_id, placements.generation, tickets.consumed_at FROM phone_handoff_tickets AS tickets INNER JOIN tenant_placements AS placements ON placements.tenant_id = tickets.tenant_id INNER JOIN tenant_authorization_state AS authorizations ON authorizations.tenant_id = tickets.tenant_id WHERE tickets.ticket_hash = ? AND tickets.expected_origin = ? AND tickets.expires_at > ? AND placements.state IN ('active', 'draining') AND placements.relay_node_id = ? AND authorizations.status = 'active' AND ( tickets.consumed_at IS NULL OR ( tickets.consumed_by_node_id = ? AND tickets.pending_phone_name = ? AND tickets.pending_ed25519_public = ? AND tickets.pending_x25519_public = ? AND tickets.pending_identity_fingerprint = ? ) )`; const selectRecord = () => db .prepare(selectionSql) .bind( ticketHash, origin, new Date().toISOString(), input.principal.nodeId, input.principal.nodeId, name, identity.ed25519Public, identity.x25519Public, identity.fingerprint, ) .first<{ id: string; tenant_id: string; generation: number; consumed_at: string | null }>(); let record = await selectRecord(); if (!record) throw new DomainError("phone_handoff_invalid", "手机交接凭证无效或已过期", 401); if (record.consumed_at === null) { const now = new Date().toISOString(); const result = await db .prepare(CONSUME_PHONE_HANDOFF_SQL) .bind( now, record.id, ticketHash, origin, input.principal.nodeId, name, identity.ed25519Public, identity.x25519Public, identity.fingerprint, ) .run(); if (Number(result.meta.changes ?? 0) !== 1) { // An identical request may have won the one-shot consume update. Only // resume it when every node and identity field still matches. record = await selectRecord(); if (!record?.consumed_at) { throw new DomainError("phone_handoff_invalid", "手机交接凭证无效或已过期", 401); } } } return { handoff_id: record.id, tenant_id: record.tenant_id, name, phone_ed25519_public: identity.ed25519Public, phone_x25519_public: identity.x25519Public, identity_fingerprint: identity.fingerprint, placement_generation: record.generation, }; } export async function completePhoneHandoffForRelay(input: { principal: RelayNodePrincipal; handoffId: string; phoneId: string; phoneTokenHash: string; routeHandleHash: string; }): Promise<{ completed: true }> { await ensureDatabase(); if (!/^handoff_[0-9a-f]{32}$/u.test(input.handoffId)) { throw new DomainError("invalid_phone_handoff_id", "手机交接编号无效"); } if (!/^phone_[A-Za-z0-9._:-]{1,120}$/u.test(input.phoneId)) { throw new DomainError("invalid_phone_id", "手机身份编号无效"); } const routeHandleHash = input.routeHandleHash.trim().toLowerCase(); const phoneTokenHash = input.phoneTokenHash.trim().toLowerCase(); if (!/^[0-9a-f]{64}$/u.test(routeHandleHash) || !/^[0-9a-f]{64}$/u.test(phoneTokenHash)) { throw new DomainError("invalid_phone_route_hash", "手机路由摘要无效"); } const now = new Date().toISOString(); const db = getD1(); type CompletionRecord = { tenant_id: string; consumed_by_node_id: string | null; consumed_at: string | null; completed_at: string | null; completed_phone_id: string | null; completed_phone_token_hash: string | null; completed_route_handle_hash: string | null; }; const loadCompletion = () => db .prepare( `SELECT tenant_id, consumed_by_node_id, consumed_at, completed_at, completed_phone_id, completed_phone_token_hash, completed_route_handle_hash FROM phone_handoff_tickets WHERE id = ?`, ) .bind(input.handoffId) .first(); const isExactCompleted = async (record: CompletionRecord | null): Promise => { if (!record?.completed_at || record.consumed_by_node_id !== input.principal.nodeId || record.completed_phone_id !== input.phoneId || record.completed_phone_token_hash !== phoneTokenHash || record.completed_route_handle_hash !== routeHandleHash) { return false; } const persisted = await db .prepare( `SELECT EXISTS ( SELECT 1 FROM relay_phone_principals AS phones INNER JOIN phone_route_handles AS handles ON handles.phone_id = phones.phone_id AND handles.tenant_id = phones.tenant_id WHERE phones.phone_id = ?1 AND phones.tenant_id = ?2 AND phones.token_hash = ?3 AND handles.handle_hash = ?4 AND phones.status = handles.status AND phones.status IN ('pending', 'active') AND phones.revoked_at IS NULL AND handles.revoked_at IS NULL ) AS completion_ok`, ) .bind( input.phoneId, record.tenant_id, phoneTokenHash, routeHandleHash, ) .first<{ completion_ok: number }>(); return persisted?.completion_ok === 1; }; const initial = await loadCompletion(); if (!initial?.consumed_at || initial.consumed_by_node_id !== input.principal.nodeId) { throw new DomainError("phone_handoff_completion_failed", "手机交接完成状态不确定", 409); } if (initial.completed_at) { if (await isExactCompleted(initial)) return { completed: true }; throw new DomainError("phone_handoff_completion_conflict", "手机交接已由另一组凭据完成", 409); } let results; try { results = await db.batch([ db.prepare(DELETE_SUPERSEDED_PENDING_PHONE_ROUTES_SQL).bind(input.handoffId, now), db.prepare(DELETE_SUPERSEDED_PENDING_PHONE_PRINCIPALS_SQL).bind(input.handoffId, now), db .prepare( `INSERT INTO relay_phone_principals (phone_id, tenant_id, token_hash, name, ed25519_public, x25519_public, identity_fingerprint, status, created_at) SELECT ?, tenant_id, ?, pending_phone_name, pending_ed25519_public, pending_x25519_public, pending_identity_fingerprint, 'pending', ? FROM phone_handoff_tickets WHERE id = ? AND consumed_by_node_id = ? AND consumed_at IS NOT NULL AND completed_at IS NULL AND pending_identity_fingerprint IS NOT NULL`, ) .bind(input.phoneId, phoneTokenHash, now, input.handoffId, input.principal.nodeId), db .prepare( `INSERT INTO phone_route_handles (id, handle_hash, tenant_id, phone_id, status, created_at) SELECT ?, ?, tenant_id, ?, 'pending', ? FROM phone_handoff_tickets WHERE id = ? AND consumed_by_node_id = ? AND consumed_at IS NOT NULL AND completed_at IS NULL AND EXISTS ( SELECT 1 FROM relay_phone_principals WHERE phone_id = ? AND token_hash = ? AND status = 'pending' )`, ) .bind( newId("route"), routeHandleHash, input.phoneId, now, input.handoffId, input.principal.nodeId, input.phoneId, phoneTokenHash, ), db .prepare( `UPDATE phone_handoff_tickets SET completed_at = ?, completed_phone_id = ?, completed_phone_token_hash = ?, completed_route_handle_hash = ? WHERE id = ? AND consumed_by_node_id = ? AND consumed_at IS NOT NULL AND completed_at IS NULL AND EXISTS (SELECT 1 FROM phone_route_handles WHERE handle_hash = ? AND phone_id = ?)`, ) .bind( now, input.phoneId, phoneTokenHash, routeHandleHash, input.handoffId, input.principal.nodeId, routeHandleHash, input.phoneId, ), ]); } catch (error) { if (await isExactCompleted(await loadCompletion())) return { completed: true }; throw error; } if (results.slice(-3).some((result) => Number(result.meta.changes ?? 0) !== 1)) { if (await isExactCompleted(await loadCompletion())) return { completed: true }; throw new DomainError("phone_handoff_completion_failed", "手机交接完成状态不确定", 409); } return { completed: true }; } export async function authorizePhoneRoute(input: { principal: RelayNodePrincipal; routeHandle: string; phoneTokenHash: string; }): Promise<{ tenant_id: string; home_region: string; relay_node_id: string; placement_generation: number; phone: { phone_id: string; name: string; ed25519_public: string; x25519_public: string; identity_fingerprint: string; }; }> { await ensureDatabase(); const routeHandle = input.routeHandle.trim().toLowerCase(); const phoneTokenHash = input.phoneTokenHash.trim().toLowerCase(); if (!/^[0-9a-f]{64}$/u.test(routeHandle) || !/^[0-9a-f]{64}$/u.test(phoneTokenHash)) { throw new DomainError("phone_route_invalid", "手机路由句柄无效", 401); } const hash = await sha256Hex(routeHandle); const now = new Date().toISOString(); const db = getD1(); type AuthorizedPhoneRoute = { tenant_id: string; home_region: string; relay_node_id: string; generation: number; phone_id: string; name: string; ed25519_public: string; x25519_public: string; identity_fingerprint: string; }; const loadAuthorized = () => db .prepare(AUTHORIZE_PHONE_ROUTE_SQL) .bind(hash, phoneTokenHash) .first(); let row = await loadAuthorized(); if (!row) { const activationNonce = newId("activation"); const activationCutoff = new Date(Date.now() - PHONE_HANDOFF_ACTIVATION_TTL_MS).toISOString(); const activationResults = await db.batch([ db.prepare(CLAIM_PHONE_HANDOFF_ACTIVATION_SQL).bind( activationNonce, hash, phoneTokenHash, activationCutoff, input.principal.nodeId, ), db.prepare(ACTIVATE_PHONE_PRINCIPAL_SQL).bind(activationNonce, phoneTokenHash), db.prepare(ACTIVATE_PHONE_ROUTE_SQL).bind(activationNonce, hash, phoneTokenHash), db.prepare(ADVANCE_AUTHORIZATION_AFTER_PHONE_ACTIVATION_SQL).bind(now, activationNonce), db.prepare(FINALIZE_PHONE_HANDOFF_ACTIVATION_SQL).bind(now, activationNonce), ]); row = await loadAuthorized(); if (activationResults.some((result) => Number(result.meta.changes ?? 0) !== 1) && !row) { throw new DomainError("phone_route_invalid", "手机路由句柄无效", 401); } } if (!row?.relay_node_id) throw new DomainError("phone_route_invalid", "手机路由句柄无效", 401); if (row.relay_node_id !== input.principal.nodeId) { throw new DomainError("route_unavailable", "该节点不是租户当前写入节点", 503, true, 5); } await db .prepare("UPDATE phone_route_handles SET last_used_at = ? WHERE handle_hash = ? AND status = 'active'") .bind(now, hash) .run(); await db .prepare("UPDATE relay_phone_principals SET last_used_at = ? WHERE phone_id = ? AND token_hash = ? AND status = 'active'") .bind(now, row.phone_id, phoneTokenHash) .run(); return { tenant_id: row.tenant_id, home_region: row.home_region, relay_node_id: row.relay_node_id, placement_generation: row.generation, phone: { phone_id: row.phone_id, name: row.name, ed25519_public: row.ed25519_public, x25519_public: row.x25519_public, identity_fingerprint: row.identity_fingerprint, }, }; } export async function revokePhoneForRelay(input: { principal: RelayNodePrincipal; tenantId: string; phoneId: string; reason: string; }): Promise<{ revoked: true; authorization_revision: number }> { if (!/^tenant_[0-9a-f]{32}$/u.test(input.tenantId)) { throw new DomainError("invalid_tenant_id", "租户标识无效"); } if (!/^phone_[A-Za-z0-9._:-]{1,120}$/u.test(input.phoneId)) { throw new DomainError("invalid_phone_id", "手机身份编号无效"); } const reason = input.reason.trim(); if (reason.length < 4 || reason.length > 256) { throw new DomainError("invalid_revoke_reason", "撤销原因需为 4 到 256 个字符"); } const db = getD1(); const placement = await db .prepare(PHONE_FOR_NODE_REVOCATION_SQL) .bind(input.tenantId, input.phoneId) .first<{ relay_node_id: string | null; status: string }>(); if (!placement || placement.relay_node_id !== input.principal.nodeId) { throw new DomainError("route_unavailable", "手机不属于当前 Relay placement", 404); } if (placement.status !== "active") { throw new DomainError("phone_credential_invalid", "手机凭据已撤销", 401); } const now = new Date().toISOString(); const correlationId = newId("corr"); const results = await db.batch([ db .prepare(REVOKE_PHONE_PRINCIPAL_SQL) .bind(now, input.phoneId, input.tenantId), db .prepare(REVOKE_PHONE_ROUTES_SQL) .bind(now, input.phoneId, input.tenantId), db .prepare(ADVANCE_AUTHORIZATION_AFTER_PHONE_REVOKE_SQL) .bind(now, input.tenantId, input.phoneId), db .prepare( `INSERT INTO audit_events (id, actor_id, action, target_type, target_id, reason, after_json, correlation_id, created_at) SELECT ?, ?, 'phone.revoked', 'phone', ?, ?, ?, ?, ? WHERE EXISTS ( SELECT 1 FROM relay_phone_principals WHERE phone_id = ? AND tenant_id = ? AND status = 'revoked' AND revoked_at = ? )`, ) .bind( newId("audit"), `relay:${input.principal.nodeId}`, input.phoneId, reason, JSON.stringify({ tenant_id: input.tenantId }), correlationId, now, input.phoneId, input.tenantId, now, ), ]); if (Number(results[0]?.meta.changes ?? 0) !== 1 || Number(results[2]?.meta.changes ?? 0) !== 1 || Number(results[3]?.meta.changes ?? 0) !== 1) { throw new DomainError("phone_revoke_indeterminate", "手机撤销状态不确定", 409); } const revision = await db .prepare("SELECT revision FROM tenant_authorization_state WHERE tenant_id = ?") .bind(input.tenantId) .first<{ revision: number }>(); if (!revision) throw new DomainError("phone_revoke_indeterminate", "手机撤销状态不确定", 409); return { revoked: true, authorization_revision: revision.revision }; }