File
Blob: src/worker/objects/auth-object.ts
| 1 | import { DurableObject } from "cloudflare:workers"; |
| 2 | |
| 3 | import { constantTimeEqual } from "@/worker/auth/bytes"; |
| 4 | import { createAuthDoDb, type AuthDoDb } from "@/worker/db/auth-do/client"; |
| 5 | import { |
| 6 | createPat, |
| 7 | createSession, |
| 8 | getPat, |
| 9 | getSession, |
| 10 | listPats, |
| 11 | revokeSession, |
| 12 | touchSession, |
| 13 | updatePat, |
| 14 | } from "@/worker/db/auth-do/repository"; |
| 15 | import type { NewPatRow, NewSessionRow, PatRow, SessionRow } from "@/worker/db/auth-do/schema"; |
| 16 | import { migrateAuthObject } from "@/worker/objects/common/migrations"; |
| 17 | |
| 18 | export interface SessionValidation { |
| 19 | ok: true; |
| 20 | session: SessionRow; |
| 21 | } |
| 22 | |
| 23 | export interface PatVerification { |
| 24 | ok: true; |
| 25 | pat: PatRow; |
| 26 | } |
| 27 | |
| 28 | export class AuthObject extends DurableObject<Env> { |
| 29 | readonly db: AuthDoDb; |
| 30 | |
| 31 | constructor(ctx: DurableObjectState, env: Env) { |
| 32 | super(ctx, env); |
| 33 | this.db = createAuthDoDb(ctx.storage); |
| 34 | |
| 35 | ctx.blockConcurrencyWhile(async () => { |
| 36 | await migrateAuthObject(this.db); |
| 37 | }); |
| 38 | } |
| 39 | |
| 40 | async ping(): Promise<{ ok: true; object: "auth" }> { |
| 41 | return { ok: true, object: "auth" }; |
| 42 | } |
| 43 | |
| 44 | async createSession(row: NewSessionRow): Promise<SessionRow> { |
| 45 | createSession(this.db, row); |
| 46 | return getSession(this.db, row.id)!; |
| 47 | } |
| 48 | |
| 49 | async validateSession(input: { sessionId: string; nowMs: number }): Promise<SessionValidation | { ok: false }> { |
| 50 | const session = getSession(this.db, input.sessionId); |
| 51 | if (!session || session.revokedAtMs !== null || session.expiresAtMs <= input.nowMs) return { ok: false }; |
| 52 | touchSession(this.db, input.sessionId, input.nowMs); |
| 53 | return { ok: true, session: getSession(this.db, input.sessionId) ?? session }; |
| 54 | } |
| 55 | |
| 56 | async revokeSession(input: { sessionId: string; nowMs: number }): Promise<{ ok: true }> { |
| 57 | revokeSession(this.db, input.sessionId, input.nowMs); |
| 58 | return { ok: true }; |
| 59 | } |
| 60 | |
| 61 | async createPat(row: NewPatRow): Promise<PatRow> { |
| 62 | createPat(this.db, row); |
| 63 | return getPat(this.db, row.id)!; |
| 64 | } |
| 65 | |
| 66 | async listPats(): Promise<PatRow[]> { |
| 67 | return listPats(this.db); |
| 68 | } |
| 69 | |
| 70 | async getPat(input: { patId: string }): Promise<PatRow | null> { |
| 71 | return getPat(this.db, input.patId) ?? null; |
| 72 | } |
| 73 | |
| 74 | async updatePat(input: { |
| 75 | patId: string; |
| 76 | name?: string; |
| 77 | expiresAtMs?: number | null; |
| 78 | revokedAtMs?: number | null; |
| 79 | }): Promise<PatRow | null> { |
| 80 | return updatePat(this.db, input.patId, input) ?? null; |
| 81 | } |
| 82 | |
| 83 | async verifyPat(input: { |
| 84 | patId: string; |
| 85 | tokenDigest: string; |
| 86 | nowMs: number; |
| 87 | }): Promise<PatVerification | { ok: false }> { |
| 88 | const pat = getPat(this.db, input.patId); |
| 89 | if (!pat || pat.revokedAtMs !== null || (pat.expiresAtMs !== null && pat.expiresAtMs <= input.nowMs)) |
| 90 | return { ok: false }; |
| 91 | if (!constantTimeEqual(pat.tokenDigest, input.tokenDigest)) return { ok: false }; |
| 92 | const updated = updatePat(this.db, input.patId, { lastUsedAtMs: input.nowMs }) ?? pat; |
| 93 | return { ok: true, pat: updated }; |
| 94 | } |
| 95 | } |