import { DurableObject } from "cloudflare:workers"; export const TTL_MS = 7 * 24 * 60 * 60 * 1000; export const MIN_INTERVAL_MS = 2 * 60 * 1000; /** Max distinct DID+origin rows retained per browser session. */ export const MAX_ACCOUNT_ROWS = 40; export type Account = { did: string; origins: string[]; }; type AccountRow = { did: string; origin: string; last_seen: number; }; export class SessionDO extends DurableObject { constructor(ctx: DurableObjectState, env: object) { super(ctx, env); ctx.storage.sql.exec(` CREATE TABLE IF NOT EXISTS accounts ( did TEXT NOT NULL, origin TEXT NOT NULL, last_seen INTEGER NOT NULL, PRIMARY KEY (did, origin) ) `); } async loggedIn(did: string, origin: string): Promise<{ ok: boolean }> { const now = Date.now(); const existing = this.ctx.storage.sql .exec( "SELECT did, origin, last_seen FROM accounts WHERE did = ? AND origin = ?", did, origin, ) .toArray()[0]; if (existing && now - existing.last_seen < MIN_INTERVAL_MS) { return { ok: true }; } if (!existing) { await this.enforceRowCap(); } this.ctx.storage.sql.exec( `INSERT INTO accounts (did, origin, last_seen) VALUES (?, ?, ?) ON CONFLICT(did, origin) DO UPDATE SET last_seen = excluded.last_seen`, did, origin, now, ); await this.touchAlarm(); return { ok: true }; } async logout(did: string, origin: string): Promise<{ ok: boolean }> { this.ctx.storage.sql.exec( "DELETE FROM accounts WHERE did = ? AND origin = ?", did, origin, ); return { ok: true }; } async list(): Promise<{ accounts: Account[] }> { const cutoff = Date.now() - TTL_MS; const rows = this.ctx.storage.sql .exec( "SELECT did, origin, last_seen FROM accounts WHERE last_seen >= ? ORDER BY last_seen DESC", cutoff, ) .toArray(); const byDid = new Map(); for (const row of rows) { const origins = byDid.get(row.did) ?? []; if (!origins.includes(row.origin)) origins.push(row.origin); byDid.set(row.did, origins); } return { accounts: [...byDid.entries()].map(([did, origins]) => ({ did, origins })), }; } async alarm(): Promise { const cutoff = Date.now() - TTL_MS; this.ctx.storage.sql.exec("DELETE FROM accounts WHERE last_seen < ?", cutoff); const next = this.ctx.storage.sql .exec<{ m: number | null }>("SELECT MIN(last_seen) AS m FROM accounts") .toArray()[0]; if (next?.m != null) { await this.ctx.storage.setAlarm(Number(next.m) + TTL_MS); } } private async enforceRowCap(): Promise { const cutoff = Date.now() - TTL_MS; this.ctx.storage.sql.exec("DELETE FROM accounts WHERE last_seen < ?", cutoff); const count = this.ctx.storage.sql .exec<{ n: number }>("SELECT COUNT(*) AS n FROM accounts") .toArray()[0]?.n; if (count == null || count < MAX_ACCOUNT_ROWS) return; const overflow = count - MAX_ACCOUNT_ROWS + 1; this.ctx.storage.sql.exec( `DELETE FROM accounts WHERE rowid IN ( SELECT rowid FROM accounts ORDER BY last_seen ASC LIMIT ? )`, overflow, ); } private async touchAlarm(): Promise { const current = await this.ctx.storage.getAlarm(); const when = Date.now() + TTL_MS; if (current == null || current > when) { await this.ctx.storage.setAlarm(when); } } }