import { fileURLToPath, pathToFileURL } from 'node:url'; const DEFAULT_TIMEOUT_MS = 10_000; function envString(env, key, fallback = '') { return typeof env[key] === 'string' && env[key].trim() ? env[key].trim() : fallback; } function envBoolean(env, key, fallback = false) { const value = envString(env, key).toLowerCase(); if (!value) return fallback; return ['1', 'true', 'yes', 'on'].includes(value); } function envNumber(env, key, fallback) { const value = Number(envString(env, key)); return Number.isFinite(value) && value > 0 ? Math.trunc(value) : fallback; } function normalizeBaseUrl(value) { return value.replace(/\/+$/, ''); } function redactToken(value) { if (!value) return ''; if (value.length <= 16) return ''; return `${value.slice(0, 6)}...${value.slice(-6)}`; } function buildConfig(env = process.env) { const apiBaseUrl = envString(env, 'AUTH_SMOKE_API_BASE_URL', envString(env, 'API_BASE', '')); const tenantId = envString(env, 'AUTH_SMOKE_TENANT_ID', envString(env, 'TENANT_ID', '')); const studentToken = envString(env, 'AUTH_SMOKE_STUDENT_ACCESS_TOKEN', envString(env, 'SUPABASE_ACCESS_TOKEN', '')); const tenantAdminToken = envString(env, 'AUTH_SMOKE_TENANT_ADMIN_ACCESS_TOKEN'); const platformAdminToken = envString(env, 'AUTH_SMOKE_PLATFORM_ADMIN_ACCESS_TOKEN'); const requireAdminTokens = envBoolean(env, 'AUTH_SMOKE_REQUIRE_ADMIN_TOKENS', false); const missing = []; if (!apiBaseUrl) missing.push('AUTH_SMOKE_API_BASE_URL'); if (!tenantId) missing.push('AUTH_SMOKE_TENANT_ID'); if (!studentToken) missing.push('AUTH_SMOKE_STUDENT_ACCESS_TOKEN'); if (requireAdminTokens && !tenantAdminToken) missing.push('AUTH_SMOKE_TENANT_ADMIN_ACCESS_TOKEN'); if (requireAdminTokens && !platformAdminToken) missing.push('AUTH_SMOKE_PLATFORM_ADMIN_ACCESS_TOKEN'); if (missing.length > 0) { throw new Error(`Missing required remote auth smoke env: ${missing.join(', ')}`); } return { apiBaseUrl: normalizeBaseUrl(apiBaseUrl), tenantId, wrongTenantId: envString(env, 'AUTH_SMOKE_WRONG_TENANT_ID'), timeoutMs: envNumber(env, 'AUTH_SMOKE_TIMEOUT_MS', DEFAULT_TIMEOUT_MS), requireAdminTokens, student: { token: studentToken, expectedUserId: envString(env, 'AUTH_SMOKE_EXPECTED_STUDENT_USER_ID'), }, tenantAdmin: tenantAdminToken ? { token: tenantAdminToken, expectedUserId: envString(env, 'AUTH_SMOKE_EXPECTED_TENANT_ADMIN_USER_ID'), } : null, platformAdmin: platformAdminToken ? { token: platformAdminToken, expectedUserId: envString(env, 'AUTH_SMOKE_EXPECTED_PLATFORM_ADMIN_USER_ID'), tenantId: envString(env, 'AUTH_SMOKE_PLATFORM_TENANT_ID', tenantId), } : null, }; } function authHeaders(token, tenantId) { return { authorization: `Bearer ${token}`, ...(tenantId ? { 'x-tenant-id': tenantId } : {}), }; } async function requestJson(config, path, { token, tenantId = config.tenantId, method = 'GET' } = {}) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), config.timeoutMs); const url = new URL(path, config.apiBaseUrl); try { const response = await fetch(url, { method, headers: { accept: 'application/json', ...(token ? authHeaders(token, tenantId) : {}), }, signal: controller.signal, }); const text = await response.text(); let payload = {}; if (text.trim()) { try { payload = JSON.parse(text); } catch { payload = { raw: text.slice(0, 500) }; } } return { status: response.status, ok: response.ok, payload }; } finally { clearTimeout(timeout); } } function resultCollector({ quiet = false } = {}) { const results = []; function pass(name, detail = {}) { results.push({ status: 'pass', name, detail }); if (!quiet) console.log(`PASS ${name}`); } function fail(name, message, detail = {}) { results.push({ status: 'fail', name, message, detail }); if (!quiet) { console.error(`FAIL ${name}: ${message}`); if (Object.keys(detail).length > 0) console.error(JSON.stringify(detail, null, 2)); } } return { results, pass, fail }; } function expectStatus(collector, name, response, expectedStatuses) { const expected = Array.isArray(expectedStatuses) ? expectedStatuses : [expectedStatuses]; if (expected.includes(response.status)) { collector.pass(name, { status: response.status }); return true; } collector.fail(name, `Expected HTTP ${expected.join('/')} but got ${response.status}`, { payload: response.payload }); return false; } function expectSupabaseJwtSession(collector, name, response, expectedUserId = '') { if (!expectStatus(collector, `${name}.status`, response, 200)) return; const source = response.payload?.session?.source; if (source === 'supabase_jwt') collector.pass(`${name}.session_source`); else collector.fail(`${name}.session_source`, 'Expected session.source=supabase_jwt', { source }); if (expectedUserId) { const actualUserId = response.payload?.user?.id || response.payload?.item?.userId; if (actualUserId === expectedUserId) collector.pass(`${name}.expected_user`); else collector.fail(`${name}.expected_user`, 'Authenticated user does not match expected id', { actualUserId, expectedUserId }); } } async function runRemoteAuthSmoke(inputConfig, options = {}) { const config = inputConfig?.apiBaseUrl ? inputConfig : buildConfig(options.env || process.env); const collector = resultCollector({ quiet: options.quiet }); if (!options.quiet) { console.log(`Remote auth smoke target: ${config.apiBaseUrl}`); console.log(`Student token: ${redactToken(config.student.token)}`); if (config.tenantAdmin) console.log(`Tenant admin token: ${redactToken(config.tenantAdmin.token)}`); if (config.platformAdmin) console.log(`Platform admin token: ${redactToken(config.platformAdmin.token)}`); } const studentMe = await requestJson(config, '/api/auth/me', { token: config.student.token, tenantId: config.tenantId, }); expectSupabaseJwtSession(collector, 'student.auth_me', studentMe, config.student.expectedUserId); const studentProfile = await requestJson(config, '/api/profile/me', { token: config.student.token, tenantId: config.tenantId, }); expectStatus(collector, 'student.profile_me', studentProfile, 200); if (config.student.expectedUserId && studentProfile.payload?.item?.userId !== config.student.expectedUserId) { collector.fail('student.profile_expected_user', 'Profile user does not match expected student id', { actualUserId: studentProfile.payload?.item?.userId, expectedUserId: config.student.expectedUserId, }); } else { collector.pass('student.profile_expected_user'); } const studentTenantAdmin = await requestJson(config, '/api/tenant-admin/overview', { token: config.student.token, tenantId: config.tenantId, }); expectStatus(collector, 'student.tenant_admin_denied', studentTenantAdmin, 403); const studentPlatformAdmin = await requestJson(config, '/api/platform-admin/overview', { token: config.student.token, tenantId: '', }); expectStatus(collector, 'student.platform_admin_denied', studentPlatformAdmin, 403); const badTokenProfile = await requestJson(config, '/api/profile/me', { token: 'invalid.jwt.token', tenantId: config.tenantId, }); expectStatus(collector, 'invalid_token.profile_denied', badTokenProfile, 401); if (config.wrongTenantId) { const wrongTenantProfile = await requestJson(config, '/api/profile/me', { token: config.student.token, tenantId: config.wrongTenantId, }); expectStatus(collector, 'student.wrong_tenant_denied', wrongTenantProfile, [401, 403, 404]); } if (config.tenantAdmin) { const tenantAdminMe = await requestJson(config, '/api/auth/me', { token: config.tenantAdmin.token, tenantId: config.tenantId, }); expectSupabaseJwtSession(collector, 'tenant_admin.auth_me', tenantAdminMe, config.tenantAdmin.expectedUserId); const tenantOverview = await requestJson(config, '/api/tenant-admin/overview', { token: config.tenantAdmin.token, tenantId: config.tenantId, }); expectStatus(collector, 'tenant_admin.overview', tenantOverview, 200); const tenantPlatformAdmin = await requestJson(config, '/api/platform-admin/overview', { token: config.tenantAdmin.token, tenantId: '', }); expectStatus(collector, 'tenant_admin.platform_admin_denied', tenantPlatformAdmin, 403); } else { collector.pass('tenant_admin.optional_skipped'); } if (config.platformAdmin) { const platformOverview = await requestJson(config, '/api/platform-admin/overview', { token: config.platformAdmin.token, tenantId: '', }); expectStatus(collector, 'platform_admin.overview', platformOverview, 200); if (config.platformAdmin.tenantId) { const platformMe = await requestJson(config, '/api/auth/me', { token: config.platformAdmin.token, tenantId: config.platformAdmin.tenantId, }); expectSupabaseJwtSession(collector, 'platform_admin.auth_me', platformMe, config.platformAdmin.expectedUserId); } } else { collector.pass('platform_admin.optional_skipped'); } const failed = collector.results.filter(item => item.status === 'fail'); if (!options.quiet) { console.log(`\nRemote Auth/JWKS smoke checks: ${collector.results.length - failed.length} passed, ${failed.length} failed.`); } if (failed.length > 0) { const error = new Error(`Remote Auth/JWKS smoke failed with ${failed.length} failing checks`); error.results = collector.results; throw error; } return collector.results; } async function main() { try { await runRemoteAuthSmoke(); } catch (error) { console.error(error.message); if (/Missing required remote auth smoke env/.test(error.message)) { console.error(` Required example: AUTH_SMOKE_API_BASE_URL=https://api.example.com AUTH_SMOKE_TENANT_ID= AUTH_SMOKE_STUDENT_ACCESS_TOKEN= Recommended full pre-production example: AUTH_SMOKE_TENANT_ADMIN_ACCESS_TOKEN= AUTH_SMOKE_PLATFORM_ADMIN_ACCESS_TOKEN= AUTH_SMOKE_WRONG_TENANT_ID= AUTH_SMOKE_REQUIRE_ADMIN_TOKENS=true `); } process.exitCode = 1; } } const currentFile = fileURLToPath(import.meta.url); if (process.argv[1] && fileURLToPath(pathToFileURL(process.argv[1])) === currentFile) { await main(); } export { buildConfig, runRemoteAuthSmoke };