feat(codegen/cloudflare): vault-wireable API credentials (#476) #17

Merged
stack72 merged 1 commit from 476-cloudflare-vault-auth into main 2026-05-29 17:25:17 +00:00
Owner

Closes swamp-club #476.

What & why

The @swamp/cloudflare extension authenticated only via environment
variables (CLOUDFLARE_API_TOKEN, or CLOUDFLARE_API_KEY + CLOUDFLARE_EMAIL),
so credentials could not be sourced from a vault. This adds optional, sensitive
global arguments — wireable with vault.get(...) expressions — that take
precedence over the environment. It mirrors the delivered Hetzner Cloud (#471)
and DigitalOcean vault-auth work.

Changes (codegen only — model/ is regenerated output)

  • codegen/cloudflare/libGenerator.tsgetAuth(overrides) resolves each
    credential as override ?? env, preferring an API token over the legacy
    key+email pair. A per-credential Set cache validates each distinct
    credential exactly once (token via GET /user/tokens/verify, key+email via
    GET /user). The overrides object is threaded through request and every
    CRUD helper; credentials are only ever sent as headers, never in a request
    body.
  • codegen/cloudflare/extensionModelGenerator.ts — injects apiToken,
    apiKey, and email as z.string().meta({ sensitive: true }).optional()
    args into both GlobalArgsSchema and InputsSchema, and threads
    { apiToken, apiKey, email } into create/get/update/delete/sync.
    Collision guard: apiToken is injected unless a resource property already
    owns that name; the apiKey+email legacy pair is injected only when both
    names are free. Five models that already define an email property
    (access/users, members/members, email/addresses, email/suppression,
    email-security/impersonation_registry) therefore get apiToken-only vault
    wiring and continue to use CLOUDFLARE_API_KEY + CLOUDFLARE_EMAIL for the
    legacy path.
  • codegen/cloudflare/pipeline.tsnewFieldNames now mirrors the full
    emitted GlobalArgsSchema field set (scope args, synthetic name,
    create+update props, and the pair-guarded auth args), filtered to
    valid-identifier names. Without this, the first changed regeneration would
    emit spurious Removed: account_id, zone_id, … upgrade entries.
  • Docscodegen/shared/readmeGenerator.ts (Cloudflare auth section) and
    codegen/designs/cloudflare.md §9 document the args, precedence, pair-guard,
    and z.meta({ sensitive: true }) redaction.

Why it's correct

  • Env-var auth is unchanged; the new args are optional and additive. Existing
    models keep working with no edits.
  • z.meta({ sensitive: true }) makes swamp-core redact the args from run logs,
    reports, and stored data. On the recommended vault.get(...) path the stored
    value is the expression, not the secret.
  • Credentials are never written into a request body (covered by a test).
  • Each regenerated model gains exactly one additive upgrade entry with identity
    upgradeAttributes — backward compatible. 159 models read
    Added: apiToken, apiKey, email; the 5 collision models read
    Added: apiToken.

Regeneration

All 70 Cloudflare service packages (168 models) regenerated to a
consistent 2026.05.29.1. Full regeneration also folded in minor incidental
upstream schema drift: a new containers service, a new
email-security/url_ignore_patterns model, and two new fields
(devices/policy.dns_search_suffixes, gateway/locations.max_ttl_secs).

Verification

  • 163 codegen tests pass, including new lib auth tests (credential precedence
    both styles, per-credential validation, credentials-never-in-body,
    no-credentials error) and generator tests (args in both schemas, threading
    into all methods, real-collision pair-guard).
  • deno check / lint / fmt --check clean across codegen/.
  • All 70 model lockfiles pass deno install --frozen; sample model dirs pass
    check/lint/fmt.
  • Model files are idempotent (second generation run: 0 changed).

Known follow-up

Pre-existing Cloudflare manifest non-idempotency (the README generator
emits prose that deno fmt rewraps, so the pipeline's README comparison always
reports a change and bumps every manifest micro on re-runs) is tracked
separately in swamp-club #488 and is intentionally out of scope here. This
PR commits a single clean run, so all versions are consistent and correct.

🤖 Generated with Claude Code

Closes swamp-club #476. ## What & why The `@swamp/cloudflare` extension authenticated **only** via environment variables (`CLOUDFLARE_API_TOKEN`, or `CLOUDFLARE_API_KEY` + `CLOUDFLARE_EMAIL`), so credentials could not be sourced from a vault. This adds optional, sensitive global arguments — wireable with `vault.get(...)` expressions — that take precedence over the environment. It mirrors the delivered Hetzner Cloud (#471) and DigitalOcean vault-auth work. ## Changes (codegen only — `model/` is regenerated output) - **`codegen/cloudflare/libGenerator.ts`** — `getAuth(overrides)` resolves each credential as `override ?? env`, preferring an API token over the legacy key+email pair. A per-credential `Set` cache validates each distinct credential exactly once (token via `GET /user/tokens/verify`, key+email via `GET /user`). The overrides object is threaded through `request` and every CRUD helper; credentials are only ever sent as headers, never in a request body. - **`codegen/cloudflare/extensionModelGenerator.ts`** — injects `apiToken`, `apiKey`, and `email` as `z.string().meta({ sensitive: true }).optional()` args into both `GlobalArgsSchema` and `InputsSchema`, and threads `{ apiToken, apiKey, email }` into create/get/update/delete/sync. **Collision guard:** `apiToken` is injected unless a resource property already owns that name; the `apiKey`+`email` legacy pair is injected only when *both* names are free. Five models that already define an `email` property (`access/users`, `members/members`, `email/addresses`, `email/suppression`, `email-security/impersonation_registry`) therefore get `apiToken`-only vault wiring and continue to use `CLOUDFLARE_API_KEY` + `CLOUDFLARE_EMAIL` for the legacy path. - **`codegen/cloudflare/pipeline.ts`** — `newFieldNames` now mirrors the full emitted `GlobalArgsSchema` field set (scope args, synthetic name, create+update props, and the pair-guarded auth args), filtered to valid-identifier names. Without this, the first changed regeneration would emit spurious `Removed: account_id, zone_id, …` upgrade entries. - **Docs** — `codegen/shared/readmeGenerator.ts` (Cloudflare auth section) and `codegen/designs/cloudflare.md` §9 document the args, precedence, pair-guard, and `z.meta({ sensitive: true })` redaction. ## Why it's correct - Env-var auth is unchanged; the new args are optional and additive. Existing models keep working with no edits. - `z.meta({ sensitive: true })` makes swamp-core redact the args from run logs, reports, and stored data. On the recommended `vault.get(...)` path the stored value is the expression, not the secret. - Credentials are never written into a request body (covered by a test). - Each regenerated model gains exactly one additive upgrade entry with identity `upgradeAttributes` — backward compatible. 159 models read `Added: apiToken, apiKey, email`; the 5 collision models read `Added: apiToken`. ## Regeneration All **70** Cloudflare service packages (**168** models) regenerated to a consistent `2026.05.29.1`. Full regeneration also folded in minor incidental upstream schema drift: a new `containers` service, a new `email-security/url_ignore_patterns` model, and two new fields (`devices/policy.dns_search_suffixes`, `gateway/locations.max_ttl_secs`). ## Verification - 163 codegen tests pass, including new lib auth tests (credential precedence both styles, per-credential validation, credentials-never-in-body, no-credentials error) and generator tests (args in both schemas, threading into all methods, real-collision pair-guard). - `deno check` / `lint` / `fmt --check` clean across `codegen/`. - All 70 model lockfiles pass `deno install --frozen`; sample model dirs pass check/lint/fmt. - Model files are idempotent (second generation run: 0 changed). ## Known follow-up Pre-existing Cloudflare **manifest** non-idempotency (the README generator emits prose that `deno fmt` rewraps, so the pipeline's README comparison always reports a change and bumps every manifest micro on re-runs) is tracked separately in **swamp-club #488** and is intentionally out of scope here. This PR commits a single clean run, so all versions are consistent and correct. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(codegen/cloudflare): vault-wireable API credentials (#476)
All checks were successful
CI / workflows/s3-bootstrap - lockfile up to date (pull_request) Has been skipped
CI / cve/dirtyfrag - check (pull_request) Has been skipped
CI / cve/dirtyfrag - fmt (pull_request) Has been skipped
CI / cve/mini-shai-hulud - check (pull_request) Has been skipped
CI / cve/dirtyfrag - lint (pull_request) Has been skipped
CI / cve/dirtyfrag - test (pull_request) Has been skipped
CI / Dependency Audit (pull_request) Successful in 4m29s
CI / cve/mini-shai-hulud - lint (pull_request) Has been skipped
CI / cve/mini-shai-hulud - test (pull_request) Has been skipped
CI / cve/dirtyfrag - lockfile up to date (pull_request) Has been skipped
CI / cve/mini-shai-hulud - lockfile up to date (pull_request) Has been skipped
CI / cve/mini-shai-hulud - fmt (pull_request) Has been skipped
CI / model/digitalocean - check (pull_request) Successful in 1m7s
CI / model/digitalocean - lockfile up to date (pull_request) Successful in 1m26s
CI / model/hetzner-cloud - check (pull_request) Successful in 1m28s
CI / model/hetzner-cloud - lockfile up to date (pull_request) Successful in 1m36s
CI / aws models - lockfiles up to date (pull_request) Successful in 1m39s
CI / codegen - fmt (pull_request) Successful in 1m26s
CI / cloudflare models - lockfiles up to date (pull_request) Successful in 1m29s
CI / gcp models - lockfiles up to date (pull_request) Successful in 1m34s
CI / cloudflare models - sample check (pull_request) Successful in 1m44s
CI / codegen - check (pull_request) Successful in 1m42s
CI / aws models - sample check (pull_request) Successful in 2m0s
CI / CI Security Review (pull_request) Has been skipped
CI / codegen - lint (pull_request) Successful in 55s
CI / gcp models - sample check (pull_request) Successful in 2m20s
CI / codegen - lockfile up to date (pull_request) Successful in 53s
CI / Claude Code Review (pull_request) Successful in 5m14s
CI / Adversarial Code Review (pull_request) Successful in 5m58s
CI / Merge Gate (pull_request) Successful in 26s
7dbe726612
Adds optional, sensitive global arguments to the @swamp/cloudflare codegen so
API credentials can be sourced from a vault instead of CLOUDFLARE_* env vars
only. Mirrors the Hetzner Cloud (#471) and DigitalOcean vault-auth work.

- libGenerator.ts: getAuth(overrides) prefers explicit creds over env (token
  preferred over key+email); per-credential validation cache so each distinct
  credential is validated once; threaded through request + every CRUD helper.
  Credentials stay in headers, never a request body.
- extensionModelGenerator.ts: injects apiToken plus the legacy apiKey+email
  pair as z.meta({ sensitive: true }) args. apiToken is injected unless a
  resource property owns the name; the apiKey+email pair is injected only when
  BOTH names are free, so the 5 models that already define an `email` property
  get apiToken-only vault wiring and fall back to env for the legacy path.
  Threaded into create/get/update/delete/sync.
- pipeline.ts: newFieldNames mirrors the full emitted GlobalArgsSchema so
  upgrade diffing is accurate (159 models "Added: apiToken, apiKey, email",
  5 "Added: apiToken").
- Docs: Cloudflare README auth section + designs/cloudflare.md section 9.
- Tests: lib auth (credential precedence both styles, per-credential
  validation via /user/tokens/verify and /user, credentials-never-in-body,
  no-credentials error) + generator (args in both schemas, threading into all
  methods, real-collision pair-guard); snapshot updated.

Regenerates all 70 Cloudflare service packages (168 models) to a consistent
2026.05.29.1. Full regeneration also folds in minor incidental upstream schema
drift: a new `containers` service, a new email-security/url_ignore_patterns
model, and 2 new fields (devices/policy dns_search_suffixes,
gateway/locations max_ttl_secs).

A pre-existing Cloudflare manifest non-idempotency (README output not
deno-fmt-clean) is tracked separately in #488 and is intentionally out of
scope here.

Closes #476.

Co-Authored-By: stack72 <public@paulstack.co.uk>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author
Owner

Code Review

Blocking Issues

None.

Suggestions

  1. pipeline.ts:574type Spec = any technically violates CLAUDE.md's no-any rule in hand-written code.
    The // deno-lint-ignore no-explicit-any directive acknowledges the intent, and using any here is genuinely pragmatic for parsing an unstructured OpenAPI JSON blob. An alternative would be Record<string, unknown> with casts throughout, but that's arguably less readable. Worth noting in case the team wants to revisit consistency with the strict-mode policy.

  2. Design doc inaccuracy in codegen/designs/cloudflare.md, Section 8 (Instance Naming), get row.
    The table says get uses result.{namingField} for natural naming fields, but extensionModelGenerator.ts:254 uses g.{namingField} (globalArgs) for all cases. The comment at line 249–252 explains this is intentional ("Cloudflare may transform the name, e.g., appending the zone domain to DNS record names"), which is correct behavior. The design doc just needs updating to match reality.

  3. Grammar: generator always emits "a" before display names, producing "Create a Address Map", "Get a Address Map", etc. for resources whose names start with a vowel sound.
    Affects create/get/delete descriptions. Cosmetic only; a simple helper function that checks the first character could emit "an" where appropriate. No functional impact.

  4. Auth-validation calls in getAuth (inside libGenerator.ts) use bare fetch(), bypassing the rate-limit retry logic in request().
    If GET /user/tokens/verify or GET /user returns 429, the token validation fails immediately rather than retrying. In practice, token validation fires at most once per distinct credential (due to the validatedCredentials cache), so this is low risk. Routing validation through request() would make the behavior consistent.

  5. InputsSchema for dual-scoped resources omits the .refine() constraint that GlobalArgsSchema carries.
    This is intentional (inputs capture partial state, not a fully-validated invocation context), but the design doc's Section 13 ("InputsSchema: Mirrors GlobalArgsSchema with all fields .optional()") doesn't mention this asymmetry. A brief note there would avoid future confusion.


Overall the implementation is solid: comprehensive snapshot + runtime tests with proper env-var restore in finally blocks and sanitizeResources: false with explanation, vault-wireable credential injection with collision guards, correct dual-scope XOR refine, create-only property detection, rate-limit retry with Retry-After support, and instance-name sanitization against path traversal. The design document is thorough and the generated models follow established patterns from other providers.

## Code Review ### Blocking Issues None. ### Suggestions 1. **`pipeline.ts:574` — `type Spec = any` technically violates CLAUDE.md's no-`any` rule in hand-written code.** The `// deno-lint-ignore no-explicit-any` directive acknowledges the intent, and using `any` here is genuinely pragmatic for parsing an unstructured OpenAPI JSON blob. An alternative would be `Record<string, unknown>` with casts throughout, but that's arguably less readable. Worth noting in case the team wants to revisit consistency with the strict-mode policy. 2. **Design doc inaccuracy in `codegen/designs/cloudflare.md`, Section 8 (Instance Naming), `get` row.** The table says `get` uses `result.{namingField}` for natural naming fields, but `extensionModelGenerator.ts:254` uses `g.{namingField}` (globalArgs) for all cases. The comment at line 249–252 explains this is intentional ("Cloudflare may transform the name, e.g., appending the zone domain to DNS record names"), which is correct behavior. The design doc just needs updating to match reality. 3. **Grammar: generator always emits "a" before display names, producing "Create a Address Map", "Get a Address Map", etc. for resources whose names start with a vowel sound.** Affects `create`/`get`/`delete` descriptions. Cosmetic only; a simple helper function that checks the first character could emit "an" where appropriate. No functional impact. 4. **Auth-validation calls in `getAuth` (inside `libGenerator.ts`) use bare `fetch()`, bypassing the rate-limit retry logic in `request()`.** If `GET /user/tokens/verify` or `GET /user` returns 429, the token validation fails immediately rather than retrying. In practice, token validation fires at most once per distinct credential (due to the `validatedCredentials` cache), so this is low risk. Routing validation through `request()` would make the behavior consistent. 5. **`InputsSchema` for dual-scoped resources omits the `.refine()` constraint that `GlobalArgsSchema` carries.** This is intentional (inputs capture partial state, not a fully-validated invocation context), but the design doc's Section 13 ("InputsSchema: Mirrors GlobalArgsSchema with all fields `.optional()`") doesn't mention this asymmetry. A brief note there would avoid future confusion. --- Overall the implementation is solid: comprehensive snapshot + runtime tests with proper env-var restore in `finally` blocks and `sanitizeResources: false` with explanation, vault-wireable credential injection with collision guards, correct dual-scope XOR refine, create-only property detection, rate-limit retry with `Retry-After` support, and instance-name sanitization against path traversal. The design document is thorough and the generated models follow established patterns from other providers.
Author
Owner

Adversarial Review

Reviewed all hand-written files: codegen/cloudflare/extensionModelGenerator.ts, codegen/cloudflare/libGenerator.ts, codegen/cloudflare/pipeline.ts, codegen/cloudflare/extensionModelGenerator_test.ts, codegen/cloudflare/libGenerator_test.ts, codegen/designs/cloudflare.md, codegen/shared/readmeGenerator.ts, and the snapshot file. Files under model/ are auto-generated and skipped per CLAUDE.md.

This PR adds a complete Cloudflare provider to the codegen pipeline: schema fetching, OpenAPI parsing, per-service model generation, a shared HTTP client library, and vault-wireable credential support. The code is well-structured and follows established patterns from the Hetzner and DigitalOcean providers.

Medium

  1. Dual-scope truthiness mismatch between Zod refine and endpoint constructionextensionModelGenerator.ts:429,446

    The dual-scope refine (line 446) uses d.account_id != null to check if a scope parameter is "provided," but the endpoint construction (line 429) uses the JavaScript truthiness check g.account_id ? .... An empty string "" is not-null (passes refine as "provided") but falsy (endpoint construction treats it as "not provided" and falls through to zone_id).

    Breaking input: account_id: "" with zone_id: undefined -- the refine passes (account_id is non-null), but the endpoint becomes /zones/undefined/..., producing a confusing Cloudflare 400/404.

    Suggested fix: Add .min(1) to the scope ID fields in the dual-scope GlobalArgsSchema, or use d.account_id != null && d.account_id !== "" in the refine, or use g.account_id != null in the endpoint construction for consistency.

Low

  1. update method lacks identifying field guard -- extensionModelGenerator.ts:309

    The sync method (line 384) checks if (!existing.identifyingField) before using it in the API URL, but update (line 309) does not. If stored state lacks the identifying field, the URL contains /undefined, producing a confusing error. Consistent with Hetzner provider's existing pattern, so not a regression.

  2. Retry-After header only handles numeric format -- libGenerator.ts:119-123 (generated code)

    Number(retryAfter) works for seconds but returns NaN for HTTP-date format, falling back to exponential backoff. Cloudflare typically sends numeric values.

  3. GlobalArgs property merge order favors create schema -- extensionModelGenerator.ts:456-459

    ...updateProperties, ...createProperties means create-schema constraints win on collision. If the update schema is more permissive for a shared field, valid update values could be rejected by GlobalArgs validation. Cloudflare validates server-side regardless.

  4. validatedCredentials cache never cleared -- libGenerator.ts:33 (generated code)

    Module-level Set caches credentials for process lifetime. A rotated token remains "validated" after revocation; subsequent API calls would get 401s. Standard for CLI-lifetime processes.

Verdict

PASS -- The code is solid. The dual-scope truthiness mismatch (Medium 1) is unlikely in practice (requires an empty-string account_id), and produces a clear API failure rather than silent misbehavior. Remaining findings are minor edge cases consistent with established codebase patterns. Well-tested with comprehensive snapshot and behavioral tests covering CRUD paths, auth override precedence, credential caching, and collision guards.

## Adversarial Review Reviewed all hand-written files: `codegen/cloudflare/extensionModelGenerator.ts`, `codegen/cloudflare/libGenerator.ts`, `codegen/cloudflare/pipeline.ts`, `codegen/cloudflare/extensionModelGenerator_test.ts`, `codegen/cloudflare/libGenerator_test.ts`, `codegen/designs/cloudflare.md`, `codegen/shared/readmeGenerator.ts`, and the snapshot file. Files under `model/` are auto-generated and skipped per CLAUDE.md. This PR adds a complete Cloudflare provider to the codegen pipeline: schema fetching, OpenAPI parsing, per-service model generation, a shared HTTP client library, and vault-wireable credential support. The code is well-structured and follows established patterns from the Hetzner and DigitalOcean providers. ### Medium 1. **Dual-scope truthiness mismatch between Zod refine and endpoint construction** — `extensionModelGenerator.ts:429,446` The dual-scope refine (line 446) uses `d.account_id != null` to check if a scope parameter is "provided," but the endpoint construction (line 429) uses the JavaScript truthiness check `g.account_id ? ...`. An empty string `""` is not-null (passes refine as "provided") but falsy (endpoint construction treats it as "not provided" and falls through to `zone_id`). **Breaking input**: `account_id: ""` with `zone_id: undefined` -- the refine passes (account_id is non-null), but the endpoint becomes `/zones/undefined/...`, producing a confusing Cloudflare 400/404. **Suggested fix**: Add `.min(1)` to the scope ID fields in the dual-scope GlobalArgsSchema, or use `d.account_id != null && d.account_id !== ""` in the refine, or use `g.account_id != null` in the endpoint construction for consistency. ### Low 1. **`update` method lacks identifying field guard** -- `extensionModelGenerator.ts:309` The `sync` method (line 384) checks `if (!existing.identifyingField)` before using it in the API URL, but `update` (line 309) does not. If stored state lacks the identifying field, the URL contains `/undefined`, producing a confusing error. Consistent with Hetzner provider's existing pattern, so not a regression. 2. **`Retry-After` header only handles numeric format** -- `libGenerator.ts:119-123` (generated code) `Number(retryAfter)` works for seconds but returns NaN for HTTP-date format, falling back to exponential backoff. Cloudflare typically sends numeric values. 3. **GlobalArgs property merge order favors create schema** -- `extensionModelGenerator.ts:456-459` `...updateProperties, ...createProperties` means create-schema constraints win on collision. If the update schema is more permissive for a shared field, valid update values could be rejected by GlobalArgs validation. Cloudflare validates server-side regardless. 4. **`validatedCredentials` cache never cleared** -- `libGenerator.ts:33` (generated code) Module-level Set caches credentials for process lifetime. A rotated token remains "validated" after revocation; subsequent API calls would get 401s. Standard for CLI-lifetime processes. ### Verdict **PASS** -- The code is solid. The dual-scope truthiness mismatch (Medium 1) is unlikely in practice (requires an empty-string account_id), and produces a clear API failure rather than silent misbehavior. Remaining findings are minor edge cases consistent with established codebase patterns. Well-tested with comprehensive snapshot and behavioral tests covering CRUD paths, auth override precedence, credential caching, and collision guards.
stack72 deleted branch 476-cloudflare-vault-auth 2026-05-29 17:25:17 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
swamp-club/swamp-extensions!17
No description provided.