Skip to content

fix(core): resolve Config values in input props - #566

Merged
sam-goodwin merged 5 commits into
mainfrom
john/handle-dev-config
Jun 9, 2026
Merged

fix(core): resolve Config values in input props#566
sam-goodwin merged 5 commits into
mainfrom
john/handle-dev-config

Conversation

@john-royal

@john-royal john-royal commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Config values are valid Inputs, but generic input resolution never handled them. resolveInput walked the Config object with Object.entries, stripping its prototype down to its enumerable parse property — so providers received { parse } instead of the configured value (and Config.isConfig returned false on it downstream).

This went unnoticed for a Worker's env because those are resolved out of band by the bindings mechanism (yieldable values are resolved before binding metadata is derived). It surfaced when a user set dev.port to a Config — the Input type says that's allowed, but nothing actually resolved it.

Resolve Config against the deploy environment during input resolution instead of walking it:

// Plan.ts resolveInput / Output.ts evaluate
} else if (Config.isConfig(input)) {
  return yield* resolveInput(yield* input);
} else if (Duration.isDuration(input) || Redacted.isRedacted(input)) {
  // opaque — Redacted stays wrapped for the secrecy boundary
  return input;
}

Resolving in plan (rather than per-provider) also means the concrete value participates in diffing/hashing — an opaque Config hashes the same regardless of the underlying value, so env changes never triggered an update. Config.redacted resolves to a Redacted, which stays wrapped end-to-end.

Notes

  • Redacted and Duration are unchanged (still opaque).
  • Raw Effect inputs are intentionally left unresolved for now.
  • Dropped the now-dead defensive Config handling in the Cloudflare Worker provider (Worker.ts vite define, WorkerAsyncBindings.ts toBinding).
  • Output.evaluate / resolveInput now surface Config.ConfigError, which the deploy pipeline already threads.

Tests

  • Output.test.ts: new Config block — top-level / nested / array resolution, resolution against a ConfigProvider, and ConfigRedacted staying wrapped.
  • plan.test.ts: new block driving the resolveInput path via plan.resources.*.props.

@john-royal
john-royal force-pushed the john/handle-dev-config branch from ee5d81d to 1ccab5c Compare June 7, 2026 00:27
@alchemy-version-bot

alchemy-version-bot Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Install the packages built from this commit:

alchemy

bun add alchemy@https://pkg.ing/alchemy/32e9ed0

@alchemy.run/better-auth

bun add @alchemy.run/better-auth@https://pkg.ing/@alchemy.run/better-auth/32e9ed0

@alchemy.run/pr-package

bun add @alchemy.run/pr-package@https://pkg.ing/@alchemy.run/pr-package/32e9ed0

@john-royal
john-royal marked this pull request as ready for review June 7, 2026 00:50
@john-royal
john-royal requested a review from sam-goodwin June 7, 2026 00:50
Comment on lines +85 to 86
} else if (Redacted.isRedacted(binding)) {
const val = Redacted.value(binding);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This now type checks without needing a separate helper 🎉

val evaluates to type Json.

@john-royal john-royal changed the title fix: handle Config in input props fix(core): handle Config values in input props Jun 7, 2026
@john-royal john-royal changed the title fix(core): handle Config values in input props fix(core): resolve Config values in input props Jun 7, 2026
typeof Redacted.value(value) === "string"
? Redacted.value(value)
: Config.isConfig(value) || Effect.isEffect(value)
: Effect.isEffect(value)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is a Config an Effect?

@sam-goodwin
sam-goodwin merged commit aa5844f into main Jun 9, 2026
9 checks passed
@sam-goodwin
sam-goodwin deleted the john/handle-dev-config branch June 9, 2026 17:20
agcty added a commit to agcty/alchemy-effect that referenced this pull request Jun 11, 2026
Input<T> admits Effect<T> for any prop, but neither resolveInput
(Plan.ts) nor Output.evaluate (Output.ts) had a branch for raw
Effects: plan-time diffing saw the unresolved Effect object (tripping
providers' isResolved guards into conservative updates) and apply-time
evaluation shredded it via the generic object walk into a plain
`{"~effect/Effect/args": ...}` object that reached provider APIs —
e.g. the Worker provider's domain reconciliation, where a
stage-conditional `domain: Stack.useSync(...)` crashed the Cloudflare
client's schema validation, and a value that should have resolved to
`undefined` was truthy and entered reconciliation anyway.

Resolve raw Effects in both walkers, mirroring how Config values are
resolved (alchemy-run#566). Function-form Effects (resource classes, effectClass
constructors, Binding.Policy tags) stay opaque: they are class
references carried in props (e.g. Worker `exports`), not per-field
values.

Fixes alchemy-run#588

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
agcty added a commit to agcty/alchemy-effect that referenced this pull request Jun 11, 2026
Input<T> admits Effect<T> for any prop, but neither resolveInput
(Plan.ts) nor Output.evaluate (Output.ts) had a branch for raw
Effects: plan-time diffing saw the unresolved Effect object (tripping
providers' isResolved guards into conservative updates) and apply-time
evaluation shredded it via the generic object walk into a plain
`{"~effect/Effect/args": ...}` object that reached provider APIs —
e.g. the Worker provider's domain reconciliation, where a
stage-conditional `domain: Stack.useSync(...)` crashed the Cloudflare
client's schema validation, and a value that should have resolved to
`undefined` was truthy and entered reconciliation anyway.

Resolve raw Effects in both walkers, mirroring how Config values are
resolved (alchemy-run#566), with two carve-outs for Effects that are resource
references rather than per-field values:

- Function-form Effects (resource classes, effectClass constructors,
  Binding.Policy tags) stay opaque: they are class references carried
  in props (e.g. Worker `exports`).
- Non-class resource references (`const db = Hyperdrive("db", ...)`)
  are object-form Effects. The Resource constructor now brands them
  (`alchemy/ResourceEffect`) and the walkers leave them opaque:
  executing one outside the construction phase re-derives its FQN from
  the ambient namespace (none at plan/apply time) and mints a phantom
  resource — `MissingSourceError: Source db not found` when a Worker's
  env holds `ADMIN_DB: db`. Branded references also count as resolved
  in Diff.isResolved so providers' custom diffs keep running, and the
  Worker vite env mapping skips them.

Plain Effects that wrap a resource reference (`AI_GATEWAY_ID:
Effect.map(aiGateway, g => g.gatewayId)`) cannot be detected without
executing them, so the Resource constructor additionally memoizes its
result per stack: re-execution during input resolution returns the
resource registered during construction instead of re-deriving the FQN,
and the resulting attribute expression resolves through the normal
Output machinery.

Fixes alchemy-run#588

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DavidJFelix pushed a commit to DavidJFelix/alchemy-effect that referenced this pull request Aug 7, 2026
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.

2 participants