tokmon 0.23.4 → 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.
Files changed (33) hide show
  1. package/README.md +3 -5
  2. package/dist/{bootstrap-ink-BIOLTP7F.js → bootstrap-ink-WA3ENW2M.js} +28 -18
  3. package/dist/{chunk-RM3ZE44Z.js → chunk-3TJVFKXV.js} +1 -1
  4. package/dist/{chunk-DJPUYMZM.js → chunk-5CWOJMAH.js} +2 -0
  5. package/dist/{chunk-2AFQKH5O.js → chunk-IELQ5EY3.js} +2 -1
  6. package/dist/{chunk-2K4L4QQK.js → chunk-MJWFCDBB.js} +62 -75
  7. package/dist/{chunk-Q2DSDWO2.js → chunk-SMPY52EV.js} +1 -30
  8. package/dist/cli.js +5 -5
  9. package/dist/{config-5CNJN3P7.js → config-QC5QSP3G.js} +1 -1
  10. package/dist/{daemon-PP3QAT6E.js → daemon-PVYTXJMS.js} +40 -15
  11. package/dist/{daemon-handle-6F2IB6CU.js → daemon-handle-Y2N2NA6D.js} +8 -9
  12. package/dist/server-XUCQWX6N.js +10 -0
  13. package/dist/web/assets/{Area-BALPhJJp.js → Area-DCES17aZ.js} +1 -1
  14. package/dist/web/assets/{analytics-uHVeOhLh.js → analytics-DDW6zcnj.js} +2 -2
  15. package/dist/web/assets/{breakdown-CR5dNNSG.js → breakdown-D5LPPtqh.js} +1 -1
  16. package/dist/web/assets/{chart-DRkqw0fm.js → chart-bHY7MQV0.js} +1 -1
  17. package/dist/web/assets/{explore-C-bHaX-I.js → explore-DdWB-Pu7.js} +1 -1
  18. package/dist/web/assets/index-C9jxb2o3.js +83 -0
  19. package/dist/web/assets/index-m0KfWnN2.css +1 -0
  20. package/dist/web/assets/{models-Qc2daHCo.js → models-o1wJnG6w.js} +2 -2
  21. package/dist/web/assets/{overview-D4VG2awg.js → overview-wq2uAEk4.js} +2 -2
  22. package/dist/web/assets/{panel-C1CsBs1H.js → panel-DBnK1oy0.js} +1 -1
  23. package/dist/web/assets/{primitives-S4UDgnZq.js → primitives-CBmteetD.js} +1 -1
  24. package/dist/web/assets/settings-sheet-CZ6GEPl3.js +1 -0
  25. package/dist/web/assets/{share-sheet-BpZBju-k.js → share-sheet-DUee3FlK.js} +1 -1
  26. package/dist/web/assets/{timeline-Cp-fyeuK.js → timeline-BxHwOJA_.js} +1 -1
  27. package/dist/web/assets/{use-dialog-trap-DaKbCG1Z.js → use-dialog-trap-30EqxYF-.js} +1 -1
  28. package/dist/web/index.html +2 -2
  29. package/package.json +1 -1
  30. package/dist/server-UHQEOVQH.js +0 -10
  31. package/dist/web/assets/index-C1jQdagn.js +0 -83
  32. package/dist/web/assets/index-DSj-Bxov.css +0 -1
  33. package/dist/web/assets/settings-sheet-DHi4Dt2z.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 open its authenticated local URL.
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,9 +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` only and reads the same data read-only — nothing leaves your machine. It renders instantly from a cached snapshot, then streams live updates over an authenticated WebSocket, 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
-
94
- The URL opened by `W` contains a capability in its fragment. Fragments are not sent with normal page requests or referrers, but keeping it in the address makes the complete URL copyable into another local browser while that daemon is running. Treat that URL as private; after the daemon restarts, press `W` again for a fresh one.
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.
95
93
 
96
94
  ### Overview
97
95
 
@@ -224,7 +222,7 @@ tokmon runs entirely on your machine and reads everything **read-only**:
224
222
 
225
223
  ## How It Works
226
224
 
227
- 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 WebSocket — 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.
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.
228
226
 
229
227
  **Usage & cost**
230
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-2AFQKH5O.js";
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-DJPUYMZM.js";
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 = 8;
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, token) {
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, options.wsToken);
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, wsToken) {
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 || !wsToken) return null;
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, wsToken]);
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, wsToken = null, mode = "degraded" }) {
3308
- const connected = mode === "connected" && baseUrl !== null && wsToken !== 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, connected ? wsToken : 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.browserUrl);
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-UHQEOVQH.js");
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.browserUrl);
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-DJPUYMZM.js";
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-DJPUYMZM.js";
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-Q2DSDWO2.js";
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-2AFQKH5O.js";
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-DJPUYMZM.js";
31
+ } from "./chunk-5CWOJMAH.js";
33
32
 
34
33
  // src/web/server.ts
35
34
  import { createServer } from "http";
36
- import { randomBytes, timingSafeEqual as timingSafeEqual2 } from "crypto";
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/ws.ts
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 isLoopbackHost(value) {
752
- if (!value) return false;
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
- return new URL(req.url ?? "/", "http://127.0.0.1").pathname === TOKMON_WS_PATH;
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 false;
757
+ return null;
762
758
  }
763
759
  }
764
- function wsToken(req) {
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
- return new URL(req.url ?? "/", "http://127.0.0.1").searchParams.get("wsToken");
776
+ const url = new URL(origin);
777
+ return (url.protocol === "http:" || url.protocol === "https:") && url.host.toLowerCase() === host;
767
778
  } catch {
768
- return null;
779
+ return false;
769
780
  }
770
781
  }
771
- function tokenMatches(actual, expected) {
772
- if (!actual) return false;
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
- function isAuthorized(req, token) {
778
- if (!isLoopbackHost(header(req, "host"))) return false;
779
- return tokenMatches(wsToken(req), token);
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 (!isLoopbackHost(header(req, "host"))) {
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 HOST = "127.0.0.1";
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 tokenMatches2(given, expected) {
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 && timingSafeEqual2(actual, token);
924
+ return actual.length === token.length && timingSafeEqual(actual, token);
929
925
  }
930
- function guardHost(req, res) {
931
- if (isLoopbackHostHeader(header2(req, "host"))) return true;
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 guardPrivileged(req, res, wsToken2) {
936
- const header3 = (n) => {
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, wsToken2, version) {
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: tokenMatches2(header2(req, "x-tokmon-token"), wsToken2)
955
+ owner: tokenMatches(header2(req, "x-tokmon-token"), wsToken)
968
956
  });
969
957
  return;
970
958
  }
971
959
  if (path === "/api/config") {
972
- if (!guardPrivileged(req, res, wsToken2)) return;
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 wsToken2 = opts.wsToken ?? randomBytes(32).toString("base64url");
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, wsToken2, version));
1014
- closeWsRpc = await mountWsRpc(server, { engine, state, wsToken: wsToken2 });
1015
- const port = await listenWithFallback(server, opts.port ?? DEFAULT_PORT);
1016
- const serverUrl = `http://${HOST}:${port}`;
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, HOST, () => {
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, HOST, () => resolve(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-PP3QAT6E.js");
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-PP3QAT6E.js");
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-5CNJN3P7.js");
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-6F2IB6CU.js");
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-BIOLTP7F.js");
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) => {
@@ -24,7 +24,7 @@ import {
24
24
  saveConfigSync,
25
25
  slugify,
26
26
  snapshotCacheFile
27
- } from "./chunk-DJPUYMZM.js";
27
+ } from "./chunk-5CWOJMAH.js";
28
28
  export {
29
29
  ACCENT_COLORS,
30
30
  COLOR_PALETTE,