tokentracker-cli 1.5.15 → 1.5.16
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/dashboard/dist/assets/{AchievementBadge-CwjI28oT.js → AchievementBadge-CnnJGct8.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-Dq544fjX.js → AchievementsPage-CFkUJVxy.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-DufV6nod.js → AchievementsSection-CoLQX04M.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-vw8xydKu.js → BadgeDetailModal-s8rObEHY.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-De0anwsh.js → BotAnimated-BmBnL-i8.js} +1 -1
- package/dashboard/dist/assets/{Card-CuSeWBDM.js → Card-BIqie5mN.js} +1 -1
- package/dashboard/dist/assets/{CloudCheckoutPage-CxFcOwAj.js → CloudCheckoutPage-B_c8Q58A.js} +1 -1
- package/dashboard/dist/assets/{CloudPage-Beb7_JLW.js → CloudPage-CfW_pDf2.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-PsUq2iRS.js → CommandPalette-BRryGPVJ.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-b54WaPbK.js → CommunityStatsModal-Cyh1Q9Fw.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-_NdunIh5.js → ConfirmModal-DWBYqpLJ.js} +1 -1
- package/dashboard/dist/assets/{Controls-5Is6MWln.js → Controls-CPSiCzVW.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-C0n0qC7h.js → DashboardPage-JcS1xwUm.js} +1 -1
- package/dashboard/dist/assets/{DateRangePopover-C3WfGy-0.js → DateRangePopover-DjuS6888.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-Vn6k-mGd.js → DevicePage-DZ3qiDbT.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-CXttnna-.js → DialogClose-CfORRnoT.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-ChihKLnm.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-B0cNIo0E.js → DialogTitle-DZO6-sTI.js} +1 -1
- package/dashboard/dist/assets/{ElectricLogo-DC3XSfIu.js → ElectricLogo-BzR7mRin.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-H7S3TMHi.js → FadeIn-BxHaUvz_.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-xgbIo5OP.js → HeaderGithubStar-KGzKAmuQ.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-CpzM5Dju.js → HoverTooltip-C6pASVDN.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-BdV7S4dY.js → IpCheckPage-BJ8TROZC.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-D9f28ryO.js → LandingPage-B15GRFpI.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardPage--WUFpkBI.js → LeaderboardPage-C3hJ2Gh3.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProBadge-CEvrQ75Y.js → LeaderboardProBadge-CRfBTxpt.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfileModal-Bn0HFr5h.js → LeaderboardProfileModal-DTGmcjWF.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-DJ7fQI68.js → LeaderboardProfilePage-BD04D7ML.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-7tPDheuX.js → LimitsPage-LsDCIUKd.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-DlGbVQS8.js → LocalOnlyNotice-B3XQKD8k.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-BZZmzUGh.js → LoginCard-DF1XcskU.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-BT5_bIxU.js → LoginModal-DuyVT0Tt.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-DMjDUPPi.js → LoginPage-kge4kJKa.js} +1 -1
- package/dashboard/dist/assets/{PetPage-Dk3j6ITn.js → PetPage-DuYtsKza.js} +2 -2
- package/dashboard/dist/assets/{PopoverPopup-pugRu3RC.js → PopoverPopup-BkNNS3VG.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-CtmL1CmY.js → ProviderIcon-z2TKXPlS.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-BB6U7t8P.js → ResetPasswordPage-CXM9E6d1.js} +1 -1
- package/dashboard/dist/assets/{Select-7w1su8lw.js → Select-CeoAayzi.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-CaEUmWkj.js → SelectItemText-CRxvAgUV.js} +1 -1
- package/dashboard/dist/assets/{SelfHostPage-DctWoF-A.js → SelfHostPage-Cavstn-d.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-Dg3ljEXC.js → ServiceStatusPage-CdIeM_DX.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-B1dvW-be.js → SessionsPage-DPihAS26.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-DuXSX1R2.js → SettingsPage-zxOsLeE5.js} +3 -3
- package/dashboard/dist/assets/{SkillsPage-CvXQFgc9.js → SkillsPage-BRGonzsC.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-DKC8Rk_Y.js → TokenGalaxy-D9-huQJ_.js} +1 -1
- package/dashboard/dist/assets/{TooltipPopup-Dp9EpHLG.js → TooltipPopup-BS99Tf44.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-DWnlBn3H.js → TrendMonitor-DlNT4ObY.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-GECNmb1Q.js → WidgetsPage-DTFnmTwq.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-Dq_1lOnb.js → WrappedPage-DwMdnusQ.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-CkF0pdTJ.js → __vite-browser-external-DR_a-OTS.js} +2 -2
- package/dashboard/dist/assets/{agent-logos-Dqe3Vrah.js → agent-logos-D6kTooyz.js} +1 -1
- package/dashboard/dist/assets/{arrow-left-Cr_2QnDu.js → arrow-left-jpuJhYL4.js} +1 -1
- package/dashboard/dist/assets/{arrow-right-BsK2sJgt.js → arrow-right-n1hLkwiK.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-DkWMGqXo.js → arrow-up-right-fZmEfh7R.js} +1 -1
- package/dashboard/dist/assets/{calendar-days-BB9VV438.js → calendar-days-B0LdECvD.js} +1 -1
- package/dashboard/dist/assets/{check-bpSCJZWh.js → check-CcnNd8bi.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-CtECtDmx.js → chevron-down-DhbHBjVC.js} +1 -1
- package/dashboard/dist/assets/{cloud-checkout-C4PrzKhN.js → cloud-checkout-9XOoux-p.js} +1 -1
- package/dashboard/dist/assets/{composite-BHisG82k.js → composite-DIDAkYhy.js} +1 -1
- package/dashboard/dist/assets/{copy-Dmxzd-Sw.js → copy-BNDBunFG.js} +1 -1
- package/dashboard/dist/assets/{download-CkdQzw2w.js → download-wlqdqJmq.js} +1 -1
- package/dashboard/dist/assets/{external-link-C971FjvC.js → external-link-BvssZ_45.js} +1 -1
- package/dashboard/dist/assets/{flame-mKbp7kxw.js → flame-DP8IUT3E.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-C35FrJnH.js → format-tokens-DMsx_fWk.js} +1 -1
- package/dashboard/dist/assets/{gift-BX2lSOmc.js → gift-zF4D6O4a.js} +1 -1
- package/dashboard/dist/assets/{icons-ov92u52T.js → icons-Batw5aOg.js} +1 -1
- package/dashboard/dist/assets/{index-DF03nIHb.js → index-3qKpTOzz.js} +1 -1
- package/dashboard/dist/assets/{index-9OCL-BL8.js → index-Cqla2Zq5.js} +1 -1
- package/dashboard/dist/assets/{index-Bgm38oHe.js → index-Dc8PQ8ZR.js} +1 -1
- package/dashboard/dist/assets/{index-Ca5xkNrK.js → index-dCfimErX.js} +1 -1
- package/dashboard/dist/assets/{info-B-f9hWcw.js → info-DcUyOPKG.js} +1 -1
- package/dashboard/dist/assets/{link-2-l1wuezTo.js → link-2-CS-cWLmx.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-Lmjrs8XX.js → loader-circle-DKc-gwgL.js} +1 -1
- package/dashboard/dist/assets/{local-usage-transfer-Ds6cF41d.js → local-usage-transfer-bHATORmq.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-tVlltJXO.js → modifiers.esm-B4QifJzx.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-XRiYiY8u.js → monitor-smartphone-D8vpN9Jv.js} +1 -1
- package/dashboard/dist/assets/{play-DaD9EMjk.js → play-D70P7DVT.js} +1 -1
- package/dashboard/dist/assets/{plus-D30fbsKQ.js → plus-YomnSdqY.js} +1 -1
- package/dashboard/dist/assets/{provider-display-CKdhBOwz.js → provider-display-VXu7Hzwq.js} +1 -1
- package/dashboard/dist/assets/{react-BY0y0K4x.js → react-CUO9Gs2G.js} +1 -1
- package/dashboard/dist/assets/{safePolygon-CSG_P7rG.js → safePolygon-CbVKM3Lv.js} +1 -1
- package/dashboard/dist/assets/{search-Kiq8wVHA.js → search-BUi1hAmV.js} +1 -1
- package/dashboard/dist/assets/{server-CjovGBGz.js → server-B-agbJYI.js} +1 -1
- package/dashboard/dist/assets/{shield-check-DF9MmMCq.js → shield-check-yHx3fQDo.js} +1 -1
- package/dashboard/dist/assets/{skills-api-CEzr6p92.js → skills-api-DojEO2dS.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-D7jd9mE_.js → sortable.esm-r4Lz4ugj.js} +1 -1
- package/dashboard/dist/assets/{store-D9_jo_do.js → store-GzQbKyT-.js} +1 -1
- package/dashboard/dist/assets/{terminal-CLOlvzR-.js → terminal-BLCZgZTm.js} +1 -1
- package/dashboard/dist/assets/{trash-DIe0fsDD.js → trash-BFGtCBJS.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-gDkdiRBz.js → use-community-stats-DGdLAa8L.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-tXGuKvua.js → use-latest-request-guard-l39bM4s0.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-CCuLFy00.js → use-limits-display-prefs-JcWgMNA0.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-hgYqaiR7.js → use-native-settings-0sXFw6mq.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-D6-YzYJI.js → use-ordered-list-mRdyZ18r.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-D2g3iGbi.js +1 -0
- package/dashboard/dist/assets/{use-transform-BEiXvAuE.js → use-transform-CbJqMa_Y.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-BlwtyPtT.js → use-usage-limits-qQKS4sHR.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-CwCGJLHb.js → useCurrency-BJyZCVtd.js} +1 -1
- package/dashboard/dist/assets/{usePositioner-BqHtRIrE.js → usePositioner-zsiJqVGU.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-BUNBUX-x.js → useTokenFormat-DxEKSp5H.js} +1 -1
- package/dashboard/dist/assets/{zap-C0RFs2qQ.js → zap-D40-8VPk.js} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/ip-check.html +1 -1
- package/dashboard/dist/leaderboard.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +1 -1
- package/src/lib/ark-agent-plan-limits.js +26 -55
- package/src/lib/ark-coding-plan-limits.js +31 -62
- package/src/lib/cloud-account.js +29 -3
- package/src/lib/local-api.js +92 -5
- package/src/lib/pricing/curated-overrides.json +8 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/qoder-limits.js +35 -69
- package/dashboard/dist/assets/DialogDescription-C3KGos_G.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-DfbDxs5a.js +0 -1
|
@@ -26,6 +26,7 @@ const os = require("node:os");
|
|
|
26
26
|
const path = require("node:path");
|
|
27
27
|
|
|
28
28
|
const { runCommand, resolveBinaryPath, statBinaryInDirs, commonGlobalBinDirectories, resolvedCliEnvironment } = require("./command-runner");
|
|
29
|
+
const { createLimitsCache } = require("./limits-cache");
|
|
29
30
|
|
|
30
31
|
const ARK_LIMITS_CACHE_FILE = "ark-agent-plan-limits-cache.json";
|
|
31
32
|
const ARK_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS = 12 * 60 * 60 * 1000;
|
|
@@ -188,68 +189,38 @@ function normalizeArkAgentPlanResponse(body) {
|
|
|
188
189
|
// Alias for shared shape
|
|
189
190
|
const normalizeArkCodingPlanResponse = normalizeArkAgentPlanResponse;
|
|
190
191
|
|
|
192
|
+
// Deliberately fail-open on identity: the guard only applies when `profile
|
|
193
|
+
// show` could establish the current identity — when it also failed, serving
|
|
194
|
+
// the stale cache beats erroring out (availability over strictness). There is
|
|
195
|
+
// no global max-age: dated windows are bounded by their own reset stamp and
|
|
196
|
+
// undated ones by the 12h unknown-reset TTL.
|
|
197
|
+
const arkLimitsCache = createLimitsCache({
|
|
198
|
+
file: ARK_LIMITS_CACHE_FILE,
|
|
199
|
+
key: null,
|
|
200
|
+
maxAgeMs: null,
|
|
201
|
+
unknownResetTtlMs: ARK_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS,
|
|
202
|
+
fields: { plan_label: "string", product: "string", profile_identity: "string" },
|
|
203
|
+
windowKeys: ["primary_window", "secondary_window", "tertiary_window"],
|
|
204
|
+
identityField: "profile_identity",
|
|
205
|
+
identityRequired: false,
|
|
206
|
+
// The normalizer throws when no periods survive, so the write guard skips
|
|
207
|
+
// the window check the other providers need.
|
|
208
|
+
writeGuard: (record) => Boolean(record?.configured) && !record.error,
|
|
209
|
+
cachedFlag: true,
|
|
210
|
+
stale: true,
|
|
211
|
+
source: { const: "disk-cache" },
|
|
212
|
+
});
|
|
213
|
+
|
|
191
214
|
function arkAgentPlanCachePath({ home = os.homedir() } = {}) {
|
|
192
|
-
return path
|
|
215
|
+
return arkLimitsCache.path({ home });
|
|
193
216
|
}
|
|
194
217
|
|
|
195
218
|
function readArkAgentPlanLimitsCache({ home = os.homedir(), nowMs = Date.now(), profileIdentity } = {}) {
|
|
196
|
-
|
|
197
|
-
const parsed = JSON.parse(fs.readFileSync(arkAgentPlanCachePath({ home }), "utf8"));
|
|
198
|
-
if (profileIdentity && parsed?.profile_identity !== profileIdentity) return null;
|
|
199
|
-
const cachedAtMs = Date.parse(parsed?.cached_at || "");
|
|
200
|
-
if (!Number.isFinite(cachedAtMs) || cachedAtMs > nowMs + 60_000) return null;
|
|
201
|
-
const windows = [parsed?.primary_window, parsed?.secondary_window, parsed?.tertiary_window];
|
|
202
|
-
const surviving = windows.map((window) => {
|
|
203
|
-
if (!window) return null;
|
|
204
|
-
const resetAtMs = Date.parse(window.reset_at || "");
|
|
205
|
-
if (Number.isFinite(resetAtMs) && resetAtMs <= nowMs) return null;
|
|
206
|
-
return window;
|
|
207
|
-
});
|
|
208
|
-
if (surviving.every((window) => !window)) return null;
|
|
209
|
-
const bounded = surviving.map((window) => {
|
|
210
|
-
if (!window) return null;
|
|
211
|
-
return Number.isFinite(Date.parse(window.reset_at || ""))
|
|
212
|
-
|| nowMs - cachedAtMs <= ARK_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS
|
|
213
|
-
? window
|
|
214
|
-
: null;
|
|
215
|
-
});
|
|
216
|
-
if (bounded.every((window) => !window)) return null;
|
|
217
|
-
return {
|
|
218
|
-
configured: true,
|
|
219
|
-
error: null,
|
|
220
|
-
plan_label: typeof parsed?.plan_label === "string" ? parsed.plan_label : null,
|
|
221
|
-
product: typeof parsed?.product === "string" ? parsed.product : null,
|
|
222
|
-
primary_window: bounded[0],
|
|
223
|
-
secondary_window: bounded[1],
|
|
224
|
-
tertiary_window: bounded[2],
|
|
225
|
-
cached: true,
|
|
226
|
-
stale: true,
|
|
227
|
-
cached_at: parsed.cached_at,
|
|
228
|
-
source: "disk-cache",
|
|
229
|
-
};
|
|
230
|
-
} catch (_error) {
|
|
231
|
-
return null;
|
|
232
|
-
}
|
|
219
|
+
return arkLimitsCache.read({ home, nowMs, identity: profileIdentity });
|
|
233
220
|
}
|
|
234
221
|
|
|
235
222
|
function writeArkAgentPlanLimitsCache(limits, { home = os.homedir(), nowMs = Date.now() } = {}) {
|
|
236
|
-
|
|
237
|
-
const cachePath = arkAgentPlanCachePath({ home });
|
|
238
|
-
const payload = {
|
|
239
|
-
plan_label: limits.plan_label || null,
|
|
240
|
-
product: limits.product || null,
|
|
241
|
-
profile_identity: limits.profile_identity || null,
|
|
242
|
-
primary_window: limits.primary_window || null,
|
|
243
|
-
secondary_window: limits.secondary_window || null,
|
|
244
|
-
tertiary_window: limits.tertiary_window || null,
|
|
245
|
-
cached_at: new Date(nowMs).toISOString(),
|
|
246
|
-
};
|
|
247
|
-
try {
|
|
248
|
-
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
249
|
-
const tmpPath = `${cachePath}.${process.pid}.tmp`;
|
|
250
|
-
fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
251
|
-
fs.renameSync(tmpPath, cachePath);
|
|
252
|
-
} catch (_error) {}
|
|
223
|
+
arkLimitsCache.write(limits, { home, nowMs });
|
|
253
224
|
}
|
|
254
225
|
|
|
255
226
|
function hasArkCliInstallEvidence({ home = os.homedir(), platform = process.platform } = {}) {
|
|
@@ -26,6 +26,7 @@ const os = require("node:os");
|
|
|
26
26
|
const path = require("node:path");
|
|
27
27
|
|
|
28
28
|
const { runCommand, resolveBinaryPath, statBinaryInDirs, commonGlobalBinDirectories, resolvedCliEnvironment } = require("./command-runner");
|
|
29
|
+
const { createLimitsCache } = require("./limits-cache");
|
|
29
30
|
|
|
30
31
|
const ARK_LIMITS_CACHE_FILE = "ark-coding-plan-limits-cache.json";
|
|
31
32
|
const ARK_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS = 12 * 60 * 60 * 1000;
|
|
@@ -212,75 +213,43 @@ function normalizeArkCodingPlanResponse(body) {
|
|
|
212
213
|
};
|
|
213
214
|
}
|
|
214
215
|
|
|
216
|
+
// Deliberately fail-open on identity: the guard only applies when `profile
|
|
217
|
+
// show` could establish the current identity — when it also failed, serving
|
|
218
|
+
// the stale cache beats erroring out (availability over strictness). There is
|
|
219
|
+
// no global max-age: dated windows are bounded by their own reset stamp and
|
|
220
|
+
// undated ones by the 12h unknown-reset TTL.
|
|
221
|
+
|
|
222
|
+
// A window whose reset_at has passed is stale — the quota has already rolled
|
|
223
|
+
// over, so serving its old used_percent would mislead. Undated windows can't be
|
|
224
|
+
// checked against a reset, so each one is dropped when the snapshot is too old;
|
|
225
|
+
// a future-dated sibling must not keep it alive.
|
|
226
|
+
const arkLimitsCache = createLimitsCache({
|
|
227
|
+
file: ARK_LIMITS_CACHE_FILE,
|
|
228
|
+
key: null,
|
|
229
|
+
maxAgeMs: null,
|
|
230
|
+
unknownResetTtlMs: ARK_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS,
|
|
231
|
+
fields: { plan_label: "string", product: "string", profile_identity: "string" },
|
|
232
|
+
windowKeys: ["primary_window", "secondary_window", "tertiary_window"],
|
|
233
|
+
identityField: "profile_identity",
|
|
234
|
+
identityRequired: false,
|
|
235
|
+
// The normalizer throws when no periods survive, so the write guard skips
|
|
236
|
+
// the window check the other providers need.
|
|
237
|
+
writeGuard: (record) => Boolean(record?.configured) && !record.error,
|
|
238
|
+
cachedFlag: true,
|
|
239
|
+
stale: true,
|
|
240
|
+
source: { const: "disk-cache" },
|
|
241
|
+
});
|
|
242
|
+
|
|
215
243
|
function arkCodingPlanCachePath({ home = os.homedir() } = {}) {
|
|
216
|
-
return path
|
|
244
|
+
return arkLimitsCache.path({ home });
|
|
217
245
|
}
|
|
218
246
|
|
|
219
247
|
function readArkCodingPlanLimitsCache({ home = os.homedir(), nowMs = Date.now(), profileIdentity } = {}) {
|
|
220
|
-
|
|
221
|
-
const parsed = JSON.parse(fs.readFileSync(arkCodingPlanCachePath({ home }), "utf8"));
|
|
222
|
-
// Deliberately fail-open: the guard only applies when `profile show`
|
|
223
|
-
// could establish the current identity. When it also failed, serving
|
|
224
|
-
// the stale cache beats erroring out — availability over strictness.
|
|
225
|
-
if (profileIdentity && parsed?.profile_identity !== profileIdentity) return null;
|
|
226
|
-
const cachedAtMs = Date.parse(parsed?.cached_at || "");
|
|
227
|
-
if (!Number.isFinite(cachedAtMs) || cachedAtMs > nowMs + 60_000) return null;
|
|
228
|
-
// A window whose reset_at has passed is stale — the quota has already
|
|
229
|
-
// rolled over, so serving its old used_percent would mislead. Drop it.
|
|
230
|
-
const windows = [parsed?.primary_window, parsed?.secondary_window, parsed?.tertiary_window];
|
|
231
|
-
const surviving = windows.map((window) => {
|
|
232
|
-
if (!window) return null;
|
|
233
|
-
const resetAtMs = Date.parse(window.reset_at || "");
|
|
234
|
-
if (Number.isFinite(resetAtMs) && resetAtMs <= nowMs) return null;
|
|
235
|
-
return window;
|
|
236
|
-
});
|
|
237
|
-
if (surviving.every((window) => !window)) return null;
|
|
238
|
-
// Undated windows can't be checked against a reset, so drop each one when
|
|
239
|
-
// the snapshot is too old. A future-dated sibling must not keep it alive.
|
|
240
|
-
const bounded = surviving.map((window) => {
|
|
241
|
-
if (!window) return null;
|
|
242
|
-
return Number.isFinite(Date.parse(window.reset_at || ""))
|
|
243
|
-
|| nowMs - cachedAtMs <= ARK_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS
|
|
244
|
-
? window
|
|
245
|
-
: null;
|
|
246
|
-
});
|
|
247
|
-
if (bounded.every((window) => !window)) return null;
|
|
248
|
-
return {
|
|
249
|
-
configured: true,
|
|
250
|
-
error: null,
|
|
251
|
-
plan_label: typeof parsed?.plan_label === "string" ? parsed.plan_label : null,
|
|
252
|
-
product: typeof parsed?.product === "string" ? parsed.product : null,
|
|
253
|
-
primary_window: bounded[0],
|
|
254
|
-
secondary_window: bounded[1],
|
|
255
|
-
tertiary_window: bounded[2],
|
|
256
|
-
cached: true,
|
|
257
|
-
stale: true,
|
|
258
|
-
cached_at: parsed.cached_at,
|
|
259
|
-
source: "disk-cache",
|
|
260
|
-
};
|
|
261
|
-
} catch (_error) {
|
|
262
|
-
return null;
|
|
263
|
-
}
|
|
248
|
+
return arkLimitsCache.read({ home, nowMs, identity: profileIdentity });
|
|
264
249
|
}
|
|
265
250
|
|
|
266
251
|
function writeArkCodingPlanLimitsCache(limits, { home = os.homedir(), nowMs = Date.now() } = {}) {
|
|
267
|
-
|
|
268
|
-
const cachePath = arkCodingPlanCachePath({ home });
|
|
269
|
-
const payload = {
|
|
270
|
-
plan_label: limits.plan_label || null,
|
|
271
|
-
product: limits.product || null,
|
|
272
|
-
profile_identity: limits.profile_identity || null,
|
|
273
|
-
primary_window: limits.primary_window || null,
|
|
274
|
-
secondary_window: limits.secondary_window || null,
|
|
275
|
-
tertiary_window: limits.tertiary_window || null,
|
|
276
|
-
cached_at: new Date(nowMs).toISOString(),
|
|
277
|
-
};
|
|
278
|
-
try {
|
|
279
|
-
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
280
|
-
const tmpPath = `${cachePath}.${process.pid}.tmp`;
|
|
281
|
-
fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
282
|
-
fs.renameSync(tmpPath, cachePath);
|
|
283
|
-
} catch (_error) {}
|
|
252
|
+
arkLimitsCache.write(limits, { home, nowMs });
|
|
284
253
|
}
|
|
285
254
|
|
|
286
255
|
// runCommand / whichBinary / resolveBinaryPath now live in ./command-runner,
|
package/src/lib/cloud-account.js
CHANGED
|
@@ -216,6 +216,28 @@ function csrfTokenFromRefreshPayload(data) {
|
|
|
216
216
|
return raw && raw.length > 0 ? raw : null;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
function userFromRefreshPayload(data) {
|
|
220
|
+
if (!data || typeof data !== "object") return null;
|
|
221
|
+
const raw = data.user && typeof data.user === "object" && !Array.isArray(data.user)
|
|
222
|
+
? data.user
|
|
223
|
+
: data.session && typeof data.session === "object" && data.session.user && typeof data.session.user === "object" && !Array.isArray(data.session.user)
|
|
224
|
+
? data.session.user
|
|
225
|
+
: null;
|
|
226
|
+
return raw;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function userFromAccessToken(accessToken) {
|
|
230
|
+
try {
|
|
231
|
+
const payload = JSON.parse(Buffer.from(String(accessToken).split(".")[1], "base64url").toString("utf8"));
|
|
232
|
+
if (typeof payload?.sub !== "string" || !payload.sub) return null;
|
|
233
|
+
const user = { id: payload.sub };
|
|
234
|
+
if (typeof payload.email === "string" && payload.email) user.email = payload.email;
|
|
235
|
+
return user;
|
|
236
|
+
} catch {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
219
241
|
/**
|
|
220
242
|
* Perform the actual refresh POST. Throws a classified `AccountAuthError` on
|
|
221
243
|
* every failure so callers can distinguish a transient outage (timeout,
|
|
@@ -283,7 +305,8 @@ async function performMint({ root, anonKey, refreshToken, fetchImpl, now, timeou
|
|
|
283
305
|
const rotated = refreshTokenFromRefreshPayload(data);
|
|
284
306
|
// The caller persists a rotation before its next account read. Key the
|
|
285
307
|
// still-valid access token by that current refresh token, not the consumed one.
|
|
286
|
-
|
|
308
|
+
const user = userFromRefreshPayload(data) || userFromAccessToken(accessToken);
|
|
309
|
+
tokenCache = { cacheKey: `${root}\0${rotated || refreshToken}`, accessToken, expMs, user };
|
|
287
310
|
return {
|
|
288
311
|
accessToken,
|
|
289
312
|
refreshToken: rotated && rotated !== refreshToken ? rotated : null,
|
|
@@ -292,6 +315,9 @@ async function performMint({ root, anonKey, refreshToken, fetchImpl, now, timeou
|
|
|
292
315
|
// relayed (refresh, csrf) pair in sync — a stale relayed csrf breaks the
|
|
293
316
|
// dashboard's cookie-path refresh with 403 Invalid CSRF.
|
|
294
317
|
csrfToken: csrfTokenFromRefreshPayload(data),
|
|
318
|
+
// The InsForge SDK's refreshSession persists the session only when the
|
|
319
|
+
// response includes a user object alongside the access token.
|
|
320
|
+
user,
|
|
295
321
|
};
|
|
296
322
|
}
|
|
297
323
|
|
|
@@ -303,7 +329,7 @@ async function performMint({ root, anonKey, refreshToken, fetchImpl, now, timeou
|
|
|
303
329
|
*
|
|
304
330
|
* @param {boolean} [throwOnFailure] when true, reject with a classified
|
|
305
331
|
* `AccountAuthError` instead of resolving to `null`.
|
|
306
|
-
* @returns {Promise<{accessToken: string, refreshToken: string|null, csrfToken: string|null}|null>}
|
|
332
|
+
* @returns {Promise<{accessToken: string, refreshToken: string|null, csrfToken: string|null, user: object|null}|null>}
|
|
307
333
|
* null when no refresh token is available or the refresh failed.
|
|
308
334
|
*/
|
|
309
335
|
async function mintAccessToken({
|
|
@@ -327,7 +353,7 @@ async function mintAccessToken({
|
|
|
327
353
|
tokenCache.accessToken &&
|
|
328
354
|
tokenCache.expMs - skewMs > now()
|
|
329
355
|
) {
|
|
330
|
-
return { accessToken: tokenCache.accessToken, refreshToken: null, csrfToken: null };
|
|
356
|
+
return { accessToken: tokenCache.accessToken, refreshToken: null, csrfToken: null, user: tokenCache.user || null };
|
|
331
357
|
}
|
|
332
358
|
|
|
333
359
|
let inflight = mintInflight.get(cacheKey);
|
package/src/lib/local-api.js
CHANGED
|
@@ -1400,6 +1400,13 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
1400
1400
|
}
|
|
1401
1401
|
}
|
|
1402
1402
|
|
|
1403
|
+
function rememberIssuedAccessToken(accessToken, sourceFingerprint) {
|
|
1404
|
+
if (!sourceFingerprint || typeof accessToken !== "string" || accessToken.length > 8192) return;
|
|
1405
|
+
const hash = crypto.createHash("sha256").update(accessToken).digest("hex");
|
|
1406
|
+
if (!issuedAccessTokenInstances.has(hash)) issuedAccessTokenInstances.set(hash, sourceFingerprint);
|
|
1407
|
+
while (issuedAccessTokenInstances.size > 64) issuedAccessTokenInstances.delete(issuedAccessTokenInstances.keys().next().value);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1403
1410
|
function captureAuthTokensFromBody(bodyBuffer, contentType, { newLogin = false, sourceFingerprint } = {}) {
|
|
1404
1411
|
if (!bodyBuffer || !String(contentType || "").toLowerCase().includes("application/json")) return;
|
|
1405
1412
|
let parsed = null;
|
|
@@ -1408,11 +1415,7 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
1408
1415
|
} catch {
|
|
1409
1416
|
return;
|
|
1410
1417
|
}
|
|
1411
|
-
|
|
1412
|
-
const hash = crypto.createHash("sha256").update(parsed.accessToken).digest("hex");
|
|
1413
|
-
if (!issuedAccessTokenInstances.has(hash)) issuedAccessTokenInstances.set(hash, sourceFingerprint);
|
|
1414
|
-
while (issuedAccessTokenInstances.size > 64) issuedAccessTokenInstances.delete(issuedAccessTokenInstances.keys().next().value);
|
|
1415
|
-
}
|
|
1418
|
+
rememberIssuedAccessToken(parsed?.accessToken, sourceFingerprint);
|
|
1416
1419
|
let changed = false;
|
|
1417
1420
|
const token = typeof parsed?.csrfToken === "string" ? parsed.csrfToken.trim() : "";
|
|
1418
1421
|
if (token) {
|
|
@@ -2073,6 +2076,90 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
2073
2076
|
const hasCsrfHeader = typeof proxyHeaders["x-csrf-token"] === "string" && proxyHeaders["x-csrf-token"].trim().length > 0;
|
|
2074
2077
|
const relayCsrfToken = getRelayCookieValue(csrfRelayCookieName);
|
|
2075
2078
|
const relayRefreshToken = getRelayCookieValue("insforge_refresh_token", { decode: true });
|
|
2079
|
+
// Issue #1074: the backend rotates refresh tokens, but this proxy and
|
|
2080
|
+
// `mintAccessToken` used to POST the same token independently. A page
|
|
2081
|
+
// refresh concurrent with a background account read then consumed the
|
|
2082
|
+
// token twice — the loser got 401 and the SPA signed the user out.
|
|
2083
|
+
// Route every refresh that still has a persisted token through the
|
|
2084
|
+
// shared mint (cache + single-flight + one retry against a just-
|
|
2085
|
+
// rotated file) so both callers share one result.
|
|
2086
|
+
if (p === "/api/auth/refresh" && relayRefreshToken && String(req.method || "GET").toUpperCase() === "POST") {
|
|
2087
|
+
for await (const _chunk of req) { /* drain unused body */ }
|
|
2088
|
+
const latestRuntime = getRuntime();
|
|
2089
|
+
if (latestRuntime.configurationError || latestRuntime.baseUrl !== runtime.baseUrl || latestRuntime.anonKey !== runtime.anonKey) {
|
|
2090
|
+
json(res, { code: "instance_changed", error: "The configured backend changed during authentication. Reload and sign in again." }, 409);
|
|
2091
|
+
return true;
|
|
2092
|
+
}
|
|
2093
|
+
const mintOpts = {
|
|
2094
|
+
baseUrl: insforgeBase,
|
|
2095
|
+
anonKey: runtime.anonKey,
|
|
2096
|
+
timeoutMs: runtime.httpTimeoutMs,
|
|
2097
|
+
throwOnFailure: true,
|
|
2098
|
+
};
|
|
2099
|
+
const rejectIfRefreshContextChanged = () => {
|
|
2100
|
+
const currentRuntime = getRuntime();
|
|
2101
|
+
if (currentRuntime.configurationError || currentRuntime.baseUrl !== runtime.baseUrl || currentRuntime.anonKey !== runtime.anonKey) {
|
|
2102
|
+
json(res, { code: "instance_changed", error: "The configured backend changed during authentication. Reload and sign in again." }, 409);
|
|
2103
|
+
return true;
|
|
2104
|
+
}
|
|
2105
|
+
if (authSessionAtStart !== getCloudAccountSessionGeneration()) {
|
|
2106
|
+
json(res, { error: "Account session changed", code: "auth_session_changed" }, 409);
|
|
2107
|
+
return true;
|
|
2108
|
+
}
|
|
2109
|
+
return false;
|
|
2110
|
+
};
|
|
2111
|
+
let minted = null;
|
|
2112
|
+
try {
|
|
2113
|
+
try {
|
|
2114
|
+
minted = await mintAccessToken({ ...mintOpts, refreshToken: relayRefreshToken });
|
|
2115
|
+
} catch (error) {
|
|
2116
|
+
// Only retry a consumed-token 401 against a just-rotated
|
|
2117
|
+
// persisted token for the SAME backend/session. Otherwise a
|
|
2118
|
+
// rejected mint from instance A could submit instance B's
|
|
2119
|
+
// newly saved refresh token to A.
|
|
2120
|
+
if (rejectIfRefreshContextChanged()) return true;
|
|
2121
|
+
const latest = getRelayCookieValue("insforge_refresh_token", { decode: true });
|
|
2122
|
+
if (latest && latest !== relayRefreshToken && error?.status === 401) {
|
|
2123
|
+
minted = await mintAccessToken({ ...mintOpts, refreshToken: latest });
|
|
2124
|
+
} else {
|
|
2125
|
+
throw error;
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
} catch (error) {
|
|
2129
|
+
if (rejectIfRefreshContextChanged()) return true;
|
|
2130
|
+
// performMint tags every non-OK HTTP status as auth_rejected.
|
|
2131
|
+
// Only a real 401 means the credential was consumed/invalid;
|
|
2132
|
+
// 429/503 must not sign the SPA out.
|
|
2133
|
+
if (error?.status === 401) {
|
|
2134
|
+
json(res, { error: "AUTH_UNAUTHORIZED", message: "Invalid or expired refresh token" }, 401);
|
|
2135
|
+
return true;
|
|
2136
|
+
}
|
|
2137
|
+
throw error;
|
|
2138
|
+
}
|
|
2139
|
+
if (rejectIfRefreshContextChanged()) return true;
|
|
2140
|
+
if (!minted?.accessToken) {
|
|
2141
|
+
json(res, { error: "AUTH_UNAUTHORIZED", message: "Invalid or expired refresh token" }, 401);
|
|
2142
|
+
return true;
|
|
2143
|
+
}
|
|
2144
|
+
const payload = { accessToken: minted.accessToken };
|
|
2145
|
+
if (minted.refreshToken) payload.refreshToken = minted.refreshToken;
|
|
2146
|
+
if (minted.user) payload.user = minted.user;
|
|
2147
|
+
// Persist the rotation without bumping sessionGeneration — that
|
|
2148
|
+
// would 409 a concurrent waiter that shared this mint.
|
|
2149
|
+
// setRelayRefreshToken matches the background account-read path.
|
|
2150
|
+
if (minted.refreshToken) setRelayRefreshToken(minted.refreshToken);
|
|
2151
|
+
if (minted.csrfToken) setRelayCsrfToken(minted.csrfToken);
|
|
2152
|
+
// Cache hits return csrfToken:null. The SDK still needs the current
|
|
2153
|
+
// CSRF to sign out; without it, logout 403s and a reload signs the
|
|
2154
|
+
// user back in. Fall back to the relayed token we just persisted.
|
|
2155
|
+
const csrfToken = minted.csrfToken || getRelayCookieValue(csrfRelayCookieName);
|
|
2156
|
+
if (csrfToken) payload.csrfToken = csrfToken;
|
|
2157
|
+
rememberIssuedAccessToken(minted.accessToken, fingerprintFor(runtime));
|
|
2158
|
+
const resBody = Buffer.from(JSON.stringify(payload));
|
|
2159
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
2160
|
+
res.end(resBody);
|
|
2161
|
+
return true;
|
|
2162
|
+
}
|
|
2076
2163
|
// A cookie-less client (fresh WebView after an app update/restart) has no
|
|
2077
2164
|
// browser session to pair a CSRF token with — the persisted refresh token
|
|
2078
2165
|
// replayed through the mobile flow is the only viable recovery. The relay
|
|
@@ -903,6 +903,14 @@
|
|
|
903
903
|
"match": "gpt-5.6",
|
|
904
904
|
"ref": "gpt-5.6-sol"
|
|
905
905
|
},
|
|
906
|
+
{
|
|
907
|
+
"match": "gemini-4-argon",
|
|
908
|
+
"ref": "gemini-4-argon"
|
|
909
|
+
},
|
|
910
|
+
{
|
|
911
|
+
"match": "gemini-4.argon",
|
|
912
|
+
"ref": "gemini-4-argon"
|
|
913
|
+
},
|
|
906
914
|
{
|
|
907
915
|
"match": "kiro",
|
|
908
916
|
"ref": "kiro-cli-agent"
|