refactor(auth): adopt cli-core 0.16 keyring TokenStore#341
Conversation
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>
doistbot
left a comment
There was a problem hiding this comment.
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.
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>
|
@doistbot /review |
doistbot
left a comment
There was a problem hiding this comment.
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.
- **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>
Summary
What gets deleted
src/lib/secure-store.ts— replaced by cli-core'screateSecureStore/SecureStoreUnavailableErrorsrc/lib/auth.ts(upsertUser,clearApiToken,removeUserById,setDefaultUserId,loadTokenForStoredUser,ensureV2Shape,stripLegacyAuthFields,buildFallbackWarning,buildConfigCleanupWarning) — cli-core'screateKeyringTokenStoreowns this surface nowsrc/lib/auth.tsresolveLegacyToken+clearLegacyToken— the v1 fallback for unmigrated installs (see "Behavioural changes" below)src/commands/auth/token-view.ts— replaced by cli-core'sattachTokenViewCommandusers.tsexports (getDefaultUser,getEffectiveDefaultUser,FindUserResult,UserRef)auth.test.ts,auth-store.test.ts(whole file),user-records.test.ts,migrate-auth.test.ts, andcommands/auth/auth.test.ts(token-view + legacy-fallback cases)What gets added
src/lib/user-records.ts—UserRecordStore<TodoistAccount>adapter cli-core'screateKeyringTokenStorereads/writes through. REPLACE-not-mergeupsert; every write runs throughensureV2(stampsconfig_version, strips legacy fields). Array manipulation delegates tousers.ts.src/lib/migrate-auth.tsrewrite — thin wrapper around cli-core'smigrateLegacyAuthsupplying the Todoist-specific callbacks (/api/v1/useridentifyAccount,loadLegacyPlaintextToken,cleanupLegacyConfig). One-way gate isconfig.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 substitutesgetRequestedUserRef()intoactive/clearand turns ref-misses intoUserNotFoundError. Used by bothauth logoutandauth token viewbecauseindex.tsstrips--userfrom argv before commander runs.src/lib/config.tsstripLegacyAuthFields— single source of truth for the v1 fields scrubbed on every v2 write.user-records.test.ts(REPLACE-not-merge,ensureV2, default-pointer cleanup on remove) andmigrate-auth.test.ts(the consumer callbacks +toLocalResulttranslation).Behavioural changes
td auth loginagain instead of staying authenticated. Acceptable one-time UX cost — anyone who's seen ≥ one release with postinstall migration is already on v2.td auth tokenno longer prints "Updated stored token for ..." vs "Saved token for ...". cli-core'sTokenStore.setisvoid, so the verb distinction is dropped. Always prints "Saved token for <email>".td user use no-such-userexits withACCOUNT_NOT_FOUND(cli-core's code) instead ofUSER_NOT_FOUND. Same UX; different code string.File map
src/lib/secure-store.ts@doist/cli-core/auth.createSecureStoresrc/commands/auth/token-view.ts@doist/cli-core/auth.attachTokenViewCommandsrc/lib/auth.tsresolveActiveUser,getApiToken,probeApiToken,getAuthMetadata,listStoredUsers,NoTokenErrorsrc/lib/auth-store.tscreateTodoistTokenStore()+ sharedSERVICE_NAME/LEGACY_ACCOUNT/accountForUserconstants +toTodoistAccountsrc/lib/migrate-auth.tssrc/lib/user-records.tsUserRecordStore<TodoistAccount>adaptersrc/commands/auth/store-wrap.ts--userinjection wrapsrc/commands/auth/logout.tswithUserRefAwaresrc/lib/users.tssrc/commands/user/{use,remove}.tsCODEBASE.mdTest plan
npm run type-check,npm test(1564/1564),npm run check— all cleantd auth loginwrites touser-<id>keyring slot,td auth logoutremoves itusers[0].api_tokenset in config{ "api_token": "...", "auth_mode": "read-write" }), confirm postinstall migrates it and setsconfig_version: 2td auth logout --user <ref>: ensure the explicit ref is honoured (regression covered by the review fixup)🤖 Generated with Claude Code