import { AUTH_DEFAULTS, AUTH_EVENT_NAMES, AUTH_REVOKE_REASONS } from '$libs/auth/consts'; import type { AuthClockPort, AuthCryptoPort, AuthStoreAdapter } from '$libs/auth/contracts'; import type { AuthRefreshFamilyId, AuthRefreshTokenId } from '$libs/auth/types'; import { AuthTokenReuseDetectedError } from '$libs/auth/errors'; import { createAuthRefreshTokenId } from '$libs/auth/ids'; import { createAuthRandomToken, hashAuthToken } from '$libs/auth/tokens'; export interface AuthRefreshRotationInput { readonly store: AuthStoreAdapter; readonly crypto: AuthCryptoPort; readonly clock: AuthClockPort; readonly token: string; } export interface AuthRefreshRotationResult { readonly refreshToken: string; readonly refreshTokenId: AuthRefreshTokenId; readonly familyId: AuthRefreshFamilyId; } export async function rotateAuthRefreshToken( input: AuthRefreshRotationInput ): Promise { const nowMs = input.clock.nowMs(); const tokenHash = await hashAuthToken(input.crypto, input.token); const current = await input.store.findRefreshTokenForUpdate({ tokenHash }); if (!current || current.revokedAt) throw new AuthTokenReuseDetectedError(undefined, { eventName: AUTH_EVENT_NAMES.REFRESH_REUSE_DETECTED }); if (current.consumedAt && nowMs - current.consumedAt > AUTH_DEFAULTS.REFRESH_REUSE_GRACE_MS) { await input.store.revokeRefreshFamily({ familyId: current.familyId, nowMs, reason: AUTH_REVOKE_REASONS.REFRESH_REUSE }); throw new AuthTokenReuseDetectedError(undefined, { eventName: AUTH_EVENT_NAMES.REFRESH_REUSE_DETECTED }); } const refreshToken = createAuthRandomToken(input.crypto); const child = { id: createAuthRefreshTokenId(input.crypto), familyId: current.familyId, tokenHash: await hashAuthToken(input.crypto, refreshToken), parentTokenId: current.id, issuedAt: nowMs }; const result = await input.store.rotateRefreshToken({ currentTokenId: current.id, childToken: child, consumedAt: nowMs }); return { refreshToken, refreshTokenId: result.child.id, familyId: result.child.familyId }; }