Spaces:
Sleeping
Sleeping
| import http from 'node:http' | |
| import https from 'node:https' | |
| import { createHmac, timingSafeEqual } from 'node:crypto' | |
| const publicPort = Number(process.env.PORT || '7860') | |
| const upstreamUrl = new URL( | |
| process.env.WORKSPACE_UPSTREAM_URL || | |
| `http://127.0.0.1:${process.env.WORKSPACE_INTERNAL_PORT || '7861'}`, | |
| ) | |
| const accessPassword = process.env.SPACE_ACCESS_PASSWORD || '' | |
| const sessionSecret = process.env.SPACE_SESSION_SECRET || accessPassword | |
| const cookieName = 'hermes-space-access' | |
| const loginPath = '/__space_auth/login' | |
| const logoutPath = '/__space_auth/logout' | |
| const healthPath = '/__space_auth/health' | |
| const authorizedToken = signToken('authorized') | |
| if (!accessPassword) { | |
| console.error( | |
| '[space-access] Missing SPACE_ACCESS_PASSWORD. Configure it as a Hugging Face Space secret.', | |
| ) | |
| process.exit(1) | |
| } | |
| function signToken(scope) { | |
| return createHmac('sha256', sessionSecret) | |
| .update(`${scope}:${accessPassword}`) | |
| .digest('base64url') | |
| } | |
| function safeEqual(left, right) { | |
| const leftBuffer = Buffer.from(left) | |
| const rightBuffer = Buffer.from(right) | |
| if (leftBuffer.length !== rightBuffer.length) { | |
| return false | |
| } | |
| return timingSafeEqual(leftBuffer, rightBuffer) | |
| } | |
| function parseCookies(cookieHeader = '') { | |
| const cookies = {} | |
| for (const chunk of cookieHeader.split(';')) { | |
| const trimmed = chunk.trim() | |
| if (!trimmed) continue | |
| const separatorIndex = trimmed.indexOf('=') | |
| if (separatorIndex === -1) continue | |
| const key = trimmed.slice(0, separatorIndex) | |
| const value = trimmed.slice(separatorIndex + 1) | |
| cookies[key] = value | |
| } | |
| return cookies | |
| } | |
| function isAuthorized(request) { | |
| const cookies = parseCookies(request.headers.cookie) | |
| const token = cookies[cookieName] | |
| if (!token) return false | |
| return safeEqual(token, authorizedToken) | |
| } | |
| function wantsHtml(request) { | |
| const accept = request.headers.accept || '' | |
| return accept.includes('text/html') | |
| } | |
| function readJsonBody(request) { | |
| return new Promise((resolve, reject) => { | |
| const chunks = [] | |
| let size = 0 | |
| request.on('data', (chunk) => { | |
| size += chunk.length | |
| if (size > 16 * 1024) { | |
| reject(new Error('Body too large')) | |
| request.destroy() | |
| return | |
| } | |
| chunks.push(chunk) | |
| }) | |
| request.on('end', () => { | |
| try { | |
| const raw = Buffer.concat(chunks).toString('utf8') | |
| resolve(raw ? JSON.parse(raw) : {}) | |
| } catch (error) { | |
| reject(error) | |
| } | |
| }) | |
| request.on('error', reject) | |
| }) | |
| } | |
| function sendJson(response, statusCode, payload, extraHeaders = {}) { | |
| response.writeHead(statusCode, { | |
| 'Content-Type': 'application/json; charset=utf-8', | |
| 'Cache-Control': 'no-store', | |
| ...extraHeaders, | |
| }) | |
| response.end(JSON.stringify(payload)) | |
| } | |
| function sendPromptPage(response) { | |
| const html = `<!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>Protected Workspace</title> | |
| <style> | |
| :root { | |
| color-scheme: light dark; | |
| font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; | |
| } | |
| body { | |
| margin: 0; | |
| min-height: 100vh; | |
| display: grid; | |
| place-items: center; | |
| background: | |
| radial-gradient(circle at top, rgba(97, 126, 255, 0.18), transparent 40%), | |
| linear-gradient(180deg, #f5f7fb 0%, #e9eef7 100%); | |
| color: #182131; | |
| } | |
| .card { | |
| width: min(460px, calc(100vw - 32px)); | |
| border: 1px solid rgba(24, 33, 49, 0.12); | |
| border-radius: 20px; | |
| padding: 28px; | |
| background: rgba(255, 255, 255, 0.88); | |
| box-shadow: 0 20px 60px rgba(24, 33, 49, 0.14); | |
| } | |
| h1 { | |
| margin: 0 0 10px; | |
| font-size: 1.2rem; | |
| } | |
| p { | |
| margin: 0; | |
| line-height: 1.6; | |
| color: #4c5b73; | |
| } | |
| button { | |
| margin-top: 18px; | |
| border: 0; | |
| border-radius: 999px; | |
| padding: 10px 16px; | |
| background: #1f4bff; | |
| color: white; | |
| font: inherit; | |
| cursor: pointer; | |
| } | |
| button:hover { | |
| background: #173ae0; | |
| } | |
| #status { | |
| margin-top: 14px; | |
| font-size: 0.95rem; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <main class="card"> | |
| <h1>Workspace Locked</h1> | |
| <p>This Space is protected. Enter the access password in the browser prompt to continue.</p> | |
| <button id="retry" type="button">Enter Password</button> | |
| <p id="status">Nothing inside the workspace will load until the correct password is entered.</p> | |
| </main> | |
| <script> | |
| const status = document.getElementById('status') | |
| async function requestPassword(message) { | |
| const password = window.prompt(message || 'Enter the access password to continue.') | |
| if (password === null) { | |
| status.textContent = 'Access is still locked. Press "Enter Password" when you are ready.' | |
| return | |
| } | |
| const response = await fetch('${loginPath}', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ password }), | |
| }) | |
| if (response.ok) { | |
| status.textContent = 'Password accepted. Loading workspace...' | |
| window.location.reload() | |
| return | |
| } | |
| status.textContent = 'Incorrect password. Try again.' | |
| window.setTimeout(() => { | |
| void requestPassword('Incorrect password. Enter the access password to continue.') | |
| }, 50) | |
| } | |
| document.getElementById('retry').addEventListener('click', () => { | |
| void requestPassword('Enter the access password to continue.') | |
| }) | |
| window.addEventListener('load', () => { | |
| void requestPassword('Enter the access password to continue.') | |
| }) | |
| </script> | |
| </body> | |
| </html>` | |
| response.writeHead(200, { | |
| 'Content-Type': 'text/html; charset=utf-8', | |
| 'Cache-Control': 'no-store', | |
| }) | |
| response.end(html) | |
| } | |
| function proxyRequest(clientRequest, clientResponse) { | |
| const proxyModule = upstreamUrl.protocol === 'https:' ? https : http | |
| const headers = { ...clientRequest.headers } | |
| headers.host = upstreamUrl.host | |
| headers['x-forwarded-host'] = clientRequest.headers.host || '' | |
| headers['x-forwarded-proto'] = 'https' | |
| headers['x-forwarded-port'] = String(publicPort) | |
| const upstreamRequest = proxyModule.request( | |
| { | |
| protocol: upstreamUrl.protocol, | |
| hostname: upstreamUrl.hostname, | |
| port: upstreamUrl.port, | |
| method: clientRequest.method, | |
| path: clientRequest.url, | |
| headers, | |
| }, | |
| (upstreamResponse) => { | |
| const responseHeaders = { ...upstreamResponse.headers } | |
| clientResponse.writeHead(upstreamResponse.statusCode || 502, responseHeaders) | |
| upstreamResponse.pipe(clientResponse) | |
| }, | |
| ) | |
| upstreamRequest.on('error', (error) => { | |
| console.error('[space-access] Upstream request failed:', error) | |
| sendJson(clientResponse, 502, { | |
| ok: false, | |
| error: 'Workspace upstream is unavailable.', | |
| }) | |
| }) | |
| clientRequest.pipe(upstreamRequest) | |
| } | |
| const server = http.createServer(async (request, response) => { | |
| const url = new URL(request.url || '/', `http://${request.headers.host || '127.0.0.1'}`) | |
| if (url.pathname === healthPath) { | |
| sendJson(response, 200, { | |
| ok: true, | |
| upstream: upstreamUrl.toString(), | |
| }) | |
| return | |
| } | |
| if (url.pathname === logoutPath && request.method === 'POST') { | |
| sendJson( | |
| response, | |
| 200, | |
| { ok: true }, | |
| { | |
| 'Set-Cookie': `${cookieName}=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0`, | |
| }, | |
| ) | |
| return | |
| } | |
| if (url.pathname === loginPath && request.method === 'POST') { | |
| try { | |
| const body = await readJsonBody(request) | |
| const submittedPassword = | |
| body && typeof body.password === 'string' ? body.password : '' | |
| if (safeEqual(submittedPassword, accessPassword)) { | |
| sendJson( | |
| response, | |
| 200, | |
| { ok: true }, | |
| { | |
| 'Set-Cookie': `${cookieName}=${authorizedToken}; HttpOnly; SameSite=Lax; Path=/; Max-Age=${30 * 24 * 60 * 60}`, | |
| }, | |
| ) | |
| return | |
| } | |
| sendJson(response, 401, { ok: false, error: 'Invalid password' }) | |
| return | |
| } catch (error) { | |
| sendJson(response, 400, { | |
| ok: false, | |
| error: error instanceof Error ? error.message : 'Invalid request', | |
| }) | |
| return | |
| } | |
| } | |
| if (!isAuthorized(request)) { | |
| if (wantsHtml(request)) { | |
| sendPromptPage(response) | |
| return | |
| } | |
| sendJson(response, 401, { | |
| ok: false, | |
| error: 'Authentication required', | |
| }) | |
| return | |
| } | |
| proxyRequest(request, response) | |
| }) | |
| server.listen(publicPort, '0.0.0.0', () => { | |
| console.log( | |
| `[space-access] Password gate listening on http://0.0.0.0:${publicPort} -> ${upstreamUrl.toString()}`, | |
| ) | |
| }) | |