// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 // The production server: Hono's Node server in front of the Start build (`vite build`), the build's // static files first, then the Start handler for everything else -- pages, `/api/*` and `/auth/*`. // `src/server.ts` sets the security headers on what the handler answers; the static files get the ones // that need no nonce here. // // Usage: node server.mjs [--port ] (PORT, HOSTNAME) import { resolve } from "node:path" import { pathToFileURL } from "node:url" import { serve } from "@hono/node-server" import { serveStatic } from "@hono/node-server/serve-static" import { Hono } from "hono" // A direct Node start uses the same production security policy as the container. process.env.NODE_ENV ??= "production" const here = new URL(".", import.meta.url).pathname const output = resolve(here, "dist") const { default: handler } = await import(pathToFileURL(resolve(output, "server/server.js")).href) const app = new Hono() app.use("*", async (c, next) => { await next() c.header("X-Content-Type-Options", "nosniff") c.header("X-Frame-Options", "DENY") c.header("Referrer-Policy", "strict-origin-when-cross-origin") c.header("Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=()") }) app.use("*", serveStatic({ root: resolve(output, "client") })) app.all("*", (c) => handler.fetch(c.req.raw)) const flag = process.argv.indexOf("--port") const port = Number(flag >= 0 ? process.argv[flag + 1] : (process.env.PORT ?? 3510)) const server = serve({ fetch: app.fetch, port, hostname: process.env.HOSTNAME ?? "0.0.0.0" }) // SIGTERM, which the platform sends before it replaces the container: this process owns the exit, in this order. // // 1. Stop taking requests and let the ones in flight finish, because their usage joins the queue as they end. // 2. Drain the runtime (`drain` of src/server/app.ts): the usage queue, the exporters, the pools. An unflushed // queue is unbilled usage, so nothing exits before this ends. No runtime means no request built one. // 3. Exit: 0 when everything finished, 1 when the drain did not. // // Each step is bounded, the two together inside compose.yaml's `stop_grace_period` (20 s), after which the container // is killed and nothing is flushed. Step 1 does not wait out an open live stream: it has no end, and the drain closes it. const requestsMs = Number(process.env.SHUTDOWN_REQUESTS_MS || 10_000) const flushMs = Number(process.env.SHUTDOWN_FLUSH_MS || 8_000) const within = (ms, work) => { let timer const elapsed = new Promise((resolve) => { timer = setTimeout(() => resolve(false), ms) }) return Promise.race([work.then(() => true), elapsed]).finally(() => clearTimeout(timer)) } let stopping = false async function stop() { if (stopping) return stopping = true await within(requestsMs, new Promise((resolve) => server.close(resolve))) const drained = await within( flushMs, Promise.resolve().then(() => globalThis[Symbol.for("conusai.app.drain")]?.()), ).catch((error) => { console.error(`web: the drain failed: ${error instanceof Error ? error.message : "failed"}`) return false }) if (!drained) console.error(`web: the runtime was not drained within ${flushMs} ms`) process.exit(drained ? 0 : 1) } for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, () => void stop())