@sverka/hub 0.0.0-stage → 0.2.41

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,121 @@
1
+ //#region src/types.d.ts
2
+ /** An API token with an access level. */
3
+ interface HubToken {
4
+ /** Human label for the token (from the tokens file). */
5
+ readonly name: string;
6
+ readonly token: string;
7
+ /** `ro` can only read; `rw` can also write. */
8
+ readonly access: "ro" | "rw";
9
+ }
10
+ /** Options for the self-hosted hub server. */
11
+ interface HubServerOptions {
12
+ /** Data directory — blobs, snapshots, and the SQLite index live here. */
13
+ readonly dataDir: string;
14
+ /** Port to listen on (default: 7357). `0` picks an ephemeral port. */
15
+ readonly port?: number;
16
+ /** Host to bind (default: "0.0.0.0" — a hub usually serves a LAN/CI net). */
17
+ readonly host?: string;
18
+ /** Explicit token list. When omitted, tokens are loaded from
19
+ * `<dataDir>/tokens` (name:token:ro|rw lines) plus the
20
+ * `SVERKA_HUB_ADMIN_TOKEN` env var. */
21
+ readonly tokens?: readonly HubToken[];
22
+ /** Max cache blob size in bytes (default: 512 MiB → 413 above). */
23
+ readonly maxBlobBytes?: number;
24
+ /** Max JSON body size for run uploads (default: 32 MiB). */
25
+ readonly maxJsonBytes?: number;
26
+ }
27
+ interface HubServer {
28
+ readonly port: number;
29
+ readonly url: string;
30
+ readonly dataDir: string;
31
+ close(): Promise<void>;
32
+ }
33
+ /** A run record stored by POST /v1/runs. */
34
+ interface HubStoredRun {
35
+ readonly runId: string;
36
+ readonly project: string;
37
+ readonly entry: string;
38
+ readonly status: string;
39
+ readonly startedAt: number | null;
40
+ readonly durationMs: number | null;
41
+ /** JSON: `{ total, bySeverity }`. */
42
+ readonly findingCounts: string;
43
+ readonly policyVerdict: string | null;
44
+ readonly uploadedAt: number;
45
+ readonly reportJson: string;
46
+ readonly findingsJson: string;
47
+ }
48
+ //#endregion
49
+ //#region src/server.d.ts
50
+ /** Start the hub server. Resolves once listening. */
51
+ export declare function startHubServer(opts: HubServerOptions): Promise<HubServer>;
52
+ //#endregion
53
+ //#region src/store.d.ts
54
+ export declare function isValidCacheKey(key: string): boolean;
55
+ export declare function isValidRunId(runId: string): boolean;
56
+ interface RunInsert {
57
+ readonly project: string;
58
+ readonly entry: string;
59
+ readonly report: Record<string, unknown>;
60
+ readonly findings: readonly unknown[];
61
+ readonly runId?: string | undefined;
62
+ }
63
+ interface RunListQuery {
64
+ readonly project?: string | undefined;
65
+ readonly limit?: number | undefined;
66
+ /** uploadedAt exclusive cursor — rows uploaded before this instant. */
67
+ readonly before?: number | undefined;
68
+ }
69
+ interface FlakyRow {
70
+ readonly stepId: string;
71
+ readonly successRate: number;
72
+ readonly runs: number;
73
+ }
74
+ /**
75
+ * The hub store: blobs + snapshots under `dataDir`, run index in
76
+ * `dataDir/hub.db`.
77
+ */
78
+ export declare function createHubStore(dataDir: string): {
79
+ dataDir: string;
80
+ putBlob: (project: string, key: string, blob: Buffer) => void;
81
+ getBlob: (project: string, key: string, opts?: {
82
+ prefix?: boolean | undefined;
83
+ }) => {
84
+ blob: Buffer;
85
+ key: string;
86
+ } | undefined;
87
+ putSnapshot: (project: string, runId: string, body: string) => void;
88
+ getSnapshot: (project: string, runId: string) => string | undefined;
89
+ deleteSnapshot: (project: string, runId: string) => boolean;
90
+ insertRun: (input: RunInsert) => HubStoredRun;
91
+ listRuns: (query: RunListQuery) => HubStoredRun[];
92
+ getRun: (runId: string) => HubStoredRun | undefined;
93
+ listProjects: () => string[];
94
+ flaky: (project: string, opts?: {
95
+ n?: number | undefined;
96
+ steps?: readonly string[] | undefined;
97
+ }) => FlakyRow[];
98
+ close: () => void;
99
+ };
100
+ type HubStore = ReturnType<typeof createHubStore>;
101
+ //#endregion
102
+ //#region src/auth.d.ts
103
+ /**
104
+ * Parse a tokens file body: one `name:token:ro|rw` per line; `#` comments
105
+ * and blank lines skipped; malformed lines skipped. Token values are
106
+ * `[A-Za-z0-9._~-]` — anything else is ignored (never echo user data).
107
+ */
108
+ export declare function parseTokensFile(text: string): HubToken[];
109
+ /**
110
+ * Resolve the effective token set: explicit option > tokens file +
111
+ * SVERKA_HUB_ADMIN_TOKEN. When nothing is configured, a fresh admin token
112
+ * is generated, appended to `<dataDir>/tokens`, and returned so the CLI
113
+ * can print it once — an unauthenticated hub is never silently open.
114
+ */
115
+ export declare function resolveTokens(dataDir: string, explicit?: readonly HubToken[]): {
116
+ tokens: readonly HubToken[];
117
+ generated: HubToken | null;
118
+ };
119
+ //#endregion
120
+ export type { FlakyRow, HubServer, HubServerOptions, HubStore, HubStoredRun, HubToken, RunInsert, RunListQuery };
121
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/server.ts","../src/store.ts","../src/auth.ts"],"mappings":";;UAGiB;;WAEN;WACA;;WAEA;;;UAIM;;WAEN;;WAEA;;WAEA;;;;WAIA,kBAAkB;;WAElB;;WAEA;;UAGM;WACN;WACA;WACA;EACT,SAAS;;;UAIM;WACN;WACA;WACA;WACA;WACA;WACA;;WAEA;WACA;WACA;WACA;WACA;;;;;wBCYK,eAAe,MAAM,mBAAmB,QAAQ;;;wBCzChD,gBAAgB;wBAIhB,aAAa;UA8BZ;WACN;WACA;WACA,QAAQ;WACR;WACA;;UAGM;WACN;WACA;;WAEA;;UAGM;WACN;WACA;WACA;;;;;;wBAOK,eAAe;;EAUH,UAAA,iBAAM,aAAa,MAAQ;EA4C1C,UAAA,iBAAM,aACJ;IACF;;IACN,MAAM;IAAQ;;EAqCW,cAAA,iBAAM,eAAe;EAMrB,cAAA,iBAAM;EAQH,iBAAA,iBAAM;EAkBb,YAAA,OAAA,cAAY;EAsDb,WAAA,OAAA,iBAAe;EAuBjB,SAAA,kBAAS;EAmBP;EAcd,QAAA,iBAAM;IAEb;IACA;QAED;;;KA8CO,WAAW,kBAAkB;;;;;;;;wBC9VzB,gBAAgB,eAAe;;;;;;;wBA2B/B,cACd,iBACA,oBAAoB;EACjB,iBAAiB;EAAY,WAAW"}
package/dist/index.mjs ADDED
@@ -0,0 +1,917 @@
1
+ import { createServer } from "node:http";
2
+ import { renderHubFlaky, renderHubIndex, renderHubRunDetail, renderHubRunList } from "@sverka/ui";
3
+ import { generateSarifHtml } from "@sverka/sarif-viewer-web";
4
+ import { appendFileSync, chmodSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
7
+ import { DatabaseSync } from "node:sqlite";
8
+ //#region src/auth.ts
9
+ const TOKENS_FILE = "tokens";
10
+ /**
11
+ * Parse a tokens file body: one `name:token:ro|rw` per line; `#` comments
12
+ * and blank lines skipped; malformed lines skipped. Token values are
13
+ * `[A-Za-z0-9._~-]` — anything else is ignored (never echo user data).
14
+ */
15
+ function parseTokensFile(text) {
16
+ const tokens = [];
17
+ for (const rawLine of text.split("\n")) {
18
+ const line = rawLine.trim();
19
+ if (line === "" || line.startsWith("#")) continue;
20
+ const parts = line.split(":");
21
+ if (parts.length !== 3) continue;
22
+ const [name, token, access] = parts;
23
+ if (name === void 0 || token === void 0 || access !== "ro" && access !== "rw" || !/^[A-Za-z0-9._~-]{8,}$/.test(token)) continue;
24
+ tokens.push({
25
+ name,
26
+ token,
27
+ access
28
+ });
29
+ }
30
+ return tokens;
31
+ }
32
+ /**
33
+ * Resolve the effective token set: explicit option > tokens file +
34
+ * SVERKA_HUB_ADMIN_TOKEN. When nothing is configured, a fresh admin token
35
+ * is generated, appended to `<dataDir>/tokens`, and returned so the CLI
36
+ * can print it once — an unauthenticated hub is never silently open.
37
+ */
38
+ function resolveTokens(dataDir, explicit) {
39
+ if (explicit !== void 0 && explicit.length > 0) return {
40
+ tokens: explicit,
41
+ generated: null
42
+ };
43
+ const tokens = [];
44
+ const file = join(dataDir, TOKENS_FILE);
45
+ try {
46
+ tokens.push(...parseTokensFile(readFileSync(file, "utf8")));
47
+ } catch {}
48
+ const admin = process.env["SVERKA_HUB_ADMIN_TOKEN"];
49
+ if (typeof admin === "string" && admin.length >= 8) tokens.push({
50
+ name: "admin-env",
51
+ token: admin,
52
+ access: "rw"
53
+ });
54
+ if (tokens.length === 0) {
55
+ const generated = {
56
+ name: "admin",
57
+ token: `svk_${randomBytes(24).toString("base64url")}`,
58
+ access: "rw"
59
+ };
60
+ try {
61
+ mkdirSync(dataDir, {
62
+ recursive: true,
63
+ mode: 448
64
+ });
65
+ appendFileSync(file, `${generated.name}:${generated.token}:${generated.access}\n`, { mode: 384 });
66
+ chmodSync(file, 384);
67
+ } catch {}
68
+ tokens.push(generated);
69
+ return {
70
+ tokens,
71
+ generated
72
+ };
73
+ }
74
+ return {
75
+ tokens,
76
+ generated: null
77
+ };
78
+ }
79
+ /**
80
+ * Constant-time token comparison. Both sides are SHA-256'd first so the
81
+ * compare time leaks neither the token's length nor a matching prefix.
82
+ */
83
+ function tokenEquals(a, b) {
84
+ const ha = createHash("sha256").update(a).digest();
85
+ const hb = createHash("sha256").update(b).digest();
86
+ return timingSafeEqual(ha, hb);
87
+ }
88
+ /** Extract the bearer token from Authorization, `?token=`, or the
89
+ * `hub_token` cookie (the dashboard sets it from `?token=`). */
90
+ function extractToken(req, query) {
91
+ const auth = req.headers.authorization;
92
+ if (typeof auth === "string" && auth.startsWith("Bearer ")) return {
93
+ token: auth.slice(7),
94
+ viaQuery: false
95
+ };
96
+ const queryToken = query.get("token");
97
+ if (queryToken !== null && queryToken !== "") return {
98
+ token: queryToken,
99
+ viaQuery: true
100
+ };
101
+ const cookie = req.headers.cookie;
102
+ if (typeof cookie === "string") for (const part of cookie.split(";")) {
103
+ const [k, ...rest] = part.trim().split("=");
104
+ if (k === "hub_token") return {
105
+ token: rest.join("="),
106
+ viaQuery: false
107
+ };
108
+ }
109
+ return {
110
+ token: null,
111
+ viaQuery: false
112
+ };
113
+ }
114
+ //#endregion
115
+ //#region src/store.ts
116
+ /** Cache keys are lowercase url-safe slugs — anything else is a 400. */
117
+ const KEY_PATTERN = /^[a-z0-9-]{1,128}$/;
118
+ /** Run ids are generated or client-supplied; keep them filename-safe. */
119
+ const RUN_ID_PATTERN = /^[A-Za-z0-9._~-]{1,128}$/;
120
+ function isValidCacheKey(key) {
121
+ return KEY_PATTERN.test(key);
122
+ }
123
+ function isValidRunId(runId) {
124
+ return RUN_ID_PATTERN.test(runId);
125
+ }
126
+ function sha256(value) {
127
+ return createHash("sha256").update(value).digest("hex");
128
+ }
129
+ /** Projects arrive URL-encoded — decoded strings may contain '/'. The
130
+ * storage dir encodes again to stay flat and traversal-proof. */
131
+ function projectDir(root, project) {
132
+ return join(root, encodeURIComponent(project));
133
+ }
134
+ const CREATE_RUNS_TABLE = `
135
+ CREATE TABLE IF NOT EXISTS runs (
136
+ run_id TEXT PRIMARY KEY,
137
+ project TEXT NOT NULL,
138
+ entry TEXT NOT NULL,
139
+ status TEXT NOT NULL,
140
+ started_at INTEGER,
141
+ duration_ms INTEGER,
142
+ finding_counts TEXT NOT NULL,
143
+ verdict TEXT,
144
+ uploaded_at INTEGER NOT NULL,
145
+ report_json TEXT NOT NULL,
146
+ findings_json TEXT NOT NULL
147
+ )
148
+ `;
149
+ /**
150
+ * The hub store: blobs + snapshots under `dataDir`, run index in
151
+ * `dataDir/hub.db`.
152
+ */
153
+ function createHubStore(dataDir) {
154
+ mkdirSync(dataDir, {
155
+ recursive: true,
156
+ mode: 448
157
+ });
158
+ const db = new DatabaseSync(join(dataDir, "hub.db"));
159
+ db.exec(CREATE_RUNS_TABLE);
160
+ const cacheDir = (project) => projectDir(join(dataDir, "cache"), project);
161
+ const putBlob = (project, key, blob) => {
162
+ const dir = cacheDir(project);
163
+ mkdirSync(dir, { recursive: true });
164
+ const base = join(dir, sha256(key));
165
+ writeFileSync(`${base}.blob`, blob);
166
+ writeFileSync(`${base}.json`, JSON.stringify({
167
+ key,
168
+ size: blob.length,
169
+ createdAt: Date.now()
170
+ }));
171
+ };
172
+ const readBlobDir = (project) => {
173
+ const dir = cacheDir(project);
174
+ let names;
175
+ try {
176
+ names = readdirSync(dir);
177
+ } catch {
178
+ return [];
179
+ }
180
+ const entries = [];
181
+ for (const file of names) {
182
+ if (!file.endsWith(".json")) continue;
183
+ try {
184
+ const meta = JSON.parse(readFileSync(join(dir, file), "utf8"));
185
+ if (typeof meta.key === "string") entries.push({
186
+ file: file.slice(0, -5),
187
+ key: meta.key,
188
+ createdAt: typeof meta.createdAt === "number" ? meta.createdAt : 0
189
+ });
190
+ } catch {}
191
+ }
192
+ return entries;
193
+ };
194
+ const getBlob = (project, key, opts) => {
195
+ const exact = join(cacheDir(project), `${sha256(key)}.blob`);
196
+ try {
197
+ return {
198
+ blob: readFileSync(exact),
199
+ key
200
+ };
201
+ } catch {}
202
+ if (opts?.prefix !== true) return void 0;
203
+ let best;
204
+ for (const entry of readBlobDir(project)) {
205
+ if (!entry.key.startsWith(key)) continue;
206
+ if (best === void 0 || entry.createdAt > best.createdAt) best = entry;
207
+ }
208
+ if (best === void 0) return void 0;
209
+ const blobPath = join(cacheDir(project), `${best.file}.blob`);
210
+ try {
211
+ return {
212
+ blob: readFileSync(blobPath),
213
+ key: best.key
214
+ };
215
+ } catch {
216
+ return;
217
+ }
218
+ };
219
+ const snapshotPath = (project, runId) => join(projectDir(join(dataDir, "snapshots"), project), `${encodeURIComponent(runId)}.json`);
220
+ const putSnapshot = (project, runId, body) => {
221
+ const dir = projectDir(join(dataDir, "snapshots"), project);
222
+ mkdirSync(dir, { recursive: true });
223
+ writeFileSync(snapshotPath(project, runId), body, { mode: 384 });
224
+ };
225
+ const getSnapshot = (project, runId) => {
226
+ try {
227
+ return readFileSync(snapshotPath(project, runId), "utf8");
228
+ } catch {
229
+ return;
230
+ }
231
+ };
232
+ const deleteSnapshot = (project, runId) => {
233
+ try {
234
+ unlinkSync(snapshotPath(project, runId));
235
+ return true;
236
+ } catch {
237
+ return false;
238
+ }
239
+ };
240
+ const insertStmt = db.prepare(`INSERT OR REPLACE INTO runs
241
+ (run_id, project, entry, status, started_at, duration_ms,
242
+ finding_counts, verdict, uploaded_at, report_json, findings_json)
243
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
244
+ const insertRun = (input) => {
245
+ const runId = input.runId !== void 0 && isValidRunId(input.runId) ? input.runId : randomUUID();
246
+ const report = input.report;
247
+ const data = typeof report["data"] === "object" && report["data"] !== null ? report["data"] : {};
248
+ const status = typeof data["status"] === "string" ? data["status"] : "unknown";
249
+ const durationMs = numOrNull(report["durationMs"]) ?? numOrNull(data["durationMs"]);
250
+ const uploadedAt = Date.now();
251
+ const startedAt = durationMs === null ? null : uploadedAt - durationMs;
252
+ const findingCounts = JSON.stringify({
253
+ total: input.findings.length,
254
+ bySeverity: severityCounts(input.findings)
255
+ });
256
+ const verdict = typeof data["verdict"] === "string" ? data["verdict"] : null;
257
+ const reportJson = JSON.stringify(report);
258
+ const findingsJson = JSON.stringify(input.findings);
259
+ insertStmt.run(runId, input.project, input.entry, status, startedAt, durationMs, findingCounts, verdict, uploadedAt, reportJson, findingsJson);
260
+ return {
261
+ runId,
262
+ project: input.project,
263
+ entry: input.entry,
264
+ status,
265
+ startedAt,
266
+ durationMs,
267
+ findingCounts,
268
+ policyVerdict: verdict,
269
+ uploadedAt,
270
+ reportJson,
271
+ findingsJson
272
+ };
273
+ };
274
+ const listRuns = (query) => {
275
+ const clauses = [];
276
+ const params = [];
277
+ if (query.project !== void 0) {
278
+ clauses.push("project = ?");
279
+ params.push(query.project);
280
+ }
281
+ if (query.before !== void 0) {
282
+ clauses.push("uploaded_at < ?");
283
+ params.push(query.before);
284
+ }
285
+ const limit = Math.min(Math.max(query.limit ?? 50, 1), 200);
286
+ const where = clauses.length === 0 ? "" : `WHERE ${clauses.join(" AND ")}`;
287
+ return db.prepare(`SELECT run_id, project, entry, status, started_at, duration_ms,
288
+ finding_counts, verdict, uploaded_at
289
+ FROM runs ${where} ORDER BY uploaded_at DESC LIMIT ?`).all(...params, limit).map(rowToSummary);
290
+ };
291
+ const getRun = (runId) => {
292
+ const row = db.prepare("SELECT * FROM runs WHERE run_id = ?").get(runId);
293
+ if (row === void 0) return void 0;
294
+ return {
295
+ runId: row["run_id"],
296
+ project: row["project"],
297
+ entry: row["entry"],
298
+ status: row["status"],
299
+ startedAt: row["started_at"] ?? null,
300
+ durationMs: row["duration_ms"] ?? null,
301
+ findingCounts: row["finding_counts"],
302
+ policyVerdict: row["verdict"] ?? null,
303
+ uploadedAt: row["uploaded_at"],
304
+ reportJson: row["report_json"],
305
+ findingsJson: row["findings_json"]
306
+ };
307
+ };
308
+ const listProjects = () => {
309
+ return db.prepare("SELECT DISTINCT project FROM runs ORDER BY project").all().map((r) => r.project);
310
+ };
311
+ /**
312
+ * Flaky-step aggregation: per-step success rate across the last N runs
313
+ * of a project. Success = "succeeded" | "cache-hit"; the denominator
314
+ * counts only runs where the step actually produced a verdict (skipped
315
+ * and cancelled steps are excluded).
316
+ */
317
+ const flaky = (project, opts) => {
318
+ const n = Math.min(Math.max(opts?.n ?? 20, 1), 100);
319
+ const rows = db.prepare(`SELECT report_json FROM runs WHERE project = ?
320
+ ORDER BY uploaded_at DESC LIMIT ?`).all(project, n);
321
+ const wanted = opts?.steps !== void 0 ? new Set(opts.steps) : null;
322
+ const stats = /* @__PURE__ */ new Map();
323
+ for (const row of rows) for (const step of reportSteps(row.report_json)) accumulateFlakyStep(stats, wanted, step);
324
+ return [...stats.entries()].map(([stepId, s]) => ({
325
+ stepId,
326
+ successRate: s.total === 0 ? 0 : s.ok / s.total,
327
+ runs: s.total
328
+ })).sort((a, b) => a.successRate - b.successRate);
329
+ };
330
+ return {
331
+ dataDir,
332
+ putBlob,
333
+ getBlob,
334
+ putSnapshot,
335
+ getSnapshot,
336
+ deleteSnapshot,
337
+ insertRun,
338
+ listRuns,
339
+ getRun,
340
+ listProjects,
341
+ flaky,
342
+ close: () => {
343
+ try {
344
+ db.close();
345
+ } catch {}
346
+ }
347
+ };
348
+ }
349
+ function numOrNull(value) {
350
+ return typeof value === "number" ? value : null;
351
+ }
352
+ /** Per-severity tally over the uploaded findings array. */
353
+ function severityCounts(findings) {
354
+ const counts = {};
355
+ for (const f of findings) {
356
+ const sev = typeof f === "object" && f !== null ? f.severity : void 0;
357
+ if (typeof sev === "string") counts[sev] = (counts[sev] ?? 0) + 1;
358
+ }
359
+ return counts;
360
+ }
361
+ /** Steps array inside a stored report JSON — [] on any shape failure. */
362
+ function reportSteps(reportJson) {
363
+ let report;
364
+ try {
365
+ report = JSON.parse(reportJson);
366
+ } catch {
367
+ return [];
368
+ }
369
+ const data = report["data"];
370
+ const steps = typeof data === "object" && data !== null ? data.steps : void 0;
371
+ return Array.isArray(steps) ? steps : [];
372
+ }
373
+ /** Statuses that produce a verdict — skipped/cancelled steps don't
374
+ * count toward a step's success rate. */
375
+ const FLAKY_STATUSES = /* @__PURE__ */ new Set([
376
+ "succeeded",
377
+ "failed",
378
+ "cache-hit"
379
+ ]);
380
+ function accumulateFlakyStep(stats, wanted, step) {
381
+ if (typeof step !== "object" || step === null) return;
382
+ const { stepId, status } = step;
383
+ if (typeof stepId !== "string" || typeof status !== "string") return;
384
+ if (wanted !== null && !wanted.has(stepId)) return;
385
+ if (!FLAKY_STATUSES.has(status)) return;
386
+ const s = stats.get(stepId) ?? {
387
+ ok: 0,
388
+ total: 0
389
+ };
390
+ s.total += 1;
391
+ if (status !== "failed") s.ok += 1;
392
+ stats.set(stepId, s);
393
+ }
394
+ function rowToSummary(row) {
395
+ return {
396
+ runId: row["run_id"],
397
+ project: row["project"],
398
+ entry: row["entry"],
399
+ status: row["status"],
400
+ startedAt: row["started_at"] ?? null,
401
+ durationMs: row["duration_ms"] ?? null,
402
+ findingCounts: row["finding_counts"],
403
+ policyVerdict: row["verdict"] ?? null,
404
+ uploadedAt: row["uploaded_at"],
405
+ reportJson: "",
406
+ findingsJson: "[]"
407
+ };
408
+ }
409
+ //#endregion
410
+ //#region src/server.ts
411
+ const MAX_BLOB_DEFAULT = 536870912;
412
+ const MAX_JSON_DEFAULT = 33554432;
413
+ const API_HEADERS = {
414
+ "Content-Type": "application/json; charset=utf-8",
415
+ "X-Content-Type-Options": "nosniff"
416
+ };
417
+ const PAGE_HEADERS = {
418
+ "Content-Type": "text/html; charset=utf-8",
419
+ "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; img-src 'self'; frame-src 'self'",
420
+ "X-Frame-Options": "DENY",
421
+ "X-Content-Type-Options": "nosniff"
422
+ };
423
+ const SARIF_PAGE_HEADERS = {
424
+ "Content-Type": "text/html; charset=utf-8",
425
+ "Content-Security-Policy": "default-src 'self'; script-src 'self' 'sha256-x9+d/4JxomKClc3VqIk1V6NKTggeahAWBzMe4jvu7Ec=' 'sha256-SKcdA9aMlus8zeJkaXi1rBBOaGcYgbLddLTB7hCBKm0=' 'sha256-RHNT17OCv3OqIHedvPQaLavcOKHQd1OiBVo17IqkhXQ='; style-src 'unsafe-inline'; img-src 'self' data:",
426
+ "X-Frame-Options": "SAMEORIGIN",
427
+ "X-Content-Type-Options": "nosniff"
428
+ };
429
+ const ENC = (s) => encodeURIComponent(s);
430
+ /** Start the hub server. Resolves once listening. */
431
+ function startHubServer(opts) {
432
+ const store = createHubStore(opts.dataDir);
433
+ const { tokens, generated } = resolveTokens(opts.dataDir, opts.tokens);
434
+ if (generated !== null) {
435
+ console.log(`sverka hub: generated admin token ${generated.token}`);
436
+ console.log(` saved to ${opts.dataDir}/tokens`);
437
+ }
438
+ const maxBlob = opts.maxBlobBytes ?? MAX_BLOB_DEFAULT;
439
+ const maxJson = opts.maxJsonBytes ?? MAX_JSON_DEFAULT;
440
+ const server = createServer((req, res) => {
441
+ handle(req, res, store, tokens, {
442
+ maxBlob,
443
+ maxJson
444
+ }).catch((err) => {
445
+ if (!res.headersSent) json(res, 500, {
446
+ code: "INTERNAL",
447
+ message: "internal error"
448
+ });
449
+ if (err instanceof Error) console.error("hub request failed:", err.message);
450
+ });
451
+ });
452
+ return new Promise((resolve, reject) => {
453
+ server.on("error", reject);
454
+ server.listen(opts.port ?? 7357, opts.host ?? "0.0.0.0", () => {
455
+ const addr = server.address();
456
+ const url = addr.family === "IPv6" ? `http://[${addr.address}]:${addr.port}` : `http://${addr.address}:${addr.port}`;
457
+ resolve({
458
+ port: addr.port,
459
+ url,
460
+ dataDir: opts.dataDir,
461
+ close: () => closeServer(server, store)
462
+ });
463
+ });
464
+ });
465
+ }
466
+ function closeServer(server, store) {
467
+ return new Promise((done) => {
468
+ server.close(() => {
469
+ store.close();
470
+ done();
471
+ });
472
+ });
473
+ }
474
+ const ROUTES = [
475
+ cacheRoute,
476
+ snapshotRoute,
477
+ runsRoute,
478
+ runRoute,
479
+ flakyApiRoute,
480
+ indexRoute,
481
+ findingsPageRoute,
482
+ runPageRoute,
483
+ flakyPageRoute
484
+ ];
485
+ async function handle(req, res, store, tokens, limits) {
486
+ const url = new URL(req.url ?? "/", "https://hub.invalid");
487
+ const ctx = {
488
+ req,
489
+ res,
490
+ store,
491
+ url,
492
+ method: req.method ?? "GET",
493
+ path: url.pathname,
494
+ limits
495
+ };
496
+ if (ctx.path === "/v1/ping") {
497
+ if (ctx.method === "GET") json(res, 200, { status: "ok" });
498
+ else methodNotAllowed(res);
499
+ return;
500
+ }
501
+ const auth = authenticate(req, url.searchParams, tokens, res);
502
+ if (auth === null) return;
503
+ if (ctx.method !== "GET" && ctx.method !== "HEAD" && auth.access !== "rw") {
504
+ json(res, 403, {
505
+ code: "READ_ONLY",
506
+ message: "token is read-only"
507
+ });
508
+ return;
509
+ }
510
+ promoteQueryToken(ctx, auth);
511
+ for (const route of ROUTES) if (await route(ctx)) return;
512
+ json(res, 404, {
513
+ code: "NOT_FOUND",
514
+ message: "unknown route"
515
+ });
516
+ }
517
+ async function cacheRoute(ctx) {
518
+ const m = match2(ctx.path, /^\/v1\/cache\/([^/]+)\/([^/]+)$/);
519
+ if (m === null) return false;
520
+ const [project, key] = m;
521
+ if (!isValidCacheKey(key)) {
522
+ json(ctx.res, 400, {
523
+ code: "BAD_KEY",
524
+ message: "invalid cache key"
525
+ });
526
+ return true;
527
+ }
528
+ if (ctx.method === "PUT") {
529
+ const body = await readBody(ctx.req, ctx.limits.maxBlob, ctx.res);
530
+ if (body === null) return true;
531
+ ctx.store.putBlob(project, key, body);
532
+ ctx.res.writeHead(201, API_HEADERS);
533
+ ctx.res.end();
534
+ return true;
535
+ }
536
+ if (ctx.method === "GET") {
537
+ const hit = ctx.store.getBlob(project, key, { prefix: ctx.url.searchParams.get("prefix") === "1" });
538
+ if (hit === void 0) {
539
+ json(ctx.res, 404, {
540
+ code: "NOT_FOUND",
541
+ message: "cache entry not found"
542
+ });
543
+ return true;
544
+ }
545
+ ctx.res.writeHead(200, {
546
+ "Content-Type": "application/octet-stream",
547
+ "x-sverka-cache-key": hit.key,
548
+ "X-Content-Type-Options": "nosniff"
549
+ });
550
+ ctx.res.end(hit.blob);
551
+ return true;
552
+ }
553
+ methodNotAllowed(ctx.res);
554
+ return true;
555
+ }
556
+ async function snapshotRoute(ctx) {
557
+ const m = match2(ctx.path, /^\/v1\/snapshots\/([^/]+)\/([^/]+)$/);
558
+ if (m === null) return false;
559
+ const [project, runId] = m;
560
+ if (!isValidRunId(runId)) {
561
+ json(ctx.res, 400, {
562
+ code: "BAD_ID",
563
+ message: "invalid run id"
564
+ });
565
+ return true;
566
+ }
567
+ if (ctx.method === "PUT") {
568
+ const body = await readBody(ctx.req, ctx.limits.maxJson, ctx.res);
569
+ if (body === null) return true;
570
+ ctx.store.putSnapshot(project, runId, body.toString("utf8"));
571
+ ctx.res.writeHead(201, API_HEADERS);
572
+ ctx.res.end();
573
+ return true;
574
+ }
575
+ if (ctx.method === "GET") {
576
+ const snap = ctx.store.getSnapshot(project, runId);
577
+ if (snap === void 0) {
578
+ json(ctx.res, 404, {
579
+ code: "NOT_FOUND",
580
+ message: "snapshot not found"
581
+ });
582
+ return true;
583
+ }
584
+ ctx.res.writeHead(200, API_HEADERS);
585
+ ctx.res.end(snap);
586
+ return true;
587
+ }
588
+ if (ctx.method === "DELETE") {
589
+ const existed = ctx.store.deleteSnapshot(project, runId);
590
+ ctx.res.writeHead(existed ? 204 : 404, API_HEADERS);
591
+ ctx.res.end();
592
+ return true;
593
+ }
594
+ methodNotAllowed(ctx.res);
595
+ return true;
596
+ }
597
+ async function runsRoute(ctx) {
598
+ if (ctx.path !== "/v1/runs") return false;
599
+ if (ctx.method === "POST") {
600
+ const body = await readBody(ctx.req, ctx.limits.maxJson, ctx.res);
601
+ if (body === null) return true;
602
+ const insert = parseRunInsert(body, ctx.res);
603
+ if (insert === null) return true;
604
+ const run = ctx.store.insertRun(insert);
605
+ json(ctx.res, 201, {
606
+ runId: run.runId,
607
+ url: `/v1/runs/${ENC(run.runId)}`
608
+ });
609
+ return true;
610
+ }
611
+ if (ctx.method === "GET") {
612
+ const runs = ctx.store.listRuns({
613
+ project: ctx.url.searchParams.get("project") ?? void 0,
614
+ limit: numParam(ctx.url, "limit"),
615
+ before: numParam(ctx.url, "before")
616
+ });
617
+ json(ctx.res, 200, runs.map(runSummary));
618
+ return true;
619
+ }
620
+ methodNotAllowed(ctx.res);
621
+ return true;
622
+ }
623
+ /** Parse + validate the POST /v1/runs body; writes the 400 itself and
624
+ * returns null when the payload is unusable. */
625
+ function parseRunInsert(body, res) {
626
+ let payload;
627
+ try {
628
+ payload = JSON.parse(body.toString("utf8"));
629
+ } catch {
630
+ json(res, 400, {
631
+ code: "BAD_JSON",
632
+ message: "invalid JSON body"
633
+ });
634
+ return null;
635
+ }
636
+ const { project, entry, report, findings, runId } = payload;
637
+ if (typeof project !== "string" || project === "") return badBody(res, "project required");
638
+ if (typeof entry !== "string" || entry === "") return badBody(res, "entry required");
639
+ if (typeof report !== "object" || report === null) return badBody(res, "report required");
640
+ if (findings !== void 0 && !Array.isArray(findings)) return badBody(res, "findings must be an array");
641
+ if (runId !== void 0 && (typeof runId !== "string" || !isValidRunId(runId))) {
642
+ json(res, 400, {
643
+ code: "BAD_ID",
644
+ message: "invalid run id"
645
+ });
646
+ return null;
647
+ }
648
+ return {
649
+ project,
650
+ entry,
651
+ report,
652
+ findings: findings ?? [],
653
+ runId
654
+ };
655
+ }
656
+ function badBody(res, message) {
657
+ json(res, 400, {
658
+ code: "BAD_BODY",
659
+ message
660
+ });
661
+ return null;
662
+ }
663
+ function runRoute(ctx) {
664
+ const runId = match1(ctx.path, /^\/v1\/runs\/([^/]+)$/);
665
+ if (runId === null) return false;
666
+ if (ctx.method === "GET") {
667
+ const run = ctx.store.getRun(runId);
668
+ if (run === void 0) {
669
+ json(ctx.res, 404, {
670
+ code: "NOT_FOUND",
671
+ message: "run not found"
672
+ });
673
+ return true;
674
+ }
675
+ json(ctx.res, 200, {
676
+ ...runSummary(run),
677
+ report: JSON.parse(run.reportJson),
678
+ findings: JSON.parse(run.findingsJson),
679
+ uploadedAt: run.uploadedAt
680
+ });
681
+ return true;
682
+ }
683
+ methodNotAllowed(ctx.res);
684
+ return true;
685
+ }
686
+ function flakyApiRoute(ctx) {
687
+ const project = match1(ctx.path, /^\/v1\/flaky\/([^/]+)$/);
688
+ if (project === null) return false;
689
+ if (ctx.method === "GET") {
690
+ json(ctx.res, 200, { rows: ctx.store.flaky(project, {
691
+ n: numParam(ctx.url, "n"),
692
+ steps: csvParam(ctx.url, "steps")
693
+ }) });
694
+ return true;
695
+ }
696
+ methodNotAllowed(ctx.res);
697
+ return true;
698
+ }
699
+ function indexRoute(ctx) {
700
+ if (ctx.path !== "/") return false;
701
+ if (ctx.method === "GET") {
702
+ const project = ctx.url.searchParams.get("project");
703
+ if (project === null) {
704
+ page(ctx.res, renderHubIndex({ projects: ctx.store.listProjects() }));
705
+ return true;
706
+ }
707
+ const limit = numParam(ctx.url, "limit") ?? 50;
708
+ const runs = ctx.store.listRuns({
709
+ project,
710
+ limit
711
+ }).map(runSummary);
712
+ page(ctx.res, renderHubRunList({
713
+ project,
714
+ runs
715
+ }));
716
+ return true;
717
+ }
718
+ methodNotAllowed(ctx.res);
719
+ return true;
720
+ }
721
+ function findingsPageRoute(ctx) {
722
+ const m = match2(ctx.path, /^\/runs\/([^/]+)\/([^/]+)\/findings$/);
723
+ if (m === null) return false;
724
+ if (ctx.method === "GET") {
725
+ const run = ctx.store.getRun(m[1]);
726
+ if (run === void 0 || run.project !== m[0]) {
727
+ notFoundPage(ctx.res);
728
+ return true;
729
+ }
730
+ const findings = JSON.parse(run.findingsJson);
731
+ ctx.res.writeHead(200, SARIF_PAGE_HEADERS);
732
+ ctx.res.end(generateSarifHtml(findings));
733
+ return true;
734
+ }
735
+ methodNotAllowed(ctx.res);
736
+ return true;
737
+ }
738
+ function runPageRoute(ctx) {
739
+ const m = match2(ctx.path, /^\/runs\/([^/]+)\/([^/]+)$/);
740
+ if (m === null) return false;
741
+ if (ctx.method === "GET") {
742
+ const run = ctx.store.getRun(m[1]);
743
+ if (run === void 0 || run.project !== m[0]) {
744
+ notFoundPage(ctx.res);
745
+ return true;
746
+ }
747
+ page(ctx.res, renderHubRunDetail({ run: {
748
+ ...runSummary(run),
749
+ report: JSON.parse(run.reportJson),
750
+ findings: JSON.parse(run.findingsJson),
751
+ uploadedAt: run.uploadedAt
752
+ } }));
753
+ return true;
754
+ }
755
+ methodNotAllowed(ctx.res);
756
+ return true;
757
+ }
758
+ function flakyPageRoute(ctx) {
759
+ const project = match1(ctx.path, /^\/flaky\/([^/]+)$/);
760
+ if (project === null) return false;
761
+ if (ctx.method === "GET") {
762
+ const window = numParam(ctx.url, "n") ?? 20;
763
+ page(ctx.res, renderHubFlaky({
764
+ project,
765
+ rows: ctx.store.flaky(project, { n: window }),
766
+ window
767
+ }));
768
+ return true;
769
+ }
770
+ methodNotAllowed(ctx.res);
771
+ return true;
772
+ }
773
+ function promoteQueryToken(ctx, auth) {
774
+ if (!auth.viaQuery || ctx.method !== "GET") return;
775
+ const tls = ctx.req.socket.encrypted === true || headerValue(ctx.req.headers["x-forwarded-proto"])?.split(",")[0]?.trim() === "https";
776
+ ctx.res.setHeader("Set-Cookie", `hub_token=${encodeURIComponent(auth.token)}; HttpOnly;${tls ? " Secure;" : ""} SameSite=Strict; Path=/`);
777
+ }
778
+ /** Numeric query param — absent or non-finite becomes undefined. */
779
+ function numParam(url, name) {
780
+ const raw = url.searchParams.get(name);
781
+ if (raw === null) return void 0;
782
+ const n = Number(raw);
783
+ return Number.isFinite(n) ? n : void 0;
784
+ }
785
+ /** Comma-separated query param — absent or empty becomes undefined. */
786
+ function csvParam(url, name) {
787
+ const raw = url.searchParams.get(name);
788
+ if (raw === null || raw === "") return void 0;
789
+ return raw.split(",").filter((s) => s !== "");
790
+ }
791
+ /** URL-decode a single path segment matched by `pattern`'s 1st group. */
792
+ function match1(path, pattern) {
793
+ const seg = pattern.exec(path)?.[1];
794
+ if (seg === void 0) return null;
795
+ try {
796
+ return decodeURIComponent(seg);
797
+ } catch {
798
+ return null;
799
+ }
800
+ }
801
+ /** URL-decode two path segments from `pattern`'s capture groups. */
802
+ function match2(path, pattern) {
803
+ const m = pattern.exec(path);
804
+ const [a, b] = [m?.[1], m?.[2]];
805
+ if (a === void 0 || b === void 0) return null;
806
+ try {
807
+ return [decodeURIComponent(a), decodeURIComponent(b)];
808
+ } catch {
809
+ return null;
810
+ }
811
+ }
812
+ function authenticate(req, query, tokens, res) {
813
+ const { token, viaQuery } = extractToken({ headers: {
814
+ authorization: headerValue(req.headers["authorization"]),
815
+ cookie: headerValue(req.headers["cookie"])
816
+ } }, query);
817
+ if (!token) {
818
+ json(res, 401, {
819
+ code: "UNAUTHORIZED",
820
+ message: "token required"
821
+ });
822
+ return null;
823
+ }
824
+ const found = tokens.find((t) => tokenEquals(t.token, token));
825
+ if (found === void 0) {
826
+ json(res, 401, {
827
+ code: "UNAUTHORIZED",
828
+ message: "invalid token"
829
+ });
830
+ return null;
831
+ }
832
+ return {
833
+ token,
834
+ access: found.access,
835
+ viaQuery
836
+ };
837
+ }
838
+ function headerValue(value) {
839
+ return Array.isArray(value) ? value[0] : value;
840
+ }
841
+ /** Read a request body up to `cap` bytes; 413 + null when exceeded. */
842
+ function readBody(req, cap, res) {
843
+ return new Promise((resolve) => {
844
+ const chunks = [];
845
+ let size = 0;
846
+ let done = false;
847
+ req.on("data", (chunk) => {
848
+ if (done) return;
849
+ size += chunk.length;
850
+ if (size > cap) {
851
+ done = true;
852
+ json(res, 413, {
853
+ code: "TOO_LARGE",
854
+ message: "payload too large"
855
+ });
856
+ req.destroy();
857
+ resolve(null);
858
+ return;
859
+ }
860
+ chunks.push(chunk);
861
+ });
862
+ req.on("end", () => {
863
+ if (!done) resolve(Buffer.concat(chunks));
864
+ });
865
+ req.on("error", () => {
866
+ if (!done) resolve(null);
867
+ });
868
+ });
869
+ }
870
+ function json(res, status, body) {
871
+ res.writeHead(status, API_HEADERS);
872
+ res.end(JSON.stringify(body));
873
+ }
874
+ function methodNotAllowed(res) {
875
+ json(res, 405, {
876
+ code: "METHOD_NOT_ALLOWED",
877
+ message: "method not allowed on this route"
878
+ });
879
+ }
880
+ function page(res, html) {
881
+ res.writeHead(200, PAGE_HEADERS);
882
+ res.end(html);
883
+ }
884
+ function notFoundPage(res) {
885
+ res.writeHead(404, PAGE_HEADERS);
886
+ res.end("<h1>404 — not found</h1>");
887
+ }
888
+ function parseCounts(json) {
889
+ try {
890
+ const parsed = JSON.parse(json);
891
+ return {
892
+ total: typeof parsed.total === "number" ? parsed.total : 0,
893
+ bySeverity: typeof parsed.bySeverity === "object" && parsed.bySeverity !== null ? parsed.bySeverity : {}
894
+ };
895
+ } catch {
896
+ return {
897
+ total: 0,
898
+ bySeverity: {}
899
+ };
900
+ }
901
+ }
902
+ function runSummary(run) {
903
+ return {
904
+ runId: run.runId,
905
+ project: run.project,
906
+ entry: run.entry,
907
+ status: run.status,
908
+ startedAt: run.startedAt,
909
+ durationMs: run.durationMs,
910
+ findingCounts: parseCounts(run.findingCounts),
911
+ policyVerdict: run.policyVerdict
912
+ };
913
+ }
914
+ //#endregion
915
+ export { createHubStore, isValidCacheKey, isValidRunId, parseTokensFile, resolveTokens, startHubServer };
916
+
917
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/auth.ts","../src/store.ts","../src/server.ts"],"sourcesContent":["// Token auth — Bearer tokens from a tokens file and/or the admin env var.\n// Spec 55: \"single read/write token plus optional read-only tokens\n// (tokens file: name:token:ro|rw)\".\n\nimport { readFileSync, appendFileSync, chmodSync, mkdirSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { createHash, randomBytes, timingSafeEqual } from \"node:crypto\";\nimport type { HubToken } from \"./types.js\";\n\nconst TOKENS_FILE = \"tokens\";\n\n/**\n * Parse a tokens file body: one `name:token:ro|rw` per line; `#` comments\n * and blank lines skipped; malformed lines skipped. Token values are\n * `[A-Za-z0-9._~-]` — anything else is ignored (never echo user data).\n */\nexport function parseTokensFile(text: string): HubToken[] {\n const tokens: HubToken[] = [];\n for (const rawLine of text.split(\"\\n\")) {\n const line = rawLine.trim();\n if (line === \"\" || line.startsWith(\"#\")) continue;\n const parts = line.split(\":\");\n if (parts.length !== 3) continue;\n const [name, token, access] = parts;\n if (\n name === undefined ||\n token === undefined ||\n (access !== \"ro\" && access !== \"rw\") ||\n !/^[A-Za-z0-9._~-]{8,}$/.test(token)\n ) {\n continue;\n }\n tokens.push({ name, token, access });\n }\n return tokens;\n}\n\n/**\n * Resolve the effective token set: explicit option > tokens file +\n * SVERKA_HUB_ADMIN_TOKEN. When nothing is configured, a fresh admin token\n * is generated, appended to `<dataDir>/tokens`, and returned so the CLI\n * can print it once — an unauthenticated hub is never silently open.\n */\nexport function resolveTokens(\n dataDir: string,\n explicit?: readonly HubToken[],\n): { tokens: readonly HubToken[]; generated: HubToken | null } {\n if (explicit !== undefined && explicit.length > 0) {\n return { tokens: explicit, generated: null };\n }\n\n const tokens: HubToken[] = [];\n const file = join(dataDir, TOKENS_FILE);\n try {\n // Read directly — an existsSync check would leave a TOCTOU window.\n tokens.push(...parseTokensFile(readFileSync(file, \"utf8\")));\n } catch {\n // Missing or unreadable tokens file — fall through to env/generated.\n }\n\n const admin = process.env[\"SVERKA_HUB_ADMIN_TOKEN\"];\n if (typeof admin === \"string\" && admin.length >= 8) {\n tokens.push({ name: \"admin-env\", token: admin, access: \"rw\" });\n }\n\n if (tokens.length === 0) {\n const generated: HubToken = {\n name: \"admin\",\n token: `svk_${randomBytes(24).toString(\"base64url\")}`,\n access: \"rw\",\n };\n try {\n mkdirSync(dataDir, { recursive: true, mode: 0o700 });\n appendFileSync(\n file,\n `${generated.name}:${generated.token}:${generated.access}\\n`,\n { mode: 0o600 },\n );\n // `mode` applies only when the file is created — a pre-existing\n // permissive file would keep the generated admin token readable by\n // other users. Force 0600 either way.\n chmodSync(file, 0o600);\n } catch {\n // If the file can't be written, still serve with the in-memory token —\n // the operator saw it printed on the console.\n }\n tokens.push(generated);\n return { tokens, generated };\n }\n\n return { tokens, generated: null };\n}\n\n/**\n * Constant-time token comparison. Both sides are SHA-256'd first so the\n * compare time leaks neither the token's length nor a matching prefix.\n */\nexport function tokenEquals(a: string, b: string): boolean {\n const ha = createHash(\"sha256\").update(a).digest();\n const hb = createHash(\"sha256\").update(b).digest();\n return timingSafeEqual(ha, hb);\n}\n\n/** Extract the bearer token from Authorization, `?token=`, or the\n * `hub_token` cookie (the dashboard sets it from `?token=`). */\nexport function extractToken(\n req: {\n headers: {\n authorization?: string | undefined;\n cookie?: string | undefined;\n };\n },\n query: URLSearchParams,\n): { token: string | null; viaQuery: boolean } {\n const auth = req.headers.authorization;\n if (typeof auth === \"string\" && auth.startsWith(\"Bearer \")) {\n return { token: auth.slice(\"Bearer \".length), viaQuery: false };\n }\n const queryToken = query.get(\"token\");\n if (queryToken !== null && queryToken !== \"\") {\n return { token: queryToken, viaQuery: true };\n }\n const cookie = req.headers.cookie;\n if (typeof cookie === \"string\") {\n for (const part of cookie.split(\";\")) {\n const [k, ...rest] = part.trim().split(\"=\");\n if (k === \"hub_token\") {\n return { token: rest.join(\"=\"), viaQuery: false };\n }\n }\n }\n return { token: null, viaQuery: false };\n}\n","// Hub persistence — cache blobs + snapshots on the filesystem, runs in a\n// SQLite index (node:sqlite, no native dep). Spec 55.\n\nimport { createHash, randomUUID } from \"node:crypto\";\nimport {\n mkdirSync,\n readdirSync,\n readFileSync,\n writeFileSync,\n unlinkSync,\n} from \"node:fs\";\nimport { join } from \"node:path\";\nimport { DatabaseSync } from \"node:sqlite\";\nimport type { HubStoredRun } from \"./types.js\";\n\n/** Cache keys are lowercase url-safe slugs — anything else is a 400. */\nconst KEY_PATTERN = /^[a-z0-9-]{1,128}$/;\n/** Run ids are generated or client-supplied; keep them filename-safe. */\nconst RUN_ID_PATTERN = /^[A-Za-z0-9._~-]{1,128}$/;\n\nexport function isValidCacheKey(key: string): boolean {\n return KEY_PATTERN.test(key);\n}\n\nexport function isValidRunId(runId: string): boolean {\n return RUN_ID_PATTERN.test(runId);\n}\n\nfunction sha256(value: string): string {\n return createHash(\"sha256\").update(value).digest(\"hex\");\n}\n\n/** Projects arrive URL-encoded — decoded strings may contain '/'. The\n * storage dir encodes again to stay flat and traversal-proof. */\nfunction projectDir(root: string, project: string): string {\n return join(root, encodeURIComponent(project));\n}\n\nconst CREATE_RUNS_TABLE = `\n CREATE TABLE IF NOT EXISTS runs (\n run_id TEXT PRIMARY KEY,\n project TEXT NOT NULL,\n entry TEXT NOT NULL,\n status TEXT NOT NULL,\n started_at INTEGER,\n duration_ms INTEGER,\n finding_counts TEXT NOT NULL,\n verdict TEXT,\n uploaded_at INTEGER NOT NULL,\n report_json TEXT NOT NULL,\n findings_json TEXT NOT NULL\n )\n`;\n\nexport interface RunInsert {\n readonly project: string;\n readonly entry: string;\n readonly report: Record<string, unknown>;\n readonly findings: readonly unknown[];\n readonly runId?: string | undefined;\n}\n\nexport interface RunListQuery {\n readonly project?: string | undefined;\n readonly limit?: number | undefined;\n /** uploadedAt exclusive cursor — rows uploaded before this instant. */\n readonly before?: number | undefined;\n}\n\nexport interface FlakyRow {\n readonly stepId: string;\n readonly successRate: number;\n readonly runs: number;\n}\n\n/**\n * The hub store: blobs + snapshots under `dataDir`, run index in\n * `dataDir/hub.db`.\n */\nexport function createHubStore(dataDir: string) {\n mkdirSync(dataDir, { recursive: true, mode: 0o700 });\n const db = new DatabaseSync(join(dataDir, \"hub.db\"));\n db.exec(CREATE_RUNS_TABLE);\n\n // --- cache blobs ---\n\n const cacheDir = (project: string) =>\n projectDir(join(dataDir, \"cache\"), project);\n\n const putBlob = (project: string, key: string, blob: Buffer): void => {\n const dir = cacheDir(project);\n mkdirSync(dir, { recursive: true });\n const base = join(dir, sha256(key));\n writeFileSync(`${base}.blob`, blob);\n writeFileSync(\n `${base}.json`,\n JSON.stringify({ key, size: blob.length, createdAt: Date.now() }),\n );\n };\n\n const readBlobDir = (\n project: string,\n ): readonly { file: string; key: string; createdAt: number }[] => {\n const dir = cacheDir(project);\n let names: string[];\n try {\n names = readdirSync(dir);\n } catch {\n return [];\n }\n const entries: { file: string; key: string; createdAt: number }[] = [];\n for (const file of names) {\n if (!file.endsWith(\".json\")) continue;\n try {\n const meta = JSON.parse(readFileSync(join(dir, file), \"utf8\")) as {\n key?: unknown;\n createdAt?: unknown;\n };\n if (typeof meta.key === \"string\") {\n entries.push({\n file: file.slice(0, -\".json\".length),\n key: meta.key,\n createdAt: typeof meta.createdAt === \"number\" ? meta.createdAt : 0,\n });\n }\n } catch {\n // Skip corrupt meta files.\n }\n }\n return entries;\n };\n\n const getBlob = (\n project: string,\n key: string,\n opts?: { prefix?: boolean | undefined },\n ): { blob: Buffer; key: string } | undefined => {\n // Exact match — O(1) and always on.\n const exact = join(cacheDir(project), `${sha256(key)}.blob`);\n try {\n return { blob: readFileSync(exact), key };\n } catch {\n // No exact hit.\n }\n // Prefix fallback is opt-in: it implements the client's restoreKeys\n // semantics. Without it, a GET for key \"b\" could return the newest\n // \"b*\" blob — wrong-key restores — and every plain miss paid an O(n)\n // synchronous scan of the whole blob dir on the request thread.\n if (opts?.prefix !== true) return undefined;\n let best: { file: string; key: string; createdAt: number } | undefined;\n for (const entry of readBlobDir(project)) {\n if (!entry.key.startsWith(key)) continue;\n if (best === undefined || entry.createdAt > best.createdAt) {\n best = entry;\n }\n }\n if (best === undefined) return undefined;\n const blobPath = join(cacheDir(project), `${best.file}.blob`);\n try {\n return { blob: readFileSync(blobPath), key: best.key };\n } catch {\n return undefined;\n }\n };\n\n // --- snapshots ---\n\n const snapshotPath = (project: string, runId: string): string =>\n join(\n projectDir(join(dataDir, \"snapshots\"), project),\n `${encodeURIComponent(runId)}.json`,\n );\n\n const putSnapshot = (project: string, runId: string, body: string): void => {\n const dir = projectDir(join(dataDir, \"snapshots\"), project);\n mkdirSync(dir, { recursive: true });\n writeFileSync(snapshotPath(project, runId), body, { mode: 0o600 });\n };\n\n const getSnapshot = (project: string, runId: string): string | undefined => {\n try {\n return readFileSync(snapshotPath(project, runId), \"utf8\");\n } catch {\n return undefined;\n }\n };\n\n const deleteSnapshot = (project: string, runId: string): boolean => {\n try {\n unlinkSync(snapshotPath(project, runId));\n return true;\n } catch {\n return false;\n }\n };\n\n // --- runs (sqlite index) ---\n\n const insertStmt = db.prepare(\n `INSERT OR REPLACE INTO runs\n (run_id, project, entry, status, started_at, duration_ms,\n finding_counts, verdict, uploaded_at, report_json, findings_json)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n );\n\n const insertRun = (input: RunInsert): HubStoredRun => {\n const runId =\n input.runId !== undefined && isValidRunId(input.runId)\n ? input.runId\n : randomUUID();\n const report = input.report;\n const data =\n typeof report[\"data\"] === \"object\" && report[\"data\"] !== null\n ? (report[\"data\"] as Record<string, unknown>)\n : {};\n const status =\n typeof data[\"status\"] === \"string\" ? data[\"status\"] : \"unknown\";\n const durationMs =\n numOrNull(report[\"durationMs\"]) ?? numOrNull(data[\"durationMs\"]);\n const uploadedAt = Date.now();\n // The report payload has no startedAt — approximate it from upload\n // time minus duration when a duration is known.\n const startedAt = durationMs === null ? null : uploadedAt - durationMs;\n const findingCounts = JSON.stringify({\n total: input.findings.length,\n bySeverity: severityCounts(input.findings),\n });\n const verdict =\n typeof data[\"verdict\"] === \"string\" ? data[\"verdict\"] : null;\n const reportJson = JSON.stringify(report);\n const findingsJson = JSON.stringify(input.findings);\n insertStmt.run(\n runId,\n input.project,\n input.entry,\n status,\n startedAt,\n durationMs,\n findingCounts,\n verdict,\n uploadedAt,\n reportJson,\n findingsJson,\n );\n return {\n runId,\n project: input.project,\n entry: input.entry,\n status,\n startedAt,\n durationMs,\n findingCounts,\n policyVerdict: verdict,\n uploadedAt,\n reportJson,\n findingsJson,\n };\n };\n\n const listRuns = (query: RunListQuery): HubStoredRun[] => {\n const clauses: string[] = [];\n const params: (string | number | null)[] = [];\n if (query.project !== undefined) {\n clauses.push(\"project = ?\");\n params.push(query.project);\n }\n if (query.before !== undefined) {\n clauses.push(\"uploaded_at < ?\");\n params.push(query.before);\n }\n const limit = Math.min(Math.max(query.limit ?? 50, 1), 200);\n const where = clauses.length === 0 ? \"\" : `WHERE ${clauses.join(\" AND \")}`;\n const rows = db\n .prepare(\n `SELECT run_id, project, entry, status, started_at, duration_ms,\n finding_counts, verdict, uploaded_at\n FROM runs ${where} ORDER BY uploaded_at DESC LIMIT ?`,\n )\n .all(...params, limit) as unknown as Record<string, unknown>[];\n return rows.map(rowToSummary);\n };\n\n const getRun = (runId: string): HubStoredRun | undefined => {\n const row = db.prepare(\"SELECT * FROM runs WHERE run_id = ?\").get(runId) as\n Record<string, unknown> | undefined;\n if (row === undefined) return undefined;\n return {\n runId: row[\"run_id\"] as string,\n project: row[\"project\"] as string,\n entry: row[\"entry\"] as string,\n status: row[\"status\"] as string,\n startedAt: (row[\"started_at\"] as number | null) ?? null,\n durationMs: (row[\"duration_ms\"] as number | null) ?? null,\n findingCounts: row[\"finding_counts\"] as string,\n policyVerdict: (row[\"verdict\"] as string | null) ?? null,\n uploadedAt: row[\"uploaded_at\"] as number,\n reportJson: row[\"report_json\"] as string,\n findingsJson: row[\"findings_json\"] as string,\n };\n };\n\n const listProjects = (): string[] => {\n const rows = db\n .prepare(\"SELECT DISTINCT project FROM runs ORDER BY project\")\n .all() as { project: string }[];\n return rows.map((r) => r.project);\n };\n\n /**\n * Flaky-step aggregation: per-step success rate across the last N runs\n * of a project. Success = \"succeeded\" | \"cache-hit\"; the denominator\n * counts only runs where the step actually produced a verdict (skipped\n * and cancelled steps are excluded).\n */\n const flaky = (\n project: string,\n opts?: {\n n?: number | undefined;\n steps?: readonly string[] | undefined;\n },\n ): FlakyRow[] => {\n const n = Math.min(Math.max(opts?.n ?? 20, 1), 100);\n const rows = db\n .prepare(\n `SELECT report_json FROM runs WHERE project = ?\n ORDER BY uploaded_at DESC LIMIT ?`,\n )\n .all(project, n) as { report_json: string }[];\n const wanted = opts?.steps !== undefined ? new Set(opts.steps) : null;\n const stats = new Map<string, { ok: number; total: number }>();\n for (const row of rows) {\n for (const step of reportSteps(row.report_json)) {\n accumulateFlakyStep(stats, wanted, step);\n }\n }\n return [...stats.entries()]\n .map(([stepId, s]) => ({\n stepId,\n successRate: s.total === 0 ? 0 : s.ok / s.total,\n runs: s.total,\n }))\n .sort((a, b) => a.successRate - b.successRate);\n };\n\n return {\n dataDir,\n putBlob,\n getBlob,\n putSnapshot,\n getSnapshot,\n deleteSnapshot,\n insertRun,\n listRuns,\n getRun,\n listProjects,\n flaky,\n close: () => {\n try {\n db.close();\n } catch {\n // already closed\n }\n },\n };\n}\n\nexport type HubStore = ReturnType<typeof createHubStore>;\n\nfunction numOrNull(value: unknown): number | null {\n return typeof value === \"number\" ? value : null;\n}\n\n/** Per-severity tally over the uploaded findings array. */\nfunction severityCounts(findings: readonly unknown[]): Record<string, number> {\n const counts: Record<string, number> = {};\n for (const f of findings) {\n const sev =\n typeof f === \"object\" && f !== null\n ? (f as { severity?: unknown }).severity\n : undefined;\n if (typeof sev === \"string\") {\n counts[sev] = (counts[sev] ?? 0) + 1;\n }\n }\n return counts;\n}\n\n/** Steps array inside a stored report JSON — [] on any shape failure. */\nfunction reportSteps(reportJson: string): readonly unknown[] {\n let report: Record<string, unknown>;\n try {\n report = JSON.parse(reportJson) as Record<string, unknown>;\n } catch {\n return [];\n }\n const data = report[\"data\"];\n const steps =\n typeof data === \"object\" && data !== null\n ? (data as { steps?: readonly unknown[] }).steps\n : undefined;\n return Array.isArray(steps) ? steps : [];\n}\n\n/** Statuses that produce a verdict — skipped/cancelled steps don't\n * count toward a step's success rate. */\nconst FLAKY_STATUSES: ReadonlySet<string> = new Set([\n \"succeeded\",\n \"failed\",\n \"cache-hit\",\n]);\n\nfunction accumulateFlakyStep(\n stats: Map<string, { ok: number; total: number }>,\n wanted: ReadonlySet<string> | null,\n step: unknown,\n): void {\n if (typeof step !== \"object\" || step === null) return;\n const { stepId, status } = step as {\n stepId?: unknown;\n status?: unknown;\n };\n if (typeof stepId !== \"string\" || typeof status !== \"string\") return;\n if (wanted !== null && !wanted.has(stepId)) return;\n if (!FLAKY_STATUSES.has(status)) return;\n const s = stats.get(stepId) ?? { ok: 0, total: 0 };\n s.total += 1;\n if (status !== \"failed\") s.ok += 1;\n stats.set(stepId, s);\n}\n\nfunction rowToSummary(row: Record<string, unknown>): HubStoredRun {\n return {\n runId: row[\"run_id\"] as string,\n project: row[\"project\"] as string,\n entry: row[\"entry\"] as string,\n status: row[\"status\"] as string,\n startedAt: (row[\"started_at\"] as number | null) ?? null,\n durationMs: (row[\"duration_ms\"] as number | null) ?? null,\n findingCounts: row[\"finding_counts\"] as string,\n policyVerdict: (row[\"verdict\"] as string | null) ?? null,\n uploadedAt: row[\"uploaded_at\"] as number,\n reportJson: \"\",\n findingsJson: \"[]\",\n };\n}\n","// Self-hosted hub server — node:http, JSON API under /v1/ plus a static\n// dashboard from @sverka/ui. Spec 55.\n\nimport {\n createServer,\n type IncomingMessage,\n type Server,\n type ServerResponse,\n} from \"node:http\";\nimport type { AddressInfo } from \"node:net\";\nimport {\n renderHubIndex,\n renderHubRunList,\n renderHubRunDetail,\n renderHubFlaky,\n} from \"@sverka/ui\";\nimport { generateSarifHtml } from \"@sverka/sarif-viewer-web\";\nimport type { Finding } from \"@sverka/verification\";\nimport type { HubServer, HubServerOptions, HubToken } from \"./types.js\";\nimport { resolveTokens, extractToken, tokenEquals } from \"./auth.js\";\nimport {\n createHubStore,\n isValidCacheKey,\n isValidRunId,\n type HubStore,\n type RunInsert,\n} from \"./store.js\";\n\nconst MAX_BLOB_DEFAULT = 512 * 1024 * 1024;\nconst MAX_JSON_DEFAULT = 32 * 1024 * 1024;\n\nconst API_HEADERS: Record<string, string> = {\n \"Content-Type\": \"application/json; charset=utf-8\",\n \"X-Content-Type-Options\": \"nosniff\",\n};\n\nconst PAGE_HEADERS: Record<string, string> = {\n \"Content-Type\": \"text/html; charset=utf-8\",\n // frame-src 'self' permits the run page's same-origin findings iframe;\n // the SARIF sub-document still gets its own pinned script hashes.\n \"Content-Security-Policy\":\n \"default-src 'none'; style-src 'unsafe-inline'; img-src 'self'; frame-src 'self'\",\n \"X-Frame-Options\": \"DENY\",\n \"X-Content-Type-Options\": \"nosniff\",\n};\n\nconst SARIF_PAGE_HEADERS: Record<string, string> = {\n \"Content-Type\": \"text/html; charset=utf-8\",\n \"Content-Security-Policy\":\n \"default-src 'self'; script-src 'self' \" +\n \"'sha256-x9+d/4JxomKClc3VqIk1V6NKTggeahAWBzMe4jvu7Ec=' \" +\n \"'sha256-SKcdA9aMlus8zeJkaXi1rBBOaGcYgbLddLTB7hCBKm0=' \" +\n \"'sha256-RHNT17OCv3OqIHedvPQaLavcOKHQd1OiBVo17IqkhXQ='; \" +\n \"style-src 'unsafe-inline'; img-src 'self' data:\",\n \"X-Frame-Options\": \"SAMEORIGIN\",\n \"X-Content-Type-Options\": \"nosniff\",\n};\n\nconst ENC = (s: string) => encodeURIComponent(s);\n\n/** Start the hub server. Resolves once listening. */\nexport function startHubServer(opts: HubServerOptions): Promise<HubServer> {\n const store = createHubStore(opts.dataDir);\n const { tokens, generated } = resolveTokens(opts.dataDir, opts.tokens);\n if (generated !== null) {\n console.log(`sverka hub: generated admin token ${generated.token}`);\n console.log(` saved to ${opts.dataDir}/tokens`);\n }\n\n const maxBlob = opts.maxBlobBytes ?? MAX_BLOB_DEFAULT;\n const maxJson = opts.maxJsonBytes ?? MAX_JSON_DEFAULT;\n\n const server = createServer((req, res) => {\n void handle(req, res, store, tokens, { maxBlob, maxJson }).catch(\n (err: unknown) => {\n if (!res.headersSent) {\n json(res, 500, { code: \"INTERNAL\", message: \"internal error\" });\n }\n if (err instanceof Error)\n console.error(\"hub request failed:\", err.message);\n },\n );\n });\n\n return new Promise((resolve, reject) => {\n server.on(\"error\", reject);\n server.listen(opts.port ?? 7357, opts.host ?? \"0.0.0.0\", () => {\n const addr = server.address() as AddressInfo;\n const url =\n addr.family === \"IPv6\"\n ? `http://[${addr.address}]:${addr.port}`\n : `http://${addr.address}:${addr.port}`;\n resolve({\n port: addr.port,\n url,\n dataDir: opts.dataDir,\n close: () => closeServer(server, store),\n });\n });\n });\n}\n\nfunction closeServer(server: Server, store: HubStore): Promise<void> {\n return new Promise<void>((done) => {\n // Drain in-flight requests before closing the SQLite index — closing\n // the store first would fail requests still reading their bodies.\n server.close(() => {\n store.close();\n done();\n });\n });\n}\n\ninterface Limits {\n maxBlob: number;\n maxJson: number;\n}\n\ntype Access = \"ro\" | \"rw\";\n\ninterface ReqCtx {\n readonly req: IncomingMessage;\n readonly res: ServerResponse;\n readonly store: HubStore;\n readonly url: URL;\n readonly method: string;\n readonly path: string;\n readonly limits: Limits;\n}\n\n/** Route handler: returns true when it answered the request. */\ntype Route = (ctx: ReqCtx) => boolean | Promise<boolean>;\n\nconst ROUTES: readonly Route[] = [\n cacheRoute,\n snapshotRoute,\n runsRoute,\n runRoute,\n flakyApiRoute,\n indexRoute,\n findingsPageRoute,\n runPageRoute,\n flakyPageRoute,\n];\n\nasync function handle(\n req: IncomingMessage,\n res: ServerResponse,\n store: HubStore,\n tokens: readonly HubToken[],\n limits: Limits,\n): Promise<void> {\n // The base is a parse placeholder only — never used on the wire.\n const url = new URL(req.url ?? \"/\", \"https://hub.invalid\");\n const ctx: ReqCtx = {\n req,\n res,\n store,\n url,\n method: req.method ?? \"GET\",\n path: url.pathname,\n limits,\n };\n\n // Health check is unauthenticated.\n if (ctx.path === \"/v1/ping\") {\n if (ctx.method === \"GET\") {\n json(res, 200, { status: \"ok\" });\n } else {\n methodNotAllowed(res);\n }\n return;\n }\n\n // Everything else needs a token.\n const auth = authenticate(req, url.searchParams, tokens, res);\n if (auth === null) return;\n const requireWrite = ctx.method !== \"GET\" && ctx.method !== \"HEAD\";\n if (requireWrite && auth.access !== \"rw\") {\n json(res, 403, { code: \"READ_ONLY\", message: \"token is read-only\" });\n return;\n }\n promoteQueryToken(ctx, auth);\n\n for (const route of ROUTES) {\n if (await route(ctx)) return;\n }\n json(res, 404, { code: \"NOT_FOUND\", message: \"unknown route\" });\n}\n\n// --- /v1 routes ---\n\nasync function cacheRoute(ctx: ReqCtx): Promise<boolean> {\n const m = match2(ctx.path, /^\\/v1\\/cache\\/([^/]+)\\/([^/]+)$/);\n if (m === null) return false;\n const [project, key] = m;\n if (!isValidCacheKey(key)) {\n json(ctx.res, 400, { code: \"BAD_KEY\", message: \"invalid cache key\" });\n return true;\n }\n if (ctx.method === \"PUT\") {\n const body = await readBody(ctx.req, ctx.limits.maxBlob, ctx.res);\n if (body === null) return true;\n ctx.store.putBlob(project, key, body);\n ctx.res.writeHead(201, API_HEADERS);\n ctx.res.end();\n return true;\n }\n if (ctx.method === \"GET\") {\n // Prefix matching is opt-in (?prefix=1) — it's the restoreKeys\n // semantic. An exact-key GET must never return a different key's\n // blob, and misses stay O(1) instead of scanning the blob dir.\n const hit = ctx.store.getBlob(project, key, {\n prefix: ctx.url.searchParams.get(\"prefix\") === \"1\",\n });\n if (hit === undefined) {\n json(ctx.res, 404, {\n code: \"NOT_FOUND\",\n message: \"cache entry not found\",\n });\n return true;\n }\n ctx.res.writeHead(200, {\n \"Content-Type\": \"application/octet-stream\",\n \"x-sverka-cache-key\": hit.key,\n \"X-Content-Type-Options\": \"nosniff\",\n });\n ctx.res.end(hit.blob);\n return true;\n }\n methodNotAllowed(ctx.res);\n return true;\n}\n\nasync function snapshotRoute(ctx: ReqCtx): Promise<boolean> {\n const m = match2(ctx.path, /^\\/v1\\/snapshots\\/([^/]+)\\/([^/]+)$/);\n if (m === null) return false;\n const [project, runId] = m;\n if (!isValidRunId(runId)) {\n json(ctx.res, 400, { code: \"BAD_ID\", message: \"invalid run id\" });\n return true;\n }\n if (ctx.method === \"PUT\") {\n const body = await readBody(ctx.req, ctx.limits.maxJson, ctx.res);\n if (body === null) return true;\n ctx.store.putSnapshot(project, runId, body.toString(\"utf8\"));\n ctx.res.writeHead(201, API_HEADERS);\n ctx.res.end();\n return true;\n }\n if (ctx.method === \"GET\") {\n const snap = ctx.store.getSnapshot(project, runId);\n if (snap === undefined) {\n json(ctx.res, 404, { code: \"NOT_FOUND\", message: \"snapshot not found\" });\n return true;\n }\n ctx.res.writeHead(200, API_HEADERS);\n ctx.res.end(snap);\n return true;\n }\n if (ctx.method === \"DELETE\") {\n const existed = ctx.store.deleteSnapshot(project, runId);\n ctx.res.writeHead(existed ? 204 : 404, API_HEADERS);\n ctx.res.end();\n return true;\n }\n methodNotAllowed(ctx.res);\n return true;\n}\n\nasync function runsRoute(ctx: ReqCtx): Promise<boolean> {\n if (ctx.path !== \"/v1/runs\") return false;\n if (ctx.method === \"POST\") {\n const body = await readBody(ctx.req, ctx.limits.maxJson, ctx.res);\n if (body === null) return true;\n const insert = parseRunInsert(body, ctx.res);\n if (insert === null) return true;\n const run = ctx.store.insertRun(insert);\n json(ctx.res, 201, {\n runId: run.runId,\n url: `/v1/runs/${ENC(run.runId)}`,\n });\n return true;\n }\n if (ctx.method === \"GET\") {\n const runs = ctx.store.listRuns({\n project: ctx.url.searchParams.get(\"project\") ?? undefined,\n limit: numParam(ctx.url, \"limit\"),\n before: numParam(ctx.url, \"before\"),\n });\n json(ctx.res, 200, runs.map(runSummary));\n return true;\n }\n methodNotAllowed(ctx.res);\n return true;\n}\n\n/** Parse + validate the POST /v1/runs body; writes the 400 itself and\n * returns null when the payload is unusable. */\nfunction parseRunInsert(body: Buffer, res: ServerResponse): RunInsert | null {\n let payload: Record<string, unknown>;\n try {\n payload = JSON.parse(body.toString(\"utf8\")) as Record<string, unknown>;\n } catch {\n json(res, 400, { code: \"BAD_JSON\", message: \"invalid JSON body\" });\n return null;\n }\n const { project, entry, report, findings, runId } = payload;\n if (typeof project !== \"string\" || project === \"\") {\n return badBody(res, \"project required\");\n }\n if (typeof entry !== \"string\" || entry === \"\") {\n return badBody(res, \"entry required\");\n }\n if (typeof report !== \"object\" || report === null) {\n return badBody(res, \"report required\");\n }\n if (findings !== undefined && !Array.isArray(findings)) {\n return badBody(res, \"findings must be an array\");\n }\n if (\n runId !== undefined &&\n (typeof runId !== \"string\" || !isValidRunId(runId))\n ) {\n json(res, 400, { code: \"BAD_ID\", message: \"invalid run id\" });\n return null;\n }\n return {\n project,\n entry,\n report: report as Record<string, unknown>,\n findings: (findings as readonly unknown[] | undefined) ?? [],\n runId: runId as string | undefined,\n };\n}\n\nfunction badBody(res: ServerResponse, message: string): null {\n json(res, 400, { code: \"BAD_BODY\", message });\n return null;\n}\n\nfunction runRoute(ctx: ReqCtx): boolean {\n const runId = match1(ctx.path, /^\\/v1\\/runs\\/([^/]+)$/);\n if (runId === null) return false;\n if (ctx.method === \"GET\") {\n const run = ctx.store.getRun(runId);\n if (run === undefined) {\n json(ctx.res, 404, { code: \"NOT_FOUND\", message: \"run not found\" });\n return true;\n }\n json(ctx.res, 200, {\n ...runSummary(run),\n report: JSON.parse(run.reportJson) as unknown,\n findings: JSON.parse(run.findingsJson) as unknown,\n uploadedAt: run.uploadedAt,\n });\n return true;\n }\n methodNotAllowed(ctx.res);\n return true;\n}\n\n// Flaky-step aggregation.\nfunction flakyApiRoute(ctx: ReqCtx): boolean {\n const project = match1(ctx.path, /^\\/v1\\/flaky\\/([^/]+)$/);\n if (project === null) return false;\n if (ctx.method === \"GET\") {\n json(ctx.res, 200, {\n rows: ctx.store.flaky(project, {\n n: numParam(ctx.url, \"n\"),\n steps: csvParam(ctx.url, \"steps\"),\n }),\n });\n return true;\n }\n methodNotAllowed(ctx.res);\n return true;\n}\n\n// --- dashboard (HTML) ---\n\nfunction indexRoute(ctx: ReqCtx): boolean {\n if (ctx.path !== \"/\") return false;\n if (ctx.method === \"GET\") {\n const project = ctx.url.searchParams.get(\"project\");\n if (project === null) {\n page(ctx.res, renderHubIndex({ projects: ctx.store.listProjects() }));\n return true;\n }\n const limit = numParam(ctx.url, \"limit\") ?? 50;\n const runs = ctx.store.listRuns({ project, limit }).map(runSummary);\n page(ctx.res, renderHubRunList({ project, runs }));\n return true;\n }\n methodNotAllowed(ctx.res);\n return true;\n}\n\nfunction findingsPageRoute(ctx: ReqCtx): boolean {\n const m = match2(ctx.path, /^\\/runs\\/([^/]+)\\/([^/]+)\\/findings$/);\n if (m === null) return false;\n if (ctx.method === \"GET\") {\n const run = ctx.store.getRun(m[1]);\n if (run === undefined || run.project !== m[0]) {\n notFoundPage(ctx.res);\n return true;\n }\n const findings = JSON.parse(run.findingsJson) as Finding[];\n ctx.res.writeHead(200, SARIF_PAGE_HEADERS);\n ctx.res.end(generateSarifHtml(findings));\n return true;\n }\n methodNotAllowed(ctx.res);\n return true;\n}\n\nfunction runPageRoute(ctx: ReqCtx): boolean {\n const m = match2(ctx.path, /^\\/runs\\/([^/]+)\\/([^/]+)$/);\n if (m === null) return false;\n if (ctx.method === \"GET\") {\n const run = ctx.store.getRun(m[1]);\n if (run === undefined || run.project !== m[0]) {\n notFoundPage(ctx.res);\n return true;\n }\n page(\n ctx.res,\n renderHubRunDetail({\n run: {\n ...runSummary(run),\n report: JSON.parse(run.reportJson) as Record<string, unknown>,\n findings: JSON.parse(run.findingsJson) as readonly unknown[],\n uploadedAt: run.uploadedAt,\n },\n }),\n );\n return true;\n }\n methodNotAllowed(ctx.res);\n return true;\n}\n\nfunction flakyPageRoute(ctx: ReqCtx): boolean {\n const project = match1(ctx.path, /^\\/flaky\\/([^/]+)$/);\n if (project === null) return false;\n if (ctx.method === \"GET\") {\n const window = numParam(ctx.url, \"n\") ?? 20;\n page(\n ctx.res,\n renderHubFlaky({\n project,\n rows: ctx.store.flaky(project, { n: window }),\n window,\n }),\n );\n return true;\n }\n methodNotAllowed(ctx.res);\n return true;\n}\n\n// --- shared helpers ---\n\n// A valid ?token= promotes to a cookie so dashboard links work.\n// Secure only when the request actually arrived over TLS — the hub\n// serves plain HTTP by default (a Secure cookie would never stick);\n// behind a TLS-terminating proxy x-forwarded-proto applies.\nfunction promoteQueryToken(\n ctx: ReqCtx,\n auth: { token: string; viaQuery: boolean },\n): void {\n if (!auth.viaQuery || ctx.method !== \"GET\") return;\n const tls =\n (ctx.req.socket as { encrypted?: boolean }).encrypted === true ||\n headerValue(ctx.req.headers[\"x-forwarded-proto\"])?.split(\",\")[0]?.trim() ===\n \"https\";\n ctx.res.setHeader(\n \"Set-Cookie\",\n `hub_token=${encodeURIComponent(auth.token)}; HttpOnly;${tls ? \" Secure;\" : \"\"} SameSite=Strict; Path=/`,\n );\n}\n\n/** Numeric query param — absent or non-finite becomes undefined. */\nfunction numParam(url: URL, name: string): number | undefined {\n const raw = url.searchParams.get(name);\n if (raw === null) return undefined;\n const n = Number(raw);\n return Number.isFinite(n) ? n : undefined;\n}\n\n/** Comma-separated query param — absent or empty becomes undefined. */\nfunction csvParam(url: URL, name: string): string[] | undefined {\n const raw = url.searchParams.get(name);\n if (raw === null || raw === \"\") return undefined;\n return raw.split(\",\").filter((s) => s !== \"\");\n}\n\n/** URL-decode a single path segment matched by `pattern`'s 1st group. */\nfunction match1(path: string, pattern: RegExp): string | null {\n const m = pattern.exec(path);\n const seg = m?.[1];\n if (seg === undefined) return null;\n try {\n return decodeURIComponent(seg);\n } catch {\n return null;\n }\n}\n\n/** URL-decode two path segments from `pattern`'s capture groups. */\nfunction match2(path: string, pattern: RegExp): [string, string] | null {\n const m = pattern.exec(path);\n const [a, b] = [m?.[1], m?.[2]];\n if (a === undefined || b === undefined) return null;\n try {\n return [decodeURIComponent(a), decodeURIComponent(b)];\n } catch {\n return null;\n }\n}\n\nfunction authenticate(\n req: IncomingMessage,\n query: URLSearchParams,\n tokens: readonly HubToken[],\n res: ServerResponse,\n): { token: string; access: Access; viaQuery: boolean } | null {\n const { token, viaQuery } = extractToken(\n {\n headers: {\n authorization: headerValue(req.headers[\"authorization\"]),\n cookie: headerValue(req.headers[\"cookie\"]),\n },\n },\n query,\n );\n if (!token) {\n json(res, 401, { code: \"UNAUTHORIZED\", message: \"token required\" });\n return null;\n }\n const found = tokens.find((t) => tokenEquals(t.token, token));\n if (found === undefined) {\n json(res, 401, { code: \"UNAUTHORIZED\", message: \"invalid token\" });\n return null;\n }\n return { token, access: found.access, viaQuery };\n}\n\nfunction headerValue(value: string | string[] | undefined): string | undefined {\n return Array.isArray(value) ? value[0] : value;\n}\n\n/** Read a request body up to `cap` bytes; 413 + null when exceeded. */\nfunction readBody(\n req: IncomingMessage,\n cap: number,\n res: ServerResponse,\n): Promise<Buffer | null> {\n return new Promise((resolve) => {\n const chunks: Buffer[] = [];\n let size = 0;\n let done = false;\n req.on(\"data\", (chunk: Buffer) => {\n // After the 413 the request is destroyed, but already-buffered\n // chunks still reach this listener — bail before they can trigger\n // a second response (ERR_HTTP_HEADERS_SENT) or grow `chunks`.\n if (done) return;\n size += chunk.length;\n if (size > cap) {\n done = true;\n json(res, 413, { code: \"TOO_LARGE\", message: \"payload too large\" });\n req.destroy();\n resolve(null);\n return;\n }\n chunks.push(chunk);\n });\n req.on(\"end\", () => {\n if (!done) resolve(Buffer.concat(chunks));\n });\n req.on(\"error\", () => {\n if (!done) resolve(null);\n });\n });\n}\n\nfunction json(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, API_HEADERS);\n res.end(JSON.stringify(body));\n}\n\nfunction methodNotAllowed(res: ServerResponse): void {\n json(res, 405, {\n code: \"METHOD_NOT_ALLOWED\",\n message: \"method not allowed on this route\",\n });\n}\n\nfunction page(res: ServerResponse, html: string): void {\n res.writeHead(200, PAGE_HEADERS);\n // Every user-controlled value that reaches these pages is escaped by\n // escapeHtml() in @sverka/ui's render functions; CSP default-src 'none'\n // is the backstop when anything slips through.\n res.end(html); // codeql[js/reflected-xss]\n}\n\nfunction notFoundPage(res: ServerResponse): void {\n res.writeHead(404, PAGE_HEADERS);\n res.end(\"<h1>404 — not found</h1>\");\n}\n\ninterface FindingCounts {\n readonly total: number;\n readonly bySeverity: Record<string, number>;\n}\n\nfunction parseCounts(json: string): FindingCounts {\n try {\n const parsed = JSON.parse(json) as Partial<FindingCounts>;\n return {\n total: typeof parsed.total === \"number\" ? parsed.total : 0,\n bySeverity:\n typeof parsed.bySeverity === \"object\" && parsed.bySeverity !== null\n ? parsed.bySeverity\n : {},\n };\n } catch {\n return { total: 0, bySeverity: {} };\n }\n}\n\nfunction runSummary(run: {\n runId: string;\n project: string;\n entry: string;\n status: string;\n startedAt: number | null;\n durationMs: number | null;\n findingCounts: string;\n policyVerdict: string | null;\n}) {\n return {\n runId: run.runId,\n project: run.project,\n entry: run.entry,\n status: run.status,\n startedAt: run.startedAt,\n durationMs: run.durationMs,\n findingCounts: parseCounts(run.findingCounts),\n policyVerdict: run.policyVerdict,\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,49 @@
1
1
  {
2
2
  "name": "@sverka/hub",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.41",
4
+ "description": "Sverka hub — self-hosted remote run hub (cache + history + findings)",
5
+ "license": "MIT",
6
+ "sideEffects": false,
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/sverka-dev/sverka.git",
10
+ "directory": "packages/hub"
11
+ },
12
+ "homepage": "https://sverka.dev",
13
+ "bugs": "https://github.com/sverka-dev/sverka/issues",
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "type": "module",
18
+ "engines": {
19
+ "node": ">=24.0.0"
20
+ },
21
+ "main": "./dist/index.mjs",
22
+ "module": "./dist/index.mjs",
23
+ "types": "./dist/index.d.mts",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.mts",
27
+ "import": "./dist/index.mjs"
28
+ }
29
+ },
30
+ "files": [
31
+ "dist"
32
+ ],
33
+ "scripts": {
34
+ "build": "tsdown",
35
+ "test": "vitest run --passWithNoTests",
36
+ "lint": "eslint src",
37
+ "typecheck": "tsc --noEmit"
38
+ },
39
+ "dependencies": {
40
+ "@sverka/sarif-viewer-web": "^0.2.41",
41
+ "@sverka/ui": "^0.2.41",
42
+ "@sverka/verification": "^0.2.41"
43
+ },
44
+ "devDependencies": {
45
+ "tsdown": "^0.23.1",
46
+ "typescript": "^5.8.0",
47
+ "vitest": "^5.0.5"
48
+ }
49
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.