@sverka/storage 0.2.24 → 0.2.26

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.d.mts CHANGED
@@ -1,4 +1,5 @@
1
- import { SnapshotStore } from "@sverka/runtime";
1
+ import { CacheStore, SnapshotStore } from "@sverka/runtime";
2
+ import { Finding } from "@sverka/verification";
2
3
  //#region src/file-store.d.ts
3
4
  interface FileSnapshotStoreConfig {
4
5
  readonly root?: string;
@@ -32,6 +33,131 @@ export declare class StorageError extends Error {
32
33
  readonly cause: unknown;
33
34
  constructor(code: StorageErrorCode, message: string, cause?: unknown);
34
35
  }
36
+ /** Error codes for hub/remote failures. Spec 55. */
37
+ type HubErrorCode = "REMOTE_UNAVAILABLE" | "REMOTE_REJECTED";
38
+ /**
39
+ * Hub remote-operation failure. Wraps fetch/network failures
40
+ * (REMOTE_UNAVAILABLE) and non-2xx hub responses (REMOTE_REJECTED).
41
+ * Remote operations degrade to local behaviour — this error is caught at
42
+ * the engine/CLI boundary and surfaces as a warn diagnostic, never a run
43
+ * failure.
44
+ */
45
+ export declare class HubError extends Error {
46
+ readonly code: HubErrorCode;
47
+ /** HTTP status for REMOTE_REJECTED, when known. */
48
+ readonly status?: number;
49
+ readonly cause: unknown;
50
+ constructor(code: HubErrorCode, message: string, cause?: unknown, status?: number);
51
+ }
52
+ //#endregion
53
+ //#region src/remote.d.ts
54
+ /**
55
+ * Connection settings for a hub. `url` is the hub base URL (no /v1
56
+ * suffix); `project` namespaces cache keys, runs, and snapshots
57
+ * (e.g. "sverka-dev/sverka").
58
+ */
59
+ interface RemoteStoreConfig {
60
+ readonly url: string;
61
+ readonly token: string;
62
+ readonly project: string;
63
+ /** Connect timeout — default 3 000 ms. */
64
+ readonly timeoutMs?: number;
65
+ /** Body transfer timeout — default 30 000 ms. */
66
+ readonly bodyTimeoutMs?: number;
67
+ /** Shared auth breaker: when set, a 401/403 marks it and every later
68
+ * request in the run fails fast as REMOTE_UNAVAILABLE (Spec 55 —
69
+ * "warn once naming `sverka login`, then behave as hub-down for the
70
+ * rest of the run"). */
71
+ readonly circuit?: RemoteCircuit;
72
+ }
73
+ /** Mutable auth breaker shared by all remote adapters of one run. */
74
+ interface RemoteCircuit {
75
+ authFailed: boolean;
76
+ }
77
+ export declare function createRemoteCircuit(): RemoteCircuit;
78
+ /** Index row returned by `GET /v1/runs` — the dashboard list view. */
79
+ interface HubRunSummary {
80
+ readonly runId: string;
81
+ readonly project: string;
82
+ readonly entry: string;
83
+ readonly status: string;
84
+ readonly startedAt: number | null;
85
+ readonly durationMs: number | null;
86
+ readonly findingCounts: {
87
+ readonly total: number;
88
+ readonly bySeverity: Record<string, number>;
89
+ };
90
+ readonly policyVerdict?: string;
91
+ }
92
+ /** Full stored run returned by `GET /v1/runs/{runId}`. */
93
+ interface HubRunDetail extends HubRunSummary {
94
+ readonly report: Record<string, unknown>;
95
+ readonly findings: readonly unknown[];
96
+ readonly uploadedAt: number;
97
+ }
98
+ /** Metadata the caller attaches to a run upload. */
99
+ interface RunUploadMeta {
100
+ /** Entry id the run executed. */
101
+ readonly entry: string;
102
+ /** The run's own id — lets the hub key the row to the local runId. */
103
+ readonly runId?: string;
104
+ }
105
+ interface RunUploadResult {
106
+ readonly runId: string;
107
+ readonly url: string;
108
+ }
109
+ /**
110
+ * Remote `CacheStore` (Spec 55): `store` archives the declared paths into
111
+ * a tar.zst blob and `PUT`s it; `restore` `GET`s the primary key and each
112
+ * restoreKey in order — the hub answers exact matches first, then the
113
+ * newest entry whose key has the requested prefix (same semantics as the
114
+ * file cache).
115
+ *
116
+ * Failures throw `HubError`; the engine treats CacheStore failures as
117
+ * non-fatal misses/warns, and `createTieredCacheStore` degrades to local.
118
+ */
119
+ export declare function createRemoteCacheStore(config: RemoteStoreConfig): CacheStore;
120
+ /**
121
+ * Remote `SnapshotStore` (Spec 55): suspend/resume across machines.
122
+ * Snapshots are the serialized `RunSnapshot` JSON unchanged — the hub
123
+ * treats them as opaque documents.
124
+ */
125
+ export declare function createRemoteSnapshotStore(config: RemoteStoreConfig): SnapshotStore;
126
+ /**
127
+ * Upload a run report + normalized findings to the hub (`POST /v1/runs`).
128
+ * `report` is the `sverka.run/v1` payload written to report.json — sent
129
+ * verbatim. Returns the hub-assigned runId and detail URL.
130
+ */
131
+ export declare function uploadRunReport(config: RemoteStoreConfig, report: Record<string, unknown>, findings: readonly Finding[], meta: RunUploadMeta): Promise<RunUploadResult>;
132
+ /** List remote run summaries, newest first. */
133
+ export declare function listRuns(config: RemoteStoreConfig, opts?: {
134
+ limit?: number;
135
+ before?: number;
136
+ }): Promise<HubRunSummary[]>;
137
+ /** Fetch one stored run (report + findings). 404 → undefined. */
138
+ export declare function getRun(config: RemoteStoreConfig, runId: string): Promise<HubRunDetail | undefined>;
139
+ //#endregion
140
+ //#region src/tiered-cache.d.ts
141
+ interface TieredCacheStoreConfig {
142
+ readonly local: CacheStore;
143
+ readonly remote: CacheStore;
144
+ /** Warn channel — receives `remote.cache-unreachable`-style messages.
145
+ * Each distinct message is emitted once per store instance. */
146
+ readonly onWarn?: (message: string) => void;
147
+ }
148
+ /**
149
+ * Compose a local and a remote cache store:
150
+ *
151
+ * - `restore`: local hit wins; on a local miss the remote is consulted and
152
+ * a remote hit is written back into the local store (write-through).
153
+ * A remote failure degrades to "miss" plus a `remote.cache-unreachable`
154
+ * warn — never a throw.
155
+ * - `store`: writes to both; failures on either side warn and continue.
156
+ *
157
+ * The returned store never throws for remote errors — a `sverka run` must
158
+ * not fail because the hub is down.
159
+ */
160
+ export declare function createTieredCacheStore(config: TieredCacheStoreConfig): CacheStore;
35
161
  //#endregion
36
- export type { FileSnapshotStoreConfig, SqliteSnapshotStoreConfig, StorageErrorCode };
162
+ export type { FileSnapshotStoreConfig, HubErrorCode, HubRunDetail, HubRunSummary, RemoteCircuit, RemoteStoreConfig, RunUploadMeta, RunUploadResult, SqliteSnapshotStoreConfig, StorageErrorCode, TieredCacheStoreConfig };
37
163
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/file-store.ts","../src/sqlite-store.ts","../src/errors.ts"],"mappings":";;UAoBiB;WACN;;;;;;;;wBA0BK,wBACd,SAAS,0BACR;;;UC9Bc;WACN;;;;;;;;wBAiEK,0BACd,SAAS,4BACR;EAAkB;;;;KCpFT;qBAGC,qBAAqB;WACvB,MAAM;WACG;EAEN,YAAA,MAAM,kBAAkB,iBAAiB"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/file-store.ts","../src/sqlite-store.ts","../src/errors.ts","../src/remote.ts","../src/tiered-cache.ts"],"mappings":";;;UAoBiB;WACN;;;;;;;;wBA0BK,wBACd,SAAS,0BACR;;;UC9Bc;WACN;;;;;;;;wBAiEK,0BACd,SAAS,4BACR;EAAkB;;;;KCpFT;qBAGC,qBAAqB;WACvB,MAAM;WACG;EAEN,YAAA,MAAM,kBAAkB,iBAAiB;;;KAW3C;;;;;;;;qBASC,iBAAiB;WACnB,MAAM;;WAEN;WACS;EAGhB,YAAA,MAAM,cACN,iBACA,iBACA;;;;;;;;;UCAa;WACN;WACA;WACA;;WAEA;;WAEA;;;;;WAKA,UAAU;;;UAIJ;EACf;;wBAGc,uBAAuB;;UAKtB;WACN;WACA;WACA;WACA;WACA;WACA;WACA;aACE;aACA,YAAY;;WAEd;;;UAIM,qBAAqB;WAC3B,QAAQ;WACR;WACA;;;UAIM;;WAEN;;WAEA;;UAGM;WACN;WACA;;;;;;;;;;;;wBAsdK,uBAAuB,QAAQ,oBAAoB;;;;;;wBA+CnD,0BACd,QAAQ,oBACP;;;;;;wBAwCmB,gBACpB,QAAQ,mBACR,QAAQ,yBACR,mBAAmB,WACnB,MAAM,gBACL,QAAQ;;wBA6BW,SACpB,QAAQ,mBACR;EAAS;EAAgB;IACxB,QAAQ;;wBAUW,OACpB,QAAQ,mBACR,gBACC,QAAQ;;;UCtrBM;WACN,OAAO;WACP,QAAQ;;;WAGR,UAAU;;;;;;;;;;;;;;wBAmBL,uBACd,QAAQ,yBACP"}
package/dist/index.mjs CHANGED
@@ -1,9 +1,11 @@
1
- import { chmod, lstat, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
2
- import { basename, dirname, join } from "node:path";
1
+ import { chmod, lstat, mkdir, open, readFile, readdir, readlink, rename, symlink, unlink, writeFile } from "node:fs/promises";
2
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
3
3
  import { randomBytes } from "node:crypto";
4
4
  import process from "node:process";
5
5
  import { chmodSync, closeSync, constants, fchmodSync, lstatSync, mkdirSync, openSync } from "node:fs";
6
6
  import { DatabaseSync } from "node:sqlite";
7
+ import { setTimeout } from "node:timers/promises";
8
+ import { zstdCompressSync, zstdDecompressSync } from "node:zlib";
7
9
  //#region src/errors.ts
8
10
  var StorageError = class extends Error {
9
11
  code;
@@ -15,6 +17,26 @@ var StorageError = class extends Error {
15
17
  if (cause !== void 0) this.cause = cause;
16
18
  }
17
19
  };
20
+ /**
21
+ * Hub remote-operation failure. Wraps fetch/network failures
22
+ * (REMOTE_UNAVAILABLE) and non-2xx hub responses (REMOTE_REJECTED).
23
+ * Remote operations degrade to local behaviour — this error is caught at
24
+ * the engine/CLI boundary and surfaces as a warn diagnostic, never a run
25
+ * failure.
26
+ */
27
+ var HubError = class extends Error {
28
+ code;
29
+ /** HTTP status for REMOTE_REJECTED, when known. */
30
+ status;
31
+ cause;
32
+ constructor(code, message, cause, status) {
33
+ super(message);
34
+ this.name = "HubError";
35
+ this.code = code;
36
+ if (status !== void 0) this.status = status;
37
+ if (cause !== void 0) this.cause = cause;
38
+ }
39
+ };
18
40
  //#endregion
19
41
  //#region src/internal/serialize.ts
20
42
  /** Serialize a RunSnapshot to pretty-printed JSON (2-space indent). */
@@ -315,6 +337,569 @@ function createSqliteSnapshotStore(config) {
315
337
  };
316
338
  }
317
339
  //#endregion
318
- export { StorageError, createFileSnapshotStore, createSqliteSnapshotStore };
340
+ //#region src/internal/tar.ts
341
+ const BLOCK = 512;
342
+ const NAME_MAX = 100;
343
+ const PREFIX_MAX = 155;
344
+ const LINKNAME_MAX = 100;
345
+ function writeString(buf, offset, length, value) {
346
+ const encoded = new TextEncoder().encode(value);
347
+ buf.set(encoded.subarray(0, Math.min(encoded.length, length)), offset);
348
+ }
349
+ function writeOctal(buf, offset, length, value) {
350
+ const text = value.toString(8).padStart(length - 1, "0");
351
+ writeString(buf, offset, length - 1, text);
352
+ buf[offset + length - 1] = 0;
353
+ }
354
+ function readString(buf, offset, length) {
355
+ let end = offset;
356
+ const limit = offset + length;
357
+ while (end < limit && buf[end] !== 0) end++;
358
+ return new TextDecoder().decode(buf.subarray(offset, end));
359
+ }
360
+ function readOctal(buf, offset, length) {
361
+ const text = readString(buf, offset, length).trim();
362
+ if (text === "") return 0;
363
+ return Number.parseInt(text, 8);
364
+ }
365
+ /** Split `name` into ustar (prefix, name) fields, or null when unsplittable. */
366
+ function splitName(name) {
367
+ const encoded = new TextEncoder();
368
+ if (encoded.encode(name).length <= NAME_MAX) return {
369
+ prefix: "",
370
+ name
371
+ };
372
+ let idx = name.lastIndexOf("/", name.length);
373
+ while (idx > 0) {
374
+ const prefix = name.slice(0, idx);
375
+ const rest = name.slice(idx + 1);
376
+ if (encoded.encode(prefix).length <= PREFIX_MAX && encoded.encode(rest).length <= NAME_MAX && rest.length > 0) return {
377
+ prefix,
378
+ name: rest
379
+ };
380
+ idx = name.lastIndexOf("/", idx - 1);
381
+ }
382
+ return null;
383
+ }
384
+ function header(entry) {
385
+ const buf = new Uint8Array(BLOCK);
386
+ const split = splitName(entry.name);
387
+ if (split === null) throw new StorageError("STORE_IO_FAILED", `tar entry name too long: ${entry.name.slice(0, 80)}…`);
388
+ const size = entry.type === "file" ? entry.data?.length ?? 0 : 0;
389
+ writeString(buf, 0, NAME_MAX, split.name);
390
+ writeOctal(buf, 100, 8, entry.mode ?? (entry.type === "dir" ? 493 : 420));
391
+ writeOctal(buf, 108, 8, 0);
392
+ writeOctal(buf, 116, 8, 0);
393
+ writeOctal(buf, 124, 12, size);
394
+ writeOctal(buf, 136, 12, entry.mtime ?? 0);
395
+ buf.fill(32, 148, 156);
396
+ buf[156] = entry.type === "dir" ? 53 : entry.type === "symlink" ? 50 : 48;
397
+ if (entry.type === "symlink") {
398
+ const link = entry.linkname ?? "";
399
+ if (new TextEncoder().encode(link).length > LINKNAME_MAX) throw new StorageError("STORE_IO_FAILED", `symlink target too long: ${entry.name}`);
400
+ writeString(buf, 157, LINKNAME_MAX, link);
401
+ }
402
+ writeString(buf, 257, 8, "ustar\0");
403
+ writeString(buf, 263, 2, "00");
404
+ writeString(buf, 265, 32, "sverka");
405
+ writeString(buf, 297, 32, "sverka");
406
+ writeString(buf, 345, PREFIX_MAX, split.prefix);
407
+ let sum = 0;
408
+ for (const byte of buf) sum += byte;
409
+ writeString(buf, 148, 6, sum.toString(8).padStart(6, "0"));
410
+ buf[154] = 0;
411
+ buf[155] = 32;
412
+ return buf;
413
+ }
414
+ /** Pack entries into a ustar archive. */
415
+ function packTar(entries) {
416
+ const chunks = [];
417
+ for (const entry of entries) {
418
+ chunks.push(header(entry));
419
+ if (entry.type === "file" && entry.data !== void 0) {
420
+ chunks.push(entry.data);
421
+ const pad = (BLOCK - entry.data.length % BLOCK) % BLOCK;
422
+ if (pad > 0) chunks.push(new Uint8Array(pad));
423
+ }
424
+ }
425
+ chunks.push(new Uint8Array(BLOCK * 2));
426
+ const out = new Uint8Array(chunks.reduce((n, c) => n + c.length, 0));
427
+ let offset = 0;
428
+ for (const chunk of chunks) {
429
+ out.set(chunk, offset);
430
+ offset += chunk.length;
431
+ }
432
+ return out;
433
+ }
434
+ function isZeroBlock(buf, offset) {
435
+ for (let i = offset; i < offset + BLOCK; i++) if (buf[i] !== 0) return false;
436
+ return true;
437
+ }
438
+ function verifyChecksum(buf, offset) {
439
+ let sum = 0;
440
+ for (let i = offset; i < offset + BLOCK; i++) sum += i >= offset + 148 && i < offset + 156 ? 32 : buf[i];
441
+ return sum === readOctal(buf, offset + 148, 8);
442
+ }
443
+ function parseHeader(archive, offset) {
444
+ const prefix = readString(archive, offset + 345, PREFIX_MAX);
445
+ const namePart = readString(archive, offset, NAME_MAX);
446
+ const name = prefix === "" ? namePart : `${prefix}/${namePart}`;
447
+ const size = readOctal(archive, offset + 124, 12);
448
+ if (!Number.isSafeInteger(size) || size < 0) throw new StorageError("CORRUPT_SNAPSHOT", "tar entry has an invalid size field");
449
+ const typeflag = archive[offset + 156];
450
+ const mtime = readOctal(archive, offset + 136, 12);
451
+ const rawMode = readOctal(archive, offset + 100, 8);
452
+ const mode = Number.isSafeInteger(rawMode) ? rawMode & 511 : void 0;
453
+ const linkname = typeflag === 50 ? readString(archive, offset + 157, LINKNAME_MAX) : void 0;
454
+ return {
455
+ name,
456
+ size,
457
+ typeflag,
458
+ mtime,
459
+ ...mode !== void 0 ? { mode } : {},
460
+ ...linkname !== void 0 ? { linkname } : {}
461
+ };
462
+ }
463
+ /** Map a parsed header (+file data) to an entry — unsupported typeflags
464
+ * return undefined (their data blocks are still consumed by the loop). */
465
+ function entryFor(h, data) {
466
+ const shared = {
467
+ ...h.mode !== void 0 ? { mode: h.mode } : {},
468
+ mtime: h.mtime
469
+ };
470
+ if (h.typeflag === 48 || h.typeflag === 0) return {
471
+ name: h.name,
472
+ type: "file",
473
+ data,
474
+ ...shared
475
+ };
476
+ if (h.typeflag === 53) return {
477
+ name: h.name.replace(/\/$/, ""),
478
+ type: "dir",
479
+ ...shared
480
+ };
481
+ if (h.typeflag === 50) return {
482
+ name: h.name,
483
+ type: "symlink",
484
+ ...h.linkname !== void 0 ? { linkname: h.linkname } : {},
485
+ ...shared
486
+ };
487
+ }
488
+ /** Unpack a ustar archive into entries. */
489
+ function unpackTar(archive) {
490
+ const entries = [];
491
+ let offset = 0;
492
+ while (offset + BLOCK <= archive.length) {
493
+ if (isZeroBlock(archive, offset)) break;
494
+ if (!verifyChecksum(archive, offset)) throw new StorageError("CORRUPT_SNAPSHOT", "tar archive checksum mismatch (corrupt blob)");
495
+ const h = parseHeader(archive, offset);
496
+ offset += BLOCK;
497
+ if (offset + h.size > archive.length) throw new StorageError("CORRUPT_SNAPSHOT", "tar archive truncated mid-entry");
498
+ const entry = entryFor(h, archive.slice(offset, offset + h.size));
499
+ if (entry !== void 0) entries.push(entry);
500
+ offset += Math.ceil(h.size / BLOCK) * BLOCK;
501
+ }
502
+ return entries;
503
+ }
504
+ //#endregion
505
+ //#region src/remote.ts
506
+ function createRemoteCircuit() {
507
+ return { authFailed: false };
508
+ }
509
+ const DEFAULT_CONNECT_TIMEOUT_MS = 3e3;
510
+ const DEFAULT_BODY_TIMEOUT_MS = 3e4;
511
+ async function hubRequest(config, method, path, body, contentType = "application/octet-stream") {
512
+ if (config.circuit?.authFailed === true) throw new HubError("REMOTE_UNAVAILABLE", `hub auth failed earlier this run — treated as hub-down (${method} ${path})`);
513
+ const base = hubBaseUrl(config);
514
+ const connectMs = config.timeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
515
+ const bodyMs = config.bodyTimeoutMs ?? DEFAULT_BODY_TIMEOUT_MS;
516
+ const ac = new AbortController();
517
+ const init = {
518
+ method,
519
+ signal: ac.signal
520
+ };
521
+ const headers = { authorization: `Bearer ${config.token}` };
522
+ if (body !== void 0) headers["content-type"] = contentType;
523
+ init.headers = headers;
524
+ if (body !== void 0) init.body = body;
525
+ const reqUrl = new URL(path.replace(/^\/+/, ""), base);
526
+ let res;
527
+ try {
528
+ const firstMs = body !== void 0 ? bodyMs : connectMs;
529
+ res = await withAbortOnTimeout(fetch(reqUrl, init), firstMs, ac, `connect/upload timeout after ${firstMs}ms`);
530
+ } catch (e) {
531
+ throw new HubError("REMOTE_UNAVAILABLE", `hub unreachable (${method} ${path}): ${e instanceof Error ? e.message : String(e)}`, e);
532
+ }
533
+ if (config.circuit !== void 0 && (res.status === 401 || res.status === 403)) config.circuit.authFailed = true;
534
+ try {
535
+ const buf = new Uint8Array(await withAbortOnTimeout(res.arrayBuffer(), bodyMs, ac, `body timeout after ${bodyMs}ms`));
536
+ return {
537
+ status: res.status,
538
+ headers: res.headers,
539
+ body: buf
540
+ };
541
+ } catch (e) {
542
+ throw new HubError("REMOTE_UNAVAILABLE", `hub body read failed (${method} ${path}): ${e instanceof Error ? e.message : String(e)}`, e);
543
+ }
544
+ }
545
+ /** Race a promise against a timeout that aborts `ac` — the abort (not a
546
+ * detached rejection) is what cancels the in-flight request or stream.
547
+ * The timer itself is cancelled once the promise settles. */
548
+ async function withAbortOnTimeout(promise, ms, ac, message) {
549
+ const cancel = new AbortController();
550
+ const armed = setTimeout(ms, void 0, { signal: cancel.signal }).then(() => {
551
+ const e = new DOMException(message, "TimeoutError");
552
+ ac.abort(e);
553
+ throw e;
554
+ });
555
+ try {
556
+ return await Promise.race([promise, armed]);
557
+ } finally {
558
+ cancel.abort();
559
+ }
560
+ }
561
+ /** The configured hub base as a URL — http(s) only. Anything else
562
+ * (file:, ws:, a bare hostname) is a config error, not a request
563
+ * failure, so it fails before the network is touched. */
564
+ function hubBaseUrl(config) {
565
+ let url;
566
+ try {
567
+ url = new URL(config.url);
568
+ } catch (e) {
569
+ throw new HubError("REMOTE_UNAVAILABLE", `invalid hub url "${config.url}"`, e instanceof Error ? e : void 0);
570
+ }
571
+ if (url.protocol !== "http:" && url.protocol !== "https:") throw new HubError("REMOTE_UNAVAILABLE", `hub url must be http(s), got ${url.protocol}`);
572
+ if (!url.pathname.endsWith("/")) url.pathname += "/";
573
+ url.search = "";
574
+ url.hash = "";
575
+ return url;
576
+ }
577
+ /** Parse a 2xx body as JSON, keeping the file's contract that every
578
+ * remote failure surfaces as a HubError (a proxy error page or truncated
579
+ * body would otherwise escape as a raw SyntaxError). */
580
+ function parseJsonBody(res, path) {
581
+ try {
582
+ return JSON.parse(new TextDecoder().decode(res.body));
583
+ } catch (e) {
584
+ throw new HubError("REMOTE_REJECTED", `hub ${path} returned malformed JSON`, e, res.status);
585
+ }
586
+ }
587
+ function rejected(method, path, res) {
588
+ const detail = new TextDecoder().decode(res.body).slice(0, 200);
589
+ const authHint = res.status === 401 || res.status === 403 ? " — run `sverka login` to refresh the hub token" : "";
590
+ const detailSuffix = detail === "" ? "" : `: ${detail}`;
591
+ return new HubError("REMOTE_REJECTED", `hub ${method} ${path} → ${res.status}${authHint}${detailSuffix}`, void 0, res.status);
592
+ }
593
+ async function collectTarEntries(sourceDir, relPath, out) {
594
+ const abs = join(sourceDir, relPath);
595
+ const name = relPath.replaceAll("\\", "/");
596
+ let handle;
597
+ let openError;
598
+ try {
599
+ handle = await open(abs, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
600
+ } catch (e) {
601
+ openError = e;
602
+ }
603
+ const st = handle !== void 0 ? await handle.stat() : await lstat(abs);
604
+ if (st.isFile()) {
605
+ if (handle === void 0) throw openError instanceof Error ? openError : /* @__PURE__ */ new Error(`cannot open ${abs}`);
606
+ try {
607
+ out.push({
608
+ name,
609
+ type: "file",
610
+ data: new Uint8Array(await handle.readFile()),
611
+ mode: st.mode & 511,
612
+ mtime: Math.floor(st.mtimeMs / 1e3)
613
+ });
614
+ } finally {
615
+ await handle.close();
616
+ }
617
+ return;
618
+ }
619
+ await handle?.close();
620
+ if (st.isDirectory()) {
621
+ out.push({
622
+ name,
623
+ type: "dir",
624
+ mode: st.mode & 511,
625
+ mtime: Math.floor(st.mtimeMs / 1e3)
626
+ });
627
+ const children = await readdir(abs);
628
+ children.sort();
629
+ for (const child of children) await collectTarEntries(sourceDir, join(relPath, child), out);
630
+ } else if (st.isSymbolicLink()) out.push({
631
+ name,
632
+ type: "symlink",
633
+ linkname: await readlink(abs),
634
+ mtime: Math.floor(st.mtimeMs / 1e3)
635
+ });
636
+ }
637
+ /** Manifest entry stored inside the tar — same shape as the file cache's
638
+ * manifest.json so both backends describe one entry identically. */
639
+ const MANIFEST_NAME = ".sverka-cache-manifest.json";
640
+ async function packCacheBlob(sourceDir, paths, key) {
641
+ const entries = [{
642
+ name: MANIFEST_NAME,
643
+ type: "file",
644
+ data: new TextEncoder().encode(JSON.stringify({
645
+ key,
646
+ paths,
647
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
648
+ }))
649
+ }];
650
+ for (const path of paths) await collectTarEntries(sourceDir, path, entries);
651
+ return new Uint8Array(zstdCompressSync(Buffer.from(packTar(entries))));
652
+ }
653
+ function assertEntryName(name) {
654
+ if (name === "" || name.startsWith("/") || name.includes("\\") || name.split("/").includes("..")) throw new HubError("REMOTE_REJECTED", `cache blob entry escapes the target dir: ${name}`);
655
+ }
656
+ /**
657
+ * Reject the entry's parent path when any *existing* ancestor component
658
+ * under targetDir is a symlink (or a non-directory). Writes must never be
659
+ * redirected outside the cache root — a hostile blob could declare a
660
+ * contained symlink first and then write through it, and targetDir may
661
+ * carry links left by an earlier restore.
662
+ *
663
+ * Missing components are fine: mkdir creates them under the verified
664
+ * prefix. (There is an inherent POSIX TOCTOU window between this check
665
+ * and the write; the leaf O_NOFOLLOW narrows it to ancestor components.)
666
+ */
667
+ async function assertNoSymlinkAncestors(targetDir, parent, name) {
668
+ const rel = relative(targetDir, parent);
669
+ if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) throw new HubError("REMOTE_REJECTED", `cache blob entry escapes the target dir: ${name}`);
670
+ if (rel === "") return;
671
+ let cur = targetDir;
672
+ for (const seg of rel.split(sep)) {
673
+ cur = join(cur, seg);
674
+ const st = await lstat(cur).catch(() => void 0);
675
+ if (st === void 0) return;
676
+ if (st.isSymbolicLink() || !st.isDirectory()) throw new HubError("REMOTE_REJECTED", `cache blob entry traverses a symlink or non-directory: ${name}`);
677
+ }
678
+ }
679
+ /** Declared paths the blob actually covers — validated before any write
680
+ * so a partial entry leaves no half-restored tree. */
681
+ function coveredPaths(tar, wanted, targetDir, containedLink) {
682
+ const covered = /* @__PURE__ */ new Set();
683
+ for (const entry of tar) {
684
+ assertEntryName(entry.name);
685
+ if (entry.type === "symlink" && !containedLink(join(targetDir, entry.name), entry.linkname ?? "")) continue;
686
+ for (const p of wanted) if (entry.name === p || entry.name.startsWith(`${p}/`)) covered.add(p);
687
+ }
688
+ return covered;
689
+ }
690
+ async function extractDirEntry(dest, entry) {
691
+ if ((await lstat(dest).catch(() => void 0))?.isSymbolicLink()) throw new HubError("REMOTE_REJECTED", `cache blob dir entry collides with a symlink: ${entry.name}`);
692
+ await mkdir(dest, {
693
+ recursive: true,
694
+ ...entry.mode !== void 0 ? { mode: entry.mode } : {}
695
+ });
696
+ }
697
+ async function extractFileEntry(dest, entry) {
698
+ await mkdir(dirname(dest), { recursive: true });
699
+ const handle = await open(dest, constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | (constants.O_NOFOLLOW ?? 0), entry.mode ?? 420);
700
+ try {
701
+ await handle.writeFile(entry.data ?? /* @__PURE__ */ new Uint8Array());
702
+ if (entry.mode !== void 0) await handle.chmod(entry.mode);
703
+ } finally {
704
+ await handle.close();
705
+ }
706
+ }
707
+ async function extractEntry(targetDir, wanted, containedLink, entry) {
708
+ if (entry.name === MANIFEST_NAME) return;
709
+ if (![...wanted].some((p) => entry.name === p || entry.name.startsWith(`${p}/`))) return;
710
+ const dest = join(targetDir, entry.name);
711
+ await assertNoSymlinkAncestors(targetDir, dirname(dest), entry.name);
712
+ if (entry.type === "dir") await extractDirEntry(dest, entry);
713
+ else if (entry.type === "file" && entry.data !== void 0) await extractFileEntry(dest, entry);
714
+ else if (entry.type === "symlink" && entry.linkname !== void 0 && containedLink(dest, entry.linkname)) {
715
+ await mkdir(dirname(dest), { recursive: true });
716
+ await symlink(entry.linkname, dest);
717
+ }
718
+ }
719
+ /**
720
+ * Extract a tar.zst blob's declared paths into targetDir. Returns the set
721
+ * of declared paths that were actually restored — a blob that lacks a
722
+ * declared path is a partial entry and must count as a miss, not a hit
723
+ * (otherwise the engine skips rebuilding outputs that never landed).
724
+ */
725
+ async function extractCacheBlob(blob, paths, targetDir) {
726
+ const tar = unpackTar(new Uint8Array(zstdDecompressSync(Buffer.from(blob))));
727
+ const wanted = new Set(paths.map((p) => p.replaceAll("\\", "/")));
728
+ const targetRoot = resolve(targetDir);
729
+ const containedLink = (dest, link) => {
730
+ if (link === "" || link.startsWith("/") || link.includes("\\")) return false;
731
+ const resolved = resolve(dirname(dest), link);
732
+ return resolved === targetRoot || resolved.startsWith(`${targetRoot}${sep}`);
733
+ };
734
+ const covered = coveredPaths(tar, wanted, targetDir, containedLink);
735
+ if (covered.size === wanted.size) for (const entry of tar) await extractEntry(targetDir, wanted, containedLink, entry);
736
+ return covered;
737
+ }
738
+ /**
739
+ * Remote `CacheStore` (Spec 55): `store` archives the declared paths into
740
+ * a tar.zst blob and `PUT`s it; `restore` `GET`s the primary key and each
741
+ * restoreKey in order — the hub answers exact matches first, then the
742
+ * newest entry whose key has the requested prefix (same semantics as the
743
+ * file cache).
744
+ *
745
+ * Failures throw `HubError`; the engine treats CacheStore failures as
746
+ * non-fatal misses/warns, and `createTieredCacheStore` degrades to local.
747
+ */
748
+ function createRemoteCacheStore(config) {
749
+ const pathFor = (key) => `/v1/cache/${encodeURIComponent(config.project)}/${encodeURIComponent(key)}`;
750
+ return {
751
+ async restore(req) {
752
+ const wanted = new Set(req.paths.map((p) => p.replaceAll("\\", "/")));
753
+ const attempts = [{
754
+ key: req.key,
755
+ prefix: false
756
+ }, ...req.restoreKeys.map((key) => ({
757
+ key,
758
+ prefix: true
759
+ }))];
760
+ for (const { key, prefix } of attempts) {
761
+ const res = await hubRequest(config, "GET", `${pathFor(key)}${prefix ? "?prefix=1" : ""}`);
762
+ if (res.status === 404) continue;
763
+ if (res.status !== 200) throw rejected("GET", pathFor(key), res);
764
+ if ((await extractCacheBlob(res.body, req.paths, req.targetDir)).size < wanted.size) continue;
765
+ return { key: res.headers.get("x-sverka-cache-key") ?? key };
766
+ }
767
+ },
768
+ async store(req) {
769
+ const blob = await packCacheBlob(req.sourceDir, req.paths, req.key);
770
+ const res = await hubRequest(config, "PUT", pathFor(req.key), blob);
771
+ if (res.status >= 300) throw rejected("PUT", pathFor(req.key), res);
772
+ }
773
+ };
774
+ }
775
+ /**
776
+ * Remote `SnapshotStore` (Spec 55): suspend/resume across machines.
777
+ * Snapshots are the serialized `RunSnapshot` JSON unchanged — the hub
778
+ * treats them as opaque documents.
779
+ */
780
+ function createRemoteSnapshotStore(config) {
781
+ const pathFor = (runId) => `/v1/snapshots/${encodeURIComponent(config.project)}/${encodeURIComponent(runId)}`;
782
+ return {
783
+ async save(snapshot) {
784
+ const res = await hubRequest(config, "PUT", pathFor(snapshot.runId), serialize(snapshot), "application/json");
785
+ if (res.status >= 300) throw rejected("PUT", pathFor(snapshot.runId), res);
786
+ },
787
+ async load(runId) {
788
+ const res = await hubRequest(config, "GET", pathFor(runId));
789
+ if (res.status === 404) return void 0;
790
+ if (res.status !== 200) throw rejected("GET", pathFor(runId), res);
791
+ return deserialize(new TextDecoder().decode(res.body), runId);
792
+ },
793
+ async delete(runId) {
794
+ const res = await hubRequest(config, "DELETE", pathFor(runId));
795
+ if (res.status === 404) return;
796
+ if (res.status >= 300) throw rejected("DELETE", pathFor(runId), res);
797
+ }
798
+ };
799
+ }
800
+ /**
801
+ * Upload a run report + normalized findings to the hub (`POST /v1/runs`).
802
+ * `report` is the `sverka.run/v1` payload written to report.json — sent
803
+ * verbatim. Returns the hub-assigned runId and detail URL.
804
+ */
805
+ async function uploadRunReport(config, report, findings, meta) {
806
+ const res = await hubRequest(config, "POST", "/v1/runs", JSON.stringify({
807
+ project: config.project,
808
+ entry: meta.entry,
809
+ ...meta.runId !== void 0 ? { runId: meta.runId } : {},
810
+ report,
811
+ findings
812
+ }), "application/json");
813
+ if (res.status !== 200 && res.status !== 201) throw rejected("POST", "/v1/runs", res);
814
+ const parsed = parseJsonBody(res, "POST /v1/runs");
815
+ return {
816
+ runId: typeof parsed.runId === "string" ? parsed.runId : "unknown",
817
+ url: typeof parsed.url === "string" ? parsed.url : ""
818
+ };
819
+ }
820
+ /** List remote run summaries, newest first. */
821
+ async function listRuns(config, opts) {
822
+ const params = new URLSearchParams({ project: config.project });
823
+ if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
824
+ if (opts?.before !== void 0) params.set("before", String(opts.before));
825
+ const res = await hubRequest(config, "GET", `/v1/runs?${params}`);
826
+ if (res.status !== 200) throw rejected("GET", "/v1/runs", res);
827
+ return parseJsonBody(res, "GET /v1/runs");
828
+ }
829
+ /** Fetch one stored run (report + findings). 404 → undefined. */
830
+ async function getRun(config, runId) {
831
+ const res = await hubRequest(config, "GET", `/v1/runs/${encodeURIComponent(runId)}`);
832
+ if (res.status === 404) return void 0;
833
+ if (res.status !== 200) throw rejected("GET", `/v1/runs/${runId}`, res);
834
+ return parseJsonBody(res, `GET /v1/runs/${runId}`);
835
+ }
836
+ //#endregion
837
+ //#region src/tiered-cache.ts
838
+ function message(e) {
839
+ return e instanceof Error ? e.message : String(e);
840
+ }
841
+ /**
842
+ * Compose a local and a remote cache store:
843
+ *
844
+ * - `restore`: local hit wins; on a local miss the remote is consulted and
845
+ * a remote hit is written back into the local store (write-through).
846
+ * A remote failure degrades to "miss" plus a `remote.cache-unreachable`
847
+ * warn — never a throw.
848
+ * - `store`: writes to both; failures on either side warn and continue.
849
+ *
850
+ * The returned store never throws for remote errors — a `sverka run` must
851
+ * not fail because the hub is down.
852
+ */
853
+ function createTieredCacheStore(config) {
854
+ const warned = /* @__PURE__ */ new Set();
855
+ const warn = (msg) => {
856
+ if (warned.has(msg)) return;
857
+ warned.add(msg);
858
+ config.onWarn?.(msg);
859
+ };
860
+ return {
861
+ async restore(req) {
862
+ let localHit;
863
+ try {
864
+ localHit = await config.local.restore(req);
865
+ } catch (e) {
866
+ warn(`local cache restore failed: ${message(e)}`);
867
+ }
868
+ if (localHit !== void 0) return localHit;
869
+ let remoteHit;
870
+ try {
871
+ remoteHit = await config.remote.restore(req);
872
+ } catch (e) {
873
+ warn(`remote.cache-unreachable: ${message(e)}`);
874
+ return;
875
+ }
876
+ if (remoteHit === void 0) return void 0;
877
+ try {
878
+ await config.local.store({
879
+ key: remoteHit.key,
880
+ paths: req.paths,
881
+ sourceDir: req.targetDir
882
+ });
883
+ } catch (e) {
884
+ warn(`local cache write-back failed: ${message(e)}`);
885
+ }
886
+ return remoteHit;
887
+ },
888
+ async store(req) {
889
+ try {
890
+ await config.local.store(req);
891
+ } catch (e) {
892
+ warn(`local cache store failed: ${message(e)}`);
893
+ }
894
+ try {
895
+ await config.remote.store(req);
896
+ } catch (e) {
897
+ warn(`remote.cache-unreachable: ${message(e)}`);
898
+ }
899
+ }
900
+ };
901
+ }
902
+ //#endregion
903
+ export { HubError, StorageError, createFileSnapshotStore, createRemoteCacheStore, createRemoteCircuit, createRemoteSnapshotStore, createSqliteSnapshotStore, createTieredCacheStore, getRun, listRuns, uploadRunReport };
319
904
 
320
905
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/errors.ts","../src/internal/serialize.ts","../src/internal/io-helpers.ts","../src/file-store.ts","../src/sqlite-store.ts"],"sourcesContent":["// StorageError — error class for @sverka/storage.\n// Spec 31 — Error handling.\n\nexport type StorageErrorCode =\n \"STORE_IO_FAILED\" | \"CORRUPT_SNAPSHOT\" | \"INVALID_RUN_ID\";\n\nexport class StorageError extends Error {\n readonly code: StorageErrorCode;\n override readonly cause: unknown;\n\n constructor(code: StorageErrorCode, message: string, cause?: unknown) {\n super(message);\n this.name = \"StorageError\";\n this.code = code;\n if (cause !== undefined) {\n this.cause = cause;\n }\n }\n}\n","// serialize / deserialize — JSON round-trip + validation for RunSnapshot.\n// Spec 31 — Serialization.\n\nimport type { RunSnapshot } from \"@sverka/runtime\";\nimport { StorageError } from \"../errors.js\";\n\n/** Serialize a RunSnapshot to pretty-printed JSON (2-space indent). */\nexport function serialize(snapshot: RunSnapshot): string {\n return JSON.stringify(snapshot, null, 2);\n}\n\n/**\n * Deserialize a JSON string into a RunSnapshot, validating required fields.\n * Throws StorageError(CORRUPT_SNAPSHOT) on parse failure or shape mismatch.\n */\nexport function deserialize(text: string, runId: string): RunSnapshot {\n const parsed = parseJson(text);\n if (typeof parsed !== \"object\" || parsed === null) {\n throw new StorageError(\"CORRUPT_SNAPSHOT\", \"snapshot is not an object\");\n }\n const obj = parsed as Record<string, unknown>;\n validateScalarFields(obj, runId);\n validateCompletedSteps(obj);\n validateResumeSchema(obj);\n return parsed as RunSnapshot;\n}\n\nfunction parseJson(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch (e) {\n throw new StorageError(\"CORRUPT_SNAPSHOT\", \"snapshot is not valid JSON\", e);\n }\n}\n\nfunction validateScalarFields(\n obj: Record<string, unknown>,\n runId: string,\n): void {\n validateStringField(obj, \"runId\");\n if (obj[\"runId\"] !== runId) {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n `snapshot runId \"${obj[\"runId\"]}\" does not match requested runId \"${runId}\"`,\n );\n }\n validateStringField(obj, \"planId\");\n validatePlanField(obj);\n if (!Array.isArray(obj[\"completedSteps\"])) {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n \"missing or invalid field: completedSteps\",\n );\n }\n validateStringField(obj, \"suspendedStepId\");\n if (obj[\"status\"] !== \"suspended\") {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n `expected status \"suspended\", got \"${String(obj[\"status\"])}\"`,\n );\n }\n if (typeof obj[\"suspendedAt\"] !== \"number\") {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n \"missing or invalid field: suspendedAt\",\n );\n }\n}\n\nfunction validateStringField(\n obj: Record<string, unknown>,\n field: string,\n): void {\n if (typeof obj[field] !== \"string\") {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n `missing or invalid field: ${field}`,\n );\n }\n}\n\nfunction isInputValue(value: unknown): boolean {\n return (\n typeof value === \"string\" ||\n // Finite only — Infinity/NaN re-serialize to null and don't round-trip.\n (typeof value === \"number\" && Number.isFinite(value)) ||\n typeof value === \"boolean\" ||\n (Array.isArray(value) && value.every((v) => typeof v === \"string\"))\n );\n}\n\nfunction validatePlanField(obj: Record<string, unknown>): void {\n const plan = obj[\"plan\"];\n if (typeof plan !== \"object\" || plan === null || Array.isArray(plan)) {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n \"missing or invalid field: plan\",\n );\n }\n const p = plan as Record<string, unknown>;\n if (p[\"apiVersion\"] !== \"sverka.dev/v1run\") {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n `plan.apiVersion must be \"sverka.dev/v1run\"`,\n );\n }\n for (const field of [\"id\", \"graphId\", \"createdAt\"] as const) {\n if (typeof p[field] !== \"string\") {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n `plan.${field} is missing or not a string`,\n );\n }\n }\n validatePlanEntry(p[\"entry\"]);\n validatePlanInputs(p[\"inputs\"]);\n validatePlanSteps(p[\"steps\"]);\n}\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\nfunction validatePlanEntry(entry: unknown): void {\n if (!isRecord(entry)) {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n \"plan.entry is missing or not an object\",\n );\n }\n if (typeof entry[\"id\"] !== \"string\" || !isRecord(entry[\"trigger\"])) {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n \"plan.entry lacks a string id or a trigger object\",\n );\n }\n}\n\nfunction validatePlanInputs(inputs: unknown): void {\n if (!isRecord(inputs)) {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n \"plan.inputs is missing or not an object\",\n );\n }\n for (const [key, value] of Object.entries(inputs)) {\n if (!isInputValue(value)) {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n `plan.inputs.${key} is not a valid InputValue`,\n );\n }\n }\n}\n\nfunction validatePlanStep(s: unknown, i: number): void {\n if (!isRecord(s) || typeof s[\"id\"] !== \"string\") {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n `plan.steps[${i}] is missing or lacks a string id`,\n );\n }\n if (!isRecord(s[\"runtime\"]) || !hasStepArrays(s)) {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n `plan.steps[${i}] lacks runtime/operations/inputs/outputs/dependencies`,\n );\n }\n}\n\nfunction hasStepArrays(s: Record<string, unknown>): boolean {\n return (\n Array.isArray(s[\"operations\"]) &&\n Array.isArray(s[\"inputs\"]) &&\n Array.isArray(s[\"outputs\"]) &&\n Array.isArray(s[\"dependencies\"])\n );\n}\n\nfunction validatePlanSteps(steps: unknown): void {\n if (!Array.isArray(steps)) {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n \"plan.steps is missing or not an array\",\n );\n }\n steps.forEach((s, i) => validatePlanStep(s, i));\n}\n\nfunction validateCompletedSteps(obj: Record<string, unknown>): void {\n const steps = obj[\"completedSteps\"] as unknown[];\n for (let i = 0; i < steps.length; i++) {\n const entry = steps[i];\n if (typeof entry !== \"object\" || entry === null) {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n `completedSteps[${i}] is not an object`,\n );\n }\n const step = entry as Record<string, unknown>;\n if (typeof step[\"stepId\"] !== \"string\") {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n `completedSteps[${i}].stepId is missing or not a string`,\n );\n }\n if (\n typeof step[\"outputs\"] !== \"object\" ||\n step[\"outputs\"] === null ||\n Array.isArray(step[\"outputs\"])\n ) {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n `completedSteps[${i}].outputs is missing or not an object`,\n );\n }\n for (const [key, value] of Object.entries(\n step[\"outputs\"] as Record<string, unknown>,\n )) {\n if (!isInputValue(value)) {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n `completedSteps[${i}].outputs.${key} is not a valid InputValue`,\n );\n }\n }\n }\n}\n\nfunction validateResumeSchema(obj: Record<string, unknown>): void {\n if (obj[\"resumeSchema\"] === undefined) return;\n if (\n typeof obj[\"resumeSchema\"] !== \"object\" ||\n obj[\"resumeSchema\"] === null ||\n Array.isArray(obj[\"resumeSchema\"])\n ) {\n throw new StorageError(\"CORRUPT_SNAPSHOT\", \"resumeSchema is not an object\");\n }\n const rs = obj[\"resumeSchema\"] as Record<string, unknown>;\n if (rs[\"required\"] !== undefined) {\n if (\n !Array.isArray(rs[\"required\"]) ||\n !rs[\"required\"].every((v) => typeof v === \"string\")\n ) {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n \"resumeSchema.required is not an array of strings\",\n );\n }\n }\n}\n","// Shared I/O helpers for snapshot store implementations.\n// Spec 31 — Error handling.\n\nimport { StorageError } from \"../errors.js\";\n\n/**\n * Check if an error is an ENOENT (file not found) error.\n */\nexport function isENOENT(e: unknown): boolean {\n return (\n typeof e === \"object\" &&\n e !== null &&\n (e as NodeJS.ErrnoException).code === \"ENOENT\"\n );\n}\n\n/**\n * Wrap an async I/O operation, throwing StorageError(STORE_IO_FAILED) on failure.\n */\nexport async function wrapIO<T>(\n label: string,\n fn: () => Promise<T>,\n): Promise<T> {\n try {\n return await fn();\n } catch (e) {\n throw new StorageError(\"STORE_IO_FAILED\", `failed to ${label}`, e);\n }\n}\n","// FileSnapshotStore — JSON file per run at <root>/.sverka/runs/<runId>/snapshot.json.\n// Spec 31 — File layout.\n\nimport {\n chmod,\n lstat,\n mkdir,\n readFile,\n rename,\n unlink,\n writeFile,\n} from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { randomBytes } from \"node:crypto\";\nimport process from \"node:process\";\nimport type { RunSnapshot, SnapshotStore } from \"@sverka/runtime\";\nimport { StorageError } from \"./errors.js\";\nimport { serialize, deserialize } from \"./internal/serialize.js\";\nimport { wrapIO, isENOENT } from \"./internal/io-helpers.js\";\n\nexport interface FileSnapshotStoreConfig {\n readonly root?: string;\n}\n\n/** Validate that a runId is safe to use as a path component (no traversal). */\nfunction validateRunId(runId: string): void {\n if (\n runId.length === 0 ||\n runId.includes(\"/\") ||\n runId.includes(\"\\\\\") ||\n runId.includes(\"..\") ||\n runId === \".\" ||\n runId.includes(\"\\0\")\n ) {\n throw new StorageError(\n \"INVALID_RUN_ID\",\n `runId contains invalid path characters`,\n );\n }\n}\n\n/**\n * Create a file-based `SnapshotStore`. Snapshots are written as pretty-printed\n * JSON to `<root>/.sverka/runs/<runId>/snapshot.json`. Zero dependencies,\n * human-debuggable. `load` returns `undefined` for missing files (ENOENT).\n * `delete` is idempotent. Writes are atomic (temp file + rename).\n */\nexport function createFileSnapshotStore(\n config?: FileSnapshotStoreConfig,\n): SnapshotStore {\n const root = config?.root ?? process.cwd();\n\n return {\n async save(snapshot: RunSnapshot): Promise<void> {\n validateRunId(snapshot.runId);\n const dir = join(root, \".sverka\", \"runs\", snapshot.runId);\n const finalPath = join(dir, \"snapshot.json\");\n const tmpPath = join(\n dir,\n `.snapshot.${randomBytes(6).toString(\"hex\")}.tmp`,\n );\n await wrapIO(`save snapshot ${snapshot.runId}`, async () => {\n // Reject symlinked store dirs before mkdir/chmod — a symlink planted\n // at .sverka or runs would redirect writes outside root, and chmod\n // would tighten the unrelated target directory.\n const candidates = await Promise.all(\n [join(root, \".sverka\"), join(root, \".sverka\", \"runs\")].map(\n async (p) => {\n try {\n return (await lstat(p)).isSymbolicLink() ? p : null;\n } catch (err) {\n if (isENOENT(err)) return null;\n throw err;\n }\n },\n ),\n );\n const symlinked = candidates.find((p) => p !== null);\n if (symlinked) {\n throw new StorageError(\n \"STORE_IO_FAILED\",\n `store directory is a symlink: ${symlinked}`,\n );\n }\n await mkdir(dir, { recursive: true, mode: 0o700 });\n // mode only applies at creation — chmod an existing dir too, but\n // never follow a planted symlink: chmod(dir) would otherwise\n // tighten an unrelated target directory.\n if ((await lstat(dir)).isSymbolicLink()) {\n throw new StorageError(\n \"STORE_IO_FAILED\",\n `run directory is a symlink: ${dir}`,\n );\n }\n await chmod(dir, 0o700);\n try {\n await writeFile(tmpPath, serialize(snapshot), {\n encoding: \"utf8\",\n mode: 0o600,\n });\n await rename(tmpPath, finalPath);\n } catch (e) {\n // Don't leave tmp files behind — repeated failures would\n // accumulate under .sverka/runs.\n await unlink(tmpPath).catch(() => {});\n throw e;\n }\n });\n },\n\n async load(runId: string): Promise<RunSnapshot | undefined> {\n validateRunId(runId);\n const filePath = join(root, \".sverka\", \"runs\", runId, \"snapshot.json\");\n let text: string;\n try {\n text = await readFile(filePath, \"utf8\");\n } catch (e) {\n if (isENOENT(e)) return undefined;\n throw new StorageError(\n \"STORE_IO_FAILED\",\n `failed to load snapshot ${runId}`,\n e,\n );\n }\n return deserialize(text, runId);\n },\n\n async delete(runId: string): Promise<void> {\n validateRunId(runId);\n const filePath = join(root, \".sverka\", \"runs\", runId, \"snapshot.json\");\n try {\n await unlink(filePath);\n } catch (e) {\n if (isENOENT(e)) return;\n throw new StorageError(\n \"STORE_IO_FAILED\",\n `failed to delete snapshot ${runId}`,\n e,\n );\n }\n },\n };\n}\n","// SqliteSnapshotStore — SQLite database via node:sqlite (built into Node 24+ and Bun).\n// Spec 31 — SQLite schema.\n\nimport {\n chmodSync,\n closeSync,\n constants,\n fchmodSync,\n lstatSync,\n mkdirSync,\n openSync,\n} from \"node:fs\";\nimport { basename, dirname } from \"node:path\";\nimport { DatabaseSync } from \"node:sqlite\";\nimport type { RunSnapshot, SnapshotStore } from \"@sverka/runtime\";\nimport { StorageError } from \"./errors.js\";\nimport { serialize, deserialize } from \"./internal/serialize.js\";\nimport { wrapIO } from \"./internal/io-helpers.js\";\n\nexport interface SqliteSnapshotStoreConfig {\n readonly path?: string;\n}\n\nconst DEFAULT_PATH = \".sverka/runs.db\";\n\nconst CREATE_TABLE_SQL = `\n CREATE TABLE IF NOT EXISTS snapshots (\n run_id TEXT PRIMARY KEY,\n plan_id TEXT NOT NULL,\n status TEXT NOT NULL,\n suspended_at INTEGER NOT NULL,\n snapshot_json TEXT NOT NULL\n )\n`;\n\n/**\n * Secure the DB file and its app-owned directory before SQLite opens it.\n * Pre-creates the file with owner-only perms (O_NOFOLLOW rejects planted\n * symlinks, fchmod applies perms to the retained descriptor, not the path)\n * and tightens only directories we own — a created leaf or \".sverka\" —\n * never an arbitrary caller dir like \"/tmp\" when path is \"/tmp/runs.db\".\n */\nfunction secureDbPath(path: string): void {\n const dir = dirname(path);\n try {\n const created = mkdirSync(dir, { recursive: true, mode: 0o700 });\n if (dir !== \".\" && (created === dir || basename(dir) === \".sverka\")) {\n if (lstatSync(dir).isSymbolicLink()) {\n throw new StorageError(\n \"STORE_IO_FAILED\",\n `store directory is a symlink: ${dir}`,\n );\n }\n chmodSync(dir, 0o700);\n }\n } catch (e) {\n if (e instanceof StorageError) throw e;\n // Directory may already exist or parent is \".\" — ignore.\n }\n try {\n const fd = openSync(\n path,\n constants.O_CREAT | constants.O_RDWR | (constants.O_NOFOLLOW ?? 0),\n 0o600,\n );\n try {\n fchmodSync(fd, 0o600);\n } finally {\n closeSync(fd);\n }\n } catch (e) {\n throw new StorageError(\n \"STORE_IO_FAILED\",\n `failed to secure sqlite database at ${path}`,\n e,\n );\n }\n}\n\n/**\n * Create a SQLite-backed `SnapshotStore` using `node:sqlite` (built into\n * Node 24+ and Bun, no install, no native build). One row per run keyed by\n * `runId`; snapshot stored as JSON text. Returns `SnapshotStore & { close(): void }`\n * — callers may call `close()` to release the `DatabaseSync` handle.\n */\nexport function createSqliteSnapshotStore(\n config?: SqliteSnapshotStoreConfig,\n): SnapshotStore & { close(): void } {\n const path = config?.path ?? DEFAULT_PATH;\n\n if (path !== \":memory:\") {\n secureDbPath(path);\n }\n\n let db: DatabaseSync;\n try {\n db = new DatabaseSync(path);\n } catch (e) {\n throw new StorageError(\n \"STORE_IO_FAILED\",\n `failed to open sqlite database at ${path}`,\n e,\n );\n }\n try {\n db.exec(CREATE_TABLE_SQL);\n } catch (e) {\n try {\n db.close();\n } catch {\n /* ignore */\n }\n throw new StorageError(\n \"STORE_IO_FAILED\",\n `failed to initialize sqlite schema at ${path}`,\n e,\n );\n }\n\n let saveStmt: ReturnType<DatabaseSync[\"prepare\"]>;\n let loadStmt: ReturnType<DatabaseSync[\"prepare\"]>;\n let deleteStmt: ReturnType<DatabaseSync[\"prepare\"]>;\n try {\n saveStmt = db.prepare(\n \"INSERT OR REPLACE INTO snapshots (run_id, plan_id, status, suspended_at, snapshot_json) VALUES (?, ?, ?, ?, ?)\",\n );\n loadStmt = db.prepare(\n \"SELECT snapshot_json FROM snapshots WHERE run_id = ?\",\n );\n deleteStmt = db.prepare(\"DELETE FROM snapshots WHERE run_id = ?\");\n } catch (e) {\n try {\n db.close();\n } catch {\n /* ignore */\n }\n throw new StorageError(\n \"STORE_IO_FAILED\",\n `failed to prepare sqlite statements at ${path}`,\n e,\n );\n }\n\n return {\n async save(snapshot: RunSnapshot): Promise<void> {\n await wrapIO(`save snapshot ${snapshot.runId}`, async () => {\n saveStmt.run(\n snapshot.runId,\n snapshot.planId,\n snapshot.status,\n snapshot.suspendedAt,\n serialize(snapshot),\n );\n });\n },\n\n async load(runId: string): Promise<RunSnapshot | undefined> {\n let row: { snapshot_json?: string } | undefined;\n try {\n row = loadStmt.get(runId) as { snapshot_json?: string } | undefined;\n } catch (e) {\n throw new StorageError(\n \"STORE_IO_FAILED\",\n `failed to load snapshot ${runId}`,\n e,\n );\n }\n if (row === undefined) return undefined;\n const text = row[\"snapshot_json\"];\n if (typeof text !== \"string\") {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n `snapshot_json is not a string for ${runId}`,\n );\n }\n return deserialize(text, runId);\n },\n\n async delete(runId: string): Promise<void> {\n try {\n deleteStmt.run(runId);\n } catch (e) {\n throw new StorageError(\n \"STORE_IO_FAILED\",\n `failed to delete snapshot ${runId}`,\n e,\n );\n }\n },\n\n close(): void {\n try {\n db.close();\n } catch {\n // Already closed — ignore.\n }\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["delay"],"sources":["../src/errors.ts","../src/internal/serialize.ts","../src/internal/io-helpers.ts","../src/file-store.ts","../src/sqlite-store.ts","../src/internal/tar.ts","../src/remote.ts","../src/tiered-cache.ts"],"sourcesContent":["// StorageError — error class for @sverka/storage.\n// Spec 31 — Error handling.\n\nexport type StorageErrorCode =\n \"STORE_IO_FAILED\" | \"CORRUPT_SNAPSHOT\" | \"INVALID_RUN_ID\";\n\nexport class StorageError extends Error {\n readonly code: StorageErrorCode;\n override readonly cause: unknown;\n\n constructor(code: StorageErrorCode, message: string, cause?: unknown) {\n super(message);\n this.name = \"StorageError\";\n this.code = code;\n if (cause !== undefined) {\n this.cause = cause;\n }\n }\n}\n\n/** Error codes for hub/remote failures. Spec 55. */\nexport type HubErrorCode = \"REMOTE_UNAVAILABLE\" | \"REMOTE_REJECTED\";\n\n/**\n * Hub remote-operation failure. Wraps fetch/network failures\n * (REMOTE_UNAVAILABLE) and non-2xx hub responses (REMOTE_REJECTED).\n * Remote operations degrade to local behaviour — this error is caught at\n * the engine/CLI boundary and surfaces as a warn diagnostic, never a run\n * failure.\n */\nexport class HubError extends Error {\n readonly code: HubErrorCode;\n /** HTTP status for REMOTE_REJECTED, when known. */\n readonly status?: number;\n override readonly cause: unknown;\n\n constructor(\n code: HubErrorCode,\n message: string,\n cause?: unknown,\n status?: number,\n ) {\n super(message);\n this.name = \"HubError\";\n this.code = code;\n if (status !== undefined) {\n this.status = status;\n }\n if (cause !== undefined) {\n this.cause = cause;\n }\n }\n}\n","// serialize / deserialize — JSON round-trip + validation for RunSnapshot.\n// Spec 31 — Serialization.\n\nimport type { RunSnapshot } from \"@sverka/runtime\";\nimport { StorageError } from \"../errors.js\";\n\n/** Serialize a RunSnapshot to pretty-printed JSON (2-space indent). */\nexport function serialize(snapshot: RunSnapshot): string {\n return JSON.stringify(snapshot, null, 2);\n}\n\n/**\n * Deserialize a JSON string into a RunSnapshot, validating required fields.\n * Throws StorageError(CORRUPT_SNAPSHOT) on parse failure or shape mismatch.\n */\nexport function deserialize(text: string, runId: string): RunSnapshot {\n const parsed = parseJson(text);\n if (typeof parsed !== \"object\" || parsed === null) {\n throw new StorageError(\"CORRUPT_SNAPSHOT\", \"snapshot is not an object\");\n }\n const obj = parsed as Record<string, unknown>;\n validateScalarFields(obj, runId);\n validateCompletedSteps(obj);\n validateResumeSchema(obj);\n return parsed as RunSnapshot;\n}\n\nfunction parseJson(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch (e) {\n throw new StorageError(\"CORRUPT_SNAPSHOT\", \"snapshot is not valid JSON\", e);\n }\n}\n\nfunction validateScalarFields(\n obj: Record<string, unknown>,\n runId: string,\n): void {\n validateStringField(obj, \"runId\");\n if (obj[\"runId\"] !== runId) {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n `snapshot runId \"${obj[\"runId\"]}\" does not match requested runId \"${runId}\"`,\n );\n }\n validateStringField(obj, \"planId\");\n validatePlanField(obj);\n if (!Array.isArray(obj[\"completedSteps\"])) {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n \"missing or invalid field: completedSteps\",\n );\n }\n validateStringField(obj, \"suspendedStepId\");\n if (obj[\"status\"] !== \"suspended\") {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n `expected status \"suspended\", got \"${String(obj[\"status\"])}\"`,\n );\n }\n if (typeof obj[\"suspendedAt\"] !== \"number\") {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n \"missing or invalid field: suspendedAt\",\n );\n }\n}\n\nfunction validateStringField(\n obj: Record<string, unknown>,\n field: string,\n): void {\n if (typeof obj[field] !== \"string\") {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n `missing or invalid field: ${field}`,\n );\n }\n}\n\nfunction isInputValue(value: unknown): boolean {\n return (\n typeof value === \"string\" ||\n // Finite only — Infinity/NaN re-serialize to null and don't round-trip.\n (typeof value === \"number\" && Number.isFinite(value)) ||\n typeof value === \"boolean\" ||\n (Array.isArray(value) && value.every((v) => typeof v === \"string\"))\n );\n}\n\nfunction validatePlanField(obj: Record<string, unknown>): void {\n const plan = obj[\"plan\"];\n if (typeof plan !== \"object\" || plan === null || Array.isArray(plan)) {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n \"missing or invalid field: plan\",\n );\n }\n const p = plan as Record<string, unknown>;\n if (p[\"apiVersion\"] !== \"sverka.dev/v1run\") {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n `plan.apiVersion must be \"sverka.dev/v1run\"`,\n );\n }\n for (const field of [\"id\", \"graphId\", \"createdAt\"] as const) {\n if (typeof p[field] !== \"string\") {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n `plan.${field} is missing or not a string`,\n );\n }\n }\n validatePlanEntry(p[\"entry\"]);\n validatePlanInputs(p[\"inputs\"]);\n validatePlanSteps(p[\"steps\"]);\n}\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\nfunction validatePlanEntry(entry: unknown): void {\n if (!isRecord(entry)) {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n \"plan.entry is missing or not an object\",\n );\n }\n if (typeof entry[\"id\"] !== \"string\" || !isRecord(entry[\"trigger\"])) {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n \"plan.entry lacks a string id or a trigger object\",\n );\n }\n}\n\nfunction validatePlanInputs(inputs: unknown): void {\n if (!isRecord(inputs)) {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n \"plan.inputs is missing or not an object\",\n );\n }\n for (const [key, value] of Object.entries(inputs)) {\n if (!isInputValue(value)) {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n `plan.inputs.${key} is not a valid InputValue`,\n );\n }\n }\n}\n\nfunction validatePlanStep(s: unknown, i: number): void {\n if (!isRecord(s) || typeof s[\"id\"] !== \"string\") {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n `plan.steps[${i}] is missing or lacks a string id`,\n );\n }\n if (!isRecord(s[\"runtime\"]) || !hasStepArrays(s)) {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n `plan.steps[${i}] lacks runtime/operations/inputs/outputs/dependencies`,\n );\n }\n}\n\nfunction hasStepArrays(s: Record<string, unknown>): boolean {\n return (\n Array.isArray(s[\"operations\"]) &&\n Array.isArray(s[\"inputs\"]) &&\n Array.isArray(s[\"outputs\"]) &&\n Array.isArray(s[\"dependencies\"])\n );\n}\n\nfunction validatePlanSteps(steps: unknown): void {\n if (!Array.isArray(steps)) {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n \"plan.steps is missing or not an array\",\n );\n }\n steps.forEach((s, i) => validatePlanStep(s, i));\n}\n\nfunction validateCompletedSteps(obj: Record<string, unknown>): void {\n const steps = obj[\"completedSteps\"] as unknown[];\n for (let i = 0; i < steps.length; i++) {\n const entry = steps[i];\n if (typeof entry !== \"object\" || entry === null) {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n `completedSteps[${i}] is not an object`,\n );\n }\n const step = entry as Record<string, unknown>;\n if (typeof step[\"stepId\"] !== \"string\") {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n `completedSteps[${i}].stepId is missing or not a string`,\n );\n }\n if (\n typeof step[\"outputs\"] !== \"object\" ||\n step[\"outputs\"] === null ||\n Array.isArray(step[\"outputs\"])\n ) {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n `completedSteps[${i}].outputs is missing or not an object`,\n );\n }\n for (const [key, value] of Object.entries(\n step[\"outputs\"] as Record<string, unknown>,\n )) {\n if (!isInputValue(value)) {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n `completedSteps[${i}].outputs.${key} is not a valid InputValue`,\n );\n }\n }\n }\n}\n\nfunction validateResumeSchema(obj: Record<string, unknown>): void {\n if (obj[\"resumeSchema\"] === undefined) return;\n if (\n typeof obj[\"resumeSchema\"] !== \"object\" ||\n obj[\"resumeSchema\"] === null ||\n Array.isArray(obj[\"resumeSchema\"])\n ) {\n throw new StorageError(\"CORRUPT_SNAPSHOT\", \"resumeSchema is not an object\");\n }\n const rs = obj[\"resumeSchema\"] as Record<string, unknown>;\n if (rs[\"required\"] !== undefined) {\n if (\n !Array.isArray(rs[\"required\"]) ||\n !rs[\"required\"].every((v) => typeof v === \"string\")\n ) {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n \"resumeSchema.required is not an array of strings\",\n );\n }\n }\n}\n","// Shared I/O helpers for snapshot store implementations.\n// Spec 31 — Error handling.\n\nimport { StorageError } from \"../errors.js\";\n\n/**\n * Check if an error is an ENOENT (file not found) error.\n */\nexport function isENOENT(e: unknown): boolean {\n return (\n typeof e === \"object\" &&\n e !== null &&\n (e as NodeJS.ErrnoException).code === \"ENOENT\"\n );\n}\n\n/**\n * Wrap an async I/O operation, throwing StorageError(STORE_IO_FAILED) on failure.\n */\nexport async function wrapIO<T>(\n label: string,\n fn: () => Promise<T>,\n): Promise<T> {\n try {\n return await fn();\n } catch (e) {\n throw new StorageError(\"STORE_IO_FAILED\", `failed to ${label}`, e);\n }\n}\n","// FileSnapshotStore — JSON file per run at <root>/.sverka/runs/<runId>/snapshot.json.\n// Spec 31 — File layout.\n\nimport {\n chmod,\n lstat,\n mkdir,\n readFile,\n rename,\n unlink,\n writeFile,\n} from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { randomBytes } from \"node:crypto\";\nimport process from \"node:process\";\nimport type { RunSnapshot, SnapshotStore } from \"@sverka/runtime\";\nimport { StorageError } from \"./errors.js\";\nimport { serialize, deserialize } from \"./internal/serialize.js\";\nimport { wrapIO, isENOENT } from \"./internal/io-helpers.js\";\n\nexport interface FileSnapshotStoreConfig {\n readonly root?: string;\n}\n\n/** Validate that a runId is safe to use as a path component (no traversal). */\nfunction validateRunId(runId: string): void {\n if (\n runId.length === 0 ||\n runId.includes(\"/\") ||\n runId.includes(\"\\\\\") ||\n runId.includes(\"..\") ||\n runId === \".\" ||\n runId.includes(\"\\0\")\n ) {\n throw new StorageError(\n \"INVALID_RUN_ID\",\n `runId contains invalid path characters`,\n );\n }\n}\n\n/**\n * Create a file-based `SnapshotStore`. Snapshots are written as pretty-printed\n * JSON to `<root>/.sverka/runs/<runId>/snapshot.json`. Zero dependencies,\n * human-debuggable. `load` returns `undefined` for missing files (ENOENT).\n * `delete` is idempotent. Writes are atomic (temp file + rename).\n */\nexport function createFileSnapshotStore(\n config?: FileSnapshotStoreConfig,\n): SnapshotStore {\n const root = config?.root ?? process.cwd();\n\n return {\n async save(snapshot: RunSnapshot): Promise<void> {\n validateRunId(snapshot.runId);\n const dir = join(root, \".sverka\", \"runs\", snapshot.runId);\n const finalPath = join(dir, \"snapshot.json\");\n const tmpPath = join(\n dir,\n `.snapshot.${randomBytes(6).toString(\"hex\")}.tmp`,\n );\n await wrapIO(`save snapshot ${snapshot.runId}`, async () => {\n // Reject symlinked store dirs before mkdir/chmod — a symlink planted\n // at .sverka or runs would redirect writes outside root, and chmod\n // would tighten the unrelated target directory.\n const candidates = await Promise.all(\n [join(root, \".sverka\"), join(root, \".sverka\", \"runs\")].map(\n async (p) => {\n try {\n return (await lstat(p)).isSymbolicLink() ? p : null;\n } catch (err) {\n if (isENOENT(err)) return null;\n throw err;\n }\n },\n ),\n );\n const symlinked = candidates.find((p) => p !== null);\n if (symlinked) {\n throw new StorageError(\n \"STORE_IO_FAILED\",\n `store directory is a symlink: ${symlinked}`,\n );\n }\n await mkdir(dir, { recursive: true, mode: 0o700 });\n // mode only applies at creation — chmod an existing dir too, but\n // never follow a planted symlink: chmod(dir) would otherwise\n // tighten an unrelated target directory.\n if ((await lstat(dir)).isSymbolicLink()) {\n throw new StorageError(\n \"STORE_IO_FAILED\",\n `run directory is a symlink: ${dir}`,\n );\n }\n await chmod(dir, 0o700);\n try {\n await writeFile(tmpPath, serialize(snapshot), {\n encoding: \"utf8\",\n mode: 0o600,\n });\n await rename(tmpPath, finalPath);\n } catch (e) {\n // Don't leave tmp files behind — repeated failures would\n // accumulate under .sverka/runs.\n await unlink(tmpPath).catch(() => {});\n throw e;\n }\n });\n },\n\n async load(runId: string): Promise<RunSnapshot | undefined> {\n validateRunId(runId);\n const filePath = join(root, \".sverka\", \"runs\", runId, \"snapshot.json\");\n let text: string;\n try {\n text = await readFile(filePath, \"utf8\");\n } catch (e) {\n if (isENOENT(e)) return undefined;\n throw new StorageError(\n \"STORE_IO_FAILED\",\n `failed to load snapshot ${runId}`,\n e,\n );\n }\n return deserialize(text, runId);\n },\n\n async delete(runId: string): Promise<void> {\n validateRunId(runId);\n const filePath = join(root, \".sverka\", \"runs\", runId, \"snapshot.json\");\n try {\n await unlink(filePath);\n } catch (e) {\n if (isENOENT(e)) return;\n throw new StorageError(\n \"STORE_IO_FAILED\",\n `failed to delete snapshot ${runId}`,\n e,\n );\n }\n },\n };\n}\n","// SqliteSnapshotStore — SQLite database via node:sqlite (built into Node 24+ and Bun).\n// Spec 31 — SQLite schema.\n\nimport {\n chmodSync,\n closeSync,\n constants,\n fchmodSync,\n lstatSync,\n mkdirSync,\n openSync,\n} from \"node:fs\";\nimport { basename, dirname } from \"node:path\";\nimport { DatabaseSync } from \"node:sqlite\";\nimport type { RunSnapshot, SnapshotStore } from \"@sverka/runtime\";\nimport { StorageError } from \"./errors.js\";\nimport { serialize, deserialize } from \"./internal/serialize.js\";\nimport { wrapIO } from \"./internal/io-helpers.js\";\n\nexport interface SqliteSnapshotStoreConfig {\n readonly path?: string;\n}\n\nconst DEFAULT_PATH = \".sverka/runs.db\";\n\nconst CREATE_TABLE_SQL = `\n CREATE TABLE IF NOT EXISTS snapshots (\n run_id TEXT PRIMARY KEY,\n plan_id TEXT NOT NULL,\n status TEXT NOT NULL,\n suspended_at INTEGER NOT NULL,\n snapshot_json TEXT NOT NULL\n )\n`;\n\n/**\n * Secure the DB file and its app-owned directory before SQLite opens it.\n * Pre-creates the file with owner-only perms (O_NOFOLLOW rejects planted\n * symlinks, fchmod applies perms to the retained descriptor, not the path)\n * and tightens only directories we own — a created leaf or \".sverka\" —\n * never an arbitrary caller dir like \"/tmp\" when path is \"/tmp/runs.db\".\n */\nfunction secureDbPath(path: string): void {\n const dir = dirname(path);\n try {\n const created = mkdirSync(dir, { recursive: true, mode: 0o700 });\n if (dir !== \".\" && (created === dir || basename(dir) === \".sverka\")) {\n if (lstatSync(dir).isSymbolicLink()) {\n throw new StorageError(\n \"STORE_IO_FAILED\",\n `store directory is a symlink: ${dir}`,\n );\n }\n chmodSync(dir, 0o700);\n }\n } catch (e) {\n if (e instanceof StorageError) throw e;\n // Directory may already exist or parent is \".\" — ignore.\n }\n try {\n const fd = openSync(\n path,\n constants.O_CREAT | constants.O_RDWR | (constants.O_NOFOLLOW ?? 0),\n 0o600,\n );\n try {\n fchmodSync(fd, 0o600);\n } finally {\n closeSync(fd);\n }\n } catch (e) {\n throw new StorageError(\n \"STORE_IO_FAILED\",\n `failed to secure sqlite database at ${path}`,\n e,\n );\n }\n}\n\n/**\n * Create a SQLite-backed `SnapshotStore` using `node:sqlite` (built into\n * Node 24+ and Bun, no install, no native build). One row per run keyed by\n * `runId`; snapshot stored as JSON text. Returns `SnapshotStore & { close(): void }`\n * — callers may call `close()` to release the `DatabaseSync` handle.\n */\nexport function createSqliteSnapshotStore(\n config?: SqliteSnapshotStoreConfig,\n): SnapshotStore & { close(): void } {\n const path = config?.path ?? DEFAULT_PATH;\n\n if (path !== \":memory:\") {\n secureDbPath(path);\n }\n\n let db: DatabaseSync;\n try {\n db = new DatabaseSync(path);\n } catch (e) {\n throw new StorageError(\n \"STORE_IO_FAILED\",\n `failed to open sqlite database at ${path}`,\n e,\n );\n }\n try {\n db.exec(CREATE_TABLE_SQL);\n } catch (e) {\n try {\n db.close();\n } catch {\n /* ignore */\n }\n throw new StorageError(\n \"STORE_IO_FAILED\",\n `failed to initialize sqlite schema at ${path}`,\n e,\n );\n }\n\n let saveStmt: ReturnType<DatabaseSync[\"prepare\"]>;\n let loadStmt: ReturnType<DatabaseSync[\"prepare\"]>;\n let deleteStmt: ReturnType<DatabaseSync[\"prepare\"]>;\n try {\n saveStmt = db.prepare(\n \"INSERT OR REPLACE INTO snapshots (run_id, plan_id, status, suspended_at, snapshot_json) VALUES (?, ?, ?, ?, ?)\",\n );\n loadStmt = db.prepare(\n \"SELECT snapshot_json FROM snapshots WHERE run_id = ?\",\n );\n deleteStmt = db.prepare(\"DELETE FROM snapshots WHERE run_id = ?\");\n } catch (e) {\n try {\n db.close();\n } catch {\n /* ignore */\n }\n throw new StorageError(\n \"STORE_IO_FAILED\",\n `failed to prepare sqlite statements at ${path}`,\n e,\n );\n }\n\n return {\n async save(snapshot: RunSnapshot): Promise<void> {\n await wrapIO(`save snapshot ${snapshot.runId}`, async () => {\n saveStmt.run(\n snapshot.runId,\n snapshot.planId,\n snapshot.status,\n snapshot.suspendedAt,\n serialize(snapshot),\n );\n });\n },\n\n async load(runId: string): Promise<RunSnapshot | undefined> {\n let row: { snapshot_json?: string } | undefined;\n try {\n row = loadStmt.get(runId) as { snapshot_json?: string } | undefined;\n } catch (e) {\n throw new StorageError(\n \"STORE_IO_FAILED\",\n `failed to load snapshot ${runId}`,\n e,\n );\n }\n if (row === undefined) return undefined;\n const text = row[\"snapshot_json\"];\n if (typeof text !== \"string\") {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n `snapshot_json is not a string for ${runId}`,\n );\n }\n return deserialize(text, runId);\n },\n\n async delete(runId: string): Promise<void> {\n try {\n deleteStmt.run(runId);\n } catch (e) {\n throw new StorageError(\n \"STORE_IO_FAILED\",\n `failed to delete snapshot ${runId}`,\n e,\n );\n }\n },\n\n close(): void {\n try {\n db.close();\n } catch {\n // Already closed — ignore.\n }\n },\n };\n}\n","// Minimal POSIX ustar archive pack/unpack for remote cache blobs.\n// Internal — not exported from the package index.\n// Supports regular files, directories, and symlinks; long names split\n// across the ustar prefix field. No pax extensions, no sparse files.\n\nimport { StorageError } from \"../errors.js\";\n\nexport interface TarEntry {\n /** Relative path, forward slashes, no leading/trailing slash. */\n readonly name: string;\n readonly type: \"file\" | \"dir\" | \"symlink\";\n readonly data?: Uint8Array;\n /** Symlink target (type === \"symlink\"). */\n readonly linkname?: string;\n readonly mode?: number;\n /** Epoch seconds. */\n readonly mtime?: number;\n}\n\nconst BLOCK = 512;\nconst NAME_MAX = 100;\nconst PREFIX_MAX = 155;\nconst LINKNAME_MAX = 100;\n\nfunction writeString(\n buf: Uint8Array,\n offset: number,\n length: number,\n value: string,\n): void {\n const encoded = new TextEncoder().encode(value);\n buf.set(encoded.subarray(0, Math.min(encoded.length, length)), offset);\n}\n\nfunction writeOctal(\n buf: Uint8Array,\n offset: number,\n length: number,\n value: number,\n): void {\n // length-1 octal digits, NUL-terminated.\n const text = value.toString(8).padStart(length - 1, \"0\");\n writeString(buf, offset, length - 1, text);\n buf[offset + length - 1] = 0;\n}\n\nfunction readString(buf: Uint8Array, offset: number, length: number): string {\n let end = offset;\n const limit = offset + length;\n while (end < limit && buf[end] !== 0) end++;\n return new TextDecoder().decode(buf.subarray(offset, end));\n}\n\nfunction readOctal(buf: Uint8Array, offset: number, length: number): number {\n const text = readString(buf, offset, length).trim();\n if (text === \"\") return 0;\n return Number.parseInt(text, 8);\n}\n\n/** Split `name` into ustar (prefix, name) fields, or null when unsplittable. */\nfunction splitName(name: string): { prefix: string; name: string } | null {\n const encoded = new TextEncoder();\n if (encoded.encode(name).length <= NAME_MAX) return { prefix: \"\", name };\n // Split at a '/' so prefix ≤155 and name ≤100 bytes.\n let idx = name.lastIndexOf(\"/\", name.length);\n while (idx > 0) {\n const prefix = name.slice(0, idx);\n const rest = name.slice(idx + 1);\n if (\n encoded.encode(prefix).length <= PREFIX_MAX &&\n encoded.encode(rest).length <= NAME_MAX &&\n rest.length > 0\n ) {\n return { prefix, name: rest };\n }\n idx = name.lastIndexOf(\"/\", idx - 1);\n }\n return null;\n}\n\nfunction header(entry: TarEntry): Uint8Array {\n const buf = new Uint8Array(BLOCK);\n const split = splitName(entry.name);\n if (split === null) {\n throw new StorageError(\n \"STORE_IO_FAILED\",\n `tar entry name too long: ${entry.name.slice(0, 80)}…`,\n );\n }\n const size = entry.type === \"file\" ? (entry.data?.length ?? 0) : 0;\n writeString(buf, 0, NAME_MAX, split.name);\n writeOctal(buf, 100, 8, entry.mode ?? (entry.type === \"dir\" ? 0o755 : 0o644));\n writeOctal(buf, 108, 8, 0); // uid\n writeOctal(buf, 116, 8, 0); // gid\n writeOctal(buf, 124, 12, size);\n writeOctal(buf, 136, 12, entry.mtime ?? 0);\n // Checksum field is spaces while summing.\n buf.fill(0x20, 148, 156);\n buf[156] =\n entry.type === \"dir\" ? 0x35 : entry.type === \"symlink\" ? 0x32 : 0x30;\n if (entry.type === \"symlink\") {\n const link = entry.linkname ?? \"\";\n if (new TextEncoder().encode(link).length > LINKNAME_MAX) {\n throw new StorageError(\n \"STORE_IO_FAILED\",\n `symlink target too long: ${entry.name}`,\n );\n }\n writeString(buf, 157, LINKNAME_MAX, link);\n }\n writeString(buf, 257, 8, \"ustar\\0\");\n writeString(buf, 263, 2, \"00\");\n writeString(buf, 265, 32, \"sverka\");\n writeString(buf, 297, 32, \"sverka\");\n writeString(buf, 345, PREFIX_MAX, split.prefix);\n let sum = 0;\n for (const byte of buf) sum += byte;\n // 6 octal digits + NUL + space.\n writeString(buf, 148, 6, sum.toString(8).padStart(6, \"0\"));\n buf[154] = 0;\n buf[155] = 0x20;\n return buf;\n}\n\n/** Pack entries into a ustar archive. */\nexport function packTar(entries: readonly TarEntry[]): Uint8Array {\n const chunks: Uint8Array[] = [];\n for (const entry of entries) {\n chunks.push(header(entry));\n if (entry.type === \"file\" && entry.data !== undefined) {\n chunks.push(entry.data);\n const pad = (BLOCK - (entry.data.length % BLOCK)) % BLOCK;\n if (pad > 0) chunks.push(new Uint8Array(pad));\n }\n }\n chunks.push(new Uint8Array(BLOCK * 2)); // end-of-archive marker\n const out = new Uint8Array(chunks.reduce((n, c) => n + c.length, 0));\n let offset = 0;\n for (const chunk of chunks) {\n out.set(chunk, offset);\n offset += chunk.length;\n }\n return out;\n}\n\nfunction isZeroBlock(buf: Uint8Array, offset: number): boolean {\n for (let i = offset; i < offset + BLOCK; i++) {\n if (buf[i] !== 0) return false;\n }\n return true;\n}\n\nfunction verifyChecksum(buf: Uint8Array, offset: number): boolean {\n let sum = 0;\n for (let i = offset; i < offset + BLOCK; i++) {\n sum += i >= offset + 148 && i < offset + 156 ? 0x20 : buf[i]!;\n }\n return sum === readOctal(buf, offset + 148, 8);\n}\n\ninterface ParsedHeader {\n readonly name: string;\n readonly size: number;\n readonly typeflag: number;\n readonly mtime: number;\n readonly mode?: number;\n readonly linkname?: string;\n}\n\nfunction parseHeader(archive: Uint8Array, offset: number): ParsedHeader {\n const prefix = readString(archive, offset + 345, PREFIX_MAX);\n const namePart = readString(archive, offset, NAME_MAX);\n const name = prefix === \"\" ? namePart : `${prefix}/${namePart}`;\n const size = readOctal(archive, offset + 124, 12);\n // A non-octal size parses to NaN — unchecked, it would silently end\n // the loop (NaN comparisons are false), reporting a truncated archive\n // as a valid restore. Reject anything that isn't a usable length.\n if (!Number.isSafeInteger(size) || size < 0) {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n \"tar entry has an invalid size field\",\n );\n }\n const typeflag = archive[offset + 156]!;\n const mtime = readOctal(archive, offset + 136, 12);\n const rawMode = readOctal(archive, offset + 100, 8);\n const mode = Number.isSafeInteger(rawMode) ? rawMode & 0o777 : undefined;\n const linkname =\n typeflag === 0x32\n ? readString(archive, offset + 157, LINKNAME_MAX)\n : undefined;\n return {\n name,\n size,\n typeflag,\n mtime,\n ...(mode !== undefined ? { mode } : {}),\n ...(linkname !== undefined ? { linkname } : {}),\n };\n}\n\n/** Map a parsed header (+file data) to an entry — unsupported typeflags\n * return undefined (their data blocks are still consumed by the loop). */\nfunction entryFor(h: ParsedHeader, data: Uint8Array): TarEntry | undefined {\n const shared = {\n ...(h.mode !== undefined ? { mode: h.mode } : {}),\n mtime: h.mtime,\n };\n if (h.typeflag === 0x30 || h.typeflag === 0x00) {\n return { name: h.name, type: \"file\", data, ...shared };\n }\n if (h.typeflag === 0x35) {\n return { name: h.name.replace(/\\/$/, \"\"), type: \"dir\", ...shared };\n }\n if (h.typeflag === 0x32) {\n return {\n name: h.name,\n type: \"symlink\",\n ...(h.linkname !== undefined ? { linkname: h.linkname } : {}),\n ...shared,\n };\n }\n return undefined;\n}\n\n/** Unpack a ustar archive into entries. */\nexport function unpackTar(archive: Uint8Array): TarEntry[] {\n const entries: TarEntry[] = [];\n let offset = 0;\n while (offset + BLOCK <= archive.length) {\n if (isZeroBlock(archive, offset)) break;\n if (!verifyChecksum(archive, offset)) {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n \"tar archive checksum mismatch (corrupt blob)\",\n );\n }\n const h = parseHeader(archive, offset);\n offset += BLOCK;\n if (offset + h.size > archive.length) {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n \"tar archive truncated mid-entry\",\n );\n }\n const entry = entryFor(h, archive.slice(offset, offset + h.size));\n if (entry !== undefined) entries.push(entry);\n offset += Math.ceil(h.size / BLOCK) * BLOCK;\n }\n return entries;\n}\n","// Remote hub client — CacheStore + SnapshotStore adapters over the hub's\n// /v1/ HTTP API, plus run-report upload and run listing.\n// Spec 55 — Remote Run Hub.\n//\n// Every remote failure surfaces as a HubError. Callers (the engine for\n// cache, the CLI for uploads) catch it and degrade to local behaviour +\n// a warn diagnostic — the hub is never in a run's critical path.\n\nimport {\n lstat,\n mkdir,\n open,\n readdir,\n readlink,\n symlink,\n type FileHandle,\n} from \"node:fs/promises\";\nimport { constants } from \"node:fs\";\nimport { setTimeout as delay } from \"node:timers/promises\";\nimport { join, dirname, isAbsolute, relative, resolve, sep } from \"node:path\";\nimport { zstdCompressSync, zstdDecompressSync } from \"node:zlib\";\nimport type {\n CacheRestoreRequest,\n CacheRestoreResult,\n CacheStore,\n CacheStoreRequest,\n RunSnapshot,\n SnapshotStore,\n} from \"@sverka/runtime\";\nimport type { Finding } from \"@sverka/verification\";\nimport { HubError } from \"./errors.js\";\nimport { serialize, deserialize } from \"./internal/serialize.js\";\nimport { packTar, unpackTar } from \"./internal/tar.js\";\nimport type { TarEntry } from \"./internal/tar.js\";\n\n/**\n * Connection settings for a hub. `url` is the hub base URL (no /v1\n * suffix); `project` namespaces cache keys, runs, and snapshots\n * (e.g. \"sverka-dev/sverka\").\n */\nexport interface RemoteStoreConfig {\n readonly url: string;\n readonly token: string;\n readonly project: string;\n /** Connect timeout — default 3 000 ms. */\n readonly timeoutMs?: number;\n /** Body transfer timeout — default 30 000 ms. */\n readonly bodyTimeoutMs?: number;\n /** Shared auth breaker: when set, a 401/403 marks it and every later\n * request in the run fails fast as REMOTE_UNAVAILABLE (Spec 55 —\n * \"warn once naming `sverka login`, then behave as hub-down for the\n * rest of the run\"). */\n readonly circuit?: RemoteCircuit;\n}\n\n/** Mutable auth breaker shared by all remote adapters of one run. */\nexport interface RemoteCircuit {\n authFailed: boolean;\n}\n\nexport function createRemoteCircuit(): RemoteCircuit {\n return { authFailed: false };\n}\n\n/** Index row returned by `GET /v1/runs` — the dashboard list view. */\nexport interface HubRunSummary {\n readonly runId: string;\n readonly project: string;\n readonly entry: string;\n readonly status: string;\n readonly startedAt: number | null;\n readonly durationMs: number | null;\n readonly findingCounts: {\n readonly total: number;\n readonly bySeverity: Record<string, number>;\n };\n readonly policyVerdict?: string;\n}\n\n/** Full stored run returned by `GET /v1/runs/{runId}`. */\nexport interface HubRunDetail extends HubRunSummary {\n readonly report: Record<string, unknown>;\n readonly findings: readonly unknown[];\n readonly uploadedAt: number;\n}\n\n/** Metadata the caller attaches to a run upload. */\nexport interface RunUploadMeta {\n /** Entry id the run executed. */\n readonly entry: string;\n /** The run's own id — lets the hub key the row to the local runId. */\n readonly runId?: string;\n}\n\nexport interface RunUploadResult {\n readonly runId: string;\n readonly url: string;\n}\n\nconst DEFAULT_CONNECT_TIMEOUT_MS = 3_000;\nconst DEFAULT_BODY_TIMEOUT_MS = 30_000;\n\ninterface HubResponse {\n readonly status: number;\n readonly headers: Headers;\n readonly body: Uint8Array;\n}\n\nasync function hubRequest(\n config: RemoteStoreConfig,\n method: \"GET\" | \"PUT\" | \"POST\" | \"DELETE\",\n path: string,\n body?: Uint8Array | string,\n contentType = \"application/octet-stream\",\n): Promise<HubResponse> {\n if (config.circuit?.authFailed === true) {\n throw new HubError(\n \"REMOTE_UNAVAILABLE\",\n `hub auth failed earlier this run — treated as hub-down (${method} ${path})`,\n );\n }\n const base = hubBaseUrl(config);\n const connectMs = config.timeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;\n const bodyMs = config.bodyTimeoutMs ?? DEFAULT_BODY_TIMEOUT_MS;\n // One controller for both phases: headers arrive only after an upload\n // has fully streamed, and aborting it is the only way to cancel a slow\n // body read — a detached timer would leave the stream running.\n const ac = new AbortController();\n const init: RequestInit = { method, signal: ac.signal };\n const headers: Record<string, string> = {\n // codeql[js/file-access-to-http]\n authorization: `Bearer ${config.token}`,\n };\n if (body !== undefined) headers[\"content-type\"] = contentType;\n // Packed workspace files flowing into the request body is this\n // adapter's purpose (hub upload). The base URL is user config — this\n // adapter IS the configured remote — scheme-validated http(s) above.\n // codeql[js/file-access-to-http]\n init.headers = headers;\n if (body !== undefined) {\n // codeql[js/file-access-to-http]\n init.body = body;\n }\n const reqUrl = new URL(path.replace(/^\\/+/, \"\"), base);\n let res: Response;\n try {\n // Uploads get the body budget (headers arrive after the blob\n // streams); requests without a body get the connect budget.\n const firstMs = body !== undefined ? bodyMs : connectMs;\n // codeql[js/file-access-to-http]\n res = await withAbortOnTimeout(\n // codeql[js/file-access-to-http]\n fetch(reqUrl, init), // nosemgrep\n firstMs,\n ac,\n `connect/upload timeout after ${firstMs}ms`,\n );\n } catch (e) {\n throw new HubError(\n \"REMOTE_UNAVAILABLE\",\n `hub unreachable (${method} ${path}): ${e instanceof Error ? e.message : String(e)}`,\n e,\n );\n }\n // A 401/403 trips the breaker — later requests skip the network.\n if (\n config.circuit !== undefined &&\n (res.status === 401 || res.status === 403)\n ) {\n config.circuit.authFailed = true;\n }\n // Response headers arrived — the body gets its own, longer budget.\n try {\n const buf = new Uint8Array(\n await withAbortOnTimeout(\n res.arrayBuffer(),\n bodyMs,\n ac,\n `body timeout after ${bodyMs}ms`,\n ),\n );\n return { status: res.status, headers: res.headers, body: buf };\n } catch (e) {\n throw new HubError(\n \"REMOTE_UNAVAILABLE\",\n `hub body read failed (${method} ${path}): ${e instanceof Error ? e.message : String(e)}`,\n e,\n );\n }\n}\n\n/** Race a promise against a timeout that aborts `ac` — the abort (not a\n * detached rejection) is what cancels the in-flight request or stream.\n * The timer itself is cancelled once the promise settles. */\nasync function withAbortOnTimeout<T>(\n promise: Promise<T>,\n ms: number,\n ac: AbortController,\n message: string,\n): Promise<T> {\n const cancel = new AbortController();\n const armed = delay(ms, undefined, { signal: cancel.signal }).then(() => {\n const e = new DOMException(message, \"TimeoutError\");\n ac.abort(e);\n throw e;\n });\n try {\n return await Promise.race([promise, armed]);\n } finally {\n cancel.abort();\n }\n}\n\n/** The configured hub base as a URL — http(s) only. Anything else\n * (file:, ws:, a bare hostname) is a config error, not a request\n * failure, so it fails before the network is touched. */\nfunction hubBaseUrl(config: RemoteStoreConfig): URL {\n let url: URL;\n try {\n url = new URL(config.url);\n } catch (e) {\n throw new HubError(\n \"REMOTE_UNAVAILABLE\",\n `invalid hub url \"${config.url}\"`,\n e instanceof Error ? e : undefined,\n );\n }\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n throw new HubError(\n \"REMOTE_UNAVAILABLE\",\n `hub url must be http(s), got ${url.protocol}`,\n );\n }\n // Keep a base-path prefix (hub mounted under a subpath) but make it\n // end in \"/\" so the route paths below resolve relative to it.\n if (!url.pathname.endsWith(\"/\")) url.pathname += \"/\";\n url.search = \"\";\n url.hash = \"\";\n return url;\n}\n\n/** Parse a 2xx body as JSON, keeping the file's contract that every\n * remote failure surfaces as a HubError (a proxy error page or truncated\n * body would otherwise escape as a raw SyntaxError). */\nfunction parseJsonBody<T>(res: HubResponse, path: string): T {\n try {\n return JSON.parse(new TextDecoder().decode(res.body)) as T;\n } catch (e) {\n throw new HubError(\n \"REMOTE_REJECTED\",\n `hub ${path} returned malformed JSON`,\n e,\n res.status,\n );\n }\n}\n\nfunction rejected(method: string, path: string, res: HubResponse): HubError {\n const detail = new TextDecoder().decode(res.body).slice(0, 200);\n const authHint =\n res.status === 401 || res.status === 403\n ? \" — run `sverka login` to refresh the hub token\"\n : \"\";\n const detailSuffix = detail === \"\" ? \"\" : `: ${detail}`;\n return new HubError(\n \"REMOTE_REJECTED\",\n `hub ${method} ${path} → ${res.status}${authHint}${detailSuffix}`,\n undefined,\n res.status,\n );\n}\n\n// --- Cache blob packing (tar.zst over declared paths) ---\n\nasync function collectTarEntries(\n sourceDir: string,\n relPath: string,\n out: TarEntry[],\n): Promise<void> {\n const abs = join(sourceDir, relPath);\n const name = relPath.replaceAll(\"\\\\\", \"/\");\n // Classify on an fd when one opens: O_NOFOLLOW fails ELOOP on symlinks,\n // O_NONBLOCK keeps fifos from hanging the open, and stat-ing the opened\n // inode (not the path) removes the lstat→open check-then-act window —\n // the packed bytes are provably the classified inode's. When open itself\n // fails (symlink, socket, unreadable) lstat takes over classification;\n // the path ops those kinds need are the same as before.\n let handle: FileHandle | undefined;\n let openError: unknown;\n try {\n handle = await open(\n abs,\n constants.O_RDONLY |\n (constants.O_NOFOLLOW ?? 0) |\n (constants.O_NONBLOCK ?? 0),\n );\n } catch (e) {\n openError = e;\n }\n const st = handle !== undefined ? await handle.stat() : await lstat(abs);\n if (st.isFile()) {\n // open failed on a real file (e.g. EACCES) — surface that error,\n // same as opening it here would have.\n if (handle === undefined) {\n throw openError instanceof Error\n ? openError\n : new Error(`cannot open ${abs}`);\n }\n try {\n out.push({\n name,\n type: \"file\",\n data: new Uint8Array(await handle.readFile()),\n mode: st.mode & 0o777,\n mtime: Math.floor(st.mtimeMs / 1000),\n });\n } finally {\n await handle.close();\n }\n return;\n }\n await handle?.close();\n if (st.isDirectory()) {\n out.push({\n name,\n type: \"dir\",\n mode: st.mode & 0o777,\n mtime: Math.floor(st.mtimeMs / 1000),\n });\n const children = await readdir(abs);\n children.sort();\n for (const child of children) {\n await collectTarEntries(sourceDir, join(relPath, child), out);\n }\n } else if (st.isSymbolicLink()) {\n out.push({\n name,\n type: \"symlink\",\n linkname: await readlink(abs),\n mtime: Math.floor(st.mtimeMs / 1000),\n });\n }\n // Sockets/fifos/devices are not cached — skipped silently.\n}\n\n/** Manifest entry stored inside the tar — same shape as the file cache's\n * manifest.json so both backends describe one entry identically. */\nconst MANIFEST_NAME = \".sverka-cache-manifest.json\";\n\nasync function packCacheBlob(\n sourceDir: string,\n paths: readonly string[],\n key: string,\n): Promise<Uint8Array> {\n const entries: TarEntry[] = [\n {\n name: MANIFEST_NAME,\n type: \"file\",\n data: new TextEncoder().encode(\n JSON.stringify({ key, paths, createdAt: new Date().toISOString() }),\n ),\n },\n ];\n for (const path of paths) {\n await collectTarEntries(sourceDir, path, entries);\n }\n return new Uint8Array(zstdCompressSync(Buffer.from(packTar(entries))));\n}\n\nfunction assertEntryName(name: string): void {\n if (\n name === \"\" ||\n name.startsWith(\"/\") ||\n name.includes(\"\\\\\") ||\n name.split(\"/\").includes(\"..\")\n ) {\n throw new HubError(\n \"REMOTE_REJECTED\",\n `cache blob entry escapes the target dir: ${name}`,\n );\n }\n}\n\n/**\n * Reject the entry's parent path when any *existing* ancestor component\n * under targetDir is a symlink (or a non-directory). Writes must never be\n * redirected outside the cache root — a hostile blob could declare a\n * contained symlink first and then write through it, and targetDir may\n * carry links left by an earlier restore.\n *\n * Missing components are fine: mkdir creates them under the verified\n * prefix. (There is an inherent POSIX TOCTOU window between this check\n * and the write; the leaf O_NOFOLLOW narrows it to ancestor components.)\n */\nasync function assertNoSymlinkAncestors(\n targetDir: string,\n parent: string,\n name: string,\n): Promise<void> {\n const rel = relative(targetDir, parent);\n if (rel === \"..\" || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {\n throw new HubError(\n \"REMOTE_REJECTED\",\n `cache blob entry escapes the target dir: ${name}`,\n );\n }\n if (rel === \"\") return;\n let cur = targetDir;\n for (const seg of rel.split(sep)) {\n cur = join(cur, seg);\n const st = await lstat(cur).catch(() => undefined);\n if (st === undefined) return; // mkdir will create the rest inside\n if (st.isSymbolicLink() || !st.isDirectory()) {\n throw new HubError(\n \"REMOTE_REJECTED\",\n `cache blob entry traverses a symlink or non-directory: ${name}`,\n );\n }\n }\n}\n\ntype ContainedLink = (dest: string, link: string) => boolean;\n\n/** Declared paths the blob actually covers — validated before any write\n * so a partial entry leaves no half-restored tree. */\nfunction coveredPaths(\n tar: readonly TarEntry[],\n wanted: ReadonlySet<string>,\n targetDir: string,\n containedLink: ContainedLink,\n): Set<string> {\n const covered = new Set<string>();\n for (const entry of tar) {\n assertEntryName(entry.name);\n if (\n entry.type === \"symlink\" &&\n !containedLink(join(targetDir, entry.name), entry.linkname ?? \"\")\n ) {\n continue;\n }\n for (const p of wanted) {\n if (entry.name === p || entry.name.startsWith(`${p}/`)) {\n covered.add(p);\n }\n }\n }\n return covered;\n}\n\nasync function extractDirEntry(dest: string, entry: TarEntry): Promise<void> {\n // A leaf symlink would silently redirect later writes — refuse it.\n const st = await lstat(dest).catch(() => undefined);\n if (st?.isSymbolicLink()) {\n throw new HubError(\n \"REMOTE_REJECTED\",\n `cache blob dir entry collides with a symlink: ${entry.name}`,\n );\n }\n await mkdir(dest, {\n recursive: true,\n ...(entry.mode !== undefined ? { mode: entry.mode } : {}),\n });\n}\n\nasync function extractFileEntry(dest: string, entry: TarEntry): Promise<void> {\n await mkdir(dirname(dest), { recursive: true });\n // O_NOFOLLOW refuses a leaf symlink — the write cannot be\n // redirected onto an existing link's target. The recorded mode is\n // applied on create so restored executables keep their +x.\n const handle = await open(\n dest,\n constants.O_WRONLY |\n constants.O_CREAT |\n constants.O_TRUNC |\n (constants.O_NOFOLLOW ?? 0),\n entry.mode ?? 0o644,\n );\n try {\n await handle.writeFile(entry.data ?? new Uint8Array());\n // The mode arg only applies on create — an existing file opened with\n // O_TRUNC keeps its permissions, so chmod restores the cached mode.\n if (entry.mode !== undefined) await handle.chmod(entry.mode);\n } finally {\n await handle.close();\n }\n}\n\nasync function extractEntry(\n targetDir: string,\n wanted: ReadonlySet<string>,\n containedLink: ContainedLink,\n entry: TarEntry,\n): Promise<void> {\n if (entry.name === MANIFEST_NAME) return;\n // Only extract declared paths (or children of declared dirs).\n if (\n ![...wanted].some((p) => entry.name === p || entry.name.startsWith(`${p}/`))\n ) {\n return;\n }\n const dest = join(targetDir, entry.name);\n await assertNoSymlinkAncestors(targetDir, dirname(dest), entry.name);\n if (entry.type === \"dir\") {\n await extractDirEntry(dest, entry);\n } else if (entry.type === \"file\" && entry.data !== undefined) {\n await extractFileEntry(dest, entry);\n } else if (\n entry.type === \"symlink\" &&\n entry.linkname !== undefined &&\n containedLink(dest, entry.linkname)\n ) {\n await mkdir(dirname(dest), { recursive: true });\n await symlink(entry.linkname, dest);\n }\n}\n\n/**\n * Extract a tar.zst blob's declared paths into targetDir. Returns the set\n * of declared paths that were actually restored — a blob that lacks a\n * declared path is a partial entry and must count as a miss, not a hit\n * (otherwise the engine skips rebuilding outputs that never landed).\n */\nasync function extractCacheBlob(\n blob: Uint8Array,\n paths: readonly string[],\n targetDir: string,\n): Promise<ReadonlySet<string>> {\n const tar = unpackTar(new Uint8Array(zstdDecompressSync(Buffer.from(blob))));\n const wanted = new Set(paths.map((p) => p.replaceAll(\"\\\\\", \"/\")));\n const targetRoot = resolve(targetDir);\n\n // Link targets must stay inside the target dir — a blob that plants a\n // symlink to /etc would escape the cache sandbox. resolve() (not join())\n // so absolute targets fail containment rather than being re-rooted.\n const containedLink = (dest: string, link: string): boolean => {\n if (link === \"\" || link.startsWith(\"/\") || link.includes(\"\\\\\")) {\n return false;\n }\n const resolved = resolve(dirname(dest), link);\n return (\n resolved === targetRoot || resolved.startsWith(`${targetRoot}${sep}`)\n );\n };\n\n // Validate names and coverage up front: a blob missing a declared path\n // (or covering it only with an unwritable entry) is a miss — bail before\n // writing so a partial entry leaves no half-restored tree.\n const covered = coveredPaths(tar, wanted, targetDir, containedLink);\n if (covered.size === wanted.size) {\n for (const entry of tar) {\n await extractEntry(targetDir, wanted, containedLink, entry);\n }\n }\n return covered;\n}\n\n/**\n * Remote `CacheStore` (Spec 55): `store` archives the declared paths into\n * a tar.zst blob and `PUT`s it; `restore` `GET`s the primary key and each\n * restoreKey in order — the hub answers exact matches first, then the\n * newest entry whose key has the requested prefix (same semantics as the\n * file cache).\n *\n * Failures throw `HubError`; the engine treats CacheStore failures as\n * non-fatal misses/warns, and `createTieredCacheStore` degrades to local.\n */\nexport function createRemoteCacheStore(config: RemoteStoreConfig): CacheStore {\n const pathFor = (key: string) =>\n `/v1/cache/${encodeURIComponent(config.project)}/${encodeURIComponent(key)}`;\n\n return {\n async restore(\n req: CacheRestoreRequest,\n ): Promise<CacheRestoreResult | undefined> {\n const wanted = new Set(req.paths.map((p) => p.replaceAll(\"\\\\\", \"/\")));\n // File-cache semantics: the primary key is exact-only; restoreKeys\n // opt into prefix matching via ?prefix=1 (the hub must not return a\n // different key's blob to an exact-key GET).\n const attempts: { key: string; prefix: boolean }[] = [\n { key: req.key, prefix: false },\n ...req.restoreKeys.map((key) => ({ key, prefix: true })),\n ];\n for (const { key, prefix } of attempts) {\n const path = `${pathFor(key)}${prefix ? \"?prefix=1\" : \"\"}`;\n const res = await hubRequest(config, \"GET\", path);\n if (res.status === 404) continue;\n if (res.status !== 200) throw rejected(\"GET\", pathFor(key), res);\n const covered = await extractCacheBlob(\n res.body,\n req.paths,\n req.targetDir,\n );\n // A blob missing a declared path is a partial entry — treating it\n // as a hit would skip rebuilding outputs that never landed.\n if (covered.size < wanted.size) continue;\n return { key: res.headers.get(\"x-sverka-cache-key\") ?? key };\n }\n return undefined;\n },\n\n async store(req: CacheStoreRequest): Promise<void> {\n const blob = await packCacheBlob(req.sourceDir, req.paths, req.key);\n const res = await hubRequest(config, \"PUT\", pathFor(req.key), blob);\n if (res.status >= 300) throw rejected(\"PUT\", pathFor(req.key), res);\n },\n };\n}\n\n/**\n * Remote `SnapshotStore` (Spec 55): suspend/resume across machines.\n * Snapshots are the serialized `RunSnapshot` JSON unchanged — the hub\n * treats them as opaque documents.\n */\nexport function createRemoteSnapshotStore(\n config: RemoteStoreConfig,\n): SnapshotStore {\n const pathFor = (runId: string) =>\n `/v1/snapshots/${encodeURIComponent(config.project)}/${encodeURIComponent(runId)}`;\n\n return {\n async save(snapshot: RunSnapshot): Promise<void> {\n const res = await hubRequest(\n config,\n \"PUT\",\n pathFor(snapshot.runId),\n serialize(snapshot),\n \"application/json\",\n );\n if (res.status >= 300) {\n throw rejected(\"PUT\", pathFor(snapshot.runId), res);\n }\n },\n\n async load(runId: string): Promise<RunSnapshot | undefined> {\n const res = await hubRequest(config, \"GET\", pathFor(runId));\n if (res.status === 404) return undefined;\n if (res.status !== 200) throw rejected(\"GET\", pathFor(runId), res);\n return deserialize(new TextDecoder().decode(res.body), runId);\n },\n\n async delete(runId: string): Promise<void> {\n const res = await hubRequest(config, \"DELETE\", pathFor(runId));\n if (res.status === 404) return;\n if (res.status >= 300) {\n throw rejected(\"DELETE\", pathFor(runId), res);\n }\n },\n };\n}\n\n/**\n * Upload a run report + normalized findings to the hub (`POST /v1/runs`).\n * `report` is the `sverka.run/v1` payload written to report.json — sent\n * verbatim. Returns the hub-assigned runId and detail URL.\n */\nexport async function uploadRunReport(\n config: RemoteStoreConfig,\n report: Record<string, unknown>,\n findings: readonly Finding[],\n meta: RunUploadMeta,\n): Promise<RunUploadResult> {\n const body = JSON.stringify({\n project: config.project,\n entry: meta.entry,\n ...(meta.runId !== undefined ? { runId: meta.runId } : {}),\n report,\n findings,\n });\n const res = await hubRequest(\n config,\n \"POST\",\n \"/v1/runs\",\n body,\n \"application/json\",\n );\n if (res.status !== 200 && res.status !== 201) {\n throw rejected(\"POST\", \"/v1/runs\", res);\n }\n const parsed = parseJsonBody<{ runId?: unknown; url?: unknown }>(\n res,\n \"POST /v1/runs\",\n );\n return {\n runId: typeof parsed.runId === \"string\" ? parsed.runId : \"unknown\",\n url: typeof parsed.url === \"string\" ? parsed.url : \"\",\n };\n}\n\n/** List remote run summaries, newest first. */\nexport async function listRuns(\n config: RemoteStoreConfig,\n opts?: { limit?: number; before?: number },\n): Promise<HubRunSummary[]> {\n const params = new URLSearchParams({ project: config.project });\n if (opts?.limit !== undefined) params.set(\"limit\", String(opts.limit));\n if (opts?.before !== undefined) params.set(\"before\", String(opts.before));\n const res = await hubRequest(config, \"GET\", `/v1/runs?${params}`);\n if (res.status !== 200) throw rejected(\"GET\", \"/v1/runs\", res);\n return parseJsonBody<HubRunSummary[]>(res, \"GET /v1/runs\");\n}\n\n/** Fetch one stored run (report + findings). 404 → undefined. */\nexport async function getRun(\n config: RemoteStoreConfig,\n runId: string,\n): Promise<HubRunDetail | undefined> {\n const res = await hubRequest(\n config,\n \"GET\",\n `/v1/runs/${encodeURIComponent(runId)}`,\n );\n if (res.status === 404) return undefined;\n if (res.status !== 200) throw rejected(\"GET\", `/v1/runs/${runId}`, res);\n return parseJsonBody<HubRunDetail>(res, `GET /v1/runs/${runId}`);\n}\n","// TieredCacheStore — local-first, remote-backed CacheStore composition.\n// Spec 55 — \"the hub is a read/write-through cache… every remote failure\n// degrades to local behaviour + a warn diagnostic\".\n\nimport type {\n CacheRestoreRequest,\n CacheRestoreResult,\n CacheStore,\n CacheStoreRequest,\n} from \"@sverka/runtime\";\n\nexport interface TieredCacheStoreConfig {\n readonly local: CacheStore;\n readonly remote: CacheStore;\n /** Warn channel — receives `remote.cache-unreachable`-style messages.\n * Each distinct message is emitted once per store instance. */\n readonly onWarn?: (message: string) => void;\n}\n\nfunction message(e: unknown): string {\n return e instanceof Error ? e.message : String(e);\n}\n\n/**\n * Compose a local and a remote cache store:\n *\n * - `restore`: local hit wins; on a local miss the remote is consulted and\n * a remote hit is written back into the local store (write-through).\n * A remote failure degrades to \"miss\" plus a `remote.cache-unreachable`\n * warn — never a throw.\n * - `store`: writes to both; failures on either side warn and continue.\n *\n * The returned store never throws for remote errors — a `sverka run` must\n * not fail because the hub is down.\n */\nexport function createTieredCacheStore(\n config: TieredCacheStoreConfig,\n): CacheStore {\n const warned = new Set<string>();\n const warn = (msg: string): void => {\n if (warned.has(msg)) return;\n warned.add(msg);\n config.onWarn?.(msg);\n };\n\n return {\n async restore(\n req: CacheRestoreRequest,\n ): Promise<CacheRestoreResult | undefined> {\n let localHit: CacheRestoreResult | undefined;\n try {\n localHit = await config.local.restore(req);\n } catch (e) {\n warn(`local cache restore failed: ${message(e)}`);\n }\n if (localHit !== undefined) return localHit;\n\n let remoteHit: CacheRestoreResult | undefined;\n try {\n remoteHit = await config.remote.restore(req);\n } catch (e) {\n warn(`remote.cache-unreachable: ${message(e)}`);\n return undefined;\n }\n if (remoteHit === undefined) return undefined;\n\n // Write-through: the remote just populated req.targetDir — mirror it\n // into the local store so subsequent runs hit locally.\n try {\n await config.local.store({\n key: remoteHit.key,\n paths: req.paths,\n sourceDir: req.targetDir,\n });\n } catch (e) {\n warn(`local cache write-back failed: ${message(e)}`);\n }\n return remoteHit;\n },\n\n async store(req: CacheStoreRequest): Promise<void> {\n try {\n await config.local.store(req);\n } catch (e) {\n warn(`local cache store failed: ${message(e)}`);\n }\n try {\n await config.remote.store(req);\n } catch (e) {\n warn(`remote.cache-unreachable: ${message(e)}`);\n }\n },\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sverka/storage",
3
- "version": "0.2.24",
3
+ "version": "0.2.26",
4
4
  "description": "Sverka storage — file and SQLite snapshot stores for run persistence",
5
5
  "license": "MIT",
6
6
  "sideEffects": false,
@@ -37,7 +37,8 @@
37
37
  "typecheck": "tsc --noEmit"
38
38
  },
39
39
  "dependencies": {
40
- "@sverka/runtime": "^0.2.24"
40
+ "@sverka/runtime": "^0.2.26",
41
+ "@sverka/verification": "^0.2.26"
41
42
  },
42
43
  "devDependencies": {
43
44
  "tsdown": "^0.23.0",