@workflow/world-local 5.0.0-beta.2 → 5.0.0-beta.3

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/fs.d.ts CHANGED
@@ -1,5 +1,44 @@
1
+ import { WorkflowWorldError } from '@workflow/errors';
1
2
  import type { PaginatedResponse } from '@workflow/world';
2
3
  import { z } from 'zod';
4
+ /**
5
+ * Thrown when a caller-supplied entity ID contains characters that would
6
+ * escape the storage root (path traversal) or otherwise produce an unsafe
7
+ * filename. Extends {@link WorkflowWorldError} so the platform's standard
8
+ * error-to-HTTP mapping handles it alongside other world-layer errors.
9
+ */
10
+ export declare class UnsafeEntityIdError extends WorkflowWorldError {
11
+ constructor(kind: string, value: string);
12
+ static is(value: unknown): value is UnsafeEntityIdError;
13
+ }
14
+ /**
15
+ * Validate that a string is safe to embed in a filesystem path as a single
16
+ * path component. Rejects values that are:
17
+ * - empty
18
+ * - starting with `.` (blocks `.`, `..`, `.locks`, `.tmp`, and other
19
+ * hidden or reserved filenames)
20
+ * - containing `/`, `\`, or a NUL byte
21
+ *
22
+ * This is the primary defense against path-traversal attacks where a
23
+ * request body supplies a `runId` / `stepId` / `correlationId` containing
24
+ * `../` sequences to read or write files outside the storage root.
25
+ * {@link resolveWithinBase} provides a belt-and-suspenders containment
26
+ * check at the point of `path.join` for defense in depth.
27
+ *
28
+ * @param kind - Human-readable label used in the error message (e.g. "runId")
29
+ * @param value - The value to validate; throws {@link UnsafeEntityIdError}
30
+ * if the value is not safe.
31
+ */
32
+ export declare function assertSafeEntityId(kind: string, value: string): void;
33
+ /**
34
+ * Join `basedir` with an entity-relative path and assert the result stays
35
+ * inside `basedir`. Complements {@link assertSafeEntityId}: per-entry-point
36
+ * validation is the primary defense, and this final check catches any path
37
+ * escape that slipped past (e.g. a new call site that forgot to validate,
38
+ * or an unusual character combination on a future filesystem). Throws
39
+ * {@link UnsafeEntityIdError} if the joined path escapes `basedir`.
40
+ */
41
+ export declare function resolveWithinBase(basedir: string, ...segments: string[]): string;
3
42
  /**
4
43
  * Clear the created files cache. Useful for testing or when files are deleted externally.
5
44
  */
@@ -13,8 +52,13 @@ export { ulidToDate } from '@workflow/world';
13
52
  export declare function stripTag(fileId: string): string;
14
53
  /**
15
54
  * Build the file path for an entity, with optional tag embedded in the filename.
16
- * `taggedPath('runs', 'wrun_ABC', 'vitest-0')` → `runs/wrun_ABC.vitest-0.json`
17
- * `taggedPath('runs', 'wrun_ABC')` → `runs/wrun_ABC.json`
55
+ * `taggedPath('/data', 'runs', 'wrun_ABC', 'vitest-0')` → `/data/runs/wrun_ABC.vitest-0.json`
56
+ * `taggedPath('/data', 'runs', 'wrun_ABC')` → `/data/runs/wrun_ABC.json`
57
+ *
58
+ * The `fileId` and `tag` are validated with {@link assertSafeEntityId} and
59
+ * the result is containment-checked with {@link resolveWithinBase} to
60
+ * prevent path-traversal attacks when values are derived from untrusted
61
+ * request input.
18
62
  */
19
63
  export declare function taggedPath(basedir: string, entityDir: string, fileId: string, tag?: string): string;
20
64
  /**
package/dist/fs.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"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"}
package/dist/fs.js CHANGED
@@ -1,9 +1,78 @@
1
1
  import { promises as fs } from 'node:fs';
2
2
  import path from 'node:path';
3
- import { EntityConflictError } from '@workflow/errors';
3
+ import { EntityConflictError, WorkflowWorldError } from '@workflow/errors';
4
4
  import { monotonicFactory } from 'ulid';
5
5
  import { z } from 'zod';
6
6
  const ulid = monotonicFactory(() => Math.random());
7
+ /**
8
+ * Truncate a possibly-untrusted value for inclusion in an error message.
9
+ * Keeps error output actionable for developers in dev mode while limiting
10
+ * the amount of attacker-controlled data reflected back if a `world-local`
11
+ * backend is ever run in a context that surfaces these messages to clients.
12
+ */
13
+ function truncateForError(value) {
14
+ const s = typeof value === 'string' ? value : String(value);
15
+ const MAX = 48;
16
+ return s.length > MAX ? `${s.slice(0, MAX)}…` : s;
17
+ }
18
+ /**
19
+ * Thrown when a caller-supplied entity ID contains characters that would
20
+ * escape the storage root (path traversal) or otherwise produce an unsafe
21
+ * filename. Extends {@link WorkflowWorldError} so the platform's standard
22
+ * error-to-HTTP mapping handles it alongside other world-layer errors.
23
+ */
24
+ export class UnsafeEntityIdError extends WorkflowWorldError {
25
+ constructor(kind, value) {
26
+ super(`Unsafe ${kind} "${truncateForError(value)}": must not be empty, start with ".", or contain path separators or null bytes`);
27
+ this.name = 'UnsafeEntityIdError';
28
+ }
29
+ static is(value) {
30
+ return value instanceof Error && value.name === 'UnsafeEntityIdError';
31
+ }
32
+ }
33
+ /**
34
+ * Validate that a string is safe to embed in a filesystem path as a single
35
+ * path component. Rejects values that are:
36
+ * - empty
37
+ * - starting with `.` (blocks `.`, `..`, `.locks`, `.tmp`, and other
38
+ * hidden or reserved filenames)
39
+ * - containing `/`, `\`, or a NUL byte
40
+ *
41
+ * This is the primary defense against path-traversal attacks where a
42
+ * request body supplies a `runId` / `stepId` / `correlationId` containing
43
+ * `../` sequences to read or write files outside the storage root.
44
+ * {@link resolveWithinBase} provides a belt-and-suspenders containment
45
+ * check at the point of `path.join` for defense in depth.
46
+ *
47
+ * @param kind - Human-readable label used in the error message (e.g. "runId")
48
+ * @param value - The value to validate; throws {@link UnsafeEntityIdError}
49
+ * if the value is not safe.
50
+ */
51
+ export function assertSafeEntityId(kind, value) {
52
+ if (value.length === 0 ||
53
+ value.startsWith('.') ||
54
+ value.includes('/') ||
55
+ value.includes('\\') ||
56
+ value.includes('\0')) {
57
+ throw new UnsafeEntityIdError(kind, value);
58
+ }
59
+ }
60
+ /**
61
+ * Join `basedir` with an entity-relative path and assert the result stays
62
+ * inside `basedir`. Complements {@link assertSafeEntityId}: per-entry-point
63
+ * validation is the primary defense, and this final check catches any path
64
+ * escape that slipped past (e.g. a new call site that forgot to validate,
65
+ * or an unusual character combination on a future filesystem). Throws
66
+ * {@link UnsafeEntityIdError} if the joined path escapes `basedir`.
67
+ */
68
+ export function resolveWithinBase(basedir, ...segments) {
69
+ const resolvedBase = path.resolve(basedir);
70
+ const joined = path.resolve(basedir, ...segments);
71
+ if (joined !== resolvedBase && !joined.startsWith(resolvedBase + path.sep)) {
72
+ throw new UnsafeEntityIdError('path', segments.join('/'));
73
+ }
74
+ return joined;
75
+ }
7
76
  const isWindows = process.platform === 'win32';
8
77
  /**
9
78
  * Execute a filesystem operation with retry logic on Windows.
@@ -60,12 +129,20 @@ export function stripTag(fileId) {
60
129
  }
61
130
  /**
62
131
  * Build the file path for an entity, with optional tag embedded in the filename.
63
- * `taggedPath('runs', 'wrun_ABC', 'vitest-0')` → `runs/wrun_ABC.vitest-0.json`
64
- * `taggedPath('runs', 'wrun_ABC')` → `runs/wrun_ABC.json`
132
+ * `taggedPath('/data', 'runs', 'wrun_ABC', 'vitest-0')` → `/data/runs/wrun_ABC.vitest-0.json`
133
+ * `taggedPath('/data', 'runs', 'wrun_ABC')` → `/data/runs/wrun_ABC.json`
134
+ *
135
+ * The `fileId` and `tag` are validated with {@link assertSafeEntityId} and
136
+ * the result is containment-checked with {@link resolveWithinBase} to
137
+ * prevent path-traversal attacks when values are derived from untrusted
138
+ * request input.
65
139
  */
66
140
  export function taggedPath(basedir, entityDir, fileId, tag) {
141
+ assertSafeEntityId('fileId', fileId);
142
+ if (tag !== undefined)
143
+ assertSafeEntityId('tag', tag);
67
144
  const filename = tag ? `${fileId}.${tag}.json` : `${fileId}.json`;
68
- return path.join(basedir, entityDir, filename);
145
+ return resolveWithinBase(basedir, entityDir, filename);
69
146
  }
70
147
  /**
71
148
  * Read a JSON entity with tagged fallback.
@@ -73,12 +150,15 @@ export function taggedPath(basedir, entityDir, fileId, tag) {
73
150
  * untagged path (so a tagged world can read entities written without a tag).
74
151
  */
75
152
  export async function readJSONWithFallback(basedir, entityDir, fileId, schema, tag) {
153
+ assertSafeEntityId('fileId', fileId);
154
+ if (tag !== undefined)
155
+ assertSafeEntityId('tag', tag);
76
156
  if (tag) {
77
- const result = await readJSON(path.join(basedir, entityDir, `${fileId}.${tag}.json`), schema);
157
+ const result = await readJSON(resolveWithinBase(basedir, entityDir, `${fileId}.${tag}.json`), schema);
78
158
  if (result !== null)
79
159
  return result;
80
160
  }
81
- return readJSON(path.join(basedir, entityDir, `${fileId}.json`), schema);
161
+ return readJSON(resolveWithinBase(basedir, entityDir, `${fileId}.json`), schema);
82
162
  }
83
163
  /**
84
164
  * List all filenames in a directory that have a specific tag.
@@ -268,6 +348,14 @@ function createCursor(timestamp, id) {
268
348
  }
269
349
  export async function paginatedFileSystemQuery(config) {
270
350
  const { directory, schema, filePrefix, filter, sortOrder = 'desc', limit = 20, cursor, getCreatedAt, getId, } = config;
351
+ // Validate filePrefix (typically `${runId}-`) so request-derived prefixes
352
+ // consistently reject unsafe characters. filePrefix is only used below to
353
+ // filter readdir() results by prefix — it doesn't participate in path
354
+ // construction — but keeping the validation rule uniform across the
355
+ // storage layer avoids special cases and catches bad values earlier.
356
+ if (filePrefix !== undefined) {
357
+ assertSafeEntityId('filePrefix', filePrefix);
358
+ }
271
359
  // 1. Get all JSON files in directory
272
360
  const fileIds = await listJSONFiles(directory);
273
361
  // 2. Filter by prefix if provided
package/dist/fs.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"fs.js","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"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","sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { EntityConflictError } from '@workflow/errors';\nimport type { PaginatedResponse } from '@workflow/world';\nimport { monotonicFactory } from 'ulid';\nimport { z } from 'zod';\n\nconst ulid = monotonicFactory(() => Math.random());\n\nconst isWindows = process.platform === 'win32';\n\n/**\n * Execute a filesystem operation with retry logic on Windows.\n * On Windows, file operations can fail with EPERM/EBUSY/EACCES when files\n * are briefly locked by another process or antivirus. This wrapper adds\n * exponential backoff retry logic. On non-Windows platforms, executes directly.\n */\nasync function withWindowsRetry<T>(\n fn: () => Promise<T>,\n maxRetries = 5\n): Promise<T> {\n if (!isWindows) return fn();\n\n const retryableErrors = ['EPERM', 'EBUSY', 'EACCES'];\n const baseDelayMs = 10;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n return await fn();\n } catch (error: any) {\n const isRetryable =\n attempt < maxRetries && retryableErrors.includes(error.code);\n if (!isRetryable) throw error;\n // Exponential backoff with jitter\n const delay = baseDelayMs * 2 ** attempt + Math.random() * baseDelayMs;\n await new Promise((resolve) => setTimeout(resolve, delay));\n }\n }\n // TypeScript: unreachable, but satisfies return type\n throw new Error('Retry loop exited unexpectedly');\n}\n\n// In-memory cache of created files to avoid expensive fs.access() calls\n// This is safe because we only write once per file path (no overwrites without explicit flag)\nconst createdFilesCache = new Set<string>();\n\n/**\n * Clear the created files cache. Useful for testing or when files are deleted externally.\n */\nexport function clearCreatedFilesCache(): void {\n createdFilesCache.clear();\n}\n\nexport { ulidToDate } from '@workflow/world';\n\n/**\n * Regex matching a tag suffix on a fileId (after `.json` has been stripped).\n * E.g., `wrun_ABC.vitest-0` → the `.vitest-0` part.\n * Tags start with a letter and contain alphanumeric chars and hyphens.\n * Entity IDs (ULIDs, step_N, etc.) never contain dots, so the first dot\n * always marks the tag boundary.\n */\nconst TAG_PATTERN = /\\.[a-zA-Z][a-zA-Z0-9-]*$/;\n\n/**\n * Strip a tag suffix from a fileId.\n * `wrun_ABC.vitest-0` → `wrun_ABC`\n * `wrun_ABC` → `wrun_ABC` (no-op if no tag)\n */\nexport function stripTag(fileId: string): string {\n return fileId.replace(TAG_PATTERN, '');\n}\n\n/**\n * Build the file path for an entity, with optional tag embedded in the filename.\n * `taggedPath('runs', 'wrun_ABC', 'vitest-0')` → `runs/wrun_ABC.vitest-0.json`\n * `taggedPath('runs', 'wrun_ABC')` → `runs/wrun_ABC.json`\n */\nexport function taggedPath(\n basedir: string,\n entityDir: string,\n fileId: string,\n tag?: string\n): string {\n const filename = tag ? `${fileId}.${tag}.json` : `${fileId}.json`;\n return path.join(basedir, entityDir, filename);\n}\n\n/**\n * Read a JSON entity with tagged fallback.\n * When a tag is set, tries the tagged path first, then falls back to the\n * untagged path (so a tagged world can read entities written without a tag).\n */\nexport async function readJSONWithFallback<T>(\n basedir: string,\n entityDir: string,\n fileId: string,\n schema: z.ZodType<T>,\n tag?: string\n): Promise<T | null> {\n if (tag) {\n const result = await readJSON(\n path.join(basedir, entityDir, `${fileId}.${tag}.json`),\n schema\n );\n if (result !== null) return result;\n }\n return readJSON(path.join(basedir, entityDir, `${fileId}.json`), schema);\n}\n\n/**\n * List all filenames in a directory that have a specific tag.\n * Returns full filenames (e.g., `wrun_ABC.vitest-0.json`).\n */\nexport async function listTaggedFiles(\n dirPath: string,\n tag: string\n): Promise<string[]> {\n const suffix = `.${tag}.json`;\n try {\n const files = await fs.readdir(dirPath);\n return files.filter((f) => f.endsWith(suffix));\n } catch (error) {\n if ((error as any).code === 'ENOENT') return [];\n throw error;\n }\n}\n\n/**\n * List all filenames in a directory that have a specific tag and extension.\n * Returns full filenames (e.g., `stream-chnk_ABC.vitest-0.bin`).\n */\nexport async function listTaggedFilesByExtension(\n dirPath: string,\n tag: string,\n extension: string\n): Promise<string[]> {\n const suffix = `.${tag}${extension}`;\n try {\n const files = await fs.readdir(dirPath);\n return files.filter((f) => f.endsWith(suffix));\n } catch (error) {\n if ((error as any).code === 'ENOENT') return [];\n throw error;\n }\n}\n\nexport async function ensureDir(dirPath: string): Promise<void> {\n try {\n await fs.mkdir(dirPath, { recursive: true });\n } catch (_error) {\n // Ignore if already exists\n }\n}\n\ninterface WriteOptions {\n overwrite?: boolean;\n}\n\n/**\n * Custom JSON replacer that encodes Uint8Array as base64 strings.\n * Format: { __type: 'Uint8Array', data: '<base64>' }\n */\nexport function jsonReplacer(_key: string, value: unknown): unknown {\n if (value instanceof Uint8Array) {\n return {\n __type: 'Uint8Array',\n data: Buffer.from(value).toString('base64'),\n };\n }\n return value;\n}\n\n/**\n * Custom JSON reviver that decodes base64 strings back to Uint8Array.\n */\nexport function jsonReviver(_key: string, value: unknown): unknown {\n if (\n value !== null &&\n typeof value === 'object' &&\n (value as any).__type === 'Uint8Array' &&\n typeof (value as any).data === 'string'\n ) {\n return new Uint8Array(Buffer.from((value as any).data, 'base64'));\n }\n return value;\n}\n\nexport async function writeJSON(\n filePath: string,\n data: any,\n opts?: WriteOptions\n): Promise<void> {\n return write(filePath, JSON.stringify(data, jsonReplacer, 2), opts);\n}\n\n/**\n * Writes data to a file using atomic write-rename pattern.\n *\n * Note: While this function uses temp files to avoid partial writes,\n * it does not provide protection against concurrent writes from multiple\n * processes. In a multi-writer scenario, the last writer wins.\n * For production use with multiple writers, consider using a proper\n * database or locking mechanism.\n */\nexport async function write(\n filePath: string,\n data: string | Buffer,\n opts?: WriteOptions\n): Promise<void> {\n if (!opts?.overwrite) {\n // Fast path: check in-memory cache first to avoid expensive fs.access() calls\n // This provides significant performance improvement when creating many files\n if (createdFilesCache.has(filePath)) {\n throw new EntityConflictError(\n `File ${filePath} already exists and 'overwrite' is false`\n );\n }\n\n // Slow path: check filesystem for files created before this process started\n try {\n await fs.access(filePath);\n // File exists on disk, add to cache for future checks\n createdFilesCache.add(filePath);\n throw new EntityConflictError(\n `File ${filePath} already exists and 'overwrite' is false`\n );\n } catch (error: any) {\n // If file doesn't exist (ENOENT), continue with write\n if (error.code !== 'ENOENT') {\n throw error;\n }\n }\n }\n\n const tempPath = `${filePath}.tmp.${ulid()}`;\n let tempFileCreated = false;\n try {\n await ensureDir(path.dirname(filePath));\n await fs.writeFile(tempPath, data);\n tempFileCreated = true;\n await withWindowsRetry(() => fs.rename(tempPath, filePath));\n // Track this file in cache so future writes know it exists\n createdFilesCache.add(filePath);\n } catch (error) {\n // Only try to clean up temp file if it was actually created\n if (tempFileCreated) {\n await withWindowsRetry(() => fs.unlink(tempPath), 3).catch(() => {});\n }\n throw error;\n }\n}\n\nexport async function readJSON<T>(\n filePath: string,\n decoder: z.ZodType<T>\n): Promise<T | null> {\n try {\n const content = await fs.readFile(filePath, 'utf-8');\n return decoder.parse(JSON.parse(content, jsonReviver));\n } catch (error) {\n if ((error as any).code === 'ENOENT') return null;\n throw error;\n }\n}\n\nexport async function readBuffer(filePath: string): Promise<Buffer> {\n const content = await fs.readFile(filePath);\n return content;\n}\n\nexport async function deleteJSON(filePath: string): Promise<void> {\n try {\n await fs.unlink(filePath);\n } catch (error) {\n if ((error as any).code !== 'ENOENT') throw error;\n }\n}\n\n/**\n * Atomically create a file using O_CREAT | O_EXCL flags.\n * Returns true if the file was created, false if it already exists.\n * This is atomic at the OS level, safe for concurrent access.\n */\nexport async function writeExclusive(\n filePath: string,\n data: string\n): Promise<boolean> {\n await ensureDir(path.dirname(filePath));\n try {\n await fs.writeFile(filePath, data, { flag: 'wx' });\n return true;\n } catch (error: any) {\n if (error.code === 'EEXIST') {\n return false;\n }\n throw error;\n }\n}\n\nexport async function listJSONFiles(dirPath: string): Promise<string[]> {\n return listFilesByExtension(dirPath, '.json');\n}\n\nexport async function listFilesByExtension(\n dirPath: string,\n extension: string\n): Promise<string[]> {\n try {\n const files = await fs.readdir(dirPath);\n return files\n .filter((f) => f.endsWith(extension))\n .map((f) => f.slice(0, -extension.length));\n } catch (error) {\n if ((error as any).code === 'ENOENT') return [];\n throw error;\n }\n}\n\ninterface PaginatedFileSystemQueryConfig<T> {\n directory: string;\n schema: z.ZodType<T>;\n filePrefix?: string;\n filter?: (item: T) => boolean;\n sortOrder?: 'asc' | 'desc';\n limit?: number;\n cursor?: string;\n getCreatedAt(filename: string): Date | null;\n getId?(item: T): string;\n}\n// Cursor format: \"timestamp|id\" for tie-breaking\ninterface ParsedCursor {\n timestamp: Date;\n id: string | null;\n}\n\nfunction parseCursor(cursor: string | undefined): ParsedCursor | null {\n if (!cursor) return null;\n\n const parts = cursor.split('|');\n return {\n timestamp: new Date(parts[0]),\n id: parts[1] || null,\n };\n}\n\nfunction createCursor(timestamp: Date, id: string | undefined): string {\n return id ? `${timestamp.toISOString()}|${id}` : timestamp.toISOString();\n}\n\nexport async function paginatedFileSystemQuery<T extends { createdAt: Date }>(\n config: PaginatedFileSystemQueryConfig<T>\n): Promise<PaginatedResponse<T>> {\n const {\n directory,\n schema,\n filePrefix,\n filter,\n sortOrder = 'desc',\n limit = 20,\n cursor,\n getCreatedAt,\n getId,\n } = config;\n\n // 1. Get all JSON files in directory\n const fileIds = await listJSONFiles(directory);\n\n // 2. Filter by prefix if provided\n const relevantFileIds = filePrefix\n ? fileIds.filter((fileId) => fileId.startsWith(filePrefix))\n : fileIds;\n\n // 3. ULID Optimization: Filter by cursor using filename timestamps before loading JSON\n const parsedCursor = parseCursor(cursor);\n let candidateFileIds = relevantFileIds;\n\n if (parsedCursor) {\n candidateFileIds = relevantFileIds.filter((fileId) => {\n const filenameDate = getCreatedAt(`${fileId}.json`);\n if (filenameDate) {\n // Use filename timestamp for cursor filtering\n // We need to be careful here: if parsedCursor has an ID (for tie-breaking),\n // we need to include items with the same timestamp for later ID-based filtering.\n // If no ID, we can use strict inequality for optimization.\n const cursorTime = parsedCursor.timestamp.getTime();\n const fileTime = filenameDate.getTime();\n\n if (parsedCursor.id) {\n // Tie-breaking mode: include items at or near cursor timestamp\n return sortOrder === 'desc'\n ? fileTime <= cursorTime\n : fileTime >= cursorTime;\n } else {\n // No tie-breaking: strict inequality\n return sortOrder === 'desc'\n ? fileTime < cursorTime\n : fileTime > cursorTime;\n }\n }\n // Can't extract timestamp from filename (e.g., steps use sequential IDs).\n // Include the file and defer to JSON-based filtering below.\n return true;\n });\n }\n\n // 4. Load files individually and collect valid items\n const validItems: T[] = [];\n\n for (const fileId of candidateFileIds) {\n const filePath = path.join(directory, `${fileId}.json`);\n let item: T | null = null;\n try {\n item = await readJSON(filePath, schema);\n } catch (error: unknown) {\n // We don't expect zod errors to happen, but if the JSON does get malformed,\n // we skip the item. Preferably, we'd have a way to mark items as malformed,\n // so that the UI can display them as such, with richer messaging. In the meantime,\n // we just log a warning and skip the item.\n if (error instanceof z.ZodError) {\n console.warn(\n `Skipping item ${fileId} due to malformed JSON: ${error.message}`\n );\n continue;\n }\n throw error;\n }\n\n if (item) {\n // Apply custom filter early if provided\n if (filter && !filter(item)) continue;\n\n // Double-check cursor filtering with actual createdAt from JSON\n // (in case ULID timestamp differs from stored createdAt)\n if (parsedCursor) {\n const itemTime = item.createdAt.getTime();\n const cursorTime = parsedCursor.timestamp.getTime();\n\n if (sortOrder === 'desc') {\n // For descending order, skip items >= cursor\n if (itemTime > cursorTime) continue;\n // If timestamps are equal, use ID for tie-breaking (skip if ID >= cursorId)\n if (itemTime === cursorTime && parsedCursor.id && getId) {\n const itemId = getId(item);\n if (itemId >= parsedCursor.id) continue;\n }\n } else {\n // For ascending order, skip items <= cursor\n if (itemTime < cursorTime) continue;\n // If timestamps are equal, use ID for tie-breaking (skip if ID <= cursorId)\n if (itemTime === cursorTime && parsedCursor.id && getId) {\n const itemId = getId(item);\n if (itemId <= parsedCursor.id) continue;\n }\n }\n }\n\n validItems.push(item);\n }\n }\n\n // 5. Sort by createdAt (and by ID for tie-breaking if getId is provided)\n validItems.sort((a, b) => {\n const aTime = a.createdAt.getTime();\n const bTime = b.createdAt.getTime();\n const timeComparison = sortOrder === 'asc' ? aTime - bTime : bTime - aTime;\n\n // If timestamps are equal and we have getId, use ID for stable sorting\n if (timeComparison === 0 && getId) {\n const aId = getId(a);\n const bId = getId(b);\n return sortOrder === 'asc'\n ? aId.localeCompare(bId)\n : bId.localeCompare(aId);\n }\n\n return timeComparison;\n });\n\n // 6. Apply pagination\n const hasMore = validItems.length > limit;\n const items = hasMore ? validItems.slice(0, limit) : validItems;\n const nextCursor =\n items.length > 0\n ? createCursor(\n items[items.length - 1].createdAt,\n getId?.(items[items.length - 1])\n )\n : null;\n\n return {\n data: items,\n cursor: nextCursor,\n hasMore,\n };\n}\n"]}
1
+ {"version":3,"file":"fs.js","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"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","sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { EntityConflictError, WorkflowWorldError } from '@workflow/errors';\nimport type { PaginatedResponse } from '@workflow/world';\nimport { monotonicFactory } from 'ulid';\nimport { z } from 'zod';\n\nconst ulid = monotonicFactory(() => Math.random());\n\n/**\n * Truncate a possibly-untrusted value for inclusion in an error message.\n * Keeps error output actionable for developers in dev mode while limiting\n * the amount of attacker-controlled data reflected back if a `world-local`\n * backend is ever run in a context that surfaces these messages to clients.\n */\nfunction truncateForError(value: unknown): string {\n const s = typeof value === 'string' ? value : String(value);\n const MAX = 48;\n return s.length > MAX ? `${s.slice(0, MAX)}…` : s;\n}\n\n/**\n * Thrown when a caller-supplied entity ID contains characters that would\n * escape the storage root (path traversal) or otherwise produce an unsafe\n * filename. Extends {@link WorkflowWorldError} so the platform's standard\n * error-to-HTTP mapping handles it alongside other world-layer errors.\n */\nexport class UnsafeEntityIdError extends WorkflowWorldError {\n constructor(kind: string, value: string) {\n super(\n `Unsafe ${kind} \"${truncateForError(value)}\": must not be empty, start with \".\", or contain path separators or null bytes`\n );\n this.name = 'UnsafeEntityIdError';\n }\n\n static is(value: unknown): value is UnsafeEntityIdError {\n return value instanceof Error && value.name === 'UnsafeEntityIdError';\n }\n}\n\n/**\n * Validate that a string is safe to embed in a filesystem path as a single\n * path component. Rejects values that are:\n * - empty\n * - starting with `.` (blocks `.`, `..`, `.locks`, `.tmp`, and other\n * hidden or reserved filenames)\n * - containing `/`, `\\`, or a NUL byte\n *\n * This is the primary defense against path-traversal attacks where a\n * request body supplies a `runId` / `stepId` / `correlationId` containing\n * `../` sequences to read or write files outside the storage root.\n * {@link resolveWithinBase} provides a belt-and-suspenders containment\n * check at the point of `path.join` for defense in depth.\n *\n * @param kind - Human-readable label used in the error message (e.g. \"runId\")\n * @param value - The value to validate; throws {@link UnsafeEntityIdError}\n * if the value is not safe.\n */\nexport function assertSafeEntityId(kind: string, value: string): void {\n if (\n value.length === 0 ||\n value.startsWith('.') ||\n value.includes('/') ||\n value.includes('\\\\') ||\n value.includes('\\0')\n ) {\n throw new UnsafeEntityIdError(kind, value);\n }\n}\n\n/**\n * Join `basedir` with an entity-relative path and assert the result stays\n * inside `basedir`. Complements {@link assertSafeEntityId}: per-entry-point\n * validation is the primary defense, and this final check catches any path\n * escape that slipped past (e.g. a new call site that forgot to validate,\n * or an unusual character combination on a future filesystem). Throws\n * {@link UnsafeEntityIdError} if the joined path escapes `basedir`.\n */\nexport function resolveWithinBase(\n basedir: string,\n ...segments: string[]\n): string {\n const resolvedBase = path.resolve(basedir);\n const joined = path.resolve(basedir, ...segments);\n if (joined !== resolvedBase && !joined.startsWith(resolvedBase + path.sep)) {\n throw new UnsafeEntityIdError('path', segments.join('/'));\n }\n return joined;\n}\n\nconst isWindows = process.platform === 'win32';\n\n/**\n * Execute a filesystem operation with retry logic on Windows.\n * On Windows, file operations can fail with EPERM/EBUSY/EACCES when files\n * are briefly locked by another process or antivirus. This wrapper adds\n * exponential backoff retry logic. On non-Windows platforms, executes directly.\n */\nasync function withWindowsRetry<T>(\n fn: () => Promise<T>,\n maxRetries = 5\n): Promise<T> {\n if (!isWindows) return fn();\n\n const retryableErrors = ['EPERM', 'EBUSY', 'EACCES'];\n const baseDelayMs = 10;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n return await fn();\n } catch (error: any) {\n const isRetryable =\n attempt < maxRetries && retryableErrors.includes(error.code);\n if (!isRetryable) throw error;\n // Exponential backoff with jitter\n const delay = baseDelayMs * 2 ** attempt + Math.random() * baseDelayMs;\n await new Promise((resolve) => setTimeout(resolve, delay));\n }\n }\n // TypeScript: unreachable, but satisfies return type\n throw new Error('Retry loop exited unexpectedly');\n}\n\n// In-memory cache of created files to avoid expensive fs.access() calls\n// This is safe because we only write once per file path (no overwrites without explicit flag)\nconst createdFilesCache = new Set<string>();\n\n/**\n * Clear the created files cache. Useful for testing or when files are deleted externally.\n */\nexport function clearCreatedFilesCache(): void {\n createdFilesCache.clear();\n}\n\nexport { ulidToDate } from '@workflow/world';\n\n/**\n * Regex matching a tag suffix on a fileId (after `.json` has been stripped).\n * E.g., `wrun_ABC.vitest-0` → the `.vitest-0` part.\n * Tags start with a letter and contain alphanumeric chars and hyphens.\n * Entity IDs (ULIDs, step_N, etc.) never contain dots, so the first dot\n * always marks the tag boundary.\n */\nconst TAG_PATTERN = /\\.[a-zA-Z][a-zA-Z0-9-]*$/;\n\n/**\n * Strip a tag suffix from a fileId.\n * `wrun_ABC.vitest-0` → `wrun_ABC`\n * `wrun_ABC` → `wrun_ABC` (no-op if no tag)\n */\nexport function stripTag(fileId: string): string {\n return fileId.replace(TAG_PATTERN, '');\n}\n\n/**\n * Build the file path for an entity, with optional tag embedded in the filename.\n * `taggedPath('/data', 'runs', 'wrun_ABC', 'vitest-0')` → `/data/runs/wrun_ABC.vitest-0.json`\n * `taggedPath('/data', 'runs', 'wrun_ABC')` → `/data/runs/wrun_ABC.json`\n *\n * The `fileId` and `tag` are validated with {@link assertSafeEntityId} and\n * the result is containment-checked with {@link resolveWithinBase} to\n * prevent path-traversal attacks when values are derived from untrusted\n * request input.\n */\nexport function taggedPath(\n basedir: string,\n entityDir: string,\n fileId: string,\n tag?: string\n): string {\n assertSafeEntityId('fileId', fileId);\n if (tag !== undefined) assertSafeEntityId('tag', tag);\n const filename = tag ? `${fileId}.${tag}.json` : `${fileId}.json`;\n return resolveWithinBase(basedir, entityDir, filename);\n}\n\n/**\n * Read a JSON entity with tagged fallback.\n * When a tag is set, tries the tagged path first, then falls back to the\n * untagged path (so a tagged world can read entities written without a tag).\n */\nexport async function readJSONWithFallback<T>(\n basedir: string,\n entityDir: string,\n fileId: string,\n schema: z.ZodType<T>,\n tag?: string\n): Promise<T | null> {\n assertSafeEntityId('fileId', fileId);\n if (tag !== undefined) assertSafeEntityId('tag', tag);\n if (tag) {\n const result = await readJSON(\n resolveWithinBase(basedir, entityDir, `${fileId}.${tag}.json`),\n schema\n );\n if (result !== null) return result;\n }\n return readJSON(\n resolveWithinBase(basedir, entityDir, `${fileId}.json`),\n schema\n );\n}\n\n/**\n * List all filenames in a directory that have a specific tag.\n * Returns full filenames (e.g., `wrun_ABC.vitest-0.json`).\n */\nexport async function listTaggedFiles(\n dirPath: string,\n tag: string\n): Promise<string[]> {\n const suffix = `.${tag}.json`;\n try {\n const files = await fs.readdir(dirPath);\n return files.filter((f) => f.endsWith(suffix));\n } catch (error) {\n if ((error as any).code === 'ENOENT') return [];\n throw error;\n }\n}\n\n/**\n * List all filenames in a directory that have a specific tag and extension.\n * Returns full filenames (e.g., `stream-chnk_ABC.vitest-0.bin`).\n */\nexport async function listTaggedFilesByExtension(\n dirPath: string,\n tag: string,\n extension: string\n): Promise<string[]> {\n const suffix = `.${tag}${extension}`;\n try {\n const files = await fs.readdir(dirPath);\n return files.filter((f) => f.endsWith(suffix));\n } catch (error) {\n if ((error as any).code === 'ENOENT') return [];\n throw error;\n }\n}\n\nexport async function ensureDir(dirPath: string): Promise<void> {\n try {\n await fs.mkdir(dirPath, { recursive: true });\n } catch (_error) {\n // Ignore if already exists\n }\n}\n\ninterface WriteOptions {\n overwrite?: boolean;\n}\n\n/**\n * Custom JSON replacer that encodes Uint8Array as base64 strings.\n * Format: { __type: 'Uint8Array', data: '<base64>' }\n */\nexport function jsonReplacer(_key: string, value: unknown): unknown {\n if (value instanceof Uint8Array) {\n return {\n __type: 'Uint8Array',\n data: Buffer.from(value).toString('base64'),\n };\n }\n return value;\n}\n\n/**\n * Custom JSON reviver that decodes base64 strings back to Uint8Array.\n */\nexport function jsonReviver(_key: string, value: unknown): unknown {\n if (\n value !== null &&\n typeof value === 'object' &&\n (value as any).__type === 'Uint8Array' &&\n typeof (value as any).data === 'string'\n ) {\n return new Uint8Array(Buffer.from((value as any).data, 'base64'));\n }\n return value;\n}\n\nexport async function writeJSON(\n filePath: string,\n data: any,\n opts?: WriteOptions\n): Promise<void> {\n return write(filePath, JSON.stringify(data, jsonReplacer, 2), opts);\n}\n\n/**\n * Writes data to a file using atomic write-rename pattern.\n *\n * Note: While this function uses temp files to avoid partial writes,\n * it does not provide protection against concurrent writes from multiple\n * processes. In a multi-writer scenario, the last writer wins.\n * For production use with multiple writers, consider using a proper\n * database or locking mechanism.\n */\nexport async function write(\n filePath: string,\n data: string | Buffer,\n opts?: WriteOptions\n): Promise<void> {\n if (!opts?.overwrite) {\n // Fast path: check in-memory cache first to avoid expensive fs.access() calls\n // This provides significant performance improvement when creating many files\n if (createdFilesCache.has(filePath)) {\n throw new EntityConflictError(\n `File ${filePath} already exists and 'overwrite' is false`\n );\n }\n\n // Slow path: check filesystem for files created before this process started\n try {\n await fs.access(filePath);\n // File exists on disk, add to cache for future checks\n createdFilesCache.add(filePath);\n throw new EntityConflictError(\n `File ${filePath} already exists and 'overwrite' is false`\n );\n } catch (error: any) {\n // If file doesn't exist (ENOENT), continue with write\n if (error.code !== 'ENOENT') {\n throw error;\n }\n }\n }\n\n const tempPath = `${filePath}.tmp.${ulid()}`;\n let tempFileCreated = false;\n try {\n await ensureDir(path.dirname(filePath));\n await fs.writeFile(tempPath, data);\n tempFileCreated = true;\n await withWindowsRetry(() => fs.rename(tempPath, filePath));\n // Track this file in cache so future writes know it exists\n createdFilesCache.add(filePath);\n } catch (error) {\n // Only try to clean up temp file if it was actually created\n if (tempFileCreated) {\n await withWindowsRetry(() => fs.unlink(tempPath), 3).catch(() => {});\n }\n throw error;\n }\n}\n\nexport async function readJSON<T>(\n filePath: string,\n decoder: z.ZodType<T>\n): Promise<T | null> {\n try {\n const content = await fs.readFile(filePath, 'utf-8');\n return decoder.parse(JSON.parse(content, jsonReviver));\n } catch (error) {\n if ((error as any).code === 'ENOENT') return null;\n throw error;\n }\n}\n\nexport async function readBuffer(filePath: string): Promise<Buffer> {\n const content = await fs.readFile(filePath);\n return content;\n}\n\nexport async function deleteJSON(filePath: string): Promise<void> {\n try {\n await fs.unlink(filePath);\n } catch (error) {\n if ((error as any).code !== 'ENOENT') throw error;\n }\n}\n\n/**\n * Atomically create a file using O_CREAT | O_EXCL flags.\n * Returns true if the file was created, false if it already exists.\n * This is atomic at the OS level, safe for concurrent access.\n */\nexport async function writeExclusive(\n filePath: string,\n data: string\n): Promise<boolean> {\n await ensureDir(path.dirname(filePath));\n try {\n await fs.writeFile(filePath, data, { flag: 'wx' });\n return true;\n } catch (error: any) {\n if (error.code === 'EEXIST') {\n return false;\n }\n throw error;\n }\n}\n\nexport async function listJSONFiles(dirPath: string): Promise<string[]> {\n return listFilesByExtension(dirPath, '.json');\n}\n\nexport async function listFilesByExtension(\n dirPath: string,\n extension: string\n): Promise<string[]> {\n try {\n const files = await fs.readdir(dirPath);\n return files\n .filter((f) => f.endsWith(extension))\n .map((f) => f.slice(0, -extension.length));\n } catch (error) {\n if ((error as any).code === 'ENOENT') return [];\n throw error;\n }\n}\n\ninterface PaginatedFileSystemQueryConfig<T> {\n directory: string;\n schema: z.ZodType<T>;\n filePrefix?: string;\n filter?: (item: T) => boolean;\n sortOrder?: 'asc' | 'desc';\n limit?: number;\n cursor?: string;\n getCreatedAt(filename: string): Date | null;\n getId?(item: T): string;\n}\n// Cursor format: \"timestamp|id\" for tie-breaking\ninterface ParsedCursor {\n timestamp: Date;\n id: string | null;\n}\n\nfunction parseCursor(cursor: string | undefined): ParsedCursor | null {\n if (!cursor) return null;\n\n const parts = cursor.split('|');\n return {\n timestamp: new Date(parts[0]),\n id: parts[1] || null,\n };\n}\n\nfunction createCursor(timestamp: Date, id: string | undefined): string {\n return id ? `${timestamp.toISOString()}|${id}` : timestamp.toISOString();\n}\n\nexport async function paginatedFileSystemQuery<T extends { createdAt: Date }>(\n config: PaginatedFileSystemQueryConfig<T>\n): Promise<PaginatedResponse<T>> {\n const {\n directory,\n schema,\n filePrefix,\n filter,\n sortOrder = 'desc',\n limit = 20,\n cursor,\n getCreatedAt,\n getId,\n } = config;\n\n // Validate filePrefix (typically `${runId}-`) so request-derived prefixes\n // consistently reject unsafe characters. filePrefix is only used below to\n // filter readdir() results by prefix — it doesn't participate in path\n // construction — but keeping the validation rule uniform across the\n // storage layer avoids special cases and catches bad values earlier.\n if (filePrefix !== undefined) {\n assertSafeEntityId('filePrefix', filePrefix);\n }\n\n // 1. Get all JSON files in directory\n const fileIds = await listJSONFiles(directory);\n\n // 2. Filter by prefix if provided\n const relevantFileIds = filePrefix\n ? fileIds.filter((fileId) => fileId.startsWith(filePrefix))\n : fileIds;\n\n // 3. ULID Optimization: Filter by cursor using filename timestamps before loading JSON\n const parsedCursor = parseCursor(cursor);\n let candidateFileIds = relevantFileIds;\n\n if (parsedCursor) {\n candidateFileIds = relevantFileIds.filter((fileId) => {\n const filenameDate = getCreatedAt(`${fileId}.json`);\n if (filenameDate) {\n // Use filename timestamp for cursor filtering\n // We need to be careful here: if parsedCursor has an ID (for tie-breaking),\n // we need to include items with the same timestamp for later ID-based filtering.\n // If no ID, we can use strict inequality for optimization.\n const cursorTime = parsedCursor.timestamp.getTime();\n const fileTime = filenameDate.getTime();\n\n if (parsedCursor.id) {\n // Tie-breaking mode: include items at or near cursor timestamp\n return sortOrder === 'desc'\n ? fileTime <= cursorTime\n : fileTime >= cursorTime;\n } else {\n // No tie-breaking: strict inequality\n return sortOrder === 'desc'\n ? fileTime < cursorTime\n : fileTime > cursorTime;\n }\n }\n // Can't extract timestamp from filename (e.g., steps use sequential IDs).\n // Include the file and defer to JSON-based filtering below.\n return true;\n });\n }\n\n // 4. Load files individually and collect valid items\n const validItems: T[] = [];\n\n for (const fileId of candidateFileIds) {\n const filePath = path.join(directory, `${fileId}.json`);\n let item: T | null = null;\n try {\n item = await readJSON(filePath, schema);\n } catch (error: unknown) {\n // We don't expect zod errors to happen, but if the JSON does get malformed,\n // we skip the item. Preferably, we'd have a way to mark items as malformed,\n // so that the UI can display them as such, with richer messaging. In the meantime,\n // we just log a warning and skip the item.\n if (error instanceof z.ZodError) {\n console.warn(\n `Skipping item ${fileId} due to malformed JSON: ${error.message}`\n );\n continue;\n }\n throw error;\n }\n\n if (item) {\n // Apply custom filter early if provided\n if (filter && !filter(item)) continue;\n\n // Double-check cursor filtering with actual createdAt from JSON\n // (in case ULID timestamp differs from stored createdAt)\n if (parsedCursor) {\n const itemTime = item.createdAt.getTime();\n const cursorTime = parsedCursor.timestamp.getTime();\n\n if (sortOrder === 'desc') {\n // For descending order, skip items >= cursor\n if (itemTime > cursorTime) continue;\n // If timestamps are equal, use ID for tie-breaking (skip if ID >= cursorId)\n if (itemTime === cursorTime && parsedCursor.id && getId) {\n const itemId = getId(item);\n if (itemId >= parsedCursor.id) continue;\n }\n } else {\n // For ascending order, skip items <= cursor\n if (itemTime < cursorTime) continue;\n // If timestamps are equal, use ID for tie-breaking (skip if ID <= cursorId)\n if (itemTime === cursorTime && parsedCursor.id && getId) {\n const itemId = getId(item);\n if (itemId <= parsedCursor.id) continue;\n }\n }\n }\n\n validItems.push(item);\n }\n }\n\n // 5. Sort by createdAt (and by ID for tie-breaking if getId is provided)\n validItems.sort((a, b) => {\n const aTime = a.createdAt.getTime();\n const bTime = b.createdAt.getTime();\n const timeComparison = sortOrder === 'asc' ? aTime - bTime : bTime - aTime;\n\n // If timestamps are equal and we have getId, use ID for stable sorting\n if (timeComparison === 0 && getId) {\n const aId = getId(a);\n const bId = getId(b);\n return sortOrder === 'asc'\n ? aId.localeCompare(bId)\n : bId.localeCompare(aId);\n }\n\n return timeComparison;\n });\n\n // 6. Apply pagination\n const hasMore = validItems.length > limit;\n const items = hasMore ? validItems.slice(0, limit) : validItems;\n const nextCursor =\n items.length > 0\n ? createCursor(\n items[items.length - 1].createdAt,\n getId?.(items[items.length - 1])\n )\n : null;\n\n return {\n data: items,\n cursor: nextCursor,\n hasMore,\n };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"events-storage.d.ts","sourceRoot":"","sources":["../../src/storage/events-storage.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAMV,OAAO,EAGR,MAAM,iBAAiB,CAAC;AAiDzB;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,EACf,GAAG,CAAC,EAAE,MAAM,GACX,OAAO,CAAC,QAAQ,CAAC,CAi9BnB"}
1
+ {"version":3,"file":"events-storage.d.ts","sourceRoot":"","sources":["../../src/storage/events-storage.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAMV,OAAO,EAGR,MAAM,iBAAiB,CAAC;AAmDzB;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,EACf,GAAG,CAAC,EAAE,MAAM,GACX,OAAO,CAAC,QAAQ,CAAC,CA8+BnB"}
@@ -3,7 +3,7 @@ import path from 'node:path';
3
3
  import { EntityConflictError, HookNotFoundError, RunExpiredError, RunNotSupportedError, TooEarlyError, WorkflowWorldError, } from '@workflow/errors';
4
4
  import { EventSchema, HookSchema, isLegacySpecVersion, requiresNewerWorld, SPEC_VERSION_CURRENT, StepSchema, validateUlidTimestamp, WaitSchema, WorkflowRunSchema, } from '@workflow/world';
5
5
  import { DEFAULT_RESOLVE_DATA_OPTION } from '../config.js';
6
- import { deleteJSON, jsonReplacer, listJSONFiles, paginatedFileSystemQuery, readJSONWithFallback, taggedPath, writeExclusive, writeJSON, } from '../fs.js';
6
+ import { assertSafeEntityId, deleteJSON, jsonReplacer, listJSONFiles, paginatedFileSystemQuery, readJSONWithFallback, resolveWithinBase, taggedPath, writeExclusive, writeJSON, } from '../fs.js';
7
7
  import { stripEventDataRefs } from './filters.js';
8
8
  import { getObjectCreatedAt, hashToken, monotonicUlid } from './helpers.js';
9
9
  import { deleteAllHooksForRun } from './hooks-storage.js';
@@ -33,6 +33,18 @@ export function createEventsStorage(basedir, tag) {
33
33
  async create(runId, data, params) {
34
34
  const eventId = `evnt_${monotonicUlid()}`;
35
35
  const now = new Date();
36
+ // Validate request-supplied IDs before they're concatenated into
37
+ // filesystem paths. This is the primary defense against path traversal
38
+ // attacks where a client supplies runId / correlationId values like
39
+ // "../../../package" to read or write files outside the storage root.
40
+ if (runId != null && runId !== '') {
41
+ assertSafeEntityId('runId', runId);
42
+ }
43
+ if ('correlationId' in data &&
44
+ typeof data.correlationId === 'string' &&
45
+ data.correlationId.length > 0) {
46
+ assertSafeEntityId('correlationId', data.correlationId);
47
+ }
36
48
  // For run_created events, use client-provided runId or generate one server-side
37
49
  let effectiveRunId;
38
50
  if (data.eventType === 'run_created' && (!runId || runId === '')) {
@@ -457,7 +469,7 @@ export function createEventsStorage(basedir, tag) {
457
469
  const lockName = tag
458
470
  ? `${stepCompositeKey}.terminal.${tag}`
459
471
  : `${stepCompositeKey}.terminal`;
460
- const terminalLockPath = path.join(basedir, '.locks', 'steps', lockName);
472
+ const terminalLockPath = resolveWithinBase(basedir, '.locks', 'steps', lockName);
461
473
  const claimed = await writeExclusive(terminalLockPath, '');
462
474
  if (!claimed) {
463
475
  throw new EntityConflictError('Cannot modify step in terminal state');
@@ -482,7 +494,7 @@ export function createEventsStorage(basedir, tag) {
482
494
  const lockName = tag
483
495
  ? `${stepCompositeKey}.terminal.${tag}`
484
496
  : `${stepCompositeKey}.terminal`;
485
- const terminalLockPath = path.join(basedir, '.locks', 'steps', lockName);
497
+ const terminalLockPath = resolveWithinBase(basedir, '.locks', 'steps', lockName);
486
498
  const claimed = await writeExclusive(terminalLockPath, '');
487
499
  if (!claimed) {
488
500
  throw new EntityConflictError('Cannot modify step in terminal state');
@@ -586,7 +598,7 @@ export function createEventsStorage(basedir, tag) {
586
598
  const hookLockName = tag
587
599
  ? `${data.correlationId}.disposed.${tag}`
588
600
  : `${data.correlationId}.disposed`;
589
- const lockPath = path.join(basedir, '.locks', 'hooks', hookLockName);
601
+ const lockPath = resolveWithinBase(basedir, '.locks', 'hooks', hookLockName);
590
602
  const claimed = await writeExclusive(lockPath, '');
591
603
  if (!claimed) {
592
604
  throw new EntityConflictError(`Hook "${data.correlationId}" already disposed`);
@@ -629,7 +641,7 @@ export function createEventsStorage(basedir, tag) {
629
641
  const waitLockName = tag
630
642
  ? `${waitCompositeKey}.completed.${tag}`
631
643
  : `${waitCompositeKey}.completed`;
632
- const lockPath = path.join(basedir, '.locks', 'waits', waitLockName);
644
+ const lockPath = resolveWithinBase(basedir, '.locks', 'waits', waitLockName);
633
645
  const claimed = await writeExclusive(lockPath, '');
634
646
  if (!claimed) {
635
647
  throw new EntityConflictError(`Wait "${data.correlationId}" already completed`);
@@ -682,6 +694,8 @@ export function createEventsStorage(basedir, tag) {
682
694
  };
683
695
  },
684
696
  async get(runId, eventId, params) {
697
+ assertSafeEntityId('runId', runId);
698
+ assertSafeEntityId('eventId', eventId);
685
699
  const compositeKey = `${runId}-${eventId}`;
686
700
  const event = await readJSONWithFallback(basedir, 'events', compositeKey, EventSchema, tag);
687
701
  if (!event) {
@@ -692,6 +706,7 @@ export function createEventsStorage(basedir, tag) {
692
706
  },
693
707
  async list(params) {
694
708
  const { runId } = params;
709
+ assertSafeEntityId('runId', runId);
695
710
  const resolveData = params.resolveData ?? DEFAULT_RESOLVE_DATA_OPTION;
696
711
  const result = await paginatedFileSystemQuery({
697
712
  directory: path.join(basedir, 'events'),
@@ -716,6 +731,7 @@ export function createEventsStorage(basedir, tag) {
716
731
  },
717
732
  async listByCorrelationId(params) {
718
733
  const correlationId = params.correlationId;
734
+ assertSafeEntityId('correlationId', correlationId);
719
735
  const resolveData = params.resolveData ?? DEFAULT_RESOLVE_DATA_OPTION;
720
736
  const result = await paginatedFileSystemQuery({
721
737
  directory: path.join(basedir, 'events'),