From 30b693b0e139ee9f482e9ca3540f8454f8fd94cc Mon Sep 17 00:00:00 2001 From: lwin Date: Mon, 9 Mar 2026 23:44:03 +0800 Subject: [PATCH 1/8] fix: fixed Ed25519 Key gen --- src/ed25519/utils.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/ed25519/utils.ts b/src/ed25519/utils.ts index acea6889..40355479 100644 --- a/src/ed25519/utils.ts +++ b/src/ed25519/utils.ts @@ -13,9 +13,6 @@ export function getED25519Key(privateKey: string | Uint8Array): { pk: Uint8Array; } { const privKey = typeof privateKey === "string" ? hexToBytes(privateKey) : privateKey; - const pk = ed25519.getPublicKey(privKey); - const sk = new Uint8Array(64); - sk.set(privKey, 0); - sk.set(pk, 32); - return { sk, pk }; + const { secretKey, publicKey } = ed25519.keygen(privKey); + return { sk: secretKey, pk: publicKey }; } From 40925edfcb80d743d94ee6b43c00d1ffa2ab6609 Mon Sep 17 00:00:00 2001 From: lwin Date: Mon, 9 Mar 2026 23:44:36 +0800 Subject: [PATCH 2/8] fix: fixed PostMessageStream._postMessage override --- src/jrpc/postMessageStream.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/jrpc/postMessageStream.ts b/src/jrpc/postMessageStream.ts index 5b6400ee..443e7ff2 100644 --- a/src/jrpc/postMessageStream.ts +++ b/src/jrpc/postMessageStream.ts @@ -57,8 +57,8 @@ export class PostMessageStream extends BasePostMessageStream { if (typeof dataObj.data === "object") { const dataObjData = dataObj.data as Record; if (Array.isArray(dataObjData.params) && dataObjData.params.length > 0) { - const dataObjDataParam = dataObjData.params[0] as Record; - if (dataObjDataParam._origin) { + const dataObjDataParam = { ...dataObjData.params[0] } as Record; + if (dataObjDataParam?._origin) { originConstraint = dataObjDataParam._origin as string; } From 48df2e1ff8acc36609232d738be057a9f2777769 Mon Sep 17 00:00:00 2001 From: lwin Date: Mon, 9 Mar 2026 23:44:53 +0800 Subject: [PATCH 3/8] feat: Stream Middleware V2 --- examples/vue-example/package-lock.json | 13 ++--- src/jrpc/v2/createStreamMiddlewareV2.ts | 71 +++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 src/jrpc/v2/createStreamMiddlewareV2.ts diff --git a/examples/vue-example/package-lock.json b/examples/vue-example/package-lock.json index a2c798ad..8e0c48e5 100644 --- a/examples/vue-example/package-lock.json +++ b/examples/vue-example/package-lock.json @@ -36,7 +36,7 @@ }, "../..": { "name": "@web3auth/auth", - "version": "11.1.1", + "version": "11.2.1", "license": "MIT", "dependencies": { "@toruslabs/constants": "^16.0.0", @@ -59,8 +59,9 @@ "@babel/runtime": "^7.28.6", "@rollup/plugin-replace": "^6.0.3", "@toruslabs/config": "^4.0.0", - "@toruslabs/eslint-config-typescript": "^5.0.0", - "@toruslabs/torus-scripts": "^8.0.0", + "@toruslabs/eslint-config-typescript": "^5.0.1", + "@toruslabs/torus-scripts": "^8.0.1", + "@toruslabs/tweetnacl-js": "^1.0.4", "@types/color": "^4.2.0", "@types/deep-freeze-strict": "^1.1.2", "@types/node": "^25", @@ -71,7 +72,7 @@ "cross-env": "^10.1.0", "eslint": "^9.39.2", "husky": "^9.1.7", - "lint-staged": "^16.2.7", + "lint-staged": "^16.3.2", "prettier": "^3.8.1", "rimraf": "^6.1.3", "tsconfig-paths": "^4.2.0", @@ -85,8 +86,8 @@ "npm": ">=10.x" }, "optionalDependencies": { - "@nx/nx-linux-x64-gnu": "^22.5.1", - "@rollup/rollup-linux-x64-gnu": "^4.57.1" + "@nx/nx-linux-x64-gnu": "^22.5.4", + "@rollup/rollup-linux-x64-gnu": "^4.59.0" }, "peerDependencies": { "@babel/runtime": "7.x", diff --git a/src/jrpc/v2/createStreamMiddlewareV2.ts b/src/jrpc/v2/createStreamMiddlewareV2.ts new file mode 100644 index 00000000..2a298719 --- /dev/null +++ b/src/jrpc/v2/createStreamMiddlewareV2.ts @@ -0,0 +1,71 @@ +import { Duplex } from "stream"; + +import { JRPCRequest, Json } from "../interfaces"; +import { SafeEventEmitter } from "../safeEventEmitter"; +import { JRPCMiddlewareV2 } from "./v2interfaces"; + +/** + * Creates a V2-compatible client-side stream middleware for the dapp ↔ iframe + * transport layer. + * + * Replaces V1's `createStreamMiddleware` by providing: + * - A terminal middleware that sends outbound requests through the stream and + * resolves when the matching response arrives. + * - A Duplex object stream to pump through the ObjectMultiplex channel. + * - Inbound notification routing via the supplied `notificationEmitter`. + */ +export function createClientStreamMiddlewareV2({ notificationEmitter }: { notificationEmitter?: SafeEventEmitter } = {}): { + middleware: JRPCMiddlewareV2, Json>; + stream: Duplex; +} { + const pendingRequests = new Map void; reject: (error: unknown) => void }>(); + + function noop() { + // noop + } + + function write(this: Duplex, data: Record, _encoding: BufferEncoding, cb: () => void) { + // eslint-disable-next-line no-console + console.log("createClientStreamMiddlewareV2::data", data); + if (data.method !== undefined) { + // Inbound request or notification from remote — route to event emitter + // (matches V1 createStreamMiddleware behavior where all non-response + // messages are emitted as "notification" regardless of whether they + // carry an id) + notificationEmitter?.emit("notification", data); + } else { + // No method → this is a response to one of our pending outbound requests + const id = data.id as number | string | undefined; + if (id !== undefined && id !== null && pendingRequests.has(id)) { + const pending = pendingRequests.get(id)!; + pendingRequests.delete(id); + + if (data.error) { + const errorObj = data.error as { code?: number; message?: string; data?: unknown }; + pending.reject(Object.assign(new Error(errorObj.message || "Internal JSON-RPC error"), { code: errorObj.code, data: errorObj.data })); + } else { + pending.resolve(data.result as Json); + } + } + } + + cb(); + } + + const stream = new Duplex({ objectMode: true, read: noop, write }); + + stream.once("close", () => { + const error = new Error("Stream closed"); + pendingRequests.forEach(({ reject }) => reject(error)); + pendingRequests.clear(); + }); + + const middleware: JRPCMiddlewareV2, Json> = ({ request }) => { + return new Promise((resolve, reject) => { + pendingRequests.set(request.id as number | string, { resolve, reject }); + stream.push(request); + }); + }; + + return { middleware, stream }; +} From aef8f4f5f5649baff2ab9db8e141446babd2512f Mon Sep 17 00:00:00 2001 From: Archit Date: Tue, 10 Mar 2026 09:37:42 +0700 Subject: [PATCH 4/8] fix: addIdtoken from storage in user info --- src/core/auth.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/core/auth.ts b/src/core/auth.ts index 00810463..0dfc7c71 100644 --- a/src/core/auth.ts +++ b/src/core/auth.ts @@ -448,11 +448,14 @@ export class Auth { if (this.authProvider) this.authProvider.cleanup(); } - getUserInfo(): AuthUserInfo { + async getUserInfo(): Promise { if (!this.sessionId) { throw LoginError.userNotLoggedIn(); } - return this.state.userInfo; + return { + ...this.state.userInfo, + idToken: await this.sessionManager.getIdToken(), + }; } private getDefaultCitadelServerUrl(buildEnv: BUILD_ENV_TYPE): string { From b036c9c397f89198e0141b26dffc0db3f9130f0d Mon Sep 17 00:00:00 2001 From: lwin Date: Tue, 10 Mar 2026 10:41:10 +0800 Subject: [PATCH 5/8] chore: minor types updates --- src/core/auth.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/auth.ts b/src/core/auth.ts index 00810463..00f932da 100644 --- a/src/core/auth.ts +++ b/src/core/auth.ts @@ -1,6 +1,6 @@ import { SESSION_SERVER_API_URL, SESSION_SERVER_SOCKET_URL } from "@toruslabs/constants"; import { AUTH_CONNECTION, AUTH_CONNECTION_TYPE, constructURL, getTimeout, UX_MODE } from "@toruslabs/customauth"; -import { add0x } from "@toruslabs/metadata-helpers"; +import { add0x, Hex } from "@toruslabs/metadata-helpers"; import { AuthSessionManager, SessionManager as StorageManager } from "@toruslabs/session-manager"; import { klona } from "klona/json"; @@ -463,14 +463,14 @@ export class Auth { return CITADEL_SERVER_URL_PRODUCTION; } - private async storeAuthPayload(loginId: string, payload: AuthRequestPayload, timeout = 600, skipAwait = false): Promise { + private async storeAuthPayload(loginId: Hex, payload: AuthRequestPayload, timeout = 600, skipAwait = false): Promise { if (!this.sessionManager) throw InitializationError.notInitialized(); const authRequestStorageManager = new StorageManager({ sessionServerBaseUrl: payload.options.storageServerUrl, sessionNamespace: payload.options.sessionNamespace, sessionTime: timeout, // each login key must be used with 10 mins (might be used at the end of popup redirect) - sessionId: add0x(loginId), + sessionId: loginId, allowedOrigin: this.options.sdkUrl, }); From b9161ad32b9b1fb069c3f53d0237bdb1b8787031 Mon Sep 17 00:00:00 2001 From: lwin Date: Tue, 10 Mar 2026 13:36:34 +0800 Subject: [PATCH 6/8] feat: updated demo --- examples/vue-example/src/App.vue | 2 +- src/ed25519/utils.ts | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/examples/vue-example/src/App.vue b/examples/vue-example/src/App.vue index fd71031f..bd166612 100644 --- a/examples/vue-example/src/App.vue +++ b/examples/vue-example/src/App.vue @@ -765,7 +765,7 @@ const getUserInfo = async () => { if (!openloginInstance.value) { throw new Error("Openlogin is not available."); } - const userInfo = openloginInstance.value.getUserInfo(); + const userInfo = await openloginInstance.value.getUserInfo(); printToConsole("User Info", userInfo); }; diff --git a/src/ed25519/utils.ts b/src/ed25519/utils.ts index 40355479..acea6889 100644 --- a/src/ed25519/utils.ts +++ b/src/ed25519/utils.ts @@ -13,6 +13,9 @@ export function getED25519Key(privateKey: string | Uint8Array): { pk: Uint8Array; } { const privKey = typeof privateKey === "string" ? hexToBytes(privateKey) : privateKey; - const { secretKey, publicKey } = ed25519.keygen(privKey); - return { sk: secretKey, pk: publicKey }; + const pk = ed25519.getPublicKey(privKey); + const sk = new Uint8Array(64); + sk.set(privKey, 0); + sk.set(pk, 32); + return { sk, pk }; } From b3b1a90364cfc2e14b1e9e656da345f1e40bb8ee Mon Sep 17 00:00:00 2001 From: lwin Date: Tue, 10 Mar 2026 14:06:57 +0800 Subject: [PATCH 7/8] feat: updated exports and fix postMessageStream --- src/jrpc/postMessageStream.ts | 23 +++++--- src/jrpc/v2/createStreamMiddlewareV2.ts | 2 - src/jrpc/v2/index.ts | 1 + test/ed25519.test.ts | 26 ++++++++- test/postMessageStream.test.ts | 75 +++++++++++++++++++++++++ 5 files changed, 115 insertions(+), 12 deletions(-) create mode 100644 test/postMessageStream.test.ts diff --git a/src/jrpc/postMessageStream.ts b/src/jrpc/postMessageStream.ts index 443e7ff2..66de21c5 100644 --- a/src/jrpc/postMessageStream.ts +++ b/src/jrpc/postMessageStream.ts @@ -1,5 +1,6 @@ import type { DuplexOptions } from "readable-stream"; +import { cloneDeep } from "../utils"; import { BasePostMessageStream, isValidStreamMessage, type PostMessageEvent } from "./basePostMessageStream"; /* istanbul ignore next */ @@ -50,19 +51,23 @@ export class PostMessageStream extends BasePostMessageStream { } protected _postMessage(data: unknown): void { + // clone the data to avoid mutating the original object and bypass the frozen state of the object + const clonedData = cloneDeep(data); let originConstraint = this._targetOrigin; - if (typeof data === "object") { - // re-create the object in case it's frozen - const dataObj = { ...data } as Record; + if (typeof clonedData === "object") { + const dataObj = clonedData as Record; if (typeof dataObj.data === "object") { const dataObjData = dataObj.data as Record; if (Array.isArray(dataObjData.params) && dataObjData.params.length > 0) { - const dataObjDataParam = { ...dataObjData.params[0] } as Record; - if (dataObjDataParam?._origin) { - originConstraint = dataObjDataParam._origin as string; + const firstParam = dataObjData.params[0]; + if (typeof firstParam === "object" && firstParam !== null) { + const dataObjDataParam = firstParam as Record; + if (dataObjDataParam._origin) { + originConstraint = dataObjDataParam._origin as string; + } + + dataObjDataParam._origin = window.location.origin; } - - dataObjDataParam._origin = window.location.origin; } } } @@ -70,7 +75,7 @@ export class PostMessageStream extends BasePostMessageStream { this._targetWindow.postMessage( { target: this._target, - data, + data: clonedData, }, originConstraint ); diff --git a/src/jrpc/v2/createStreamMiddlewareV2.ts b/src/jrpc/v2/createStreamMiddlewareV2.ts index 2a298719..bf252ec1 100644 --- a/src/jrpc/v2/createStreamMiddlewareV2.ts +++ b/src/jrpc/v2/createStreamMiddlewareV2.ts @@ -25,8 +25,6 @@ export function createClientStreamMiddlewareV2({ notificationEmitter }: { notifi } function write(this: Duplex, data: Record, _encoding: BufferEncoding, cb: () => void) { - // eslint-disable-next-line no-console - console.log("createClientStreamMiddlewareV2::data", data); if (data.method !== undefined) { // Inbound request or notification from remote — route to event emitter // (matches V1 createStreamMiddleware behavior where all non-response diff --git a/src/jrpc/v2/index.ts b/src/jrpc/v2/index.ts index 0c6c8460..37ec2be8 100644 --- a/src/jrpc/v2/index.ts +++ b/src/jrpc/v2/index.ts @@ -2,6 +2,7 @@ export { getUniqueId, isNotification, isRequest } from "../../utils/jrpc"; export { asLegacyMiddleware } from "./asLegacyMiddleware"; export { deepClone, fromLegacyRequest, makeContext, propagateToContext, propagateToMutableRequest, propagateToRequest } from "./compatibility-utils"; export { createScaffoldMiddleware as createScaffoldMiddlewareV2 } from "./createScaffoldMiddleware"; +export { createClientStreamMiddlewareV2 } from "./createStreamMiddlewareV2"; export { JRPCEngineV2 } from "./jrpcEngineV2"; export { JRPCServer } from "./jrpcServer"; export { createEngineStreamV2 } from "./messageStream"; diff --git a/test/ed25519.test.ts b/test/ed25519.test.ts index bc6cec2b..c222749f 100644 --- a/test/ed25519.test.ts +++ b/test/ed25519.test.ts @@ -1,4 +1,4 @@ -import { hexToBytes } from "@toruslabs/metadata-helpers"; +import { getEd25519, hexToBytes } from "@toruslabs/metadata-helpers"; import nacl from "@toruslabs/tweetnacl-js"; import { describe, expect, it } from "vitest"; @@ -77,6 +77,12 @@ const testVectors: { seed: string; pk: string }[] = [ }, ]; +const signingTestVector = { + seed: "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", + pk: "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", + signature: "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b", +}; + describe("ED25519", () => { it("should return 64-byte sk with seed prefix and 32-byte pk suffix", () => { const { sk, pk } = getED25519Key(testVectors[0].seed); @@ -154,3 +160,21 @@ describe("getED25519Key vs getED25519KeyOld", () => { expect(newResult.sk).toEqual(oldResult.sk); }); }); + +describe("getED25519Key signing", () => { + it("should sign and verify the RFC 8032 empty-message test vector", () => { + const message = new Uint8Array(0); + const { sk, pk } = getED25519Key(signingTestVector.seed); + const signature = nacl.sign.detached(message, sk); + + expect(pk).toEqual(hexToBytes(signingTestVector.pk)); + expect(signature).toEqual(hexToBytes(signingTestVector.signature)); + expect(nacl.sign.detached.verify(message, signature, pk)).toBe(true); + + const nobleEd25519 = getEd25519(); + const nobleCompatibleKey = sk.slice(0, 32); + const nobleSig = nobleEd25519.sign(message, nobleCompatibleKey); + expect(nobleSig).toEqual(signature); + expect(nobleEd25519.verify(nobleSig, message, pk)).toBe(true); + }); +}); diff --git a/test/postMessageStream.test.ts b/test/postMessageStream.test.ts new file mode 100644 index 00000000..5e133d68 --- /dev/null +++ b/test/postMessageStream.test.ts @@ -0,0 +1,75 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +type PostMessagePayload = { + data: { + params: Array>; + }; +}; + +describe("PostMessageStream", () => { + let targetWindow: { postMessage: ReturnType }; + + async function createStream() { + const { PostMessageStream } = await import("../src/jrpc/postMessageStream"); + const stream = new PostMessageStream({ + name: "provider", + target: "auth", + targetWindow: targetWindow as unknown as Window, + }); + + targetWindow.postMessage.mockClear(); + + return stream; + } + + beforeEach(() => { + vi.resetModules(); + targetWindow = { postMessage: vi.fn() }; + + vi.stubGlobal("window", { + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + location: { origin: "https://current.example" }, + postMessage: vi.fn(), + }); + vi.stubGlobal("MessageEvent", class MessageEventMock {}); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("writes the current origin into the posted payload params", async () => { + const stream = await createStream(); + const payload: PostMessagePayload = { data: { params: [{}] } }; + + (stream as unknown as { _postMessage: (data: unknown) => void })._postMessage(payload); + + expect(targetWindow.postMessage).toHaveBeenCalledOnce(); + const [message, originConstraint] = targetWindow.postMessage.mock.calls[0] as [{ target: string; data: PostMessagePayload }, string]; + + expect(originConstraint).toBe("*"); + expect(message.target).toBe("auth"); + expect(message.data.data.params[0]._origin).toBe("https://current.example"); + // the original payload should not be mutated + expect(payload.data.params[0]._origin).toBeUndefined(); + }); + + it("uses the previous _origin as the postMessage origin constraint before rewriting it", async () => { + const stream = await createStream(); + const payload: PostMessagePayload = { + data: { + params: [{ _origin: "https://allowed.example" }], + }, + }; + + (stream as unknown as { _postMessage: (data: unknown) => void })._postMessage(payload); + + expect(targetWindow.postMessage).toHaveBeenCalledOnce(); + const [message, originConstraint] = targetWindow.postMessage.mock.calls[0] as [{ target: string; data: PostMessagePayload }, string]; + + expect(originConstraint).toBe("https://allowed.example"); + expect(message.target).toBe("auth"); + expect(message.data.data.params[0]._origin).toBe("https://current.example"); + }); +}); From b2b7b80d81d44847d3bcb55c5ab3cc0ccf6601f0 Mon Sep 17 00:00:00 2001 From: lwin Date: Tue, 10 Mar 2026 14:15:19 +0800 Subject: [PATCH 8/8] fix: fixed duplex import --- src/jrpc/v2/createStreamMiddlewareV2.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jrpc/v2/createStreamMiddlewareV2.ts b/src/jrpc/v2/createStreamMiddlewareV2.ts index bf252ec1..9649cbf8 100644 --- a/src/jrpc/v2/createStreamMiddlewareV2.ts +++ b/src/jrpc/v2/createStreamMiddlewareV2.ts @@ -1,4 +1,4 @@ -import { Duplex } from "stream"; +import { Duplex } from "readable-stream"; import { JRPCRequest, Json } from "../interfaces"; import { SafeEventEmitter } from "../safeEventEmitter";