Skip to content

fix(state): injective fqn encoding, output-file isolation, and name validation - #1075

Open
sam-goodwin wants to merge 1 commit into
mainfrom
claude/state-name-hardening
Open

fix(state): injective fqn encoding, output-file isolation, and name validation#1075
sam-goodwin wants to merge 1 commit into
mainfrom
claude/state-name-hardening

Conversation

@sam-goodwin

Copy link
Copy Markdown
Contributor

Fixes the three name-handling weaknesses found while writing the LocalState unit suite (#1072).

Injective fqn codec

encodeFqn escapes %, _, and \ before replacing / with __, so every __ in a filename is unambiguously a separator:

encodeFqn("foo/bar");   // "foo__bar"       (unchanged)
encodeFqn("foo__bar");  // "foo%5F%5Fbar"   (was "foo__bar" — collided)
decodeFqn(encodeFqn(fqn)) === fqn; // for every fqn

Fqns without _/%/\ — including all /-separated namespaced fqns — produce the same filename as before, so most existing state is read in place. For the rest, the stores fall back and migrate:

  • get falls back to the legacy filename when the new one is absent
  • set writes the new filename and removes the legacy one
  • delete removes both

encodeFqnLegacy preserves the old encoding for that fallback.

__stack_output__ isolation

A resource with fqn __stack_output__ now encodes to its own file (%5F%5Fstack%5Foutput%5F%5F.json) instead of clobbering the stack-output bookkeeping object, and it appears in list. The legacy fallback explicitly refuses to read or delete the bookkeeping file as a resource.

Stack/stage name validation

All backends embed stack/stage as path segments (local directory tree, S3 keys, HTTP routes), where a name like .. escapes the store — deleteStack({ stack: ".." }) would have removed .alchemy wholesale. A shared withValidatedNames wrapper now guards every operation on the local, S3, and HTTP-client stores:

validateStateName("stack", "..");  // StateStoreError
// rejected: "", ".", "..", and names containing "/" or "\"

The Cloudflare store is covered via the HTTP client; InMemoryService (test util, plain Map keys) is unwrapped.

Compatibility

  • Reads are fully backward compatible (legacy fallback + identical filenames for the common cases).
  • Forward compatibility caveat: state written by this version for fqns containing _/%/\ uses the escaped filename, which an older CLI would not find. Same-direction upgrades are unaffected.

Covered by 6 new codec tests in test/FQN.test.ts and 7 new/updated LocalState tests (legacy migration, output-file isolation, validation); verified green against the live S3 and Cloudflare state store suites.

🤖 Generated with Claude Code

…ation

- encodeFqn now escapes %/_/\ before replacing / with __, making the
  filename codec injective: foo__bar and foo/bar no longer collide, and
  a resource named __stack_output__ no longer clobbers the stack output.
  encodeFqnLegacy keeps the old encoding for read-fallback: get falls
  back to the legacy filename, set/delete clean it up, so state written
  by older versions stays readable and migrates on write.
- withValidatedNames wraps every backend (local, S3, HTTP client) so
  stack/stage names that would escape or restructure the store layout
  (empty, ".", "..", path separators) fail with a typed
  StateStoreError instead of touching paths outside the store.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alchemy-version-bot

Copy link
Copy Markdown
Contributor

Install the packages built from this commit:

alchemy

bun add alchemy@https://pkg.ing/alchemy/79a4b91

@alchemy.run/better-auth

bun add @alchemy.run/better-auth@https://pkg.ing/@alchemy.run/better-auth/79a4b91

@alchemy.run/pr-package

bun add @alchemy.run/pr-package@https://pkg.ing/@alchemy.run/pr-package/79a4b91

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.

1 participant