diff --git a/packages/extension-base/src/background/handlers/State.ts b/packages/extension-base/src/background/handlers/State.ts index aedcbdc..d3402ee 100644 --- a/packages/extension-base/src/background/handlers/State.ts +++ b/packages/extension-base/src/background/handlers/State.ts @@ -132,6 +132,9 @@ export default class State { #maxEntries = 10; #rateLimitInterval = 3000; // 3 seconds + // Track pending authorization URLs to prevent race conditions + readonly #pendingAuthUrls = new Set(); + readonly #authRequests: Record = {}; readonly #metaStore = new MetadataStore(); @@ -248,7 +251,7 @@ export default class State { }); } - private authComplete = (id: string, resolve: (resValue: AuthResponse) => void, reject: (error: Error) => void): Resolver => { + private authComplete = (id: string, resolve: (resValue: AuthResponse) => void, reject: (error: Error) => void, pendingIdStr?: string): Resolver => { const complete = async (authorizedAccounts: string[] = []) => { const { idStr, request: { origin }, url } = this.#authRequests[id]; @@ -273,12 +276,23 @@ export default class State { await this.saveCurrentAuthList(); await this.updateDefaultAuthAccounts(authorizedAccounts); delete this.#authRequests[id]; + + // Remove from pending set to allow future requests + if (pendingIdStr) { + this.#pendingAuthUrls.delete(pendingIdStr); + } + this.updateIconAuth(true); }; return { // eslint-disable-next-line @typescript-eslint/no-misused-promises reject: async (error: Error): Promise => { + // Always remove from pending set on rejection + if (pendingIdStr) { + this.#pendingAuthUrls.delete(pendingIdStr); + } + if (error.message === 'Cancelled') { delete this.#authRequests[id]; this.updateIconAuth(true); @@ -329,6 +343,13 @@ export default class State { } public deleteAuthRequest (requestId: string) { + // Remove from pending set before deleting + const request = this.#authRequests[requestId]; + + if (request?.idStr) { + this.#pendingAuthUrls.delete(request.idStr); + } + delete this.#authRequests[requestId]; this.updateIconAuth(true); } @@ -470,7 +491,10 @@ export default class State { public async authorizeUrl (url: string, request: RequestAuthorizeTab): Promise { const idStr = this.stripUrl(url); - // Do not enqueue duplicate authorization requests. + // Synchronous check to prevent race conditions - check pending Set first + assert(!this.#pendingAuthUrls.has(idStr), `The source ${url} has a pending authorization request`); + + // Do not enqueue duplicate authorization requests (secondary check for existing requests). const isDuplicate = Object .values(this.#authRequests) .some((request) => request.idStr === idStr); @@ -489,11 +513,14 @@ export default class State { }; } + // Add to pending set immediately (synchronous) to prevent race conditions + this.#pendingAuthUrls.add(idStr); + return new Promise((resolve, reject): void => { const id = getId(); this.#authRequests[id] = { - ...this.authComplete(id, resolve, reject), + ...this.authComplete(id, resolve, reject, idStr), id, idStr, request, diff --git a/packages/extension-base/src/page/index.ts b/packages/extension-base/src/page/index.ts index 9974af9..079b4f8 100644 --- a/packages/extension-base/src/page/index.ts +++ b/packages/extension-base/src/page/index.ts @@ -47,7 +47,7 @@ export function sendMessage (message: TMessag request: request || null as RequestTypes[TMessageType] }; - window.postMessage(transportRequestMessage, '*'); + window.postMessage(transportRequestMessage, window.location.origin); }); } diff --git a/packages/extension/src/content.ts b/packages/extension/src/content.ts index b9702a5..f6a70b8 100644 --- a/packages/extension/src/content.ts +++ b/packages/extension/src/content.ts @@ -10,7 +10,7 @@ import { chrome } from '@pezkuwi/extension-inject/chrome'; let port: chrome.runtime.Port | undefined; function onPortMessageHandler (data: Message['data']): void { - window.postMessage({ ...data, origin: MESSAGE_ORIGIN_CONTENT }, '*'); + window.postMessage({ ...data, origin: MESSAGE_ORIGIN_CONTENT }, window.location.origin); } function onPortDisconnectHandler (): void {