wulf-pulse/lib/services/user-timezone.ts
lorentz ea5532c5c3 feat(07.1-03): add lib/services/user-timezone.ts helper
- getUserTimezone(session) returns validated IANA tz string with safe fallback
- DEFAULT_TIMEZONE_FALLBACK reads process.env.DEFAULT_TIMEZONE || 'UTC'
- Validates against Intl.supportedValuesOf('timeZone'); 64-char length cap
- Pure / synchronous / no DB / no @/lib/auth-utils import (avoids circular)
2026-05-07 08:01:23 -04:00

40 lines
1.5 KiB
TypeScript

// Server-side helper for resolving the calling user's IANA timezone.
//
// After Phase 7.1 Plan 01, `session.user.timezone` is a string populated
// either from the stored `"user".timezone` column or from Better Auth's
// additionalField `defaultValue` (`process.env.DEFAULT_TIMEZONE || 'UTC'`).
//
// This helper:
// - reads the value off a Better Auth session
// - validates it against `Intl.supportedValuesOf('timeZone')` (defence in
// depth — Plan 02 already validates writes, but a corrupt row from
// before this phase, or a manual SQL edit, must not crash dashboards)
// - falls back to `process.env.DEFAULT_TIMEZONE || 'UTC'` if invalid
//
// Use this in every API route that does day/week/month boundary math.
export const DEFAULT_TIMEZONE_FALLBACK = (): string =>
process.env.DEFAULT_TIMEZONE || 'UTC';
type SessionLike = {
user?: { timezone?: unknown } | null;
} | null | undefined;
function isValidIanaTimezone(tz: unknown): tz is string {
if (typeof tz !== 'string' || tz.length === 0 || tz.length > 64) return false;
try {
return Intl.supportedValuesOf('timeZone').includes(tz);
} catch {
return false;
}
}
/**
* Returns a validated IANA timezone string for the given session.
* Never throws; always returns a usable string (worst case: 'UTC').
*/
export function getUserTimezone(session: SessionLike): string {
const raw = session?.user?.timezone;
if (isValidIanaTimezone(raw)) return raw;
return DEFAULT_TIMEZONE_FALLBACK();
}