opencode-qwen-cli-auth 2.3.6 → 2.3.8
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/README.md +10 -1
- package/README.vi.md +10 -1
- package/dist/index.js +28 -8
- package/dist/lib/auth/auth.js +165 -80
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,6 +10,8 @@ OAuth plugin for [OpenCode](https://opencode.ai) to use Qwen for free via Qwen A
|
|
|
10
10
|
- **Multi-account support** - add multiple Qwen accounts and keep one active account
|
|
11
11
|
- **DashScope compatibility** - automatically injects required headers for the OAuth flow
|
|
12
12
|
- **Smart output token limit** - auto-caps tokens based on model (65K for coder-model, 8K for vision-model)
|
|
13
|
+
- **Reasoning capability in UI (coder-model)** - model tooltip shows reasoning support in OpenCode
|
|
14
|
+
- **Reasoning-effort safety** - strips reasoning control fields from outbound payload for OAuth compatibility
|
|
13
15
|
- **Retry & Fallback** - handles quota/rate limit errors with payload degradation mechanism
|
|
14
16
|
- **Logging & Debugging** - detailed debugging support via environment variables
|
|
15
17
|
|
|
@@ -55,6 +57,12 @@ The plugin stores each successful login in the multi-account store and can auto-
|
|
|
55
57
|
| Qwen Coder (Qwen 3.5 Plus) | `coder-model` | text | text | 1M tokens | 65,536 tokens | Free |
|
|
56
58
|
| Qwen VL Plus (Vision) | `vision-model` | text, image | text | 128K tokens | 8,192 tokens | Free |
|
|
57
59
|
|
|
60
|
+
### Reasoning Note
|
|
61
|
+
|
|
62
|
+
- `coder-model` is marked as reasoning-capable in OpenCode UI.
|
|
63
|
+
- This release is UI-only for reasoning and does not enable runtime reasoning-effort controls for Qwen OAuth.
|
|
64
|
+
- If clients send `reasoning`, `reasoningEffort`, or `reasoning_effort`, the plugin removes these fields before forwarding requests.
|
|
65
|
+
|
|
58
66
|
## Configuration
|
|
59
67
|
|
|
60
68
|
### Environment Variables
|
|
@@ -131,7 +139,8 @@ When hitting a `429 insufficient_quota` error, the plugin automatically:
|
|
|
131
139
|
|
|
132
140
|
- Automatically uses refresh token
|
|
133
141
|
- Retries up to 2 times for transient errors (timeout, network)
|
|
134
|
-
-
|
|
142
|
+
- On refresh `401/403`, marks current account as `auth_invalid` and switches to next healthy account when available
|
|
143
|
+
- If no healthy account is available, requests re-authentication (`opencode auth login`)
|
|
135
144
|
|
|
136
145
|
## Authentication Management
|
|
137
146
|
|
package/README.vi.md
CHANGED
|
@@ -10,6 +10,8 @@ Plugin OAuth cho [OpenCode](https://opencode.ai) để sử dụng Qwen miễn p
|
|
|
10
10
|
- **Hỗ trợ đa tài khoản** - thêm nhiều Qwen account và duy trì một tài khoản active
|
|
11
11
|
- **Tương thích DashScope** - tự động inject headers cần thiết cho OAuth flow
|
|
12
12
|
- **Giới hạn output token thông minh** - tự động cap theo model (65K cho coder-model, 8K cho vision-model)
|
|
13
|
+
- **Hiển thị reasoning trên UI (coder-model)** - tooltip model trong OpenCode sẽ hiện hỗ trợ reasoning
|
|
14
|
+
- **An toàn reasoning-effort** - loại bỏ các trường điều khiển reasoning khỏi payload để giữ tương thích OAuth
|
|
13
15
|
- **Retry & Fallback** - xử lý lỗi quota/rate limit với cơ chế degrade (giảm tải payload)
|
|
14
16
|
- **Logging & Debugging** - hỗ trợ debug chi tiết qua biến môi trường
|
|
15
17
|
|
|
@@ -55,6 +57,12 @@ Plugin sẽ lưu từng lần đăng nhập thành công vào kho đa tài kho
|
|
|
55
57
|
| Qwen Coder (Qwen 3.5 Plus) | `coder-model` | text | text | 1M tokens | 65,536 tokens | Miễn phí |
|
|
56
58
|
| Qwen VL Plus (Vision) | `vision-model` | text, image | text | 128K tokens | 8,192 tokens | Miễn phí |
|
|
57
59
|
|
|
60
|
+
### Ghi chú reasoning
|
|
61
|
+
|
|
62
|
+
- `coder-model` được đánh dấu có reasoning trong UI của OpenCode.
|
|
63
|
+
- Bản phát hành này chỉ hỗ trợ reasoning ở mức hiển thị UI, chưa bật điều khiển reasoning-effort ở runtime cho Qwen OAuth.
|
|
64
|
+
- Nếu client gửi `reasoning`, `reasoningEffort` hoặc `reasoning_effort`, plugin sẽ tự loại bỏ trước khi gửi request đi.
|
|
65
|
+
|
|
58
66
|
## Cấu hình
|
|
59
67
|
|
|
60
68
|
### Biến môi trường
|
|
@@ -131,7 +139,8 @@ Khi gặp lỗi `429 insufficient_quota`, plugin sẽ tự động:
|
|
|
131
139
|
|
|
132
140
|
- Tự động sử dụng refresh token để lấy token mới
|
|
133
141
|
- Thử lại tối đa 2 lần đối với các lỗi tạm thời (timeout, lỗi mạng)
|
|
134
|
-
-
|
|
142
|
+
- Nếu refresh gặp `401/403`, plugin đánh dấu account hiện tại là `auth_invalid` và tự chuyển sang account khỏe tiếp theo nếu có
|
|
143
|
+
- Nếu không còn account khỏe, plugin sẽ yêu cầu đăng nhập lại (`opencode auth login`)
|
|
135
144
|
|
|
136
145
|
## Quản lý xác thực
|
|
137
146
|
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* @license MIT with Usage Disclaimer (see LICENSE file)
|
|
13
13
|
* @repository https://github.com/TVD-00/opencode-qwen-cli-auth
|
|
14
|
-
* @version 2.3.
|
|
14
|
+
* @version 2.3.8
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { randomUUID } from "node:crypto";
|
|
@@ -40,7 +40,7 @@ const CLI_FALLBACK_MAX_BUFFER_CHARS = 1024 * 1024;
|
|
|
40
40
|
/** Enable CLI fallback feature via environment variable */
|
|
41
41
|
const ENABLE_CLI_FALLBACK = process.env.OPENCODE_QWEN_ENABLE_CLI_FALLBACK === "1";
|
|
42
42
|
/** User agent string for plugin identification */
|
|
43
|
-
const PLUGIN_USER_AGENT = "opencode-qwen-cli-auth/2.3.
|
|
43
|
+
const PLUGIN_USER_AGENT = "opencode-qwen-cli-auth/2.3.8";
|
|
44
44
|
/** Output token limits per model for DashScope OAuth */
|
|
45
45
|
const DASH_SCOPE_OUTPUT_LIMITS = {
|
|
46
46
|
"coder-model": 65536,
|
|
@@ -104,6 +104,11 @@ const CLIENT_ONLY_BODY_FIELDS = new Set([
|
|
|
104
104
|
"options",
|
|
105
105
|
"debug",
|
|
106
106
|
]);
|
|
107
|
+
const REASONING_CONTROL_FIELDS = new Set([
|
|
108
|
+
"reasoning",
|
|
109
|
+
"reasoningEffort",
|
|
110
|
+
"reasoning_effort",
|
|
111
|
+
]);
|
|
107
112
|
function resolveQwenCliCommand() {
|
|
108
113
|
const fromEnv = process.env.QWEN_CLI_PATH;
|
|
109
114
|
if (typeof fromEnv === "string" && fromEnv.trim().length > 0) {
|
|
@@ -394,6 +399,12 @@ function sanitizeOutgoingPayload(payload) {
|
|
|
394
399
|
delete sanitized.stream_options;
|
|
395
400
|
changed = true;
|
|
396
401
|
}
|
|
402
|
+
for (const field of REASONING_CONTROL_FIELDS) {
|
|
403
|
+
if (field in sanitized) {
|
|
404
|
+
delete sanitized[field];
|
|
405
|
+
changed = true;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
397
408
|
// Cap max_tokens fields
|
|
398
409
|
if (typeof sanitized.max_tokens === "number" && sanitized.max_tokens > CHAT_MAX_TOKENS_CAP) {
|
|
399
410
|
sanitized.max_tokens = CHAT_MAX_TOKENS_CAP;
|
|
@@ -1184,7 +1195,10 @@ async function getValidAccessToken(getAuth) {
|
|
|
1184
1195
|
accessToken = refreshResult.access;
|
|
1185
1196
|
resourceUrl = refreshResult.resourceUrl;
|
|
1186
1197
|
saveToken(refreshResult);
|
|
1187
|
-
await upsertOAuthAccount(refreshResult, {
|
|
1198
|
+
await upsertOAuthAccount(refreshResult, {
|
|
1199
|
+
setActive: false,
|
|
1200
|
+
accountId: activeOAuthAccount?.accountId,
|
|
1201
|
+
});
|
|
1188
1202
|
}
|
|
1189
1203
|
else {
|
|
1190
1204
|
if (LOGGING_ENABLED) {
|
|
@@ -1210,7 +1224,10 @@ async function getValidAccessToken(getAuth) {
|
|
|
1210
1224
|
resourceUrl,
|
|
1211
1225
|
};
|
|
1212
1226
|
saveToken(sdkToken);
|
|
1213
|
-
await upsertOAuthAccount(sdkToken, {
|
|
1227
|
+
await upsertOAuthAccount(sdkToken, {
|
|
1228
|
+
setActive: false,
|
|
1229
|
+
accountId: activeOAuthAccount?.accountId,
|
|
1230
|
+
});
|
|
1214
1231
|
}
|
|
1215
1232
|
catch (e) {
|
|
1216
1233
|
logWarn("Failed to bootstrap .qwen token from SDK auth state:", e);
|
|
@@ -1400,14 +1417,17 @@ export const QwenAuthPlugin = async (_input) => {
|
|
|
1400
1417
|
models: {
|
|
1401
1418
|
"coder-model": {
|
|
1402
1419
|
id: "coder-model",
|
|
1403
|
-
name: "Qwen 3.5 Plus
|
|
1404
|
-
// Qwen does not support reasoning_effort from OpenCode UI
|
|
1405
|
-
// Thinking is always enabled by default on server side (qwen3.5-plus)
|
|
1420
|
+
name: "Qwen 3.5 Plus",
|
|
1406
1421
|
attachment: false,
|
|
1407
|
-
reasoning:
|
|
1422
|
+
reasoning: true,
|
|
1408
1423
|
limit: { context: 1048576, output: CHAT_MAX_TOKENS_CAP },
|
|
1409
1424
|
cost: { input: 0, output: 0 },
|
|
1410
1425
|
modalities: { input: ["text"], output: ["text"] },
|
|
1426
|
+
variants: {
|
|
1427
|
+
low: { disabled: true },
|
|
1428
|
+
medium: { disabled: true },
|
|
1429
|
+
high: { disabled: true },
|
|
1430
|
+
},
|
|
1411
1431
|
},
|
|
1412
1432
|
"vision-model": {
|
|
1413
1433
|
id: "vision-model",
|
package/dist/lib/auth/auth.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { generatePKCE } from "@openauthjs/openauth/pkce";
|
|
9
9
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync, renameSync, statSync } from "fs";
|
|
10
|
+
import { dirname } from "path";
|
|
10
11
|
import { QWEN_OAUTH, DEFAULT_QWEN_BASE_URL, TOKEN_REFRESH_BUFFER_MS, VERIFICATION_URI } from "../constants.js";
|
|
11
12
|
import { getTokenPath, getQwenDir, getTokenLockPath, getLegacyTokenPath, getAccountsPath, getAccountsLockPath } from "../config.js";
|
|
12
13
|
import { logError, logWarn, logInfo, LOGGING_ENABLED } from "../logger.js";
|
|
@@ -307,11 +308,15 @@ function buildAccountEntry(tokenData, accountId, accountKey) {
|
|
|
307
308
|
}
|
|
308
309
|
|
|
309
310
|
function writeAccountsStoreData(store) {
|
|
311
|
+
const accountsPath = getAccountsPath();
|
|
312
|
+
const accountsDir = dirname(accountsPath);
|
|
310
313
|
const qwenDir = getQwenDir();
|
|
311
314
|
if (!existsSync(qwenDir)) {
|
|
312
315
|
mkdirSync(qwenDir, { recursive: true, mode: 0o700 });
|
|
313
316
|
}
|
|
314
|
-
|
|
317
|
+
if (!existsSync(accountsDir)) {
|
|
318
|
+
mkdirSync(accountsDir, { recursive: true, mode: 0o700 });
|
|
319
|
+
}
|
|
315
320
|
const tempPath = `${accountsPath}.tmp.${Date.now().toString(36)}-${Math.random().toString(16).slice(2)}`;
|
|
316
321
|
const payload = {
|
|
317
322
|
version: ACCOUNT_STORE_VERSION,
|
|
@@ -583,9 +588,9 @@ function releaseTokenLock(lockPath) {
|
|
|
583
588
|
|
|
584
589
|
async function acquireAccountsLock() {
|
|
585
590
|
const lockPath = getAccountsLockPath();
|
|
586
|
-
const
|
|
587
|
-
if (!existsSync(
|
|
588
|
-
mkdirSync(
|
|
591
|
+
const lockDir = dirname(lockPath);
|
|
592
|
+
if (!existsSync(lockDir)) {
|
|
593
|
+
mkdirSync(lockDir, { recursive: true, mode: 0o700 });
|
|
589
594
|
}
|
|
590
595
|
const lockValue = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
591
596
|
let waitMs = LOCK_ATTEMPT_INTERVAL_MS;
|
|
@@ -1096,89 +1101,140 @@ export async function upsertOAuthAccount(tokenResult, options = {}) {
|
|
|
1096
1101
|
|
|
1097
1102
|
export async function getActiveOAuthAccount(options = {}) {
|
|
1098
1103
|
migrateLegacyTokenToAccountsIfNeeded();
|
|
1099
|
-
const
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
const
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
const
|
|
1110
|
-
if (
|
|
1111
|
-
|
|
1104
|
+
const preferredAccountId = typeof options.preferredAccountId === "string" && options.preferredAccountId.length > 0
|
|
1105
|
+
? options.preferredAccountId
|
|
1106
|
+
: null;
|
|
1107
|
+
const attemptedAuthRejected = new Set();
|
|
1108
|
+
for (;;) {
|
|
1109
|
+
const lockPath = await acquireAccountsLock();
|
|
1110
|
+
let selected = null;
|
|
1111
|
+
let dirty = false;
|
|
1112
|
+
try {
|
|
1113
|
+
const store = loadAccountsStoreData();
|
|
1114
|
+
const now = Date.now();
|
|
1115
|
+
if (store.accounts.length === 0) {
|
|
1116
|
+
return null;
|
|
1117
|
+
}
|
|
1118
|
+
if (attemptedAuthRejected.size === 0 && preferredAccountId) {
|
|
1119
|
+
const exists = store.accounts.some(account => account.id === preferredAccountId);
|
|
1120
|
+
if (exists && store.activeAccountId !== preferredAccountId) {
|
|
1121
|
+
store.activeAccountId = preferredAccountId;
|
|
1122
|
+
dirty = true;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
let active = store.accounts.find(account => account.id === store.activeAccountId);
|
|
1126
|
+
if (!active) {
|
|
1127
|
+
active = store.accounts[0];
|
|
1128
|
+
store.activeAccountId = active.id;
|
|
1112
1129
|
dirty = true;
|
|
1113
1130
|
}
|
|
1131
|
+
const activeHealthy = !(typeof active.exhaustedUntil === "number" && active.exhaustedUntil > now);
|
|
1132
|
+
if (!activeHealthy && !options.allowExhausted) {
|
|
1133
|
+
const replacement = pickNextHealthyAccount(store, new Set(), now);
|
|
1134
|
+
if (!replacement) {
|
|
1135
|
+
return null;
|
|
1136
|
+
}
|
|
1137
|
+
if (store.activeAccountId !== replacement.id) {
|
|
1138
|
+
store.activeAccountId = replacement.id;
|
|
1139
|
+
dirty = true;
|
|
1140
|
+
}
|
|
1141
|
+
active = replacement;
|
|
1142
|
+
}
|
|
1143
|
+
const healthyCount = countHealthyAccounts(store, now);
|
|
1144
|
+
selected = {
|
|
1145
|
+
account: { ...active },
|
|
1146
|
+
healthyCount,
|
|
1147
|
+
totalCount: store.accounts.length,
|
|
1148
|
+
};
|
|
1149
|
+
if (dirty) {
|
|
1150
|
+
writeAccountsStoreData(store);
|
|
1151
|
+
}
|
|
1114
1152
|
}
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
active = store.accounts[0];
|
|
1118
|
-
store.activeAccountId = active.id;
|
|
1119
|
-
dirty = true;
|
|
1153
|
+
finally {
|
|
1154
|
+
releaseAccountsLock(lockPath);
|
|
1120
1155
|
}
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1156
|
+
if (!selected) {
|
|
1157
|
+
return null;
|
|
1158
|
+
}
|
|
1159
|
+
if (options.requireHealthy && selected.account.exhaustedUntil > Date.now()) {
|
|
1160
|
+
return null;
|
|
1161
|
+
}
|
|
1162
|
+
try {
|
|
1163
|
+
syncAccountToLegacyTokenFile(selected.account);
|
|
1164
|
+
}
|
|
1165
|
+
catch (error) {
|
|
1166
|
+
logWarn("Failed to sync active account token to oauth_creds.json", error);
|
|
1167
|
+
return null;
|
|
1168
|
+
}
|
|
1169
|
+
const valid = await getValidTokenDetailed({ clearOnFailure: false });
|
|
1170
|
+
if (valid.type === "success") {
|
|
1171
|
+
const latest = loadStoredToken();
|
|
1172
|
+
if (latest) {
|
|
1173
|
+
try {
|
|
1174
|
+
await withAccountsStoreLock((store) => {
|
|
1175
|
+
const target = store.accounts.find(account => account.id === selected.account.id);
|
|
1176
|
+
if (!target) {
|
|
1177
|
+
return store;
|
|
1178
|
+
}
|
|
1179
|
+
target.token = latest;
|
|
1180
|
+
target.resource_url = latest.resource_url;
|
|
1181
|
+
target.updatedAt = Date.now();
|
|
1182
|
+
return store;
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
catch (error) {
|
|
1186
|
+
logWarn("Failed to update account token from refreshed legacy token", error);
|
|
1187
|
+
}
|
|
1130
1188
|
}
|
|
1131
|
-
|
|
1189
|
+
return buildRuntimeAccountResponse(selected.account, selected.healthyCount, selected.totalCount, valid.accessToken, valid.resourceUrl);
|
|
1132
1190
|
}
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
account: { ...active },
|
|
1136
|
-
healthyCount,
|
|
1137
|
-
totalCount: store.accounts.length,
|
|
1138
|
-
};
|
|
1139
|
-
if (dirty) {
|
|
1140
|
-
writeAccountsStoreData(store);
|
|
1191
|
+
if (valid.type !== "auth_rejected") {
|
|
1192
|
+
return null;
|
|
1141
1193
|
}
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
syncAccountToLegacyTokenFile(selected.account);
|
|
1154
|
-
}
|
|
1155
|
-
catch (error) {
|
|
1156
|
-
logWarn("Failed to sync active account token to oauth_creds.json", error);
|
|
1157
|
-
return null;
|
|
1158
|
-
}
|
|
1159
|
-
const valid = await getValidToken();
|
|
1160
|
-
if (!valid) {
|
|
1161
|
-
return null;
|
|
1162
|
-
}
|
|
1163
|
-
const latest = loadStoredToken();
|
|
1164
|
-
if (latest) {
|
|
1194
|
+
attemptedAuthRejected.add(selected.account.id);
|
|
1195
|
+
if (attemptedAuthRejected.size >= selected.totalCount) {
|
|
1196
|
+
if (LOGGING_ENABLED) {
|
|
1197
|
+
logWarn("All OAuth accounts rejected with auth_invalid, re-authentication required", {
|
|
1198
|
+
attempted: attemptedAuthRejected.size,
|
|
1199
|
+
total: selected.totalCount,
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
return null;
|
|
1203
|
+
}
|
|
1204
|
+
let switchedToHealthy = false;
|
|
1165
1205
|
try {
|
|
1166
1206
|
await withAccountsStoreLock((store) => {
|
|
1207
|
+
const now = Date.now();
|
|
1167
1208
|
const target = store.accounts.find(account => account.id === selected.account.id);
|
|
1168
1209
|
if (!target) {
|
|
1169
1210
|
return store;
|
|
1170
1211
|
}
|
|
1171
|
-
target.
|
|
1172
|
-
target.
|
|
1173
|
-
target.updatedAt =
|
|
1212
|
+
target.exhaustedUntil = now + getQuotaCooldownMs();
|
|
1213
|
+
target.lastErrorCode = "auth_invalid";
|
|
1214
|
+
target.updatedAt = now;
|
|
1215
|
+
const next = pickNextHealthyAccount(store, attemptedAuthRejected, now);
|
|
1216
|
+
if (next) {
|
|
1217
|
+
store.activeAccountId = next.id;
|
|
1218
|
+
switchedToHealthy = true;
|
|
1219
|
+
}
|
|
1174
1220
|
return store;
|
|
1175
1221
|
});
|
|
1176
1222
|
}
|
|
1177
1223
|
catch (error) {
|
|
1178
|
-
logWarn("Failed to
|
|
1224
|
+
logWarn("Failed to switch OAuth account after auth_invalid", error);
|
|
1225
|
+
return null;
|
|
1226
|
+
}
|
|
1227
|
+
if (!switchedToHealthy) {
|
|
1228
|
+
if (LOGGING_ENABLED) {
|
|
1229
|
+
logWarn("No healthy OAuth account available after auth_invalid", {
|
|
1230
|
+
accountID: selected.account.id,
|
|
1231
|
+
attempted: attemptedAuthRejected.size,
|
|
1232
|
+
total: selected.totalCount,
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
return null;
|
|
1179
1236
|
}
|
|
1180
1237
|
}
|
|
1181
|
-
return buildRuntimeAccountResponse(selected.account, selected.healthyCount, selected.totalCount, valid.accessToken, valid.resourceUrl);
|
|
1182
1238
|
}
|
|
1183
1239
|
|
|
1184
1240
|
export async function markOAuthAccountQuotaExhausted(accountId, errorCode = "insufficient_quota") {
|
|
@@ -1248,18 +1304,15 @@ export function isTokenExpired(expiresAt) {
|
|
|
1248
1304
|
return Date.now() >= expiresAt - TOKEN_REFRESH_BUFFER_MS;
|
|
1249
1305
|
}
|
|
1250
1306
|
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
* @returns {Promise<{ accessToken: string, resourceUrl?: string }|null>} Valid token or null if unavailable
|
|
1254
|
-
*/
|
|
1255
|
-
export async function getValidToken() {
|
|
1307
|
+
async function getValidTokenDetailed(options = {}) {
|
|
1308
|
+
const clearOnFailure = options.clearOnFailure === true;
|
|
1256
1309
|
const stored = loadStoredToken();
|
|
1257
1310
|
if (!stored) {
|
|
1258
|
-
return
|
|
1311
|
+
return { type: "missing" };
|
|
1259
1312
|
}
|
|
1260
|
-
// Return cached token if still valid
|
|
1261
1313
|
if (!isTokenExpired(stored.expiry_date)) {
|
|
1262
1314
|
return {
|
|
1315
|
+
type: "success",
|
|
1263
1316
|
accessToken: stored.access_token,
|
|
1264
1317
|
resourceUrl: stored.resource_url,
|
|
1265
1318
|
};
|
|
@@ -1267,16 +1320,48 @@ export async function getValidToken() {
|
|
|
1267
1320
|
if (LOGGING_ENABLED) {
|
|
1268
1321
|
logInfo("Token expired, refreshing...");
|
|
1269
1322
|
}
|
|
1270
|
-
// Token expired, try to refresh
|
|
1271
1323
|
const refreshResult = await refreshAccessToken(stored.refresh_token);
|
|
1272
|
-
if (refreshResult.type
|
|
1273
|
-
|
|
1324
|
+
if (refreshResult.type === "success") {
|
|
1325
|
+
return {
|
|
1326
|
+
type: "success",
|
|
1327
|
+
accessToken: refreshResult.access,
|
|
1328
|
+
resourceUrl: refreshResult.resourceUrl,
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
const status = typeof refreshResult.status === "number" ? refreshResult.status : undefined;
|
|
1332
|
+
const isAuthRejected = status === 401 ||
|
|
1333
|
+
status === 403 ||
|
|
1334
|
+
refreshResult.error === "refresh_token_rejected";
|
|
1335
|
+
if (clearOnFailure) {
|
|
1274
1336
|
clearStoredToken();
|
|
1337
|
+
}
|
|
1338
|
+
if (isAuthRejected) {
|
|
1339
|
+
return {
|
|
1340
|
+
type: "auth_rejected",
|
|
1341
|
+
status,
|
|
1342
|
+
error: refreshResult.error || "refresh_token_rejected",
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
return {
|
|
1346
|
+
type: "transient_or_unknown",
|
|
1347
|
+
status,
|
|
1348
|
+
error: refreshResult.error || "refresh_failed",
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
/**
|
|
1353
|
+
* Gets valid access token, refreshing if expired
|
|
1354
|
+
* @returns {Promise<{ accessToken: string, resourceUrl?: string }|null>} Valid token or null if unavailable
|
|
1355
|
+
*/
|
|
1356
|
+
export async function getValidToken() {
|
|
1357
|
+
const result = await getValidTokenDetailed({ clearOnFailure: true });
|
|
1358
|
+
if (result.type !== "success") {
|
|
1359
|
+
logError("Token refresh failed, re-authentication required");
|
|
1275
1360
|
return null;
|
|
1276
1361
|
}
|
|
1277
1362
|
return {
|
|
1278
|
-
accessToken:
|
|
1279
|
-
resourceUrl:
|
|
1363
|
+
accessToken: result.accessToken,
|
|
1364
|
+
resourceUrl: result.resourceUrl,
|
|
1280
1365
|
};
|
|
1281
1366
|
}
|
|
1282
1367
|
|
package/package.json
CHANGED