Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions examples/vue-example/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion examples/vue-example/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};

Expand Down
13 changes: 8 additions & 5 deletions src/core/auth.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -448,11 +448,14 @@ export class Auth {
if (this.authProvider) this.authProvider.cleanup();
}

getUserInfo(): AuthUserInfo {
async getUserInfo(): Promise<AuthUserInfo> {
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 {
Expand All @@ -463,14 +466,14 @@ export class Auth {
return CITADEL_SERVER_URL_PRODUCTION;
}

private async storeAuthPayload(loginId: string, payload: AuthRequestPayload, timeout = 600, skipAwait = false): Promise<void> {
private async storeAuthPayload(loginId: Hex, payload: AuthRequestPayload, timeout = 600, skipAwait = false): Promise<void> {
if (!this.sessionManager) throw InitializationError.notInitialized();

const authRequestStorageManager = new StorageManager<AuthRequestPayload>({
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,
});

Expand Down
23 changes: 14 additions & 9 deletions src/jrpc/postMessageStream.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { DuplexOptions } from "readable-stream";

import { cloneDeep } from "../utils";
import { BasePostMessageStream, isValidStreamMessage, type PostMessageEvent } from "./basePostMessageStream";

/* istanbul ignore next */
Expand Down Expand Up @@ -50,27 +51,31 @@ 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<string, unknown>;
if (typeof clonedData === "object") {
const dataObj = clonedData as Record<string, unknown>;
if (typeof dataObj.data === "object") {
const dataObjData = dataObj.data as Record<string, unknown>;
if (Array.isArray(dataObjData.params) && dataObjData.params.length > 0) {
const dataObjDataParam = dataObjData.params[0] as Record<string, unknown>;
if (dataObjDataParam._origin) {
originConstraint = dataObjDataParam._origin as string;
const firstParam = dataObjData.params[0];
if (typeof firstParam === "object" && firstParam !== null) {
const dataObjDataParam = firstParam as Record<string, unknown>;
if (dataObjDataParam._origin) {
originConstraint = dataObjDataParam._origin as string;
}

dataObjDataParam._origin = window.location.origin;
}

dataObjDataParam._origin = window.location.origin;
}
}
}

this._targetWindow.postMessage(
{
target: this._target,
data,
data: clonedData,
},
originConstraint
);
Expand Down
69 changes: 69 additions & 0 deletions src/jrpc/v2/createStreamMiddlewareV2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Duplex } from "readable-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<JRPCRequest<unknown>, Json>;
stream: Duplex;
} {
const pendingRequests = new Map<number | string, { resolve: (result: Json) => void; reject: (error: unknown) => void }>();

function noop() {
// noop
}

function write(this: Duplex, data: Record<string, unknown>, _encoding: BufferEncoding, cb: () => void) {
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<JRPCRequest<unknown>, Json> = ({ request }) => {
return new Promise<Json>((resolve, reject) => {
pendingRequests.set(request.id as number | string, { resolve, reject });
stream.push(request);
});
};

return { middleware, stream };
}
1 change: 1 addition & 0 deletions src/jrpc/v2/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
26 changes: 25 additions & 1 deletion test/ed25519.test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
});
});
75 changes: 75 additions & 0 deletions test/postMessageStream.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

type PostMessagePayload = {
data: {
params: Array<Record<string, unknown>>;
};
};

describe("PostMessageStream", () => {
let targetWindow: { postMessage: ReturnType<typeof vi.fn> };

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");
});
});
Loading