mirror of
https://github.com/stablyai/orca.git
synced 2026-09-25 00:02:35 +00:00
* perf(relay): index and gate the credential cleanup sweeps that seq-scan relay_invites The credential cleanup ran every 30s in all 23 cells as well as the director. Both of its relay_invites passes matched columns no index covered, so each one seq-scanned the whole table inside the maintenance transaction: 56 calls/min fleet-wide, 129ms and 63ms typical and 57s at the tail, to return about one row every nine minutes. Adds partial indexes matching each sweep predicate, gives the cleanup the same owner as the assignment sweep, and reaps terminal invites after seven days so the table stops growing for the life of the database. Every index carries the schema-deferrable marker: an operator builds them with CREATE INDEX CONCURRENTLY, and the catalog pre-check skips them from then on. * perf(relay): index live bases and reap settled connection authorizations relay_connection_bases is the dominant cost in the cleanup transaction: 195 ms of the 268 ms average, with ~5,800 shared buffer hits per call even though it already uses relay_connection_bases_active_deadline. That index spans all 6.65M rows, and only a few hundred are ever active. Adds a partial index on the live rows alone, and reaps settled rows from relay_connection_bases and relay_direct_authorizations once their deadline is more than a day past. Both readers of either table require the row active/unconsumed and inside its deadline, and every deadline is set at most 30s past insert, so a settled row can never authorize anything again. The composite index stays: it is the only one covering active = 0, and it is what lets the drained reaper learn there is nothing to do from the index rather than the 1.5 GB heap. Measured at 200k rows, 5 buffers with it and 1,274 without. * test(relay): accept either bases index in the sweep plan assertion The negative assertion pinned a planner choice rather than the invariant: either index keeps the sweep off the 1.5 GB heap, and which one wins on cost is not something the test should fix. Matches how the same file already handles the two invite sweep indexes. Also names the column the authorization reaper actually measures, which is consumed_at rather than deadline.
795 lines
31 KiB
TypeScript
795 lines
31 KiB
TypeScript
import { createHash, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto'
|
|
import {
|
|
decideResumeCommit,
|
|
RELAY_PROTOCOL_LIMITS,
|
|
type DeviceCredentialInstalled,
|
|
type DeviceResumeConfirmed
|
|
} from '@orca-cloud/relay-contract'
|
|
import type { RelayDatabase, SqlRow } from './database.js'
|
|
|
|
const CREDENTIAL_GRACE_MS = 24 * 60 * 60 * 1000
|
|
// Released desktops validate invite expiry against their own clock with zero
|
|
// tolerance at exactly inviteTtlMs; issuing under the ceiling keeps pairing
|
|
// working for clients whose clocks trail the cell by up to this margin.
|
|
const INVITE_ISSUE_SKEW_MARGIN_MS = 30 * 1000
|
|
// Terminal invites are read by nothing: every reader re-checks expiry and state at read time, so
|
|
// the row only serves the audit trail, which relay_audit_events already keeps. A week is long
|
|
// enough to answer a support question about a pairing that failed.
|
|
const TERMINAL_INVITE_RETENTION_MS = 7 * 24 * 60 * 60 * 1000
|
|
// Why: both readers of a connection basis and of a direct authorization require it still
|
|
// active/unconsumed AND inside its deadline, and every deadline is set at most 30s past insert, so
|
|
// a settled row can never authorize anything again. A day is margin for forensics, not for reads.
|
|
const INACTIVE_AUTHORIZATION_RETENTION_MS = 24 * 60 * 60 * 1000
|
|
// Bounded so one cycle cannot hold row locks or grow WAL without limit; the backlog drains over
|
|
// however many cycles it takes.
|
|
const REAP_BATCH_ROWS = 5000
|
|
|
|
export type RelayIdentity = { userId: string; relayHostId: string }
|
|
export type CredentialReservation = RelayIdentity & {
|
|
credentialKind: 'invite' | 'resume'
|
|
relayDeviceId: string
|
|
tokenHash: string
|
|
reservationId: string
|
|
leaseExpiresAt: number
|
|
acceptedCredentialVersion?: number
|
|
acceptedAs?: 'current' | 'grace'
|
|
resumeExpiresAt?: number
|
|
graceExpiresAt?: number
|
|
}
|
|
|
|
export type InstallInput = RelayIdentity & {
|
|
relayDeviceId: string
|
|
reqId: string
|
|
newResumeTokenHash: string
|
|
expectedCurrentHash?: string
|
|
owningControlGeneration: number
|
|
authorization:
|
|
| { mode: 'relay-basis'; basisConnId: string }
|
|
| { mode: 'authenticated-direct'; directAuthId: string }
|
|
}
|
|
|
|
export class RelayStoreError extends Error {
|
|
constructor(readonly code: string) {
|
|
super(code)
|
|
}
|
|
}
|
|
|
|
export function hashCredential(token: string): string {
|
|
return createHash('sha256').update(token).digest('base64url')
|
|
}
|
|
|
|
function equalHash(left: string, right: string): boolean {
|
|
const leftBytes = Buffer.from(left)
|
|
const rightBytes = Buffer.from(right)
|
|
return leftBytes.length === rightBytes.length && timingSafeEqual(leftBytes, rightBytes)
|
|
}
|
|
|
|
function number(row: SqlRow, field: string): number {
|
|
const value = Number(row[field])
|
|
if (!Number.isSafeInteger(value)) throw new RelayStoreError(`invalid_${field}`)
|
|
return value
|
|
}
|
|
|
|
function optionalNumber(row: SqlRow, field: string): number | undefined {
|
|
return row[field] === null || row[field] === undefined ? undefined : number(row, field)
|
|
}
|
|
|
|
function string(row: SqlRow, field: string): string {
|
|
const value = row[field]
|
|
if (typeof value !== 'string') throw new RelayStoreError(`invalid_${field}`)
|
|
return value
|
|
}
|
|
|
|
export class RelayCredentialStore {
|
|
constructor(
|
|
private readonly database: RelayDatabase,
|
|
private readonly now: () => number = Date.now
|
|
) {}
|
|
|
|
async createInvite(identity: RelayIdentity, relayDeviceId: string): Promise<{
|
|
inviteToken: string
|
|
expiresAt: number
|
|
maxAttempts: number
|
|
}> {
|
|
const inviteToken = randomBytes(32).toString('base64url')
|
|
const tokenHash = hashCredential(inviteToken)
|
|
const now = this.now()
|
|
const expiresAt = now + RELAY_PROTOCOL_LIMITS.inviteTtlMs - INVITE_ISSUE_SKEW_MARGIN_MS
|
|
await this.database.transaction(async (transaction) => {
|
|
await this.consumeRateWith(transaction, `account:${identity.userId}`, 'invite-mint', 30, 60_000, now)
|
|
await transaction.query(
|
|
`UPDATE relay_invites SET state = ?, updated_at = ?
|
|
WHERE user_id = ? AND relay_host_id = ? AND relay_device_id = ?
|
|
AND state IN (?, ?, ?)`,
|
|
[
|
|
'invalidated', now, identity.userId, identity.relayHostId, relayDeviceId,
|
|
'available', 'reserved', 'cooldown'
|
|
]
|
|
)
|
|
await this.auditWith(transaction, {
|
|
type: 'invite-created',
|
|
...identity,
|
|
relayDeviceId,
|
|
detail: { expiresAt }
|
|
})
|
|
await transaction.query(
|
|
`INSERT INTO relay_invites
|
|
(user_id, relay_host_id, relay_device_id, token_hash, state, attempt_count,
|
|
max_attempts, expires_at, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
identity.userId, identity.relayHostId, relayDeviceId, tokenHash, 'available', 0,
|
|
RELAY_PROTOCOL_LIMITS.inviteMaxAttempts, expiresAt, now, now
|
|
]
|
|
)
|
|
})
|
|
return { inviteToken, expiresAt, maxAttempts: RELAY_PROTOCOL_LIMITS.inviteMaxAttempts }
|
|
}
|
|
|
|
async reserveCredential(
|
|
relayHostId: string,
|
|
token: string,
|
|
reservationId: string = randomUUID()
|
|
): Promise<CredentialReservation | null> {
|
|
const tokenHash = hashCredential(token)
|
|
const now = this.now()
|
|
return await this.database.transaction(async (transaction) => {
|
|
const inviteRows = await transaction.query(
|
|
`SELECT * FROM relay_invites WHERE relay_host_id = ? AND token_hash = ?`,
|
|
[relayHostId, tokenHash]
|
|
)
|
|
const invite = inviteRows[0]
|
|
if (invite && equalHash(string(invite, 'token_hash'), tokenHash)) {
|
|
const expiresAt = number(invite, 'expires_at')
|
|
const attempts = number(invite, 'attempt_count')
|
|
const maxAttempts = number(invite, 'max_attempts')
|
|
let state = string(invite, 'state')
|
|
const reservationExpiresAt = optionalNumber(invite, 'reservation_expires_at')
|
|
if (expiresAt <= now) state = 'expired'
|
|
else if (state === 'reserved' && (reservationExpiresAt ?? 0) <= now) {
|
|
await transaction.query(
|
|
`UPDATE relay_invites SET state = ?, reservation_id = NULL,
|
|
reservation_expires_at = NULL, cooldown_until = ?, updated_at = ?
|
|
WHERE token_hash = ?`,
|
|
[
|
|
'cooldown',
|
|
now + RELAY_PROTOCOL_LIMITS.inviteAttemptCooldownMs,
|
|
now,
|
|
tokenHash
|
|
]
|
|
)
|
|
return null
|
|
}
|
|
const cooldownUntil = optionalNumber(invite, 'cooldown_until') ?? 0
|
|
if (
|
|
(state !== 'available' && !(state === 'cooldown' && cooldownUntil <= now)) ||
|
|
attempts >= maxAttempts
|
|
) {
|
|
return null
|
|
}
|
|
const leaseExpiresAt = Math.min(
|
|
expiresAt,
|
|
now + RELAY_PROTOCOL_LIMITS.inviteReservationLeaseMs
|
|
)
|
|
await transaction.query(
|
|
`UPDATE relay_invites SET state = ?, attempt_count = ?, reservation_id = ?,
|
|
reservation_expires_at = ?, cooldown_until = NULL, updated_at = ?
|
|
WHERE token_hash = ?`,
|
|
['reserved', attempts + 1, reservationId, leaseExpiresAt, now, tokenHash]
|
|
)
|
|
return {
|
|
userId: string(invite, 'user_id'),
|
|
relayHostId,
|
|
relayDeviceId: string(invite, 'relay_device_id'),
|
|
credentialKind: 'invite',
|
|
tokenHash,
|
|
reservationId,
|
|
leaseExpiresAt
|
|
}
|
|
}
|
|
|
|
const deviceRows = await transaction.query(
|
|
`SELECT * FROM relay_devices
|
|
WHERE relay_host_id = ? AND (current_hash = ? OR grace_hash = ?)`,
|
|
[relayHostId, tokenHash, tokenHash]
|
|
)
|
|
const device = deviceRows[0]
|
|
if (!device || optionalNumber(device, 'revoked_at') !== undefined) return null
|
|
const currentHash = string(device, 'current_hash')
|
|
const currentExpiresAt = number(device, 'current_expires_at')
|
|
const graceHash = device.grace_hash
|
|
const graceExpiresAt = optionalNumber(device, 'grace_expires_at')
|
|
let acceptedAs: 'current' | 'grace'
|
|
let acceptedCredentialVersion: number
|
|
if (equalHash(currentHash, tokenHash) && currentExpiresAt > now) {
|
|
acceptedAs = 'current'
|
|
acceptedCredentialVersion = number(device, 'current_version')
|
|
} else if (
|
|
typeof graceHash === 'string' &&
|
|
equalHash(graceHash, tokenHash) &&
|
|
(graceExpiresAt ?? 0) > now
|
|
) {
|
|
acceptedAs = 'grace'
|
|
acceptedCredentialVersion = number(device, 'grace_version')
|
|
} else {
|
|
return null
|
|
}
|
|
return {
|
|
userId: string(device, 'user_id'),
|
|
relayHostId,
|
|
relayDeviceId: string(device, 'relay_device_id'),
|
|
credentialKind: 'resume',
|
|
tokenHash,
|
|
reservationId,
|
|
leaseExpiresAt: now + RELAY_PROTOCOL_LIMITS.hostAttachDeadlineMs,
|
|
acceptedCredentialVersion,
|
|
acceptedAs,
|
|
resumeExpiresAt: currentExpiresAt,
|
|
graceExpiresAt
|
|
}
|
|
})
|
|
}
|
|
|
|
async recordConnectionBasis(input: CredentialReservation & {
|
|
basisConnId: string
|
|
owningControlGeneration: number
|
|
deadline: number
|
|
}): Promise<void> {
|
|
const now = this.now()
|
|
await this.database.query(
|
|
`INSERT INTO relay_connection_bases
|
|
(basis_conn_id, user_id, relay_host_id, relay_device_id, owning_control_generation,
|
|
credential_kind, invite_token_hash, accepted_credential_version, accepted_as,
|
|
deadline, active, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
input.basisConnId, input.userId, input.relayHostId, input.relayDeviceId,
|
|
input.owningControlGeneration, input.credentialKind,
|
|
input.credentialKind === 'invite' ? input.tokenHash : null,
|
|
input.acceptedCredentialVersion, input.acceptedAs, input.deadline, 1, now
|
|
]
|
|
)
|
|
}
|
|
|
|
async failReservation(reservation: CredentialReservation): Promise<void> {
|
|
if (reservation.credentialKind !== 'invite') return
|
|
const now = this.now()
|
|
await this.database.transaction(async (transaction) => {
|
|
const rows = await transaction.query(
|
|
`SELECT attempt_count, max_attempts FROM relay_invites
|
|
WHERE token_hash = ? AND reservation_id = ? AND state = ?`,
|
|
[reservation.tokenHash, reservation.reservationId, 'reserved']
|
|
)
|
|
const invite = rows[0]
|
|
if (!invite) return
|
|
const exhausted = number(invite, 'attempt_count') >= number(invite, 'max_attempts')
|
|
await transaction.query(
|
|
`UPDATE relay_invites SET state = ?, reservation_id = NULL,
|
|
reservation_expires_at = NULL, cooldown_until = ?, updated_at = ?
|
|
WHERE token_hash = ? AND reservation_id = ?`,
|
|
[
|
|
exhausted ? 'invalidated' : 'cooldown',
|
|
exhausted ? null : now + RELAY_PROTOCOL_LIMITS.inviteAttemptCooldownMs,
|
|
now,
|
|
reservation.tokenHash,
|
|
reservation.reservationId
|
|
]
|
|
)
|
|
})
|
|
}
|
|
|
|
async recordDirectAuthorization(input: RelayIdentity & {
|
|
relayDeviceId: string
|
|
directAuthId: string
|
|
owningControlGeneration: number
|
|
deadline: number
|
|
}): Promise<void> {
|
|
await this.database.query(
|
|
`INSERT INTO relay_direct_authorizations
|
|
(direct_auth_id, user_id, relay_host_id, relay_device_id,
|
|
owning_control_generation, deadline)
|
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
input.directAuthId, input.userId, input.relayHostId, input.relayDeviceId,
|
|
input.owningControlGeneration, input.deadline
|
|
]
|
|
)
|
|
}
|
|
|
|
async installCredential(input: InstallInput): Promise<DeviceCredentialInstalled> {
|
|
let lastError: unknown
|
|
for (let attempt = 0; attempt <= 3; attempt++) {
|
|
try {
|
|
return await this.installCredentialOnce(input)
|
|
} catch (error) {
|
|
if (error instanceof RelayStoreError) throw error
|
|
const committed = await this.installStatus(input)
|
|
if (committed) return committed
|
|
const code = (error as { code?: unknown }).code
|
|
const retryable = ['23505', '40P01', '55P03', '40001'].includes(String(code))
|
|
if (!retryable || attempt === 3) throw error
|
|
lastError = error
|
|
}
|
|
}
|
|
throw lastError
|
|
}
|
|
|
|
private async installCredentialOnce(input: InstallInput): Promise<DeviceCredentialInstalled> {
|
|
return await this.database.transaction(async (transaction) => {
|
|
const existing = await this.installStatusWith(transaction, input)
|
|
if (existing) return existing
|
|
const now = this.now()
|
|
const basisInviteHash = await this.validateInstallAuthorization(transaction, input, now)
|
|
const devices = await transaction.query(
|
|
`SELECT * FROM relay_devices
|
|
WHERE user_id = ? AND relay_host_id = ? AND relay_device_id = ?`,
|
|
[input.userId, input.relayHostId, input.relayDeviceId]
|
|
)
|
|
const current = devices[0]
|
|
if (input.expectedCurrentHash) {
|
|
if (!current || !equalHash(string(current, 'current_hash'), input.expectedCurrentHash)) {
|
|
throw new RelayStoreError('current_hash_mismatch')
|
|
}
|
|
}
|
|
const currentVersion = current ? number(current, 'current_version') + 1 : 1
|
|
const resumeExpiresAt = now + RELAY_PROTOCOL_LIMITS.resumeTtlMs
|
|
const graceExpiresAt = current ? now + CREDENTIAL_GRACE_MS : undefined
|
|
await transaction.query(
|
|
`INSERT INTO relay_devices
|
|
(user_id, relay_host_id, relay_device_id, current_hash, current_version,
|
|
current_expires_at, grace_hash, grace_version, grace_expires_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT (user_id, relay_host_id, relay_device_id) DO UPDATE SET
|
|
current_hash = excluded.current_hash,
|
|
current_version = excluded.current_version,
|
|
current_expires_at = excluded.current_expires_at,
|
|
grace_hash = excluded.grace_hash,
|
|
grace_version = excluded.grace_version,
|
|
grace_expires_at = excluded.grace_expires_at,
|
|
revoked_at = NULL,
|
|
updated_at = excluded.updated_at`,
|
|
[
|
|
input.userId, input.relayHostId, input.relayDeviceId, input.newResumeTokenHash,
|
|
currentVersion, resumeExpiresAt, current ? string(current, 'current_hash') : null,
|
|
current ? number(current, 'current_version') : null, graceExpiresAt, now
|
|
]
|
|
)
|
|
if (basisInviteHash) {
|
|
await transaction.query(
|
|
`UPDATE relay_invites SET state = ?, reservation_id = NULL,
|
|
reservation_expires_at = NULL, updated_at = ? WHERE token_hash = ?`,
|
|
['consumed', now, basisInviteHash]
|
|
)
|
|
} else {
|
|
await transaction.query(
|
|
`UPDATE relay_invites SET state = ?, reservation_id = NULL,
|
|
reservation_expires_at = NULL, updated_at = ?
|
|
WHERE user_id = ? AND relay_host_id = ? AND relay_device_id = ?
|
|
AND state IN (?, ?, ?)`,
|
|
[
|
|
'invalidated', now, input.userId, input.relayHostId, input.relayDeviceId,
|
|
'available', 'reserved', 'cooldown'
|
|
]
|
|
)
|
|
}
|
|
await transaction.query(
|
|
`UPDATE relay_direct_authorizations SET consumed_at = ?
|
|
WHERE user_id = ? AND relay_host_id = ? AND relay_device_id = ?
|
|
AND consumed_at IS NULL`,
|
|
[now, input.userId, input.relayHostId, input.relayDeviceId]
|
|
)
|
|
const result: DeviceCredentialInstalled = {
|
|
v: 1,
|
|
reqId: input.reqId,
|
|
authorizationMode: input.authorization.mode,
|
|
currentVersion,
|
|
resumeExpiresAt,
|
|
...(graceExpiresAt === undefined ? {} : { graceExpiresAt })
|
|
}
|
|
await transaction.query(
|
|
`INSERT INTO relay_install_results
|
|
(user_id, relay_host_id, relay_device_id, req_id, authorization_mode,
|
|
result_json, committed_at) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
input.userId, input.relayHostId, input.relayDeviceId, input.reqId,
|
|
input.authorization.mode, JSON.stringify(result), now
|
|
]
|
|
)
|
|
await this.auditWith(transaction, {
|
|
type: 'credential-installed',
|
|
userId: input.userId,
|
|
relayHostId: input.relayHostId,
|
|
relayDeviceId: input.relayDeviceId,
|
|
detail: { reqId: input.reqId, authorizationMode: input.authorization.mode, currentVersion }
|
|
})
|
|
return result
|
|
})
|
|
}
|
|
|
|
async installStatus(input: RelayIdentity & {
|
|
relayDeviceId: string
|
|
reqId: string
|
|
}): Promise<DeviceCredentialInstalled | null> {
|
|
return await this.installStatusWith(this.database, input)
|
|
}
|
|
|
|
async confirmResume(input: RelayIdentity & {
|
|
reqId: string
|
|
basisConnId: string
|
|
owningControlGeneration: number
|
|
}): Promise<DeviceResumeConfirmed> {
|
|
return await this.database.transaction(async (transaction) => {
|
|
const prior = await transaction.query(
|
|
`SELECT basis_conn_id, result_json FROM relay_confirm_results
|
|
WHERE user_id = ? AND relay_host_id = ? AND req_id = ?`,
|
|
[input.userId, input.relayHostId, input.reqId]
|
|
)
|
|
if (prior[0]) {
|
|
if (string(prior[0], 'basis_conn_id') !== input.basisConnId) {
|
|
throw new RelayStoreError('confirmation_tuple_mismatch')
|
|
}
|
|
return JSON.parse(string(prior[0], 'result_json')) as DeviceResumeConfirmed
|
|
}
|
|
const rows = await transaction.query(
|
|
`SELECT * FROM relay_connection_bases WHERE basis_conn_id = ?`,
|
|
[input.basisConnId]
|
|
)
|
|
const basis = rows[0]
|
|
const now = this.now()
|
|
if (
|
|
!basis ||
|
|
string(basis, 'user_id') !== input.userId ||
|
|
string(basis, 'relay_host_id') !== input.relayHostId ||
|
|
string(basis, 'credential_kind') !== 'resume' ||
|
|
number(basis, 'owning_control_generation') !== input.owningControlGeneration ||
|
|
number(basis, 'active') !== 1 ||
|
|
number(basis, 'deadline') < now
|
|
) {
|
|
throw new RelayStoreError('confirmation_not_active')
|
|
}
|
|
const relayDeviceId = string(basis, 'relay_device_id')
|
|
await transaction.query(
|
|
`UPDATE relay_devices SET updated_at = updated_at
|
|
WHERE user_id = ? AND relay_host_id = ? AND relay_device_id = ?`,
|
|
[input.userId, input.relayHostId, relayDeviceId]
|
|
)
|
|
const devices = await transaction.query(
|
|
`SELECT * FROM relay_devices
|
|
WHERE user_id = ? AND relay_host_id = ? AND relay_device_id = ?`,
|
|
[input.userId, input.relayHostId, relayDeviceId]
|
|
)
|
|
const device = devices[0]
|
|
if (!device) throw new RelayStoreError('credential_not_found')
|
|
const acceptedVersion = number(basis, 'accepted_credential_version')
|
|
const decision = decideResumeCommit(
|
|
{
|
|
currentVersion: number(device, 'current_version'),
|
|
currentHash: string(device, 'current_hash'),
|
|
currentExpiresAt: number(device, 'current_expires_at'),
|
|
graceVersion: optionalNumber(device, 'grace_version'),
|
|
graceHash: typeof device.grace_hash === 'string' ? device.grace_hash : undefined,
|
|
graceExpiresAt: optionalNumber(device, 'grace_expires_at'),
|
|
revokedAt: optionalNumber(device, 'revoked_at')
|
|
},
|
|
acceptedVersion,
|
|
now
|
|
)
|
|
if (decision.startsWith('reject-')) throw new RelayStoreError(decision)
|
|
const renewed = decision === 'renew-current'
|
|
const resumeExpiresAt = renewed
|
|
? now + RELAY_PROTOCOL_LIMITS.resumeTtlMs
|
|
: number(device, 'current_expires_at')
|
|
if (renewed) {
|
|
await transaction.query(
|
|
`UPDATE relay_devices SET current_expires_at = ?, updated_at = ?
|
|
WHERE user_id = ? AND relay_host_id = ? AND relay_device_id = ?`,
|
|
[resumeExpiresAt, now, input.userId, input.relayHostId, relayDeviceId]
|
|
)
|
|
}
|
|
const result: DeviceResumeConfirmed = {
|
|
v: 1,
|
|
reqId: input.reqId,
|
|
currentVersion: number(device, 'current_version'),
|
|
acceptedAs: string(basis, 'accepted_as') as 'current' | 'grace',
|
|
renewed,
|
|
resumeExpiresAt,
|
|
...(optionalNumber(device, 'grace_expires_at') === undefined
|
|
? {}
|
|
: { graceExpiresAt: optionalNumber(device, 'grace_expires_at') })
|
|
}
|
|
await transaction.query(
|
|
`INSERT INTO relay_confirm_results
|
|
(user_id, relay_host_id, req_id, basis_conn_id, tuple_json, result_json, committed_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
input.userId, input.relayHostId, input.reqId, input.basisConnId,
|
|
JSON.stringify({
|
|
relayDeviceId,
|
|
acceptedCredentialVersion: acceptedVersion,
|
|
acceptedAs: result.acceptedAs,
|
|
confirmDeadline: number(basis, 'deadline'),
|
|
owningControlGeneration: input.owningControlGeneration
|
|
}),
|
|
JSON.stringify(result),
|
|
now
|
|
]
|
|
)
|
|
await this.auditWith(transaction, {
|
|
type: 'resume-confirmed',
|
|
userId: input.userId,
|
|
relayHostId: input.relayHostId,
|
|
relayDeviceId,
|
|
detail: { reqId: input.reqId, basisConnId: input.basisConnId, renewed }
|
|
})
|
|
return result
|
|
})
|
|
}
|
|
|
|
async deactivateBasis(basisConnId: string): Promise<void> {
|
|
await this.database.query(`UPDATE relay_connection_bases SET active = ? WHERE basis_conn_id = ?`, [
|
|
0,
|
|
basisConnId
|
|
])
|
|
}
|
|
|
|
async revoke(identity: RelayIdentity, relayDeviceId: string): Promise<void> {
|
|
await this.database.transaction(async (transaction) => {
|
|
const now = this.now()
|
|
await transaction.query(
|
|
`UPDATE relay_devices SET revoked_at = ?, updated_at = ?
|
|
WHERE user_id = ? AND relay_host_id = ? AND relay_device_id = ?`,
|
|
[now, now, identity.userId, identity.relayHostId, relayDeviceId]
|
|
)
|
|
await transaction.query(
|
|
`UPDATE relay_invites SET state = ?, updated_at = ?
|
|
WHERE user_id = ? AND relay_host_id = ? AND relay_device_id = ?`,
|
|
['invalidated', now, identity.userId, identity.relayHostId, relayDeviceId]
|
|
)
|
|
await this.auditWith(transaction, {
|
|
type: 'device-revoked',
|
|
...identity,
|
|
relayDeviceId,
|
|
detail: {}
|
|
})
|
|
})
|
|
}
|
|
|
|
async resolveResume(
|
|
relayHostId: string,
|
|
token: string
|
|
): Promise<{ userId: string; relayDeviceId: string } | null> {
|
|
const tokenHash = hashCredential(token)
|
|
const rows = await this.database.query(
|
|
`SELECT * FROM relay_devices
|
|
WHERE relay_host_id = ? AND (current_hash = ? OR grace_hash = ?)`,
|
|
[relayHostId, tokenHash, tokenHash]
|
|
)
|
|
const device = rows[0]
|
|
if (!device || optionalNumber(device, 'revoked_at') !== undefined) return null
|
|
const now = this.now()
|
|
const currentValid =
|
|
equalHash(string(device, 'current_hash'), tokenHash) &&
|
|
number(device, 'current_expires_at') > now
|
|
const graceHash = device.grace_hash
|
|
const graceValid =
|
|
typeof graceHash === 'string' &&
|
|
equalHash(graceHash, tokenHash) &&
|
|
(optionalNumber(device, 'grace_expires_at') ?? 0) > now
|
|
return currentValid || graceValid
|
|
? { userId: string(device, 'user_id'), relayDeviceId: string(device, 'relay_device_id') }
|
|
: null
|
|
}
|
|
|
|
async validateInviteForMove(relayHostId: string, token: string): Promise<boolean> {
|
|
return Boolean(await this.resolveInviteForMove(relayHostId, token))
|
|
}
|
|
|
|
async resolveInviteForMove(
|
|
relayHostId: string,
|
|
token: string
|
|
): Promise<{ userId: string; relayDeviceId: string } | null> {
|
|
const tokenHash = hashCredential(token)
|
|
const rows = await this.database.query(
|
|
`SELECT user_id, relay_device_id, token_hash, state, attempt_count, max_attempts, expires_at
|
|
FROM relay_invites WHERE relay_host_id = ? AND token_hash = ?`,
|
|
[relayHostId, tokenHash]
|
|
)
|
|
const invite = rows[0]
|
|
const valid = Boolean(
|
|
invite &&
|
|
equalHash(string(invite, 'token_hash'), tokenHash) &&
|
|
['available', 'reserved', 'cooldown'].includes(string(invite, 'state')) &&
|
|
number(invite, 'attempt_count') < number(invite, 'max_attempts') &&
|
|
number(invite, 'expires_at') > this.now()
|
|
)
|
|
return valid && invite
|
|
? { userId: string(invite, 'user_id'), relayDeviceId: string(invite, 'relay_device_id') }
|
|
: null
|
|
}
|
|
|
|
async consumeRate(input: {
|
|
scopeKey: string
|
|
kind: string
|
|
limit: number
|
|
windowMs: number
|
|
}): Promise<void> {
|
|
await this.database.transaction(async (transaction) => {
|
|
await this.consumeRateWith(
|
|
transaction,
|
|
input.scopeKey,
|
|
input.kind,
|
|
input.limit,
|
|
input.windowMs,
|
|
this.now()
|
|
)
|
|
})
|
|
}
|
|
|
|
async cleanup(): Promise<void> {
|
|
const now = this.now()
|
|
await this.database.transaction(async (transaction) => {
|
|
await transaction.query(
|
|
`UPDATE relay_invites SET state = ?, reservation_id = NULL,
|
|
reservation_expires_at = NULL, updated_at = ?
|
|
WHERE expires_at <= ? AND state IN (?, ?, ?)`,
|
|
['expired', now, now, 'available', 'reserved', 'cooldown']
|
|
)
|
|
await transaction.query(
|
|
`UPDATE relay_invites SET state = ?, reservation_id = NULL,
|
|
reservation_expires_at = NULL, cooldown_until = ?, updated_at = ?
|
|
WHERE state = ? AND reservation_expires_at <= ? AND expires_at > ?`,
|
|
['cooldown', now + RELAY_PROTOCOL_LIMITS.inviteAttemptCooldownMs, now, 'reserved', now, now]
|
|
)
|
|
await transaction.query(
|
|
`UPDATE relay_connection_bases SET active = ? WHERE active = ? AND deadline <= ?`,
|
|
[0, 1, now]
|
|
)
|
|
await transaction.query(
|
|
`UPDATE relay_direct_authorizations SET consumed_at = ?
|
|
WHERE consumed_at IS NULL AND deadline <= ?`,
|
|
[now, now]
|
|
)
|
|
await transaction.query(
|
|
`DELETE FROM relay_rate_windows WHERE window_started_at < ?`,
|
|
[now - 24 * 60 * 60 * 1000]
|
|
)
|
|
})
|
|
await this.reapSettledCredentials(now)
|
|
}
|
|
|
|
// Outside the sweep transaction on purpose: each delete is idempotent and independent of the
|
|
// state transitions above, so batching them in would only hold their row locks for longer.
|
|
private async reapSettledCredentials(now: number): Promise<void> {
|
|
await this.reapBatch(
|
|
'relay_invites',
|
|
'state IN (?, ?, ?) AND updated_at <= ?',
|
|
['expired', 'consumed', 'invalidated', now - TERMINAL_INVITE_RETENTION_MS]
|
|
)
|
|
// deadline, not created_at: it is the second column of relay_connection_bases_active_deadline,
|
|
// so once the backlog is drained this batch learns there is nothing left to do from the index
|
|
// instead of the 1.5 GB heap. Both readers reject a passed deadline, so a day past one is
|
|
// unusable whatever the active flag says.
|
|
await this.reapBatch('relay_connection_bases', 'active = ? AND deadline <= ?', [
|
|
0,
|
|
now - INACTIVE_AUTHORIZATION_RETENTION_MS
|
|
])
|
|
// consumed_at, not deadline: consumption is what settles this row, and it can happen well
|
|
// before the deadline, so measuring from it retains the row for the full window either way.
|
|
await this.reapBatch(
|
|
'relay_direct_authorizations',
|
|
'consumed_at IS NOT NULL AND consumed_at <= ?',
|
|
[now - INACTIVE_AUTHORIZATION_RETENTION_MS]
|
|
)
|
|
}
|
|
|
|
// ctid/rowid, not the primary key: the physical address lets the delete re-find exactly the batch
|
|
// the subquery located instead of re-matching the predicate per row.
|
|
private async reapBatch(table: string, predicate: string, params: unknown[]): Promise<void> {
|
|
const address = this.database.dialect === 'sqlite' ? 'rowid' : 'ctid'
|
|
await this.database.query(
|
|
`DELETE FROM ${table} WHERE ${address} IN (
|
|
SELECT ${address} FROM ${table} WHERE ${predicate} LIMIT ${REAP_BATCH_ROWS}
|
|
)`,
|
|
params
|
|
)
|
|
}
|
|
|
|
private async installStatusWith(
|
|
database: RelayDatabase,
|
|
input: RelayIdentity & { relayDeviceId: string; reqId: string }
|
|
): Promise<DeviceCredentialInstalled | null> {
|
|
const rows = await database.query(
|
|
`SELECT result_json FROM relay_install_results
|
|
WHERE user_id = ? AND relay_host_id = ? AND relay_device_id = ? AND req_id = ?`,
|
|
[input.userId, input.relayHostId, input.relayDeviceId, input.reqId]
|
|
)
|
|
return rows[0] ? (JSON.parse(string(rows[0], 'result_json')) as DeviceCredentialInstalled) : null
|
|
}
|
|
|
|
private async validateInstallAuthorization(
|
|
transaction: RelayDatabase,
|
|
input: InstallInput,
|
|
now: number
|
|
): Promise<string | null> {
|
|
if (input.authorization.mode === 'relay-basis') {
|
|
const rows = await transaction.query(
|
|
`SELECT * FROM relay_connection_bases WHERE basis_conn_id = ?`,
|
|
[input.authorization.basisConnId]
|
|
)
|
|
const basis = rows[0]
|
|
if (
|
|
!basis ||
|
|
string(basis, 'user_id') !== input.userId ||
|
|
string(basis, 'relay_host_id') !== input.relayHostId ||
|
|
string(basis, 'relay_device_id') !== input.relayDeviceId ||
|
|
string(basis, 'credential_kind') !== 'invite' ||
|
|
number(basis, 'owning_control_generation') !== input.owningControlGeneration ||
|
|
number(basis, 'active') !== 1 ||
|
|
number(basis, 'deadline') < now
|
|
) {
|
|
throw new RelayStoreError('invalid_relay_basis')
|
|
}
|
|
return string(basis, 'invite_token_hash')
|
|
}
|
|
const rows = await transaction.query(
|
|
`SELECT * FROM relay_direct_authorizations WHERE direct_auth_id = ?`,
|
|
[input.authorization.directAuthId]
|
|
)
|
|
const direct = rows[0]
|
|
if (
|
|
!direct ||
|
|
string(direct, 'user_id') !== input.userId ||
|
|
string(direct, 'relay_host_id') !== input.relayHostId ||
|
|
string(direct, 'relay_device_id') !== input.relayDeviceId ||
|
|
number(direct, 'owning_control_generation') !== input.owningControlGeneration ||
|
|
number(direct, 'deadline') < now ||
|
|
optionalNumber(direct, 'consumed_at') !== undefined
|
|
) {
|
|
throw new RelayStoreError('invalid_direct_authorization')
|
|
}
|
|
await transaction.query(
|
|
`UPDATE relay_direct_authorizations SET consumed_at = ? WHERE direct_auth_id = ?`,
|
|
[now, input.authorization.directAuthId]
|
|
)
|
|
return null
|
|
}
|
|
|
|
private async consumeRateWith(
|
|
transaction: RelayDatabase,
|
|
scopeKey: string,
|
|
kind: string,
|
|
limit: number,
|
|
windowMs: number,
|
|
now: number
|
|
): Promise<void> {
|
|
const windowStartedAt = Math.floor(now / windowMs) * windowMs
|
|
const rows = await transaction.query(
|
|
`INSERT INTO relay_rate_windows
|
|
(scope_key, window_kind, window_started_at, count) VALUES (?, ?, ?, ?)
|
|
ON CONFLICT (scope_key, window_kind, window_started_at) DO UPDATE
|
|
SET count = relay_rate_windows.count + 1 RETURNING count`,
|
|
[scopeKey, kind, windowStartedAt, 1]
|
|
)
|
|
if (number(rows[0]!, 'count') > limit) throw new RelayStoreError('rate_limit_exceeded')
|
|
}
|
|
|
|
private async auditWith(
|
|
transaction: RelayDatabase,
|
|
event: RelayIdentity & {
|
|
type: string
|
|
relayDeviceId?: string
|
|
detail: Record<string, unknown>
|
|
}
|
|
): Promise<void> {
|
|
await transaction.query(
|
|
`INSERT INTO relay_audit_events
|
|
(id, at, type, user_id, relay_host_id, relay_device_id, detail_json)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
randomUUID(), this.now(), event.type, event.userId, event.relayHostId,
|
|
event.relayDeviceId, JSON.stringify(event.detail)
|
|
]
|
|
)
|
|
}
|
|
}
|