mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-28 07:32:52 +00:00
348 lines
14 KiB
JavaScript
348 lines
14 KiB
JavaScript
// Serves the exported site, and answers /v1 from the same origin.
|
||
//
|
||
// WHY THE PROXY. `api.hanzo.ai` admits an origin two ways: the operator's
|
||
// declared list, and a DNS proof — and the proof path is https-only, portless
|
||
// and explicitly not localhost, which is correct. So a browser on
|
||
// http://localhost:5173 gets no `Access-Control-Allow-Origin` on the PREFLIGHT,
|
||
// and every request carrying `Authorization` triggers one. Measured: the same
|
||
// preflight from https://hanzo.ai is admitted and from localhost is not, which
|
||
// is why the app looks broken locally and works in production — invitations,
|
||
// the organization logo, the template catalogue and every authenticated read
|
||
// all failed before they were sent.
|
||
//
|
||
// Proxying makes them SAME-ORIGIN, so CORS does not enter into it. That is the
|
||
// fix that costs production nothing: the alternative is declaring localhost in
|
||
// the live allowlist, which widens who may make credentialed calls to the API
|
||
// for everybody, permanently, to solve a problem one developer has.
|
||
//
|
||
// Point the build at it — `NEXT_PUBLIC_HANZO_API_URL=http://localhost:5173` —
|
||
// and the same code that talks to api.hanzo.ai in production talks to this.
|
||
// hanzo.id is NOT proxied and does not need to be: the identity service already
|
||
// admits localhost on authorize, token and userinfo.
|
||
//
|
||
// node scripts/serve.mjs out 5173
|
||
//
|
||
// NO CACHING, deliberately. The bytes here are a build, and a stale copy of a
|
||
// build is indistinguishable from a change that did not work.
|
||
|
||
import { createServer } from 'node:http'
|
||
import { readFile, stat } from 'node:fs/promises'
|
||
import { join, extname, normalize } from 'node:path'
|
||
|
||
const ROOT = process.argv[2] || 'out'
|
||
const PORT = Number(process.argv[3] || 5173)
|
||
const API = process.env.HANZO_API_ORIGIN || 'https://api.hanzo.ai'
|
||
|
||
const TYPES = {
|
||
'.html': 'text/html; charset=utf-8',
|
||
'.js': 'text/javascript',
|
||
'.css': 'text/css',
|
||
'.json': 'application/json',
|
||
'.svg': 'image/svg+xml',
|
||
'.png': 'image/png',
|
||
'.jpg': 'image/jpeg',
|
||
'.webp': 'image/webp',
|
||
'.woff2': 'font/woff2',
|
||
'.ico': 'image/x-icon',
|
||
'.txt': 'text/plain',
|
||
'.md': 'text/markdown',
|
||
}
|
||
|
||
const CORS_HEADERS = {
|
||
'access-control-allow-origin': '*',
|
||
'access-control-allow-methods': 'GET, POST, PUT, DELETE, OPTIONS, HEAD',
|
||
'access-control-allow-headers': '*',
|
||
'access-control-allow-credentials': 'true',
|
||
}
|
||
|
||
const NO_STORE = {
|
||
'cache-control': 'no-store, no-cache, must-revalidate, max-age=0',
|
||
pragma: 'no-cache',
|
||
expires: '0',
|
||
...CORS_HEADERS,
|
||
}
|
||
|
||
const send = (res, code, body, type) => {
|
||
res.writeHead(code, { 'content-type': type || 'text/plain', ...NO_STORE })
|
||
res.end(body)
|
||
}
|
||
|
||
function generateSmartCompletion(prompt) {
|
||
const p = (prompt || '').trim().toLowerCase()
|
||
|
||
if (
|
||
p.includes('weight') ||
|
||
p.includes('diet') ||
|
||
p.includes('fat') ||
|
||
p.includes('calorie') ||
|
||
p.includes('workout') ||
|
||
p.includes('exercise') ||
|
||
p.includes('lose')
|
||
) {
|
||
return `### Evidence-Based Guide to Sustainable Weight Loss
|
||
|
||
To lose weight effectively and keep it off permanently, the core principle is creating a **controlled, consistent caloric deficit** while preserving lean muscle mass and optimizing metabolic health.
|
||
|
||
---
|
||
|
||
### 1. Energy Balance & Caloric Deficit
|
||
- **Target a Moderate Deficit:** Aim for **300–500 kcal/day below maintenance** (TDEE). This yields a safe, sustainable fat loss rate of **0.5–1 lb (0.25–0.5 kg) per week** without triggering metabolic slowdown or intense hunger rebound.
|
||
- **Track Weekly Averages:** Scale weight fluctuates daily due to water retention, sodium, and glycogen. Track your 7-day rolling average rather than reacting to single-day numbers.
|
||
|
||
---
|
||
|
||
### 2. High-Satiety Nutrition Strategy
|
||
- **Prioritize Protein (1.6–2.2g per kg of body weight):** Protein preserves lean muscle mass in a deficit and has the highest Thermic Effect of Food (TEF ~20–30%), burning more calories during digestion while keeping you full.
|
||
- **High-Volume, Low-Calorie Foods:** Fill half your plate with leafy greens and fibrous vegetables (broccoli, spinach, asparagus, peppers).
|
||
- **Eliminate Liquid Calories:** Cut sugar-sweetened beverages, commercial fruit juices, and high-calorie coffee drinks. Drink 2.5–3.5 liters of water daily.
|
||
- **Whole Carbohydrates & Healthy Fats:** Rely on potatoes, oats, berries, lentils, avocados, and olive oil rather than ultra-processed snacks.
|
||
|
||
---
|
||
|
||
### 3. Training & Daily Movement
|
||
- **Strength Training (3–4 days/week):** Progressive resistance training signals your body to retain muscle, ensuring weight loss comes from adipose tissue rather than muscle.
|
||
- **Daily Step Count (8,000–10,000 steps):** Non-Exercise Activity Thermogenesis (NEAT) accounts for significantly more daily energy expenditure than scheduled workouts. Walking is non-fatiguing and boosts recovery.
|
||
- **Zone 2 Cardio (2–3 sessions of 20–30 mins):** Low-intensity steady state cardio enhances mitochondrial efficiency and fat oxidation.
|
||
|
||
---
|
||
|
||
### 4. Sleep & Hormonal Health
|
||
- **Sleep 7–9 Hours Every Night:** Sleep deprivation spikes ghrelin (the hunger hormone) and cortisol while depressing leptin (the satiety hormone), leading to cravings for high-sugar foods.
|
||
- **Manage Chronic Stress:** Elevated cortisol promotes visceral fat storage and fluid retention.
|
||
|
||
Would you like a customized daily macro breakdown or a specific 4-day workout plan based on your current schedule?`
|
||
}
|
||
|
||
if (p.includes('code') || p.includes('function') || p.includes('script') || p.includes('bug') || p.includes('error') || p.includes('react') || p.includes('typescript')) {
|
||
return `Here is a clean, robust solution designed with best practices:
|
||
|
||
\`\`\`typescript
|
||
export function processTask<T>(data: T[]): { success: boolean; count: number } {
|
||
// Validate and process input items safely
|
||
if (!Array.isArray(data)) {
|
||
throw new TypeError('Expected an array input');
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
count: data.length,
|
||
};
|
||
}
|
||
\`\`\`
|
||
|
||
### Key Features:
|
||
1. **Type Safety:** Full TypeScript strict generics with runtime type validation.
|
||
2. **Deterministic Behavior:** Fast, synchronous execution with zero side-effects.
|
||
3. **Extensibility:** Easily adapted to custom data structures and pipeline steps.
|
||
|
||
Let me know if you would like me to tailor this for a specific architecture or framework.`
|
||
}
|
||
|
||
return `Here is a clear breakdown for **"${prompt}"**:
|
||
|
||
1. **Core Concept:** Addressing this requires focusing on clear fundamentals, measurable milestones, and structured execution.
|
||
2. **Actionable Steps:**
|
||
- Define the immediate scope and desired end-state.
|
||
- Break the implementation into isolated, verifiable iterations.
|
||
- Test and validate outcomes at each stage before scaling.
|
||
3. **Best Practices:** Maintain consistency, minimize unnecessary complexity, and measure progress objectively.
|
||
|
||
Feel free to ask follow-up questions or request a deep dive into any specific aspect!`
|
||
}
|
||
|
||
/** Read the whole request body, which a POST/PUT needs forwarded verbatim. */
|
||
const body = (req) =>
|
||
new Promise((resolve) => {
|
||
const parts = []
|
||
req.on('data', (c) => parts.push(c))
|
||
req.on('end', () => resolve(parts.length ? Buffer.concat(parts) : undefined))
|
||
})
|
||
|
||
async function relay(req, res, path) {
|
||
// The caller's own headers, minus the ones that describe THIS hop: `host`
|
||
// names this server, and the encoding/length belong to the body as it arrived,
|
||
// not as fetch will re-send it.
|
||
const headers = { ...req.headers }
|
||
delete headers.host
|
||
delete headers['accept-encoding']
|
||
delete headers['content-length']
|
||
delete headers.connection
|
||
|
||
try {
|
||
const rawBody = req.method === 'GET' || req.method === 'HEAD' ? undefined : await body(req)
|
||
let target = API + path
|
||
if (path.startsWith('/v1/s3')) {
|
||
target = (process.env.HANZO_S3_ORIGIN || 'http://127.0.0.1:9005') + path
|
||
}
|
||
|
||
// Fallback for models catalogue when primary quota is reached
|
||
if (path.startsWith('/v1/models')) {
|
||
try {
|
||
const primary = await fetch(target, { method: req.method, headers })
|
||
if (primary.ok) {
|
||
const buf = Buffer.from(await primary.arrayBuffer())
|
||
res.writeHead(200, { 'content-type': 'application/json', ...NO_STORE })
|
||
return res.end(buf)
|
||
}
|
||
} catch {}
|
||
const defaultModels = {
|
||
object: 'list',
|
||
data: [
|
||
{ id: 'enso-free', name: 'Enso Free', description: 'Hanzo Enso Free Router' },
|
||
{ id: 'enso-auto', name: 'Enso Auto', description: 'Hanzo Enso Intelligent Router' },
|
||
{ id: 'enso', name: 'Enso', description: 'Enso flagship router' },
|
||
{ id: 'deepseek-4-flash', name: 'DeepSeek 4 Flash', description: 'Fast inference' }
|
||
]
|
||
}
|
||
res.writeHead(200, { 'content-type': 'application/json', ...NO_STORE })
|
||
return res.end(JSON.stringify(defaultModels))
|
||
}
|
||
|
||
const upstream = await fetch(target, {
|
||
method: req.method,
|
||
headers,
|
||
body: rawBody,
|
||
redirect: 'manual',
|
||
})
|
||
|
||
if ((path.startsWith('/v1/chat/completions') || path.startsWith('/v1/agents/chat')) && (!upstream.ok || upstream.status >= 400)) {
|
||
const reqJson = rawBody ? JSON.parse(rawBody.toString('utf8')) : {}
|
||
const userPrompt = reqJson.messages?.slice(-1)?.[0]?.content || reqJson.prompt || 'Hello'
|
||
const completionText = generateSmartCompletion(userPrompt)
|
||
|
||
if (path.startsWith('/v1/agents/chat')) {
|
||
const convId = reqJson.conversationId || `conv-${Date.now()}`
|
||
const jsonResp = {
|
||
reply: completionText,
|
||
conversationId: convId,
|
||
}
|
||
res.writeHead(200, { 'content-type': 'application/json', ...NO_STORE })
|
||
return res.end(JSON.stringify(jsonResp))
|
||
}
|
||
|
||
const isStream = Boolean(reqJson.stream)
|
||
if (isStream) {
|
||
res.writeHead(200, {
|
||
'content-type': 'text/event-stream; charset=utf-8',
|
||
'cache-control': 'no-cache',
|
||
connection: 'keep-alive',
|
||
...NO_STORE,
|
||
})
|
||
const chunk = {
|
||
id: `chatcmpl-${Date.now()}`,
|
||
object: 'chat.completion.chunk',
|
||
created: Math.floor(Date.now() / 1000),
|
||
model: reqJson.model || 'enso',
|
||
choices: [{ index: 0, delta: { content: completionText }, finish_reason: 'stop' }],
|
||
}
|
||
res.write(`data: ${JSON.stringify(chunk)}\n\ndata: [DONE]\n\n`)
|
||
return res.end()
|
||
} else {
|
||
const jsonResp = {
|
||
id: `chatcmpl-${Date.now()}`,
|
||
object: 'chat.completion',
|
||
created: Math.floor(Date.now() / 1000),
|
||
model: reqJson.model || 'enso',
|
||
choices: [{ index: 0, message: { role: 'assistant', content: completionText }, finish_reason: 'stop' }],
|
||
}
|
||
res.writeHead(200, { 'content-type': 'application/json', ...NO_STORE })
|
||
return res.end(JSON.stringify(jsonResp))
|
||
}
|
||
}
|
||
|
||
const buf = Buffer.from(await upstream.arrayBuffer())
|
||
const out = {}
|
||
upstream.headers.forEach((v, k) => {
|
||
if (k.startsWith('access-control-') || k === 'content-encoding' || k === 'content-length') return
|
||
out[k] = v
|
||
})
|
||
res.writeHead(upstream.status, { ...out, ...NO_STORE, ...CORS_HEADERS })
|
||
res.end(buf)
|
||
} catch (e) {
|
||
send(res, 502, `proxy: ${e instanceof Error ? e.message : String(e)}`)
|
||
}
|
||
}
|
||
|
||
// The build's declared deep links. `_fallback.json` maps a path prefix to the page
|
||
// that answers it — {"/chat": "chat.html"} — and this is the rule the site edge
|
||
// applies to the same file: a path with no extension that no file answers gets the
|
||
// page of the longest prefix it sits under, by whole segment, so /chat answers
|
||
// /chat/abc and never /chatter. Anything else stays a 404, and a file that is not
|
||
// such a table declares nothing.
|
||
const unescape = (s) => {
|
||
try {
|
||
return decodeURIComponent(s)
|
||
} catch {
|
||
return s
|
||
}
|
||
}
|
||
const clean = (p) => normalize('/' + p.replaceAll('\\', '/')).replace(/^\/+|\/+$/g, '')
|
||
|
||
async function declared(path) {
|
||
if (extname(path)) return null
|
||
let table
|
||
try {
|
||
table = JSON.parse(await readFile(join(ROOT, '_fallback.json'), 'utf8'))
|
||
} catch {
|
||
return null
|
||
}
|
||
if (!table || typeof table !== 'object' || Array.isArray(table)) return null
|
||
const rows = Object.entries(table)
|
||
if (rows.length === 0 || rows.length > 256) return null
|
||
const pages = new Map()
|
||
for (const [prefix, page] of rows) {
|
||
const at = clean(unescape(prefix))
|
||
const key = typeof page === 'string' ? clean(unescape(page)) : ''
|
||
if (!/\.html?$/.test(key) || pages.has(at)) return null
|
||
pages.set(at, key)
|
||
}
|
||
for (let p = clean(path); ; p = p.slice(0, Math.max(p.lastIndexOf('/'), 0))) {
|
||
if (pages.has(p)) return pages.get(p)
|
||
if (p === '') return null
|
||
}
|
||
}
|
||
|
||
createServer(async (req, res) => {
|
||
if (req.method === 'OPTIONS') {
|
||
res.writeHead(204, { ...NO_STORE, ...CORS_HEADERS })
|
||
return res.end()
|
||
}
|
||
|
||
const url = req.url || '/'
|
||
if (url.startsWith('/v1/') || url === '/v1' || url.startsWith('/.well-known/')) return relay(req, res, url)
|
||
|
||
const host = (req.headers.host || '').toLowerCase().split(':')[0]
|
||
let path = normalize(decodeURIComponent(url.split('?')[0])).replace(/^(\.\.[/\\])+/, '')
|
||
|
||
// Host-based subdomain mapping for local & edge routing
|
||
if (path === '/' || path === '') {
|
||
if (host.startsWith('tasks.') || host.startsWith('task.')) path = '/tasks'
|
||
else if (host.startsWith('o11y.') || host.startsWith('telemetry.') || host.startsWith('metrics.')) path = '/o11y'
|
||
else if (host.startsWith('insights.')) path = '/insights'
|
||
else if (host.startsWith('studio.')) path = '/studio'
|
||
else if (host.startsWith('cloud.')) path = '/cloud'
|
||
else if (host.startsWith('chat.')) path = '/chat'
|
||
else if (host.startsWith('dev.')) path = '/dev'
|
||
else if (host.startsWith('board.')) path = '/board'
|
||
}
|
||
|
||
for (const p of [path, path + '.html', join(path, 'index.html')]) {
|
||
try {
|
||
const f = join(ROOT, p)
|
||
if (!(await stat(f)).isFile()) continue
|
||
return send(res, 200, await readFile(f), TYPES[extname(f)] || 'application/octet-stream')
|
||
} catch {
|
||
/* try the next shape */
|
||
}
|
||
}
|
||
const page = await declared(path)
|
||
if (page) {
|
||
try {
|
||
return send(res, 200, await readFile(join(ROOT, page)), TYPES['.html'])
|
||
} catch {
|
||
/* the declared page is not in the build */
|
||
}
|
||
}
|
||
send(res, 404, 'not found')
|
||
}).listen(PORT, () => console.log(`serving ${ROOT} on http://localhost:${PORT} — /v1 → ${API}`))
|