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.
- package/README.de.md +2 -2
- package/README.ja.md +2 -2
- package/README.ko.md +2 -2
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/dashboard/dist/assets/{AchievementBadge-_dRLgxnp.js → AchievementBadge-BTqlXUP1.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-DtvlZLlv.js → AchievementsPage-DbKUhaY-.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-OgzKkbes.js → AchievementsSection-El1F-OC7.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-D4Rlm_um.js → BadgeDetailModal--8PySLzP.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-errJ5IvB.js → BotAnimated-DBcOS4vh.js} +1 -1
- package/dashboard/dist/assets/{Card-esELVQcD.js → Card-CeGs8TaY.js} +1 -1
- package/dashboard/dist/assets/{CloudCheckoutPage-3sH4Dbx9.js → CloudCheckoutPage-DCg3VVAi.js} +1 -1
- package/dashboard/dist/assets/{CloudPage-Bi_vgiqE.js → CloudPage-CmeFDrmk.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-I0aze2EJ.js → CommandPalette-CrSNoZW9.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-CEn2g4J9.js → CommunityStatsModal-ynB4j0Qb.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-D7SfFCaf.js → ConfirmModal-DDTUf-My.js} +1 -1
- package/dashboard/dist/assets/{Controls-Dyb5Vmk_.js → Controls-DXsJjEU1.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-CMHX8zDq.js → DashboardPage-fr4sgbLB.js} +1 -1
- package/dashboard/dist/assets/{DateRangePopover-CgmxsHVR.js → DateRangePopover-gTiPISL_.js} +1 -1
- package/dashboard/dist/assets/DevicePage-DvHVjCCH.js +1 -0
- package/dashboard/dist/assets/{DialogClose-DzR109OA.js → DialogClose-CXwouQe4.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-BzbzETUC.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-BLH1MgOX.js → DialogTitle-Cd_k-Nyw.js} +1 -1
- package/dashboard/dist/assets/{ElectricLogo-DcpBcJIy.js → ElectricLogo-C4vUNTCI.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-CBAXe_zb.js → FadeIn-CGM5RBFy.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-BZjQ3uJo.js → HeaderGithubStar-el5-DhVb.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-Bq4HuWOZ.js → HoverTooltip-C7YaWwJx.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-Oiah1-YB.js → IpCheckPage-EKhbH9hJ.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-Cc1FfkWg.js → LandingPage-DJYOv1AM.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardPage-Bc2j_DQY.js → LeaderboardPage-CwzJxA8p.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProBadge-w1olVHVp.js → LeaderboardProBadge-DYLp9pJY.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfileModal-mgG7mdl8.js → LeaderboardProfileModal-DqD2h9dD.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-DqxyooT5.js → LeaderboardProfilePage-CarYvpYe.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-4T0BpfYS.js +6 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-DZLgI75v.js → LocalOnlyNotice-CAR-mqy0.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-DVNU_QtS.js → LoginCard-Czq-UgAU.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-yxi5pCwC.js → LoginModal-D4uxfzZb.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-CW9pu4mf.js → LoginPage-Bg3BG0Wv.js} +1 -1
- package/dashboard/dist/assets/{PetPage-DwZsq4nC.js → PetPage-BgwBxRyx.js} +2 -2
- package/dashboard/dist/assets/{PopoverPopup-DYGmmoD8.js → PopoverPopup-k3o_Jp2b.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-BKfj89GN.js → ProviderIcon-Bbw6MUSf.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-gQdgCp9s.js → ResetPasswordPage-DIKPVu5K.js} +1 -1
- package/dashboard/dist/assets/{Select-DvSR56Lj.js → Select-kA0dV5Qw.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-DXo30s3O.js → SelectItemText-D4YCIn1x.js} +1 -1
- package/dashboard/dist/assets/{SelfHostPage-jA51Nckw.js → SelfHostPage-C3Y9qF3P.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-DEkRRPx8.js → ServiceStatusPage-o1G3SJhV.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-J7hvGPCj.js → SessionsPage-CA-efxhU.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-ewH1Mzsm.js → SettingsPage-Mr84vxQL.js} +3 -3
- package/dashboard/dist/assets/{SkillsPage-BaeBHhU7.js → SkillsPage-BtRhLSpz.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-D52pHpzj.js → TokenGalaxy-CcFjCCuC.js} +1 -1
- package/dashboard/dist/assets/{TooltipPopup-mLpESQcv.js → TooltipPopup-6aTx3jDf.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-ErowR5GE.js → TrendMonitor-DoywXr8h.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-Bf1uuKE7.js → WidgetsPage-BuGSzvR0.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-C8qacvpR.js → WrappedPage-Bm6X-SUZ.js} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-C9a2vCBN.css +1 -0
- package/dashboard/dist/assets/{__vite-browser-external-CIpYjfAT.js → __vite-browser-external-ClVJNQu3.js} +3 -3
- package/dashboard/dist/assets/{agent-logos-tmdKvCcK.js → agent-logos-Sc8Sejgx.js} +1 -1
- package/dashboard/dist/assets/{arrow-left-CRKi420c.js → arrow-left-Cm1LG2i_.js} +1 -1
- package/dashboard/dist/assets/{arrow-right-C_o_iQbK.js → arrow-right-WP3kIwiy.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-N5YqKKnI.js → arrow-up-right-CjyprVMj.js} +1 -1
- package/dashboard/dist/assets/{calendar-days-BJh4FDj9.js → calendar-days-Bm88-Gcb.js} +1 -1
- package/dashboard/dist/assets/{check-BfHGb92g.js → check-CzIxFg0I.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-D1pO8Xgc.js → chevron-down-bfGK8629.js} +1 -1
- package/dashboard/dist/assets/{cloud-checkout-Ds13TmYz.js → cloud-checkout-C9DFQ7yM.js} +1 -1
- package/dashboard/dist/assets/{composite-DDEsQKPD.js → composite-CdExmgTx.js} +1 -1
- package/dashboard/dist/assets/{copy-CJTosYLe.js → copy-CPPdujn2.js} +1 -1
- package/dashboard/dist/assets/{download-Bkqx3c3y.js → download-Cl6_dKeK.js} +1 -1
- package/dashboard/dist/assets/{external-link-IYUo5LcK.js → external-link-B0r6Wo4e.js} +1 -1
- package/dashboard/dist/assets/{flame-BD_hiMfM.js → flame-wEezvzXx.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-DxwbhPag.js → format-tokens-vWpBpKpM.js} +1 -1
- package/dashboard/dist/assets/{gift-DLzFdL-1.js → gift-BFjblRqU.js} +1 -1
- package/dashboard/dist/assets/{icons-CbrWeFPS.js → icons-DX3dCNJP.js} +1 -1
- package/dashboard/dist/assets/{index-AVEWo-aY.js → index-D-TnqdBv.js} +1 -1
- package/dashboard/dist/assets/{index-DfzCE-FL.js → index-D1Zdrpom.js} +1 -1
- package/dashboard/dist/assets/{index-Dj1QnoPF.js → index-DBF1bJlm.js} +1 -1
- package/dashboard/dist/assets/{index-3xGpnZnP.js → index-p8Gt3NFg.js} +1 -1
- package/dashboard/dist/assets/{info-odlIlCnm.js → info-Cqi971MA.js} +1 -1
- package/dashboard/dist/assets/{link-2-Bt28Ey9r.js → link-2-79sm__cT.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-lGnIhIFS.js → loader-circle-CDoVfOUb.js} +1 -1
- package/dashboard/dist/assets/{local-usage-transfer-DSS0WaL-.js → local-usage-transfer-EcXnuwCj.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-CitRa_Z1.js → modifiers.esm-v5_BKSmY.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-1T0GFcOz.js → monitor-smartphone-CO67DffO.js} +1 -1
- package/dashboard/dist/assets/{play-d6YYOmED.js → play-CbvuGtc3.js} +1 -1
- package/dashboard/dist/assets/{plus-GrGV9Fkm.js → plus-jc2gXlRM.js} +1 -1
- package/dashboard/dist/assets/{provider-display-D1v3cgDm.js → provider-display-BK3EbRLM.js} +1 -1
- package/dashboard/dist/assets/{react-DYCAyQbo.js → react-DMs4C4qS.js} +1 -1
- package/dashboard/dist/assets/{safePolygon-iPu060Qe.js → safePolygon-cY9H0GBN.js} +1 -1
- package/dashboard/dist/assets/{search-AQhOTwzm.js → search-fqTJoYYA.js} +1 -1
- package/dashboard/dist/assets/{server-uYTbWPMd.js → server-jpBtvTvr.js} +1 -1
- package/dashboard/dist/assets/{shield-check-DB1NVCDf.js → shield-check-DQiBlJbN.js} +1 -1
- package/dashboard/dist/assets/{skills-api-D7X10n0t.js → skills-api-CN2r_rmt.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-B13KhrCd.js → sortable.esm-DrS52yOK.js} +1 -1
- package/dashboard/dist/assets/{store-D6XHiq8o.js → store-K2Mk6aMn.js} +1 -1
- package/dashboard/dist/assets/{terminal-t37F8qyF.js → terminal-Q4SruAlh.js} +1 -1
- package/dashboard/dist/assets/{trash-CDHRwGsy.js → trash-C1Ob-g8E.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats--kaGg0EW.js → use-community-stats-C-Gxpp9D.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-ChyODWU3.js → use-latest-request-guard-DK-A4bBp.js} +1 -1
- package/dashboard/dist/assets/use-limits-display-prefs-ZauQzxE_.js +1 -0
- package/dashboard/dist/assets/{use-native-settings-i3O4GTY1.js → use-native-settings-BZLEIUK8.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-BtewEEX_.js → use-ordered-list-De_Mcm_H.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-DatTtp2N.js +1 -0
- package/dashboard/dist/assets/{use-transform-NBAyuphu.js → use-transform-XCDD5IrX.js} +1 -1
- package/dashboard/dist/assets/use-usage-limits-BncGDhoF.js +1 -0
- package/dashboard/dist/assets/{useCurrency-BdsxkAID.js → useCurrency-DXPclxBT.js} +1 -1
- package/dashboard/dist/assets/{usePositioner-BAN79miB.js → usePositioner-C-E1npCy.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-CGEO_mX1.js → useTokenFormat-B70xhy9u.js} +1 -1
- package/dashboard/dist/assets/{zap-BDHWp-gL.js → zap-D7gRgGCj.js} +1 -1
- package/dashboard/dist/index.html +5 -5
- package/dashboard/dist/ip-check.html +2 -2
- package/dashboard/dist/leaderboard.html +2 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +2 -2
- package/src/lib/grok-limits.js +1 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/usage-limits.js +20 -8
- package/src/lib/workbuddy-ipc.js +191 -0
- package/src/lib/workbuddy-limits.js +564 -0
- package/dashboard/dist/assets/DevicePage-9R2MfK2Z.js +0 -1
- package/dashboard/dist/assets/DialogDescription-CdNjUDLZ.js +0 -1
- package/dashboard/dist/assets/LimitsPage-BIvK0GFj.js +0 -6
- package/dashboard/dist/assets/__vite-browser-external-CnVnsbqV.css +0 -1
- package/dashboard/dist/assets/use-limits-display-prefs-katFAxOA.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-DPacKl9n.js +0 -1
- package/dashboard/dist/assets/use-usage-limits-hsv-93TF.js +0 -1
package/src/lib/usage-limits.js
CHANGED
|
@@ -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
|
|
753
|
-
*
|
|
754
|
-
*
|
|
755
|
-
*
|
|
756
|
-
*
|
|
757
|
-
*
|
|
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 };
|