@doist/cli-core 0.12.0 → 0.13.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [0.13.0](https://github.com/Doist/cli-core/compare/v0.12.0...v0.13.0) (2026-05-16)
2
+
3
+ ### Features
4
+
5
+ * **auth:** add createSecureStore keyring primitive (1/4) ([#25](https://github.com/Doist/cli-core/issues/25)) ([30bcbd7](https://github.com/Doist/cli-core/commit/30bcbd7e49aede98d0777424169324af03f56eaf))
6
+
1
7
  ## [0.12.0](https://github.com/Doist/cli-core/compare/v0.11.0...v0.12.0) (2026-05-14)
2
8
 
3
9
  ### 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`, PKCE helpers, `AuthProvider` / `TokenStore` / `AccountRef` 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`); `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) and `open` (browser launch) are optional peer-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`, 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). |
29
29
 
30
30
  ## Usage
31
31
 
@@ -288,6 +288,33 @@ export const tokenStore: TokenStore<Account> = {
288
288
 
289
289
  For multi-account storage (OS keychain, per-user config slots, …), implement the same five methods against your backend and honour `ref` on `active` / `clear` / `setDefault`. `AccountRef` is an opaque `string` — cli-core does not constrain matching semantics (id-exact, email-case-insensitive, label, …). The store impl owns that.
290
290
 
291
+ #### Keyring primitive (`createSecureStore`)
292
+
293
+ When the OS credential manager is the right place for your token, `createSecureStore` is a thin cross-platform wrapper around `@napi-rs/keyring`. It exposes a three-method handle (`getSecret` / `setSecret` / `deleteSecret`) for one slot identified by `serviceName` + `account`:
294
+
295
+ ```ts
296
+ import { createSecureStore, SecureStoreUnavailableError } from '@doist/cli-core/auth'
297
+
298
+ const slot = createSecureStore({ serviceName: 'todoist-cli', account: 'api-token' })
299
+
300
+ try {
301
+ await slot.setSecret(token)
302
+ } catch (error) {
303
+ if (error instanceof SecureStoreUnavailableError) {
304
+ // Keyring unreachable (WSL without D-Bus, missing native binary on an
305
+ // exotic arch, Keychain locked, …). Fall back to wherever your CLI
306
+ // stores fallback state — config file with a clear plaintext warning,
307
+ // an env-var prompt, etc.
308
+ } else {
309
+ throw error
310
+ }
311
+ }
312
+ ```
313
+
314
+ Every failure mode — `@napi-rs/keyring` failing to load on an arch without a prebuilt binary, libsecret not running on a headless Linux box, the Keychain prompting and the user denying — is normalised into `SecureStoreUnavailableError`. The `@napi-rs/keyring` module is dynamic-imported so a missing native binary doesn't crash module load before the error can surface.
315
+
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
+
291
318
  #### `--user <ref>` and multi-user wiring
292
319
 
293
320
  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,4 +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
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"}
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"}
@@ -5,4 +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
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"}
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"}
@@ -0,0 +1,3 @@
1
+ export { SecureStoreUnavailableError, createSecureStore } from './secure-store.js';
2
+ export type { CreateSecureStoreOptions, SecureStore } from './secure-store.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,2 @@
1
+ export { SecureStoreUnavailableError, createSecureStore } from './secure-store.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Thrown when the OS credential manager cannot be reached — missing native
3
+ * binary for the current architecture, libsecret/D-Bus unavailable
4
+ * (common in WSL / headless Linux / containers / CI), Keychain locked, or
5
+ * any other error returned by the underlying keyring backend.
6
+ *
7
+ * The original throwable is preserved on `cause` so platform-specific
8
+ * diagnostics (stack, native error code) aren't lost — callers that want
9
+ * to degrade to a plaintext fallback should catch this specific class
10
+ * rather than swallowing every `Error`.
11
+ */
12
+ export declare class SecureStoreUnavailableError extends Error {
13
+ constructor(message?: string, options?: {
14
+ cause?: unknown;
15
+ });
16
+ }
17
+ export type SecureStore = {
18
+ getSecret(): Promise<string | null>;
19
+ setSecret(secret: string): Promise<void>;
20
+ deleteSecret(): Promise<boolean>;
21
+ };
22
+ export type CreateSecureStoreOptions = {
23
+ /** Stable per-application identifier (Keychain "service", Credential Manager "target prefix", libsecret "service"). */
24
+ serviceName: string;
25
+ /** Per-credential identifier within the service. For multi-account CLIs, typically `user-${id}`. */
26
+ account: string;
27
+ };
28
+ /**
29
+ * Thin wrapper around `@napi-rs/keyring` that normalizes every failure mode
30
+ * into `SecureStoreUnavailableError`. `serviceName` + `account` together
31
+ * identify one credential slot in the OS keyring.
32
+ *
33
+ * The dynamic import + `AsyncEntry` construction is memoised per-store so
34
+ * repeated reads/writes share one entry and a missing native binary
35
+ * fast-fails on subsequent calls instead of retrying the import every time
36
+ * (the rejected promise replays its rejection on each `await`).
37
+ */
38
+ export declare function createSecureStore(options: CreateSecureStoreOptions): SecureStore;
39
+ //# sourceMappingURL=secure-store.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,71 @@
1
+ import { getErrorMessage } from '../../errors.js';
2
+ /**
3
+ * Thrown when the OS credential manager cannot be reached — missing native
4
+ * binary for the current architecture, libsecret/D-Bus unavailable
5
+ * (common in WSL / headless Linux / containers / CI), Keychain locked, or
6
+ * any other error returned by the underlying keyring backend.
7
+ *
8
+ * The original throwable is preserved on `cause` so platform-specific
9
+ * diagnostics (stack, native error code) aren't lost — callers that want
10
+ * to degrade to a plaintext fallback should catch this specific class
11
+ * rather than swallowing every `Error`.
12
+ */
13
+ export class SecureStoreUnavailableError extends Error {
14
+ constructor(message = 'System credential storage is unavailable', options) {
15
+ super(message, options);
16
+ this.name = 'SecureStoreUnavailableError';
17
+ }
18
+ }
19
+ /**
20
+ * Thin wrapper around `@napi-rs/keyring` that normalizes every failure mode
21
+ * into `SecureStoreUnavailableError`. `serviceName` + `account` together
22
+ * identify one credential slot in the OS keyring.
23
+ *
24
+ * The dynamic import + `AsyncEntry` construction is memoised per-store so
25
+ * repeated reads/writes share one entry and a missing native binary
26
+ * fast-fails on subsequent calls instead of retrying the import every time
27
+ * (the rejected promise replays its rejection on each `await`).
28
+ */
29
+ export function createSecureStore(options) {
30
+ const { serviceName, account } = options;
31
+ let entryPromise;
32
+ async function withEntry(fn) {
33
+ if (!entryPromise) {
34
+ // Dynamic import: `@napi-rs/keyring` is an optional dependency.
35
+ // On unsupported architectures the native binary is absent and
36
+ // a static import would crash module load before we can surface
37
+ // `SecureStoreUnavailableError`.
38
+ entryPromise = (async () => {
39
+ const { AsyncEntry } = await import('@napi-rs/keyring');
40
+ return new AsyncEntry(serviceName, account);
41
+ })();
42
+ }
43
+ try {
44
+ const entry = await entryPromise;
45
+ return await fn(entry);
46
+ }
47
+ catch (error) {
48
+ throw toUnavailableError(error);
49
+ }
50
+ }
51
+ return {
52
+ async getSecret() {
53
+ return withEntry(async (entry) => (await entry.getPassword()) ?? null);
54
+ },
55
+ async setSecret(secret) {
56
+ return withEntry(async (entry) => {
57
+ await entry.setPassword(secret);
58
+ });
59
+ },
60
+ async deleteSecret() {
61
+ return withEntry((entry) => entry.deleteCredential());
62
+ },
63
+ };
64
+ }
65
+ function toUnavailableError(error) {
66
+ if (error instanceof SecureStoreUnavailableError) {
67
+ return error;
68
+ }
69
+ return new SecureStoreUnavailableError(getErrorMessage(error), { cause: error });
70
+ }
71
+ //# sourceMappingURL=secure-store.js.map
@@ -0,0 +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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doist/cli-core",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Shared core utilities for Doist CLI projects",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -83,6 +83,9 @@
83
83
  "chalk": "5.6.2",
84
84
  "yocto-spinner": "1.2.0"
85
85
  },
86
+ "optionalDependencies": {
87
+ "@napi-rs/keyring": "1.3.0"
88
+ },
86
89
  "peerDependencies": {
87
90
  "commander": ">=14",
88
91
  "marked": ">=18",