@sverka/storage 0.1.31 → 0.1.33

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/file-store.ts","../src/sqlite-store.ts","../src/errors.ts"],"mappings":";;UAYiB;WACN;;;;;;;;wBA0BK,wBACd,SAAS,0BACR;;;UC9Bc;WACN;;;;;;;;wBAqBK,0BACd,SAAS,4BACR;EAAkB;;;;KChCT;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"],"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"}
package/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
- import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
2
- import { dirname, join } from "node:path";
1
+ import { chmod, lstat, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
2
+ import { basename, dirname, join } from "node:path";
3
3
  import { randomBytes } from "node:crypto";
4
4
  import process from "node:process";
5
- import { mkdirSync } from "node:fs";
5
+ import { chmodSync, closeSync, constants, fchmodSync, lstatSync, mkdirSync, openSync } from "node:fs";
6
6
  import { DatabaseSync } from "node:sqlite";
7
7
  //#region src/errors.ts
8
8
  var StorageError = class extends Error {
@@ -54,8 +54,44 @@ function validateScalarFields(obj, runId) {
54
54
  function validateStringField(obj, field) {
55
55
  if (typeof obj[field] !== "string") throw new StorageError("CORRUPT_SNAPSHOT", `missing or invalid field: ${field}`);
56
56
  }
57
+ function isInputValue(value) {
58
+ return typeof value === "string" || typeof value === "number" && Number.isFinite(value) || typeof value === "boolean" || Array.isArray(value) && value.every((v) => typeof v === "string");
59
+ }
57
60
  function validatePlanField(obj) {
58
- if (typeof obj["plan"] !== "object" || obj["plan"] === null || Array.isArray(obj["plan"])) throw new StorageError("CORRUPT_SNAPSHOT", "missing or invalid field: plan");
61
+ const plan = obj["plan"];
62
+ if (typeof plan !== "object" || plan === null || Array.isArray(plan)) throw new StorageError("CORRUPT_SNAPSHOT", "missing or invalid field: plan");
63
+ const p = plan;
64
+ if (p["apiVersion"] !== "sverka.dev/v1run") throw new StorageError("CORRUPT_SNAPSHOT", `plan.apiVersion must be "sverka.dev/v1run"`);
65
+ for (const field of [
66
+ "id",
67
+ "graphId",
68
+ "createdAt"
69
+ ]) if (typeof p[field] !== "string") throw new StorageError("CORRUPT_SNAPSHOT", `plan.${field} is missing or not a string`);
70
+ validatePlanEntry(p["entry"]);
71
+ validatePlanInputs(p["inputs"]);
72
+ validatePlanSteps(p["steps"]);
73
+ }
74
+ function isRecord(v) {
75
+ return typeof v === "object" && v !== null && !Array.isArray(v);
76
+ }
77
+ function validatePlanEntry(entry) {
78
+ if (!isRecord(entry)) throw new StorageError("CORRUPT_SNAPSHOT", "plan.entry is missing or not an object");
79
+ if (typeof entry["id"] !== "string" || !isRecord(entry["trigger"])) throw new StorageError("CORRUPT_SNAPSHOT", "plan.entry lacks a string id or a trigger object");
80
+ }
81
+ function validatePlanInputs(inputs) {
82
+ if (!isRecord(inputs)) throw new StorageError("CORRUPT_SNAPSHOT", "plan.inputs is missing or not an object");
83
+ for (const [key, value] of Object.entries(inputs)) if (!isInputValue(value)) throw new StorageError("CORRUPT_SNAPSHOT", `plan.inputs.${key} is not a valid InputValue`);
84
+ }
85
+ function validatePlanStep(s, i) {
86
+ if (!isRecord(s) || typeof s["id"] !== "string") throw new StorageError("CORRUPT_SNAPSHOT", `plan.steps[${i}] is missing or lacks a string id`);
87
+ if (!isRecord(s["runtime"]) || !hasStepArrays(s)) throw new StorageError("CORRUPT_SNAPSHOT", `plan.steps[${i}] lacks runtime/operations/inputs/outputs/dependencies`);
88
+ }
89
+ function hasStepArrays(s) {
90
+ return Array.isArray(s["operations"]) && Array.isArray(s["inputs"]) && Array.isArray(s["outputs"]) && Array.isArray(s["dependencies"]);
91
+ }
92
+ function validatePlanSteps(steps) {
93
+ if (!Array.isArray(steps)) throw new StorageError("CORRUPT_SNAPSHOT", "plan.steps is missing or not an array");
94
+ steps.forEach((s, i) => validatePlanStep(s, i));
59
95
  }
60
96
  function validateCompletedSteps(obj) {
61
97
  const steps = obj["completedSteps"];
@@ -65,11 +101,12 @@ function validateCompletedSteps(obj) {
65
101
  const step = entry;
66
102
  if (typeof step["stepId"] !== "string") throw new StorageError("CORRUPT_SNAPSHOT", `completedSteps[${i}].stepId is missing or not a string`);
67
103
  if (typeof step["outputs"] !== "object" || step["outputs"] === null || Array.isArray(step["outputs"])) throw new StorageError("CORRUPT_SNAPSHOT", `completedSteps[${i}].outputs is missing or not an object`);
104
+ for (const [key, value] of Object.entries(step["outputs"])) if (!isInputValue(value)) throw new StorageError("CORRUPT_SNAPSHOT", `completedSteps[${i}].outputs.${key} is not a valid InputValue`);
68
105
  }
69
106
  }
70
107
  function validateResumeSchema(obj) {
71
108
  if (obj["resumeSchema"] === void 0) return;
72
- if (typeof obj["resumeSchema"] !== "object" || obj["resumeSchema"] === null) throw new StorageError("CORRUPT_SNAPSHOT", "resumeSchema is not an object");
109
+ if (typeof obj["resumeSchema"] !== "object" || obj["resumeSchema"] === null || Array.isArray(obj["resumeSchema"])) throw new StorageError("CORRUPT_SNAPSHOT", "resumeSchema is not an object");
73
110
  const rs = obj["resumeSchema"];
74
111
  if (rs["required"] !== void 0) {
75
112
  if (!Array.isArray(rs["required"]) || !rs["required"].every((v) => typeof v === "string")) throw new StorageError("CORRUPT_SNAPSHOT", "resumeSchema.required is not an array of strings");
@@ -81,7 +118,7 @@ function validateResumeSchema(obj) {
81
118
  * Check if an error is an ENOENT (file not found) error.
82
119
  */
83
120
  function isENOENT(e) {
84
- return e.code === "ENOENT";
121
+ return typeof e === "object" && e !== null && e.code === "ENOENT";
85
122
  }
86
123
  /**
87
124
  * Wrap an async I/O operation, throwing StorageError(STORE_IO_FAILED) on failure.
@@ -114,9 +151,31 @@ function createFileSnapshotStore(config) {
114
151
  const finalPath = join(dir, "snapshot.json");
115
152
  const tmpPath = join(dir, `.snapshot.${randomBytes(6).toString("hex")}.tmp`);
116
153
  await wrapIO(`save snapshot ${snapshot.runId}`, async () => {
117
- await mkdir(dir, { recursive: true });
118
- await writeFile(tmpPath, serialize(snapshot), "utf8");
119
- await rename(tmpPath, finalPath);
154
+ const symlinked = (await Promise.all([join(root, ".sverka"), join(root, ".sverka", "runs")].map(async (p) => {
155
+ try {
156
+ return (await lstat(p)).isSymbolicLink() ? p : null;
157
+ } catch (err) {
158
+ if (isENOENT(err)) return null;
159
+ throw err;
160
+ }
161
+ }))).find((p) => p !== null);
162
+ if (symlinked) throw new StorageError("STORE_IO_FAILED", `store directory is a symlink: ${symlinked}`);
163
+ await mkdir(dir, {
164
+ recursive: true,
165
+ mode: 448
166
+ });
167
+ if ((await lstat(dir)).isSymbolicLink()) throw new StorageError("STORE_IO_FAILED", `run directory is a symlink: ${dir}`);
168
+ await chmod(dir, 448);
169
+ try {
170
+ await writeFile(tmpPath, serialize(snapshot), {
171
+ encoding: "utf8",
172
+ mode: 384
173
+ });
174
+ await rename(tmpPath, finalPath);
175
+ } catch (e) {
176
+ await unlink(tmpPath).catch(() => {});
177
+ throw e;
178
+ }
120
179
  });
121
180
  },
122
181
  async load(runId) {
@@ -156,6 +215,38 @@ const CREATE_TABLE_SQL = `
156
215
  )
157
216
  `;
158
217
  /**
218
+ * Secure the DB file and its app-owned directory before SQLite opens it.
219
+ * Pre-creates the file with owner-only perms (O_NOFOLLOW rejects planted
220
+ * symlinks, fchmod applies perms to the retained descriptor, not the path)
221
+ * and tightens only directories we own — a created leaf or ".sverka" —
222
+ * never an arbitrary caller dir like "/tmp" when path is "/tmp/runs.db".
223
+ */
224
+ function secureDbPath(path) {
225
+ const dir = dirname(path);
226
+ try {
227
+ const created = mkdirSync(dir, {
228
+ recursive: true,
229
+ mode: 448
230
+ });
231
+ if (dir !== "." && (created === dir || basename(dir) === ".sverka")) {
232
+ if (lstatSync(dir).isSymbolicLink()) throw new StorageError("STORE_IO_FAILED", `store directory is a symlink: ${dir}`);
233
+ chmodSync(dir, 448);
234
+ }
235
+ } catch (e) {
236
+ if (e instanceof StorageError) throw e;
237
+ }
238
+ try {
239
+ const fd = openSync(path, constants.O_CREAT | constants.O_RDWR | (constants.O_NOFOLLOW ?? 0), 384);
240
+ try {
241
+ fchmodSync(fd, 384);
242
+ } finally {
243
+ closeSync(fd);
244
+ }
245
+ } catch (e) {
246
+ throw new StorageError("STORE_IO_FAILED", `failed to secure sqlite database at ${path}`, e);
247
+ }
248
+ }
249
+ /**
159
250
  * Create a SQLite-backed `SnapshotStore` using `node:sqlite` (built into
160
251
  * Node 24+ and Bun, no install, no native build). One row per run keyed by
161
252
  * `runId`; snapshot stored as JSON text. Returns `SnapshotStore & { close(): void }`
@@ -163,9 +254,7 @@ const CREATE_TABLE_SQL = `
163
254
  */
164
255
  function createSqliteSnapshotStore(config) {
165
256
  const path = config?.path ?? DEFAULT_PATH;
166
- if (path !== ":memory:") try {
167
- mkdirSync(dirname(path), { recursive: true });
168
- } catch {}
257
+ if (path !== ":memory:") secureDbPath(path);
169
258
  let db;
170
259
  try {
171
260
  db = new DatabaseSync(path);
@@ -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 validatePlanField(obj: Record<string, unknown>): void {\n if (\n typeof obj[\"plan\"] !== \"object\" ||\n obj[\"plan\"] === null ||\n Array.isArray(obj[\"plan\"])\n ) {\n throw new StorageError(\n \"CORRUPT_SNAPSHOT\",\n \"missing or invalid field: plan\",\n );\n }\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 }\n}\n\nfunction validateResumeSchema(obj: Record<string, unknown>): void {\n if (obj[\"resumeSchema\"] === undefined) return;\n if (typeof obj[\"resumeSchema\"] !== \"object\" || obj[\"resumeSchema\"] === null) {\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 (e as NodeJS.ErrnoException).code === \"ENOENT\";\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 { mkdir, readFile, rename, unlink, writeFile } 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 await mkdir(dir, { recursive: true });\n await writeFile(tmpPath, serialize(snapshot), \"utf8\");\n await rename(tmpPath, finalPath);\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 { mkdirSync } from \"node:fs\";\nimport { 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 * 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 try {\n mkdirSync(dirname(path), { recursive: true });\n } catch {\n // Directory may already exist or parent is \".\" — ignore.\n }\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":[],"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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sverka/storage",
3
- "version": "0.1.31",
3
+ "version": "0.1.33",
4
4
  "description": "Sverka storage — file and SQLite snapshot stores for run persistence",
5
5
  "license": "MIT",
6
6
  "sideEffects": false,
@@ -37,7 +37,7 @@
37
37
  "typecheck": "tsc --noEmit"
38
38
  },
39
39
  "dependencies": {
40
- "@sverka/runtime": "^0.1.31"
40
+ "@sverka/runtime": "^0.1.33"
41
41
  },
42
42
  "devDependencies": {
43
43
  "tsdown": "^0.23.0",