tokmon 0.22.6 → 0.23.0
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 +5 -5
- package/dist/{bootstrap-ink-CHJWH5XC.js → bootstrap-ink-UD3KDTOB.js} +1113 -934
- package/dist/{chunk-FO7IUYCM.js → chunk-AON6GE7D.js} +368 -280
- package/dist/{chunk-QTHCHB7S.js → chunk-DJPUYMZM.js} +52 -18
- package/dist/chunk-HV6AYQDB.js +192 -0
- package/dist/chunk-HXDAOOSI.js +154 -0
- package/dist/{chunk-AQNFQRWV.js → chunk-PGQK7MOE.js} +2583 -1711
- package/dist/cli.js +93 -54
- package/dist/{config-64N7KBZ7.js → config-5CNJN3P7.js} +1 -1
- package/dist/daemon-AW3YHFSJ.js +193 -0
- package/dist/daemon-handle-EYAXNNKZ.js +126 -0
- package/dist/server-4CDZMLH6.js +10 -0
- package/dist/web/assets/Area-DGo9K-U3.js +2 -0
- package/dist/web/assets/analytics-BOhbhV5Z.js +2 -0
- package/dist/web/assets/breakdown-CO04nSN-.js +4 -0
- package/dist/web/assets/chart-bNxdfXxA.js +69 -0
- package/dist/web/assets/explore-sZnmjzAj.js +22 -0
- package/dist/web/assets/index-DSj-Bxov.css +1 -0
- package/dist/web/assets/index-DnDr1VUr.js +83 -0
- package/dist/web/assets/models-Cl6D8VVF.js +2 -0
- package/dist/web/assets/overview-C18nvaAK.js +2 -0
- package/dist/web/assets/panel-DTjMnHaz.js +1 -0
- package/dist/web/assets/primitives-Dj_9YxTF.js +1 -0
- package/dist/web/assets/settings-sheet-0BGb5Ahv.js +1 -0
- package/dist/web/assets/share-sheet-BHtt-OmT.js +2 -0
- package/dist/web/assets/timeline-DmzB8Z3W.js +1 -0
- package/dist/web/assets/use-dialog-trap-DeXFtxx8.js +1 -0
- package/dist/web/index.html +3 -2
- package/package.json +10 -6
- package/dist/daemon-ATXHNYTK.js +0 -213
- package/dist/daemon-handle-HLSKLMWU.js +0 -142
- package/dist/server-56C7MMEZ.js +0 -9
- package/dist/web/assets/breakdown-Bj0BAVYf.js +0 -4
- package/dist/web/assets/index-BCaPGYQA.css +0 -1
- package/dist/web/assets/index-Dziiggw_.js +0 -173
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-700-normal-BWTpRfYl.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-700-normal-CEoEElIJ.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-700-normal-C6CZE3T8.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-700-normal-DEigVDxa.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-700-normal-CZipNAKV.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-700-normal-CxPITLHs.woff +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-700-normal-BDLVIk2r.woff +0 -0
- package/dist/web/assets/timeline-BQpqTU-a.js +0 -1
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
appVersion,
|
|
4
|
+
browserUrl,
|
|
5
|
+
findWebRoot,
|
|
6
|
+
send,
|
|
7
|
+
sendJson,
|
|
8
|
+
serveStatic
|
|
9
|
+
} from "./chunk-HXDAOOSI.js";
|
|
2
10
|
import {
|
|
3
11
|
PROVIDERS,
|
|
12
|
+
TOKMON_CAPABILITIES,
|
|
13
|
+
TOKMON_PROTOCOL_VERSION,
|
|
4
14
|
TOKMON_WS_METHODS,
|
|
5
15
|
TOKMON_WS_PATH,
|
|
6
16
|
TokmonRpcGroup,
|
|
@@ -11,7 +21,7 @@ import {
|
|
|
11
21
|
namedHex,
|
|
12
22
|
resolveTimezone,
|
|
13
23
|
withTimeout
|
|
14
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-PGQK7MOE.js";
|
|
15
25
|
import {
|
|
16
26
|
cacheDir,
|
|
17
27
|
expandHome,
|
|
@@ -19,11 +29,11 @@ import {
|
|
|
19
29
|
normalizeConfig,
|
|
20
30
|
saveConfig,
|
|
21
31
|
snapshotCacheFile
|
|
22
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-DJPUYMZM.js";
|
|
23
33
|
|
|
24
34
|
// src/web/server.ts
|
|
25
35
|
import { createServer } from "http";
|
|
26
|
-
import { randomBytes } from "crypto";
|
|
36
|
+
import { randomBytes, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
27
37
|
|
|
28
38
|
// src/web/data.ts
|
|
29
39
|
async function resolveAccounts(config) {
|
|
@@ -103,115 +113,11 @@ function tzFor(config) {
|
|
|
103
113
|
return resolveTimezone(config.timezone);
|
|
104
114
|
}
|
|
105
115
|
|
|
106
|
-
// src/web/static.ts
|
|
107
|
-
import { createReadStream, existsSync, readFileSync } from "fs";
|
|
108
|
-
import { stat } from "fs/promises";
|
|
109
|
-
import { fileURLToPath } from "url";
|
|
110
|
-
import { extname, join, normalize, sep } from "path";
|
|
111
|
-
var MIME = {
|
|
112
|
-
".html": "text/html; charset=utf-8",
|
|
113
|
-
".js": "text/javascript; charset=utf-8",
|
|
114
|
-
".mjs": "text/javascript; charset=utf-8",
|
|
115
|
-
".css": "text/css; charset=utf-8",
|
|
116
|
-
".json": "application/json; charset=utf-8",
|
|
117
|
-
".svg": "image/svg+xml",
|
|
118
|
-
".png": "image/png",
|
|
119
|
-
".jpg": "image/jpeg",
|
|
120
|
-
".jpeg": "image/jpeg",
|
|
121
|
-
".gif": "image/gif",
|
|
122
|
-
".ico": "image/x-icon",
|
|
123
|
-
".woff": "font/woff",
|
|
124
|
-
".woff2": "font/woff2",
|
|
125
|
-
".ttf": "font/ttf",
|
|
126
|
-
".otf": "font/otf",
|
|
127
|
-
".map": "application/json; charset=utf-8",
|
|
128
|
-
".webmanifest": "application/manifest+json"
|
|
129
|
-
};
|
|
130
|
-
function findWebRoot() {
|
|
131
|
-
const candidates = ["./web/", "../web/", "../dist/web/", "../../dist/web/"];
|
|
132
|
-
for (const rel of candidates) {
|
|
133
|
-
try {
|
|
134
|
-
const dir = fileURLToPath(new URL(rel, import.meta.url));
|
|
135
|
-
if (existsSync(join(dir, "index.html"))) return dir.replace(/[\\/]+$/, "");
|
|
136
|
-
} catch {
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
141
|
-
function resolveStaticPath(webRoot, urlPath) {
|
|
142
|
-
let clean;
|
|
143
|
-
try {
|
|
144
|
-
clean = decodeURIComponent(urlPath.split("?")[0]);
|
|
145
|
-
} catch {
|
|
146
|
-
return null;
|
|
147
|
-
}
|
|
148
|
-
const rel = normalize(clean).replace(/^(\.\.[/\\])+/, "").replace(/^[/\\]+/, "");
|
|
149
|
-
const full = join(webRoot, rel);
|
|
150
|
-
if (full !== webRoot && !full.startsWith(webRoot + sep)) return null;
|
|
151
|
-
return full;
|
|
152
|
-
}
|
|
153
|
-
function send(res, status, type, body) {
|
|
154
|
-
res.writeHead(status, { "Content-Type": type, "Cache-Control": "no-store" });
|
|
155
|
-
res.end(body);
|
|
156
|
-
}
|
|
157
|
-
function sendJson(res, status, data) {
|
|
158
|
-
send(res, status, "application/json; charset=utf-8", JSON.stringify(data));
|
|
159
|
-
}
|
|
160
|
-
function serveStatic(webRoot, urlPath, res) {
|
|
161
|
-
const path = urlPath.split("?")[0];
|
|
162
|
-
const filePath = resolveStaticPath(webRoot, path === "/" ? "/index.html" : path);
|
|
163
|
-
if (!filePath) {
|
|
164
|
-
send(res, 403, "text/plain", "forbidden");
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
void stat(filePath).then((st) => {
|
|
168
|
-
if (st.isFile()) {
|
|
169
|
-
const type = MIME[extname(filePath).toLowerCase()] || "application/octet-stream";
|
|
170
|
-
const immutable = filePath.includes(`${sep}assets${sep}`);
|
|
171
|
-
res.writeHead(200, {
|
|
172
|
-
"Content-Type": type,
|
|
173
|
-
"Cache-Control": immutable ? "public, max-age=31536000, immutable" : "no-cache"
|
|
174
|
-
});
|
|
175
|
-
createReadStream(filePath).pipe(res);
|
|
176
|
-
} else {
|
|
177
|
-
throw new Error("not a file");
|
|
178
|
-
}
|
|
179
|
-
}).catch(() => {
|
|
180
|
-
if (extname(path)) {
|
|
181
|
-
send(res, 404, "text/plain", "not found");
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
const indexPath = join(webRoot, "index.html");
|
|
185
|
-
if (!existsSync(indexPath)) {
|
|
186
|
-
send(res, 404, "text/plain", "not found");
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
res.writeHead(200, { "Content-Type": MIME[".html"], "Cache-Control": "no-cache" });
|
|
190
|
-
createReadStream(indexPath).on("error", () => {
|
|
191
|
-
try {
|
|
192
|
-
res.destroy();
|
|
193
|
-
} catch {
|
|
194
|
-
}
|
|
195
|
-
}).pipe(res);
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
function appVersion() {
|
|
199
|
-
for (const rel of ["../package.json", "../../package.json"]) {
|
|
200
|
-
try {
|
|
201
|
-
const p = fileURLToPath(new URL(rel, import.meta.url));
|
|
202
|
-
const pkg = JSON.parse(readFileSync(p, "utf-8"));
|
|
203
|
-
if (typeof pkg.version === "string") return pkg.version;
|
|
204
|
-
} catch {
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return "";
|
|
208
|
-
}
|
|
209
|
-
|
|
210
116
|
// src/web/vite-dev.ts
|
|
211
|
-
import { existsSync
|
|
212
|
-
import { fileURLToPath
|
|
117
|
+
import { existsSync } from "fs";
|
|
118
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
213
119
|
import { createRequire } from "module";
|
|
214
|
-
import { join
|
|
120
|
+
import { join } from "path";
|
|
215
121
|
function isDevMode() {
|
|
216
122
|
const forced = process.env.TOKMON_WEB_MODE;
|
|
217
123
|
if (forced === "dev") return true;
|
|
@@ -221,8 +127,8 @@ function isDevMode() {
|
|
|
221
127
|
function findWebSource() {
|
|
222
128
|
for (const rel of ["../../web/", "../web/", "./web/"]) {
|
|
223
129
|
try {
|
|
224
|
-
const dir =
|
|
225
|
-
if (
|
|
130
|
+
const dir = fileURLToPath(new URL(rel, import.meta.url));
|
|
131
|
+
if (existsSync(join(dir, "vite.config.ts")) && existsSync(join(dir, "index.html"))) {
|
|
226
132
|
return dir.replace(/[\\/]+$/, "");
|
|
227
133
|
}
|
|
228
134
|
} catch {
|
|
@@ -237,12 +143,12 @@ async function createViteDevServer(httpServer, log) {
|
|
|
237
143
|
return null;
|
|
238
144
|
}
|
|
239
145
|
try {
|
|
240
|
-
const req = createRequire(pathToFileURL(
|
|
146
|
+
const req = createRequire(pathToFileURL(join(root, "package.json")).href);
|
|
241
147
|
const vitePath = req.resolve("vite");
|
|
242
148
|
const vite = await import(pathToFileURL(vitePath).href);
|
|
243
149
|
const dev = await vite.createServer({
|
|
244
150
|
root,
|
|
245
|
-
configFile:
|
|
151
|
+
configFile: join(root, "vite.config.ts"),
|
|
246
152
|
server: { middlewareMode: true, hmr: { server: httpServer } },
|
|
247
153
|
appType: "spa",
|
|
248
154
|
clearScreen: false,
|
|
@@ -266,13 +172,90 @@ code{color:#e6b450}</style></head><body>
|
|
|
266
172
|
</body></html>`;
|
|
267
173
|
|
|
268
174
|
// src/web/data-engine.ts
|
|
269
|
-
import { readFileSync
|
|
175
|
+
import { readFileSync, writeFileSync, mkdirSync, renameSync } from "fs";
|
|
176
|
+
|
|
177
|
+
// src/web/refresh-queue.ts
|
|
178
|
+
function deferred() {
|
|
179
|
+
let resolve;
|
|
180
|
+
let reject;
|
|
181
|
+
const promise = new Promise((onResolve, onReject) => {
|
|
182
|
+
resolve = onResolve;
|
|
183
|
+
reject = onReject;
|
|
184
|
+
});
|
|
185
|
+
return { promise, resolve, reject };
|
|
186
|
+
}
|
|
187
|
+
function createRefreshQueue(perform, skipAutomatic = () => false) {
|
|
188
|
+
let activePass = null;
|
|
189
|
+
let activeResult = null;
|
|
190
|
+
let queued = null;
|
|
191
|
+
let stopped = false;
|
|
192
|
+
const launch = (result = deferred()) => {
|
|
193
|
+
const pass = Promise.resolve().then(perform);
|
|
194
|
+
activePass = pass;
|
|
195
|
+
activeResult = result;
|
|
196
|
+
const settled = (succeeded, cause) => {
|
|
197
|
+
if (activePass !== pass) return;
|
|
198
|
+
activePass = null;
|
|
199
|
+
activeResult = null;
|
|
200
|
+
const next = queued;
|
|
201
|
+
queued = null;
|
|
202
|
+
if (next && !stopped) {
|
|
203
|
+
void next.promise.then(
|
|
204
|
+
() => {
|
|
205
|
+
if (succeeded) result.resolve();
|
|
206
|
+
else result.reject(cause);
|
|
207
|
+
},
|
|
208
|
+
result.reject
|
|
209
|
+
);
|
|
210
|
+
launch(next);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (next) next.resolve();
|
|
214
|
+
if (succeeded) result.resolve();
|
|
215
|
+
else result.reject(cause);
|
|
216
|
+
};
|
|
217
|
+
void pass.then(() => settled(true), (cause) => settled(false, cause));
|
|
218
|
+
return result.promise;
|
|
219
|
+
};
|
|
220
|
+
const run = (force = false) => {
|
|
221
|
+
if (stopped || !force && skipAutomatic()) return Promise.resolve();
|
|
222
|
+
if (!activePass) return launch();
|
|
223
|
+
if (!force) return activeResult.promise;
|
|
224
|
+
if (!queued) queued = deferred();
|
|
225
|
+
return queued.promise;
|
|
226
|
+
};
|
|
227
|
+
return {
|
|
228
|
+
run,
|
|
229
|
+
stop() {
|
|
230
|
+
stopped = true;
|
|
231
|
+
activeResult?.resolve();
|
|
232
|
+
queued?.resolve();
|
|
233
|
+
queued = null;
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
async function settleRefreshTasks(tasks) {
|
|
238
|
+
const results = await Promise.allSettled(tasks);
|
|
239
|
+
const failure = results.find((result) => result.status === "rejected");
|
|
240
|
+
if (failure) throw failure.reason;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/web/data-engine.ts
|
|
270
244
|
var TABLE_INTERVAL_MS = 3e5;
|
|
271
245
|
var PEAK_INTERVAL_MS = 3e5;
|
|
272
246
|
var IDLE_PAUSE_MS = 6e4;
|
|
273
247
|
var SNAPSHOT_CACHE_THROTTLE_MS = 2e4;
|
|
274
248
|
var REVEAL_THROTTLE_MS = 500;
|
|
275
249
|
var FETCH_TIMEOUT_MS = 3e4;
|
|
250
|
+
function runInBackground(task) {
|
|
251
|
+
void task.catch(() => {
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
function throwIfRefreshFailures(scope, failures) {
|
|
255
|
+
if (failures.length > 0) {
|
|
256
|
+
throw new AggregateError(failures, `${scope} refresh failed for ${failures.length} account${failures.length === 1 ? "" : "s"}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
276
259
|
function createDataEngine(opts) {
|
|
277
260
|
const { version } = opts;
|
|
278
261
|
let tz = opts.tz;
|
|
@@ -324,7 +307,7 @@ function createDataEngine(opts) {
|
|
|
324
307
|
});
|
|
325
308
|
const hydrateFromCache = () => {
|
|
326
309
|
try {
|
|
327
|
-
const cached = JSON.parse(
|
|
310
|
+
const cached = JSON.parse(readFileSync(snapshotCacheFile(), "utf-8"));
|
|
328
311
|
if (!cached || !Array.isArray(cached.accounts)) return;
|
|
329
312
|
for (const a of cached.accounts) {
|
|
330
313
|
if (a.dashboard || a.table) {
|
|
@@ -375,47 +358,35 @@ function createDataEngine(opts) {
|
|
|
375
358
|
};
|
|
376
359
|
let usageAccounts = resolved.filter((r) => r.hasUsage);
|
|
377
360
|
let billingAccounts = resolved.filter((r) => r.hasBilling);
|
|
378
|
-
const makeRefreshLoop = (opts2) =>
|
|
379
|
-
|
|
380
|
-
let forcePending = false;
|
|
381
|
-
const run = async (force = false) => {
|
|
361
|
+
const makeRefreshLoop = (opts2) => createRefreshQueue(
|
|
362
|
+
async () => {
|
|
382
363
|
if (stopped) return;
|
|
383
|
-
if (busy) {
|
|
384
|
-
if (force) forcePending = true;
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
if (!force && idle()) return;
|
|
388
364
|
const epoch = configEpoch;
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
}
|
|
400
|
-
if (stopped || epoch !== configEpoch) return;
|
|
401
|
-
if (ok) {
|
|
402
|
-
opts2.apply(r.account.id, value);
|
|
403
|
-
opts2.state.set(r.account.id, "ready");
|
|
404
|
-
} else opts2.state.set(r.account.id, "error");
|
|
405
|
-
reveal();
|
|
406
|
-
}
|
|
407
|
-
rebuild();
|
|
408
|
-
} finally {
|
|
409
|
-
busy = false;
|
|
410
|
-
if (forcePending && !stopped) {
|
|
411
|
-
forcePending = false;
|
|
412
|
-
void run(true);
|
|
365
|
+
const failures = [];
|
|
366
|
+
for (const r of opts2.accounts()) {
|
|
367
|
+
if (stopped) return;
|
|
368
|
+
let value = null;
|
|
369
|
+
let ok = true;
|
|
370
|
+
try {
|
|
371
|
+
value = await withTimeout(opts2.fetch(r), FETCH_TIMEOUT_MS);
|
|
372
|
+
} catch (cause) {
|
|
373
|
+
ok = false;
|
|
374
|
+
failures.push(cause);
|
|
413
375
|
}
|
|
376
|
+
if (stopped || epoch !== configEpoch) return;
|
|
377
|
+
if (ok) {
|
|
378
|
+
opts2.apply(r.account.id, value);
|
|
379
|
+
opts2.state.set(r.account.id, "ready");
|
|
380
|
+
} else opts2.state.set(r.account.id, "error");
|
|
381
|
+
reveal();
|
|
414
382
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
383
|
+
rebuild();
|
|
384
|
+
throwIfRefreshFailures(opts2.scope, failures);
|
|
385
|
+
},
|
|
386
|
+
idle
|
|
387
|
+
);
|
|
418
388
|
const refreshSummary = makeRefreshLoop({
|
|
389
|
+
scope: "summary",
|
|
419
390
|
accounts: () => usageAccounts,
|
|
420
391
|
fetch: (r) => fetchAccountSummary(r.account, tz),
|
|
421
392
|
apply: (id, dashboard) => {
|
|
@@ -424,6 +395,7 @@ function createDataEngine(opts) {
|
|
|
424
395
|
state: summaryState
|
|
425
396
|
});
|
|
426
397
|
const refreshTable = makeRefreshLoop({
|
|
398
|
+
scope: "history",
|
|
427
399
|
accounts: () => usageAccounts,
|
|
428
400
|
fetch: (r) => fetchAccountTable(r.account, tz),
|
|
429
401
|
apply: (id, table) => {
|
|
@@ -432,6 +404,7 @@ function createDataEngine(opts) {
|
|
|
432
404
|
state: tableState
|
|
433
405
|
});
|
|
434
406
|
const refreshBilling = makeRefreshLoop({
|
|
407
|
+
scope: "billing",
|
|
435
408
|
accounts: () => billingAccounts,
|
|
436
409
|
fetch: (r) => fetchAccountBilling(r.account, tz),
|
|
437
410
|
apply: (id, result) => {
|
|
@@ -439,22 +412,19 @@ function createDataEngine(opts) {
|
|
|
439
412
|
},
|
|
440
413
|
state: billingState
|
|
441
414
|
});
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
peakBusy = true;
|
|
447
|
-
try {
|
|
415
|
+
const refreshPeak = createRefreshQueue(
|
|
416
|
+
async () => {
|
|
417
|
+
if (stopped || !hasClaude) return;
|
|
418
|
+
const epoch = configEpoch;
|
|
448
419
|
const next = await fetchPeak();
|
|
449
420
|
if (stopped || epoch !== configEpoch || !hasClaude) return;
|
|
450
421
|
if (next) {
|
|
451
422
|
peak = next;
|
|
452
423
|
rebuild();
|
|
453
424
|
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
};
|
|
425
|
+
},
|
|
426
|
+
() => !hasClaude || idle()
|
|
427
|
+
);
|
|
458
428
|
const clearTimers = () => {
|
|
459
429
|
clearInterval(summaryTimer);
|
|
460
430
|
summaryTimer = void 0;
|
|
@@ -467,20 +437,20 @@ function createDataEngine(opts) {
|
|
|
467
437
|
};
|
|
468
438
|
const startTimers = () => {
|
|
469
439
|
summaryTimer = setInterval(() => {
|
|
470
|
-
|
|
440
|
+
runInBackground(refreshSummary.run());
|
|
471
441
|
}, summaryIntervalMs);
|
|
472
442
|
tableTimer = setInterval(() => {
|
|
473
|
-
|
|
443
|
+
runInBackground(refreshTable.run());
|
|
474
444
|
}, TABLE_INTERVAL_MS);
|
|
475
445
|
billingTimer = setInterval(() => {
|
|
476
|
-
|
|
446
|
+
runInBackground(refreshBilling.run());
|
|
477
447
|
}, billingIntervalMs);
|
|
478
448
|
summaryTimer.unref?.();
|
|
479
449
|
tableTimer.unref?.();
|
|
480
450
|
billingTimer.unref?.();
|
|
481
451
|
if (hasClaude) {
|
|
482
452
|
peakTimer = setInterval(() => {
|
|
483
|
-
|
|
453
|
+
runInBackground(refreshPeak.run());
|
|
484
454
|
}, PEAK_INTERVAL_MS);
|
|
485
455
|
peakTimer.unref?.();
|
|
486
456
|
}
|
|
@@ -489,21 +459,23 @@ function createDataEngine(opts) {
|
|
|
489
459
|
return {
|
|
490
460
|
snapshot: () => current,
|
|
491
461
|
start() {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
if (hasClaude)
|
|
462
|
+
runInBackground(refreshSummary.run(true));
|
|
463
|
+
runInBackground(refreshTable.run(true));
|
|
464
|
+
runInBackground(refreshBilling.run(true));
|
|
465
|
+
if (hasClaude) runInBackground(refreshPeak.run(true));
|
|
496
466
|
startTimers();
|
|
497
467
|
},
|
|
498
468
|
touch() {
|
|
499
469
|
lastActivity = Date.now();
|
|
500
470
|
},
|
|
501
471
|
refresh(scope = "all") {
|
|
502
|
-
if (stopped) return;
|
|
503
|
-
|
|
504
|
-
if (scope === "all" || scope === "
|
|
505
|
-
if (scope === "all" || scope === "
|
|
506
|
-
if (
|
|
472
|
+
if (stopped) return Promise.resolve();
|
|
473
|
+
const tasks = [];
|
|
474
|
+
if (scope === "all" || scope === "summary") tasks.push(refreshSummary.run(true));
|
|
475
|
+
if (scope === "all" || scope === "table") tasks.push(refreshTable.run(true));
|
|
476
|
+
if (scope === "all" || scope === "billing") tasks.push(refreshBilling.run(true));
|
|
477
|
+
if ((scope === "all" || scope === "peak") && hasClaude) tasks.push(refreshPeak.run(true));
|
|
478
|
+
return settleRefreshTasks(tasks);
|
|
507
479
|
},
|
|
508
480
|
setConfig(next) {
|
|
509
481
|
if (stopped) return;
|
|
@@ -528,10 +500,10 @@ function createDataEngine(opts) {
|
|
|
528
500
|
for (const id of [...map.keys()]) if (!survivors.has(id)) map.delete(id);
|
|
529
501
|
}
|
|
530
502
|
rebuild();
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
if (hasClaude)
|
|
503
|
+
runInBackground(refreshSummary.run(true));
|
|
504
|
+
runInBackground(refreshTable.run(true));
|
|
505
|
+
runInBackground(refreshBilling.run(true));
|
|
506
|
+
if (hasClaude) runInBackground(refreshPeak.run(true));
|
|
535
507
|
startTimers();
|
|
536
508
|
},
|
|
537
509
|
broadcastConfig(config) {
|
|
@@ -554,8 +526,8 @@ function createDataEngine(opts) {
|
|
|
554
526
|
snapshotSubscribers.add(onSnapshot);
|
|
555
527
|
lastActivity = Date.now();
|
|
556
528
|
if (!current || Date.now() - current.generatedAt > summaryIntervalMs) {
|
|
557
|
-
|
|
558
|
-
|
|
529
|
+
runInBackground(refreshSummary.run(true));
|
|
530
|
+
runInBackground(refreshTable.run(true));
|
|
559
531
|
}
|
|
560
532
|
return () => {
|
|
561
533
|
snapshotSubscribers.delete(onSnapshot);
|
|
@@ -574,6 +546,10 @@ function createDataEngine(opts) {
|
|
|
574
546
|
stop() {
|
|
575
547
|
stopped = true;
|
|
576
548
|
clearTimers();
|
|
549
|
+
refreshSummary.stop();
|
|
550
|
+
refreshTable.stop();
|
|
551
|
+
refreshBilling.stop();
|
|
552
|
+
refreshPeak.stop();
|
|
577
553
|
snapshotSubscribers.clear();
|
|
578
554
|
configSubscribers.clear();
|
|
579
555
|
}
|
|
@@ -585,6 +561,32 @@ var MIN_SUMMARY_INTERVAL_MS = 8e3;
|
|
|
585
561
|
var BILLING_INTERVAL_FALLBACK_MIN = 5;
|
|
586
562
|
var summaryIntervalFor = (config) => Math.max(MIN_SUMMARY_INTERVAL_MS, (config.interval || 2) * 1e3);
|
|
587
563
|
var billingIntervalFor = (config) => Math.max(1, config.billingInterval || BILLING_INTERVAL_FALLBACK_MIN) * 6e4;
|
|
564
|
+
function toConfigState(config) {
|
|
565
|
+
return {
|
|
566
|
+
protocol: {
|
|
567
|
+
version: TOKMON_PROTOCOL_VERSION,
|
|
568
|
+
capabilities: [...TOKMON_CAPABILITIES]
|
|
569
|
+
},
|
|
570
|
+
config
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
var ConfigConflictError = class extends Error {
|
|
574
|
+
constructor(state) {
|
|
575
|
+
super(`config changed on the daemon (current revision ${state.config.revision})`);
|
|
576
|
+
this.state = state;
|
|
577
|
+
this.name = "ConfigConflictError";
|
|
578
|
+
}
|
|
579
|
+
state;
|
|
580
|
+
kind = "conflict";
|
|
581
|
+
};
|
|
582
|
+
var ConfigPersistenceError = class extends Error {
|
|
583
|
+
kind = "persistence";
|
|
584
|
+
constructor(cause) {
|
|
585
|
+
super(cause instanceof Error ? cause.message : "config could not be persisted");
|
|
586
|
+
this.name = "ConfigPersistenceError";
|
|
587
|
+
this.cause = cause;
|
|
588
|
+
}
|
|
589
|
+
};
|
|
588
590
|
async function resolveEngineConfig(config) {
|
|
589
591
|
return {
|
|
590
592
|
resolved: await resolveAccounts(config),
|
|
@@ -593,27 +595,50 @@ async function resolveEngineConfig(config) {
|
|
|
593
595
|
billingIntervalMs: billingIntervalFor(config)
|
|
594
596
|
};
|
|
595
597
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
+
var updateQueues = /* @__PURE__ */ new WeakMap();
|
|
599
|
+
async function applyConfigUpdateUnlocked(engine, state, input) {
|
|
600
|
+
if (input.expectedRevision !== state.config.revision) {
|
|
601
|
+
throw new ConfigConflictError(toConfigState(state.config));
|
|
602
|
+
}
|
|
603
|
+
const normalized = normalizeConfig({
|
|
604
|
+
...input.config,
|
|
605
|
+
revision: state.config.revision + 1
|
|
606
|
+
});
|
|
607
|
+
const engineConfig = await resolveEngineConfig(normalized);
|
|
608
|
+
try {
|
|
609
|
+
await saveConfig(normalized);
|
|
610
|
+
} catch (error) {
|
|
611
|
+
throw new ConfigPersistenceError(error);
|
|
612
|
+
}
|
|
598
613
|
state.config = normalized;
|
|
599
|
-
|
|
600
|
-
engine.setConfig(await resolveEngineConfig(normalized));
|
|
614
|
+
engine.setConfig(engineConfig);
|
|
601
615
|
engine.broadcastConfig(normalized);
|
|
602
|
-
return normalized;
|
|
616
|
+
return toConfigState(normalized);
|
|
617
|
+
}
|
|
618
|
+
async function applyConfigUpdate(engine, state, input) {
|
|
619
|
+
const previous = updateQueues.get(state) ?? Promise.resolve();
|
|
620
|
+
const operation = previous.catch(() => void 0).then(() => applyConfigUpdateUnlocked(engine, state, input));
|
|
621
|
+
const settled = operation.then(() => void 0, () => void 0);
|
|
622
|
+
updateQueues.set(state, settled);
|
|
623
|
+
void settled.then(() => {
|
|
624
|
+
if (updateQueues.get(state) === settled) updateQueues.delete(state);
|
|
625
|
+
});
|
|
626
|
+
return operation;
|
|
603
627
|
}
|
|
604
628
|
|
|
605
629
|
// src/web/ws.ts
|
|
630
|
+
import { timingSafeEqual } from "crypto";
|
|
606
631
|
import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
|
|
607
632
|
import { NodeWS } from "@effect/platform-node/NodeSocket";
|
|
608
633
|
import { Effect, Exit, Layer, Queue, Scope, Stream } from "effect";
|
|
609
634
|
import { RpcSerialization, RpcServer } from "effect/unstable/rpc";
|
|
610
635
|
|
|
611
636
|
// src/web/fs.ts
|
|
612
|
-
import { readdir, stat
|
|
637
|
+
import { readdir, stat, realpath } from "fs/promises";
|
|
613
638
|
import { homedir } from "os";
|
|
614
|
-
import { join as
|
|
639
|
+
import { join as join2, resolve as resolvePath, isAbsolute, sep } from "path";
|
|
615
640
|
function isContained(root, target) {
|
|
616
|
-
return target === root || target.startsWith(root +
|
|
641
|
+
return target === root || target.startsWith(root + sep);
|
|
617
642
|
}
|
|
618
643
|
function parentFor(root, abs) {
|
|
619
644
|
const parentResolved = resolvePath(abs, "..");
|
|
@@ -633,7 +658,7 @@ async function listHomeDirectory(rawPath) {
|
|
|
633
658
|
const abs = isContained(root, real) ? real : root;
|
|
634
659
|
let st;
|
|
635
660
|
try {
|
|
636
|
-
st = await
|
|
661
|
+
st = await stat(abs);
|
|
637
662
|
} catch {
|
|
638
663
|
return { path: abs, parent: parentFor(root, abs), entries: [] };
|
|
639
664
|
}
|
|
@@ -648,7 +673,7 @@ async function listHomeDirectory(rawPath) {
|
|
|
648
673
|
for (const d of dirents) {
|
|
649
674
|
if (d.name.startsWith(".")) continue;
|
|
650
675
|
let dir = d.isDirectory();
|
|
651
|
-
const full =
|
|
676
|
+
const full = join2(abs, d.name);
|
|
652
677
|
if (d.isSymbolicLink()) {
|
|
653
678
|
let real2;
|
|
654
679
|
try {
|
|
@@ -658,7 +683,7 @@ async function listHomeDirectory(rawPath) {
|
|
|
658
683
|
}
|
|
659
684
|
if (!isContained(root, real2)) continue;
|
|
660
685
|
try {
|
|
661
|
-
dir = (await
|
|
686
|
+
dir = (await stat(full)).isDirectory();
|
|
662
687
|
} catch {
|
|
663
688
|
continue;
|
|
664
689
|
}
|
|
@@ -670,41 +695,15 @@ async function listHomeDirectory(rawPath) {
|
|
|
670
695
|
}
|
|
671
696
|
|
|
672
697
|
// src/web/ws.ts
|
|
673
|
-
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1"]);
|
|
674
698
|
function header(req, name) {
|
|
675
699
|
const value = req.headers[name.toLowerCase()];
|
|
676
700
|
return Array.isArray(value) ? value[0] : value;
|
|
677
701
|
}
|
|
678
|
-
function hostOnly(value) {
|
|
679
|
-
if (!value) return null;
|
|
680
|
-
let host = value.trim().toLowerCase();
|
|
681
|
-
if (!host) return null;
|
|
682
|
-
if (host.startsWith("[")) {
|
|
683
|
-
const end = host.indexOf("]");
|
|
684
|
-
return end === -1 ? host.slice(1) : host.slice(1, end);
|
|
685
|
-
}
|
|
686
|
-
return host.split(":")[0] ?? null;
|
|
687
|
-
}
|
|
688
702
|
function isLoopbackHost(value) {
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
if (!origin || origin === "null") return null;
|
|
694
|
-
try {
|
|
695
|
-
return new URL(origin).host;
|
|
696
|
-
} catch {
|
|
697
|
-
return null;
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
function isLoopbackOrigin(origin) {
|
|
701
|
-
if (!origin || origin === "null") return true;
|
|
702
|
-
return isLoopbackHost(originHost(origin) ?? void 0);
|
|
703
|
-
}
|
|
704
|
-
function isSameOrigin(req) {
|
|
705
|
-
const origin = originHost(header(req, "origin"));
|
|
706
|
-
if (!origin) return false;
|
|
707
|
-
return origin.toLowerCase() === (header(req, "host") ?? "").toLowerCase();
|
|
703
|
+
if (!value) return false;
|
|
704
|
+
const host = value.trim().toLowerCase();
|
|
705
|
+
if (!host) return false;
|
|
706
|
+
return /^(?:127\.0\.0\.1|localhost)(?::\d{1,5})?$/.test(host) || /^\[::1\](?::\d{1,5})?$/.test(host);
|
|
708
707
|
}
|
|
709
708
|
function isWsPath(req) {
|
|
710
709
|
try {
|
|
@@ -720,12 +719,15 @@ function wsToken(req) {
|
|
|
720
719
|
return null;
|
|
721
720
|
}
|
|
722
721
|
}
|
|
722
|
+
function tokenMatches(actual, expected) {
|
|
723
|
+
if (!actual) return false;
|
|
724
|
+
const left = Buffer.from(actual);
|
|
725
|
+
const right = Buffer.from(expected);
|
|
726
|
+
return left.length === right.length && timingSafeEqual(left, right);
|
|
727
|
+
}
|
|
723
728
|
function isAuthorized(req, token) {
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
if (!isLoopbackHost(host) || !isLoopbackOrigin(origin)) return false;
|
|
727
|
-
if (wsToken(req) === token) return true;
|
|
728
|
-
return isSameOrigin(req);
|
|
729
|
+
if (!isLoopbackHost(header(req, "host"))) return false;
|
|
730
|
+
return tokenMatches(wsToken(req), token);
|
|
729
731
|
}
|
|
730
732
|
function rejectUpgrade(socket, status = 403, message = "Forbidden") {
|
|
731
733
|
try {
|
|
@@ -761,19 +763,29 @@ function configStream(engine) {
|
|
|
761
763
|
yield* Scope.addFinalizer(scope, Effect.sync(unsubscribe));
|
|
762
764
|
}), { bufferSize: 16, strategy: "sliding" });
|
|
763
765
|
}
|
|
766
|
+
function toConfigUpdateFailure(error) {
|
|
767
|
+
if (error instanceof ConfigConflictError) {
|
|
768
|
+
return { kind: "conflict", state: error.state };
|
|
769
|
+
}
|
|
770
|
+
if (error instanceof ConfigPersistenceError) {
|
|
771
|
+
return { kind: "persistence", message: error.message };
|
|
772
|
+
}
|
|
773
|
+
return { kind: "persistence", message: error instanceof Error ? error.message : "config update failed" };
|
|
774
|
+
}
|
|
764
775
|
async function mountWsRpc(server, deps) {
|
|
765
776
|
const scope = await Effect.runPromise(Scope.make());
|
|
766
777
|
const wss = new NodeWS.WebSocketServer({ noServer: true });
|
|
767
778
|
const handlersLayer = TokmonRpcGroup.toLayer(
|
|
768
779
|
TokmonRpcGroup.of({
|
|
769
|
-
[TOKMON_WS_METHODS.getConfig]: () => Effect.tryPromise(() => Promise.resolve(deps.state.config ?? loadConfig())),
|
|
770
|
-
[TOKMON_WS_METHODS.setConfig]: (config) => Effect.tryPromise(
|
|
771
|
-
|
|
772
|
-
|
|
780
|
+
[TOKMON_WS_METHODS.getConfig]: () => Effect.tryPromise(() => Promise.resolve(deps.state.config ?? loadConfig()).then(toConfigState)),
|
|
781
|
+
[TOKMON_WS_METHODS.setConfig]: (config) => Effect.tryPromise({
|
|
782
|
+
try: () => applyConfigUpdate(deps.engine, deps.state, config),
|
|
783
|
+
catch: toConfigUpdateFailure
|
|
773
784
|
}),
|
|
785
|
+
[TOKMON_WS_METHODS.refresh]: ({ scope: scope2 }) => Effect.promise(() => deps.engine.refresh(scope2)),
|
|
774
786
|
[TOKMON_WS_METHODS.browseFs]: ({ path }) => Effect.tryPromise(() => listHomeDirectory(path)),
|
|
775
787
|
[TOKMON_WS_METHODS.snapshot]: () => snapshotStream(deps.engine),
|
|
776
|
-
[TOKMON_WS_METHODS.config]: () => configStream(deps.engine)
|
|
788
|
+
[TOKMON_WS_METHODS.config]: () => configStream(deps.engine).pipe(Stream.map(toConfigState))
|
|
777
789
|
})
|
|
778
790
|
);
|
|
779
791
|
const httpEffect = await Effect.runPromise(
|
|
@@ -794,6 +806,10 @@ async function mountWsRpc(server, deps) {
|
|
|
794
806
|
)
|
|
795
807
|
);
|
|
796
808
|
const onUpgrade = (req, socket, head) => {
|
|
809
|
+
if (!isLoopbackHost(header(req, "host"))) {
|
|
810
|
+
rejectUpgrade(socket);
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
797
813
|
if (!isWsPath(req)) return;
|
|
798
814
|
if (!isAuthorized(req, deps.wsToken)) {
|
|
799
815
|
rejectUpgrade(socket);
|
|
@@ -801,13 +817,36 @@ async function mountWsRpc(server, deps) {
|
|
|
801
817
|
}
|
|
802
818
|
upgradeHandler(req, socket, head);
|
|
803
819
|
};
|
|
804
|
-
server.
|
|
820
|
+
server.prependListener("upgrade", onUpgrade);
|
|
805
821
|
return async () => {
|
|
806
822
|
server.off("upgrade", onUpgrade);
|
|
823
|
+
for (const client of wss.clients) {
|
|
824
|
+
try {
|
|
825
|
+
client.terminate();
|
|
826
|
+
} catch {
|
|
827
|
+
}
|
|
828
|
+
}
|
|
807
829
|
await new Promise((resolve) => {
|
|
808
|
-
|
|
830
|
+
const timer = setTimeout(resolve, 750);
|
|
831
|
+
timer.unref?.();
|
|
832
|
+
try {
|
|
833
|
+
wss.close(() => {
|
|
834
|
+
clearTimeout(timer);
|
|
835
|
+
resolve();
|
|
836
|
+
});
|
|
837
|
+
} catch {
|
|
838
|
+
clearTimeout(timer);
|
|
839
|
+
resolve();
|
|
840
|
+
}
|
|
809
841
|
});
|
|
810
|
-
await
|
|
842
|
+
await Promise.race([
|
|
843
|
+
Effect.runPromise(Scope.close(scope, Exit.void)).catch(() => {
|
|
844
|
+
}),
|
|
845
|
+
new Promise((resolve) => {
|
|
846
|
+
const timer = setTimeout(resolve, 750);
|
|
847
|
+
timer.unref?.();
|
|
848
|
+
})
|
|
849
|
+
]);
|
|
811
850
|
};
|
|
812
851
|
}
|
|
813
852
|
|
|
@@ -817,12 +856,10 @@ var DEFAULT_PORT = 4317;
|
|
|
817
856
|
var MAX_PORT_TRIES = 20;
|
|
818
857
|
function isLoopbackHostHeader(value) {
|
|
819
858
|
if (!value) return false;
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
else host = host.split(":")[0];
|
|
823
|
-
return host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
859
|
+
const host = value.trim().toLowerCase();
|
|
860
|
+
return /^(?:127\.0\.0\.1|localhost)(?::\d{1,5})?$/.test(host) || /^\[::1\](?::\d{1,5})?$/.test(host);
|
|
824
861
|
}
|
|
825
|
-
function
|
|
862
|
+
function isLoopbackOrigin(origin) {
|
|
826
863
|
if (!origin || origin === "null") return true;
|
|
827
864
|
try {
|
|
828
865
|
const u = new URL(origin);
|
|
@@ -831,37 +868,59 @@ function isLoopbackOrigin2(origin) {
|
|
|
831
868
|
return false;
|
|
832
869
|
}
|
|
833
870
|
}
|
|
834
|
-
function
|
|
835
|
-
const
|
|
871
|
+
function header2(req, name) {
|
|
872
|
+
const value = req.headers[name];
|
|
873
|
+
return Array.isArray(value) ? value[0] : value;
|
|
874
|
+
}
|
|
875
|
+
function tokenMatches2(given, expected) {
|
|
876
|
+
if (!given) return false;
|
|
877
|
+
const actual = Buffer.from(given);
|
|
878
|
+
const token = Buffer.from(expected);
|
|
879
|
+
return actual.length === token.length && timingSafeEqual2(actual, token);
|
|
880
|
+
}
|
|
881
|
+
function guardHost(req, res) {
|
|
882
|
+
if (isLoopbackHostHeader(header2(req, "host"))) return true;
|
|
883
|
+
sendJson(res, 403, { error: "forbidden" });
|
|
884
|
+
return false;
|
|
885
|
+
}
|
|
886
|
+
function guardPrivileged(req, res, wsToken2) {
|
|
887
|
+
const header3 = (n) => {
|
|
836
888
|
const v = req.headers[n];
|
|
837
889
|
return Array.isArray(v) ? v[0] : v;
|
|
838
890
|
};
|
|
839
|
-
if (
|
|
891
|
+
if (!tokenMatches2(header3("x-tokmon-token"), wsToken2)) {
|
|
840
892
|
sendJson(res, 403, { error: "forbidden" });
|
|
841
893
|
return false;
|
|
842
894
|
}
|
|
843
|
-
if (!
|
|
895
|
+
if (!isLoopbackOrigin(header3("origin"))) {
|
|
844
896
|
sendJson(res, 403, { error: "forbidden" });
|
|
845
897
|
return false;
|
|
846
898
|
}
|
|
847
899
|
return true;
|
|
848
900
|
}
|
|
849
|
-
function createRouter(engine, state, vite, webRoot) {
|
|
901
|
+
function createRouter(engine, state, vite, webRoot, wsToken2, version) {
|
|
850
902
|
return (req, res) => {
|
|
851
903
|
const url = req.url || "/";
|
|
852
904
|
const path = url.split("?")[0];
|
|
853
905
|
const method = req.method || "GET";
|
|
906
|
+
if (!guardHost(req, res)) return;
|
|
854
907
|
if (path === "/api/data") {
|
|
855
908
|
engine.touch();
|
|
856
909
|
sendJson(res, 200, engine.snapshot() ?? { pending: true });
|
|
857
910
|
return;
|
|
858
911
|
}
|
|
859
912
|
if (path === "/healthz") {
|
|
860
|
-
sendJson(res, 200, {
|
|
913
|
+
sendJson(res, 200, {
|
|
914
|
+
ok: true,
|
|
915
|
+
ready: engine.snapshot() !== null,
|
|
916
|
+
version,
|
|
917
|
+
// Discovery requires this proof; public health checks retain their useful 200 response.
|
|
918
|
+
owner: tokenMatches2(header2(req, "x-tokmon-token"), wsToken2)
|
|
919
|
+
});
|
|
861
920
|
return;
|
|
862
921
|
}
|
|
863
922
|
if (path === "/api/config") {
|
|
864
|
-
if (!guardPrivileged(req, res)) return;
|
|
923
|
+
if (!guardPrivileged(req, res, wsToken2)) return;
|
|
865
924
|
if (method === "GET") {
|
|
866
925
|
sendJson(res, 200, state.config);
|
|
867
926
|
return;
|
|
@@ -888,51 +947,65 @@ async function startWebServer(opts) {
|
|
|
888
947
|
const version = appVersion();
|
|
889
948
|
const summaryIntervalMs = summaryIntervalFor(state.config);
|
|
890
949
|
const billingIntervalMs = billingIntervalFor(state.config);
|
|
891
|
-
const wsToken2 = randomBytes(32).toString("base64url");
|
|
950
|
+
const wsToken2 = opts.wsToken ?? randomBytes(32).toString("base64url");
|
|
892
951
|
const log = (msg) => {
|
|
893
952
|
if (opts.log) process.stdout.write(msg + "\n");
|
|
894
953
|
};
|
|
895
954
|
const resolved = await resolveAccounts(state.config);
|
|
896
955
|
const server = createServer();
|
|
897
956
|
let vite = null;
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
957
|
+
let engine = null;
|
|
958
|
+
let closeWsRpc = null;
|
|
959
|
+
try {
|
|
960
|
+
if (isDevMode()) vite = await createViteDevServer(server, log);
|
|
961
|
+
const webRoot = vite ? null : findWebRoot();
|
|
962
|
+
if (!vite && !webRoot) log(" \u26A0 no dashboard available \u2014 see the page for build/dev instructions");
|
|
963
|
+
engine = createDataEngine({ version, config: state.config, tz, summaryIntervalMs, billingIntervalMs, resolved });
|
|
964
|
+
server.addListener("request", createRouter(engine, state, vite, webRoot, wsToken2, version));
|
|
965
|
+
closeWsRpc = await mountWsRpc(server, { engine, state, wsToken: wsToken2 });
|
|
966
|
+
const port = await listenWithFallback(server, opts.port ?? DEFAULT_PORT);
|
|
967
|
+
const serverUrl = `http://${HOST}:${port}`;
|
|
968
|
+
if (vite?.warmupRequest) {
|
|
969
|
+
try {
|
|
970
|
+
await Promise.race([vite.warmupRequest("/src/main.tsx"), delay(5e3)]);
|
|
971
|
+
} catch {
|
|
972
|
+
}
|
|
910
973
|
}
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
974
|
+
engine.start();
|
|
975
|
+
let stopped = false;
|
|
976
|
+
return {
|
|
977
|
+
url: serverUrl,
|
|
978
|
+
browserUrl: browserUrl(serverUrl, wsToken2),
|
|
979
|
+
port,
|
|
980
|
+
wsToken: wsToken2,
|
|
981
|
+
snapshot: engine.snapshot,
|
|
982
|
+
config: () => state.config,
|
|
983
|
+
stop: async () => {
|
|
984
|
+
if (stopped) return;
|
|
985
|
+
stopped = true;
|
|
986
|
+
engine?.stop();
|
|
987
|
+
await closeWsRpc?.().catch(() => {
|
|
988
|
+
});
|
|
925
989
|
server.closeAllConnections?.();
|
|
926
|
-
|
|
927
|
-
if (vite) {
|
|
990
|
+
await closeServer(server);
|
|
928
991
|
try {
|
|
929
|
-
await vite
|
|
992
|
+
await vite?.close();
|
|
930
993
|
} catch {
|
|
931
994
|
}
|
|
932
995
|
}
|
|
933
|
-
|
|
996
|
+
};
|
|
997
|
+
} catch (error) {
|
|
998
|
+
engine?.stop();
|
|
999
|
+
await closeWsRpc?.().catch(() => {
|
|
1000
|
+
});
|
|
1001
|
+
server.closeAllConnections?.();
|
|
1002
|
+
await closeServer(server);
|
|
1003
|
+
try {
|
|
1004
|
+
await vite?.close();
|
|
1005
|
+
} catch {
|
|
934
1006
|
}
|
|
935
|
-
|
|
1007
|
+
throw error;
|
|
1008
|
+
}
|
|
936
1009
|
}
|
|
937
1010
|
function delay(ms) {
|
|
938
1011
|
return new Promise((resolve) => {
|
|
@@ -940,6 +1013,22 @@ function delay(ms) {
|
|
|
940
1013
|
t.unref?.();
|
|
941
1014
|
});
|
|
942
1015
|
}
|
|
1016
|
+
function closeServer(server, timeoutMs = 1e3) {
|
|
1017
|
+
return new Promise((resolve) => {
|
|
1018
|
+
const done = () => resolve();
|
|
1019
|
+
const timer = setTimeout(done, timeoutMs);
|
|
1020
|
+
timer.unref?.();
|
|
1021
|
+
try {
|
|
1022
|
+
server.close(() => {
|
|
1023
|
+
clearTimeout(timer);
|
|
1024
|
+
done();
|
|
1025
|
+
});
|
|
1026
|
+
} catch {
|
|
1027
|
+
clearTimeout(timer);
|
|
1028
|
+
done();
|
|
1029
|
+
}
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
943
1032
|
function listenWithFallback(server, startPort) {
|
|
944
1033
|
return new Promise((resolve, reject) => {
|
|
945
1034
|
if (startPort === 0) {
|
|
@@ -969,6 +1058,5 @@ function listenWithFallback(server, startPort) {
|
|
|
969
1058
|
}
|
|
970
1059
|
|
|
971
1060
|
export {
|
|
972
|
-
appVersion,
|
|
973
1061
|
startWebServer
|
|
974
1062
|
};
|