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.
- package/README.md +1 -1
- package/dist/{bootstrap-ink-5LQMSU4B.js → bootstrap-ink-IMT3S5HU.js} +120 -9
- package/dist/{chunk-KCY53RUE.js → chunk-44ZSQEOS.js} +2 -2
- package/dist/{chunk-JK3U54G7.js → chunk-6ZEW2M6P.js} +167 -39
- package/dist/{chunk-E2YXYU73.js → chunk-HP5UZCXP.js} +29 -0
- package/dist/{chunk-QM5E5RJZ.js → chunk-J3JW3RFP.js} +10 -7
- package/dist/{chunk-IWKSA64G.js → chunk-LCAHTRGP.js} +15 -281
- package/dist/{chunk-AMD4PXDG.js → chunk-LKYOWBD4.js} +15 -15
- package/dist/{chunk-QOA6KOJQ.js → chunk-M7XMV36F.js} +1 -1
- package/dist/chunk-WUK3MQUB.js +249 -0
- package/dist/{cli-command-HXKWB6IP.js → cli-command-DCENRCO3.js} +6 -6
- package/dist/cli.js +6 -6
- package/dist/{config-2PXUENQL.js → config-HXFJTNLM.js} +5 -1
- package/dist/{daemon-JCQF645I.js → daemon-6Y4O7NXS.js} +36 -14
- package/dist/daemon-handle-GVCYVDPO.js +10 -0
- package/dist/server-GXXOWUVZ.js +11 -0
- package/dist/web/assets/{Area-DgGVYaNT.js → Area-CMXvOw33.js} +1 -1
- package/dist/web/assets/{analytics-D5EvgF4d.js → analytics-DqdZXOJL.js} +2 -2
- package/dist/web/assets/{breakdown-CGdJkAHx.js → breakdown-kOSSJ3mI.js} +1 -1
- package/dist/web/assets/{chart-D4KHj2-J.js → chart-cZoLgpmG.js} +1 -1
- package/dist/web/assets/{explore-B2QnGzfO.js → explore-vcsM8K9I.js} +1 -1
- package/dist/web/assets/index-BErCXT7f.css +1 -0
- package/dist/web/assets/index-DH3dOnbg.js +83 -0
- package/dist/web/assets/{models-D7w7JJCX.js → models-B5ikm83_.js} +2 -2
- package/dist/web/assets/{overview-BPz0j397.js → overview-DD1kP8CP.js} +2 -2
- package/dist/web/assets/{panel-Da3rkvLu.js → panel-DTQBwExW.js} +1 -1
- package/dist/web/assets/{primitives-B8eGD8S_.js → primitives-C36qJlA2.js} +1 -1
- package/dist/web/assets/settings-sheet-_i2zTQjf.js +1 -0
- package/dist/web/assets/{share-sheet-BXrrMoLk.js → share-sheet-BLHmrFbm.js} +2 -2
- package/dist/web/assets/{timeline-CiGI4EmA.js → timeline-B4uG-Emo.js} +1 -1
- package/dist/web/assets/{use-dialog-trap-Dhs8W7s1.js → use-dialog-trap-bfaXEMz9.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/chunk-SMPY52EV.js +0 -125
- package/dist/daemon-handle-2GVZT55B.js +0 -10
- package/dist/server-BJOXAGRI.js +0 -11
- package/dist/web/assets/index-C0txIEHK.js +0 -83
- package/dist/web/assets/index-NnKaHxPO.css +0 -1
- 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-
|
|
4
|
+
} from "./chunk-M7XMV36F.js";
|
|
5
5
|
import {
|
|
6
6
|
accountsByProvider,
|
|
7
7
|
buildAccounts,
|
|
8
8
|
fetchPeak,
|
|
9
9
|
modelColor
|
|
10
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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 =
|
|
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:
|
|
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 &&
|
|
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-
|
|
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-
|
|
8
|
+
} from "./chunk-LCAHTRGP.js";
|
|
9
9
|
import {
|
|
10
10
|
expandHome,
|
|
11
11
|
slugify
|
|
12
|
-
} from "./chunk-
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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 =
|
|
136
|
-
if (
|
|
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(
|
|
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:
|
|
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(
|
|
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
|
|
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 +
|
|
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
|
|
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 =
|
|
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
|
|
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,
|
|
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
|
|
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(
|
|
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
|
};
|