resumable-workflow 1.4.1 → 1.4.2

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
@@ -70,6 +70,9 @@ declare class FileStorage implements StorageProvider {
70
70
  private ensureDir;
71
71
  private validateRunId;
72
72
  private getFilePath;
73
+ private isValidRunFileName;
74
+ private isValidWorkflowStatus;
75
+ private isValidRunStateShape;
73
76
  saveRun(state: WorkflowRunState<unknown, Record<string, unknown>>): Promise<void>;
74
77
  getRun(runId: string): Promise<WorkflowRunState<unknown, Record<string, unknown>> | null>;
75
78
  listIncompleteRuns(workflowId: string): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]>;
package/dist/index.d.ts CHANGED
@@ -70,6 +70,9 @@ declare class FileStorage implements StorageProvider {
70
70
  private ensureDir;
71
71
  private validateRunId;
72
72
  private getFilePath;
73
+ private isValidRunFileName;
74
+ private isValidWorkflowStatus;
75
+ private isValidRunStateShape;
73
76
  saveRun(state: WorkflowRunState<unknown, Record<string, unknown>>): Promise<void>;
74
77
  getRun(runId: string): Promise<WorkflowRunState<unknown, Record<string, unknown>> | null>;
75
78
  listIncompleteRuns(workflowId: string): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]>;
package/dist/index.js CHANGED
@@ -42,6 +42,7 @@ var import_node_crypto = require("crypto");
42
42
  // src/storage/file-storage.ts
43
43
  var import_promises = __toESM(require("fs/promises"));
44
44
  var import_node_path = __toESM(require("path"));
45
+ var RUN_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
45
46
  var FileStorage = class {
46
47
  constructor(baseDir = ".resumable-workflow") {
47
48
  this.baseDir = import_node_path.default.resolve(baseDir);
@@ -56,8 +57,10 @@ var FileStorage = class {
56
57
  if (runId.includes("../") || runId.includes("..\\") || runId.startsWith("..")) {
57
58
  throw new Error(`Invalid runId: Path traversal detected in ${runId}`);
58
59
  }
59
- if (!/^[a-zA-Z0-9_-]+$/.test(runId)) {
60
- throw new Error(`Invalid runId: Only alphanumeric characters, hyphens, and underscores are allowed in ${runId}`);
60
+ if (!RUN_ID_PATTERN.test(runId)) {
61
+ throw new Error(
62
+ `Invalid runId: Only alphanumeric characters, hyphens, and underscores are allowed in ${runId}`
63
+ );
61
64
  }
62
65
  }
63
66
  getFilePath(runId) {
@@ -66,10 +69,27 @@ var FileStorage = class {
66
69
  const resolvedPath = import_node_path.default.resolve(filePath);
67
70
  const resolvedBaseDir = import_node_path.default.resolve(this.baseDir);
68
71
  if (!resolvedPath.startsWith(resolvedBaseDir)) {
69
- throw new Error(`Invalid runId: Path traversal detected`);
72
+ throw new Error("Invalid runId: Path traversal detected");
70
73
  }
71
74
  return resolvedPath;
72
75
  }
76
+ isValidRunFileName(fileName) {
77
+ if (!fileName.endsWith(".json")) {
78
+ return false;
79
+ }
80
+ const runId = fileName.slice(0, -".json".length);
81
+ return RUN_ID_PATTERN.test(runId);
82
+ }
83
+ isValidWorkflowStatus(value) {
84
+ return value === "pending" || value === "completed" || value === "failed";
85
+ }
86
+ isValidRunStateShape(run) {
87
+ if (typeof run !== "object" || run === null || Array.isArray(run)) {
88
+ return false;
89
+ }
90
+ const candidate = run;
91
+ return typeof candidate.workflowId === "string" && typeof candidate.runId === "string" && RUN_ID_PATTERN.test(candidate.runId) && this.isValidWorkflowStatus(candidate.status) && typeof candidate.currentStepIndex === "number" && Number.isInteger(candidate.currentStepIndex) && candidate.currentStepIndex >= 0 && "input" in candidate && typeof candidate.state === "object" && candidate.state !== null && !Array.isArray(candidate.state);
92
+ }
73
93
  async saveRun(state) {
74
94
  await this.ensureDir();
75
95
  await import_promises.default.writeFile(
@@ -101,18 +121,22 @@ var FileStorage = class {
101
121
  const files = await import_promises.default.readdir(this.baseDir);
102
122
  const runs = [];
103
123
  for (const file of files) {
104
- if (file.endsWith(".json")) {
105
- try {
106
- const data = await import_promises.default.readFile(
107
- import_node_path.default.join(this.baseDir, file),
108
- "utf-8"
109
- );
110
- const run = JSON.parse(data);
111
- if (run.workflowId === workflowId && run.status === status) {
112
- runs.push(run);
113
- }
114
- } catch (_e) {
124
+ if (!this.isValidRunFileName(file)) {
125
+ continue;
126
+ }
127
+ try {
128
+ const data = await import_promises.default.readFile(import_node_path.default.join(this.baseDir, file), "utf-8");
129
+ const run = JSON.parse(data);
130
+ if (!this.isValidRunStateShape(run)) {
131
+ continue;
132
+ }
133
+ if (run.runId !== file.slice(0, -".json".length)) {
134
+ continue;
115
135
  }
136
+ if (run.workflowId === workflowId && run.status === status) {
137
+ runs.push(run);
138
+ }
139
+ } catch (_e) {
116
140
  }
117
141
  }
118
142
  return runs;
@@ -140,10 +164,7 @@ var Workflow = class {
140
164
  this.workflowId = config.id;
141
165
  if (config.autoResume) {
142
166
  this.resumeAllIncomplete().catch((err) => {
143
- console.error(
144
- `[ResumableWorkflow: ${this.workflowId}] Auto-resume failed:`,
145
- err
146
- );
167
+ console.error("[ResumableWorkflow] Auto-resume failed:", err);
147
168
  });
148
169
  }
149
170
  }
@@ -255,7 +276,9 @@ var Workflow = class {
255
276
  */
256
277
  async clearCompleted() {
257
278
  const completed = await this.storage.listCompletedRuns(this.workflowId);
258
- await Promise.all(completed.map((run) => this.storage.deleteRun(run.runId)));
279
+ await Promise.all(
280
+ completed.map((run) => this.storage.deleteRun(run.runId))
281
+ );
259
282
  }
260
283
  };
261
284
  function createWorkflow(config) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/core/engine.ts","../src/storage/file-storage.ts"],"sourcesContent":["export { createWorkflow, Workflow } from './core/engine';\nexport { FileStorage } from './storage/file-storage';\nexport type {\n Step,\n StorageProvider,\n WorkflowConfig,\n WorkflowResult,\n WorkflowRunState,\n WorkflowStatus,\n} from './types';\n","import { randomUUID } from 'node:crypto';\nimport { FileStorage } from '../storage/file-storage';\nimport type {\n StorageProvider,\n WorkflowConfig,\n WorkflowResult,\n WorkflowRunState,\n WorkflowStatus,\n} from '../types';\n\ntype Mutable<T> = {\n -readonly [P in keyof T]: T[P];\n};\n\nexport class Workflow<\n TInput = unknown,\n TState extends Record<string, unknown> = Record<string, unknown>,\n> {\n private readonly storage: StorageProvider;\n private readonly workflowId: string;\n private readonly config: WorkflowConfig<TInput, TState>;\n\n constructor(config: WorkflowConfig<TInput, TState>) {\n this.config = config;\n this.storage = config.storage || new FileStorage();\n this.workflowId = config.id;\n\n if (config.autoResume) {\n this.resumeAllIncomplete().catch((err) => {\n console.error(\n `[ResumableWorkflow: ${this.workflowId}] Auto-resume failed:`,\n err\n );\n });\n }\n }\n\n private async executeStep(\n currentState: Mutable<WorkflowRunState<TInput, TState>>,\n stepIndex: number\n ): Promise<void> {\n const step = this.config.steps[stepIndex];\n if (!step) {\n return;\n }\n\n try {\n const result = await step.run({\n input: currentState.input,\n state: currentState.state,\n });\n\n if (\n result !== null &&\n typeof result === 'object' &&\n !Array.isArray(result)\n ) {\n currentState.state = {\n ...currentState.state,\n ...(result as Partial<TState>),\n };\n } else if (result !== undefined) {\n (currentState.state as Record<string, unknown>)[\n step.name || `step_${stepIndex}`\n ] = result;\n }\n\n currentState.currentStepIndex = stepIndex + 1;\n\n if (currentState.currentStepIndex === this.config.steps.length) {\n currentState.status = 'completed';\n }\n\n await this.storage.saveRun(currentState);\n } catch (error) {\n currentState.status = 'failed';\n currentState.error =\n error instanceof Error ? error.message : String(error);\n await this.storage.saveRun(currentState);\n throw error;\n }\n }\n\n private async run(\n input: TInput,\n existingRun?: WorkflowRunState<TInput, TState>\n ): Promise<WorkflowResult<TState>> {\n const runId = existingRun?.runId || randomUUID();\n const currentState: Mutable<WorkflowRunState<TInput, TState>> =\n (existingRun as Mutable<WorkflowRunState<TInput, TState>>) || {\n workflowId: this.workflowId,\n runId,\n status: 'pending' as WorkflowStatus,\n currentStepIndex: 0,\n input,\n state: {} as TState,\n };\n\n if (!existingRun) {\n await this.storage.saveRun(currentState);\n }\n\n try {\n for (\n let i = currentState.currentStepIndex;\n i < this.config.steps.length;\n i++\n ) {\n await this.executeStep(currentState, i);\n }\n } catch (_error) {\n return {\n success: false,\n error: currentState.error || 'Unknown error',\n runId: currentState.runId,\n stepName:\n this.config.steps[currentState.currentStepIndex]?.name ||\n `step_${currentState.currentStepIndex}`,\n };\n }\n\n // Auto-cleanup if enabled and successful\n if (this.config.autoCleanup) {\n await this.storage.deleteRun(currentState.runId);\n }\n\n return {\n success: true,\n result: currentState.state,\n runId: currentState.runId,\n };\n }\n\n start(input: TInput): Promise<WorkflowResult<TState>> {\n return this.run(input);\n }\n\n async resume(runId: string): Promise<WorkflowResult<TState>> {\n const runState = await this.storage.getRun(runId);\n if (!runState) {\n return {\n success: false,\n error: `Run ${runId} not found`,\n runId,\n stepName: 'unknown',\n };\n }\n if (runState.status === 'completed') {\n return {\n success: true,\n result: runState.state as TState,\n runId: runState.runId,\n };\n }\n return this.run(\n runState.input as TInput,\n runState as WorkflowRunState<TInput, TState>\n );\n }\n\n listIncomplete(): Promise<\n WorkflowRunState<unknown, Record<string, unknown>>[]\n > {\n return this.storage.listIncompleteRuns(this.workflowId);\n }\n\n async resumeAllIncomplete(): Promise<WorkflowResult<TState>[]> {\n const incomplete = await this.storage.listIncompleteRuns(this.workflowId);\n return Promise.all(\n incomplete.map((runState) =>\n this.run(\n runState.input as TInput,\n runState as WorkflowRunState<TInput, TState>\n )\n )\n );\n }\n\n /**\n * Manually clears all completed workflow runs for this workflow ID from storage.\n */\n async clearCompleted(): Promise<void> {\n const completed = await this.storage.listCompletedRuns(this.workflowId);\n await Promise.all(completed.map((run) => this.storage.deleteRun(run.runId)));\n }\n}\n\n/**\n * Factory function for backward compatibility and ease of use\n */\nexport function createWorkflow<\n TInput = unknown,\n TState extends Record<string, unknown> = Record<string, unknown>,\n>(config: WorkflowConfig<TInput, TState>) {\n return new Workflow(config);\n}","import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport type { StorageProvider, WorkflowRunState } from '../types';\n\nexport class FileStorage implements StorageProvider {\n private readonly baseDir: string;\n\n constructor(baseDir = '.resumable-workflow') {\n this.baseDir = path.resolve(baseDir);\n }\n\n private async ensureDir(): Promise<void> {\n await fs.mkdir(this.baseDir, { recursive: true });\n }\n\n private validateRunId(runId: string): void {\n if (!runId || typeof runId !== 'string') {\n throw new Error(`Invalid runId: ${runId}`);\n }\n \n // Check for path traversal attempts\n if (runId.includes('../') || runId.includes('..\\\\') || runId.startsWith('..')) {\n throw new Error(`Invalid runId: Path traversal detected in ${runId}`);\n }\n \n // Additional validation: only allow alphanumeric, hyphens, and underscores\n if (!/^[a-zA-Z0-9_-]+$/.test(runId)) {\n throw new Error(`Invalid runId: Only alphanumeric characters, hyphens, and underscores are allowed in ${runId}`);\n }\n }\n\n private getFilePath(runId: string): string {\n this.validateRunId(runId);\n const filePath = path.join(this.baseDir, `${runId}.json`);\n \n // Additional security check: ensure the resolved path is within the base directory\n const resolvedPath = path.resolve(filePath);\n const resolvedBaseDir = path.resolve(this.baseDir);\n \n if (!resolvedPath.startsWith(resolvedBaseDir)) {\n throw new Error(`Invalid runId: Path traversal detected`);\n }\n \n return resolvedPath;\n }\n\n async saveRun(\n state: WorkflowRunState<unknown, Record<string, unknown>>\n ): Promise<void> {\n await this.ensureDir();\n await fs.writeFile(\n this.getFilePath(state.runId),\n JSON.stringify(state, null, 2)\n );\n }\n\n async getRun(\n runId: string\n ): Promise<WorkflowRunState<unknown, Record<string, unknown>> | null> {\n // Validate runId first before attempting file operations\n this.validateRunId(runId);\n \n try {\n const data = await fs.readFile(this.getFilePath(runId), 'utf-8');\n return JSON.parse(data) as WorkflowRunState<\n unknown,\n Record<string, unknown>\n >;\n } catch (e) {\n // Only return null for file system errors, not validation errors\n // If it's a validation error, it should have been caught by validateRunId\n if ((e as Error).message.includes('Invalid runId') || (e as Error).message.includes('Path traversal')) {\n throw e; // Re-throw validation errors\n }\n // Return null if file doesn't exist or is invalid\n return null;\n }\n }\n\n listIncompleteRuns(\n workflowId: string\n ): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]> {\n return this.listRuns(workflowId, 'pending');\n }\n\n listCompletedRuns(\n workflowId: string\n ): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]> {\n return this.listRuns(workflowId, 'completed');\n }\n\n private async listRuns(\n workflowId: string,\n status: 'pending' | 'completed'\n ): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]> {\n try {\n await this.ensureDir();\n const files = await fs.readdir(this.baseDir);\n const runs: WorkflowRunState<unknown, Record<string, unknown>>[] = [];\n\n for (const file of files) {\n if (file.endsWith('.json')) {\n try {\n const data = await fs.readFile(\n path.join(this.baseDir, file),\n 'utf-8'\n );\n const run = JSON.parse(data) as WorkflowRunState<\n unknown,\n Record<string, unknown>\n >;\n if (run.workflowId === workflowId && run.status === status) {\n runs.push(run);\n }\n } catch (_e) {\n // Ignore corrupted files\n }\n }\n }\n return runs;\n } catch (_e) {\n return [];\n }\n }\n\n async deleteRun(runId: string): Promise<void> {\n // Validate runId first before attempting file operations\n this.validateRunId(runId);\n \n try {\n await fs.unlink(this.getFilePath(runId));\n } catch (e) {\n // Only ignore file system errors, not validation errors\n if ((e as Error).message.includes('Invalid runId') || (e as Error).message.includes('Path traversal')) {\n throw e; // Re-throw validation errors\n }\n // Ignore if file doesn't exist\n }\n }\n}\n"],"mappings":"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","names":["path","fs"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/core/engine.ts","../src/storage/file-storage.ts"],"sourcesContent":["export { createWorkflow, Workflow } from './core/engine';\nexport { FileStorage } from './storage/file-storage';\nexport type {\n Step,\n StorageProvider,\n WorkflowConfig,\n WorkflowResult,\n WorkflowRunState,\n WorkflowStatus,\n} from './types';\n","import { randomUUID } from 'node:crypto';\nimport { FileStorage } from '../storage/file-storage';\nimport type {\n StorageProvider,\n WorkflowConfig,\n WorkflowResult,\n WorkflowRunState,\n WorkflowStatus,\n} from '../types';\n\ntype Mutable<T> = {\n -readonly [P in keyof T]: T[P];\n};\n\nexport class Workflow<\n TInput = unknown,\n TState extends Record<string, unknown> = Record<string, unknown>,\n> {\n private readonly storage: StorageProvider;\n private readonly workflowId: string;\n private readonly config: WorkflowConfig<TInput, TState>;\n\n constructor(config: WorkflowConfig<TInput, TState>) {\n this.config = config;\n this.storage = config.storage || new FileStorage();\n this.workflowId = config.id;\n\n if (config.autoResume) {\n this.resumeAllIncomplete().catch((err) => {\n console.error('[ResumableWorkflow] Auto-resume failed:', err);\n });\n }\n }\n\n private async executeStep(\n currentState: Mutable<WorkflowRunState<TInput, TState>>,\n stepIndex: number\n ): Promise<void> {\n const step = this.config.steps[stepIndex];\n if (!step) {\n return;\n }\n\n try {\n const result = await step.run({\n input: currentState.input,\n state: currentState.state,\n });\n\n if (\n result !== null &&\n typeof result === 'object' &&\n !Array.isArray(result)\n ) {\n currentState.state = {\n ...currentState.state,\n ...(result as Partial<TState>),\n };\n } else if (result !== undefined) {\n (currentState.state as Record<string, unknown>)[\n step.name || `step_${stepIndex}`\n ] = result;\n }\n\n currentState.currentStepIndex = stepIndex + 1;\n\n if (currentState.currentStepIndex === this.config.steps.length) {\n currentState.status = 'completed';\n }\n\n await this.storage.saveRun(currentState);\n } catch (error) {\n currentState.status = 'failed';\n currentState.error =\n error instanceof Error ? error.message : String(error);\n await this.storage.saveRun(currentState);\n throw error;\n }\n }\n\n private async run(\n input: TInput,\n existingRun?: WorkflowRunState<TInput, TState>\n ): Promise<WorkflowResult<TState>> {\n const runId = existingRun?.runId || randomUUID();\n const currentState: Mutable<WorkflowRunState<TInput, TState>> =\n (existingRun as Mutable<WorkflowRunState<TInput, TState>>) || {\n workflowId: this.workflowId,\n runId,\n status: 'pending' as WorkflowStatus,\n currentStepIndex: 0,\n input,\n state: {} as TState,\n };\n\n if (!existingRun) {\n await this.storage.saveRun(currentState);\n }\n\n try {\n for (\n let i = currentState.currentStepIndex;\n i < this.config.steps.length;\n i++\n ) {\n await this.executeStep(currentState, i);\n }\n } catch (_error) {\n return {\n success: false,\n error: currentState.error || 'Unknown error',\n runId: currentState.runId,\n stepName:\n this.config.steps[currentState.currentStepIndex]?.name ||\n `step_${currentState.currentStepIndex}`,\n };\n }\n\n // Auto-cleanup if enabled and successful\n if (this.config.autoCleanup) {\n await this.storage.deleteRun(currentState.runId);\n }\n\n return {\n success: true,\n result: currentState.state,\n runId: currentState.runId,\n };\n }\n\n start(input: TInput): Promise<WorkflowResult<TState>> {\n return this.run(input);\n }\n\n async resume(runId: string): Promise<WorkflowResult<TState>> {\n const runState = await this.storage.getRun(runId);\n if (!runState) {\n return {\n success: false,\n error: `Run ${runId} not found`,\n runId,\n stepName: 'unknown',\n };\n }\n if (runState.status === 'completed') {\n return {\n success: true,\n result: runState.state as TState,\n runId: runState.runId,\n };\n }\n return this.run(\n runState.input as TInput,\n runState as WorkflowRunState<TInput, TState>\n );\n }\n\n listIncomplete(): Promise<\n WorkflowRunState<unknown, Record<string, unknown>>[]\n > {\n return this.storage.listIncompleteRuns(this.workflowId);\n }\n\n async resumeAllIncomplete(): Promise<WorkflowResult<TState>[]> {\n const incomplete = await this.storage.listIncompleteRuns(this.workflowId);\n return Promise.all(\n incomplete.map((runState) =>\n this.run(\n runState.input as TInput,\n runState as WorkflowRunState<TInput, TState>\n )\n )\n );\n }\n\n /**\n * Manually clears all completed workflow runs for this workflow ID from storage.\n */\n async clearCompleted(): Promise<void> {\n const completed = await this.storage.listCompletedRuns(this.workflowId);\n await Promise.all(\n completed.map((run) => this.storage.deleteRun(run.runId))\n );\n }\n}\n\n/**\n * Factory function for backward compatibility and ease of use\n */\nexport function createWorkflow<\n TInput = unknown,\n TState extends Record<string, unknown> = Record<string, unknown>,\n>(config: WorkflowConfig<TInput, TState>) {\n return new Workflow(config);\n}\n","import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport type { StorageProvider, WorkflowRunState } from '../types';\n\nconst RUN_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;\n\nexport class FileStorage implements StorageProvider {\n private readonly baseDir: string;\n\n constructor(baseDir = '.resumable-workflow') {\n this.baseDir = path.resolve(baseDir);\n }\n\n private async ensureDir(): Promise<void> {\n await fs.mkdir(this.baseDir, { recursive: true });\n }\n\n private validateRunId(runId: string): void {\n if (!runId || typeof runId !== 'string') {\n throw new Error(`Invalid runId: ${runId}`);\n }\n\n // Check for path traversal attempts\n if (\n runId.includes('../') ||\n runId.includes('..\\\\') ||\n runId.startsWith('..')\n ) {\n throw new Error(`Invalid runId: Path traversal detected in ${runId}`);\n }\n\n // Additional validation: only allow alphanumeric, hyphens, and underscores\n if (!RUN_ID_PATTERN.test(runId)) {\n throw new Error(\n `Invalid runId: Only alphanumeric characters, hyphens, and underscores are allowed in ${runId}`\n );\n }\n }\n\n private getFilePath(runId: string): string {\n this.validateRunId(runId);\n const filePath = path.join(this.baseDir, `${runId}.json`);\n\n // Additional security check: ensure the resolved path is within the base directory\n const resolvedPath = path.resolve(filePath);\n const resolvedBaseDir = path.resolve(this.baseDir);\n\n if (!resolvedPath.startsWith(resolvedBaseDir)) {\n throw new Error('Invalid runId: Path traversal detected');\n }\n\n return resolvedPath;\n }\n\n private isValidRunFileName(fileName: string): boolean {\n if (!fileName.endsWith('.json')) {\n return false;\n }\n\n const runId = fileName.slice(0, -'.json'.length);\n return RUN_ID_PATTERN.test(runId);\n }\n\n private isValidWorkflowStatus(\n value: unknown\n ): value is 'pending' | 'completed' | 'failed' {\n return value === 'pending' || value === 'completed' || value === 'failed';\n }\n\n private isValidRunStateShape(\n run: unknown\n ): run is WorkflowRunState<unknown, Record<string, unknown>> {\n if (typeof run !== 'object' || run === null || Array.isArray(run)) {\n return false;\n }\n\n const candidate = run as Record<string, unknown>;\n return (\n typeof candidate.workflowId === 'string' &&\n typeof candidate.runId === 'string' &&\n RUN_ID_PATTERN.test(candidate.runId) &&\n this.isValidWorkflowStatus(candidate.status) &&\n typeof candidate.currentStepIndex === 'number' &&\n Number.isInteger(candidate.currentStepIndex) &&\n candidate.currentStepIndex >= 0 &&\n 'input' in candidate &&\n typeof candidate.state === 'object' &&\n candidate.state !== null &&\n !Array.isArray(candidate.state)\n );\n }\n\n async saveRun(\n state: WorkflowRunState<unknown, Record<string, unknown>>\n ): Promise<void> {\n await this.ensureDir();\n await fs.writeFile(\n this.getFilePath(state.runId),\n JSON.stringify(state, null, 2)\n );\n }\n\n async getRun(\n runId: string\n ): Promise<WorkflowRunState<unknown, Record<string, unknown>> | null> {\n // Validate runId first before attempting file operations\n this.validateRunId(runId);\n\n try {\n const data = await fs.readFile(this.getFilePath(runId), 'utf-8');\n return JSON.parse(data) as WorkflowRunState<\n unknown,\n Record<string, unknown>\n >;\n } catch (e) {\n // Only return null for file system errors, not validation errors\n // If it's a validation error, it should have been caught by validateRunId\n if (\n (e as Error).message.includes('Invalid runId') ||\n (e as Error).message.includes('Path traversal')\n ) {\n throw e; // Re-throw validation errors\n }\n // Return null if file doesn't exist or is invalid\n return null;\n }\n }\n\n listIncompleteRuns(\n workflowId: string\n ): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]> {\n return this.listRuns(workflowId, 'pending');\n }\n\n listCompletedRuns(\n workflowId: string\n ): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]> {\n return this.listRuns(workflowId, 'completed');\n }\n\n private async listRuns(\n workflowId: string,\n status: 'pending' | 'completed'\n ): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]> {\n try {\n await this.ensureDir();\n const files = await fs.readdir(this.baseDir);\n const runs: WorkflowRunState<unknown, Record<string, unknown>>[] = [];\n\n for (const file of files) {\n if (!this.isValidRunFileName(file)) {\n continue;\n }\n\n try {\n const data = await fs.readFile(path.join(this.baseDir, file), 'utf-8');\n const run = JSON.parse(data) as unknown;\n\n if (!this.isValidRunStateShape(run)) {\n continue;\n }\n\n if (run.runId !== file.slice(0, -'.json'.length)) {\n continue;\n }\n\n if (run.workflowId === workflowId && run.status === status) {\n runs.push(run);\n }\n } catch (_e) {\n // Ignore corrupted files\n }\n }\n return runs;\n } catch (_e) {\n return [];\n }\n }\n\n async deleteRun(runId: string): Promise<void> {\n // Validate runId first before attempting file operations\n this.validateRunId(runId);\n\n try {\n await fs.unlink(this.getFilePath(runId));\n } catch (e) {\n // Only ignore file system errors, not validation errors\n if (\n (e as Error).message.includes('Invalid runId') ||\n (e as Error).message.includes('Path traversal')\n ) {\n throw e; // Re-throw validation errors\n }\n // Ignore if file doesn't exist\n }\n }\n}\n"],"mappings":"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","names":["path","fs"]}
package/dist/index.mjs CHANGED
@@ -4,6 +4,7 @@ import { randomUUID } from "crypto";
4
4
  // src/storage/file-storage.ts
5
5
  import fs from "fs/promises";
6
6
  import path from "path";
7
+ var RUN_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
7
8
  var FileStorage = class {
8
9
  constructor(baseDir = ".resumable-workflow") {
9
10
  this.baseDir = path.resolve(baseDir);
@@ -18,8 +19,10 @@ var FileStorage = class {
18
19
  if (runId.includes("../") || runId.includes("..\\") || runId.startsWith("..")) {
19
20
  throw new Error(`Invalid runId: Path traversal detected in ${runId}`);
20
21
  }
21
- if (!/^[a-zA-Z0-9_-]+$/.test(runId)) {
22
- throw new Error(`Invalid runId: Only alphanumeric characters, hyphens, and underscores are allowed in ${runId}`);
22
+ if (!RUN_ID_PATTERN.test(runId)) {
23
+ throw new Error(
24
+ `Invalid runId: Only alphanumeric characters, hyphens, and underscores are allowed in ${runId}`
25
+ );
23
26
  }
24
27
  }
25
28
  getFilePath(runId) {
@@ -28,10 +31,27 @@ var FileStorage = class {
28
31
  const resolvedPath = path.resolve(filePath);
29
32
  const resolvedBaseDir = path.resolve(this.baseDir);
30
33
  if (!resolvedPath.startsWith(resolvedBaseDir)) {
31
- throw new Error(`Invalid runId: Path traversal detected`);
34
+ throw new Error("Invalid runId: Path traversal detected");
32
35
  }
33
36
  return resolvedPath;
34
37
  }
38
+ isValidRunFileName(fileName) {
39
+ if (!fileName.endsWith(".json")) {
40
+ return false;
41
+ }
42
+ const runId = fileName.slice(0, -".json".length);
43
+ return RUN_ID_PATTERN.test(runId);
44
+ }
45
+ isValidWorkflowStatus(value) {
46
+ return value === "pending" || value === "completed" || value === "failed";
47
+ }
48
+ isValidRunStateShape(run) {
49
+ if (typeof run !== "object" || run === null || Array.isArray(run)) {
50
+ return false;
51
+ }
52
+ const candidate = run;
53
+ return typeof candidate.workflowId === "string" && typeof candidate.runId === "string" && RUN_ID_PATTERN.test(candidate.runId) && this.isValidWorkflowStatus(candidate.status) && typeof candidate.currentStepIndex === "number" && Number.isInteger(candidate.currentStepIndex) && candidate.currentStepIndex >= 0 && "input" in candidate && typeof candidate.state === "object" && candidate.state !== null && !Array.isArray(candidate.state);
54
+ }
35
55
  async saveRun(state) {
36
56
  await this.ensureDir();
37
57
  await fs.writeFile(
@@ -63,18 +83,22 @@ var FileStorage = class {
63
83
  const files = await fs.readdir(this.baseDir);
64
84
  const runs = [];
65
85
  for (const file of files) {
66
- if (file.endsWith(".json")) {
67
- try {
68
- const data = await fs.readFile(
69
- path.join(this.baseDir, file),
70
- "utf-8"
71
- );
72
- const run = JSON.parse(data);
73
- if (run.workflowId === workflowId && run.status === status) {
74
- runs.push(run);
75
- }
76
- } catch (_e) {
86
+ if (!this.isValidRunFileName(file)) {
87
+ continue;
88
+ }
89
+ try {
90
+ const data = await fs.readFile(path.join(this.baseDir, file), "utf-8");
91
+ const run = JSON.parse(data);
92
+ if (!this.isValidRunStateShape(run)) {
93
+ continue;
94
+ }
95
+ if (run.runId !== file.slice(0, -".json".length)) {
96
+ continue;
77
97
  }
98
+ if (run.workflowId === workflowId && run.status === status) {
99
+ runs.push(run);
100
+ }
101
+ } catch (_e) {
78
102
  }
79
103
  }
80
104
  return runs;
@@ -102,10 +126,7 @@ var Workflow = class {
102
126
  this.workflowId = config.id;
103
127
  if (config.autoResume) {
104
128
  this.resumeAllIncomplete().catch((err) => {
105
- console.error(
106
- `[ResumableWorkflow: ${this.workflowId}] Auto-resume failed:`,
107
- err
108
- );
129
+ console.error("[ResumableWorkflow] Auto-resume failed:", err);
109
130
  });
110
131
  }
111
132
  }
@@ -217,7 +238,9 @@ var Workflow = class {
217
238
  */
218
239
  async clearCompleted() {
219
240
  const completed = await this.storage.listCompletedRuns(this.workflowId);
220
- await Promise.all(completed.map((run) => this.storage.deleteRun(run.runId)));
241
+ await Promise.all(
242
+ completed.map((run) => this.storage.deleteRun(run.runId))
243
+ );
221
244
  }
222
245
  };
223
246
  function createWorkflow(config) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core/engine.ts","../src/storage/file-storage.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { FileStorage } from '../storage/file-storage';\nimport type {\n StorageProvider,\n WorkflowConfig,\n WorkflowResult,\n WorkflowRunState,\n WorkflowStatus,\n} from '../types';\n\ntype Mutable<T> = {\n -readonly [P in keyof T]: T[P];\n};\n\nexport class Workflow<\n TInput = unknown,\n TState extends Record<string, unknown> = Record<string, unknown>,\n> {\n private readonly storage: StorageProvider;\n private readonly workflowId: string;\n private readonly config: WorkflowConfig<TInput, TState>;\n\n constructor(config: WorkflowConfig<TInput, TState>) {\n this.config = config;\n this.storage = config.storage || new FileStorage();\n this.workflowId = config.id;\n\n if (config.autoResume) {\n this.resumeAllIncomplete().catch((err) => {\n console.error(\n `[ResumableWorkflow: ${this.workflowId}] Auto-resume failed:`,\n err\n );\n });\n }\n }\n\n private async executeStep(\n currentState: Mutable<WorkflowRunState<TInput, TState>>,\n stepIndex: number\n ): Promise<void> {\n const step = this.config.steps[stepIndex];\n if (!step) {\n return;\n }\n\n try {\n const result = await step.run({\n input: currentState.input,\n state: currentState.state,\n });\n\n if (\n result !== null &&\n typeof result === 'object' &&\n !Array.isArray(result)\n ) {\n currentState.state = {\n ...currentState.state,\n ...(result as Partial<TState>),\n };\n } else if (result !== undefined) {\n (currentState.state as Record<string, unknown>)[\n step.name || `step_${stepIndex}`\n ] = result;\n }\n\n currentState.currentStepIndex = stepIndex + 1;\n\n if (currentState.currentStepIndex === this.config.steps.length) {\n currentState.status = 'completed';\n }\n\n await this.storage.saveRun(currentState);\n } catch (error) {\n currentState.status = 'failed';\n currentState.error =\n error instanceof Error ? error.message : String(error);\n await this.storage.saveRun(currentState);\n throw error;\n }\n }\n\n private async run(\n input: TInput,\n existingRun?: WorkflowRunState<TInput, TState>\n ): Promise<WorkflowResult<TState>> {\n const runId = existingRun?.runId || randomUUID();\n const currentState: Mutable<WorkflowRunState<TInput, TState>> =\n (existingRun as Mutable<WorkflowRunState<TInput, TState>>) || {\n workflowId: this.workflowId,\n runId,\n status: 'pending' as WorkflowStatus,\n currentStepIndex: 0,\n input,\n state: {} as TState,\n };\n\n if (!existingRun) {\n await this.storage.saveRun(currentState);\n }\n\n try {\n for (\n let i = currentState.currentStepIndex;\n i < this.config.steps.length;\n i++\n ) {\n await this.executeStep(currentState, i);\n }\n } catch (_error) {\n return {\n success: false,\n error: currentState.error || 'Unknown error',\n runId: currentState.runId,\n stepName:\n this.config.steps[currentState.currentStepIndex]?.name ||\n `step_${currentState.currentStepIndex}`,\n };\n }\n\n // Auto-cleanup if enabled and successful\n if (this.config.autoCleanup) {\n await this.storage.deleteRun(currentState.runId);\n }\n\n return {\n success: true,\n result: currentState.state,\n runId: currentState.runId,\n };\n }\n\n start(input: TInput): Promise<WorkflowResult<TState>> {\n return this.run(input);\n }\n\n async resume(runId: string): Promise<WorkflowResult<TState>> {\n const runState = await this.storage.getRun(runId);\n if (!runState) {\n return {\n success: false,\n error: `Run ${runId} not found`,\n runId,\n stepName: 'unknown',\n };\n }\n if (runState.status === 'completed') {\n return {\n success: true,\n result: runState.state as TState,\n runId: runState.runId,\n };\n }\n return this.run(\n runState.input as TInput,\n runState as WorkflowRunState<TInput, TState>\n );\n }\n\n listIncomplete(): Promise<\n WorkflowRunState<unknown, Record<string, unknown>>[]\n > {\n return this.storage.listIncompleteRuns(this.workflowId);\n }\n\n async resumeAllIncomplete(): Promise<WorkflowResult<TState>[]> {\n const incomplete = await this.storage.listIncompleteRuns(this.workflowId);\n return Promise.all(\n incomplete.map((runState) =>\n this.run(\n runState.input as TInput,\n runState as WorkflowRunState<TInput, TState>\n )\n )\n );\n }\n\n /**\n * Manually clears all completed workflow runs for this workflow ID from storage.\n */\n async clearCompleted(): Promise<void> {\n const completed = await this.storage.listCompletedRuns(this.workflowId);\n await Promise.all(completed.map((run) => this.storage.deleteRun(run.runId)));\n }\n}\n\n/**\n * Factory function for backward compatibility and ease of use\n */\nexport function createWorkflow<\n TInput = unknown,\n TState extends Record<string, unknown> = Record<string, unknown>,\n>(config: WorkflowConfig<TInput, TState>) {\n return new Workflow(config);\n}","import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport type { StorageProvider, WorkflowRunState } from '../types';\n\nexport class FileStorage implements StorageProvider {\n private readonly baseDir: string;\n\n constructor(baseDir = '.resumable-workflow') {\n this.baseDir = path.resolve(baseDir);\n }\n\n private async ensureDir(): Promise<void> {\n await fs.mkdir(this.baseDir, { recursive: true });\n }\n\n private validateRunId(runId: string): void {\n if (!runId || typeof runId !== 'string') {\n throw new Error(`Invalid runId: ${runId}`);\n }\n \n // Check for path traversal attempts\n if (runId.includes('../') || runId.includes('..\\\\') || runId.startsWith('..')) {\n throw new Error(`Invalid runId: Path traversal detected in ${runId}`);\n }\n \n // Additional validation: only allow alphanumeric, hyphens, and underscores\n if (!/^[a-zA-Z0-9_-]+$/.test(runId)) {\n throw new Error(`Invalid runId: Only alphanumeric characters, hyphens, and underscores are allowed in ${runId}`);\n }\n }\n\n private getFilePath(runId: string): string {\n this.validateRunId(runId);\n const filePath = path.join(this.baseDir, `${runId}.json`);\n \n // Additional security check: ensure the resolved path is within the base directory\n const resolvedPath = path.resolve(filePath);\n const resolvedBaseDir = path.resolve(this.baseDir);\n \n if (!resolvedPath.startsWith(resolvedBaseDir)) {\n throw new Error(`Invalid runId: Path traversal detected`);\n }\n \n return resolvedPath;\n }\n\n async saveRun(\n state: WorkflowRunState<unknown, Record<string, unknown>>\n ): Promise<void> {\n await this.ensureDir();\n await fs.writeFile(\n this.getFilePath(state.runId),\n JSON.stringify(state, null, 2)\n );\n }\n\n async getRun(\n runId: string\n ): Promise<WorkflowRunState<unknown, Record<string, unknown>> | null> {\n // Validate runId first before attempting file operations\n this.validateRunId(runId);\n \n try {\n const data = await fs.readFile(this.getFilePath(runId), 'utf-8');\n return JSON.parse(data) as WorkflowRunState<\n unknown,\n Record<string, unknown>\n >;\n } catch (e) {\n // Only return null for file system errors, not validation errors\n // If it's a validation error, it should have been caught by validateRunId\n if ((e as Error).message.includes('Invalid runId') || (e as Error).message.includes('Path traversal')) {\n throw e; // Re-throw validation errors\n }\n // Return null if file doesn't exist or is invalid\n return null;\n }\n }\n\n listIncompleteRuns(\n workflowId: string\n ): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]> {\n return this.listRuns(workflowId, 'pending');\n }\n\n listCompletedRuns(\n workflowId: string\n ): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]> {\n return this.listRuns(workflowId, 'completed');\n }\n\n private async listRuns(\n workflowId: string,\n status: 'pending' | 'completed'\n ): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]> {\n try {\n await this.ensureDir();\n const files = await fs.readdir(this.baseDir);\n const runs: WorkflowRunState<unknown, Record<string, unknown>>[] = [];\n\n for (const file of files) {\n if (file.endsWith('.json')) {\n try {\n const data = await fs.readFile(\n path.join(this.baseDir, file),\n 'utf-8'\n );\n const run = JSON.parse(data) as WorkflowRunState<\n unknown,\n Record<string, unknown>\n >;\n if (run.workflowId === workflowId && run.status === status) {\n runs.push(run);\n }\n } catch (_e) {\n // Ignore corrupted files\n }\n }\n }\n return runs;\n } catch (_e) {\n return [];\n }\n }\n\n async deleteRun(runId: string): Promise<void> {\n // Validate runId first before attempting file operations\n this.validateRunId(runId);\n \n try {\n await fs.unlink(this.getFilePath(runId));\n } catch (e) {\n // Only ignore file system errors, not validation errors\n if ((e as Error).message.includes('Invalid runId') || (e as Error).message.includes('Path traversal')) {\n throw e; // Re-throw validation errors\n }\n // Ignore if file doesn't exist\n }\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/core/engine.ts","../src/storage/file-storage.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { FileStorage } from '../storage/file-storage';\nimport type {\n StorageProvider,\n WorkflowConfig,\n WorkflowResult,\n WorkflowRunState,\n WorkflowStatus,\n} from '../types';\n\ntype Mutable<T> = {\n -readonly [P in keyof T]: T[P];\n};\n\nexport class Workflow<\n TInput = unknown,\n TState extends Record<string, unknown> = Record<string, unknown>,\n> {\n private readonly storage: StorageProvider;\n private readonly workflowId: string;\n private readonly config: WorkflowConfig<TInput, TState>;\n\n constructor(config: WorkflowConfig<TInput, TState>) {\n this.config = config;\n this.storage = config.storage || new FileStorage();\n this.workflowId = config.id;\n\n if (config.autoResume) {\n this.resumeAllIncomplete().catch((err) => {\n console.error('[ResumableWorkflow] Auto-resume failed:', err);\n });\n }\n }\n\n private async executeStep(\n currentState: Mutable<WorkflowRunState<TInput, TState>>,\n stepIndex: number\n ): Promise<void> {\n const step = this.config.steps[stepIndex];\n if (!step) {\n return;\n }\n\n try {\n const result = await step.run({\n input: currentState.input,\n state: currentState.state,\n });\n\n if (\n result !== null &&\n typeof result === 'object' &&\n !Array.isArray(result)\n ) {\n currentState.state = {\n ...currentState.state,\n ...(result as Partial<TState>),\n };\n } else if (result !== undefined) {\n (currentState.state as Record<string, unknown>)[\n step.name || `step_${stepIndex}`\n ] = result;\n }\n\n currentState.currentStepIndex = stepIndex + 1;\n\n if (currentState.currentStepIndex === this.config.steps.length) {\n currentState.status = 'completed';\n }\n\n await this.storage.saveRun(currentState);\n } catch (error) {\n currentState.status = 'failed';\n currentState.error =\n error instanceof Error ? error.message : String(error);\n await this.storage.saveRun(currentState);\n throw error;\n }\n }\n\n private async run(\n input: TInput,\n existingRun?: WorkflowRunState<TInput, TState>\n ): Promise<WorkflowResult<TState>> {\n const runId = existingRun?.runId || randomUUID();\n const currentState: Mutable<WorkflowRunState<TInput, TState>> =\n (existingRun as Mutable<WorkflowRunState<TInput, TState>>) || {\n workflowId: this.workflowId,\n runId,\n status: 'pending' as WorkflowStatus,\n currentStepIndex: 0,\n input,\n state: {} as TState,\n };\n\n if (!existingRun) {\n await this.storage.saveRun(currentState);\n }\n\n try {\n for (\n let i = currentState.currentStepIndex;\n i < this.config.steps.length;\n i++\n ) {\n await this.executeStep(currentState, i);\n }\n } catch (_error) {\n return {\n success: false,\n error: currentState.error || 'Unknown error',\n runId: currentState.runId,\n stepName:\n this.config.steps[currentState.currentStepIndex]?.name ||\n `step_${currentState.currentStepIndex}`,\n };\n }\n\n // Auto-cleanup if enabled and successful\n if (this.config.autoCleanup) {\n await this.storage.deleteRun(currentState.runId);\n }\n\n return {\n success: true,\n result: currentState.state,\n runId: currentState.runId,\n };\n }\n\n start(input: TInput): Promise<WorkflowResult<TState>> {\n return this.run(input);\n }\n\n async resume(runId: string): Promise<WorkflowResult<TState>> {\n const runState = await this.storage.getRun(runId);\n if (!runState) {\n return {\n success: false,\n error: `Run ${runId} not found`,\n runId,\n stepName: 'unknown',\n };\n }\n if (runState.status === 'completed') {\n return {\n success: true,\n result: runState.state as TState,\n runId: runState.runId,\n };\n }\n return this.run(\n runState.input as TInput,\n runState as WorkflowRunState<TInput, TState>\n );\n }\n\n listIncomplete(): Promise<\n WorkflowRunState<unknown, Record<string, unknown>>[]\n > {\n return this.storage.listIncompleteRuns(this.workflowId);\n }\n\n async resumeAllIncomplete(): Promise<WorkflowResult<TState>[]> {\n const incomplete = await this.storage.listIncompleteRuns(this.workflowId);\n return Promise.all(\n incomplete.map((runState) =>\n this.run(\n runState.input as TInput,\n runState as WorkflowRunState<TInput, TState>\n )\n )\n );\n }\n\n /**\n * Manually clears all completed workflow runs for this workflow ID from storage.\n */\n async clearCompleted(): Promise<void> {\n const completed = await this.storage.listCompletedRuns(this.workflowId);\n await Promise.all(\n completed.map((run) => this.storage.deleteRun(run.runId))\n );\n }\n}\n\n/**\n * Factory function for backward compatibility and ease of use\n */\nexport function createWorkflow<\n TInput = unknown,\n TState extends Record<string, unknown> = Record<string, unknown>,\n>(config: WorkflowConfig<TInput, TState>) {\n return new Workflow(config);\n}\n","import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport type { StorageProvider, WorkflowRunState } from '../types';\n\nconst RUN_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;\n\nexport class FileStorage implements StorageProvider {\n private readonly baseDir: string;\n\n constructor(baseDir = '.resumable-workflow') {\n this.baseDir = path.resolve(baseDir);\n }\n\n private async ensureDir(): Promise<void> {\n await fs.mkdir(this.baseDir, { recursive: true });\n }\n\n private validateRunId(runId: string): void {\n if (!runId || typeof runId !== 'string') {\n throw new Error(`Invalid runId: ${runId}`);\n }\n\n // Check for path traversal attempts\n if (\n runId.includes('../') ||\n runId.includes('..\\\\') ||\n runId.startsWith('..')\n ) {\n throw new Error(`Invalid runId: Path traversal detected in ${runId}`);\n }\n\n // Additional validation: only allow alphanumeric, hyphens, and underscores\n if (!RUN_ID_PATTERN.test(runId)) {\n throw new Error(\n `Invalid runId: Only alphanumeric characters, hyphens, and underscores are allowed in ${runId}`\n );\n }\n }\n\n private getFilePath(runId: string): string {\n this.validateRunId(runId);\n const filePath = path.join(this.baseDir, `${runId}.json`);\n\n // Additional security check: ensure the resolved path is within the base directory\n const resolvedPath = path.resolve(filePath);\n const resolvedBaseDir = path.resolve(this.baseDir);\n\n if (!resolvedPath.startsWith(resolvedBaseDir)) {\n throw new Error('Invalid runId: Path traversal detected');\n }\n\n return resolvedPath;\n }\n\n private isValidRunFileName(fileName: string): boolean {\n if (!fileName.endsWith('.json')) {\n return false;\n }\n\n const runId = fileName.slice(0, -'.json'.length);\n return RUN_ID_PATTERN.test(runId);\n }\n\n private isValidWorkflowStatus(\n value: unknown\n ): value is 'pending' | 'completed' | 'failed' {\n return value === 'pending' || value === 'completed' || value === 'failed';\n }\n\n private isValidRunStateShape(\n run: unknown\n ): run is WorkflowRunState<unknown, Record<string, unknown>> {\n if (typeof run !== 'object' || run === null || Array.isArray(run)) {\n return false;\n }\n\n const candidate = run as Record<string, unknown>;\n return (\n typeof candidate.workflowId === 'string' &&\n typeof candidate.runId === 'string' &&\n RUN_ID_PATTERN.test(candidate.runId) &&\n this.isValidWorkflowStatus(candidate.status) &&\n typeof candidate.currentStepIndex === 'number' &&\n Number.isInteger(candidate.currentStepIndex) &&\n candidate.currentStepIndex >= 0 &&\n 'input' in candidate &&\n typeof candidate.state === 'object' &&\n candidate.state !== null &&\n !Array.isArray(candidate.state)\n );\n }\n\n async saveRun(\n state: WorkflowRunState<unknown, Record<string, unknown>>\n ): Promise<void> {\n await this.ensureDir();\n await fs.writeFile(\n this.getFilePath(state.runId),\n JSON.stringify(state, null, 2)\n );\n }\n\n async getRun(\n runId: string\n ): Promise<WorkflowRunState<unknown, Record<string, unknown>> | null> {\n // Validate runId first before attempting file operations\n this.validateRunId(runId);\n\n try {\n const data = await fs.readFile(this.getFilePath(runId), 'utf-8');\n return JSON.parse(data) as WorkflowRunState<\n unknown,\n Record<string, unknown>\n >;\n } catch (e) {\n // Only return null for file system errors, not validation errors\n // If it's a validation error, it should have been caught by validateRunId\n if (\n (e as Error).message.includes('Invalid runId') ||\n (e as Error).message.includes('Path traversal')\n ) {\n throw e; // Re-throw validation errors\n }\n // Return null if file doesn't exist or is invalid\n return null;\n }\n }\n\n listIncompleteRuns(\n workflowId: string\n ): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]> {\n return this.listRuns(workflowId, 'pending');\n }\n\n listCompletedRuns(\n workflowId: string\n ): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]> {\n return this.listRuns(workflowId, 'completed');\n }\n\n private async listRuns(\n workflowId: string,\n status: 'pending' | 'completed'\n ): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]> {\n try {\n await this.ensureDir();\n const files = await fs.readdir(this.baseDir);\n const runs: WorkflowRunState<unknown, Record<string, unknown>>[] = [];\n\n for (const file of files) {\n if (!this.isValidRunFileName(file)) {\n continue;\n }\n\n try {\n const data = await fs.readFile(path.join(this.baseDir, file), 'utf-8');\n const run = JSON.parse(data) as unknown;\n\n if (!this.isValidRunStateShape(run)) {\n continue;\n }\n\n if (run.runId !== file.slice(0, -'.json'.length)) {\n continue;\n }\n\n if (run.workflowId === workflowId && run.status === status) {\n runs.push(run);\n }\n } catch (_e) {\n // Ignore corrupted files\n }\n }\n return runs;\n } catch (_e) {\n return [];\n }\n }\n\n async deleteRun(runId: string): Promise<void> {\n // Validate runId first before attempting file operations\n this.validateRunId(runId);\n\n try {\n await fs.unlink(this.getFilePath(runId));\n } catch (e) {\n // Only ignore file system errors, not validation errors\n if (\n (e as Error).message.includes('Invalid runId') ||\n (e as Error).message.includes('Path traversal')\n ) {\n throw e; // Re-throw validation errors\n }\n // Ignore if file doesn't exist\n }\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "resumable-workflow",
3
- "version": "1.4.1",
3
+ "version": "1.4.2",
4
4
  "description": "A lightweight, persistent workflow engine for Node.js. Ensure multi-step processes survive crashes and resume automatically.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",