@holdpoint/live-daemon 0.1.0-alpha.4 → 0.1.0-alpha.6

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/dist/index.js CHANGED
@@ -133,7 +133,7 @@ async function readHealthyDaemonLock(homeDir) {
133
133
  // src/server.ts
134
134
  import { createServer as createServer2 } from "http";
135
135
  import { randomUUID as randomUUID2 } from "crypto";
136
- import { createReadStream, existsSync as existsSync4 } from "fs";
136
+ import { createReadStream, existsSync as existsSync4, renameSync, writeFileSync as writeFileSync2 } from "fs";
137
137
  import { dirname, extname, join as join3, resolve as resolve3, sep } from "path";
138
138
  import { fileURLToPath, URL } from "url";
139
139
  import {
@@ -142,6 +142,7 @@ import {
142
142
  EventV1Schema as EventV1Schema2,
143
143
  EventsBatchSchema
144
144
  } from "@holdpoint/live-protocol";
145
+ import { parseHoldpointYaml } from "@holdpoint/yaml-core";
145
146
  import { WebSocket, WebSocketServer } from "ws";
146
147
 
147
148
  // src/auth.ts
@@ -181,6 +182,17 @@ async function readJsonBody(req) {
181
182
  if (chunks.length === 0) return null;
182
183
  return JSON.parse(Buffer.concat(chunks).toString("utf8"));
183
184
  }
185
+ async function readTextBody(req, maxBytes = 512e3) {
186
+ const chunks = [];
187
+ let total = 0;
188
+ for await (const chunk of req) {
189
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
190
+ total += buf.length;
191
+ if (total > maxBytes) throw new Error("Request body too large");
192
+ chunks.push(buf);
193
+ }
194
+ return Buffer.concat(chunks).toString("utf8");
195
+ }
184
196
  function authorizeRequest(req, res, token, port) {
185
197
  const origin = req.headers.origin;
186
198
  if (origin && origin !== `http://127.0.0.1:${port}`) {
@@ -363,33 +375,37 @@ function sha12(value) {
363
375
  return createHash("sha256").update(value).digest("hex").slice(0, 12);
364
376
  }
365
377
  function identifyProject(cwd) {
378
+ let root;
366
379
  try {
367
- const root = realpathSync2(
380
+ root = realpathSync2(
368
381
  execFileSync("git", ["rev-parse", "--show-toplevel"], {
369
382
  cwd,
370
383
  encoding: "utf8",
371
384
  stdio: ["ignore", "pipe", "ignore"]
372
385
  }).trim()
373
386
  );
374
- return {
375
- hash: sha12(root),
376
- name: basename(root),
377
- root
378
- };
379
387
  } catch {
380
- const root = realpathSync2(cwd);
381
- return {
382
- hash: sha12(root),
383
- name: basename(root),
384
- root
385
- };
388
+ try {
389
+ root = realpathSync2(cwd);
390
+ } catch {
391
+ root = cwd;
392
+ }
386
393
  }
394
+ return {
395
+ hash: sha12(root),
396
+ name: basename(root) || root,
397
+ root
398
+ };
387
399
  }
388
400
 
389
401
  // src/store.ts
390
402
  function sessionFileName(engine, sessionId) {
391
403
  return `${encodeURIComponent(engine)}-${encodeURIComponent(sessionId)}.jsonl`;
392
404
  }
405
+ function describeError(err) {
406
+ if (err instanceof Error) return err.message || err.name;
407
+ return String(err);
408
+ }
393
409
  async function readJsonl(filePath) {
394
410
  const raw = await readFile(filePath, "utf8");
395
411
  return raw.split("\n").map((line) => line.trim()).filter(Boolean).flatMap((line) => {
@@ -421,14 +437,33 @@ var LiveStore = class _LiveStore {
421
437
  async replayPending() {
422
438
  const pendingDir = join2(this.homeDir, "spool", "pending");
423
439
  if (!existsSync3(pendingDir)) return;
424
- const entries = await readdir(pendingDir);
440
+ let entries;
441
+ try {
442
+ entries = await readdir(pendingDir);
443
+ } catch (err) {
444
+ console.error(`[holdpoint] failed to read pending dir: ${describeError(err)}`);
445
+ return;
446
+ }
425
447
  for (const entry of entries.filter((name) => name.endsWith(".jsonl"))) {
426
448
  const filePath = join2(pendingDir, entry);
427
- const events = await readJsonl(filePath);
428
- if (events.length > 0) {
429
- await this.ingestMany(events);
449
+ let events = [];
450
+ try {
451
+ events = await readJsonl(filePath);
452
+ } catch (err) {
453
+ console.error(`[holdpoint] failed to read pending file ${entry}: ${describeError(err)}`);
454
+ }
455
+ for (const event of events) {
456
+ try {
457
+ await this.ingest(event);
458
+ } catch (err) {
459
+ console.error(`[holdpoint] dropped pending event from ${entry}: ${describeError(err)}`);
460
+ }
461
+ }
462
+ try {
463
+ await rm(filePath, { force: true });
464
+ } catch (err) {
465
+ console.error(`[holdpoint] failed to remove pending file ${entry}: ${describeError(err)}`);
430
466
  }
431
- await rm(filePath, { force: true });
432
467
  }
433
468
  }
434
469
  async ingestMany(events) {
@@ -680,7 +715,6 @@ function matchesSubscription(subscription, event) {
680
715
  // src/server.ts
681
716
  var __dirname = dirname(fileURLToPath(import.meta.url));
682
717
  var LIVE_UI_DIR = join3(__dirname, "live-ui");
683
- var BUILDER_UI_DIR = join3(__dirname, "builder-ui");
684
718
  var CONTENT_SECURITY_POLICY = "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; connect-src 'self' ws: http:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'";
685
719
  var MIME = {
686
720
  ".css": "text/css; charset=utf-8",
@@ -768,10 +802,10 @@ function resolveUiFilePath(uiDir, requestedPath) {
768
802
  return existsSync4(join3(uiDir, "index.html")) ? join3(uiDir, "index.html") : null;
769
803
  }
770
804
  function normalizeUiPath(path) {
771
- if (!path) return "/live/";
772
- if (path === "/live" || path.startsWith("/live/")) return "/live/";
773
- if (path === "/builder" || path.startsWith("/builder/")) return "/builder/";
774
- return "/live/";
805
+ if (path === "/builder" || path?.startsWith("/builder/")) {
806
+ return { pathname: "/live/", tab: "checks" };
807
+ }
808
+ return { pathname: "/live/" };
775
809
  }
776
810
  function registerProjectFromAuthUrl(url, registeredProjects) {
777
811
  const hash = url.searchParams.get("project");
@@ -990,14 +1024,17 @@ async function startLiveServer(options) {
990
1024
  return;
991
1025
  }
992
1026
  registerProjectFromAuthUrl(url, registered);
993
- const redirectPath = normalizeUiPath(url.searchParams.get("path"));
1027
+ const redirectTarget = normalizeUiPath(url.searchParams.get("path"));
994
1028
  const redirectUrl = new URL(`http://${host}:${actualPort}`);
995
- redirectUrl.pathname = redirectPath;
1029
+ redirectUrl.pathname = redirectTarget.pathname;
996
1030
  for (const [key, value] of url.searchParams.entries()) {
997
1031
  if (key !== "token" && key !== "path") {
998
1032
  redirectUrl.searchParams.set(key, value);
999
1033
  }
1000
1034
  }
1035
+ if (redirectTarget.tab) {
1036
+ redirectUrl.searchParams.set("tab", redirectTarget.tab);
1037
+ }
1001
1038
  writeUiAuthCookie(res, state.token);
1002
1039
  res.writeHead(302, {
1003
1040
  location: redirectUrl.toString(),
@@ -1048,6 +1085,47 @@ async function startLiveServer(options) {
1048
1085
  createReadStream(reportsPath).pipe(res);
1049
1086
  return;
1050
1087
  }
1088
+ if (req.method === "PUT" && url.pathname === "/__holdpoint/checks") {
1089
+ if (!authorizeRequest(req, res, state.token, actualPort)) {
1090
+ return;
1091
+ }
1092
+ const project = getProjectForRequest(url, registered);
1093
+ if (!project) {
1094
+ writeJson(res, 404, { ok: false, error: "Project not registered for this UI session" });
1095
+ return;
1096
+ }
1097
+ const checksPath = resolve3(project.root, "checks.yaml");
1098
+ if (!isWithinRoot(checksPath, project.root)) {
1099
+ writeJson(res, 400, { ok: false, error: "Invalid checks path" });
1100
+ return;
1101
+ }
1102
+ let body;
1103
+ try {
1104
+ body = await readTextBody(req);
1105
+ } catch {
1106
+ writeJson(res, 413, { ok: false, error: "checks.yaml is too large" });
1107
+ return;
1108
+ }
1109
+ try {
1110
+ parseHoldpointYaml(body);
1111
+ } catch (parseError) {
1112
+ writeJson(res, 422, {
1113
+ ok: false,
1114
+ error: `Invalid checks.yaml: ${parseError.message}`
1115
+ });
1116
+ return;
1117
+ }
1118
+ try {
1119
+ const tmpPath = `${checksPath}.holdpoint-tmp-${randomUUID2().slice(0, 8)}`;
1120
+ writeFileSync2(tmpPath, body, "utf8");
1121
+ renameSync(tmpPath, checksPath);
1122
+ } catch (writeError) {
1123
+ writeJson(res, 500, { ok: false, error: writeError.message });
1124
+ return;
1125
+ }
1126
+ writeJson(res, 200, { ok: true });
1127
+ return;
1128
+ }
1051
1129
  if (url.pathname.startsWith("/v1/") && !authorizeRequest(req, res, state.token, actualPort)) {
1052
1130
  return;
1053
1131
  }
@@ -1142,28 +1220,30 @@ async function startLiveServer(options) {
1142
1220
  res.end();
1143
1221
  return;
1144
1222
  }
1145
- if (url.pathname === "/live" || url.pathname === "/builder") {
1146
- res.writeHead(302, { location: `${url.pathname}/`, "cache-control": "no-store" });
1223
+ if (url.pathname === "/builder" || url.pathname.startsWith("/builder/")) {
1224
+ const target = new URL("/live/", `http://${host}:${actualPort}`);
1225
+ for (const [key, value] of url.searchParams.entries()) target.searchParams.set(key, value);
1226
+ target.searchParams.set("tab", "checks");
1227
+ res.writeHead(302, {
1228
+ location: `${target.pathname}${target.search}`,
1229
+ "cache-control": "no-store"
1230
+ });
1231
+ res.end();
1232
+ return;
1233
+ }
1234
+ if (url.pathname === "/live") {
1235
+ res.writeHead(302, { location: "/live/", "cache-control": "no-store" });
1147
1236
  res.end();
1148
1237
  return;
1149
1238
  }
1150
- const uiRoute = url.pathname.startsWith("/builder/") ? {
1151
- appName: "Holdpoint Builder",
1152
- dir: BUILDER_UI_DIR,
1153
- path: url.pathname.replace(/^\/builder\/?/, "")
1154
- } : url.pathname.startsWith("/live/") ? {
1155
- appName: "Holdpoint Live",
1156
- dir: LIVE_UI_DIR,
1157
- path: url.pathname.replace(/^\/live\/?/, "")
1158
- } : null;
1159
- if (!uiRoute) {
1239
+ if (!url.pathname.startsWith("/live/")) {
1160
1240
  res.writeHead(302, { location: "/live/", "cache-control": "no-store" });
1161
1241
  res.end();
1162
1242
  return;
1163
1243
  }
1164
- const filePath = resolveUiFilePath(uiRoute.dir, uiRoute.path);
1244
+ const filePath = resolveUiFilePath(LIVE_UI_DIR, url.pathname.replace(/^\/live\/?/, ""));
1165
1245
  if (!filePath) {
1166
- servePlaceholder(res, uiRoute.appName);
1246
+ servePlaceholder(res, "Holdpoint Live");
1167
1247
  return;
1168
1248
  }
1169
1249
  serveUiAsset(res, filePath);