Files
CodeWhale BotandClaude Opus 5.5 6046aeea14 fix(web): serve security.txt, drop x-powered-by, send plain http to https
codewhale.net had no /.well-known/security.txt, every page response carried
x-powered-by: Next.js and x-opennext: 1, and a plain-http request to
codewhale.net was answered over http. www.codewhale.net redirected to
codewhale.net but kept the http scheme.

- web/public/.well-known/security.txt (RFC 9116): Contact is the GitHub
  private vulnerability report form named in SECURITY.md, Policy is
  SECURITY.md, Expires 2027-10-19, Preferred-Languages en, zh, Canonical
  https://codewhale.net/.well-known/security.txt.
- next.config.ts: poweredByHeader: false. OpenNext reads the same setting,
  so it also removes x-opennext.
- middleware.ts: the canonical-host redirect now covers both site hosts.
  A request to codewhale.net or www.codewhale.net whose URL scheme or
  X-Forwarded-Proto is http gets one 308 to https://codewhale.net with the
  same path and query, before any other branch. Other hosts (localhost,
  previews) are untouched.

Not covered: paths the Workers asset layer serves before the Worker runs,
for example /install.sh and /.well-known/security.txt itself. Middleware
never sees them, so plain http for those needs the zone's HTTPS redirect
or worker-first routing for those paths. Not deployed by this commit.

Evidence (local only):
- npx tsc --noEmit: exit 0.
- npx eslint middleware.ts next.config.ts: exit 0, no findings.
- npm test (web): node --test 7 passed, 0 failed; vitest 74 files,
  772 tests passed.
- npx next build + next start: security.txt 200 text/plain; /en 200 with
  no x-powered-by; Host codewhale.net with X-Forwarded-Proto http -> 308
  https://codewhale.net/en/install?ref=a; Host www.codewhale.net -> 308
  https://codewhale.net/en; X-Forwarded-Proto https -> 200; localhost
  unchanged; POST over http -> 308 (method kept).
- OpenNext Cloudflare build (next build in standalone mode, then
  opennextjs-cloudflare build --skipNextBuild) under local wrangler dev:
  worker saw http://codewhale.net/en and answered 308; over https /en is
  200 with no x-powered-by and no x-opennext; /install.sh and
  /.well-known/security.txt were served by the asset layer with no worker
  invocation (0 worker spans).
- No existing test covers the middleware and none was added.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-20 15:34:38 -07:00

66 lines
2.2 KiB
TypeScript

import type { NextConfig } from "next";
import { execFileSync } from "node:child_process";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const webRoot = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(webRoot, "..");
function git(args: string[]): string | null {
try {
return execFileSync("git", args, {
cwd: repoRoot,
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
}).trim() || null;
} catch {
return null;
}
}
const requestedSourceRevision =
process.env.CODEWHALE_SOURCE_REVISION?.trim() ||
process.env.GITHUB_SHA?.trim();
const gitSourceRevision = git(["rev-parse", "HEAD"]);
const sourceRevision = /^[0-9a-f]{40}$/i.test(requestedSourceRevision ?? "")
? requestedSourceRevision ?? ""
: /^[0-9a-f]{40}$/i.test(gitSourceRevision ?? "")
? gitSourceRevision ?? ""
: "";
const sourceCommittedAt =
process.env.CODEWHALE_SOURCE_COMMITTED_AT?.trim() ||
(sourceRevision ? git(["show", "-s", "--format=%cI", sourceRevision]) : null) ||
"";
// Security headers are set in middleware.ts (more reliable under OpenNext on
// Cloudflare than next.config.ts headers(), which doesn't always apply to
// prerendered/cached responses).
const nextConfig: NextConfig = {
outputFileTracingRoot: webRoot,
poweredByHeader: false,
reactStrictMode: true,
// Public, non-secret provenance for the exact source used to build the
// deployed worker. Next inlines these values into server output, allowing a
// credential-free post-deploy comparison without dirtying tracked facts.
env: {
NEXT_PUBLIC_CODEWHALE_SOURCE_REVISION: sourceRevision,
NEXT_PUBLIC_CODEWHALE_SOURCE_COMMITTED_AT: sourceCommittedAt,
},
images: {
remotePatterns: [
{ protocol: "https", hostname: "avatars.githubusercontent.com" },
],
},
typedRoutes: false,
};
export default nextConfig;
if (process.env.NODE_ENV === "development") {
// Initialize Cloudflare bindings (KV, etc.) when running `next dev`.
// No-op in production builds.
void import("@opennextjs/cloudflare").then(({ initOpenNextCloudflareForDev }) => {
initOpenNextCloudflareForDev({ configPath: ".cloudflare/opennext.json" });
}).catch(() => { /* dev-only convenience */ });
}