diff --git a/README.md b/README.md index dd15cdd..b4c79ed 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ technical compatibility. ## Current status -This checkout contains the merged M0-M5 implementation through the M5-03 -reliability matrix. The project is maintained in the public +This checkout contains the merged M0-M5 implementation through the M5-06 +performance gate. The project is maintained in the public [OriginShift/AnteSig repository](https://github.com/OriginShift/AnteSig), and the current public demo is [antesig.vercel.app](https://antesig.vercel.app). The engineering foundation provides a pnpm workspace with a registered @@ -142,6 +142,8 @@ or strengthen evidence semantics. - [Known issues and operational limits](docs/known-issues.md) - [Five-minute demo draft](docs/demo-script.md) - [Reliability QA report](docs/reliability-report.md) +- [Performance acceptance report](docs/performance-report.md) +- [Production release runbook](docs/release-runbook.md) - [M1 completion evidence and criteria](docs/m1-completion-evidence.md) - [Governance](docs/governance.md) - [Architecture decision records](docs/adr/README.md) diff --git a/apps/web/app/api/health/route.ts b/apps/web/app/api/health/route.ts index c0515cd..ab5094f 100644 --- a/apps/web/app/api/health/route.ts +++ b/apps/web/app/api/health/route.ts @@ -5,6 +5,12 @@ import { MOSS_BUILD_INFO } from "../../../src/server/moss-build-info"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; +const nodeProcess = ( + globalThis as typeof globalThis & { + process: { readonly versions: { readonly node: string } }; + } +).process; + export async function GET(): Promise { const health = HealthResponseSchema.parse({ contractVersion: "0.1", @@ -13,7 +19,7 @@ export async function GET(): Promise { name: "antesig", version: "0.0.0", runtime: "nodejs", - nodeVersion: "22.23.1", + nodeVersion: nodeProcess.versions.node, }, moss: MOSS_BUILD_INFO, network: { diff --git a/apps/web/next-env.d.ts b/apps/web/next-env.d.ts index 9edff1c..ce4e94a 100644 --- a/apps/web/next-env.d.ts +++ b/apps/web/next-env.d.ts @@ -1,6 +1,7 @@ /// /// import "./.next/types/routes.d.ts"; +import "./.next/types/root-params.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/web/package.json b/apps/web/package.json index f08384b..a02391e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -3,6 +3,11 @@ "version": "0.0.0", "private": true, "type": "module", + "packageManager": "pnpm@11.16.0", + "engines": { + "node": "22.x", + "pnpm": ">=11 <12" + }, "scripts": { "dev": "next dev", "start": "next start", diff --git a/apps/web/src/contracts/health.ts b/apps/web/src/contracts/health.ts index 1c43097..d0e592c 100644 --- a/apps/web/src/contracts/health.ts +++ b/apps/web/src/contracts/health.ts @@ -7,7 +7,7 @@ export const HealthResponseSchema = z.strictObject({ name: z.literal("antesig"), version: z.literal("0.0.0"), runtime: z.literal("nodejs"), - nodeVersion: z.literal("22.23.1"), + nodeVersion: z.string().regex(/^22\.\d+\.\d+$/), }), moss: z.strictObject({ sourceMode: z.literal("INTEGRATION_FORK"), diff --git a/apps/web/test/contracts.test.ts b/apps/web/test/contracts.test.ts index 49e0d6f..c6d558b 100644 --- a/apps/web/test/contracts.test.ts +++ b/apps/web/test/contracts.test.ts @@ -267,7 +267,7 @@ describe("health contract", () => { name: "antesig", version: "0.0.0", runtime: "nodejs", - nodeVersion: "22.23.1", + nodeVersion: process.versions.node, }, moss: { sourceMode: "INTEGRATION_FORK", @@ -297,5 +297,11 @@ describe("health contract", () => { expect( HealthResponseSchema.safeParse({ ...health, timestamp: "now" }).success, ).toBe(false); + expect( + HealthResponseSchema.safeParse({ + ...health, + app: { ...health.app, nodeVersion: "24.0.0" }, + }).success, + ).toBe(false); }); }); diff --git a/apps/web/test/production-start.smoke.mjs b/apps/web/test/production-start.smoke.mjs index 29bfee1..8e2fb56 100644 --- a/apps/web/test/production-start.smoke.mjs +++ b/apps/web/test/production-start.smoke.mjs @@ -14,7 +14,7 @@ const expectedHealth = { name: "antesig", version: "0.0.0", runtime: "nodejs", - nodeVersion: "22.23.1", + nodeVersion: process.versions.node, }, moss: { sourceMode: "INTEGRATION_FORK", diff --git a/apps/web/test/routes.test.ts b/apps/web/test/routes.test.ts index f3cd520..a3f6e56 100644 --- a/apps/web/test/routes.test.ts +++ b/apps/web/test/routes.test.ts @@ -111,7 +111,7 @@ describe("GET /api/health", () => { name: "antesig", version: "0.0.0", runtime: "nodejs", - nodeVersion: "22.23.1", + nodeVersion: process.versions.node, }, moss: { sourceMode: "INTEGRATION_FORK", diff --git a/config/production.env.example b/config/production.env.example new file mode 100644 index 0000000..ee151f5 --- /dev/null +++ b/config/production.env.example @@ -0,0 +1,5 @@ +# Public production defaults to the deterministic Fixture-capable profile. +CLEAR402_ENABLED=false + +# MOSS_RPC_URL is intentionally absent. The hosted Web route has no Live +# session, and a missing value must remain an explicit LIVE_UNAVAILABLE result. diff --git a/docs/release-runbook.md b/docs/release-runbook.md new file mode 100644 index 0000000..fd30ee1 --- /dev/null +++ b/docs/release-runbook.md @@ -0,0 +1,109 @@ +# Production release runbook + +## Release contract + +AnteSig is deployed as a Vercel Next.js project at +. The project Root Directory is `apps/web`, whose +`package.json` pins Vercel builds and Functions to Node `22.x`; Vercel may +update the minor and patch release. `/api/health` reports the actual +`process.versions.node` value and rejects a non-22 runtime. + +The public URL requires no login. The production profile is intentionally +bounded: + +- `CLEAR402_ENABLED=false` disables Credential actions and browser resources. +- `MOSS_RPC_URL` is not configured or consumed by the hosted Web route. A Live + request must fail with `503 LIVE_UNAVAILABLE` and must not return a report or + Decision. +- Fixture requests remain deterministic and return `provenance: FIXTURE`. +- `/api/health` exposes only bounded configuration and Moss build identity. It + must never expose a secret, credential, private key, hostname, filesystem + path, or full RPC URL. + +Start from `config/production.env.example`. Do not commit `.env` files, +credentials, Vercel tokens, private keys, or RPC URLs. A missing optional +variable is the supported public baseline; it must not prevent startup. + +## Pre-deployment gate + +Run from a clean checkout of the exact release commit with Node `22.23.1` and +pnpm `11.16.0`: + +```bash +corepack enable +pnpm install --frozen-lockfile +ASDF_NODEJS_VERSION=22.23.1 pnpm audit --prod --audit-level=moderate +ASDF_NODEJS_VERSION=22.23.1 pnpm check +ASDF_NODEJS_VERSION=22.23.1 CLEAR402_ENABLED=false pnpm build +ASDF_NODEJS_VERSION=22.23.1 pnpm test:web:production +ASDF_NODEJS_VERSION=22.23.1 pnpm test:web:production +``` + +The two production-start runs each allocate a new port, start a fresh Next.js +process, wait for health, exercise the page and logo, and stop the process. +They are the repeatable restart/cold-process smoke. They do not claim that a +specific Vercel request reached a newly allocated serverless isolate. + +For a manual local fallback: + +```bash +ASDF_NODEJS_VERSION=22.23.1 CLEAR402_ENABLED=false pnpm build +ASDF_NODEJS_VERSION=22.23.1 CLEAR402_ENABLED=false PORT=3000 pnpm start +curl -fsS http://127.0.0.1:3000/api/health | jq +BASE_URL=http://127.0.0.1:3000 ASDF_NODEJS_VERSION=22.23.1 pnpm test:e2e:smoke +``` + +## Deploy and verify + +Production deploys must come from the merged `main` commit after its GitHub +`quality-gate` succeeds. Link the checkout to the existing project, inspect +the target, deploy, then record the immutable deployment URL before changing +traffic: + +```bash +npx --yes vercel@latest link --yes --project antesig --scope +npx --yes vercel@latest project inspect antesig --scope +npx --yes vercel@latest deploy --prod --yes --scope +``` + +Verify the public alias and the immutable deployment URL: + +```bash +curl -fsS https://antesig.vercel.app/api/health | jq +BASE_URL=https://antesig.vercel.app ASDF_NODEJS_VERSION=22.23.1 pnpm test:e2e:smoke +``` + +Required observations are HTTP 200 health, `app.nodeVersion` beginning `22.`, +`network.configured=false`, the expected Clear402 flag, a working Fixture +flow, explicit fail-closed Live behavior, and no authentication challenge. +Check HTTPS/HSTS headers as a separate observation. Never infer Live chain +evidence from health or Fixture success. + +## Rollback + +Before deployment, use `vercel ls antesig` to identify the current immutable +production URL. Select only a previously verified healthy production +deployment as the rollback target; do not guess from timestamps or aliases. +During an incident, the release owner runs: + +```bash +npx --yes vercel@latest rollback --scope +npx --yes vercel@latest rollback status --scope +curl -fsS https://antesig.vercel.app/api/health | jq +BASE_URL=https://antesig.vercel.app ASDF_NODEJS_VERSION=22.23.1 pnpm test:e2e:smoke +``` + +Rollback repoints production traffic without rebuilding. Vercel disables +automatic production assignment after a rollback; after the incident is +resolved, restore a newly verified deployment explicitly: + +```bash +npx --yes vercel@latest promote --scope +``` + +If the hosted service cannot be restored, use the local production fallback +above and describe it as local production, never as the public deployment. +The command behavior and plan limits are defined by Vercel's +[rollback CLI](https://vercel.com/docs/cli/rollback) and +[production rollback](https://vercel.com/docs/deployments/rollback-production-deployment) +documentation.