Skip to content

refactor(auth): add UserRecordStore adapter over current flat config (foundation for cli-core keyring store) - #229

Merged
scottlovegrove merged 3 commits into
mainfrom
feat/auth-user-records-adapter
May 17, 2026
Merged

refactor(auth): add UserRecordStore adapter over current flat config (foundation for cli-core keyring store)#229
scottlovegrove merged 3 commits into
mainfrom
feat/auth-user-records-adapter

Conversation

@scottlovegrove

@scottlovegrove scottlovegrove commented May 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

Foundation PR for adopting cli-core's createKeyringTokenStore. PR β will swap twist's custom TwistTokenStore + lib/auth.ts probe/save/clear over to cli-core's keyring-backed store; this PR adds the UserRecordStore adapter the swap will hand to cli-core.

  • New src/lib/user-records.tscreateTwistUserRecordStore() returns a UserRecordStore<TwistAccount> that translates twist's current flat config fields (authUserId / authUserName / authMode / authScope / token) into cli-core's UserRecord list shape. Single-user today: list() returns at most one record; upsert and remove write or strip those flat fields; getDefaultId / setDefaultId map to authUserId.
  • No consumer wiredTwistTokenStore and auth.ts probe/save/clear stay exactly as they are. Zero behaviour change.
  • No on-disk schema change — the adapter writes the same flat fields twist already persists. Existing users see nothing different after this lands; the future swap (PR β) keeps the schema unchanged too via the same adapter.
  • token is surfaced as fallbackToken (cli-core's name for the plaintext copy persisted when the OS keyring is unreachable at write time) — preserves twist's existing keyring-fallback behaviour.

Test plan

  • npm run type-check
  • npm run lint:check
  • npm test — 10 new adapter tests (list / upsert / remove / getDefaultId / setDefaultId, happy + edge cases including replace-not-merge for the token slot); 624 total pass
  • npm run build
  • N/A manual smoke — no consumer wired, no user-facing behaviour change.

Follow-up (PR β)

  • Replace createTwistTokenStore with createKeyringTokenStore({ serviceName: 'twist-cli', accountForUser: () => 'api-token', userRecords: createTwistUserRecordStore(), recordsLocation: getConfigPath() }).
  • Override accountForUser to the existing 'api-token' keyring slot so tokens already in users' keychains stay readable — no keyring slot migration needed.
  • Delete lib/auth.ts probe/save/clear; rewire callers (config view, doctor, api, etc.) to use store.active() / store.set() / store.clear() directly.
  • Keep a thin getApiToken() shim with TWIST_API_TOKEN env precedence (cli-core's keyring store doesn't know about the env var).

🤖 Generated with Claude Code

Foundation for the upcoming swap to cli-core's createKeyringTokenStore
(PR β follows). The adapter translates twist's current flat config
fields (authUserId / authUserName / authMode / authScope / token) into
cli-core's UserRecord list shape without changing the on-disk schema.

- list() returns at most one record built from the flat fields.
- upsert() writes the flat fields, replacing the token with the
  record's fallbackToken (or stripping it when absent, per cli-core's
  replace-not-merge contract).
- remove(id) clears the auth fields when the id matches; no-op
  otherwise.
- getDefaultId / setDefaultId map to authUserId; null clears it.

No consumer yet — the adapter is unused. Zero behaviour change.
Existing TwistTokenStore + auth.ts probe/save/clear stay in place
until PR β swaps them out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@doistbot
doistbot requested a review from nats12 May 16, 2026 21:20
@scottlovegrove scottlovegrove self-assigned this May 16, 2026
Drop six tests that only verified single-line code (`list` empty branch,
field-rename happy paths, single-line accessor getters). The remaining
four lock in the cli-core contracts the consumer relies on:

- round-trip via upsert + list (covers both happy paths in one test)
- upsert replace-not-merge for the token slot
- remove no-op on mismatch
- setDefaultId(null) preserves non-auth fields

Any other regression would surface in PR β's integration tests against
the live `KeyringTokenStore`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@scottlovegrove scottlovegrove changed the title feat(auth): add UserRecordStore adapter over current flat config (foundation for cli-core keyring store) refactor(auth): add UserRecordStore adapter over current flat config (foundation for cli-core keyring store) May 16, 2026
@scottlovegrove

Copy link
Copy Markdown
Collaborator Author

@doistbot /review

@doistbot doistbot left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This PR introduces the UserRecordStore adapter to map the existing flat auth configuration into cli-core's expected structure, laying a solid groundwork for the upcoming keyring migration. The approach safely prepares for the transition without altering the current on-disk schema. A few adjustments are needed before the final swap, specifically to preserve legacy token-only authentication sessions, ensure atomic configuration updates, and consolidate duplicated token normalization and state management logic.

Share FeedbackReview Logs

Comment thread src/lib/user-records.ts Outdated
Comment thread src/lib/user-records.ts Outdated
Comment thread src/lib/user-records.ts Outdated
Comment thread src/lib/user-records.ts Outdated
Comment thread src/lib/user-records.ts Outdated
Comment thread src/lib/user-records.ts Outdated
Comment thread src/lib/user-records.ts Outdated
Comment thread src/lib/user-records.ts Outdated
Comment thread src/lib/user-records.test.ts
P1: legacy `tw auth token` users have no `authUserId` but do have
`authMode` (+ a token in the keyring). Previously `buildRecords`
returned `[]` for them, which would orphan their keyring entry once
PR β wires the adapter into `KeyringTokenStore` — effectively a
silent logout on upgrade. Now synthesises a record with `id: ''`
when any auth metadata or token is present.

Also addressed:
- Switch get+setConfig to atomic `updateConfig({field: undefined})`,
  reducing the read-write race window from full-config to just the
  auth fields.
- Trim the persisted `fallbackToken` to match `saveApiToken`'s
  normalisation.
- `setDefaultId` is now a no-op for the single-user adapter: clearing
  via `null` would orphan the token + label, and setting an unknown
  id would synthesise a phantom record (would surface as a blank-label
  account on the next `list()`).
- Short-circuit `remove` / `setDefaultId` to skip the disk write when
  the operation is a no-op.
- Simplify `getDefaultId` to read the id from the synthesised record
  (correctly returns `''` for the legacy token-only case).

Tests updated: kept round-trip, replace-not-merge, remove-mismatch;
added token-only synthesis, remove-matching, and `setDefaultId`
no-op coverage (six tests total).

Declined the shared-serializer nit: the existing
`createTwistTokenStore().set()` path goes away in PR β, so abstracting
a shared `TwistAccount → config` mapping now would couple two code
paths that only briefly coexist between α and β.

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 17, 2026
@scottlovegrove
scottlovegrove merged commit 15f0228 into main May 17, 2026
5 checks passed
@scottlovegrove
scottlovegrove deleted the feat/auth-user-records-adapter branch May 17, 2026 07:38
@doist-release-bot

Copy link
Copy Markdown
Contributor

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

released 👀 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