diff --git a/src/handlers/signer-errors.handlers.spec.ts b/src/handlers/signer-errors.handlers.spec.ts index bedb3c06..94282331 100644 --- a/src/handlers/signer-errors.handlers.spec.ts +++ b/src/handlers/signer-errors.handlers.spec.ts @@ -17,23 +17,21 @@ describe('Signer-errors.handlers', () => { const testOrigin = 'https://hello.com'; - let originalOpener: typeof window.opener; + let sourceMock: Window; let postMessageMock: Mock; beforeEach(() => { - originalOpener = window.opener; - postMessageMock = vi.fn(); - vi.stubGlobal('opener', {postMessage: postMessageMock}); + sourceMock = { + postMessage: postMessageMock + } as unknown as Window; requestId = crypto.randomUUID(); }); afterEach(() => { - window.opener = originalOpener; - vi.clearAllMocks(); }); @@ -44,7 +42,7 @@ describe('Signer-errors.handlers', () => { message: 'The request sent by the relying party is not supported by the signer.' }; - notifyErrorRequestNotSupported({id: requestId, origin: testOrigin}); + notifyErrorRequestNotSupported({id: requestId, origin: testOrigin, source: sourceMock}); const expectedMessage: RpcResponseWithError = { jsonrpc: JSON_RPC_VERSION_2, @@ -52,7 +50,7 @@ describe('Signer-errors.handlers', () => { error }; - expect(postMessageMock).toHaveBeenCalledWith(expectedMessage, testOrigin); + expect(postMessageMock).toHaveBeenCalledWith(expectedMessage); }); it('should post an error message with custom message when provided', () => { @@ -63,7 +61,12 @@ describe('Signer-errors.handlers', () => { message }; - notifyErrorRequestNotSupported({id: requestId, origin: testOrigin, message}); + notifyErrorRequestNotSupported({ + id: requestId, + origin: testOrigin, + message, + source: sourceMock + }); const expectedMessage: RpcResponseWithError = { jsonrpc: JSON_RPC_VERSION_2, @@ -71,7 +74,7 @@ describe('Signer-errors.handlers', () => { error }; - expect(postMessageMock).toHaveBeenCalledWith(expectedMessage, testOrigin); + expect(postMessageMock).toHaveBeenCalledWith(expectedMessage); }); }); @@ -83,7 +86,7 @@ describe('Signer-errors.handlers', () => { 'The signer has not granted the necessary permissions to process the request from the relying party.' }; - notifyErrorPermissionNotGranted({id: requestId, origin: testOrigin}); + notifyErrorPermissionNotGranted({id: requestId, origin: testOrigin, source: sourceMock}); const expectedMessage: RpcResponseWithError = { jsonrpc: JSON_RPC_VERSION_2, @@ -91,13 +94,13 @@ describe('Signer-errors.handlers', () => { error }; - expect(postMessageMock).toHaveBeenCalledWith(expectedMessage, testOrigin); + expect(postMessageMock).toHaveBeenCalledWith(expectedMessage); }); }); describe('notifyErrorActionAborted', () => { it('should post an error message indicating action was aborted', () => { - notifyErrorActionAborted({id: requestId, origin: testOrigin}); + notifyErrorActionAborted({id: requestId, origin: testOrigin, source: sourceMock}); const expectedMessage: RpcResponseWithError = { jsonrpc: JSON_RPC_VERSION_2, @@ -105,7 +108,7 @@ describe('Signer-errors.handlers', () => { error: mockErrorNotify }; - expect(postMessageMock).toHaveBeenCalledWith(expectedMessage, testOrigin); + expect(postMessageMock).toHaveBeenCalledWith(expectedMessage); }); }); @@ -117,7 +120,12 @@ describe('Signer-errors.handlers', () => { message: customMessage }; - notifyNetworkError({id: requestId, origin: testOrigin, message: customMessage}); + notifyNetworkError({ + id: requestId, + origin: testOrigin, + message: customMessage, + source: sourceMock + }); const expectedMessage: RpcResponseWithError = { jsonrpc: JSON_RPC_VERSION_2, @@ -125,7 +133,7 @@ describe('Signer-errors.handlers', () => { error }; - expect(postMessageMock).toHaveBeenCalledWith(expectedMessage, testOrigin); + expect(postMessageMock).toHaveBeenCalledWith(expectedMessage); }); }); @@ -136,7 +144,7 @@ describe('Signer-errors.handlers', () => { message: 'The signer has not registered a prompt to respond to permission requests.' }; - notifyErrorMissingPrompt({id: requestId, origin: testOrigin}); + notifyErrorMissingPrompt({id: requestId, origin: testOrigin, source: sourceMock}); const expectedMessage: RpcResponseWithError = { jsonrpc: JSON_RPC_VERSION_2, @@ -144,7 +152,7 @@ describe('Signer-errors.handlers', () => { error }; - expect(postMessageMock).toHaveBeenCalledWith(expectedMessage, testOrigin); + expect(postMessageMock).toHaveBeenCalledWith(expectedMessage); }); }); @@ -155,7 +163,7 @@ describe('Signer-errors.handlers', () => { message: 'The sender must match the owner of the signer.' }; - notifyErrorSenderNotAllowed({id: requestId, origin: testOrigin}); + notifyErrorSenderNotAllowed({id: requestId, origin: testOrigin, source: sourceMock}); const expectedMessage: RpcResponseWithError = { jsonrpc: JSON_RPC_VERSION_2, @@ -163,7 +171,7 @@ describe('Signer-errors.handlers', () => { error }; - expect(postMessageMock).toHaveBeenCalledWith(expectedMessage, testOrigin); + expect(postMessageMock).toHaveBeenCalledWith(expectedMessage); }); }); @@ -175,7 +183,7 @@ describe('Signer-errors.handlers', () => { 'The signer is currently processing a request and cannot handle new requests at this time.' }; - notifyErrorBusy({id: requestId, origin: testOrigin}); + notifyErrorBusy({id: requestId, origin: testOrigin, source: sourceMock}); const expectedMessage: RpcResponseWithError = { jsonrpc: JSON_RPC_VERSION_2, @@ -183,7 +191,7 @@ describe('Signer-errors.handlers', () => { error }; - expect(postMessageMock).toHaveBeenCalledWith(expectedMessage, testOrigin); + expect(postMessageMock).toHaveBeenCalledWith(expectedMessage); }); }); }); diff --git a/src/handlers/signer-success.handlers.spec.ts b/src/handlers/signer-success.handlers.spec.ts index 130e7ac6..73614288 100644 --- a/src/handlers/signer-success.handlers.spec.ts +++ b/src/handlers/signer-success.handlers.spec.ts @@ -22,28 +22,27 @@ describe('Signer-success.handlers', () => { let id: RpcId; const origin = 'https://hello.com'; - let originalOpener: typeof window.opener; + let sourceMock: Window; let postMessageMock: Mock; beforeEach(() => { id = crypto.randomUUID(); - originalOpener = window.opener; postMessageMock = vi.fn(); - vi.stubGlobal('opener', {postMessage: postMessageMock}); + sourceMock = { + postMessage: postMessageMock + } as unknown as Window; }); afterEach(() => { - window.opener = originalOpener; - vi.restoreAllMocks(); }); describe('notifyReady', () => { it('should post a message with the msg', () => { - notifyReady({id, origin}); + notifyReady({id, origin, source: sourceMock}); const expectedMessage: IcrcReadyResponse = { jsonrpc: JSON_RPC_VERSION_2, @@ -51,13 +50,13 @@ describe('Signer-success.handlers', () => { result: 'ready' }; - expect(postMessageMock).toHaveBeenCalledWith(expectedMessage, origin); + expect(postMessageMock).toHaveBeenCalledWith(expectedMessage); }); }); describe('notifySupportedStandards', () => { it('should post a message with the msg', () => { - notifySupportedStandards({id, origin}); + notifySupportedStandards({id, origin, source: sourceMock}); const expectedMessage: IcrcSupportedStandardsResponse = { jsonrpc: JSON_RPC_VERSION_2, @@ -67,7 +66,7 @@ describe('Signer-success.handlers', () => { } }; - expect(postMessageMock).toHaveBeenCalledWith(expectedMessage, origin); + expect(postMessageMock).toHaveBeenCalledWith(expectedMessage); }); }); @@ -82,7 +81,7 @@ describe('Signer-success.handlers', () => { } ]; - notifyPermissionScopes({id, origin, scopes}); + notifyPermissionScopes({id, origin, scopes, source: sourceMock}); const expectedMessage: IcrcScopesResponse = { jsonrpc: JSON_RPC_VERSION_2, @@ -92,13 +91,13 @@ describe('Signer-success.handlers', () => { } }; - expect(postMessageMock).toHaveBeenCalledWith(expectedMessage, origin); + expect(postMessageMock).toHaveBeenCalledWith(expectedMessage); }); }); describe('notifyAccounts', () => { it('should post a message with the accounts', () => { - notifyAccounts({id, origin, accounts: mockAccounts}); + notifyAccounts({id, origin, accounts: mockAccounts, source: sourceMock}); const expectedMessage = { jsonrpc: JSON_RPC_VERSION_2, @@ -106,13 +105,13 @@ describe('Signer-success.handlers', () => { result: {accounts: mockAccounts} }; - expect(postMessageMock).toHaveBeenCalledWith(expectedMessage, origin); + expect(postMessageMock).toHaveBeenCalledWith(expectedMessage); }); }); describe('notifyCallCanister', () => { it('should post a message with the call canister result', () => { - notifyCallCanister({id, origin, result: mockCallCanisterSuccess}); + notifyCallCanister({id, origin, result: mockCallCanisterSuccess, source: sourceMock}); const expectedMessage = { jsonrpc: JSON_RPC_VERSION_2, @@ -120,7 +119,7 @@ describe('Signer-success.handlers', () => { result: mockCallCanisterSuccess }; - expect(postMessageMock).toHaveBeenCalledWith(expectedMessage, origin); + expect(postMessageMock).toHaveBeenCalledWith(expectedMessage); }); }); }); diff --git a/src/handlers/signer-success.handlers.ts b/src/handlers/signer-success.handlers.ts index 86bc339f..f6694086 100644 --- a/src/handlers/signer-success.handlers.ts +++ b/src/handlers/signer-success.handlers.ts @@ -13,17 +13,17 @@ import {JSON_RPC_VERSION_2} from '../types/rpc'; import type {Notify} from '../types/signer-handlers'; import {notify} from './signer.handlers'; -export const notifyReady = ({id, origin}: Notify): void => { +export const notifyReady = ({id, origin, source}: Notify): void => { const msg: IcrcReadyResponse = { jsonrpc: JSON_RPC_VERSION_2, id, result: 'ready' }; - notify({msg, origin}); + notify({msg, origin, source}); }; -export const notifySupportedStandards = ({id, origin}: Notify): void => { +export const notifySupportedStandards = ({id, origin, source}: Notify): void => { const msg: IcrcSupportedStandardsResponse = { jsonrpc: JSON_RPC_VERSION_2, id, @@ -32,41 +32,41 @@ export const notifySupportedStandards = ({id, origin}: Notify): void => { } }; - notify({msg, origin}); + notify({msg, origin, source}); }; export type NotifyPermissions = Notify & {scopes: IcrcScopesArray}; -export const notifyPermissionScopes = ({id, origin, scopes}: NotifyPermissions): void => { +export const notifyPermissionScopes = ({id, origin, scopes, source}: NotifyPermissions): void => { const msg: IcrcScopesResponse = { jsonrpc: JSON_RPC_VERSION_2, id, result: {scopes} }; - notify({msg, origin}); + notify({msg, origin, source}); }; export type NotifyAccounts = Notify & {accounts: IcrcAccounts}; -export const notifyAccounts = ({id, origin, accounts}: NotifyAccounts): void => { +export const notifyAccounts = ({id, origin, accounts, source}: NotifyAccounts): void => { const msg: IcrcAccountsResponse = { jsonrpc: JSON_RPC_VERSION_2, id, result: {accounts} }; - notify({msg, origin}); + notify({msg, origin, source}); }; export type NotifyCallCanister = Notify & {result: IcrcCallCanisterResult}; -export const notifyCallCanister = ({id, origin, result}: NotifyCallCanister): void => { +export const notifyCallCanister = ({id, origin, result, source}: NotifyCallCanister): void => { const msg: IcrcCallCanisterResponse = { jsonrpc: JSON_RPC_VERSION_2, id, result }; - notify({msg, origin}); + notify({msg, origin, source}); }; diff --git a/src/handlers/signer.handlers.spec.ts b/src/handlers/signer.handlers.spec.ts index eb810da4..5a5ab680 100644 --- a/src/handlers/signer.handlers.spec.ts +++ b/src/handlers/signer.handlers.spec.ts @@ -8,22 +8,21 @@ describe('Signer handlers', () => { let id: RpcId; const origin = 'https://hello.com'; - let originalOpener: typeof window.opener; + let sourceMock: Window; let postMessageMock: Mock; beforeEach(() => { id = crypto.randomUUID(); - originalOpener = window.opener; postMessageMock = vi.fn(); - vi.stubGlobal('opener', {postMessage: postMessageMock}); + sourceMock = { + postMessage: postMessageMock + } as unknown as Window; }); afterEach(() => { - window.opener = originalOpener; - vi.restoreAllMocks(); }); @@ -34,7 +33,7 @@ describe('Signer handlers', () => { message: 'This is an error test.' }; - notifyError({id, origin, error}); + notifyError({id, origin, error, source: sourceMock}); const expectedMessage: RpcResponseWithError = { jsonrpc: JSON_RPC_VERSION_2, @@ -42,7 +41,7 @@ describe('Signer handlers', () => { error }; - expect(postMessageMock).toHaveBeenCalledWith(expectedMessage, origin); + expect(postMessageMock).toHaveBeenCalledWith(expectedMessage); }); }); @@ -54,9 +53,9 @@ describe('Signer handlers', () => { result: 'ready' }; - notify({msg, origin}); + notify({msg, origin, source: sourceMock}); - expect(postMessageMock).toHaveBeenCalledWith(msg, origin); + expect(postMessageMock).toHaveBeenCalledWith(msg); }); }); }); diff --git a/src/handlers/signer.handlers.ts b/src/handlers/signer.handlers.ts index 57a4f308..e884722b 100644 --- a/src/handlers/signer.handlers.ts +++ b/src/handlers/signer.handlers.ts @@ -9,7 +9,8 @@ import type {Notify} from '../types/signer-handlers'; export const notifyError = ({ id, error, - origin + origin, + source }: { error: RpcResponseError; } & Notify): void => { @@ -19,11 +20,11 @@ export const notifyError = ({ error }; - notify({msg, origin}); + notify({msg, origin, source}); }; -// TODO: instead of window.opener try to sent the message to MessageEvent.source first. -// This is safer in case the signer is opened with redirect in the future. -// e.g. per user canister pattern -export const notify = ({msg, origin}: {msg: RpcResponse} & Pick): void => - window.opener.postMessage(msg, origin); +export const notify = ({ + msg, + origin, + source +}: {msg: RpcResponse} & Pick): void => source.postMessage(msg); diff --git a/src/services/signer.services.spec.ts b/src/services/signer.services.spec.ts index bf7f79a5..5f3c1f0a 100644 --- a/src/services/signer.services.spec.ts +++ b/src/services/signer.services.spec.ts @@ -44,30 +44,29 @@ describe('Signer services', () => { host: 'http://localhost:4943' }; - let originalOpener: typeof window.opener; + let sourceMock: Window; let postMessageMock: Mock; beforeEach(() => { - originalOpener = window.opener; - postMessageMock = vi.fn(); - vi.stubGlobal('opener', {postMessage: postMessageMock}); + sourceMock = { + postMessage: postMessageMock + } as unknown as Window; requestId = crypto.randomUUID(); notify = { id: requestId, - origin: testOrigin + origin: testOrigin, + source: sourceMock }; signerService = new SignerService(); }); afterEach(() => { - window.opener = originalOpener; - vi.clearAllMocks(); }); @@ -144,7 +143,7 @@ describe('Signer services', () => { error: mockErrorNotify }; - expect(postMessageMock).toHaveBeenCalledWith(expectedMessage, testOrigin); + expect(postMessageMock).toHaveBeenCalledWith(expectedMessage); }); }); @@ -187,7 +186,7 @@ describe('Signer services', () => { error: errorNotify }; - expect(postMessageMock).toHaveBeenCalledWith(expectedMessage, testOrigin); + expect(postMessageMock).toHaveBeenCalledWith(expectedMessage); }); it('should trigger prompt "error" with the error', async () => { @@ -251,7 +250,7 @@ describe('Signer services', () => { error: errorNotify }; - expect(postMessageMock).toHaveBeenCalledWith(expectedMessage, testOrigin); + expect(postMessageMock).toHaveBeenCalledWith(expectedMessage); }); it('should call notifyNetworkError with an the error message when consentMessage throws an error', async () => { @@ -279,7 +278,7 @@ describe('Signer services', () => { error: errorNotify }; - expect(postMessageMock).toHaveBeenCalledWith(expectedMessage, testOrigin); + expect(postMessageMock).toHaveBeenCalledWith(expectedMessage); }); }); @@ -302,7 +301,7 @@ describe('Signer services', () => { error: errorNotify }; - expect(postMessageMock).toHaveBeenCalledWith(expectedMessage, testOrigin); + expect(postMessageMock).toHaveBeenCalledWith(expectedMessage); }); describe('Without consent message fallback', () => { @@ -671,7 +670,7 @@ describe('Signer services', () => { error: errorNotify }; - expect(postMessageMock).toHaveBeenCalledWith(expectedMessage, testOrigin); + expect(postMessageMock).toHaveBeenCalledWith(expectedMessage); }); }); }); @@ -711,7 +710,8 @@ describe('Signer services', () => { expect(notifyCallCanisterSpy).toHaveBeenCalledWith({ id: requestId, origin: testOrigin, - result: mockCanisterCallSuccess + result: mockCanisterCallSuccess, + source: sourceMock }); }); @@ -733,7 +733,8 @@ describe('Signer services', () => { error: { code: SignerErrorCode.NETWORK_ERROR, message: errorMsg - } + }, + source: sourceMock }); }); @@ -753,7 +754,8 @@ describe('Signer services', () => { error: { code: SignerErrorCode.NETWORK_ERROR, message: 'An unknown error occurred' - } + }, + source: sourceMock }); }); diff --git a/src/signer.spec.ts b/src/signer.spec.ts index 609232cd..a3a613aa 100644 --- a/src/signer.spec.ts +++ b/src/signer.spec.ts @@ -154,7 +154,7 @@ describe('Signer', () => { describe('Origin', () => { const testId = crypto.randomUUID(); - let originalOpener: typeof window.opener; + let sourceMock: Window; let notifyReadySpy: MockInstance; let signer: Signer; @@ -165,14 +165,15 @@ describe('Signer', () => { signer = Signer.init(signerOptions); notifyReadySpy = vi.spyOn(signerSuccessHandlers, 'notifyReady'); postMessageMock = vi.fn(); - vi.stubGlobal('opener', {postMessage: postMessageMock}); + + sourceMock = { + postMessage: postMessageMock + } as unknown as Window; }); afterEach(() => { signer.disconnect(); - window.opener = originalOpener; - vi.clearAllMocks(); vi.restoreAllMocks(); }); @@ -193,7 +194,8 @@ describe('Signer', () => { expect(notifyReadySpy).toHaveBeenCalledWith({ id: testId, - origin: testOrigin + origin: testOrigin, + source: sourceMock }); }); @@ -340,18 +342,24 @@ describe('Signer', () => { origin: testOrigin }; - let originalOpener: typeof window.opener; + let sourceMock: Window; + + let signer: Signer; let postMessageMock: MockInstance; beforeEach(() => { + signer = Signer.init(signerOptions); + postMessageMock = vi.fn(); - vi.stubGlobal('opener', {postMessage: postMessageMock}); + sourceMock = { + postMessage: postMessageMock + } as unknown as Window; }); afterEach(() => { - window.opener = originalOpener; + signer.disconnect(); vi.clearAllMocks(); vi.restoreAllMocks(); @@ -2262,7 +2270,8 @@ describe('Signer', () => { expect(notifyReadySpy).toHaveBeenCalledWith({ id: testId, - origin: testOrigin + origin: testOrigin, + source: sourceMock }); signer.register({ diff --git a/src/signer.ts b/src/signer.ts index cb893dca..d83ad9c1 100644 --- a/src/signer.ts +++ b/src/signer.ts @@ -46,7 +46,6 @@ import { type IcrcPermissionState, type IcrcScopedMethod } from './types/icrc-standards'; -import type {Origin} from './types/post-message'; import {RpcRequestSchema, type RpcId} from './types/rpc'; import type {SignerMessageEvent} from './types/signer'; import {MissingPromptError} from './types/signer-errors'; @@ -116,13 +115,18 @@ export class Signer { // This means that the signer will have to keep track of its activity. // See https://github.com/dfinity/wg-identity-authentication/pull/212 - private readonly onMessageListener = (message: SignerMessageEvent): void => { + private readonly onMessageListener = (message: MessageEvent): void => { void this.onMessage(message); }; - private readonly onMessage = async (message: SignerMessageEvent): Promise => { + private readonly onMessage = async ({source, ...message}: MessageEvent): Promise => { const {data, origin} = message; + if (isNullish(source)) { + // An unknown source is unlikely, but if it occurs, we simply ignore it without notifying the signer, as it is irrelevant. Additionally, the source is essential for responding to the relying party. + return; + } + const {success, data: requestData} = RpcRequestSchema.safeParse(data); if (!success) { @@ -137,14 +141,19 @@ export class Signer { // TODO: wrap a try catch around all handler and notify "Unexpected exception" in case if issues - const {handled} = await this.handleMessage(message); + const {handled} = await this.handleMessage({ + ...message, + source + }); + if (handled) { return; } notifyErrorRequestNotSupported({ id: requestData?.id ?? null, - origin + origin, + source }); }; @@ -210,7 +219,7 @@ export class Signer { * @returns {object} An object containing a `valid` boolean property. * @returns {boolean} returns `true` if the origin is either undefined or matches the expected wallet origin, otherwise returns `false` and notifies the error. */ - private assertUndefinedOrSameOrigin({data: msgData, origin}: SignerMessageEvent): { + private assertUndefinedOrSameOrigin({data: msgData, origin, source}: SignerMessageEvent): { valid: boolean; } { if (nonNullish(this.#walletOrigin) && this.#walletOrigin !== origin) { @@ -222,7 +231,8 @@ export class Signer { error: { code: SignerErrorCode.ORIGIN_ERROR, message: `The relying party's origin is not permitted to obtain the status of the signer.` - } + }, + source }); return {valid: false}; @@ -288,7 +298,7 @@ export class Signer { * @returns {object} An object containing a `valid` boolean property. * @returns {boolean} returns `true` if the wallet origin is defined and matches the origin of the message event, otherwise returns `false` and notifies the error. */ - private assertNotUndefinedAndSameOrigin({data: msgData, origin}: SignerMessageEvent): { + private assertNotUndefinedAndSameOrigin({data: msgData, origin, source}: SignerMessageEvent): { valid: boolean; } { if (isNullish(this.#walletOrigin) || this.#walletOrigin !== origin) { @@ -302,7 +312,8 @@ export class Signer { message: isNullish(this.#walletOrigin) ? 'The relying party has not established a connection to the signer.' : `The relying party's origin is not allowed to interact with the signer.` - } + }, + source }); return {valid: false}; @@ -370,17 +381,19 @@ export class Signer { * @param {SignerMessageEvent} message - The incoming message event containing the data and origin. * @returns {Object} An object with a boolean property `handled` indicating whether the request was handled. */ - private handleStatusRequest({data, origin, ...rest}: SignerMessageEvent): {handled: boolean} { + private handleStatusRequest({data, origin, source, ...rest}: SignerMessageEvent): { + handled: boolean; + } { const {success: isStatusRequest, data: statusData} = IcrcStatusRequestSchema.safeParse(data); if (isStatusRequest) { - const {valid} = this.assertUndefinedOrSameOrigin({data, origin, ...rest}); + const {valid} = this.assertUndefinedOrSameOrigin({data, origin, source, ...rest}); if (!valid) { return {handled: true}; } const {id} = statusData; - notifyReady({id, origin}); + notifyReady({id, origin, source}); this.setWalletOrigin({origin}); @@ -398,13 +411,13 @@ export class Signer { * @param {SignerMessageEvent} message - The incoming message event containing the data and origin. * @returns {Object} An object with a boolean property `handled` indicating whether the request was handled. */ - private handleSupportedStandards({data, origin}: SignerMessageEvent): {handled: boolean} { + private handleSupportedStandards({data, origin, source}: SignerMessageEvent): {handled: boolean} { const {success: isSupportedStandardsRequest, data: supportedStandardsData} = IcrcSupportedStandardsRequestSchema.safeParse(data); if (isSupportedStandardsRequest) { const {id} = supportedStandardsData; - notifySupportedStandards({id, origin}); + notifySupportedStandards({id, origin, source}); return {handled: true}; } @@ -419,14 +432,14 @@ export class Signer { * @param {SignerMessageEvent} message - The incoming message event containing the data and origin. * @returns {Object} An object with a boolean property `handled` indicating whether the request was handled. */ - private handlePermissionsRequest({data}: SignerMessageEvent): {handled: boolean} { + private handlePermissionsRequest({data, source}: SignerMessageEvent): {handled: boolean} { const {success: isPermissionsRequestRequest, data: permissionsRequestData} = IcrcPermissionsRequestSchema.safeParse(data); if (isPermissionsRequestRequest) { const {id} = permissionsRequestData; - this.emitPermissions({id}); + this.emitPermissions({id, source}); return {handled: true}; } @@ -449,7 +462,8 @@ export class Signer { */ private async handleRequestPermissionsRequest({ data, - origin + origin, + source }: SignerMessageEvent): Promise<{handled: boolean}> { const handler = async (): Promise<{handled: boolean}> => { const {success: isRequestPermissionsRequest, data: requestPermissionsData} = @@ -465,7 +479,7 @@ export class Signer { } = requestPermissionsData; if (isNullish(this.#permissionsPrompt)) { - this.assertWalletOriginAndNotifyMissingPromptError(requestId); + this.assertWalletOriginAndNotifyMissingPromptError({id: requestId, source}); return {handled: true}; } @@ -499,12 +513,13 @@ export class Signer { }); this.savePermissions({scopes: confirmedScopes}); - this.emitPermissions({id: requestId}); + this.emitPermissions({id: requestId, source}); }; await this.prompt({ requestId, - promptFn + promptFn, + source }); return {handled: true}; @@ -533,7 +548,10 @@ export class Signer { return await promise; } - private emitPermissions({id}: Pick): void { + private emitPermissions({ + id, + source + }: Pick & Pick): void { assertNonNullish(this.#walletOrigin, "The relying party's origin is unknown."); const {owner, sessionOptions} = this.#signerOptions; @@ -556,16 +574,21 @@ export class Signer { notifyPermissionScopes({ id, origin: this.#walletOrigin, - scopes: allScopes + scopes: allScopes, + source }); } - private assertWalletOriginAndNotifyMissingPromptError(id: RpcId | undefined): void { + private assertWalletOriginAndNotifyMissingPromptError({ + id, + source + }: {id: RpcId | undefined} & Pick): void { assertNonNullish(this.#walletOrigin, "The relying party's origin is unknown."); notifyErrorMissingPrompt({ id: id ?? null, - origin: this.#walletOrigin + origin: this.#walletOrigin, + source }); } @@ -585,7 +608,11 @@ export class Signer { * @param {SignerMessageEvent} message - The incoming message event containing the data and origin. * @returns {Object} An object with a boolean property `handled` indicating whether the request was handled. */ - private async handleAccounts({data, origin}: SignerMessageEvent): Promise<{handled: boolean}> { + private async handleAccounts({ + data, + origin, + source + }: SignerMessageEvent): Promise<{handled: boolean}> { const handler = async (): Promise<{handled: boolean}> => { const {success: isAccountsRequest, data: accountsData} = IcrcAccountsRequestSchema.safeParse(data); @@ -601,30 +628,33 @@ export class Signer { const {result, accounts} = await this.promptAccounts({origin}); if (result === 'rejected') { - notifyErrorActionAborted({id: requestId, origin}); + notifyErrorActionAborted({id: requestId, origin, source}); return; } - notifyAccountsHandlers({accounts, id: requestId, origin}); + notifyAccountsHandlers({accounts, id: requestId, origin, source}); }; await this.prompt({ requestId, - promptFn + promptFn, + source }); }; const permission = await this.assertAndPromptPermissions({ method: ICRC27_ACCOUNTS, requestId, - origin + origin, + source }); switch (permission) { case ICRC25_PERMISSION_DENIED: { notifyErrorPermissionNotGranted({ id: requestId ?? null, - origin + origin, + source }); break; } @@ -642,16 +672,17 @@ export class Signer { private async prompt({ requestId, - promptFn + promptFn, + source }: { promptFn: () => Promise; requestId: RpcId; - }): Promise { + } & Pick): Promise { try { await promptFn(); } catch (err: unknown) { if (err instanceof MissingPromptError) { - this.assertWalletOriginAndNotifyMissingPromptError(requestId); + this.assertWalletOriginAndNotifyMissingPromptError({id: requestId, source}); return; } @@ -696,7 +727,8 @@ export class Signer { */ private async handleCallCanister({ data, - origin + origin, + source }: SignerMessageEvent): Promise<{handled: boolean}> { const handler = async (): Promise<{handled: boolean}> => { const {success: isCallCanisterRequest, data: callData} = @@ -711,20 +743,23 @@ export class Signer { const permission = await this.assertAndPromptPermissions({ method: ICRC49_CALL_CANISTER, requestId, - origin + origin, + source }); if (permission === ICRC25_PERMISSION_DENIED) { notifyErrorPermissionNotGranted({ id: requestId ?? null, - origin + origin, + source }); return {handled: true}; } const notify: Notify = { id: requestId, - origin + origin, + source }; const {result: userConsent} = await this.#signerService.assertAndPromptConsentMessage({ @@ -771,12 +806,14 @@ export class Signer { private async assertAndPromptPermissions({ method, origin, - requestId + requestId, + source }: { method: IcrcScopedMethod; - origin: Origin; requestId: RpcId; - }): Promise> { + } & Pick): Promise< + Omit + > { const {owner} = this.#signerOptions; const currentPermission = sessionScopeState({ @@ -820,7 +857,8 @@ export class Signer { this.prompt({ requestId, - promptFn + promptFn, + source }).catch((err) => { reject(err); }); diff --git a/src/types/signer-handlers.ts b/src/types/signer-handlers.ts index 6b1dde1e..84da6733 100644 --- a/src/types/signer-handlers.ts +++ b/src/types/signer-handlers.ts @@ -1,9 +1,16 @@ import * as z from 'zod'; import {RpcIdSchema} from './rpc'; +const MessageEventSourceSchema = z.union([ + z.instanceof(Window), + z.instanceof(MessagePort), + z.instanceof(ServiceWorker) +]); + const NotifySchema = z.object({ id: RpcIdSchema, - origin: z.string() + origin: z.string(), + source: MessageEventSourceSchema }); export type Notify = z.infer; diff --git a/src/types/signer.ts b/src/types/signer.ts index dc99e630..742e300c 100644 --- a/src/types/signer.ts +++ b/src/types/signer.ts @@ -19,4 +19,6 @@ const SignerMessageEventDataSchema = z export type SignerMessageEventData = z.infer; -export type SignerMessageEvent = MessageEvent; +export type SignerMessageEvent = Omit, 'source'> & { + source: MessageEventSource; +};