logicsrc/apps/logicsrc-web/next.config.ts
Anthony Ettinger 8e4ea2f997
docs: OpenStack.md 0.1, one Markdown file for what a project is built on (#191)
A new LogicSRC spec in the catalogs family. One heading, an identity block
(Kind, Web, Repo, Operator, License, Extends, Updated), one line, then eleven
layers: Languages, Runtimes, Interfaces (one ### per way in: web, api, cli,
tui, mcp, desktop, mobile, worker, extension, bot), Data, Services, Modules,
Tooling, Hosting, Auth, Conventions, Not. An item is one bullet: name,
version, an optional status word (trial, hold, leaving) and a role. Extends
inherits a parent file, sections replace, Conventions and Not accumulate.
Rule 8 is the reading rule for agents: use what is listed, prefer listed
over new, ask before adding a layer or a service, never add a Not, keep the
file true. Discovery at OpenStack.md in the repo, /.well-known/openstack.md,
rel=openstack, or a platform path. JSON is derived and never the source.

logicsrc.com serves its own at /.well-known/openstack.md: the route extracts
the worked example from docs/openstack.md, and a contract test holds the two
together and checks the file follows its own rules. rel=openstack in <head>
and in the Link header beside openprofile.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 02:21:12 -07:00

104 lines
4.3 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 and OpenStack.md discovery for responses that are not HTML
// (feeds, JSON, the specs as Markdown): the same relations the root layout
// puts in <head>.
{
key: "Link",
value: [
`<${(process.env.PUBLIC_URL ?? "https://logicsrc.com").replace(/\/$/, "")}/.well-known/openprofile.md>; rel="openprofile"`,
`<${(process.env.PUBLIC_URL ?? "https://logicsrc.com").replace(/\/$/, "")}/.well-known/openstack.md>; rel="openstack"`,
].join(", "),
},
];
/**
* 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;