@ipv9/tokentracker-cli 0.39.37 → 0.39.39
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 +41 -9
- package/dashboard/dist/assets/{Card-DKmo-v8a.js → Card-BbRYRVXf.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-DYKHv0m3.js +60 -0
- package/dashboard/dist/assets/{FadeIn-Cf30rPTW.js → FadeIn-CjAacJ5t.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-D3CL8XP0.js → IpCheckPage-BGALK8Vy.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-BqST7DxD.js → LimitsPage-LkZGPYvg.js} +2 -2
- package/dashboard/dist/assets/{LocalOnlyNotice-CqHzPooT.js → LocalOnlyNotice-Dxo2vFMQ.js} +1 -1
- package/dashboard/dist/assets/PopoverPopup-DP9fwdtl.js +1 -0
- package/dashboard/dist/assets/Select-DPE53NmM.js +1 -0
- package/dashboard/dist/assets/SelectItemText-D69v77is.js +12 -0
- package/dashboard/dist/assets/SettingsPage-Dxzqpi02.js +1 -0
- package/dashboard/dist/assets/SkillsPage-Bo7e39AB.js +1 -0
- package/dashboard/dist/assets/WidgetsPage-HHcIr_Eq.js +1 -0
- package/dashboard/dist/assets/WrappedPage-C-_jLXXU.js +1 -0
- package/dashboard/dist/assets/{arrow-up-right-BJzUcPig.js → arrow-up-right-BJvJHUGj.js} +1 -1
- package/dashboard/dist/assets/{download-CsXweDLj.js → download-CnzY_EgN.js} +1 -1
- package/dashboard/dist/assets/format-1w-q5n2f.js +1 -0
- package/dashboard/dist/assets/main-1kxAVM3m.css +1 -0
- package/dashboard/dist/assets/main-3gBEI6Jl.js +694 -0
- package/dashboard/dist/assets/mock-data-CJOMGcL9.js +1 -0
- package/dashboard/dist/assets/use-limits-display-prefs-D1nT9-mr.js +1 -0
- package/dashboard/dist/assets/use-native-settings-Dh6NzqjY.js +1 -0
- package/dashboard/dist/assets/use-usage-limits-BXR6lzGf.js +1 -0
- package/dashboard/dist/assets/useCurrency-Hhs5jbWw.js +1 -0
- package/dashboard/dist/index.html +16 -28
- package/dashboard/dist/llms.txt +0 -2
- package/dashboard/dist/sitemap.xml +0 -12
- package/package.json +7 -4
- package/src/cli.js +4 -13
- package/src/commands/doctor.js +1 -3
- package/src/commands/init.js +63 -53
- package/src/commands/serve.js +2 -50
- package/src/commands/status.js +1 -55
- package/src/commands/sync.js +134 -227
- package/src/lib/{browser-auth.js → browser-open.js} +3 -149
- package/src/lib/claude-categorizer.js +204 -32
- package/src/lib/codex-context-breakdown.js +83 -7
- package/src/lib/codex-rollout-parser.js +52 -10
- package/src/lib/diagnostics.js +1 -49
- package/src/lib/doctor.js +12 -87
- package/src/lib/file-identity.js +69 -0
- package/src/lib/init-flow.js +6 -3
- package/src/lib/local-api.js +6 -364
- package/src/lib/pricing/curated-overrides.json +1 -1
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +392 -144
- package/src/lib/runtime-config.js +0 -32
- package/src/lib/sqlite-reader.js +83 -8
- package/dashboard/dist/assets/ActivityHeatmap-DPZ_Aj6E.js +0 -42
- package/dashboard/dist/assets/DashboardPage-C9Q5HCDh.js +0 -19
- package/dashboard/dist/assets/DevicePage-DhQFtROo.js +0 -1
- package/dashboard/dist/assets/DialogTitle-DduJCLiL.js +0 -1
- package/dashboard/dist/assets/InsforgeUserHeaderControls-CDqYVWBQ.js +0 -1
- package/dashboard/dist/assets/LandingPage-9345o0vr.js +0 -4356
- package/dashboard/dist/assets/LeaderboardAvatar-DNORVh3n.js +0 -1
- package/dashboard/dist/assets/LeaderboardPage-DBsdCCzY.js +0 -6
- package/dashboard/dist/assets/LeaderboardProfileModal-BuYdBVh_.js +0 -72
- package/dashboard/dist/assets/LeaderboardProfilePage-D4Fe1LOk.js +0 -1
- package/dashboard/dist/assets/LoginPage-BmcybmeK.js +0 -1
- package/dashboard/dist/assets/PopoverPopup-CDL7kqy5.js +0 -1
- package/dashboard/dist/assets/Select-BkzhG41t.js +0 -1
- package/dashboard/dist/assets/SelectItemText-BmRzs9ud.js +0 -1
- package/dashboard/dist/assets/SettingsPage-gXQl_o6s.js +0 -1
- package/dashboard/dist/assets/SkillsPage-BRXYEsKm.js +0 -1
- package/dashboard/dist/assets/WidgetsPage-CLHLz2UW.js +0 -1
- package/dashboard/dist/assets/WrappedPage-p4ZFwQjO.js +0 -1
- package/dashboard/dist/assets/index-BeoRn2gJ.js +0 -2
- package/dashboard/dist/assets/info-CuLr7ZIa.js +0 -1
- package/dashboard/dist/assets/main-CPGI-smk.js +0 -1004
- package/dashboard/dist/assets/main-DAcSUW2t.css +0 -1
- package/dashboard/dist/assets/use-limits-display-prefs-DOB09__q.js +0 -1
- package/dashboard/dist/assets/use-native-settings-JSylS2xS.js +0 -1
- package/dashboard/dist/assets/use-spring-qjUEvMQX.js +0 -1
- package/dashboard/dist/assets/use-usage-limits-B0bzPxNR.js +0 -1
- package/dashboard/dist/assets/useCurrency-BRorShq7.js +0 -1
- package/dashboard/dist/assets/useOpenInteractionType-DPP-6ZsL.js +0 -12
- package/dashboard/dist/share.html +0 -140
- package/src/commands/device-login.js +0 -161
- package/src/lib/upload-throttle.js +0 -148
package/src/lib/local-api.js
CHANGED
|
@@ -3,7 +3,6 @@ const os = require("node:os");
|
|
|
3
3
|
const path = require("node:path");
|
|
4
4
|
const { spawn } = require("node:child_process");
|
|
5
5
|
const crypto = require("node:crypto");
|
|
6
|
-
const { DEFAULT_BASE_URL, resolveRuntimeConfig } = require("./runtime-config");
|
|
7
6
|
const {
|
|
8
7
|
filterRowsByUsageScope,
|
|
9
8
|
getSourceScope,
|
|
@@ -445,35 +444,7 @@ function trimOutput(value, max = 4000) {
|
|
|
445
444
|
return t.length <= max ? t : t.slice(t.length - max);
|
|
446
445
|
}
|
|
447
446
|
|
|
448
|
-
function normalizeRemoteHttpBaseUrl(value) {
|
|
449
|
-
if (typeof value !== "string") return null;
|
|
450
|
-
const trimmed = value.trim();
|
|
451
|
-
if (!trimmed) return null;
|
|
452
|
-
try {
|
|
453
|
-
const url = new URL(trimmed);
|
|
454
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
455
|
-
url.username = "";
|
|
456
|
-
url.password = "";
|
|
457
|
-
url.hash = "";
|
|
458
|
-
return url.toString().replace(/\/$/, "");
|
|
459
|
-
} catch (_e) {
|
|
460
|
-
return null;
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
447
|
|
|
464
|
-
function resolveAllowedInsforgeBaseUrl(value) {
|
|
465
|
-
const requested = normalizeRemoteHttpBaseUrl(value);
|
|
466
|
-
if (!requested) return null;
|
|
467
|
-
|
|
468
|
-
const runtime = resolveRuntimeConfig();
|
|
469
|
-
const allowed = new Set(
|
|
470
|
-
[runtime.baseUrl, DEFAULT_BASE_URL]
|
|
471
|
-
.map((entry) => normalizeRemoteHttpBaseUrl(entry))
|
|
472
|
-
.filter(Boolean),
|
|
473
|
-
);
|
|
474
|
-
|
|
475
|
-
return allowed.has(requested) ? requested : null;
|
|
476
|
-
}
|
|
477
448
|
|
|
478
449
|
function parseCookieHeader(value) {
|
|
479
450
|
const out = new Map();
|
|
@@ -709,45 +680,6 @@ const HOP_BY_HOP_HEADERS = new Set([
|
|
|
709
680
|
"trailers",
|
|
710
681
|
]);
|
|
711
682
|
|
|
712
|
-
// Strip forbidden + hop-by-hop headers when forwarding an inbound request to
|
|
713
|
-
// fetch(). Honours the Connection header's named-headers list (RFC 7230 §6.1)
|
|
714
|
-
// so values like `Connection: keep-alive, x-custom` also drop x-custom.
|
|
715
|
-
function buildProxyHeaders(headers) {
|
|
716
|
-
const entries =
|
|
717
|
-
headers && typeof headers.entries === "function"
|
|
718
|
-
? Array.from(headers.entries())
|
|
719
|
-
: Object.entries(headers || {});
|
|
720
|
-
|
|
721
|
-
const connectionNamed = new Set();
|
|
722
|
-
const normalized = [];
|
|
723
|
-
for (const [rawKey, rawValue] of entries) {
|
|
724
|
-
if (rawValue == null) continue;
|
|
725
|
-
const key = String(rawKey).toLowerCase();
|
|
726
|
-
normalized.push([key, rawValue]);
|
|
727
|
-
if (key === "connection") {
|
|
728
|
-
const values = Array.isArray(rawValue) ? rawValue : [rawValue];
|
|
729
|
-
for (const v of values) {
|
|
730
|
-
String(v)
|
|
731
|
-
.split(",")
|
|
732
|
-
.map((part) => part.trim().toLowerCase())
|
|
733
|
-
.filter(Boolean)
|
|
734
|
-
.forEach((part) => connectionNamed.add(part));
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
const out = {};
|
|
740
|
-
for (const [key, rawValue] of normalized) {
|
|
741
|
-
if (HOP_BY_HOP_HEADERS.has(key) || connectionNamed.has(key)) continue;
|
|
742
|
-
if (Array.isArray(rawValue)) {
|
|
743
|
-
const joined = rawValue.filter((e) => e != null).map(String).join(", ");
|
|
744
|
-
if (joined) out[key] = joined;
|
|
745
|
-
continue;
|
|
746
|
-
}
|
|
747
|
-
out[key] = String(rawValue);
|
|
748
|
-
}
|
|
749
|
-
return out;
|
|
750
|
-
}
|
|
751
683
|
|
|
752
684
|
// ---------------------------------------------------------------------------
|
|
753
685
|
// Main handler factory
|
|
@@ -756,169 +688,7 @@ function buildProxyHeaders(headers) {
|
|
|
756
688
|
function createLocalApiHandler({ queuePath }) {
|
|
757
689
|
const qp = queuePath || resolveQueuePath();
|
|
758
690
|
|
|
759
|
-
// Server-side cookie relay: captures auth cookies from InsForge cloud responses
|
|
760
|
-
// so that both browser and WKWebView share the same login session via the proxy.
|
|
761
|
-
// Persisted to disk so cookies survive server restarts.
|
|
762
|
-
const csrfRelayCookieName = "insforge_csrf_token";
|
|
763
|
-
let relayCookies = new Map();
|
|
764
691
|
const localAuthToken = crypto.randomBytes(24).toString("hex");
|
|
765
|
-
const trackerDataDir = path.join(os.homedir(), ".tokentracker", "tracker");
|
|
766
|
-
const cookiePath = path.join(trackerDataDir, "relay-cookies.json");
|
|
767
|
-
|
|
768
|
-
// Load persisted cookies on startup
|
|
769
|
-
try {
|
|
770
|
-
if (!fs.existsSync(trackerDataDir)) fs.mkdirSync(trackerDataDir, { recursive: true });
|
|
771
|
-
if (fs.existsSync(cookiePath)) {
|
|
772
|
-
const content = fs.readFileSync(cookiePath, "utf8");
|
|
773
|
-
const saved = JSON.parse(content);
|
|
774
|
-
if (saved && typeof saved === "object") {
|
|
775
|
-
let count = 0;
|
|
776
|
-
for (const [k, v] of Object.entries(saved)) {
|
|
777
|
-
relayCookies.set(k, v);
|
|
778
|
-
count++;
|
|
779
|
-
}
|
|
780
|
-
if (count > 0) console.log(`[LocalAPI] Loaded ${count} relay cookies from ${cookiePath}`);
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
} catch (e) {
|
|
784
|
-
console.error("[LocalAPI] Failed to load relay cookies:", e.message);
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
function persistRelayCookies() {
|
|
788
|
-
try {
|
|
789
|
-
// Sticky semantics: never replace an existing on-disk session with an empty cookie map.
|
|
790
|
-
if (relayCookies.size === 0) return;
|
|
791
|
-
|
|
792
|
-
const json = JSON.stringify(Object.fromEntries(relayCookies));
|
|
793
|
-
fs.writeFileSync(cookiePath, json, { encoding: "utf8", mode: 0o600 });
|
|
794
|
-
} catch (e) {
|
|
795
|
-
console.error("[LocalAPI] Failed to persist relay cookies:", e.message);
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
function clearRelayCookies(reason) {
|
|
800
|
-
if (relayCookies.size === 0) return;
|
|
801
|
-
relayCookies.clear();
|
|
802
|
-
try {
|
|
803
|
-
if (fs.existsSync(cookiePath)) fs.unlinkSync(cookiePath);
|
|
804
|
-
} catch (e) {
|
|
805
|
-
console.error("[LocalAPI] Failed to clear relay cookies:", e.message);
|
|
806
|
-
return;
|
|
807
|
-
}
|
|
808
|
-
if (reason) console.warn(`[LocalAPI] Cleared relay cookies: ${reason}`);
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
function captureSetCookies(headerValue) {
|
|
812
|
-
if (!headerValue) return;
|
|
813
|
-
const parts = headerValue.split(/,(?=\s*\w+=)/);
|
|
814
|
-
let changed = false;
|
|
815
|
-
for (const raw of parts) {
|
|
816
|
-
const eqIdx = raw.indexOf("=");
|
|
817
|
-
if (eqIdx < 1) continue;
|
|
818
|
-
const name = raw.substring(0, eqIdx).trim();
|
|
819
|
-
if (!name) continue;
|
|
820
|
-
|
|
821
|
-
// Basic sticky logic: if it's a deletion cookie (Max-Age=0 or past date),
|
|
822
|
-
// we only remove it if we have it.
|
|
823
|
-
const lower = raw.toLowerCase();
|
|
824
|
-
const isDeletion = lower.includes("max-age=0") || lower.includes("expires=thu, 01 jan 1970");
|
|
825
|
-
|
|
826
|
-
if (isDeletion) {
|
|
827
|
-
if (relayCookies.has(name)) {
|
|
828
|
-
relayCookies.delete(name);
|
|
829
|
-
changed = true;
|
|
830
|
-
console.log(`[LocalAPI] Cookie deleted: ${name}`);
|
|
831
|
-
}
|
|
832
|
-
} else {
|
|
833
|
-
const oldVal = relayCookies.get(name);
|
|
834
|
-
if (oldVal !== raw.trim()) {
|
|
835
|
-
relayCookies.set(name, raw.trim());
|
|
836
|
-
changed = true;
|
|
837
|
-
console.log(`[LocalAPI] Cookie captured: ${name}`);
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
if (changed) persistRelayCookies();
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
function getRelayCookieValue(name, { decode = false } = {}) {
|
|
845
|
-
const raw = relayCookies.get(name);
|
|
846
|
-
if (!raw || typeof raw !== "string") return "";
|
|
847
|
-
const pair = raw.split(";")[0] || "";
|
|
848
|
-
const eqIdx = pair.indexOf("=");
|
|
849
|
-
if (eqIdx < 1) return "";
|
|
850
|
-
const value = pair.substring(eqIdx + 1).trim();
|
|
851
|
-
if (!decode) return value;
|
|
852
|
-
try {
|
|
853
|
-
return decodeURIComponent(value);
|
|
854
|
-
} catch {
|
|
855
|
-
return value;
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
function captureAuthTokensFromBody(bodyBuffer, contentType) {
|
|
860
|
-
if (!bodyBuffer || !String(contentType || "").toLowerCase().includes("application/json")) return;
|
|
861
|
-
let parsed = null;
|
|
862
|
-
try {
|
|
863
|
-
parsed = JSON.parse(bodyBuffer.toString("utf8"));
|
|
864
|
-
} catch {
|
|
865
|
-
return;
|
|
866
|
-
}
|
|
867
|
-
let changed = false;
|
|
868
|
-
const token = typeof parsed?.csrfToken === "string" ? parsed.csrfToken.trim() : "";
|
|
869
|
-
if (token) {
|
|
870
|
-
const cookie = `${csrfRelayCookieName}=${encodeURIComponent(token)}; Path=/; SameSite=Lax`;
|
|
871
|
-
if (relayCookies.get(csrfRelayCookieName) !== cookie) {
|
|
872
|
-
relayCookies.set(csrfRelayCookieName, cookie);
|
|
873
|
-
changed = true;
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
const refreshToken = typeof parsed?.refreshToken === "string" ? parsed.refreshToken.trim() : "";
|
|
877
|
-
if (refreshToken) {
|
|
878
|
-
const cookie = `insforge_refresh_token=${encodeURIComponent(refreshToken)}; Path=/; HttpOnly; SameSite=Lax`;
|
|
879
|
-
if (relayCookies.get("insforge_refresh_token") !== cookie) {
|
|
880
|
-
relayCookies.set("insforge_refresh_token", cookie);
|
|
881
|
-
changed = true;
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
if (changed) persistRelayCookies();
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
function normalizeCookieHeader(value) {
|
|
888
|
-
if (Array.isArray(value)) return value.filter(Boolean).join("; ");
|
|
889
|
-
return typeof value === "string" ? value : "";
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
function buildRelayCookieHeader(clientCookieHeader, { relayPrecedenceNames = [] } = {}) {
|
|
893
|
-
const normalizedClientCookieHeader = normalizeCookieHeader(clientCookieHeader);
|
|
894
|
-
if (relayCookies.size === 0) return normalizedClientCookieHeader;
|
|
895
|
-
const relayPrecedence = new Set(relayPrecedenceNames);
|
|
896
|
-
const clientPairs = new Map();
|
|
897
|
-
if (normalizedClientCookieHeader) {
|
|
898
|
-
for (const part of normalizedClientCookieHeader.split(";")) {
|
|
899
|
-
const eqIdx = part.indexOf("=");
|
|
900
|
-
if (eqIdx < 1) continue;
|
|
901
|
-
const n = part.substring(0, eqIdx).trim();
|
|
902
|
-
if (n) clientPairs.set(n, part.trim());
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
// Merge relay cookies. Normal requests keep client precedence; refresh
|
|
906
|
-
// recovery can opt relay cookies into precedence over stale WebView cookies.
|
|
907
|
-
for (const [name, raw] of relayCookies) {
|
|
908
|
-
if (clientPairs.has(name) && !relayPrecedence.has(name)) continue;
|
|
909
|
-
const scIdx = raw.indexOf(";");
|
|
910
|
-
const pair = scIdx > 0 ? raw.substring(0, scIdx).trim() : raw;
|
|
911
|
-
clientPairs.set(name, pair);
|
|
912
|
-
}
|
|
913
|
-
return [...clientPairs.values()].join("; ");
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
// Ephemeral auth bridge: WebView sets a "native" flag before opening system browser
|
|
917
|
-
// for OAuth. The callback page (in browser) checks this flag to decide whether to
|
|
918
|
-
// relay the code back to the app or handle it as a normal web flow.
|
|
919
|
-
let _nativeAuthPending = false;
|
|
920
|
-
let _nativeAuthExpiry = 0;
|
|
921
|
-
|
|
922
692
|
function isAuthorizedLocalMutation(req) {
|
|
923
693
|
const headerToken = req?.headers?.["x-tokentracker-local-auth"];
|
|
924
694
|
const cookieToken = parseCookieHeader(req?.headers?.cookie).get("tokentracker_local_auth");
|
|
@@ -945,124 +715,6 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
945
715
|
return true;
|
|
946
716
|
}
|
|
947
717
|
|
|
948
|
-
// --- Auth bridge: native OAuth flag (WebView ↔ system browser) ---
|
|
949
|
-
if (p === "/api/auth-bridge/verifier") {
|
|
950
|
-
const method = String(req.method || "GET").toUpperCase();
|
|
951
|
-
if (method === "PUT" || method === "POST") {
|
|
952
|
-
if (!isAuthorizedLocalMutation(req)) {
|
|
953
|
-
json(res, { error: "Unauthorized" }, 401);
|
|
954
|
-
return true;
|
|
955
|
-
}
|
|
956
|
-
const body = await readJsonBody(req);
|
|
957
|
-
_nativeAuthPending = Boolean(body?.native);
|
|
958
|
-
_nativeAuthExpiry = Date.now() + 5 * 60 * 1000; // 5 min TTL
|
|
959
|
-
json(res, { ok: true });
|
|
960
|
-
return true;
|
|
961
|
-
}
|
|
962
|
-
if (method === "GET") {
|
|
963
|
-
const isNative = _nativeAuthPending && Date.now() < _nativeAuthExpiry;
|
|
964
|
-
_nativeAuthPending = false; // one-time read
|
|
965
|
-
_nativeAuthExpiry = 0;
|
|
966
|
-
json(res, { native: isNative });
|
|
967
|
-
return true;
|
|
968
|
-
}
|
|
969
|
-
json(res, { error: "Method Not Allowed" }, 405);
|
|
970
|
-
return true;
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
// --- auth proxy: forward /api/auth/* to InsForge cloud ---
|
|
974
|
-
if (p.startsWith("/api/auth/")) {
|
|
975
|
-
const runtime = resolveRuntimeConfig();
|
|
976
|
-
const insforgeBase = runtime.baseUrl || DEFAULT_BASE_URL;
|
|
977
|
-
try {
|
|
978
|
-
const targetUrl = `${insforgeBase.replace(/\/$/, "")}${p}${url.search || ""}`;
|
|
979
|
-
const proxyHeaders = buildProxyHeaders(req.headers);
|
|
980
|
-
const hasClientCookie = normalizeCookieHeader(proxyHeaders["cookie"]).trim().length > 0;
|
|
981
|
-
const hasCsrfHeader = typeof proxyHeaders["x-csrf-token"] === "string" && proxyHeaders["x-csrf-token"].trim().length > 0;
|
|
982
|
-
const relayCsrfToken = getRelayCookieValue(csrfRelayCookieName);
|
|
983
|
-
if (p === "/api/auth/refresh" && relayCsrfToken) {
|
|
984
|
-
proxyHeaders["x-csrf-token"] = relayCsrfToken;
|
|
985
|
-
}
|
|
986
|
-
const hasEffectiveCsrfHeader =
|
|
987
|
-
hasCsrfHeader ||
|
|
988
|
-
(typeof proxyHeaders["x-csrf-token"] === "string" && proxyHeaders["x-csrf-token"].trim().length > 0);
|
|
989
|
-
let shouldInjectRelayCookies =
|
|
990
|
-
p !== "/api/auth/refresh" || hasClientCookie || hasEffectiveCsrfHeader;
|
|
991
|
-
const relayRefreshToken = getRelayCookieValue("insforge_refresh_token", { decode: true });
|
|
992
|
-
const shouldUseRelayRefreshFallback =
|
|
993
|
-
p === "/api/auth/refresh" && !hasClientCookie && !hasEffectiveCsrfHeader && relayRefreshToken;
|
|
994
|
-
if (shouldUseRelayRefreshFallback) {
|
|
995
|
-
shouldInjectRelayCookies = false;
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
// Inject relay cookies so WebView benefits from browser's login session.
|
|
999
|
-
// Refresh requests need either a browser cookie or an explicit CSRF token;
|
|
1000
|
-
// otherwise replaying a stale persisted refresh cookie just manufactures
|
|
1001
|
-
// Invalid CSRF errors on startup.
|
|
1002
|
-
const originalCookieHeader = normalizeCookieHeader(proxyHeaders["cookie"]);
|
|
1003
|
-
const mergedCookie = shouldInjectRelayCookies
|
|
1004
|
-
? buildRelayCookieHeader(originalCookieHeader, {
|
|
1005
|
-
relayPrecedenceNames: p === "/api/auth/refresh"
|
|
1006
|
-
? [csrfRelayCookieName, "insforge_refresh_token"]
|
|
1007
|
-
: [],
|
|
1008
|
-
})
|
|
1009
|
-
: originalCookieHeader;
|
|
1010
|
-
const injectedRelayCookies =
|
|
1011
|
-
shouldInjectRelayCookies && relayCookies.size > 0 && mergedCookie !== originalCookieHeader;
|
|
1012
|
-
if (mergedCookie) proxyHeaders["cookie"] = mergedCookie;
|
|
1013
|
-
|
|
1014
|
-
const bodyChunks = [];
|
|
1015
|
-
for await (const chunk of req) bodyChunks.push(chunk);
|
|
1016
|
-
let proxyBody = bodyChunks.length > 0 ? Buffer.concat(bodyChunks) : undefined;
|
|
1017
|
-
let effectiveTargetUrl = targetUrl;
|
|
1018
|
-
if (shouldUseRelayRefreshFallback) {
|
|
1019
|
-
effectiveTargetUrl = `${insforgeBase.replace(/\/$/, "")}/api/auth/refresh?client_type=mobile`;
|
|
1020
|
-
proxyHeaders["content-type"] = "application/json";
|
|
1021
|
-
delete proxyHeaders["content-length"];
|
|
1022
|
-
proxyBody = Buffer.from(JSON.stringify({ refresh_token: relayRefreshToken }), "utf8");
|
|
1023
|
-
}
|
|
1024
|
-
const proxyRes = await fetch(effectiveTargetUrl, {
|
|
1025
|
-
method: req.method || "GET",
|
|
1026
|
-
headers: proxyHeaders,
|
|
1027
|
-
body: proxyBody,
|
|
1028
|
-
credentials: "include",
|
|
1029
|
-
redirect: "manual",
|
|
1030
|
-
});
|
|
1031
|
-
const responseHeaders = [...proxyRes.headers.entries()]
|
|
1032
|
-
.filter(([k]) => !["transfer-encoding", "connection"].includes(k.toLowerCase()))
|
|
1033
|
-
.map(([k, v]) => {
|
|
1034
|
-
if (k.toLowerCase() === "set-cookie") {
|
|
1035
|
-
const rewritten = v.replace(/;\s*[Dd]omain=[^;]*/g, "; Domain=localhost");
|
|
1036
|
-
captureSetCookies(rewritten);
|
|
1037
|
-
return [k, rewritten];
|
|
1038
|
-
}
|
|
1039
|
-
return [k, v];
|
|
1040
|
-
});
|
|
1041
|
-
res.writeHead(proxyRes.status, Object.fromEntries(responseHeaders));
|
|
1042
|
-
const resBody = Buffer.from(await proxyRes.arrayBuffer());
|
|
1043
|
-
if (proxyRes.status >= 200 && proxyRes.status < 300) {
|
|
1044
|
-
if (p === "/api/auth/logout") {
|
|
1045
|
-
clearRelayCookies("sign out");
|
|
1046
|
-
} else {
|
|
1047
|
-
captureAuthTokensFromBody(resBody, proxyRes.headers.get("content-type"));
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
if (
|
|
1051
|
-
p === "/api/auth/refresh"
|
|
1052
|
-
&& proxyRes.status === 403
|
|
1053
|
-
&& injectedRelayCookies
|
|
1054
|
-
&& !hasClientCookie
|
|
1055
|
-
&& /invalid csrf token/i.test(resBody.toString("utf8"))
|
|
1056
|
-
) {
|
|
1057
|
-
clearRelayCookies("stale refresh cookie without local CSRF context");
|
|
1058
|
-
}
|
|
1059
|
-
res.end(resBody);
|
|
1060
|
-
} catch (e) {
|
|
1061
|
-
json(res, { error: `Auth proxy error: ${e?.message || e}` }, 502);
|
|
1062
|
-
}
|
|
1063
|
-
return true;
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
718
|
// --- ip-check proxy: reverse-proxy ip.net.coffee (issue #81) ---
|
|
1067
719
|
// Lock-down: GET/HEAD only, restricted path prefixes, do not forward
|
|
1068
720
|
// browser credentials or fingerprintable headers. Without these limits
|
|
@@ -1101,7 +753,7 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
1101
753
|
accept: req.headers["accept"] || "*/*",
|
|
1102
754
|
"accept-language": req.headers["accept-language"] || "en",
|
|
1103
755
|
"accept-encoding": req.headers["accept-encoding"] || "gzip",
|
|
1104
|
-
"user-agent": "TokenTracker/IPCheck
|
|
756
|
+
"user-agent": "TokenTracker/IPCheck",
|
|
1105
757
|
referer: `${IP_CHECK_TARGET}${targetPath}`,
|
|
1106
758
|
};
|
|
1107
759
|
|
|
@@ -1207,7 +859,7 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
1207
859
|
headers: {
|
|
1208
860
|
accept: req.headers["accept"] || "image/*",
|
|
1209
861
|
"accept-language": req.headers["accept-language"] || "en",
|
|
1210
|
-
"user-agent": "TokenTracker/AvatarProxy
|
|
862
|
+
"user-agent": "TokenTracker/AvatarProxy",
|
|
1211
863
|
},
|
|
1212
864
|
});
|
|
1213
865
|
if (!upstream.ok) {
|
|
@@ -1257,19 +909,10 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
1257
909
|
} catch {
|
|
1258
910
|
body = {};
|
|
1259
911
|
}
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
}
|
|
1264
|
-
if (body.insforgeBaseUrl != null) {
|
|
1265
|
-
const allowedBaseUrl = resolveAllowedInsforgeBaseUrl(body.insforgeBaseUrl);
|
|
1266
|
-
if (!allowedBaseUrl) {
|
|
1267
|
-
json(res, { ok: false, error: "Unsupported insforgeBaseUrl override" }, 400);
|
|
1268
|
-
return true;
|
|
1269
|
-
}
|
|
1270
|
-
extraEnv.TOKENTRACKER_INSFORGE_BASE_URL = allowedBaseUrl;
|
|
1271
|
-
}
|
|
1272
|
-
const result = await runSyncCommand(extraEnv);
|
|
912
|
+
// Local-only: the sync command takes no cloud credentials or endpoint
|
|
913
|
+
// overrides any more, so the request body carries nothing to forward.
|
|
914
|
+
void body;
|
|
915
|
+
const result = await runSyncCommand({});
|
|
1273
916
|
try {
|
|
1274
917
|
const { resetUsageLimitsCache } = require("./usage-limits");
|
|
1275
918
|
resetUsageLimitsCache();
|
|
@@ -1876,7 +1519,6 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
1876
1519
|
|
|
1877
1520
|
module.exports = {
|
|
1878
1521
|
createLocalApiHandler,
|
|
1879
|
-
resolveAllowedInsforgeBaseUrl,
|
|
1880
1522
|
resolveQueuePath,
|
|
1881
1523
|
// Exported for cross-consumer tests (pricing + native contract lock).
|
|
1882
1524
|
MODEL_PRICING,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"note": "Curated price overrides. Always wins over LiteLLM. Two reasons to live here: (1) self-defined alias names that LiteLLM will never carry (kiro-*, hy3-*, composer-*, kimi-for-coding, free-tier OpenRouter routes); (2) prices we want to pin even if LiteLLM has the model (e.g. cache_write fields LiteLLM often omits). Units: USD per million tokens. Edit this file to override pricing without redeploying.",
|
|
4
4
|
"units": "usd_per_million_tokens",
|
|
5
5
|
"deepseek_v4_pro_discount_expiry": "2026-05-31T15:59:00Z — DeepSeek v4-pro is currently at a 75% promotional discount. After expiry the prices revert to 4x: input $1.74/M, output $3.48/M, cache_read $0.0145/M, cache_write $1.74/M. Update this file before the cutover.",
|
|
6
|
-
"sonnet5_intro_price_expiry": "2026-08-31 — Sonnet 5 intro price 2/10 reverts to 3/15 sticker. The src pricing path auto-tracks LiteLLM (no edit needed)
|
|
6
|
+
"sonnet5_intro_price_expiry": "2026-08-31 — Sonnet 5 intro price 2/10 reverts to 3/15 sticker. The src pricing path auto-tracks LiteLLM (no edit needed); re-vendor the seed after the cutover. See issue #16."
|
|
7
7
|
},
|
|
8
8
|
"exact": {
|
|
9
9
|
"claude-fable-5": { "input": 10, "output": 50, "cache_read": 1, "cache_write": 12.5, "note": "Pinned from Anthropic public pricing until bundled LiteLLM seed carries Fable 5." },
|