import { sql } from 'kysely' import type * as Db from '../Db.js' import type * as db_Schema from '../Schema.js' /** Columns of the `api_key_owner_tombstones` table. */ export type Table = db_Schema.ApiKeyOwnerTombstone /** Returns whether an organization or its attributed project has been deleted. */ export async function isDeleted(db: Db.Db, owner: Owner): Promise { const keys = ownerKeys(owner) return ( (await db.kysely .selectFrom('api_key_owner_tombstones') .select('id') .where('id', 'in', keys) .limit(1) .executeTakeFirst()) !== undefined ) } /** Records a permanent owner deletion fence in the caller's transaction. */ export async function markDeleted(db: Db.Db, owner: Owner): Promise { await lock(db, ownerKeys(owner), 'exclusive') await db.kysely .insertInto('api_key_owner_tombstones') .values({ createdAt: new Date().toISOString(), id: key(owner) }) .onConflict((conflict) => conflict.column('id').doNothing()) .execute() } /** Holds shared owner locks and returns whether neither owner fence exists. */ export async function lockActive(db: Db.Db, owner: Owner): Promise { return lockActiveMany(db, [owner]) } /** Holds shared locks for every owner and returns whether none has a deletion fence. */ export async function lockActiveMany(db: Db.Db, owners: readonly Owner[]): Promise { const keys = [...new Set(owners.flatMap(ownerKeys))].sort() await lock(db, keys, 'shared') return ( (await db.kysely .selectFrom('api_key_owner_tombstones') .select('id') .where('id', 'in', keys) .limit(1) .executeTakeFirst()) === undefined ) } /** API-key owner attribution checked by the deletion fence. */ export type Owner = { /** Owning organization id. */ orgId: string /** Project id when the fence applies only to one project. */ projectId?: string | undefined } /** Builds a collision-free key from typed Tempo resource ids. */ function key(owner: Owner) { return owner.projectId === undefined ? `organization:${owner.orgId}` : `project:${owner.orgId}:${owner.projectId}` } /** Builds the hierarchical lock keys for an owner. */ function ownerKeys(owner: Owner) { const keys = [key({ orgId: owner.orgId })] if (owner.projectId !== undefined) keys.push(key(owner)) return keys } /** Locks sorted owner keys, preventing mint/delete races without deadlocks. */ async function lock(db: Db.Db, keys: readonly string[], mode: 'exclusive' | 'shared') { for (const value of keys) if (mode === 'shared') await sql`SELECT pg_advisory_xact_lock_shared(hashtextextended(${value}, 0))`.execute( db.kysely, ) else await sql`SELECT pg_advisory_xact_lock(hashtextextended(${value}, 0))`.execute(db.kysely) }