fix(site): use astro:env for server-only environment variables (#574)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Darius Cepulis
2026-02-19 09:23:46 -06:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 52d20bfca8
commit 10eb92e5ea
6 changed files with 68 additions and 20 deletions
+23 -1
View File
@@ -8,7 +8,7 @@ import react from '@astrojs/react';
import sitemap from '@astrojs/sitemap';
import sentry from '@sentry/astro';
import tailwindcss from '@tailwindcss/vite';
import { defineConfig, fontProviders } from 'astro/config';
import { defineConfig, envField, fontProviders } from 'astro/config';
import checkV8Urls from './integrations/check-v8-urls';
import llmsMarkdown from './integrations/llms-markdown';
import pagefind from './integrations/pagefind';
@@ -24,6 +24,28 @@ export default defineConfig({
site: SITE_URL,
trailingSlash: 'never',
adapter: netlify(),
// Server-only secrets read at runtime (not inlined at build time).
// All optional — the site degrades gracefully without auth/Mux configured.
// See site/CLAUDE.md "Environment Variables" for full documentation.
env: {
schema: {
// OAuth — powers the video uploader login flow
OAUTH_CLIENT_ID: envField.string({ context: 'server', access: 'secret', optional: true }),
OAUTH_CLIENT_SECRET: envField.string({ context: 'server', access: 'secret', optional: true }),
OAUTH_REDIRECT_URI: envField.string({ context: 'server', access: 'secret', optional: true }),
OAUTH_URL: envField.string({ context: 'server', access: 'secret', optional: true }),
SESSION_COOKIE_PASSWORD: envField.string({ context: 'server', access: 'secret', optional: true }),
MUX_API_URL: envField.string({
context: 'server',
access: 'secret',
optional: true,
default: 'https://api.mux.com',
}),
// Mux service account credentials — only used by the /api/health/mux endpoint
MUX_TOKEN_ID: envField.string({ context: 'server', access: 'secret', optional: true }),
MUX_TOKEN_SECRET: envField.string({ context: 'server', access: 'secret', optional: true }),
},
},
redirects: {
// Redirects are configured in netlify.toml
},
+2 -3
View File
@@ -1,9 +1,8 @@
import { ActionError, defineAction } from 'astro:actions';
import { OAUTH_CLIENT_ID, OAUTH_REDIRECT_URI, OAUTH_URL } from 'astro:env/server';
import { z } from 'astro:schema';
import { SESSION_COOKIE_NAME } from '@/utils/auth';
const { OAUTH_CLIENT_ID, OAUTH_REDIRECT_URI, OAUTH_URL, PROD } = import.meta.env;
export const auth = {
/**
* Initiates the OAuth 2.0 login flow
@@ -39,7 +38,7 @@ export const auth = {
// Store state in a short-lived cookie for verification in the callback
ctx.cookies.set('state', state, {
httpOnly: true,
secure: PROD,
secure: import.meta.env.PROD,
sameSite: 'lax',
maxAge: 600, // Expires after 10 minutes
path: '/',
+4 -3
View File
@@ -1,4 +1,5 @@
import { ActionError, defineAction } from 'astro:actions';
import { MUX_API_URL, MUX_TOKEN_ID, MUX_TOKEN_SECRET } from 'astro:env/server';
import { z } from 'astro:schema';
import Mux from '@mux/mux-node';
@@ -18,13 +19,13 @@ function getMuxClient(token: string | undefined) {
return new Mux({
authorizationToken: token,
baseURL: import.meta.env.MUX_API_URL ?? 'https://api.mux.com',
baseURL: MUX_API_URL,
});
}
function getHealthMuxClient() {
const tokenId = process.env.MUX_TOKEN_ID || import.meta.env.MUX_TOKEN_ID;
const tokenSecret = process.env.MUX_TOKEN_SECRET || import.meta.env.MUX_TOKEN_SECRET;
const tokenId = MUX_TOKEN_ID;
const tokenSecret = MUX_TOKEN_SECRET;
if (!tokenId || !tokenSecret) {
throw new ActionError({
@@ -2,8 +2,28 @@
import type { APIContext } from 'astro';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { GET as callbackHandler } from '@/pages/api/auth/callback';
import { exchangeAuthorizationCode, seal } from '@/utils/auth';
const env = vi.hoisted(() => ({
OAUTH_CLIENT_ID: 'test-client-id' as string | undefined,
OAUTH_CLIENT_SECRET: 'test-client-secret' as string | undefined,
OAUTH_REDIRECT_URI: 'https://example.com/callback' as string | undefined,
OAUTH_URL: 'https://auth.example.com' as string | undefined,
}));
vi.mock('astro:env/server', () => ({
get OAUTH_CLIENT_ID() {
return env.OAUTH_CLIENT_ID;
},
get OAUTH_CLIENT_SECRET() {
return env.OAUTH_CLIENT_SECRET;
},
get OAUTH_REDIRECT_URI() {
return env.OAUTH_REDIRECT_URI;
},
get OAUTH_URL() {
return env.OAUTH_URL;
},
}));
vi.mock('@/utils/auth', () => ({
exchangeAuthorizationCode: vi.fn(),
@@ -12,6 +32,9 @@ vi.mock('@/utils/auth', () => ({
INACTIVITY_EXPIRY: 300,
}));
import { GET as callbackHandler } from '@/pages/api/auth/callback';
import { exchangeAuthorizationCode, seal } from '@/utils/auth';
const mockOAuthResponse = {
access_token: 'mock-access-token',
refresh_token: 'mock-refresh-token',
@@ -49,16 +72,16 @@ describe('callback endpoint', () => {
vi.mocked(exchangeAuthorizationCode).mockResolvedValue(mockOAuthResponse);
vi.mocked(seal).mockResolvedValue('encrypted-session-data');
vi.stubEnv('OAUTH_CLIENT_ID', 'test-client-id');
vi.stubEnv('OAUTH_CLIENT_SECRET', 'test-client-secret');
vi.stubEnv('OAUTH_REDIRECT_URI', 'https://example.com/callback');
vi.stubEnv('OAUTH_URL', 'https://auth.example.com');
vi.stubEnv('PROD', false);
env.OAUTH_CLIENT_ID = 'test-client-id';
env.OAUTH_CLIENT_SECRET = 'test-client-secret';
env.OAUTH_REDIRECT_URI = 'https://example.com/callback';
env.OAUTH_URL = 'https://auth.example.com';
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
consoleErrorSpy.mockRestore();
vi.unstubAllEnvs();
});
@@ -110,7 +133,7 @@ describe('callback endpoint', () => {
'OAUTH_CLIENT_ID',
'OAUTH_CLIENT_SECRET',
])('should redirect to error when %s is missing', async (envVar) => {
vi.stubEnv(envVar, undefined);
env[envVar as keyof typeof env] = undefined;
const mockContext = createMockContext({ code: '123', state: 'abc', storedState: 'abc' });
await callbackHandler(mockContext);
+1 -2
View File
@@ -1,3 +1,4 @@
import { OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, OAUTH_REDIRECT_URI, OAUTH_URL } from 'astro:env/server';
import type { APIRoute } from 'astro';
import { exchangeAuthorizationCode, INACTIVITY_EXPIRY, SESSION_COOKIE_NAME, seal } from '@/utils/auth';
@@ -15,8 +16,6 @@ export const prerender = false;
* 4. Redirects to success or error page
*/
export const GET: APIRoute = async ({ request, cookies, redirect }) => {
const { OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, OAUTH_REDIRECT_URI, OAUTH_URL } = import.meta.env;
const url = new URL(request.url);
const code = url.searchParams.get('code');
const state = url.searchParams.get('state');
+7 -3
View File
@@ -1,9 +1,13 @@
import {
OAUTH_CLIENT_ID,
OAUTH_CLIENT_SECRET,
OAUTH_REDIRECT_URI,
OAUTH_URL,
SESSION_COOKIE_PASSWORD,
} from 'astro:env/server';
import { sealData, unsealData } from 'iron-session';
import { createRemoteJWKSet } from 'jose';
const { OAUTH_URL, OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, OAUTH_REDIRECT_URI, SESSION_COOKIE_PASSWORD } = import.meta
.env;
// =============================================================================
// Types
// =============================================================================