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/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 b3f253928b..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';
@@ -95,6 +95,63 @@ 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;
+ // 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',
+ });
+ }
+ const { resolve, promise } = Promise.withResolvers();
+
+ let token: string | null = null;
+ 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),
+ );
+ await Promise.race([promise, timeout]);
+ if (!token) {
+ throw new HttpError(408, 'Request timeout.', {
+ legacyCode: 'request_timeout',
+ });
+ }
+ this.clients.event.off(`pubsub.login.${session}`, listener);
+
+ 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 || !validateUuid(session)) {
+ throw new HttpError(400, 'session and auth_token are required.', {
+ legacyCode: 'bad_request',
+ });
+ }
+
+ this.clients.event.emit(
+ `pubsub.login.${session}`,
+ {
+ authtoken: auth_token,
+ },
+ {},
+ );
+
+ res.json({ success: true });
+ }
+
// -- Login -------------------------------------------------------
@Post('/login', {
diff --git a/src/backend/services/broadcast/BroadcastService.ts b/src/backend/services/broadcast/BroadcastService.ts
index 237b2e805f..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';
@@ -81,15 +81,14 @@ 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. */
#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();
@@ -103,12 +102,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 +124,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 +237,62 @@ 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,
+ meta: safeMeta,
+ source: this.#redisSourceId,
+ }),
+ );
+ }
+
+ // 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, 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, {
+ ...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(
+ '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)
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!
-
+
-
+
-
+
@@ -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..e26dc8ed25 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,32 @@ class Auth {
window.removeEventListener('message', messageHandler);
};
+ if ( window.crossOriginIsolated ) {
+ (async () => {
+ while (true) {
+ 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();
+ if (settled) return;
+ settled = true;
+ cleanup();
+ 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
@@ -137,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