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

17 KiB
Raw Blame History

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

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:

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
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

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
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
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
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
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
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.