@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 +119 -39
- package/dist/index.js.map +1 -1
- package/dist/live-ui/assets/index-BAgfQlSi.css +2 -0
- package/dist/live-ui/assets/index-DZZKTCKK.js +405 -0
- package/dist/live-ui/assets/index-DZZKTCKK.js.map +1 -0
- package/dist/live-ui/index.html +2 -2
- package/package.json +4 -4
- package/dist/builder-ui/assets/index-34-pmyiJ.js +0 -391
- package/dist/builder-ui/assets/index-34-pmyiJ.js.map +0 -1
- package/dist/builder-ui/assets/index-Bwt9-ehF.css +0 -2
- package/dist/builder-ui/favicon.svg +0 -10
- package/dist/builder-ui/index.html +0 -14
- package/dist/live-ui/assets/index-CGJwpNcJ.js +0 -10
- package/dist/live-ui/assets/index-CGJwpNcJ.js.map +0 -1
- package/dist/live-ui/assets/index-z6W1Fyph.css +0 -2
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
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
await
|
|
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 (
|
|
772
|
-
|
|
773
|
-
|
|
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
|
|
1027
|
+
const redirectTarget = normalizeUiPath(url.searchParams.get("path"));
|
|
994
1028
|
const redirectUrl = new URL(`http://${host}:${actualPort}`);
|
|
995
|
-
redirectUrl.pathname =
|
|
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 === "/
|
|
1146
|
-
|
|
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
|
-
|
|
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(
|
|
1244
|
+
const filePath = resolveUiFilePath(LIVE_UI_DIR, url.pathname.replace(/^\/live\/?/, ""));
|
|
1165
1245
|
if (!filePath) {
|
|
1166
|
-
servePlaceholder(res,
|
|
1246
|
+
servePlaceholder(res, "Holdpoint Live");
|
|
1167
1247
|
return;
|
|
1168
1248
|
}
|
|
1169
1249
|
serveUiAsset(res, filePath);
|