Skip to content

feat(auth): add createSecureStore keyring primitive (1/4)#25

Merged
scottlovegrove merged 2 commits into
mainfrom
feat/keyring-primitive
May 16, 2026
Merged

feat(auth): add createSecureStore keyring primitive (1/4)#25
scottlovegrove merged 2 commits into
mainfrom
feat/keyring-primitive

Conversation

@scottlovegrove
Copy link
Copy Markdown
Collaborator

Stack position: 1 / 4 — replaces the monolithic #24.

Summary

  • Thin cross-platform wrapper around @napi-rs/keyring (Keychain / Credential Manager / libsecret) exposed as createSecureStore from @doist/cli-core/auth.
  • Every failure mode (missing native binary, libsecret offline on WSL/headless Linux, locked Keychain, …) is normalised into SecureStoreUnavailableError.
  • @napi-rs/keyring declared in cli-core's own optionalDependencies so it arrives transitively — consumer CLIs don't add it themselves.
  • Dynamic import so a missing native binary surfaces as the typed error instead of crashing module load.

Why this is a stack

The original PR (#24) was ~1900 LoC across primitive, multi-account TokenStore, migration helper, and attacher behavioural change. Splitting into 4 reviewable pieces:

  1. This PR — keyring primitive (createSecureStore). Usable on its own; todoist-cli / twist-cli can drop their duplicate secure-store.ts against just this.
  2. feat/auth-store-read-failedAUTH_STORE_READ_FAILED error code + logout --user tolerance so consumer TokenStores can throw it without breaking logout.
  3. feat/keyring-token-store-multi-accountcreateKeyringTokenStore multi-account factory + UserRecordStore port + shared write helper.
  4. feat/keyring-migratemigrateLegacyAuth v1→v2 helper.

Test plan

  • npm run type-check
  • npm run check
  • npm test (304 passing — 3 new tests for the primitive)
  • Smoke npm link into a downstream CLI, force SecureStoreUnavailableError by unsetting DBUS_SESSION_BUS_ADDRESS in WSL.

🤖 Generated with Claude Code

Thin cross-platform wrapper around `@napi-rs/keyring` exposed from the
`./auth` subpath as a building block for any consumer that needs OS-keyring
backed token storage. Every failure mode (missing native binary on an
exotic arch, libsecret unreachable on headless Linux / WSL without D-Bus,
Keychain locked, …) is normalised into `SecureStoreUnavailableError`. The
`@napi-rs/keyring` import is dynamic so a missing binary doesn't crash
module load before the typed error can surface.

Declared in cli-core's own `optionalDependencies` so it ships transitively
to consumers without them having to add it themselves.

This is the first slice of a 4-PR series that splits the original
keyring-backed multi-account TokenStore work into independently reviewable
units: primitive → AUTH_STORE_READ_FAILED + logout tolerance →
multi-account TokenStore → migration helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Member

@doistbot doistbot left a comment

Choose a reason for hiding this comment

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

This PR successfully introduces the createSecureStore keyring primitive, laying a solid foundation for the multi-account auth stack. The dynamic import approach and normalization of platform-specific failures into a unified SecureStoreUnavailableError will significantly streamline downstream CLI integrations. There are a few opportunities to clean up dead exports, consolidate repetitive error handling and entry initialization, preserve underlying error causes, and refine the test suite to safely handle mocks and comprehensively cover empty and failure states.

Share FeedbackReview Logs

Comment thread src/auth/keyring/secure-store.ts Outdated
Comment thread src/auth/index.ts Outdated
Comment thread src/auth/keyring/secure-store.ts Outdated
Comment thread src/auth/keyring/secure-store.ts Outdated
Comment thread src/auth/keyring/secure-store.ts Outdated
Comment thread src/auth/keyring/secure-store.ts
Comment thread src/auth/keyring/secure-store.test.ts Outdated
Comment thread src/auth/keyring/secure-store.test.ts
Comment thread src/auth/keyring/secure-store.test.ts Outdated
@scottlovegrove
Copy link
Copy Markdown
Collaborator Author

@doistbot /review

Copy link
Copy Markdown
Member

@doistbot doistbot left a comment

Choose a reason for hiding this comment

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

This PR introduces the foundational createSecureStore keyring primitive to wrap @napi-rs/keyring and gracefully standardize authentication storage failures. The implementation cleanly abstracts the native binary dependencies and sets a solid groundwork for the upcoming multi-account token store. A few minor adjustments have been flagged regarding an unused export and test suite robustness, specifically around resetting mock implementations and verifying the rejected promise memoisation.

Share FeedbackReview Logs

Comment thread src/auth/keyring/secure-store.ts Outdated
Comment thread src/auth/keyring/secure-store.test.ts Outdated
Comment thread src/auth/keyring/secure-store.test.ts Outdated
- Drop the unused `DEFAULT_ACCOUNT_FOR_USER` const — no caller in this
  diff reaches for it; a future change that needs a shared per-user
  slug helper can add it back where the consumer lands.
- Drop the public `SECURE_STORE_DESCRIPTION` export. The constant is
  still defined for internal use within `secure-store.ts` (it composes
  the fallback warning string elsewhere in the module), but it is no
  longer reachable from `@doist/cli-core/auth` until a real public
  caller asks for it.
- Extract a `withEntry` helper inside `createSecureStore` so the
  per-method try/catch/getEntry boilerplate collapses into one place,
  and memoise the dynamic-import + `AsyncEntry` construction in a
  per-store `entryPromise` so repeated reads/writes share one entry
  and a missing native binary fast-fails on subsequent calls.
- `toUnavailableError` now uses the shared `getErrorMessage` helper
  from `src/errors.ts` and attaches the original throwable as `cause`
  on `SecureStoreUnavailableError`, so platform-specific stack/native
  error codes aren't lost on the way to the consumer.
- Tests rework: a single hoisted `vi.mock` with a getter checks a
  `throwOnImport` toggle on every access — replaces `vi.doUnmock`
  cleanup that was leaving the real keyring exposed to any test added
  later. Adds back the empty-slot `getSecret` case and a parameterised
  failure test covering `getSecret` / `setSecret` / `deleteSecret`
  (each asserts the cause is preserved). Adds a memoisation test that
  locks the entry-construction caching.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@scottlovegrove scottlovegrove force-pushed the feat/keyring-primitive branch from 165fb07 to 5b0cfd7 Compare May 16, 2026 13:50
@scottlovegrove scottlovegrove merged commit 30bcbd7 into main May 16, 2026
4 checks passed
@scottlovegrove scottlovegrove deleted the feat/keyring-primitive branch May 16, 2026 13:57
doist-release-bot Bot added a commit that referenced this pull request May 16, 2026
## [0.13.0](v0.12.0...v0.13.0) (2026-05-16)

### Features

* **auth:** add createSecureStore keyring primitive (1/4) ([#25](#25)) ([30bcbd7](30bcbd7))
@doist-release-bot
Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 0.13.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants