From 2faf0c1ba5ddfe22e9e93f5787ad97dbfc18d8da Mon Sep 17 00:00:00 2001 From: James Watkins-Harvey Date: Mon, 10 Aug 2026 13:34:52 -0400 Subject: [PATCH 1/4] Don't assume default-valued proto fields survive a round trip The two protobuf data converter features build their expectation with DataBlob.create({ encodingType: ENCODING_TYPE_UNSPECIFIED, ... }), which sets an enum to its zero value, and then deepEqual it against a decoded message. protobufjs 7 materialized proto3 implicit-presence fields holding their default value as own properties when decoding; protobufjs 8 leaves them absent. Since deepEqual compares own properties, the expectation fails against any SDK built on protobufjs 8. Compare against an expectation that has itself been through the wire, so the two sides have the same shape under either version. --- features/data_converter/binary_protobuf/feature.ts | 12 ++++++++++-- features/data_converter/json_protobuf/feature.ts | 12 ++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/features/data_converter/binary_protobuf/feature.ts b/features/data_converter/binary_protobuf/feature.ts index ed60d7ec..71126b0e 100644 --- a/features/data_converter/binary_protobuf/feature.ts +++ b/features/data_converter/binary_protobuf/feature.ts @@ -12,6 +12,14 @@ const expectedResult = proto.temporal.api.common.v1.DataBlob.create({ data: new Uint8Array([0xde, 0xad, 0xbe, 0xef]), }); +// `encodingType` above holds its default value, so it never makes it onto the wire. protobufjs 7 +// nonetheless materialized such fields as own properties when decoding, whereas protobufjs 8 leaves +// them absent; `deepEqual` compares own properties, so the two disagree. Compare against an +// expectation that has been through the wire itself, which holds for either version. +const expectedResultOnWire = proto.temporal.api.common.v1.DataBlob.decode( + proto.temporal.api.common.v1.DataBlob.encode(expectedResult).finish(), +); + // An "echo" workflow export async function workflow( res: proto.temporal.api.common.v1.DataBlob, @@ -30,7 +38,7 @@ export const feature = new Feature({ async checkResult(runner, handle) { // verify client result is DataBlob `0xdeadbeef` const result = await handle.result(); - assert.deepEqual(result, expectedResult); + assert.deepEqual(result, expectedResultOnWire); // get result payload of WorkflowExecutionCompleted event from workflow history const payload = await runner.getWorkflowResultPayload(handle); @@ -45,7 +53,7 @@ export const feature = new Feature({ assert.ok(payload.data); const resultInHistory = proto.temporal.api.common.v1.DataBlob.decode(payload.data); - assert.deepEqual(resultInHistory, expectedResult); + assert.deepEqual(resultInHistory, expectedResultOnWire); // get argument payload of WorkflowExecutionStarted event from workflow history const payloadArg = await runner.getWorkflowArgumentPayload(handle); diff --git a/features/data_converter/json_protobuf/feature.ts b/features/data_converter/json_protobuf/feature.ts index fa59a758..1f55c7b8 100644 --- a/features/data_converter/json_protobuf/feature.ts +++ b/features/data_converter/json_protobuf/feature.ts @@ -19,6 +19,14 @@ const expectedResult = proto.temporal.api.common.v1.DataBlob.create({ data: new Uint8Array([0xde, 0xad, 0xbe, 0xef]), }); +// `encodingType` above holds its default value, so ProtoJSON omits it. protobufjs 7 nonetheless +// materialized such fields as own properties when decoding, whereas protobufjs 8 leaves them +// absent; `deepEqual` compares own properties, so the two disagree. Compare against an expectation +// that has been through the wire itself, which holds for either version. +const expectedResultOnWire = proto.temporal.api.common.v1.DataBlob.decode( + proto.temporal.api.common.v1.DataBlob.encode(expectedResult).finish(), +); + // An "echo" workflow export async function workflow( res: proto.temporal.api.common.v1.DataBlob, @@ -37,7 +45,7 @@ export const feature = new Feature({ async checkResult(runner, handle) { // verify client result is DataBlob `0xdeadbeef` const result = await handle.result(); - assert.deepEqual(result, expectedResult); + assert.deepEqual(result, expectedResultOnWire); // get result payload of WorkflowExecutionCompleted event from workflow history const payload = await runner.getWorkflowResultPayload(handle); @@ -52,7 +60,7 @@ export const feature = new Feature({ assert.ok(payload.data); const resultInHistory = fromProto3JSON(dataBlobType, JSON.parse(decode(payload.data))); assert.ok(resultInHistory); - assert.deepEqual(resultInHistory, expectedResult); + assert.deepEqual(resultInHistory, expectedResultOnWire); // get argument payload of WorkflowExecutionStarted event from workflow history const payloadArg = await runner.getWorkflowArgumentPayload(handle); From 4235442651df34cc7c6e837eab7aab506315d094 Mon Sep 17 00:00:00 2001 From: James Watkins-Harvey Date: Mon, 10 Aug 2026 14:09:12 -0400 Subject: [PATCH 2/4] Bump protobufjs to v8 and drop proto3-json-serializer The TS SDK is moving to protobufjs 8, and its CHANGELOG documents the same upgrade as a requirement for applications that use the Protobuf payload converters with their own message definitions. Our e2e tests should model what we ask users to do, so json_protobuf now parses ProtoJSON with protobufjs/ext/protojson instead of proto3-json-serializer. The pnpm override pinning protobufjs 7.5.1 (#625) worked around temporalio/sdk-typescript#1717, which protobufjs 8 fixes upstream, so it goes away rather than being bumped. It was never needed to keep a single protobufjs instance either: the generated program maps every bare specifier to its own node_modules through tsconfig-paths, which is what lets protojson's `instanceof Type` check hold across the SDK under test. --- .../data_converter/json_protobuf/feature.ts | 6 +- package-lock.json | 58 +++++++++---------- package.json | 3 +- sdkbuild/typescript.go | 7 +-- 4 files changed, 33 insertions(+), 41 deletions(-) diff --git a/features/data_converter/json_protobuf/feature.ts b/features/data_converter/json_protobuf/feature.ts index 1f55c7b8..267002d0 100644 --- a/features/data_converter/json_protobuf/feature.ts +++ b/features/data_converter/json_protobuf/feature.ts @@ -1,5 +1,5 @@ import * as assert from 'assert'; -import { fromProto3JSON } from 'proto3-json-serializer'; +import * as protojson from 'protobufjs/ext/protojson'; import { Feature } from '@temporalio/harness'; import * as proto from '@temporalio/proto'; import { patchProtobufRoot } from '@temporalio/proto/lib/patch-protobuf-root'; @@ -58,7 +58,7 @@ export const feature = new Feature({ assert.equal(Buffer.from(payload.metadata.messageType).toString(), 'temporal.api.common.v1.DataBlob'); assert.ok(payload.data); - const resultInHistory = fromProto3JSON(dataBlobType, JSON.parse(decode(payload.data))); + const resultInHistory = protojson.fromJson(dataBlobType, JSON.parse(decode(payload.data))); assert.ok(resultInHistory); assert.deepEqual(resultInHistory, expectedResultOnWire); @@ -73,7 +73,7 @@ export const feature = new Feature({ assert.equal(Buffer.from(payloadArg.metadata.messageType).toString(), 'temporal.api.common.v1.DataBlob'); assert.ok(payloadArg.data); - const resultArgInHistory = fromProto3JSON(dataBlobType, JSON.parse(decode(payloadArg.data))); + const resultArgInHistory = protojson.fromJson(dataBlobType, JSON.parse(decode(payloadArg.data))); assert.ok(resultArgInHistory); assert.deepEqual(resultInHistory, resultArgInHistory); }, diff --git a/package-lock.json b/package-lock.json index 01544086..73cda0eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,8 +18,7 @@ "commander": "^8.3.0", "ms": "^3.0.0-canary.1", "nexus-rpc": "^0.0.1", - "proto3-json-serializer": "^1.1.1", - "protobufjs": "7.5.1" + "protobufjs": "^8.7.1" }, "devDependencies": { "@tsconfig/node24": "^24.0.4", @@ -1281,6 +1280,29 @@ "node": ">=14.0.0" } }, + "node_modules/@temporalio/common/node_modules/protobufjs": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/@temporalio/core-bridge": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/@temporalio/core-bridge/-/core-bridge-1.18.1.tgz", @@ -5024,37 +5046,13 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/proto3-json-serializer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-1.1.1.tgz", - "integrity": "sha512-AwAuY4g9nxx0u52DnSMkqqgyLHaW/XaPLtaAo3y/ZCfeaQB/g4YDH4kb8Wc/mWzWvu0YjOznVnfn373MVZZrgw==", - "license": "Apache-2.0", - "dependencies": { - "protobufjs": "^7.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/protobufjs": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.1.tgz", - "integrity": "sha512-3qx3IRjR9WPQKagdwrKjO3Gu8RgQR2qqw+1KnigWhoVjFqegIj1K3bP11sGqhxrO46/XL7lekuG4jmjL+4cLsw==", - "hasInstallScript": true, + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.7.1.tgz", + "integrity": "sha512-agdGHrXNTv0IrYscJPDou/PlEJk1c/hBZ9o/B5NH2i/nSPtPqacNxzgwf1CebXxFMjMrZH5sqv9uQuw96aGt/A==", "license": "BSD-3-Clause", "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" + "long": "^5.3.2" }, "engines": { "node": ">=12.0.0" diff --git a/package.json b/package.json index 9edd2c25..2a6b9eb4 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,7 @@ "commander": "^8.3.0", "ms": "^3.0.0-canary.1", "nexus-rpc": "^0.0.1", - "proto3-json-serializer": "^1.1.1", - "protobufjs": "7.5.1" + "protobufjs": "^8.7.1" }, "devDependencies": { "@tsconfig/node24": "^24.0.4", diff --git a/sdkbuild/typescript.go b/sdkbuild/typescript.go index 3ba3db5a..3334bf38 100644 --- a/sdkbuild/typescript.go +++ b/sdkbuild/typescript.go @@ -141,18 +141,13 @@ func BuildTypeScriptProgram(ctx context.Context, options BuildTypeScriptProgramO "commander": "^8.3.0", "ms": "^3.0.0-canary.1", "nexus-rpc": "^0.0.1", - "proto3-json-serializer": "^1.1.1" + "protobufjs": "^8.7.1" }, "devDependencies": { "@tsconfig/node24": "^24.0.4", "@types/node": "^24.1.0", "tsconfig-paths": "^3.12.0", "typescript": "^5.9.3" - }, - "pnpm": { - "overrides": { - "protobufjs": "7.5.1" - } } }` if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte(packageJSON), 0644); err != nil { From 036113d8736804b4e1c85970d5d7b5591f44a55a Mon Sep 17 00:00:00 2001 From: James Watkins-Harvey Date: Mon, 10 Aug 2026 14:32:03 -0400 Subject: [PATCH 3/4] Trim down comments --- features/data_converter/binary_protobuf/feature.ts | 6 ++---- features/data_converter/json_protobuf/feature.ts | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/features/data_converter/binary_protobuf/feature.ts b/features/data_converter/binary_protobuf/feature.ts index 71126b0e..6201a865 100644 --- a/features/data_converter/binary_protobuf/feature.ts +++ b/features/data_converter/binary_protobuf/feature.ts @@ -12,10 +12,8 @@ const expectedResult = proto.temporal.api.common.v1.DataBlob.create({ data: new Uint8Array([0xde, 0xad, 0xbe, 0xef]), }); -// `encodingType` above holds its default value, so it never makes it onto the wire. protobufjs 7 -// nonetheless materialized such fields as own properties when decoding, whereas protobufjs 8 leaves -// them absent; `deepEqual` compares own properties, so the two disagree. Compare against an -// expectation that has been through the wire itself, which holds for either version. +// Do an encode/decode roundtrip to make sure our test expectations match exactly +// what protobufjs 8 will produce (e.g. default values are omitted, etc.) const expectedResultOnWire = proto.temporal.api.common.v1.DataBlob.decode( proto.temporal.api.common.v1.DataBlob.encode(expectedResult).finish(), ); diff --git a/features/data_converter/json_protobuf/feature.ts b/features/data_converter/json_protobuf/feature.ts index 267002d0..87db0fce 100644 --- a/features/data_converter/json_protobuf/feature.ts +++ b/features/data_converter/json_protobuf/feature.ts @@ -19,10 +19,8 @@ const expectedResult = proto.temporal.api.common.v1.DataBlob.create({ data: new Uint8Array([0xde, 0xad, 0xbe, 0xef]), }); -// `encodingType` above holds its default value, so ProtoJSON omits it. protobufjs 7 nonetheless -// materialized such fields as own properties when decoding, whereas protobufjs 8 leaves them -// absent; `deepEqual` compares own properties, so the two disagree. Compare against an expectation -// that has been through the wire itself, which holds for either version. +// Do an encode/decode roundtrip to make sure our test expectations match exactly +// what protobufjs 8 will produce (e.g. default values are omitted, etc.) const expectedResultOnWire = proto.temporal.api.common.v1.DataBlob.decode( proto.temporal.api.common.v1.DataBlob.encode(expectedResult).finish(), ); From 6fe87ed6e9590b2d88fe76afd7b4a0ac524006c1 Mon Sep 17 00:00:00 2001 From: James Watkins-Harvey Date: Mon, 10 Aug 2026 14:42:58 -0400 Subject: [PATCH 4/4] Drop the Buffer/Uint8Array injection workarounds Both protobuf data converter features reached into the Node realm to overwrite a global inside the workflow sandbox: binary_protobuf replaced Uint8Array, and json_protobuf replaced Buffer. They date back to #286 and reference an unnamed SDK bug around how `bytes` fields cross the sandbox boundary. The SDK now normalizes protobufjs's `Buffer` allocations to `Uint8Array` when decoding, so neither injection is needed; both features pass without them. --- features/data_converter/binary_protobuf/feature.ts | 5 ----- features/data_converter/json_protobuf/feature.ts | 5 ----- 2 files changed, 10 deletions(-) diff --git a/features/data_converter/binary_protobuf/feature.ts b/features/data_converter/binary_protobuf/feature.ts index 6201a865..3d0f3854 100644 --- a/features/data_converter/binary_protobuf/feature.ts +++ b/features/data_converter/binary_protobuf/feature.ts @@ -2,11 +2,6 @@ import * as assert from 'assert'; import { Feature } from '@temporalio/harness'; import * as proto from '@temporalio/proto'; -// Inject Buffer and Uint8Array from the node context to the workflow context to workaround SDK bug -// TODO(antlai-temporal) Remove when SDK bug is fixed -const g = globalThis as any; -g.Uint8Array = g.constructor.constructor('return globalThis.Uint8Array')(); - const expectedResult = proto.temporal.api.common.v1.DataBlob.create({ encodingType: proto.temporal.api.enums.v1.EncodingType.ENCODING_TYPE_UNSPECIFIED, data: new Uint8Array([0xde, 0xad, 0xbe, 0xef]), diff --git a/features/data_converter/json_protobuf/feature.ts b/features/data_converter/json_protobuf/feature.ts index 87db0fce..d1853c09 100644 --- a/features/data_converter/json_protobuf/feature.ts +++ b/features/data_converter/json_protobuf/feature.ts @@ -9,11 +9,6 @@ import { decode } from '@temporalio/common/lib/encoding'; const patched = patchProtobufRoot(proto) as any; const dataBlobType = patched.lookupType('temporal.api.common.v1.DataBlob'); -// Inject Buffer and Uint8Array from the node context to the workflow context to workaround SDK bug -// TODO(antlai-temporal) Remove workaround when SDK bug is fixed -const g = globalThis as any; -g.Buffer = g.constructor.constructor('return globalThis.Buffer')(); - const expectedResult = proto.temporal.api.common.v1.DataBlob.create({ encodingType: proto.temporal.api.enums.v1.EncodingType.ENCODING_TYPE_UNSPECIFIED, data: new Uint8Array([0xde, 0xad, 0xbe, 0xef]),