Skip to content

payments-stripe: read STRIPE_API_KEY from env, drop api_key from Input - #38

Merged
mike-hanono merged 1 commit into
chore/payments-stripe-offchain-conventionsfrom
fix/payments-stripe-credential-env
May 25, 2026
Merged

payments-stripe: read STRIPE_API_KEY from env, drop api_key from Input#38
mike-hanono merged 1 commit into
chore/payments-stripe-offchain-conventionsfrom
fix/payments-stripe-credential-env

Conversation

@mike-hanono

Copy link
Copy Markdown

Summary

Stacks on #37. Removes the pub api_key: String field from all 6 Stripe endpoint Input structs and switches the tool to the env-var credential pattern already used by memory-memwal and storage-walrus.

Why this is critical: tool inputs flow through the Nexus DAG on Sui as plaintext. Any field on Input is effectively published on-chain. The current api_key field meant every Stripe DAG invocation was about to write the secret key (sk_test_… or sk_live_…) to a public ledger forever.

What changes

  • src/stripe_client.rsfrom_env() reads STRIPE_API_KEY once at startup; wraps in zeroize::Zeroizing<String> (heap-zeroed on drop); validates prefix (sk_test_ / sk_live_ / rk_test_ / rk_live_) and length. Hand-written Debug prints <redacted> — no #[derive(Debug)] on the credential-bearing type. Shared HTTP client lives in a OnceLock so the connection pool survives across calls. #[cfg(test)] for_testing(base_url, bearer) constructor bypasses env validation for mockito tests.

  • src/main.rs — Explicit multi-thread tokio runtime; env_logger init before anything else; --meta short-circuit so CI's prepare step can pull /meta from an image with no env mounted; load_dotenv_if_present() and validate_credentials_at_startup() before bootstrap!. main is the only process::exit site. Exit 1 if STRIPE_API_KEY is unset, empty, or has an unrecognized prefix.

  • All 6 endpoint files — Dropped pub api_key: String from Input. Dropped .with_auth(&input.api_key) from invoke(). NexusTool::new() calls StripeClient::from_env().unwrap_or_else(|e| { log::error!(...); panic!(...) }) — startup-only panic, mirrors memwal. idempotency_key stays on Input (per-call dedup data, not a credential — explicit whitelist in the auditor).

  • Tests — Every StripeClient::new(Some(&server.url()))StripeClient::for_testing(&server.url(), "sk_test_FAKE"). api_key: field dropped from every test_input() and inline Input literal. get-balance Input is now {} (no fields needed).

  • Cargo.toml — Added workspace deps anyhow, dotenvy, env_logger, log, zeroize.

  • README.md — Rewrote credential section to document the env var. Dropped api_key from every per-FQN Input table. Added an env-vars table. Threaded <TOOL_FQN_VERSION> into each FQN heading (matching what's actually in source via concat!(..., env!("TOOL_FQN_VERSION"))).

  • .env.example — Documents STRIPE_API_KEY for local dev. In production Cloud Run mounts it via secretKeyRef from GCP Secret Manager, configured by the operator out-of-band — the deploy pipeline does NOT provision the upstream API key. tools.json's environment block is RUST_LOG only (C10 conformance).

  • Skill markdown lints — 4 files in .claude/skills/nexus-tool-builder/reference/ had compact table separators (|---|) rewritten to | --- | to satisfy MD060 so this branch's pre-commit md-lint passes. These same files are rewritten by Add nexus-tool-builder skill + nexus-tool-auditor agent #31; the conflict at merge is a no-op since both branches use | --- |.

Out of scope

  • AUDIT.md regeneration — deferred. Run the nexus-tool-auditor agent against the merged tree, or invoke it after this branch lands. The version currently in the crate is stale (it documents the pre-fix Input shape).

Test plan

  • Reviewer reads the diff and confirms api_key no longer appears on any Input struct.
  • CI runs cargo +stable check --package payments-stripe (blocked locally — sandbox missing pkg-config/libssl-dev for openssl-sys).
  • CI runs cargo +stable clippy --package payments-stripe -- -D warnings.
  • CI runs cargo +stable test --package payments-stripe (mockito tests with for_testing() constructor).
  • Manual smoke against a real test key: STRIPE_API_KEY=sk_test_… cargo run --release --package payments-stripe then curl /create-payment-intent/health and curl /create-payment-intent/meta.
  • Re-run the auditor agent against the merged crate and update AUDIT.md.

Verification done locally

  • Ran the updated audit script (from Add nexus-tool-builder skill + nexus-tool-auditor agent #31's HEAD) against this crate in a sibling worktree rebased onto main:
    • PASS static:input-credential (C1 — the central check this PR fixes)
    • PASS static:debug-on-credentials (C8)
    • PASS static:stateful (C9)
    • PASS tools-json:shape / tools-json:secret-in-env (C10, H7)
    • PASS conform:output-enum / description / fqn-versioned
  • npx markdownlint-cli2 '**/*.md' '!.git/' → 0 errors across 26 files.

🤖 Generated with Claude Code

Stacks on #37 (which already moved the crate to offchain/tools/ and added
tools.json + build.rs). This PR is the security fix #37 deliberately left
out: every endpoint's Input struct had `pub api_key: String`, which
flowed through the Nexus DAG to Sui as plaintext on every invocation.

Adopts the env-var credential pattern from offchain/tools/memory-memwal:

- stripe_client.rs: `from_env()` reads STRIPE_API_KEY once at startup,
  wraps in `zeroize::Zeroizing<String>` (heap-zeroed on drop), validates
  the prefix (`sk_test_` / `sk_live_` / `rk_test_` / `rk_live_`). Hand-
  written `Debug` prints `<redacted>` — never `#[derive(Debug)]` on the
  credential-bearing type. Shared HTTP client lives in a `OnceLock` so
  the connection pool survives across `invoke` calls. `#[cfg(test)]
  for_testing(base_url, bearer)` constructor bypasses env validation for
  mockito tests.

- main.rs: explicit multi-thread tokio runtime; `env_logger` init before
  anything else; `--meta` short-circuit so CI's prepare step can pull
  /meta from an image with no env; `load_dotenv_if_present()` and
  `validate_credentials_at_startup()` before `bootstrap!`. `main` is the
  only `process::exit` site. Exit 1 if STRIPE_API_KEY is unset, empty,
  or has an unrecognized prefix.

- All six endpoint files: dropped `pub api_key: String` from Input.
  Dropped `.with_auth(&input.api_key)` from invoke(). NexusTool::new()
  calls `StripeClient::from_env().unwrap_or_else(|e| { log::error!(...);
  panic!(...) })` — startup-only panic, mirrors memwal. `idempotency_key`
  stays on Input (per-call dedup data, not a credential — explicit
  whitelist in the auditor).

- Tests: every `StripeClient::new(Some(&server.url()))` →
  `StripeClient::for_testing(&server.url(), "sk_test_FAKE")`. Every
  `api_key:` field dropped from `test_input()` and from inline Input
  literals. `get-balance` Input is now empty (`{}`) — only the
  STRIPE_API_KEY env var matters; the `_input` arg in invoke is unused.

- Cargo.toml: added workspace deps `anyhow`, `dotenvy`, `env_logger`,
  `log`, `zeroize` for the env-var path.

- README.md: rewrote the credential section to document the env var.
  Dropped `api_key` from every per-FQN Input table. Added an env-vars
  table. Threaded `<TOOL_FQN_VERSION>` into each FQN heading (matching
  the actual `concat!(..., env!("TOOL_FQN_VERSION"))` in source).

- .env.example: documents STRIPE_API_KEY for local dev. In production
  Cloud Run mounts it via `secretKeyRef` from GCP Secret Manager,
  configured by the operator out-of-band — the deploy pipeline does NOT
  provision the upstream API key. tools.json's `environment` block is
  RUST_LOG only (C10).

- Regenerated AUDIT.md is deferred — run the auditor agent against the
  merged tree, or invoke it after this branch lands.

Markdown linter fix: aligned 4 skill reference files' compact table
separators (`|---|` → `| --- |`) to satisfy MD060. These same files are
rewritten by PR #31; the conflict at merge is a no-op since both
branches use `| --- |`. Touched only to unblock `just pre-commit::md-lint`
on this branch.

Verified locally:
- bash <audit.sh from #31's tip> off-chain payments-stripe
  → PASS static:input-credential
  → PASS static:debug-on-credentials
  → PASS static:stateful
  → PASS tools-json:shape / secret-in-env
  → PASS conform:output-enum / description / fqn-versioned
  (cargo:check fails — sandbox is missing pkg-config/libssl-dev for
   openssl-sys; orthogonal to this change. PR #37 author noted the same
   in their PR description; verification happens via CI on push.)
- npx markdownlint-cli2 '**/*.md' '!.git/' → 0 errors across 26 files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mike-hanono
mike-hanono requested a review from a team May 25, 2026 20:49
@mike-hanono
mike-hanono merged commit 90cfdd7 into chore/payments-stripe-offchain-conventions May 25, 2026
7 checks passed
mike-hanono pushed a commit that referenced this pull request May 25, 2026
PR #38 added anyhow / dotenvy / env_logger / log / zeroize to
payments-stripe's Cargo.toml but never refreshed the lockfile. CI builds
with `cargo build --locked` which refuses to update Cargo.lock at build
time, so every tool's build job failed (shared workspace lockfile).

Ran `cargo update --workspace --manifest-path offchain/Cargo.toml`.
Diff is +8/-2 lines on offchain/Cargo.lock — only payments-stripe's
entry changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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