From 0b2303a3e2b7ff0bf788ae82248c7252368e52b1 Mon Sep 17 00:00:00 2001 From: ced4568 Date: Fri, 28 Aug 2026 12:07:25 -0500 Subject: [PATCH] docs(phase10): canonical state audit, secrets inventory, production/security/launch readiness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gate 1 — canonical state audit: this workstation's HEAD/origin/main and every claimed milestone SHA (Phase 2A through 9.1) verified consistent; no stranded branches/tags on Gitea; full-history secret scan clean (no rotation required); Phase 9.5 confirmed absent from canonical history (pricing was given directly by Chase and used without it); second-PC reconciliation commands provided since this session cannot see that machine. Gate 3 — secrets: real inventory derived from process.env greps across every service (not guessed); Infisical auth fixed (wrong domain was the actual root cause of Phase 9.1's failures — the identity belongs to infisical.cedshomelab.com, not app.infisical.com) but no RootNode Academy project exists there yet, contradicting this phase's own authorization — flagged for Chase to create. Gates 4-9/15-16 — production: verified, not assumed, that no dedicated server/Proxmox VM has been provisioned (empty terraform.tfvars, no state file, placeholder group_vars/production.yml) — IaC validated (tofu validate, ansible --syntax-check both clean) and left unexecuted per this phase's own 'do not fake production deployment' instruction. rootnodeacademy.com confirmed live (Cloudflare, real HTTPS) — the separate, not-RootNode-owned website; no Platform API subdomain exists yet. Gate 18 — security audit: full review against every regression suite rerun this phase; no launch-critical defect found. Gates 19/20 — full regression (131 checks: e2e-phase5b/5c/6f/7/8/9, content validation, migrations, frontend build) and the Winter Break commerce rehearsal, both rerun clean against current main. Gates 10-14/21 — website integration, Winter Break pricing (99/49, real, Stripe-validated), agreements/legal, tax, and refund policy all reviewed; legal text/tax/refund policy/dates/venue/ capacity correctly left as open owner decisions, not invented. No application code changed this phase — audit/launch-prep only. --- docs/PHASE10_CANONICAL_STATE_AUDIT.md | 169 +++++++++++++++++++++++++ docs/PHASE10_LAUNCH_READINESS.md | 152 +++++++++++++++++++++++ docs/PHASE10_PRODUCTION_DEPLOYMENT.md | 90 ++++++++++++++ docs/PHASE10_SECURITY_AUDIT.md | 44 +++++++ docs/SECRETS_AND_INFISICAL.md | 87 +++++++++++++ docs/WINTER_BREAK_LAUNCH_CHECKLIST.md | 172 ++++++++++++++------------ 6 files changed, 634 insertions(+), 80 deletions(-) create mode 100644 docs/PHASE10_CANONICAL_STATE_AUDIT.md create mode 100644 docs/PHASE10_LAUNCH_READINESS.md create mode 100644 docs/PHASE10_PRODUCTION_DEPLOYMENT.md create mode 100644 docs/PHASE10_SECURITY_AUDIT.md create mode 100644 docs/SECRETS_AND_INFISICAL.md diff --git a/docs/PHASE10_CANONICAL_STATE_AUDIT.md b/docs/PHASE10_CANONICAL_STATE_AUDIT.md new file mode 100644 index 0000000..e58de38 --- /dev/null +++ b/docs/PHASE10_CANONICAL_STATE_AUDIT.md @@ -0,0 +1,169 @@ +# Phase 10 — Canonical State Audit + +Gate 1. Performed before any Phase 10 production/launch work, per Phase 10's own explicit +requirement — Chase has been developing from two workstations, so nothing downstream was +started until canonical state was actually verified, not assumed. + +## Baseline + +- Starting SHA (as reported at the end of Phase 9.1): `a4c00580...` (full: + `a4c0058f8c3d9612f1d30508626fa5be00431fa7`). +- This workstation's `HEAD` and `origin/main`, confirmed via `git fetch --all --prune`, are + **both exactly `a4c0058f8c3d9612f1d30508626fa5be00431fa7`** — this workstation is fully + up to date with canonical Gitea, and canonical Gitea has not moved since Phase 9.1 ended. + +## 1.1 Current workstation state + +- Branch: `main`. Working tree: clean (`git status --short` — no output). +- `git remote -v`: `origin` → `gitea:ced4568/rootnode-academy.git` (fetch + push) — canonical, + no GitHub `kubekosh` remote configured in this clone at all. +- `git branch -vv`: `* main a4c0058 [origin/main]` — tracking correctly, no divergence. +- No stashes, no dangling work, no uncommitted changes. + +## 1.2 Remote audit + +`git ls-remote --heads --tags origin` shows exactly one branch (`main`, at `a4c0058`) and four +tags (`v0.4.1-preprod`, `v0.5.0-student-auth`, `v0.5.3-enrollment-comms`, `v0.6.0-root-v1`) — no +other branches, no unexpected experimental branches, no stale branches to reconcile. +`git log --all --oneline` and `git log --oneline` both return exactly **184** commits — every +commit reachable in this repository is reachable from `main`. **There is no orphaned or +unmerged work sitting in Gitea outside `main`.** + +## 1.3 Second-PC reconciliation + +This execution environment has no access to Chase's other workstation. Gitea's own state (§1.2) +proves there is no SECOND branch or unpushed-but-fetchable ref waiting anywhere on the remote — +if work exists only on the other PC, it is **local and unpushed there**, not merely unmerged +here. This is not automatically resolvable from this side. + +**Exact commands for Chase to run on PC #2** to prove no local-only RootNode work is stranded: + +```bash +cd ~/Dev/Gitea/ced4568/rootnode-academy # or wherever the clone lives on that machine +git status # must show "nothing to commit, working tree clean" +git branch -vv # every branch should show [origin/main] with no + # "ahead" count, or should not exist at all +git fetch origin +git log origin/main..HEAD --oneline # MUST be empty — any output here is unpushed work +git stash list # should be empty, or each entry reviewed manually +``` + +If `git log origin/main..HEAD --oneline` on PC #2 produces ANY output, that is real, unpushed +work — it must be reviewed and pushed (or explicitly discarded by Chase's own decision) before +being considered part of canonical state. **This audit cannot rule that out from here — it can +only confirm Gitea itself has no evidence of it.** + +## 1.4 Expected milestone audit + +Every phase SHA Phase 10's own authorization listed (Phase 2A through Phase 9.1 final) was +verified present in canonical history, in full agreement with the commit message labels: + +| Phase | SHA (verified) | Commit subject | +|---|---|---| +| 2A | `42dfb1c` | docs(phase2a): document gateway and student launch architecture | +| 2B | `08ff531` | docs(phase2b): document admin and platform-data architecture | +| 3A | `4069292` | docs(phase3a): document production identity architecture | +| 3B | `45a147a` | docs(phase3b): document the learning and assessment engine | +| 4 | `bf1e1b5` | docs(phase4): production infrastructure documentation + runbooks | +| 4.1 | `5bc0743` | docs(phase4.1): pre-production hardening documentation | +| 5A | `896c230` | docs(phase5a): student identity and remote classroom documentation | +| 5B | `1c08a30` | docs(phase5b): self-paced enrollment documentation + website integration contract | +| 5C | `777efd1` | docs(phase5c): enrollment communications documentation + Google Workspace runbook | +| 6A | `fd38d06` | docs(phase6a): document RootNode course foundation | +| 6B | `2db1368` | docs(phase6b): document curriculum expansion | +| 6C | `bd0a10f` | docs(phase6c): pilot readiness standard, pilot curriculum manifest, ROOT 11 scope | +| 6D | `b6516a7` | docs(phase6d): document the ROOT Foundations milestone | +| 6E | `87ad125` | docs(phase6e): document the ROOT Foundations Completion milestone | +| 6F | `d066410` | docs(phase6f): document ROOT_ V1 pilot readiness (tag `v0.6.0-root-v1`) | +| 7 | `df5e39f` | docs(phase7): document the business/product architecture foundation | +| 8 | `788fb19` | docs(phase8): document enrollment operations | +| 9 | `80a5f6e` | docs(phase9): document commerce integration and Winter Break launch readiness | +| 9.1 (prep) | `77fd186` | docs(stripe): document Phase 9.1 blocked status and live-mode readiness checklist | +| 9.1 (final) | `a4c0058` | docs(stripe): document real Stripe TEST-mode validation results | + +**Phase 9.5 — NOT FOUND.** No commit, branch, or tag matching "Phase 9.5" or a founder-pricing +document exists anywhere in this repository's reachable history (`git log --all` contains no +such commit), and no `docs/ROOTNODE_PRICING_AND_OFFERS_V1.md` file exists at any point in +history. This exact discrepancy was already surfaced during Phase 9.1 (Chase explained it was +done on the desktop PC, separate from this laptop) — §1.3's commands above are how to confirm +whether that work is sitting unpushed on PC #2, or whether it was a decision made directly rather +than a planned commit. **The founder-approved pricing itself ($399 launch / $449 standard, +39900/44900 cents, "Founding Cohort," paid in full, no deposit) was given directly by Chase in +conversation and used for Phase 9.1's real Stripe validation and this phase's documentation — it +did not require a committed document to act on, and is not invented by this phase.** + +## 1.5 File/artifact audit + +Searched all tracked files for secret-shaped filenames (`.env*`, `.local`, `credential`, +`secret`, `.key`, `.pem`, `.p12`, service-account JSON). Every match is a legitimate +template/placeholder or educational-content reference, never a real runtime secret: + +- Course content (`content/root/root-07-.../m02-configuration-and-secrets/`) — teaching material + about Kubernetes Secrets, no real secret data. +- `infra/ansible/roles/*/templates/*.env.j2` — Ansible Jinja2 templates referencing + `{{ vault_* }}` variables (values live in an Ansible Vault-encrypted file, never plaintext in + git). +- `infra/environments/{production,staging}/*.env.template` — checked-in templates with explicit + `__REPLACE_WITH_...__`/`__REPLACE_openssl_rand_hex_32__` placeholders, never real values. +- `platform/provisioner/.env.example`, migration `017_student_credentials.sql` (schema, not + data), `scenarios/data/*secret*.json` (fake Kubernetes lab-scenario fixtures). + +`.gitignore` correctly excludes `.env` (blanket pattern) — confirmed via `git status +--porcelain --ignored`: `platform/.env` and `platform/provisioner/.env` (both containing real +local secrets) are ignored, never tracked. + +## 1.6 Secret history audit + +Searched the **entire** git history (`git log --all -p`, pickaxe search `-S`) for: +`sk_test_`, `sk_live_`, `whsec_`, `BEGIN PRIVATE KEY`, `BEGIN RSA PRIVATE KEY`, `"private_key"`, +and a broad regex for any `sk_test_`/`sk_live_`/`whsec_`-prefixed string of realistic secret +length (20+ trailing characters). + +**Result: clean. No real secret value has ever been committed.** Every `sk_test_`/`sk_live_`/ +`whsec_` hit is a code guard-clause literal (e.g. `.startsWith('sk_test_')`) or documentation +prose referencing the prefix pattern, from this session's own Phase 9.1 commits — never a full +key. The one `"private_key"` hit (`b38c46a`, Phase 5C) is a code comment showing an EXAMPLE +env-var invocation with a `"..."` placeholder, not a real value. No `BEGIN PRIVATE KEY`/`BEGIN +RSA PRIVATE KEY` header appears anywhere in history. **No credential rotation is required.** + +## 1.7 Current runtime secret state (this workstation) + +`platform/.env` (gitignored, never committed) holds real local-dev values for +`PROVISIONER_SHARED_SECRET`, `INTEGRATION_SHARED_SECRET`, `GOOGLE_SERVICE_ACCOUNT_JSON`, +`EMAIL_FROM_ADDRESS`, `GOOGLE_DELEGATED_USER` — all pre-existing from earlier phases. Its +`PAYMENT_PROVIDER`/`STRIPE_SECRET_KEY`/`STRIPE_WEBHOOK_SECRET` lines are present but **blank**, +per this repo's own "set only for a deliberate session, unset again after" convention (matching +`EMAIL_PROVIDER`'s established pattern) — confirmed after Phase 9.1's real-Stripe validation +session ended. See `docs/SECRETS_AND_INFISICAL.md` for the full inventory and centralization +status. + +## Discrepancies found + +1. **"Phase 9.5" has no canonical commit** — see §1.4. Not a blocker (the pricing decision was + usable directly), but flagged so it isn't assumed to exist in code review later. +2. **Chase's Phase 10 authorization states "Chase has ALREADY created RootNode Academy" as an + Infisical project** on `infisical.cedshomelab.com` — verified NOT present. The organization + ("Ceds HomeLab") has exactly six existing projects (Core Infrastructure, Hermes Fleet, + Kubernetes, Media Platform, Monitoring, Network Service), none named RootNode Academy or + similar. See `docs/SECRETS_AND_INFISICAL.md` §"Project status" for the full finding and what + Chase needs to do (create the project via the Infisical web dashboard — the CLI has no + non-interactive project-creation command). +3. **No dedicated production server/Proxmox VM has been provisioned** — `infra/terraform/` has + no `terraform.tfvars` (only `.example`) and no state file; `infra/ansible/group_vars/ + production.yml` still holds literal placeholders (`__PLATFORM_VM_IP__`, + `__LAB_HOST_VM_IP__`, `__PROXMOX_HOST_MGMT_CIDR__`). See `docs/ + PHASE10_PRODUCTION_DEPLOYMENT.md`. + +## Remediation performed + +None required at the git/history level (nothing was broken). Documentation created/updated to +record all of the above (this document, plus `docs/SECRETS_AND_INFISICAL.md` and `docs/ +PHASE10_PRODUCTION_DEPLOYMENT.md`). + +## Conclusion + +**Canonical Gitea state is internally consistent, fully verified against every claimed +milestone, and free of any committed secret.** The only open item from this gate is Chase +running the PC #2 commands in §1.3 to positively rule out stranded local work there — Gitea's +own history gives no reason to believe any exists, but this audit cannot see that machine +directly. diff --git a/docs/PHASE10_LAUNCH_READINESS.md b/docs/PHASE10_LAUNCH_READINESS.md new file mode 100644 index 0000000..d52b726 --- /dev/null +++ b/docs/PHASE10_LAUNCH_READINESS.md @@ -0,0 +1,152 @@ +# Phase 10 — Launch Readiness + +Gates 2, 10-14, 19-20, and the final launch matrix. See `docs/PHASE10_CANONICAL_STATE_AUDIT.md` +(Gate 1), `docs/SECRETS_AND_INFISICAL.md` (Gate 3), `docs/PHASE10_PRODUCTION_DEPLOYMENT.md` +(Gates 4-9/15-16), and `docs/PHASE10_SECURITY_AUDIT.md` (Gate 18) for the rest. + +## Gate 2 — Full application audit + +Reviewed for architectural coherence across every phase; nothing was found inconsistent or in +need of a fix. Identity/auth, learning, Lab platform, business (offerings/registrations/ +guardians/agreements), and commerce (pricing/payments/webhooks) all remain exactly as documented +in their own phase docs — re-verified by actually rerunning every suite (Gate 19 below), not by +re-reading code and assuming it still works. + +## Gate 10 — Website integration + +`docs/WEBSITE_REGISTRATION_AND_CHECKOUT.md` and `docs/WEBSITE_ENROLLMENT_INTEGRATION.md` remain +the full, accurate, already-tested contract: public offerings API, registration submission, +agreement acceptance, checkout creation, and the status-lookup endpoint. Nothing new was built — +the contract was fully proven end to end (including against real Stripe) in Phase 9/9.1. The +website itself (`rootnodeacademy.com`) is confirmed live and real (Gate 6, `docs/ +PHASE10_PRODUCTION_DEPLOYMENT.md`) but remains a separate, not-RootNode-owned repository — this +phase did not access or modify it, per the standing boundary. + +## Gate 11 — Winter Break offering + +Pricing is locked and real: **$399.00 launch ("Founding Cohort") / $449.00 standard reference, +39900/44900 cents, paid in full, no deposit, no payment plan** — given directly by Chase, +validated against real Stripe TEST mode at the exact $399 amount in Phase 9.1. **Dates, venue, +and capacity remain undecided** — not invented by this phase, exactly as every prior phase's own +documentation already states. The offering therefore cannot be transitioned to +`enrollment_open` for real, public registration yet — the platform mechanism is ready +(`PATCH /api/admin/offerings/:id` accepts all four fields the moment they're decided). + +## Gate 12 — Agreements / legal + +`agreement_definitions`/`agreement_acceptances` architecture is real, tested, and versioned +(Phase 7-9). **No attorney-approved legal text exists for any required Winter Break agreement** +— `agreement_definitions.body_summary` remains a plain-language placeholder, exactly as every +prior phase's docs have stated since Phase 7. This phase did not write legal language and did +not treat any placeholder as attorney-approved, per Phase 10's own explicit instruction. + +**LAUNCH BLOCKED — APPROVED AGREEMENT TEXT REQUIRED.** Likely required categories (not +authored, just the taxonomy Phase 10 asked to identify): program participation terms, guardian +consent (minor registrants), a participation/liability waiver, acceptable-use / lab/technology +rules, a cancellation/refund acknowledgment tied to whatever policy Gate 14 resolves, and a +privacy/data-handling notice if RootNode's counsel determines one is needed beyond what's already +in `docs/STUDENT_GUARDIAN_DATA_MODEL.md`. A media release is optional — RootNode's own choice, +not decided here. + +## Gate 13 — Tax + +**TAX DECISION REQUIRED.** No deliberate Mississippi (or any jurisdiction's) tax treatment has +been configured anywhere in this codebase — `payment_transactions.amount` is charged exactly as +the offering's own price, with no tax line item, no tax calculation, and no tax remittance logic +anywhere (Phase 9's own explicit "no complex tax automation" scope boundary, unchanged). This is +not this phase's decision to make, and the current absence of tax handling should not be read as +an implicit "no tax owed" determination — that requires Chase (or a professional) to actually +decide, then tell the platform what to do (charge tax as a line item, treat the $399 as +tax-inclusive, or confirm no sales tax applies to this specific service in this jurisdiction). + +## Gate 14 — Refund / cancellation policy + +**LAUNCH BLOCKED — REFUND/CANCELLATION POLICY REQUIRED.** The platform's MECHANICAL behavior is +real, tested, and proven against actual Stripe money-movement in Phase 9.1: a refund is recorded +as its own tracked state and **never automatically** cancels the registration or revokes academic +enrollment — confirmed directly against the database after a real Stripe TEST refund. What is +still missing is the actual customer-facing POLICY text (how many days before the camp is a +refund allowed, is it full or partial, etc.) — a founder decision this phase does not invent. + +## Gate 19 — Complete regression + +Fresh stack, this phase, after confirming canonical baseline (Gate 1): + +| Suite | Result | +|---|---| +| `e2e-phase5b.js` | 32/32 | +| `e2e-phase5c.js` | 29/29 | +| `e2e-phase6f.js` | 17/17 | +| `e2e-phase7.js` | 17/17 | +| `e2e-phase8.js` | 16/16 | +| `e2e-phase9.js` | 20/20 | +| Course-content validator | PASS, 0 errors | +| Migration idempotency | Confirmed both ways — a full `down -v`/`up` cycle applies all 37 migrations cleanly from empty, and a same-database container restart re-applies with zero errors | +| Frontend production build | Clean (only the pre-existing chunk-size advisory) | +| `tofu validate` (Terraform/OpenTofu) | Clean | +| `ansible-playbook --syntax-check` | Clean | + +**131 automated checks, 0 failures.** No code changed this phase (Phase 10 is audit/launch-prep, +not feature work), so this is confirmation, not new coverage. + +`platform/test/stripe-integration-test.js` (the Phase 9.1 opt-in real-Stripe check) was **not** +rerun this phase — it requires real Stripe TEST credentials, which currently have no durable home +(Gate 3's Infisical project doesn't exist yet) and were correctly unset from the local `.env` +after Phase 9.1 ended. Phase 9.1's own evidence (`docs/PHASE9_1_STRIPE_TEST_VALIDATION.md`) +stands as the real-Stripe proof; nothing in this phase's own code changes touches that path. + +## Gate 20 — Pre-launch dress rehearsal + +`infra/scripts/drills/test-phase9-winter-camp-commerce.js` rerun fresh this phase against +current `main`: **13/13** — website registration → guardian → agreement → checkout → webhook → +enrollment → activation → classroom → a real disposable Lab task → instructor visibility, plus +failed-payment/duplicate-webhook/abandoned-checkout/last-seat-race, all using the deterministic +test provider (no real money, as instructed). This is the closest possible complete rehearsal +without real production infrastructure or real dates/venue/capacity — it cannot be run against +the ACTUAL Winter Break offering because that offering cannot legitimately open registration yet +(Gate 11). No fake evidence was created for the steps genuinely blocked by unresolved business +decisions. + +## Final launch matrix + +| Gate | Status | Evidence | Owner action | Blocks registration? | +|---|---|---|---|---| +| 1. Canonical repo reconciled | ✅ COMPLETE | `docs/PHASE10_CANONICAL_STATE_AUDIT.md` | None | No | +| 2. Second-PC state reconciled | ⚠️ DECISION REQUIRED | Gitea shows no stranded work; PC #2 itself unverified from here | Run §1.3's commands on PC #2 | No (informational) | +| 3. Secrets centralized (Infisical) | 🔧 MANUAL OWNER ACTION | `docs/SECRETS_AND_INFISICAL.md` | Create the RootNode Academy Infisical project | No (dev secrets already safe locally) | +| 4. Production server | ⛔ BLOCKED | `docs/PHASE10_PRODUCTION_DEPLOYMENT.md` | Purchase/provision hardware | **Yes** | +| 5. Proxmox / VMs | ⛔ BLOCKED | Same — no host to provision on | Same | **Yes** | +| 6. Firewall/isolation | ✅ COMPLETE (code), ⏸ unverified live | `docs/PHASE10_SECURITY_AUDIT.md` | None until real host exists | No (code ready) | +| 7. Production database | ⛔ BLOCKED | Same hardware blocker | Same | **Yes** | +| 8. DNS | ✅ PARTIAL | `rootnodeacademy.com` live; no Platform API subdomain yet | Decide/create the API subdomain once a server exists | **Yes**, for the API portion | +| 9. HTTPS | ✅ PARTIAL | Website real HTTPS; Platform API HTTPS pending a real host | Same as above | **Yes**, for the API portion | +| 10. Monitoring | ✅ COMPLETE (code) | `/healthz`/`/metrics` live in every test run | None until deployed | No | +| 11. Backups | ✅ COMPLETE (code), ⛔ unexecuted | Ansible `backup` role exists, never run | Run once a real host exists | **Yes** (restore-test) | +| 12. Restore test | ⛔ BLOCKED | Never executed — no target | Same | **Yes** | +| 13. Stripe TEST validation | ✅ COMPLETE | `docs/PHASE9_1_STRIPE_TEST_VALIDATION.md` | None | No | +| 14. Stripe LIVE configuration | ⛔ BLOCKED | Not configured, not attempted | Chase completes Stripe business/identity verification, provides LIVE credentials | **Yes** | +| 15. Live webhook configuration | ⛔ BLOCKED | Depends on #14 and a real HTTPS endpoint (#9) | Same | **Yes** | +| 16. Google Workspace | ✅ COMPLETE (validated in Phase 5C) | `docs/RUNBOOK_GOOGLE_WORKSPACE_EMAIL.md` | Deploy alongside the real server | No (already proven) | +| 17. Website integration | ✅ COMPLETE | Contract tested end to end, including real Stripe | None | No | +| 18. Winter Break price | ✅ COMPLETE | $399/$449, real, Stripe-validated | None | No | +| 19. Winter Break dates | ⚠️ DECISION REQUIRED | Not decided | Chase decides | **Yes** | +| 20. Venue | ⚠️ DECISION REQUIRED | Not decided | Chase decides | **Yes** | +| 21. Capacity | ⚠️ DECISION REQUIRED | Not decided | Chase decides | **Yes** | +| 22. Agreements (legal text) | ⛔ BLOCKED | Placeholder only | Attorney-approved text required | **Yes** | +| 23. Refund/cancellation policy | ⚠️ DECISION REQUIRED | Mechanism proven, policy undecided | Chase decides | **Yes** | +| 24. Tax decision | ⚠️ DECISION REQUIRED | Not configured | Chase/professional decides | **Yes** | +| 25. Production offering | ⛔ BLOCKED | Cannot legitimately open — depends on #19-24 | Depends on all of the above | **Yes** | +| 26. Instructor assignment | 🔧 MANUAL OWNER ACTION | Mechanism ready (`instructor_offerings`) | Chase assigns once the real offering exists | **Yes** | +| 27. Roster process | ✅ COMPLETE (mechanism) | Rehearsed in Gate 20 | None | No | +| 28. Student activation | ✅ COMPLETE | Proven real, twice (Phase 9.1 + Gate 20) | None | No | +| 29. Lab capacity | ✅ COMPLETE (code), ⛔ real capacity unverified | Capacity/race logic proven; real server capacity planning needs #4 | Plan once real hardware specs are known | No (mechanism), **yes** (real capacity) | +| 30. Complete dress rehearsal | ✅ COMPLETE (deterministic) | Gate 20, 13/13 | Repeat against the real offering once it can legitimately open | No | + +## Bottom line + +## PHASE 10 PARTIAL / WINTER BREAK LAUNCH BLOCKED + +Every gate that is genuinely software/audit work is complete and verified. Every gate that is a +real business, legal, tax, or hardware decision remains exactly that — a decision, not +something this phase invented on Chase's behalf. See `docs/WINTER_BREAK_LAUNCH_CHECKLIST.md` for +the single consolidated, categorized checklist Chase can work down. diff --git a/docs/PHASE10_PRODUCTION_DEPLOYMENT.md b/docs/PHASE10_PRODUCTION_DEPLOYMENT.md new file mode 100644 index 0000000..026fa0b --- /dev/null +++ b/docs/PHASE10_PRODUCTION_DEPLOYMENT.md @@ -0,0 +1,90 @@ +# Phase 10 — Production Deployment + +Gates 4-9, 15-16. Covers production infrastructure, network security, DNS/HTTPS, database +productionization, Stripe LIVE configuration, Google Workspace production email, observability, +and backups/restore. + +## Hardware status — PRODUCTION DEPLOYMENT BLOCKED — SERVER REQUIRED + +Verified, not assumed: `infra/terraform/` has no real `terraform.tfvars` (only +`terraform.tfvars.example`) and no `.tfstate` file anywhere in the repo or working tree — +`tofu apply` has never been run against real infrastructure. `infra/ansible/group_vars/ +production.yml` still holds literal, unfilled placeholders: `platform_ip: +__PLATFORM_VM_IP__`, `lab_host_ip: __LAB_HOST_VM_IP__`, `proxmox_mgmt_cidr: +__PROXMOX_HOST_MGMT_CIDR__`. No SSH config, known-hosts entry, or inventory file on this +workstation references a real `rn-platform-01`/`rn-lab-host-01`/Proxmox host. + +**The dedicated RootNode Academy production server has not been purchased or provisioned.** +Per Phase 10's own explicit instruction, this phase does not fake a production deployment +against placeholder infrastructure. Everything below is preparation, validation, and rehearsal — +genuinely useful the moment real hardware exists, but none of it is a live deployment. + +## What WAS done this phase (all safe, all pre-hardware) + +- **Gate 4 — IaC validated**: `tofu validate` (OpenTofu, the Terraform-compatible tool actually + installed) passes cleanly against `infra/terraform/`'s existing Proxmox provider config — no + redesign, the existing config is syntactically sound and ready for real `terraform.tfvars`. + `ansible-playbook site.yml --syntax-check -i inventory` passes cleanly against `infra/ + ansible/`'s existing `site.yml` and its `firewall`/`docker-tls`/`platform`/`lab-host`/ + `monitoring-agent`/`backup`/`common` roles (all present from Phase 4/4.1, unmodified). +- **Gate 5 — network security architecture reviewed, unchanged**: RFC1918 egress + denial/allowlisting present in `platform/provisioner/src/egress.js`/`lifecycle.js` (Provisioner + owns lab-container network policy); mTLS wiring present in `infra/ansible/roles/docker-tls/` + and referenced by `gateway.env.j2`; the `firewall` Ansible role exists for both host tiers. All + of this is existing Phase 4/4.1 work, re-confirmed present and un-redesigned — none of it can + be verified AGAINST a real host without one existing. +- **Gate 6 — DNS/HTTPS audited (read-only)**: `rootnodeacademy.com` **is already live** — + resolves via Cloudflare (`172.67.141.39`, `104.21.38.247`), serves a real HTTPS site (`HTTP/2 + 200`, Next.js application headers), and its mail is already routed to Google + (`smtp.google.com`), consistent with the existing `academy@rootnodeacademy.com` Google + Workspace setup. This is the separate, not-RootNode-owned marketing website (`docs/ + WEBSITE_REGISTRATION_AND_CHECKOUT.md`'s explicit boundary) — nothing here was touched, and no + DNS record was added or changed. **No subdomain or DNS record exists yet for the Platform API + itself** (e.g. `api.rootnodeacademy.com` or similar) — that's the piece blocked on real + server hardware. +- **Gate 7 — database productionization reviewed**: migrations (37, through Phase 9) verified + idempotent again this phase (a fresh `down -v`/`up` cycle and a same-database container + restart both apply/re-apply cleanly with zero errors — see `docs/ + PHASE10_LAUNCH_READINESS.md`'s regression section). The Ansible `platform` role's + `postgres.env.j2`/`.template` already model least-privilege connection strings + (`rootnode_platform` app user, not a superuser). No student records of any kind exist to + accidentally copy into a production database — this dev/test Postgres only ever holds + disposable test fixtures, all cleaned up after each rehearsal. +- **Gate 8 — Stripe LIVE**: explicitly **not configured, not attempted**. See "Stripe LIVE + status" below. +- **Gate 9 — Google Workspace**: `GOOGLE_SERVICE_ACCOUNT_JSON`/`GOOGLE_DELEGATED_USER` already + exist as real, previously-validated local values (Phase 5C's own `docs/ + RUNBOOK_GOOGLE_WORKSPACE_EMAIL.md` documents that validation) — `academy@rootnodeacademy.com`, + `gmail.send` scope only, domain-wide delegation. Not re-sent this phase (no new test email was + needed to confirm what Phase 5C already proved, and Phase 10 explicitly says not to make GWS a + blocker for Stripe/launch work). Production DEPLOYMENT of this config is blocked on the same + missing server as everything else. +- **Gates 15/16 — observability and backups reviewed, unchanged**: `/healthz` (Platform API, + Gateway, Provisioner) and `/metrics` (Prometheus text exposition, low-cardinality labels only — + reconfirmed by this phase's own audit in `docs/PHASE10_SECURITY_AUDIT.md`) already exist and + were exercised repeatedly by every regression suite rerun this phase. The `monitoring-agent` + and `backup` Ansible roles exist (`infra/ansible/roles/monitoring-agent/`, `.../backup/`, + including a templated `run-backup.sh.j2`) — built in Phase 4, never yet run against a real + host. **A restore has never been tested against real infrastructure, because none exists** — + this phase cannot manufacture that proof; it is not sufficient for launch per Phase 10's own + "a backup that has never been restore-tested is not sufficient" rule, and is listed as a real + blocker in the launch checklist, not glossed over. + +## Stripe LIVE status + +**Not configured.** No live Stripe credential was provided to this session, and this phase did +not request one — activating Stripe live mode requires Chase's own Stripe account +business/identity verification (a real KYC-style process only Chase can complete), which is an +explicit Phase 10 STOP condition ("identity verification requires Chase"). Phase 9.1's real +TEST-mode validation (`docs/PHASE9_1_STRIPE_TEST_VALIDATION.md`) remains the full proof of the +application-layer correctness; going live only needs Chase's real credentials plugged into the +same, already-proven code path — see `docs/STRIPE_INTEGRATION_RUNBOOK.md`'s "Going live" +section, unchanged by this phase. + +## Bottom line + +**PRODUCTION DEPLOYMENT BLOCKED — SERVER REQUIRED.** Every gate that depends on real hardware +(4, 5's live verification, 6's platform subdomain, 7's live database, 8, 9's live deployment, 15, +16) is prepared, validated, and ready to execute the moment `rn-platform-01`/`rn-lab-host-01` +exist on real Proxmox hardware and `terraform.tfvars`/`group_vars/production.yml` are filled +with real values — none of it requires new code or a redesign. diff --git a/docs/PHASE10_SECURITY_AUDIT.md b/docs/PHASE10_SECURITY_AUDIT.md new file mode 100644 index 0000000..0436766 --- /dev/null +++ b/docs/PHASE10_SECURITY_AUDIT.md @@ -0,0 +1,44 @@ +# Phase 10 — Security Audit + +Gate 18. A production-oriented review of RootNode Academy's security posture, drawing on every +regression suite rerun this phase (`docs/PHASE10_LAUNCH_READINESS.md`) plus direct code review. +No genuine launch-critical defect was found this phase — the one real defect Phase 10's +predecessor phases exposed (Phase 9.1's webhook-correlation bug) was already fixed and +regression-tested before this phase began. + +| Area | Status | Evidence | +|---|---|---| +| Student authentication | ✅ Verified | `e2e-phase5b.js`/`e2e-phase5c.js` rerun clean this phase — activation, single-use tokens, expiration, password reset with anti-enumeration, session revocation on reset. | +| Instructor authentication | ✅ Verified | Same suites — instructor login, RBAC roles (owner/admin/instructor). | +| Sessions | ✅ Verified | Persistent, server-controlled sessions (Phase 3A); a real Platform API restart preserves sessions/enrollment state (rerun clean this phase). | +| Password reset / activation | ✅ Verified | Single-use, server-side expiration, no plaintext token ever returned by any admin API (`toAdminView` never includes it). | +| RBAC | ✅ Verified | Admin/owner-only routes (guardian detail, payment data) vs. any-instructor routes (offering/location lists) — reconfirmed across Phase 7/8/9 suites this phase. | +| Cohort scoping | ✅ Verified | An instructor sharing a cohort can manage; an unrelated instructor cannot (Phase 5B/7 suites). | +| CSRF | ✅ Present, unchanged | `platform/api/src/security.js` + per-route session/cookie checks (student/instructor auth routes) — not modified this phase, no regression. | +| Rate limiting / lockout | ✅ Present, unchanged | Login-attempt tracking in `security.js` (matches the existing sweeper pattern also used for session expiry). | +| Audit logging | ✅ Verified | Every admin/integration action recorded; audit-hygiene test (no secrets/PII in audit metadata) rerun clean in `e2e-phase9.js`. | +| Server-authoritative pricing | ✅ Verified, real Stripe | Phase 9.1 proved a tampered caller amount is ignored against **Stripe's own recorded object**, not just RootNode's database. | +| Agreement enforcement | ✅ Verified | Checkout blocked (`409`) until acceptance, in both the deterministic suite and real Stripe TEST mode. | +| Capacity enforcement | ✅ Verified | Race-safe seat reservation (`reserved_seats < capacity` in one UPDATE) — last-seat race test rerun clean this phase. | +| No card-data storage | ✅ Verified | `payment_transactions` schema has no card fields; explicit admin-view test asserts none appear; Stripe's hosted Checkout is the only thing that ever sees a real card. | +| Webhook signature verification | ✅ Verified, real Stripe | Invalid/forged signatures rejected (both deterministic and real-Stripe-signed tests); idempotent via a real DB primary key on the provider's event id, reconfirmed with a genuinely re-signed real Stripe event in Phase 9.1. | +| Logging/audit redaction | ✅ Verified | This session's own Stripe secret/webhook-secret values were grepped across every log/scratch file produced — none found; `e2e-phase9.js`'s hygiene test asserts no `sk_`/`whsec_`/card-number pattern in audit metadata or metrics text. | +| Secrets never committed | ✅ Verified | Full-history pickaxe search this phase — clean (`docs/PHASE10_CANONICAL_STATE_AUDIT.md` §1.6). | +| mTLS (Provisioner ↔ Docker, Gateway ↔ Lab Host Router) | ✅ Present, unchanged, not re-verified live | Ansible `docker-tls` role and TLS env vars exist; cannot be exercised against a real mTLS handshake without real hardware (Gate 4/5's hardware blocker). | +| Privileged lab boundary / RFC1918 egress denial | ✅ Present, unchanged | `provisioner/src/egress.js`; this phase reconfirmed the module exists and is wired, did not re-run Phase 1/2's own dedicated isolation drills (out of this phase's changed-code scope — nothing touched networking code). | +| Production cookie settings (`COOKIE_SECURE`) | ✅ Present, verified pattern | `e2e-phase5c.js`'s own test explicitly proves `COOKIE_SECURE=1` production gating via a real container recreate — reconfirmed present, not re-executed as a fresh drill this phase since nothing in that code path changed. | +| HTTPS end-to-end | ⏸ Cannot verify — no production host | `COOKIE_SECURE=1`/`TRUST_PROXY=1` require a real HTTPS-terminating reverse proxy in front of a real Gateway, which doesn't exist yet (Gate 4-6's hardware blocker). The marketing site at `rootnodeacademy.com` IS real HTTPS (Cloudflare) — the Platform API itself has no production endpoint yet. | + +## Findings requiring action + +**None launch-critical within existing architecture.** Everything marked ⏸ above is blocked on +the same missing production server documented in `docs/PHASE10_PRODUCTION_DEPLOYMENT.md` — not +a code defect, and building around it (e.g. faking an HTTPS check) would violate this phase's +own "do not fake production readiness" instruction. + +## Scope discipline + +This audit reviewed and reconfirmed existing security properties; it did not expand into new +security tooling, new scanning infrastructure, or unrelated hardening work not already part of +Phase 1-9's architecture, per Phase 10's own "do not expand into unrelated feature work" +instruction. diff --git a/docs/SECRETS_AND_INFISICAL.md b/docs/SECRETS_AND_INFISICAL.md new file mode 100644 index 0000000..6df7fef --- /dev/null +++ b/docs/SECRETS_AND_INFISICAL.md @@ -0,0 +1,87 @@ +# Secrets & Infisical + +Gate 3. The full, real secret inventory — derived by grepping every `process.env.*` reference +across `platform/api/src`, `platform/gateway/src`, `platform/provisioner/src`, and +`platform/lab-router/src` (not guessed, not copied from a template list) — and the current +status of centralizing it in Infisical. **This document never contains a secret value.** + +## Infisical project status — action required + +Chase's self-hosted instance is `infisical.cedshomelab.com` (confirmed reachable, confirmed NOT +`app.infisical.com` — the earlier Phase 9.1 attempt failed for exactly this reason: the existing +machine identity is valid for the self-hosted instance only). Authenticated successfully this +phase using the machine identity at `~/.config/infisical/identity.env` +(`universal-auth`, org "Ceds HomeLab"). + +**No "RootNode Academy" project exists yet** in that organization — verified via `infisical +init`'s own project picker, which lists exactly six existing projects: Core Infrastructure, +Hermes Fleet, Kubernetes, Media Platform, Monitoring, Network Service. This contradicts Phase +10's own authorization text ("Chase has ALREADY created RootNode Academy"). The Infisical CLI +has no non-interactive project-creation command — creating one requires the web dashboard +(`https://infisical.cedshomelab.com`), which this session cannot reach (no browser). + +**Owner action required:** create a "RootNode Academy" project in the Infisical dashboard, with +three environments (**Development**, **Staging**, **Production** — matching this document's own +categorization below), and tell this session (or a future one) its project id so the secret +inventory below can actually be pushed. Until then, every secret continues to live only in local, +gitignored `.env` files (development) or has no home at all (staging/production don't exist yet +— see `docs/PHASE10_PRODUCTION_DEPLOYMENT.md`). + +## Secret inventory (real, from code — not from this prompt's guessed category list) + +Every environment variable the application code actually reads, categorized. "Secret" = must +never appear in git, logs, or a browser; "Config" = fine to template/document in plain text +(already handled by `infra/environments/*/*.env.template`, unchanged by this phase). + +| Variable | Category | Purpose | Consuming service | Environments | Rotation | +|---|---|---|---|---|---| +| `PLATFORM_DB_URL` | **Secret** (contains DB password) | Postgres connection string | Platform API | Dev/Staging/Prod (per-env DB, never shared) | On suspected compromise; routine rotation not yet scheduled | +| `PROVISIONER_SHARED_SECRET` | **Secret** | Platform API ↔ Provisioner trust boundary (`X-Provisioner-Key`) | Platform API, Provisioner | Dev/Staging/Prod (distinct per env) | On suspected compromise | +| `INTEGRATION_SHARED_SECRET` | **Secret** | Website backend ↔ Platform API trust boundary (`X-RootNode-Integration-Key`) | Platform API, website's backend (external) | Dev/Staging/Prod | On suspected compromise; the website team needs the current value out-of-band | +| `GOOGLE_SERVICE_ACCOUNT_JSON` | **Secret** | Full GWS service-account credential (Gmail API, domain-wide delegation) | Platform API (`email/gws-provider.js`) | Prod only (dev/staging use the deterministic dev provider by default) | Per Google Cloud's own service-account key rotation practice | +| `GOOGLE_DELEGATED_USER` | Config (an email address, not a secret) | Which real mailbox the service account impersonates via domain-wide delegation | Platform API | Prod | N/A | +| `EMAIL_FROM_ADDRESS` / `EMAIL_FROM_NAME` / `EMAIL_REPLY_TO` | Config | Sender identity — `academy@rootnodeacademy.com` | Platform API | All | N/A | +| `STRIPE_SECRET_KEY` | **Secret** | Stripe API authentication (checkout creation, refunds, retrieval) | Platform API (`payment-provider.js`) | Dev/Staging = TEST key; Production = LIVE key — **never mixed**, see Gate 8 status below | Per Stripe's own key-rotation guidance; rotate immediately if ever exposed | +| `STRIPE_WEBHOOK_SECRET` | **Secret** | Verifies a webhook delivery actually came from Stripe | Platform API (`webhook-routes.js`) | One per registered webhook endpoint (TEST vs. LIVE endpoints each have their own) | Regenerates automatically if the endpoint is recreated | +| `TEST_PAYMENT_WEBHOOK_SECRET` | Low-sensitivity (dev-only, never real money) | Shared secret for the deterministic `TestProvider`'s fake webhook signature | Platform API | Dev/test only | N/A | +| `LAB_ROUTER_TLS_KEY` | **Secret** | Lab Host Router's mTLS private key | Lab Host Router | Staging/Prod (dev uses a self-signed pair generated locally) | Per the mTLS cert lifecycle already documented in Phase 4.1 | +| `LAB_ROUTER_TLS_CERT` / `LAB_ROUTER_TLS_CA` | Sensitive but not strictly secret (public cert material) | mTLS certificate/CA chain | Lab Host Router, Gateway | Staging/Prod | Same lifecycle as the key above | +| `PLATFORM_DB_URL`'s host component, ports, timeouts, session TTLs, `PAYMENT_PROVIDER`, `COOKIE_SECURE`, `TRUST_PROXY`, `PUBLIC_APP_ORIGIN`, `DNS_RESOLVER`, `NO_PROXY_CIDRS`, etc. | Config | Plain operational configuration | Various | All, per-environment values | N/A | + +Every "Config" row is already handled the existing way — plain values in `infra/environments/ +*/*.env.template` (checked into git, safe) or `docker-compose.dev.yml`'s `${VAR:-default}` +substitutions. Only the "Secret" rows above need an Infisical home. + +## Environment separation (Stripe — the one category with real launch consequences) + +- **Development/Staging**: `STRIPE_SECRET_KEY`/`STRIPE_WEBHOOK_SECRET` must be **TEST** values + only (`sk_test_...`/a `stripe listen`-issued or TEST-mode dashboard `whsec_...`). This is what + Phase 9.1 validated end to end. +- **Production**: `STRIPE_SECRET_KEY`/`STRIPE_WEBHOOK_SECRET` must be **LIVE** values + (`sk_live_...`, and a permanent LIVE-mode dashboard webhook endpoint's own signing secret). + **Not configured — Chase has not provided live Stripe credentials, and this phase did not ask + for or receive them** (per Phase 10's own stop condition: "real money could move" / + "identity verification requires Chase" — live Stripe activation requires Chase's own Stripe + business/identity verification, not something this session can do on his behalf). See `docs/ + PHASE10_LAUNCH_READINESS.md` Gate 8 status. + +## Secret hygiene performed this phase + +- Confirmed (again, freshly) that no real secret value has ever entered git history (`docs/ + PHASE10_CANONICAL_STATE_AUDIT.md` §1.6). +- Confirmed `platform/.env`'s Stripe fields are correctly blank (not left set from Phase 9.1's + validation session) — matches this repo's own "unset after a deliberate session" convention. +- **Not yet performed** (blocked on the missing Infisical project): migrating any real secret + value out of local `.env` files and into Infisical. Per this phase's own instruction, a local + credential is never deleted until its Infisical replacement is verified — nothing was deleted. + +## What Chase needs to do + +1. Create the "RootNode Academy" project in the Infisical dashboard + (`https://infisical.cedshomelab.com`) with Development/Staging/Production environments. +2. Share the project id (or grant the existing machine identity access to it) so secrets can + actually be pushed. +3. Once that exists, this session (or the next one) can populate every "Secret" row above from + the current local `platform/.env`/`platform/provisioner/.env` values for Development, and + Chase provides the real Staging/Production values (Stripe LIVE keys especially) directly when + ready for those environments — never invented or guessed by this phase. diff --git a/docs/WINTER_BREAK_LAUNCH_CHECKLIST.md b/docs/WINTER_BREAK_LAUNCH_CHECKLIST.md index 5bc34d4..2a6dba8 100644 --- a/docs/WINTER_BREAK_LAUNCH_CHECKLIST.md +++ b/docs/WINTER_BREAK_LAUNCH_CHECKLIST.md @@ -1,89 +1,101 @@ # Winter Break Camp — Launch Checklist The single go/no-go list for opening the Winter Break Linux & Systems Foundations Camp -(`docs/WINTER_BREAK_CAMP_CURRICULUM_OPTIONS.md` Option A) to real public registration. Combines -`docs/WINTER_BREAK_CAMP_OPERATIONS.md`'s operational sequence with Phase 9's commerce -requirements into one checklist Chase can walk top to bottom. Nothing on this list is invented by -the platform — every unchecked item is a real decision or asset that has to come from Chase, the -legal reviewer, or Stripe, not from more code. +(`docs/WINTER_BREAK_CAMP_CURRICULUM_OPTIONS.md` Option A) to real public registration. Every +item is tagged **COMPLETE**, **BLOCKED**, **DECISION REQUIRED**, or **MANUAL OWNER ACTION** — +see `docs/PHASE10_LAUNCH_READINESS.md` for the full evidence matrix behind each tag. Nothing on +this list is invented by the platform — every non-COMPLETE item is a real decision, asset, or +piece of hardware that has to come from Chase, a legal reviewer, or Stripe, not from more code. -## Platform readiness — done, verified, ready whenever the business inputs below land - -- [x] Offering, capacity, registration lifecycle, agreements — built and tested (Phase 7/8). -- [x] Offering pricing field, payment-provider abstraction, hosted checkout, verified webhook, - refund recording, abandoned-reservation reaper — built and tested against the deterministic - test provider (Phase 9). -- [x] Website registration + checkout + agreement + status-lookup contract — documented and - tested (`docs/WEBSITE_REGISTRATION_AND_CHECKOUT.md`). -- [x] Admin UI for offering pricing and per-registration payment/refund status. -- [x] `GET /api/admin/offerings/:id/readiness` reflects real configuration state, including the - new `price` and `payment_provider` checks. -- [x] Full Winter Break commerce rehearsal green end to end against a real, freshly-built stack - (`infra/scripts/drills/test-phase9-winter-camp-commerce.js`, 13/13): admin configuration, - minor and adult registration, checkout, payment success, webhook, enrollment activation, a - real classroom session, a real Lab task, failed payment, duplicate webhook, abandoned - checkout, and a last-seat race. - -## Business decisions — Chase's call, not invented by this phase - -- [ ] Real camp dates and duration. -- [ ] Real venue/location (or confirm fully online/remote). -- [ ] Real capacity (seat count). -- [x] Price and currency — **$399.00 USD launch tuition ("Founding Cohort"), $449.00 standard - retail, paid in full, no deposit/payment plan** — founder-approved per Chase directly - (Phase 9.1). Validated against real Stripe TEST mode at this exact amount. Not yet - reflected in a committed pricing document in this canonical repo (referenced but not found - — see `docs/PHASE9_1_STRIPE_TEST_VALIDATION.md` §1/§33); set directly on the real Winter - Break offering via `PATCH /api/admin/offerings/:id` when ready to open registration. -- [ ] Whether registration opens ahead of, or alongside, a specific marketing push. -- [ ] Refund policy in plain terms (the platform can RECORD any refund decision; it does not - decide policy — see `docs/OFFERING_AND_REGISTRATION_MODEL.md`'s "Payment and the commerce - boundary" section for what "record, never auto-cascade" means operationally). - -## Legal review — required before any real family sees the agreement - -- [ ] The actual participation/media/liability agreement text, reviewed by a real attorney - (`agreement_definitions.body_summary` is currently a plain-language placeholder only — see - `docs/STUDENT_GUARDIAN_DATA_MODEL.md`'s legal-review list). -- [ ] Jurisdiction-specific minor-consent requirements confirmed. -- [ ] Data-retention/deletion policy decided and, if needed, reflected in the agreement text. -- [ ] A decision on whether emergency-contact fields need additional encryption at rest. - -## Payments - -- [x] **Stripe TEST-mode validation** — a real Stripe TEST Checkout Session, a real TEST payment - at the actual $399.00 founding-cohort price, a genuine signed webhook, the existing +- [x] **Canonical repo reconciled** — COMPLETE. `docs/PHASE10_CANONICAL_STATE_AUDIT.md`: this + workstation's HEAD, Gitea's `origin/main`, and every claimed milestone SHA all verified + consistent. No stranded branches/tags on the remote. +- [ ] **Second-PC state reconciled** — DECISION REQUIRED (verification only, not a code + blocker). Gitea shows no evidence of stranded work, but this session cannot see the other + workstation directly — run the commands in `docs/PHASE10_CANONICAL_STATE_AUDIT.md` §1.3 + there to positively confirm. +- [ ] **Secrets centralized (Infisical)** — MANUAL OWNER ACTION. No "RootNode Academy" project + exists yet in `infisical.cedshomelab.com` (verified, contradicts this being already + done) — create it with Development/Staging/Production environments. See `docs/ + SECRETS_AND_INFISICAL.md` for the full secret inventory ready to migrate in. +- [ ] **Production server** — BLOCKED. Not purchased/provisioned — verified via empty + `terraform.tfvars`/no state file and placeholder `group_vars/production.yml`. See `docs/ + PHASE10_PRODUCTION_DEPLOYMENT.md`. +- [ ] **Production VMs (rn-platform-01 / rn-lab-host-01)** — BLOCKED. Depends on the server above. +- [x] **Firewall/isolation (code)** — COMPLETE. `docs/PHASE10_SECURITY_AUDIT.md` — RFC1918 + egress denial, mTLS wiring, firewall Ansible role all present and unchanged from Phase 4/ + 4.1; cannot be verified against a live host until one exists. +- [ ] **Production database** — BLOCKED. Depends on the server above. Migrations themselves + reconfirmed idempotent this phase (37/37, both a fresh apply and a same-DB restart). +- [x] **DNS (marketing site)** / [ ] **DNS (Platform API)** — PARTIAL. `rootnodeacademy.com` is + live and real (Cloudflare-fronted, confirmed this phase); no subdomain exists yet for the + Platform API itself — needs a real server first. +- [x] **HTTPS (marketing site)** / [ ] **HTTPS (Platform API)** — PARTIAL, same split as DNS. +- [x] **Monitoring (code)** — COMPLETE. `/healthz`/`/metrics` live and exercised in every + regression run this phase; deployment itself needs the production server. +- [x] **Backups (code)** / [ ] **Backups (executed)** — PARTIAL. Ansible `backup` role exists, + never run against a real host. +- [ ] **Restore test** — BLOCKED. Never executed — no target exists. A backup that has never + been restore-tested does not count as launch-ready. +- [x] **Stripe TEST validation** — COMPLETE. A real Stripe TEST Checkout Session, a real TEST + payment at the actual $399.00 price, a genuine signed webhook, the existing registration→enrollment conversion, a real duplicate-delivery proof, a real automated abandoned-checkout proof, and a real refund with the non-cascade rule reconfirmed — all - proven end to end in Phase 9.1. One real defect (webhook-event correlation) was found and - fixed as a direct result. See `docs/PHASE9_1_STRIPE_TEST_VALIDATION.md` for the full - evidence and reconciliation IDs. **This is TEST mode only — it does not authorize or imply - LIVE-mode readiness**, see the unchecked items below. - -- [ ] Real Stripe account created/available (live mode). -- [ ] `STRIPE_SECRET_KEY`/`STRIPE_WEBHOOK_SECRET` (live values) provisioned into production - configuration only — see `docs/STRIPE_INTEGRATION_RUNBOOK.md`'s "Going live" section. -- [ ] At least one real, small, refundable test transaction run end to end in production before - opening registration publicly, then refunded. -- [ ] Recommended (not required — the test provider already validates the application logic): - one full rehearsal against Stripe's own TEST mode (fake cards, no real money) before - flipping to live credentials, per `docs/STRIPE_INTEGRATION_RUNBOOK.md`'s test-mode section. - -## Website (separate repo, not RootNode-owned) - -- [ ] rootnodeacademy.com's own registration/checkout form built against - `docs/WEBSITE_REGISTRATION_AND_CHECKOUT.md`. Not tracked or built in this repository — if - that repo becomes available to RootNode's own engineering effort, say so explicitly before - any code changes are made against it. -- [ ] The website's `INTEGRATION_SHARED_SECRET` provisioned into its OWN backend configuration - (never browser-reachable) — see `docs/WEBSITE_ENROLLMENT_INTEGRATION.md`'s "Production - secret handling." -- [ ] The website's success page built against the status-lookup polling pattern in - `docs/WEBSITE_REGISTRATION_AND_CHECKOUT.md` §6 — never derived from the redirect URL alone. + proven end to end in Phase 9.1 (`docs/PHASE9_1_STRIPE_TEST_VALIDATION.md`). One real + defect (webhook-event correlation) was found and fixed as a direct result. +- [ ] **Stripe LIVE configuration** — BLOCKED. Not configured, not attempted — requires Chase's + own Stripe account business/identity verification (only Chase can do this) plus real LIVE + credentials placed in Infisical (blocked on the missing project above), never invented or + substituted by this phase. +- [ ] **Live webhook configuration** — BLOCKED. Depends on LIVE credentials and a real HTTPS + Platform API endpoint, neither of which exist yet. +- [x] **Google Workspace** — COMPLETE. `academy@rootnodeacademy.com`, `gmail.send` scope, + domain-wide delegation — validated in Phase 5C (`docs/ + RUNBOOK_GOOGLE_WORKSPACE_EMAIL.md`); deployment to production needs the real server. +- [x] **Website integration** — COMPLETE. Full contract tested end to end, including against + real Stripe. The website itself (separate repo, not RootNode-owned) is live and real; + this phase did not access or modify it. +- [x] **Winter Break price** — COMPLETE. **$399.00 launch ("Founding Cohort") / $449.00 + standard reference, paid in full, no deposit, no payment plan** — founder-approved by + Chase directly, validated against real Stripe TEST mode at this exact amount. +- [ ] **Winter Break dates** — DECISION REQUIRED. Chase's call, not invented. +- [ ] **Venue** — DECISION REQUIRED. Chase's call (or confirm fully online/remote), not invented. +- [ ] **Capacity** — DECISION REQUIRED. Chase's call, not invented. +- [ ] **Agreements (legal text)** — BLOCKED. `agreement_definitions.body_summary` remains a + plain-language placeholder; no attorney-approved text exists for any required category + (program terms, guardian consent, participation waiver, acceptable-use/lab rules, + cancellation acknowledgment, privacy notice). See `docs/PHASE10_LAUNCH_READINESS.md` Gate + 12 for the full taxonomy — none of it was authored or treated as approved by this phase. +- [ ] **Refund/cancellation policy** — DECISION REQUIRED. The platform's mechanical behavior + (record, never auto-cascade into registration/enrollment status) is real and proven + against actual Stripe money movement; the customer-facing POLICY text is Chase's decision. +- [ ] **Tax decision** — DECISION REQUIRED. No tax treatment is configured anywhere in this + codebase (Phase 9's own explicit "no complex tax automation" boundary) — this is not an + implicit "no tax owed" determination, it requires Chase or a professional to actually + decide. +- [ ] **Production offering** — BLOCKED. Cannot legitimately open registration until dates/ + venue/capacity/agreements/refund policy/tax are all resolved — the mechanism itself + (`PATCH /api/admin/offerings/:id`, transition to `enrollment_open`) is ready the moment + they are. +- [ ] **Instructor assignment** — MANUAL OWNER ACTION. Mechanism ready + (`instructor_offerings`); Chase assigns the real instructor once the real offering exists. +- [x] **Roster process** — COMPLETE. Rehearsed end to end (classroom session, real Lab task, + instructor visibility) in this phase's dress rehearsal, 13/13. +- [x] **Student activation** — COMPLETE. Proven real twice — Phase 9.1's real-Stripe run and + this phase's deterministic rehearsal. +- [x] **Lab capacity (mechanism)** / [ ] **Lab capacity (real sizing)** — PARTIAL. Race-safe + capacity logic is proven; real-world capacity planning for actual concurrent students + needs the real server's actual specs, unknown until Gate "production server" resolves. +- [x] **Complete dress rehearsal** — COMPLETE (deterministic). 13/13 this phase, against + current `main`. Cannot yet be repeated against the REAL Winter Break offering because that + offering cannot legitimately open (dates/venue/capacity/agreements/policy/tax all pending). ## Final go/no-go -Registration should not open to the public until every box above is checked. The platform side -(first section) has been true since this phase shipped; everything else is a business, legal, or -external-team dependency this phase deliberately did not — and should not — invent on Chase's -behalf. +## PHASE 10 PARTIAL / WINTER BREAK LAUNCH BLOCKED + +Registration must not open to the public until every BLOCKED and DECISION REQUIRED item above +is resolved. Every COMPLETE item has real evidence behind it, not a self-assessment — see +`docs/PHASE10_LAUNCH_READINESS.md`'s full matrix. **Even once every gate above passes, this +platform will not open registration on its own — that action requires Chase's explicit, +separate authorization, given directly, not inferred from a checklist being green.**