diff --git a/src/docs/src/Perms/requestEmail.md b/src/docs/src/Perms/requestEmail.md index 34813b752..07dd1968a 100644 --- a/src/docs/src/Perms/requestEmail.md +++ b/src/docs/src/Perms/requestEmail.md @@ -6,6 +6,12 @@ platforms: [websites, apps] Request to see a user's email. If the user has already granted this permission the user will not be prompted and their email address will be returned. If the user grants permission their email address will be returned. If the user does not allow access `undefined` will be returned. If the user does not have an email address, the value of their email address will be `null`. +On a website, sign the user in to your site first with [`puter.auth.signIn()`](/Auth/signIn/). This method reads the signed-in user's identity before it can prompt, so for a signed-out visitor it rejects with `Unauthorized` and no prompt is shown. Answering a permission prompt does not by itself sign the user in to your site, so guard the call: + +```js +if (!puter.authToken) await puter.auth.signIn(); +``` + ## Syntax ```js diff --git a/src/docs/src/Perms/requestManageApps.md b/src/docs/src/Perms/requestManageApps.md index a10d9e665..abff6048a 100644 --- a/src/docs/src/Perms/requestManageApps.md +++ b/src/docs/src/Perms/requestManageApps.md @@ -6,6 +6,12 @@ platforms: [websites, apps] Request write (manage) access to the user's apps. If the user has already granted this permission the user will not be prompted and `true` will be returned. If the user grants permission `true` will be returned. If the user does not allow access `false` will be returned. +On a website, sign the user in to your site first with [`puter.auth.signIn()`](/Auth/signIn/). This method reads the signed-in user's identity before it can prompt, so for a signed-out visitor it rejects with `Unauthorized` and no prompt is shown. Answering a permission prompt does not by itself sign the user in to your site, so guard the call: + +```js +if (!puter.authToken) await puter.auth.signIn(); +``` + ## Syntax ```js diff --git a/src/docs/src/Perms/requestManageSubdomains.md b/src/docs/src/Perms/requestManageSubdomains.md index 012ee3d5c..1cce0b305 100644 --- a/src/docs/src/Perms/requestManageSubdomains.md +++ b/src/docs/src/Perms/requestManageSubdomains.md @@ -6,6 +6,12 @@ platforms: [websites, apps] Request write (manage) access to the user's subdomains. If the user has already granted this permission the user will not be prompted and `true` will be returned. If the user grants permission `true` will be returned. If the user does not allow access `false` will be returned. +On a website, sign the user in to your site first with [`puter.auth.signIn()`](/Auth/signIn/). This method reads the signed-in user's identity before it can prompt, so for a signed-out visitor it rejects with `Unauthorized` and no prompt is shown. Answering a permission prompt does not by itself sign the user in to your site, so guard the call: + +```js +if (!puter.authToken) await puter.auth.signIn(); +``` + ## Syntax ```js diff --git a/src/docs/src/Perms/requestReadApps.md b/src/docs/src/Perms/requestReadApps.md index 5097710bb..eebea9e2b 100644 --- a/src/docs/src/Perms/requestReadApps.md +++ b/src/docs/src/Perms/requestReadApps.md @@ -6,6 +6,12 @@ platforms: [websites, apps] Request read access to the user's apps. If the user has already granted this permission the user will not be prompted and `true` will be returned. If the user grants permission `true` will be returned. If the user does not allow access `false` will be returned. +On a website, sign the user in to your site first with [`puter.auth.signIn()`](/Auth/signIn/). This method reads the signed-in user's identity before it can prompt, so for a signed-out visitor it rejects with `Unauthorized` and no prompt is shown. Answering a permission prompt does not by itself sign the user in to your site, so guard the call: + +```js +if (!puter.authToken) await puter.auth.signIn(); +``` + ## Syntax ```js diff --git a/src/docs/src/Perms/requestReadDesktop.md b/src/docs/src/Perms/requestReadDesktop.md index 4b6e8d466..10e860ead 100644 --- a/src/docs/src/Perms/requestReadDesktop.md +++ b/src/docs/src/Perms/requestReadDesktop.md @@ -6,6 +6,12 @@ platforms: [websites, apps] Request read access to the user's Desktop folder. If the user has already granted this permission the user will not be prompted and the path will be returned. If the user grants permission the path will be returned. If the user does not allow access `undefined` will be returned. +On a website, sign the user in to your site first with [`puter.auth.signIn()`](/Auth/signIn/). This method reads the signed-in user's identity before it can prompt, so for a signed-out visitor it rejects with `Unauthorized` and no prompt is shown. Answering a permission prompt does not by itself sign the user in to your site, so guard the call: + +```js +if (!puter.authToken) await puter.auth.signIn(); +``` + ## Syntax ```js diff --git a/src/docs/src/Perms/requestReadDocuments.md b/src/docs/src/Perms/requestReadDocuments.md index c3d02406b..d2ab8834d 100644 --- a/src/docs/src/Perms/requestReadDocuments.md +++ b/src/docs/src/Perms/requestReadDocuments.md @@ -6,6 +6,12 @@ platforms: [websites, apps] Request read access to the user's Documents folder. If the user has already granted this permission the user will not be prompted and the path will be returned. If the user grants permission the path will be returned. If the user does not allow access `undefined` will be returned. +On a website, sign the user in to your site first with [`puter.auth.signIn()`](/Auth/signIn/). This method reads the signed-in user's identity before it can prompt, so for a signed-out visitor it rejects with `Unauthorized` and no prompt is shown. Answering a permission prompt does not by itself sign the user in to your site, so guard the call: + +```js +if (!puter.authToken) await puter.auth.signIn(); +``` + ## Syntax ```js diff --git a/src/docs/src/Perms/requestReadPictures.md b/src/docs/src/Perms/requestReadPictures.md index f5756d664..be80fb685 100644 --- a/src/docs/src/Perms/requestReadPictures.md +++ b/src/docs/src/Perms/requestReadPictures.md @@ -6,6 +6,12 @@ platforms: [websites, apps] Request read access to the user's Pictures folder. If the user has already granted this permission the user will not be prompted and the path will be returned. If the user grants permission the path will be returned. If the user does not allow access `undefined` will be returned. +On a website, sign the user in to your site first with [`puter.auth.signIn()`](/Auth/signIn/). This method reads the signed-in user's identity before it can prompt, so for a signed-out visitor it rejects with `Unauthorized` and no prompt is shown. Answering a permission prompt does not by itself sign the user in to your site, so guard the call: + +```js +if (!puter.authToken) await puter.auth.signIn(); +``` + ## Syntax ```js diff --git a/src/docs/src/Perms/requestReadSubdomains.md b/src/docs/src/Perms/requestReadSubdomains.md index 4219fb067..efa5ff7f6 100644 --- a/src/docs/src/Perms/requestReadSubdomains.md +++ b/src/docs/src/Perms/requestReadSubdomains.md @@ -6,6 +6,12 @@ platforms: [websites, apps] Request read access to the user's subdomains. If the user has already granted this permission the user will not be prompted and `true` will be returned. If the user grants permission `true` will be returned. If the user does not allow access `false` will be returned. +On a website, sign the user in to your site first with [`puter.auth.signIn()`](/Auth/signIn/). This method reads the signed-in user's identity before it can prompt, so for a signed-out visitor it rejects with `Unauthorized` and no prompt is shown. Answering a permission prompt does not by itself sign the user in to your site, so guard the call: + +```js +if (!puter.authToken) await puter.auth.signIn(); +``` + ## Syntax ```js diff --git a/src/docs/src/Perms/requestReadVideos.md b/src/docs/src/Perms/requestReadVideos.md index 8465092aa..27afa2bee 100644 --- a/src/docs/src/Perms/requestReadVideos.md +++ b/src/docs/src/Perms/requestReadVideos.md @@ -6,6 +6,12 @@ platforms: [websites, apps] Request read access to the user's Videos folder. If the user has already granted this permission the user will not be prompted and the path will be returned. If the user grants permission the path will be returned. If the user does not allow access `undefined` will be returned. +On a website, sign the user in to your site first with [`puter.auth.signIn()`](/Auth/signIn/). This method reads the signed-in user's identity before it can prompt, so for a signed-out visitor it rejects with `Unauthorized` and no prompt is shown. Answering a permission prompt does not by itself sign the user in to your site, so guard the call: + +```js +if (!puter.authToken) await puter.auth.signIn(); +``` + ## Syntax ```js diff --git a/src/docs/src/Perms/requestWriteDesktop.md b/src/docs/src/Perms/requestWriteDesktop.md index 2260cf3e1..05e33ac05 100644 --- a/src/docs/src/Perms/requestWriteDesktop.md +++ b/src/docs/src/Perms/requestWriteDesktop.md @@ -6,6 +6,12 @@ platforms: [websites, apps] Request write access to the user's Desktop folder. If the user has already granted this permission the user will not be prompted and the path will be returned. If the user grants permission the path will be returned. If the user does not allow access `undefined` will be returned. +On a website, sign the user in to your site first with [`puter.auth.signIn()`](/Auth/signIn/). This method reads the signed-in user's identity before it can prompt, so for a signed-out visitor it rejects with `Unauthorized` and no prompt is shown. Answering a permission prompt does not by itself sign the user in to your site, so guard the call: + +```js +if (!puter.authToken) await puter.auth.signIn(); +``` + ## Syntax ```js diff --git a/src/docs/src/Perms/requestWriteDocuments.md b/src/docs/src/Perms/requestWriteDocuments.md index 97c484aba..db765664d 100644 --- a/src/docs/src/Perms/requestWriteDocuments.md +++ b/src/docs/src/Perms/requestWriteDocuments.md @@ -6,6 +6,12 @@ platforms: [websites, apps] Request write access to the user's Documents folder. If the user has already granted this permission the user will not be prompted and the path will be returned. If the user grants permission the path will be returned. If the user does not allow access `undefined` will be returned. +On a website, sign the user in to your site first with [`puter.auth.signIn()`](/Auth/signIn/). This method reads the signed-in user's identity before it can prompt, so for a signed-out visitor it rejects with `Unauthorized` and no prompt is shown. Answering a permission prompt does not by itself sign the user in to your site, so guard the call: + +```js +if (!puter.authToken) await puter.auth.signIn(); +``` + ## Syntax ```js diff --git a/src/docs/src/Perms/requestWritePictures.md b/src/docs/src/Perms/requestWritePictures.md index fe2f74874..ba3baf8c6 100644 --- a/src/docs/src/Perms/requestWritePictures.md +++ b/src/docs/src/Perms/requestWritePictures.md @@ -6,6 +6,12 @@ platforms: [websites, apps] Request write access to the user's Pictures folder. If the user has already granted this permission the user will not be prompted and the path will be returned. If the user grants permission the path will be returned. If the user does not allow access `undefined` will be returned. +On a website, sign the user in to your site first with [`puter.auth.signIn()`](/Auth/signIn/). This method reads the signed-in user's identity before it can prompt, so for a signed-out visitor it rejects with `Unauthorized` and no prompt is shown. Answering a permission prompt does not by itself sign the user in to your site, so guard the call: + +```js +if (!puter.authToken) await puter.auth.signIn(); +``` + ## Syntax ```js diff --git a/src/docs/src/Perms/requestWriteVideos.md b/src/docs/src/Perms/requestWriteVideos.md index 01050ae08..0ede7bc7e 100644 --- a/src/docs/src/Perms/requestWriteVideos.md +++ b/src/docs/src/Perms/requestWriteVideos.md @@ -6,6 +6,12 @@ platforms: [websites, apps] Request write access to the user's Videos folder. If the user has already granted this permission the user will not be prompted and the path will be returned. If the user grants permission the path will be returned. If the user does not allow access `undefined` will be returned. +On a website, sign the user in to your site first with [`puter.auth.signIn()`](/Auth/signIn/). This method reads the signed-in user's identity before it can prompt, so for a signed-out visitor it rejects with `Unauthorized` and no prompt is shown. Answering a permission prompt does not by itself sign the user in to your site, so guard the call: + +```js +if (!puter.authToken) await puter.auth.signIn(); +``` + ## Syntax ```js diff --git a/src/gui/src/IPC.js b/src/gui/src/IPC.js index 2475e9582..6de69628b 100644 --- a/src/gui/src/IPC.js +++ b/src/gui/src/IPC.js @@ -1331,8 +1331,10 @@ const ipc_listener = async (event, handled) => { return; } - // options must be an object - if ( event.data.options === undefined || typeof event.data.options !== 'object' ) + // options must be an object. `typeof null === 'object'`, so null has to + // be caught here too — reading `.permission` off it throws, and the + // throw escapes before any reply, hanging the caller's promise forever. + if ( ! event.data.options || typeof event.data.options !== 'object' ) { event.data.options = {}; } diff --git a/src/puter-js/src/modules/UI.js b/src/puter-js/src/modules/UI.js index 3365a7816..0cdb2d278 100644 --- a/src/puter-js/src/modules/UI.js +++ b/src/puter-js/src/modules/UI.js @@ -1291,35 +1291,37 @@ class UI extends EventListener { settle(false); }; - if ( hasUserActivation() ) { - // A user gesture is active — open the popup immediately. - // Unique window name per request: window.open() reuses a - // window with the same name, which would hijack a popup an - // earlier, still-pending request is waiting on. - watchPopup(openAuthPopup(url, `puter-permission-${msg_id}`)); - } else { - // No user gesture: a popup opened now would be blocked by the - // browser. Show a consent dialog first; the popup is then - // opened from the user's click on that dialog, which provides - // the gesture the browser requires. - const dialog = new PuterDialog(() => {}, () => {}, { - popupURL: url, - // Same unique-name reasoning as the direct path above. - popupName: `puter-permission-${msg_id}`, - onLaunch: (popup) => watchPopup(popup), - onCancel: () => settle(false), - }); - consentDialog = dialog; - try { + // Every path out of here resolves a boolean, so anything that + // throws while launching — `window.open` refused outright by a + // policy or an override rather than returning null, a dialog that + // won't construct, no `document.body` yet because this was called + // from a script — has to deny rather than reject. + try { + if ( hasUserActivation() ) { + // A user gesture is active — open the popup immediately. + // Unique window name per request: window.open() reuses a + // window with the same name, which would hijack a popup an + // earlier, still-pending request is waiting on. + watchPopup(openAuthPopup(url, `puter-permission-${msg_id}`)); + } else { + // No user gesture: a popup opened now would be blocked by + // the browser. Show a consent dialog first; the popup is + // then opened from the user's click on that dialog, which + // provides the gesture the browser requires. + const dialog = new PuterDialog(() => {}, () => {}, { + popupURL: url, + // Same unique-name reasoning as the direct path above. + popupName: `puter-permission-${msg_id}`, + onLaunch: (popup) => watchPopup(popup), + onCancel: () => settle(false), + }); + consentDialog = dialog; document.body.appendChild(dialog); dialog.open(); - } catch (e) { - // Nothing to show the user (e.g. called from a - // script, so there is no body yet). This resolves to a - // boolean for every other caller, so deny rather than - // reject — and let cleanup drop the message listener. - settle(false); } + } catch (e) { + // `settle` runs cleanup, so the message listener is dropped too. + settle(false); } }); }; diff --git a/src/puter-js/tests/e2e/specs/requestPermission.spec.js b/src/puter-js/tests/e2e/specs/requestPermission.spec.js index f22bd45d2..38555cd2d 100644 --- a/src/puter-js/tests/e2e/specs/requestPermission.spec.js +++ b/src/puter-js/tests/e2e/specs/requestPermission.spec.js @@ -90,6 +90,31 @@ test.describe('puter.ui.requestPermission (env=app)', () => { } }); + test('a malformed options argument is denied rather than left hanging', async ({ page }) => { + // `typeof null === 'object'`, so a null `options` slipped past the + // "must be an object" guard and faulted on the `.permission` read — + // before any reply had been posted, leaving the caller's promise + // pending forever. env=web already answered false for the same input. + const appName = await registerTestApp(page, { fixtureURL: PERMISSION_FIXTURE_URL }); + try { + const appFrame = await gotoTestApp(page, appName); + const outcome = await appFrame.locator('body').evaluate(async () => { + const settled = puter.ui.requestPermission(null).then( + v => `resolved:${v}`, + e => `rejected:${e?.message ?? e}`, + ); + return Promise.race([ + settled, + new Promise(r => setTimeout(() => r('never settled'), 10_000)), + ]); + }); + expect(outcome).toBe('resolved:false'); + await expect(page.locator('dialog.perm-dialog')).toHaveCount(0); + } finally { + await deleteTestApp(page, appName); + } + }); + test('Escape dismisses the dialog as a denial', async ({ page }) => { const appName = await registerTestApp(page, { fixtureURL: PERMISSION_FIXTURE_URL }); try { @@ -480,6 +505,30 @@ test.describe('puter.ui.requestPermission (env=web popup)', () => { expect(await page.evaluate(() => window.__permPromise)).toBe(false); }); + test('a refused window.open denies instead of rejecting', async ({ page }) => { + // requestPermission is documented `Promise` and every handled + // path resolves false. `window.open` refused outright — by a policy or + // an override that throws rather than returning null — used to escape + // the launch branch and reject, since only the consent-dialog path was + // wrapped. + 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 }); + + const outcome = await page.evaluate(async () => { + window.open = () => { throw new Error('blocked by policy'); }; + const settled = puter.ui.requestPermission({ + permission: 'driver:puter-image-generation:generate', + }).then(v => `resolved:${v}`, e => `rejected:${e?.message ?? e}`); + return Promise.race([ + settled, + new Promise(r => setTimeout(() => r('never settled'), 10_000)), + ]); + }); + expect(outcome).toBe('resolved:false'); + }); + test('closing the popup without answering resolves false', async ({ page }) => { await page.goto('/'); await page.waitForFunction(() => !!window.puter?.authToken, null, { timeout: 60_000 });