Skip to content

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

Closed
coderdan wants to merge 1 commit intomainfrom
dan/bug-272-stash-db-install-databaseurl
Closed

fix(cli): unwrap default export when loading stash.config.ts#373
coderdan wants to merge 1 commit intomainfrom
dan/bug-272-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: 68d82f5

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 36 minutes and 12 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: 98682fb4-de18-46ee-95e9-2e5d6ff4eee4

📥 Commits

Reviewing files that changed from the base of the PR and between 0e781b8 and 68d82f5.

📒 Files selected for processing (2)
  • packages/cli/src/__tests__/config-jiti-integration.test.ts
  • packages/cli/src/config/index.ts
📝 Walkthrough

Walkthrough

New integration test suite validates jiti default-export unwrapping for loading stash.config.ts, with tests covering successful configuration parsing and error handling. Simultaneously, jiti configuration module refactored to apply default-export unwrapping via per-call options rather than constructor options.

Changes

Cohort / File(s) Summary
Integration Tests
packages/cli/src/__tests__/config-jiti-integration.test.ts
New test suite for jiti configuration loading, provisioning temporary project directories and verifying that loadStashConfig() correctly unwraps default exports and handles missing environment variables with appropriate error logging.
Jiti Configuration
packages/cli/src/config/index.ts
Updated jiti invocation: added { default: true } option per-call for stash.config.ts loading, removed interopDefault: true constructor option from both stash.config and encrypt.config loaders.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • auxesis

Poem

🐰 Jiti hops with options now, per call instead of code,
Default exports unwrapped smooth along the config road,
Tests confirm the rabbit's way—no namespaces astray!
Configuration loading fixed—hip hip, hooray! 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: unwrapping the default export when loading stash.config.ts, which directly addresses the root cause documented in the PR objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dan/bug-272-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 36 minutes and 12 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.
@coderdan coderdan force-pushed the dan/bug-272-stash-db-install-databaseurl branch from 0e781b8 to 68d82f5 Compare April 30, 2026 10:20
@coderdan coderdan closed this Apr 30, 2026
@coderdan coderdan deleted the dan/bug-272-stash-db-install-databaseurl branch April 30, 2026 10:22
@coderdan
Copy link
Copy Markdown
Contributor Author

coderdan commented Apr 30, 2026

Replaced by #375 — branch was renamed (dan/bug-272-…dan/fix-stash-db-install-databaseurl) to keep internal issue codes out of public refs, and GitHub's branch-rename API closed this PR rather than retargeting it. Same commit, same diff.

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

1 participant