feat(codegen/cloudflare): vault-wireable API credentials (#476) #17
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "476-cloudflare-vault-auth"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes swamp-club #476.
What & why
The
@swamp/cloudflareextension authenticated only via environmentvariables (
CLOUDFLARE_API_TOKEN, orCLOUDFLARE_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 takeprecedence 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 eachcredential as
override ?? env, preferring an API token over the legacykey+email pair. A per-credential
Setcache validates each distinctcredential exactly once (token via
GET /user/tokens/verify, key+email viaGET /user). The overrides object is threaded throughrequestand everyCRUD helper; credentials are only ever sent as headers, never in a request
body.
codegen/cloudflare/extensionModelGenerator.ts— injectsapiToken,apiKey, andemailasz.string().meta({ sensitive: true }).optional()args into both
GlobalArgsSchemaandInputsSchema, and threads{ apiToken, apiKey, email }into create/get/update/delete/sync.Collision guard:
apiTokenis injected unless a resource property alreadyowns that name; the
apiKey+emaillegacy pair is injected only when bothnames are free. Five models that already define an
emailproperty(
access/users,members/members,email/addresses,email/suppression,email-security/impersonation_registry) therefore getapiToken-only vaultwiring and continue to use
CLOUDFLARE_API_KEY+CLOUDFLARE_EMAILfor thelegacy path.
codegen/cloudflare/pipeline.ts—newFieldNamesnow mirrors the fullemitted
GlobalArgsSchemafield 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.codegen/shared/readmeGenerator.ts(Cloudflare auth section) andcodegen/designs/cloudflare.md§9 document the args, precedence, pair-guard,and
z.meta({ sensitive: true })redaction.Why it's correct
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 storedvalue is the expression, not the secret.
upgradeAttributes— backward compatible. 159 models readAdded: apiToken, apiKey, email; the 5 collision models readAdded: apiToken.Regeneration
All 70 Cloudflare service packages (168 models) regenerated to a
consistent
2026.05.29.1. Full regeneration also folded in minor incidentalupstream schema drift: a new
containersservice, a newemail-security/url_ignore_patternsmodel, and two new fields(
devices/policy.dns_search_suffixes,gateway/locations.max_ttl_secs).Verification
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 --checkclean acrosscodegen/.deno install --frozen; sample model dirs passcheck/lint/fmt.
Known follow-up
Pre-existing Cloudflare manifest non-idempotency (the README generator
emits prose that
deno fmtrewraps, so the pipeline's README comparison alwaysreports 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
Code Review
Blocking Issues
None.
Suggestions
pipeline.ts:574—type Spec = anytechnically violates CLAUDE.md's no-anyrule in hand-written code.The
// deno-lint-ignore no-explicit-anydirective acknowledges the intent, and usinganyhere is genuinely pragmatic for parsing an unstructured OpenAPI JSON blob. An alternative would beRecord<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.Design doc inaccuracy in
codegen/designs/cloudflare.md, Section 8 (Instance Naming),getrow.The table says
getusesresult.{namingField}for natural naming fields, butextensionModelGenerator.ts:254usesg.{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.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/deletedescriptions. Cosmetic only; a simple helper function that checks the first character could emit "an" where appropriate. No functional impact.Auth-validation calls in
getAuth(insidelibGenerator.ts) use barefetch(), bypassing the rate-limit retry logic inrequest().If
GET /user/tokens/verifyorGET /userreturns 429, the token validation fails immediately rather than retrying. In practice, token validation fires at most once per distinct credential (due to thevalidatedCredentialscache), so this is low risk. Routing validation throughrequest()would make the behavior consistent.InputsSchemafor dual-scoped resources omits the.refine()constraint thatGlobalArgsSchemacarries.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
finallyblocks andsanitizeResources: falsewith explanation, vault-wireable credential injection with collision guards, correct dual-scope XOR refine, create-only property detection, rate-limit retry withRetry-Aftersupport, and instance-name sanitization against path traversal. The design document is thorough and the generated models follow established patterns from other providers.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 undermodel/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
Dual-scope truthiness mismatch between Zod refine and endpoint construction —
extensionModelGenerator.ts:429,446The dual-scope refine (line 446) uses
d.account_id != nullto check if a scope parameter is "provided," but the endpoint construction (line 429) uses the JavaScript truthiness checkg.account_id ? .... An empty string""is not-null (passes refine as "provided") but falsy (endpoint construction treats it as "not provided" and falls through tozone_id).Breaking input:
account_id: ""withzone_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 used.account_id != null && d.account_id !== ""in the refine, or useg.account_id != nullin the endpoint construction for consistency.Low
updatemethod lacks identifying field guard --extensionModelGenerator.ts:309The
syncmethod (line 384) checksif (!existing.identifyingField)before using it in the API URL, butupdate(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.Retry-Afterheader 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.GlobalArgs property merge order favors create schema --
extensionModelGenerator.ts:456-459...updateProperties, ...createPropertiesmeans 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.validatedCredentialscache 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.