@doist/cli-core 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +97 -14
  3. package/dist/auth/index.d.ts +2 -2
  4. package/dist/auth/index.d.ts.map +1 -1
  5. package/dist/auth/index.js +1 -1
  6. package/dist/auth/index.js.map +1 -1
  7. package/dist/auth/keyring/index.d.ts +5 -0
  8. package/dist/auth/keyring/index.d.ts.map +1 -1
  9. package/dist/auth/keyring/index.js +2 -0
  10. package/dist/auth/keyring/index.js.map +1 -1
  11. package/dist/auth/keyring/migrate.d.ts +93 -0
  12. package/dist/auth/keyring/migrate.d.ts.map +1 -0
  13. package/dist/auth/keyring/migrate.js +143 -0
  14. package/dist/auth/keyring/migrate.js.map +1 -0
  15. package/dist/auth/keyring/record-write.d.ts +34 -0
  16. package/dist/auth/keyring/record-write.d.ts.map +1 -0
  17. package/dist/auth/keyring/record-write.js +50 -0
  18. package/dist/auth/keyring/record-write.js.map +1 -0
  19. package/dist/auth/keyring/secure-store.d.ts +13 -0
  20. package/dist/auth/keyring/secure-store.d.ts.map +1 -1
  21. package/dist/auth/keyring/secure-store.js +13 -0
  22. package/dist/auth/keyring/secure-store.js.map +1 -1
  23. package/dist/auth/keyring/token-store.d.ts +58 -0
  24. package/dist/auth/keyring/token-store.d.ts.map +1 -0
  25. package/dist/auth/keyring/token-store.js +240 -0
  26. package/dist/auth/keyring/token-store.js.map +1 -0
  27. package/dist/auth/keyring/types.d.ts +48 -0
  28. package/dist/auth/keyring/types.d.ts.map +1 -0
  29. package/dist/auth/keyring/types.js +2 -0
  30. package/dist/auth/keyring/types.js.map +1 -0
  31. package/dist/auth/user-flag.d.ts +3 -0
  32. package/dist/auth/user-flag.d.ts.map +1 -1
  33. package/dist/auth/user-flag.js +5 -1
  34. package/dist/auth/user-flag.js.map +1 -1
  35. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [0.16.0](https://github.com/Doist/cli-core/compare/v0.15.0...v0.16.0) (2026-05-16)
2
+
3
+ ### Features
4
+
5
+ * **auth:** add migrateLegacyAuth helper ([#28](https://github.com/Doist/cli-core/issues/28)) ([cd7baee](https://github.com/Doist/cli-core/commit/cd7baeebdb9b3dfc5909862a0af7ae5bfee17723))
6
+
7
+ ## [0.15.0](https://github.com/Doist/cli-core/compare/v0.14.0...v0.15.0) (2026-05-16)
8
+
9
+ ### Features
10
+
11
+ * **auth:** add createKeyringTokenStore (multi-account) ([#27](https://github.com/Doist/cli-core/issues/27)) ([adc07d1](https://github.com/Doist/cli-core/commit/adc07d1830657471bc945a5b2a5599f81a066ee3))
12
+
1
13
  ## [0.14.0](https://github.com/Doist/cli-core/compare/v0.13.0...v0.14.0) (2026-05-16)
2
14
 
3
15
  ### Features
package/README.md CHANGED
@@ -12,20 +12,20 @@ npm install @doist/cli-core
12
12
 
13
13
  ## What's in it
14
14
 
15
- | Module | Key exports | Purpose |
16
- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
17
- | `auth` (subpath) | `attachLoginCommand`, `attachLogoutCommand`, `attachStatusCommand`, `attachTokenViewCommand`, `runOAuthFlow`, `createPkceProvider`, `createSecureStore`, PKCE helpers, `AuthProvider` / `TokenStore` / `AccountRef` / `SecureStore` types, `AttachLogoutRevokeContext` | OAuth runtime plus the Commander attachers for `<cli> [auth] login` / `logout` / `status` / `token`. `attachLogoutCommand` accepts an optional `revokeToken` hook for best-effort server-side token revocation. Ships the standard public-client PKCE flow (`createPkceProvider`) and a thin cross-platform OS-keyring wrapper (`createSecureStore`) backed by `@napi-rs/keyring`. `AuthProvider` and `TokenStore` are the escape hatches for DCR, OS-keychain, multi-account, etc. consumers implement `TokenStore` directly (single-user store implements `list` / `setDefault` trivially against the one account). `logout` / `status` / `token` always attach `--user <ref>` and thread the parsed ref to `store.active(ref)` (and `store.clear(ref)` on `logout`). `commander` (when using the attachers), `open` (browser launch), and `@napi-rs/keyring` (when using `createSecureStore`) are optional peer/optional deps. |
18
- | `commands` (subpath) | `registerChangelogCommand`, `registerUpdateCommand` (+ semver helpers) | Commander wiring for cli-core's standard commands (e.g. `<cli> changelog`, `<cli> update`, `<cli> update switch`). **Requires** `commander` as an optional peer-dep. |
19
- | `config` | `getConfigPath`, `readConfig`, `readConfigStrict`, `writeConfig`, `updateConfig`, `CoreConfig`, `UpdateChannel` | Read / write a per-CLI JSON config file with typed error codes; `CoreConfig` is the shape of fields cli-core itself owns (extend it for per-CLI fields). |
20
- | `empty` | `printEmpty` | Print an empty-state message gated on `--json` / `--ndjson` so machine consumers never see human strings on stdout. |
21
- | `errors` | `CliError` | Typed CLI error class with `code` and exit-code mapping. |
22
- | `global-args` | `parseGlobalArgs`, `stripUserFlag`, `createGlobalArgsStore`, `createAccessibleGate`, `createSpinnerGate`, `getProgressJsonlPath`, `isProgressJsonlEnabled` | Parse well-known global flags (`--json`, `--ndjson`, `--quiet`, `--verbose`, `--accessible`, `--no-spinner`, `--progress-jsonl`, `--user <ref>`) and derive predicates from them. `stripUserFlag` removes `--user` tokens from argv so the cleaned array can be forwarded to Commander when the flag has no root-program attachment. |
23
- | `json` | `formatJson`, `formatNdjson` | Stable JSON / newline-delimited JSON formatting for stdout. |
24
- | `markdown` (subpath) | `preloadMarkdown`, `renderMarkdown`, `TerminalRendererOptions` | Lazy-init terminal markdown renderer. **Requires** `marked` and `marked-terminal-renderer` as peer-deps — install only if your CLI uses this subpath. |
25
- | `options` | `ViewOptions` | Type contract for `{ json?, ndjson? }` per-command options that machine-output gates derive from. |
26
- | `spinner` | `createSpinner` | Loading spinner factory wrapping `yocto-spinner` with disable gates. |
27
- | `terminal` | `isCI`, `isStderrTTY`, `isStdinTTY`, `isStdoutTTY` | TTY / CI detection helpers. |
28
- | `testing` (subpath) | `describeEmptyMachineOutput` | Vitest helpers reusable by consuming CLIs (e.g. parametrised empty-state suite covering `--json` / `--ndjson` / human modes). |
15
+ | Module | Key exports | Purpose |
16
+ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
17
+ | `auth` (subpath) | `attachLoginCommand`, `attachLogoutCommand`, `attachStatusCommand`, `attachTokenViewCommand`, `runOAuthFlow`, `createPkceProvider`, `createSecureStore`, `createKeyringTokenStore`, `migrateLegacyAuth`, PKCE helpers, `AuthProvider` / `TokenStore` / `AccountRef` / `SecureStore` / `UserRecordStore` types, `AttachLogoutRevokeContext` | OAuth runtime plus the Commander attachers for `<cli> [auth] login` / `logout` / `status` / `token`. `attachLogoutCommand` accepts an optional `revokeToken` hook for best-effort server-side token revocation. Ships the standard public-client PKCE flow (`createPkceProvider`), a thin cross-platform OS-keyring wrapper (`createSecureStore`), and a multi-account keyring-backed `TokenStore` (`createKeyringTokenStore`) that stores secrets in the OS credential manager and degrades to plaintext in the consumer's config when the keyring is unavailable (WSL/headless Linux/containers). `AuthProvider` and `TokenStore` remain the escape hatches for DCR or fully bespoke backends. `logout` / `status` / `token` always attach `--user <ref>` and thread the parsed ref to `store.active(ref)` (and `store.clear(ref)` on `logout`). `commander` (when using the attachers), `open` (browser launch), and `@napi-rs/keyring` (when using `createSecureStore` or the keyring `TokenStore`) are optional peer/optional deps. |
18
+ | `commands` (subpath) | `registerChangelogCommand`, `registerUpdateCommand` (+ semver helpers) | Commander wiring for cli-core's standard commands (e.g. `<cli> changelog`, `<cli> update`, `<cli> update switch`). **Requires** `commander` as an optional peer-dep. |
19
+ | `config` | `getConfigPath`, `readConfig`, `readConfigStrict`, `writeConfig`, `updateConfig`, `CoreConfig`, `UpdateChannel` | Read / write a per-CLI JSON config file with typed error codes; `CoreConfig` is the shape of fields cli-core itself owns (extend it for per-CLI fields). |
20
+ | `empty` | `printEmpty` | Print an empty-state message gated on `--json` / `--ndjson` so machine consumers never see human strings on stdout. |
21
+ | `errors` | `CliError` | Typed CLI error class with `code` and exit-code mapping. |
22
+ | `global-args` | `parseGlobalArgs`, `stripUserFlag`, `createGlobalArgsStore`, `createAccessibleGate`, `createSpinnerGate`, `getProgressJsonlPath`, `isProgressJsonlEnabled` | Parse well-known global flags (`--json`, `--ndjson`, `--quiet`, `--verbose`, `--accessible`, `--no-spinner`, `--progress-jsonl`, `--user <ref>`) and derive predicates from them. `stripUserFlag` removes `--user` tokens from argv so the cleaned array can be forwarded to Commander when the flag has no root-program attachment. |
23
+ | `json` | `formatJson`, `formatNdjson` | Stable JSON / newline-delimited JSON formatting for stdout. |
24
+ | `markdown` (subpath) | `preloadMarkdown`, `renderMarkdown`, `TerminalRendererOptions` | Lazy-init terminal markdown renderer. **Requires** `marked` and `marked-terminal-renderer` as peer-deps — install only if your CLI uses this subpath. |
25
+ | `options` | `ViewOptions` | Type contract for `{ json?, ndjson? }` per-command options that machine-output gates derive from. |
26
+ | `spinner` | `createSpinner` | Loading spinner factory wrapping `yocto-spinner` with disable gates. |
27
+ | `terminal` | `isCI`, `isStderrTTY`, `isStdinTTY`, `isStdoutTTY` | TTY / CI detection helpers. |
28
+ | `testing` (subpath) | `describeEmptyMachineOutput` | Vitest helpers reusable by consuming CLIs (e.g. parametrised empty-state suite covering `--json` / `--ndjson` / human modes). |
29
29
 
30
30
  ## Usage
31
31
 
@@ -315,6 +315,89 @@ Every failure mode — `@napi-rs/keyring` failing to load on an arch without a p
315
315
 
316
316
  `@napi-rs/keyring` is declared in cli-core's own `optionalDependencies`, so npm pulls it in transitively when you install `@doist/cli-core` — your consumer CLI does not need to add it explicitly. The library ships pre-built native binaries for Windows (Credential Manager), macOS (Keychain), and Linux glibc + musl (libsecret / Secret Service).
317
317
 
318
+ #### Multi-account keyring-backed `TokenStore`
319
+
320
+ `createKeyringTokenStore` wires `createSecureStore` into the `TokenStore` contract for multi-account CLIs. Secrets live in the OS credential manager; per-user metadata stays in the consumer's config via a small `UserRecordStore` port the consumer implements. When the keyring is unreachable the store transparently falls back to a `fallbackToken` field on the user record and exposes a warning on `getLastStorageResult()` for the login command to surface.
321
+
322
+ ```ts
323
+ import { createKeyringTokenStore, type UserRecordStore } from '@doist/cli-core/auth'
324
+
325
+ type Account = { id: string; label?: string; email: string }
326
+
327
+ // Adapter over the consumer's existing config.json shape.
328
+ const userRecords: UserRecordStore<Account> = {
329
+ async list() {
330
+ /* read from config */
331
+ },
332
+ async upsert(record) {
333
+ /* replace, do not merge — see UserRecordStore docs */
334
+ },
335
+ async remove(id) {
336
+ /* … */
337
+ },
338
+ async getDefaultId() {
339
+ /* … */
340
+ },
341
+ async setDefaultId(id) {
342
+ /* … */
343
+ },
344
+ describeLocation() {
345
+ return '~/.config/todoist-cli/config.json'
346
+ },
347
+ }
348
+
349
+ export const tokenStore = createKeyringTokenStore<Account>({
350
+ serviceName: 'todoist-cli',
351
+ userRecords,
352
+ })
353
+
354
+ // In your login command's onSuccess:
355
+ const storage = tokenStore.getLastStorageResult()
356
+ if (storage?.warning) console.error('Warning:', storage.warning)
357
+ ```
358
+
359
+ The returned store satisfies the full `TokenStore` contract — including `list()` / `setDefault(ref)` / `ref`-aware `active` / `clear` — so it plugs straight into the `logout` / `status` / `token` attachers. Default ref matching is `account.id === ref || account.label === ref`; override `matchAccount` to broaden it (e.g. case-insensitive email).
360
+
361
+ When a matching record exists but the keyring read fails, `active(ref)` throws `CliError('AUTH_STORE_READ_FAILED', …)`. `attachLogoutCommand` catches it specifically so `logout --user <ref>` still clears the local record even with the keyring offline; status / token-view propagate it because they can't render without the token.
362
+
363
+ For sync/lazy-decrypt or fully bespoke backends, implement `TokenStore` directly.
364
+
365
+ For one-time migration of a v1 single-user token into the v2 multi-user shape, use `migrateLegacyAuth` from a postinstall hook. The helper requires a durable **migration marker** the consumer owns — a boolean persisted in their config — so the migration is genuinely one-way: a later `logout` (which empties `userRecords`) followed by a reinstall won't re-migrate a stale legacy token.
366
+
367
+ ```ts
368
+ import { getConfigPath, readConfig, updateConfig } from '@doist/cli-core'
369
+ import { migrateLegacyAuth } from '@doist/cli-core/auth'
370
+
371
+ const configPath = getConfigPath('todoist-cli')
372
+
373
+ const result = await migrateLegacyAuth<Account>({
374
+ serviceName: 'todoist-cli',
375
+ legacyAccount: 'api-token',
376
+ userRecords,
377
+ // Durable one-way gate. Persist `migrated_v2: true` in your config
378
+ // after a successful migration; check it on every run.
379
+ hasMigrated: async () =>
380
+ (await readConfig<{ migrated_v2?: boolean }>(configPath)).migrated_v2 === true,
381
+ markMigrated: async () =>
382
+ updateConfig<{ migrated_v2: boolean }>(configPath, { migrated_v2: true }),
383
+ loadLegacyPlaintextToken: async () =>
384
+ (await readConfig<{ api_token?: string }>(configPath)).api_token ?? null,
385
+ identifyAccount: async (token) => fetchUser(token),
386
+ cleanupLegacyConfig: async () => clearLegacyAuthFields(configPath),
387
+ silent: true,
388
+ logPrefix: 'todoist-cli',
389
+ })
390
+
391
+ if (result.status === 'skipped' && result.reason === 'legacy-keyring-unreachable') {
392
+ // Retryable — the next postinstall run with the keyring online will
393
+ // pick up where this one left off.
394
+ }
395
+ ```
396
+
397
+ `MigrateAuthResult` is a discriminated union on `status` (`'already-migrated' | 'no-legacy-state' | 'migrated' | 'skipped'`). `migrated` carries the resolved `account`; `skipped` carries a stable `reason` (`'identify-failed' | 'legacy-keyring-unreachable' | 'user-record-write-failed' | 'marker-write-failed'`) plus a free-form `detail`.
398
+
399
+ The helper is best-effort throughout: any failure (offline keyring, network error fetching the user, upsert blip) leaves the v1 state untouched so the consumer's runtime fallback can keep serving the legacy token until the next attempt. `markMigrated()` is called **before** the legacy keyring delete + `cleanupLegacyConfig`, so cleanup failures can't cause re-migration on the next run — the marker is the one-way gate, not cleanup success. The legacy delete and `cleanupLegacyConfig` run concurrently via `Promise.allSettled`. stderr output uses fixed phrases keyed off `MigrateSkipReason` and the success log omits the account identifier entirely so consumer-supplied error text (and PII-shaped `account.id` values like emails) can't leak into logs.
400
+
318
401
  #### `--user <ref>` and multi-user wiring
319
402
 
320
403
  The three account-touching attachers (`attachLogoutCommand` / `attachStatusCommand` / `attachTokenViewCommand`) always attach `--user <ref>` on their subcommand. `attachLogoutCommand` threads the parsed ref to both `store.active(ref)` and `store.clear(ref)`; `attachStatusCommand` and `attachTokenViewCommand` only call `store.active(ref)`. When `--user` is supplied but `store.active(ref)` returns `null`, each attacher throws `CliError('ACCOUNT_NOT_FOUND', …)` so the user sees a typed miss rather than `NOT_AUTHENTICATED` or a silent `✓ Logged out`. Single-user stores returning `null` for a non-matching ref is the supported way to feed this guard.
@@ -14,6 +14,6 @@ export type { GenerateVerifierOptions } from './pkce.js';
14
14
  export { createPkceProvider } from './providers/pkce.js';
15
15
  export type { PkceLazyString, PkceProviderOptions } from './providers/pkce.js';
16
16
  export type { AccountRef, AuthAccount, AuthorizeInput, AuthorizeResult, AuthProvider, ExchangeInput, ExchangeResult, PrepareInput, PrepareResult, TokenStore, ValidateInput, } from './types.js';
17
- export { SecureStoreUnavailableError, createSecureStore } from './keyring/index.js';
18
- export type { CreateSecureStoreOptions, SecureStore } from './keyring/index.js';
17
+ export { SecureStoreUnavailableError, createKeyringTokenStore, createSecureStore, migrateLegacyAuth, } from './keyring/index.js';
18
+ export type { CreateKeyringTokenStoreOptions, CreateSecureStoreOptions, KeyringTokenStore, MigrateAuthResult, MigrateLegacyAuthOptions, MigrateSkipReason, SecureStore, TokenStorageLocation, TokenStorageResult, UserRecord, UserRecordStore, } from './keyring/index.js';
19
19
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACxC,YAAY,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAA;AACxE,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAC/C,YAAY,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAC/E,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AACjD,YAAY,EACR,0BAA0B,EAC1B,mBAAmB,EACnB,yBAAyB,GAC5B,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AACjD,YAAY,EAAE,0BAA0B,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AAClF,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAA;AACxD,YAAY,EAAE,6BAA6B,EAAE,MAAM,iBAAiB,CAAA;AACpE,OAAO,EACH,yBAAyB,EACzB,eAAe,EACf,aAAa,EACb,gBAAgB,GACnB,MAAM,WAAW,CAAA;AAClB,YAAY,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAA;AACxD,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AACxD,YAAY,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAC9E,YAAY,EACR,UAAU,EACV,WAAW,EACX,cAAc,EACd,eAAe,EACf,YAAY,EACZ,aAAa,EACb,cAAc,EACd,YAAY,EACZ,aAAa,EACb,UAAU,EACV,aAAa,GAChB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,2BAA2B,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AACnF,YAAY,EAAE,wBAAwB,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACxC,YAAY,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAA;AACxE,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAC/C,YAAY,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAC/E,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AACjD,YAAY,EACR,0BAA0B,EAC1B,mBAAmB,EACnB,yBAAyB,GAC5B,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AACjD,YAAY,EAAE,0BAA0B,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AAClF,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAA;AACxD,YAAY,EAAE,6BAA6B,EAAE,MAAM,iBAAiB,CAAA;AACpE,OAAO,EACH,yBAAyB,EACzB,eAAe,EACf,aAAa,EACb,gBAAgB,GACnB,MAAM,WAAW,CAAA;AAClB,YAAY,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAA;AACxD,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AACxD,YAAY,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAC9E,YAAY,EACR,UAAU,EACV,WAAW,EACX,cAAc,EACd,eAAe,EACf,YAAY,EACZ,aAAa,EACb,cAAc,EACd,YAAY,EACZ,aAAa,EACb,UAAU,EACV,aAAa,GAChB,MAAM,YAAY,CAAA;AACnB,OAAO,EACH,2BAA2B,EAC3B,uBAAuB,EACvB,iBAAiB,EACjB,iBAAiB,GACpB,MAAM,oBAAoB,CAAA;AAC3B,YAAY,EACR,8BAA8B,EAC9B,wBAAwB,EACxB,iBAAiB,EACjB,iBAAiB,EACjB,wBAAwB,EACxB,iBAAiB,EACjB,WAAW,EACX,oBAAoB,EACpB,kBAAkB,EAClB,UAAU,EACV,eAAe,GAClB,MAAM,oBAAoB,CAAA"}
@@ -5,5 +5,5 @@ export { attachStatusCommand } from './status.js';
5
5
  export { attachTokenViewCommand } from './token-view.js';
6
6
  export { DEFAULT_VERIFIER_ALPHABET, deriveChallenge, generateState, generateVerifier, } from './pkce.js';
7
7
  export { createPkceProvider } from './providers/pkce.js';
8
- export { SecureStoreUnavailableError, createSecureStore } from './keyring/index.js';
8
+ export { SecureStoreUnavailableError, createKeyringTokenStore, createSecureStore, migrateLegacyAuth, } from './keyring/index.js';
9
9
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAExC,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAE/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AAMjD,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AAEjD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAA;AAExD,OAAO,EACH,yBAAyB,EACzB,eAAe,EACf,aAAa,EACb,gBAAgB,GACnB,MAAM,WAAW,CAAA;AAElB,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AAexD,OAAO,EAAE,2BAA2B,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAExC,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAE/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AAMjD,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AAEjD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAA;AAExD,OAAO,EACH,yBAAyB,EACzB,eAAe,EACf,aAAa,EACb,gBAAgB,GACnB,MAAM,WAAW,CAAA;AAElB,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AAexD,OAAO,EACH,2BAA2B,EAC3B,uBAAuB,EACvB,iBAAiB,EACjB,iBAAiB,GACpB,MAAM,oBAAoB,CAAA"}
@@ -1,3 +1,8 @@
1
1
  export { SecureStoreUnavailableError, createSecureStore } from './secure-store.js';
2
2
  export type { CreateSecureStoreOptions, SecureStore } from './secure-store.js';
3
+ export { createKeyringTokenStore } from './token-store.js';
4
+ export type { CreateKeyringTokenStoreOptions, KeyringTokenStore } from './token-store.js';
5
+ export { migrateLegacyAuth } from './migrate.js';
6
+ export type { MigrateAuthResult, MigrateLegacyAuthOptions, MigrateSkipReason } from './migrate.js';
7
+ export type { TokenStorageLocation, TokenStorageResult, UserRecord, UserRecordStore, } from './types.js';
3
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/auth/keyring/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,2BAA2B,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAClF,YAAY,EAAE,wBAAwB,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/auth/keyring/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,2BAA2B,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAClF,YAAY,EAAE,wBAAwB,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAE9E,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAA;AAC1D,YAAY,EAAE,8BAA8B,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAEzF,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAChD,YAAY,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAElG,YAAY,EACR,oBAAoB,EACpB,kBAAkB,EAClB,UAAU,EACV,eAAe,GAClB,MAAM,YAAY,CAAA"}
@@ -1,2 +1,4 @@
1
1
  export { SecureStoreUnavailableError, createSecureStore } from './secure-store.js';
2
+ export { createKeyringTokenStore } from './token-store.js';
3
+ export { migrateLegacyAuth } from './migrate.js';
2
4
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/auth/keyring/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,2BAA2B,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/auth/keyring/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,2BAA2B,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAGlF,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAA;AAG1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA"}
@@ -0,0 +1,93 @@
1
+ import type { AuthAccount } from '../types.js';
2
+ import type { UserRecordStore } from './types.js';
3
+ export type MigrateLegacyAuthOptions<TAccount extends AuthAccount> = {
4
+ serviceName: string;
5
+ /** Legacy single-user keyring account slug, e.g. `'api-token'`. */
6
+ legacyAccount: string;
7
+ /** v2 user-record store the migrated record is written into. */
8
+ userRecords: UserRecordStore<TAccount>;
9
+ /** Per-user keyring slug for the new entry. Defaults to `user-${id}`. */
10
+ accountForUser?: (id: string) => string;
11
+ /**
12
+ * Reads the durable "migration already ran" marker the consumer owns
13
+ * (typically a flag in their config). When this returns `true`, the
14
+ * helper short-circuits with `already-migrated` and never touches the
15
+ * legacy state. This is the **one-way gate** — without it, a later
16
+ * `logout` (which empties `userRecords`) followed by a reinstall would
17
+ * cause the helper to re-migrate a stale legacy token.
18
+ */
19
+ hasMigrated: () => Promise<boolean>;
20
+ /**
21
+ * Persists the durable migration marker. Called **after** the v2 record
22
+ * write succeeds and **before** legacy cleanup so the gate is set before
23
+ * any best-effort follow-up runs. If this throws, the helper returns a
24
+ * `skipped` result with reason `marker-write-failed` — the v2 record is
25
+ * already on disk, but the marker isn't, so the caller should retry.
26
+ */
27
+ markMigrated: () => Promise<void>;
28
+ /**
29
+ * Returns the v1 token from the consumer's *plaintext* config slot, or
30
+ * `null` if absent. cli-core handles the legacy keyring slot itself.
31
+ */
32
+ loadLegacyPlaintextToken: () => Promise<string | null>;
33
+ /**
34
+ * Identifies the user behind the v1 token. Implementations typically hit
35
+ * the product API with the token to fetch the canonical `id` / `email`
36
+ * for the new account record.
37
+ */
38
+ identifyAccount: (token: string) => Promise<TAccount>;
39
+ /**
40
+ * Optional best-effort cleanup of v1-only config fields after a
41
+ * successful migration (e.g. unset legacy `api_token` / `auth_mode`).
42
+ * Runs concurrently with the legacy keyring delete; failures are
43
+ * swallowed because the marker (above) is what gates re-migration.
44
+ */
45
+ cleanupLegacyConfig?: () => Promise<void>;
46
+ /** Suppress stderr output (postinstall hooks set this). */
47
+ silent?: boolean;
48
+ /** Label used in the stderr log line. Defaults to `'cli'`. */
49
+ logPrefix?: string;
50
+ };
51
+ /**
52
+ * Stable skip reasons. `legacy-keyring-unreachable` is retryable (a later
53
+ * run with the keyring online would succeed); the others are diagnostic.
54
+ */
55
+ export type MigrateSkipReason = 'identify-failed' | 'legacy-keyring-unreachable' | 'user-record-write-failed' | 'marker-write-failed';
56
+ /**
57
+ * Discriminated by `status`. Narrowing on `status === 'skipped'` exposes
58
+ * the structured `reason` + free-form `detail`; `migrated` carries the
59
+ * resolved account.
60
+ */
61
+ export type MigrateAuthResult<TAccount extends AuthAccount = AuthAccount> = {
62
+ status: 'already-migrated';
63
+ } | {
64
+ status: 'no-legacy-state';
65
+ } | {
66
+ status: 'migrated';
67
+ account: TAccount;
68
+ } | {
69
+ status: 'skipped';
70
+ reason: MigrateSkipReason;
71
+ detail: string;
72
+ };
73
+ /**
74
+ * One-time migration of a v1 single-user auth state into a v2 multi-user
75
+ * shape. Best-effort: any failure (offline keyring, network error fetching
76
+ * the user, …) leaves the v1 state untouched so the consumer's runtime
77
+ * fallback can keep serving the legacy token until the next attempt.
78
+ *
79
+ * Order of operations is deliberate so the migration is genuinely one-way:
80
+ *
81
+ * 1. `hasMigrated()` short-circuits if the durable marker is already set.
82
+ * 2. Read the v1 token (legacy keyring slot first, then plaintext).
83
+ * 3. `identifyAccount(token)` resolves the v2 `account` shape.
84
+ * 4. `writeRecordWithKeyringFallback` writes the v2 record.
85
+ * 5. Best-effort `setDefaultId(account.id)` so the new record is active.
86
+ * 6. `markMigrated()` persists the marker. **If this fails, we return
87
+ * `skipped` even though the v2 record is on disk** — the marker is
88
+ * what prevents re-migration on the next run.
89
+ * 7. Best-effort legacy keyring delete + `cleanupLegacyConfig()` run
90
+ * concurrently. Failures here are harmless because the marker is set.
91
+ */
92
+ export declare function migrateLegacyAuth<TAccount extends AuthAccount>(options: MigrateLegacyAuthOptions<TAccount>): Promise<MigrateAuthResult<TAccount>>;
93
+ //# sourceMappingURL=migrate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../../src/auth/keyring/migrate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAQ9C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEjD,MAAM,MAAM,wBAAwB,CAAC,QAAQ,SAAS,WAAW,IAAI;IACjE,WAAW,EAAE,MAAM,CAAA;IACnB,mEAAmE;IACnE,aAAa,EAAE,MAAM,CAAA;IACrB,gEAAgE;IAChE,WAAW,EAAE,eAAe,CAAC,QAAQ,CAAC,CAAA;IACtC,yEAAyE;IACzE,cAAc,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAA;IACvC;;;;;;;OAOG;IACH,WAAW,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;IACnC;;;;;;OAMG;IACH,YAAY,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IACjC;;;OAGG;IACH,wBAAwB,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACtD;;;;OAIG;IACH,eAAe,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;IACrD;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IACzC,2DAA2D;IAC3D,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA;AAED;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GACvB,iBAAiB,GACjB,4BAA4B,GAC5B,0BAA0B,GAC1B,qBAAqB,CAAA;AAS3B;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,CAAC,QAAQ,SAAS,WAAW,GAAG,WAAW,IAClE;IAAE,MAAM,EAAE,kBAAkB,CAAA;CAAE,GAC9B;IAAE,MAAM,EAAE,iBAAiB,CAAA;CAAE,GAC7B;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,GACzC;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,iBAAiB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAOtE;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,iBAAiB,CAAC,QAAQ,SAAS,WAAW,EAChE,OAAO,EAAE,wBAAwB,CAAC,QAAQ,CAAC,GAC5C,OAAO,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAuGtC"}
@@ -0,0 +1,143 @@
1
+ import { getErrorMessage } from '../../errors.js';
2
+ import { writeRecordWithKeyringFallback } from './record-write.js';
3
+ import { createSecureStore, DEFAULT_ACCOUNT_FOR_USER, SecureStoreUnavailableError, } from './secure-store.js';
4
+ const SKIP_REASON_MESSAGES = {
5
+ 'identify-failed': 'could not identify user',
6
+ 'legacy-keyring-unreachable': 'legacy credential is unreachable (keyring offline)',
7
+ 'user-record-write-failed': 'failed to update user records',
8
+ 'marker-write-failed': 'failed to persist migration marker',
9
+ };
10
+ /**
11
+ * One-time migration of a v1 single-user auth state into a v2 multi-user
12
+ * shape. Best-effort: any failure (offline keyring, network error fetching
13
+ * the user, …) leaves the v1 state untouched so the consumer's runtime
14
+ * fallback can keep serving the legacy token until the next attempt.
15
+ *
16
+ * Order of operations is deliberate so the migration is genuinely one-way:
17
+ *
18
+ * 1. `hasMigrated()` short-circuits if the durable marker is already set.
19
+ * 2. Read the v1 token (legacy keyring slot first, then plaintext).
20
+ * 3. `identifyAccount(token)` resolves the v2 `account` shape.
21
+ * 4. `writeRecordWithKeyringFallback` writes the v2 record.
22
+ * 5. Best-effort `setDefaultId(account.id)` so the new record is active.
23
+ * 6. `markMigrated()` persists the marker. **If this fails, we return
24
+ * `skipped` even though the v2 record is on disk** — the marker is
25
+ * what prevents re-migration on the next run.
26
+ * 7. Best-effort legacy keyring delete + `cleanupLegacyConfig()` run
27
+ * concurrently. Failures here are harmless because the marker is set.
28
+ */
29
+ export async function migrateLegacyAuth(options) {
30
+ const { serviceName, legacyAccount, userRecords, hasMigrated, markMigrated, loadLegacyPlaintextToken, identifyAccount, cleanupLegacyConfig, silent, } = options;
31
+ const accountForUser = options.accountForUser ?? DEFAULT_ACCOUNT_FOR_USER;
32
+ const logPrefix = options.logPrefix ?? 'cli';
33
+ if (await hasMigrated()) {
34
+ return { status: 'already-migrated' };
35
+ }
36
+ // One legacy-keyring handle covers both the initial read and the
37
+ // post-success cleanup delete.
38
+ const legacyStore = createSecureStore({ serviceName, account: legacyAccount });
39
+ const legacyToken = await readLegacyToken(legacyStore, loadLegacyPlaintextToken);
40
+ if (legacyToken.kind === 'none')
41
+ return { status: 'no-legacy-state' };
42
+ if (legacyToken.kind === 'keyring-unavailable') {
43
+ return skipped(silent, logPrefix, 'legacy-keyring-unreachable', 'OS keyring unreachable while reading legacy slot');
44
+ }
45
+ let account;
46
+ try {
47
+ account = await identifyAccount(legacyToken.token);
48
+ }
49
+ catch (error) {
50
+ return skipped(silent, logPrefix, 'identify-failed', getErrorMessage(error));
51
+ }
52
+ // `writeRecordWithKeyringFallback` swallows `SecureStoreUnavailableError`
53
+ // internally (writing to `fallbackToken` instead), so any error here is
54
+ // a non-keyring failure — typically a `userRecords.upsert` rejection.
55
+ try {
56
+ await writeRecordWithKeyringFallback({
57
+ secureStore: createSecureStore({
58
+ serviceName,
59
+ account: accountForUser(account.id),
60
+ }),
61
+ userRecords,
62
+ account,
63
+ token: legacyToken.token,
64
+ });
65
+ }
66
+ catch (error) {
67
+ return skipped(silent, logPrefix, 'user-record-write-failed', getErrorMessage(error));
68
+ }
69
+ // Default promotion is best-effort and **only fires when nothing is
70
+ // already pinned**. A retry after a previous `markMigrated()` failure
71
+ // can land on a store where the user has since logged in to a different
72
+ // account and picked it as default — blindly setting the legacy account
73
+ // back as default would silently switch the user.
74
+ try {
75
+ const existingDefault = await userRecords.getDefaultId();
76
+ if (!existingDefault) {
77
+ await userRecords.setDefaultId(account.id);
78
+ }
79
+ }
80
+ catch {
81
+ // best-effort
82
+ }
83
+ // Persist the one-way marker BEFORE legacy cleanup. If this fails, the
84
+ // v2 record is already written but the gate is unset — surface as
85
+ // `skipped` so the caller retries. Without this ordering, a later
86
+ // `logout` could let the next run re-migrate the stale v1 token.
87
+ try {
88
+ await markMigrated();
89
+ }
90
+ catch (error) {
91
+ return skipped(silent, logPrefix, 'marker-write-failed', getErrorMessage(error));
92
+ }
93
+ // Best-effort legacy cleanup — runs concurrently so we don't pay
94
+ // keyring latency followed by config-write latency on every install.
95
+ // The marker is already set, so a failure here can't cause
96
+ // re-migration on the next run. The `Promise.resolve().then(...)`
97
+ // wrappers convert any *synchronous* throw from a consumer-supplied
98
+ // `cleanupLegacyConfig` (or an oddly-implemented `SecureStore`) into
99
+ // a rejected promise that `Promise.allSettled` can swallow.
100
+ await Promise.allSettled([
101
+ Promise.resolve().then(() => legacyStore.deleteSecret()),
102
+ Promise.resolve().then(() => cleanupLegacyConfig?.()),
103
+ ]);
104
+ if (!silent) {
105
+ // No identifier in the log line — `account.id` is typed as `string`
106
+ // but consumers can legitimately use an email or other PII there.
107
+ // Callers that need richer telemetry can compose it from the
108
+ // returned `account`.
109
+ console.error(`${logPrefix}: migrated existing token to multi-user store.`);
110
+ }
111
+ return { status: 'migrated', account };
112
+ }
113
+ async function readLegacyToken(legacyStore, loadLegacyPlaintextToken) {
114
+ let keyringUnavailable = false;
115
+ try {
116
+ const stored = await legacyStore.getSecret();
117
+ if (stored?.trim())
118
+ return { kind: 'found', token: stored.trim() };
119
+ }
120
+ catch (error) {
121
+ if (!(error instanceof SecureStoreUnavailableError))
122
+ throw error;
123
+ keyringUnavailable = true;
124
+ }
125
+ const plaintext = await loadLegacyPlaintextToken();
126
+ if (plaintext?.trim())
127
+ return { kind: 'found', token: plaintext.trim() };
128
+ return keyringUnavailable ? { kind: 'keyring-unavailable' } : { kind: 'none' };
129
+ }
130
+ /**
131
+ * Emit the migration skip line. The stderr text is a fixed phrase keyed off
132
+ * `MigrateSkipReason` so consumer-supplied callbacks (`identifyAccount`,
133
+ * the `UserRecordStore`, …) can't leak emails, paths, or auth diagnostics
134
+ * into logs. The raw error message is still attached to the returned
135
+ * `MigrateAuthResult.detail` for in-process callers that need it.
136
+ */
137
+ function skipped(silent, logPrefix, reason, detail) {
138
+ if (!silent) {
139
+ console.error(`${logPrefix}: skipped legacy auth migration — ${SKIP_REASON_MESSAGES[reason]}.`);
140
+ }
141
+ return { status: 'skipped', reason, detail };
142
+ }
143
+ //# sourceMappingURL=migrate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrate.js","sourceRoot":"","sources":["../../../src/auth/keyring/migrate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ import type { AuthAccount } from '../types.js';
2
+ import { type SecureStore } from './secure-store.js';
3
+ import type { UserRecordStore } from './types.js';
4
+ type WriteRecordOptions<TAccount extends AuthAccount> = {
5
+ /** Per-account keyring slot, already configured by the caller (e.g. via `createSecureStore`). */
6
+ secureStore: SecureStore;
7
+ userRecords: UserRecordStore<TAccount>;
8
+ account: TAccount;
9
+ token: string;
10
+ };
11
+ type WriteRecordResult = {
12
+ /** `true` when the secret landed in the OS keyring; `false` when the keyring was unavailable and the token was written to `fallbackToken` on the user record. */
13
+ storedSecurely: boolean;
14
+ };
15
+ /**
16
+ * Shared keyring-then-record write used by `createKeyringTokenStore.set` and
17
+ * `migrateLegacyAuth`. Encapsulates the order-of-operations contract that
18
+ * matters for credential safety:
19
+ *
20
+ * 1. Keyring `setSecret` first. On `SecureStoreUnavailableError`, swallow
21
+ * the failure and record a `fallbackToken` on the user record instead.
22
+ * Any other error rethrows.
23
+ * 2. `userRecords.upsert(record)`. On failure, best-effort rollback the
24
+ * keyring write so we don't leave an orphan credential for an account
25
+ * cli-core never managed to register. Original error rethrows.
26
+ *
27
+ * Default promotion (`setDefaultId`) is intentionally **not** in here — both
28
+ * call sites do it best-effort outside the critical section because it is a
29
+ * preference, not a correctness requirement, and an error there must not
30
+ * dirty up a successful credential write.
31
+ */
32
+ export declare function writeRecordWithKeyringFallback<TAccount extends AuthAccount>(options: WriteRecordOptions<TAccount>): Promise<WriteRecordResult>;
33
+ export {};
34
+ //# sourceMappingURL=record-write.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record-write.d.ts","sourceRoot":"","sources":["../../../src/auth/keyring/record-write.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,KAAK,WAAW,EAA+B,MAAM,mBAAmB,CAAA;AACjF,OAAO,KAAK,EAAc,eAAe,EAAE,MAAM,YAAY,CAAA;AAE7D,KAAK,kBAAkB,CAAC,QAAQ,SAAS,WAAW,IAAI;IACpD,iGAAiG;IACjG,WAAW,EAAE,WAAW,CAAA;IACxB,WAAW,EAAE,eAAe,CAAC,QAAQ,CAAC,CAAA;IACtC,OAAO,EAAE,QAAQ,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,KAAK,iBAAiB,GAAG;IACrB,iKAAiK;IACjK,cAAc,EAAE,OAAO,CAAA;CAC1B,CAAA;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,8BAA8B,CAAC,QAAQ,SAAS,WAAW,EAC7E,OAAO,EAAE,kBAAkB,CAAC,QAAQ,CAAC,GACtC,OAAO,CAAC,iBAAiB,CAAC,CA8B5B"}
@@ -0,0 +1,50 @@
1
+ import { SecureStoreUnavailableError } from './secure-store.js';
2
+ /**
3
+ * Shared keyring-then-record write used by `createKeyringTokenStore.set` and
4
+ * `migrateLegacyAuth`. Encapsulates the order-of-operations contract that
5
+ * matters for credential safety:
6
+ *
7
+ * 1. Keyring `setSecret` first. On `SecureStoreUnavailableError`, swallow
8
+ * the failure and record a `fallbackToken` on the user record instead.
9
+ * Any other error rethrows.
10
+ * 2. `userRecords.upsert(record)`. On failure, best-effort rollback the
11
+ * keyring write so we don't leave an orphan credential for an account
12
+ * cli-core never managed to register. Original error rethrows.
13
+ *
14
+ * Default promotion (`setDefaultId`) is intentionally **not** in here — both
15
+ * call sites do it best-effort outside the critical section because it is a
16
+ * preference, not a correctness requirement, and an error there must not
17
+ * dirty up a successful credential write.
18
+ */
19
+ export async function writeRecordWithKeyringFallback(options) {
20
+ const { secureStore, userRecords, account, token } = options;
21
+ const trimmed = token.trim();
22
+ let storedSecurely = false;
23
+ try {
24
+ await secureStore.setSecret(trimmed);
25
+ storedSecurely = true;
26
+ }
27
+ catch (error) {
28
+ if (!(error instanceof SecureStoreUnavailableError))
29
+ throw error;
30
+ }
31
+ const record = storedSecurely
32
+ ? { account }
33
+ : { account, fallbackToken: trimmed };
34
+ try {
35
+ await userRecords.upsert(record);
36
+ }
37
+ catch (error) {
38
+ if (storedSecurely) {
39
+ try {
40
+ await secureStore.deleteSecret();
41
+ }
42
+ catch {
43
+ // best-effort — the user record failure is the real cause
44
+ }
45
+ }
46
+ throw error;
47
+ }
48
+ return { storedSecurely };
49
+ }
50
+ //# sourceMappingURL=record-write.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record-write.js","sourceRoot":"","sources":["../../../src/auth/keyring/record-write.ts"],"names":[],"mappings":"AACA,OAAO,EAAoB,2BAA2B,EAAE,MAAM,mBAAmB,CAAA;AAgBjF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,KAAK,UAAU,8BAA8B,CAChD,OAAqC;IAErC,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,OAAO,CAAA;IAC5D,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAA;IAE5B,IAAI,cAAc,GAAG,KAAK,CAAA;IAC1B,IAAI,CAAC;QACD,MAAM,WAAW,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA;QACpC,cAAc,GAAG,IAAI,CAAA;IACzB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACb,IAAI,CAAC,CAAC,KAAK,YAAY,2BAA2B,CAAC;YAAE,MAAM,KAAK,CAAA;IACpE,CAAC;IAED,MAAM,MAAM,GAAyB,cAAc;QAC/C,CAAC,CAAC,EAAE,OAAO,EAAE;QACb,CAAC,CAAC,EAAE,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,CAAA;IAEzC,IAAI,CAAC;QACD,MAAM,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IACpC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACb,IAAI,cAAc,EAAE,CAAC;YACjB,IAAI,CAAC;gBACD,MAAM,WAAW,CAAC,YAAY,EAAE,CAAA;YACpC,CAAC;YAAC,MAAM,CAAC;gBACL,0DAA0D;YAC9D,CAAC;QACL,CAAC;QACD,MAAM,KAAK,CAAA;IACf,CAAC;IAED,OAAO,EAAE,cAAc,EAAE,CAAA;AAC7B,CAAC"}
@@ -1,3 +1,16 @@
1
+ /**
2
+ * User-facing label for the OS credential manager. Used by the keyring
3
+ * `TokenStore`'s fallback-warning composition; internal to the keyring
4
+ * module until a public caller asks for it.
5
+ */
6
+ export declare const SECURE_STORE_DESCRIPTION = "system credential manager";
7
+ /**
8
+ * Default keyring `account` slug for a stored user. Lives here so every
9
+ * caller that derives a per-user slot name agrees on the wire format — a
10
+ * future rename can't silently park tokens in a slot the runtime no longer
11
+ * reads from.
12
+ */
13
+ export declare const DEFAULT_ACCOUNT_FOR_USER: (id: string) => string;
1
14
  /**
2
15
  * Thrown when the OS credential manager cannot be reached — missing native
3
16
  * binary for the current architecture, libsecret/D-Bus unavailable
@@ -1 +1 @@
1
- {"version":3,"file":"secure-store.d.ts","sourceRoot":"","sources":["../../../src/auth/keyring/secure-store.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AACH,qBAAa,2BAA4B,SAAQ,KAAK;gBAE9C,OAAO,SAA6C,EACpD,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAKpC;AAED,MAAM,MAAM,WAAW,GAAG;IACtB,SAAS,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACnC,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACxC,YAAY,IAAI,OAAO,CAAC,OAAO,CAAC,CAAA;CACnC,CAAA;AAED,MAAM,MAAM,wBAAwB,GAAG;IACnC,uHAAuH;IACvH,WAAW,EAAE,MAAM,CAAA;IACnB,oGAAoG;IACpG,OAAO,EAAE,MAAM,CAAA;CAClB,CAAA;AAID;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,WAAW,CAoChF"}
1
+ {"version":3,"file":"secure-store.d.ts","sourceRoot":"","sources":["../../../src/auth/keyring/secure-store.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,8BAA8B,CAAA;AAEnE;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,GAAI,IAAI,MAAM,KAAG,MAAsB,CAAA;AAE5E;;;;;;;;;;GAUG;AACH,qBAAa,2BAA4B,SAAQ,KAAK;gBAE9C,OAAO,SAA6C,EACpD,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAKpC;AAED,MAAM,MAAM,WAAW,GAAG;IACtB,SAAS,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACnC,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACxC,YAAY,IAAI,OAAO,CAAC,OAAO,CAAC,CAAA;CACnC,CAAA;AAED,MAAM,MAAM,wBAAwB,GAAG;IACnC,uHAAuH;IACvH,WAAW,EAAE,MAAM,CAAA;IACnB,oGAAoG;IACpG,OAAO,EAAE,MAAM,CAAA;CAClB,CAAA;AAID;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,WAAW,CAoChF"}
@@ -1,4 +1,17 @@
1
1
  import { getErrorMessage } from '../../errors.js';
2
+ /**
3
+ * User-facing label for the OS credential manager. Used by the keyring
4
+ * `TokenStore`'s fallback-warning composition; internal to the keyring
5
+ * module until a public caller asks for it.
6
+ */
7
+ export const SECURE_STORE_DESCRIPTION = 'system credential manager';
8
+ /**
9
+ * Default keyring `account` slug for a stored user. Lives here so every
10
+ * caller that derives a per-user slot name agrees on the wire format — a
11
+ * future rename can't silently park tokens in a slot the runtime no longer
12
+ * reads from.
13
+ */
14
+ export const DEFAULT_ACCOUNT_FOR_USER = (id) => `user-${id}`;
2
15
  /**
3
16
  * Thrown when the OS credential manager cannot be reached — missing native
4
17
  * binary for the current architecture, libsecret/D-Bus unavailable
@@ -1 +1 @@
1
- {"version":3,"file":"secure-store.js","sourceRoot":"","sources":["../../../src/auth/keyring/secure-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAEjD;;;;;;;;;;GAUG;AACH,MAAM,OAAO,2BAA4B,SAAQ,KAAK;IAClD,YACI,OAAO,GAAG,0CAA0C,EACpD,OAA6B;QAE7B,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;QACvB,IAAI,CAAC,IAAI,GAAG,6BAA6B,CAAA;IAC7C,CAAC;CACJ;AAiBD;;;;;;;;;GASG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAiC;IAC/D,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,GAAG,OAAO,CAAA;IACxC,IAAI,YAA6C,CAAA;IAEjD,KAAK,UAAU,SAAS,CAAI,EAAqC;QAC7D,IAAI,CAAC,YAAY,EAAE,CAAC;YAChB,gEAAgE;YAChE,+DAA+D;YAC/D,gEAAgE;YAChE,iCAAiC;YACjC,YAAY,GAAG,CAAC,KAAK,IAAI,EAAE;gBACvB,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAA;gBACvD,OAAO,IAAI,UAAU,CAAC,WAAW,EAAE,OAAO,CAAC,CAAA;YAC/C,CAAC,CAAC,EAAE,CAAA;QACR,CAAC;QACD,IAAI,CAAC;YACD,MAAM,KAAK,GAAG,MAAM,YAAY,CAAA;YAChC,OAAO,MAAM,EAAE,CAAC,KAAK,CAAC,CAAA;QAC1B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACb,MAAM,kBAAkB,CAAC,KAAK,CAAC,CAAA;QACnC,CAAC;IACL,CAAC;IAED,OAAO;QACH,KAAK,CAAC,SAAS;YACX,OAAO,SAAS,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,MAAM,KAAK,CAAC,WAAW,EAAE,CAAC,IAAI,IAAI,CAAC,CAAA;QAC1E,CAAC;QACD,KAAK,CAAC,SAAS,CAAC,MAAM;YAClB,OAAO,SAAS,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;gBAC7B,MAAM,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAA;YACnC,CAAC,CAAC,CAAA;QACN,CAAC;QACD,KAAK,CAAC,YAAY;YACd,OAAO,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,gBAAgB,EAAE,CAAC,CAAA;QACzD,CAAC;KACJ,CAAA;AACL,CAAC;AAED,SAAS,kBAAkB,CAAC,KAAc;IACtC,IAAI,KAAK,YAAY,2BAA2B,EAAE,CAAC;QAC/C,OAAO,KAAK,CAAA;IAChB,CAAC;IACD,OAAO,IAAI,2BAA2B,CAAC,eAAe,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAA;AACpF,CAAC"}
1
+ {"version":3,"file":"secure-store.js","sourceRoot":"","sources":["../../../src/auth/keyring/secure-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAEjD;;;;GAIG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,2BAA2B,CAAA;AAEnE;;;;;GAKG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,EAAU,EAAU,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAA;AAE5E;;;;;;;;;;GAUG;AACH,MAAM,OAAO,2BAA4B,SAAQ,KAAK;IAClD,YACI,OAAO,GAAG,0CAA0C,EACpD,OAA6B;QAE7B,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;QACvB,IAAI,CAAC,IAAI,GAAG,6BAA6B,CAAA;IAC7C,CAAC;CACJ;AAiBD;;;;;;;;;GASG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAiC;IAC/D,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,GAAG,OAAO,CAAA;IACxC,IAAI,YAA6C,CAAA;IAEjD,KAAK,UAAU,SAAS,CAAI,EAAqC;QAC7D,IAAI,CAAC,YAAY,EAAE,CAAC;YAChB,gEAAgE;YAChE,+DAA+D;YAC/D,gEAAgE;YAChE,iCAAiC;YACjC,YAAY,GAAG,CAAC,KAAK,IAAI,EAAE;gBACvB,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAA;gBACvD,OAAO,IAAI,UAAU,CAAC,WAAW,EAAE,OAAO,CAAC,CAAA;YAC/C,CAAC,CAAC,EAAE,CAAA;QACR,CAAC;QACD,IAAI,CAAC;YACD,MAAM,KAAK,GAAG,MAAM,YAAY,CAAA;YAChC,OAAO,MAAM,EAAE,CAAC,KAAK,CAAC,CAAA;QAC1B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACb,MAAM,kBAAkB,CAAC,KAAK,CAAC,CAAA;QACnC,CAAC;IACL,CAAC;IAED,OAAO;QACH,KAAK,CAAC,SAAS;YACX,OAAO,SAAS,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,MAAM,KAAK,CAAC,WAAW,EAAE,CAAC,IAAI,IAAI,CAAC,CAAA;QAC1E,CAAC;QACD,KAAK,CAAC,SAAS,CAAC,MAAM;YAClB,OAAO,SAAS,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;gBAC7B,MAAM,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAA;YACnC,CAAC,CAAC,CAAA;QACN,CAAC;QACD,KAAK,CAAC,YAAY;YACd,OAAO,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,gBAAgB,EAAE,CAAC,CAAA;QACzD,CAAC;KACJ,CAAA;AACL,CAAC;AAED,SAAS,kBAAkB,CAAC,KAAc;IACtC,IAAI,KAAK,YAAY,2BAA2B,EAAE,CAAC;QAC/C,OAAO,KAAK,CAAA;IAChB,CAAC;IACD,OAAO,IAAI,2BAA2B,CAAC,eAAe,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAA;AACpF,CAAC"}
@@ -0,0 +1,58 @@
1
+ import type { AccountRef, AuthAccount, TokenStore } from '../types.js';
2
+ import type { TokenStorageResult, UserRecordStore } from './types.js';
3
+ export type CreateKeyringTokenStoreOptions<TAccount extends AuthAccount> = {
4
+ /** Application identifier used for every keyring entry (e.g. `'todoist-cli'`). */
5
+ serviceName: string;
6
+ /** Consumer-owned per-user record store (typically backed by their config file). */
7
+ userRecords: UserRecordStore<TAccount>;
8
+ /**
9
+ * Human-readable location of the record store, used in the fallback-warning
10
+ * text (e.g. `~/.config/todoist-cli/config.json`). Plain string; cli-core
11
+ * does not interpret it.
12
+ */
13
+ recordsLocation: string;
14
+ /**
15
+ * Builds the keyring `account` slug for a user id. Defaults to
16
+ * `user-${id}`. Override only when migrating from a legacy naming scheme.
17
+ */
18
+ accountForUser?: (id: string) => string;
19
+ /**
20
+ * Decides whether an account matches an `AccountRef` supplied via
21
+ * `--user <ref>`. Defaults to id-or-label equality. Override to broaden
22
+ * (e.g. case-insensitive email, alias map).
23
+ */
24
+ matchAccount?: (account: TAccount, ref: AccountRef) => boolean;
25
+ };
26
+ export type KeyringTokenStore<TAccount extends AuthAccount> = TokenStore<TAccount> & {
27
+ /** Storage result from the most recent `set()` call, or `undefined` before any (and reset to `undefined` when the most recent `set()` threw). */
28
+ getLastStorageResult(): TokenStorageResult | undefined;
29
+ /** Storage result from the most recent `clear()` call, or `undefined` before any (and reset to `undefined` when the most recent `clear()` threw or was a no-op). */
30
+ getLastClearResult(): TokenStorageResult | undefined;
31
+ };
32
+ /**
33
+ * Multi-account `TokenStore` that keeps secrets in the OS credential manager
34
+ * and per-user metadata in the consumer's `UserRecordStore`. Falls back to a
35
+ * plaintext token on the user record when the keyring is unreachable (WSL
36
+ * without D-Bus, missing native binary, locked Keychain, …) so the CLI keeps
37
+ * working at the cost of a visible warning.
38
+ *
39
+ * Read order in `active()` is `fallbackToken` first, then the keyring. That
40
+ * matches the write semantics in `writeRecordWithKeyringFallback`: when the
41
+ * keyring is online the record is written with no `fallbackToken`, so the
42
+ * keyring read is the only path. When the keyring is offline the token is
43
+ * parked on the record and must be reachable on every subsequent read.
44
+ *
45
+ * Write order is keyring first, then `userRecords.upsert`. If the upsert
46
+ * fails after a successful keyring write, the keyring entry is rolled back
47
+ * via `deleteSecret()` to avoid orphan credentials for a user that cli-core
48
+ * never managed to record.
49
+ *
50
+ * Clear order is the inverse: record removal first (the source of truth that
51
+ * the rest of the CLI reads), then keyring delete. Any keyring delete
52
+ * failure after a successful removal is downgraded to a warning — the orphan
53
+ * secret is harmless because no record references it anymore, and surfacing
54
+ * the error would corrupt local state (record gone, but caller sees a thrown
55
+ * exception and assumes the clear failed).
56
+ */
57
+ export declare function createKeyringTokenStore<TAccount extends AuthAccount>(options: CreateKeyringTokenStoreOptions<TAccount>): KeyringTokenStore<TAccount>;
58
+ //# sourceMappingURL=token-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-store.d.ts","sourceRoot":"","sources":["../../../src/auth/keyring/token-store.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAUtE,OAAO,KAAK,EAAE,kBAAkB,EAAc,eAAe,EAAE,MAAM,YAAY,CAAA;AAEjF,MAAM,MAAM,8BAA8B,CAAC,QAAQ,SAAS,WAAW,IAAI;IACvE,kFAAkF;IAClF,WAAW,EAAE,MAAM,CAAA;IACnB,oFAAoF;IACpF,WAAW,EAAE,eAAe,CAAC,QAAQ,CAAC,CAAA;IACtC;;;;OAIG;IACH,eAAe,EAAE,MAAM,CAAA;IACvB;;;OAGG;IACH,cAAc,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAA;IACvC;;;;OAIG;IACH,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,UAAU,KAAK,OAAO,CAAA;CACjE,CAAA;AAED,MAAM,MAAM,iBAAiB,CAAC,QAAQ,SAAS,WAAW,IAAI,UAAU,CAAC,QAAQ,CAAC,GAAG;IACjF,iJAAiJ;IACjJ,oBAAoB,IAAI,kBAAkB,GAAG,SAAS,CAAA;IACtD,oKAAoK;IACpK,kBAAkB,IAAI,kBAAkB,GAAG,SAAS,CAAA;CACvD,CAAA;AAOD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,SAAS,WAAW,EAChE,OAAO,EAAE,8BAA8B,CAAC,QAAQ,CAAC,GAClD,iBAAiB,CAAC,QAAQ,CAAC,CAgP7B"}
@@ -0,0 +1,240 @@
1
+ import { CliError } from '../../errors.js';
2
+ import { accountNotFoundError } from '../user-flag.js';
3
+ import { writeRecordWithKeyringFallback } from './record-write.js';
4
+ import { createSecureStore, DEFAULT_ACCOUNT_FOR_USER, SECURE_STORE_DESCRIPTION, SecureStoreUnavailableError, } from './secure-store.js';
5
+ const DEFAULT_MATCH_ACCOUNT = (account, ref) => account.id === ref || account.label === ref;
6
+ /**
7
+ * Multi-account `TokenStore` that keeps secrets in the OS credential manager
8
+ * and per-user metadata in the consumer's `UserRecordStore`. Falls back to a
9
+ * plaintext token on the user record when the keyring is unreachable (WSL
10
+ * without D-Bus, missing native binary, locked Keychain, …) so the CLI keeps
11
+ * working at the cost of a visible warning.
12
+ *
13
+ * Read order in `active()` is `fallbackToken` first, then the keyring. That
14
+ * matches the write semantics in `writeRecordWithKeyringFallback`: when the
15
+ * keyring is online the record is written with no `fallbackToken`, so the
16
+ * keyring read is the only path. When the keyring is offline the token is
17
+ * parked on the record and must be reachable on every subsequent read.
18
+ *
19
+ * Write order is keyring first, then `userRecords.upsert`. If the upsert
20
+ * fails after a successful keyring write, the keyring entry is rolled back
21
+ * via `deleteSecret()` to avoid orphan credentials for a user that cli-core
22
+ * never managed to record.
23
+ *
24
+ * Clear order is the inverse: record removal first (the source of truth that
25
+ * the rest of the CLI reads), then keyring delete. Any keyring delete
26
+ * failure after a successful removal is downgraded to a warning — the orphan
27
+ * secret is harmless because no record references it anymore, and surfacing
28
+ * the error would corrupt local state (record gone, but caller sees a thrown
29
+ * exception and assumes the clear failed).
30
+ */
31
+ export function createKeyringTokenStore(options) {
32
+ const { serviceName, userRecords, recordsLocation } = options;
33
+ const accountForUser = options.accountForUser ?? DEFAULT_ACCOUNT_FOR_USER;
34
+ const matchAccount = options.matchAccount ?? DEFAULT_MATCH_ACCOUNT;
35
+ let lastStorageResult;
36
+ let lastClearResult;
37
+ function secureStoreFor(account) {
38
+ return createSecureStore({ serviceName, account: accountForUser(account.id) });
39
+ }
40
+ /**
41
+ * Read both `list()` and `getDefaultId()` concurrently. Used by paths
42
+ * that need the pinned default (no-ref `active`/`clear`, `list`, and
43
+ * `clear`'s default-unpin check).
44
+ */
45
+ async function readFullSnapshot() {
46
+ const [records, defaultId] = await Promise.all([
47
+ userRecords.list(),
48
+ userRecords.getDefaultId(),
49
+ ]);
50
+ return { records, defaultId };
51
+ }
52
+ /**
53
+ * Resolve the snapshot target for a given ref (or the implicit default
54
+ * when `ref === undefined`). Two failure modes:
55
+ *
56
+ * - Multiple records match the `ref`: ambiguous (the default matcher
57
+ * includes `account.label`, and labels aren't guaranteed unique).
58
+ * Throws `NO_ACCOUNT_SELECTED` so the user picks a tighter ref instead
59
+ * of silently acting on whichever record `list()` returned first.
60
+ * - `ref === undefined`, no `defaultId` pinned, and more than one record
61
+ * exists. Same code — `setDefaultId` is best-effort during `set()`,
62
+ * so a typed failure here is the only non-misleading signal for "you
63
+ * have multiple accounts; pick one".
64
+ */
65
+ function resolveTarget(snapshot, ref) {
66
+ if (ref !== undefined) {
67
+ const matches = snapshot.records.filter((record) => matchAccount(record.account, ref));
68
+ if (matches.length > 1) {
69
+ throw new CliError('NO_ACCOUNT_SELECTED', `Multiple stored accounts match "${ref}". Pass a more specific --user <ref> (e.g. a unique account id).`);
70
+ }
71
+ return matches[0] ?? null;
72
+ }
73
+ if (snapshot.defaultId) {
74
+ const pinned = snapshot.records.find((r) => r.account.id === snapshot.defaultId);
75
+ if (pinned)
76
+ return pinned;
77
+ }
78
+ if (snapshot.records.length === 1)
79
+ return snapshot.records[0];
80
+ if (snapshot.records.length === 0)
81
+ return null;
82
+ throw new CliError('NO_ACCOUNT_SELECTED', 'Multiple accounts are stored but none is set as the default. Pass --user <ref>, or set a default in your CLI.');
83
+ }
84
+ function fallbackResult(action) {
85
+ return {
86
+ storage: 'config-file',
87
+ warning: `${SECURE_STORE_DESCRIPTION} unavailable; ${action} ${recordsLocation}`,
88
+ };
89
+ }
90
+ return {
91
+ async active(ref) {
92
+ // Ref-only path skips `getDefaultId()` — `resolveTarget` never
93
+ // touches it when `ref` is supplied, so the extra read would be
94
+ // pure latency on every authenticated command.
95
+ const snapshot = ref === undefined
96
+ ? await readFullSnapshot()
97
+ : { records: await userRecords.list(), defaultId: null };
98
+ const record = resolveTarget(snapshot, ref);
99
+ if (!record)
100
+ return null;
101
+ const fallback = record.fallbackToken?.trim();
102
+ if (fallback) {
103
+ return { token: fallback, account: record.account };
104
+ }
105
+ let raw;
106
+ try {
107
+ raw = await secureStoreFor(record.account).getSecret();
108
+ }
109
+ catch (error) {
110
+ // A matching record exists but the keyring can't be read.
111
+ // Surface a typed failure instead of returning `null`, which
112
+ // would otherwise be indistinguishable from "no stored
113
+ // account" and trigger `ACCOUNT_NOT_FOUND` on `--user <ref>`.
114
+ // `attachLogoutCommand` catches this specific code so an
115
+ // explicit `logout --user <ref>` can still clear the matching
116
+ // record without needing the unreadable token.
117
+ if (error instanceof SecureStoreUnavailableError) {
118
+ throw new CliError('AUTH_STORE_READ_FAILED', `${SECURE_STORE_DESCRIPTION} unavailable; could not read stored token (${error.message})`);
119
+ }
120
+ throw error;
121
+ }
122
+ const token = raw?.trim();
123
+ if (token) {
124
+ return { token, account: record.account };
125
+ }
126
+ // Record exists, no `fallbackToken`, and the keyring slot is
127
+ // empty — the credential was deleted out-of-band (user ran
128
+ // `security delete-generic-password`, `secret-tool clear`, …).
129
+ // This is corrupted state, not a miss; collapsing it to `null`
130
+ // would make `--user <ref>` surface as `ACCOUNT_NOT_FOUND` and
131
+ // hide the real problem.
132
+ throw new CliError('AUTH_STORE_READ_FAILED', `${SECURE_STORE_DESCRIPTION} returned no credential for the stored account; the keyring entry may have been removed externally.`);
133
+ },
134
+ async set(account, token) {
135
+ // Reset the cached storage result up front so a caller that
136
+ // catches a thrown `set()` doesn't observe the previous call's
137
+ // warning leaking through `getLastStorageResult`.
138
+ lastStorageResult = undefined;
139
+ const { storedSecurely } = await writeRecordWithKeyringFallback({
140
+ secureStore: secureStoreFor(account),
141
+ userRecords,
142
+ account,
143
+ token,
144
+ });
145
+ // Best-effort default promotion: the record is already persisted,
146
+ // so a failure here must not turn into `AUTH_STORE_WRITE_FAILED`
147
+ // (the user can recover by setting a default later).
148
+ try {
149
+ const existingDefault = await userRecords.getDefaultId();
150
+ if (!existingDefault) {
151
+ await userRecords.setDefaultId(account.id);
152
+ }
153
+ }
154
+ catch {
155
+ // best-effort
156
+ }
157
+ lastStorageResult = storedSecurely
158
+ ? { storage: 'secure-store' }
159
+ : fallbackResult('token saved as plaintext in');
160
+ },
161
+ async clear(ref) {
162
+ // Reset up front for the same reason as `set` — and so a no-op
163
+ // (no matching record) clears any stale result from a previous
164
+ // call.
165
+ lastClearResult = undefined;
166
+ // `clear` always needs the pinned default to decide whether to
167
+ // un-pin after the removal, so we can't skip `getDefaultId()`
168
+ // even on the explicit-ref path.
169
+ const snapshot = await readFullSnapshot();
170
+ const record = resolveTarget(snapshot, ref);
171
+ if (!record)
172
+ return;
173
+ await userRecords.remove(record.account.id);
174
+ // Default un-pinning is best-effort: a failure here must not
175
+ // skip the keyring delete below, otherwise we leave an
176
+ // unreachable orphan secret behind for the just-removed record.
177
+ if (snapshot.defaultId === record.account.id) {
178
+ try {
179
+ await userRecords.setDefaultId(null);
180
+ }
181
+ catch {
182
+ // best-effort
183
+ }
184
+ }
185
+ const fallbackClear = fallbackResult('local auth state cleared in');
186
+ // Always attempt the keyring delete. Even when the record carried
187
+ // a `fallbackToken`, an older keyring entry may still be parked
188
+ // there from a prior keyring-online write that was later replaced
189
+ // by an offline-fallback write — skipping the delete would leak
190
+ // that orphan. Downgrade *any* failure to a warning: the record
191
+ // is already gone, so re-throwing would corrupt local state
192
+ // (caller sees an exception and assumes nothing was cleared,
193
+ // even though the next `account list` will show the user gone).
194
+ try {
195
+ await secureStoreFor(record.account).deleteSecret();
196
+ lastClearResult =
197
+ record.fallbackToken !== undefined ? fallbackClear : { storage: 'secure-store' };
198
+ }
199
+ catch {
200
+ lastClearResult = fallbackClear;
201
+ }
202
+ },
203
+ async list() {
204
+ const snapshot = await readFullSnapshot();
205
+ // Use `resolveTarget` to compute the *effective* default so the
206
+ // `isDefault` markers match what `active()` would resolve — that
207
+ // includes the implicit single-record case. `resolveTarget` can
208
+ // throw `NO_ACCOUNT_SELECTED`, which we want to swallow here
209
+ // (listing accounts is a diagnostic operation that must work
210
+ // even when no default is pinned).
211
+ let implicitDefault = null;
212
+ try {
213
+ implicitDefault = resolveTarget(snapshot, undefined);
214
+ }
215
+ catch {
216
+ // multiple records, no default → `isDefault: false` for all
217
+ }
218
+ return snapshot.records.map((record) => ({
219
+ account: record.account,
220
+ isDefault: record.account.id === implicitDefault?.account.id,
221
+ }));
222
+ },
223
+ async setDefault(ref) {
224
+ // Ref-only path — skip `getDefaultId()` like `active(ref)`.
225
+ const snapshot = { records: await userRecords.list(), defaultId: null };
226
+ const record = resolveTarget(snapshot, ref);
227
+ if (!record) {
228
+ throw accountNotFoundError(ref);
229
+ }
230
+ await userRecords.setDefaultId(record.account.id);
231
+ },
232
+ getLastStorageResult() {
233
+ return lastStorageResult;
234
+ },
235
+ getLastClearResult() {
236
+ return lastClearResult;
237
+ },
238
+ };
239
+ }
240
+ //# sourceMappingURL=token-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-store.js","sourceRoot":"","sources":["../../../src/auth/keyring/token-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ import type { AuthAccount } from '../types.js';
2
+ /** Where a token was (or wasn't) persisted on the most recent write/clear. */
3
+ export type TokenStorageLocation = 'secure-store' | 'config-file';
4
+ export type TokenStorageResult = {
5
+ storage: TokenStorageLocation;
6
+ /**
7
+ * Present when the OS keyring was unavailable and the operation fell back
8
+ * to (or left state in) the consumer's config file. Suitable for surfacing
9
+ * to the user as a `Warning:` line on stderr.
10
+ */
11
+ warning?: string;
12
+ };
13
+ export type UserRecord<TAccount extends AuthAccount> = {
14
+ account: TAccount;
15
+ /**
16
+ * Plaintext token, present only when the keyring was unavailable at write
17
+ * time. The runtime reads it in preference to the keyring slot, so a
18
+ * stale fallback would mask a fresh keyring-backed write — consumers
19
+ * implementing `upsert` as replace-not-merge (per the contract below)
20
+ * guarantees the field is cleared on every successful keyring write.
21
+ * Surface its presence as security-relevant: it is the same material
22
+ * that would otherwise live in the OS credential manager.
23
+ */
24
+ fallbackToken?: string;
25
+ };
26
+ /**
27
+ * Port the consumer implements to expose their per-user config records to
28
+ * cli-core's keyring-backed `TokenStore`. The shape of the record map (file
29
+ * format, path, schema versioning) stays in the consumer — cli-core only
30
+ * needs CRUD on these primitives plus a default-user pointer.
31
+ */
32
+ export type UserRecordStore<TAccount extends AuthAccount> = {
33
+ list(): Promise<UserRecord<TAccount>[]>;
34
+ /**
35
+ * **Replace**, do not merge. The persisted record must equal `record` field
36
+ * for field — an absent `fallbackToken` means "no plaintext token", and a
37
+ * merge-style implementation would let a stale plaintext token outlive a
38
+ * later keyring-backed write (the runtime preferentially reads
39
+ * `fallbackToken` over the keyring). Records are keyed by `account.id`.
40
+ */
41
+ upsert(record: UserRecord<TAccount>): Promise<void>;
42
+ /** Remove the record whose `account.id` matches. */
43
+ remove(id: string): Promise<void>;
44
+ /** The pinned default's `account.id`, or `null` when nothing is pinned. */
45
+ getDefaultId(): Promise<string | null>;
46
+ setDefaultId(id: string | null): Promise<void>;
47
+ };
48
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/auth/keyring/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAE9C,8EAA8E;AAC9E,MAAM,MAAM,oBAAoB,GAAG,cAAc,GAAG,aAAa,CAAA;AAEjE,MAAM,MAAM,kBAAkB,GAAG;IAC7B,OAAO,EAAE,oBAAoB,CAAA;IAC7B;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,UAAU,CAAC,QAAQ,SAAS,WAAW,IAAI;IACnD,OAAO,EAAE,QAAQ,CAAA;IACjB;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACzB,CAAA;AAED;;;;;GAKG;AACH,MAAM,MAAM,eAAe,CAAC,QAAQ,SAAS,WAAW,IAAI;IACxD,IAAI,IAAI,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;IACvC;;;;;;OAMG;IACH,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACnD,oDAAoD;IACpD,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjC,2EAA2E;IAC3E,YAAY,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACtC,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACjD,CAAA"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/auth/keyring/types.ts"],"names":[],"mappings":""}
@@ -1,8 +1,11 @@
1
1
  import type { Command } from 'commander';
2
+ import { CliError } from '../errors.js';
2
3
  import type { AccountRef, AuthAccount, TokenStore } from './types.js';
3
4
  export declare function attachUserFlag(command: Command): Command;
4
5
  /** `cmd.user` as an `AccountRef`, or `undefined` when absent. */
5
6
  export declare function extractUserRef(cmd: Record<string, unknown>): AccountRef | undefined;
7
+ /** Shared constructor so multiple call sites can't drift on the `ACCOUNT_NOT_FOUND` wording. */
8
+ export declare function accountNotFoundError(ref: AccountRef): CliError;
6
9
  /**
7
10
  * Read `store.active(ref)` and throw `ACCOUNT_NOT_FOUND` if the explicit
8
11
  * `ref` doesn't match. With `ref === undefined` returns the snapshot
@@ -1 +1 @@
1
- {"version":3,"file":"user-flag.d.ts","sourceRoot":"","sources":["../../src/auth/user-flag.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAExC,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AASrE,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAExD;AAED,iEAAiE;AACjE,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,UAAU,GAAG,SAAS,CAEnF;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CAAC,QAAQ,SAAS,WAAW,EACpE,KAAK,EAAE,UAAU,CAAC,QAAQ,CAAC,EAC3B,GAAG,EAAE,UAAU,GAAG,SAAS,GAC5B,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,GAAG,IAAI,CAAC,CAMtD"}
1
+ {"version":3,"file":"user-flag.d.ts","sourceRoot":"","sources":["../../src/auth/user-flag.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACxC,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AACvC,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AASrE,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAExD;AAED,iEAAiE;AACjE,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,UAAU,GAAG,SAAS,CAEnF;AAED,gGAAgG;AAChG,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,UAAU,GAAG,QAAQ,CAE9D;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CAAC,QAAQ,SAAS,WAAW,EACpE,KAAK,EAAE,UAAU,CAAC,QAAQ,CAAC,EAC3B,GAAG,EAAE,UAAU,GAAG,SAAS,GAC5B,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,GAAG,IAAI,CAAC,CAMtD"}
@@ -11,6 +11,10 @@ export function attachUserFlag(command) {
11
11
  export function extractUserRef(cmd) {
12
12
  return typeof cmd.user === 'string' ? cmd.user : undefined;
13
13
  }
14
+ /** Shared constructor so multiple call sites can't drift on the `ACCOUNT_NOT_FOUND` wording. */
15
+ export function accountNotFoundError(ref) {
16
+ return new CliError('ACCOUNT_NOT_FOUND', `No stored account matches "${ref}".`);
17
+ }
14
18
  /**
15
19
  * Read `store.active(ref)` and throw `ACCOUNT_NOT_FOUND` if the explicit
16
20
  * `ref` doesn't match. With `ref === undefined` returns the snapshot
@@ -19,7 +23,7 @@ export function extractUserRef(cmd) {
19
23
  export async function requireSnapshotForRef(store, ref) {
20
24
  const snapshot = await store.active(ref);
21
25
  if (ref !== undefined && snapshot === null) {
22
- throw new CliError('ACCOUNT_NOT_FOUND', `No stored account matches "${ref}".`);
26
+ throw accountNotFoundError(ref);
23
27
  }
24
28
  return snapshot;
25
29
  }
@@ -1 +1 @@
1
- {"version":3,"file":"user-flag.js","sourceRoot":"","sources":["../../src/auth/user-flag.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAGvC,4EAA4E;AAC5E,0EAA0E;AAC1E,eAAe;AAEf,MAAM,SAAS,GAAG,cAAc,CAAA;AAChC,MAAM,qBAAqB,GAAG,kCAAkC,CAAA;AAEhE,MAAM,UAAU,cAAc,CAAC,OAAgB;IAC3C,OAAO,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,qBAAqB,CAAC,CAAA;AAC3D,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,cAAc,CAAC,GAA4B;IACvD,OAAO,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAA;AAC9D,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACvC,KAA2B,EAC3B,GAA2B;IAE3B,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IACxC,IAAI,GAAG,KAAK,SAAS,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACzC,MAAM,IAAI,QAAQ,CAAC,mBAAmB,EAAE,8BAA8B,GAAG,IAAI,CAAC,CAAA;IAClF,CAAC;IACD,OAAO,QAAQ,CAAA;AACnB,CAAC"}
1
+ {"version":3,"file":"user-flag.js","sourceRoot":"","sources":["../../src/auth/user-flag.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAGvC,4EAA4E;AAC5E,0EAA0E;AAC1E,eAAe;AAEf,MAAM,SAAS,GAAG,cAAc,CAAA;AAChC,MAAM,qBAAqB,GAAG,kCAAkC,CAAA;AAEhE,MAAM,UAAU,cAAc,CAAC,OAAgB;IAC3C,OAAO,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,qBAAqB,CAAC,CAAA;AAC3D,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,cAAc,CAAC,GAA4B;IACvD,OAAO,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAA;AAC9D,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,oBAAoB,CAAC,GAAe;IAChD,OAAO,IAAI,QAAQ,CAAC,mBAAmB,EAAE,8BAA8B,GAAG,IAAI,CAAC,CAAA;AACnF,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACvC,KAA2B,EAC3B,GAA2B;IAE3B,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IACxC,IAAI,GAAG,KAAK,SAAS,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACzC,MAAM,oBAAoB,CAAC,GAAG,CAAC,CAAA;IACnC,CAAC;IACD,OAAO,QAAQ,CAAA;AACnB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doist/cli-core",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "Shared core utilities for Doist CLI projects",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",