`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>
83 lines
2.6 KiB
TypeScript
83 lines
2.6 KiB
TypeScript
import { SignJWT, jwtVerify } from 'jose';
|
|
import { authEnv } from './env';
|
|
|
|
let secretCache: Uint8Array | null = null;
|
|
function getJwtSecret(): Uint8Array {
|
|
if (!secretCache) {
|
|
secretCache = new TextEncoder().encode(authEnv.JWT_SECRET);
|
|
}
|
|
return secretCache;
|
|
}
|
|
|
|
export function generateCodeVerifier(): string {
|
|
const array = new Uint8Array(64);
|
|
crypto.getRandomValues(array);
|
|
return base64UrlEncode(array);
|
|
}
|
|
|
|
export async function generateCodeChallenge(verifier: string): Promise<string> {
|
|
const encoder = new TextEncoder();
|
|
const data = encoder.encode(verifier);
|
|
const hash = await crypto.subtle.digest('SHA-256', data);
|
|
return base64UrlEncode(new Uint8Array(hash));
|
|
}
|
|
|
|
function base64UrlEncode(buffer: Uint8Array): string {
|
|
let binary = '';
|
|
for (let i = 0; i < buffer.byteLength; i++) {
|
|
binary += String.fromCharCode(buffer[i]);
|
|
}
|
|
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
}
|
|
|
|
export function generateState(): string {
|
|
const array = new Uint8Array(32);
|
|
crypto.getRandomValues(array);
|
|
return base64UrlEncode(array);
|
|
}
|
|
|
|
// Lazy getters so env validation doesn't fire at module import time
|
|
// (which happens during `astro build` when secrets are absent).
|
|
export const vkOAuthConfig = {
|
|
get clientId() { return authEnv.VK_CLIENT_ID; },
|
|
get clientSecret() { return authEnv.VK_CLIENT_SECRET; },
|
|
authUrl: 'https://id.vk.ru/authorize',
|
|
tokenUrl: 'https://id.vk.ru/oauth2/auth',
|
|
scope: 'email phone',
|
|
};
|
|
|
|
export const yandexOAuthConfig = {
|
|
get clientId() { return authEnv.YANDEX_CLIENT_ID; },
|
|
get clientSecret() { return authEnv.YANDEX_CLIENT_SECRET; },
|
|
authUrl: 'https://oauth.yandex.com/authorize',
|
|
tokenUrl: 'https://oauth.yandex.com/token',
|
|
scope: 'login:email login:info login:avatar',
|
|
};
|
|
|
|
export async function createSessionToken(userId: number): Promise<string> {
|
|
return new SignJWT({ sub: String(userId) })
|
|
.setProtectedHeader({ alg: 'HS256' })
|
|
.setIssuedAt()
|
|
.setExpirationTime('7d')
|
|
.sign(getJwtSecret());
|
|
}
|
|
|
|
export async function verifySessionToken(token: string): Promise<{ userId: number } | null> {
|
|
try {
|
|
const { payload } = await jwtVerify(token, getJwtSecret(), { clockTolerance: 60 });
|
|
if (!payload.sub) return null;
|
|
return { userId: Number(payload.sub) };
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export const COOKIE_NAME = 'auth_token';
|
|
export const VERIFIER_COOKIE_NAME = 'oauth_verifier';
|
|
|
|
export function getCookieValue(cookieHeader: string | null, name: string): string | null {
|
|
if (!cookieHeader) return null;
|
|
const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${name}=([^;]+)`));
|
|
return match ? decodeURIComponent(match[1]) : null;
|
|
}
|