Skip to content

fix(cli): unwrap default export when loading stash.config.ts#375

Open
coderdan wants to merge 1 commit intomainfrom
dan/fix-stash-db-install-databaseurl
Open

fix(cli): unwrap default export when loading stash.config.ts#375
coderdan wants to merge 1 commit intomainfrom
dan/fix-stash-db-install-databaseurl

Conversation

@coderdan
Copy link
Copy Markdown
Contributor

@coderdan coderdan commented Apr 30, 2026

Closes #374.

Symptom

$ npx @cipherstash/cli db install
Error: Invalid stash.config.ts
  - databaseUrl: Invalid input: expected nonoptional, received undefined

Even when the user's stash.config.ts clearly sets databaseUrl from process.env.DATABASE_URL, and a console.log of the same env var prints the URL just before the error fires.

Root cause

loadStashConfig (packages/cli/src/config/index.ts) was constructing jiti with interopDefault: true and then calling jiti.import(...):

const jiti = createJiti(configPath, { interopDefault: true })
const rawConfig = await jiti.import(configPath)   // ← option silently ignored

In jiti 2.x, the constructor's interopDefault only applies to the deprecated synchronous jiti(id) callable form. The async jiti.import() ignores it and always returns the module namespace. So for export default defineConfig({...}), rawConfig is { default: { databaseUrl, client } }. Zod then validates the wrapper, finds no top-level databaseUrl, and emits the misleading "expected nonoptional, received undefined" error.

Verified empirically against jiti@2.6.1:

> jiti.import(configPath)                       → {"default":{"databaseUrl":"..."}}
> jiti.import(configPath, { default: true })    → {"databaseUrl":"..."}

Fix

Switch to the per-call { default: true } option (the jiti 2.x async-API way to ask for default-export unwrapping). Drop the now-misleading interopDefault: true from both loadStashConfig and the symmetric loadEncryptConfig call site (which wasn't symptom-bugged because it iterates Object.values to find the EncryptionClient — but consistency keeps the next reader from reaching the same wrong conclusion).

Why the existing test missed it

packages/cli/src/__tests__/config.test.ts mocks jiti.import to return the already-unwrapped shape:

mockJiti.import = vi.fn().mockResolvedValue({ databaseUrl: '...' })

So the test never exercised the real jiti behavior and would pass regardless of which option was set.

Regression test

New src/__tests__/config-jiti-integration.test.ts drives loadStashConfig against real jiti and a real stash.config.ts file written into a temp dir. Two cases:

  1. export default {...} is unwrapped to the inner config (the regression we're fixing).
  2. A genuinely-missing databaseUrl produces a useful error message containing Invalid stash.config.ts and databaseUrl.

The mocked config.test.ts stays in place for fast schema iteration.

Test plan

  • pnpm --filter @cipherstash/cli build clean
  • pnpm --filter @cipherstash/cli test — 78 pass (was 76; +2 from the new integration test)
  • biome check clean on changed files
  • Confirm the user's reproduction (npx @cipherstash/cli db install against a config that reads databaseUrl from process.env.DATABASE_URL) succeeds with this branch

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 30, 2026

⚠️ No Changeset found

Latest commit: e3fe3cd

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

Warning

Rate limit exceeded

@coderdan has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 28 minutes and 46 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 307ee702-d004-4a94-97d8-63014527c373

📥 Commits

Reviewing files that changed from the base of the PR and between 3d12510 and e3fe3cd.

📒 Files selected for processing (2)
  • packages/cli/src/__tests__/config-jiti-integration.test.ts
  • packages/cli/src/config/index.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dan/fix-stash-db-install-databaseurl

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 28 minutes and 46 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Closes #374. `loadStashConfig` was passing `interopDefault: true` to
`createJiti(...)`, but in jiti 2.x the constructor option only applies
to the deprecated synchronous `jiti(id)` callable form — the async
`jiti.import()` ignores it and always returns the full module
namespace. With `export default defineConfig({...})` that meant Zod
was validating `{ default: { databaseUrl, client } }` and emitting

  databaseUrl: Invalid input: expected nonoptional, received undefined

even though the user's config plainly set the field.

The jiti 2.x async API exposes a per-call `{ default: true }` option
that does work. Switch to it and drop the now-misleading constructor
option from both `loadStashConfig` and `loadEncryptConfig`.
`loadEncryptConfig` wasn't symptom-bugged (it iterates `Object.values`
to find the EncryptionClient, which flattens both shapes equally) but
keeping the two call sites consistent prevents the next reader from
reasoning their way to the same wrong conclusion.

Adds `config-jiti-integration.test.ts` — drives `loadStashConfig`
against real jiti and a real temp `stash.config.ts`. The existing
`config.test.ts` mocks `jiti.import` past the bug and so couldn't
catch wrap/unwrap regressions on its own.
Copy link
Copy Markdown
Contributor

@auxesis auxesis left a comment

Choose a reason for hiding this comment

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

LGTM, thanks for fixing this @coderdan.

Just curious how you want to test the last item on the checklist in the PR description?

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.

stash db install fails with "databaseUrl: received undefined" when reading from process.env

2 participants