tokmon 0.23.3 → 0.23.5
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 +3 -3
- package/dist/{bootstrap-ink-MBN74IBQ.js → bootstrap-ink-WA3ENW2M.js} +28 -18
- package/dist/{chunk-RM3ZE44Z.js → chunk-3TJVFKXV.js} +1 -1
- package/dist/{chunk-DJPUYMZM.js → chunk-5CWOJMAH.js} +2 -0
- package/dist/{chunk-2AFQKH5O.js → chunk-IELQ5EY3.js} +2 -1
- package/dist/{chunk-VVJF5H4D.js → chunk-MJWFCDBB.js} +62 -75
- package/dist/{chunk-HXDAOOSI.js → chunk-SMPY52EV.js} +1 -30
- package/dist/cli.js +5 -5
- package/dist/{config-5CNJN3P7.js → config-QC5QSP3G.js} +1 -1
- package/dist/{daemon-F556JO7S.js → daemon-PVYTXJMS.js} +40 -15
- package/dist/{daemon-handle-4F4TXGXJ.js → daemon-handle-Y2N2NA6D.js} +8 -9
- package/dist/server-XUCQWX6N.js +10 -0
- package/dist/web/assets/{Area-DcTSmlQN.js → Area-DCES17aZ.js} +1 -1
- package/dist/web/assets/{analytics-B3HJJ-sr.js → analytics-DDW6zcnj.js} +2 -2
- package/dist/web/assets/{breakdown-DQ7IarBK.js → breakdown-D5LPPtqh.js} +1 -1
- package/dist/web/assets/{chart-BW9JY_zJ.js → chart-bHY7MQV0.js} +1 -1
- package/dist/web/assets/{explore-DuOeq8Ih.js → explore-DdWB-Pu7.js} +1 -1
- package/dist/web/assets/index-C9jxb2o3.js +83 -0
- package/dist/web/assets/index-m0KfWnN2.css +1 -0
- package/dist/web/assets/{models-Cn6g6Mqe.js → models-o1wJnG6w.js} +2 -2
- package/dist/web/assets/{overview-kWfchbjn.js → overview-wq2uAEk4.js} +2 -2
- package/dist/web/assets/{panel-BXWxMzXF.js → panel-DBnK1oy0.js} +1 -1
- package/dist/web/assets/{primitives-DiYNjIRG.js → primitives-CBmteetD.js} +1 -1
- package/dist/web/assets/settings-sheet-CZ6GEPl3.js +1 -0
- package/dist/web/assets/{share-sheet-BauPSkwE.js → share-sheet-DUee3FlK.js} +1 -1
- package/dist/web/assets/{timeline-BUwFGhw1.js → timeline-BxHwOJA_.js} +1 -1
- package/dist/web/assets/{use-dialog-trap-BZBp1Cz5.js → use-dialog-trap-30EqxYF-.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/server-GBB3CR7Y.js +0 -10
- package/dist/web/assets/index-DSj-Bxov.css +0 -1
- package/dist/web/assets/index-Dkbzrra_.js +0 -83
- package/dist/web/assets/settings-sheet-CmXSOk_F.js +0 -1
package/README.md
CHANGED
|
@@ -81,7 +81,7 @@ For **Claude / Codex / Cursor / Grok** — Daily, Weekly, and Monthly breakdowns
|
|
|
81
81
|
|
|
82
82
|
## Web Dashboard
|
|
83
83
|
|
|
84
|
-
Prefer a browser? `tokmon serve` starts a local web dashboard with the same data as the TUI — charts, global filtering, and shareable images — in a terminal-styled UI. Press `w` (or `W`) inside the TUI to
|
|
84
|
+
Prefer a browser? `tokmon serve` starts a local web dashboard with the same data as the TUI — charts, global filtering, and shareable images — in a terminal-styled UI. Press `w` (or `W`) inside the TUI to open its ordinary local URL.
|
|
85
85
|
|
|
86
86
|
```bash
|
|
87
87
|
tokmon serve # opens http://127.0.0.1:4317 in your browser
|
|
@@ -89,7 +89,7 @@ tokmon serve --port 8080
|
|
|
89
89
|
tokmon serve --no-open # don't auto-open the browser
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
It binds to `127.0.0.1`
|
|
92
|
+
It binds to `127.0.0.1` by default and reads the same local daemon state — nothing leaves your machine. The browser connects directly over a same-origin WebSocket; dashboard URLs have no token or login step. Optional LAN access can be enabled in settings, with an explicit unsafe-access warning, and takes effect after the daemon restarts. The dashboard renders instantly from a cached snapshot, then streams live updates and goes idle when no tab is open. Filter by provider, model, account, and period, flip between dark and light, and export any panel — or a summary card — as a PNG with the **Share** button.
|
|
93
93
|
|
|
94
94
|
### Overview
|
|
95
95
|
|
|
@@ -222,7 +222,7 @@ tokmon runs entirely on your machine and reads everything **read-only**:
|
|
|
222
222
|
|
|
223
223
|
## How It Works
|
|
224
224
|
|
|
225
|
-
tokmon runs a small local **daemon** that does all the data collection. The terminal UI and the web dashboard are both thin clients of it, talking over a loopback-only
|
|
225
|
+
tokmon runs a small local **daemon** that does all the data collection. The terminal UI and the web dashboard are both thin clients of it, talking over a local WebSocket — loopback-only by default — so a single process does the work and the TUI and web always show the same numbers. The daemon starts automatically with the TUI (and standalone via `tokmon serve`), and idle-pauses when nothing is watching.
|
|
226
226
|
|
|
227
227
|
**Usage & cost**
|
|
228
228
|
- Parses each tool's local session logs — Claude / Codex / pi `JSONL`, Cursor / opencode `SQLite` — and aggregates cost and token usage per day, week, and month.
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
time,
|
|
20
20
|
tokens,
|
|
21
21
|
withTimeout
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-IELQ5EY3.js";
|
|
23
23
|
import {
|
|
24
24
|
glyphs
|
|
25
25
|
} from "./chunk-RF4GGQGM.js";
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
sanitizeTyped,
|
|
38
38
|
saveConfigSync,
|
|
39
39
|
snapshotCacheFile
|
|
40
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-5CWOJMAH.js";
|
|
41
41
|
|
|
42
42
|
// src/bootstrap-ink.tsx
|
|
43
43
|
import { render } from "ink";
|
|
@@ -530,7 +530,7 @@ import { Box as Box4, Text as Text4 } from "ink";
|
|
|
530
530
|
import { memo as memo3 } from "react";
|
|
531
531
|
import { Box as Box3, Text as Text3 } from "ink";
|
|
532
532
|
import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
533
|
-
var GENERAL_ROWS =
|
|
533
|
+
var GENERAL_ROWS = 9;
|
|
534
534
|
var SETTINGS_TABS = ["general", "providers", "accounts"];
|
|
535
535
|
var SETTINGS_TAB_LABELS = {
|
|
536
536
|
general: "General",
|
|
@@ -624,7 +624,9 @@ var SettingsView = memo3(function SettingsView2({
|
|
|
624
624
|
" ",
|
|
625
625
|
glyphs().caretR
|
|
626
626
|
] })
|
|
627
|
-
] })
|
|
627
|
+
] }),
|
|
628
|
+
/* @__PURE__ */ jsx3(Row, { cursor, idx: 8, label: "Network access", children: /* @__PURE__ */ jsx3(Text3, { bold: true, color: config.allowNetworkAccess ? "red" : "green", children: config.allowNetworkAccess ? "LAN (unsafe)" : "local only" }) }),
|
|
629
|
+
config.allowNetworkAccess && /* @__PURE__ */ jsx3(Text3, { color: "red", children: " Warning: dashboard data and settings will be exposed to your local network after daemon restart." })
|
|
628
630
|
] }),
|
|
629
631
|
activeTab === "providers" && /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
630
632
|
/* @__PURE__ */ jsx3(Text3, { bold: true, dimColor: true, children: "Providers" }),
|
|
@@ -1615,7 +1617,7 @@ var TokmonRpcClient = class extends Context.Service()(
|
|
|
1615
1617
|
) {
|
|
1616
1618
|
};
|
|
1617
1619
|
var dynamicImport = new Function("specifier", "return import(specifier)");
|
|
1618
|
-
function toWsUrl(baseUrl
|
|
1620
|
+
function toWsUrl(baseUrl) {
|
|
1619
1621
|
const base = typeof window === "undefined" ? void 0 : window.location.origin;
|
|
1620
1622
|
const url = new URL(baseUrl, base);
|
|
1621
1623
|
if (url.protocol === "http:") url.protocol = "ws:";
|
|
@@ -1625,8 +1627,8 @@ function toWsUrl(baseUrl, token) {
|
|
|
1625
1627
|
}
|
|
1626
1628
|
url.hash = "";
|
|
1627
1629
|
url.searchParams.delete("tokmonToken");
|
|
1630
|
+
url.searchParams.delete("wsToken");
|
|
1628
1631
|
url.pathname = TOKMON_WS_PATH;
|
|
1629
|
-
if (token) url.searchParams.set("wsToken", token);
|
|
1630
1632
|
return url.toString();
|
|
1631
1633
|
}
|
|
1632
1634
|
function shouldUseNodeTransport(transport) {
|
|
@@ -1651,7 +1653,7 @@ function retryPolicy(options) {
|
|
|
1651
1653
|
return typeof options.reconnectAttempts === "number" ? policy.pipe(Schedule.both(Schedule.recurs(options.reconnectAttempts))) : policy;
|
|
1652
1654
|
}
|
|
1653
1655
|
function createDaemonRpcClient(baseUrl, options = {}) {
|
|
1654
|
-
const url = toWsUrl(baseUrl
|
|
1656
|
+
const url = toWsUrl(baseUrl);
|
|
1655
1657
|
const fibers = /* @__PURE__ */ new Set();
|
|
1656
1658
|
let session = null;
|
|
1657
1659
|
let sessionPromise = null;
|
|
@@ -1872,20 +1874,19 @@ function createDaemonRpcClient(baseUrl, options = {}) {
|
|
|
1872
1874
|
}
|
|
1873
1875
|
|
|
1874
1876
|
// src/client/use-daemon.ts
|
|
1875
|
-
function useDaemon(baseUrl
|
|
1877
|
+
function useDaemon(baseUrl) {
|
|
1876
1878
|
const [snapshot, setSnapshot] = useState3(null);
|
|
1877
1879
|
const [conn, setConn] = useState3("connecting");
|
|
1878
1880
|
const [config, setConfigState] = useState3(null);
|
|
1879
1881
|
const client = useMemo(() => {
|
|
1880
|
-
if (!baseUrl
|
|
1882
|
+
if (!baseUrl) return null;
|
|
1881
1883
|
return createDaemonRpcClient(baseUrl, {
|
|
1882
1884
|
transport: "node",
|
|
1883
|
-
wsToken,
|
|
1884
1885
|
onConn: (state) => {
|
|
1885
1886
|
if (state !== "closed") setConn(state);
|
|
1886
1887
|
}
|
|
1887
1888
|
});
|
|
1888
|
-
}, [baseUrl
|
|
1889
|
+
}, [baseUrl]);
|
|
1889
1890
|
const clientRef = useRef2(client);
|
|
1890
1891
|
clientRef.current = client;
|
|
1891
1892
|
useEffect3(() => {
|
|
@@ -2383,6 +2384,10 @@ function handleSettings(input, key, ctx) {
|
|
|
2383
2384
|
}
|
|
2384
2385
|
if (index === 7 && (inputKey.leftArrow || inputKey.rightArrow || inputKey.return)) {
|
|
2385
2386
|
updateConfig((current) => ({ ...current, defaultFocus: current.defaultFocus === "all" ? "last" : "all" }));
|
|
2387
|
+
return;
|
|
2388
|
+
}
|
|
2389
|
+
if (index === 8 && (inputKey.leftArrow || inputKey.rightArrow || inputKey.return)) {
|
|
2390
|
+
updateConfig((current) => ({ ...current, allowNetworkAccess: !current.allowNetworkAccess }));
|
|
2386
2391
|
}
|
|
2387
2392
|
}
|
|
2388
2393
|
}
|
|
@@ -3304,10 +3309,10 @@ function useAccountForm({ cfg, detected, updateConfig, trackedAccountRows, setSe
|
|
|
3304
3309
|
|
|
3305
3310
|
// src/app.tsx
|
|
3306
3311
|
import { Fragment as Fragment4, jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
3307
|
-
function App({ interval: cliInterval, initialConfig, baseUrl = null,
|
|
3308
|
-
const connected = mode === "connected" && baseUrl !== null
|
|
3312
|
+
function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "degraded" }) {
|
|
3313
|
+
const connected = mode === "connected" && baseUrl !== null;
|
|
3309
3314
|
const degraded = !connected;
|
|
3310
|
-
const daemon = useDaemon(connected ? baseUrl : null
|
|
3315
|
+
const daemon = useDaemon(connected ? baseUrl : null);
|
|
3311
3316
|
const { config, configSaveError, updateConfig } = useConfigState({
|
|
3312
3317
|
initialConfig,
|
|
3313
3318
|
cliInterval,
|
|
@@ -3643,16 +3648,16 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
|
|
|
3643
3648
|
return;
|
|
3644
3649
|
}
|
|
3645
3650
|
if (webRef.current) {
|
|
3646
|
-
openUrl(webRef.current.
|
|
3651
|
+
openUrl(webRef.current.url);
|
|
3647
3652
|
return;
|
|
3648
3653
|
}
|
|
3649
3654
|
if (webStartingRef.current) return;
|
|
3650
3655
|
webStartingRef.current = true;
|
|
3651
3656
|
try {
|
|
3652
|
-
const { startWebServer } = await import("./server-
|
|
3657
|
+
const { startWebServer } = await import("./server-XUCQWX6N.js");
|
|
3653
3658
|
const ctrl = await startWebServer({ config: cfg, log: false });
|
|
3654
3659
|
webRef.current = ctrl;
|
|
3655
|
-
openUrl(ctrl.
|
|
3660
|
+
openUrl(ctrl.url);
|
|
3656
3661
|
} catch {
|
|
3657
3662
|
} finally {
|
|
3658
3663
|
webStartingRef.current = false;
|
|
@@ -3850,6 +3855,12 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
|
|
|
3850
3855
|
glyphs().middot,
|
|
3851
3856
|
" showing last known data"
|
|
3852
3857
|
] }),
|
|
3858
|
+
cfg.allowNetworkAccess && /* @__PURE__ */ jsxs12(Text12, { color: "red", children: [
|
|
3859
|
+
glyphs().warn,
|
|
3860
|
+
" unsafe network access enabled ",
|
|
3861
|
+
glyphs().middot,
|
|
3862
|
+
" dashboard may be reachable from your LAN after daemon restart"
|
|
3863
|
+
] }),
|
|
3853
3864
|
configSaveError && /* @__PURE__ */ jsxs12(Text12, { color: "red", children: [
|
|
3854
3865
|
glyphs().warn,
|
|
3855
3866
|
" ",
|
|
@@ -3970,7 +3981,6 @@ async function bootstrapInk({ interval, config, daemon, mode }) {
|
|
|
3970
3981
|
interval,
|
|
3971
3982
|
initialConfig: config,
|
|
3972
3983
|
baseUrl: daemon.baseUrl,
|
|
3973
|
-
wsToken: daemon.wsToken,
|
|
3974
3984
|
mode
|
|
3975
3985
|
}
|
|
3976
3986
|
) })
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
cacheDir
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-5CWOJMAH.js";
|
|
5
5
|
|
|
6
6
|
// src/web/lockfile.ts
|
|
7
7
|
import { closeSync, fchmodSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
@@ -25,6 +25,7 @@ var DEFAULTS = {
|
|
|
25
25
|
dashboardLayout: "grid",
|
|
26
26
|
defaultFocus: "all",
|
|
27
27
|
ascii: "auto",
|
|
28
|
+
allowNetworkAccess: false,
|
|
28
29
|
knownProviders: []
|
|
29
30
|
};
|
|
30
31
|
var LEGACY_KNOWN = ["claude", "codex", "cursor"];
|
|
@@ -214,6 +215,7 @@ function repairConfig(input) {
|
|
|
214
215
|
dashboardLayout: parsed.dashboardLayout === "single" ? "single" : "grid",
|
|
215
216
|
defaultFocus: parsed.defaultFocus === "last" ? "last" : "all",
|
|
216
217
|
ascii: parsed.ascii === "on" ? "on" : parsed.ascii === "off" ? "off" : "auto",
|
|
218
|
+
allowNetworkAccess: parsed.allowNetworkAccess === true,
|
|
217
219
|
knownProviders
|
|
218
220
|
};
|
|
219
221
|
for (const key of Object.keys(DEFAULTS)) {
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
expandHome,
|
|
7
7
|
isValidTimezone,
|
|
8
8
|
slugify
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-5CWOJMAH.js";
|
|
10
10
|
|
|
11
11
|
// src/providers/usage-core.ts
|
|
12
12
|
import { readFileSync, readdirSync } from "fs";
|
|
@@ -4631,6 +4631,7 @@ var ConfigSchema = Schema.Struct({
|
|
|
4631
4631
|
dashboardLayout: Schema.Literals(["grid", "single"]),
|
|
4632
4632
|
defaultFocus: Schema.Literals(["all", "last"]),
|
|
4633
4633
|
ascii: Schema.Literals(["auto", "on", "off"]),
|
|
4634
|
+
allowNetworkAccess: Schema.Boolean,
|
|
4634
4635
|
knownProviders: Schema.Array(ProviderIdSchema)
|
|
4635
4636
|
});
|
|
4636
4637
|
var ProtocolInfoSchema = Schema.Struct({
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
appVersion,
|
|
4
|
-
browserUrl,
|
|
5
4
|
findWebRoot,
|
|
6
5
|
send,
|
|
7
6
|
sendJson,
|
|
8
7
|
serveStatic
|
|
9
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-SMPY52EV.js";
|
|
10
9
|
import {
|
|
11
10
|
PROVIDERS,
|
|
12
11
|
TOKMON_CAPABILITIES,
|
|
@@ -21,7 +20,7 @@ import {
|
|
|
21
20
|
namedHex,
|
|
22
21
|
resolveTimezone,
|
|
23
22
|
withTimeout
|
|
24
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-IELQ5EY3.js";
|
|
25
24
|
import {
|
|
26
25
|
cacheDir,
|
|
27
26
|
expandHome,
|
|
@@ -29,11 +28,11 @@ import {
|
|
|
29
28
|
normalizeConfig,
|
|
30
29
|
saveConfig,
|
|
31
30
|
snapshotCacheFile
|
|
32
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-5CWOJMAH.js";
|
|
33
32
|
|
|
34
33
|
// src/web/server.ts
|
|
35
34
|
import { createServer } from "http";
|
|
36
|
-
import { randomBytes, timingSafeEqual
|
|
35
|
+
import { randomBytes, timingSafeEqual } from "crypto";
|
|
37
36
|
|
|
38
37
|
// src/web/data.ts
|
|
39
38
|
async function resolveAccounts(config) {
|
|
@@ -676,7 +675,6 @@ async function applyConfigUpdate(engine, state, input) {
|
|
|
676
675
|
}
|
|
677
676
|
|
|
678
677
|
// src/web/ws.ts
|
|
679
|
-
import { timingSafeEqual } from "crypto";
|
|
680
678
|
import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
|
|
681
679
|
import { NodeWS } from "@effect/platform-node/NodeSocket";
|
|
682
680
|
import { Effect, Exit, Layer, Queue, Scope, Stream } from "effect";
|
|
@@ -743,40 +741,55 @@ async function listHomeDirectory(rawPath) {
|
|
|
743
741
|
return { path: abs, parent: parentFor(root, abs), entries };
|
|
744
742
|
}
|
|
745
743
|
|
|
746
|
-
// src/web/
|
|
744
|
+
// src/web/request-guard.ts
|
|
745
|
+
import { isIP } from "net";
|
|
747
746
|
function header(req, name) {
|
|
748
747
|
const value = req.headers[name.toLowerCase()];
|
|
749
748
|
return Array.isArray(value) ? value[0] : value;
|
|
750
749
|
}
|
|
751
|
-
function
|
|
752
|
-
if (!value) return
|
|
753
|
-
const host = value.trim().toLowerCase();
|
|
754
|
-
if (!host) return false;
|
|
755
|
-
return /^(?:127\.0\.0\.1|localhost)(?::\d{1,5})?$/.test(host) || /^\[::1\](?::\d{1,5})?$/.test(host);
|
|
756
|
-
}
|
|
757
|
-
function isWsPath(req) {
|
|
750
|
+
function hostnameFromHost(value) {
|
|
751
|
+
if (!value) return null;
|
|
758
752
|
try {
|
|
759
|
-
|
|
753
|
+
const url = new URL(`http://${value.trim()}`);
|
|
754
|
+
if (url.username || url.password || url.pathname !== "/" || url.search || url.hash) return null;
|
|
755
|
+
return url.hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
760
756
|
} catch {
|
|
761
|
-
return
|
|
757
|
+
return null;
|
|
762
758
|
}
|
|
763
759
|
}
|
|
764
|
-
function
|
|
760
|
+
function isLoopbackHostHeader(value) {
|
|
761
|
+
const hostname = hostnameFromHost(value);
|
|
762
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
|
|
763
|
+
}
|
|
764
|
+
function isAllowedHostHeader(value, allowNetworkAccess) {
|
|
765
|
+
if (isLoopbackHostHeader(value)) return true;
|
|
766
|
+
if (!allowNetworkAccess) return false;
|
|
767
|
+
const hostname = hostnameFromHost(value);
|
|
768
|
+
return hostname !== null && isIP(hostname) !== 0;
|
|
769
|
+
}
|
|
770
|
+
function isSameOriginRequest(req) {
|
|
771
|
+
const origin = header(req, "origin");
|
|
772
|
+
if (!origin || origin === "null") return true;
|
|
773
|
+
const host = header(req, "host")?.trim().toLowerCase();
|
|
774
|
+
if (!host) return false;
|
|
765
775
|
try {
|
|
766
|
-
|
|
776
|
+
const url = new URL(origin);
|
|
777
|
+
return (url.protocol === "http:" || url.protocol === "https:") && url.host.toLowerCase() === host;
|
|
767
778
|
} catch {
|
|
768
|
-
return
|
|
779
|
+
return false;
|
|
769
780
|
}
|
|
770
781
|
}
|
|
771
|
-
function
|
|
772
|
-
|
|
773
|
-
const left = Buffer.from(actual);
|
|
774
|
-
const right = Buffer.from(expected);
|
|
775
|
-
return left.length === right.length && timingSafeEqual(left, right);
|
|
782
|
+
function isAllowedLocalRequest(req, allowNetworkAccess) {
|
|
783
|
+
return isAllowedHostHeader(header(req, "host"), allowNetworkAccess) && isSameOriginRequest(req);
|
|
776
784
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
785
|
+
|
|
786
|
+
// src/web/ws.ts
|
|
787
|
+
function isWsPath(req) {
|
|
788
|
+
try {
|
|
789
|
+
return new URL(req.url ?? "/", "http://127.0.0.1").pathname === TOKMON_WS_PATH;
|
|
790
|
+
} catch {
|
|
791
|
+
return false;
|
|
792
|
+
}
|
|
780
793
|
}
|
|
781
794
|
function rejectUpgrade(socket, status = 403, message = "Forbidden") {
|
|
782
795
|
try {
|
|
@@ -855,15 +868,11 @@ async function mountWsRpc(server, deps) {
|
|
|
855
868
|
)
|
|
856
869
|
);
|
|
857
870
|
const onUpgrade = (req, socket, head) => {
|
|
858
|
-
if (!
|
|
871
|
+
if (!isAllowedLocalRequest(req, deps.state.config.allowNetworkAccess)) {
|
|
859
872
|
rejectUpgrade(socket);
|
|
860
873
|
return;
|
|
861
874
|
}
|
|
862
875
|
if (!isWsPath(req)) return;
|
|
863
|
-
if (!isAuthorized(req, deps.wsToken)) {
|
|
864
|
-
rejectUpgrade(socket);
|
|
865
|
-
return;
|
|
866
|
-
}
|
|
867
876
|
upgradeHandler(req, socket, head);
|
|
868
877
|
};
|
|
869
878
|
server.prependListener("upgrade", onUpgrade);
|
|
@@ -900,59 +909,38 @@ async function mountWsRpc(server, deps) {
|
|
|
900
909
|
}
|
|
901
910
|
|
|
902
911
|
// src/web/server.ts
|
|
903
|
-
var
|
|
912
|
+
var LOOPBACK_HOST = "127.0.0.1";
|
|
913
|
+
var NETWORK_HOST = "0.0.0.0";
|
|
904
914
|
var DEFAULT_PORT = 4317;
|
|
905
915
|
var MAX_PORT_TRIES = 20;
|
|
906
|
-
function isLoopbackHostHeader(value) {
|
|
907
|
-
if (!value) return false;
|
|
908
|
-
const host = value.trim().toLowerCase();
|
|
909
|
-
return /^(?:127\.0\.0\.1|localhost)(?::\d{1,5})?$/.test(host) || /^\[::1\](?::\d{1,5})?$/.test(host);
|
|
910
|
-
}
|
|
911
|
-
function isLoopbackOrigin(origin) {
|
|
912
|
-
if (!origin || origin === "null") return true;
|
|
913
|
-
try {
|
|
914
|
-
const u = new URL(origin);
|
|
915
|
-
return isLoopbackHostHeader(u.host);
|
|
916
|
-
} catch {
|
|
917
|
-
return false;
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
916
|
function header2(req, name) {
|
|
921
917
|
const value = req.headers[name];
|
|
922
918
|
return Array.isArray(value) ? value[0] : value;
|
|
923
919
|
}
|
|
924
|
-
function
|
|
920
|
+
function tokenMatches(given, expected) {
|
|
925
921
|
if (!given) return false;
|
|
926
922
|
const actual = Buffer.from(given);
|
|
927
923
|
const token = Buffer.from(expected);
|
|
928
|
-
return actual.length === token.length &&
|
|
924
|
+
return actual.length === token.length && timingSafeEqual(actual, token);
|
|
929
925
|
}
|
|
930
|
-
function guardHost(req, res) {
|
|
931
|
-
if (
|
|
926
|
+
function guardHost(req, res, allowNetworkAccess) {
|
|
927
|
+
if (isAllowedHostHeader(header2(req, "host"), allowNetworkAccess)) return true;
|
|
932
928
|
sendJson(res, 403, { error: "forbidden" });
|
|
933
929
|
return false;
|
|
934
930
|
}
|
|
935
|
-
function
|
|
936
|
-
|
|
937
|
-
const v = req.headers[n];
|
|
938
|
-
return Array.isArray(v) ? v[0] : v;
|
|
939
|
-
};
|
|
940
|
-
if (!tokenMatches2(header3("x-tokmon-token"), wsToken2)) {
|
|
941
|
-
sendJson(res, 403, { error: "forbidden" });
|
|
942
|
-
return false;
|
|
943
|
-
}
|
|
944
|
-
if (!isLoopbackOrigin(header3("origin"))) {
|
|
931
|
+
function guardSameOrigin(req, res) {
|
|
932
|
+
if (!isSameOriginRequest(req)) {
|
|
945
933
|
sendJson(res, 403, { error: "forbidden" });
|
|
946
934
|
return false;
|
|
947
935
|
}
|
|
948
936
|
return true;
|
|
949
937
|
}
|
|
950
|
-
function createRouter(engine, state, vite, webRoot,
|
|
938
|
+
function createRouter(engine, state, vite, webRoot, wsToken, version) {
|
|
951
939
|
return (req, res) => {
|
|
952
940
|
const url = req.url || "/";
|
|
953
941
|
const path = url.split("?")[0];
|
|
954
942
|
const method = req.method || "GET";
|
|
955
|
-
if (!guardHost(req, res)) return;
|
|
943
|
+
if (!guardHost(req, res, state.config.allowNetworkAccess)) return;
|
|
956
944
|
if (path === "/api/data") {
|
|
957
945
|
engine.touch();
|
|
958
946
|
sendJson(res, 200, engine.snapshot() ?? { pending: true });
|
|
@@ -964,12 +952,12 @@ function createRouter(engine, state, vite, webRoot, wsToken2, version) {
|
|
|
964
952
|
ready: engine.snapshot() !== null,
|
|
965
953
|
version,
|
|
966
954
|
// Discovery requires this proof; public health checks retain their useful 200 response.
|
|
967
|
-
owner:
|
|
955
|
+
owner: tokenMatches(header2(req, "x-tokmon-token"), wsToken)
|
|
968
956
|
});
|
|
969
957
|
return;
|
|
970
958
|
}
|
|
971
959
|
if (path === "/api/config") {
|
|
972
|
-
if (!
|
|
960
|
+
if (!guardSameOrigin(req, res)) return;
|
|
973
961
|
if (method === "GET") {
|
|
974
962
|
sendJson(res, 200, state.config);
|
|
975
963
|
return;
|
|
@@ -996,7 +984,7 @@ async function startWebServer(opts) {
|
|
|
996
984
|
const version = appVersion();
|
|
997
985
|
const summaryIntervalMs = summaryIntervalFor(state.config);
|
|
998
986
|
const billingIntervalMs = billingIntervalFor(state.config);
|
|
999
|
-
const
|
|
987
|
+
const wsToken = opts.wsToken ?? randomBytes(32).toString("base64url");
|
|
1000
988
|
const log = (msg) => {
|
|
1001
989
|
if (opts.log) process.stdout.write(msg + "\n");
|
|
1002
990
|
};
|
|
@@ -1010,10 +998,11 @@ async function startWebServer(opts) {
|
|
|
1010
998
|
const webRoot = vite ? null : findWebRoot();
|
|
1011
999
|
if (!vite && !webRoot) log(" \u26A0 no dashboard available \u2014 see the page for build/dev instructions");
|
|
1012
1000
|
engine = createDataEngine({ version, config: state.config, tz, summaryIntervalMs, billingIntervalMs, resolved });
|
|
1013
|
-
server.addListener("request", createRouter(engine, state, vite, webRoot,
|
|
1014
|
-
closeWsRpc = await mountWsRpc(server, { engine, state
|
|
1015
|
-
const
|
|
1016
|
-
const
|
|
1001
|
+
server.addListener("request", createRouter(engine, state, vite, webRoot, wsToken, version));
|
|
1002
|
+
closeWsRpc = await mountWsRpc(server, { engine, state });
|
|
1003
|
+
const bindHost = state.config.allowNetworkAccess ? NETWORK_HOST : LOOPBACK_HOST;
|
|
1004
|
+
const port = await listenWithFallback(server, opts.port ?? DEFAULT_PORT, bindHost);
|
|
1005
|
+
const serverUrl = `http://${LOOPBACK_HOST}:${port}`;
|
|
1017
1006
|
if (vite?.warmupRequest) {
|
|
1018
1007
|
try {
|
|
1019
1008
|
await Promise.race([vite.warmupRequest("/src/main.tsx"), delay(5e3)]);
|
|
@@ -1024,9 +1013,7 @@ async function startWebServer(opts) {
|
|
|
1024
1013
|
let stopped = false;
|
|
1025
1014
|
return {
|
|
1026
1015
|
url: serverUrl,
|
|
1027
|
-
browserUrl: browserUrl(serverUrl, wsToken2),
|
|
1028
1016
|
port,
|
|
1029
|
-
wsToken: wsToken2,
|
|
1030
1017
|
snapshot: engine.snapshot,
|
|
1031
1018
|
config: () => state.config,
|
|
1032
1019
|
stop: async () => {
|
|
@@ -1078,11 +1065,11 @@ function closeServer(server, timeoutMs = 1e3) {
|
|
|
1078
1065
|
}
|
|
1079
1066
|
});
|
|
1080
1067
|
}
|
|
1081
|
-
function listenWithFallback(server, startPort) {
|
|
1068
|
+
function listenWithFallback(server, startPort, host) {
|
|
1082
1069
|
return new Promise((resolve, reject) => {
|
|
1083
1070
|
if (startPort === 0) {
|
|
1084
1071
|
server.once("error", reject);
|
|
1085
|
-
server.listen(0,
|
|
1072
|
+
server.listen(0, host, () => {
|
|
1086
1073
|
const addr = server.address();
|
|
1087
1074
|
resolve(typeof addr === "object" && addr ? addr.port : 0);
|
|
1088
1075
|
});
|
|
@@ -1100,7 +1087,7 @@ function listenWithFallback(server, startPort) {
|
|
|
1100
1087
|
reject(err);
|
|
1101
1088
|
}
|
|
1102
1089
|
});
|
|
1103
|
-
server.listen(port,
|
|
1090
|
+
server.listen(port, host, () => resolve(port));
|
|
1104
1091
|
};
|
|
1105
1092
|
attempt();
|
|
1106
1093
|
});
|
|
@@ -116,39 +116,10 @@ function appVersion() {
|
|
|
116
116
|
return "";
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
// src/web/open.ts
|
|
120
|
-
import { spawn } from "child_process";
|
|
121
|
-
import { appendFileSync } from "fs";
|
|
122
|
-
function browserUrl(url, wsToken) {
|
|
123
|
-
return wsToken ? `${url}#tokmonToken=${encodeURIComponent(wsToken)}` : url;
|
|
124
|
-
}
|
|
125
|
-
function openBrowser(url, wsToken) {
|
|
126
|
-
const target = browserUrl(url, wsToken);
|
|
127
|
-
if (process.env.TOKMON_OPENLOG) {
|
|
128
|
-
try {
|
|
129
|
-
appendFileSync(process.env.TOKMON_OPENLOG, target + "\n");
|
|
130
|
-
} catch {
|
|
131
|
-
}
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
try {
|
|
135
|
-
if (process.platform === "darwin") {
|
|
136
|
-
spawn("open", [target], { stdio: "ignore", detached: true }).unref();
|
|
137
|
-
} else if (process.platform === "win32") {
|
|
138
|
-
spawn("cmd", ["/c", "start", "", target], { stdio: "ignore", detached: true }).unref();
|
|
139
|
-
} else {
|
|
140
|
-
spawn("xdg-open", [target], { stdio: "ignore", detached: true }).unref();
|
|
141
|
-
}
|
|
142
|
-
} catch {
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
119
|
export {
|
|
147
120
|
findWebRoot,
|
|
148
121
|
send,
|
|
149
122
|
sendJson,
|
|
150
123
|
serveStatic,
|
|
151
|
-
appVersion
|
|
152
|
-
browserUrl,
|
|
153
|
-
openBrowser
|
|
124
|
+
appVersion
|
|
154
125
|
};
|
package/dist/cli.js
CHANGED
|
@@ -41,14 +41,14 @@ function validateServeArgs(serveArgs) {
|
|
|
41
41
|
}
|
|
42
42
|
async function main() {
|
|
43
43
|
if (subcommand === "__daemon") {
|
|
44
|
-
const { runDaemon } = await import("./daemon-
|
|
44
|
+
const { runDaemon } = await import("./daemon-PVYTXJMS.js");
|
|
45
45
|
await runDaemon(args.slice(1), { foreground: false });
|
|
46
46
|
process.exitCode ??= 0;
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
49
|
if (subcommand === "serve" || subcommand === "web") {
|
|
50
50
|
validateServeArgs(args.slice(1));
|
|
51
|
-
const { runDaemon } = await import("./daemon-
|
|
51
|
+
const { runDaemon } = await import("./daemon-PVYTXJMS.js");
|
|
52
52
|
await runDaemon(args.slice(1), { foreground: true });
|
|
53
53
|
process.exitCode ??= 0;
|
|
54
54
|
return;
|
|
@@ -90,9 +90,9 @@ async function main() {
|
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
|
-
const { loadConfig } = await import("./config-
|
|
93
|
+
const { loadConfig } = await import("./config-QC5QSP3G.js");
|
|
94
94
|
const { resolveGlyphs, setGlyphs } = await import("./glyphs-NKCSZLGO.js");
|
|
95
|
-
const { attachOrSpawn } = await import("./daemon-handle-
|
|
95
|
+
const { attachOrSpawn } = await import("./daemon-handle-Y2N2NA6D.js");
|
|
96
96
|
const config = await loadConfig();
|
|
97
97
|
setGlyphs(resolveGlyphs({
|
|
98
98
|
flag: asciiFlag,
|
|
@@ -103,7 +103,7 @@ async function main() {
|
|
|
103
103
|
}));
|
|
104
104
|
const daemon = await attachOrSpawn();
|
|
105
105
|
const mode = daemon.kind === "spawned" ? "connected" : "degraded";
|
|
106
|
-
const { bootstrapInk } = await import("./bootstrap-ink-
|
|
106
|
+
const { bootstrapInk } = await import("./bootstrap-ink-WA3ENW2M.js");
|
|
107
107
|
await bootstrapInk({ interval, config, daemon, mode });
|
|
108
108
|
}
|
|
109
109
|
void main().catch((error) => {
|