Files
orca/cloud/apps/push/src/host-session-store.ts
T
Jinwoo Hong eb2f2d52ae feat(cloud): native push gateway and dedicated infrastructure (1/3) (#19912)
* refactor(cloud): share PostgreSQL schema startup between services

* feat(cloud): add durable native push notification gateway

* infra(push): define dedicated gateway resources and operational checks

* fix(push): bound cross-host admission and simplify gateway configuration

* fix(push): validate deploy configuration and preserve topic-error registrations
2026-09-10 17:59:46 -04:00

66 lines
2.6 KiB
TypeScript

import { createHash, randomBytes } from 'node:crypto'
import { PUSH_LIMITS } from '@orca-cloud/push-contract'
import type { PushDatabase } from './push-database.js'
export type IssuedPushSession = {
sessionToken: string
expiresAt: number
hostFingerprint: string
}
export type PushSessionLookup =
| { ok: true; hostFingerprint: string; expiresAt: number }
| { ok: false; reason: 'unknown_session' | 'session_expired' }
function hashSessionToken(sessionToken: string): string {
return createHash('sha256').update(sessionToken).digest('base64url')
}
export class PushHostSessionStore {
constructor(
private readonly database: PushDatabase,
private readonly now: () => number = Date.now
) {}
async create(hostFingerprint: string): Promise<IssuedPushSession> {
const sessionToken = randomBytes(32).toString('base64url')
const createdAt = this.now()
const expiresAt = createdAt + PUSH_LIMITS.sessionTtlMs
await this.database.transaction(async (transaction) => {
// Why: a desktop holds one session at a time and only re-proves once it is
// gone, so an earlier row is dead weight. It also bounds the table to one
// row per host however many proofs a self-minted identity answers.
await transaction.lockQuotaScope(`orca-push-session:${hostFingerprint}`)
await transaction.query('DELETE FROM push_sessions WHERE host_fingerprint = ?', [
hostFingerprint
])
await transaction.query(
`INSERT INTO push_sessions (token_hash, host_fingerprint, expires_at, created_at)
VALUES (?, ?, ?, ?)`,
[hashSessionToken(sessionToken), hostFingerprint, expiresAt, createdAt]
)
})
return { sessionToken, expiresAt, hostFingerprint }
}
async resolve(sessionToken: string): Promise<PushSessionLookup> {
const [row] = await this.database.query(
'SELECT host_fingerprint, expires_at FROM push_sessions WHERE token_hash = ?',
[hashSessionToken(sessionToken)]
)
if (!row) return { ok: false, reason: 'unknown_session' }
const expiresAt = Number(row.expires_at)
// No skew grace here: a 24h session that just expired should be re-minted
// through the challenge, which is cheap and already handled by the host.
if (this.now() > expiresAt) return { ok: false, reason: 'session_expired' }
return { ok: true, hostFingerprint: String(row.host_fingerprint), expiresAt }
}
async pruneExpired(): Promise<number> {
const [result] = await this.database.query('DELETE FROM push_sessions WHERE expires_at < ?', [
this.now()
])
return Number(result?.changes ?? 0)
}
}