Skip to content

[config] Add optional option to bindings.secret() - #15273

Open
penalosa wants to merge 4 commits into
mainfrom
penalosa/config-optional-secret
Open

[config] Add optional option to bindings.secret()#15273
penalosa wants to merge 4 commits into
mainfrom
penalosa/config-optional-secret

Conversation

@penalosa

@penalosa penalosa commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Adds an optional option to bindings.secret() in cloudflare.config.ts, plus a matching secrets.optional field in the Wrangler configuration, so a secret that may legitimately be absent at runtime can be declared in config and inferred as string | undefined.

bindings.secret(); // → string   (unchanged)
bindings.secret({ optional: true }); // → string | undefined

Why

Absent secrets are a real runtime state, not a hypothetical — a freshly created Worker has none set, and a rolled-back Worker can lose one. Today bindings.secret() types the env member as a required string, so code that correctly guards with if (env.TOKEN) { … } is typed as if the check were unnecessary, and secrets.required makes the first deploy of a Worker that lacks the secret fail outright.

The only workaround was to leave the secret undeclared in cloudflare.config.ts and hand-type it optional in a separate env.d.ts — which defeats the point of the config format, since the binding then becomes invisible to anything reading the config as the source of truth.

What changed

@cloudflare/config

  • bindings.secret() takes an optional { optional?: boolean } — it is still callable with no arguments and its runtime shape is unchanged ({ type: "secret" }).
  • New OptionalSecretBinding interface (SecretBinding narrowed to optional: true), returned by an overload of secret() and exported from the public surface.
  • BindingTypeMap moves secret into the parameterised group — TBinding extends OptionalSecretBinding ? string | undefined : string — matching how TypedKvBinding / TypedAiBinding / JsonBinding are refined. The load-bearing comment above the map is updated; the import stays narrowed to the non-colliding Typed*Binding / JsonBinding / TextBinding / OptionalSecretBinding names.
  • convertToWranglerConfig() emits optional secrets as secrets.optional instead of secrets.required. Each key is omitted when empty, so an all-required Worker produces exactly the config it does today.
  • BindingSchema accepts optional: z.boolean().optional() on the secret variant.

InferEnv itself is unchanged — the | undefined flows through the existing mapped type.

@cloudflare/workers-utils

  • New secrets.optional?: string[] alongside secrets.required, validated the same way (validateOptionalTypedArray, added to the allowed-properties list so it no longer warns as unexpected).
  • Binding-name collision checking now covers optional secrets, so a name declared in both required and optional, or colliding with a var or another binding, is still an error.

wrangler

  • getVarsForDev() loads declared optional secrets from .dev.vars/.env/process.env exactly as required ones — this is the behaviour that would otherwise have regressed, since keys that are neither config vars nor declared secrets are dropped once secrets is set. Missing optional secrets are deliberately excluded from the Missing required secrets: warning; an absent optional secret is the expected state, so it stays silent. JSDoc updated accordingly.
  • wrangler types emits optional secrets as string | undefined rather than dropping them. The secrets record threaded through type generation already carried an unused value per key, so it now carries the type to emit.
  • The createTestHarness() override filter in dev.ts applies to optional as well as required, so a harness-supplied value is not re-loaded over the top.

addRequiredSecretsInheritBindings on the deploy path is deliberately unchanged — it reads secrets.required only, which is exactly right: optional secrets must not get an inherit binding, so deploying without them no longer fails.

| undefined, not an optional key

The key stays required and gains | undefined, rather than becoming SENTRY_TOKEN?: string:

  • It is the smaller change — InferEnv stays a plain mapped type with no key remapping.
  • It is what the reported need calls for: consumers must narrow before use either way.
  • Making the key optional would change keyof Env and interact with exactOptionalPropertyTypes for anyone building on the inferred type.

A type-level test pins keyof Env so this stays deliberate rather than accidental.

Required by default

bindings.secret() still means required. The counter-argument — that optional is the truthful type, since any secret can be missing at runtime — is real, and was considered and rejected:

  • Most secrets genuinely are required, and a required type surfaces a missing-secret bug at the point of use rather than pushing a narrowing burden onto every call site.
  • Required-by-default with an opt-out is the smaller change and needs no migration.

Overload nuance

Only a literal true selects the optional overload:

bindings.secret({ optional: true }); // → string | undefined
const opt = someCondition; // boolean
bindings.secret({ optional: opt }); // → string  (widened `boolean` picks the required overload)

The emitted config is still correct at runtime in both cases — it is only the inferred type that falls back to string. Worth knowing if you build binding options dynamically.

Audit of the other bindings.* helpers

The bug report asked whether other helpers have the same all-or-nothing treatment. They don't — secret is the only binding whose env member can be absent while the config declares it:

  • secretsStoreSecret — closest case. The value in the store can be absent, but the env member is always a SecretsStoreSecret handle; absence surfaces at .get(), so the env type is already honest.
  • Resource bindings (kv, r2, d1, queue, hyperdrive, vectorize, pipeline, analyticsEngineDataset, dispatchNamespace, mtlsCertificate, vpcService, vpcNetwork, sendEmail, agentMemory, aiSearch, artifacts) — a missing resource fails at deploy or at call time; the env member itself is always injected.
  • Runtime-provided bindings (ai, browser, images, media, stream, webSearch, rateLimit, flagship, versionMetadata, workerLoader, assets) — injected by the runtime whenever declared.
  • Cross-worker bindings (worker, durableObject, workflow) — the Fetcher/namespace is present even when the target is broken; failure is at call time.
  • json / text — inline literals in the config, always present.

Secret values are the only ones that live outside the config and the deploy payload entirely — set out-of-band via wrangler secret put or the dashboard, and removable independently of the code. No code change to the other helpers in this PR.

Tests

  • Type-level assertions through InferEnv for secret(), secret({ optional: false }) and secret({ optional: true }), plus a keyof Env assertion. These are checked by tsc — verified they actually fail when the expected type is wrong.
  • convertToWranglerConfig cases for the mixed, all-required and all-optional shapes.
  • getVarsForDev cases for the mixed Worker specifically: both secrets present → both in the dev bindings and no warning; optional absent → silent, required still loaded; required absent → still warns and does not mention the optional one; optional loaded from .env; undeclared keys still excluded when only optional secrets are declared.
  • wrangler types snapshot for secrets.optional emitting string | undefined.
  • Wrangler config validation cases for optional on its own, alongside required, wrong types, and a name appearing in both lists.

  • Tests
    • Tests included/updated
    • Automated tests not possible - manual testing has been completed as follows:
    • Additional testing not necessary because:
  • Public documentation
    • Cloudflare docs PR(s):
    • Documentation not necessary because: the cloudflare.config.ts format and @cloudflare/config are not publicly documented — the package is marked prerelease and its README states it "is not yet stable enough for external use — APIs may change without notice". Docs for bindings.secret() and secrets.optional should land as part of documenting the config format.

A picture of a cute animal (not mandatory, but encouraged)

`bindings.secret({ optional: true })` infers the env member as
`string | undefined` and excludes it from `secrets.required` in the
generated Wrangler config, so deploying a Worker that does not have the
secret set no longer fails. Secrets remain required by default.
@changeset-bot

changeset-bot Bot commented Aug 19, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d2209f1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
@cloudflare/config Minor
@cloudflare/workers-utils Minor
@cloudflare/vite-plugin Minor
wrangler Minor
@cloudflare/build-output-utils Patch
@cloudflare/autoconfig Patch
@cloudflare/cli-shared-helpers Patch
@cloudflare/deploy-helpers Patch
@cloudflare/remote-bindings Patch
@cloudflare/vitest-plugin Patch
@cloudflare/workers-auth Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@ask-bonk

ask-bonk Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

github run

@ask-bonk

ask-bonk Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

@penalosa Bonk workflow failed. Check the logs for details.

View workflow run · To retry, trigger Bonk again.

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

⚠️ Issues found

  1. Missing experimental feature opt-in note: The changeset includes an example using wrangler/experimental-config, indicating this is an experimental feature. However, it does not explicitly call out that the feature is experimental or provide a clear note on how users can opt in. Per changeset guidelines, "Changesets for experimental features should include note on how users can opt in." Consider adding a brief sentence such as: "This feature is part of the experimental cloudflare.config.ts system. To opt in, import bindings and defineWorker from wrangler/experimental-config."

@pkg-pr-new

pkg-pr-new Bot commented Aug 19, 2026

Copy link
Copy Markdown
@cloudflare/autoconfig

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/autoconfig@15273

@cloudflare/build-output-utils

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/build-output-utils@15273

@cloudflare/codemods

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/codemods@15273

@cloudflare/config

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/config@15273

create-cloudflare

npm i https://pkg.pr.new/cloudflare/workers-sdk/create-cloudflare@15273

@cloudflare/deploy-helpers

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/deploy-helpers@15273

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/kv-asset-handler@15273

miniflare

npm i https://pkg.pr.new/cloudflare/workers-sdk/miniflare@15273

@cloudflare/pages-functions

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/pages-functions@15273

@cloudflare/pages-shared

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/pages-shared@15273

@cloudflare/unenv-preset

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/unenv-preset@15273

@cloudflare/vite-plugin

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/vite-plugin@15273

@cloudflare/vitest-plugin

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/vitest-plugin@15273

@cloudflare/workers-auth

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/workers-auth@15273

@cloudflare/workers-editor-shared

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/workers-editor-shared@15273

@cloudflare/workers-utils

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/workers-utils@15273

wrangler

npm i https://pkg.pr.new/cloudflare/workers-sdk/wrangler@15273

commit: d2209f1

@penalosa
penalosa marked this pull request as ready for review August 19, 2026 23:20
@workers-devprod
workers-devprod requested review from a team and emily-shen and removed request for a team August 19, 2026 23:20
@workers-devprod

workers-devprod commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/olive-donkeys-shave.md: [@cloudflare/wrangler]
  • packages/config/src/tests/convert.test.ts: [@cloudflare/wrangler]
  • packages/config/src/tests/inference.test.ts: [@cloudflare/wrangler]
  • packages/config/src/bindings.ts: [@cloudflare/wrangler]
  • packages/config/src/convert.ts: [@cloudflare/wrangler]
  • packages/config/src/inference.ts: [@cloudflare/wrangler]
  • packages/config/src/public.ts: [@cloudflare/wrangler]
  • packages/config/src/schema.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/config/environment.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/config/validation.ts: [@cloudflare/wrangler]
  • packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/tests/dev/dev-vars.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/tests/type-generation.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/dev.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/dev/dev-vars.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/type-generation/index.ts: [@cloudflare/wrangler]

devin-ai-integration[bot]

This comment was marked as resolved.

…neration

Add `secrets.optional` to the Wrangler configuration and emit optional
secrets there instead of dropping them. Optional secrets are now loaded
from .dev.vars/.env in local dev without triggering the missing-secret
warning, are included in `wrangler types` as `string | undefined`, and
participate in binding-name collision validation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Untriaged

Development

Successfully merging this pull request may close these issues.

2 participants