@wirecat/cli-core 0.536.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 CHANGED
@@ -7,6 +7,14 @@ 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
+
10
18
  ## 0.536.0 — 01.11.2026
11
19
 
12
20
  ### Fixed
@@ -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";
@@ -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";
@@ -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
  }>;
@@ -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
- const tokens = async () => {
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 (fresh(seen.tokens))
95
- return seen.tokens;
96
- const kept = remembered(seen.tokens);
97
- if (kept)
98
- return kept;
99
- return shared(async () => {
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 (fresh(current.tokens) || current.tokens.refreshToken !== seen.tokens.refreshToken)
105
- return current.tokens;
106
- const keptMeanwhile = remembered(current.tokens);
107
- if (keptMeanwhile)
108
- return keptMeanwhile;
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.536.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",