From 98baa32c45af877cdead6133cc6a67579d61cd6c Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 30 Jul 2024 09:19:08 +0200 Subject: [PATCH 01/15] feat: signer ready --- src/signer.ts | 49 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/src/signer.ts b/src/signer.ts index e3843cb5..f31633ab 100644 --- a/src/signer.ts +++ b/src/signer.ts @@ -1,8 +1,12 @@ -import type { - IcrcWalletPermissionsRequestType, - IcrcWalletRequestPermissionsRequestType, - IcrcWalletSupportedStandardsRequestType +import {nonNullish} from '@dfinity/utils'; +import { + IcrcWalletStatusRequest, + type IcrcWalletPermissionsRequestType, + type IcrcWalletRequestPermissionsRequestType, + type IcrcWalletSupportedStandardsRequestType } from './types/icrc-requests'; +import type {IcrcReadyResponseType} from './types/icrc-responses'; +import {JSON_RPC_VERSION_2} from './types/rpc'; /** * The parameters to initialize a signer. @@ -20,7 +24,7 @@ type SignerMessageEvent = MessageEvent< >; export class Signer { - readonly #walletOrigin: string | undefined; + #walletOrigin: string | undefined; private constructor(_parameters: SignerParameters) { window.addEventListener('message', this.onMessageListener); @@ -50,5 +54,38 @@ export class Signer { void this.onMessage(message); }; - private readonly onMessage = async (_message: SignerMessageEvent): Promise => {}; + private readonly onMessage = async ({ + data: msgData, + origin + }: SignerMessageEvent): Promise => { + this.assertAndSetOrigin(origin); + + const {success: isStatusRequest, data} = IcrcWalletStatusRequest.safeParse(msgData); + + if (isStatusRequest) { + const notifyReady = (): void => { + const msg: IcrcReadyResponseType = { + jsonrpc: JSON_RPC_VERSION_2, + id: data.id, + result: 'ready' + }; + + window.opener.postMessage(msg, origin); + }; + + notifyReady(); + } + }; + + private assertAndSetOrigin(origin: string): void { + if (nonNullish(this.#walletOrigin) && this.#walletOrigin !== origin) { + throw new Error('Origin is not allowed to interact with the signer'); + } + + if (nonNullish(this.#walletOrigin)) { + return; + } + + this.#walletOrigin = origin; + } } From b0e0feb9f6f14ccdc71d177edac9da836943b0d0 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 30 Jul 2024 09:41:48 +0200 Subject: [PATCH 02/15] feat: extract signer handler --- src/handlers/signer.handlers.ts | 12 ++++++++++++ src/signer.ts | 16 +++------------- 2 files changed, 15 insertions(+), 13 deletions(-) create mode 100644 src/handlers/signer.handlers.ts diff --git a/src/handlers/signer.handlers.ts b/src/handlers/signer.handlers.ts new file mode 100644 index 00000000..a64ba9ef --- /dev/null +++ b/src/handlers/signer.handlers.ts @@ -0,0 +1,12 @@ +import type {IcrcReadyResponseType} from '../types/icrc-responses'; +import {JSON_RPC_VERSION_2} from '../types/rpc'; + +export const notifyReady = ({msgId: id}: {msgId: string}): void => { + const msg: IcrcReadyResponseType = { + jsonrpc: JSON_RPC_VERSION_2, + id, + result: 'ready' + }; + + window.opener.postMessage(msg, origin); +}; diff --git a/src/signer.ts b/src/signer.ts index f31633ab..bf995962 100644 --- a/src/signer.ts +++ b/src/signer.ts @@ -1,12 +1,11 @@ import {nonNullish} from '@dfinity/utils'; +import {notifyReady} from './handlers/signer.handlers'; import { IcrcWalletStatusRequest, type IcrcWalletPermissionsRequestType, type IcrcWalletRequestPermissionsRequestType, type IcrcWalletSupportedStandardsRequestType } from './types/icrc-requests'; -import type {IcrcReadyResponseType} from './types/icrc-responses'; -import {JSON_RPC_VERSION_2} from './types/rpc'; /** * The parameters to initialize a signer. @@ -63,17 +62,8 @@ export class Signer { const {success: isStatusRequest, data} = IcrcWalletStatusRequest.safeParse(msgData); if (isStatusRequest) { - const notifyReady = (): void => { - const msg: IcrcReadyResponseType = { - jsonrpc: JSON_RPC_VERSION_2, - id: data.id, - result: 'ready' - }; - - window.opener.postMessage(msg, origin); - }; - - notifyReady(); + const {id: msgId} = data; + notifyReady({msgId}); } }; From f8479aa7ee181fa0497c01b7b158a6ec55e78d85 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 30 Jul 2024 11:48:56 +0200 Subject: [PATCH 03/15] feat: request type --- src/handlers/signer.handlers.ts | 4 ++-- src/signer.ts | 4 ++-- src/types/rpc.ts | 2 ++ 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/handlers/signer.handlers.ts b/src/handlers/signer.handlers.ts index a64ba9ef..2f04c564 100644 --- a/src/handlers/signer.handlers.ts +++ b/src/handlers/signer.handlers.ts @@ -1,7 +1,7 @@ import type {IcrcReadyResponseType} from '../types/icrc-responses'; -import {JSON_RPC_VERSION_2} from '../types/rpc'; +import {JSON_RPC_VERSION_2, type RpcIdType} from '../types/rpc'; -export const notifyReady = ({msgId: id}: {msgId: string}): void => { +export const notifyReady = ({id}: {id: RpcIdType}): void => { const msg: IcrcReadyResponseType = { jsonrpc: JSON_RPC_VERSION_2, id, diff --git a/src/signer.ts b/src/signer.ts index bf995962..4cb75274 100644 --- a/src/signer.ts +++ b/src/signer.ts @@ -62,8 +62,8 @@ export class Signer { const {success: isStatusRequest, data} = IcrcWalletStatusRequest.safeParse(msgData); if (isStatusRequest) { - const {id: msgId} = data; - notifyReady({msgId}); + const {id} = data; + notifyReady({id}); } }; diff --git a/src/types/rpc.ts b/src/types/rpc.ts index 0c42f896..74a7e88e 100644 --- a/src/types/rpc.ts +++ b/src/types/rpc.ts @@ -9,6 +9,8 @@ const JsonRpc = z.literal(JSON_RPC_VERSION_2); const RpcId = z.union([z.string(), z.number(), z.null()]); +export type RpcIdType = z.infer; + const Rpc = z.object({ jsonrpc: JsonRpc, id: z.optional(RpcId) From 0e9abec8a3c0739cdc028e760ca7c4fcdc800737 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 30 Jul 2024 14:02:32 +0200 Subject: [PATCH 04/15] feat: post message to origin --- src/handlers/signer.handlers.ts | 2 +- src/signer.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/handlers/signer.handlers.ts b/src/handlers/signer.handlers.ts index 2f04c564..315bb7bc 100644 --- a/src/handlers/signer.handlers.ts +++ b/src/handlers/signer.handlers.ts @@ -1,7 +1,7 @@ import type {IcrcReadyResponseType} from '../types/icrc-responses'; import {JSON_RPC_VERSION_2, type RpcIdType} from '../types/rpc'; -export const notifyReady = ({id}: {id: RpcIdType}): void => { +export const notifyReady = ({id, origin}: {id: RpcIdType; origin: string}): void => { const msg: IcrcReadyResponseType = { jsonrpc: JSON_RPC_VERSION_2, id, diff --git a/src/signer.ts b/src/signer.ts index 4cb75274..f49fa944 100644 --- a/src/signer.ts +++ b/src/signer.ts @@ -63,15 +63,16 @@ export class Signer { if (isStatusRequest) { const {id} = data; - notifyReady({id}); + notifyReady({id, origin}); } }; private assertAndSetOrigin(origin: string): void { if (nonNullish(this.#walletOrigin) && this.#walletOrigin !== origin) { - throw new Error('Origin is not allowed to interact with the signer'); + throw new Error(`The relying party's origin is not allowed to interact with the signer.`); } + // We do not reassign the origin with the same value if it is already set. It is not a significant performance win. if (nonNullish(this.#walletOrigin)) { return; } From 31172f534045aa56495041053256fb78914bfc96 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 30 Jul 2024 14:14:49 +0200 Subject: [PATCH 05/15] test: signer handler --- src/handlers/signer.handlers.spec.ts | 41 ++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/handlers/signer.handlers.spec.ts diff --git a/src/handlers/signer.handlers.spec.ts b/src/handlers/signer.handlers.spec.ts new file mode 100644 index 00000000..a0a07802 --- /dev/null +++ b/src/handlers/signer.handlers.spec.ts @@ -0,0 +1,41 @@ +import type {Mock} from 'vitest'; +import type {IcrcReadyResponseType} from '../types/icrc-responses'; +import {JSON_RPC_VERSION_2, RpcIdType} from '../types/rpc'; +import {notifyReady} from './signer.handlers'; + +describe('Signer handlers', () => { + let originalOpener: typeof window.opener; + + let postMessageMock: Mock; + + beforeEach(() => { + originalOpener = window.opener; + + postMessageMock = vi.fn(); + + vi.stubGlobal('opener', {postMessage: postMessageMock}); + }); + + afterEach(() => { + window.opener = originalOpener; + + vi.restoreAllMocks(); + }); + + describe('notifyReady', () => { + it('should post a message with the msg', () => { + const id: RpcIdType = 'test-123'; + const origin = 'https://hello.com'; + + notifyReady({id, origin}); + + const expectedMessage: IcrcReadyResponseType = { + jsonrpc: JSON_RPC_VERSION_2, + id, + result: 'ready' + }; + + expect(postMessageMock).toHaveBeenCalledWith(expectedMessage, origin); + }); + }); +}); From 24bdb02371d7f03f92d126b775937a5521c5f707 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 30 Jul 2024 14:20:49 +0200 Subject: [PATCH 06/15] chore: lint --- src/handlers/signer.handlers.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/handlers/signer.handlers.spec.ts b/src/handlers/signer.handlers.spec.ts index a0a07802..3874ddf3 100644 --- a/src/handlers/signer.handlers.spec.ts +++ b/src/handlers/signer.handlers.spec.ts @@ -1,6 +1,6 @@ import type {Mock} from 'vitest'; import type {IcrcReadyResponseType} from '../types/icrc-responses'; -import {JSON_RPC_VERSION_2, RpcIdType} from '../types/rpc'; +import {JSON_RPC_VERSION_2, type RpcIdType} from '../types/rpc'; import {notifyReady} from './signer.handlers'; describe('Signer handlers', () => { From e8528a2c0c79ce7a8622b57a0020caa24f124626 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 30 Jul 2024 14:43:50 +0200 Subject: [PATCH 07/15] feat: reset origin --- src/signer.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/signer.ts b/src/signer.ts index f49fa944..e1400fd1 100644 --- a/src/signer.ts +++ b/src/signer.ts @@ -23,7 +23,7 @@ type SignerMessageEvent = MessageEvent< >; export class Signer { - #walletOrigin: string | undefined; + #walletOrigin: string | undefined | null; private constructor(_parameters: SignerParameters) { window.addEventListener('message', this.onMessageListener); @@ -47,6 +47,7 @@ export class Signer { */ disconnect = (): void => { window.removeEventListener('message', this.onMessageListener); + this.#walletOrigin = null; }; private readonly onMessageListener = (message: SignerMessageEvent): void => { From 02f854db75eaadd77839d5c46312521829039bbf Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 30 Jul 2024 14:43:57 +0200 Subject: [PATCH 08/15] test: init origin test --- src/signer.spec.ts | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/signer.spec.ts b/src/signer.spec.ts index 81ca2ab4..9d6a0fda 100644 --- a/src/signer.spec.ts +++ b/src/signer.spec.ts @@ -1,5 +1,8 @@ import {type MockInstance} from 'vitest'; +import * as signerHandlers from './handlers/signer.handlers'; import {Signer, type SignerParameters} from './signer'; +import {ICRC29_STATUS} from './types/icrc'; +import {JSON_RPC_VERSION_2} from './types/rpc'; describe('Signer', () => { const mockParameters: SignerParameters = {}; @@ -61,4 +64,41 @@ describe('Signer', () => { expect(onMessageListenerSpy).not.toHaveBeenCalled(); }); }); + + describe('assertAndSetOrigin', () => { + let notifyReadySpy: MockInstance; + let signer: Signer; + + beforeEach(() => { + signer = Signer.init(mockParameters); + notifyReadySpy = vi.spyOn(signerHandlers, 'notifyReady'); + }); + + afterEach(() => { + signer.disconnect(); + vi.clearAllMocks(); + }); + + it('should set the origin if it is not set', () => { + const testOrigin = 'https://hello.com'; + + let testId = 'test-123'; + + const messageEvent = new MessageEvent('message', { + data: { + id: testId, + jsonrpc: JSON_RPC_VERSION_2, + method: ICRC29_STATUS + }, + origin: testOrigin + }); + + window.dispatchEvent(messageEvent); + + expect(notifyReadySpy).toHaveBeenCalledWith({ + id: testId, + origin: testOrigin + }); + }); + }); }); From d6336b0c896652336f3ce921ad7f0110dea6c90b Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 30 Jul 2024 15:25:02 +0200 Subject: [PATCH 09/15] test: origin and post msg --- src/signer.spec.ts | 63 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/src/signer.spec.ts b/src/signer.spec.ts index 9d6a0fda..8611bfe1 100644 --- a/src/signer.spec.ts +++ b/src/signer.spec.ts @@ -65,21 +65,28 @@ describe('Signer', () => { }); }); - describe('assertAndSetOrigin', () => { + describe('origin and postMessage', () => { + let originalOpener: typeof window.opener; + let notifyReadySpy: MockInstance; let signer: Signer; beforeEach(() => { signer = Signer.init(mockParameters); notifyReadySpy = vi.spyOn(signerHandlers, 'notifyReady'); + vi.stubGlobal('opener', {postMessage: vi.fn()}); }); afterEach(() => { signer.disconnect(); + + window.opener = originalOpener; + vi.clearAllMocks(); + vi.restoreAllMocks(); }); - it('should set the origin if it is not set', () => { + it('should use the origin and respond with a post message', () => { const testOrigin = 'https://hello.com'; let testId = 'test-123'; @@ -100,5 +107,57 @@ describe('Signer', () => { origin: testOrigin }); }); + + it('should throw an error if a message from different origin is dispatched', () => { + const testOrigin = 'https://hello.com'; + const differentOrigin = 'https://test.com'; + + let testId = 'test-123'; + + const msg = { + data: { + id: testId, + jsonrpc: JSON_RPC_VERSION_2, + method: ICRC29_STATUS + }, + origin: testOrigin + }; + + const messageEvent = new MessageEvent('message', msg); + window.dispatchEvent(messageEvent); + + const messageEventDiff = new MessageEvent('message', {...msg, origin: differentOrigin}); + + // TODO: error + + window.dispatchEvent(messageEventDiff); + }); + + it('should reset #walletOrigin to null after disconnect', () => { + const testOrigin = 'https://hello.com'; + const differentOrigin = 'https://world.com'; + + let testId = 'test-123'; + + const msg = { + data: { + id: testId, + jsonrpc: JSON_RPC_VERSION_2, + method: ICRC29_STATUS + }, + origin: testOrigin + }; + + const messageEvent = new MessageEvent('message', msg); + window.dispatchEvent(messageEvent); + + const messageEventDiff = new MessageEvent('message', {...msg, origin: differentOrigin}); + + signer.disconnect(); + + expect(() => { + window.dispatchEvent(messageEventDiff); + }).not.toThrow(); + }); }); }); From 5d1aff65ae4bc74d1fa3970f936d532b012edd5a Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 30 Jul 2024 16:37:17 +0200 Subject: [PATCH 10/15] test: skip for now --- src/signer.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/signer.spec.ts b/src/signer.spec.ts index 8611bfe1..bf884f03 100644 --- a/src/signer.spec.ts +++ b/src/signer.spec.ts @@ -108,7 +108,7 @@ describe('Signer', () => { }); }); - it('should throw an error if a message from different origin is dispatched', () => { + it.skip('should throw an error if a message from different origin is dispatched', () => { const testOrigin = 'https://hello.com'; const differentOrigin = 'https://test.com'; From d89801783c358b018d5dba8ea64874f9936ed9ed Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 31 Jul 2024 08:56:25 +0200 Subject: [PATCH 11/15] feat: rpc errors --- src/constants/signer.constants.ts | 6 +++++ src/handlers/signer.handlers.ts | 38 +++++++++++++++++++++++++++--- src/signer.spec.ts | 8 ++----- src/signer.ts | 39 +++++++++++++++++++++++-------- src/types/rpc.ts | 29 +++++++++++++++-------- 5 files changed, 91 insertions(+), 29 deletions(-) create mode 100644 src/constants/signer.constants.ts diff --git a/src/constants/signer.constants.ts b/src/constants/signer.constants.ts new file mode 100644 index 00000000..196044b7 --- /dev/null +++ b/src/constants/signer.constants.ts @@ -0,0 +1,6 @@ +export enum SignerErrorCode { + /** + * The relying party's origin is not allowed to interact with the signer. + */ + ORIGIN_ERROR = 500 +} diff --git a/src/handlers/signer.handlers.ts b/src/handlers/signer.handlers.ts index 315bb7bc..7c085a4d 100644 --- a/src/handlers/signer.handlers.ts +++ b/src/handlers/signer.handlers.ts @@ -1,12 +1,44 @@ import type {IcrcReadyResponseType} from '../types/icrc-responses'; -import {JSON_RPC_VERSION_2, type RpcIdType} from '../types/rpc'; +import { + JSON_RPC_VERSION_2, + type RpcIdType, + type RpcResponseErrorType, + type RpcResponseType, + type RpcResponseWithErrorType +} from '../types/rpc'; -export const notifyReady = ({id, origin}: {id: RpcIdType; origin: string}): void => { +interface Notify { + id: RpcIdType; + origin: string; +} + +export const notifyReady = ({id, origin}: Notify): void => { const msg: IcrcReadyResponseType = { jsonrpc: JSON_RPC_VERSION_2, id, result: 'ready' }; - window.opener.postMessage(msg, origin); + notify({msg, origin}); }; + +export const notifyAndThrowError = ({ + id, + error, + origin +}: { + error: RpcResponseErrorType; +} & Notify): never => { + const msg: RpcResponseWithErrorType = { + jsonrpc: JSON_RPC_VERSION_2, + id, + error + }; + + notify({msg, origin}); + + throw new Error(error.message); +}; + +const notify = ({msg, origin}: {msg: RpcResponseType} & Pick): void => + window.opener.postMessage(msg, origin); diff --git a/src/signer.spec.ts b/src/signer.spec.ts index bf884f03..b2e7077a 100644 --- a/src/signer.spec.ts +++ b/src/signer.spec.ts @@ -66,6 +66,8 @@ describe('Signer', () => { }); describe('origin and postMessage', () => { + const testId = 'test-123'; + let originalOpener: typeof window.opener; let notifyReadySpy: MockInstance; @@ -89,8 +91,6 @@ describe('Signer', () => { it('should use the origin and respond with a post message', () => { const testOrigin = 'https://hello.com'; - let testId = 'test-123'; - const messageEvent = new MessageEvent('message', { data: { id: testId, @@ -112,8 +112,6 @@ describe('Signer', () => { const testOrigin = 'https://hello.com'; const differentOrigin = 'https://test.com'; - let testId = 'test-123'; - const msg = { data: { id: testId, @@ -137,8 +135,6 @@ describe('Signer', () => { const testOrigin = 'https://hello.com'; const differentOrigin = 'https://world.com'; - let testId = 'test-123'; - const msg = { data: { id: testId, diff --git a/src/signer.ts b/src/signer.ts index e1400fd1..cf330483 100644 --- a/src/signer.ts +++ b/src/signer.ts @@ -1,11 +1,13 @@ import {nonNullish} from '@dfinity/utils'; -import {notifyReady} from './handlers/signer.handlers'; +import {SignerErrorCode} from './constants/signer.constants'; +import {notifyAndThrowError, notifyReady} from './handlers/signer.handlers'; import { IcrcWalletStatusRequest, type IcrcWalletPermissionsRequestType, type IcrcWalletRequestPermissionsRequestType, type IcrcWalletSupportedStandardsRequestType } from './types/icrc-requests'; +import {RpcRequest} from './types/rpc'; /** * The parameters to initialize a signer. @@ -14,14 +16,14 @@ import { // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface SignerParameters {} -type SignerMessageEvent = MessageEvent< - Partial< - | IcrcWalletRequestPermissionsRequestType - | IcrcWalletPermissionsRequestType - | IcrcWalletSupportedStandardsRequestType - > +type SignerMessageEventData = Partial< + | IcrcWalletRequestPermissionsRequestType + | IcrcWalletPermissionsRequestType + | IcrcWalletSupportedStandardsRequestType >; +type SignerMessageEvent = MessageEvent; + export class Signer { #walletOrigin: string | undefined | null; @@ -58,7 +60,7 @@ export class Signer { data: msgData, origin }: SignerMessageEvent): Promise => { - this.assertAndSetOrigin(origin); + this.assertAndSetOrigin({msgData, origin}); const {success: isStatusRequest, data} = IcrcWalletStatusRequest.safeParse(msgData); @@ -68,9 +70,26 @@ export class Signer { } }; - private assertAndSetOrigin(origin: string): void { + private assertAndSetOrigin({ + msgData, + origin + }: { + origin: string; + msgData: SignerMessageEventData; + }): void { if (nonNullish(this.#walletOrigin) && this.#walletOrigin !== origin) { - throw new Error(`The relying party's origin is not allowed to interact with the signer.`); + const {data} = RpcRequest.safeParse(msgData); + + notifyAndThrowError({ + id: data?.id ?? null, + origin, + error: { + code: SignerErrorCode.ORIGIN_ERROR, + message: `The relying party's origin is not allowed to interact with the signer.` + } + }); + // Typescript safety + return; } // We do not reassign the origin with the same value if it is already set. It is not a significant performance win. diff --git a/src/types/rpc.ts b/src/types/rpc.ts index 74a7e88e..0a0e106b 100644 --- a/src/types/rpc.ts +++ b/src/types/rpc.ts @@ -16,7 +16,7 @@ const Rpc = z.object({ id: z.optional(RpcId) }); -const RpcRequest = Rpc.extend({ +export const RpcRequest = Rpc.extend({ id: RpcId }) .merge( @@ -95,11 +95,13 @@ const RpcResponseError = z.object({ data: z.optional(z.never()) }); +export type RpcResponseErrorType = z.infer; + const RpcResponse = Rpc.extend({ id: RpcId }); -type RpcResponseType = z.infer; +export type RpcResponseType = z.infer; const RpcResponseContent = z .object({ @@ -110,17 +112,24 @@ const RpcResponseContent = z type RpcResponseContentType = z.infer; +const RpcResponseWithError = RpcResponse.extend({ + error: RpcResponseError +}); + +export type RpcResponseWithErrorType = z.infer; + export const inferRpcResponse = ( result: T ): z.ZodType => - RpcResponse.merge( - z - .object({ - result, - error: RpcResponseError - }) - .partial() - ) + RpcResponseWithError.omit({error: true}) + .merge( + z + .object({ + result, + error: RpcResponseError + }) + .partial() + ) .strict() .refine( ({result, error}) => result !== undefined || error !== undefined, From 2d5935f3fc2cd6f5905cf3add12e353c5453a399 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 31 Jul 2024 09:14:53 +0200 Subject: [PATCH 12/15] feat: test and do not throw on notify error --- src/handlers/signer.handlers.ts | 6 ++---- src/signer.spec.ts | 22 +++++++++++++++++----- src/signer.ts | 6 +++--- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/handlers/signer.handlers.ts b/src/handlers/signer.handlers.ts index 7c085a4d..375f4112 100644 --- a/src/handlers/signer.handlers.ts +++ b/src/handlers/signer.handlers.ts @@ -22,13 +22,13 @@ export const notifyReady = ({id, origin}: Notify): void => { notify({msg, origin}); }; -export const notifyAndThrowError = ({ +export const notifyError = ({ id, error, origin }: { error: RpcResponseErrorType; -} & Notify): never => { +} & Notify): void => { const msg: RpcResponseWithErrorType = { jsonrpc: JSON_RPC_VERSION_2, id, @@ -36,8 +36,6 @@ export const notifyAndThrowError = ({ }; notify({msg, origin}); - - throw new Error(error.message); }; const notify = ({msg, origin}: {msg: RpcResponseType} & Pick): void => diff --git a/src/signer.spec.ts b/src/signer.spec.ts index b2e7077a..8c9267f2 100644 --- a/src/signer.spec.ts +++ b/src/signer.spec.ts @@ -73,10 +73,13 @@ describe('Signer', () => { let notifyReadySpy: MockInstance; let signer: Signer; + let postMessageMock: MockInstance; + beforeEach(() => { signer = Signer.init(mockParameters); notifyReadySpy = vi.spyOn(signerHandlers, 'notifyReady'); - vi.stubGlobal('opener', {postMessage: vi.fn()}); + postMessageMock = vi.fn(); + vi.stubGlobal('opener', {postMessage: postMessageMock}); }); afterEach(() => { @@ -108,7 +111,7 @@ describe('Signer', () => { }); }); - it.skip('should throw an error if a message from different origin is dispatched', () => { + it('should notify an error if a message from different origin is dispatched', () => { const testOrigin = 'https://hello.com'; const differentOrigin = 'https://test.com'; @@ -125,10 +128,19 @@ describe('Signer', () => { window.dispatchEvent(messageEvent); const messageEventDiff = new MessageEvent('message', {...msg, origin: differentOrigin}); - - // TODO: error - window.dispatchEvent(messageEventDiff); + + expect(postMessageMock).toHaveBeenCalledWith( + { + jsonrpc: JSON_RPC_VERSION_2, + id: testId, + error: { + code: 500, + message: "The relying party's origin is not allowed to interact with the signer." + } + }, + differentOrigin + ); }); it('should reset #walletOrigin to null after disconnect', () => { diff --git a/src/signer.ts b/src/signer.ts index cf330483..5a2bdebe 100644 --- a/src/signer.ts +++ b/src/signer.ts @@ -1,6 +1,6 @@ import {nonNullish} from '@dfinity/utils'; import {SignerErrorCode} from './constants/signer.constants'; -import {notifyAndThrowError, notifyReady} from './handlers/signer.handlers'; +import {notifyError, notifyReady} from './handlers/signer.handlers'; import { IcrcWalletStatusRequest, type IcrcWalletPermissionsRequestType, @@ -80,7 +80,7 @@ export class Signer { if (nonNullish(this.#walletOrigin) && this.#walletOrigin !== origin) { const {data} = RpcRequest.safeParse(msgData); - notifyAndThrowError({ + notifyError({ id: data?.id ?? null, origin, error: { @@ -88,7 +88,7 @@ export class Signer { message: `The relying party's origin is not allowed to interact with the signer.` } }); - // Typescript safety + return; } From bdfd69487e114fa66ef4e846f50c5f8f539a0dc6 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 31 Jul 2024 12:30:18 +0200 Subject: [PATCH 13/15] docs: todo --- src/signer.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/signer.ts b/src/signer.ts index 5a2bdebe..d646fd5b 100644 --- a/src/signer.ts +++ b/src/signer.ts @@ -60,6 +60,9 @@ export class Signer { data: msgData, origin }: SignerMessageEvent): Promise => { + // TODO: ignore messages that are not Rpc Requests. + // TODO: assert messages to notify error if methods are not supported. + this.assertAndSetOrigin({msgData, origin}); const {success: isStatusRequest, data} = IcrcWalletStatusRequest.safeParse(msgData); From 4269c4e338683c85004832d9cbfd02d71409a425 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 31 Jul 2024 12:41:46 +0200 Subject: [PATCH 14/15] test: READY --- src/signer.spec.ts | 52 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/signer.spec.ts b/src/signer.spec.ts index 8c9267f2..619df53e 100644 --- a/src/signer.spec.ts +++ b/src/signer.spec.ts @@ -168,4 +168,56 @@ describe('Signer', () => { }).not.toThrow(); }); }); + + describe('READY postMessage', () => { + const testId = 'test-123'; + + let originalOpener: typeof window.opener; + + let signer: Signer; + + let postMessageMock: MockInstance; + + beforeEach(() => { + signer = Signer.init(mockParameters); + + postMessageMock = vi.fn(); + + vi.stubGlobal('opener', {postMessage: postMessageMock}); + }); + + afterEach(() => { + signer.disconnect(); + + window.opener = originalOpener; + + vi.clearAllMocks(); + vi.restoreAllMocks(); + }); + + it('should notify READY', () => { + const testOrigin = 'https://hello.com'; + + const msg = { + data: { + id: testId, + jsonrpc: JSON_RPC_VERSION_2, + method: ICRC29_STATUS + }, + origin: testOrigin + }; + + const messageEvent = new MessageEvent('message', msg); + window.dispatchEvent(messageEvent); + + expect(postMessageMock).toHaveBeenCalledWith( + { + jsonrpc: JSON_RPC_VERSION_2, + id: testId, + result: 'ready' + }, + testOrigin + ); + }); + }); }); From fa2cd7c9b08c514c5bf45e7676f6169aad88ad69 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 31 Jul 2024 18:12:49 +0200 Subject: [PATCH 15/15] chore: merge main --- src/signer.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/signer.ts b/src/signer.ts index c50eb462..d646fd5b 100644 --- a/src/signer.ts +++ b/src/signer.ts @@ -63,7 +63,6 @@ export class Signer { // TODO: ignore messages that are not Rpc Requests. // TODO: assert messages to notify error if methods are not supported. - this.assertAndSetOrigin({msgData, origin}); const {success: isStatusRequest, data} = IcrcWalletStatusRequest.safeParse(msgData);