Skip to content

feat(auth): add migrateLegacyAuth helper (4/4)#28

Merged
scottlovegrove merged 1 commit into
mainfrom
feat/keyring-migrate
May 16, 2026
Merged

feat(auth): add migrateLegacyAuth helper (4/4)#28
scottlovegrove merged 1 commit into
mainfrom
feat/keyring-migrate

Conversation

@scottlovegrove
Copy link
Copy Markdown
Collaborator

Stack position: 4 / 4 — base is #27 (feat/keyring-token-store-multi-account). Replaces the last part of #24.

Summary

Generic one-time helper that walks a v1 single-user keyring + plaintext config slot into the v2 multi-account UserRecordStore + per-user keyring slots. Builds on writeRecordWithKeyringFallback from #27 so the migration write path is the same as runtime set(): keyring → fallbackToken on SecureStoreUnavailableError, with rollback on upsert failure.

Best-effort throughout: identify-failure, keyring-write-failure, and upsert-failure all return a skipped status without touching the v1 state, so the consumer's runtime fallback can keep serving the legacy token until the next attempt. Default promotion, legacy keyring delete, and cleanupLegacyConfig are all best-effort outside the critical section.

stderr output uses a fixed phrase keyed off an internal SkipReason enum so consumer-supplied exception text (which may contain emails, paths, or auth diagnostics) can't leak into logs. The raw detail is still attached to MigrateAuthResult.reason for in-process callers.

Usage

import { migrateLegacyAuth } from '@doist/cli-core/auth'

await migrateLegacyAuth<Account>({
    serviceName: 'todoist-cli',
    legacyAccount: 'api-token',
    userRecords,
    loadLegacyPlaintextToken: async () => readConfig().api_token ?? null,
    identifyAccount: async (token) => fetchUser(token),
    cleanupLegacyConfig: async () => clearLegacyAuthFields(),
    silent: true,
    logPrefix: 'todoist-cli',
})

Test plan

  • npm run type-check, npm run check, npm test (336 passing, +8 for migrate covering already-migrated / no-legacy-state / online keyring / plaintext fallback / fully-offline keyring / identify-failure / two privacy assertions)
  • Smoke from a todoist-cli postinstall against a v1 install.

🤖 Generated with Claude Code

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 final piece of the multi-account auth migration by adding a helper to transition legacy single-user states into the new UserRecordStore. The implementation provides a thoughtful, best-effort fallback mechanism that gracefully handles runtime errors while aiming to protect sensitive user data. However, there are a few areas to refine, such as ensuring the migration is strictly idempotent to prevent stale tokens from triggering accidental re-logins if the initial cleanup fails. Additional adjustments are noted around tightening the return type with a discriminated union, running I/O cleanup operations concurrently, refining how unavailable keyrings are handled, and removing unreachable skip reasons or potentially sensitive log fields.

Share FeedbackReview Logs

Comment thread src/auth/keyring/migrate.ts Outdated
Comment thread src/auth/keyring/migrate.ts
Comment thread src/auth/keyring/migrate.ts
Comment thread src/auth/keyring/migrate.ts Outdated
Comment thread src/auth/keyring/migrate.ts Outdated
Comment thread src/auth/keyring/migrate.ts Outdated
Comment thread src/auth/keyring/migrate.ts
Comment thread src/auth/keyring/migrate.test.ts
@scottlovegrove scottlovegrove force-pushed the feat/keyring-token-store-multi-account branch from 492efcf to d196148 Compare May 16, 2026 12:39
@scottlovegrove scottlovegrove force-pushed the feat/keyring-migrate branch from 469a73a to 856dc50 Compare May 16, 2026 12:40
@scottlovegrove scottlovegrove force-pushed the feat/keyring-token-store-multi-account branch from d196148 to 4c1b71a Compare May 16, 2026 13:51
@scottlovegrove scottlovegrove force-pushed the feat/keyring-migrate branch from 856dc50 to dbb9fe7 Compare May 16, 2026 13:52
@scottlovegrove scottlovegrove force-pushed the feat/keyring-token-store-multi-account branch from 4c1b71a to e808bd1 Compare May 16, 2026 14:11
@scottlovegrove scottlovegrove force-pushed the feat/keyring-migrate branch from dbb9fe7 to 6f11571 Compare May 16, 2026 14:11
@scottlovegrove scottlovegrove force-pushed the feat/keyring-token-store-multi-account branch 6 times, most recently from 76f4a3e to 5fb4526 Compare May 16, 2026 15:27
Base automatically changed from feat/keyring-token-store-multi-account to main May 16, 2026 15:30
@scottlovegrove scottlovegrove force-pushed the feat/keyring-migrate branch 3 times, most recently from ee4b302 to 0ef044b Compare May 16, 2026 15:49
@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 a well-structured helper for migrating legacy single-user authentication states into the new v2 multi-account architecture. The best-effort design provides a robust upgrade path while successfully avoiding credential leakage in the logs. A few adjustments are needed to preserve existing default accounts during retries, safely catch synchronous errors in cleanup callbacks, and properly export the new skip reason types. Additionally, refining the legacy store instantiation, updating the README examples to include config paths, and adding tests for custom account mappings and legacy precedence will help ensure the migration logic is completely solid.

Share FeedbackReview Logs

Comment thread src/auth/index.ts
Comment thread src/auth/keyring/migrate.ts Outdated
Comment thread src/auth/keyring/migrate.ts Outdated
Comment thread src/auth/keyring/migrate.ts Outdated
Comment thread README.md Outdated
Comment thread src/auth/keyring/migrate.test.ts
Comment thread src/auth/keyring/migrate.test.ts
Generic one-time helper that walks a v1 single-user keyring + plaintext
config slot into the v2 multi-account `UserRecordStore` + per-user
keyring slots. Builds on the shared `writeRecordWithKeyringFallback`
helper so the migration write path is the same as runtime `set()`:
keyring → fallbackToken on `SecureStoreUnavailableError`, with rollback
on upsert failure.

Best-effort throughout: identify-failure, keyring-write-failure, and
upsert-failure all return a `skipped` status without touching the v1
state, so the consumer's runtime fallback can keep serving the legacy
token until the next attempt. Default promotion + legacy keyring delete
+ `cleanupLegacyConfig` are all best-effort outside the critical
section.

stderr output uses a fixed phrase keyed off an internal `SkipReason`
enum so consumer-supplied exception text (which may contain emails,
paths, or auth diagnostics) can't leak into logs. The raw detail is
still attached to `MigrateAuthResult.reason` for in-process callers
(doctor commands, tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@scottlovegrove scottlovegrove force-pushed the feat/keyring-migrate branch from 0ef044b to 5c5559f Compare May 16, 2026 16:03
@scottlovegrove scottlovegrove merged commit cd7baee into main May 16, 2026
4 checks passed
@scottlovegrove scottlovegrove deleted the feat/keyring-migrate branch May 16, 2026 16:11
doist-release-bot Bot added a commit that referenced this pull request May 16, 2026
## [0.16.0](v0.15.0...v0.16.0) (2026-05-16)

### Features

* **auth:** add migrateLegacyAuth helper ([#28](#28)) ([cd7baee](cd7baee))
@doist-release-bot
Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 0.16.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