@doist/cli-core 0.16.1 → 0.18.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 +12 -0
- package/README.md +22 -14
- package/dist/auth/index.d.ts +3 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +1 -0
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/keyring/internal.d.ts +25 -0
- package/dist/auth/keyring/internal.d.ts.map +1 -0
- package/dist/auth/keyring/internal.js +31 -0
- package/dist/auth/keyring/internal.js.map +1 -0
- package/dist/auth/keyring/migrate.d.ts +19 -17
- package/dist/auth/keyring/migrate.d.ts.map +1 -1
- package/dist/auth/keyring/migrate.js +111 -49
- package/dist/auth/keyring/migrate.js.map +1 -1
- package/dist/auth/keyring/record-write.d.ts +70 -16
- package/dist/auth/keyring/record-write.d.ts.map +1 -1
- package/dist/auth/keyring/record-write.js +139 -30
- package/dist/auth/keyring/record-write.js.map +1 -1
- package/dist/auth/keyring/slot-naming.d.ts +6 -0
- package/dist/auth/keyring/slot-naming.d.ts.map +1 -0
- package/dist/auth/keyring/slot-naming.js +8 -0
- package/dist/auth/keyring/slot-naming.js.map +1 -0
- package/dist/auth/keyring/token-store.d.ts +10 -2
- package/dist/auth/keyring/token-store.d.ts.map +1 -1
- package/dist/auth/keyring/token-store.js +93 -64
- package/dist/auth/keyring/token-store.js.map +1 -1
- package/dist/auth/keyring/types.d.ts +20 -0
- package/dist/auth/keyring/types.d.ts.map +1 -1
- package/dist/auth/persist.d.ts +23 -0
- package/dist/auth/persist.d.ts.map +1 -0
- package/dist/auth/persist.js +38 -0
- package/dist/auth/persist.js.map +1 -0
- package/dist/auth/types.d.ts +27 -1
- package/dist/auth/types.d.ts.map +1 -1
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [0.18.0](https://github.com/Doist/cli-core/compare/v0.17.0...v0.18.0) (2026-05-19)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
* **auth:** two-phase migrate write for the bundle-shape contract ([#38](https://github.com/Doist/cli-core/issues/38)) ([9cf5e9c](https://github.com/Doist/cli-core/commit/9cf5e9caaed30ec0a989a026bf5b00f6815a2512))
|
|
6
|
+
|
|
7
|
+
## [0.17.0](https://github.com/Doist/cli-core/compare/v0.16.1...v0.17.0) (2026-05-19)
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **auth:** add TokenBundle storage contract to TokenStore ([#37](https://github.com/Doist/cli-core/issues/37)) ([6513aaa](https://github.com/Doist/cli-core/commit/6513aaa8b6222eb421239f6e41ec63b12cf946e9))
|
|
12
|
+
|
|
1
13
|
## [0.16.1](https://github.com/Doist/cli-core/compare/v0.16.0...v0.16.1) (2026-05-16)
|
|
2
14
|
|
|
3
15
|
### Bug Fixes
|
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
|
|
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)
|
|
19
|
-
| `config` | `getConfigPath`, `readConfig`, `readConfigStrict`, `writeConfig`, `updateConfig`, `CoreConfig`, `UpdateChannel`
|
|
20
|
-
| `empty` | `printEmpty`
|
|
21
|
-
| `errors` | `CliError`
|
|
22
|
-
| `global-args` | `parseGlobalArgs`, `stripUserFlag`, `createGlobalArgsStore`, `createAccessibleGate`, `createSpinnerGate`, `getProgressJsonlPath`, `isProgressJsonlEnabled`
|
|
23
|
-
| `json` | `formatJson`, `formatNdjson`
|
|
24
|
-
| `markdown` (subpath) | `preloadMarkdown`, `renderMarkdown`, `TerminalRendererOptions`
|
|
25
|
-
| `options` | `ViewOptions`
|
|
26
|
-
| `spinner` | `createSpinner`
|
|
27
|
-
| `terminal` | `isCI`, `isStderrTTY`, `isStdinTTY`, `isStdoutTTY`
|
|
28
|
-
| `testing` (subpath) | `describeEmptyMachineOutput`
|
|
15
|
+
| Module | Key exports | Purpose |
|
|
16
|
+
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
17
|
+
| `auth` (subpath) | `attachLoginCommand`, `attachLogoutCommand`, `attachStatusCommand`, `attachTokenViewCommand`, `runOAuthFlow`, `createPkceProvider`, `createSecureStore`, `createKeyringTokenStore`, `migrateLegacyAuth`, `persistBundle`, PKCE helpers, `AuthProvider` / `TokenStore` / `TokenBundle` / `RefreshInput` / `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). The store contract supports an optional `setBundle(account, bundle)` write method (required on `KeyringTokenStore`) so consumers that need refresh-token persistence can opt in via `TokenBundle`; `active()` stays narrow (access token + account only) so callers that don't need refresh state don't pay extra keyring IPC. `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
|
|
|
@@ -288,6 +288,14 @@ 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
|
+
##### Refresh-token storage (`TokenBundle`)
|
|
292
|
+
|
|
293
|
+
Stores that target servers issuing refresh tokens may implement the optional `setBundle(account, bundle, options?)` method. `TokenBundle` carries `{ accessToken, refreshToken?, accessTokenExpiresAt?, refreshTokenExpiresAt? }`. Stores that omit `setBundle` continue to work — cli-core helpers (`persistBundle`) fall back to `set(account, bundle.accessToken)` and silently drop refresh state. `KeyringTokenStore` implements `setBundle` as a required override and routes the refresh token to a sibling keyring slot.
|
|
294
|
+
|
|
295
|
+
`active()` still returns the narrow `{ token, account }` snapshot — refresh-state material is stored but not surfaced on the hot read path so commands that only need the access token don't pay an extra keyring IPC. A bundle-aware read path lands alongside the silent-refresh helper in a follow-up PR.
|
|
296
|
+
|
|
297
|
+
The `persistBundle({ store, account, bundle, promoteDefault? })` helper is the recommended write path for bundle-capable consumers — it prefers `setBundle` when available and falls back to `set` otherwise (the `set` fallback can't honour `promoteDefault`, so multi-account stores wanting silent-refresh-safe selection must implement `setBundle`). cli-core's own `runOAuthFlow` still persists via `store.set()` today; it switches to `persistBundle` when the refresh helper lands.
|
|
298
|
+
|
|
291
299
|
#### Keyring primitive (`createSecureStore`)
|
|
292
300
|
|
|
293
301
|
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`:
|
package/dist/auth/index.d.ts
CHANGED
|
@@ -11,9 +11,11 @@ export { attachTokenViewCommand } from './token-view.js';
|
|
|
11
11
|
export type { AttachTokenViewCommandOptions } from './token-view.js';
|
|
12
12
|
export { DEFAULT_VERIFIER_ALPHABET, deriveChallenge, generateState, generateVerifier, } from './pkce.js';
|
|
13
13
|
export type { GenerateVerifierOptions } from './pkce.js';
|
|
14
|
+
export { persistBundle } from './persist.js';
|
|
15
|
+
export type { PersistBundleOptions } from './persist.js';
|
|
14
16
|
export { createPkceProvider } from './providers/pkce.js';
|
|
15
17
|
export type { PkceLazyString, PkceProviderOptions } from './providers/pkce.js';
|
|
16
|
-
export type { AccountRef, AuthAccount, AuthorizeInput, AuthorizeResult, AuthProvider, ExchangeInput, ExchangeResult, PrepareInput, PrepareResult, TokenStore, ValidateInput, } from './types.js';
|
|
18
|
+
export type { AccountRef, AuthAccount, AuthorizeInput, AuthorizeResult, AuthProvider, ExchangeInput, ExchangeResult, PrepareInput, PrepareResult, RefreshInput, TokenBundle, TokenStore, ValidateInput, } from './types.js';
|
|
17
19
|
export { SecureStoreUnavailableError, createKeyringTokenStore, createSecureStore, migrateLegacyAuth, } from './keyring/index.js';
|
|
18
20
|
export type { CreateKeyringTokenStoreOptions, CreateSecureStoreOptions, KeyringTokenStore, MigrateAuthResult, MigrateLegacyAuthOptions, MigrateSkipReason, SecureStore, TokenStorageLocation, TokenStorageResult, UserRecord, UserRecordStore, } from './keyring/index.js';
|
|
19
21
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/auth/index.d.ts.map
CHANGED
|
@@ -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,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"}
|
|
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,aAAa,EAAE,MAAM,cAAc,CAAA;AAC5C,YAAY,EAAE,oBAAoB,EAAE,MAAM,cAAc,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,YAAY,EACZ,WAAW,EACX,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"}
|
package/dist/auth/index.js
CHANGED
|
@@ -4,6 +4,7 @@ export { attachLogoutCommand } from './logout.js';
|
|
|
4
4
|
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
|
+
export { persistBundle } from './persist.js';
|
|
7
8
|
export { createPkceProvider } from './providers/pkce.js';
|
|
8
9
|
export { SecureStoreUnavailableError, createKeyringTokenStore, createSecureStore, migrateLegacyAuth, } from './keyring/index.js';
|
|
9
10
|
//# sourceMappingURL=index.js.map
|
package/dist/auth/index.js.map
CHANGED
|
@@ -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,aAAa,EAAE,MAAM,cAAc,CAAA;AAE5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AAiBxD,OAAO,EACH,2BAA2B,EAC3B,uBAAuB,EACvB,iBAAiB,EACjB,iBAAiB,GACpB,MAAM,oBAAoB,CAAA"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { AuthAccount } from '../types.js';
|
|
2
|
+
import { type SecureStore } from './secure-store.js';
|
|
3
|
+
import type { UserRecord } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Outcome of resolving the access token for a record. Callers map the
|
|
6
|
+
* structured failure variants to whatever error contract they expose —
|
|
7
|
+
* `KeyringTokenStore.active()` throws `CliError('AUTH_STORE_READ_FAILED', …)`;
|
|
8
|
+
* `migrateLegacyAuth` translates each variant into a `MigrateSkipReason`.
|
|
9
|
+
*/
|
|
10
|
+
export type ReadAccessTokenOutcome = {
|
|
11
|
+
ok: true;
|
|
12
|
+
token: string;
|
|
13
|
+
} | {
|
|
14
|
+
ok: false;
|
|
15
|
+
reason: 'slot-empty' | 'slot-unavailable' | 'slot-error';
|
|
16
|
+
detail: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* `fallbackToken` first (so an offline-keyring write is preferred over a
|
|
20
|
+
* stale slot), then the keyring slot. Single-source for "is this record
|
|
21
|
+
* readable in the current environment" — `KeyringTokenStore.active()` and
|
|
22
|
+
* `migrateLegacyAuth`'s readability probe both call this.
|
|
23
|
+
*/
|
|
24
|
+
export declare function readAccessTokenForRecord<TAccount extends AuthAccount>(record: UserRecord<TAccount>, secureStore: SecureStore): Promise<ReadAccessTokenOutcome>;
|
|
25
|
+
//# sourceMappingURL=internal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["../../../src/auth/keyring/internal.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,KAAK,WAAW,EAA+B,MAAM,mBAAmB,CAAA;AACjF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C;;;;;GAKG;AACH,MAAM,MAAM,sBAAsB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC3B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,YAAY,GAAG,kBAAkB,GAAG,YAAY,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAE7F;;;;;GAKG;AACH,wBAAsB,wBAAwB,CAAC,QAAQ,SAAS,WAAW,EACvE,MAAM,EAAE,UAAU,CAAC,QAAQ,CAAC,EAC5B,WAAW,EAAE,WAAW,GACzB,OAAO,CAAC,sBAAsB,CAAC,CAmBjC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { getErrorMessage } from '../../errors.js';
|
|
2
|
+
import { SecureStoreUnavailableError } from './secure-store.js';
|
|
3
|
+
/**
|
|
4
|
+
* `fallbackToken` first (so an offline-keyring write is preferred over a
|
|
5
|
+
* stale slot), then the keyring slot. Single-source for "is this record
|
|
6
|
+
* readable in the current environment" — `KeyringTokenStore.active()` and
|
|
7
|
+
* `migrateLegacyAuth`'s readability probe both call this.
|
|
8
|
+
*/
|
|
9
|
+
export async function readAccessTokenForRecord(record, secureStore) {
|
|
10
|
+
const fallback = record.fallbackToken?.trim();
|
|
11
|
+
if (fallback)
|
|
12
|
+
return { ok: true, token: fallback };
|
|
13
|
+
try {
|
|
14
|
+
const raw = await secureStore.getSecret();
|
|
15
|
+
const trimmed = raw?.trim();
|
|
16
|
+
if (trimmed)
|
|
17
|
+
return { ok: true, token: trimmed };
|
|
18
|
+
return {
|
|
19
|
+
ok: false,
|
|
20
|
+
reason: 'slot-empty',
|
|
21
|
+
detail: 'keyring slot returned no credential',
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (error instanceof SecureStoreUnavailableError) {
|
|
26
|
+
return { ok: false, reason: 'slot-unavailable', detail: error.message };
|
|
27
|
+
}
|
|
28
|
+
return { ok: false, reason: 'slot-error', detail: getErrorMessage(error) };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=internal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"internal.js","sourceRoot":"","sources":["../../../src/auth/keyring/internal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAEjD,OAAO,EAAoB,2BAA2B,EAAE,MAAM,mBAAmB,CAAA;AAajF;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC1C,MAA4B,EAC5B,WAAwB;IAExB,MAAM,QAAQ,GAAG,MAAM,CAAC,aAAa,EAAE,IAAI,EAAE,CAAA;IAC7C,IAAI,QAAQ;QAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAA;IAElD,IAAI,CAAC;QACD,MAAM,GAAG,GAAG,MAAM,WAAW,CAAC,SAAS,EAAE,CAAA;QACzC,MAAM,OAAO,GAAG,GAAG,EAAE,IAAI,EAAE,CAAA;QAC3B,IAAI,OAAO;YAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,CAAA;QAChD,OAAO;YACH,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,YAAY;YACpB,MAAM,EAAE,qCAAqC;SAChD,CAAA;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACb,IAAI,KAAK,YAAY,2BAA2B,EAAE,CAAC;YAC/C,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,EAAE,KAAK,CAAC,OAAO,EAAE,CAAA;QAC3E,CAAC;QACD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,eAAe,CAAC,KAAK,CAAC,EAAE,CAAA;IAC9E,CAAC;AACL,CAAC"}
|
|
@@ -49,10 +49,11 @@ export type MigrateLegacyAuthOptions<TAccount extends AuthAccount> = {
|
|
|
49
49
|
logPrefix?: string;
|
|
50
50
|
};
|
|
51
51
|
/**
|
|
52
|
-
* Stable skip reasons.
|
|
53
|
-
* run with the keyring online would succeed); the others are
|
|
52
|
+
* Stable skip reasons. `*-keyring-unreachable` variants are retryable (a
|
|
53
|
+
* later run with the keyring online would succeed); the others are
|
|
54
|
+
* diagnostic.
|
|
54
55
|
*/
|
|
55
|
-
export type MigrateSkipReason = 'identify-failed' | 'legacy-keyring-unreachable' | 'user-record-write-failed' | 'marker-write-failed';
|
|
56
|
+
export type MigrateSkipReason = 'identify-failed' | 'legacy-keyring-unreachable' | 'user-keyring-unreachable' | 'user-record-write-failed' | 'marker-write-failed';
|
|
56
57
|
/**
|
|
57
58
|
* Discriminated by `status`. Narrowing on `status === 'skipped'` exposes
|
|
58
59
|
* the structured `reason` + free-form `detail`; `migrated` carries the
|
|
@@ -72,22 +73,23 @@ export type MigrateAuthResult<TAccount extends AuthAccount = AuthAccount> = {
|
|
|
72
73
|
};
|
|
73
74
|
/**
|
|
74
75
|
* One-time migration of a v1 single-user auth state into a v2 multi-user
|
|
75
|
-
* shape. Best-effort: any failure
|
|
76
|
-
*
|
|
77
|
-
* fallback can keep serving the legacy token until the next attempt.
|
|
76
|
+
* shape. Best-effort: any failure leaves v1 untouched so the runtime
|
|
77
|
+
* fallback keeps serving the legacy token until the next attempt.
|
|
78
78
|
*
|
|
79
|
-
* Order
|
|
79
|
+
* Order is deliberate so the migration is one-way AND safe under retry:
|
|
80
80
|
*
|
|
81
|
-
* 1. `hasMigrated()` short-circuits
|
|
82
|
-
* 2. Read the v1 token (legacy keyring
|
|
83
|
-
* 3. `identifyAccount(token)` resolves the v2 `account
|
|
84
|
-
* 4. `
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
81
|
+
* 1. `hasMigrated()` short-circuits when the marker is set.
|
|
82
|
+
* 2. Read the v1 token (legacy keyring first, then plaintext).
|
|
83
|
+
* 3. `identifyAccount(token)` resolves the v2 `account`.
|
|
84
|
+
* 4. **Phase 1** — `ensureV2Record` writes a fallback-bearing record (or
|
|
85
|
+
* no-ops when a v2 record already exists).
|
|
86
|
+
* 5. **Phase 2** — when Phase 1 wrote: move the token to the per-user
|
|
87
|
+
* keyring slot and upsert the clean record. When Phase 1 didn't:
|
|
88
|
+
* verify the existing record is readable before retiring legacy.
|
|
89
|
+
* 6. Best-effort `setDefaultId(account.id)`.
|
|
90
|
+
* 7. `markMigrated()` — the one-way gate. Failure here surfaces as
|
|
91
|
+
* `skipped(marker-write-failed)` so the caller retries.
|
|
92
|
+
* 8. Best-effort legacy cleanup runs concurrently.
|
|
91
93
|
*/
|
|
92
94
|
export declare function migrateLegacyAuth<TAccount extends AuthAccount>(options: MigrateLegacyAuthOptions<TAccount>): Promise<MigrateAuthResult<TAccount>>;
|
|
93
95
|
//# sourceMappingURL=migrate.d.ts.map
|
|
@@ -1 +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;
|
|
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;AAS9C,OAAO,KAAK,EAAc,eAAe,EAAE,MAAM,YAAY,CAAA;AAE7D,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;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GACvB,iBAAiB,GACjB,4BAA4B,GAC5B,0BAA0B,GAC1B,0BAA0B,GAC1B,qBAAqB,CAAA;AAU3B;;;;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;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,iBAAiB,CAAC,QAAQ,SAAS,WAAW,EAChE,OAAO,EAAE,wBAAwB,CAAC,QAAQ,CAAC,GAC5C,OAAO,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CA4GtC"}
|
|
@@ -1,30 +1,33 @@
|
|
|
1
1
|
import { getErrorMessage } from '../../errors.js';
|
|
2
|
-
import {
|
|
2
|
+
import { readAccessTokenForRecord } from './internal.js';
|
|
3
|
+
import { buildSingleTokenRecord } from './record-write.js';
|
|
3
4
|
import { createSecureStore, DEFAULT_ACCOUNT_FOR_USER, SecureStoreUnavailableError, } from './secure-store.js';
|
|
4
5
|
const SKIP_REASON_MESSAGES = {
|
|
5
6
|
'identify-failed': 'could not identify user',
|
|
6
7
|
'legacy-keyring-unreachable': 'legacy credential is unreachable (keyring offline)',
|
|
8
|
+
'user-keyring-unreachable': 'per-user credential slot is unreachable (keyring offline)',
|
|
7
9
|
'user-record-write-failed': 'failed to update user records',
|
|
8
10
|
'marker-write-failed': 'failed to persist migration marker',
|
|
9
11
|
};
|
|
10
12
|
/**
|
|
11
13
|
* One-time migration of a v1 single-user auth state into a v2 multi-user
|
|
12
|
-
* shape. Best-effort: any failure
|
|
13
|
-
*
|
|
14
|
-
* fallback can keep serving the legacy token until the next attempt.
|
|
14
|
+
* shape. Best-effort: any failure leaves v1 untouched so the runtime
|
|
15
|
+
* fallback keeps serving the legacy token until the next attempt.
|
|
15
16
|
*
|
|
16
|
-
* Order
|
|
17
|
+
* Order is deliberate so the migration is one-way AND safe under retry:
|
|
17
18
|
*
|
|
18
|
-
* 1. `hasMigrated()` short-circuits
|
|
19
|
-
* 2. Read the v1 token (legacy keyring
|
|
20
|
-
* 3. `identifyAccount(token)` resolves the v2 `account
|
|
21
|
-
* 4. `
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
19
|
+
* 1. `hasMigrated()` short-circuits when the marker is set.
|
|
20
|
+
* 2. Read the v1 token (legacy keyring first, then plaintext).
|
|
21
|
+
* 3. `identifyAccount(token)` resolves the v2 `account`.
|
|
22
|
+
* 4. **Phase 1** — `ensureV2Record` writes a fallback-bearing record (or
|
|
23
|
+
* no-ops when a v2 record already exists).
|
|
24
|
+
* 5. **Phase 2** — when Phase 1 wrote: move the token to the per-user
|
|
25
|
+
* keyring slot and upsert the clean record. When Phase 1 didn't:
|
|
26
|
+
* verify the existing record is readable before retiring legacy.
|
|
27
|
+
* 6. Best-effort `setDefaultId(account.id)`.
|
|
28
|
+
* 7. `markMigrated()` — the one-way gate. Failure here surfaces as
|
|
29
|
+
* `skipped(marker-write-failed)` so the caller retries.
|
|
30
|
+
* 8. Best-effort legacy cleanup runs concurrently.
|
|
28
31
|
*/
|
|
29
32
|
export async function migrateLegacyAuth(options) {
|
|
30
33
|
const { serviceName, legacyAccount, userRecords, hasMigrated, markMigrated, loadLegacyPlaintextToken, identifyAccount, cleanupLegacyConfig, silent, } = options;
|
|
@@ -33,8 +36,6 @@ export async function migrateLegacyAuth(options) {
|
|
|
33
36
|
if (await hasMigrated()) {
|
|
34
37
|
return { status: 'already-migrated' };
|
|
35
38
|
}
|
|
36
|
-
// One legacy-keyring handle covers both the initial read and the
|
|
37
|
-
// post-success cleanup delete.
|
|
38
39
|
const legacyStore = createSecureStore({ serviceName, account: legacyAccount });
|
|
39
40
|
const legacyToken = await readLegacyToken(legacyStore, loadLegacyPlaintextToken);
|
|
40
41
|
if (legacyToken.kind === 'none')
|
|
@@ -49,28 +50,41 @@ export async function migrateLegacyAuth(options) {
|
|
|
49
50
|
catch (error) {
|
|
50
51
|
return skipped(silent, logPrefix, 'identify-failed', getErrorMessage(error));
|
|
51
52
|
}
|
|
52
|
-
|
|
53
|
-
// internally (writing to `fallbackToken` instead), so any error here is
|
|
54
|
-
// a non-keyring failure — typically a `userRecords.upsert` rejection.
|
|
53
|
+
let phase1;
|
|
55
54
|
try {
|
|
56
|
-
await
|
|
57
|
-
secureStore: createSecureStore({
|
|
58
|
-
serviceName,
|
|
59
|
-
account: accountForUser(account.id),
|
|
60
|
-
}),
|
|
61
|
-
userRecords,
|
|
62
|
-
account,
|
|
63
|
-
token: legacyToken.token,
|
|
64
|
-
});
|
|
55
|
+
phase1 = await ensureV2Record(userRecords, account, legacyToken.token);
|
|
65
56
|
}
|
|
66
57
|
catch (error) {
|
|
67
58
|
return skipped(silent, logPrefix, 'user-record-write-failed', getErrorMessage(error));
|
|
68
59
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
//
|
|
60
|
+
const userSlot = createSecureStore({
|
|
61
|
+
serviceName,
|
|
62
|
+
account: accountForUser(account.id),
|
|
63
|
+
});
|
|
64
|
+
// Run Phase 2 when EITHER Phase 1 just wrote the fallback record OR
|
|
65
|
+
// the existing record's fallback matches our legacy token — that's a
|
|
66
|
+
// prior-run Phase 1 we owe an upgrade. Other existing records are
|
|
67
|
+
// external state and get a readability check instead.
|
|
68
|
+
const isOurPriorPhase1 = !phase1.written && phase1.existing.fallbackToken?.trim() === legacyToken.token;
|
|
69
|
+
if (phase1.written || isOurPriorPhase1) {
|
|
70
|
+
const phase2Error = await runPhase2(userRecords, userSlot, account, legacyToken.token);
|
|
71
|
+
if (phase2Error) {
|
|
72
|
+
return skipped(silent, logPrefix, phase2Error.reason, phase2Error.detail);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
// External record — cleaning up legacy is safe only if it can be
|
|
77
|
+
// read in the current environment.
|
|
78
|
+
const outcome = await readAccessTokenForRecord(phase1.existing, userSlot);
|
|
79
|
+
if (!outcome.ok) {
|
|
80
|
+
const reason = outcome.reason === 'slot-unavailable'
|
|
81
|
+
? 'user-keyring-unreachable'
|
|
82
|
+
: 'user-record-write-failed';
|
|
83
|
+
return skipped(silent, logPrefix, reason, outcome.detail);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Only promote when nothing is pinned — a retry must not overwrite a
|
|
87
|
+
// default the user chose between attempts.
|
|
74
88
|
try {
|
|
75
89
|
const existingDefault = await userRecords.getDefaultId();
|
|
76
90
|
if (!existingDefault) {
|
|
@@ -80,36 +94,84 @@ export async function migrateLegacyAuth(options) {
|
|
|
80
94
|
catch {
|
|
81
95
|
// best-effort
|
|
82
96
|
}
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
// `skipped` so the caller retries. Without this ordering, a later
|
|
86
|
-
// `logout` could let the next run re-migrate the stale v1 token.
|
|
97
|
+
// Marker BEFORE cleanup: the gate, not cleanup, is what prevents the
|
|
98
|
+
// next run from re-migrating after a later `logout`.
|
|
87
99
|
try {
|
|
88
100
|
await markMigrated();
|
|
89
101
|
}
|
|
90
102
|
catch (error) {
|
|
91
103
|
return skipped(silent, logPrefix, 'marker-write-failed', getErrorMessage(error));
|
|
92
104
|
}
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
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.
|
|
105
|
+
// `Promise.resolve().then(...)` converts any *synchronous* throw from
|
|
106
|
+
// a consumer's `cleanupLegacyConfig` into a rejection that
|
|
107
|
+
// `allSettled` can swallow.
|
|
100
108
|
await Promise.allSettled([
|
|
101
109
|
Promise.resolve().then(() => legacyStore.deleteSecret()),
|
|
102
110
|
Promise.resolve().then(() => cleanupLegacyConfig?.()),
|
|
103
111
|
]);
|
|
104
112
|
if (!silent) {
|
|
105
|
-
//
|
|
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`.
|
|
113
|
+
// Account id may carry PII (email, etc.) — keep it out of logs.
|
|
109
114
|
console.error(`${logPrefix}: migrated existing token to multi-user store.`);
|
|
110
115
|
}
|
|
111
116
|
return { status: 'migrated', account };
|
|
112
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Phase 1. Writes a `fallbackToken`-bearing record so a crash before
|
|
120
|
+
* Phase 2 still leaves a working credential. Returns `{ written: true }`
|
|
121
|
+
* when this call wrote, or `{ written: false, existing }` when a v2
|
|
122
|
+
* record already existed — the existing record is returned so callers
|
|
123
|
+
* decide whether to upgrade it (Phase 2 retry) or treat it as external
|
|
124
|
+
* state, without paying a second `list()`.
|
|
125
|
+
*/
|
|
126
|
+
async function ensureV2Record(userRecords, account, legacyToken) {
|
|
127
|
+
const record = buildSingleTokenRecord(account, legacyToken);
|
|
128
|
+
if (userRecords.tryInsert) {
|
|
129
|
+
const wrote = await userRecords.tryInsert(record);
|
|
130
|
+
if (wrote)
|
|
131
|
+
return { written: true };
|
|
132
|
+
const existing = (await userRecords.list()).find((r) => r.account.id === account.id);
|
|
133
|
+
if (!existing) {
|
|
134
|
+
throw new Error('tryInsert returned false but no matching record was listed');
|
|
135
|
+
}
|
|
136
|
+
return { written: false, existing };
|
|
137
|
+
}
|
|
138
|
+
// Non-atomic path. Narrow time-of-check, time-of-use race between
|
|
139
|
+
// `list()` and `upsert()`; acceptable for one-time migration since
|
|
140
|
+
// concurrent runs would write the same shape.
|
|
141
|
+
const all = await userRecords.list();
|
|
142
|
+
const existing = all.find((r) => r.account.id === account.id);
|
|
143
|
+
if (existing)
|
|
144
|
+
return { written: false, existing };
|
|
145
|
+
await userRecords.upsert(record);
|
|
146
|
+
return { written: true };
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Phase 2: move the legacy token into the per-user keyring slot and
|
|
150
|
+
* upsert a clean (no `fallbackToken`) record. Inlined rather than
|
|
151
|
+
* delegating to `writeRecordWithKeyringFallback` so the offline-keyring
|
|
152
|
+
* branch doesn't double-upsert the same fallback record Phase 1 just
|
|
153
|
+
* wrote. Returns `null` on success (including the silently-handled
|
|
154
|
+
* SecureStoreUnavailable case); a skip descriptor when a non-keyring
|
|
155
|
+
* failure leaves the marker unset for retry.
|
|
156
|
+
*/
|
|
157
|
+
async function runPhase2(userRecords, userSlot, account, legacyToken) {
|
|
158
|
+
try {
|
|
159
|
+
await userSlot.setSecret(legacyToken);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
if (error instanceof SecureStoreUnavailableError) {
|
|
163
|
+
return null; // Phase 1 fallback record continues to serve reads.
|
|
164
|
+
}
|
|
165
|
+
return { reason: 'user-record-write-failed', detail: getErrorMessage(error) };
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
await userRecords.upsert(buildSingleTokenRecord(account));
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
return { reason: 'user-record-write-failed', detail: getErrorMessage(error) };
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
113
175
|
async function readLegacyToken(legacyStore, loadLegacyPlaintextToken) {
|
|
114
176
|
let keyringUnavailable = false;
|
|
115
177
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrate.js","sourceRoot":"","sources":["../../../src/auth/keyring/migrate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAEjD,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"migrate.js","sourceRoot":"","sources":["../../../src/auth/keyring/migrate.ts"],"names":[],"mappings":"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"}
|