@oh-my-pi/omp-stats 17.2.8 → 17.2.9
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/CHANGELOG.md +6 -0
- package/dist/types/port-conflict.d.ts +11 -1
- package/dist/types/server.d.ts +1 -0
- package/package.json +4 -4
- package/src/index.ts +2 -2
- package/src/port-conflict.ts +37 -24
- package/src/server.ts +25 -15
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.2.9] - 2026-08-05
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Restricted the stats dashboard to IPv4 loopback and removed wildcard CORS access to its API ([#7633](https://github.com/can1357/oh-my-pi/issues/7633)).
|
|
10
|
+
|
|
5
11
|
## [17.2.4] - 2026-08-01
|
|
6
12
|
|
|
7
13
|
### Fixed
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
/** Header stamped on every dashboard response so reuse probes can identify us. */
|
|
2
2
|
export declare const STATS_DASHBOARD_HEADER = "x-omp-stats-dashboard";
|
|
3
|
-
/**
|
|
3
|
+
/** Identity-header value for dashboards enforcing loopback-only, same-origin access. */
|
|
4
|
+
export declare const STATS_DASHBOARD_SECURITY_VERSION = "2";
|
|
5
|
+
/** IPv4 loopback address shared by the dashboard server and reuse probe. */
|
|
6
|
+
export declare const STATS_DASHBOARD_HOSTNAME = "127.0.0.1";
|
|
7
|
+
/**
|
|
8
|
+
* Reuse a secure dashboard or reclaim an insecure HTTP dashboard before binding.
|
|
9
|
+
* The preflight is needed on platforms that permit wildcard and loopback-specific
|
|
10
|
+
* listeners to coexist on one port.
|
|
11
|
+
*/
|
|
12
|
+
export declare function prepareStatsPort(port: number): Promise<"retry" | "reuse">;
|
|
13
|
+
/** Reuse or reclaim a listener found after the server bind reports EADDRINUSE. */
|
|
4
14
|
export declare function recoverStatsPort(port: number): Promise<"retry" | "reuse">;
|
package/dist/types/server.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export declare function handleApi(req: Request): Promise<Response>;
|
|
|
6
6
|
* Start the HTTP server, reusing a live dashboard or reclaiming a stale omp listener.
|
|
7
7
|
*/
|
|
8
8
|
export declare function startServer(port?: number): Promise<{
|
|
9
|
+
hostname: string;
|
|
9
10
|
port: number;
|
|
10
11
|
stop: () => void;
|
|
11
12
|
}>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/omp-stats",
|
|
4
|
-
"version": "17.2.
|
|
4
|
+
"version": "17.2.9",
|
|
5
5
|
"description": "Local observability dashboard for pi AI usage statistics",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"fmt": "biome format --write ."
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@oh-my-pi/pi-ai": "17.2.
|
|
43
|
-
"@oh-my-pi/pi-catalog": "17.2.
|
|
44
|
-
"@oh-my-pi/pi-utils": "17.2.
|
|
42
|
+
"@oh-my-pi/pi-ai": "17.2.9",
|
|
43
|
+
"@oh-my-pi/pi-catalog": "17.2.9",
|
|
44
|
+
"@oh-my-pi/pi-utils": "17.2.9",
|
|
45
45
|
"@tailwindcss/node": "^4.3.2",
|
|
46
46
|
"chart.js": "^4.5.1",
|
|
47
47
|
"date-fns": "^4.4.0",
|
package/src/index.ts
CHANGED
|
@@ -171,8 +171,8 @@ Examples:
|
|
|
171
171
|
|
|
172
172
|
// Start server
|
|
173
173
|
const port = parseInt(values.port || "3847", 10);
|
|
174
|
-
const { port: actualPort } = await startServer(port);
|
|
175
|
-
console.log(`Dashboard available at: http
|
|
174
|
+
const { hostname, port: actualPort } = await startServer(port);
|
|
175
|
+
console.log(`Dashboard available at: http://${hostname}:${actualPort}`);
|
|
176
176
|
console.log("Press Ctrl+C to stop\n");
|
|
177
177
|
|
|
178
178
|
// Keep process running
|
package/src/port-conflict.ts
CHANGED
|
@@ -18,30 +18,27 @@ interface PortHolder {
|
|
|
18
18
|
/** Header stamped on every dashboard response so reuse probes can identify us. */
|
|
19
19
|
export const STATS_DASHBOARD_HEADER = "x-omp-stats-dashboard";
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
/** Identity-header value for dashboards enforcing loopback-only, same-origin access. */
|
|
22
|
+
export const STATS_DASHBOARD_SECURITY_VERSION = "2";
|
|
23
|
+
|
|
24
|
+
/** IPv4 loopback address shared by the dashboard server and reuse probe. */
|
|
25
|
+
export const STATS_DASHBOARD_HOSTNAME = "127.0.0.1";
|
|
26
|
+
|
|
27
|
+
type StatsDashboardProbe = "reusable" | "occupied" | "unreachable";
|
|
28
|
+
|
|
29
|
+
async function probeStatsDashboard(port: number): Promise<StatsDashboardProbe> {
|
|
22
30
|
try {
|
|
23
|
-
const response = await fetch(`http
|
|
31
|
+
const response = await fetch(`http://${STATS_DASHBOARD_HOSTNAME}:${port}/api/stats/models`, {
|
|
24
32
|
signal: AbortSignal.timeout(STATS_PROBE_TIMEOUT_MS),
|
|
25
33
|
});
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
await response.body?.cancel();
|
|
33
|
-
return true;
|
|
34
|
-
}
|
|
35
|
-
// Older dashboards predate the header; fall back to the response shape
|
|
36
|
-
// (`/api/stats/models` returns a JSON array) so we never reuse — or later
|
|
37
|
-
// kill — a foreign 200 responder such as an SPA dev server catch-all.
|
|
38
|
-
if (!(response.headers.get("content-type") ?? "").includes("application/json")) {
|
|
39
|
-
await response.body?.cancel();
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
return Array.isArray(await response.json());
|
|
34
|
+
const reusable =
|
|
35
|
+
response.status === 200 &&
|
|
36
|
+
response.headers.get(STATS_DASHBOARD_HEADER) === STATS_DASHBOARD_SECURITY_VERSION &&
|
|
37
|
+
!response.headers.has("Access-Control-Allow-Origin");
|
|
38
|
+
await response.body?.cancel();
|
|
39
|
+
return reusable ? "reusable" : "occupied";
|
|
43
40
|
} catch {
|
|
44
|
-
return
|
|
41
|
+
return "unreachable";
|
|
45
42
|
}
|
|
46
43
|
}
|
|
47
44
|
|
|
@@ -216,10 +213,7 @@ async function terminatePortHolder(holder: PortHolder): Promise<void> {
|
|
|
216
213
|
await Bun.sleep(PROCESS_EXIT_POLL_MS);
|
|
217
214
|
}
|
|
218
215
|
|
|
219
|
-
|
|
220
|
-
export async function recoverStatsPort(port: number): Promise<"retry" | "reuse"> {
|
|
221
|
-
if (await probeStatsDashboard(port)) return "reuse";
|
|
222
|
-
|
|
216
|
+
async function reclaimStatsPort(port: number): Promise<"retry"> {
|
|
223
217
|
const holder = await findPortHolder(port);
|
|
224
218
|
if (!holder) {
|
|
225
219
|
throw new Error(`Port ${port} is in use, but the listening process could not be identified.`);
|
|
@@ -248,3 +242,22 @@ export async function recoverStatsPort(port: number): Promise<"retry" | "reuse">
|
|
|
248
242
|
await terminatePortHolder(holder);
|
|
249
243
|
return "retry";
|
|
250
244
|
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Reuse a secure dashboard or reclaim an insecure HTTP dashboard before binding.
|
|
248
|
+
* The preflight is needed on platforms that permit wildcard and loopback-specific
|
|
249
|
+
* listeners to coexist on one port.
|
|
250
|
+
*/
|
|
251
|
+
export async function prepareStatsPort(port: number): Promise<"retry" | "reuse"> {
|
|
252
|
+
if (port === 0) return "retry";
|
|
253
|
+
const probe = await probeStatsDashboard(port);
|
|
254
|
+
if (probe === "reusable") return "reuse";
|
|
255
|
+
if (probe === "occupied") return reclaimStatsPort(port);
|
|
256
|
+
return "retry";
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Reuse or reclaim a listener found after the server bind reports EADDRINUSE. */
|
|
260
|
+
export async function recoverStatsPort(port: number): Promise<"retry" | "reuse"> {
|
|
261
|
+
if ((await probeStatsDashboard(port)) === "reusable") return "reuse";
|
|
262
|
+
return reclaimStatsPort(port);
|
|
263
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -21,7 +21,13 @@ import {
|
|
|
21
21
|
import { decodeEmbeddedClientArchive } from "./embedded-client";
|
|
22
22
|
import embeddedClientArchiveTxt from "./embedded-client.generated.txt";
|
|
23
23
|
import { getGainDashboardStats } from "./gain-aggregator";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
prepareStatsPort,
|
|
26
|
+
recoverStatsPort,
|
|
27
|
+
STATS_DASHBOARD_HEADER,
|
|
28
|
+
STATS_DASHBOARD_HOSTNAME,
|
|
29
|
+
STATS_DASHBOARD_SECURITY_VERSION,
|
|
30
|
+
} from "./port-conflict";
|
|
25
31
|
|
|
26
32
|
const EMBEDDED_CLIENT_ARCHIVE = decodeEmbeddedClientArchive(embeddedClientArchiveTxt);
|
|
27
33
|
|
|
@@ -303,21 +309,19 @@ async function handleStatic(requestPath: string): Promise<Response> {
|
|
|
303
309
|
function createDashboardServer(port: number) {
|
|
304
310
|
const server = Bun.serve({
|
|
305
311
|
port,
|
|
312
|
+
hostname: STATS_DASHBOARD_HOSTNAME,
|
|
306
313
|
async fetch(req) {
|
|
307
314
|
const url = new URL(req.url);
|
|
308
315
|
const path = url.pathname;
|
|
309
316
|
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
315
|
-
"Access-Control-Allow-Headers": "Content-Type",
|
|
316
|
-
[STATS_DASHBOARD_HEADER]: "1",
|
|
317
|
+
// The identity header lets another omp session's reuse probe positively
|
|
318
|
+
// recognize this dashboard without allowing cross-origin API reads.
|
|
319
|
+
const dashboardHeaders: Record<string, string> = {
|
|
320
|
+
[STATS_DASHBOARD_HEADER]: STATS_DASHBOARD_SECURITY_VERSION,
|
|
317
321
|
};
|
|
318
322
|
|
|
319
323
|
if (req.method === "OPTIONS") {
|
|
320
|
-
return new Response(null, { headers:
|
|
324
|
+
return new Response(null, { headers: dashboardHeaders });
|
|
321
325
|
}
|
|
322
326
|
|
|
323
327
|
try {
|
|
@@ -329,10 +333,10 @@ function createDashboardServer(port: number) {
|
|
|
329
333
|
response = await handleStatic(path);
|
|
330
334
|
}
|
|
331
335
|
|
|
332
|
-
// Add
|
|
336
|
+
// Add the dashboard identity header to all responses.
|
|
333
337
|
const headers = new Headers(response.headers);
|
|
334
|
-
for (const key in
|
|
335
|
-
headers.set(key,
|
|
338
|
+
for (const key in dashboardHeaders) {
|
|
339
|
+
headers.set(key, dashboardHeaders[key]);
|
|
336
340
|
}
|
|
337
341
|
|
|
338
342
|
return new Response(response.body, {
|
|
@@ -343,7 +347,7 @@ function createDashboardServer(port: number) {
|
|
|
343
347
|
console.error("Server error:", error);
|
|
344
348
|
return Response.json(
|
|
345
349
|
{ error: error instanceof Error ? error.message : "Unknown error" },
|
|
346
|
-
{ status: 500, headers:
|
|
350
|
+
{ status: 500, headers: dashboardHeaders },
|
|
347
351
|
);
|
|
348
352
|
}
|
|
349
353
|
},
|
|
@@ -354,12 +358,17 @@ function createDashboardServer(port: number) {
|
|
|
354
358
|
/**
|
|
355
359
|
* Start the HTTP server, reusing a live dashboard or reclaiming a stale omp listener.
|
|
356
360
|
*/
|
|
357
|
-
export async function startServer(port = 3847): Promise<{ port: number; stop: () => void }> {
|
|
361
|
+
export async function startServer(port = 3847): Promise<{ hostname: string; port: number; stop: () => void }> {
|
|
358
362
|
await ensureClientBuild();
|
|
363
|
+
const preparation = await prepareStatsPort(port);
|
|
364
|
+
if (preparation === "reuse") {
|
|
365
|
+
return { hostname: STATS_DASHBOARD_HOSTNAME, port, stop: () => {} };
|
|
366
|
+
}
|
|
359
367
|
|
|
360
368
|
try {
|
|
361
369
|
const server = createDashboardServer(port);
|
|
362
370
|
return {
|
|
371
|
+
hostname: STATS_DASHBOARD_HOSTNAME,
|
|
363
372
|
port: server.port ?? port,
|
|
364
373
|
stop: () => server.stop(),
|
|
365
374
|
};
|
|
@@ -368,12 +377,13 @@ export async function startServer(port = 3847): Promise<{ port: number; stop: ()
|
|
|
368
377
|
|
|
369
378
|
const recovery = await recoverStatsPort(port);
|
|
370
379
|
if (recovery === "reuse") {
|
|
371
|
-
return { port, stop: () => {} };
|
|
380
|
+
return { hostname: STATS_DASHBOARD_HOSTNAME, port, stop: () => {} };
|
|
372
381
|
}
|
|
373
382
|
|
|
374
383
|
try {
|
|
375
384
|
const server = createDashboardServer(port);
|
|
376
385
|
return {
|
|
386
|
+
hostname: STATS_DASHBOARD_HOSTNAME,
|
|
377
387
|
port: server.port ?? port,
|
|
378
388
|
stop: () => server.stop(),
|
|
379
389
|
};
|