@doist/cli-core 0.16.0 → 0.17.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 +26 -18
- package/dist/auth/flow.d.ts +9 -2
- package/dist/auth/flow.d.ts.map +1 -1
- package/dist/auth/flow.js +75 -9
- package/dist/auth/flow.js.map +1 -1
- 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/record-write.d.ts +57 -15
- package/dist/auth/keyring/record-write.d.ts.map +1 -1
- package/dist/auth/keyring/record-write.js +121 -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 +89 -50
- 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
|
@@ -1,50 +1,141 @@
|
|
|
1
|
+
import { CliError } from '../../errors.js';
|
|
1
2
|
import { SecureStoreUnavailableError } from './secure-store.js';
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Single-token write. Thin wrapper over `writeBundleWithKeyringFallback`
|
|
5
|
+
* passing a refresh-less bundle, so trim/validate, access-slot fallback,
|
|
6
|
+
* upsert rollback, and the deferred refresh-slot wipe all share one
|
|
7
|
+
* implementation.
|
|
6
8
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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.
|
|
9
|
+
* `refreshStore` is optional purely for legacy callers (`migrateLegacyAuth`)
|
|
10
|
+
* that don't have one wired; the migrate path never had refresh state so
|
|
11
|
+
* skipping the wipe is correct there.
|
|
18
12
|
*/
|
|
19
13
|
export async function writeRecordWithKeyringFallback(options) {
|
|
20
|
-
const { secureStore, userRecords, account, token } = options;
|
|
21
|
-
const
|
|
22
|
-
|
|
14
|
+
const { secureStore, refreshStore, userRecords, account, token } = options;
|
|
15
|
+
const { accessStoredSecurely } = await writeBundleWithKeyringFallback({
|
|
16
|
+
accessStore: secureStore,
|
|
17
|
+
// No-op store when the caller didn't wire one — the deferred wipe
|
|
18
|
+
// becomes inert and we don't accidentally create a refresh slot
|
|
19
|
+
// for legacy/migrate paths.
|
|
20
|
+
refreshStore: refreshStore ?? NOOP_SECURE_STORE,
|
|
21
|
+
userRecords,
|
|
22
|
+
account,
|
|
23
|
+
bundle: { accessToken: token },
|
|
24
|
+
});
|
|
25
|
+
return { storedSecurely: accessStoredSecurely };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Two-slot write. Order: access slot → refresh slot → upsert → deferred
|
|
29
|
+
* refresh wipe.
|
|
30
|
+
*
|
|
31
|
+
* 1. Validate `bundle.accessToken` (non-empty after trim).
|
|
32
|
+
* 2. `accessStore.setSecret`. `SecureStoreUnavailableError` degrades to
|
|
33
|
+
* `fallbackToken` on the record; any other error rethrows.
|
|
34
|
+
* 3. `refreshStore.setSecret` when `bundle.refreshToken` is present.
|
|
35
|
+
* `SecureStoreUnavailableError` degrades to `fallbackRefreshToken`. A
|
|
36
|
+
* non-keyring failure rolls back the access slot before rethrowing
|
|
37
|
+
* (no partial credentials left behind for an unregistered user).
|
|
38
|
+
* 4. `userRecords.upsert(record)`. On failure, best-effort
|
|
39
|
+
* `Promise.allSettled` rollback of any slot writes that succeeded.
|
|
40
|
+
* 5. Only after a successful upsert: if the bundle has no refresh token,
|
|
41
|
+
* wipe any orphan slot from a prior `setBundle` (best-effort). Doing
|
|
42
|
+
* this BEFORE the upsert would lose refresh state if the upsert then
|
|
43
|
+
* rejected — the new record's `hasRefreshToken` would still claim
|
|
44
|
+
* false but the old slot would be gone with no rollback path.
|
|
45
|
+
*
|
|
46
|
+
* Default promotion is external — preference, not correctness, and an
|
|
47
|
+
* error there must not dirty up a successful credential write.
|
|
48
|
+
*/
|
|
49
|
+
export async function writeBundleWithKeyringFallback(options) {
|
|
50
|
+
const { accessStore, refreshStore, userRecords, account, bundle } = options;
|
|
51
|
+
const accessToken = bundle.accessToken.trim();
|
|
52
|
+
if (!accessToken) {
|
|
53
|
+
throw new CliError('AUTH_STORE_WRITE_FAILED', 'Refusing to persist a bundle with an empty access token.');
|
|
54
|
+
}
|
|
55
|
+
const refreshToken = bundle.refreshToken?.trim();
|
|
56
|
+
let accessStoredSecurely = false;
|
|
23
57
|
try {
|
|
24
|
-
await
|
|
25
|
-
|
|
58
|
+
await accessStore.setSecret(accessToken);
|
|
59
|
+
accessStoredSecurely = true;
|
|
26
60
|
}
|
|
27
61
|
catch (error) {
|
|
28
62
|
if (!(error instanceof SecureStoreUnavailableError))
|
|
29
63
|
throw error;
|
|
30
64
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
65
|
+
let refreshStoredSecurely;
|
|
66
|
+
if (refreshToken) {
|
|
67
|
+
try {
|
|
68
|
+
await refreshStore.setSecret(refreshToken);
|
|
69
|
+
refreshStoredSecurely = true;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error instanceof SecureStoreUnavailableError) {
|
|
73
|
+
refreshStoredSecurely = false;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
if (accessStoredSecurely) {
|
|
77
|
+
try {
|
|
78
|
+
await accessStore.deleteSecret();
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// best-effort
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const record = {
|
|
89
|
+
account,
|
|
90
|
+
...(accessStoredSecurely ? {} : { fallbackToken: accessToken }),
|
|
91
|
+
...(refreshToken && refreshStoredSecurely === false
|
|
92
|
+
? { fallbackRefreshToken: refreshToken }
|
|
93
|
+
: {}),
|
|
94
|
+
...(bundle.accessTokenExpiresAt !== undefined
|
|
95
|
+
? { accessTokenExpiresAt: bundle.accessTokenExpiresAt }
|
|
96
|
+
: {}),
|
|
97
|
+
...(bundle.refreshTokenExpiresAt !== undefined
|
|
98
|
+
? { refreshTokenExpiresAt: bundle.refreshTokenExpiresAt }
|
|
99
|
+
: {}),
|
|
100
|
+
hasRefreshToken: Boolean(refreshToken),
|
|
101
|
+
};
|
|
34
102
|
try {
|
|
35
103
|
await userRecords.upsert(record);
|
|
36
104
|
}
|
|
37
105
|
catch (error) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
106
|
+
const rollbacks = [];
|
|
107
|
+
if (accessStoredSecurely)
|
|
108
|
+
rollbacks.push(accessStore.deleteSecret());
|
|
109
|
+
if (refreshStoredSecurely === true)
|
|
110
|
+
rollbacks.push(refreshStore.deleteSecret());
|
|
111
|
+
if (rollbacks.length > 0) {
|
|
112
|
+
await Promise.allSettled(rollbacks);
|
|
45
113
|
}
|
|
46
114
|
throw error;
|
|
47
115
|
}
|
|
48
|
-
|
|
116
|
+
// Deferred: wipe any orphan refresh slot from a prior setBundle now
|
|
117
|
+
// that the new record (with `hasRefreshToken: false`) is durable. If
|
|
118
|
+
// this fails the gate already prevents readers from consulting it; the
|
|
119
|
+
// worst case is a stale keyring entry that `clear()` will pick up.
|
|
120
|
+
if (!refreshToken) {
|
|
121
|
+
try {
|
|
122
|
+
await refreshStore.deleteSecret();
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// best-effort
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return { accessStoredSecurely, refreshStoredSecurely };
|
|
49
129
|
}
|
|
130
|
+
const NOOP_SECURE_STORE = {
|
|
131
|
+
async getSecret() {
|
|
132
|
+
return null;
|
|
133
|
+
},
|
|
134
|
+
async setSecret() {
|
|
135
|
+
// no-op
|
|
136
|
+
},
|
|
137
|
+
async deleteSecret() {
|
|
138
|
+
return false;
|
|
139
|
+
},
|
|
140
|
+
};
|
|
50
141
|
//# sourceMappingURL=record-write.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"record-write.js","sourceRoot":"","sources":["../../../src/auth/keyring/record-write.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"record-write.js","sourceRoot":"","sources":["../../../src/auth/keyring/record-write.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derives the refresh slot name from the access slug. Single-sourced so the
|
|
3
|
+
* write and read paths can't drift onto different suffixes. Internal.
|
|
4
|
+
*/
|
|
5
|
+
export declare function refreshAccountSlot(accountSlug: string): string;
|
|
6
|
+
//# sourceMappingURL=slot-naming.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slot-naming.d.ts","sourceRoot":"","sources":["../../../src/auth/keyring/slot-naming.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE9D"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derives the refresh slot name from the access slug. Single-sourced so the
|
|
3
|
+
* write and read paths can't drift onto different suffixes. Internal.
|
|
4
|
+
*/
|
|
5
|
+
export function refreshAccountSlot(accountSlug) {
|
|
6
|
+
return `${accountSlug}/refresh`;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=slot-naming.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slot-naming.js","sourceRoot":"","sources":["../../../src/auth/keyring/slot-naming.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,WAAmB;IAClD,OAAO,GAAG,WAAW,UAAU,CAAA;AACnC,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AccountRef, AuthAccount, TokenStore } from '../types.js';
|
|
1
|
+
import type { AccountRef, AuthAccount, TokenBundle, TokenStore } from '../types.js';
|
|
2
2
|
import type { TokenStorageResult, UserRecordStore } from './types.js';
|
|
3
3
|
export type CreateKeyringTokenStoreOptions<TAccount extends AuthAccount> = {
|
|
4
4
|
/** Application identifier used for every keyring entry (e.g. `'todoist-cli'`). */
|
|
@@ -24,7 +24,15 @@ export type CreateKeyringTokenStoreOptions<TAccount extends AuthAccount> = {
|
|
|
24
24
|
matchAccount?: (account: TAccount, ref: AccountRef) => boolean;
|
|
25
25
|
};
|
|
26
26
|
export type KeyringTokenStore<TAccount extends AuthAccount> = TokenStore<TAccount> & {
|
|
27
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Override `setBundle` as required (not optional) — the keyring store
|
|
29
|
+
* always knows how to persist refresh state. Lets cli-core helpers
|
|
30
|
+
* (`persistBundle`) call it without a non-null assertion.
|
|
31
|
+
*/
|
|
32
|
+
setBundle(account: TAccount, bundle: TokenBundle, options?: {
|
|
33
|
+
promoteDefault?: boolean;
|
|
34
|
+
}): Promise<void>;
|
|
35
|
+
/** Storage result from the most recent `set()` / `setBundle()` call, or `undefined` before any (and reset to `undefined` when the most recent write threw). */
|
|
28
36
|
getLastStorageResult(): TokenStorageResult | undefined;
|
|
29
37
|
/** 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
38
|
getLastClearResult(): TokenStorageResult | undefined;
|
|
@@ -1 +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;
|
|
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,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAWnF,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;;;;OAIG;IACH,SAAS,CACL,OAAO,EAAE,QAAQ,EACjB,MAAM,EAAE,WAAW,EACnB,OAAO,CAAC,EAAE;QAAE,cAAc,CAAC,EAAE,OAAO,CAAA;KAAE,GACvC,OAAO,CAAC,IAAI,CAAC,CAAA;IAChB,+JAA+J;IAC/J,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,CAoS7B"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { CliError } from '../../errors.js';
|
|
1
|
+
import { CliError, getErrorMessage } from '../../errors.js';
|
|
2
2
|
import { accountNotFoundError } from '../user-flag.js';
|
|
3
|
-
import { writeRecordWithKeyringFallback } from './record-write.js';
|
|
3
|
+
import { writeBundleWithKeyringFallback, writeRecordWithKeyringFallback } from './record-write.js';
|
|
4
4
|
import { createSecureStore, DEFAULT_ACCOUNT_FOR_USER, SECURE_STORE_DESCRIPTION, SecureStoreUnavailableError, } from './secure-store.js';
|
|
5
|
+
import { refreshAccountSlot } from './slot-naming.js';
|
|
5
6
|
const DEFAULT_MATCH_ACCOUNT = (account, ref) => account.id === ref || account.label === ref;
|
|
6
7
|
/**
|
|
7
8
|
* Multi-account `TokenStore` that keeps secrets in the OS credential manager
|
|
@@ -37,6 +38,12 @@ export function createKeyringTokenStore(options) {
|
|
|
37
38
|
function secureStoreFor(account) {
|
|
38
39
|
return createSecureStore({ serviceName, account: accountForUser(account.id) });
|
|
39
40
|
}
|
|
41
|
+
function refreshSecureStoreFor(account) {
|
|
42
|
+
return createSecureStore({
|
|
43
|
+
serviceName,
|
|
44
|
+
account: refreshAccountSlot(accountForUser(account.id)),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
40
47
|
/**
|
|
41
48
|
* Read both `list()` and `getDefaultId()` concurrently. Used by paths
|
|
42
49
|
* that need the pinned default (no-ref `active`/`clear`, `list`, and
|
|
@@ -87,6 +94,43 @@ export function createKeyringTokenStore(options) {
|
|
|
87
94
|
warning: `${SECURE_STORE_DESCRIPTION} unavailable; ${action} ${recordsLocation}`,
|
|
88
95
|
};
|
|
89
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Compose a storage result for a write that may have fallen back on
|
|
99
|
+
* either slot. `accessStored === false` indicates the access token went
|
|
100
|
+
* to `fallbackToken`; `refreshStored === false` indicates the refresh
|
|
101
|
+
* token went to `fallbackRefreshToken`. Either falsy slot downgrades
|
|
102
|
+
* the result to `config-file` so consumers see the warning — refresh
|
|
103
|
+
* plaintext is just as security-relevant as access plaintext.
|
|
104
|
+
*/
|
|
105
|
+
function bundleStorageResult(accessStored, refreshStored) {
|
|
106
|
+
const accessFallback = !accessStored;
|
|
107
|
+
const refreshFallback = refreshStored === false;
|
|
108
|
+
if (!accessFallback && !refreshFallback)
|
|
109
|
+
return { storage: 'secure-store' };
|
|
110
|
+
const subject = accessFallback && refreshFallback
|
|
111
|
+
? 'access + refresh tokens'
|
|
112
|
+
: accessFallback
|
|
113
|
+
? 'access token'
|
|
114
|
+
: 'refresh token';
|
|
115
|
+
return fallbackResult(`${subject} saved as plaintext in`);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Best-effort default promotion shared by `set` and `setBundle`. The
|
|
119
|
+
* record is already persisted, so a failure here must not surface as
|
|
120
|
+
* `AUTH_STORE_WRITE_FAILED` — the user can recover by setting a
|
|
121
|
+
* default later.
|
|
122
|
+
*/
|
|
123
|
+
async function promoteDefaultIfNeeded(accountId) {
|
|
124
|
+
try {
|
|
125
|
+
const existingDefault = await userRecords.getDefaultId();
|
|
126
|
+
if (!existingDefault) {
|
|
127
|
+
await userRecords.setDefaultId(accountId);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// best-effort
|
|
132
|
+
}
|
|
133
|
+
}
|
|
90
134
|
return {
|
|
91
135
|
async active(ref) {
|
|
92
136
|
// Ref-only path skips `getDefaultId()` — `resolveTarget` never
|
|
@@ -98,37 +142,31 @@ export function createKeyringTokenStore(options) {
|
|
|
98
142
|
const record = resolveTarget(snapshot, ref);
|
|
99
143
|
if (!record)
|
|
100
144
|
return null;
|
|
145
|
+
// Reads the access slot only. Refresh-state material lives in
|
|
146
|
+
// the keyring and on the record, but `active()` stays cheap and
|
|
147
|
+
// returns the pre-PR1 snapshot shape — a future bundle-aware
|
|
148
|
+
// read path lights up the refresh slot only when callers
|
|
149
|
+
// actually need it (silent refresh).
|
|
101
150
|
const fallback = record.fallbackToken?.trim();
|
|
102
|
-
if (fallback)
|
|
151
|
+
if (fallback)
|
|
103
152
|
return { token: fallback, account: record.account };
|
|
104
|
-
}
|
|
105
153
|
let raw;
|
|
106
154
|
try {
|
|
107
155
|
raw = await secureStoreFor(record.account).getSecret();
|
|
108
156
|
}
|
|
109
157
|
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
158
|
if (error instanceof SecureStoreUnavailableError) {
|
|
118
159
|
throw new CliError('AUTH_STORE_READ_FAILED', `${SECURE_STORE_DESCRIPTION} unavailable; could not read stored token (${error.message})`);
|
|
119
160
|
}
|
|
120
|
-
|
|
161
|
+
// Non-keyring backend failures wrap into the typed code too —
|
|
162
|
+
// a raw exception escaping `active()` would crash the CLI
|
|
163
|
+
// with no useful exit signal.
|
|
164
|
+
throw new CliError('AUTH_STORE_READ_FAILED', `Access-slot read failed (${getErrorMessage(error)})`);
|
|
121
165
|
}
|
|
122
166
|
const token = raw?.trim();
|
|
123
|
-
if (token)
|
|
167
|
+
if (token)
|
|
124
168
|
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.
|
|
169
|
+
// Record exists, no `fallbackToken`, slot empty — corruption.
|
|
132
170
|
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
171
|
},
|
|
134
172
|
async set(account, token) {
|
|
@@ -138,25 +176,29 @@ export function createKeyringTokenStore(options) {
|
|
|
138
176
|
lastStorageResult = undefined;
|
|
139
177
|
const { storedSecurely } = await writeRecordWithKeyringFallback({
|
|
140
178
|
secureStore: secureStoreFor(account),
|
|
179
|
+
refreshStore: refreshSecureStoreFor(account),
|
|
141
180
|
userRecords,
|
|
142
181
|
account,
|
|
143
182
|
token,
|
|
144
183
|
});
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
184
|
+
await promoteDefaultIfNeeded(account.id);
|
|
185
|
+
lastStorageResult = bundleStorageResult(storedSecurely, undefined);
|
|
186
|
+
},
|
|
187
|
+
async setBundle(account, bundle, options) {
|
|
188
|
+
lastStorageResult = undefined;
|
|
189
|
+
const { accessStoredSecurely, refreshStoredSecurely } = await writeBundleWithKeyringFallback({
|
|
190
|
+
accessStore: secureStoreFor(account),
|
|
191
|
+
refreshStore: refreshSecureStoreFor(account),
|
|
192
|
+
userRecords,
|
|
193
|
+
account,
|
|
194
|
+
bundle,
|
|
195
|
+
});
|
|
196
|
+
// Opt-in: silent refresh omits `promoteDefault` so it can't
|
|
197
|
+
// re-pin selection; login passes `true` to match `set()`.
|
|
198
|
+
if (options?.promoteDefault) {
|
|
199
|
+
await promoteDefaultIfNeeded(account.id);
|
|
156
200
|
}
|
|
157
|
-
lastStorageResult =
|
|
158
|
-
? { storage: 'secure-store' }
|
|
159
|
-
: fallbackResult('token saved as plaintext in');
|
|
201
|
+
lastStorageResult = bundleStorageResult(accessStoredSecurely, refreshStoredSecurely);
|
|
160
202
|
},
|
|
161
203
|
async clear(ref) {
|
|
162
204
|
// Reset up front for the same reason as `set` — and so a no-op
|
|
@@ -183,22 +225,19 @@ export function createKeyringTokenStore(options) {
|
|
|
183
225
|
}
|
|
184
226
|
}
|
|
185
227
|
const fallbackClear = fallbackResult('local auth state cleared in');
|
|
186
|
-
// Always attempt
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
catch {
|
|
200
|
-
lastClearResult = fallbackClear;
|
|
201
|
-
}
|
|
228
|
+
// Always attempt both deletes — a record's `fallbackToken`
|
|
229
|
+
// doesn't rule out an orphan keyring entry from a prior online
|
|
230
|
+
// write. Failures downgrade to a warning: the record is already
|
|
231
|
+
// gone, re-throwing would corrupt the caller's state.
|
|
232
|
+
const [accessOutcome, refreshOutcome] = await Promise.allSettled([
|
|
233
|
+
secureStoreFor(record.account).deleteSecret(),
|
|
234
|
+
refreshSecureStoreFor(record.account).deleteSecret(),
|
|
235
|
+
]);
|
|
236
|
+
const fellBack = accessOutcome.status === 'rejected' ||
|
|
237
|
+
refreshOutcome.status === 'rejected' ||
|
|
238
|
+
record.fallbackToken !== undefined ||
|
|
239
|
+
record.fallbackRefreshToken !== undefined;
|
|
240
|
+
lastClearResult = fellBack ? fallbackClear : { storage: 'secure-store' };
|
|
202
241
|
},
|
|
203
242
|
async list() {
|
|
204
243
|
const snapshot = await readFullSnapshot();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-store.js","sourceRoot":"","sources":["../../../src/auth/keyring/token-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;
|
|
1
|
+
{"version":3,"file":"token-store.js","sourceRoot":"","sources":["../../../src/auth/keyring/token-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -22,6 +22,20 @@ export type UserRecord<TAccount extends AuthAccount> = {
|
|
|
22
22
|
* that would otherwise live in the OS credential manager.
|
|
23
23
|
*/
|
|
24
24
|
fallbackToken?: string;
|
|
25
|
+
/** Same lifecycle and security profile as `fallbackToken`, for the refresh slot. */
|
|
26
|
+
fallbackRefreshToken?: string;
|
|
27
|
+
/** Access-token expiry, unix-epoch ms. */
|
|
28
|
+
accessTokenExpiresAt?: number;
|
|
29
|
+
/** Refresh-token expiry, unix-epoch ms. */
|
|
30
|
+
refreshTokenExpiresAt?: number;
|
|
31
|
+
/**
|
|
32
|
+
* `true` when a refresh secret is stored (in the keyring or as
|
|
33
|
+
* `fallbackRefreshToken`); `false` when explicitly cleared by `set()`
|
|
34
|
+
* or by a no-refresh `setBundle`; `undefined` on legacy records that
|
|
35
|
+
* predate the bundle contract. Read by future bundle-aware accessors;
|
|
36
|
+
* `active()` itself doesn't consult it.
|
|
37
|
+
*/
|
|
38
|
+
hasRefreshToken?: boolean;
|
|
25
39
|
};
|
|
26
40
|
/**
|
|
27
41
|
* Port the consumer implements to expose their per-user config records to
|
|
@@ -39,6 +53,12 @@ export type UserRecordStore<TAccount extends AuthAccount> = {
|
|
|
39
53
|
* `fallbackToken` over the keyring). Records are keyed by `account.id`.
|
|
40
54
|
*/
|
|
41
55
|
upsert(record: UserRecord<TAccount>): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Optional atomic insert. Returns `true` on write, `false` if `account.id`
|
|
58
|
+
* already exists. Migration prefers it to eliminate the existence-check
|
|
59
|
+
* TOCTOU race; callers fall back to list-then-upsert when absent.
|
|
60
|
+
*/
|
|
61
|
+
tryInsert?(record: UserRecord<TAccount>): Promise<boolean>;
|
|
42
62
|
/** Remove the record whose `account.id` matches. */
|
|
43
63
|
remove(id: string): Promise<void>;
|
|
44
64
|
/** The pinned default's `account.id`, or `null` when nothing is pinned. */
|
|
@@ -1 +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;
|
|
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;IACtB,oFAAoF;IACpF,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B,0CAA0C;IAC1C,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B,2CAA2C;IAC3C,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;CAC5B,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;;;;OAIG;IACH,SAAS,CAAC,CAAC,MAAM,EAAE,UAAU,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC1D,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,23 @@
|
|
|
1
|
+
import type { AuthAccount, TokenBundle, TokenStore } from './types.js';
|
|
2
|
+
export type PersistBundleOptions<TAccount extends AuthAccount> = {
|
|
3
|
+
store: TokenStore<TAccount>;
|
|
4
|
+
account: TAccount;
|
|
5
|
+
bundle: TokenBundle;
|
|
6
|
+
/** Forwarded to `setBundle` when present. See `TokenStore.setBundle`. */
|
|
7
|
+
promoteDefault?: boolean;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Persist a bundle against any `TokenStore`. Prefers `setBundle` when the
|
|
11
|
+
* store implements it; otherwise falls back to `set(account, accessToken)`
|
|
12
|
+
* and silently drops refresh state. Wraps non-`CliError` failures as
|
|
13
|
+
* `AUTH_STORE_WRITE_FAILED`.
|
|
14
|
+
*
|
|
15
|
+
* `promoteDefault` is only honoured on the `setBundle` path — the base
|
|
16
|
+
* `set()` contract has no promotion control, so a custom multi-account
|
|
17
|
+
* store that opts out of `setBundle` will run its own promotion policy
|
|
18
|
+
* (typically first-account-wins). Multi-account stores that need
|
|
19
|
+
* silent-refresh-safe selection (no re-pinning on background rotation)
|
|
20
|
+
* MUST implement `setBundle`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function persistBundle<TAccount extends AuthAccount>(options: PersistBundleOptions<TAccount>): Promise<void>;
|
|
23
|
+
//# sourceMappingURL=persist.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persist.d.ts","sourceRoot":"","sources":["../../src/auth/persist.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAEtE,MAAM,MAAM,oBAAoB,CAAC,QAAQ,SAAS,WAAW,IAAI;IAC7D,KAAK,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAA;IAC3B,OAAO,EAAE,QAAQ,CAAA;IACjB,MAAM,EAAE,WAAW,CAAA;IACnB,yEAAyE;IACzE,cAAc,CAAC,EAAE,OAAO,CAAA;CAC3B,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,aAAa,CAAC,QAAQ,SAAS,WAAW,EAC5D,OAAO,EAAE,oBAAoB,CAAC,QAAQ,CAAC,GACxC,OAAO,CAAC,IAAI,CAAC,CAqBf"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { CliError, getErrorMessage } from '../errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Persist a bundle against any `TokenStore`. Prefers `setBundle` when the
|
|
4
|
+
* store implements it; otherwise falls back to `set(account, accessToken)`
|
|
5
|
+
* and silently drops refresh state. Wraps non-`CliError` failures as
|
|
6
|
+
* `AUTH_STORE_WRITE_FAILED`.
|
|
7
|
+
*
|
|
8
|
+
* `promoteDefault` is only honoured on the `setBundle` path — the base
|
|
9
|
+
* `set()` contract has no promotion control, so a custom multi-account
|
|
10
|
+
* store that opts out of `setBundle` will run its own promotion policy
|
|
11
|
+
* (typically first-account-wins). Multi-account stores that need
|
|
12
|
+
* silent-refresh-safe selection (no re-pinning on background rotation)
|
|
13
|
+
* MUST implement `setBundle`.
|
|
14
|
+
*/
|
|
15
|
+
export async function persistBundle(options) {
|
|
16
|
+
const { store, account, bundle, promoteDefault } = options;
|
|
17
|
+
try {
|
|
18
|
+
if (store.setBundle) {
|
|
19
|
+
// Omit the options arg entirely when unset so presence-based
|
|
20
|
+
// handlers can distinguish "default" from explicit opt-out.
|
|
21
|
+
if (promoteDefault === undefined) {
|
|
22
|
+
await store.setBundle(account, bundle);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
await store.setBundle(account, bundle, { promoteDefault });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
await store.set(account, bundle.accessToken);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error instanceof CliError)
|
|
34
|
+
throw error;
|
|
35
|
+
throw new CliError('AUTH_STORE_WRITE_FAILED', `Failed to persist token: ${getErrorMessage(error)}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=persist.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persist.js","sourceRoot":"","sources":["../../src/auth/persist.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAWxD;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAC/B,OAAuC;IAEvC,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,GAAG,OAAO,CAAA;IAC1D,IAAI,CAAC;QACD,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;YAClB,6DAA6D;YAC7D,4DAA4D;YAC5D,IAAI,cAAc,KAAK,SAAS,EAAE,CAAC;gBAC/B,MAAM,KAAK,CAAC,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;YAC1C,CAAC;iBAAM,CAAC;gBACJ,MAAM,KAAK,CAAC,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,cAAc,EAAE,CAAC,CAAA;YAC9D,CAAC;QACL,CAAC;aAAM,CAAC;YACJ,MAAM,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,WAAW,CAAC,CAAA;QAChD,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACb,IAAI,KAAK,YAAY,QAAQ;YAAE,MAAM,KAAK,CAAA;QAC1C,MAAM,IAAI,QAAQ,CACd,yBAAyB,EACzB,4BAA4B,eAAe,CAAC,KAAK,CAAC,EAAE,CACvD,CAAA;IACL,CAAC;AACL,CAAC"}
|