From 1b2a5f4083d80a44408afec075e1af43bf55667c Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 08:57:36 +0000 Subject: [PATCH] fix: exclude initializeState from TaskEntity method dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TaskEntity.findMethod() walks the prototype chain to find methods matching an entity operation name. It excludes 'constructor' and 'run', but not 'initializeState'. When users override initializeState() in their entity subclass (which is the common pattern), it becomes discoverable and can be invoked as a regular entity operation by external callers. This allows any client to send an 'initializeState' operation, silently resetting the entity's state to its initial value — a security and correctness issue. Add 'initializeState' to the exclusion list in findMethod() and add tests verifying lifecycle methods are not dispatchable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/entities/task-entity.ts | 2 +- .../durabletask-js/test/task-entity.spec.ts | 53 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/packages/durabletask-js/src/entities/task-entity.ts b/packages/durabletask-js/src/entities/task-entity.ts index 0eb8db1..6f01216 100644 --- a/packages/durabletask-js/src/entities/task-entity.ts +++ b/packages/durabletask-js/src/entities/task-entity.ts @@ -169,7 +169,7 @@ export abstract class TaskEntity implements ITaskEntity { if (name.toLowerCase() === operationName) { const prop = (this as unknown as Record)[name]; // Skip non-functions and built-in methods - if (typeof prop === "function" && name !== "constructor" && name !== "run") { + if (typeof prop === "function" && name !== "constructor" && name !== "run" && name !== "initializeState") { return name; } } diff --git a/packages/durabletask-js/test/task-entity.spec.ts b/packages/durabletask-js/test/task-entity.spec.ts index cff12fd..ae0c46e 100644 --- a/packages/durabletask-js/test/task-entity.spec.ts +++ b/packages/durabletask-js/test/task-entity.spec.ts @@ -422,4 +422,57 @@ describe("TaskEntity", () => { ); }); }); + + describe("lifecycle method exclusion", () => { + it("should not dispatch to overridden initializeState as an operation", async () => { + const entity = new CounterEntity(); + const { operation } = createMockOperation("initializeState", undefined, { count: 42 }); + + // initializeState is a lifecycle method, not a user-facing operation. + // Even though CounterEntity overrides it, it should not be callable. + await expect(entity.run(operation)).rejects.toThrow( + "No suitable method found for entity operation 'initializeState'", + ); + }); + + it("should not dispatch to initializeState case-insensitively", async () => { + const entity = new CounterEntity(); + const { operation } = createMockOperation("INITIALIZESTATE", undefined, { count: 42 }); + + await expect(entity.run(operation)).rejects.toThrow( + "No suitable method found for entity operation 'INITIALIZESTATE'", + ); + }); + + it("should still call initializeState internally when state is null", async () => { + const entity = new CounterEntity(); + // No initial state — initializeState should be called internally + const { operation } = createMockOperation("get", undefined, undefined); + + const result = await entity.run(operation); + + // initializeState returns { count: 0 }, so get() returns 0 + expect(result).toBe(0); + }); + + it("should not dispatch to 'run' as an operation", async () => { + // Verify that 'run' remains excluded from dispatch + class SimpleEntity extends TaskEntity<{ value: number }> { + getValue(): number { + return this.state.value; + } + + protected initializeState(): { value: number } { + return { value: 0 }; + } + } + + const entity = new SimpleEntity(); + const { operation } = createMockOperation("run", undefined, { value: 10 }); + + await expect(entity.run(operation)).rejects.toThrow( + "No suitable method found for entity operation 'run'", + ); + }); + }); });