create-op-node 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,8 +13,8 @@ That's it. The wizard walks you through:
13
13
  2. **GitHub** — creates your region's node repo from the [`OpusPopuli/opuspopuli-node`](https://github.com/OpusPopuli/opuspopuli-node) template via the GitHub API. Seeds the 5 required GitHub Secrets (Cloudflare token, account ID, zone ID, Terraform Cloud token, TFC org) for you.
14
14
  3. **Terraform Cloud** — verifies your TFC token, prepares the workspace.
15
15
  4. **First PR** — writes `environments/prod.tfvars` from your answers, commits, opens the first PR. The node repo's `cloudflare-infra.yml` workflow runs `terraform plan` against the PR; on merge to `main` it applies — Tunnel, DNS, R2 buckets, and Pages project come up automatically.
16
- 5. **pgsodium master key** — generates a fresh 64-hex root key, stores it in 1Password (via the `op` CLI if available, otherwise prompts you to paste it once).
17
- 6. **Tunnel token retrieval** — after `terraform apply` lands, fetches the Tunnel token from Terraform Cloud outputs and stores it in 1Password alongside the pgsodium key.
16
+ 5. **pgsodium master key** — generates a fresh 64-hex root key, stores it in your **macOS login Keychain** as `org.opuspopuli.<region>/pgsodium-root-key`. No third-party password manager required.
17
+ 6. **Tunnel token retrieval** — after `terraform apply` lands, fetches the Tunnel token from Terraform Cloud outputs and stores it alongside the pgsodium key in your Keychain.
18
18
 
19
19
  Then on the Mac Studio itself:
20
20
 
@@ -22,7 +22,50 @@ Then on the Mac Studio itself:
22
22
  npx create-op-node bootstrap
23
23
  ```
24
24
 
25
- Configures macOS power settings, installs Homebrew + the CLI tool list, sets up Docker Desktop + Tailscale + Ollama, clones the node repo you created, materializes the pgsodium key from 1Password, writes the LaunchAgent plist, logs into ghcr.io, pulls + warms the LLM model, and finally `docker compose pull && up -d` brings the whole stack online. Health-check loop waits until all 10 containers are `(healthy)`.
25
+ Configures macOS power settings, installs Homebrew + the CLI tool list, sets up Docker Desktop + Tailscale + Ollama, clones the node repo you created, reads the pgsodium key + Tunnel token from the Studio's Keychain (or prompts you to paste them once, then persists for re-runs), writes the LaunchAgent plist, logs into ghcr.io, pulls + warms the LLM model, and finally `docker compose --profile public pull && up -d` brings the whole stack online. Health-check loop waits until all containers are `(healthy)`.
26
+
27
+ ### Local-only mode (no Cloudflare)
28
+
29
+ ```bash
30
+ npx create-op-node bootstrap --region us-ca --local-only
31
+ ```
32
+
33
+ Brings the Studio up for local dev / testing — frontend on your laptop
34
+ hits the Studio over Tailscale, no public exposure. Differences from
35
+ the standard run:
36
+
37
+ - **No Tunnel token required.** `init` is unnecessary; if the pgsodium
38
+ key isn't in Keychain, bootstrap generates one inline and persists it.
39
+ - **`cloudflared` stays down.** It's gated behind the `public` compose
40
+ profile, which `--local-only` doesn't activate. Bootstrap also evicts
41
+ any leftover cloudflared from a prior public run so it doesn't strand
42
+ in `compose ps`.
43
+ - **Backup stack skipped by default.** `docker-compose-backup.yml`
44
+ isn't loaded; pass `--compose-file docker-compose-backup.yml` to
45
+ include it explicitly.
46
+ - **LaunchAgent omits `TUNNEL_TOKEN`.** Only `PGSODIUM_ROOT_KEY` is
47
+ exported into the launchd session.
48
+ - **Outro tells you to use Tailscale**, not `npx create-op-node verify`.
49
+
50
+ When you're ready to go public, re-run `bootstrap` without `--local-only`
51
+ and the same Studio promotes to the full production-shaped deploy.
52
+
53
+ > **Template version**: this mode depends on the `opuspopuli-node`
54
+ > template having `profiles: [public]` on its cloudflared service. If
55
+ > you cloned the template before that landed, refresh your fork
56
+ > (or recreate from template) before using `--local-only` — otherwise
57
+ > cloudflared starts regardless and will restart-loop without a
58
+ > TUNNEL_TOKEN.
59
+
60
+ > **Secret transport between laptop and Studio**
61
+ >
62
+ > The macOS `security` CLI writes to the local login keychain — items
63
+ > don't sync to iCloud Keychain automatically. On the Studio's first
64
+ > bootstrap, the operator pastes the pgsodium key + Tunnel token once
65
+ > (from the laptop's Keychain Access, or wherever you copied them); the
66
+ > Studio bootstrap validates the format and persists locally so re-runs
67
+ > read straight through. Use AirDrop / `security find-generic-password`
68
+ > output / Tailscale `scp` to ferry the values.
26
69
 
27
70
  ## Resetting the Studio
28
71
 
@@ -52,7 +95,10 @@ with `bootstrap` without losing data.
52
95
  those need separate cleanup.
53
96
 
54
97
  Reset does **not** touch cloud-side state: the Cloudflare resources,
55
- the GitHub repo, the TFC workspace, and the 1Password items remain.
98
+ the GitHub repo, and the TFC workspace remain. Keychain items on the
99
+ Studio are also left in place — `security delete-generic-password -s
100
+ org.opuspopuli.<region> -a pgsodium-root-key` etc. if you want them
101
+ gone.
56
102
  `init` is idempotent against existing cloud setup, so re-running it
57
103
  won't duplicate anything.
58
104
 
@@ -280,13 +326,63 @@ rather than in the regions repo itself:
280
326
  green run here, as the real signal. If the two ever disagree, the schema
281
327
  wins and this command needs updating.
282
328
 
329
+ ## What lands where
330
+
331
+ `create-op-node` touches several secret stores. Here's the full map of
332
+ what we own (the two macOS Keychain items) vs. what we just route to its
333
+ destination.
334
+
335
+ ### Stored by `create-op-node` in macOS Keychain
336
+
337
+ Two items per region, both generic-password class
338
+ (`kSecClassGenericPassword`). Visible in **Keychain Access.app**
339
+ (`/System/Applications/Utilities/Keychain Access.app`) — **not** in the
340
+ new Passwords.app, which is filtered to website-login items only.
341
+
342
+ | # | Service | Account | Label (GUI display) | Value format | Written by | Read by |
343
+ |---|---|---|---|---|---|---|
344
+ | 1 | `org.opuspopuli.<region>` | `pgsodium-root-key` | `Opus Populi (<region>) — pgsodium root key` | 64 lowercase hex chars | `init` on laptop | `bootstrap` on Studio |
345
+ | 2 | `org.opuspopuli.<region>` | `tunnel-token` | `Opus Populi (<region>) — Cloudflare Tunnel token` | JWT-style base64url string | `init` on laptop (after TFC apply) | `bootstrap` on Studio |
346
+
347
+ Both items also carry `-D 'Opus Populi secret'` (the "Kind" column in
348
+ Keychain Access) so you can filter for them at a glance.
349
+
350
+ Inspect from a shell:
351
+
352
+ ```bash
353
+ # Metadata only (safe to share output):
354
+ security find-generic-password -s org.opuspopuli.us-ca -a pgsodium-root-key
355
+
356
+ # Reveal the value (you'll be prompted to allow access on first call):
357
+ security find-generic-password -s org.opuspopuli.us-ca -a pgsodium-root-key -w
358
+ ```
359
+
360
+ ### Stored elsewhere (we don't put these in Keychain)
361
+
362
+ Everything else flows through transiently or lives in its destination
363
+ system's own credential store.
364
+
365
+ | Secret | Where it lives | Why not in Keychain |
366
+ |---|---|---|
367
+ | Cloudflare API token | Pasted into `init` prompt → forwarded to **GitHub Secrets** + **Terraform Cloud** vars | One-shot during init. Re-runs prompt again. We could store it; adds risk vs benefit. |
368
+ | Cloudflare account ID, zone ID | Same as above | Not really a "secret" but flow alongside the token |
369
+ | Terraform Cloud token | Pasted, used to verify + poll runs | Same one-shot pattern |
370
+ | GitHub PAT | Read from `gh auth token` if available, else pasted | `gh` already manages it |
371
+ | pgsodium key (Studio runtime form) | `~/.config/opuspopuli/pgsodium_root_key` (mode `0400`) | LaunchAgent reads it at every login → interpolates into the `PGSODIUM_ROOT_KEY` env var. Same value as in Keychain; file is runtime form. |
372
+ | Cloudflare Tunnel token (Studio runtime form) | Baked into `~/Library/LaunchAgents/org.opuspopuli.envloader.plist` (mode `0600`) | launchd's `launchctl setenv TUNNEL_TOKEN` injects it into the session at every boot. Same value as in Keychain; plist is runtime form. |
373
+ | ghcr.io credentials | `~/.docker/config.json` or `docker-credential-osxkeychain` | Docker manages its own credential store — it actually saves the ghcr token to a separate Keychain item under service `ghcr.io`. We just call `docker login`. |
374
+
375
+ Per region, the **only** persistent secrets `create-op-node` owns are
376
+ the two Keychain items above. Everything else is either transient
377
+ (prompted, used, forgotten) or lives in its destination system.
378
+
283
379
  ## Why this exists
284
380
 
285
381
  Each Opus Populi region is operated independently by a local maintainer — its own Cloudflare account, its own Mac Studio, its own domain. The full bootstrap is a few hours of manual steps across Cloudflare, GitHub, Terraform Cloud, macOS Setup Assistant, Docker Desktop, Tailscale, Ollama, and the node's own Docker Compose stack. Doable from the runbook, but error-prone.
286
382
 
287
383
  This CLI exists to make that bootstrap **foolproof** — every prompt validates immediately, every secret is retrieved from a secure source (never echoed, never written to disk in plaintext), and every step has an explicit "what happens next" message. The goal is zero documentation reading required to get a node running.
288
384
 
289
- The CLI itself never holds any credentials beyond the scope of a single command — secrets flow from your 1Password vault → through the CLI → directly into the destination (GitHub Secrets, Terraform Cloud workspace variables, Mac Studio LaunchAgent). Nothing persists in this process.
385
+ The CLI itself never holds any credentials beyond the scope of a single command — secrets flow from your macOS Keychain → through the CLI → directly into the destination (GitHub Secrets, Terraform Cloud workspace variables, Mac Studio LaunchAgent). Nothing persists in this process.
290
386
 
291
387
  ## Architecture
292
388
 
@@ -322,9 +418,9 @@ The CLI itself never holds any credentials beyond the scope of a single command
322
418
 
323
419
  ## Status
324
420
 
325
- **`init` — fully wired.** Full Phase 1 of the runbook: prompts → Cloudflare 5-scope probe → Terraform Cloud verify → GitHub template clone → 5 repo secrets seeded → branch + prod.tfvars committed → PR opened → pgsodium key generated → (after operator merges PR) Terraform apply polled → Tunnel token retrieved + saved to 1Password.
421
+ **`init` — fully wired.** Full Phase 1 of the runbook: prompts → Cloudflare 5-scope probe → Terraform Cloud verify → GitHub template clone → 5 repo secrets seeded → branch + prod.tfvars committed → PR opened → pgsodium key generated → (after operator merges PR) Terraform apply polled → Tunnel token retrieved + saved to the macOS Keychain.
326
422
 
327
- **`bootstrap` — fully wired.** Phase 2 on the Mac Studio: macOS sanity (auto-restart, disk sleep), Homebrew + tool installs (gh, pnpm, jq, cloudflared, rclone, ollama, docker, tailscale), GitHub + Tailscale signin prompts, pgsodium key + Tunnel token read from 1Password, LaunchAgent written + loaded, ghcr.io login, Ollama models pulled + warmed, region repo located or cloned, `docker compose pull && up -d`, health-check loop until everything reports `(healthy)`.
423
+ **`bootstrap` — fully wired.** Phase 2 on the Mac Studio: macOS sanity (auto-restart, disk sleep), Homebrew + tool installs (gh, pnpm, jq, cloudflared, rclone, ollama, docker, tailscale), GitHub + Tailscale signin prompts, pgsodium key + Tunnel token read from the Studio's Keychain (or pasted in once if first run on that machine, then persisted), LaunchAgent written + loaded, ghcr.io login, Ollama models pulled + warmed, region repo located or cloned, `docker compose pull && up -d`, health-check loop until everything reports `(healthy)`.
328
424
 
329
425
  **`verify` — scaffold stub.** Type-safe argument parsing only; prints a roadmap-style message and exits.
330
426