From 707891969889e32c361e203829e403f2e5e0d1af Mon Sep 17 00:00:00 2001 From: juslesan Date: Mon, 22 Dec 2025 14:46:55 +0200 Subject: [PATCH 01/28] Draft: validation in worker thread --- packages/sdk/package.json | 5 +- .../signature/BrowserSignatureValidation.mts | 38 ++++++++++ .../signature/ServerSignatureValidation.ts | 20 +++++ .../signature/SignatureValidationContext.ts | 12 +++ .../signature/SignatureValidationWorker.ts | 18 +++++ .../sdk/src/signature/SignatureValidator.ts | 74 ++++++++++--------- .../sdk/src/signature/signatureValidation.ts | 72 ++++++++++++++++++ packages/sdk/webpack.config.js | 3 +- 8 files changed, 205 insertions(+), 37 deletions(-) create mode 100644 packages/sdk/src/signature/BrowserSignatureValidation.mts create mode 100644 packages/sdk/src/signature/ServerSignatureValidation.ts create mode 100644 packages/sdk/src/signature/SignatureValidationContext.ts create mode 100644 packages/sdk/src/signature/SignatureValidationWorker.ts create mode 100644 packages/sdk/src/signature/signatureValidation.ts diff --git a/packages/sdk/package.json b/packages/sdk/package.json index e586b916f5..80ed9679ec 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -13,7 +13,9 @@ "script": "./dist/streamr-sdk.web.min.js", "browser": { "./src/utils/persistence/ServerPersistence.ts": "./src/utils/persistence/BrowserPersistence.mts", - "./dist/src/utils/persistence/ServerPersistence.js": "./dist/src/utils/persistence/BrowserPersistence.mjs" + "./dist/src/utils/persistence/ServerPersistence.js": "./dist/src/utils/persistence/BrowserPersistence.mjs", + "./src/signature/ServerSignatureValidation.ts": "./src/signature/BrowserSignatureValidation.mts", + "./dist/src/signature/ServerSignatureValidation.js": "./dist/src/signature/BrowserSignatureValidation.mjs" }, "exports": { "default": { @@ -93,6 +95,7 @@ "#IMPORTANT": "babel-runtime must be in dependencies, not devDependencies", "dependencies": { "@babel/runtime": "^7.28.4", + "comlink": "^4.4.2", "@babel/runtime-corejs3": "^7.28.4", "@noble/post-quantum": "^0.4.1", "@protobuf-ts/runtime": "^2.8.2", diff --git a/packages/sdk/src/signature/BrowserSignatureValidation.mts b/packages/sdk/src/signature/BrowserSignatureValidation.mts new file mode 100644 index 0000000000..771ecf0965 --- /dev/null +++ b/packages/sdk/src/signature/BrowserSignatureValidation.mts @@ -0,0 +1,38 @@ +/** + * Browser implementation of signature validation using Web Worker. + * This offloads CPU-intensive cryptographic operations to a separate thread. + */ +import * as Comlink from 'comlink' +import { SignatureValidationContext } from './SignatureValidationContext.js' +import { SignatureValidationResult } from './signatureValidation.js' +import type { SignatureValidationWorkerApi } from './SignatureValidationWorker.js' +import { StreamMessage } from '../protocol/StreamMessage.js' + +export default class BrowserSignatureValidation implements SignatureValidationContext { + private worker: Worker | null = null + private workerApi: Comlink.Remote | null = null + + private ensureWorker(): Comlink.Remote { + if (!this.workerApi) { + // Webpack 5 handles this pattern automatically, creating a separate chunk for the worker + this.worker = new Worker( + /* webpackChunkName: "signature-worker" */ + new URL('./SignatureValidationWorker.js', import.meta.url) + ) + this.workerApi = Comlink.wrap(this.worker) + } + return this.workerApi + } + + async validateSignature(message: StreamMessage): Promise { + return this.ensureWorker().validateSignature(message) + } + + destroy(): void { + if (this.worker) { + this.worker.terminate() + this.worker = null + } + this.workerApi = null + } +} diff --git a/packages/sdk/src/signature/ServerSignatureValidation.ts b/packages/sdk/src/signature/ServerSignatureValidation.ts new file mode 100644 index 0000000000..fefce283b3 --- /dev/null +++ b/packages/sdk/src/signature/ServerSignatureValidation.ts @@ -0,0 +1,20 @@ +/** + * Node.js implementation of signature validation. + * Runs on the main thread (worker threads can be added later if needed). + */ +import { StreamMessage } from '../protocol/StreamMessage' +import { SignatureValidationContext } from './SignatureValidationContext' +import { SignatureValidationResult, validateSignatureData } from './signatureValidation' + +export default class ServerSignatureValidation implements SignatureValidationContext { + + async validateSignature(message: StreamMessage): Promise { + return validateSignatureData(message) + } + + // eslint-disable-next-line class-methods-use-this + destroy(): void { + // No-op for server implementation + } +} + diff --git a/packages/sdk/src/signature/SignatureValidationContext.ts b/packages/sdk/src/signature/SignatureValidationContext.ts new file mode 100644 index 0000000000..77f8afb4a0 --- /dev/null +++ b/packages/sdk/src/signature/SignatureValidationContext.ts @@ -0,0 +1,12 @@ +/** + * Interface for signature validation backend. + * Browser implementation uses a Web Worker, Node.js runs on main thread. + */ +import { StreamMessage } from '../protocol/StreamMessage' +import { SignatureValidationResult } from './signatureValidation' + +export interface SignatureValidationContext { + validateSignature(message: StreamMessage): Promise + destroy(): void +} + diff --git a/packages/sdk/src/signature/SignatureValidationWorker.ts b/packages/sdk/src/signature/SignatureValidationWorker.ts new file mode 100644 index 0000000000..21f14288ca --- /dev/null +++ b/packages/sdk/src/signature/SignatureValidationWorker.ts @@ -0,0 +1,18 @@ +/** + * Web Worker for signature validation. + * This worker handles CPU-intensive cryptographic operations off the main thread. + */ +import * as Comlink from 'comlink' +import { validateSignatureData, SignatureValidationResult } from './signatureValidation' +import { StreamMessage } from '../protocol/StreamMessage' + +const workerApi = { + validateSignature: async (data: StreamMessage): Promise => { + return validateSignatureData(data) + } +} + +export type SignatureValidationWorkerApi = typeof workerApi + +Comlink.expose(workerApi) + diff --git a/packages/sdk/src/signature/SignatureValidator.ts b/packages/sdk/src/signature/SignatureValidator.ts index 0c7ba17605..7f7d10f9e1 100644 --- a/packages/sdk/src/signature/SignatureValidator.ts +++ b/packages/sdk/src/signature/SignatureValidator.ts @@ -1,26 +1,33 @@ -import { toEthereumAddress, toUserIdRaw, SigningUtil } from '@streamr/utils' +import { toEthereumAddress } from '@streamr/utils' import { Lifecycle, scoped } from 'tsyringe' import { ERC1271ContractFacade } from '../contracts/ERC1271ContractFacade' +import { DestroySignal } from '../DestroySignal' import { StreamMessage } from '../protocol/StreamMessage' import { StreamrClientError } from '../StreamrClientError' -import { createLegacySignaturePayload } from './createLegacySignaturePayload' import { createSignaturePayload } from './createSignaturePayload' +import { SignatureValidationContext } from './SignatureValidationContext' +// This import will be swapped to BrowserSignatureValidation.mts in browser builds +import SignatureValidation from './ServerSignatureValidation' import { SignatureType } from '@streamr/trackerless-network' -import { IDENTITY_MAPPING } from '../identity/IdentityMapping' - -// Lookup structure SignatureType -> SigningUtil -const signingUtilBySignatureType: Record = Object.fromEntries( - IDENTITY_MAPPING.map((idMapping) => [idMapping.signatureType, SigningUtil.getInstance(idMapping.keyType)]) -) - -const evmSigner = SigningUtil.getInstance('ECDSA_SECP256K1_EVM') @scoped(Lifecycle.ContainerScoped) export class SignatureValidator { private readonly erc1271ContractFacade: ERC1271ContractFacade + private validationContext: SignatureValidationContext | undefined - constructor(erc1271ContractFacade: ERC1271ContractFacade) { + constructor( + erc1271ContractFacade: ERC1271ContractFacade, + destroySignal: DestroySignal + ) { this.erc1271ContractFacade = erc1271ContractFacade + destroySignal.onDestroy.listen(() => this.destroy()) + } + + private getValidationContext(): SignatureValidationContext { + if (!this.validationContext) { + this.validationContext = new SignatureValidation() + } + return this.validationContext } /** @@ -41,36 +48,33 @@ export class SignatureValidator { } private async validate(streamMessage: StreamMessage): Promise { - const signingUtil = signingUtilBySignatureType[streamMessage.signatureType] - - // Common case - if (signingUtil) { - return signingUtil.verifySignature( - toUserIdRaw(streamMessage.getPublisherId()), - createSignaturePayload(streamMessage), - streamMessage.signature - ) - } - - // Special handling: different payload computation, same SigningUtil - if (streamMessage.signatureType === SignatureType.ECDSA_SECP256K1_LEGACY) { - return evmSigner.verifySignature( - // publisherId is hex encoded Ethereum address string - toUserIdRaw(streamMessage.getPublisherId()), - createLegacySignaturePayload(streamMessage), - streamMessage.signature - ) - } - - // Special handling: check signature with ERC-1271 contract facade if (streamMessage.signatureType === SignatureType.ERC_1271) { return this.erc1271ContractFacade.isValidSignature( - toEthereumAddress(streamMessage.getPublisherId()), + toEthereumAddress(streamMessage.messageId.publisherId), createSignaturePayload(streamMessage), streamMessage.signature ) } + const result = await this.getValidationContext().validateSignature(streamMessage) + switch (result.type) { + case 'valid': + return true + case 'invalid': + return false + case 'error': + throw new Error(result.message) + default: + throw new Error(`Unknown signature validation result type: ${result.type}`) + } + } - throw new Error(`Cannot validate message signature, unsupported signatureType: "${streamMessage.signatureType}"`) + /** + * Cleanup worker resources when the validator is no longer needed. + */ + destroy(): void { + if (this.validationContext) { + this.validationContext.destroy() + this.validationContext = undefined + } } } diff --git a/packages/sdk/src/signature/signatureValidation.ts b/packages/sdk/src/signature/signatureValidation.ts new file mode 100644 index 0000000000..0a01f35e4f --- /dev/null +++ b/packages/sdk/src/signature/signatureValidation.ts @@ -0,0 +1,72 @@ +/** + * Core signature validation logic - shared between worker and main thread implementations. + * This file contains pure cryptographic validation functions without any network dependencies. + */ +import { SigningUtil, toUserIdRaw } from '@streamr/utils' +import { SignatureType } from '@streamr/trackerless-network' +import { IDENTITY_MAPPING } from '../identity/IdentityMapping' +import { createSignaturePayload } from './createSignaturePayload' +import { createLegacySignaturePayload } from './createLegacySignaturePayload' +import { StreamMessage } from '../protocol/StreamMessage' + +// Lookup structure SignatureType -> SigningUtil +const signingUtilBySignatureType: Record = Object.fromEntries( + IDENTITY_MAPPING.map((idMapping) => [idMapping.signatureType, SigningUtil.getInstance(idMapping.keyType)]) +) + +const evmSigner = SigningUtil.getInstance('ECDSA_SECP256K1_EVM') + +/** + * Result of signature validation + */ +export type SignatureValidationResult = + | { type: 'valid' } + | { type: 'invalid' } + | { type: 'requires_erc1271' } + | { type: 'error'; message: string } + +/** + * Validate signature using extracted data. + * This is the core validation logic that can be run in a worker. + */ +export async function validateSignatureData(message: StreamMessage): Promise { + try { + const signingUtil = signingUtilBySignatureType[message.signatureType] + // Common case: standard signature types + if (signingUtil) { + const payload = createSignaturePayload({ + messageId: message.messageId, + content: message.content, + messageType: message.messageType, + prevMsgRef: message.prevMsgRef, + newGroupKey: message.newGroupKey, + }) + const isValid = await signingUtil.verifySignature( + toUserIdRaw(message.messageId.publisherId), + payload, + message.signature + ) + return isValid ? { type: 'valid' } : { type: 'invalid' } + } + // Special handling: legacy signature type + if (message.signatureType === SignatureType.ECDSA_SECP256K1_LEGACY) { + const payload = createLegacySignaturePayload({ + messageId: message.messageId, + content: message.content, + encryptionType: message.encryptionType, + prevMsgRef: message.prevMsgRef, + newGroupKey: message.newGroupKey, + }) + const isValid = await evmSigner.verifySignature( + toUserIdRaw(message.messageId.publisherId), + payload, + message.signature + ) + return isValid ? { type: 'valid' } : { type: 'invalid' } + } + return { type: 'error', message: `Unsupported signatureType: "${message.signatureType}"` } + } catch (err) { + return { type: 'error', message: String(err) } + } +} + diff --git a/packages/sdk/webpack.config.js b/packages/sdk/webpack.config.js index d7005da143..d996bbe75d 100644 --- a/packages/sdk/webpack.config.js +++ b/packages/sdk/webpack.config.js @@ -67,7 +67,8 @@ module.exports = (env, argv) => { GIT_BRANCH: gitRevisionPlugin.branch(), }), new webpack.optimize.LimitChunkCountPlugin({ - maxChunks: 1 + // Allow 2 chunks: main bundle + signature validation worker + maxChunks: 2 }) ], performance: { From 9bf8431397d36f643016d31cee4f74d7647ddab2 Mon Sep 17 00:00:00 2001 From: juslesan Date: Mon, 22 Dec 2025 14:51:01 +0200 Subject: [PATCH 02/28] package-lock --- package-lock.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/package-lock.json b/package-lock.json index 8ee7081d6a..d6d7593c82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11578,6 +11578,12 @@ "node": ">= 0.8" } }, + "node_modules/comlink": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/comlink/-/comlink-4.4.2.tgz", + "integrity": "sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g==", + "license": "Apache-2.0" + }, "node_modules/commander": { "version": "14.0.2", "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", @@ -29067,6 +29073,7 @@ "@streamr/proto-rpc": "103.2.0", "@streamr/trackerless-network": "103.2.0", "@streamr/utils": "103.2.0", + "comlink": "^4.4.2", "core-js": "^3.47.0", "env-paths": "^2.2.1", "ethers": "^6.13.0", From 19ce5d700ca4c49a9472b8dd58e76852ca6f6d8b Mon Sep 17 00:00:00 2001 From: juslesan Date: Mon, 22 Dec 2025 14:59:42 +0200 Subject: [PATCH 03/28] DestroySignal to tests --- packages/sdk/test/unit/MessageFactory.test.ts | 3 ++- packages/sdk/test/unit/SignatureValidator.test.ts | 3 ++- packages/sdk/test/unit/messagePipeline.test.ts | 2 +- packages/sdk/test/unit/validateStreamMessage.test.ts | 3 ++- packages/sdk/test/unit/validateStreamMessage2.test.ts | 3 ++- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/sdk/test/unit/MessageFactory.test.ts b/packages/sdk/test/unit/MessageFactory.test.ts index 93c17213a4..2d93a05696 100644 --- a/packages/sdk/test/unit/MessageFactory.test.ts +++ b/packages/sdk/test/unit/MessageFactory.test.ts @@ -16,6 +16,7 @@ import { StreamMessage, StreamMessageType } from './../../src/protocol/StreamMes import { EthereumKeyPairIdentity } from '../../src/identity/EthereumKeyPairIdentity' import { EncryptionType, SignatureType, ContentType } from '@streamr/trackerless-network' import { StrictStreamrClientConfig } from '../../src/Config' +import { DestroySignal } from '../../src/DestroySignal' const CONTENT = { foo: 'bar' } const TIMESTAMP = Date.parse('2001-02-03T04:05:06Z') @@ -58,7 +59,7 @@ describe('MessageFactory', () => { isStreamPublisher: true }), groupKeyQueue: await createGroupKeyQueue(identity, GROUP_KEY), - signatureValidator: new SignatureValidator(opts?.erc1271ContractFacade ?? mock()), + signatureValidator: new SignatureValidator(opts?.erc1271ContractFacade ?? mock(), new DestroySignal()), messageSigner: new MessageSigner(identity), config: { validation: { diff --git a/packages/sdk/test/unit/SignatureValidator.test.ts b/packages/sdk/test/unit/SignatureValidator.test.ts index f303546dea..8b199679b7 100644 --- a/packages/sdk/test/unit/SignatureValidator.test.ts +++ b/packages/sdk/test/unit/SignatureValidator.test.ts @@ -11,6 +11,7 @@ import { StreamrClientError } from './../../src/StreamrClientError' import { MessageID } from './../../src/protocol/MessageID' import { StreamMessage, StreamMessageType } from './../../src/protocol/StreamMessage' import { ContentType, EncryptionType, SignatureType } from '@streamr/trackerless-network' +import { DestroySignal } from '../../src/DestroySignal' describe('SignatureValidator', () => { let erc1271ContractFacade: MockProxy @@ -18,7 +19,7 @@ describe('SignatureValidator', () => { beforeEach(() => { erc1271ContractFacade = mock() - signatureValidator = new SignatureValidator(erc1271ContractFacade) + signatureValidator = new SignatureValidator(erc1271ContractFacade, new DestroySignal()) }) describe('SECP256K1', () => { diff --git a/packages/sdk/test/unit/messagePipeline.test.ts b/packages/sdk/test/unit/messagePipeline.test.ts index ad7c0e5699..5cf75a458a 100644 --- a/packages/sdk/test/unit/messagePipeline.test.ts +++ b/packages/sdk/test/unit/messagePipeline.test.ts @@ -88,7 +88,7 @@ describe('messagePipeline', () => { getStorageNodes: undefined as any, resends: undefined as any, streamRegistry: streamRegistry as any, - signatureValidator: new SignatureValidator(mock()), + signatureValidator: new SignatureValidator(mock(), destroySignal), groupKeyManager: new GroupKeyManager( mock(), groupKeyStore, diff --git a/packages/sdk/test/unit/validateStreamMessage.test.ts b/packages/sdk/test/unit/validateStreamMessage.test.ts index 851521fb8c..e60c253d82 100644 --- a/packages/sdk/test/unit/validateStreamMessage.test.ts +++ b/packages/sdk/test/unit/validateStreamMessage.test.ts @@ -12,6 +12,7 @@ import { validateStreamMessage } from '../../src/utils/validateStreamMessage' import { createMockMessage } from '../test-utils/utils' import { StreamMessage } from './../../src/protocol/StreamMessage' import { StrictStreamrClientConfig } from '../../src/Config' +import { DestroySignal } from '../../src/DestroySignal' const PARTITION_COUNT = 3 @@ -47,7 +48,7 @@ describe('Validator', () => { await validateStreamMessage( msg, streamRegistry as any, - new SignatureValidator(mock()), + new SignatureValidator(mock(), new DestroySignal()), { validation: { permissions: true, diff --git a/packages/sdk/test/unit/validateStreamMessage2.test.ts b/packages/sdk/test/unit/validateStreamMessage2.test.ts index 1b570fe421..cd2cfec32f 100644 --- a/packages/sdk/test/unit/validateStreamMessage2.test.ts +++ b/packages/sdk/test/unit/validateStreamMessage2.test.ts @@ -14,6 +14,7 @@ import { MessageID } from './../../src/protocol/MessageID' import { MessageRef } from './../../src/protocol/MessageRef' import { StreamMessage, StreamMessageType } from './../../src/protocol/StreamMessage' import { StrictStreamrClientConfig } from '../../src/Config' +import { DestroySignal } from '../../src/DestroySignal' const groupKeyRequestToStreamMessage = async ( groupKeyRequest: GroupKeyRequest, @@ -71,7 +72,7 @@ describe('Validator2', () => { isStreamPublisher: (streamId: string, userId: UserID) => isPublisher(userId, streamId), isStreamSubscriber: (streamId: string, userId: UserID) => isSubscriber(userId, streamId) } as any, - new SignatureValidator(mock()), + new SignatureValidator(mock(), new DestroySignal()), { validation: { permissions: true, From c4586b86bdc5ad305687537a0ab1bfbb745bda7b Mon Sep 17 00:00:00 2001 From: juslesan Date: Mon, 22 Dec 2025 15:09:28 +0200 Subject: [PATCH 04/28] eslint --- packages/sdk/src/signature/ServerSignatureValidation.ts | 1 + packages/sdk/src/signature/SignatureValidator.ts | 7 ++----- packages/sdk/src/signature/signatureValidation.ts | 3 +-- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/sdk/src/signature/ServerSignatureValidation.ts b/packages/sdk/src/signature/ServerSignatureValidation.ts index fefce283b3..ba38835558 100644 --- a/packages/sdk/src/signature/ServerSignatureValidation.ts +++ b/packages/sdk/src/signature/ServerSignatureValidation.ts @@ -8,6 +8,7 @@ import { SignatureValidationResult, validateSignatureData } from './signatureVal export default class ServerSignatureValidation implements SignatureValidationContext { + // eslint-disable-next-line class-methods-use-this async validateSignature(message: StreamMessage): Promise { return validateSignatureData(message) } diff --git a/packages/sdk/src/signature/SignatureValidator.ts b/packages/sdk/src/signature/SignatureValidator.ts index 7f7d10f9e1..bbbf0ac3d9 100644 --- a/packages/sdk/src/signature/SignatureValidator.ts +++ b/packages/sdk/src/signature/SignatureValidator.ts @@ -24,10 +24,7 @@ export class SignatureValidator { } private getValidationContext(): SignatureValidationContext { - if (!this.validationContext) { - this.validationContext = new SignatureValidation() - } - return this.validationContext + return this.validationContext ??= new SignatureValidation() } /** @@ -64,7 +61,7 @@ export class SignatureValidator { case 'error': throw new Error(result.message) default: - throw new Error(`Unknown signature validation result type: ${result.type}`) + throw new Error(`Unknown signature validation result type '${result}'`) } } diff --git a/packages/sdk/src/signature/signatureValidation.ts b/packages/sdk/src/signature/signatureValidation.ts index 0a01f35e4f..8f294f49d5 100644 --- a/packages/sdk/src/signature/signatureValidation.ts +++ b/packages/sdk/src/signature/signatureValidation.ts @@ -22,8 +22,7 @@ const evmSigner = SigningUtil.getInstance('ECDSA_SECP256K1_EVM') export type SignatureValidationResult = | { type: 'valid' } | { type: 'invalid' } - | { type: 'requires_erc1271' } - | { type: 'error'; message: string } + | { type: 'error', message: string } /** * Validate signature using extracted data. From c2a74edabb44064c45098bcfc9834b831eb22f03 Mon Sep 17 00:00:00 2001 From: juslesan Date: Mon, 22 Dec 2025 15:14:40 +0200 Subject: [PATCH 05/28] npm run version --- packages/sdk/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 80ed9679ec..008af1e844 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -95,7 +95,6 @@ "#IMPORTANT": "babel-runtime must be in dependencies, not devDependencies", "dependencies": { "@babel/runtime": "^7.28.4", - "comlink": "^4.4.2", "@babel/runtime-corejs3": "^7.28.4", "@noble/post-quantum": "^0.4.1", "@protobuf-ts/runtime": "^2.8.2", @@ -106,6 +105,7 @@ "@streamr/proto-rpc": "103.2.0", "@streamr/trackerless-network": "103.2.0", "@streamr/utils": "103.2.0", + "comlink": "^4.4.2", "core-js": "^3.47.0", "env-paths": "^2.2.1", "ethers": "^6.13.0", From 2dd5dd42968ea5cef0487ad1b8f34489ed58feb6 Mon Sep 17 00:00:00 2001 From: juslesan Date: Mon, 22 Dec 2025 15:55:31 +0200 Subject: [PATCH 06/28] refactors --- .../signature/BrowserSignatureValidation.mts | 25 ++++++++----------- .../signature/SignatureValidationWorker.ts | 9 +++---- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/packages/sdk/src/signature/BrowserSignatureValidation.mts b/packages/sdk/src/signature/BrowserSignatureValidation.mts index 771ecf0965..9e202ffef7 100644 --- a/packages/sdk/src/signature/BrowserSignatureValidation.mts +++ b/packages/sdk/src/signature/BrowserSignatureValidation.mts @@ -9,30 +9,25 @@ import type { SignatureValidationWorkerApi } from './SignatureValidationWorker.j import { StreamMessage } from '../protocol/StreamMessage.js' export default class BrowserSignatureValidation implements SignatureValidationContext { - private worker: Worker | null = null - private workerApi: Comlink.Remote | null = null + private worker: Worker + private workerApi: Comlink.Remote - private ensureWorker(): Comlink.Remote { - if (!this.workerApi) { - // Webpack 5 handles this pattern automatically, creating a separate chunk for the worker - this.worker = new Worker( - /* webpackChunkName: "signature-worker" */ - new URL('./SignatureValidationWorker.js', import.meta.url) - ) - this.workerApi = Comlink.wrap(this.worker) - } - return this.workerApi + constructor() { + // Webpack 5 handles this pattern automatically, creating a separate chunk for the worker + this.worker = new Worker( + /* webpackChunkName: "signature-worker" */ + new URL('./SignatureValidationWorker.js', import.meta.url) + ) + this.workerApi = Comlink.wrap(this.worker) } async validateSignature(message: StreamMessage): Promise { - return this.ensureWorker().validateSignature(message) + return this.workerApi.validateSignature(message) } destroy(): void { if (this.worker) { this.worker.terminate() - this.worker = null } - this.workerApi = null } } diff --git a/packages/sdk/src/signature/SignatureValidationWorker.ts b/packages/sdk/src/signature/SignatureValidationWorker.ts index 21f14288ca..173059cbc5 100644 --- a/packages/sdk/src/signature/SignatureValidationWorker.ts +++ b/packages/sdk/src/signature/SignatureValidationWorker.ts @@ -6,13 +6,10 @@ import * as Comlink from 'comlink' import { validateSignatureData, SignatureValidationResult } from './signatureValidation' import { StreamMessage } from '../protocol/StreamMessage' -const workerApi = { - validateSignature: async (data: StreamMessage): Promise => { +export class SignatureValidationWorkerApi { + async validateSignature(data: StreamMessage): Promise { return validateSignatureData(data) } } -export type SignatureValidationWorkerApi = typeof workerApi - -Comlink.expose(workerApi) - +Comlink.expose(SignatureValidationWorkerApi) From d6481e2235df8dcabc1e13a4b54c32e762a24b64 Mon Sep 17 00:00:00 2001 From: juslesan Date: Mon, 22 Dec 2025 16:00:14 +0200 Subject: [PATCH 07/28] fix issue with StreamMessage passing --- packages/sdk/src/signature/SignatureValidationWorker.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/sdk/src/signature/SignatureValidationWorker.ts b/packages/sdk/src/signature/SignatureValidationWorker.ts index 173059cbc5..015ff016ee 100644 --- a/packages/sdk/src/signature/SignatureValidationWorker.ts +++ b/packages/sdk/src/signature/SignatureValidationWorker.ts @@ -6,10 +6,12 @@ import * as Comlink from 'comlink' import { validateSignatureData, SignatureValidationResult } from './signatureValidation' import { StreamMessage } from '../protocol/StreamMessage' -export class SignatureValidationWorkerApi { - async validateSignature(data: StreamMessage): Promise { +const workerApi = { + validateSignature: async (data: StreamMessage): Promise => { return validateSignatureData(data) } } -Comlink.expose(SignatureValidationWorkerApi) +export type SignatureValidationWorkerApi = typeof workerApi + +Comlink.expose(workerApi) From 46a9c6f59869aa58f4996c2f4fe0348ddcca6fa1 Mon Sep 17 00:00:00 2001 From: juslesan Date: Mon, 22 Dec 2025 16:05:48 +0200 Subject: [PATCH 08/28] revert --- packages/sdk/src/signature/SignatureValidationWorker.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/sdk/src/signature/SignatureValidationWorker.ts b/packages/sdk/src/signature/SignatureValidationWorker.ts index 015ff016ee..6bbcb971cf 100644 --- a/packages/sdk/src/signature/SignatureValidationWorker.ts +++ b/packages/sdk/src/signature/SignatureValidationWorker.ts @@ -1,7 +1,3 @@ -/** - * Web Worker for signature validation. - * This worker handles CPU-intensive cryptographic operations off the main thread. - */ import * as Comlink from 'comlink' import { validateSignatureData, SignatureValidationResult } from './signatureValidation' import { StreamMessage } from '../protocol/StreamMessage' From d31d1c18d5b3313973b958976ddc8453ea1eac35 Mon Sep 17 00:00:00 2001 From: juslesan Date: Mon, 22 Dec 2025 17:44:14 +0200 Subject: [PATCH 09/28] nodejs workers for validation --- .../signature/ServerSignatureValidation.ts | 26 ++++++++++++++----- .../signature/SignatureValidationWorker.ts | 13 +++++++++- .../sdk/test/unit/SignatureValidator.test.ts | 7 ++++- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/packages/sdk/src/signature/ServerSignatureValidation.ts b/packages/sdk/src/signature/ServerSignatureValidation.ts index ba38835558..fda6103d9c 100644 --- a/packages/sdk/src/signature/ServerSignatureValidation.ts +++ b/packages/sdk/src/signature/ServerSignatureValidation.ts @@ -1,16 +1,28 @@ -/** - * Node.js implementation of signature validation. - * Runs on the main thread (worker threads can be added later if needed). - */ +import * as Comlink from 'comlink' +import nodeEndpoint from 'comlink/dist/umd/node-adapter' +import { Worker } from "worker_threads" import { StreamMessage } from '../protocol/StreamMessage' import { SignatureValidationContext } from './SignatureValidationContext' -import { SignatureValidationResult, validateSignatureData } from './signatureValidation' +import { SignatureValidationWorkerApi } from './SignatureValidationWorker' +import { SignatureValidationResult } from './signatureValidation' +import { join } from 'path' export default class ServerSignatureValidation implements SignatureValidationContext { - // eslint-disable-next-line class-methods-use-this + private readonly worker: Worker + private readonly workerApi: Comlink.Remote + constructor() { + const isRunningFromDist = __dirname.includes('/dist/') + const workerPath = isRunningFromDist + ? join(__dirname, 'SignatureValidationWorker.js') + : join(__dirname, '../../dist/src/signature/SignatureValidationWorker.js') + this.worker = new Worker(workerPath) + this.workerApi = Comlink.wrap(nodeEndpoint(this.worker)) + } + async validateSignature(message: StreamMessage): Promise { - return validateSignatureData(message) + console.log('validateSignature', message) + return this.workerApi.validateSignature(message) } // eslint-disable-next-line class-methods-use-this diff --git a/packages/sdk/src/signature/SignatureValidationWorker.ts b/packages/sdk/src/signature/SignatureValidationWorker.ts index 6bbcb971cf..7fd6a39561 100644 --- a/packages/sdk/src/signature/SignatureValidationWorker.ts +++ b/packages/sdk/src/signature/SignatureValidationWorker.ts @@ -10,4 +10,15 @@ const workerApi = { export type SignatureValidationWorkerApi = typeof workerApi -Comlink.expose(workerApi) +// Detect environment and expose accordingly +if (typeof self !== 'undefined') { + // Browser Web Worker + Comlink.expose(workerApi) +} else { + // Node.js Worker Thread + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { parentPort } = require('worker_threads') + // eslint-disable-next-line @typescript-eslint/no-require-imports + const nodeEndpoint = require('comlink/dist/umd/node-adapter') + Comlink.expose(workerApi, nodeEndpoint(parentPort)) +} diff --git a/packages/sdk/test/unit/SignatureValidator.test.ts b/packages/sdk/test/unit/SignatureValidator.test.ts index 8b199679b7..bebeceb352 100644 --- a/packages/sdk/test/unit/SignatureValidator.test.ts +++ b/packages/sdk/test/unit/SignatureValidator.test.ts @@ -24,7 +24,7 @@ describe('SignatureValidator', () => { describe('SECP256K1', () => { - it('unencrypted message passes signature validation', async () => { + it.only('unencrypted message passes signature validation', async () => { const message = new StreamMessage({ messageId: new MessageID( toStreamID('streamr.eth/foo/bar'), @@ -43,6 +43,11 @@ describe('SignatureValidator', () => { signature: hexToBinary('e53045adef4e01f7fe11d4b3073c6053688912e4db0ee780c189cd0d128c923457e1f6cbc1e47d9cd57e115afa9eb8524288887777c1056d638b193cae112dda1b'), signatureType: SignatureType.ECDSA_SECP256K1_EVM }) + try { + await signatureValidator.assertSignatureIsValid(message) + } catch (error) { + console.log(error) + } await expect(signatureValidator.assertSignatureIsValid(message)).toResolve() }) From 360dfdc40a1cba63c300cadd35bed57e5d151a70 Mon Sep 17 00:00:00 2001 From: juslesan Date: Mon, 22 Dec 2025 18:02:12 +0200 Subject: [PATCH 10/28] most unit tests work now --- .../signature/SignatureValidationWorker.ts | 20 +++++++++++++------ .../sdk/test/unit/SignatureValidator.test.ts | 2 +- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/sdk/src/signature/SignatureValidationWorker.ts b/packages/sdk/src/signature/SignatureValidationWorker.ts index 7fd6a39561..c316201106 100644 --- a/packages/sdk/src/signature/SignatureValidationWorker.ts +++ b/packages/sdk/src/signature/SignatureValidationWorker.ts @@ -11,14 +11,22 @@ const workerApi = { export type SignatureValidationWorkerApi = typeof workerApi // Detect environment and expose accordingly -if (typeof self !== 'undefined') { - // Browser Web Worker - Comlink.expose(workerApi) -} else { - // Node.js Worker Thread +// Check for Node.js worker_threads first, since `self` is defined in both environments +// but only browser Web Workers have WorkerGlobalScope with addEventListener +let parentPort: import('worker_threads').MessagePort | null = null +try { // eslint-disable-next-line @typescript-eslint/no-require-imports - const { parentPort } = require('worker_threads') + parentPort = require('worker_threads').parentPort +} catch { + // Not in Node.js environment +} + +if (parentPort) { + // Node.js Worker Thread // eslint-disable-next-line @typescript-eslint/no-require-imports const nodeEndpoint = require('comlink/dist/umd/node-adapter') Comlink.expose(workerApi, nodeEndpoint(parentPort)) +} else { + // Browser Web Worker + Comlink.expose(workerApi) } diff --git a/packages/sdk/test/unit/SignatureValidator.test.ts b/packages/sdk/test/unit/SignatureValidator.test.ts index bebeceb352..7db870af91 100644 --- a/packages/sdk/test/unit/SignatureValidator.test.ts +++ b/packages/sdk/test/unit/SignatureValidator.test.ts @@ -24,7 +24,7 @@ describe('SignatureValidator', () => { describe('SECP256K1', () => { - it.only('unencrypted message passes signature validation', async () => { + it('unencrypted message passes signature validation', async () => { const message = new StreamMessage({ messageId: new MessageID( toStreamID('streamr.eth/foo/bar'), From 494cc2135ef6887fd7d8183f1182e49cf1b1ed50 Mon Sep 17 00:00:00 2001 From: juslesan Date: Mon, 22 Dec 2025 18:23:18 +0200 Subject: [PATCH 11/28] SignatureValidationData --- .../signature/BrowserSignatureValidation.mts | 6 +- .../signature/ServerSignatureValidation.ts | 7 +- .../signature/SignatureValidationWorker.ts | 5 +- .../signature/createLegacySignaturePayload.ts | 7 +- .../src/signature/createSignaturePayload.ts | 28 ++++-- .../sdk/src/signature/signatureValidation.ts | 85 ++++++++++++++----- 6 files changed, 99 insertions(+), 39 deletions(-) diff --git a/packages/sdk/src/signature/BrowserSignatureValidation.mts b/packages/sdk/src/signature/BrowserSignatureValidation.mts index 9e202ffef7..55080d3d46 100644 --- a/packages/sdk/src/signature/BrowserSignatureValidation.mts +++ b/packages/sdk/src/signature/BrowserSignatureValidation.mts @@ -4,7 +4,7 @@ */ import * as Comlink from 'comlink' import { SignatureValidationContext } from './SignatureValidationContext.js' -import { SignatureValidationResult } from './signatureValidation.js' +import { SignatureValidationResult, toSignatureValidationData } from './signatureValidation.js' import type { SignatureValidationWorkerApi } from './SignatureValidationWorker.js' import { StreamMessage } from '../protocol/StreamMessage.js' @@ -22,7 +22,9 @@ export default class BrowserSignatureValidation implements SignatureValidationCo } async validateSignature(message: StreamMessage): Promise { - return this.workerApi.validateSignature(message) + // Convert class instance to plain serializable data before sending to worker + const data = toSignatureValidationData(message) + return this.workerApi.validateSignature(data) } destroy(): void { diff --git a/packages/sdk/src/signature/ServerSignatureValidation.ts b/packages/sdk/src/signature/ServerSignatureValidation.ts index fda6103d9c..552171756e 100644 --- a/packages/sdk/src/signature/ServerSignatureValidation.ts +++ b/packages/sdk/src/signature/ServerSignatureValidation.ts @@ -4,7 +4,7 @@ import { Worker } from "worker_threads" import { StreamMessage } from '../protocol/StreamMessage' import { SignatureValidationContext } from './SignatureValidationContext' import { SignatureValidationWorkerApi } from './SignatureValidationWorker' -import { SignatureValidationResult } from './signatureValidation' +import { SignatureValidationResult, toSignatureValidationData } from './signatureValidation' import { join } from 'path' export default class ServerSignatureValidation implements SignatureValidationContext { @@ -21,8 +21,9 @@ export default class ServerSignatureValidation implements SignatureValidationCon } async validateSignature(message: StreamMessage): Promise { - console.log('validateSignature', message) - return this.workerApi.validateSignature(message) + // Convert class instance to plain serializable data before sending to worker + const data = toSignatureValidationData(message) + return this.workerApi.validateSignature(data) } // eslint-disable-next-line class-methods-use-this diff --git a/packages/sdk/src/signature/SignatureValidationWorker.ts b/packages/sdk/src/signature/SignatureValidationWorker.ts index c316201106..11c8cf1626 100644 --- a/packages/sdk/src/signature/SignatureValidationWorker.ts +++ b/packages/sdk/src/signature/SignatureValidationWorker.ts @@ -1,9 +1,8 @@ import * as Comlink from 'comlink' -import { validateSignatureData, SignatureValidationResult } from './signatureValidation' -import { StreamMessage } from '../protocol/StreamMessage' +import { validateSignatureData, SignatureValidationResult, SignatureValidationData } from './signatureValidation' const workerApi = { - validateSignature: async (data: StreamMessage): Promise => { + validateSignature: async (data: SignatureValidationData): Promise => { return validateSignatureData(data) } } diff --git a/packages/sdk/src/signature/createLegacySignaturePayload.ts b/packages/sdk/src/signature/createLegacySignaturePayload.ts index 3aadd0828e..42c785c2fe 100644 --- a/packages/sdk/src/signature/createLegacySignaturePayload.ts +++ b/packages/sdk/src/signature/createLegacySignaturePayload.ts @@ -1,7 +1,6 @@ import { binaryToHex, binaryToUtf8 } from '@streamr/utils' import { EncryptedGroupKey, EncryptionType } from '@streamr/trackerless-network' -import { MessageID } from '../protocol/MessageID' -import { MessageRef } from '../protocol/MessageRef' +import { MessageIdLike, MessageRefLike } from './createSignaturePayload' const serializeGroupKey = ({ id, data }: EncryptedGroupKey): string => { return JSON.stringify([id, binaryToHex(data)]) @@ -11,10 +10,10 @@ const serializeGroupKey = ({ id, data }: EncryptedGroupKey): string => { * Only to be used for LEGACY_SECP256K1 signature type. */ export const createLegacySignaturePayload = (opts: { - messageId: MessageID + messageId: MessageIdLike content: Uint8Array encryptionType: EncryptionType - prevMsgRef?: MessageRef + prevMsgRef?: MessageRefLike newGroupKey?: EncryptedGroupKey }): Uint8Array => { const prev = ((opts.prevMsgRef !== undefined) ? `${opts.prevMsgRef.timestamp}${opts.prevMsgRef.sequenceNumber}` : '') diff --git a/packages/sdk/src/signature/createSignaturePayload.ts b/packages/sdk/src/signature/createSignaturePayload.ts index 9cfadf7d32..0162139680 100644 --- a/packages/sdk/src/signature/createSignaturePayload.ts +++ b/packages/sdk/src/signature/createSignaturePayload.ts @@ -3,16 +3,34 @@ import { GroupKeyRequest as NewGroupKeyRequest, GroupKeyResponse as NewGroupKeyResponse } from '@streamr/trackerless-network' -import { utf8ToBinary } from '@streamr/utils' -import { MessageID } from '../protocol/MessageID' -import { MessageRef } from '../protocol/MessageRef' +import { StreamID, UserID, utf8ToBinary } from '@streamr/utils' import { StreamMessageType } from '../protocol/StreamMessage' +/** + * Plain data for message ID - accepts class instances or plain objects with the same properties. + */ +export interface MessageIdLike { + streamId: StreamID + streamPartition: number + timestamp: number + sequenceNumber: number + publisherId: UserID + msgChainId: string +} + +/** + * Plain data for message reference - accepts class instances or plain objects with the same properties. + */ +export interface MessageRefLike { + timestamp: number + sequenceNumber: number +} + export const createSignaturePayload = (opts: { - messageId: MessageID + messageId: MessageIdLike content: Uint8Array messageType: StreamMessageType - prevMsgRef?: MessageRef + prevMsgRef?: MessageRefLike newGroupKey?: EncryptedGroupKey }): Uint8Array | never => { const header = Buffer.concat([ diff --git a/packages/sdk/src/signature/signatureValidation.ts b/packages/sdk/src/signature/signatureValidation.ts index 8f294f49d5..e23b73ee1d 100644 --- a/packages/sdk/src/signature/signatureValidation.ts +++ b/packages/sdk/src/signature/signatureValidation.ts @@ -2,12 +2,12 @@ * Core signature validation logic - shared between worker and main thread implementations. * This file contains pure cryptographic validation functions without any network dependencies. */ -import { SigningUtil, toUserIdRaw } from '@streamr/utils' -import { SignatureType } from '@streamr/trackerless-network' +import { SigningUtil, StreamID, toUserIdRaw, UserID } from '@streamr/utils' +import { EncryptedGroupKey, EncryptionType, SignatureType } from '@streamr/trackerless-network' import { IDENTITY_MAPPING } from '../identity/IdentityMapping' -import { createSignaturePayload } from './createSignaturePayload' +import { createSignaturePayload, MessageIdLike, MessageRefLike } from './createSignaturePayload' import { createLegacySignaturePayload } from './createLegacySignaturePayload' -import { StreamMessage } from '../protocol/StreamMessage' +import { StreamMessage, StreamMessageType } from '../protocol/StreamMessage' // Lookup structure SignatureType -> SigningUtil const signingUtilBySignatureType: Record = Object.fromEntries( @@ -24,46 +24,87 @@ export type SignatureValidationResult = | { type: 'invalid' } | { type: 'error', message: string } +/** + * Plain data type for signature validation that can be serialized to a worker. + * This contains only primitive values and simple objects (no class instances). + */ +export interface SignatureValidationData { + messageId: MessageIdLike + prevMsgRef?: MessageRefLike + messageType: StreamMessageType + content: Uint8Array + signature: Uint8Array + signatureType: SignatureType + encryptionType: EncryptionType + newGroupKey?: EncryptedGroupKey +} + +/** + * Extract plain serializable data from a StreamMessage for worker communication. + */ +export function toSignatureValidationData(message: StreamMessage): SignatureValidationData { + return { + messageId: { + streamId: message.messageId.streamId, + streamPartition: message.messageId.streamPartition, + timestamp: message.messageId.timestamp, + sequenceNumber: message.messageId.sequenceNumber, + publisherId: message.messageId.publisherId, + msgChainId: message.messageId.msgChainId, + }, + prevMsgRef: message.prevMsgRef ? { + timestamp: message.prevMsgRef.timestamp, + sequenceNumber: message.prevMsgRef.sequenceNumber, + } : undefined, + messageType: message.messageType, + content: message.content, + signature: message.signature, + signatureType: message.signatureType, + encryptionType: message.encryptionType, + newGroupKey: message.newGroupKey, + } +} + /** * Validate signature using extracted data. * This is the core validation logic that can be run in a worker. */ -export async function validateSignatureData(message: StreamMessage): Promise { +export async function validateSignatureData(data: SignatureValidationData): Promise { try { - const signingUtil = signingUtilBySignatureType[message.signatureType] + const signingUtil = signingUtilBySignatureType[data.signatureType] // Common case: standard signature types if (signingUtil) { const payload = createSignaturePayload({ - messageId: message.messageId, - content: message.content, - messageType: message.messageType, - prevMsgRef: message.prevMsgRef, - newGroupKey: message.newGroupKey, + messageId: data.messageId, + content: data.content, + messageType: data.messageType, + prevMsgRef: data.prevMsgRef, + newGroupKey: data.newGroupKey, }) const isValid = await signingUtil.verifySignature( - toUserIdRaw(message.messageId.publisherId), + toUserIdRaw(data.messageId.publisherId), payload, - message.signature + data.signature ) return isValid ? { type: 'valid' } : { type: 'invalid' } } // Special handling: legacy signature type - if (message.signatureType === SignatureType.ECDSA_SECP256K1_LEGACY) { + if (data.signatureType === SignatureType.ECDSA_SECP256K1_LEGACY) { const payload = createLegacySignaturePayload({ - messageId: message.messageId, - content: message.content, - encryptionType: message.encryptionType, - prevMsgRef: message.prevMsgRef, - newGroupKey: message.newGroupKey, + messageId: data.messageId, + content: data.content, + encryptionType: data.encryptionType, + prevMsgRef: data.prevMsgRef, + newGroupKey: data.newGroupKey, }) const isValid = await evmSigner.verifySignature( - toUserIdRaw(message.messageId.publisherId), + toUserIdRaw(data.messageId.publisherId), payload, - message.signature + data.signature ) return isValid ? { type: 'valid' } : { type: 'invalid' } } - return { type: 'error', message: `Unsupported signatureType: "${message.signatureType}"` } + return { type: 'error', message: `Unsupported signatureType: "${data.signatureType}"` } } catch (err) { return { type: 'error', message: String(err) } } From 4741a1794731d1fa08a8831b991e79b195baa3f2 Mon Sep 17 00:00:00 2001 From: juslesan Date: Mon, 22 Dec 2025 18:33:26 +0200 Subject: [PATCH 12/28] =?UTF-8?q?es=C3=B6int?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/sdk/src/signature/ServerSignatureValidation.ts | 3 ++- packages/sdk/src/signature/signatureValidation.ts | 2 +- packages/sdk/test/unit/SignatureValidator.test.ts | 5 ----- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/sdk/src/signature/ServerSignatureValidation.ts b/packages/sdk/src/signature/ServerSignatureValidation.ts index 552171756e..4321c1a067 100644 --- a/packages/sdk/src/signature/ServerSignatureValidation.ts +++ b/packages/sdk/src/signature/ServerSignatureValidation.ts @@ -1,6 +1,7 @@ import * as Comlink from 'comlink' +// eslint-disable-next-line no-restricted-imports import nodeEndpoint from 'comlink/dist/umd/node-adapter' -import { Worker } from "worker_threads" +import { Worker } from 'worker_threads' import { StreamMessage } from '../protocol/StreamMessage' import { SignatureValidationContext } from './SignatureValidationContext' import { SignatureValidationWorkerApi } from './SignatureValidationWorker' diff --git a/packages/sdk/src/signature/signatureValidation.ts b/packages/sdk/src/signature/signatureValidation.ts index e23b73ee1d..6f1cd3d348 100644 --- a/packages/sdk/src/signature/signatureValidation.ts +++ b/packages/sdk/src/signature/signatureValidation.ts @@ -2,7 +2,7 @@ * Core signature validation logic - shared between worker and main thread implementations. * This file contains pure cryptographic validation functions without any network dependencies. */ -import { SigningUtil, StreamID, toUserIdRaw, UserID } from '@streamr/utils' +import { SigningUtil, toUserIdRaw } from '@streamr/utils' import { EncryptedGroupKey, EncryptionType, SignatureType } from '@streamr/trackerless-network' import { IDENTITY_MAPPING } from '../identity/IdentityMapping' import { createSignaturePayload, MessageIdLike, MessageRefLike } from './createSignaturePayload' diff --git a/packages/sdk/test/unit/SignatureValidator.test.ts b/packages/sdk/test/unit/SignatureValidator.test.ts index 7db870af91..8b199679b7 100644 --- a/packages/sdk/test/unit/SignatureValidator.test.ts +++ b/packages/sdk/test/unit/SignatureValidator.test.ts @@ -43,11 +43,6 @@ describe('SignatureValidator', () => { signature: hexToBinary('e53045adef4e01f7fe11d4b3073c6053688912e4db0ee780c189cd0d128c923457e1f6cbc1e47d9cd57e115afa9eb8524288887777c1056d638b193cae112dda1b'), signatureType: SignatureType.ECDSA_SECP256K1_EVM }) - try { - await signatureValidator.assertSignatureIsValid(message) - } catch (error) { - console.log(error) - } await expect(signatureValidator.assertSignatureIsValid(message)).toResolve() }) From 49ba698f9bfa8352ded15cafc80c3a68bff2b08a Mon Sep 17 00:00:00 2001 From: juslesan Date: Mon, 12 Jan 2026 13:09:05 +0200 Subject: [PATCH 13/28] increase tiemout --- packages/sdk/test/integration/update-encryption-key.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/test/integration/update-encryption-key.test.ts b/packages/sdk/test/integration/update-encryption-key.test.ts index c07f31b310..0ef0e75df6 100644 --- a/packages/sdk/test/integration/update-encryption-key.test.ts +++ b/packages/sdk/test/integration/update-encryption-key.test.ts @@ -26,7 +26,7 @@ describe('update encryption key', () => { publisher = environment.createClient() subscriber = environment.createClient({ encryption: { - keyRequestTimeout: 200 + keyRequestTimeout: 1000 } }) const stream = await publisher.createStream('/path') From 8eef44493ffea14d803df8de703e450d3d35cf42 Mon Sep 17 00:00:00 2001 From: juslesan Date: Tue, 20 Jan 2026 15:00:54 +0200 Subject: [PATCH 14/28] process.exit(0) after a comand is completed to avoid hanging because of workers --- packages/cli-tools/src/command.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/cli-tools/src/command.ts b/packages/cli-tools/src/command.ts index 034213411d..cada66d1e3 100644 --- a/packages/cli-tools/src/command.ts +++ b/packages/cli-tools/src/command.ts @@ -53,6 +53,8 @@ export const createClientCommand = ( await client.destroy() } } + // Exit cleanly after command completes - worker threads may keep event loop alive + process.exit(0) } catch (e: any) { console.error(e) process.exit(1) From bde83db83a270fce4b75fd51d879115ee68f39e4 Mon Sep 17 00:00:00 2001 From: juslesan Date: Tue, 20 Jan 2026 15:06:36 +0200 Subject: [PATCH 15/28] destroy ServerSignatureValidation worker --- packages/sdk/src/signature/ServerSignatureValidation.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/sdk/src/signature/ServerSignatureValidation.ts b/packages/sdk/src/signature/ServerSignatureValidation.ts index 4321c1a067..8ff489e4d0 100644 --- a/packages/sdk/src/signature/ServerSignatureValidation.ts +++ b/packages/sdk/src/signature/ServerSignatureValidation.ts @@ -27,9 +27,10 @@ export default class ServerSignatureValidation implements SignatureValidationCon return this.workerApi.validateSignature(data) } - // eslint-disable-next-line class-methods-use-this - destroy(): void { - // No-op for server implementation + async destroy(): Promise { + this.workerApi[Comlink.releaseProxy]() + this.worker.removeAllListeners() + await this.worker.terminate() } } From 558ee879d015ab2a0f045ca893fb2ab871c9cdc0 Mon Sep 17 00:00:00 2001 From: juslesan Date: Tue, 20 Jan 2026 15:28:03 +0200 Subject: [PATCH 16/28] fix resend.ts --- packages/cli-tools/src/resend.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/cli-tools/src/resend.ts b/packages/cli-tools/src/resend.ts index ca77fdedc8..adcacfae3c 100644 --- a/packages/cli-tools/src/resend.ts +++ b/packages/cli-tools/src/resend.ts @@ -19,16 +19,19 @@ export const resend = async ( subscribe: boolean ): Promise => { try { - const handler = (message: any) => { - console.info(JSON.stringify(message)) - } if (subscribe) { + const handler = (message: any) => { + console.info(JSON.stringify(message)) + } await client.subscribe({ stream: streamId, resend: resendOpts }, handler) } else { - await client.resend(streamId, resendOpts, handler) + const messageStream = await client.resend(streamId, resendOpts) + for await (const message of messageStream) { + console.info(JSON.stringify(message.content)) + } } } catch (err) { console.error(err.message ?? err) From 31dbc7df2129cc5f2590bf0eed98eb1a1eed1a04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20Roli=C5=84ski?= Date: Tue, 27 Jan 2026 22:42:36 +0100 Subject: [PATCH 17/28] Install `web-worker` --- package-lock.json | 7 +++++++ packages/sdk/package.json | 1 + 2 files changed, 8 insertions(+) diff --git a/package-lock.json b/package-lock.json index 4d1611329f..131da3a94d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27198,6 +27198,12 @@ "dev": true, "license": "MIT" }, + "node_modules/web-worker": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", + "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==", + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -28949,6 +28955,7 @@ "ts-toolbelt": "^9.6.0", "tsyringe": "^4.10.0", "uuid": "^11.1.0", + "web-worker": "^1.5.0", "zod": "^4.1.13" }, "devDependencies": { diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 3ab76b89b7..d8a09064cc 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -113,6 +113,7 @@ "ts-toolbelt": "^9.6.0", "tsyringe": "^4.10.0", "uuid": "^11.1.0", + "web-worker": "^1.5.0", "zod": "^4.1.13" }, "optionalDependencies": { From cce4c3526aa0c39925289537bdc47c1b4145c8ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20Roli=C5=84ski?= Date: Tue, 27 Jan 2026 22:57:50 +0100 Subject: [PATCH 18/28] =?UTF-8?q?Refactor=20signature=20validation=20?= =?UTF-8?q?=E2=80=93=20use=20`web-worker`=20to=20unify=20worker=20code=20a?= =?UTF-8?q?cross=20envs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/sdk/package.json | 1 + packages/sdk/rollup.config.mts | 57 +++++++++++++++++++ .../createSignatureValidationWorker.ts | 11 ++++ .../createSignatureValidationWorker.ts | 11 ++++ .../signature/ServerSignatureValidation.ts | 36 ------------ ...ation.mts => SignatureValidationClient.ts} | 23 ++++---- .../signature/SignatureValidationContext.ts | 12 ---- .../signature/SignatureValidationWorker.ts | 21 +------ .../sdk/src/signature/SignatureValidator.ts | 18 +++--- 9 files changed, 99 insertions(+), 91 deletions(-) create mode 100644 packages/sdk/src/_browser/createSignatureValidationWorker.ts create mode 100644 packages/sdk/src/_nodejs/createSignatureValidationWorker.ts delete mode 100644 packages/sdk/src/signature/ServerSignatureValidation.ts rename packages/sdk/src/signature/{BrowserSignatureValidation.mts => SignatureValidationClient.ts} (55%) delete mode 100644 packages/sdk/src/signature/SignatureValidationContext.ts diff --git a/packages/sdk/package.json b/packages/sdk/package.json index d8a09064cc..36149596af 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -31,6 +31,7 @@ "dist/exports-browser.*", "dist/exports-umd.*", "dist/encryption/migrations", + "dist/workers", "!*.tsbuildinfo", "LICENSE", "README.md", diff --git a/packages/sdk/rollup.config.mts b/packages/sdk/rollup.config.mts index 183eb3fca3..16d46f509d 100644 --- a/packages/sdk/rollup.config.mts +++ b/packages/sdk/rollup.config.mts @@ -35,6 +35,8 @@ export default defineConfig([ browserTypes(), umd(), umdMinified(), + workerNodejs(), + workerBrowser(), ]) function onwarn(log: RollupLog, rollupWarn: (log: RollupLog) => void): void { @@ -204,3 +206,58 @@ function umdMinified(): RollupOptions { onwarn, } } + +/** + * Worker bundle for Node.js - ESM format for use with web-worker {type: 'module'} + */ +function workerNodejs(): RollupOptions { + return { + input: './dist/nodejs/src/signature/SignatureValidationWorker.js', + context: 'globalThis', + output: { + format: 'es', + file: './dist/workers/SignatureValidationWorker.node.js', + sourcemap: true, + }, + plugins: [ + json(), + alias({ + entries: nodejsAliases, + }), + nodeResolve({ + preferBuiltins: true, + }), + cjs(), + ], + external: [], + onwarn, + } +} + +/** + * Worker bundle for browser - ESM format for use with web-worker {type: 'module'} + */ +function workerBrowser(): RollupOptions { + return { + input: './dist/browser/src/signature/SignatureValidationWorker.js', + context: 'self', + output: { + format: 'es', + file: './dist/workers/SignatureValidationWorker.browser.js', + sourcemap: true, + }, + plugins: [ + json(), + alias({ + entries: browserAliases, + }), + nodeResolve({ + browser: true, + preferBuiltins: false, + }), + cjs(), + ], + external: [], + onwarn, + } +} diff --git a/packages/sdk/src/_browser/createSignatureValidationWorker.ts b/packages/sdk/src/_browser/createSignatureValidationWorker.ts new file mode 100644 index 0000000000..e497d2f4ff --- /dev/null +++ b/packages/sdk/src/_browser/createSignatureValidationWorker.ts @@ -0,0 +1,11 @@ +/** + * Browser-specific signature validation worker factory. + */ +import Worker from 'web-worker' + +export function createSignatureValidationWorker(): InstanceType { + return new Worker( + new URL('./workers/SignatureValidationWorker.browser.js', import.meta.url), + { type: 'module' } + ) +} diff --git a/packages/sdk/src/_nodejs/createSignatureValidationWorker.ts b/packages/sdk/src/_nodejs/createSignatureValidationWorker.ts new file mode 100644 index 0000000000..0f1d48a1a4 --- /dev/null +++ b/packages/sdk/src/_nodejs/createSignatureValidationWorker.ts @@ -0,0 +1,11 @@ +/** + * Node.js-specific signature validation worker factory. + */ +import Worker from 'web-worker' + +export function createSignatureValidationWorker(): InstanceType { + return new Worker( + new URL('./workers/SignatureValidationWorker.node.js', import.meta.url), + { type: 'module' } + ) +} diff --git a/packages/sdk/src/signature/ServerSignatureValidation.ts b/packages/sdk/src/signature/ServerSignatureValidation.ts deleted file mode 100644 index 8ff489e4d0..0000000000 --- a/packages/sdk/src/signature/ServerSignatureValidation.ts +++ /dev/null @@ -1,36 +0,0 @@ -import * as Comlink from 'comlink' -// eslint-disable-next-line no-restricted-imports -import nodeEndpoint from 'comlink/dist/umd/node-adapter' -import { Worker } from 'worker_threads' -import { StreamMessage } from '../protocol/StreamMessage' -import { SignatureValidationContext } from './SignatureValidationContext' -import { SignatureValidationWorkerApi } from './SignatureValidationWorker' -import { SignatureValidationResult, toSignatureValidationData } from './signatureValidation' -import { join } from 'path' - -export default class ServerSignatureValidation implements SignatureValidationContext { - - private readonly worker: Worker - private readonly workerApi: Comlink.Remote - constructor() { - const isRunningFromDist = __dirname.includes('/dist/') - const workerPath = isRunningFromDist - ? join(__dirname, 'SignatureValidationWorker.js') - : join(__dirname, '../../dist/src/signature/SignatureValidationWorker.js') - this.worker = new Worker(workerPath) - this.workerApi = Comlink.wrap(nodeEndpoint(this.worker)) - } - - async validateSignature(message: StreamMessage): Promise { - // Convert class instance to plain serializable data before sending to worker - const data = toSignatureValidationData(message) - return this.workerApi.validateSignature(data) - } - - async destroy(): Promise { - this.workerApi[Comlink.releaseProxy]() - this.worker.removeAllListeners() - await this.worker.terminate() - } -} - diff --git a/packages/sdk/src/signature/BrowserSignatureValidation.mts b/packages/sdk/src/signature/SignatureValidationClient.ts similarity index 55% rename from packages/sdk/src/signature/BrowserSignatureValidation.mts rename to packages/sdk/src/signature/SignatureValidationClient.ts index 55080d3d46..a0e4f45766 100644 --- a/packages/sdk/src/signature/BrowserSignatureValidation.mts +++ b/packages/sdk/src/signature/SignatureValidationClient.ts @@ -1,23 +1,20 @@ /** - * Browser implementation of signature validation using Web Worker. + * Unified signature validation using Web Worker. * This offloads CPU-intensive cryptographic operations to a separate thread. + * Works in both browser and Node.js environments via platform-specific config. */ import * as Comlink from 'comlink' -import { SignatureValidationContext } from './SignatureValidationContext.js' -import { SignatureValidationResult, toSignatureValidationData } from './signatureValidation.js' -import type { SignatureValidationWorkerApi } from './SignatureValidationWorker.js' -import { StreamMessage } from '../protocol/StreamMessage.js' +import { createSignatureValidationWorker } from '@/createSignatureValidationWorker' +import { SignatureValidationResult, toSignatureValidationData } from './signatureValidation' +import type { SignatureValidationWorkerApi } from './SignatureValidationWorker' +import { StreamMessage } from '../protocol/StreamMessage' -export default class BrowserSignatureValidation implements SignatureValidationContext { - private worker: Worker - private workerApi: Comlink.Remote +export class SignatureValidation { + private worker: ReturnType + private workerApi: Comlink.Remote constructor() { - // Webpack 5 handles this pattern automatically, creating a separate chunk for the worker - this.worker = new Worker( - /* webpackChunkName: "signature-worker" */ - new URL('./SignatureValidationWorker.js', import.meta.url) - ) + this.worker = createSignatureValidationWorker() this.workerApi = Comlink.wrap(this.worker) } diff --git a/packages/sdk/src/signature/SignatureValidationContext.ts b/packages/sdk/src/signature/SignatureValidationContext.ts deleted file mode 100644 index 77f8afb4a0..0000000000 --- a/packages/sdk/src/signature/SignatureValidationContext.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Interface for signature validation backend. - * Browser implementation uses a Web Worker, Node.js runs on main thread. - */ -import { StreamMessage } from '../protocol/StreamMessage' -import { SignatureValidationResult } from './signatureValidation' - -export interface SignatureValidationContext { - validateSignature(message: StreamMessage): Promise - destroy(): void -} - diff --git a/packages/sdk/src/signature/SignatureValidationWorker.ts b/packages/sdk/src/signature/SignatureValidationWorker.ts index 11c8cf1626..5a759877c0 100644 --- a/packages/sdk/src/signature/SignatureValidationWorker.ts +++ b/packages/sdk/src/signature/SignatureValidationWorker.ts @@ -9,23 +9,4 @@ const workerApi = { export type SignatureValidationWorkerApi = typeof workerApi -// Detect environment and expose accordingly -// Check for Node.js worker_threads first, since `self` is defined in both environments -// but only browser Web Workers have WorkerGlobalScope with addEventListener -let parentPort: import('worker_threads').MessagePort | null = null -try { - // eslint-disable-next-line @typescript-eslint/no-require-imports - parentPort = require('worker_threads').parentPort -} catch { - // Not in Node.js environment -} - -if (parentPort) { - // Node.js Worker Thread - // eslint-disable-next-line @typescript-eslint/no-require-imports - const nodeEndpoint = require('comlink/dist/umd/node-adapter') - Comlink.expose(workerApi, nodeEndpoint(parentPort)) -} else { - // Browser Web Worker - Comlink.expose(workerApi) -} +Comlink.expose(workerApi) diff --git a/packages/sdk/src/signature/SignatureValidator.ts b/packages/sdk/src/signature/SignatureValidator.ts index bbbf0ac3d9..0a355e2da0 100644 --- a/packages/sdk/src/signature/SignatureValidator.ts +++ b/packages/sdk/src/signature/SignatureValidator.ts @@ -5,15 +5,13 @@ import { DestroySignal } from '../DestroySignal' import { StreamMessage } from '../protocol/StreamMessage' import { StreamrClientError } from '../StreamrClientError' import { createSignaturePayload } from './createSignaturePayload' -import { SignatureValidationContext } from './SignatureValidationContext' -// This import will be swapped to BrowserSignatureValidation.mts in browser builds -import SignatureValidation from './ServerSignatureValidation' +import { SignatureValidation } from './SignatureValidationClient' import { SignatureType } from '@streamr/trackerless-network' @scoped(Lifecycle.ContainerScoped) export class SignatureValidator { private readonly erc1271ContractFacade: ERC1271ContractFacade - private validationContext: SignatureValidationContext | undefined + private signatureValidation: SignatureValidation | undefined constructor( erc1271ContractFacade: ERC1271ContractFacade, @@ -23,8 +21,8 @@ export class SignatureValidator { destroySignal.onDestroy.listen(() => this.destroy()) } - private getValidationContext(): SignatureValidationContext { - return this.validationContext ??= new SignatureValidation() + private getSignatureValidation(): SignatureValidation { + return this.signatureValidation ??= new SignatureValidation() } /** @@ -52,7 +50,7 @@ export class SignatureValidator { streamMessage.signature ) } - const result = await this.getValidationContext().validateSignature(streamMessage) + const result = await this.getSignatureValidation().validateSignature(streamMessage) switch (result.type) { case 'valid': return true @@ -69,9 +67,9 @@ export class SignatureValidator { * Cleanup worker resources when the validator is no longer needed. */ destroy(): void { - if (this.validationContext) { - this.validationContext.destroy() - this.validationContext = undefined + if (this.signatureValidation) { + this.signatureValidation.destroy() + this.signatureValidation = undefined } } } From 364baf15c833c0b3fc52de517b5b20e3743f47ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20Roli=C5=84ski?= Date: Tue, 27 Jan 2026 23:19:39 +0100 Subject: [PATCH 19/28] Rename `signatureValidation` to `signatureValidationUtils` --- packages/sdk/src/signature/SignatureValidationClient.ts | 2 +- packages/sdk/src/signature/SignatureValidationWorker.ts | 2 +- .../{signatureValidation.ts => signatureValidationUtils.ts} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename packages/sdk/src/signature/{signatureValidation.ts => signatureValidationUtils.ts} (100%) diff --git a/packages/sdk/src/signature/SignatureValidationClient.ts b/packages/sdk/src/signature/SignatureValidationClient.ts index a0e4f45766..f1899b7e43 100644 --- a/packages/sdk/src/signature/SignatureValidationClient.ts +++ b/packages/sdk/src/signature/SignatureValidationClient.ts @@ -5,7 +5,7 @@ */ import * as Comlink from 'comlink' import { createSignatureValidationWorker } from '@/createSignatureValidationWorker' -import { SignatureValidationResult, toSignatureValidationData } from './signatureValidation' +import { SignatureValidationResult, toSignatureValidationData } from './signatureValidationUtils' import type { SignatureValidationWorkerApi } from './SignatureValidationWorker' import { StreamMessage } from '../protocol/StreamMessage' diff --git a/packages/sdk/src/signature/SignatureValidationWorker.ts b/packages/sdk/src/signature/SignatureValidationWorker.ts index 5a759877c0..80b28676b6 100644 --- a/packages/sdk/src/signature/SignatureValidationWorker.ts +++ b/packages/sdk/src/signature/SignatureValidationWorker.ts @@ -1,5 +1,5 @@ import * as Comlink from 'comlink' -import { validateSignatureData, SignatureValidationResult, SignatureValidationData } from './signatureValidation' +import { validateSignatureData, SignatureValidationResult, SignatureValidationData } from './signatureValidationUtils' const workerApi = { validateSignature: async (data: SignatureValidationData): Promise => { diff --git a/packages/sdk/src/signature/signatureValidation.ts b/packages/sdk/src/signature/signatureValidationUtils.ts similarity index 100% rename from packages/sdk/src/signature/signatureValidation.ts rename to packages/sdk/src/signature/signatureValidationUtils.ts From 5fb810b58f9ea5df5fade315b5a48f96832e5522 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20Roli=C5=84ski?= Date: Tue, 27 Jan 2026 23:20:45 +0100 Subject: [PATCH 20/28] Fix filenames --- .../{SignatureValidationClient.ts => SignatureValidation.ts} | 0 packages/sdk/src/signature/SignatureValidator.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/sdk/src/signature/{SignatureValidationClient.ts => SignatureValidation.ts} (100%) diff --git a/packages/sdk/src/signature/SignatureValidationClient.ts b/packages/sdk/src/signature/SignatureValidation.ts similarity index 100% rename from packages/sdk/src/signature/SignatureValidationClient.ts rename to packages/sdk/src/signature/SignatureValidation.ts diff --git a/packages/sdk/src/signature/SignatureValidator.ts b/packages/sdk/src/signature/SignatureValidator.ts index 0a355e2da0..87328bfe0c 100644 --- a/packages/sdk/src/signature/SignatureValidator.ts +++ b/packages/sdk/src/signature/SignatureValidator.ts @@ -5,7 +5,7 @@ import { DestroySignal } from '../DestroySignal' import { StreamMessage } from '../protocol/StreamMessage' import { StreamrClientError } from '../StreamrClientError' import { createSignaturePayload } from './createSignaturePayload' -import { SignatureValidation } from './SignatureValidationClient' +import { SignatureValidation } from './SignatureValidation' import { SignatureType } from '@streamr/trackerless-network' @scoped(Lifecycle.ContainerScoped) From 9f2ab702f3485d78885a76bfef84ea28eb0ed468 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20Roli=C5=84ski?= Date: Tue, 27 Jan 2026 23:23:55 +0100 Subject: [PATCH 21/28] Use named exports from `comlink` --- packages/sdk/src/signature/SignatureValidation.ts | 6 +++--- packages/sdk/src/signature/SignatureValidationWorker.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/sdk/src/signature/SignatureValidation.ts b/packages/sdk/src/signature/SignatureValidation.ts index f1899b7e43..751dadfb8b 100644 --- a/packages/sdk/src/signature/SignatureValidation.ts +++ b/packages/sdk/src/signature/SignatureValidation.ts @@ -3,7 +3,7 @@ * This offloads CPU-intensive cryptographic operations to a separate thread. * Works in both browser and Node.js environments via platform-specific config. */ -import * as Comlink from 'comlink' +import { wrap, type Remote } from 'comlink' import { createSignatureValidationWorker } from '@/createSignatureValidationWorker' import { SignatureValidationResult, toSignatureValidationData } from './signatureValidationUtils' import type { SignatureValidationWorkerApi } from './SignatureValidationWorker' @@ -11,11 +11,11 @@ import { StreamMessage } from '../protocol/StreamMessage' export class SignatureValidation { private worker: ReturnType - private workerApi: Comlink.Remote + private workerApi: Remote constructor() { this.worker = createSignatureValidationWorker() - this.workerApi = Comlink.wrap(this.worker) + this.workerApi = wrap(this.worker) } async validateSignature(message: StreamMessage): Promise { diff --git a/packages/sdk/src/signature/SignatureValidationWorker.ts b/packages/sdk/src/signature/SignatureValidationWorker.ts index 80b28676b6..c80adb2a8f 100644 --- a/packages/sdk/src/signature/SignatureValidationWorker.ts +++ b/packages/sdk/src/signature/SignatureValidationWorker.ts @@ -1,4 +1,4 @@ -import * as Comlink from 'comlink' +import { expose } from 'comlink' import { validateSignatureData, SignatureValidationResult, SignatureValidationData } from './signatureValidationUtils' const workerApi = { @@ -9,4 +9,4 @@ const workerApi = { export type SignatureValidationWorkerApi = typeof workerApi -Comlink.expose(workerApi) +expose(workerApi) From a7c4f0f86c2e72bc0712d595e059d00b920ce893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20Roli=C5=84ski?= Date: Tue, 27 Jan 2026 23:37:34 +0100 Subject: [PATCH 22/28] Custom expose for nodejs (using Comlink's `nodeAdapter`) --- packages/sdk/src/_browser/exposeWorkerApi.ts | 8 ++++++++ packages/sdk/src/_nodejs/exposeWorkerApi.ts | 11 +++++++++++ .../sdk/src/signature/SignatureValidationWorker.ts | 4 ++-- 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 packages/sdk/src/_browser/exposeWorkerApi.ts create mode 100644 packages/sdk/src/_nodejs/exposeWorkerApi.ts diff --git a/packages/sdk/src/_browser/exposeWorkerApi.ts b/packages/sdk/src/_browser/exposeWorkerApi.ts new file mode 100644 index 0000000000..03ad46b6e8 --- /dev/null +++ b/packages/sdk/src/_browser/exposeWorkerApi.ts @@ -0,0 +1,8 @@ +/** + * Browser-specific Comlink expose wrapper. + */ +import { expose } from 'comlink' + +export function exposeWorkerApi(api: T): void { + expose(api) +} diff --git a/packages/sdk/src/_nodejs/exposeWorkerApi.ts b/packages/sdk/src/_nodejs/exposeWorkerApi.ts new file mode 100644 index 0000000000..acaf00723e --- /dev/null +++ b/packages/sdk/src/_nodejs/exposeWorkerApi.ts @@ -0,0 +1,11 @@ +/** + * Node.js-specific Comlink expose wrapper. + */ +import { expose } from 'comlink' +// eslint-disable-next-line no-restricted-imports +import nodeEndpoint from 'comlink/dist/esm/node-adapter' +import { parentPort } from 'worker_threads' + +export function exposeWorkerApi(api: T): void { + expose(api, nodeEndpoint(parentPort!)) +} diff --git a/packages/sdk/src/signature/SignatureValidationWorker.ts b/packages/sdk/src/signature/SignatureValidationWorker.ts index c80adb2a8f..c03d5e67ec 100644 --- a/packages/sdk/src/signature/SignatureValidationWorker.ts +++ b/packages/sdk/src/signature/SignatureValidationWorker.ts @@ -1,4 +1,4 @@ -import { expose } from 'comlink' +import { exposeWorkerApi } from '@/exposeWorkerApi' import { validateSignatureData, SignatureValidationResult, SignatureValidationData } from './signatureValidationUtils' const workerApi = { @@ -9,4 +9,4 @@ const workerApi = { export type SignatureValidationWorkerApi = typeof workerApi -expose(workerApi) +exposeWorkerApi(workerApi) From 21b3c6ee04f2b14525de07996594f5075c4dfea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20Roli=C5=84ski?= Date: Tue, 27 Jan 2026 23:44:46 +0100 Subject: [PATCH 23/28] Release proxy --- packages/sdk/src/signature/SignatureValidation.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/sdk/src/signature/SignatureValidation.ts b/packages/sdk/src/signature/SignatureValidation.ts index 751dadfb8b..760334e0b5 100644 --- a/packages/sdk/src/signature/SignatureValidation.ts +++ b/packages/sdk/src/signature/SignatureValidation.ts @@ -3,7 +3,7 @@ * This offloads CPU-intensive cryptographic operations to a separate thread. * Works in both browser and Node.js environments via platform-specific config. */ -import { wrap, type Remote } from 'comlink' +import { wrap, releaseProxy, type Remote } from 'comlink' import { createSignatureValidationWorker } from '@/createSignatureValidationWorker' import { SignatureValidationResult, toSignatureValidationData } from './signatureValidationUtils' import type { SignatureValidationWorkerApi } from './SignatureValidationWorker' @@ -25,8 +25,7 @@ export class SignatureValidation { } destroy(): void { - if (this.worker) { - this.worker.terminate() - } + this.workerApi[releaseProxy]() + this.worker.terminate() } } From c45177f3314b85dad8afe843d7db628fa3c5cc05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20Roli=C5=84ski?= Date: Wed, 28 Jan 2026 21:51:34 +0100 Subject: [PATCH 24/28] Fix signature validation tests --- packages/sdk/jest.config.ts | 1 + packages/sdk/rollup.config.mts | 6 +++--- .../_browser/createSignatureValidationWorker.ts | 2 +- packages/sdk/src/_browser/exposeWorkerApi.ts | 8 -------- .../src/_jest/createSignatureValidationWorker.ts | 11 +++++++++++ .../_nodejs/createSignatureValidationWorker.ts | 2 +- packages/sdk/src/_nodejs/exposeWorkerApi.ts | 11 ----------- .../src/signature/SignatureValidationWorker.ts | 16 +++++++++++----- 8 files changed, 28 insertions(+), 29 deletions(-) delete mode 100644 packages/sdk/src/_browser/exposeWorkerApi.ts create mode 100644 packages/sdk/src/_jest/createSignatureValidationWorker.ts delete mode 100644 packages/sdk/src/_nodejs/exposeWorkerApi.ts diff --git a/packages/sdk/jest.config.ts b/packages/sdk/jest.config.ts index 2b30dcad50..e2c55eb2cb 100644 --- a/packages/sdk/jest.config.ts +++ b/packages/sdk/jest.config.ts @@ -11,6 +11,7 @@ const config: Config.InitialOptions = { '@streamr/test-utils/setupCustomMatchers', ], moduleNameMapper: { + "^@/createSignatureValidationWorker$": "/src/_jest/createSignatureValidationWorker.ts", "^@/(.*)$": "/src/_nodejs/$1", }, transform: { diff --git a/packages/sdk/rollup.config.mts b/packages/sdk/rollup.config.mts index 16d46f509d..27d2cf78b9 100644 --- a/packages/sdk/rollup.config.mts +++ b/packages/sdk/rollup.config.mts @@ -216,7 +216,7 @@ function workerNodejs(): RollupOptions { context: 'globalThis', output: { format: 'es', - file: './dist/workers/SignatureValidationWorker.node.js', + file: './dist/workers/SignatureValidationWorker.node.mjs', sourcemap: true, }, plugins: [ @@ -229,7 +229,7 @@ function workerNodejs(): RollupOptions { }), cjs(), ], - external: [], + external: [/node_modules/, /@streamr\//], onwarn, } } @@ -243,7 +243,7 @@ function workerBrowser(): RollupOptions { context: 'self', output: { format: 'es', - file: './dist/workers/SignatureValidationWorker.browser.js', + file: './dist/workers/SignatureValidationWorker.browser.mjs', sourcemap: true, }, plugins: [ diff --git a/packages/sdk/src/_browser/createSignatureValidationWorker.ts b/packages/sdk/src/_browser/createSignatureValidationWorker.ts index e497d2f4ff..73388eefb3 100644 --- a/packages/sdk/src/_browser/createSignatureValidationWorker.ts +++ b/packages/sdk/src/_browser/createSignatureValidationWorker.ts @@ -5,7 +5,7 @@ import Worker from 'web-worker' export function createSignatureValidationWorker(): InstanceType { return new Worker( - new URL('./workers/SignatureValidationWorker.browser.js', import.meta.url), + new URL('./workers/SignatureValidationWorker.browser.mjs', import.meta.url), { type: 'module' } ) } diff --git a/packages/sdk/src/_browser/exposeWorkerApi.ts b/packages/sdk/src/_browser/exposeWorkerApi.ts deleted file mode 100644 index 03ad46b6e8..0000000000 --- a/packages/sdk/src/_browser/exposeWorkerApi.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Browser-specific Comlink expose wrapper. - */ -import { expose } from 'comlink' - -export function exposeWorkerApi(api: T): void { - expose(api) -} diff --git a/packages/sdk/src/_jest/createSignatureValidationWorker.ts b/packages/sdk/src/_jest/createSignatureValidationWorker.ts new file mode 100644 index 0000000000..73eedd434f --- /dev/null +++ b/packages/sdk/src/_jest/createSignatureValidationWorker.ts @@ -0,0 +1,11 @@ +/** + * Jest-specific signature validation worker factory. + */ +import Worker from 'web-worker' + +export function createSignatureValidationWorker(): InstanceType { + return new Worker( + new URL('../../dist/workers/SignatureValidationWorker.node.mjs', import.meta.url), + { type: 'module' } + ) +} diff --git a/packages/sdk/src/_nodejs/createSignatureValidationWorker.ts b/packages/sdk/src/_nodejs/createSignatureValidationWorker.ts index 0f1d48a1a4..ad891da4f6 100644 --- a/packages/sdk/src/_nodejs/createSignatureValidationWorker.ts +++ b/packages/sdk/src/_nodejs/createSignatureValidationWorker.ts @@ -5,7 +5,7 @@ import Worker from 'web-worker' export function createSignatureValidationWorker(): InstanceType { return new Worker( - new URL('./workers/SignatureValidationWorker.node.js', import.meta.url), + new URL('./workers/SignatureValidationWorker.node.mjs', import.meta.url), { type: 'module' } ) } diff --git a/packages/sdk/src/_nodejs/exposeWorkerApi.ts b/packages/sdk/src/_nodejs/exposeWorkerApi.ts deleted file mode 100644 index acaf00723e..0000000000 --- a/packages/sdk/src/_nodejs/exposeWorkerApi.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Node.js-specific Comlink expose wrapper. - */ -import { expose } from 'comlink' -// eslint-disable-next-line no-restricted-imports -import nodeEndpoint from 'comlink/dist/esm/node-adapter' -import { parentPort } from 'worker_threads' - -export function exposeWorkerApi(api: T): void { - expose(api, nodeEndpoint(parentPort!)) -} diff --git a/packages/sdk/src/signature/SignatureValidationWorker.ts b/packages/sdk/src/signature/SignatureValidationWorker.ts index c03d5e67ec..f55ac77e7b 100644 --- a/packages/sdk/src/signature/SignatureValidationWorker.ts +++ b/packages/sdk/src/signature/SignatureValidationWorker.ts @@ -1,12 +1,18 @@ -import { exposeWorkerApi } from '@/exposeWorkerApi' -import { validateSignatureData, SignatureValidationResult, SignatureValidationData } from './signatureValidationUtils' +import { expose } from 'comlink' +import { + validateSignatureData, + SignatureValidationResult, + SignatureValidationData, +} from './signatureValidationUtils' const workerApi = { - validateSignature: async (data: SignatureValidationData): Promise => { + validateSignature: async ( + data: SignatureValidationData + ): Promise => { return validateSignatureData(data) - } + }, } export type SignatureValidationWorkerApi = typeof workerApi -exposeWorkerApi(workerApi) +expose(workerApi) From 856a0126702bf01e935f58bab2df12b02297599a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20Roli=C5=84ski?= Date: Wed, 28 Jan 2026 23:50:27 +0100 Subject: [PATCH 25/28] Fix SDK's browser tests --- .../src/createKarmaConfig.ts | 18 ++++++++++++++++-- packages/browser-test-runner/src/exports.ts | 1 + packages/sdk/createKarmaConfig.ts | 6 +++++- packages/sdk/rollup.config.mts | 4 ++-- .../_karma/createSignatureValidationWorker.ts | 11 +++++++++++ 5 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 packages/sdk/src/_karma/createSignatureValidationWorker.ts diff --git a/packages/browser-test-runner/src/createKarmaConfig.ts b/packages/browser-test-runner/src/createKarmaConfig.ts index b38a4ea05c..3f826cc4a1 100644 --- a/packages/browser-test-runner/src/createKarmaConfig.ts +++ b/packages/browser-test-runner/src/createKarmaConfig.ts @@ -4,8 +4,16 @@ import type { Configuration, ExternalItem } from 'webpack' const DEBUG_MODE = process.env.BROWSER_TEST_DEBUG_MODE ?? false +export interface KarmaConfigOptions { + // File patterns to serve but not include in the test bundle (e.g. worker files) + servedFiles?: string[] +} + export const createKarmaConfig = ( - testPaths: string[], webpackConfig: () => Configuration, localDirectory?: string + testPaths: string[], + webpackConfig: () => Configuration, + localDirectory?: string, + options: KarmaConfigOptions = {} ): (config: any) => any => { const setupFiles = [fileURLToPath(new URL('./karma-setup.js', import.meta.url))] @@ -57,7 +65,13 @@ export const createKarmaConfig = ( reporters: ['spec'], files: [ ...setupFiles, - ...testPaths + ...testPaths, + ...(options.servedFiles ?? []).map((pattern) => ({ + pattern, + included: false, + served: true, + watched: false + })) ], preprocessors, customLaunchers: { diff --git a/packages/browser-test-runner/src/exports.ts b/packages/browser-test-runner/src/exports.ts index da76c5cee4..e04593074b 100644 --- a/packages/browser-test-runner/src/exports.ts +++ b/packages/browser-test-runner/src/exports.ts @@ -1,2 +1,3 @@ export { createKarmaConfig } from './createKarmaConfig' +export type { KarmaConfigOptions } from './createKarmaConfig' export { createWebpackConfig } from './createWebpackConfig' diff --git a/packages/sdk/createKarmaConfig.ts b/packages/sdk/createKarmaConfig.ts index 8c0769f11e..2543c4e4fc 100644 --- a/packages/sdk/createKarmaConfig.ts +++ b/packages/sdk/createKarmaConfig.ts @@ -15,6 +15,7 @@ export function createKarmaConfig(testPaths: string[]): ReturnType void): void { diff --git a/packages/sdk/src/_karma/createSignatureValidationWorker.ts b/packages/sdk/src/_karma/createSignatureValidationWorker.ts new file mode 100644 index 0000000000..e2e4090127 --- /dev/null +++ b/packages/sdk/src/_karma/createSignatureValidationWorker.ts @@ -0,0 +1,11 @@ +/** + * Browser-specific signature validation worker factory. + */ +import Worker from 'web-worker' + +export function createSignatureValidationWorker(): InstanceType { + return new Worker( + new URL('../../dist/workers/SignatureValidationWorker.browser.mjs', import.meta.url), + { type: 'module' } + ) +} From 6853ccd205c9c2b88efb345291f96d61dd6333d2 Mon Sep 17 00:00:00 2001 From: juslesan Date: Thu, 29 Jan 2026 11:23:02 +0200 Subject: [PATCH 26/28] remove process.exit --- packages/cli-tools/src/command.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/cli-tools/src/command.ts b/packages/cli-tools/src/command.ts index 3f34ec5ad7..30f49ad10f 100644 --- a/packages/cli-tools/src/command.ts +++ b/packages/cli-tools/src/command.ts @@ -53,8 +53,6 @@ export const createClientCommand = ( await client.destroy() } } - // Exit cleanly after command completes - worker threads may keep event loop alive - process.exit(0) } catch (e: any) { console.error(e) process.exit(1) From ae4576210bae64559775975dac90b8849dc0588d Mon Sep 17 00:00:00 2001 From: juslesan Date: Thu, 29 Jan 2026 11:29:55 +0200 Subject: [PATCH 27/28] keep the process.exit --- packages/cli-tools/src/command.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/cli-tools/src/command.ts b/packages/cli-tools/src/command.ts index 30f49ad10f..3f34ec5ad7 100644 --- a/packages/cli-tools/src/command.ts +++ b/packages/cli-tools/src/command.ts @@ -53,6 +53,8 @@ export const createClientCommand = ( await client.destroy() } } + // Exit cleanly after command completes - worker threads may keep event loop alive + process.exit(0) } catch (e: any) { console.error(e) process.exit(1) From 738a12656f265abe4dcaad8ab9c671942050cc1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20Roli=C5=84ski?= Date: Thu, 29 Jan 2026 16:00:08 +0100 Subject: [PATCH 28/28] Add comment explaining `servedFiles` config in SDK's Karma --- packages/sdk/createKarmaConfig.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/sdk/createKarmaConfig.ts b/packages/sdk/createKarmaConfig.ts index 2543c4e4fc..a1adab036e 100644 --- a/packages/sdk/createKarmaConfig.ts +++ b/packages/sdk/createKarmaConfig.ts @@ -33,6 +33,11 @@ export function createKarmaConfig(testPaths: string[]): ReturnType