Files
Randify.pro/src/lib/auth/jwt.ts
T
emilandClaude Opus 4.7 4b78e9edad fix(auth): defer env validation to first access so build works without secrets
`validateAuthEnv()` was running at module-load time, which fired during
`astro build` while constructing the route manifest — before any actual
request needed the secrets. CI build failed even though build-time code
doesn't use JWT_SECRET / OAuth secrets.

- src/lib/auth/env.ts: wrap authEnv in a Proxy that runs validation
  on first property read; cache the validated object after.
- src/lib/auth/jwt.ts: defer `new TextEncoder().encode(...)` of the
  secret behind a memoised getSecret() helper.
- src/lib/auth/oauth.ts: same for the JWT secret; vkOAuthConfig and
  yandexOAuthConfig switched to getter-based properties so credential
  access is also lazy.
- src/lib/auth/auth.test.ts: 3 tests previously asserted "throws at
  module load"; updated to assert "throws on first access" — semantic
  guarantee (invalid env throws) is preserved.

Runtime fail-fast is intact: any auth route reading authEnv.X will
throw with the same descriptive Zod error if a var is missing. Build
just no longer crashes when secrets aren't in env (e.g. CI deploy
pipeline running tsc/lint/build without secrets).

Verified: npm run build succeeds with no auth env vars set; tsc 0,
lint 0, vitest 339/339.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 23:45:03 +03:00

46 lines
1.2 KiB
TypeScript

import { SignJWT, jwtVerify } from 'jose';
import type { APIContext } from 'astro';
import { authEnv } from './env';
let secretCache: Uint8Array | null = null;
function getSecret(): Uint8Array {
if (!secretCache) {
secretCache = new TextEncoder().encode(authEnv.JWT_SECRET);
}
return secretCache;
}
export async function createToken(userId: string): Promise<string> {
return new SignJWT({ sub: userId })
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('7d')
.sign(getSecret());
}
export async function verifyToken(token: string): Promise<{ sub: string }> {
const { payload } = await jwtVerify(token, getSecret(), {
clockTolerance: 60,
});
if (!payload.sub || typeof payload.sub !== 'string') {
throw new Error('Invalid token payload');
}
return { sub: payload.sub };
}
export function setAuthCookie(token: string, context: APIContext): void {
context.cookies.set('auth_token', token, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7,
path: '/',
});
}
export function clearAuthCookie(context: APIContext): void {
context.cookies.delete('auth_token', {
path: '/',
});
}