import jwt from 'jsonwebtoken'; function envFlag(name, fallback) { const value = process.env[name]; if (value === undefined) return fallback; return ['1', 'true', 'yes', 'on'].includes(value.toLowerCase()); } function trimTrailingSlash(value) { return value.replace(/\/+$/, ''); } export function labAuthConfig() { const gnommowebUrl = trimTrailingSlash(process.env.GNOMMOWEB_URL ?? 'https://glitch.university'); return { enabled: envFlag('LAB_AUTH_ENABLED', process.env.NODE_ENV === 'production'), gnommowebUrl, jwtSecret: process.env.JWT_SECRET ?? '', keycardMeritSlug: process.env.LAB_KEYCARD_MERIT_SLUG ?? 'lab-keycard', keycardRequired: envFlag('LAB_KEYCARD_REQUIRED', true), signInUrl: process.env.LAB_SIGN_IN_URL ?? `${gnommowebUrl}/auth/google`, userProfileUrl: process.env.LAB_USER_PROFILE_URL ?? `${gnommowebUrl}/api/user/profile` }; } function wantsHtml(request) { const accept = request.get('accept') ?? ''; return accept.includes('text/html') || accept.includes('*/*'); } function currentUrl(request) { const proto = request.get('x-forwarded-proto') ?? request.protocol; const host = request.get('x-forwarded-host') ?? request.get('host') ?? 'localhost'; return `${proto}://${host}${request.originalUrl}`; } function loginUrl(request, config) { const returnTo = currentUrl(request); if (config.signInUrl.includes('{returnTo}')) { return config.signInUrl.replace('{returnTo}', encodeURIComponent(returnTo)); } const url = new URL(config.signInUrl); url.searchParams.set('returnTo', returnTo); return url.toString(); } function lockedPage(access, config) { const unlockUrl = access.unlockUrl ?? `${config.gnommowebUrl}/tech-tree`; const reason = access.reason ?? 'Your Glitch University account does not have the CA Lab Keycard yet.'; return ` CA Lab Locked
CA Lab Computer

Lab keycard required

${escapeHtml(reason)}

Earn the Cellular Automata lab merit on glitch.university, then return here.

Go to Glitch University
`; } function escapeHtml(value) { return value .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } function isMeritSlug(value, requiredSlug) { return typeof value === 'string' && value.trim() === requiredSlug; } function collectionHasMeritSlug(value, requiredSlug) { if (!Array.isArray(value)) return false; return value.some((entry) => { if (isMeritSlug(entry, requiredSlug)) return true; if (!entry || typeof entry !== 'object') return false; const record = entry; return isMeritSlug(record.slug, requiredSlug); }); } function payloadHasMeritSlug(payload, requiredSlug) { if (!payload || typeof payload !== 'object') return false; const record = payload; const user = record.user && typeof record.user === 'object' ? record.user : undefined; return (isMeritSlug(record.merit_slug, requiredSlug) || collectionHasMeritSlug(record.merit_slugs, requiredSlug) || collectionHasMeritSlug(record.merits, requiredSlug) || (user ? collectionHasMeritSlug(user.merits, requiredSlug) : false)); } function jwtClaimsHaveKeycard(user, config) { if (!user) return false; return isMeritSlug(user.merit_slug, config.keycardMeritSlug) || collectionHasMeritSlug(user.merit_slugs, config.keycardMeritSlug); } async function checkUserProfileForKeycard(token, config) { const response = await fetch(config.userProfileUrl, { headers: { accept: 'application/json', cookie: `auth_token=${encodeURIComponent(token)}` } }); if (!response.ok) { return { allowed: false, reason: response.status === 401 ? 'Sign in to glitch.university to access the CA lab.' : `The CA Lab Keycard could not be verified. The user profile endpoint returned HTTP ${response.status}.` }; } const profile = await response.json(); if (payloadHasMeritSlug(profile, config.keycardMeritSlug)) { return { allowed: true, reason: 'CA Lab Keycard found in gnommoweb user profile.' }; } return { allowed: false, reason: 'Your Glitch University account does not have the CA Lab Keycard yet.' }; } export function requireLabAccess(config = labAuthConfig()) { return async (request, response, next) => { if (!config.enabled) { next(); return; } if (!config.jwtSecret) { response.status(500).json({ error: 'Lab authentication is enabled but JWT_SECRET is not configured' }); return; } const token = request.cookies?.auth_token; if (!token) { if (wantsHtml(request)) { response.redirect(loginUrl(request, config)); return; } response.status(401).json({ error: 'Authentication required', signInUrl: loginUrl(request, config) }); return; } try { request.labUser = jwt.verify(token, config.jwtSecret); } catch { if (wantsHtml(request)) { response.redirect(loginUrl(request, config)); return; } response.status(401).json({ error: 'Invalid or expired token', signInUrl: loginUrl(request, config) }); return; } if (!config.keycardRequired) { next(); return; } if (jwtClaimsHaveKeycard(request.labUser, config)) { request.labAccess = { allowed: true, reason: 'CA Lab Keycard found in JWT claims.' }; next(); return; } let access; try { access = await checkUserProfileForKeycard(token, config); } catch { access = { allowed: false, reason: 'The CA Lab Keycard service is unavailable.' }; } request.labAccess = access; if (access.allowed === true) { next(); return; } if (wantsHtml(request)) { response.status(403).type('html').send(lockedPage(access, config)); return; } response.status(403).json({ error: 'CA Lab Keycard required', reason: access.reason, unlockUrl: access.unlockUrl ?? `${config.gnommowebUrl}/tech-tree` }); }; } //# sourceMappingURL=labAuth.js.map