tokmon 0.28.0 → 0.28.2

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 (39) hide show
  1. package/README.md +1 -1
  2. package/dist/{bootstrap-ink-5LQMSU4B.js → bootstrap-ink-IMT3S5HU.js} +120 -9
  3. package/dist/{chunk-KCY53RUE.js → chunk-44ZSQEOS.js} +2 -2
  4. package/dist/{chunk-JK3U54G7.js → chunk-6ZEW2M6P.js} +167 -39
  5. package/dist/{chunk-E2YXYU73.js → chunk-HP5UZCXP.js} +29 -0
  6. package/dist/{chunk-QM5E5RJZ.js → chunk-J3JW3RFP.js} +10 -7
  7. package/dist/{chunk-IWKSA64G.js → chunk-LCAHTRGP.js} +15 -281
  8. package/dist/{chunk-AMD4PXDG.js → chunk-LKYOWBD4.js} +15 -15
  9. package/dist/{chunk-QOA6KOJQ.js → chunk-M7XMV36F.js} +1 -1
  10. package/dist/chunk-WUK3MQUB.js +249 -0
  11. package/dist/{cli-command-HXKWB6IP.js → cli-command-DCENRCO3.js} +6 -6
  12. package/dist/cli.js +6 -6
  13. package/dist/{config-2PXUENQL.js → config-HXFJTNLM.js} +5 -1
  14. package/dist/{daemon-JCQF645I.js → daemon-6Y4O7NXS.js} +36 -14
  15. package/dist/daemon-handle-GVCYVDPO.js +10 -0
  16. package/dist/server-GXXOWUVZ.js +11 -0
  17. package/dist/web/assets/{Area-DgGVYaNT.js → Area-CMXvOw33.js} +1 -1
  18. package/dist/web/assets/{analytics-D5EvgF4d.js → analytics-DqdZXOJL.js} +2 -2
  19. package/dist/web/assets/{breakdown-CGdJkAHx.js → breakdown-kOSSJ3mI.js} +1 -1
  20. package/dist/web/assets/{chart-D4KHj2-J.js → chart-cZoLgpmG.js} +1 -1
  21. package/dist/web/assets/{explore-B2QnGzfO.js → explore-vcsM8K9I.js} +1 -1
  22. package/dist/web/assets/index-BErCXT7f.css +1 -0
  23. package/dist/web/assets/index-DH3dOnbg.js +83 -0
  24. package/dist/web/assets/{models-D7w7JJCX.js → models-B5ikm83_.js} +2 -2
  25. package/dist/web/assets/{overview-BPz0j397.js → overview-DD1kP8CP.js} +2 -2
  26. package/dist/web/assets/{panel-Da3rkvLu.js → panel-DTQBwExW.js} +1 -1
  27. package/dist/web/assets/{primitives-B8eGD8S_.js → primitives-C36qJlA2.js} +1 -1
  28. package/dist/web/assets/settings-sheet-_i2zTQjf.js +1 -0
  29. package/dist/web/assets/{share-sheet-BXrrMoLk.js → share-sheet-BLHmrFbm.js} +2 -2
  30. package/dist/web/assets/{timeline-CiGI4EmA.js → timeline-B4uG-Emo.js} +1 -1
  31. package/dist/web/assets/{use-dialog-trap-Dhs8W7s1.js → use-dialog-trap-bfaXEMz9.js} +1 -1
  32. package/dist/web/index.html +2 -2
  33. package/package.json +1 -1
  34. package/dist/chunk-SMPY52EV.js +0 -125
  35. package/dist/daemon-handle-2GVZT55B.js +0 -10
  36. package/dist/server-BJOXAGRI.js +0 -11
  37. package/dist/web/assets/index-C0txIEHK.js +0 -83
  38. package/dist/web/assets/index-NnKaHxPO.css +0 -1
  39. package/dist/web/assets/settings-sheet-D6tnWqmt.js +0 -1
package/README.md CHANGED
@@ -105,7 +105,7 @@ tokmon serve --port 8080
105
105
  tokmon serve --no-open # don't auto-open the browser
106
106
  ```
107
107
 
108
- 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. Press `R` or use the visible **Refresh** control to update it. 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.
108
+ 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. When publishing the dashboard through an ingress or reverse proxy, add each exact DNS name to **Allowed hosts** in settings (for example, `tokmon.example.com`); IP hosts remain allowed in LAN mode. The dashboard renders instantly from a cached snapshot, then streams live updates and goes idle when no tab is open. Press `R` or use the visible **Refresh** control to update it. 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.
109
109
 
110
110
  ### Overview
111
111
 
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createDaemonRpcClient
4
- } from "./chunk-QOA6KOJQ.js";
4
+ } from "./chunk-M7XMV36F.js";
5
5
  import {
6
6
  accountsByProvider,
7
7
  buildAccounts,
8
8
  fetchPeak,
9
9
  modelColor
10
- } from "./chunk-KCY53RUE.js";
10
+ } from "./chunk-44ZSQEOS.js";
11
11
  import {
12
12
  PROVIDERS,
13
13
  PROVIDER_ORDER,
@@ -24,7 +24,8 @@ import {
24
24
  time,
25
25
  tokens,
26
26
  withTimeout
27
- } from "./chunk-IWKSA64G.js";
27
+ } from "./chunk-LCAHTRGP.js";
28
+ import "./chunk-WUK3MQUB.js";
28
29
  import {
29
30
  COLOR_PALETTE,
30
31
  DEFAULTS,
@@ -33,13 +34,14 @@ import {
33
34
  getTrackedAccountRows,
34
35
  isValidTimezone,
35
36
  loadConfig,
37
+ normalizeAllowedHost,
36
38
  normalizeConfig,
37
39
  pickAccentColor,
38
40
  redactEmail,
39
41
  sanitizeTyped,
40
42
  saveConfigSync,
41
43
  snapshotCacheFile
42
- } from "./chunk-E2YXYU73.js";
44
+ } from "./chunk-HP5UZCXP.js";
43
45
  import {
44
46
  glyphs
45
47
  } from "./chunk-RF4GGQGM.js";
@@ -589,7 +591,7 @@ import { Box as Box4, Text as Text4 } from "ink";
589
591
  import { memo as memo3 } from "react";
590
592
  import { Box as Box3, Text as Text3 } from "ink";
591
593
  import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
592
- var GENERAL_ROWS = 10;
594
+ var GENERAL_ROWS = 11;
593
595
  var SETTINGS_TABS = ["general", "providers", "accounts"];
594
596
  var SETTINGS_TAB_LABELS = {
595
597
  general: "General",
@@ -604,6 +606,9 @@ var SettingsView = memo3(function SettingsView2({
604
606
  tzEdit,
605
607
  tzCaret,
606
608
  tzError,
609
+ allowedHostsEdit,
610
+ allowedHostsCaret,
611
+ allowedHostsError,
607
612
  resolvedTz,
608
613
  accountForm,
609
614
  activeAccountId,
@@ -612,6 +617,7 @@ var SettingsView = memo3(function SettingsView2({
612
617
  }) {
613
618
  if (accountForm) return /* @__PURE__ */ jsx3(AccountFormView, { form: accountForm, accounts: config.accounts });
614
619
  const editingTz = tzEdit !== null;
620
+ const editingAllowedHosts = allowedHostsEdit !== null;
615
621
  const tzDisplay = config.timezone === null ? `System (${resolvedTz})` : config.timezone;
616
622
  const tabFocused = cursor < 0;
617
623
  return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginTop: 1, children: [
@@ -685,8 +691,17 @@ var SettingsView = memo3(function SettingsView2({
685
691
  ] })
686
692
  ] }),
687
693
  /* @__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" }) }),
694
+ /* @__PURE__ */ jsx3(Row, { cursor, idx: 9, label: "Allowed hosts", children: editingAllowedHosts ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
695
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "[" }),
696
+ /* @__PURE__ */ jsx3(CaretText, { value: allowedHostsEdit ?? "", caret: allowedHostsCaret, color: "cyan" }),
697
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "]" })
698
+ ] }) : /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: config.allowedHosts.join(", ") || "none" }) }),
699
+ cursor === 9 && allowedHostsError && /* @__PURE__ */ jsxs3(Text3, { color: "red", children: [
700
+ " ",
701
+ allowedHostsError
702
+ ] }),
688
703
  config.allowNetworkAccess && /* @__PURE__ */ jsx3(Text3, { color: "red", children: " Warning: dashboard data and settings will be exposed to your local network after daemon restart." }),
689
- /* @__PURE__ */ jsxs3(Row, { cursor, idx: 9, label: "Reset times", children: [
704
+ /* @__PURE__ */ jsxs3(Row, { cursor, idx: 10, label: "Reset times", children: [
690
705
  /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
691
706
  glyphs().caretL,
692
707
  " "
@@ -779,6 +794,12 @@ var SettingsView = memo3(function SettingsView2({
779
794
  " Enter save ",
780
795
  glyphs().middot,
781
796
  " Esc cancel"
797
+ ] }) : editingAllowedHosts ? /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
798
+ "comma-separated exact DNS names ",
799
+ glyphs().middot,
800
+ " Enter save ",
801
+ glyphs().middot,
802
+ " Esc cancel"
782
803
  ] }) : activeTab === "providers" ? /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
783
804
  glyphs().arrowU,
784
805
  glyphs().arrowD,
@@ -1992,6 +2013,56 @@ function handleTimezoneEditor(input, key, editor, text, updateConfig) {
1992
2013
  if (clean) text.insert(clean);
1993
2014
  }
1994
2015
  }
2016
+ function handleAllowedHostsEditor(input, key, editor, text, updateConfig) {
2017
+ const { value, setValue, setError, setCaret, valueRef, caretRef } = editor;
2018
+ if (value === null) return;
2019
+ if (key.escape) {
2020
+ setValue(null);
2021
+ setError(null);
2022
+ return;
2023
+ }
2024
+ if (key.return) {
2025
+ const parts = value.split(",").map((part) => part.trim()).filter(Boolean);
2026
+ const normalized = parts.map(normalizeAllowedHost);
2027
+ if (normalized.some((host) => host === null)) {
2028
+ setError("Use exact DNS names without a scheme, port, path, or wildcard");
2029
+ return;
2030
+ }
2031
+ updateConfig((config) => ({ ...config, allowedHosts: [...new Set(normalized)] }));
2032
+ setValue(null);
2033
+ setError(null);
2034
+ return;
2035
+ }
2036
+ if (key.leftArrow) {
2037
+ setCaret((caret) => clampCaret(caret - 1, value.length));
2038
+ return;
2039
+ }
2040
+ if (key.rightArrow) {
2041
+ setCaret((caret) => clampCaret(caret + 1, value.length));
2042
+ return;
2043
+ }
2044
+ if (key.ctrl && input === "a") {
2045
+ setCaret(0);
2046
+ return;
2047
+ }
2048
+ if (key.ctrl && input === "e") {
2049
+ setCaret(value.length);
2050
+ return;
2051
+ }
2052
+ if (key.backspace || key.delete) {
2053
+ const result = spliceBackspace(valueRef.current, caretRef.current);
2054
+ valueRef.current = result.value;
2055
+ caretRef.current = result.caret;
2056
+ setValue(result.value);
2057
+ setCaret(result.caret);
2058
+ setError(null);
2059
+ return;
2060
+ }
2061
+ if (text.isPrintable(input, key)) {
2062
+ const clean = sanitizeTyped(input);
2063
+ if (clean) text.insert(clean);
2064
+ }
2065
+ }
1995
2066
  function handleSearchEditor(input, key, table, text) {
1996
2067
  const { search, setSearch, setSearchMode, setSearchCaret, searchValueRef, searchCaretRef } = table;
1997
2068
  if (key.return || key.escape) {
@@ -2064,6 +2135,8 @@ function handleSettings(input, key, ctx) {
2064
2135
  setCursor(-1);
2065
2136
  timezoneEditor.setValue(null);
2066
2137
  timezoneEditor.setError(null);
2138
+ ctx.allowedHostsEditor.setValue(null);
2139
+ ctx.allowedHostsEditor.setError(null);
2067
2140
  };
2068
2141
  const rowCount = tab === "general" ? GENERAL_ROWS : tab === "providers" ? PROVIDER_ORDER.length : trackedAccounts.length + 1;
2069
2142
  if (key.tab) {
@@ -2194,7 +2267,14 @@ function handleSettings(input, key, ctx) {
2194
2267
  updateConfig((current) => ({ ...current, allowNetworkAccess: !current.allowNetworkAccess }));
2195
2268
  return;
2196
2269
  }
2197
- if (index === 9 && (inputKey.leftArrow || inputKey.rightArrow || inputKey.return)) {
2270
+ if (index === 9 && inputKey.return) {
2271
+ const initial = config.allowedHosts.join(", ");
2272
+ context.allowedHostsEditor.setValue(initial);
2273
+ context.allowedHostsEditor.setCaret(initial.length);
2274
+ context.allowedHostsEditor.setError(null);
2275
+ return;
2276
+ }
2277
+ if (index === 10 && (inputKey.leftArrow || inputKey.rightArrow || inputKey.return)) {
2198
2278
  updateConfig((current) => ({ ...current, resetDisplay: current.resetDisplay === "relative" ? "absolute" : "relative" }));
2199
2279
  }
2200
2280
  }
@@ -2357,7 +2437,7 @@ function handleTerminalFocusInput(input) {
2357
2437
  return true;
2358
2438
  }
2359
2439
  function isRefreshAllShortcut(input, mode) {
2360
- return (input === "r" || input === "R") && !mode.unavailable && !mode.showPicker && !mode.editingAccount && !mode.editingTimezone && !mode.editingSearch;
2440
+ return (input === "r" || input === "R") && !mode.unavailable && !mode.showPicker && !mode.editingAccount && !mode.editingTimezone && !mode.editingAllowedHosts && !mode.editingSearch;
2361
2441
  }
2362
2442
  function handleOnboarding(input, key, ctx) {
2363
2443
  const { onboarding, global } = ctx;
@@ -2386,11 +2466,13 @@ function handleOnboarding(input, key, ctx) {
2386
2466
  function handleKey(input, key, ctx) {
2387
2467
  const editingAccount = ctx.settings.show && ctx.accountEditor.form !== null;
2388
2468
  const editingTimezone = ctx.settings.show && ctx.timezoneEditor.value !== null;
2469
+ const editingAllowedHosts = ctx.settings.show && ctx.allowedHostsEditor.value !== null;
2389
2470
  const editingSearch = ctx.table.tab === 1 && ctx.table.searchMode;
2390
2471
  if (isRefreshAllShortcut(input, {
2391
2472
  showPicker: ctx.onboarding.show,
2392
2473
  editingAccount,
2393
2474
  editingTimezone,
2475
+ editingAllowedHosts,
2394
2476
  editingSearch,
2395
2477
  unavailable: !ctx.global.configReady
2396
2478
  })) {
@@ -2409,6 +2491,10 @@ function handleKey(input, key, ctx) {
2409
2491
  handleTimezoneEditor(input, key, ctx.timezoneEditor, ctx.textInput, ctx.global.updateConfig);
2410
2492
  return;
2411
2493
  }
2494
+ if (editingAllowedHosts) {
2495
+ handleAllowedHostsEditor(input, key, ctx.allowedHostsEditor, ctx.textInput, ctx.global.updateConfig);
2496
+ return;
2497
+ }
2412
2498
  if (editingSearch) {
2413
2499
  handleSearchEditor(input, key, ctx.table, ctx.textInput);
2414
2500
  return;
@@ -3142,6 +3228,9 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
3142
3228
  const [tzEdit, setTzEdit] = useState10(null);
3143
3229
  const [tzError, setTzError] = useState10(null);
3144
3230
  const [tzCaret, setTzCaret] = useState10(0);
3231
+ const [allowedHostsEdit, setAllowedHostsEdit] = useState10(null);
3232
+ const [allowedHostsError, setAllowedHostsError] = useState10(null);
3233
+ const [allowedHostsCaret, setAllowedHostsCaret] = useState10(0);
3145
3234
  const [searchCaret, setSearchCaret] = useState10(0);
3146
3235
  const [onboardSel, setOnboardSel] = useState10(null);
3147
3236
  const [onboardCursor, setOnboardCursor] = useState10(0);
@@ -3169,6 +3258,8 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
3169
3258
  const dashPageCountRef = useRef8(1);
3170
3259
  const tzValueRef = useRef8("");
3171
3260
  const tzCaretRef = useRef8(0);
3261
+ const allowedHostsValueRef = useRef8("");
3262
+ const allowedHostsCaretRef = useRef8(0);
3172
3263
  const searchValueRef = useRef8("");
3173
3264
  const searchCaretRef = useRef8(0);
3174
3265
  const accountsKey = useMemo2(() => accounts.map(acctKey).join("|"), [accounts]);
@@ -3274,6 +3365,8 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
3274
3365
  dashPageCountRef.current = dashPageCount;
3275
3366
  tzValueRef.current = tzEdit ?? "";
3276
3367
  tzCaretRef.current = clampCaret(tzCaret, (tzEdit ?? "").length);
3368
+ allowedHostsValueRef.current = allowedHostsEdit ?? "";
3369
+ allowedHostsCaretRef.current = clampCaret(allowedHostsCaret, (allowedHostsEdit ?? "").length);
3277
3370
  searchValueRef.current = search;
3278
3371
  searchCaretRef.current = clampCaret(searchCaret, search.length);
3279
3372
  const isPrintable = (input, key) => !!input && !key.ctrl && !key.meta && !isPasteInput(input);
@@ -3291,6 +3384,13 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
3291
3384
  setTzEdit(r.value);
3292
3385
  setTzCaret(r.caret);
3293
3386
  setTzError(null);
3387
+ } else if (showSettings && allowedHostsEdit !== null) {
3388
+ const r = spliceInsert(allowedHostsValueRef.current, allowedHostsCaretRef.current, text);
3389
+ allowedHostsValueRef.current = r.value;
3390
+ allowedHostsCaretRef.current = r.caret;
3391
+ setAllowedHostsEdit(r.value);
3392
+ setAllowedHostsCaret(r.caret);
3393
+ setAllowedHostsError(null);
3294
3394
  } else if (tab === 1 && searchMode) {
3295
3395
  const r = spliceInsert(searchValueRef.current, searchCaretRef.current, text);
3296
3396
  searchValueRef.current = r.value;
@@ -3461,7 +3561,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
3461
3561
  if (webStartingRef.current) return;
3462
3562
  webStartingRef.current = true;
3463
3563
  try {
3464
- const { startWebServer } = await import("./server-BJOXAGRI.js");
3564
+ const { startWebServer } = await import("./server-GXXOWUVZ.js");
3465
3565
  const ctrl = await startWebServer({ config: cfg, log: false });
3466
3566
  webRef.current = ctrl;
3467
3567
  openUrl(ctrl.url);
@@ -3535,6 +3635,14 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
3535
3635
  valueRef: tzValueRef,
3536
3636
  caretRef: tzCaretRef
3537
3637
  },
3638
+ allowedHostsEditor: {
3639
+ value: allowedHostsEdit,
3640
+ setValue: setAllowedHostsEdit,
3641
+ setError: setAllowedHostsError,
3642
+ setCaret: setAllowedHostsCaret,
3643
+ valueRef: allowedHostsValueRef,
3644
+ caretRef: allowedHostsCaretRef
3645
+ },
3538
3646
  textInput: { isPrintable, insert: insertText },
3539
3647
  settings: {
3540
3648
  show: showSettings,
@@ -3674,6 +3782,9 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
3674
3782
  tzEdit,
3675
3783
  tzCaret,
3676
3784
  tzError,
3785
+ allowedHostsEdit,
3786
+ allowedHostsCaret,
3787
+ allowedHostsError,
3677
3788
  resolvedTz: tz,
3678
3789
  accountForm,
3679
3790
  activeAccountId: cfg.activeAccountId,
@@ -5,11 +5,11 @@ import {
5
5
  identityFromIdToken,
6
6
  readClaudeIdentity,
7
7
  readJson
8
- } from "./chunk-IWKSA64G.js";
8
+ } from "./chunk-LCAHTRGP.js";
9
9
  import {
10
10
  expandHome,
11
11
  slugify
12
- } from "./chunk-E2YXYU73.js";
12
+ } from "./chunk-HP5UZCXP.js";
13
13
 
14
14
  // src/accounts.ts
15
15
  import { existsSync, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
@@ -1,28 +1,23 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- appVersion,
4
- findWebRoot,
5
- send,
6
- sendJson,
7
- serveStatic
8
- } from "./chunk-SMPY52EV.js";
9
2
  import {
10
3
  buildAccounts,
11
4
  colorHex,
12
5
  fetchPeak,
13
6
  namedHex
14
- } from "./chunk-KCY53RUE.js";
7
+ } from "./chunk-44ZSQEOS.js";
15
8
  import {
16
9
  PROVIDERS,
10
+ detectProviders,
11
+ resolveTimezone,
12
+ withTimeout
13
+ } from "./chunk-LCAHTRGP.js";
14
+ import {
17
15
  TOKMON_CAPABILITIES,
18
16
  TOKMON_PROTOCOL_VERSION,
19
17
  TOKMON_WS_METHODS,
20
18
  TOKMON_WS_PATH,
21
- TokmonRpcGroup,
22
- detectProviders,
23
- resolveTimezone,
24
- withTimeout
25
- } from "./chunk-IWKSA64G.js";
19
+ TokmonRpcGroup
20
+ } from "./chunk-WUK3MQUB.js";
26
21
  import {
27
22
  cacheDir,
28
23
  expandHome,
@@ -30,7 +25,7 @@ import {
30
25
  normalizeConfig,
31
26
  saveConfig,
32
27
  snapshotCacheFile
33
- } from "./chunk-E2YXYU73.js";
28
+ } from "./chunk-HP5UZCXP.js";
34
29
 
35
30
  // src/web/server.ts
36
31
  import { createServer } from "http";
@@ -118,11 +113,127 @@ function tzFor(config) {
118
113
  return resolveTimezone(config.timezone);
119
114
  }
120
115
 
116
+ // src/web/static.ts
117
+ import { createReadStream, existsSync, readFileSync } from "fs";
118
+ import { stat } from "fs/promises";
119
+ import { fileURLToPath } from "url";
120
+ import { extname, join, normalize, sep } from "path";
121
+ var MIME = {
122
+ ".html": "text/html; charset=utf-8",
123
+ ".js": "text/javascript; charset=utf-8",
124
+ ".mjs": "text/javascript; charset=utf-8",
125
+ ".css": "text/css; charset=utf-8",
126
+ ".json": "application/json; charset=utf-8",
127
+ ".svg": "image/svg+xml",
128
+ ".png": "image/png",
129
+ ".jpg": "image/jpeg",
130
+ ".jpeg": "image/jpeg",
131
+ ".gif": "image/gif",
132
+ ".ico": "image/x-icon",
133
+ ".woff": "font/woff",
134
+ ".woff2": "font/woff2",
135
+ ".ttf": "font/ttf",
136
+ ".otf": "font/otf",
137
+ ".map": "application/json; charset=utf-8",
138
+ ".webmanifest": "application/manifest+json"
139
+ };
140
+ function findWebRoot() {
141
+ const candidates = ["./web/", "../web/", "../dist/web/", "../../dist/web/"];
142
+ for (const rel of candidates) {
143
+ try {
144
+ const dir = fileURLToPath(new URL(rel, import.meta.url));
145
+ if (existsSync(join(dir, "index.html"))) return dir.replace(/[\\/]+$/, "");
146
+ } catch {
147
+ }
148
+ }
149
+ return null;
150
+ }
151
+ function resolveStaticPath(webRoot, urlPath) {
152
+ let clean;
153
+ try {
154
+ clean = decodeURIComponent(urlPath.split("?")[0]);
155
+ } catch {
156
+ return null;
157
+ }
158
+ const rel = normalize(clean).replace(/^(\.\.[/\\])+/, "").replace(/^[/\\]+/, "");
159
+ const full = join(webRoot, rel);
160
+ if (full !== webRoot && !full.startsWith(webRoot + sep)) return null;
161
+ return full;
162
+ }
163
+ function send(res, status, type, body) {
164
+ res.writeHead(status, {
165
+ "Content-Type": type,
166
+ "Cache-Control": "no-store",
167
+ "Referrer-Policy": "no-referrer",
168
+ "X-Content-Type-Options": "nosniff"
169
+ });
170
+ res.end(body);
171
+ }
172
+ function sendJson(res, status, data) {
173
+ send(res, status, "application/json; charset=utf-8", JSON.stringify(data));
174
+ }
175
+ function serveStatic(webRoot, urlPath, res) {
176
+ const path = urlPath.split("?")[0];
177
+ const filePath = resolveStaticPath(webRoot, path === "/" ? "/index.html" : path);
178
+ if (!filePath) {
179
+ send(res, 403, "text/plain", "forbidden");
180
+ return;
181
+ }
182
+ void stat(filePath).then((st) => {
183
+ if (st.isFile()) {
184
+ const type = MIME[extname(filePath).toLowerCase()] || "application/octet-stream";
185
+ const immutable = filePath.includes(`${sep}assets${sep}`);
186
+ res.writeHead(200, {
187
+ "Content-Type": type,
188
+ "Cache-Control": immutable ? "public, max-age=31536000, immutable" : "no-cache",
189
+ "Referrer-Policy": "no-referrer",
190
+ "X-Content-Type-Options": "nosniff"
191
+ });
192
+ createReadStream(filePath).pipe(res);
193
+ } else {
194
+ throw new Error("not a file");
195
+ }
196
+ }).catch(() => {
197
+ if (extname(path)) {
198
+ send(res, 404, "text/plain", "not found");
199
+ return;
200
+ }
201
+ const indexPath = join(webRoot, "index.html");
202
+ if (!existsSync(indexPath)) {
203
+ send(res, 404, "text/plain", "not found");
204
+ return;
205
+ }
206
+ res.writeHead(200, {
207
+ "Content-Type": MIME[".html"],
208
+ "Cache-Control": "no-cache",
209
+ "Referrer-Policy": "no-referrer",
210
+ "X-Content-Type-Options": "nosniff"
211
+ });
212
+ createReadStream(indexPath).on("error", () => {
213
+ try {
214
+ res.destroy();
215
+ } catch {
216
+ }
217
+ }).pipe(res);
218
+ });
219
+ }
220
+ function appVersion() {
221
+ for (const rel of ["../package.json", "../../package.json"]) {
222
+ try {
223
+ const p = fileURLToPath(new URL(rel, import.meta.url));
224
+ const pkg = JSON.parse(readFileSync(p, "utf-8"));
225
+ if (typeof pkg.version === "string") return pkg.version;
226
+ } catch {
227
+ }
228
+ }
229
+ return "";
230
+ }
231
+
121
232
  // src/web/vite-dev.ts
122
- import { existsSync } from "fs";
123
- import { fileURLToPath, pathToFileURL } from "url";
233
+ import { existsSync as existsSync2 } from "fs";
234
+ import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
124
235
  import { createRequire } from "module";
125
- import { join } from "path";
236
+ import { join as join2 } from "path";
126
237
  function isDevMode() {
127
238
  const forced = process.env.TOKMON_WEB_MODE;
128
239
  if (forced === "dev") return true;
@@ -132,8 +243,8 @@ function isDevMode() {
132
243
  function findWebSource() {
133
244
  for (const rel of ["../../web/", "../web/", "./web/"]) {
134
245
  try {
135
- const dir = fileURLToPath(new URL(rel, import.meta.url));
136
- if (existsSync(join(dir, "vite.config.ts")) && existsSync(join(dir, "index.html"))) {
246
+ const dir = fileURLToPath2(new URL(rel, import.meta.url));
247
+ if (existsSync2(join2(dir, "vite.config.ts")) && existsSync2(join2(dir, "index.html"))) {
137
248
  return dir.replace(/[\\/]+$/, "");
138
249
  }
139
250
  } catch {
@@ -148,12 +259,12 @@ async function createViteDevServer(httpServer, log) {
148
259
  return null;
149
260
  }
150
261
  try {
151
- const req = createRequire(pathToFileURL(join(root, "package.json")).href);
262
+ const req = createRequire(pathToFileURL(join2(root, "package.json")).href);
152
263
  const vitePath = req.resolve("vite");
153
264
  const vite = await import(pathToFileURL(vitePath).href);
154
265
  const dev = await vite.createServer({
155
266
  root,
156
- configFile: join(root, "vite.config.ts"),
267
+ configFile: join2(root, "vite.config.ts"),
157
268
  server: { middlewareMode: true, hmr: { server: httpServer } },
158
269
  appType: "spa",
159
270
  clearScreen: false,
@@ -177,7 +288,7 @@ code{color:#e6b450}</style></head><body>
177
288
  </body></html>`;
178
289
 
179
290
  // src/web/data-engine.ts
180
- import { readFileSync, writeFileSync, mkdirSync, renameSync } from "fs";
291
+ import { readFileSync as readFileSync2, writeFileSync, mkdirSync, renameSync } from "fs";
181
292
 
182
293
  // src/web/refresh-queue.ts
183
294
  function deferred() {
@@ -335,7 +446,7 @@ function createDataEngine(opts) {
335
446
  });
336
447
  const hydrateFromCache = () => {
337
448
  try {
338
- const cached = JSON.parse(readFileSync(snapshotCacheFile(), "utf-8"));
449
+ const cached = JSON.parse(readFileSync2(snapshotCacheFile(), "utf-8"));
339
450
  if (!cached || !Array.isArray(cached.accounts)) return;
340
451
  for (const a of cached.accounts) {
341
452
  if (a.dashboard || a.table) {
@@ -703,11 +814,11 @@ import { Effect, Exit, Layer, Queue, Scope, Stream } from "effect";
703
814
  import { RpcSerialization, RpcServer } from "effect/unstable/rpc";
704
815
 
705
816
  // src/web/fs.ts
706
- import { readdir, stat, realpath } from "fs/promises";
817
+ import { readdir, stat as stat2, realpath } from "fs/promises";
707
818
  import { homedir } from "os";
708
- import { join as join2, resolve as resolvePath, isAbsolute, sep } from "path";
819
+ import { join as join3, resolve as resolvePath, isAbsolute, sep as sep2 } from "path";
709
820
  function isContained(root, target) {
710
- return target === root || target.startsWith(root + sep);
821
+ return target === root || target.startsWith(root + sep2);
711
822
  }
712
823
  function parentFor(root, abs) {
713
824
  const parentResolved = resolvePath(abs, "..");
@@ -727,7 +838,7 @@ async function listHomeDirectory(rawPath) {
727
838
  const abs = isContained(root, real) ? real : root;
728
839
  let st;
729
840
  try {
730
- st = await stat(abs);
841
+ st = await stat2(abs);
731
842
  } catch {
732
843
  return { path: abs, parent: parentFor(root, abs), entries: [] };
733
844
  }
@@ -742,7 +853,7 @@ async function listHomeDirectory(rawPath) {
742
853
  for (const d of dirents) {
743
854
  if (d.name.startsWith(".")) continue;
744
855
  let dir = d.isDirectory();
745
- const full = join2(abs, d.name);
856
+ const full = join3(abs, d.name);
746
857
  if (d.isSymbolicLink()) {
747
858
  let real2;
748
859
  try {
@@ -752,7 +863,7 @@ async function listHomeDirectory(rawPath) {
752
863
  }
753
864
  if (!isContained(root, real2)) continue;
754
865
  try {
755
- dir = (await stat(full)).isDirectory();
866
+ dir = (await stat2(full)).isDirectory();
756
867
  } catch {
757
868
  continue;
758
869
  }
@@ -783,11 +894,11 @@ function isLoopbackHostHeader(value) {
783
894
  const hostname = hostnameFromHost(value);
784
895
  return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
785
896
  }
786
- function isAllowedHostHeader(value, allowNetworkAccess) {
897
+ function isAllowedHostHeader(value, allowNetworkAccess, allowedHosts = []) {
787
898
  if (isLoopbackHostHeader(value)) return true;
788
899
  if (!allowNetworkAccess) return false;
789
900
  const hostname = hostnameFromHost(value);
790
- return hostname !== null && isIP(hostname) !== 0;
901
+ return hostname !== null && (isIP(hostname) !== 0 || allowedHosts.includes(hostname));
791
902
  }
792
903
  function isSameOriginRequest(req) {
793
904
  const origin = header(req, "origin");
@@ -801,8 +912,8 @@ function isSameOriginRequest(req) {
801
912
  return false;
802
913
  }
803
914
  }
804
- function isAllowedLocalRequest(req, allowNetworkAccess) {
805
- return isAllowedHostHeader(header(req, "host"), allowNetworkAccess) && isSameOriginRequest(req);
915
+ function isAllowedLocalRequest(req, allowNetworkAccess, allowedHosts = []) {
916
+ return isAllowedHostHeader(header(req, "host"), allowNetworkAccess, allowedHosts) && isSameOriginRequest(req);
806
917
  }
807
918
 
808
919
  // src/web/ws.ts
@@ -890,7 +1001,7 @@ async function mountWsRpc(server, deps) {
890
1001
  )
891
1002
  );
892
1003
  const onUpgrade = (req, socket, head) => {
893
- if (!isAllowedLocalRequest(req, deps.state.config.allowNetworkAccess)) {
1004
+ if (!isAllowedLocalRequest(req, deps.state.config.allowNetworkAccess, deps.state.config.allowedHosts)) {
894
1005
  rejectUpgrade(socket);
895
1006
  return;
896
1007
  }
@@ -945,8 +1056,8 @@ function tokenMatches(given, expected) {
945
1056
  const token = Buffer.from(expected);
946
1057
  return actual.length === token.length && timingSafeEqual(actual, token);
947
1058
  }
948
- function guardHost(req, res, allowNetworkAccess) {
949
- if (isAllowedHostHeader(header2(req, "host"), allowNetworkAccess)) return true;
1059
+ function guardHost(req, res, config) {
1060
+ if (isAllowedHostHeader(header2(req, "host"), config.allowNetworkAccess, config.allowedHosts)) return true;
950
1061
  sendJson(res, 403, { error: "forbidden" });
951
1062
  return false;
952
1063
  }
@@ -957,12 +1068,12 @@ function guardSameOrigin(req, res) {
957
1068
  }
958
1069
  return true;
959
1070
  }
960
- function createRouter(engine, state, vite, webRoot, wsToken, version) {
1071
+ function createRouter(engine, state, vite, webRoot, wsToken, version, protocolVersion, capabilities, ownerKind) {
961
1072
  return (req, res) => {
962
1073
  const url = req.url || "/";
963
1074
  const path = url.split("?")[0];
964
1075
  const method = req.method || "GET";
965
- if (!guardHost(req, res, state.config.allowNetworkAccess)) return;
1076
+ if (!guardHost(req, res, state.config)) return;
966
1077
  if (path === "/api/data") {
967
1078
  engine.touch();
968
1079
  sendJson(res, 200, engine.snapshot() ?? { pending: true });
@@ -973,6 +1084,9 @@ function createRouter(engine, state, vite, webRoot, wsToken, version) {
973
1084
  ok: true,
974
1085
  ready: engine.snapshot() !== null,
975
1086
  version,
1087
+ protocolVersion,
1088
+ capabilities,
1089
+ ownerKind,
976
1090
  // Discovery requires this proof; public health checks retain their useful 200 response.
977
1091
  owner: tokenMatches(header2(req, "x-tokmon-token"), wsToken)
978
1092
  });
@@ -1004,6 +1118,9 @@ async function startWebServer(opts) {
1004
1118
  const state = { config: opts.config };
1005
1119
  const tz = tzFor(state.config);
1006
1120
  const version = appVersion();
1121
+ const protocolVersion = opts.protocolVersion ?? TOKMON_PROTOCOL_VERSION;
1122
+ const capabilities = opts.capabilities ?? TOKMON_CAPABILITIES;
1123
+ const ownerKind = opts.ownerKind ?? "cli";
1007
1124
  const summaryIntervalMs = summaryIntervalFor(state.config);
1008
1125
  const billingIntervalMs = billingIntervalFor(state.config);
1009
1126
  const wsToken = opts.wsToken ?? randomBytes(32).toString("base64url");
@@ -1020,7 +1137,17 @@ async function startWebServer(opts) {
1020
1137
  const webRoot = vite ? null : findWebRoot();
1021
1138
  if (!vite && !webRoot) log(" \u26A0 no dashboard available \u2014 see the page for build/dev instructions");
1022
1139
  engine = createDataEngine({ version, config: state.config, tz, summaryIntervalMs, billingIntervalMs, resolved });
1023
- server.addListener("request", createRouter(engine, state, vite, webRoot, wsToken, version));
1140
+ server.addListener("request", createRouter(
1141
+ engine,
1142
+ state,
1143
+ vite,
1144
+ webRoot,
1145
+ wsToken,
1146
+ version,
1147
+ protocolVersion,
1148
+ capabilities,
1149
+ ownerKind
1150
+ ));
1024
1151
  closeWsRpc = await mountWsRpc(server, { engine, state });
1025
1152
  const bindHost = state.config.allowNetworkAccess ? NETWORK_HOST : LOOPBACK_HOST;
1026
1153
  const port = await listenWithFallback(server, opts.port ?? DEFAULT_PORT, bindHost);
@@ -1116,5 +1243,6 @@ function listenWithFallback(server, startPort, host) {
1116
1243
  }
1117
1244
 
1118
1245
  export {
1246
+ appVersion,
1119
1247
  startWebServer
1120
1248
  };