Skip to content

refactor(auth): adopt cli-core 0.16 keyring TokenStore#341

Merged
scottlovegrove merged 5 commits into
mainfrom
scottl/cli-core-keyring-auth
May 16, 2026
Merged

refactor(auth): adopt cli-core 0.16 keyring TokenStore#341
scottlovegrove merged 5 commits into
mainfrom
scottl/cli-core-keyring-auth

Conversation

@scottlovegrove
Copy link
Copy Markdown
Collaborator

@scottlovegrove scottlovegrove commented May 16, 2026

Summary

What gets deleted

  • src/lib/secure-store.ts — replaced by cli-core's createSecureStore / SecureStoreUnavailableError
  • The multi-account write path inside src/lib/auth.ts (upsertUser, clearApiToken, removeUserById, setDefaultUserId, loadTokenForStoredUser, ensureV2Shape, stripLegacyAuthFields, buildFallbackWarning, buildConfigCleanupWarning) — cli-core's createKeyringTokenStore owns this surface now
  • src/lib/auth.ts resolveLegacyToken + clearLegacyToken — the v1 fallback for unmigrated installs (see "Behavioural changes" below)
  • src/commands/auth/token-view.ts — replaced by cli-core's attachTokenViewCommand
  • Unused users.ts exports (getDefaultUser, getEffectiveDefaultUser, FindUserResult, UserRef)
  • ~14 paranoid / cli-core-covered tests across auth.test.ts, auth-store.test.ts (whole file), user-records.test.ts, migrate-auth.test.ts, and commands/auth/auth.test.ts (token-view + legacy-fallback cases)

What gets added

  • src/lib/user-records.tsUserRecordStore<TodoistAccount> adapter cli-core's createKeyringTokenStore reads/writes through. REPLACE-not-merge upsert; every write runs through ensureV2 (stamps config_version, strips legacy fields). Array manipulation delegates to users.ts.
  • src/lib/migrate-auth.ts rewrite — thin wrapper around cli-core's migrateLegacyAuth supplying the Todoist-specific callbacks (/api/v1/user identifyAccount, loadLegacyPlaintextToken, cleanupLegacyConfig). One-way gate is config.config_version === CONFIG_VERSION, which survives logout so a reinstall over a logged-out v2 install can't re-migrate a stale legacy slot.
  • src/commands/auth/store-wrap.ts (withUserRefAware) — shared wrap that substitutes getRequestedUserRef() into active/clear and turns ref-misses into UserNotFoundError. Used by both auth logout and auth token view because index.ts strips --user from argv before commander runs.
  • src/lib/config.ts stripLegacyAuthFields — single source of truth for the v1 fields scrubbed on every v2 write.
  • Adapter-level tests in user-records.test.ts (REPLACE-not-merge, ensureV2, default-pointer cleanup on remove) and migrate-auth.test.ts (the consumer callbacks + toLocalResult translation).

Behavioural changes

  • Dropped the v1 read + clear fallback. A user on an unmigrated v1 install whose postinstall never ran (offline first-run only) now has to run td auth login again instead of staying authenticated. Acceptable one-time UX cost — anyone who's seen ≥ one release with postinstall migration is already on v2.
  • td auth token no longer prints "Updated stored token for ..." vs "Saved token for ...". cli-core's TokenStore.set is void, so the verb distinction is dropped. Always prints "Saved token for <email>".
  • td user use no-such-user exits with ACCOUNT_NOT_FOUND (cli-core's code) instead of USER_NOT_FOUND. Same UX; different code string.

File map

File Status Role
src/lib/secure-store.ts deleted replaced by @doist/cli-core/auth.createSecureStore
src/commands/auth/token-view.ts deleted replaced by @doist/cli-core/auth.attachTokenViewCommand
src/lib/auth.ts gutted (320 → ~165 LOC) read-side only: resolveActiveUser, getApiToken, probeApiToken, getAuthMetadata, listStoredUsers, NoTokenError
src/lib/auth-store.ts rewritten createTodoistTokenStore() + shared SERVICE_NAME / LEGACY_ACCOUNT / accountForUser constants + toTodoistAccount
src/lib/migrate-auth.ts rewritten cli-core wrapper
src/lib/user-records.ts new UserRecordStore<TodoistAccount> adapter
src/commands/auth/store-wrap.ts new shared --user injection wrap
src/commands/auth/logout.ts rewritten wraps the store with withUserRefAware
src/lib/users.ts slimmed drops 4 unused exports
src/commands/user/{use,remove}.ts slimmed route directly through the store
CODEBASE.md updated auth-storage section rewritten

Test plan

  • npm run type-check, npm test (1564/1564), npm run check — all clean
  • Manual smoke on Linux with libsecret available: td auth login writes to user-<id> keyring slot, td auth logout removes it
  • Manual smoke on WSL without D-Bus: fallback warning on stderr, users[0].api_token set in config
  • Manual smoke v1 → v2 migration: hand-craft a v1 config ({ "api_token": "...", "auth_mode": "read-write" }), confirm postinstall migrates it and sets config_version: 2
  • Manual smoke td auth logout --user <ref>: ensure the explicit ref is honoured (regression covered by the review fixup)

🤖 Generated with Claude Code

Drop the local `secure-store`, `auth-store`, and `migrate-auth`
implementations in favour of `@doist/cli-core/auth`'s
`createKeyringTokenStore` + `migrateLegacyAuth` primitives. The Todoist
side keeps only the bits cli-core can't own: a `UserRecordStore`
adapter over the existing `config.users` array, the `/api/v1/user`
identify callback, and a thin read-side resolver (`resolveActiveUser`
and friends) that the SDK / uploads / stats / doctor still call into.

`auth.ts` shrinks from 538 lines to ~270 (read-side only); the
write-side commands (`auth token`, `user remove`, `user use`) route
through the shared `TodoistTokenStore` directly. v1 → v2 migration
moves to cli-core's helper with the consumer-supplied marker pinned
to `config_version === CONFIG_VERSION` so a logout-then-reinstall
can't re-migrate a stale legacy keyring entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@scottlovegrove scottlovegrove self-assigned this May 16, 2026
@doistbot doistbot requested a review from pedroalves0 May 16, 2026 16:23
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 effectively transitions the CLI's auth and token storage to the new @doist/cli-core primitives, drastically reducing local boilerplate and centralizing our keyring interactions. The architectural simplification is a great improvement for long-term maintainability, cleanly aligning our storage mechanisms with the core library. A few adjustments are needed to finalize the implementation, including restoring user-specific targeting and legacy cleanup during logout, resolving some logic duplication around user records and account shapes, filling test coverage gaps for the new adapter, and updating the codebase documentation to reflect the structural changes.

Share FeedbackReview Logs

Comment thread src/lib/auth.ts
Comment thread src/lib/auth.ts
Comment thread src/lib/user-records.ts Outdated
Comment thread src/lib/migrate-auth.ts Outdated
Comment thread src/lib/user-records.ts Outdated
Comment thread src/lib/migrate-auth.ts Outdated
Comment thread src/lib/user-records.ts
Comment thread src/lib/migrate-auth.test.ts Outdated
Comment thread src/commands/auth/auth.test.ts
Comment thread src/lib/auth-store.ts Outdated
scottlovegrove and others added 3 commits May 16, 2026 17:43
Apply the doistbot review pass on top of the cli-core 0.16.0 adoption:

- Restore `td auth logout --user <ref>` targeting. `index.ts` strips the
  flag from argv before commander runs, so cli-core's generic logout
  command can't recover it. The new `attachTodoistLogoutCommand` wraps
  the store to substitute `getRequestedUserRef()` and surfaces a typed
  `UserNotFoundError` on miss.
- Restore the legacy v1 clear path. `clearLegacyToken()` in `auth.ts`
  is invoked by the wrap when cli-core's v2 clear is a no-op (empty
  store + no explicit ref), so unmigrated installs can actually log
  out before postinstall migration runs.
- Pull the persisted-identifier constants (`SERVICE_NAME`,
  `LEGACY_ACCOUNT`, `accountForUser`) into `auth-store.ts` so the
  read/write/migrate paths share one source of truth for the keyring
  + config ABI.
- Pass `accountForUser` and a case-insensitive email `matchAccount`
  explicitly to `createKeyringTokenStore` so a cli-core default change
  can't silently break the persisted slot name or `--user <ref>` match
  rules.
- Reuse `users.ts` helpers (`upsertStoredUser`, `removeStoredUser`,
  `setDefaultUser`, new `clearDefaultUser`) and `toTodoistAccount` from
  `user-records.ts` instead of reinventing the array logic.
- Centralize `stripLegacyAuthFields` in `config.ts`, called from both
  the `UserRecordStore` adapter's `ensureV2` and the migration's
  `cleanupLegacyConfig`.
- Cache the config across cli-core's migrate callbacks so one
  postinstall run does one read+write pass instead of four.
- Add `src/lib/user-records.test.ts` to cover the REPLACE-not-merge
  upsert, the v2-shape strip on every write, and default-pointer
  cleanup on remove.
- Add `--user` / legacy-fallback / `--json` logout assertions and
  let `migrate-auth.test.ts` override the mocked cli-core return so
  the `toLocalResult` `migrated` / `skipped` branches are covered.
- Update `CODEBASE.md` for the new auth-storage architecture.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ed code

The cli-core 0.16.0 adoption left a lot of transitional code and
duplicated logic still in place. This pass deletes the surplus:

- Drop the v1 legacy fallback (read + clear). Postinstall migration
  has shipped for long enough that any v1 install has had multiple
  chances to migrate; an offline-first-run can re-`td auth login`.
  Removes `resolveLegacyToken`, `clearLegacyToken`, and the wrap's
  legacy fallback branch.
- Simplify `resolveActiveUser` — drop the unused `Config` import,
  inline `pickDefault`, collapse the multi-branch resolver to one
  ref-or-default discriminator.
- Replace the hand-rolled `commands/auth/token-view.ts` with
  cli-core's `attachTokenViewCommand`. Extract a shared
  `withUserRefAware` wrap (`commands/auth/store-wrap.ts`) so logout
  and token-view both thread `getRequestedUserRef()` into the store.
- Slim `commands/user/use.ts` to a direct `store.setDefault(ref)`
  call. cli-core throws `CliError('ACCOUNT_NOT_FOUND', …)` on miss
  natively — no pre-check needed.
- Slim `users.ts`: drop `getDefaultUser`, `getEffectiveDefaultUser`,
  `FindUserResult`, `UserRef` — all unused. The remaining helpers
  are the mutators the `UserRecordStore` adapter drives.
- Trim tests: drop the 4 legacy-fallback tests in `auth.test.ts`,
  the 4 token-view tests (cli-core covers them), and the paranoid
  `user-records.test.ts` cases that exercise transitive behaviour.
- Trim `CODEBASE.md` prose around the new architecture.

Net diff vs main: **+893 / −1826 = −933 LOC**.

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

@doistbot /review

@scottlovegrove scottlovegrove changed the title feat(auth): adopt cli-core 0.16.0 keyring TokenStore + migration refactor(auth): adopt cli-core 0.16 keyring TokenStore (−933 LOC) May 16, 2026
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 update successfully transitions our authentication setup to the new @doist/cli-core keyring primitives, replacing complex local implementations with a much cleaner adapter pattern. Shrinking this custom auth surface significantly improves long-term maintainability and standardizes our credential management. There are just a few details to refine before merging, primarily around preserving the environment variable short-circuit, avoiding crashes from offline keyring checks, safely wrapping the store instance without losing prototype methods, consolidating user-reference matching, and expanding test coverage to capture JSON outputs, migration gates, and specific flag interactions.

Share FeedbackReview Logs

Comment thread src/commands/auth/store-wrap.ts Outdated
Comment thread src/lib/auth-store.ts Outdated
Comment thread src/lib/auth-store.ts Outdated
Comment thread src/commands/auth/index.ts Outdated
Comment thread src/commands/auth/store-wrap.ts Outdated
Comment thread src/commands/auth/auth.test.ts Outdated
Comment thread src/commands/auth/auth.test.ts Outdated
Comment thread src/lib/migrate-auth.test.ts
- **P1 — env-token short-circuit restored.** Baked into
  `createTodoistTokenStore.active()` so `td auth status` still routes
  through `onNotAuthenticated` (and the metadata `source: 'env'`
  reporting matches the displayed account) when `TODOIST_API_TOKEN`
  is set alongside a stored user. `TOKEN_ENV_VAR` moved to
  `auth-store.ts` (re-exported from `auth.ts` for back-compat).
- **P2 — existence check via `store.list()`.** The wrap was using
  `store.active(ref)` to check whether `--user <ref>` matched a
  stored account; that loads the token and can throw
  `SecureStoreUnavailableError` on an offline keyring, which would
  crash `td auth logout --user <ref>` instead of letting it clear
  the broken credential.
- **P2 — shared `--user` matcher.** Extracted `matchUserRef()` into
  `users.ts` and routed both `findUserByRef()` and the keyring
  store's `matchAccount` through it, so the two paths can no longer
  drift (the previous copies disagreed on whether to trim the ref).
- **P2 — wrap survives prototype methods.**
  `Object.assign(Object.create(store), …)` instead of `…store`
  spread, so any cli-core method that's later moved onto a prototype
  still resolves via the prototype chain.
- **P3 — single `refAware` instance.** Hoisted to
  `registerAuthCommand()`; logout no longer re-wraps internally.
- **P2 — JSON envelope positive assertion.** The `--json` logout test
  now asserts `"ok": true` is present in addition to the negative
  "Stored token removed" check.
- **P2 — `auth token view --user <ref>` coverage.** Two new tests
  for `withUserRefAware.active()`: ref matched → token printed; ref
  missed → `USER_NOT_FOUND`.
- **P2 — `hasMigrated` / `markMigrated` coverage.** Three new tests
  for the durable migration gate: true on `config_version === 2`,
  false on pre-v2, `markMigrated` stamps the gate without touching
  legacy fields (that's `cleanupLegacyConfig`'s job).

Net vs main: +1014 / −1810 = −796 LOC.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@scottlovegrove scottlovegrove added the 👀 Show PR PR must be reviewed before or after merging label May 16, 2026
@scottlovegrove scottlovegrove changed the title refactor(auth): adopt cli-core 0.16 keyring TokenStore (−933 LOC) refactor(auth): adopt cli-core 0.16 keyring TokenStore May 16, 2026
@scottlovegrove scottlovegrove merged commit 98065d0 into main May 16, 2026
7 checks passed
@scottlovegrove scottlovegrove deleted the scottl/cli-core-keyring-auth branch May 16, 2026 19:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

👀 Show PR PR must be reviewed before or after merging

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants