Files
Portfolio/docs/superpowers/plans/2026-09-01-portfolio-phase-4-release.md

291 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Portfolio Phase 4: Security, Packaging, and Deployment Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Produce a reproducible, privacy-cleared, signed static release and verify an atomic Nginx deployment with correct security headers, caching, metadata, rollback, and post-launch SEO setup.
**Architecture:** Source control remains private for the initial launch, so the publication set contains one public deployment target rather than a public Git mirror. Canonical digests bind the four inputs and final artifacts; isolated scan/reconciliation jobs produce a signed release manifest. The server stages immutable release directories and atomically switches one `current` symlink only after local and remote verification.
**Tech Stack:** Node.js crypto, RFC 8785 canonical JSON, npm audit/license tooling, CycloneDX, containerized privacy workers, Nginx, SSH/rsync after explicit deployment authorization, Playwright
**Spec:** `docs/superpowers/specs/2026-09-01-portfolio-journey-design.md`
## Global Constraints
- No remote, domain, server, DNS, Search Console, or public-repository mutation occurs without Luca’s explicit approval at the deployment checkpoint.
- Production configuration contains only `environment`, canonical HTTPS URL, and immutable build ID.
- Release/signing keys, issuer state, denylist, approval ledger, original sources, and deployment credentials live outside the repository and build output.
- Every signed object uses canonical JSON, lowercase SHA-256, explicit schema version, scoped key ID, and issuer epoch.
- A scan or signature failure leaves the previous deployed release untouched.
- The launch source repository stays private; no public Git mirror or source map is published.
- Production deployment is one atomic Nginx release-pointer change with a recorded predecessor and tested rollback.
---
### Task 1: Generate and test the static-host security contract
**Files:**
- Create: `src/security/security-policy.ts`
- Create: `scripts/generate-host-config.mjs`
- Create: `nginx.portfolio.conf.example`
- Create: `tests/unit/security-policy.test.ts`
- Create: `tests/integration/nginx-config.test.ts`
- Modify: `scripts/prerender.mjs`
- Modify: `scripts/verify-artifacts.mjs`
**Interfaces:**
- Consumes: validated `SiteConfig`, JSON-LD CSP hash, hashed asset manifest.
- Produces: exact CSP/header map, Nginx include, redirect rules, cache rules, and preview noindex rules.
- [ ] **Step 1: Write failing policy tests**
```ts
expect(productionHeaders.csp).toContain("default-src 'self'");
expect(productionHeaders.csp).toContain("connect-src 'none'");
expect(productionHeaders.csp).toContain("object-src 'none'");
expect(productionHeaders.csp).toContain("frame-ancestors 'none'");
expect(productionHeaders.referrerPolicy).toBe('no-referrer');
expect(previewHeaders.xRobotsTag).toBe('noindex, nofollow, noarchive');
```
Also reject wildcard source expressions, `unsafe-inline`, external scripts/fonts/media, public source maps, and production sitemap output in preview mode.
- [ ] **Step 2: Implement the exact policy builder**
Use:
```text
default-src 'self'; script-src 'self' <JSON-LD-hash>; style-src 'self';
img-src 'self' data:; media-src 'self'; font-src 'self'; connect-src 'none';
object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self';
upgrade-insecure-requests
```
Set HSTS `max-age=31536000` after HTTPS verification, `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer`, `X-Frame-Options: DENY`, and least-privilege Permissions Policy. Do not add `includeSubDomains` until all subdomains are audited for HTTPS.
- [ ] **Step 3: Generate Nginx cache/range/redirect rules**
Hashed JS/CSS/assets receive `public, max-age=31536000, immutable`; HTML/robots/sitemap receive revalidation; correct MIME types and byte ranges remain enabled for MP4/WebM. HTTP and alternate host redirect to the canonical HTTPS host in one permanent hop.
- [ ] **Step 4: Validate in a local Nginx container**
Run an unprivileged Nginx container mounting `dist/` read-only and the generated config. Test `nginx -t`, headers, redirect simulation, cache headers, content types, and `Range: bytes=0-1023` video response.
- [ ] **Step 5: Commit the hosting contract**
```bash
npm run test:unit -- tests/unit/security-policy.test.ts tests/integration/nginx-config.test.ts
git add src/security scripts/generate-host-config.mjs scripts/prerender.mjs scripts/verify-artifacts.mjs nginx.portfolio.conf.example tests
git commit -m "feat: define secure static hosting contract"
```
---
### Task 2: Implement canonical provenance and approval verification
**Files:**
- Create: `src/security/canonical-digest.ts`
- Create: `src/security/approval-envelope.ts`
- Create: `src/security/approval-closure.ts`
- Create: `src/security/production-input-root.ts`
- Create: `src/security/release-manifest.ts`
- Create: `scripts/release/verify-approvals.mjs`
- Test: `tests/unit/canonical-digest.test.ts`
- Test: `tests/unit/approval-envelope.test.ts`
- Test: `tests/unit/approval-closure.test.ts`
- Test: `tests/integration/release-manifest.test.ts`
**Interfaces:**
- Consumes: four public inputs, external signed approval envelopes, protected ledger head/revision, scoped public keys.
- Produces: `approvedRecordRoot`, `approvedAssetRoot`, `approvalProofSetDigest`, `productionInputRoot`, and `ApprovalProjectionAttestation` verification result.
- [ ] **Step 1: Install and pin canonical/SBOM tooling**
```bash
npm install canonicalize
npm install --save-dev @cyclonedx/cyclonedx-npm license-checker-rseidelsohn
```
- [ ] **Step 2: Write independent canonical vectors first**
Tests contain fixed canonical bytes and expected SHA-256 values for public records, asset records, signed envelopes, proof set, PreparedAssetSet member index, site config, and four-input root. Reject duplicate JSON keys, unknown fields, unpaired surrogates, non-NFC strings/paths, alternate base64/signature encodings, duplicate IDs, case-fold path collisions, and extra files.
- [ ] **Step 3: Implement canonical digest primitives**
```ts
export function canonicalJsonDigest(schema: string, value: unknown): Sha256Hex;
export function byteDigest(bytes: Uint8Array): Sha256Hex;
export function verifyEd25519Envelope<T>(envelope: SignedEnvelope<T>, trust: TrustPolicy): T;
```
Use RFC 8785 bytes and lowercase 64-character hexadecimal SHA-256. Signature payloads include key ID, issuer epoch, scope, issued/expiry timestamps, and policy version.
- [ ] **Step 4: Implement exact closure/root verification**
Reproduce the spec’s tuple ordering and schemas for content, assets, proof set, PreparedAssetSet, and four fixed labeled production inputs. Require one current valid approval for every emitted record and asset. Protected ledger equality and trusted current time are mandatory inputs, not advisory metadata.
- [ ] **Step 5: Implement release-manifest verification**
The manifest binds all four input digests/root, every built/precompressed artifact hash, source commit/tree when available, exact deploy file set, approval roots/proof, scan evidence, policy/tool versions, predecessor manifest, issue time, and minimum expiry. It cannot name a public Git target in the initial launch mode.
- [ ] **Step 6: Run and commit**
```bash
npm run test:unit -- tests/unit/canonical-digest.test.ts tests/unit/approval-envelope.test.ts tests/unit/approval-closure.test.ts tests/integration/release-manifest.test.ts
git add src/security scripts/release package.json package-lock.json tests
git commit -m "feat: verify canonical portfolio release provenance"
```
---
### Task 3: Build isolated privacy reconciliation and release signing
**Files:**
- Create: `containers/privacy-worker-a/Dockerfile`
- Create: `containers/privacy-worker-b/Dockerfile`
- Create: `containers/privacy-matcher/Dockerfile`
- Create: `containers/privacy-coordinator/Dockerfile`
- Create: `scripts/release/enumerate-closure.mjs`
- Create: `scripts/release/run-privacy-workers.mjs`
- Create: `scripts/release/reconcile-scan.mjs`
- Create: `scripts/release/sign-release.mjs`
- Create: `scripts/release/trust-policy.schema.json`
- Test: `tests/security/privacy-workers.test.ts`
- Test: `tests/security/release-signing.test.ts`
**Interfaces:**
- Consumes: read-only build/source closure, external denylist capability, external trust policy/keys.
- Produces: immutable per-digest worker reports, matcher results, scan-evidence summary, workload attestation, and signed release manifest.
- [ ] **Step 1: Write compromise and closure tests**
Cover a schema-valid false pass from one authenticated worker, mismatched normalized output, missing/extra/reordered reports, unsupported/encrypted/malformed containers, nested archive limits, archive traversal/symlink/device entries, OCR/QR canaries, stale ledger head, expired approval without ledger append, forged issuer epoch, and substituted artifact.
- [ ] **Step 2: Implement independent workers and matcher**
Workers use different images/toolchains, receive one digest/object at a time, run networkless/read-only/non-root with CPU/memory/time limits, and emit immutable typed reports bound to input/check/normalized output/image. The matcher compares agreed normalized evidence and applies rule IDs; disagreement fails.
- [ ] **Step 3: Implement trusted reconciliation**
The coordinator independently enumerates every Git/build/precompressed/release object applicable to the private source deployment, requires both worker reports plus matcher result per check, rejects any coverage mismatch, and emits canonical evidence/candidate objects to content-addressed write-once storage.
- [ ] **Step 4: Implement scoped signing**
`sign-release.mjs` receives canonical candidate/evidence bytes through a read-only digest interface, verifies approval projection, privacy workload, policy, tool provenance, ledger, expiry, predecessor, and all semantic fields, then signs exact candidate bytes with an external non-exportable or read-only mounted Ed25519 key. Scanner/coordinator processes never receive the release key.
- [ ] **Step 5: Test every trust-root compromise response**
Exercise approval, projection, privacy workload, policy attestation, tool provenance, release signing, and publisher credential epochs. Old epochs fail; approval compromise requires checkpoint audit/reapproval; policy/tool compromise invalidates dependents and forces fresh scan; publisher compromise advances the fence.
- [ ] **Step 6: Run and commit**
```bash
npm run test:unit -- tests/security/privacy-workers.test.ts tests/security/release-signing.test.ts
git add containers scripts/release tests/security
git commit -m "feat: isolate privacy scanning and release signing"
```
Do not commit keys, attestations containing private evidence, denylist contents, or scanner payload logs.
---
### Task 4: Package an atomic, reversible Nginx release
**Files:**
- Create: `scripts/build-release.mjs`
- Create: `scripts/deploy/prepare-release.sh`
- Create: `scripts/deploy/activate-release.sh`
- Create: `scripts/deploy/rollback-release.sh`
- Create: `scripts/deploy/verify-staged-release.mjs`
- Create: `docs/deployment/nginx-runbook.md`
- Test: `tests/integration/release-package.test.ts`
- Test: `tests/integration/atomic-deploy.test.ts`
**Interfaces:**
- Consumes: signed manifest, exact `dist/` closure, current release pointer/fence.
- Produces: immutable release directory `<buildId>-<manifestDigest>`, readiness receipt, atomic `current` pointer, rollback receipt.
- [ ] **Step 1: Write failing package and transaction tests**
Test extra/substituted files, invalid signature, stale predecessor, stale fence, crash before pointer swap, crash after pointer swap, concurrent activation, expiry crossing, rollback with a newly signed manifest, and orphan-stage cleanup.
- [ ] **Step 2: Build the release package**
`build-release.mjs` copies only manifest-listed files, verifies hashes, writes the signed manifest and SBOM, creates deterministic tar ordering/timestamps, and verifies the archive by extracting into a temporary read-only test root.
- [ ] **Step 3: Implement server-side staging**
`prepare-release.sh` uses fixed server paths supplied as arguments, creates a new immutable release directory, verifies manifest/signature/hashes/time, applies least-privilege ownership, runs the staged smoke check through an internal Nginx location, and writes a readiness receipt. It never changes `current`.
- [ ] **Step 4: Implement fenced atomic activation**
`activate-release.sh` obtains an exclusive lock, compares expected current manifest/fence, rechecks expiry/readiness, increments the fence, creates `current.next` symlink to the staged directory, and atomically renames it over `current`. A failed check leaves the old symlink untouched. Receipts record predecessor/new digest and fence.
- [ ] **Step 5: Implement signed rollback**
Rollback selects an existing immutable release but first creates/verifies a new signed rollback manifest against the current approval/ledger state. Activation uses the same fenced symlink swap; an old manifest is never replayed.
- [ ] **Step 6: Verify locally and commit**
```bash
npm run test:unit -- tests/integration/release-package.test.ts tests/integration/atomic-deploy.test.ts
git add scripts/build-release.mjs scripts/deploy docs/deployment tests/integration
git commit -m "feat: package atomic reversible nginx releases"
```
---
### Task 5: Run supply-chain, deployment, and SEO verification
**Files:**
- Create: `scripts/supply-chain-check.mjs`
- Create: `scripts/smoke-production.mjs`
- Create: `tests/deployment/headers.spec.ts`
- Create: `tests/deployment/seo.spec.ts`
- Create: `tests/deployment/no-javascript.spec.ts`
- Create: `docs/deployment/release-checklist.md`
- Create: `docs/deployment/rollback-record-template.md`
- Modify: `package.json`
- Modify: `.gitignore`
**Interfaces:**
- Consumes: canonical public URL and deployed signed manifest digest.
- Produces: SBOM/license/vulnerability evidence, deployed smoke report, SEO validation, rollback record, and Search Console action record.
- [ ] **Step 1: Implement clean supply-chain verification**
Use `npm ci --ignore-scripts` by default, explicitly run only approved package scripts in a credentialless sandbox, verify lockfile cleanliness, run production/development vulnerability and license checks, generate CycloneDX SBOM, and fail on unsigned/unverifiable tool images or Critical/High findings. Medium findings require a committed triage record before release.
- [ ] **Step 2: Write deployed response tests**
Assert canonical HTTPS 200, one-hop alternate redirects, CSP/HSTS/nosniff/referrer/permissions/frame headers, HTML revalidation, immutable assets, Brotli/Gzip, MIME, video ranges, canonical/OG/JSON-LD/robots/sitemap consistency, JavaScript-disabled content, and no source maps/private identifiers.
- [ ] **Step 3: Build and verify the production artifact locally**
After Luca supplies the approved canonical host, remove `config/site.production.json` from `.gitignore`, create it with exactly `environment`, approved canonical host, and immutable build ID, then run the complete build, privacy scan, signing, package, local Nginx smoke, browser matrix, performance gate, and accessibility gate. Any failure blocks remote work.
- [ ] **Step 4: Stop for explicit remote-deployment authorization**
Present the signed manifest digest, release size, local test summary, target server path, planned canonical host, exact activation command, and rollback predecessor. Do not connect to or mutate the server until Luca explicitly authorizes this release.
- [ ] **Step 5: Stage, verify, and atomically activate after authorization**
Transfer the immutable package, run server-side preparation/readiness, execute the fenced pointer switch, then run `smoke-production.mjs` against the public URL. If smoke fails, use the newly signed rollback path and record the outcome.
- [ ] **Step 6: Complete post-launch SEO actions**
Verify preferred-host redirects and indexability again, add the property in Search Console, submit the exact production sitemap, request indexing for the canonical home page, and record the submission date. Do not promise first-place ranking; monitor branded-name indexing separately.
- [ ] **Step 7: Commit non-secret release evidence**
```bash
git add scripts/supply-chain-check.mjs scripts/smoke-production.mjs tests/deployment docs/deployment package.json package-lock.json config/site.production.json
git commit -m "release: verify production portfolio deployment"
```
If `config/site.production.json` contains only the approved public host/build ID, it may be committed; credentials and private server paths remain outside Git.
## Phase 4 completion checkpoint
The project is complete only when the signed deployed digest matches the locally approved artifact, the public smoke/SEO/accessibility checks pass, the previous release remains recoverable, Search Console submission is recorded, and no private source or credential has entered the repository or public output.