resumable-workflow 1.4.0 → 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 +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +68 -19
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +68 -19
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -68,7 +68,11 @@ declare class FileStorage implements StorageProvider {
|
|
|
68
68
|
private readonly baseDir;
|
|
69
69
|
constructor(baseDir?: string);
|
|
70
70
|
private ensureDir;
|
|
71
|
+
private validateRunId;
|
|
71
72
|
private getFilePath;
|
|
73
|
+
private isValidRunFileName;
|
|
74
|
+
private isValidWorkflowStatus;
|
|
75
|
+
private isValidRunStateShape;
|
|
72
76
|
saveRun(state: WorkflowRunState<unknown, Record<string, unknown>>): Promise<void>;
|
|
73
77
|
getRun(runId: string): Promise<WorkflowRunState<unknown, Record<string, unknown>> | null>;
|
|
74
78
|
listIncompleteRuns(workflowId: string): Promise<WorkflowRunState<unknown, Record<string, unknown>>[]>;
|
package/dist/index.d.ts
CHANGED
|
@@ -68,7 +68,11 @@ declare class FileStorage implements StorageProvider {
|
|
|
68
68
|
private readonly baseDir;
|
|
69
69
|
constructor(baseDir?: string);
|
|
70
70
|
private ensureDir;
|
|
71
|
+
private validateRunId;
|
|
71
72
|
private getFilePath;
|
|
73
|
+
private isValidRunFileName;
|
|
74
|
+
private isValidWorkflowStatus;
|
|
75
|
+
private isValidRunStateShape;
|
|
72
76
|
saveRun(state: WorkflowRunState<unknown, Record<string, unknown>>): Promise<void>;
|
|
73
77
|
getRun(runId: string): Promise<WorkflowRunState<unknown, Record<string, unknown>> | null>;
|
|
74
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);
|
|
@@ -49,8 +50,45 @@ var FileStorage = class {
|
|
|
49
50
|
async ensureDir() {
|
|
50
51
|
await import_promises.default.mkdir(this.baseDir, { recursive: true });
|
|
51
52
|
}
|
|
53
|
+
validateRunId(runId) {
|
|
54
|
+
if (!runId || typeof runId !== "string") {
|
|
55
|
+
throw new Error(`Invalid runId: ${runId}`);
|
|
56
|
+
}
|
|
57
|
+
if (runId.includes("../") || runId.includes("..\\") || runId.startsWith("..")) {
|
|
58
|
+
throw new Error(`Invalid runId: Path traversal detected in ${runId}`);
|
|
59
|
+
}
|
|
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
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
52
66
|
getFilePath(runId) {
|
|
53
|
-
|
|
67
|
+
this.validateRunId(runId);
|
|
68
|
+
const filePath = import_node_path.default.join(this.baseDir, `${runId}.json`);
|
|
69
|
+
const resolvedPath = import_node_path.default.resolve(filePath);
|
|
70
|
+
const resolvedBaseDir = import_node_path.default.resolve(this.baseDir);
|
|
71
|
+
if (!resolvedPath.startsWith(resolvedBaseDir)) {
|
|
72
|
+
throw new Error("Invalid runId: Path traversal detected");
|
|
73
|
+
}
|
|
74
|
+
return resolvedPath;
|
|
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);
|
|
54
92
|
}
|
|
55
93
|
async saveRun(state) {
|
|
56
94
|
await this.ensureDir();
|
|
@@ -60,10 +98,14 @@ var FileStorage = class {
|
|
|
60
98
|
);
|
|
61
99
|
}
|
|
62
100
|
async getRun(runId) {
|
|
101
|
+
this.validateRunId(runId);
|
|
63
102
|
try {
|
|
64
103
|
const data = await import_promises.default.readFile(this.getFilePath(runId), "utf-8");
|
|
65
104
|
return JSON.parse(data);
|
|
66
|
-
} catch (
|
|
105
|
+
} catch (e) {
|
|
106
|
+
if (e.message.includes("Invalid runId") || e.message.includes("Path traversal")) {
|
|
107
|
+
throw e;
|
|
108
|
+
}
|
|
67
109
|
return null;
|
|
68
110
|
}
|
|
69
111
|
}
|
|
@@ -79,18 +121,22 @@ var FileStorage = class {
|
|
|
79
121
|
const files = await import_promises.default.readdir(this.baseDir);
|
|
80
122
|
const runs = [];
|
|
81
123
|
for (const file of files) {
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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;
|
|
93
135
|
}
|
|
136
|
+
if (run.workflowId === workflowId && run.status === status) {
|
|
137
|
+
runs.push(run);
|
|
138
|
+
}
|
|
139
|
+
} catch (_e) {
|
|
94
140
|
}
|
|
95
141
|
}
|
|
96
142
|
return runs;
|
|
@@ -99,9 +145,13 @@ var FileStorage = class {
|
|
|
99
145
|
}
|
|
100
146
|
}
|
|
101
147
|
async deleteRun(runId) {
|
|
148
|
+
this.validateRunId(runId);
|
|
102
149
|
try {
|
|
103
150
|
await import_promises.default.unlink(this.getFilePath(runId));
|
|
104
|
-
} catch (
|
|
151
|
+
} catch (e) {
|
|
152
|
+
if (e.message.includes("Invalid runId") || e.message.includes("Path traversal")) {
|
|
153
|
+
throw e;
|
|
154
|
+
}
|
|
105
155
|
}
|
|
106
156
|
}
|
|
107
157
|
};
|
|
@@ -114,10 +164,7 @@ var Workflow = class {
|
|
|
114
164
|
this.workflowId = config.id;
|
|
115
165
|
if (config.autoResume) {
|
|
116
166
|
this.resumeAllIncomplete().catch((err) => {
|
|
117
|
-
console.error(
|
|
118
|
-
`[ResumableWorkflow: ${this.workflowId}] Auto-resume failed:`,
|
|
119
|
-
err
|
|
120
|
-
);
|
|
167
|
+
console.error("[ResumableWorkflow] Auto-resume failed:", err);
|
|
121
168
|
});
|
|
122
169
|
}
|
|
123
170
|
}
|
|
@@ -229,7 +276,9 @@ var Workflow = class {
|
|
|
229
276
|
*/
|
|
230
277
|
async clearCompleted() {
|
|
231
278
|
const completed = await this.storage.listCompletedRuns(this.workflowId);
|
|
232
|
-
await Promise.all(
|
|
279
|
+
await Promise.all(
|
|
280
|
+
completed.map((run) => this.storage.deleteRun(run.runId))
|
|
281
|
+
);
|
|
233
282
|
}
|
|
234
283
|
};
|
|
235
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 getFilePath(runId: string): string {\n return path.join(this.baseDir, `${runId}.json`);\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 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 // 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 try {\n await fs.unlink(this.getFilePath(runId));\n } catch (_e) {\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);
|
|
@@ -11,8 +12,45 @@ var FileStorage = class {
|
|
|
11
12
|
async ensureDir() {
|
|
12
13
|
await fs.mkdir(this.baseDir, { recursive: true });
|
|
13
14
|
}
|
|
15
|
+
validateRunId(runId) {
|
|
16
|
+
if (!runId || typeof runId !== "string") {
|
|
17
|
+
throw new Error(`Invalid runId: ${runId}`);
|
|
18
|
+
}
|
|
19
|
+
if (runId.includes("../") || runId.includes("..\\") || runId.startsWith("..")) {
|
|
20
|
+
throw new Error(`Invalid runId: Path traversal detected in ${runId}`);
|
|
21
|
+
}
|
|
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
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
14
28
|
getFilePath(runId) {
|
|
15
|
-
|
|
29
|
+
this.validateRunId(runId);
|
|
30
|
+
const filePath = path.join(this.baseDir, `${runId}.json`);
|
|
31
|
+
const resolvedPath = path.resolve(filePath);
|
|
32
|
+
const resolvedBaseDir = path.resolve(this.baseDir);
|
|
33
|
+
if (!resolvedPath.startsWith(resolvedBaseDir)) {
|
|
34
|
+
throw new Error("Invalid runId: Path traversal detected");
|
|
35
|
+
}
|
|
36
|
+
return resolvedPath;
|
|
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);
|
|
16
54
|
}
|
|
17
55
|
async saveRun(state) {
|
|
18
56
|
await this.ensureDir();
|
|
@@ -22,10 +60,14 @@ var FileStorage = class {
|
|
|
22
60
|
);
|
|
23
61
|
}
|
|
24
62
|
async getRun(runId) {
|
|
63
|
+
this.validateRunId(runId);
|
|
25
64
|
try {
|
|
26
65
|
const data = await fs.readFile(this.getFilePath(runId), "utf-8");
|
|
27
66
|
return JSON.parse(data);
|
|
28
|
-
} catch (
|
|
67
|
+
} catch (e) {
|
|
68
|
+
if (e.message.includes("Invalid runId") || e.message.includes("Path traversal")) {
|
|
69
|
+
throw e;
|
|
70
|
+
}
|
|
29
71
|
return null;
|
|
30
72
|
}
|
|
31
73
|
}
|
|
@@ -41,18 +83,22 @@ var FileStorage = class {
|
|
|
41
83
|
const files = await fs.readdir(this.baseDir);
|
|
42
84
|
const runs = [];
|
|
43
85
|
for (const file of files) {
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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;
|
|
55
97
|
}
|
|
98
|
+
if (run.workflowId === workflowId && run.status === status) {
|
|
99
|
+
runs.push(run);
|
|
100
|
+
}
|
|
101
|
+
} catch (_e) {
|
|
56
102
|
}
|
|
57
103
|
}
|
|
58
104
|
return runs;
|
|
@@ -61,9 +107,13 @@ var FileStorage = class {
|
|
|
61
107
|
}
|
|
62
108
|
}
|
|
63
109
|
async deleteRun(runId) {
|
|
110
|
+
this.validateRunId(runId);
|
|
64
111
|
try {
|
|
65
112
|
await fs.unlink(this.getFilePath(runId));
|
|
66
|
-
} catch (
|
|
113
|
+
} catch (e) {
|
|
114
|
+
if (e.message.includes("Invalid runId") || e.message.includes("Path traversal")) {
|
|
115
|
+
throw e;
|
|
116
|
+
}
|
|
67
117
|
}
|
|
68
118
|
}
|
|
69
119
|
};
|
|
@@ -76,10 +126,7 @@ var Workflow = class {
|
|
|
76
126
|
this.workflowId = config.id;
|
|
77
127
|
if (config.autoResume) {
|
|
78
128
|
this.resumeAllIncomplete().catch((err) => {
|
|
79
|
-
console.error(
|
|
80
|
-
`[ResumableWorkflow: ${this.workflowId}] Auto-resume failed:`,
|
|
81
|
-
err
|
|
82
|
-
);
|
|
129
|
+
console.error("[ResumableWorkflow] Auto-resume failed:", err);
|
|
83
130
|
});
|
|
84
131
|
}
|
|
85
132
|
}
|
|
@@ -191,7 +238,9 @@ var Workflow = class {
|
|
|
191
238
|
*/
|
|
192
239
|
async clearCompleted() {
|
|
193
240
|
const completed = await this.storage.listCompletedRuns(this.workflowId);
|
|
194
|
-
await Promise.all(
|
|
241
|
+
await Promise.all(
|
|
242
|
+
completed.map((run) => this.storage.deleteRun(run.runId))
|
|
243
|
+
);
|
|
195
244
|
}
|
|
196
245
|
};
|
|
197
246
|
function createWorkflow(config) {
|
package/dist/index.mjs.map
CHANGED
|
@@ -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 getFilePath(runId: string): string {\n return path.join(this.baseDir, `${runId}.json`);\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 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 // 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 try {\n await fs.unlink(this.getFilePath(runId));\n } catch (_e) {\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.
|
|
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",
|