logicsrc/apps/logicsrc-web/next.config.ts

100 lines
4.1 KiB
TypeScript

import type { NextConfig } from "next";
// The CommandBoard API (boards, tasks, plugins, /health) runs as its own
// service. In the old custom server.js it was mounted in-process; here we proxy
// those paths to it via rewrites. Our own /api routes (hire-us, oauth/coinpay,
// webhooks) are filesystem routes and match before these afterFiles rewrites.
const commandboardApiUrl = process.env.COMMANDBOARD_API_URL;
// The credentials app (apps/pwa) is also its own service, and it owns the CLI
// login flow: `logicsrc login` talks to /cli/*, and the browser half of that
// flow needs a session, which lives behind /auth/*.
//
// Proxying those paths is what lets all of it live on logicsrc.com. Pointing
// the apex at the pwa instead would take the marketing site down with it, since
// the pwa serves `/` too; a subdomain would work but needs a Railway custom
// domain and a DNS record. This needs neither, and it makes the CLI's default
// origin (https://logicsrc.com) correct as it already stands.
const credentialsAppUrl = process.env.CREDENTIALS_APP_URL;
const securityHeaders = [
// HSTS — site is HTTPS-only behind Railway. No `preload` (irreversible).
{ key: "Strict-Transport-Security", value: "max-age=31536000; includeSubDomains" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "X-Frame-Options", value: "SAMEORIGIN" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
// OpenProfile.md discovery for responses that are not HTML (feeds, JSON,
// the specs as Markdown): the same relation the root layout puts in <head>.
{
key: "Link",
value: `<${(process.env.PUBLIC_URL ?? "https://logicsrc.com").replace(/\/$/, "")}/.well-known/openprofile.md>; rel="openprofile"`,
},
];
/**
* The paths the credentials app owns.
*
* `/api/me` and `/api/credshare/*` are named individually, and the caller must
* place these BEFORE the CommandBoard `/api/:path*` catch-all — otherwise the
* catch-all swallows them and sends CLI auth to the wrong service.
*/
export function credentialsRewrites(base: string) {
return [
// the device-code and loopback login flows themselves
{ source: "/cli/:path*", destination: `${base}/cli/:path*` },
// identity, and the credential-sharing API the CLI uses once logged in
{ source: "/api/me", destination: `${base}/api/me` },
{ source: "/api/credshare/:path*", destination: `${base}/api/credshare/:path*` },
// /cli/authorize and /cli/device sit behind requireAuth, so an
// unauthenticated visitor gets redirected here to sign in. Without this the
// browser half of the flow dead-ends on a 404.
{ source: "/auth/:path*", destination: `${base}/auth/:path*` },
];
}
/** CommandBoard's paths. The `/api` entry is a catch-all, so it goes last. */
export function commandboardRewrites(base: string) {
return [
{ source: "/health", destination: `${base}/health` },
{ source: "/api/:path*", destination: `${base}/api/:path*` },
];
}
/** Built as a function so the ordering above is testable without booting Next. */
export function buildRewrites(
credentials = credentialsAppUrl,
commandboard = commandboardApiUrl,
) {
const afterFiles = [
...(credentials ? credentialsRewrites(credentials.replace(/\/$/, "")) : []),
...(commandboard ? commandboardRewrites(commandboard.replace(/\/$/, "")) : []),
];
return afterFiles.length ? { afterFiles } : [];
}
/**
* Paths that moved. `/agent-swarm` was the AgentSwarm "coming soon" band on
* the home page; the spec it promised is OpenFleet, which has its own landing
* page. Permanent, so the old URL in the sitemap and in links keeps working.
*/
export function buildRedirects() {
return [
{ source: "/agent-swarm", destination: "/openfleet", permanent: true },
{ source: "/openprofile/skills", destination: "/openskill", permanent: true }
];
}
const nextConfig: NextConfig = {
async headers() {
return [{ source: "/:path*", headers: securityHeaders }];
},
async rewrites() {
return buildRewrites();
},
async redirects() {
return buildRedirects();
},
};
export default nextConfig;