tokentracker-cli 1.4.9 → 1.4.10

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 (124) hide show
  1. package/README.de.md +2 -2
  2. package/README.ja.md +2 -2
  3. package/README.ko.md +2 -2
  4. package/README.md +2 -2
  5. package/README.zh-CN.md +2 -2
  6. package/dashboard/dist/assets/{AchievementBadge-_dRLgxnp.js → AchievementBadge-BTqlXUP1.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-DtvlZLlv.js → AchievementsPage-DbKUhaY-.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-OgzKkbes.js → AchievementsSection-El1F-OC7.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-D4Rlm_um.js → BadgeDetailModal--8PySLzP.js} +1 -1
  10. package/dashboard/dist/assets/{BotAnimated-errJ5IvB.js → BotAnimated-DBcOS4vh.js} +1 -1
  11. package/dashboard/dist/assets/{Card-esELVQcD.js → Card-CeGs8TaY.js} +1 -1
  12. package/dashboard/dist/assets/{CloudCheckoutPage-3sH4Dbx9.js → CloudCheckoutPage-DCg3VVAi.js} +1 -1
  13. package/dashboard/dist/assets/{CloudPage-Bi_vgiqE.js → CloudPage-CmeFDrmk.js} +2 -2
  14. package/dashboard/dist/assets/{CommandPalette-I0aze2EJ.js → CommandPalette-CrSNoZW9.js} +1 -1
  15. package/dashboard/dist/assets/{CommunityStatsModal-CEn2g4J9.js → CommunityStatsModal-ynB4j0Qb.js} +1 -1
  16. package/dashboard/dist/assets/{ConfirmModal-D7SfFCaf.js → ConfirmModal-DDTUf-My.js} +1 -1
  17. package/dashboard/dist/assets/{Controls-Dyb5Vmk_.js → Controls-DXsJjEU1.js} +1 -1
  18. package/dashboard/dist/assets/{DashboardPage-CMHX8zDq.js → DashboardPage-fr4sgbLB.js} +1 -1
  19. package/dashboard/dist/assets/{DateRangePopover-CgmxsHVR.js → DateRangePopover-gTiPISL_.js} +1 -1
  20. package/dashboard/dist/assets/DevicePage-DvHVjCCH.js +1 -0
  21. package/dashboard/dist/assets/{DialogClose-DzR109OA.js → DialogClose-CXwouQe4.js} +1 -1
  22. package/dashboard/dist/assets/DialogDescription-BzbzETUC.js +1 -0
  23. package/dashboard/dist/assets/{DialogTitle-BLH1MgOX.js → DialogTitle-Cd_k-Nyw.js} +1 -1
  24. package/dashboard/dist/assets/{ElectricLogo-DcpBcJIy.js → ElectricLogo-C4vUNTCI.js} +1 -1
  25. package/dashboard/dist/assets/{FadeIn-CBAXe_zb.js → FadeIn-CGM5RBFy.js} +1 -1
  26. package/dashboard/dist/assets/{HeaderGithubStar-BZjQ3uJo.js → HeaderGithubStar-el5-DhVb.js} +1 -1
  27. package/dashboard/dist/assets/{HoverTooltip-Bq4HuWOZ.js → HoverTooltip-C7YaWwJx.js} +1 -1
  28. package/dashboard/dist/assets/{IpCheckPage-Oiah1-YB.js → IpCheckPage-EKhbH9hJ.js} +1 -1
  29. package/dashboard/dist/assets/{LandingPage-Cc1FfkWg.js → LandingPage-DJYOv1AM.js} +2 -2
  30. package/dashboard/dist/assets/{LeaderboardPage-Bc2j_DQY.js → LeaderboardPage-CwzJxA8p.js} +2 -2
  31. package/dashboard/dist/assets/{LeaderboardProBadge-w1olVHVp.js → LeaderboardProBadge-DYLp9pJY.js} +1 -1
  32. package/dashboard/dist/assets/{LeaderboardProfileModal-mgG7mdl8.js → LeaderboardProfileModal-DqD2h9dD.js} +3 -3
  33. package/dashboard/dist/assets/{LeaderboardProfilePage-DqxyooT5.js → LeaderboardProfilePage-CarYvpYe.js} +1 -1
  34. package/dashboard/dist/assets/LimitsPage-4T0BpfYS.js +6 -0
  35. package/dashboard/dist/assets/{LocalOnlyNotice-DZLgI75v.js → LocalOnlyNotice-CAR-mqy0.js} +1 -1
  36. package/dashboard/dist/assets/{LoginCard-DVNU_QtS.js → LoginCard-Czq-UgAU.js} +1 -1
  37. package/dashboard/dist/assets/{LoginModal-yxi5pCwC.js → LoginModal-D4uxfzZb.js} +1 -1
  38. package/dashboard/dist/assets/{LoginPage-CW9pu4mf.js → LoginPage-Bg3BG0Wv.js} +1 -1
  39. package/dashboard/dist/assets/{PetPage-DwZsq4nC.js → PetPage-BgwBxRyx.js} +2 -2
  40. package/dashboard/dist/assets/{PopoverPopup-DYGmmoD8.js → PopoverPopup-k3o_Jp2b.js} +1 -1
  41. package/dashboard/dist/assets/{ProviderIcon-BKfj89GN.js → ProviderIcon-Bbw6MUSf.js} +1 -1
  42. package/dashboard/dist/assets/{ResetPasswordPage-gQdgCp9s.js → ResetPasswordPage-DIKPVu5K.js} +1 -1
  43. package/dashboard/dist/assets/{Select-DvSR56Lj.js → Select-kA0dV5Qw.js} +1 -1
  44. package/dashboard/dist/assets/{SelectItemText-DXo30s3O.js → SelectItemText-D4YCIn1x.js} +1 -1
  45. package/dashboard/dist/assets/{SelfHostPage-jA51Nckw.js → SelfHostPage-C3Y9qF3P.js} +1 -1
  46. package/dashboard/dist/assets/{ServiceStatusPage-DEkRRPx8.js → ServiceStatusPage-o1G3SJhV.js} +1 -1
  47. package/dashboard/dist/assets/{SessionsPage-J7hvGPCj.js → SessionsPage-CA-efxhU.js} +1 -1
  48. package/dashboard/dist/assets/{SettingsPage-ewH1Mzsm.js → SettingsPage-Mr84vxQL.js} +3 -3
  49. package/dashboard/dist/assets/{SkillsPage-BaeBHhU7.js → SkillsPage-BtRhLSpz.js} +1 -1
  50. package/dashboard/dist/assets/{TokenGalaxy-D52pHpzj.js → TokenGalaxy-CcFjCCuC.js} +1 -1
  51. package/dashboard/dist/assets/{TooltipPopup-mLpESQcv.js → TooltipPopup-6aTx3jDf.js} +1 -1
  52. package/dashboard/dist/assets/{TrendMonitor-ErowR5GE.js → TrendMonitor-DoywXr8h.js} +1 -1
  53. package/dashboard/dist/assets/{WidgetsPage-Bf1uuKE7.js → WidgetsPage-BuGSzvR0.js} +1 -1
  54. package/dashboard/dist/assets/{WrappedPage-C8qacvpR.js → WrappedPage-Bm6X-SUZ.js} +1 -1
  55. package/dashboard/dist/assets/__vite-browser-external-C9a2vCBN.css +1 -0
  56. package/dashboard/dist/assets/{__vite-browser-external-CIpYjfAT.js → __vite-browser-external-ClVJNQu3.js} +3 -3
  57. package/dashboard/dist/assets/{agent-logos-tmdKvCcK.js → agent-logos-Sc8Sejgx.js} +1 -1
  58. package/dashboard/dist/assets/{arrow-left-CRKi420c.js → arrow-left-Cm1LG2i_.js} +1 -1
  59. package/dashboard/dist/assets/{arrow-right-C_o_iQbK.js → arrow-right-WP3kIwiy.js} +1 -1
  60. package/dashboard/dist/assets/{arrow-up-right-N5YqKKnI.js → arrow-up-right-CjyprVMj.js} +1 -1
  61. package/dashboard/dist/assets/{calendar-days-BJh4FDj9.js → calendar-days-Bm88-Gcb.js} +1 -1
  62. package/dashboard/dist/assets/{check-BfHGb92g.js → check-CzIxFg0I.js} +1 -1
  63. package/dashboard/dist/assets/{chevron-down-D1pO8Xgc.js → chevron-down-bfGK8629.js} +1 -1
  64. package/dashboard/dist/assets/{cloud-checkout-Ds13TmYz.js → cloud-checkout-C9DFQ7yM.js} +1 -1
  65. package/dashboard/dist/assets/{composite-DDEsQKPD.js → composite-CdExmgTx.js} +1 -1
  66. package/dashboard/dist/assets/{copy-CJTosYLe.js → copy-CPPdujn2.js} +1 -1
  67. package/dashboard/dist/assets/{download-Bkqx3c3y.js → download-Cl6_dKeK.js} +1 -1
  68. package/dashboard/dist/assets/{external-link-IYUo5LcK.js → external-link-B0r6Wo4e.js} +1 -1
  69. package/dashboard/dist/assets/{flame-BD_hiMfM.js → flame-wEezvzXx.js} +1 -1
  70. package/dashboard/dist/assets/{format-tokens-DxwbhPag.js → format-tokens-vWpBpKpM.js} +1 -1
  71. package/dashboard/dist/assets/{gift-DLzFdL-1.js → gift-BFjblRqU.js} +1 -1
  72. package/dashboard/dist/assets/{icons-CbrWeFPS.js → icons-DX3dCNJP.js} +1 -1
  73. package/dashboard/dist/assets/{index-AVEWo-aY.js → index-D-TnqdBv.js} +1 -1
  74. package/dashboard/dist/assets/{index-DfzCE-FL.js → index-D1Zdrpom.js} +1 -1
  75. package/dashboard/dist/assets/{index-Dj1QnoPF.js → index-DBF1bJlm.js} +1 -1
  76. package/dashboard/dist/assets/{index-3xGpnZnP.js → index-p8Gt3NFg.js} +1 -1
  77. package/dashboard/dist/assets/{info-odlIlCnm.js → info-Cqi971MA.js} +1 -1
  78. package/dashboard/dist/assets/{link-2-Bt28Ey9r.js → link-2-79sm__cT.js} +1 -1
  79. package/dashboard/dist/assets/{loader-circle-lGnIhIFS.js → loader-circle-CDoVfOUb.js} +1 -1
  80. package/dashboard/dist/assets/{local-usage-transfer-DSS0WaL-.js → local-usage-transfer-EcXnuwCj.js} +1 -1
  81. package/dashboard/dist/assets/{modifiers.esm-CitRa_Z1.js → modifiers.esm-v5_BKSmY.js} +1 -1
  82. package/dashboard/dist/assets/{monitor-smartphone-1T0GFcOz.js → monitor-smartphone-CO67DffO.js} +1 -1
  83. package/dashboard/dist/assets/{play-d6YYOmED.js → play-CbvuGtc3.js} +1 -1
  84. package/dashboard/dist/assets/{plus-GrGV9Fkm.js → plus-jc2gXlRM.js} +1 -1
  85. package/dashboard/dist/assets/{provider-display-D1v3cgDm.js → provider-display-BK3EbRLM.js} +1 -1
  86. package/dashboard/dist/assets/{react-DYCAyQbo.js → react-DMs4C4qS.js} +1 -1
  87. package/dashboard/dist/assets/{safePolygon-iPu060Qe.js → safePolygon-cY9H0GBN.js} +1 -1
  88. package/dashboard/dist/assets/{search-AQhOTwzm.js → search-fqTJoYYA.js} +1 -1
  89. package/dashboard/dist/assets/{server-uYTbWPMd.js → server-jpBtvTvr.js} +1 -1
  90. package/dashboard/dist/assets/{shield-check-DB1NVCDf.js → shield-check-DQiBlJbN.js} +1 -1
  91. package/dashboard/dist/assets/{skills-api-D7X10n0t.js → skills-api-CN2r_rmt.js} +1 -1
  92. package/dashboard/dist/assets/{sortable.esm-B13KhrCd.js → sortable.esm-DrS52yOK.js} +1 -1
  93. package/dashboard/dist/assets/{store-D6XHiq8o.js → store-K2Mk6aMn.js} +1 -1
  94. package/dashboard/dist/assets/{terminal-t37F8qyF.js → terminal-Q4SruAlh.js} +1 -1
  95. package/dashboard/dist/assets/{trash-CDHRwGsy.js → trash-C1Ob-g8E.js} +1 -1
  96. package/dashboard/dist/assets/{use-community-stats--kaGg0EW.js → use-community-stats-C-Gxpp9D.js} +1 -1
  97. package/dashboard/dist/assets/{use-latest-request-guard-ChyODWU3.js → use-latest-request-guard-DK-A4bBp.js} +1 -1
  98. package/dashboard/dist/assets/use-limits-display-prefs-ZauQzxE_.js +1 -0
  99. package/dashboard/dist/assets/{use-native-settings-i3O4GTY1.js → use-native-settings-BZLEIUK8.js} +1 -1
  100. package/dashboard/dist/assets/{use-ordered-list-BtewEEX_.js → use-ordered-list-De_Mcm_H.js} +1 -1
  101. package/dashboard/dist/assets/use-reduced-motion-DatTtp2N.js +1 -0
  102. package/dashboard/dist/assets/{use-transform-NBAyuphu.js → use-transform-XCDD5IrX.js} +1 -1
  103. package/dashboard/dist/assets/use-usage-limits-BncGDhoF.js +1 -0
  104. package/dashboard/dist/assets/{useCurrency-BdsxkAID.js → useCurrency-DXPclxBT.js} +1 -1
  105. package/dashboard/dist/assets/{usePositioner-BAN79miB.js → usePositioner-C-E1npCy.js} +1 -1
  106. package/dashboard/dist/assets/{useTokenFormat-CGEO_mX1.js → useTokenFormat-B70xhy9u.js} +1 -1
  107. package/dashboard/dist/assets/{zap-BDHWp-gL.js → zap-D7gRgGCj.js} +1 -1
  108. package/dashboard/dist/index.html +5 -5
  109. package/dashboard/dist/ip-check.html +2 -2
  110. package/dashboard/dist/leaderboard.html +2 -2
  111. package/dashboard/dist/share.html +2 -2
  112. package/package.json +2 -2
  113. package/src/lib/grok-limits.js +1 -0
  114. package/src/lib/pricing/seed-snapshot.json +1 -1
  115. package/src/lib/usage-limits.js +20 -8
  116. package/src/lib/workbuddy-ipc.js +191 -0
  117. package/src/lib/workbuddy-limits.js +564 -0
  118. package/dashboard/dist/assets/DevicePage-9R2MfK2Z.js +0 -1
  119. package/dashboard/dist/assets/DialogDescription-CdNjUDLZ.js +0 -1
  120. package/dashboard/dist/assets/LimitsPage-BIvK0GFj.js +0 -6
  121. package/dashboard/dist/assets/__vite-browser-external-CnVnsbqV.css +0 -1
  122. package/dashboard/dist/assets/use-limits-display-prefs-katFAxOA.js +0 -1
  123. package/dashboard/dist/assets/use-reduced-motion-DPacKl9n.js +0 -1
  124. package/dashboard/dist/assets/use-usage-limits-hsv-93TF.js +0 -1
@@ -35,6 +35,7 @@ const { fetchDevinLimits } = require("./devin-limits");
35
35
  const { fetchQoderLimits, fetchQoderCnLimits } = require("./qoder-limits");
36
36
  const { fetchArkCodingPlanLimits } = require("./ark-coding-plan-limits");
37
37
  const { fetchArkAgentPlanLimits } = require("./ark-agent-plan-limits");
38
+ const { fetchWorkbuddyLimits, fetchWorkbuddyAiLimits, selectWorkbuddyLimits } = require("./workbuddy-limits");
38
39
  const { fetchProviderServiceStatus } = require("./provider-status");
39
40
  const { resolveKimiProfile } = require("./kimi-profile");
40
41
  const { readSqliteJsonRows, readSqliteJsonRowsAsync } = require("./sqlite-reader");
@@ -749,13 +750,12 @@ function normalizeCursorUsageSummary(body) {
749
750
  }
750
751
 
751
752
  /**
752
- * Cursor's Sand "Grok Bot" RPC reports the shared Super weekly pool (same
753
- * reset and about the same percent as Grok billing's creditUsagePercent).
754
- * Once the Grok card lists a Grok Bot product share, that Sand bar would
755
- * show the whole pool a second time under Cursor. Hide it only in that
756
- * case. A real separate Sand quota (different reset, or a percent more than
757
- * a point away from the pool) stays. Accounts with no Grok Bot product row
758
- * keep the Sand bar.
753
+ * Cursor's Sand RPC reports Grok Bot usage. grok.com shows that as a weekly
754
+ * Grok Bot limit of its own, separate from the SuperGrok pool and with its own
755
+ * reset, so the Sand bar normally stays. It is hidden only when it is
756
+ * indistinguishable from the Grok pool, so the same numbers are not drawn
757
+ * twice: Grok billing lists a GrokBot product share, and the two windows reset
758
+ * at the same instant and sit within a point of each other.
759
759
  */
760
760
  function cursorSandDuplicatesGrokPool(cursor, grok) {
761
761
  const sand = cursor?.quaternary_window;
@@ -4107,7 +4107,7 @@ async function fetchUsageLimitsUncached({
4107
4107
  : null;
4108
4108
 
4109
4109
  const providerFetch = withFetchTimeout(fetchImpl, providerTimeoutMs);
4110
- const [claudeResult, codexResult, cursor, kimi, gemini, kiro, antigravity, copilot, grok, zcode, opencodeGoRaw, qoder, qoderCn, codingPlan, agentPlan, commandCodeRaw, devinRaw, claudeServiceStatus] = await Promise.all([
4110
+ const [claudeResult, codexResult, cursor, kimi, gemini, kiro, antigravity, copilot, grok, zcode, opencodeGoRaw, qoder, qoderCn, codingPlan, agentPlan, commandCodeRaw, devinRaw, workbuddy, workbuddyAi, claudeServiceStatus] = await Promise.all([
4111
4111
  claudeToken && !freshClaudeCache && !claudeRetryAtMs
4112
4112
  ? withProviderTimeout(fetchClaudeUsageLimits(claudeToken, { fetchImpl: providerFetch, maxAttempts: 1 }), "Claude", providerTimeoutMs).then(
4113
4113
  (value) => ({ status: "fulfilled", value }),
@@ -4232,6 +4232,13 @@ async function fetchUsageLimitsUncached({
4232
4232
  (value) => ({ status: "fulfilled", value }),
4233
4233
  (reason) => ({ status: "rejected", reason }),
4234
4234
  ),
4235
+ // WorkBuddy CN and WorkBuddy AI are separate editions. Each one calls only
4236
+ // its own local authenticated billing proxy. Abort closes the IPC socket
4237
+ // when the provider budget expires. Reward credits are not a plan window.
4238
+ withAbortableProviderTimeout((signal) => fetchWorkbuddyLimits({ home, env, fetchImpl: providerFetch, signal }), "WorkBuddy", providerTimeoutMs)
4239
+ .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4240
+ withAbortableProviderTimeout((signal) => fetchWorkbuddyAiLimits({ home, env, fetchImpl: providerFetch, signal }), "WorkBuddy AI", providerTimeoutMs)
4241
+ .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4235
4242
  // Public status-page probe (fail-soft, own 5-min cache in provider-status.js).
4236
4243
  // Only probed for configured accounts — without a token the Claude section
4237
4244
  // never renders, so the reading would have nowhere to go.
@@ -4498,6 +4505,9 @@ async function fetchUsageLimitsUncached({
4498
4505
  qoderCn: withPlanLabel(qoderCn, qoderCn?.plan_label, "Qoder CN"),
4499
4506
  codingPlan: withPlanLabel(codingPlan, codingPlan?.plan_label, "Ark Coding Plan"),
4500
4507
  agentPlan: withPlanLabel(agentPlan, agentPlan?.plan_label, "Ark Agent Plan"),
4508
+ // WorkBuddy preserves provider English names and official edition labels
4509
+ // verbatim (including Free, STANDARD and Pro-Trial).
4510
+ workbuddy: selectWorkbuddyLimits(workbuddy, workbuddyAi),
4501
4511
  };
4502
4512
 
4503
4513
  for (const [providerName, provider] of Object.entries(data)) {
@@ -4587,4 +4597,6 @@ module.exports = {
4587
4597
  fetchCommandcodeLimits,
4588
4598
  fetchQoderLimits,
4589
4599
  fetchQoderCnLimits,
4600
+ fetchWorkbuddyLimits,
4601
+ fetchWorkbuddyAiLimits,
4590
4602
  };
@@ -0,0 +1,191 @@
1
+ "use strict";
2
+
3
+ // WorkBuddy's authenticated local proxy. The ticket proves both peers; the
4
+ // desktop daemon adds its own access token to relative billing requests.
5
+ const fs = require("node:fs");
6
+ const path = require("node:path");
7
+ const net = require("node:net");
8
+ const crypto = require("node:crypto");
9
+
10
+ const UNAVAILABLE = "Start WorkBuddy and sign in to read its subscription quota.";
11
+ const MAX_FRAME_BYTES = 1024 * 1024;
12
+ const BILLING_PATHS = new Set([
13
+ "/billing/meter/get-user-resource-summary",
14
+ "/billing/meter/get-user-resource-paid-packages",
15
+ "/billing/meter/get-user-resource-free-packages",
16
+ ]);
17
+
18
+ function readWorkbuddyIpcDiscovery(workbuddyHome) {
19
+ if (!workbuddyHome) return null;
20
+ let fd;
21
+ try {
22
+ fd = fs.openSync(path.join(workbuddyHome, "wbipc", "endpoint.json"),
23
+ fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
24
+ const stat = fs.fstatSync(fd);
25
+ if (!stat.isFile() || stat.size > 16384 || (process.platform !== "win32"
26
+ && (stat.uid !== process.getuid() || (stat.mode & 0o077) !== 0))) {
27
+ throw new Error("Untrusted discovery file.");
28
+ }
29
+ const value = JSON.parse(fs.readFileSync(fd, "utf8"));
30
+ if (typeof value.ticket !== "string" || !/^[\w-]{43}$/.test(value.ticket)
31
+ || typeof value.endpoint !== "string") throw new Error("Invalid discovery file.");
32
+ if (process.platform === "win32") {
33
+ if (!/^\\\\\.\\pipe\\wbipc-[\w-]+$/.test(value.endpoint)) throw new Error("Invalid pipe.");
34
+ } else {
35
+ if (!path.isAbsolute(value.endpoint)) throw new Error("Invalid socket.");
36
+ const socket = fs.lstatSync(value.endpoint);
37
+ if (!socket.isSocket() || socket.uid !== process.getuid() || (socket.mode & 0o077) !== 0) {
38
+ throw new Error("Untrusted socket.");
39
+ }
40
+ }
41
+ return { endpoint: value.endpoint, ticket: value.ticket };
42
+ } catch (error) {
43
+ if (error.code === "ENOENT" || error.code === "ENOTDIR") return null;
44
+ // Never expose discovery contents, tickets, paths or JSON parser excerpts.
45
+ throw new Error("WorkBuddy local connection information is unavailable.");
46
+ } finally {
47
+ if (fd !== undefined) fs.closeSync(fd);
48
+ }
49
+ }
50
+
51
+ function computeProof(ticket, role, endpoint, clientNonce, serverNonce) {
52
+ const parts = [];
53
+ for (const field of [role === "server" ? "wbipc-s" : "wbipc-c", "1", endpoint, clientNonce, serverNonce]) {
54
+ const bytes = Buffer.from(field, "utf8");
55
+ const length = Buffer.alloc(4);
56
+ length.writeUInt32BE(bytes.length);
57
+ parts.push(length, bytes);
58
+ }
59
+ return crypto.createHmac("sha256", ticket).update(Buffer.concat(parts)).digest("base64url");
60
+ }
61
+
62
+ function openWorkbuddyIpc({ discovery, signal, timeoutMs = 12000, connectImpl = net.createConnection }) {
63
+ return new Promise((resolve, reject) => {
64
+ const { endpoint, ticket } = discovery;
65
+ const clientNonce = crypto.randomBytes(16).toString("base64url");
66
+ const pending = new Map();
67
+ let socket;
68
+ let phase = "challenge";
69
+ let channel;
70
+ let nextId = 1;
71
+ let buffer = Buffer.alloc(0);
72
+ let closed = false;
73
+ const timer = setTimeout(() => fail(new Error("WorkBuddy quota request timed out.")), timeoutMs);
74
+ const onAbort = () => fail(new Error("WorkBuddy quota request was cancelled."));
75
+
76
+ function fail(error) {
77
+ if (closed) return;
78
+ closed = true;
79
+ clearTimeout(timer);
80
+ signal?.removeEventListener("abort", onAbort);
81
+ reject(error);
82
+ for (const handlers of pending.values()) handlers.reject(error);
83
+ pending.clear();
84
+ socket?.destroy();
85
+ }
86
+ function send(packet) {
87
+ socket.write(JSON.stringify(packet) + "\n");
88
+ }
89
+ function rpc(method, params, mode) {
90
+ if (closed) return Promise.reject(new Error(UNAVAILABLE));
91
+ return new Promise((resolve, reject) => {
92
+ const id = nextId++;
93
+ pending.set(id, { resolve, reject });
94
+ send({ jsonrpc: "2.0", id, method, params, ...(mode ? { mode } : {}) });
95
+ });
96
+ }
97
+ const client = {
98
+ close: () => fail(new Error("WorkBuddy local connection closed.")),
99
+ async postBilling(apiPath, body) {
100
+ if (!BILLING_PATHS.has(apiPath)) throw new Error("Unsupported WorkBuddy quota request.");
101
+ const response = await rpc(`${channel}/http.fetch`, {
102
+ method: "POST",
103
+ path: apiPath,
104
+ headers: { "content-type": "application/json", accept: "application/json" },
105
+ body_b64: Buffer.from(JSON.stringify(body)).toString("base64"),
106
+ }, "call");
107
+ if (response?.status === 401 || response?.status === 403) {
108
+ const error = new Error("WorkBuddy sign-in expired.");
109
+ error.code = "AUTH_EXPIRED";
110
+ throw error;
111
+ }
112
+ if (response?.status !== 200) throw new Error("WorkBuddy quota request failed.");
113
+ try {
114
+ if (typeof response.body_b64 !== "string" || !/^[A-Za-z0-9+/]*={0,2}$/.test(response.body_b64)) {
115
+ throw new Error("Invalid body.");
116
+ }
117
+ return JSON.parse(Buffer.from(response.body_b64, "base64").toString("utf8"));
118
+ } catch {
119
+ throw new Error("WorkBuddy quota response was malformed.");
120
+ }
121
+ },
122
+ };
123
+ function onPacket(packet) {
124
+ if (!packet || typeof packet !== "object") throw new Error("Invalid packet.");
125
+ if (phase === "challenge" && packet.type === "session_challenge" && packet.protocol === 1) {
126
+ if (typeof packet.server_nonce !== "string" || !/^[\w-]{16,128}$/.test(packet.server_nonce)
127
+ || typeof packet.server_proof !== "string") throw new Error("Invalid challenge.");
128
+ const expected = Buffer.from(computeProof(ticket, "server", endpoint, clientNonce, packet.server_nonce));
129
+ const actual = Buffer.from(packet.server_proof);
130
+ if (expected.length !== actual.length || !crypto.timingSafeEqual(expected, actual)) {
131
+ throw new Error("Invalid server proof.");
132
+ }
133
+ phase = "ack";
134
+ send({ type: "session_prove", client_proof: computeProof(ticket, "client", endpoint, clientNonce, packet.server_nonce) });
135
+ } else if (phase === "ack" && packet.type === "session_hello_ack" && packet.protocol === 1
136
+ && Array.isArray(packet.pipes) && packet.pipes.includes("wb.request")) {
137
+ phase = "ready";
138
+ rpc("broker/GetPipe", { pipe: "wb.request" }).then((result) => {
139
+ if (closed) return;
140
+ if (result?.channel !== "c:wb.request" || !Array.isArray(result.methods)
141
+ || !result.methods.includes("http.fetch")) {
142
+ fail(new Error("WorkBuddy billing proxy is unavailable."));
143
+ return;
144
+ }
145
+ channel = result.channel;
146
+ resolve(client);
147
+ }, () => fail(new Error(UNAVAILABLE)));
148
+ } else if (phase === "ready" && packet.jsonrpc === "2.0" && pending.has(packet.id)) {
149
+ const handlers = pending.get(packet.id);
150
+ pending.delete(packet.id);
151
+ if (packet.error) handlers.reject(new Error("WorkBuddy local quota request was rejected."));
152
+ else handlers.resolve(packet.result);
153
+ } else {
154
+ throw new Error("Unexpected packet.");
155
+ }
156
+ }
157
+ signal?.addEventListener("abort", onAbort, { once: true });
158
+ if (signal?.aborted) { onAbort(); return; }
159
+ try {
160
+ socket = connectImpl(endpoint);
161
+ socket.on("connect", () => send({
162
+ type: "session_hello", protocol_min: 1, protocol_max: 1,
163
+ client_nonce: clientNonce,
164
+ ticket_id: crypto.createHash("sha256").update(ticket, "utf8").digest("hex").slice(0, 16),
165
+ client: { kind: "tokentracker", id: "subscription-quota", version: "1" },
166
+ }));
167
+ socket.on("data", (chunk) => {
168
+ if (closed) return;
169
+ buffer = Buffer.concat([buffer, chunk]);
170
+ if (buffer.length > MAX_FRAME_BYTES) { fail(new Error("WorkBuddy quota response was too large.")); return; }
171
+ try {
172
+ let index;
173
+ while ((index = buffer.indexOf(10)) >= 0 && !closed) {
174
+ const line = buffer.subarray(0, index);
175
+ buffer = buffer.subarray(index + 1);
176
+ if (line.length) onPacket(JSON.parse(line.toString("utf8")));
177
+ }
178
+ } catch {
179
+ fail(new Error("WorkBuddy local connection authentication or protocol failed."));
180
+ }
181
+ });
182
+ socket.on("error", () => fail(new Error(UNAVAILABLE)));
183
+ socket.on("close", () => fail(new Error(UNAVAILABLE)));
184
+ socket.on("end", () => fail(new Error(UNAVAILABLE)));
185
+ } catch {
186
+ fail(new Error(UNAVAILABLE));
187
+ }
188
+ });
189
+ }
190
+
191
+ module.exports = { UNAVAILABLE, readWorkbuddyIpcDiscovery, openWorkbuddyIpc };