mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-24 15:07:17 +00:00
Match the popup's messages against a canonical GUI origin
The web flow compared `event.origin` to `puter.defaultGUIOrigin` as raw strings, but they are different kinds of value: the event carries the browser's canonical origin serialization, while the configured origin is whatever text was supplied — a trailing slash, an explicit default port, or a stray path all name the same origin and all fail the comparison. The mismatch doesn't read as a config error, it reads as the user's answer: with every message from the popup dropped, the missing `permissionPromptReady` makes the popup's close look like a severed opener, and the missing decision leaves that path to answer on its own — "denied", for a guest, moments after the user clicked Allow and the grant committed. Parse the configured origin once and compare canonical-to-canonical; the popup URL is built from the same parsed origin, so a trailing slash no longer yields a `//action/...` path either. A configured origin that cannot parse could never have hosted the prompt, so it now denies up front instead of opening a broken window. Pinned by an e2e test that re-points the SDK at the same GUI through a trailing-slash origin and expects the grant to be heard; it fails against the raw comparison.
This commit is contained in:
@@ -1140,9 +1140,25 @@ class UI extends EventListener {
|
||||
// decision message that may still be in flight.
|
||||
const CLOSE_GRACE_MS = 1000;
|
||||
|
||||
// The popup's messages arrive tagged with the browser's canonical
|
||||
// serialization of its origin, while `defaultGUIOrigin` is
|
||||
// configuration-supplied text — a trailing slash, an explicit default
|
||||
// port, or a stray path would fail a raw comparison. A dropped message
|
||||
// here doesn't just hang: an unseen `permissionPromptReady` makes the
|
||||
// popup's close read as a severed opener, and an unseen decision then
|
||||
// reports a permission the user granted as denied. Parse once and
|
||||
// compare canonical-to-canonical. A configured origin that can't parse
|
||||
// can't host the prompt at all, so deny up front.
|
||||
let gui_origin;
|
||||
try {
|
||||
gui_origin = new URL(puter.defaultGUIOrigin).origin;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const msg_id = this.#messageID++;
|
||||
const url = `${puter.defaultGUIOrigin}/action/request-permission?embedded_in_popup=true&msg_id=${msg_id}&permission=${encodeURIComponent(permission)}`;
|
||||
const url = `${gui_origin}/action/request-permission?embedded_in_popup=true&msg_id=${msg_id}&permission=${encodeURIComponent(permission)}`;
|
||||
|
||||
// Guards against settling more than once across the message,
|
||||
// popup-closed, and dialog-cancel code paths.
|
||||
@@ -1181,7 +1197,7 @@ class UI extends EventListener {
|
||||
// the popup we opened. Origin alone is insufficient (any frame
|
||||
// on the GUI domain could post), so also pin event.source.
|
||||
// msg_id binds the message to this request.
|
||||
if ( e.origin !== puter.defaultGUIOrigin ) return;
|
||||
if ( e.origin !== gui_origin ) return;
|
||||
if ( popupWindow && e.source !== popupWindow ) return;
|
||||
if ( e.data?.original_msg_id != msg_id ) return;
|
||||
// The popup reporting that it is up and can reach us. Carries
|
||||
|
||||
@@ -453,6 +453,35 @@ test.describe('puter.ui.requestPermission (env=web popup)', () => {
|
||||
await expect(page.locator('#log [data-entry="perm:driver:true"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('a non-canonical configured origin still hears the decision', async ({ page }) => {
|
||||
// `defaultGUIOrigin` is configuration-supplied text, while the popup's
|
||||
// messages arrive tagged with the browser's canonical origin
|
||||
// serialization. A trailing slash used to fail the raw comparison and
|
||||
// drop both messages — and a dropped `permissionPromptReady` makes the
|
||||
// popup's close read as a severed opener, reporting the grant the user
|
||||
// just made as a denial. The SDK compares canonical-to-canonical now.
|
||||
await page.goto('/');
|
||||
await page.waitForFunction(() => !!window.puter?.authToken, null, { timeout: 60_000 });
|
||||
|
||||
await page.goto(PERMISSION_FIXTURE_URL);
|
||||
await page.locator('body.ready').waitFor({ timeout: 60_000 });
|
||||
// Re-point the SDK at the same GUI through a non-canonical string.
|
||||
// The getter reads `globalThis.PUTER_ORIGIN` on every access, so this
|
||||
// takes effect for the request below.
|
||||
await page.evaluate(() => {
|
||||
globalThis.PUTER_ORIGIN = `${window.PUTER_ORIGIN}/`;
|
||||
});
|
||||
|
||||
const [popup] = await Promise.all([
|
||||
page.waitForEvent('popup'),
|
||||
page.locator('#req-driver-perm').click(),
|
||||
]);
|
||||
const dialog = popup.locator('dialog.perm-dialog');
|
||||
await expect(dialog).toBeVisible({ timeout: 60_000 });
|
||||
await dialog.locator('.perm-dialog-allow').click();
|
||||
await expect(page.locator('#log [data-entry="perm:driver:true"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('without a user gesture, a consent dialog collects the click that opens the popup', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForFunction(() => !!window.puter?.authToken, null, { timeout: 60_000 });
|
||||
|
||||
Reference in New Issue
Block a user