tokmon 0.28.0 → 0.28.1

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 (36) hide show
  1. package/README.md +1 -1
  2. package/dist/{bootstrap-ink-5LQMSU4B.js → bootstrap-ink-L7QSJBMS.js} +119 -9
  3. package/dist/{chunk-QOA6KOJQ.js → chunk-3RTWFGGD.js} +1 -1
  4. package/dist/{chunk-AMD4PXDG.js → chunk-4HU4EI5T.js} +1 -1
  5. package/dist/{chunk-IWKSA64G.js → chunk-CVKCVHS7.js} +4 -3
  6. package/dist/{chunk-KCY53RUE.js → chunk-FMP3P2WV.js} +2 -2
  7. package/dist/{chunk-E2YXYU73.js → chunk-HP5UZCXP.js} +29 -0
  8. package/dist/{chunk-JK3U54G7.js → chunk-S33XIUAW.js} +11 -11
  9. package/dist/{chunk-QM5E5RJZ.js → chunk-XDA5RJST.js} +1 -1
  10. package/dist/{cli-command-HXKWB6IP.js → cli-command-75LP4IDS.js} +5 -5
  11. package/dist/cli.js +6 -6
  12. package/dist/{config-2PXUENQL.js → config-HXFJTNLM.js} +5 -1
  13. package/dist/{daemon-JCQF645I.js → daemon-CL4FJW26.js} +5 -5
  14. package/dist/{daemon-handle-2GVZT55B.js → daemon-handle-QQLJE46Y.js} +3 -3
  15. package/dist/server-EUO7CWYH.js +11 -0
  16. package/dist/web/assets/{Area-DgGVYaNT.js → Area-CMXvOw33.js} +1 -1
  17. package/dist/web/assets/{analytics-D5EvgF4d.js → analytics-DqdZXOJL.js} +2 -2
  18. package/dist/web/assets/{breakdown-CGdJkAHx.js → breakdown-kOSSJ3mI.js} +1 -1
  19. package/dist/web/assets/{chart-D4KHj2-J.js → chart-cZoLgpmG.js} +1 -1
  20. package/dist/web/assets/{explore-B2QnGzfO.js → explore-vcsM8K9I.js} +1 -1
  21. package/dist/web/assets/index-BErCXT7f.css +1 -0
  22. package/dist/web/assets/index-DH3dOnbg.js +83 -0
  23. package/dist/web/assets/{models-D7w7JJCX.js → models-B5ikm83_.js} +2 -2
  24. package/dist/web/assets/{overview-BPz0j397.js → overview-DD1kP8CP.js} +2 -2
  25. package/dist/web/assets/{panel-Da3rkvLu.js → panel-DTQBwExW.js} +1 -1
  26. package/dist/web/assets/{primitives-B8eGD8S_.js → primitives-C36qJlA2.js} +1 -1
  27. package/dist/web/assets/settings-sheet-_i2zTQjf.js +1 -0
  28. package/dist/web/assets/{share-sheet-BXrrMoLk.js → share-sheet-BLHmrFbm.js} +2 -2
  29. package/dist/web/assets/{timeline-CiGI4EmA.js → timeline-B4uG-Emo.js} +1 -1
  30. package/dist/web/assets/{use-dialog-trap-Dhs8W7s1.js → use-dialog-trap-bfaXEMz9.js} +1 -1
  31. package/dist/web/index.html +2 -2
  32. package/package.json +1 -1
  33. package/dist/server-BJOXAGRI.js +0 -11
  34. package/dist/web/assets/index-C0txIEHK.js +0 -83
  35. package/dist/web/assets/index-NnKaHxPO.css +0 -1
  36. 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-3RTWFGGD.js";
5
5
  import {
6
6
  accountsByProvider,
7
7
  buildAccounts,
8
8
  fetchPeak,
9
9
  modelColor
10
- } from "./chunk-KCY53RUE.js";
10
+ } from "./chunk-FMP3P2WV.js";
11
11
  import {
12
12
  PROVIDERS,
13
13
  PROVIDER_ORDER,
@@ -24,7 +24,7 @@ import {
24
24
  time,
25
25
  tokens,
26
26
  withTimeout
27
- } from "./chunk-IWKSA64G.js";
27
+ } from "./chunk-CVKCVHS7.js";
28
28
  import {
29
29
  COLOR_PALETTE,
30
30
  DEFAULTS,
@@ -33,13 +33,14 @@ import {
33
33
  getTrackedAccountRows,
34
34
  isValidTimezone,
35
35
  loadConfig,
36
+ normalizeAllowedHost,
36
37
  normalizeConfig,
37
38
  pickAccentColor,
38
39
  redactEmail,
39
40
  sanitizeTyped,
40
41
  saveConfigSync,
41
42
  snapshotCacheFile
42
- } from "./chunk-E2YXYU73.js";
43
+ } from "./chunk-HP5UZCXP.js";
43
44
  import {
44
45
  glyphs
45
46
  } from "./chunk-RF4GGQGM.js";
@@ -589,7 +590,7 @@ import { Box as Box4, Text as Text4 } from "ink";
589
590
  import { memo as memo3 } from "react";
590
591
  import { Box as Box3, Text as Text3 } from "ink";
591
592
  import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
592
- var GENERAL_ROWS = 10;
593
+ var GENERAL_ROWS = 11;
593
594
  var SETTINGS_TABS = ["general", "providers", "accounts"];
594
595
  var SETTINGS_TAB_LABELS = {
595
596
  general: "General",
@@ -604,6 +605,9 @@ var SettingsView = memo3(function SettingsView2({
604
605
  tzEdit,
605
606
  tzCaret,
606
607
  tzError,
608
+ allowedHostsEdit,
609
+ allowedHostsCaret,
610
+ allowedHostsError,
607
611
  resolvedTz,
608
612
  accountForm,
609
613
  activeAccountId,
@@ -612,6 +616,7 @@ var SettingsView = memo3(function SettingsView2({
612
616
  }) {
613
617
  if (accountForm) return /* @__PURE__ */ jsx3(AccountFormView, { form: accountForm, accounts: config.accounts });
614
618
  const editingTz = tzEdit !== null;
619
+ const editingAllowedHosts = allowedHostsEdit !== null;
615
620
  const tzDisplay = config.timezone === null ? `System (${resolvedTz})` : config.timezone;
616
621
  const tabFocused = cursor < 0;
617
622
  return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginTop: 1, children: [
@@ -685,8 +690,17 @@ var SettingsView = memo3(function SettingsView2({
685
690
  ] })
686
691
  ] }),
687
692
  /* @__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" }) }),
693
+ /* @__PURE__ */ jsx3(Row, { cursor, idx: 9, label: "Allowed hosts", children: editingAllowedHosts ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
694
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "[" }),
695
+ /* @__PURE__ */ jsx3(CaretText, { value: allowedHostsEdit ?? "", caret: allowedHostsCaret, color: "cyan" }),
696
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "]" })
697
+ ] }) : /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: config.allowedHosts.join(", ") || "none" }) }),
698
+ cursor === 9 && allowedHostsError && /* @__PURE__ */ jsxs3(Text3, { color: "red", children: [
699
+ " ",
700
+ allowedHostsError
701
+ ] }),
688
702
  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: [
703
+ /* @__PURE__ */ jsxs3(Row, { cursor, idx: 10, label: "Reset times", children: [
690
704
  /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
691
705
  glyphs().caretL,
692
706
  " "
@@ -779,6 +793,12 @@ var SettingsView = memo3(function SettingsView2({
779
793
  " Enter save ",
780
794
  glyphs().middot,
781
795
  " Esc cancel"
796
+ ] }) : editingAllowedHosts ? /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
797
+ "comma-separated exact DNS names ",
798
+ glyphs().middot,
799
+ " Enter save ",
800
+ glyphs().middot,
801
+ " Esc cancel"
782
802
  ] }) : activeTab === "providers" ? /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
783
803
  glyphs().arrowU,
784
804
  glyphs().arrowD,
@@ -1992,6 +2012,56 @@ function handleTimezoneEditor(input, key, editor, text, updateConfig) {
1992
2012
  if (clean) text.insert(clean);
1993
2013
  }
1994
2014
  }
2015
+ function handleAllowedHostsEditor(input, key, editor, text, updateConfig) {
2016
+ const { value, setValue, setError, setCaret, valueRef, caretRef } = editor;
2017
+ if (value === null) return;
2018
+ if (key.escape) {
2019
+ setValue(null);
2020
+ setError(null);
2021
+ return;
2022
+ }
2023
+ if (key.return) {
2024
+ const parts = value.split(",").map((part) => part.trim()).filter(Boolean);
2025
+ const normalized = parts.map(normalizeAllowedHost);
2026
+ if (normalized.some((host) => host === null)) {
2027
+ setError("Use exact DNS names without a scheme, port, path, or wildcard");
2028
+ return;
2029
+ }
2030
+ updateConfig((config) => ({ ...config, allowedHosts: [...new Set(normalized)] }));
2031
+ setValue(null);
2032
+ setError(null);
2033
+ return;
2034
+ }
2035
+ if (key.leftArrow) {
2036
+ setCaret((caret) => clampCaret(caret - 1, value.length));
2037
+ return;
2038
+ }
2039
+ if (key.rightArrow) {
2040
+ setCaret((caret) => clampCaret(caret + 1, value.length));
2041
+ return;
2042
+ }
2043
+ if (key.ctrl && input === "a") {
2044
+ setCaret(0);
2045
+ return;
2046
+ }
2047
+ if (key.ctrl && input === "e") {
2048
+ setCaret(value.length);
2049
+ return;
2050
+ }
2051
+ if (key.backspace || key.delete) {
2052
+ const result = spliceBackspace(valueRef.current, caretRef.current);
2053
+ valueRef.current = result.value;
2054
+ caretRef.current = result.caret;
2055
+ setValue(result.value);
2056
+ setCaret(result.caret);
2057
+ setError(null);
2058
+ return;
2059
+ }
2060
+ if (text.isPrintable(input, key)) {
2061
+ const clean = sanitizeTyped(input);
2062
+ if (clean) text.insert(clean);
2063
+ }
2064
+ }
1995
2065
  function handleSearchEditor(input, key, table, text) {
1996
2066
  const { search, setSearch, setSearchMode, setSearchCaret, searchValueRef, searchCaretRef } = table;
1997
2067
  if (key.return || key.escape) {
@@ -2064,6 +2134,8 @@ function handleSettings(input, key, ctx) {
2064
2134
  setCursor(-1);
2065
2135
  timezoneEditor.setValue(null);
2066
2136
  timezoneEditor.setError(null);
2137
+ ctx.allowedHostsEditor.setValue(null);
2138
+ ctx.allowedHostsEditor.setError(null);
2067
2139
  };
2068
2140
  const rowCount = tab === "general" ? GENERAL_ROWS : tab === "providers" ? PROVIDER_ORDER.length : trackedAccounts.length + 1;
2069
2141
  if (key.tab) {
@@ -2194,7 +2266,14 @@ function handleSettings(input, key, ctx) {
2194
2266
  updateConfig((current) => ({ ...current, allowNetworkAccess: !current.allowNetworkAccess }));
2195
2267
  return;
2196
2268
  }
2197
- if (index === 9 && (inputKey.leftArrow || inputKey.rightArrow || inputKey.return)) {
2269
+ if (index === 9 && inputKey.return) {
2270
+ const initial = config.allowedHosts.join(", ");
2271
+ context.allowedHostsEditor.setValue(initial);
2272
+ context.allowedHostsEditor.setCaret(initial.length);
2273
+ context.allowedHostsEditor.setError(null);
2274
+ return;
2275
+ }
2276
+ if (index === 10 && (inputKey.leftArrow || inputKey.rightArrow || inputKey.return)) {
2198
2277
  updateConfig((current) => ({ ...current, resetDisplay: current.resetDisplay === "relative" ? "absolute" : "relative" }));
2199
2278
  }
2200
2279
  }
@@ -2357,7 +2436,7 @@ function handleTerminalFocusInput(input) {
2357
2436
  return true;
2358
2437
  }
2359
2438
  function isRefreshAllShortcut(input, mode) {
2360
- return (input === "r" || input === "R") && !mode.unavailable && !mode.showPicker && !mode.editingAccount && !mode.editingTimezone && !mode.editingSearch;
2439
+ return (input === "r" || input === "R") && !mode.unavailable && !mode.showPicker && !mode.editingAccount && !mode.editingTimezone && !mode.editingAllowedHosts && !mode.editingSearch;
2361
2440
  }
2362
2441
  function handleOnboarding(input, key, ctx) {
2363
2442
  const { onboarding, global } = ctx;
@@ -2386,11 +2465,13 @@ function handleOnboarding(input, key, ctx) {
2386
2465
  function handleKey(input, key, ctx) {
2387
2466
  const editingAccount = ctx.settings.show && ctx.accountEditor.form !== null;
2388
2467
  const editingTimezone = ctx.settings.show && ctx.timezoneEditor.value !== null;
2468
+ const editingAllowedHosts = ctx.settings.show && ctx.allowedHostsEditor.value !== null;
2389
2469
  const editingSearch = ctx.table.tab === 1 && ctx.table.searchMode;
2390
2470
  if (isRefreshAllShortcut(input, {
2391
2471
  showPicker: ctx.onboarding.show,
2392
2472
  editingAccount,
2393
2473
  editingTimezone,
2474
+ editingAllowedHosts,
2394
2475
  editingSearch,
2395
2476
  unavailable: !ctx.global.configReady
2396
2477
  })) {
@@ -2409,6 +2490,10 @@ function handleKey(input, key, ctx) {
2409
2490
  handleTimezoneEditor(input, key, ctx.timezoneEditor, ctx.textInput, ctx.global.updateConfig);
2410
2491
  return;
2411
2492
  }
2493
+ if (editingAllowedHosts) {
2494
+ handleAllowedHostsEditor(input, key, ctx.allowedHostsEditor, ctx.textInput, ctx.global.updateConfig);
2495
+ return;
2496
+ }
2412
2497
  if (editingSearch) {
2413
2498
  handleSearchEditor(input, key, ctx.table, ctx.textInput);
2414
2499
  return;
@@ -3142,6 +3227,9 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
3142
3227
  const [tzEdit, setTzEdit] = useState10(null);
3143
3228
  const [tzError, setTzError] = useState10(null);
3144
3229
  const [tzCaret, setTzCaret] = useState10(0);
3230
+ const [allowedHostsEdit, setAllowedHostsEdit] = useState10(null);
3231
+ const [allowedHostsError, setAllowedHostsError] = useState10(null);
3232
+ const [allowedHostsCaret, setAllowedHostsCaret] = useState10(0);
3145
3233
  const [searchCaret, setSearchCaret] = useState10(0);
3146
3234
  const [onboardSel, setOnboardSel] = useState10(null);
3147
3235
  const [onboardCursor, setOnboardCursor] = useState10(0);
@@ -3169,6 +3257,8 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
3169
3257
  const dashPageCountRef = useRef8(1);
3170
3258
  const tzValueRef = useRef8("");
3171
3259
  const tzCaretRef = useRef8(0);
3260
+ const allowedHostsValueRef = useRef8("");
3261
+ const allowedHostsCaretRef = useRef8(0);
3172
3262
  const searchValueRef = useRef8("");
3173
3263
  const searchCaretRef = useRef8(0);
3174
3264
  const accountsKey = useMemo2(() => accounts.map(acctKey).join("|"), [accounts]);
@@ -3274,6 +3364,8 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
3274
3364
  dashPageCountRef.current = dashPageCount;
3275
3365
  tzValueRef.current = tzEdit ?? "";
3276
3366
  tzCaretRef.current = clampCaret(tzCaret, (tzEdit ?? "").length);
3367
+ allowedHostsValueRef.current = allowedHostsEdit ?? "";
3368
+ allowedHostsCaretRef.current = clampCaret(allowedHostsCaret, (allowedHostsEdit ?? "").length);
3277
3369
  searchValueRef.current = search;
3278
3370
  searchCaretRef.current = clampCaret(searchCaret, search.length);
3279
3371
  const isPrintable = (input, key) => !!input && !key.ctrl && !key.meta && !isPasteInput(input);
@@ -3291,6 +3383,13 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
3291
3383
  setTzEdit(r.value);
3292
3384
  setTzCaret(r.caret);
3293
3385
  setTzError(null);
3386
+ } else if (showSettings && allowedHostsEdit !== null) {
3387
+ const r = spliceInsert(allowedHostsValueRef.current, allowedHostsCaretRef.current, text);
3388
+ allowedHostsValueRef.current = r.value;
3389
+ allowedHostsCaretRef.current = r.caret;
3390
+ setAllowedHostsEdit(r.value);
3391
+ setAllowedHostsCaret(r.caret);
3392
+ setAllowedHostsError(null);
3294
3393
  } else if (tab === 1 && searchMode) {
3295
3394
  const r = spliceInsert(searchValueRef.current, searchCaretRef.current, text);
3296
3395
  searchValueRef.current = r.value;
@@ -3461,7 +3560,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
3461
3560
  if (webStartingRef.current) return;
3462
3561
  webStartingRef.current = true;
3463
3562
  try {
3464
- const { startWebServer } = await import("./server-BJOXAGRI.js");
3563
+ const { startWebServer } = await import("./server-EUO7CWYH.js");
3465
3564
  const ctrl = await startWebServer({ config: cfg, log: false });
3466
3565
  webRef.current = ctrl;
3467
3566
  openUrl(ctrl.url);
@@ -3535,6 +3634,14 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
3535
3634
  valueRef: tzValueRef,
3536
3635
  caretRef: tzCaretRef
3537
3636
  },
3637
+ allowedHostsEditor: {
3638
+ value: allowedHostsEdit,
3639
+ setValue: setAllowedHostsEdit,
3640
+ setError: setAllowedHostsError,
3641
+ setCaret: setAllowedHostsCaret,
3642
+ valueRef: allowedHostsValueRef,
3643
+ caretRef: allowedHostsCaretRef
3644
+ },
3538
3645
  textInput: { isPrintable, insert: insertText },
3539
3646
  settings: {
3540
3647
  show: showSettings,
@@ -3674,6 +3781,9 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
3674
3781
  tzEdit,
3675
3782
  tzCaret,
3676
3783
  tzError,
3784
+ allowedHostsEdit,
3785
+ allowedHostsCaret,
3786
+ allowedHostsError,
3677
3787
  resolvedTz: tz,
3678
3788
  accountForm,
3679
3789
  activeAccountId: cfg.activeAccountId,
@@ -3,7 +3,7 @@ import {
3
3
  TOKMON_WS_METHODS,
4
4
  TOKMON_WS_PATH,
5
5
  TokmonRpcGroup
6
- } from "./chunk-IWKSA64G.js";
6
+ } from "./chunk-CVKCVHS7.js";
7
7
 
8
8
  // src/client/daemon-rpc-client.ts
9
9
  import { Cause, Context, Duration, Effect, Exit, Fiber, Layer, ManagedRuntime, Schedule, Stream } from "effect";
@@ -5,7 +5,7 @@ import {
5
5
  readLock,
6
6
  reclaimDeadLock,
7
7
  verifyLock
8
- } from "./chunk-QM5E5RJZ.js";
8
+ } from "./chunk-XDA5RJST.js";
9
9
  import {
10
10
  appVersion
11
11
  } from "./chunk-SMPY52EV.js";
@@ -5,7 +5,7 @@ import {
5
5
  envDir,
6
6
  expandHome,
7
7
  isValidTimezone
8
- } from "./chunk-E2YXYU73.js";
8
+ } from "./chunk-HP5UZCXP.js";
9
9
 
10
10
  // src/async.ts
11
11
  var DEFAULT_TIMEOUT_MS = 3e4;
@@ -744,8 +744,8 @@ import { Schema } from "effect";
744
744
  import * as Rpc from "effect/unstable/rpc/Rpc";
745
745
  import * as RpcGroup from "effect/unstable/rpc/RpcGroup";
746
746
  var TOKMON_WS_PATH = "/ws";
747
- var TOKMON_PROTOCOL_VERSION = 2;
748
- var TOKMON_CAPABILITIES = ["config-cas", "config-revision"];
747
+ var TOKMON_PROTOCOL_VERSION = 3;
748
+ var TOKMON_CAPABILITIES = ["config-cas", "config-revision", "allowed-hosts"];
749
749
  var TOKMON_WS_METHODS = {
750
750
  getConfig: "tokmon.getConfig",
751
751
  setConfig: "tokmon.setConfig",
@@ -787,6 +787,7 @@ var ConfigSchema = Schema.Struct({
787
787
  defaultFocus: Schema.Literals(["all", "last"]),
788
788
  ascii: Schema.Literals(["auto", "on", "off"]),
789
789
  allowNetworkAccess: Schema.Boolean,
790
+ allowedHosts: Schema.Array(Schema.String),
790
791
  resetDisplay: Schema.Literals(["relative", "absolute"]),
791
792
  knownProviders: Schema.Array(ProviderIdSchema)
792
793
  });
@@ -5,11 +5,11 @@ import {
5
5
  identityFromIdToken,
6
6
  readClaudeIdentity,
7
7
  readJson
8
- } from "./chunk-IWKSA64G.js";
8
+ } from "./chunk-CVKCVHS7.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";
@@ -26,6 +26,7 @@ var DEFAULTS = {
26
26
  defaultFocus: "all",
27
27
  ascii: "auto",
28
28
  allowNetworkAccess: false,
29
+ allowedHosts: [],
29
30
  resetDisplay: "relative",
30
31
  knownProviders: []
31
32
  };
@@ -147,6 +148,27 @@ function sameJson(a, b) {
147
148
  return false;
148
149
  }
149
150
  }
151
+ function normalizeAllowedHost(value) {
152
+ if (typeof value !== "string") return null;
153
+ const candidate = value.trim();
154
+ if (!candidate) return null;
155
+ try {
156
+ const url = new URL(`http://${candidate}`);
157
+ if (url.username || url.password || url.port || url.pathname !== "/" || url.search || url.hash) return null;
158
+ const hostname = url.hostname.toLowerCase().replace(/\.$/, "");
159
+ if (!hostname || hostname.length > 253 || hostname.includes("*")) return null;
160
+ const labels = hostname.split(".");
161
+ if (labels.some((label) => !label || label.length > 63 || !/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(label))) return null;
162
+ return hostname;
163
+ } catch {
164
+ return null;
165
+ }
166
+ }
167
+ function normalizeAllowedHosts(value) {
168
+ if (!Array.isArray(value)) return [];
169
+ const hosts = value.map(normalizeAllowedHost).filter((host) => host !== null);
170
+ return [...new Set(hosts)];
171
+ }
150
172
  function repairConfig(input) {
151
173
  const reasons = [];
152
174
  const parsed = isRecord(input) ? input : {};
@@ -192,6 +214,10 @@ function repairConfig(input) {
192
214
  if (parsed.knownProviders !== void 0 && !Array.isArray(parsed.knownProviders)) {
193
215
  reasons.push("knownProviders was not an array");
194
216
  }
217
+ const allowedHosts = normalizeAllowedHosts(parsed.allowedHosts);
218
+ if (parsed.allowedHosts !== void 0 && !sameJson(parsed.allowedHosts, allowedHosts)) {
219
+ reasons.push("allowedHosts contained invalid or duplicate hostnames");
220
+ }
195
221
  const activeAccountId = typeof parsed.activeAccountId === "string" && (accountIds.has(parsed.activeAccountId) || PROVIDER_IDS.includes(parsed.activeAccountId)) ? parsed.activeAccountId : null;
196
222
  if (parsed.activeAccountId !== void 0 && parsed.activeAccountId !== null && activeAccountId === null) {
197
223
  reasons.push("activeAccountId did not match a known account/provider");
@@ -217,6 +243,7 @@ function repairConfig(input) {
217
243
  defaultFocus: parsed.defaultFocus === "last" ? "last" : "all",
218
244
  ascii: parsed.ascii === "on" ? "on" : parsed.ascii === "off" ? "off" : "auto",
219
245
  allowNetworkAccess: parsed.allowNetworkAccess === true,
246
+ allowedHosts,
220
247
  resetDisplay: parsed.resetDisplay === "absolute" ? "absolute" : "relative",
221
248
  knownProviders
222
249
  };
@@ -389,6 +416,8 @@ export {
389
416
  getTrackedAccountRows,
390
417
  clampNum,
391
418
  isValidTimezone,
419
+ normalizeAllowedHost,
420
+ normalizeAllowedHosts,
392
421
  repairConfig,
393
422
  normalizeConfig,
394
423
  slugify,
@@ -11,7 +11,7 @@ import {
11
11
  colorHex,
12
12
  fetchPeak,
13
13
  namedHex
14
- } from "./chunk-KCY53RUE.js";
14
+ } from "./chunk-FMP3P2WV.js";
15
15
  import {
16
16
  PROVIDERS,
17
17
  TOKMON_CAPABILITIES,
@@ -22,7 +22,7 @@ import {
22
22
  detectProviders,
23
23
  resolveTimezone,
24
24
  withTimeout
25
- } from "./chunk-IWKSA64G.js";
25
+ } from "./chunk-CVKCVHS7.js";
26
26
  import {
27
27
  cacheDir,
28
28
  expandHome,
@@ -30,7 +30,7 @@ import {
30
30
  normalizeConfig,
31
31
  saveConfig,
32
32
  snapshotCacheFile
33
- } from "./chunk-E2YXYU73.js";
33
+ } from "./chunk-HP5UZCXP.js";
34
34
 
35
35
  // src/web/server.ts
36
36
  import { createServer } from "http";
@@ -783,11 +783,11 @@ function isLoopbackHostHeader(value) {
783
783
  const hostname = hostnameFromHost(value);
784
784
  return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
785
785
  }
786
- function isAllowedHostHeader(value, allowNetworkAccess) {
786
+ function isAllowedHostHeader(value, allowNetworkAccess, allowedHosts = []) {
787
787
  if (isLoopbackHostHeader(value)) return true;
788
788
  if (!allowNetworkAccess) return false;
789
789
  const hostname = hostnameFromHost(value);
790
- return hostname !== null && isIP(hostname) !== 0;
790
+ return hostname !== null && (isIP(hostname) !== 0 || allowedHosts.includes(hostname));
791
791
  }
792
792
  function isSameOriginRequest(req) {
793
793
  const origin = header(req, "origin");
@@ -801,8 +801,8 @@ function isSameOriginRequest(req) {
801
801
  return false;
802
802
  }
803
803
  }
804
- function isAllowedLocalRequest(req, allowNetworkAccess) {
805
- return isAllowedHostHeader(header(req, "host"), allowNetworkAccess) && isSameOriginRequest(req);
804
+ function isAllowedLocalRequest(req, allowNetworkAccess, allowedHosts = []) {
805
+ return isAllowedHostHeader(header(req, "host"), allowNetworkAccess, allowedHosts) && isSameOriginRequest(req);
806
806
  }
807
807
 
808
808
  // src/web/ws.ts
@@ -890,7 +890,7 @@ async function mountWsRpc(server, deps) {
890
890
  )
891
891
  );
892
892
  const onUpgrade = (req, socket, head) => {
893
- if (!isAllowedLocalRequest(req, deps.state.config.allowNetworkAccess)) {
893
+ if (!isAllowedLocalRequest(req, deps.state.config.allowNetworkAccess, deps.state.config.allowedHosts)) {
894
894
  rejectUpgrade(socket);
895
895
  return;
896
896
  }
@@ -945,8 +945,8 @@ function tokenMatches(given, expected) {
945
945
  const token = Buffer.from(expected);
946
946
  return actual.length === token.length && timingSafeEqual(actual, token);
947
947
  }
948
- function guardHost(req, res, allowNetworkAccess) {
949
- if (isAllowedHostHeader(header2(req, "host"), allowNetworkAccess)) return true;
948
+ function guardHost(req, res, config) {
949
+ if (isAllowedHostHeader(header2(req, "host"), config.allowNetworkAccess, config.allowedHosts)) return true;
950
950
  sendJson(res, 403, { error: "forbidden" });
951
951
  return false;
952
952
  }
@@ -962,7 +962,7 @@ function createRouter(engine, state, vite, webRoot, wsToken, version) {
962
962
  const url = req.url || "/";
963
963
  const path = url.split("?")[0];
964
964
  const method = req.method || "GET";
965
- if (!guardHost(req, res, state.config.allowNetworkAccess)) return;
965
+ if (!guardHost(req, res, state.config)) return;
966
966
  if (path === "/api/data") {
967
967
  engine.touch();
968
968
  sendJson(res, 200, engine.snapshot() ?? { pending: true });
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  cacheDir
4
- } from "./chunk-E2YXYU73.js";
4
+ } from "./chunk-HP5UZCXP.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";
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  attachOrSpawn
4
- } from "./chunk-AMD4PXDG.js";
5
- import "./chunk-QM5E5RJZ.js";
4
+ } from "./chunk-4HU4EI5T.js";
5
+ import "./chunk-XDA5RJST.js";
6
6
  import "./chunk-SMPY52EV.js";
7
7
  import {
8
8
  createDaemonRpcClient
9
- } from "./chunk-QOA6KOJQ.js";
9
+ } from "./chunk-3RTWFGGD.js";
10
10
  import {
11
11
  PROVIDERS,
12
12
  antigravityStateDb,
@@ -26,11 +26,11 @@ import {
26
26
  startOfMonth,
27
27
  startOfWeek,
28
28
  withTimeout
29
- } from "./chunk-IWKSA64G.js";
29
+ } from "./chunk-CVKCVHS7.js";
30
30
  import {
31
31
  PROVIDER_IDS,
32
32
  configLocation
33
- } from "./chunk-E2YXYU73.js";
33
+ } from "./chunk-HP5UZCXP.js";
34
34
 
35
35
  // src/provider-locations.ts
36
36
  import { access } from "fs/promises";
package/dist/cli.js CHANGED
@@ -41,7 +41,7 @@ function validateServeArgs(serveArgs) {
41
41
  }
42
42
  async function main() {
43
43
  if (subcommand && ["usage", "models", "query", "providers", "snapshot", "config"].includes(subcommand)) {
44
- const { runQueryCommand } = await import("./cli-command-HXKWB6IP.js");
44
+ const { runQueryCommand } = await import("./cli-command-75LP4IDS.js");
45
45
  try {
46
46
  const output = await runQueryCommand(
47
47
  subcommand,
@@ -60,14 +60,14 @@ Run tokmon ${subcommand} --help for usage.
60
60
  return;
61
61
  }
62
62
  if (subcommand === "__daemon") {
63
- const { runDaemon } = await import("./daemon-JCQF645I.js");
63
+ const { runDaemon } = await import("./daemon-CL4FJW26.js");
64
64
  await runDaemon(args.slice(1), { foreground: false });
65
65
  process.exitCode ??= 0;
66
66
  return;
67
67
  }
68
68
  if (subcommand === "serve" || subcommand === "web") {
69
69
  validateServeArgs(args.slice(1));
70
- const { runDaemon } = await import("./daemon-JCQF645I.js");
70
+ const { runDaemon } = await import("./daemon-CL4FJW26.js");
71
71
  await runDaemon(args.slice(1), { foreground: true });
72
72
  process.exitCode ??= 0;
73
73
  return;
@@ -115,9 +115,9 @@ Run tokmon ${subcommand} --help for usage.
115
115
  return;
116
116
  }
117
117
  }
118
- const { loadConfig } = await import("./config-2PXUENQL.js");
118
+ const { loadConfig } = await import("./config-HXFJTNLM.js");
119
119
  const { resolveGlyphs, setGlyphs } = await import("./glyphs-NKCSZLGO.js");
120
- const { attachOrSpawn } = await import("./daemon-handle-2GVZT55B.js");
120
+ const { attachOrSpawn } = await import("./daemon-handle-QQLJE46Y.js");
121
121
  const config = await loadConfig();
122
122
  setGlyphs(resolveGlyphs({
123
123
  flag: asciiFlag,
@@ -128,7 +128,7 @@ Run tokmon ${subcommand} --help for usage.
128
128
  }));
129
129
  const daemon = await attachOrSpawn();
130
130
  const mode = daemon.kind === "spawned" ? "connected" : "degraded";
131
- const { bootstrapInk } = await import("./bootstrap-ink-5LQMSU4B.js");
131
+ const { bootstrapInk } = await import("./bootstrap-ink-L7QSJBMS.js");
132
132
  await bootstrapInk({ interval, config, daemon, mode });
133
133
  }
134
134
  void main().catch((error) => {
@@ -15,6 +15,8 @@ import {
15
15
  generateAccountId,
16
16
  getTrackedAccountRows,
17
17
  loadConfig,
18
+ normalizeAllowedHost,
19
+ normalizeAllowedHosts,
18
20
  normalizeConfig,
19
21
  pickAccentColor,
20
22
  redactEmail,
@@ -24,7 +26,7 @@ import {
24
26
  saveConfigSync,
25
27
  slugify,
26
28
  snapshotCacheFile
27
- } from "./chunk-E2YXYU73.js";
29
+ } from "./chunk-HP5UZCXP.js";
28
30
  export {
29
31
  ACCENT_COLORS,
30
32
  COLOR_PALETTE,
@@ -41,6 +43,8 @@ export {
41
43
  generateAccountId,
42
44
  getTrackedAccountRows,
43
45
  loadConfig,
46
+ normalizeAllowedHost,
47
+ normalizeAllowedHosts,
44
48
  normalizeConfig,
45
49
  pickAccentColor,
46
50
  redactEmail,
@@ -8,20 +8,20 @@ import {
8
8
  unlinkLock,
9
9
  verifyLock,
10
10
  writeLock
11
- } from "./chunk-QM5E5RJZ.js";
11
+ } from "./chunk-XDA5RJST.js";
12
12
  import {
13
13
  startWebServer
14
- } from "./chunk-JK3U54G7.js";
14
+ } from "./chunk-S33XIUAW.js";
15
15
  import {
16
16
  appVersion
17
17
  } from "./chunk-SMPY52EV.js";
18
- import "./chunk-KCY53RUE.js";
18
+ import "./chunk-FMP3P2WV.js";
19
19
  import {
20
20
  flushDisk
21
- } from "./chunk-IWKSA64G.js";
21
+ } from "./chunk-CVKCVHS7.js";
22
22
  import {
23
23
  loadConfig
24
- } from "./chunk-E2YXYU73.js";
24
+ } from "./chunk-HP5UZCXP.js";
25
25
 
26
26
  // src/web/daemon.ts
27
27
  import { randomBytes } from "crypto";
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  attachOrSpawn
4
- } from "./chunk-AMD4PXDG.js";
5
- import "./chunk-QM5E5RJZ.js";
4
+ } from "./chunk-4HU4EI5T.js";
5
+ import "./chunk-XDA5RJST.js";
6
6
  import "./chunk-SMPY52EV.js";
7
- import "./chunk-E2YXYU73.js";
7
+ import "./chunk-HP5UZCXP.js";
8
8
  export {
9
9
  attachOrSpawn
10
10
  };
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ startWebServer
4
+ } from "./chunk-S33XIUAW.js";
5
+ import "./chunk-SMPY52EV.js";
6
+ import "./chunk-FMP3P2WV.js";
7
+ import "./chunk-CVKCVHS7.js";
8
+ import "./chunk-HP5UZCXP.js";
9
+ export {
10
+ startWebServer
11
+ };