diff --git a/prisma/migrations/0260_step_up_elevation/migration.sql b/prisma/migrations/0267_step_up_elevation/migration.sql similarity index 100% rename from prisma/migrations/0260_step_up_elevation/migration.sql rename to prisma/migrations/0267_step_up_elevation/migration.sql diff --git a/prisma/schema.prisma b/prisma/schema.prisma index bcec3d1b9..61e8fff79 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -3257,8 +3257,11 @@ model StepUpElevation { // HMAC-SHA256 (`hashToken`) of the raw `hle_<64 hex>` secret. Unique so a // redemption is a single indexed lookup and a collision cannot alias two rows. tokenHash String @unique @map("token_hash") - // Which factor was re-proved: "password" or "passkey". Audit/telemetry only — - // never an authorisation input. + // Which factor was re-proved: "password", "totp", "webauthn", or "passkey". + // This IS an authorisation input, not telemetry — see `FRESH_FACTOR_METHODS` + // in `@/lib/auth/step-up`. Only totp / webauthn / passkey satisfy a + // fresh-factor gate; password reaches what a plain cookie session reaches + // and stops there. method String expiresAt DateTime @map("expires_at") consumedAt DateTime? @map("consumed_at") @@ -3270,7 +3273,7 @@ model StepUpElevation { @@index([userId]) @@index([apiTokenId]) @@index([expiresAt]) - // NOTE: migration 0260 additionally creates a PARTIAL unique index — + // NOTE: migration 0267 additionally creates a PARTIAL unique index — // `(api_token_id) WHERE consumed_at IS NULL` — so at most one elevation per // token is ever redeemable. Prisma cannot express a partial index, and a // plain `@@unique([apiTokenId])` would be wrong: it would also forbid minting diff --git a/src/lib/auth/step-up.ts b/src/lib/auth/step-up.ts index d3aa78834..b718e54c2 100644 --- a/src/lib/auth/step-up.ts +++ b/src/lib/auth/step-up.ts @@ -328,18 +328,8 @@ export async function revokeStepUpElevations(userId: string): Promise { await prisma.stepUpElevation.deleteMany({ where: { userId } }); } -/** - * Delete every elevation whose window has closed. - * - * Runs on the queue, not on a request. The earlier fire-and-forget sweep on the - * mint path was a cross-tenant unbounded DELETE competing with the request for a - * pool connection, and recurring work belongs on pg-boss. An expired row is - * already unredeemable — the predicate in `claimStepUpElevation` refuses it — so - * this is hygiene, not enforcement, and a missed run breaks nothing. - */ -export async function pruneExpiredStepUpElevations(): Promise { - const { count } = await prisma.stepUpElevation.deleteMany({ - where: { expiresAt: { lt: new Date() } }, - }); - return count; -} +// The expired-elevation sweep runs on the pg-boss queue against the WORKER +// Prisma client, not this module's request-scoped one — see +// `handleStepUpElevationCleanup` in `@/lib/jobs/reminder/cleanup-handlers`. +// A copy of the predicate here would be unreachable and only invite drift +// between the two, so it does not exist.