[config] Add optional option to bindings.secret() - #15273
Open
penalosa wants to merge 4 commits into
Open
Conversation
`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 detectedLatest commit: d2209f1 The changes in this PR will be included in the next version bump. This PR includes changesets to release 11 packages
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 |
Contributor
Contributor
|
@penalosa Bonk workflow failed. Check the logs for details. View workflow run · To retry, trigger Bonk again. |
Contributor
|
@cloudflare/autoconfig
@cloudflare/build-output-utils
@cloudflare/codemods
@cloudflare/config
create-cloudflare
@cloudflare/deploy-helpers
@cloudflare/kv-asset-handler
miniflare
@cloudflare/pages-functions
@cloudflare/pages-shared
@cloudflare/unenv-preset
@cloudflare/vite-plugin
@cloudflare/vitest-plugin
@cloudflare/workers-auth
@cloudflare/workers-editor-shared
@cloudflare/workers-utils
wrangler
commit: |
penalosa
marked this pull request as ready for review
August 19, 2026 23:20
workers-devprod
requested review from
a team and
emily-shen
and removed request for
a team
August 19, 2026 23:20
Contributor
|
Codeowners approval required for this PR:
Show detailed file reviewers
|
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds an
optionaloption tobindings.secret()incloudflare.config.ts, plus a matchingsecrets.optionalfield in the Wrangler configuration, so a secret that may legitimately be absent at runtime can be declared in config and inferred asstring | 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 requiredstring, so code that correctly guards withif (env.TOKEN) { … }is typed as if the check were unnecessary, andsecrets.requiredmakes the first deploy of a Worker that lacks the secret fail outright.The only workaround was to leave the secret undeclared in
cloudflare.config.tsand hand-type it optional in a separateenv.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/configbindings.secret()takes an optional{ optional?: boolean }— it is still callable with no arguments and its runtime shape is unchanged ({ type: "secret" }).OptionalSecretBindinginterface (SecretBindingnarrowed tooptional: true), returned by an overload ofsecret()and exported from the public surface.BindingTypeMapmovessecretinto the parameterised group —TBinding extends OptionalSecretBinding ? string | undefined : string— matching howTypedKvBinding/TypedAiBinding/JsonBindingare refined. The load-bearing comment above the map is updated; the import stays narrowed to the non-collidingTyped*Binding/JsonBinding/TextBinding/OptionalSecretBindingnames.convertToWranglerConfig()emits optional secrets assecrets.optionalinstead ofsecrets.required. Each key is omitted when empty, so an all-required Worker produces exactly the config it does today.BindingSchemaacceptsoptional: z.boolean().optional()on the secret variant.InferEnvitself is unchanged — the| undefinedflows through the existing mapped type.@cloudflare/workers-utilssecrets.optional?: string[]alongsidesecrets.required, validated the same way (validateOptionalTypedArray, added to the allowed-properties list so it no longer warns as unexpected).requiredandoptional, or colliding with avaror another binding, is still an error.wranglergetVarsForDev()loads declared optional secrets from.dev.vars/.env/process.envexactly as required ones — this is the behaviour that would otherwise have regressed, since keys that are neither configvarsnor declared secrets are dropped oncesecretsis set. Missing optional secrets are deliberately excluded from theMissing required secrets:warning; an absent optional secret is the expected state, so it stays silent. JSDoc updated accordingly.wrangler typesemits optional secrets asstring | undefinedrather than dropping them. Thesecretsrecord threaded through type generation already carried an unused value per key, so it now carries the type to emit.createTestHarness()override filter indev.tsapplies tooptionalas well asrequired, so a harness-supplied value is not re-loaded over the top.addRequiredSecretsInheritBindingson the deploy path is deliberately unchanged — it readssecrets.requiredonly, which is exactly right: optional secrets must not get aninheritbinding, so deploying without them no longer fails.| undefined, not an optional keyThe key stays required and gains
| undefined, rather than becomingSENTRY_TOKEN?: string:InferEnvstays a plain mapped type with no key remapping.keyof Envand interact withexactOptionalPropertyTypesfor anyone building on the inferred type.A type-level test pins
keyof Envso 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:Overload nuance
Only a literal
trueselects the optional 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.*helpersThe bug report asked whether other helpers have the same all-or-nothing treatment. They don't —
secretis 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 aSecretsStoreSecrethandle; absence surfaces at.get(), so the env type is already honest.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.ai,browser,images,media,stream,webSearch,rateLimit,flagship,versionMetadata,workerLoader,assets) — injected by the runtime whenever declared.worker,durableObject,workflow) — theFetcher/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 putor the dashboard, and removable independently of the code. No code change to the other helpers in this PR.Tests
InferEnvforsecret(),secret({ optional: false })andsecret({ optional: true }), plus akeyof Envassertion. These are checked bytsc— verified they actually fail when the expected type is wrong.convertToWranglerConfigcases for the mixed, all-required and all-optional shapes.getVarsForDevcases 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 typessnapshot forsecrets.optionalemittingstring | undefined.optionalon its own, alongsiderequired, wrong types, and a name appearing in both lists.cloudflare.config.tsformat and@cloudflare/configare not publicly documented — the package is markedprereleaseand its README states it "is not yet stable enough for external use — APIs may change without notice". Docs forbindings.secret()andsecrets.optionalshould land as part of documenting the config format.A picture of a cute animal (not mandatory, but encouraged)