@gajae-code/stats 0.11.5 → 0.11.7

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.
@@ -0,0 +1,3 @@
1
+ export declare function createCompiledClientAssetHandler(loadArchiveBytes: () => Uint8Array | null | Promise<Uint8Array | null>): {
2
+ response(requestPath: string): Promise<Response>;
3
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/stats",
4
- "version": "0.11.5",
4
+ "version": "0.11.7",
5
5
  "description": "Local observability dashboard for pi AI usage statistics",
6
6
  "homepage": "https://gajae-code.com",
7
7
  "author": "Yeachan-Heo",
@@ -37,8 +37,8 @@
37
37
  "fmt": "biome format --write ."
38
38
  },
39
39
  "dependencies": {
40
- "@gajae-code/ai": "0.11.5",
41
- "@gajae-code/utils": "0.11.5",
40
+ "@gajae-code/ai": "0.11.7",
41
+ "@gajae-code/utils": "0.11.7",
42
42
  "@tailwindcss/node": "^4.2.4",
43
43
  "chart.js": "^4.5.1",
44
44
  "date-fns": "^4.1.0",
@@ -0,0 +1,96 @@
1
+ import * as path from "node:path";
2
+
3
+ const INDEX_ASSET = "index.html";
4
+ const UNSAFE_ARCHIVE_NAME = /[\u0000-\u001f\u007f%?#]/u;
5
+ const WINDOWS_ABSOLUTE_PATH = /^[a-zA-Z]:\//;
6
+
7
+ const CONTENT_TYPES: Readonly<Record<string, string>> = Object.freeze({
8
+ ".css": "text/css; charset=utf-8",
9
+ ".gif": "image/gif",
10
+ ".html": "text/html; charset=utf-8",
11
+ ".ico": "image/x-icon",
12
+ ".jpeg": "image/jpeg",
13
+ ".jpg": "image/jpeg",
14
+ ".js": "text/javascript; charset=utf-8",
15
+ ".json": "application/json; charset=utf-8",
16
+ ".map": "application/json; charset=utf-8",
17
+ ".png": "image/png",
18
+ ".svg": "image/svg+xml",
19
+ ".ttf": "font/ttf",
20
+ ".webp": "image/webp",
21
+ ".woff": "font/woff",
22
+ ".woff2": "font/woff2",
23
+ });
24
+
25
+ function normalizeArchiveName(archiveName: string): string {
26
+ const normalized = archiveName.normalize("NFC");
27
+ const segments = normalized.split("/");
28
+ if (
29
+ !normalized ||
30
+ normalized.startsWith("/") ||
31
+ WINDOWS_ABSOLUTE_PATH.test(normalized) ||
32
+ normalized.includes("\\") ||
33
+ UNSAFE_ARCHIVE_NAME.test(normalized) ||
34
+ segments.some(segment => !segment || segment === "." || segment === "..")
35
+ ) {
36
+ throw new Error(`Unsafe compiled stats client archive entry: ${JSON.stringify(archiveName)}`);
37
+ }
38
+ return normalized;
39
+ }
40
+
41
+ function contentType(assetName: string): string {
42
+ return CONTENT_TYPES[path.posix.extname(assetName).toLowerCase()] ?? "application/octet-stream";
43
+ }
44
+
45
+ async function parseArchive(archiveBytes: Uint8Array): Promise<ReadonlyMap<string, Blob>> {
46
+ const files = await new Bun.Archive(archiveBytes).files();
47
+ const assets = new Map<string, Blob>();
48
+ for (const [archiveName, file] of files) {
49
+ const assetName = normalizeArchiveName(archiveName);
50
+ if (assets.has(assetName)) {
51
+ throw new Error(`Duplicate compiled stats client archive entry: ${JSON.stringify(assetName)}`);
52
+ }
53
+ assets.set(assetName, new Blob([file], { type: contentType(assetName) }));
54
+ }
55
+ if (!assets.has(INDEX_ASSET)) {
56
+ throw new Error("Compiled stats client archive is missing index.html");
57
+ }
58
+ return assets;
59
+ }
60
+
61
+ function responseForPath(assets: ReadonlyMap<string, Blob>, requestPath: string): Response {
62
+ const assetName = requestPath === "/" ? INDEX_ASSET : requestPath.replace(/^\//, "");
63
+ const asset = assets.get(assetName);
64
+ if (asset) return new Response(asset);
65
+ return new Response(assets.get(INDEX_ASSET));
66
+ }
67
+
68
+ export function createCompiledClientAssetHandler(
69
+ loadArchiveBytes: () => Uint8Array | null | Promise<Uint8Array | null>,
70
+ ): { response(requestPath: string): Promise<Response> } {
71
+ let initialization: Promise<ReadonlyMap<string, Blob>> | null = null;
72
+
73
+ async function getAssets(): Promise<ReadonlyMap<string, Blob>> {
74
+ if (initialization) return await initialization;
75
+ const attempt = (async () => {
76
+ const archiveBytes = await loadArchiveBytes();
77
+ if (!archiveBytes) {
78
+ throw new Error("Compiled stats client bundle missing. Rebuild binary with embedded stats assets.");
79
+ }
80
+ return await parseArchive(archiveBytes);
81
+ })();
82
+ initialization = attempt;
83
+ try {
84
+ return await attempt;
85
+ } catch (error) {
86
+ if (initialization === attempt) initialization = null;
87
+ throw error;
88
+ }
89
+ }
90
+
91
+ return {
92
+ async response(requestPath) {
93
+ return responseForPath(await getAssets(), requestPath);
94
+ },
95
+ };
96
+ }
package/src/server.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import * as fs from "node:fs/promises";
2
- import * as os from "node:os";
3
2
  import * as path from "node:path";
4
3
  import { $ } from "bun";
5
4
  import {
@@ -14,6 +13,7 @@ import {
14
13
  getTotalMessageCount,
15
14
  syncAllSessions,
16
15
  } from "./aggregator";
16
+ import { createCompiledClientAssetHandler } from "./compiled-client-assets";
17
17
  import embeddedClientArchiveTxt from "./embedded-client.generated.txt";
18
18
 
19
19
  const getEmbeddedClientArchive = (() => {
@@ -29,59 +29,7 @@ const IS_BUN_COMPILED =
29
29
  import.meta.url.includes("$bunfs") ||
30
30
  import.meta.url.includes("~BUN") ||
31
31
  import.meta.url.includes("%7EBUN");
32
-
33
- const COMPILED_CLIENT_DIR_ROOT = path.join(os.tmpdir(), "gjc-stats-client");
34
- let compiledClientDirPromise: Promise<string> | null = null;
35
-
36
- function sanitizeArchivePath(archivePath: string): string | null {
37
- const normalized = archivePath.replaceAll("\\", "/").replace(/^\.\//, "");
38
- if (!normalized || normalized === ".") return null;
39
- if (normalized.includes("..") || path.isAbsolute(normalized)) return null;
40
- return normalized;
41
- }
42
-
43
- async function extractEmbeddedClientArchive(archiveBytes: Buffer, outputDir: string): Promise<void> {
44
- const archive = new Bun.Archive(archiveBytes);
45
- const files = await archive.files();
46
- const extractRoot = path.resolve(outputDir);
47
-
48
- for (const [archivePath, file] of files) {
49
- const sanitizedPath = sanitizeArchivePath(archivePath);
50
- if (!sanitizedPath) continue;
51
- const destinationPath = path.resolve(extractRoot, sanitizedPath);
52
- if (!destinationPath.startsWith(extractRoot + path.sep)) {
53
- throw new Error(`Archive entry escapes extraction directory: ${archivePath}`);
54
- }
55
- await Bun.write(destinationPath, file);
56
- }
57
- }
58
-
59
- async function getCompiledClientDir(): Promise<string> {
60
- if (!IS_BUN_COMPILED) return STATIC_DIR;
61
- if (compiledClientDirPromise) return compiledClientDirPromise;
62
-
63
- const archiveBytes = getEmbeddedClientArchive?.();
64
- if (!archiveBytes) {
65
- throw new Error("Compiled stats client bundle missing. Rebuild binary with embedded stats assets.");
66
- }
67
-
68
- compiledClientDirPromise = (async () => {
69
- const bundleHash = Bun.hash(archiveBytes).toString(16);
70
- const outputDir = path.join(COMPILED_CLIENT_DIR_ROOT, bundleHash);
71
- const markerPath = path.join(outputDir, "index.html");
72
- try {
73
- const marker = await fs.stat(markerPath);
74
- if (marker.isFile()) return outputDir;
75
- } catch {}
76
-
77
- await fs.rm(outputDir, { recursive: true, force: true });
78
- await fs.mkdir(outputDir, { recursive: true });
79
- await extractEmbeddedClientArchive(archiveBytes, outputDir);
80
- return outputDir;
81
- })();
82
-
83
- return compiledClientDirPromise;
84
- }
32
+ const compiledClientAssets = createCompiledClientAssetHandler(() => getEmbeddedClientArchive?.() ?? null);
85
33
 
86
34
  async function getLatestMtime(dir: string): Promise<number> {
87
35
  const entries = await fs.readdir(dir, { withFileTypes: true });
@@ -247,9 +195,10 @@ async function handleApi(req: Request): Promise<Response> {
247
195
  * Handle static file requests.
248
196
  */
249
197
  async function handleStatic(requestPath: string): Promise<Response> {
250
- const staticDir = IS_BUN_COMPILED ? await getCompiledClientDir() : STATIC_DIR;
198
+ if (IS_BUN_COMPILED) return await compiledClientAssets.response(requestPath);
199
+
251
200
  const filePath = requestPath === "/" ? "/index.html" : requestPath;
252
- const fullPath = path.join(staticDir, filePath);
201
+ const fullPath = path.join(STATIC_DIR, filePath);
253
202
 
254
203
  const file = Bun.file(fullPath);
255
204
  if (await file.exists()) {
@@ -257,7 +206,7 @@ async function handleStatic(requestPath: string): Promise<Response> {
257
206
  }
258
207
 
259
208
  // SPA fallback
260
- const index = Bun.file(path.join(staticDir, "index.html"));
209
+ const index = Bun.file(path.join(STATIC_DIR, "index.html"));
261
210
  if (await index.exists()) {
262
211
  return new Response(index);
263
212
  }