# registry-deployment spec — v1 ## ADDED Requirements ### Requirement: Public HTTPS endpoint at `ui.suaveplan.com` The deployment SHALL serve the registry on `https://ui.suaveplan.com` with a valid X.509 certificate issued by Let's Encrypt. The certificate SHALL chain to a trusted root, SHALL list `ui.suaveplan.com` as the subject CN or as a SAN, and SHALL have a `notAfter` value at least 30 days in the future at any observation point during normal operation. Caddy 2's automatic ACME flow is the issuance mechanism; manual cert provisioning is not used. **Verified by:** `openssl s_client -connect ui.suaveplan.com:443 -servername ui.suaveplan.com` issued from the post-deploy verification job; cert validity, subject, and renewal window are asserted. ### Requirement: Registry JSON conforms to shadcn `registry-item` schema Every JSON file served under `https://ui.suaveplan.com/r/*.json` SHALL conform to the shadcn `registry-item` v1 schema: required fields are `$schema`, `name`, `type`, `files`; optional fields include `dependencies`, `registryDependencies`, `tailwind`, `cssVars`. The build step SHALL validate every emitted file against a Zod schema before the deploy job uploads it; a schema violation SHALL fail the build. **Verified by:** the build script's pre-upload validation pass plus the post-deploy Playwright check that fetches `/r/index.json` and a sampled subset of per-component manifests and re-validates each against the same schema. ### Requirement: `npx shadcn@latest add @suaveplan/` works A consumer running `npx shadcn@latest add @suaveplan/` in a project whose `components.json` references `https://ui.suaveplan.com/r/{name}.json` SHALL successfully fetch and write the component source into the consumer's `src/components/ui/.tsx` (or the path the consumer's `components.json` `aliases` resolve to). The fetched file SHALL be byte-identical to the file declared in the registry manifest. **Verified by:** the end-to-end contract test that creates a throwaway Vite project, points it at the public registry, runs `npx shadcn@latest add @suaveplan/button`, and asserts the output file exists and exports the `Button` symbol. ### Requirement: Storybook static loads without console errors A request to `https://ui.suaveplan.com/storybook/` SHALL return the Storybook static index page. A headless Chromium navigating to that URL SHALL render the Storybook root element, SHALL log zero `console.error` messages over a 5-second observation window, and every subresource request fired during navigation SHALL return a 2xx status. **Verified by:** Playwright test in the post-deploy verification job using `page.on('console', ...)` and `page.on('response', ...)` to capture the full session. ### Requirement: Deploy is idempotent Re-running the GitHub Actions deploy workflow against the same input commit SHALL leave the served file tree byte-identical to the prior deploy. Successive deploys with no source changes SHALL NOT thrash the live tree. The workflow's `concurrency:` block SHALL serialise simultaneous runs of the same workflow on the same ref so back-to-back pushes never race. **Verified by:** checksum comparison between the pre- and post-deploy snapshots in the verification job, plus a manual re-run of the workflow to confirm the second run is a no-op beyond the snapshot copy. ### Requirement: Rollback is one command The previous registry tree SHALL be retained at `/srv/registry/.previous//` after every deploy. A single command (`bash ops/registry/rollback.sh`) executed on the OVH host SHALL atomically swap the live tree with the most recent snapshot. A second invocation immediately after SHALL roll forward to what was previously live. **Verified by:** the runbook test in the verification gate, which runs the rollback once, confirms the live registry now serves the prior contents, then rolls forward and confirms current contents are restored. ### Requirement: TLS 1.2 minimum, HSTS, security headers The Caddy configuration SHALL pin `protocols tls1.2 tls1.3` — TLS 1.0 and TLS 1.1 are rejected. Every response from the domain SHALL carry the headers `Strict-Transport-Security: max-age=31536000; includeSubDomains; preload`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer`, and `Permissions-Policy: interest-cohort=()`. Storybook responses additionally carry a restrictive Content-Security-Policy. **Verified by:** `curl -sI https://ui.suaveplan.com/r/index.json` and `curl -sI https://ui.suaveplan.com/storybook/` in the post-deploy job; assertions on each required header. TLS protocol floor verified by `openssl s_client -tls1_1` returning a handshake failure. ### Requirement: No secrets in committed files No file under `ops/registry/` or `.github/workflows/` SHALL contain the value of any secret (SSH private key, host name, user name, API token). Placeholders (`__OVH_HOST__`, `__SECRET__`) appear in `ops/registry/.env.example`; real values live in GitHub Actions secrets named `OVH_SSH_PRIVATE_KEY`, `OVH_SSH_HOST`, `OVH_SSH_USER` and are documented in `ops/registry/README.md` by name only. The runbook's secret-rotation section names the secrets but never their values. **Verified by:** `git grep -nE '(BEGIN OPENSSH PRIVATE KEY|ssh-(rsa|ed25519) AAAA)'` returning zero matches in the change set; manual review of every committed file under `ops/registry/` and the workflow file. ### Requirement: CI deploys as a scoped non-root user The GitHub Actions workflow SHALL connect to OVH as `deploy-registry`, NOT as `root`. The `deploy-registry` user's sudoers rule SHALL permit only `rsync` writes into `/srv/registry/`; no shell, no general sudo. The user's `ssh root@ovh` access remains available for ad-hoc admin tasks and is intentionally not used by CI to keep the CI blast radius minimal. **Verified by:** review of the runbook's user-creation section and the sudoers rule it documents; review of the workflow file to confirm the SSH connection target is the scoped user.