@wirecat/cli-core 0.535.0 → 0.537.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 +17 -0
- package/dist/mail/server-search.js +11 -7
- package/dist/oauth/index.d.ts +1 -1
- package/dist/oauth/index.js +1 -1
- package/dist/oauth/keeper.d.ts +9 -2
- package/dist/oauth/keeper.js +29 -15
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.537.0 — 01.11.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`tokenKeeper.tokens({ refresh: true })`** (and `accessToken({ refresh: true })`): after a server refuses an
|
|
15
|
+
access token whose `expiresAt` still called it fresh (a revoked session), refresh under the shared lock anyway. A
|
|
16
|
+
different fresh token another process saved meanwhile is answered instead of a second refresh.
|
|
17
|
+
|
|
18
|
+
## 0.536.0 — 01.11.2026
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **A server mail search that ran out of time says so.** When the server's own deadline cut a search or the read of
|
|
23
|
+
its finds short, `searchMailServer` reported `search.server-failed` (`provider_unavailable`), as if the server had
|
|
24
|
+
refused. It now reports `search.server-timeout` (`timeout`), as when `--server-time` ends it, so a caller knows to
|
|
25
|
+
give it more time.
|
|
26
|
+
|
|
10
27
|
## 0.535.0 — 01.11.2026
|
|
11
28
|
|
|
12
29
|
### Added
|
|
@@ -18,6 +18,12 @@ const refusal = {
|
|
|
18
18
|
no_words: "--backend server needs words, people, a subject, a date or a state to send to the server",
|
|
19
19
|
};
|
|
20
20
|
const BOUND = Symbol("bound");
|
|
21
|
+
/** A call the server's own deadline cut short is the time bound's, as one cut by --server-time is. */
|
|
22
|
+
const failureOf = (error) => error instanceof CliError && error.code === "rate_limited"
|
|
23
|
+
? "rate_limited"
|
|
24
|
+
: error instanceof CliError && error.code === "timeout"
|
|
25
|
+
? "time_or_abort_bound"
|
|
26
|
+
: "search_failed";
|
|
21
27
|
/** The newest first: a server's UIDs grow as mail arrives in the folder. */
|
|
22
28
|
const newest = (uids, count) => [...uids].sort((one, other) => Number(other) - Number(one)).slice(0, count);
|
|
23
29
|
/**
|
|
@@ -112,7 +118,7 @@ export const searchMailServer = async (store, mailbox, access, request) => {
|
|
|
112
118
|
if (!open)
|
|
113
119
|
return;
|
|
114
120
|
pending.delete(call);
|
|
115
|
-
fail(call, error
|
|
121
|
+
fail(call, failureOf(error));
|
|
116
122
|
}
|
|
117
123
|
}));
|
|
118
124
|
if (!open)
|
|
@@ -143,10 +149,7 @@ export const searchMailServer = async (store, mailbox, access, request) => {
|
|
|
143
149
|
});
|
|
144
150
|
let outcome;
|
|
145
151
|
try {
|
|
146
|
-
outcome = await Promise.race([
|
|
147
|
-
run().then(() => "done", () => "failed"),
|
|
148
|
-
bound,
|
|
149
|
-
]);
|
|
152
|
+
outcome = await Promise.race([run().then(() => "done", failureOf), bound]);
|
|
150
153
|
}
|
|
151
154
|
finally {
|
|
152
155
|
open = false;
|
|
@@ -158,12 +161,13 @@ export const searchMailServer = async (store, mailbox, access, request) => {
|
|
|
158
161
|
throw openFailure;
|
|
159
162
|
if (unsupported)
|
|
160
163
|
return skip("unsupported");
|
|
164
|
+
const ended = outcome === BOUND || outcome === "done" ? "time_or_abort_bound" : outcome;
|
|
161
165
|
for (const call of pending)
|
|
162
|
-
fail(call,
|
|
166
|
+
fail(call, ended);
|
|
163
167
|
if (outcome !== "done" || (fresh.length > 0 && raws === undefined)) {
|
|
164
168
|
// What was found but not read in time is the archive's to answer: none of it is claimed as the server's.
|
|
165
169
|
if (pending.size === 0)
|
|
166
|
-
fail(null,
|
|
170
|
+
fail(null, ended);
|
|
167
171
|
for (const item of fresh)
|
|
168
172
|
sources.delete(sourceKey(item.threadId, item.messageId));
|
|
169
173
|
report.returned = sources.size;
|
package/dist/oauth/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { type Authorization, type AuthorizeOptions, authorize } from "./authorize.js";
|
|
2
|
-
export { type SignInOptions, type TokenKeeper, type TokenKeeperOptions, tokenKeeper } from "./keeper.js";
|
|
2
|
+
export { type SignInOptions, type TokenKeeper, type TokenKeeperOptions, type TokensOptions, tokenKeeper, } from "./keeper.js";
|
|
3
3
|
export type { ExchangeContext, OAuthClient, OAuthProvider, RequestContext, TokenSet } from "./provider.js";
|
|
4
4
|
export { requestTokens, revokeToken, TOKEN_LOG_ALLOW, TOKEN_LOG_EVENT } from "./requests.js";
|
|
5
5
|
export { type CredentialStorageOptions, credentialStorage, driveSessionCodec, type EncodeInput, oauthAccount, type StoredTokens, standardCodec, type TokenCodec, type TokenStorage, zoomTokenCodec, } from "./storage.js";
|
package/dist/oauth/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { authorize } from "./authorize.js";
|
|
2
|
-
export { tokenKeeper } from "./keeper.js";
|
|
2
|
+
export { tokenKeeper, } from "./keeper.js";
|
|
3
3
|
export { requestTokens, revokeToken, TOKEN_LOG_ALLOW, TOKEN_LOG_EVENT } from "./requests.js";
|
|
4
4
|
export { credentialStorage, driveSessionCodec, oauthAccount, standardCodec, zoomTokenCodec, } from "./storage.js";
|
package/dist/oauth/keeper.d.ts
CHANGED
|
@@ -23,9 +23,16 @@ export type SignInOptions = Omit<AuthorizeOptions, "provider" | "signal"> & {
|
|
|
23
23
|
/** Runs before anything is saved; it may refuse the account or add fields to the entry (drive's `principal`). */
|
|
24
24
|
accept?: (tokens: TokenSet) => Promise<Record<string, unknown> | undefined> | Record<string, unknown> | undefined;
|
|
25
25
|
};
|
|
26
|
+
export interface TokensOptions {
|
|
27
|
+
/**
|
|
28
|
+
* The server refused the access token this keeper last answered, though `expiresAt` called it fresh (a revoked
|
|
29
|
+
* session): refresh under the lock anyway, unless another process already saved a different fresh one.
|
|
30
|
+
*/
|
|
31
|
+
refresh?: boolean;
|
|
32
|
+
}
|
|
26
33
|
export interface TokenKeeper {
|
|
27
|
-
accessToken(): Promise<string>;
|
|
28
|
-
tokens(): Promise<TokenSet>;
|
|
34
|
+
accessToken(options?: TokensOptions): Promise<string>;
|
|
35
|
+
tokens(options?: TokensOptions): Promise<TokenSet>;
|
|
29
36
|
signIn(options: SignInOptions): Promise<{
|
|
30
37
|
scopes: string[];
|
|
31
38
|
}>;
|
package/dist/oauth/keeper.js
CHANGED
|
@@ -87,25 +87,39 @@ export const tokenKeeper = (options) => {
|
|
|
87
87
|
return kept && fresh(kept) && kept.refreshToken === saved.refreshToken ? kept : undefined;
|
|
88
88
|
};
|
|
89
89
|
const forget = () => refreshedInMemory.delete(lockPath);
|
|
90
|
-
|
|
90
|
+
let answered;
|
|
91
|
+
const answer = (given) => {
|
|
92
|
+
answered = given;
|
|
93
|
+
return given;
|
|
94
|
+
};
|
|
95
|
+
const tokens = async ({ refresh = false } = {}) => {
|
|
91
96
|
const seen = storage.read();
|
|
92
97
|
if (!seen)
|
|
93
98
|
throw notSignedIn();
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
if (!refresh) {
|
|
100
|
+
const kept = remembered(seen.tokens);
|
|
101
|
+
if (kept)
|
|
102
|
+
return answer(kept);
|
|
103
|
+
if (fresh(seen.tokens))
|
|
104
|
+
return answer(seen.tokens);
|
|
105
|
+
}
|
|
106
|
+
const refused = answered ?? remembered(seen.tokens) ?? seen.tokens;
|
|
107
|
+
return answer(await shared(async () => {
|
|
100
108
|
const current = storage.read();
|
|
101
109
|
if (!current)
|
|
102
110
|
throw notSignedIn();
|
|
111
|
+
const latest = remembered(current.tokens) ?? current.tokens;
|
|
103
112
|
// Another process refreshed while this one waited for the lock: its result is the one to use.
|
|
104
|
-
if (
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
113
|
+
if (refresh) {
|
|
114
|
+
if (latest.accessToken !== refused.accessToken && fresh(latest))
|
|
115
|
+
return latest;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
if (latest !== current.tokens)
|
|
119
|
+
return latest;
|
|
120
|
+
if (fresh(current.tokens) || current.tokens.refreshToken !== seen.tokens.refreshToken)
|
|
121
|
+
return current.tokens;
|
|
122
|
+
}
|
|
109
123
|
const client = clientOf(current);
|
|
110
124
|
const refreshed = await exchange({ grant_type: "refresh_token", refresh_token: current.tokens.refreshToken }, context(client, current.tokens));
|
|
111
125
|
// A rotated refresh token is saved even so: kept only in memory, it would die with the process.
|
|
@@ -114,12 +128,12 @@ export const tokenKeeper = (options) => {
|
|
|
114
128
|
else
|
|
115
129
|
storage.write({ tokens: refreshed, client });
|
|
116
130
|
return refreshed;
|
|
117
|
-
});
|
|
131
|
+
}));
|
|
118
132
|
};
|
|
119
133
|
return {
|
|
120
134
|
tokens,
|
|
121
|
-
accessToken: async () => {
|
|
122
|
-
const current = await tokens();
|
|
135
|
+
accessToken: async (tokensOptions) => {
|
|
136
|
+
const current = await tokens(tokensOptions);
|
|
123
137
|
if (!current.accessToken)
|
|
124
138
|
throw notSignedIn();
|
|
125
139
|
return current.accessToken;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.537.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",
|