@wirecat/cli-core 0.493.0 → 0.494.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/dist/oauth/keeper.js +26 -3
- package/dist/oauth/storage.d.ts +5 -0
- package/dist/oauth/storage.js +8 -2
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.494.0 — 31.10.2026
|
|
11
|
+
|
|
12
|
+
### Security
|
|
13
|
+
|
|
14
|
+
- **A run with moved folders no longer copies the owner's sign-in into the keyring when it refreshes a token.** Since
|
|
15
|
+
cli-base 0.65.5 such a run (`<PREFIX>_*_DIR` overrides, or `HOME` or XDG folders away from the account's home) writes
|
|
16
|
+
to a temporary entry `<app>:<config folder>`, so every refresh of an expired Google token left a full copy of the
|
|
17
|
+
owner's sign-in there until `session end`. `tokenKeeper` now keeps such a refresh in memory for the rest of the
|
|
18
|
+
process and saves nothing; a new sign-in, a token saved by the CLI's own flow, and a refresh that rotated the
|
|
19
|
+
refresh token are still saved. `credentialStorage` also no longer copies an older account's entry to the new
|
|
20
|
+
account in such a run. New optional `TokenStorage.moved`.
|
|
21
|
+
|
|
10
22
|
## 0.493.0 — 31.10.2026
|
|
11
23
|
|
|
12
24
|
### Added
|
package/dist/oauth/keeper.js
CHANGED
|
@@ -6,6 +6,7 @@ import { acquireLock } from "../files/index.js";
|
|
|
6
6
|
import { authorize } from "./authorize.js";
|
|
7
7
|
import { requestTokens, revokeToken, TOKEN_LOG_EVENT } from "./requests.js";
|
|
8
8
|
const inFlight = new Map();
|
|
9
|
+
const refreshedInMemory = new Map();
|
|
9
10
|
export const tokenKeeper = (options) => {
|
|
10
11
|
const { provider, storage, signal, log } = options;
|
|
11
12
|
const now = options.now ?? Date.now;
|
|
@@ -81,12 +82,20 @@ export const tokenKeeper = (options) => {
|
|
|
81
82
|
inFlight.set(lockPath, started);
|
|
82
83
|
return started;
|
|
83
84
|
};
|
|
85
|
+
const remembered = (saved) => {
|
|
86
|
+
const kept = refreshedInMemory.get(lockPath);
|
|
87
|
+
return kept && fresh(kept) && kept.refreshToken === saved.refreshToken ? kept : undefined;
|
|
88
|
+
};
|
|
89
|
+
const forget = () => refreshedInMemory.delete(lockPath);
|
|
84
90
|
const tokens = async () => {
|
|
85
91
|
const seen = storage.read();
|
|
86
92
|
if (!seen)
|
|
87
93
|
throw notSignedIn();
|
|
88
94
|
if (fresh(seen.tokens))
|
|
89
95
|
return seen.tokens;
|
|
96
|
+
const kept = remembered(seen.tokens);
|
|
97
|
+
if (kept)
|
|
98
|
+
return kept;
|
|
90
99
|
return shared(async () => {
|
|
91
100
|
const current = storage.read();
|
|
92
101
|
if (!current)
|
|
@@ -94,9 +103,16 @@ export const tokenKeeper = (options) => {
|
|
|
94
103
|
// Another process refreshed while this one waited for the lock: its result is the one to use.
|
|
95
104
|
if (fresh(current.tokens) || current.tokens.refreshToken !== seen.tokens.refreshToken)
|
|
96
105
|
return current.tokens;
|
|
106
|
+
const keptMeanwhile = remembered(current.tokens);
|
|
107
|
+
if (keptMeanwhile)
|
|
108
|
+
return keptMeanwhile;
|
|
97
109
|
const client = clientOf(current);
|
|
98
110
|
const refreshed = await exchange({ grant_type: "refresh_token", refresh_token: current.tokens.refreshToken }, context(client, current.tokens));
|
|
99
|
-
|
|
111
|
+
// A rotated refresh token is saved even so: kept only in memory, it would die with the process.
|
|
112
|
+
if (storage.moved && refreshed.refreshToken === current.tokens.refreshToken)
|
|
113
|
+
refreshedInMemory.set(lockPath, refreshed);
|
|
114
|
+
else
|
|
115
|
+
storage.write({ tokens: refreshed, client });
|
|
100
116
|
return refreshed;
|
|
101
117
|
});
|
|
102
118
|
};
|
|
@@ -119,11 +135,18 @@ export const tokenKeeper = (options) => {
|
|
|
119
135
|
code_verifier: authorization.codeVerifier,
|
|
120
136
|
}, context(client));
|
|
121
137
|
const extra = (await accept?.(received)) ?? undefined;
|
|
122
|
-
await locked(async () =>
|
|
138
|
+
await locked(async () => {
|
|
139
|
+
forget();
|
|
140
|
+
storage.write({ tokens: received, client, ...(extra ? { extra } : {}) });
|
|
141
|
+
});
|
|
123
142
|
return { scopes: received.scopes };
|
|
124
143
|
},
|
|
125
|
-
save: (saved, extra) => locked(async () =>
|
|
144
|
+
save: (saved, extra) => locked(async () => {
|
|
145
|
+
forget();
|
|
146
|
+
storage.write({ tokens: saved, client: clientOf(storage.read()), ...(extra ? { extra } : {}) });
|
|
147
|
+
}),
|
|
126
148
|
signOut: ({ revoke = false } = {}) => locked(async () => {
|
|
149
|
+
forget();
|
|
127
150
|
const current = storage.read();
|
|
128
151
|
let revoked = false;
|
|
129
152
|
if (revoke && current) {
|
package/dist/oauth/storage.d.ts
CHANGED
|
@@ -21,6 +21,11 @@ export interface TokenCodec {
|
|
|
21
21
|
export interface TokenStorage {
|
|
22
22
|
/** Names the entry across processes; the lock is keyed by it. */
|
|
23
23
|
readonly key: string;
|
|
24
|
+
/**
|
|
25
|
+
* A run with moved folders: cli-base sends its writes to a temporary keyring entry, so a refresh is kept in memory
|
|
26
|
+
* instead of copying the owner's sign-in there. Only a new sign-in is saved.
|
|
27
|
+
*/
|
|
28
|
+
readonly moved?: boolean;
|
|
24
29
|
/** Throws when the entry cannot be saved, so a sign-in fails before the browser, not after consent. */
|
|
25
30
|
probe(): void;
|
|
26
31
|
read(): StoredTokens | undefined;
|
package/dist/oauth/storage.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CliError, Credentials, keyringService, } from "@wirecat/cli-base";
|
|
1
|
+
import { CliError, Credentials, isMovedRun, keyringService, systemKeyring, } from "@wirecat/cli-base";
|
|
2
2
|
const invalid = () => new CliError("authentication_error", "the saved sign-in cannot be read; sign in again", { reason: "oauth.invalid" });
|
|
3
3
|
const parse = (raw) => {
|
|
4
4
|
let value;
|
|
@@ -149,6 +149,10 @@ export const credentialStorage = (options) => {
|
|
|
149
149
|
throw new CliError("configuration_error", "a sign-in store needs its keyring passed in, never a default");
|
|
150
150
|
const credentials = new Credentials(options.credentials);
|
|
151
151
|
const { account, codec } = options;
|
|
152
|
+
// cli-base also counts a moved HOME as moved, but only with the system keyring; the sandboxed tests move HOME.
|
|
153
|
+
const moved = options.credentials.isolated === true ||
|
|
154
|
+
(options.credentials.keyring === systemKeyring &&
|
|
155
|
+
isMovedRun({ appName: options.credentials.service, env: options.credentials.env ?? process.env }));
|
|
152
156
|
const service = keyringService(options.credentials.service, options.credentials.configDir, options.credentials.isolated);
|
|
153
157
|
const raw = (name) => {
|
|
154
158
|
try {
|
|
@@ -175,13 +179,15 @@ export const credentialStorage = (options) => {
|
|
|
175
179
|
if (found === undefined)
|
|
176
180
|
continue;
|
|
177
181
|
codec.decode(found);
|
|
178
|
-
|
|
182
|
+
if (!moved)
|
|
183
|
+
save(found);
|
|
179
184
|
return found;
|
|
180
185
|
}
|
|
181
186
|
return undefined;
|
|
182
187
|
};
|
|
183
188
|
return {
|
|
184
189
|
key: `${service}\u0000${account}`,
|
|
190
|
+
moved,
|
|
185
191
|
probe: () => {
|
|
186
192
|
raw(account);
|
|
187
193
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.494.0",
|
|
4
4
|
"description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|
|
@@ -247,7 +247,7 @@
|
|
|
247
247
|
"@types/node": "^22.20.5",
|
|
248
248
|
"@types/pngjs": "6.0.5",
|
|
249
249
|
"@vitest/coverage-v8": "^5.0.5",
|
|
250
|
-
"@wirecat/cli-base": "0.65.
|
|
250
|
+
"@wirecat/cli-base": "0.65.5",
|
|
251
251
|
"@wirecat/cli-core-legacy-tags": "npm:@wirecat/cli-core@0.414.0",
|
|
252
252
|
"commander": "15.0.0",
|
|
253
253
|
"cspell": "10.4.0",
|