Skip to content

feat(deploy): production hardening — Secret Manager mode, B9 import script, prod runbook (S6)#304

Merged
oratis merged 1 commit into
mainfrom
feat/s6-prod-hardening
Jul 25, 2026
Merged

feat(deploy): production hardening — Secret Manager mode, B9 import script, prod runbook (S6)#304
oratis merged 1 commit into
mainfrom
feat/s6-prod-hardening

Conversation

@oratis

@oratis oratis commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Part of PLAN_WEB_SIGNUP_v1.0 — milestone S6, the hard gate before public signups. Stacked on #302 (S4) → #301#299#297.

What

SECRETS_MODE=sm for deploy.sh

The deploy script passed every secret (LLM keys, Stripe keys, the web token…) as plain --set-env-vars, visible to anyone with console read access. In sm mode those values are now pushed to Secret Manager (secret created if absent, new version per deploy, runtime SA granted accessor) and reach the container via --set-secrets. Default env mode keeps today's demo behavior byte-identical. All plumbing routes through one addenv helper — verified with a bash unit test of the routing and bash -n.

The missing B9 import script

RUNBOOK_ACCOUNTS_LAUNCH's Firestore section literally said "问我要导入脚本" — flipping LISA_FIRESTORE=1 on an empty database would orphan every existing account. scripts/import-accounts-firestore.ts imports the three file-backed stores into the exact docs the runtime seam reads:

  • accounts.jsonlisa-global/accounts {list}
  • users/<uid>/billing/balance.jsonlisa-balances/<uid>
  • iap-transactions.jsonlisa-txindex/<txId> (create-only, so replays are naturally deduped)

Idempotent; --dry-run / --force. firestore.ts gains a LISA_FIRESTORE_TOKEN override so admin tooling can authenticate with gcloud auth print-access-token outside Cloud Run (no metadata server there).

docs/RUNBOOK_CLOUD_PROD.md

The operator checklist from reviewer-demo to public signup: dedicated prod project → Firestore create + import → SECRETS_MODE=sm LISA_FIRESTORE=1 MAX_INSTANCES=3 deploy → third-party console table (Google OAuth client, Apple Services ID, Turnstile widget, Resend domain, Stripe webhook) → cloud.meetlisa.ai domain mapping (DNS-only/grey-cloud, TLS by Google) → Cloud Scheduler sweep job → uptime check + budget alerts + the built-in anomaly log alert → three kill switches → a launch smoke checklist. Every credential-gated step is flagged for the owner.

Tests

npm test: 1324 pass / 0 fail; bash -n + an addenv routing check on deploy.sh.

🤖 Generated with Claude Code

@oratis

oratis commented Jul 25, 2026

Copy link
Copy Markdown
Owner Author

Review — request changes 🔴 (adversarial pass)

Secret Manager routing is right (every sensitive var covered, pushed via stdin not argv, removed from --set-env-vars so gcloud run describe shows only secretKeyRefs, no set -x/echo $SECRET; session secret is stable across deploys — sessions survive the cutover). The tx-index migration is idempotent/replay-safe and account fields round-trip losslessly. But the account/balance migration + runbook have real hazards:

MUST-FIX 1 — a malformed/missing accounts.json silently writes {list:[]} → every account orphaned. readJson swallows all errors → null → coerced to []; on a first import the else-branch writes an empty list, then the runbook flips LISA_FIRESTORE=1 and everyone is locked out. The runbook's own gcloud storage rsync is exactly what can half-populate the local dir. Fix I'll push: distinguish parse-failure from a genuinely-empty file and abort; refuse a zero-length accounts write without an explicit --allow-empty.

MUST-FIX 2 — --force clobbers prod accounts + balances (money) with the stale snapshot, no CAS/backup/confirm. setDoc with no updateTime precondition = full-document overwrite. A --force re-run post-cutover erases every account created after the snapshot and rolls paidMicroUSD back to snapshot values. Fix I'll push: CAS --force on the updateTime read (refuse if prod changed), snapshot-first + typed confirmation.

MUST-FIX 3 — runbook sweep-token wiring is broken. LISA_SWEEP_TOKEN="$(openssl rand -hex 24)" is a command-scoped prefix, never exported → the Scheduler Authorization: Bearer $LISA_SWEEP_TOKEN expands empty → 401 every tick → the S4 autonomy sweep never runs; and it rotates each deploy. Fix I'll push: generate once, persist + reuse.

Non-blocking but real: deploy.sh prints ?token=$LISA_WEB_TOKEN (the master bearer) to stdout even in sm mode → the master key lands in CI/terminal logs, undercutting the whole PR; I'll gate that echo to env mode. Also: no prod-target guardrail (defaults to the shared personal project + SECRETS_MODE=env), session HMAC key lives in the GCS bucket (bucket-read = session forgery).

@oratis
oratis force-pushed the feat/s4-peruid-autonomy branch from 46f291a to 37b0104 Compare July 25, 2026 05:20
…cript, prod runbook (S6)

The gate before public signups (PLAN_WEB_SIGNUP S6):

- deploy.sh SECRETS_MODE=sm: sensitive values (tokens, LLM/Stripe/Resend
  keys, Turnstile secret, reviewer seed) go to Secret Manager — created/
  versioned idempotently per deploy, granted to the runtime SA, delivered
  via --set-secrets — instead of console-visible plain env vars. Default
  'env' mode keeps demo behavior byte-identical. All env plumbing now
  routes through one addenv helper.
- scripts/import-accounts-firestore.ts: the missing B9 cutover piece —
  imports accounts.json, per-uid balances, and the IAP tx index into the
  exact Firestore docs the runtime reads (lisa-global/accounts,
  lisa-balances/<uid>, lisa-txindex/<tx> create-only). Idempotent, with
  --dry-run/--force. firestore.ts gains LISA_FIRESTORE_TOKEN so admin
  tooling can run outside Cloud Run (no metadata server).
- docs/RUNBOOK_CLOUD_PROD.md: the operator checklist from demo to public
  signup — prod project, Firestore create+import, sm-mode deploy, the
  third-party console table (Google OAuth, Apple SID, Turnstile, Resend,
  Stripe webhook), domain mapping (DNS-only), Scheduler sweep job,
  uptime/budget/anomaly alerts, three kill switches, smoke checklist.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@oratis
oratis force-pushed the feat/s6-prod-hardening branch from dd25710 to ad785ba Compare July 25, 2026 05:27
@oratis
oratis changed the base branch from feat/s4-peruid-autonomy to main July 25, 2026 05:27
@oratis

oratis commented Jul 25, 2026

Copy link
Copy Markdown
Owner Author

Adversarial review + rebase onto main

Rebased S6's commit onto current main (S3+S4 merged). One deploy.sh conflict: S6 refactors the env wiring into an addenv helper (with Secret Manager routing) while main carries the A-series Google vars inline — resolved to S6's addenv style + the A-series LISA_GOOGLE_{WEB,IOS}_CLIENT_ID, dropping S1's dead LISA_CLOUD_GOOGLE_* (0 readers). Verified the merged script has every var (A-series Google, S3 Turnstile/blocklist, S4 sweep, billing) once, no duplicates. bash -n clean; typecheck clean; full suite 1383 pass / 0 fail.

Findings & resolutions

1. Import script — empty source wipes Firestore (data loss, high). scripts/import-accounts-firestore.ts reads accounts.json as … ?? [], so a wrong/missing home dir (or an unmounted GCS bucket) yields [] — and with --force it would setDoc("lisa-global/accounts", { list: [] }), erasing every account. Added an empty-guard: accounts.length === 0 ⇒ skip with a warning (an empty source is a misinvocation, not an import). The existing-doc guard + --force semantics are unchanged for real data.

2. Runbook — sweep token never reaches the scheduler. §2 generated LISA_SWEEP_TOKEN=… inline on the deploy command, so it lived only for that one process; §5's gcloud scheduler jobs create … Bearer $LISA_SWEEP_TOKEN and the verify curl then ran in a fresh shell with the var unset → the scheduler would authenticate with an empty bearer and every tick would 401. Fixed: export LISA_SWEEP_TOKEN="$(openssl rand -hex 24)" as its own step so both the deploy and §5 share it. Same pass fixed the runbook's dead S1 LISA_CLOUD_GOOGLE_* → A-series LISA_GOOGLE_WEB_CLIENT_ID / LISA_GOOGLE_IOS_CLIENT_ID (deploy block + the third-party-console table).

3. deploy.sh — master token leaked into prod logs. The final line echoed …/?token=$LISA_WEB_TOKEN unconditionally, printing the master token into the deploy log even under SECRETS_MODE=sm (production, where accounts — not the shared token — are the real auth). Gated: sm mode prints the plain console URL; the token-in-URL convenience stays for the env demo mode only.

firestore.ts's LISA_FIRESTORE_TOKEN operator override (so the import runs outside Cloud Run) is clean and carried as-is.

@oratis
oratis merged commit 6056ed8 into main Jul 25, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant