From 5d5b1fd01e0a965a3c23516c03a103a64d1a828c Mon Sep 17 00:00:00 2001 From: velzie Date: Thu, 11 Jun 2026 18:04:21 -0400 Subject: [PATCH 1/5] poc --- .../controllers/auth/AuthController.ts | 66 ++ src/gui/src/initgui.js | 966 ++++++------------ src/puter-js/src/modules/Auth.js | 26 +- 3 files changed, 427 insertions(+), 631 deletions(-) diff --git a/src/backend/controllers/auth/AuthController.ts b/src/backend/controllers/auth/AuthController.ts index b5afe366a0..d536f3c0a8 100644 --- a/src/backend/controllers/auth/AuthController.ts +++ b/src/backend/controllers/auth/AuthController.ts @@ -95,6 +95,72 @@ const RESERVED_USERNAMES = new Set([ */ @Controller('') export class AuthController extends PuterController { + @Post('/login/wait', { + subdomain: ['api'], + }) + async loginWait(req: Request, res: Response) { + const { session } = req.body; + const key = `loginsession:${session}`; + + let token = await this.clients.redis.get(key); + + if (!token) { + const subscriber = this.clients.redis.duplicate(); + + token = await new Promise(async (resolve) => { + const timeout = setTimeout(() => resolve(null), 10000); + + const cleanup = async () => { + clearTimeout(timeout); + subscriber.off('message', onMessage); + await subscriber.unsubscribe(key).catch(() => undefined); + }; + const onMessage = async (channel: string, message: string) => { + if (channel !== key) return; + await cleanup(); + resolve(message); + }; + subscriber.on('message', onMessage); + await subscriber.subscribe(key); + + // just in case the token was set between the initial get and the subscribe + const tt = await this.clients.redis.get(key); + if (tt) { + await cleanup(); + resolve(tt); + } + }); + } + + if (!token) { + throw new HttpError(408, 'Request timeout.', { + legacyCode: 'request_timeout', + }); + } + + await this.clients.redis.del(key); + res.json({ + auth_token: token, + }); + } + @Post('/login/set', { + subdomain: ['api'], + }) + async loginSet(req: Request, res: Response) { + const { session, auth_token } = req.body; + if (!session || !auth_token) { + throw new HttpError(400, 'session and auth_token are required.', { + legacyCode: 'bad_request', + }); + } + + const key = `loginsession:${session}`; + await this.clients.redis.set(key, auth_token, 'EX', 60); + await this.clients.redis.publish(key, auth_token); + + res.json({ success: true }); + } + // -- Login ------------------------------------------------------- @Post('/login', { diff --git a/src/gui/src/initgui.js b/src/gui/src/initgui.js index e02f7a05b8..e185eb7d84 100644 --- a/src/gui/src/initgui.js +++ b/src/gui/src/initgui.js @@ -52,6 +52,336 @@ import { ProcessService } from './services/ProcessService.js'; import { ThemeService } from './services/ThemeService.js'; import { privacy_aware_path } from './util/desktop.js'; +const postAuthActions = async (action) => { + // ------------------------------------------------------------------------------------- + // Action: AuthMe — redirect to a third-party URL with the user's auth token + // ------------------------------------------------------------------------------------- + if ( action === 'authme' ) { + const redirectURL = window.url_query_params.get('redirectURL'); + if ( redirectURL ) { + const approved = await UIWindowAuthMe({ + redirect_url: redirectURL, + }); + if ( approved ) { + // Hand the app a named, revocable full-API-access + // token instead of the raw GUI/session token: it can + // use the whole API but can't manage the account. + let host = ''; + try { host = new URL(redirectURL).host; } catch ( e ) { /* ignore */ } + let token; + try { + token = await create_access_token({ + label: host + ? `${i18n('token_label_external_app')} (${host})` + : i18n('token_label_external_app'), + }); + } catch ( e ) { + await UIAlert({ message: e?.message ?? String(e) }); + return; + } + const url = new URL(redirectURL); + url.searchParams.set('token', token); + window.location.href = url.href; + return; + } + } + } + + // ------------------------------------------------------------------------------------- + // Action: CopyAuth — show dialog to copy auth token + // ------------------------------------------------------------------------------------- + if ( action === 'copyauth' ) { + await UIWindowCopyToken({ show_header: true }); + } + + // ------------------------------------------------------------------------------------- + // Load desktop, only if we're not embedded in a popup and not on the dashboard page + // ------------------------------------------------------------------------------------- + if ( !window.embedded_in_popup && !window.is_dashboard_mode ) { + if ( window.is_fullpage_mode ) { + // In fullpage mode, skip loading desktop items and background + UIDesktop({}); + } else { + await window.get_auto_arrange_data(); + puter.fs.stat({ path: window.desktop_path, consistency: 'eventual' }).then(desktop_fsentry => { + UIDesktop({ desktop_fsentry: desktop_fsentry }); + }); + } + } + // ------------------------------------------------------------------------------------- + // Dashboard mode + // ------------------------------------------------------------------------------------- + else if ( window.is_dashboard_mode ) { + UIDashboard(); + } + // ------------------------------------------------------------------------------------- + // If embedded in a popup, send the token to the opener and close the popup + // ------------------------------------------------------------------------------------- + else { + let msg_id = window.url_query_params.get('msg_id'); + let isolated = window.url_query_params.get("cross_origin_isolated") === 'true'; + let session = window.url_query_params.get('signin_session'); + if (isolated) { + let data = await window.getUserAppToken(new URL(window.openerOrigin).origin); + await fetch(`${window.api_origin}/login/set`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + auth_token: data.token, + session: session, + }), + }) + window.close(); + window.open('', '_self').close(); + } else { + try { + let data = await window.getUserAppToken(new URL(window.openerOrigin).origin); + // This is an implicit app and the app_uid is sent back from the server + // we cache it here so that we can use it later + window.host_app_uid = data.app_uid; + // send token to parent + window.opener.postMessage({ + msg: 'puter.token', + success: true, + token: data.token, + app_uid: data.app_uid, + username: window.user.username, + msg_id: msg_id, + }, window.openerOrigin); + // close popup + if ( !action || action === 'sign-in' ) { + window.close(); + window.open('', '_self').close(); + } + } catch ( err ) { + // send error to parent + window.opener.postMessage({ + msg: 'puter.token', + success: false, + token: null, + msg_id: msg_id, + }, window.openerOrigin); + // close popup + window.close(); + window.open('', '_self').close(); + } + } + + let app_uid; + + if ( window.openerOrigin ) { + app_uid = await window.getAppUIDFromOrigin(window.openerOrigin); + window.host_app_uid = app_uid; + } + + if ( action === 'show-open-file-picker' ) { + let options = window.url_query_params.get('options'); + options = JSON.parse(options ?? '{}'); + + // Open dialog + UIWindow({ + allowed_file_types: options?.accept, + selectable_body: options?.multiple, + path: `/${ window.user.username }/Desktop`, + // this is the uuid of the window to which this dialog will return + return_to_parent_window: true, + show_maximize_button: false, + show_minimize_button: false, + title: 'Open', + is_dir: true, + is_openFileDialog: true, + is_resizable: false, + has_head: false, + cover_page: true, + // selectable_body: is_selectable_body, + iframe_msg_uid: msg_id, + center: true, + initiating_app_uuid: app_uid, + on_close: function () { + window.opener.postMessage({ + msg: 'fileOpenCanceled', + original_msg_id: msg_id, + }, '*'); + }, + }); + } + //-------------------------------------------------------------------------------------- + // Action: Show Directory Picker + //-------------------------------------------------------------------------------------- + else if ( action === 'show-directory-picker' ) { + // open directory picker dialog + UIWindow({ + path: `/${ window.user.username }/Desktop`, + // this is the uuid of the window to which this dialog will return + // parent_uuid: event.data.appInstanceID, + return_to_parent_window: true, + show_maximize_button: false, + show_minimize_button: false, + title: 'Open', + is_dir: true, + is_directoryPicker: true, + is_resizable: false, + has_head: false, + cover_page: true, + // selectable_body: is_selectable_body, + iframe_msg_uid: msg_id, + center: true, + initiating_app_uuid: app_uid, + on_close: function () { + window.opener.postMessage({ + msg: 'directoryOpenCanceled', + original_msg_id: msg_id, + }, '*'); + }, + }); + } + //-------------------------------------------------------------------------------------- + // Action: Show Save File Dialog + //-------------------------------------------------------------------------------------- + else if ( action === 'show-save-file-picker' ) { + let allowed_file_types = window.url_query_params.get('allowed_file_types'); + + // send 'sendMeFileData' event to parent + window.opener.postMessage({ + msg: 'sendMeFileData', + }, '*'); + + // listen for 'showSaveFilePickerPopup' event from parent + window.addEventListener('message', async (event) => { + if ( event.data.msg !== 'showSaveFilePickerPopup' ) + { + return; + } + + // Open dialog + UIWindow({ + allowed_file_types: allowed_file_types, + path: `/${ window.user.username }/Desktop`, + // this is the uuid of the window to which this dialog will return + return_to_parent_window: true, + show_maximize_button: false, + show_minimize_button: false, + title: 'Save', + is_dir: true, + is_saveFileDialog: true, + is_resizable: false, + has_head: false, + cover_page: true, + // selectable_body: is_selectable_body, + iframe_msg_uid: msg_id, + center: true, + initiating_app_uuid: app_uid, + on_close: function () { + window.opener.postMessage({ + msg: 'fileSaveCanceled', + original_msg_id: msg_id, + }, '*'); + }, + onSaveFileDialogSave: async function (target_path, el_filedialog_window) { + $(el_filedialog_window).find('.window-disable-mask, .busy-indicator').show(); + let busy_init_ts = Date.now(); + + let overwrite = false; + let file_to_upload = new File([event.data.content], path.basename(target_path)); + let item_with_same_name_already_exists = true; + while ( item_with_same_name_already_exists ) { + // overwrite? + if ( overwrite ) + { + item_with_same_name_already_exists = false; + } + // upload + try { + const res = await puter.fs.write( + target_path, + file_to_upload, + { + dedupeName: false, + overwrite: overwrite, + }, + ); + + let file_signature = await puter.fs.sign(app_uid, { uid: res.uid, action: 'write' }); + file_signature = file_signature.items; + + item_with_same_name_already_exists = false; + window.opener.postMessage({ + msg: 'fileSaved', + original_msg_id: msg_id, + filename: res.name, + saved_file: { + name: file_signature.fsentry_name, + readURL: file_signature.read_url, + writeURL: file_signature.write_url, + metadataURL: file_signature.metadata_url, + type: file_signature.type, + uid: file_signature.uid, + path: privacy_aware_path(res.path), + }, + }, '*'); + + window.close(); + window.open('', '_self').close(); + } + catch ( err ) { + // item with same name exists + if ( err.code === 'item_with_same_name_exists' ) { + const alert_resp = await UIAlert({ + message: `${html_encode(err.entry_name)} already exists.`, + buttons: [ + { + label: i18n('replace'), + value: 'replace', + type: 'primary', + }, + { + label: i18n('cancel'), + value: 'cancel', + }, + ], + parent_uuid: $(el_filedialog_window).attr('data-element_uuid'), + }); + if ( alert_resp === 'replace' ) { + overwrite = true; + } else if ( alert_resp === 'cancel' ) { + // enable parent window + $(el_filedialog_window).find('.window-disable-mask, .busy-indicator').hide(); + return; + } + } + else { + console.log(err); + // show error + await UIAlert({ + message: err.message ?? 'Upload failed.', + parent_uuid: $(el_filedialog_window).attr('data-element_uuid'), + }); + // enable parent window + $(el_filedialog_window).find('.window-disable-mask, .busy-indicator').hide(); + return; + } + } + } + + // done + let busy_duration = (Date.now() - busy_init_ts); + if ( busy_duration >= window.busy_indicator_hide_delay ) { + $(el_filedialog_window).close(); + } else { + setTimeout(() => { + // close this dialog + $(el_filedialog_window).close(); + }, Math.abs(window.busy_indicator_hide_delay - busy_duration)); + } + }, + }); + }); + } + } +}; + const launch_services = async function (options) { // === Services Data Structures === const services_l_ = []; @@ -204,16 +534,16 @@ window.showTurnstileChallenge = function (options) {

Welcome to Puter!

- +
- +
${message}
- +
@@ -846,316 +1176,7 @@ window.initgui = async function (options) { } await window.update_auth_data(whoami.token || window.auth_token, whoami); - // ------------------------------------------------------------------------------------- - // Action: AuthMe — redirect to a third-party URL with the user's auth token - // ------------------------------------------------------------------------------------- - if ( action === 'authme' ) { - const redirectURL = window.url_query_params.get('redirectURL'); - if ( redirectURL ) { - const approved = await UIWindowAuthMe({ - redirect_url: redirectURL, - }); - if ( approved ) { - // Hand the app a named, revocable full-API-access - // token instead of the raw GUI/session token: it can - // use the whole API but can't manage the account. - let host = ''; - try { host = new URL(redirectURL).host; } catch ( e ) { /* ignore */ } - let token; - try { - token = await create_access_token({ - label: host - ? `${i18n('token_label_external_app')} (${host})` - : i18n('token_label_external_app'), - }); - } catch ( e ) { - await UIAlert({ message: e?.message ?? String(e) }); - return; - } - const url = new URL(redirectURL); - url.searchParams.set('token', token); - window.location.href = url.href; - return; - } - } - } - - // ------------------------------------------------------------------------------------- - // Action: CopyAuth — show dialog to copy auth token - // ------------------------------------------------------------------------------------- - if ( action === 'copyauth' ) { - await UIWindowCopyToken({ show_header: true }); - } - - // ------------------------------------------------------------------------------------- - // Load desktop, only if we're not embedded in a popup and not on the dashboard page - // ------------------------------------------------------------------------------------- - if ( !window.embedded_in_popup && !window.is_dashboard_mode ) { - if ( window.is_fullpage_mode ) { - // In fullpage mode, skip loading desktop items and background - UIDesktop({}); - } else { - await window.get_auto_arrange_data(); - puter.fs.stat({ path: window.desktop_path, consistency: 'eventual' }).then(desktop_fsentry => { - UIDesktop({ desktop_fsentry: desktop_fsentry }); - }); - } - } - // ------------------------------------------------------------------------------------- - // Dashboard mode - // ------------------------------------------------------------------------------------- - else if ( window.is_dashboard_mode ) { - UIDashboard(); - } - // ------------------------------------------------------------------------------------- - // If embedded in a popup, send the token to the opener and close the popup - // ------------------------------------------------------------------------------------- - else { - let msg_id = window.url_query_params.get('msg_id'); - try { - let data = await window.getUserAppToken(new URL(window.openerOrigin).origin); - // This is an implicit app and the app_uid is sent back from the server - // we cache it here so that we can use it later - window.host_app_uid = data.app_uid; - // send token to parent - window.opener.postMessage({ - msg: 'puter.token', - success: true, - token: data.token, - app_uid: data.app_uid, - username: window.user.username, - msg_id: msg_id, - }, window.openerOrigin); - // close popup - if ( !action || action === 'sign-in' ) { - window.close(); - window.open('', '_self').close(); - } - } catch ( err ) { - // send error to parent - window.opener.postMessage({ - msg: 'puter.token', - success: false, - token: null, - msg_id: msg_id, - }, window.openerOrigin); - // close popup - window.close(); - window.open('', '_self').close(); - } - - let app_uid; - - if ( window.openerOrigin ) { - app_uid = await window.getAppUIDFromOrigin(window.openerOrigin); - window.host_app_uid = app_uid; - } - - if ( action === 'show-open-file-picker' ) { - let options = window.url_query_params.get('options'); - options = JSON.parse(options ?? '{}'); - - // Open dialog - UIWindow({ - allowed_file_types: options?.accept, - selectable_body: options?.multiple, - path: `/${ window.user.username }/Desktop`, - // this is the uuid of the window to which this dialog will return - return_to_parent_window: true, - show_maximize_button: false, - show_minimize_button: false, - title: 'Open', - is_dir: true, - is_openFileDialog: true, - is_resizable: false, - has_head: false, - cover_page: true, - // selectable_body: is_selectable_body, - iframe_msg_uid: msg_id, - center: true, - initiating_app_uuid: app_uid, - on_close: function () { - window.opener.postMessage({ - msg: 'fileOpenCanceled', - original_msg_id: msg_id, - }, '*'); - }, - }); - } - //-------------------------------------------------------------------------------------- - // Action: Show Directory Picker - //-------------------------------------------------------------------------------------- - else if ( action === 'show-directory-picker' ) { - // open directory picker dialog - UIWindow({ - path: `/${ window.user.username }/Desktop`, - // this is the uuid of the window to which this dialog will return - // parent_uuid: event.data.appInstanceID, - return_to_parent_window: true, - show_maximize_button: false, - show_minimize_button: false, - title: 'Open', - is_dir: true, - is_directoryPicker: true, - is_resizable: false, - has_head: false, - cover_page: true, - // selectable_body: is_selectable_body, - iframe_msg_uid: msg_id, - center: true, - initiating_app_uuid: app_uid, - on_close: function () { - window.opener.postMessage({ - msg: 'directoryOpenCanceled', - original_msg_id: msg_id, - }, '*'); - }, - }); - } - //-------------------------------------------------------------------------------------- - // Action: Show Save File Dialog - //-------------------------------------------------------------------------------------- - else if ( action === 'show-save-file-picker' ) { - let allowed_file_types = window.url_query_params.get('allowed_file_types'); - - // send 'sendMeFileData' event to parent - window.opener.postMessage({ - msg: 'sendMeFileData', - }, '*'); - - // listen for 'showSaveFilePickerPopup' event from parent - window.addEventListener('message', async (event) => { - if ( event.data.msg !== 'showSaveFilePickerPopup' ) - { - return; - } - - // Open dialog - UIWindow({ - allowed_file_types: allowed_file_types, - path: `/${ window.user.username }/Desktop`, - // this is the uuid of the window to which this dialog will return - return_to_parent_window: true, - show_maximize_button: false, - show_minimize_button: false, - title: 'Save', - is_dir: true, - is_saveFileDialog: true, - is_resizable: false, - has_head: false, - cover_page: true, - // selectable_body: is_selectable_body, - iframe_msg_uid: msg_id, - center: true, - initiating_app_uuid: app_uid, - on_close: function () { - window.opener.postMessage({ - msg: 'fileSaveCanceled', - original_msg_id: msg_id, - }, '*'); - }, - onSaveFileDialogSave: async function (target_path, el_filedialog_window) { - $(el_filedialog_window).find('.window-disable-mask, .busy-indicator').show(); - let busy_init_ts = Date.now(); - - let overwrite = false; - let file_to_upload = new File([event.data.content], path.basename(target_path)); - let item_with_same_name_already_exists = true; - while ( item_with_same_name_already_exists ) { - // overwrite? - if ( overwrite ) - { - item_with_same_name_already_exists = false; - } - // upload - try { - const res = await puter.fs.write( - target_path, - file_to_upload, - { - dedupeName: false, - overwrite: overwrite, - }, - ); - - let file_signature = await puter.fs.sign(app_uid, { uid: res.uid, action: 'write' }); - file_signature = file_signature.items; - - item_with_same_name_already_exists = false; - window.opener.postMessage({ - msg: 'fileSaved', - original_msg_id: msg_id, - filename: res.name, - saved_file: { - name: file_signature.fsentry_name, - readURL: file_signature.read_url, - writeURL: file_signature.write_url, - metadataURL: file_signature.metadata_url, - type: file_signature.type, - uid: file_signature.uid, - path: privacy_aware_path(res.path), - }, - }, '*'); - - window.close(); - window.open('', '_self').close(); - } - catch ( err ) { - // item with same name exists - if ( err.code === 'item_with_same_name_exists' ) { - const alert_resp = await UIAlert({ - message: `${html_encode(err.entry_name)} already exists.`, - buttons: [ - { - label: i18n('replace'), - value: 'replace', - type: 'primary', - }, - { - label: i18n('cancel'), - value: 'cancel', - }, - ], - parent_uuid: $(el_filedialog_window).attr('data-element_uuid'), - }); - if ( alert_resp === 'replace' ) { - overwrite = true; - } else if ( alert_resp === 'cancel' ) { - // enable parent window - $(el_filedialog_window).find('.window-disable-mask, .busy-indicator').hide(); - return; - } - } - else { - console.log(err); - // show error - await UIAlert({ - message: err.message ?? 'Upload failed.', - parent_uuid: $(el_filedialog_window).attr('data-element_uuid'), - }); - // enable parent window - $(el_filedialog_window).find('.window-disable-mask, .busy-indicator').hide(); - return; - } - } - } - - // done - let busy_duration = (Date.now() - busy_init_ts); - if ( busy_duration >= window.busy_indicator_hide_delay ) { - $(el_filedialog_window).close(); - } else { - setTimeout(() => { - // close this dialog - $(el_filedialog_window).close(); - }, Math.abs(window.busy_indicator_hide_delay - busy_duration)); - } - }, - }); - }); - } - } - + await postAuthActions(action); // ---------------------------------------------------------- // Get user's sites // ---------------------------------------------------------- @@ -1276,7 +1297,7 @@ window.initgui = async function (options) { $(this).remove(); resolve(); }); - + // Just in case anything fails, also resolve after 500ms await window.sleep(500); resolve(); @@ -1419,47 +1440,6 @@ window.initgui = async function (options) { // close all windows $('.window').close(); - // ------------------------------------------------------------------------------------- - // Action: AuthMe — redirect to a third-party URL with the user's auth token - // ------------------------------------------------------------------------------------- - if ( action === 'authme' ) { - const redirectURL = window.url_query_params.get('redirectURL'); - if ( redirectURL ) { - const approved = await UIWindowAuthMe({ - redirect_url: redirectURL, - }); - if ( approved ) { - // Hand the app a named, revocable full-API-access token - // instead of the raw GUI/session token: it can use the - // whole API but can't manage the account. - let host = ''; - try { host = new URL(redirectURL).host; } catch ( e ) { /* ignore */ } - let token; - try { - token = await create_access_token({ - label: host - ? `${i18n('token_label_external_app')} (${host})` - : i18n('token_label_external_app'), - }); - } catch ( e ) { - await UIAlert({ message: e?.message ?? String(e) }); - return; - } - const url = new URL(redirectURL); - url.searchParams.set('token', token); - window.location.href = url.href; - return; - } - } - } - - // ------------------------------------------------------------------------------------- - // Action: CopyAuth — show dialog to copy auth token - // ------------------------------------------------------------------------------------- - if ( action === 'copyauth' ) { - await UIWindowCopyToken({ show_header: true }); - } - // ------------------------------------------------------------------------------------- // Early check for fullpage mode from app metadata (after login) // ------------------------------------------------------------------------------------- @@ -1476,281 +1456,7 @@ window.initgui = async function (options) { } } - // ------------------------------------------------------------------------------------- - // Load desktop, if not embedded in a popup and not on the dashboard page - // ------------------------------------------------------------------------------------- - if ( !window.embedded_in_popup && !window.is_dashboard_mode ) { - if ( window.is_fullpage_mode ) { - // In fullpage mode, skip loading desktop items and background - UIDesktop({}); - } else { - await window.get_auto_arrange_data(); - puter.fs.stat({ path: window.desktop_path, consistency: 'eventual' }).then(desktop_fsentry => { - UIDesktop({ desktop_fsentry: desktop_fsentry }); - }); - } - } - // ------------------------------------------------------------------------------------- - // Dashboard mode: open explorer pointing to home directory - // ------------------------------------------------------------------------------------- - else if ( window.is_dashboard_mode ) { - UIDashboard(); - } - // ------------------------------------------------------------------------------------- - // If embedded in a popup, send the 'ready' event to referrer and close the popup - // ------------------------------------------------------------------------------------- - else { - let msg_id = window.url_query_params.get('msg_id'); - try { - - let data = await window.getUserAppToken(new URL(window.openerOrigin).origin); - // This is an implicit app and the app_uid is sent back from the server - // we cache it here so that we can use it later - window.host_app_uid = data.app_uid; - // send token to parent - window.opener.postMessage({ - msg: 'puter.token', - success: true, - msg_id: msg_id, - token: data.token, - username: window.user.username, - app_uid: data.app_uid, - }, window.openerOrigin); - // close popup - if ( !action || action === 'sign-in' ) { - window.close(); - window.open('', '_self').close(); - } - } catch ( err ) { - // send error to parent - window.opener.postMessage({ - msg: 'puter.token', - msg_id: msg_id, - success: false, - token: null, - }, window.openerOrigin); - // close popup - window.close(); - window.open('', '_self').close(); - } - - let app_uid; - - if ( window.openerOrigin ) { - app_uid = await window.getAppUIDFromOrigin(window.openerOrigin); - window.host_app_uid = app_uid; - } - - //-------------------------------------------------------------------------------------- - // Action: Show Open File Picker - //-------------------------------------------------------------------------------------- - if ( action === 'show-open-file-picker' ) { - let options = window.url_query_params.get('options'); - options = JSON.parse(options ?? '{}'); - - // Open dialog - UIWindow({ - allowed_file_types: options?.accept, - selectable_body: options?.multiple, - path: `/${ window.user.username }/Desktop`, - return_to_parent_window: true, - show_maximize_button: false, - show_minimize_button: false, - title: 'Open', - is_dir: true, - is_openFileDialog: true, - is_resizable: false, - has_head: false, - cover_page: true, - iframe_msg_uid: msg_id, - center: true, - initiating_app_uuid: app_uid, - on_close: function () { - window.opener.postMessage({ - msg: 'fileOpenCanceled', - original_msg_id: msg_id, - }, '*'); - }, - }); - } - //-------------------------------------------------------------------------------------- - // Action: Show Directory Picker - //-------------------------------------------------------------------------------------- - else if ( action === 'show-directory-picker' ) { - // open directory picker dialog - UIWindow({ - path: `/${ window.user.username }/Desktop`, - // this is the uuid of the window to which this dialog will return - // parent_uuid: event.data.appInstanceID, - return_to_parent_window: true, - show_maximize_button: false, - show_minimize_button: false, - title: 'Open', - is_dir: true, - is_directoryPicker: true, - is_resizable: false, - has_head: false, - cover_page: true, - // selectable_body: is_selectable_body, - iframe_msg_uid: msg_id, - center: true, - initiating_app_uuid: app_uid, - on_close: function () { - window.opener.postMessage({ - msg: 'directoryOpenCanceled', - original_msg_id: msg_id, - }, '*'); - }, - }); - } - - //-------------------------------------------------------------------------------------- - // Action: Show Save File Dialog - //-------------------------------------------------------------------------------------- - else if ( action === 'show-save-file-picker' ) { - let allowed_file_types = window.url_query_params.get('allowed_file_types'); - - // send 'sendMeFileData' event to parent - window.opener.postMessage({ - msg: 'sendMeFileData', - }, '*'); - - // listen for 'showSaveFilePickerPopup' event from parent - window.addEventListener('message', async (event) => { - if ( event.data.msg !== 'showSaveFilePickerPopup' ) - { - return; - } - - // Open dialog - UIWindow({ - allowed_file_types: allowed_file_types, - path: `/${ window.user.username }/Desktop`, - // this is the uuid of the window to which this dialog will return - return_to_parent_window: true, - show_maximize_button: false, - show_minimize_button: false, - title: 'Save', - is_dir: true, - is_saveFileDialog: true, - is_resizable: false, - has_head: false, - cover_page: true, - // selectable_body: is_selectable_body, - iframe_msg_uid: msg_id, - center: true, - initiating_app_uuid: app_uid, - on_close: function () { - window.opener.postMessage({ - msg: 'fileSaveCanceled', - original_msg_id: msg_id, - }, '*'); - }, - onSaveFileDialogSave: async function (target_path, el_filedialog_window) { - $(el_filedialog_window).find('.window-disable-mask, .busy-indicator').show(); - let busy_init_ts = Date.now(); - - let overwrite = false; - let file_to_upload = new File([event.data.content], path.basename(target_path)); - let item_with_same_name_already_exists = true; - while ( item_with_same_name_already_exists ) { - // overwrite? - if ( overwrite ) - { - item_with_same_name_already_exists = false; - } - // upload - try { - const res = await puter.fs.write( - target_path, - file_to_upload, - { - dedupeName: false, - overwrite: overwrite, - }, - ); - - let file_signature = await puter.fs.sign(app_uid, { uid: res.uid, action: 'write' }); - file_signature = file_signature.items; - - item_with_same_name_already_exists = false; - window.opener.postMessage({ - msg: 'fileSaved', - original_msg_id: msg_id, - filename: res.name, - saved_file: { - name: file_signature.fsentry_name, - readURL: file_signature.read_url, - writeURL: file_signature.write_url, - metadataURL: file_signature.metadata_url, - type: file_signature.type, - uid: file_signature.uid, - path: privacy_aware_path(res.path), - }, - }, '*'); - - window.close(); - window.open('', '_self').close(); - // show_save_account_notice_if_needed(); - } - catch ( err ) { - // item with same name exists - if ( err.code === 'item_with_same_name_exists' ) { - const alert_resp = await UIAlert({ - message: `${html_encode(err.entry_name)} already exists.`, - buttons: [ - { - label: i18n('replace'), - value: 'replace', - type: 'primary', - }, - { - label: i18n('cancel'), - value: 'cancel', - }, - ], - parent_uuid: $(el_filedialog_window).attr('data-element_uuid'), - }); - if ( alert_resp === 'replace' ) { - overwrite = true; - } else if ( alert_resp === 'cancel' ) { - // enable parent window - $(el_filedialog_window).find('.window-disable-mask, .busy-indicator').hide(); - return; - } - } - else { - console.log(err); - // show error - await UIAlert({ - message: err.message ?? 'Upload failed.', - parent_uuid: $(el_filedialog_window).attr('data-element_uuid'), - }); - // enable parent window - $(el_filedialog_window).find('.window-disable-mask, .busy-indicator').hide(); - return; - } - } - } - - // done - let busy_duration = (Date.now() - busy_init_ts); - if ( busy_duration >= window.busy_indicator_hide_delay ) { - $(el_filedialog_window).close(); - } else { - setTimeout(() => { - // close this dialog - $(el_filedialog_window).close(); - }, Math.abs(window.busy_indicator_hide_delay - busy_duration)); - } - }, - - }); - }); - } - - } - + await postAuthActions(action); }); if ( window.__login_completed ) { diff --git a/src/puter-js/src/modules/Auth.js b/src/puter-js/src/modules/Auth.js index 2ccd451e52..3ba9832a2d 100644 --- a/src/puter-js/src/modules/Auth.js +++ b/src/puter-js/src/modules/Auth.js @@ -48,8 +48,9 @@ class Auth { options = options || {}; return new Promise((resolve, reject) => { + const signinsession = crypto.randomUUID(); const msg_id = this.#messageID++; - const url = `${puter.defaultGUIOrigin}/action/sign-in?embedded_in_popup=true&msg_id=${msg_id}${window.crossOriginIsolated ? '&cross_origin_isolated=true' : ''}${options.attempt_temp_user_creation ? '&attempt_temp_user_creation=true' : ''}`; + const url = `${puter.defaultGUIOrigin}/action/sign-in?embedded_in_popup=true&msg_id=${msg_id}${window.crossOriginIsolated ? `&cross_origin_isolated=true&signin_session=${signinsession}` : ''}${options.attempt_temp_user_creation ? '&attempt_temp_user_creation=true' : ''}`; // Guards against settling the promise more than once across the // message, popup-closed, and dialog-cancel code paths. @@ -68,6 +69,29 @@ class Auth { window.removeEventListener('message', messageHandler); }; + if ( window.crossOriginIsolated ) { + (async () => { + for ( let i = 0; i < 5; i++ ) { + try { + const result = await fetch(`${this.APIOrigin}/login/wait`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ session: signinsession }), + }); + + if ( result.ok ) { + const { auth_token } = await result.json(); + puter.setAuthToken(auth_token); + resolve({ success: true, token: auth_token }); + return ''; + } + } catch {} + await new Promise(r => setTimeout(r, 1000)); + } + })(); + } function messageHandler (e) { // Only accept the token from the Puter GUI origin AND from the // popup window we opened. Origin alone is insufficient (any From 66ced946640d0327714dbe2f400c3245e0409059 Mon Sep 17 00:00:00 2001 From: velzie Date: Mon, 22 Jun 2026 17:41:20 -0400 Subject: [PATCH 2/5] route login key through broadcastservice --- src/backend/clients/event/types.ts | 2 + .../controllers/auth/AuthController.ts | 54 ++++++++----------- .../services/broadcast/BroadcastService.ts | 53 ++++++++++++++++-- 3 files changed, 72 insertions(+), 37 deletions(-) diff --git a/src/backend/clients/event/types.ts b/src/backend/clients/event/types.ts index 6bb7c251be..89a0eeae70 100644 --- a/src/backend/clients/event/types.ts +++ b/src/backend/clients/event/types.ts @@ -288,6 +288,8 @@ export type EventMap = { // normalized path: `route...before|after|error|reject`. Same // wildcard + veto semantics as the driver lifecycle above. [K in `route.${string}`]: RouteLifecycleEvent; +} & { + [K in `pubsub.login.${string}`]: { authtoken: string }; }; /** diff --git a/src/backend/controllers/auth/AuthController.ts b/src/backend/controllers/auth/AuthController.ts index 17b00225c4..afa14bbad2 100644 --- a/src/backend/controllers/auth/AuthController.ts +++ b/src/backend/controllers/auth/AuthController.ts @@ -100,45 +100,29 @@ export class AuthController extends PuterController { }) async loginWait(req: Request, res: Response) { const { session } = req.body; - const key = `loginsession:${session}`; - - let token = await this.clients.redis.get(key); - - if (!token) { - const subscriber = this.clients.redis.duplicate(); - - token = await new Promise(async (resolve) => { - const timeout = setTimeout(() => resolve(null), 10000); - - const cleanup = async () => { - clearTimeout(timeout); - subscriber.off('message', onMessage); - await subscriber.unsubscribe(key).catch(() => undefined); - }; - const onMessage = async (channel: string, message: string) => { - if (channel !== key) return; - await cleanup(); - resolve(message); - }; - subscriber.on('message', onMessage); - await subscriber.subscribe(key); - - // just in case the token was set between the initial get and the subscribe - const tt = await this.clients.redis.get(key); - if (tt) { - await cleanup(); - resolve(tt); - } + if (!session) { + throw new HttpError(400, 'session is required.', { + legacyCode: 'bad_request', }); } + const { resolve, promise } = Promise.withResolvers(); + let token: string | null = null; + this.clients.event.on(`pubsub.login.${session}`, (key, value) => { + token = value.authtoken; + resolve(); + }); + + const timeout = new Promise((resolve) => + setTimeout(resolve, 10000), + ); + await Promise.race([promise, timeout]); if (!token) { throw new HttpError(408, 'Request timeout.', { legacyCode: 'request_timeout', }); } - await this.clients.redis.del(key); res.json({ auth_token: token, }); @@ -154,9 +138,13 @@ export class AuthController extends PuterController { }); } - const key = `loginsession:${session}`; - await this.clients.redis.set(key, auth_token, 'EX', 60); - await this.clients.redis.publish(key, auth_token); + this.clients.event.emit( + `pubsub.login.${session}`, + { + authtoken: auth_token, + }, + {}, + ); res.json({ success: true }); } diff --git a/src/backend/services/broadcast/BroadcastService.ts b/src/backend/services/broadcast/BroadcastService.ts index 237b2e805f..8e0be94613 100644 --- a/src/backend/services/broadcast/BroadcastService.ts +++ b/src/backend/services/broadcast/BroadcastService.ts @@ -81,9 +81,6 @@ interface IncomingHeaders { * - Inbound handler ignores POSTs whose `X-Broadcast-Peer-Id` matches * this server's own peerId (catches misconfigured loopbacks). * - * No Redis pub/sub here — webhooks are the only transport. Same-cluster - * fan-out is handled by sockets via the Redis streams adapter, so an - * additional Redis channel here would just duplicate work. */ export class BroadcastService extends PuterService { /** peerId → resolved peer config, used for incoming-verify lookup. */ @@ -103,12 +100,14 @@ export class BroadcastService extends PuterService { #webhookHostHeader: string | null = null; /** Self-signed certs are common between Puter nodes — accept them. */ #webhookHttpsAgent = new HttpsAgent({ rejectUnauthorized: false }); + #redisSub: ReturnType | null = null; // -- Lifecycle --------------------------------------------------- override onServerStart(): void { this.#loadConfig(); this.#subscribeOutbound(); + this.#subscribeRedisOutbound(); } override async onServerPrepareShutdown(): Promise { @@ -123,6 +122,11 @@ export class BroadcastService extends PuterService { } catch (err) { console.warn('[broadcast] final flush failed', err); } + if (this.#redisSub) { + await this.#redisSub.unsubscribe('pubsub'); + this.#redisSub.quit(); + this.#redisSub = null; + } } // -- Public API used by BroadcastController ---------------------- @@ -231,8 +235,49 @@ export class BroadcastService extends PuterService { return { ok: true }; } - // -- Outbound: subscribe + queue + flush ------------------------ + #pubsubFanout(key: string, data: unknown, meta: object): void { + const safeMeta = this.#normalizeMeta(meta); + if (safeMeta.from_fanout) return; + this.clients.redis.publish( + 'pubsub', + JSON.stringify({ key, data, safeMeta }), + ); + } + + // outer.pubsub.* events will be broadcast to other clusters through webhooks + // pubsub.* will only fan-out to same-cluster nodes. + #subscribeRedisOutbound(): void { + this.#redisSub = this.clients.redis.duplicate(); + this.#redisSub.subscribe('pubsub'); + this.#redisSub.on('message', (channel: string, message: string) => { + if (channel !== 'pubsub') return; + const parsed = JSON.parse(message); + const { key, data, meta } = parsed as { + key: string; + data: unknown; + meta: object; + }; + + this.clients.event.emit(key, data, { ...meta, from_fanout: true }); + }); + + this.clients.event.on( + 'outer.pubsub.*', + (key: string, data: unknown, meta: object) => { + this.#pubsubFanout(key, data, meta); + }, + ); + this.clients.event.on( + 'pubsub.*', + (key: string, data: unknown, meta: object) => { + this.#pubsubFanout(key, data, meta); + }, + ); + } + // -- Outbound: subscribe + queue + flush ------------------------ + // outer.* events will be broadcast to other clusters through webhooks + // outer will NOT automatically sync to same-cluster peers. #subscribeOutbound(): void { // Wildcard: every `outer.*` event gets considered for broadcast. // The handler skips events that came in via webhook (meta.from_outside) From 141cce74135f8c946b020f5c5e27cbad5be4414d Mon Sep 17 00:00:00 2001 From: velzie Date: Mon, 22 Jun 2026 18:00:10 -0400 Subject: [PATCH 3/5] cleanup --- src/backend/clients/event/EventClient.ts | 13 +++++++++++++ src/backend/controllers/auth/AuthController.ts | 6 ++++-- src/backend/services/broadcast/BroadcastService.ts | 2 +- src/puter-js/src/modules/Auth.js | 11 +++++++++-- 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/backend/clients/event/EventClient.ts b/src/backend/clients/event/EventClient.ts index 15ebd95b6f..03f48bf24c 100644 --- a/src/backend/clients/event/EventClient.ts +++ b/src/backend/clients/event/EventClient.ts @@ -146,6 +146,19 @@ export class EventClient extends PuterClient { this.#eventListeners[key] ?? (this.#eventListeners[key] = []); listeners.push(callback as EventListener); } + off

( + key: P, + callback: ( + key: MatchingEvents

, + data: EventMap[MatchingEvents

], + meta: EventMetadata, + ) => Promise | void, + ) { + const listeners = this.#eventListeners[key]; + if (!listeners) return; + const idx = listeners.indexOf(callback as EventListener); + if (idx !== -1) listeners.splice(idx, 1); + } async #emitEvent( listener: EventListener, key: T, diff --git a/src/backend/controllers/auth/AuthController.ts b/src/backend/controllers/auth/AuthController.ts index afa14bbad2..814b8047bf 100644 --- a/src/backend/controllers/auth/AuthController.ts +++ b/src/backend/controllers/auth/AuthController.ts @@ -108,10 +108,11 @@ export class AuthController extends PuterController { const { resolve, promise } = Promise.withResolvers(); let token: string | null = null; - this.clients.event.on(`pubsub.login.${session}`, (key, value) => { + const listener = (_key: string, value: { authtoken: string }) => { token = value.authtoken; resolve(); - }); + }; + this.clients.event.on(`pubsub.login.${session}`, listener); const timeout = new Promise((resolve) => setTimeout(resolve, 10000), @@ -122,6 +123,7 @@ export class AuthController extends PuterController { legacyCode: 'request_timeout', }); } + this.clients.event.off(`pubsub.login.${session}`, listener); res.json({ auth_token: token, diff --git a/src/backend/services/broadcast/BroadcastService.ts b/src/backend/services/broadcast/BroadcastService.ts index 8e0be94613..d281eab3c3 100644 --- a/src/backend/services/broadcast/BroadcastService.ts +++ b/src/backend/services/broadcast/BroadcastService.ts @@ -240,7 +240,7 @@ export class BroadcastService extends PuterService { if (safeMeta.from_fanout) return; this.clients.redis.publish( 'pubsub', - JSON.stringify({ key, data, safeMeta }), + JSON.stringify({ key, data, meta: safeMeta }), ); } diff --git a/src/puter-js/src/modules/Auth.js b/src/puter-js/src/modules/Auth.js index 3ba9832a2d..e26dc8ed25 100644 --- a/src/puter-js/src/modules/Auth.js +++ b/src/puter-js/src/modules/Auth.js @@ -71,7 +71,7 @@ class Auth { if ( window.crossOriginIsolated ) { (async () => { - for ( let i = 0; i < 5; i++ ) { + while (true) { try { const result = await fetch(`${this.APIOrigin}/login/wait`, { method: 'POST', @@ -83,6 +83,9 @@ class Auth { if ( result.ok ) { const { auth_token } = await result.json(); + if (settled) return; + settled = true; + cleanup(); puter.setAuthToken(auth_token); resolve({ success: true, token: auth_token }); return ''; @@ -161,7 +164,11 @@ class Auth { if ( hasUserActivation() ) { // A user gesture is active — open the popup immediately. - watchPopup(openAuthPopup(url)); + const popup = openAuthPopup(url); + if ( !window.crossOriginIsolated ) { + // cannot watch in isolated mode + watchPopup(); + } } else { // No user gesture: a popup opened now would be blocked by the // browser. Show a consent dialog first; the popup is then From e07c68704b44c0b2a8ce60cf8e209eab9e16e9b7 Mon Sep 17 00:00:00 2001 From: velzie Date: Mon, 22 Jun 2026 18:30:23 -0400 Subject: [PATCH 4/5] security validation and prevent additional webhook sends --- src/backend/controllers/auth/AuthController.ts | 7 ++++--- src/backend/services/broadcast/BroadcastService.ts | 8 +++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/backend/controllers/auth/AuthController.ts b/src/backend/controllers/auth/AuthController.ts index 814b8047bf..a3171cd773 100644 --- a/src/backend/controllers/auth/AuthController.ts +++ b/src/backend/controllers/auth/AuthController.ts @@ -20,7 +20,7 @@ import bcrypt from 'bcrypt'; import type { Request, RequestHandler, Response } from 'express'; import crypto from 'node:crypto'; -import { v4 as uuidv4 } from 'uuid'; +import { v4 as uuidv4, validate as validateUuid } from 'uuid'; import validator from 'validator'; import { Controller, Get, Post } from '../../core/http/decorators.js'; import { HttpError } from '../../core/http/HttpError.js'; @@ -100,7 +100,8 @@ export class AuthController extends PuterController { }) async loginWait(req: Request, res: Response) { const { session } = req.body; - if (!session) { + // validate uuid to prevent ultra long key or listening on pubsub.login.* + if (!session || !validateUuid(session)) { throw new HttpError(400, 'session is required.', { legacyCode: 'bad_request', }); @@ -134,7 +135,7 @@ export class AuthController extends PuterController { }) async loginSet(req: Request, res: Response) { const { session, auth_token } = req.body; - if (!session || !auth_token) { + if (!session || !auth_token || !validateUuid(session)) { throw new HttpError(400, 'session and auth_token are required.', { legacyCode: 'bad_request', }); diff --git a/src/backend/services/broadcast/BroadcastService.ts b/src/backend/services/broadcast/BroadcastService.ts index d281eab3c3..ea26fcc026 100644 --- a/src/backend/services/broadcast/BroadcastService.ts +++ b/src/backend/services/broadcast/BroadcastService.ts @@ -257,8 +257,14 @@ export class BroadcastService extends PuterService { data: unknown; meta: object; }; + const safeMeta = this.#normalizeMeta(meta); - this.clients.event.emit(key, data, { ...meta, from_fanout: true }); + this.clients.event.emit(key, data, { + ...safeMeta, + from_fanout: true, + // it's not from outside, but mark it as to prevent sending the webhook twice + from_outside: true, + }); }); this.clients.event.on( From d1833a37b5fd3365214e2be6c9c4a489f6187b4f Mon Sep 17 00:00:00 2001 From: velzie Date: Fri, 26 Jun 2026 18:59:34 -0400 Subject: [PATCH 5/5] block double-broadcast from redis --- .../services/broadcast/BroadcastService.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/backend/services/broadcast/BroadcastService.ts b/src/backend/services/broadcast/BroadcastService.ts index ea26fcc026..bb6b57c3a6 100644 --- a/src/backend/services/broadcast/BroadcastService.ts +++ b/src/backend/services/broadcast/BroadcastService.ts @@ -18,7 +18,7 @@ */ import axios from 'axios'; -import { createHmac, timingSafeEqual } from 'node:crypto'; +import { createHmac, randomUUID, timingSafeEqual } from 'node:crypto'; import { Agent as HttpsAgent } from 'node:https'; import { IBroadcastPeerConfig } from '../../types.js'; import { PuterService } from '../types.js'; @@ -87,6 +87,8 @@ export class BroadcastService extends PuterService { #peersByKey: Record = {}; /** Subset of peers with `webhook: true`, used for outbound fan-out. */ #webhookPeers: IBroadcastPeerConfig[] = []; + /** Identifier used to tell what server a redis fan-out is coming from. */ + #redisSourceId: string = `${this.config.serverId}:${randomUUID()}`; /** Coalesced outbound events, keyed by serialized shape. */ #outboundEventsByDedupKey = new Map(); @@ -240,7 +242,12 @@ export class BroadcastService extends PuterService { if (safeMeta.from_fanout) return; this.clients.redis.publish( 'pubsub', - JSON.stringify({ key, data, meta: safeMeta }), + JSON.stringify({ + key, + data, + meta: safeMeta, + source: this.#redisSourceId, + }), ); } @@ -252,11 +259,13 @@ export class BroadcastService extends PuterService { this.#redisSub.on('message', (channel: string, message: string) => { if (channel !== 'pubsub') return; const parsed = JSON.parse(message); - const { key, data, meta } = parsed as { + const { key, data, meta, source } = parsed as { key: string; data: unknown; meta: object; + source: string; }; + if (source === this.#redisSourceId) return; const safeMeta = this.#normalizeMeta(meta); this.clients.event.emit(key, data, {