@timo972/cc-router 0.12.4 → 0.13.0-rc.1
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 +41 -0
- package/README.md +2 -2
- package/dist/cli/account-flows.js +422 -0
- package/dist/cli/cli-errors.js +37 -0
- package/dist/cli/cmd-accounts.js +215 -203
- package/dist/cli/cmd-setup.js +5 -186
- package/dist/cli/cmd-status.js +93 -24
- package/dist/cli/index.js +15 -3
- package/dist/config/manager.js +16 -7
- package/dist/providers/anthropic/claude-cli.js +104 -0
- package/dist/providers/anthropic/scopes.js +5 -0
- package/dist/providers/anthropic/usage-refresher.js +38 -31
- package/dist/providers/openai/device-oauth.js +20 -1
- package/dist/providers/xai/device-oauth.js +9 -0
- package/dist/proxy/account-info-cache.js +69 -20
- package/dist/proxy/account-post-validation.js +23 -0
- package/dist/proxy/account-refresh.js +59 -0
- package/dist/proxy/account-replace.js +12 -12
- package/dist/proxy/server.js +66 -14
- package/dist/proxy/token-refresher.js +36 -1
- package/dist/proxy/types.js +19 -0
- package/dist/telemetry/contracts.js +2 -0
- package/dist/ui/Dashboard.js +72 -6
- package/dist/ui/accountsApi.js +21 -0
- package/dist/utils/browser.js +36 -0
- package/dist/utils/expiry-input.js +21 -0
- package/dist/utils/token-extractor.js +9 -5
- package/package.json +1 -1
package/dist/ui/Dashboard.js
CHANGED
|
@@ -791,6 +791,12 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
|
|
|
791
791
|
? Math.max(0, orderedAccounts.findIndex(a => a.id === selectedAccountId))
|
|
792
792
|
: 0;
|
|
793
793
|
const selectedAccount = orderedAccounts[selectedAccountIndex] ?? null;
|
|
794
|
+
// Identity for the selected row: health never carries it, so it comes from
|
|
795
|
+
// the authenticated /cc-router/accounts poll above. The detail line and the
|
|
796
|
+
// re-auth intent must agree on which email they mean, so both read this.
|
|
797
|
+
const selectedAccountInfo = selectedAccount
|
|
798
|
+
? accountInfo[`${selectedAccount.provider ?? "anthropic_subscription"}:${selectedAccount.id}`]
|
|
799
|
+
: undefined;
|
|
794
800
|
// Same follow-scroll for the accounts window: when the fitting controller
|
|
795
801
|
// shrinks the list below the fleet size, the selected account must stay on
|
|
796
802
|
// screen — account actions (caps, toggle, delete confirmation) target the
|
|
@@ -955,6 +961,42 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
|
|
|
955
961
|
refreshAllInFlightRef.current = false;
|
|
956
962
|
}
|
|
957
963
|
}, [api, modelsApi, modelsStatus, onRefreshAll, refreshCliRouting, showBanner]);
|
|
964
|
+
// ── Reload one account ───────────────────────────────────────────────────
|
|
965
|
+
// R with ACCOUNTS focused narrows the whole-pool reload to the selection, so
|
|
966
|
+
// a single stale account can be refreshed without paying for every provider.
|
|
967
|
+
// Its own in-flight guard: the pool reload and this one are separate calls.
|
|
968
|
+
const refreshOneInFlightRef = useRef(false);
|
|
969
|
+
const doRefreshSelected = useCallback(async () => {
|
|
970
|
+
if (!selectedAccount)
|
|
971
|
+
return;
|
|
972
|
+
if (refreshOneInFlightRef.current) {
|
|
973
|
+
showBanner("Refresh already running…", "gray");
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
refreshOneInFlightRef.current = true;
|
|
977
|
+
showBanner(`Refreshing ${selectedAccount.id}…`, "cyan", REFRESH_ALL_BANNER_MS);
|
|
978
|
+
try {
|
|
979
|
+
// Grok has no server-side account record to refresh — its snapshot is
|
|
980
|
+
// read locally from ~/.grok, so re-read that instead of POSTing.
|
|
981
|
+
if (isXaiAccount(selectedAccount)) {
|
|
982
|
+
await onRefreshAll?.();
|
|
983
|
+
showBanner(`Refreshed ${selectedAccount.id}`, "green");
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
const result = await api.refreshAccount(selectedAccount.id);
|
|
987
|
+
const problems = [
|
|
988
|
+
result.usageRefreshed ? "" : "usage fetch failed",
|
|
989
|
+
result.tokenRefreshed === false ? "token refresh failed" : "",
|
|
990
|
+
].filter(Boolean);
|
|
991
|
+
showBanner(`Refreshed ${result.id} — ${problems.length ? problems.join(", ") : "usage fresh"}`, problems.length ? "yellow" : "green");
|
|
992
|
+
}
|
|
993
|
+
catch (err) {
|
|
994
|
+
showBanner(`Refresh error: ${errMsg(err)}`, "red");
|
|
995
|
+
}
|
|
996
|
+
finally {
|
|
997
|
+
refreshOneInFlightRef.current = false;
|
|
998
|
+
}
|
|
999
|
+
}, [api, onRefreshAll, selectedAccount, showBanner]);
|
|
958
1000
|
const doToggleCli = useCallback((target) => {
|
|
959
1001
|
const current = target === "claude" ? cliRouting.claude.enabled : cliRouting.codex.enabled;
|
|
960
1002
|
const result = target === "claude"
|
|
@@ -1080,7 +1122,7 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
|
|
|
1080
1122
|
// Always call exit() so Ink fully unmounts and releases stdin.
|
|
1081
1123
|
// The outer dashboardLoop reads `pendingIntent` after waitUntilExit().
|
|
1082
1124
|
if (input === "q") {
|
|
1083
|
-
onIntent?.("quit");
|
|
1125
|
+
onIntent?.({ kind: "quit" });
|
|
1084
1126
|
exit();
|
|
1085
1127
|
return;
|
|
1086
1128
|
}
|
|
@@ -1089,7 +1131,7 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
|
|
|
1089
1131
|
setFocus("logs");
|
|
1090
1132
|
return;
|
|
1091
1133
|
}
|
|
1092
|
-
onIntent?.("quit");
|
|
1134
|
+
onIntent?.({ kind: "quit" });
|
|
1093
1135
|
exit();
|
|
1094
1136
|
return;
|
|
1095
1137
|
}
|
|
@@ -1115,7 +1157,10 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
|
|
|
1115
1157
|
return;
|
|
1116
1158
|
}
|
|
1117
1159
|
if (input === "R") {
|
|
1118
|
-
|
|
1160
|
+
if (focus === "accounts" && selectedAccount)
|
|
1161
|
+
void doRefreshSelected();
|
|
1162
|
+
else
|
|
1163
|
+
void doRefreshAll();
|
|
1119
1164
|
return;
|
|
1120
1165
|
}
|
|
1121
1166
|
if (key.tab) {
|
|
@@ -1199,6 +1244,27 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
|
|
|
1199
1244
|
setMode("confirmDelete");
|
|
1200
1245
|
return;
|
|
1201
1246
|
}
|
|
1247
|
+
// l = re-authenticate the selection. Like `n`, the sign-in flow runs in
|
|
1248
|
+
// the outer loop after Ink unmounts, so without a handler this is a no-op.
|
|
1249
|
+
if (input === "l") {
|
|
1250
|
+
if (!selectedAccount)
|
|
1251
|
+
return;
|
|
1252
|
+
if (isXaiAccount(selectedAccount)) {
|
|
1253
|
+
showBanner("Grok credentials live in ~/.grok — run grok login, then cc-router accounts add grok", "yellow");
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
if (!onIntent)
|
|
1257
|
+
return;
|
|
1258
|
+
const provider = selectedAccount.provider === "openai_subscription" ? "openai_subscription" : "anthropic_subscription";
|
|
1259
|
+
onIntent({
|
|
1260
|
+
kind: "reauth",
|
|
1261
|
+
id: selectedAccount.id,
|
|
1262
|
+
provider,
|
|
1263
|
+
...(selectedAccountInfo?.email ? { email: selectedAccountInfo.email } : {}),
|
|
1264
|
+
});
|
|
1265
|
+
exit();
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1202
1268
|
}
|
|
1203
1269
|
if (focus === "models") {
|
|
1204
1270
|
if (key.upArrow) {
|
|
@@ -1251,7 +1317,7 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
|
|
|
1251
1317
|
// flow after Ink unmounts; if none is wired, this key is a no-op.
|
|
1252
1318
|
if (input === "n") {
|
|
1253
1319
|
if (onIntent) {
|
|
1254
|
-
onIntent("addAccount");
|
|
1320
|
+
onIntent({ kind: "addAccount" });
|
|
1255
1321
|
exit();
|
|
1256
1322
|
}
|
|
1257
1323
|
return;
|
|
@@ -1264,10 +1330,10 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
|
|
|
1264
1330
|
: "daemon version unreported (older build)", ` · dashboard v${DASHBOARD_VERSION}`] }), LOCAL_TARGET_RE.test(baseUrl) ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: " \u2014 restart: " }), _jsx(Text, { color: "cyan", children: "cc-router stop --keep-config && cc-router start" })] })) : (
|
|
1265
1331
|
// A remote router can only be restarted where it runs; printing a
|
|
1266
1332
|
// local restart command here would never clear the banner.
|
|
1267
|
-
_jsxs(Text, { color: "gray", children: [" \u2014 update and restart the daemon on ", baseUrl] }))] })), _jsx(Box, { marginTop: 1 }), data.operational && (_jsxs(_Fragment, { children: [_jsx(OperationsPanel, { operational: data.operational, baseUrl: baseUrl, focus: focus, cliRouting: cliRouting }), _jsx(Box, { marginTop: 1 })] })), (focus === "models" || modelsStatus) && (_jsxs(_Fragment, { children: [_jsx(ModelsPanel, { status: modelsStatus, selectedIndex: selectedModelIndex, focused: focus === "models", visibleRows: modelsVisible, rowsRef: modelRowsRef }), _jsx(Box, { marginTop: 1 })] })), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { bold: true, children: [" ACCOUNTS ", _jsxs(Text, { color: healthyCount === orderedAccounts.length ? "green" : "yellow", children: [healthyCount, "/", orderedAccounts.length, " healthy"] }), weeklyFullCount > 0 && _jsx(Text, { color: "red", children: ` · ${weeklyFullCount} 7d full` })] }), shownAccounts < orderedAccounts.length && (_jsxs(Text, { color: "gray", children: [" · showing ", accountWindowTop + 1, "\u2013", accountWindowTop + shownAccounts] })), compact && _jsx(Text, { color: "cyan", children: " · compact" })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", ref: accountRowsRef, children: _jsx(AccountGroups, { visible: orderedAccounts.slice(accountWindowTop, accountWindowTop + shownAccounts), fleet: orderedAccounts, windowTop: accountWindowTop, selectedIndex: selectedAccountIndex, focused: focus === "accounts" }) }), focus === "accounts" && selectedAccount && (_jsxs(Text, { color: "gray", wrap: "truncate-end", children: [" ",
|
|
1333
|
+
_jsxs(Text, { color: "gray", children: [" \u2014 update and restart the daemon on ", baseUrl] }))] })), _jsx(Box, { marginTop: 1 }), data.operational && (_jsxs(_Fragment, { children: [_jsx(OperationsPanel, { operational: data.operational, baseUrl: baseUrl, focus: focus, cliRouting: cliRouting }), _jsx(Box, { marginTop: 1 })] })), (focus === "models" || modelsStatus) && (_jsxs(_Fragment, { children: [_jsx(ModelsPanel, { status: modelsStatus, selectedIndex: selectedModelIndex, focused: focus === "models", visibleRows: modelsVisible, rowsRef: modelRowsRef }), _jsx(Box, { marginTop: 1 })] })), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { bold: true, children: [" ACCOUNTS ", _jsxs(Text, { color: healthyCount === orderedAccounts.length ? "green" : "yellow", children: [healthyCount, "/", orderedAccounts.length, " healthy"] }), weeklyFullCount > 0 && _jsx(Text, { color: "red", children: ` · ${weeklyFullCount} 7d full` })] }), shownAccounts < orderedAccounts.length && (_jsxs(Text, { color: "gray", children: [" · showing ", accountWindowTop + 1, "\u2013", accountWindowTop + shownAccounts] })), compact && _jsx(Text, { color: "cyan", children: " · compact" })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", ref: accountRowsRef, children: _jsx(AccountGroups, { visible: orderedAccounts.slice(accountWindowTop, accountWindowTop + shownAccounts), fleet: orderedAccounts, windowTop: accountWindowTop, selectedIndex: selectedAccountIndex, focused: focus === "accounts" }) }), focus === "accounts" && selectedAccount && (_jsxs(Text, { color: "gray", wrap: "truncate-end", children: [" ", [selectedAccount.tokenOnly ? "token-only" : "", formatAccountInfo(selectedAccountInfo)].filter(Boolean).join(" · ")] }))] }), banner && (_jsx(Box, { marginTop: 1, paddingLeft: 2, children: _jsxs(Text, { color: banner.color, children: [" ", banner.text] }) })), !compact && (_jsxs(_Fragment, { children: [_jsx(Box, { marginTop: 1 }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: " TOTALS " }), _jsx(Text, { children: "requests " }), _jsx(Text, { color: "cyan", children: data.totalRequests }), _jsx(Text, { color: "gray", children: " \u00B7 " }), _jsx(Text, { children: "errors " }), _jsx(Text, { color: data.totalErrors > 0 ? "red" : "green", children: data.totalErrors }), _jsx(Text, { color: "gray", children: " \u00B7 " }), _jsx(Text, { children: "refreshes " }), _jsx(Text, { color: "yellow", children: data.totalRefreshes }), _jsx(CacheHealthBadge, { read: data.totalCacheReadTokens, created: data.totalCacheCreationTokens, input: data.totalInputTokens })] }), _jsx(TokenSummary, { cacheRead: data.totalCacheReadTokens, cacheCreated: data.totalCacheCreationTokens, uncached: data.totalInputTokens, output: data.totalOutputTokens ?? 0 })] }), _jsx(Box, { marginTop: 1 }), _jsx(Text, { bold: true, children: " RECENT ACTIVITY" }), _jsx(Box, { marginTop: 1 })] }))] }), !compact && (_jsx(Box, { flexDirection: "column", ref: logRowsRef, children: visibleLogs.length === 0
|
|
1268
1334
|
? _jsx(Text, { color: "gray", children: " No activity yet" })
|
|
1269
1335
|
: visibleLogs.map((log, i) => (_jsx(LogRow, { log: log, selected: focus === "logs" && logWindowTop + i === selectedLogIndex }, `${log.ts}-${i}`))) })), !compact && focus === "logs" && selectedLog && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginTop: 1 }), _jsx(DetailPanel, { log: selectedLog })] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: focus === "accounts"
|
|
1270
|
-
? " [Tab] [e] toggle [a]/[o]/[g] provider [n] add [d] delete [w] 7d [s] 5h [Ctrl+R] reset [R] reload [z] compact [q]"
|
|
1336
|
+
? " [Tab] [e] toggle [a]/[o]/[g] provider [n] add [l] re-auth [d] delete [w] 7d [s] 5h [Ctrl+R] reset [R] reload [z] compact [q]"
|
|
1271
1337
|
: focus === "models"
|
|
1272
1338
|
? " [Tab] [m/r] refresh [c]/[o] default [R] reload all [Esc] logs [z] compact [q]"
|
|
1273
1339
|
: " [Tab] [m] models [R] reload [z] compact [q] quit" }) })] }) }));
|
package/dist/ui/accountsApi.js
CHANGED
|
@@ -12,6 +12,10 @@ const REQUEST_TIMEOUT_MS = 3_000;
|
|
|
12
12
|
// 10s each. The server single-flights the pass, so a client that does give
|
|
13
13
|
// up and presses again joins the running one rather than stacking another.
|
|
14
14
|
const REFRESH_ALL_TIMEOUT_MS = 120_000;
|
|
15
|
+
// One account only: a token refresh, one usage fetch and the identity fetch.
|
|
16
|
+
// Far short of the whole-pool budget, but long enough that a slow upstream
|
|
17
|
+
// still lands rather than the dashboard reporting a timeout it caused.
|
|
18
|
+
const REFRESH_ONE_TIMEOUT_MS = 30_000;
|
|
15
19
|
const MAX_PUBLIC_ROWS = 12;
|
|
16
20
|
export function createAccountsApi(baseUrl, authToken) {
|
|
17
21
|
const root = baseUrl.replace(/\/+$/, "");
|
|
@@ -58,6 +62,23 @@ export function createAccountsApi(baseUrl, authToken) {
|
|
|
58
62
|
return {
|
|
59
63
|
list,
|
|
60
64
|
refreshAll,
|
|
65
|
+
async refreshAccount(id) {
|
|
66
|
+
const res = await fetch(`${base}/${encodeURIComponent(id)}/refresh`, {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: authHeaders,
|
|
69
|
+
signal: AbortSignal.timeout(REFRESH_ONE_TIMEOUT_MS),
|
|
70
|
+
});
|
|
71
|
+
if (!res.ok)
|
|
72
|
+
throw new Error(`HTTP ${res.status}`);
|
|
73
|
+
const payload = await res.json();
|
|
74
|
+
const r = isRecord(payload.refresh) ? payload.refresh : {};
|
|
75
|
+
return {
|
|
76
|
+
id: publicText(r.id, 128, id),
|
|
77
|
+
tokenRefreshed: r.tokenRefreshed === true ? true : r.tokenRefreshed === false ? false : null,
|
|
78
|
+
usageRefreshed: r.usageRefreshed === true,
|
|
79
|
+
durationMs: publicInteger(r.durationMs),
|
|
80
|
+
};
|
|
81
|
+
},
|
|
61
82
|
async resetUsage(id, redeemRequestId) {
|
|
62
83
|
const response = await fetch(`${base}/${encodeURIComponent(id)}/reset-usage`, {
|
|
63
84
|
method: "POST",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
|
+
export function browserCommandFor(platform, url) {
|
|
3
|
+
if (platform === "darwin")
|
|
4
|
+
return { command: "open", args: [url] };
|
|
5
|
+
if (platform === "win32")
|
|
6
|
+
return { command: "cmd", args: ["/c", "start", "", url] };
|
|
7
|
+
return { command: "xdg-open", args: [url] };
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Best-effort: a headless box, a missing opener or an odd desktop must never
|
|
11
|
+
* fail a sign-in. The caller always prints the URL as well.
|
|
12
|
+
*
|
|
13
|
+
* Resolves as soon as the opener has been launched, never when it exits:
|
|
14
|
+
* on some Linux desktops `xdg-open` blocks until the browser it started
|
|
15
|
+
* closes, and a sign-in that awaited that would never reach its polling
|
|
16
|
+
* loop while the operator was approving it in that very browser.
|
|
17
|
+
*/
|
|
18
|
+
export function openInBrowser(url, deps = {}) {
|
|
19
|
+
const env = deps.env ?? process.env;
|
|
20
|
+
if (env.CC_ROUTER_NO_BROWSER === "1")
|
|
21
|
+
return Promise.resolve(false);
|
|
22
|
+
const { command, args } = browserCommandFor(deps.platform ?? process.platform, url);
|
|
23
|
+
const spawn = deps.spawn ?? nodeSpawn;
|
|
24
|
+
return new Promise(resolve => {
|
|
25
|
+
try {
|
|
26
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
27
|
+
child.once("spawn", () => resolve(true));
|
|
28
|
+
child.once("error", () => resolve(false));
|
|
29
|
+
// Let the process exit without waiting on the opener.
|
|
30
|
+
child.unref();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
resolve(false);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse an operator-typed expiry: a Unix timestamp in milliseconds or an ISO
|
|
3
|
+
* date. Returns null when the input is neither.
|
|
4
|
+
*
|
|
5
|
+
* `new Date("1790000000000")` is `NaN` — the Date constructor only parses
|
|
6
|
+
* date strings — so a pasted millisecond timestamp has to go through
|
|
7
|
+
* `Number()` first, or the saved account carries an invalid expiry.
|
|
8
|
+
*/
|
|
9
|
+
export function parseExpiryInput(raw) {
|
|
10
|
+
const value = raw.trim();
|
|
11
|
+
if (!value)
|
|
12
|
+
return null;
|
|
13
|
+
// Any integer-looking input is a timestamp, negative ones included: V8's
|
|
14
|
+
// Date.parse would otherwise read "-5" as a year.
|
|
15
|
+
if (/^-?\d+$/.test(value)) {
|
|
16
|
+
const ms = Number(value);
|
|
17
|
+
return Number.isFinite(ms) && ms > 0 ? ms : null;
|
|
18
|
+
}
|
|
19
|
+
const parsed = Date.parse(value);
|
|
20
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
21
|
+
}
|
|
@@ -123,11 +123,10 @@ function parseCredentialJson(raw) {
|
|
|
123
123
|
const accessToken = obj["accessToken"];
|
|
124
124
|
const refreshToken = obj["refreshToken"];
|
|
125
125
|
const expiresAt = obj["expiresAt"];
|
|
126
|
-
if (typeof accessToken !== "string" ||
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
if (typeof accessToken !== "string" || !accessToken.startsWith("sk-ant-"))
|
|
127
|
+
return null;
|
|
128
|
+
if (refreshToken !== undefined && typeof refreshToken !== "string")
|
|
129
129
|
return null;
|
|
130
|
-
}
|
|
131
130
|
const scopes = Array.isArray(obj["scopes"])
|
|
132
131
|
? obj["scopes"]
|
|
133
132
|
: ["user:inference", "user:profile"];
|
|
@@ -142,7 +141,12 @@ function parseCredentialJson(raw) {
|
|
|
142
141
|
// No expiry info — assume 8h from now (standard OAuth token lifetime)
|
|
143
142
|
expiresAtMs = Date.now() + 8 * 60 * 60 * 1000;
|
|
144
143
|
}
|
|
145
|
-
return {
|
|
144
|
+
return {
|
|
145
|
+
accessToken,
|
|
146
|
+
refreshToken: typeof refreshToken === "string" ? refreshToken : undefined,
|
|
147
|
+
expiresAt: expiresAtMs,
|
|
148
|
+
scopes,
|
|
149
|
+
};
|
|
146
150
|
}
|
|
147
151
|
catch {
|
|
148
152
|
return null;
|
package/package.json
CHANGED