tokentracker-cli 1.2.1 → 1.3.3

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.
Files changed (161) hide show
  1. package/README.md +2 -0
  2. package/README.zh-CN.md +2 -0
  3. package/dashboard/dist/assets/AchievementBadge-BaqdyZaV.js +1 -0
  4. package/dashboard/dist/assets/AchievementsPage-B4Ju0guv.js +2 -0
  5. package/dashboard/dist/assets/{AchievementsSection-DoYMz2Rr.js → AchievementsSection-CDUUEfIi.js} +1 -1
  6. package/dashboard/dist/assets/BadgeDetailModal-B-XMJAvL.js +1 -0
  7. package/dashboard/dist/assets/{BotAnimated-C21PdYSf.js → BotAnimated-Dx_vvTtC.js} +1 -1
  8. package/dashboard/dist/assets/Card-D-5mABVt.js +1 -0
  9. package/dashboard/dist/assets/CloudCheckoutPage-BELoh56h.js +1 -0
  10. package/dashboard/dist/assets/CloudPage-CfBruNxe.js +1 -0
  11. package/dashboard/dist/assets/CloudUsageExport-BoFU7xCT.js +4 -0
  12. package/dashboard/dist/assets/CommandPalette-BmmKZWn-.js +1 -0
  13. package/dashboard/dist/assets/CommunityStatsModal-BF77zVhN.js +1 -0
  14. package/dashboard/dist/assets/{ConfirmModal-zUljpLNe.js → ConfirmModal-BQ-xB1S_.js} +1 -1
  15. package/dashboard/dist/assets/Controls-BnTeX7T4.js +1 -0
  16. package/dashboard/dist/assets/DashboardPage-BWulHa5J.js +1 -0
  17. package/dashboard/dist/assets/{DevicePage-CCIyAYH3.js → DevicePage-Dg6XkIaY.js} +1 -1
  18. package/dashboard/dist/assets/DialogClose-DAyilsH-.js +1 -0
  19. package/dashboard/dist/assets/DialogDescription-j4XnRy6y.js +1 -0
  20. package/dashboard/dist/assets/{DialogTitle-6q0jyME6.js → DialogTitle-B1iNGD2S.js} +1 -1
  21. package/dashboard/dist/assets/{FadeIn-BUm1z22V.js → FadeIn-ChVf45WW.js} +1 -1
  22. package/dashboard/dist/assets/{HeaderGithubStar-CxdmloMy.js → HeaderGithubStar-B740_G7a.js} +1 -1
  23. package/dashboard/dist/assets/{HoverTooltip-CrMdGYZj.js → HoverTooltip-BzP5FudR.js} +1 -1
  24. package/dashboard/dist/assets/{IpCheckPage-w6UUK_1K.js → IpCheckPage-BuZH3F14.js} +1 -1
  25. package/dashboard/dist/assets/LandingPage-Dtgq_caz.js +2 -0
  26. package/dashboard/dist/assets/LeaderboardPage-Di1kEg2O.js +2 -0
  27. package/dashboard/dist/assets/LeaderboardProBadge-h8FfR0au.js +1 -0
  28. package/dashboard/dist/assets/LeaderboardProfileModal-OUZvO6u-.js +73 -0
  29. package/dashboard/dist/assets/{LeaderboardProfilePage-Fljho5RP.js → LeaderboardProfilePage-bHI1oTEK.js} +2 -2
  30. package/dashboard/dist/assets/{LimitsPage-Do_uJqdQ.js → LimitsPage-DKojB73_.js} +2 -2
  31. package/dashboard/dist/assets/{LocalOnlyNotice-RIj2XT_X.js → LocalOnlyNotice-BaEq6qN7.js} +1 -1
  32. package/dashboard/dist/assets/LoginCard-Dn5Xy4If.js +1 -0
  33. package/dashboard/dist/assets/LoginModal-C5h-5GOA.js +1 -0
  34. package/dashboard/dist/assets/LoginPage-BpX8Tgjj.js +1 -0
  35. package/dashboard/dist/assets/PetPage-BPBsUHyL.js +1 -0
  36. package/dashboard/dist/assets/PopoverPopup-CVTiEVHs.js +1 -0
  37. package/dashboard/dist/assets/{ProviderIcon-BSI-1Cy8.js → ProviderIcon-BshO2uER.js} +1 -1
  38. package/dashboard/dist/assets/ResetPasswordPage-RTFTD8Dr.js +1 -0
  39. package/dashboard/dist/assets/{Select-B6CM0ico.js → Select-CtTceC4T.js} +1 -1
  40. package/dashboard/dist/assets/SelectItemText-C7xi8iGp.js +1 -0
  41. package/dashboard/dist/assets/SelfHostPage-BlwTt8XM.js +1 -0
  42. package/dashboard/dist/assets/{ServiceStatusPage-B4lIs2fc.js → ServiceStatusPage-BkIcJpYK.js} +1 -1
  43. package/dashboard/dist/assets/SessionsPage-okHobk_L.js +3 -0
  44. package/dashboard/dist/assets/SettingsPage-C-cc6wcF.js +2 -0
  45. package/dashboard/dist/assets/SkillsPage-D2LlKKJN.js +1 -0
  46. package/dashboard/dist/assets/{TokenGalaxy-Bk3kGSGi.js → TokenGalaxy-DiXUpXKF.js} +2 -2
  47. package/dashboard/dist/assets/TrendMonitor-C88S-5aa.js +19 -0
  48. package/dashboard/dist/assets/WidgetsPage-WsEFPNwp.js +1 -0
  49. package/dashboard/dist/assets/{WrappedPage-DHRmP4Ey.js → WrappedPage-ClEo-RmR.js} +1 -1
  50. package/dashboard/dist/assets/__vite-browser-external-DFRX9dp-.css +1 -0
  51. package/dashboard/dist/assets/__vite-browser-external-DUJeOWgd.js +45 -0
  52. package/dashboard/dist/assets/agent-logos-BM9jlReI.js +1 -0
  53. package/dashboard/dist/assets/arrow-left-BEM-HVmC.js +1 -0
  54. package/dashboard/dist/assets/arrow-right-CwBz5dwb.js +1 -0
  55. package/dashboard/dist/assets/{arrow-up-right-c39fdiDQ.js → arrow-up-right-jPmRN562.js} +1 -1
  56. package/dashboard/dist/assets/{check-8LQ7IvkH.js → check-CKwHc4Hv.js} +1 -1
  57. package/dashboard/dist/assets/cloud-checkout-nYNHDFoR.js +1 -0
  58. package/dashboard/dist/assets/composite-B4Z3F65o.js +1 -0
  59. package/dashboard/dist/assets/{download-x3Gmv-wu.js → download-CJUnX1Bs.js} +1 -1
  60. package/dashboard/dist/assets/{external-link-daw-XTjZ.js → external-link-B5yXxdPy.js} +1 -1
  61. package/dashboard/dist/assets/{flame-B9o0_EF8.js → flame-CKjAluj_.js} +1 -1
  62. package/dashboard/dist/assets/{format-tokens-o9atQGtV.js → format-tokens-mk49THtv.js} +1 -1
  63. package/dashboard/dist/assets/{host-mode-GPY2DxE4.js → host-mode-efKlSYjX.js} +1 -1
  64. package/dashboard/dist/assets/{icons-CmM2z-Jt.js → icons-ZB1ZthjH.js} +1 -1
  65. package/dashboard/dist/assets/{index-CPcJP0uH.js → index-BImBG3eS.js} +1 -1
  66. package/dashboard/dist/assets/{index-CtQOew7C.js → index-BgdM7kBl.js} +1 -1
  67. package/dashboard/dist/assets/{index-Bfgblk6o.js → index-DweUMAUD.js} +1 -1
  68. package/dashboard/dist/assets/{info-BaK8Hoi2.js → info-CyDY7NYM.js} +1 -1
  69. package/dashboard/dist/assets/{link-2-CV3uvMH3.js → link-2-Dn2soiyd.js} +1 -1
  70. package/dashboard/dist/assets/{loader-circle-DYpa-xTZ.js → loader-circle-CNT630zn.js} +1 -1
  71. package/dashboard/dist/assets/{modifiers.esm-Ds4Uh7JO.js → modifiers.esm-WPx0bc2K.js} +1 -1
  72. package/dashboard/dist/assets/{monitor-smartphone-BbeYHvY-.js → monitor-smartphone-C-qC0RAt.js} +1 -1
  73. package/dashboard/dist/assets/os-DMmhX9tk.js +1 -0
  74. package/dashboard/dist/assets/{plus-C9qGVLFv.js → plus-Cx82jb7o.js} +1 -1
  75. package/dashboard/dist/assets/{provider-display-BWYNyitK.js → provider-display-CodMmiRb.js} +2 -2
  76. package/dashboard/dist/assets/{react-CO1HovF5.js → react-B7SkZTgg.js} +1 -1
  77. package/dashboard/dist/assets/safePolygon-CKpREa4K.js +1 -0
  78. package/dashboard/dist/assets/{search-DBnhkzG_.js → search-Ol3OZBm4.js} +1 -1
  79. package/dashboard/dist/assets/server-DFCbznC7.js +1 -0
  80. package/dashboard/dist/assets/shield-check-YcmLwVZ6.js +1 -0
  81. package/dashboard/dist/assets/skills-api-C-9vK884.js +1 -0
  82. package/dashboard/dist/assets/{sortable.esm-SVl6OixH.js → sortable.esm-CKC9NuR-.js} +1 -1
  83. package/dashboard/dist/assets/store-C9yWYHfg.js +1 -0
  84. package/dashboard/dist/assets/{terminal-kUMdPZhq.js → terminal-B3_fCLFs.js} +1 -1
  85. package/dashboard/dist/assets/{trash-Dd_vbrYo.js → trash-CZGfgbFa.js} +1 -1
  86. package/dashboard/dist/assets/{use-community-stats-Bmlmv4H-.js → use-community-stats-CW-qkYn7.js} +1 -1
  87. package/dashboard/dist/assets/{use-latest-request-guard-CqZwzrCW.js → use-latest-request-guard-DHv7vfFE.js} +1 -1
  88. package/dashboard/dist/assets/{use-limits-display-prefs-CDC0MZRY.js → use-limits-display-prefs-NZ-AAYD5.js} +1 -1
  89. package/dashboard/dist/assets/use-native-settings-CWiLqfZh.js +1 -0
  90. package/dashboard/dist/assets/{use-ordered-list-DvBI7mUU.js → use-ordered-list-Df8pw-WJ.js} +1 -1
  91. package/dashboard/dist/assets/use-pet-settings-Bg2CDprr.js +2 -0
  92. package/dashboard/dist/assets/use-reduced-motion-iDZEUyua.js +1 -0
  93. package/dashboard/dist/assets/{use-transform-uSKYTYaW.js → use-transform-BHwEAfmO.js} +1 -1
  94. package/dashboard/dist/assets/use-usage-limits-CSxFFn3_.js +1 -0
  95. package/dashboard/dist/assets/{useCurrency-t1iy_pC3.js → useCurrency-pwClnCZ5.js} +1 -1
  96. package/dashboard/dist/assets/usePositioner-B0j1orGj.js +1 -0
  97. package/dashboard/dist/assets/useTokenFormat-BMvVAe6V.js +1 -0
  98. package/dashboard/dist/assets/{zap-DFM6AsYo.js → zap-DvX2aYx5.js} +1 -1
  99. package/dashboard/dist/index.html +2 -2
  100. package/dashboard/dist/ip-check.html +2 -2
  101. package/dashboard/dist/leaderboard.html +2 -2
  102. package/dashboard/dist/pricing.html +6 -5
  103. package/dashboard/dist/privacy.html +3 -3
  104. package/dashboard/dist/share.html +2 -2
  105. package/dashboard/dist/terms.html +6 -6
  106. package/package.json +6 -2
  107. package/src/commands/device-login.js +60 -33
  108. package/src/commands/init.js +26 -7
  109. package/src/commands/serve.js +3 -3
  110. package/src/commands/sync.js +43 -22
  111. package/src/lib/atomic-json.js +27 -0
  112. package/src/lib/bearer-token.js +11 -0
  113. package/src/lib/cloud-account.js +4 -1
  114. package/src/lib/local-api.js +282 -69
  115. package/src/lib/openclaw-cli.js +104 -0
  116. package/src/lib/openclaw-hook.js +1 -46
  117. package/src/lib/openclaw-session-plugin.js +1 -59
  118. package/src/lib/pricing/seed-snapshot.json +1 -1
  119. package/src/lib/proxy-env.js +8 -3
  120. package/src/lib/rollout.js +15 -10
  121. package/src/lib/runtime-config.js +84 -14
  122. package/src/lib/skills-manager.js +8 -1
  123. package/src/lib/static-server.js +19 -4
  124. package/src/lib/upload-throttle.js +18 -4
  125. package/dashboard/dist/assets/AchievementBadge-DPlXNqoK.js +0 -1
  126. package/dashboard/dist/assets/AchievementsPage-B49wjdK_.js +0 -2
  127. package/dashboard/dist/assets/BadgeDetailModal-DUCS6K1n.js +0 -1
  128. package/dashboard/dist/assets/Card-BU2CqxfU.js +0 -1
  129. package/dashboard/dist/assets/CommandPalette-O3pOLyJo.js +0 -1
  130. package/dashboard/dist/assets/CommunityStatsModal-Bt82neQ7.js +0 -1
  131. package/dashboard/dist/assets/Controls-D56v-YJw.js +0 -1
  132. package/dashboard/dist/assets/DashboardPage-CKEuFzuc.js +0 -1
  133. package/dashboard/dist/assets/DialogClose-C-QK97qp.js +0 -1
  134. package/dashboard/dist/assets/DialogDescription-COXweCuc.js +0 -1
  135. package/dashboard/dist/assets/LandingPage-DLDE7wdR.js +0 -2
  136. package/dashboard/dist/assets/LeaderboardAvatar-BqD4dEvQ.js +0 -1
  137. package/dashboard/dist/assets/LeaderboardPage-kgHSryiT.js +0 -2
  138. package/dashboard/dist/assets/LeaderboardProfileModal-zLdeOiA3.js +0 -73
  139. package/dashboard/dist/assets/LoginCard-Dx6MmGjU.js +0 -1
  140. package/dashboard/dist/assets/LoginModal-BWaZVRnT.js +0 -1
  141. package/dashboard/dist/assets/LoginPage-BNkU-riS.js +0 -1
  142. package/dashboard/dist/assets/PetPage-CyEgU2Vm.js +0 -1
  143. package/dashboard/dist/assets/PopoverPopup-CmUIY8lF.js +0 -1
  144. package/dashboard/dist/assets/ResetPasswordPage-CUXKkLb4.js +0 -1
  145. package/dashboard/dist/assets/SelectItemText-CZ7puxuB.js +0 -1
  146. package/dashboard/dist/assets/SessionsPage-D99UfBgy.js +0 -3
  147. package/dashboard/dist/assets/SettingsPage-DT3rld91.js +0 -2
  148. package/dashboard/dist/assets/SkillsPage-Dv47kVCB.js +0 -1
  149. package/dashboard/dist/assets/TrendMonitor-mB1x3c_o.js +0 -19
  150. package/dashboard/dist/assets/WidgetsPage-BhUArowe.js +0 -1
  151. package/dashboard/dist/assets/__vite-browser-external-BW1rxvYq.js +0 -45
  152. package/dashboard/dist/assets/__vite-browser-external-gKkiQad5.css +0 -1
  153. package/dashboard/dist/assets/agent-logos-D7f7AMF-.js +0 -1
  154. package/dashboard/dist/assets/composite-BgLSXWKA.js +0 -1
  155. package/dashboard/dist/assets/skills-api-BSkbxC8-.js +0 -1
  156. package/dashboard/dist/assets/store-DrEEdO_u.js +0 -1
  157. package/dashboard/dist/assets/use-pet-settings-C_FrqySO.js +0 -2
  158. package/dashboard/dist/assets/use-reduced-motion-BEOENqBa.js +0 -1
  159. package/dashboard/dist/assets/use-session-efficiency-pref-D8cAb4A9.js +0 -1
  160. package/dashboard/dist/assets/use-usage-limits-DFgOKTtv.js +0 -1
  161. package/dashboard/dist/assets/useTokenFormat-0bPjtaaM.js +0 -1
@@ -6,30 +6,17 @@ const fs = require("node:fs/promises");
6
6
 
7
7
  const { readJson, updateJsonLocked } = require("../lib/fs");
8
8
  const { resolveTrackerPaths } = require("../lib/tracker-paths");
9
- const { resolveRuntimeConfig } = require("../lib/runtime-config");
9
+ const { resolveRuntimeConfig, assertInsforgeRuntime, normalizeInstanceBaseUrl, clearDeviceIdentity, resetInstanceState } = require("../lib/runtime-config");
10
10
  const { functionUrlFor, fetchFunctionResponse } = require("../lib/function-url");
11
11
 
12
12
  const POLL_INTERVAL_MS = 5_000;
13
13
  const ABSOLUTE_TIMEOUT_MS = 16 * 60 * 1000; // matches the 15-min server window with a small buffer
14
14
 
15
- function readBaseUrl(config) {
16
- return resolveRuntimeConfig({
17
- // Preserve device-login's explicit legacy override names and precedence,
18
- // while routing persisted config through the shared retired-host filter.
19
- cli: {
20
- baseUrl:
21
- process.env.TOKENTRACKER_BASE_URL ||
22
- process.env.TOKENTRACKER_API_URL,
23
- },
24
- config: config || {},
25
- env: {},
26
- }).baseUrl;
27
- }
28
-
29
- async function authorize({ baseUrl, clientInfo, machineId }) {
15
+ async function authorize({ baseUrl, clientInfo, machineId, anonKey, timeoutMs = 20_000 }) {
30
16
  const res = await fetchFunctionResponse(functionUrlFor(baseUrl, "tokentracker-device-flow-authorize"), {
31
17
  method: "POST",
32
- headers: { "Content-Type": "application/json" },
18
+ headers: { "Content-Type": "application/json", ...(anonKey ? { apikey: anonKey } : {}) },
19
+ ...(timeoutMs > 0 ? { signal: AbortSignal.timeout(timeoutMs) } : {}),
33
20
  body: JSON.stringify({
34
21
  client_info: clientInfo,
35
22
  // Machine-stable identity: the server anchors the issued device to this
@@ -46,10 +33,11 @@ async function authorize({ baseUrl, clientInfo, machineId }) {
46
33
  return res.json();
47
34
  }
48
35
 
49
- async function pollOnce({ baseUrl, deviceCode }) {
36
+ async function pollOnce({ baseUrl, deviceCode, anonKey, timeoutMs = 20_000 }) {
50
37
  const res = await fetchFunctionResponse(functionUrlFor(baseUrl, "tokentracker-device-flow-poll"), {
51
38
  method: "POST",
52
- headers: { "Content-Type": "application/json" },
39
+ headers: { "Content-Type": "application/json", ...(anonKey ? { apikey: anonKey } : {}) },
40
+ ...(timeoutMs > 0 ? { signal: AbortSignal.timeout(timeoutMs) } : {}),
53
41
  body: JSON.stringify({ device_code: deviceCode }),
54
42
  });
55
43
  const data = await res.json().catch(() => ({}));
@@ -80,7 +68,12 @@ async function cmdDeviceLogin(argv = [], options = {}) {
80
68
  const { trackerDir } = await resolveTrackerPaths({ home });
81
69
  const configPath = path.join(trackerDir, "config.json");
82
70
  const config = (await readJson(configPath)) || {};
83
- const baseUrl = opts.baseUrl || readBaseUrl(config);
71
+ if (opts.anonKeyFile) opts.anonKey = (await fs.readFile(opts.anonKeyFile, "utf8")).trim();
72
+ const runtime = assertInsforgeRuntime(resolveRuntimeConfig({
73
+ cli: { baseUrl: opts.baseUrl || process.env.TOKENTRACKER_BASE_URL || process.env.TOKENTRACKER_API_URL,
74
+ anonKey: opts.anonKey, dashboardUrl: opts.dashboardUrl }, config, env: process.env,
75
+ }));
76
+ const baseUrl = runtime.baseUrl;
84
77
 
85
78
  const clientInfo = `${os.platform()}-${os.arch()} ${os.hostname()}`;
86
79
  // Same machineId the local API serves to the dashboard — both login paths
@@ -88,7 +81,30 @@ async function cmdDeviceLogin(argv = [], options = {}) {
88
81
  const { getOrCreateMachineId } = require("../lib/local-api");
89
82
  const machineId = getOrCreateMachineId(path.join(trackerDir, "queue.jsonl"));
90
83
  process.stdout.write(`Requesting device code from ${baseUrl}...\n`);
91
- const authResp = await authorize({ baseUrl, clientInfo, machineId });
84
+ const authResp = await authorize({ baseUrl, clientInfo, machineId, anonKey: runtime.anonKey, timeoutMs: runtime.httpTimeoutMs });
85
+ if (typeof authResp.verification_uri !== "string" || !authResp.verification_uri || !authResp.device_code) {
86
+ throw new Error("Device authorization returned an incomplete response");
87
+ }
88
+ const dashboard = new URL(runtime.dashboardUrl);
89
+ const expectedPath = dashboard.pathname.replace(/\/$/, "") + "/device";
90
+ for (const value of [authResp.verification_uri, authResp.verification_uri_complete].filter(Boolean)) {
91
+ const returned = new URL(value);
92
+ if (returned.origin !== dashboard.origin || returned.pathname !== expectedPath || returned.username || returned.password) {
93
+ throw new Error("Device authorization returned a different dashboard. Configure --dashboard-url for this instance.");
94
+ }
95
+ }
96
+ await updateJsonLocked(configPath, async current => {
97
+ const previous = resolveRuntimeConfig({ config, env: {} });
98
+ const latest = resolveRuntimeConfig({ config: current, env: {} });
99
+ if ((latest.baseUrl !== previous.baseUrl || latest.anonKey !== previous.anonKey) &&
100
+ (latest.baseUrl !== baseUrl || latest.anonKey !== runtime.anonKey)) {
101
+ throw new Error("Backend instance changed during device login");
102
+ }
103
+ const changedInstance = previous.baseUrl !== baseUrl || previous.anonKey !== runtime.anonKey;
104
+ if (changedInstance) resetInstanceState(trackerDir);
105
+ return { ...(changedInstance ? clearDeviceIdentity(current) : current), baseUrl,
106
+ anonKey: runtime.anonKey, dashboardUrl: runtime.dashboardUrl };
107
+ });
92
108
 
93
109
  if (opts.json) {
94
110
  process.stdout.write(JSON.stringify(authResp, null, 2) + "\n");
@@ -129,7 +145,7 @@ async function cmdDeviceLogin(argv = [], options = {}) {
129
145
  await sleepFn(wait);
130
146
  let result;
131
147
  try {
132
- result = await pollOnce({ baseUrl, deviceCode: authResp.device_code });
148
+ result = await pollOnce({ baseUrl, deviceCode: authResp.device_code, anonKey: runtime.anonKey, timeoutMs: runtime.httpTimeoutMs });
133
149
  consecutiveErrors = 0;
134
150
  } catch (e) {
135
151
  consecutiveErrors++;
@@ -140,16 +156,21 @@ async function cmdDeviceLogin(argv = [], options = {}) {
140
156
  if (!result.deviceToken) {
141
157
  throw new Error("device login approved but server did not return a device token");
142
158
  }
143
- await updateJsonLocked(configPath, async (current) => ({
144
- ...current,
145
- ...(machineId ? { machineId } : {}),
146
- baseUrl,
147
- user_id: result.user_id,
148
- deviceToken: result.deviceToken,
149
- deviceId: result.deviceId || current.deviceId || config.deviceId,
150
- device_login_at: new Date().toISOString(),
151
- }));
152
- process.stdout.write(`\n✓ Approved. device token written to ${configPath}\n`);
159
+ await updateJsonLocked(configPath, async (current) => {
160
+ if (normalizeInstanceBaseUrl(current.baseUrl) !== baseUrl || current.anonKey !== runtime.anonKey) {
161
+ throw new Error("Backend instance changed during device login");
162
+ }
163
+ return { ...current,
164
+ ...(machineId ? { machineId } : {}),
165
+ baseUrl,
166
+ user_id: result.user_id,
167
+ deviceToken: result.deviceToken,
168
+ deviceTokenBaseUrl: baseUrl,
169
+ deviceId: result.deviceId || current.deviceId || config.deviceId,
170
+ device_login_at: new Date().toISOString(),
171
+ };
172
+ });
173
+ process.stdout.write(`\nApproved. device token written to ${configPath}\n`);
153
174
  return;
154
175
  }
155
176
  if (result.status === "expired") {
@@ -164,12 +185,18 @@ async function cmdDeviceLogin(argv = [], options = {}) {
164
185
  }
165
186
 
166
187
  function parseArgs(argv) {
167
- const out = { json: false, baseUrl: null };
188
+ const out = { json: false, baseUrl: null, anonKey: null, anonKeyFile: null, dashboardUrl: null };
168
189
  for (let i = 0; i < argv.length; i++) {
169
190
  const a = argv[i];
170
191
  if (a === "--json") out.json = true;
171
192
  else if (a === "--base-url") {
172
193
  out.baseUrl = argv[++i] || null;
194
+ } else if (a === "--anon-key") {
195
+ out.anonKey = argv[++i] || null;
196
+ } else if (a === "--anon-key-file") {
197
+ out.anonKeyFile = argv[++i] || null;
198
+ } else if (a === "--dashboard-url") {
199
+ out.dashboardUrl = argv[++i] || null;
173
200
  } else throw new Error(`Unknown option: ${a}`);
174
201
  }
175
202
  return out;
@@ -79,7 +79,8 @@ const {
79
79
  resolveReasonixHome,
80
80
  resolveTraeStoragePath,
81
81
  } = require("../lib/rollout");
82
- const { resolveRuntimeConfig, DEFAULT_BASE_URL } = require("../lib/runtime-config");
82
+ const { resolveRuntimeConfig, DEFAULT_BASE_URL, normalizeInstanceBaseUrl, assertInsforgeRuntime,
83
+ clearDeviceIdentity, resetInstanceState } = require("../lib/runtime-config");
83
84
  const {
84
85
  BOLD,
85
86
  DIM,
@@ -159,9 +160,10 @@ const SUPPORTED_PROVIDERS = [
159
160
  "Command Code",
160
161
  ];
161
162
 
162
- async function cmdInit(argv) {
163
+ async function cmdInit(argv, options = {}) {
163
164
  const opts = parseArgs(argv);
164
- const home = os.homedir();
165
+ const home = options.home || os.homedir();
166
+ if (opts.anonKeyFile) opts.anonKey = (await fs.readFile(opts.anonKeyFile, "utf8")).trim();
165
167
 
166
168
  const { rootDir, trackerDir, binDir } = await resolveTrackerPaths({ home });
167
169
 
@@ -171,10 +173,11 @@ async function cmdInit(argv) {
171
173
 
172
174
  const existingConfig = await readJson(configPath);
173
175
  const runtime = resolveRuntimeConfig({
174
- cli: { baseUrl: opts.baseUrl, dashboardUrl: opts.dashboardUrl },
176
+ cli: { baseUrl: opts.baseUrl, dashboardUrl: opts.dashboardUrl, anonKey: opts.anonKey },
175
177
  config: existingConfig || {},
176
178
  env: process.env,
177
179
  });
180
+ assertInsforgeRuntime(runtime);
178
181
  const notifyPath = path.join(binDir, "notify.cjs");
179
182
  const appDir = path.join(trackerDir, "app");
180
183
  const trackerBinPath = path.join(appDir, "bin", "tracker.js");
@@ -369,16 +372,28 @@ async function runSetup({
369
372
  existingConfig && typeof existingConfig === "object" && !Array.isArray(existingConfig)
370
373
  ? existingConfig
371
374
  : {};
375
+ const previousRuntime = resolveRuntimeConfig({ config: existingPlainConfig, env: {} });
376
+ const changedInstance = normalizeInstanceBaseUrl(previousRuntime.baseUrl) !== runtime.baseUrl ||
377
+ previousRuntime.anonKey !== runtime.anonKey;
378
+ if (changedInstance) {
379
+ resetInstanceState(trackerDir);
380
+ deviceToken = null; deviceId = null;
381
+ }
372
382
  const config = {
373
- ...existingPlainConfig,
383
+ ...(changedInstance ? clearDeviceIdentity(existingPlainConfig) : existingPlainConfig),
374
384
  installedAt,
375
385
  // Keep a persisted legacy URL until the first sync. sync owns the migration
376
386
  // lock and must reset the upload offset/backoff before removing this marker;
377
387
  // rewriting it here would skip the historical replay permanently.
378
388
  baseUrl: opts.baseUrl || existingPlainConfig.baseUrl || DEFAULT_BASE_URL,
379
389
  };
380
- if (opts.dashboardUrl) {
381
- config.dashboardUrl = opts.dashboardUrl;
390
+ if (changedInstance || opts.dashboardUrl) {
391
+ config.dashboardUrl = runtime.dashboardUrl;
392
+ }
393
+ if (changedInstance || opts.anonKey || runtime.sources.anonKey === "env") config.anonKey = runtime.anonKey;
394
+ if (runtime.baseUrl !== DEFAULT_BASE_URL) {
395
+ config.baseUrl = runtime.baseUrl;
396
+ config.dashboardUrl = runtime.dashboardUrl;
382
397
  }
383
398
 
384
399
  await writeJson(configPath, config);
@@ -1333,6 +1348,8 @@ function parseArgs(argv) {
1333
1348
  const out = {
1334
1349
  baseUrl: null,
1335
1350
  dashboardUrl: null,
1351
+ anonKey: null,
1352
+ anonKeyFile: null,
1336
1353
  email: null,
1337
1354
  password: null,
1338
1355
  deviceName: null,
@@ -1347,6 +1364,8 @@ function parseArgs(argv) {
1347
1364
  const a = argv[i];
1348
1365
  if (a === "--base-url") out.baseUrl = argv[++i] || null;
1349
1366
  else if (a === "--dashboard-url") out.dashboardUrl = argv[++i] || null;
1367
+ else if (a === "--anon-key") out.anonKey = argv[++i] || null;
1368
+ else if (a === "--anon-key-file") out.anonKeyFile = argv[++i] || null;
1350
1369
  else if (a === "--email") out.email = argv[++i] || null;
1351
1370
  else if (a === "--password") out.password = argv[++i] || null;
1352
1371
  else if (a === "--device-name") out.deviceName = argv[++i] || null;
@@ -159,7 +159,7 @@ async function cmdServe(argv) {
159
159
  const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
160
160
 
161
161
  // CORS preflight
162
- if (req.method === "OPTIONS") {
162
+ if (req.method === "OPTIONS" && !url.pathname.startsWith("/api/auth/")) {
163
163
  res.writeHead(204, {
164
164
  "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
165
165
  "Access-Control-Allow-Headers": "Content-Type, Authorization",
@@ -179,12 +179,12 @@ async function cmdServe(argv) {
179
179
  }
180
180
 
181
181
  // Static files
182
- const served = await serveStaticFile(dashboardDir, url.pathname, res);
182
+ const served = await serveStaticFile(dashboardDir, url.pathname, res, { localRuntimeConfig: true });
183
183
  if (served) return;
184
184
 
185
185
  // SPA fallback
186
186
  if (shouldServeSpaFallback(req, url)) {
187
- await serveStaticFile(dashboardDir, "/index.html", res);
187
+ await serveStaticFile(dashboardDir, "/index.html", res, { localRuntimeConfig: true });
188
188
  return;
189
189
  }
190
190
 
@@ -156,7 +156,7 @@ const {
156
156
  parseRetryAfterMs,
157
157
  } = require("../lib/upload-throttle");
158
158
  const AUTO_UPLOAD_CONFIG = {
159
- intervalMs: 5 * 60_000,
159
+ intervalMs: 15 * 60_000,
160
160
  batchSize: 200,
161
161
  maxBatchesSmall: 5,
162
162
  maxBatchesLarge: 5,
@@ -507,6 +507,7 @@ async function clearSyncSkip(trackerDir) {
507
507
  }
508
508
 
509
509
  async function cmdSync(argv, context = {}) {
510
+ context = context && typeof context === "object" ? context : {};
510
511
  const opts = parseArgs(argv);
511
512
  const diagnostics = context && typeof context === "object" ? context.diagnostics : null;
512
513
  const cursorStoreOptions = context && typeof context === "object"
@@ -528,8 +529,8 @@ async function cmdSync(argv, context = {}) {
528
529
  ? context.cursorSyncDeps
529
530
  : {};
530
531
  const syncDiagnostics = diagnostics && typeof diagnostics === "object" ? diagnostics : null;
531
- const home = os.homedir();
532
- const { trackerDir } = await resolveTrackerPaths({ home });
532
+ const home = context.home || os.homedir();
533
+ const trackerDir = context.trackerDataDir || (await resolveTrackerPaths({ home })).trackerDir;
533
534
  // Manual CLI sync is a one-time upload request, without changing the toggle.
534
535
  // Hooks, native publication and detached retries must honor the saved opt-in.
535
536
  const requiresCloudSyncPref = opts.auto || opts.background || opts.fromRetry || opts.fromNotify || opts.fromOpenclaw ||
@@ -589,7 +590,8 @@ async function cmdSync(argv, context = {}) {
589
590
  // token unless the local API has already minted a replacement from the
590
591
  // user's current login session and passed it through the environment.
591
592
  // Ingest is a whole-row upsert per bucket key, making replay idempotent.
592
- if (config && isLegacyInsforgeBaseUrl(config.baseUrl)) {
593
+ if (config && isLegacyInsforgeBaseUrl(config.baseUrl) &&
594
+ !Object.hasOwn(process.env, "TOKENTRACKER_LOCAL_SYNC_DEVICE_TOKEN")) {
593
595
  const priorQueueState = (await readJson(queueStatePath)) || {};
594
596
  // Prepare the replay exactly once. The note survives successful batches,
595
597
  // so retryable failures retain both their committed offset and backoff
@@ -708,6 +710,7 @@ async function cmdSync(argv, context = {}) {
708
710
  const isBackgroundAllLocalSync = isBackgroundLightweightSync && opts.allLocalSources;
709
711
  const isFullSourceScan = !autoSourceScope && !isBackgroundLightweightSync;
710
712
  const sourceAllowed = (...sources) => {
713
+ if (Array.isArray(context.scanSources) && !sources.some(source => context.scanSources.includes(source))) return false;
711
714
  if (isBackgroundLightweightSync) {
712
715
  if (autoSourceScope) {
713
716
  return (
@@ -3373,7 +3376,7 @@ async function cmdSync(argv, context = {}) {
3373
3376
  }
3374
3377
  // An authenticated local API supplies this capability for this upload.
3375
3378
  // Keep a separately configured CLI account from overriding its owner.
3376
- const runtime = resolveRuntimeConfig({
3379
+ const resolvedRuntime = resolveRuntimeConfig({
3377
3380
  cli: {
3378
3381
  deviceToken: process.env.TOKENTRACKER_LOCAL_SYNC_DEVICE_TOKEN,
3379
3382
  ...(process.env.TOKENTRACKER_LOCAL_SYNC_DEVICE_TOKEN ? {
@@ -3385,6 +3388,12 @@ async function cmdSync(argv, context = {}) {
3385
3388
  env: process.env,
3386
3389
  });
3387
3390
 
3391
+ // Empty is an explicit local-server denial, so it cannot fall back to a
3392
+ // separately configured CLI account when the browser is signed out.
3393
+ const runtime = Object.hasOwn(process.env, "TOKENTRACKER_LOCAL_SYNC_DEVICE_TOKEN")
3394
+ ? { ...resolvedRuntime, deviceToken: resolvedRuntime.configurationError ? null : process.env.TOKENTRACKER_LOCAL_SYNC_DEVICE_TOKEN }
3395
+ : resolvedRuntime;
3396
+
3388
3397
  let uploadResult = { inserted: 0, skipped: 0 };
3389
3398
  let uploadAttempted = false;
3390
3399
  let autoUploadDecision = null;
@@ -3397,22 +3406,25 @@ async function cmdSync(argv, context = {}) {
3397
3406
  0,
3398
3407
  queueSizeBefore - Number(uploadStateBefore.offset || 0),
3399
3408
  );
3400
- // Native publication owns a five-minute timer. Drains and a pending
3401
- // backend migration also bypass the success interval, but all automatic
3402
- // producers must respect a failed upload's retry deadline.
3403
- const bypassSuccessInterval = opts.publishAccount || opts.drain || legacyBaseUrlMigration;
3409
+ // Automatic producers honor success and failure deadlines. A verified
3410
+ // manual recheck may probe changed membership; the server decides access.
3411
+ const bypassSuccessInterval = (!opts.auto && opts.drain) || legacyBaseUrlMigration;
3404
3412
  const lastSuccessMs = Number(uploadThrottleState.lastSuccessMs || 0);
3405
3413
  const successDeadline = lastSuccessMs > 0
3406
- ? Math.min(Number(uploadThrottleState.nextAllowedAtMs || 0),
3407
- lastSuccessMs + AUTO_UPLOAD_CONFIG.intervalMs + AUTO_UPLOAD_DEFAULTS.jitterMsMax)
3414
+ ? Math.max(Number(uploadThrottleState.serverNextAllowedAtMs || 0), Math.min(Number(uploadThrottleState.nextAllowedAtMs || 0),
3415
+ lastSuccessMs + AUTO_UPLOAD_CONFIG.intervalMs + AUTO_UPLOAD_DEFAULTS.jitterMsMax))
3408
3416
  : Number(uploadThrottleState.nextAllowedAtMs || 0);
3417
+ const recheckCloudAccess = opts.drain && process.env.TOKENTRACKER_CLOUD_RECHECK === "1" &&
3418
+ Boolean(process.env.TOKENTRACKER_LOCAL_SYNC_DEVICE_TOKEN?.trim());
3419
+ const storedBackoff = recheckCloudAccess ? 0 : Number(uploadThrottleState.backoffUntilMs || 0);
3409
3420
  autoUploadDecision = decideAutoUpload({
3410
3421
  nowMs: Date.now(),
3411
3422
  pendingBytes: pendingBytesBefore,
3412
3423
  state: {
3413
3424
  ...uploadThrottleState,
3414
- nextAllowedAtMs: bypassSuccessInterval
3415
- ? Number(uploadThrottleState.backoffUntilMs || 0)
3425
+ backoffUntilMs: storedBackoff,
3426
+ nextAllowedAtMs: recheckCloudAccess ? 0 : bypassSuccessInterval
3427
+ ? Math.max(storedBackoff, Number(uploadThrottleState.serverNextAllowedAtMs || 0))
3416
3428
  : successDeadline,
3417
3429
  },
3418
3430
  config: AUTO_UPLOAD_CONFIG,
@@ -3458,6 +3470,7 @@ async function cmdSync(argv, context = {}) {
3458
3470
  const replacementDeviceToken = legacyBaseUrlMigration?.replacementDeviceToken;
3459
3471
  const stateAfterFailure = (await readJson(queueStatePath)) || { offset: 0 };
3460
3472
  const canFallbackWithoutSplittingHistory =
3473
+ !Object.hasOwn(process.env, "TOKENTRACKER_LOCAL_SYNC_DEVICE_TOKEN") &&
3461
3474
  (error?.status === 401 || error?.status === 403) &&
3462
3475
  replacementDeviceToken &&
3463
3476
  fallbackDeviceToken &&
@@ -3496,12 +3509,16 @@ async function cmdSync(argv, context = {}) {
3496
3509
  }
3497
3510
  // Record success so the exponential backoff step resets — otherwise
3498
3511
  // a single past failure keeps us pessimistically throttled forever.
3499
- uploadThrottleState = recordUploadSuccess({
3500
- nowMs: Date.now(),
3501
- state: uploadThrottleState,
3502
- config: AUTO_UPLOAD_CONFIG,
3503
- });
3504
- await writeJson(uploadThrottlePath, uploadThrottleState);
3512
+ if (uploadResult.batches > 0) {
3513
+ uploadThrottleState = recordUploadSuccess({
3514
+ nowMs: Date.now(),
3515
+ state: uploadThrottleState,
3516
+ config: AUTO_UPLOAD_CONFIG,
3517
+ cloudAccess: uploadResult.cloudAccess,
3518
+ attemptId: process.env.TOKENTRACKER_LOCAL_SYNC_ATTEMPT_ID,
3519
+ });
3520
+ await writeJson(uploadThrottlePath, uploadThrottleState);
3521
+ }
3505
3522
  } catch (e) {
3506
3523
  // Persist a backoff on 429 / 5xx so the next auto-sync waits instead
3507
3524
  // of retrying immediately and making the rate-limit worse. The
@@ -4191,6 +4208,8 @@ async function drainQueueToCloud({ baseUrl, anonKey, deviceToken, queuePath, que
4191
4208
  let inserted = 0;
4192
4209
  let skipped = 0;
4193
4210
  let batches = 0;
4211
+ let cloudAccess = null;
4212
+ const uploadId = crypto.randomUUID();
4194
4213
 
4195
4214
  const queueSize = await safeStatSize(queuePath);
4196
4215
  const limit = Math.min(Math.max(1, Math.floor(Number(batchSize || 200))), MAX_INGEST_BUCKETS);
@@ -4216,6 +4235,7 @@ async function drainQueueToCloud({ baseUrl, anonKey, deviceToken, queuePath, que
4216
4235
  method: "POST",
4217
4236
  headers,
4218
4237
  body: JSON.stringify({
4238
+ upload_id: uploadId,
4219
4239
  hourly: result.buckets,
4220
4240
  ...(result.sessionStates.length > 0
4221
4241
  ? { account_session_states: result.sessionStates }
@@ -4230,14 +4250,15 @@ async function drainQueueToCloud({ baseUrl, anonKey, deviceToken, queuePath, que
4230
4250
  const err = new Error(`HTTP ${res.status}: ${rawText.substring(0, 500)}`);
4231
4251
  err.status = res.status;
4232
4252
  err.code = res.status === 401 ? "CLOUD_DEVICE_TOKEN_REJECTED"
4233
- : res.status === 403 ? "CLOUD_UPLOAD_FORBIDDEN"
4234
- : "CLOUD_UPLOAD_FAILED";
4253
+ : typeof data?.code === "string" ? data.code
4254
+ : res.status === 403 ? "CLOUD_UPLOAD_FORBIDDEN" : "CLOUD_UPLOAD_FAILED";
4235
4255
  const retryAfter = res.headers?.get?.("Retry-After") ?? null;
4236
4256
  const retryAfterMs = parseRetryAfterMs(retryAfter);
4237
4257
  if (retryAfterMs !== null) err.retryAfterMs = retryAfterMs;
4238
4258
  throw err;
4239
4259
  }
4240
4260
 
4261
+ cloudAccess = data;
4241
4262
  inserted += Number(data?.inserted || 0);
4242
4263
  skipped += Number(data?.skipped || 0);
4243
4264
  batches += 1;
@@ -4248,7 +4269,7 @@ async function drainQueueToCloud({ baseUrl, anonKey, deviceToken, queuePath, que
4248
4269
  await writeJson(queueStatePath, state);
4249
4270
  }
4250
4271
 
4251
- return { inserted, skipped, batches };
4272
+ return { inserted, skipped, batches, ...(cloudAccess?.next_allowed_at ? { cloudAccess } : {}) };
4252
4273
  }
4253
4274
 
4254
4275
  async function readQueueBatch(queuePath, startOffset, maxBuckets) {
@@ -0,0 +1,27 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+ const crypto = require("node:crypto");
4
+
5
+ function writeJsonAtomicSync(filePath, value) {
6
+ const contents = JSON.stringify(value, null, 2) + "\n";
7
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
8
+ const tmp = `${filePath}.tmp.${crypto.randomUUID()}`;
9
+ let fd;
10
+ let created = false;
11
+ try {
12
+ fd = fs.openSync(tmp, "wx", 0o600);
13
+ created = true;
14
+ fs.writeFileSync(fd, contents, "utf8");
15
+ fs.closeSync(fd);
16
+ fd = undefined;
17
+ // Replace the final directory entry rather than following a link that
18
+ // appeared after the caller read or checked the previous configuration.
19
+ fs.renameSync(tmp, filePath);
20
+ created = false;
21
+ } finally {
22
+ if (fd !== undefined) fs.closeSync(fd);
23
+ if (created) fs.unlinkSync(tmp);
24
+ }
25
+ }
26
+
27
+ module.exports = { writeJsonAtomicSync };
@@ -0,0 +1,11 @@
1
+ function parseBearerToken(authorization) {
2
+ // Match only the prefix so an all-whitespace header cannot make two
3
+ // overlapping quantifiers repeatedly backtrack over the same characters.
4
+ const value = String(authorization || "");
5
+ const prefix = /^Bearer[ \t]+/i.exec(value);
6
+ if (!prefix) return null;
7
+ const token = value.slice(prefix[0].length);
8
+ return token && !/[\r\n]/.test(token) ? token : null;
9
+ }
10
+
11
+ module.exports = { parseBearerToken };
@@ -383,6 +383,9 @@ async function fetchAccountFunction({
383
383
  if (!res || !res.ok) {
384
384
  const err = new Error(`Account fetch failed with HTTP ${res ? res.status : "?"}`);
385
385
  err.status = res ? res.status : 0;
386
+ const failure = res?.json ? await res.json().catch(() => ({})) : {};
387
+ if (typeof failure?.code === "string") err.code = failure.code;
388
+ if (typeof failure?.recovery_url === "string") err.recoveryUrl = failure.recovery_url;
386
389
  throw err;
387
390
  }
388
391
  const data = await res.json();
@@ -495,7 +498,7 @@ async function fetchAccountUsage({
495
498
  }
496
499
  return JSON.stringify(data);
497
500
  } catch (error) {
498
- if ((error.status === 401 || error.status === 403) && sessionAtStart === sessionGeneration) {
501
+ if (error.status === 401 && sessionAtStart === sessionGeneration) {
499
502
  invalidateCloudAccountPayloadCache({ sessionChanged: true });
500
503
  error.invalidatedSessionGeneration = sessionGeneration;
501
504
  }