From 3cd50fbaf31e6999949c8d134a0e104e5965a4bb Mon Sep 17 00:00:00 2001 From: Daniel Salazar Date: Wed, 3 Jun 2026 18:54:22 -0700 Subject: [PATCH] move the token migration to root (#3206) --- BUG-BOUNTY.md | 32 +- .../controllers/auth/AuthController.ts | 1 - src/puter-js/src/index.js | 893 +++++++++++------- 3 files changed, 605 insertions(+), 321 deletions(-) diff --git a/BUG-BOUNTY.md b/BUG-BOUNTY.md index 8c172b956..755ac74c9 100644 --- a/BUG-BOUNTY.md +++ b/BUG-BOUNTY.md @@ -17,18 +17,48 @@ Out-of-scope: * Denial of Service (DoS), spam, or volumetric attacks. * Physical security issues. +## Known Non-Issues (Please Check Before Submitting) + +The following have already been reviewed and determined **not to be vulnerabilities**. Reports that only re-describe one of these are **not eligible for a reward** and will be closed as non-issues — even if they include new code references. Please review this list before submitting: + +* **XSS / CORS / token issues scoped to `api.puter.com`.** The API origin holds no sensitive session cookies; the user session lives on `puter.com`. +* **SSRF via `secureFetch`.** Production routes outbound requests through an isolated proxy that has no access to internal/SSRF-sensitive resources. +* **Attacks that depend on guessing an `appInstanceID` or app UID.** These are random 128-bit secret values and are not considered guessable. +* **Apps invoking drivers, creating workers, or using KV.** Applications are intended to do this; worker permissions are scoped to the owning app. This is by design. +* **App metadata or app user-count "leaks".** This information is currently public by design. +* **General "token in a URL" / token-lifetime designs** — signed directory URLs exposing children, a write signature implying read, or app tokens outliving a web session. These are current intended behaviors. +* **Missing PKCE or other OIDC hardening** where the provider's token is already verified over TLS. Please open a GitHub issue/PR for hardening suggestions. +* **Best-practice suggestions** such as login/registration username enumeration (kept intentionally for UX) or unauthenticated unsubscribe links (industry norm). +* **Rate-limiting suggestions for TURN credential issuance** (intentional; not billed per tunnel). + +If you believe you have a **genuinely new** exploit chain that defeats one of these rationales (for example, demonstrating a sensitive credential that really is reachable on `api.puter.com`), say so explicitly and show why the reasoning above does not apply. + ## Rules of Engagement To participate, you must: -1. **Report responsibly**: Provide detailed steps to reproduce the issue, including proof-of-concept code or screenshots where applicable. +1. **Report responsibly**: Provide detailed steps to reproduce the issue, including proof-of-concept code, screenshots, or a screen recording (see *Proof of Reproduction* below). 2. **Do no harm**: Do not exfiltrate, modify, or delete data. Only access your own account or test data. 3. **Respect availability**: Do not perform denial-of-service attacks or automated scans that degrade service. 4. **Follow disclosure policy**: Do not publicly disclose vulnerabilities until we have confirmed and patched the issue. 5. **Act in good faith**: Make every effort to avoid privacy violations, destruction of data, and interruption or degradation of services. +6. **Check the Known Non-Issues list**: Reports matching an item in the "Known Non-Issues" section above are not eligible and will be closed. Reports that do not meet these guidelines may not be eligible for a reward. +## Proof of Reproduction + +Reports must demonstrate a **working, reproducible exploit with real impact** — not a theoretical or static-source-review finding. Please include: + +* Exact steps to reproduce, the relevant request/response or code path, and the commit or version you tested. +* The **observed** result versus the **expected** result. +* For client-side, UI, or authentication-flow bugs: a short screen recording (≤ 2 minutes) showing the exploit working end-to-end on a real Puter instance. +* For server-side bugs: a runnable proof-of-concept. + +Reports based solely on reading the source ("source review only, not tested") or unverified AI/LLM-generated reports are the **lowest triage priority and are generally not eligible**. If you used an AI tool to help find an issue, you must personally verify that it actually reproduces before submitting. + +Please submit **one issue per report**. Bundled "audit packs" of many speculative findings will be declined; send each confirmed issue separately. + ## Reporting Process To report a vulnerability, email us at: **[security@puter.com](mailto:security@puter.com)**. diff --git a/src/backend/controllers/auth/AuthController.ts b/src/backend/controllers/auth/AuthController.ts index 361f56d4f..1458f7ae3 100644 --- a/src/backend/controllers/auth/AuthController.ts +++ b/src/backend/controllers/auth/AuthController.ts @@ -2789,7 +2789,6 @@ export class AuthController extends PuterController { router.post( '/auth/migrate-token', { - subdomain: 'api', rateLimit: { scope: 'migrate-token', limit: 20, diff --git a/src/puter-js/src/index.js b/src/puter-js/src/index.js index 28fb2dd84..a4009b4ad 100644 --- a/src/puter-js/src/index.js +++ b/src/puter-js/src/index.js @@ -26,61 +26,61 @@ import Peer from './modules/Peer.js'; import { registerComponents } from './ui/registerComponents.js'; class SimpleLogger { - constructor (fields = {}) { + constructor(fields = {}) { this.fieldsObj = fields; this.enabled = new Set(); } - on (category) { + on(category) { this.enabled.add(category); } - fields (extra = {}) { + fields(extra = {}) { return new SimpleLogger({ ...this.fieldsObj, ...extra }); } - info (...args) { + info(...args) { console.log(...this._prefix(), ...args); } - warn (...args) { + warn(...args) { console.warn(...this._prefix(), ...args); } - error (...args) { + error(...args) { console.error(...this._prefix(), ...args); } - debug (...args) { + debug(...args) { console.debug(...this._prefix(), ...args); } - _prefix () { + _prefix() { const entries = Object.entries(this.fieldsObj); - if ( ! entries.length ) return []; - return [`[${ entries.map(([k, v]) => `${k}=${v}`).join(' ')}]`]; + if (!entries.length) return []; + return [`[${entries.map(([k, v]) => `${k}=${v}`).join(' ')}]`]; } } class Lock { - constructor () { + constructor() { this.locked = false; this.queue = []; } - async acquire () { - if ( ! this.locked ) { + async acquire() { + if (!this.locked) { this.locked = true; return; } - await new Promise(resolve => this.queue.push(resolve)); + await new Promise((resolve) => this.queue.push(resolve)); this.locked = true; } - release () { + release() { const next = this.queue.shift(); - if ( next ) { + if (next) { next(); return; } @@ -100,7 +100,7 @@ const PROD_ORIGIN = 'https://puter.com'; const STORAGE_KEY_V1 = 'puter.auth.token'; const STORAGE_KEY_V2 = 'puter.auth.token.v2'; -const puterInit = (function () { +const puterInit = function () { 'use strict'; class Puter { @@ -113,17 +113,25 @@ const puterInit = (function () { #defaultAPIOrigin = 'https://api.puter.com'; #defaultGUIOrigin = 'https://puter.com'; - get defaultAPIOrigin () { - return globalThis.PUTER_API_ORIGIN || globalThis.PUTER_API_ORIGIN_ENV || this.#defaultAPIOrigin; + get defaultAPIOrigin() { + return ( + globalThis.PUTER_API_ORIGIN || + globalThis.PUTER_API_ORIGIN_ENV || + this.#defaultAPIOrigin + ); } - set defaultAPIOrigin (v) { + set defaultAPIOrigin(v) { this.#defaultAPIOrigin = v; } - get defaultGUIOrigin () { - return globalThis.PUTER_ORIGIN || globalThis.PUTER_ORIGIN_ENV || this.#defaultGUIOrigin; + get defaultGUIOrigin() { + return ( + globalThis.PUTER_ORIGIN || + globalThis.PUTER_ORIGIN_ENV || + this.#defaultGUIOrigin + ); } - set defaultGUIOrigin (v) { + set defaultGUIOrigin(v) { this.#defaultGUIOrigin = v; } @@ -202,7 +210,7 @@ const puterInit = (function () { }; normalizeAuthTokenCandidate = function (tokenCandidate) { - if ( typeof tokenCandidate !== 'string' ) return null; + if (typeof tokenCandidate !== 'string') return null; const trimmedTokenCandidate = tokenCandidate.trim(); if ( !trimmedTokenCandidate || @@ -215,30 +223,34 @@ const puterInit = (function () { }; decodeJwtPayload = function (tokenCandidate) { - if ( typeof tokenCandidate !== 'string' ) return null; + if (typeof tokenCandidate !== 'string') return null; const tokenParts = tokenCandidate.split('.'); - if ( tokenParts.length < 2 ) return null; + if (tokenParts.length < 2) return null; let payloadPart = tokenParts[1]; payloadPart = payloadPart.replace(/-/g, '+').replace(/_/g, '/'); const missingPaddingLength = payloadPart.length % 4; - if ( missingPaddingLength ) { + if (missingPaddingLength) { payloadPart += '='.repeat(4 - missingPaddingLength); } try { let decodedPayloadText; - if ( typeof globalThis.atob === 'function' ) { + if (typeof globalThis.atob === 'function') { decodedPayloadText = decodeURIComponent( - Array.prototype.map.call( - globalThis.atob(payloadPart), - character => `%${`00${character.charCodeAt(0).toString(16)}`.slice(-2)}`, - ).join(''), + Array.prototype.map + .call( + globalThis.atob(payloadPart), + (character) => + `%${`00${character.charCodeAt(0).toString(16)}`.slice(-2)}`, + ) + .join(''), ); - } else if ( typeof globalThis.Buffer !== 'undefined' ) { - decodedPayloadText = globalThis.Buffer - .from(payloadPart, 'base64') - .toString('utf8'); + } else if (typeof globalThis.Buffer !== 'undefined') { + decodedPayloadText = globalThis.Buffer.from( + payloadPart, + 'base64', + ).toString('utf8'); } else { return null; } @@ -252,56 +264,59 @@ const puterInit = (function () { }; normalizeStringCandidate = function (valueCandidate) { - if ( typeof valueCandidate !== 'string' ) return null; + if (typeof valueCandidate !== 'string') return null; const trimmedValueCandidate = valueCandidate.trim(); return trimmedValueCandidate || null; }; decodeCompressedAppID = function (compressedAppIDCandidate) { - const normalizedCompressedAppID = this.normalizeStringCandidate(compressedAppIDCandidate); - if ( ! normalizedCompressedAppID ) return null; + const normalizedCompressedAppID = this.normalizeStringCandidate( + compressedAppIDCandidate, + ); + if (!normalizedCompressedAppID) return null; // TokenService may already provide an expanded UID value. - if ( normalizedCompressedAppID.includes('-') ) { + if (normalizedCompressedAppID.includes('-')) { return normalizedCompressedAppID; } try { let decodedBytes; - if ( typeof globalThis.Buffer !== 'undefined' ) { - decodedBytes = globalThis.Buffer.from(normalizedCompressedAppID, 'base64'); - } else if ( typeof globalThis.atob === 'function' ) { - const decodedBinary = globalThis.atob(normalizedCompressedAppID); - decodedBytes = Uint8Array.from( - decodedBinary, - character => character.charCodeAt(0), + if (typeof globalThis.Buffer !== 'undefined') { + decodedBytes = globalThis.Buffer.from( + normalizedCompressedAppID, + 'base64', + ); + } else if (typeof globalThis.atob === 'function') { + const decodedBinary = globalThis.atob( + normalizedCompressedAppID, + ); + decodedBytes = Uint8Array.from(decodedBinary, (character) => + character.charCodeAt(0), ); } else { return null; } - if ( !decodedBytes || decodedBytes.length !== 16 ) return null; + if (!decodedBytes || decodedBytes.length !== 16) return null; - const decodedHex = ( + const decodedHex = typeof globalThis.Buffer !== 'undefined' && typeof globalThis.Buffer.isBuffer === 'function' && globalThis.Buffer.isBuffer(decodedBytes) - ) - ? decodedBytes.toString('hex') - : Array.from(decodedBytes) - .map(byte => byte.toString(16).padStart(2, '0')) - .join(''); - if ( decodedHex.length !== 32 ) return null; + ? decodedBytes.toString('hex') + : Array.from(decodedBytes) + .map((byte) => byte.toString(16).padStart(2, '0')) + .join(''); + if (decodedHex.length !== 32) return null; - return `app-${ - [ - decodedHex.slice(0, 8), - decodedHex.slice(8, 12), - decodedHex.slice(12, 16), - decodedHex.slice(16, 20), - decodedHex.slice(20), - ].join('-') - }`; + return `app-${[ + decodedHex.slice(0, 8), + decodedHex.slice(8, 12), + decodedHex.slice(12, 16), + decodedHex.slice(16, 20), + decodedHex.slice(20), + ].join('-')}`; } catch { return null; } @@ -309,10 +324,12 @@ const puterInit = (function () { getAppIDFromAuthToken = function (tokenCandidate) { const payload = this.decodeJwtPayload(tokenCandidate); - if ( ! payload ) return null; + if (!payload) return null; - const uncompressedAppUid = this.normalizeStringCandidate(payload.app_uid); - if ( uncompressedAppUid ) return uncompressedAppUid; + const uncompressedAppUid = this.normalizeStringCandidate( + payload.app_uid, + ); + if (uncompressedAppUid) return uncompressedAppUid; // `auth` JWT scope may compress `app_uid` to `au`. return this.decodeCompressedAppID(payload.au); @@ -321,8 +338,7 @@ const puterInit = (function () { // -------------------------------------------- // Constructor // -------------------------------------------- - constructor () { - + constructor() { // Initialize the cache using kv.js this._cache = new kvjs({ dbName: 'puter_cache' }); this._opscache = new kvjs(); @@ -334,42 +350,39 @@ const puterInit = (function () { let URLParams = new URLSearchParams(globalThis.location?.search); // Figure out the environment in which the SDK is running - if ( URLParams.has('puter.app_instance_id') ) { + if (URLParams.has('puter.app_instance_id')) { this.env = 'app'; - } else if ( globalThis.puter_gui_enabled === true ) - { + } else if (globalThis.puter_gui_enabled === true) { this.env = 'gui'; - } - else if ( globalThis.WorkerGlobalScope ) { - if ( globalThis.ServiceWorkerGlobalScope ) { + } else if (globalThis.WorkerGlobalScope) { + if (globalThis.ServiceWorkerGlobalScope) { this.env = 'service-worker'; - if ( ! globalThis.XMLHttpRequest ) { + if (!globalThis.XMLHttpRequest) { globalThis.XMLHttpRequest = xhrshim; } - if ( ! globalThis.location ) { + if (!globalThis.location) { globalThis.location = new URL('https://puter.site/'); } // XHRShimGlobalize here } else { this.env = 'web-worker'; } - if ( ! globalThis.localStorage ) { + if (!globalThis.localStorage) { globalThis.localStorage = localStorageMemory; } - } else if ( globalThis.process ) { + } else if (globalThis.process) { this.env = 'nodejs'; - if ( ! globalThis.localStorage ) { + if (!globalThis.localStorage) { globalThis.localStorage = localStorageMemory; } - if ( ! globalThis.XMLHttpRequest ) { + if (!globalThis.XMLHttpRequest) { globalThis.XMLHttpRequest = xhrshim; } - if ( ! globalThis.location ) { + if (!globalThis.location) { globalThis.location = new URL('https://nodejs.puter.site/'); } - if ( ! globalThis.addEventListener ) { - globalThis.addEventListener = () => { - }; // API Stub + if (!globalThis.addEventListener) { + globalThis.addEventListener = () => {}; // API Stub } } else { this.env = 'web'; @@ -378,7 +391,7 @@ const puterInit = (function () { // There are some specific situations where puter is definitely loaded in GUI mode // we're going to check for those situations here so that we don't break anything unintentionally // if navigator URL's hostname is 'puter.com' - if ( this.env !== 'gui' ) { + if (this.env !== 'gui') { // Retrieve the hostname from the URL: Remove the trailing dot if it exists. This is to handle the case where the URL is, for example, `https://puter.com.` (note the trailing dot). // This is necessary because the trailing dot can cause the hostname to not match the expected value. let hostname = location.hostname.replace(/\.$/, ''); @@ -390,14 +403,16 @@ const puterInit = (function () { const gui_hostname = url.hostname; // If the hostname matches the GUI hostname, then the SDK is running in the GUI environment - if ( hostname === gui_hostname ) { + if (hostname === gui_hostname) { this.env = 'gui'; } } // Get the 'args' from the URL. This is used to pass arguments to the app. - if ( URLParams.has('puter.args') ) { - this.args = JSON.parse(decodeURIComponent(URLParams.get('puter.args'))); + if (URLParams.has('puter.args')) { + this.args = JSON.parse( + decodeURIComponent(URLParams.get('puter.args')), + ); } else { this.args = {}; } @@ -405,30 +420,36 @@ const puterInit = (function () { // Try to extract appInstanceID from the URL. appInstanceID is included in every messaage // sent to the host environment. This is used to help host environment identify the app // instance that sent the message and communicate back to it. - if ( URLParams.has('puter.app_instance_id') ) { - this.appInstanceID = decodeURIComponent(URLParams.get('puter.app_instance_id')); + if (URLParams.has('puter.app_instance_id')) { + this.appInstanceID = decodeURIComponent( + URLParams.get('puter.app_instance_id'), + ); } // Try to extract parentInstanceID from the URL. If another app launched this app instance, parentInstanceID // holds its instance ID, and is used to communicate with that parent app. - if ( URLParams.has('puter.parent_instance_id') ) { - this.parentInstanceID = decodeURIComponent(URLParams.get('puter.parent_instance_id')); + if (URLParams.has('puter.parent_instance_id')) { + this.parentInstanceID = decodeURIComponent( + URLParams.get('puter.parent_instance_id'), + ); } // Try to extract `puter.app.id` from the URL. `puter.app.id` is the unique ID of the app. // App ID is useful for identifying the app when communicating with the Puter API, among other things. - if ( URLParams.has('puter.app.id') ) { + if (URLParams.has('puter.app.id')) { this.appID = decodeURIComponent(URLParams.get('puter.app.id')); } // Extract app name (added later) - if ( URLParams.has('puter.app.name') ) { - this.appName = decodeURIComponent(URLParams.get('puter.app.name')); + if (URLParams.has('puter.app.name')) { + this.appName = decodeURIComponent( + URLParams.get('puter.app.name'), + ); } // Construct this App's AppData path based on the appID. AppData path is used to store files that are specific to this app. // The default AppData path is `~/AppData/`. - if ( this.appID ) { + if (this.appID) { this.appDataPath = `~/AppData/${this.appID}`; } @@ -438,10 +459,12 @@ const puterInit = (function () { // is constructed as `https://api.`. // This should only be done when the SDK is running in 'app' mode. this.APIOrigin = this.defaultAPIOrigin; - if ( URLParams.has('puter.api_origin') && this.env === 'app' ) { - this.APIOrigin = decodeURIComponent(URLParams.get('puter.api_origin')); - } else if ( URLParams.has('puter.domain') && this.env === 'app' ) { - this.APIOrigin = `https://api.${ URLParams.get('puter.domain')}`; + if (URLParams.has('puter.api_origin') && this.env === 'app') { + this.APIOrigin = decodeURIComponent( + URLParams.get('puter.api_origin'), + ); + } else if (URLParams.has('puter.domain') && this.env === 'app') { + this.APIOrigin = `https://api.${URLParams.get('puter.domain')}`; } // === START :: Logger === @@ -458,21 +481,22 @@ const puterInit = (function () { // === Start :: Modules === // // The SDK is running in the Puter GUI (i.e. 'gui') - if ( this.env === 'gui' ) { + if (this.env === 'gui') { this.authToken = window.auth_token; // initialize submodules this.initSubmodules(); } // Loaded in an iframe in the Puter GUI (i.e. 'app') // When SDK is loaded in App mode the initiation process should start when the DOM is ready - else if ( this.env === 'app' ) { + else if (this.env === 'app') { const bootstrapAuthToken = this.normalizeAuthTokenCandidate( - URLParams.get('puter.auth.token') ?? URLParams.get('auth_token'), + URLParams.get('puter.auth.token') ?? + URLParams.get('auth_token'), ); try { let selectedAuthToken = bootstrapAuthToken; let needsSilentMigration = false; - if ( bootstrapAuthToken ) { + if (bootstrapAuthToken) { // URL-param tokens may still be v1 (host apps that // haven't rebuilt yet). Set immediately so submodules // can run, then attempt silent migration in the @@ -485,36 +509,38 @@ const puterInit = (function () { const v2 = this.normalizeAuthTokenCandidate( localStorage.getItem(STORAGE_KEY_V2), ); - if ( v2 ) { + if (v2) { this.setAuthToken(v2); selectedAuthToken = v2; } else { const v1 = this.normalizeAuthTokenCandidate( localStorage.getItem(STORAGE_KEY_V1), ); - if ( v1 ) { + if (v1) { this.setAuthToken(v1); selectedAuthToken = v1; needsSilentMigration = true; } } } - if ( needsSilentMigration && selectedAuthToken ) { + if (needsSilentMigration && selectedAuthToken) { // Fire-and-forget. On success, setAuthToken inside the // migration helper updates submodules with the v2 // token. On failure the next 401 reauth_required // triggers the interactive flow. this._silentMigrateV1Token(selectedAuthToken); } - const tokenAppID = this.getAppIDFromAuthToken(selectedAuthToken); - if ( !tokenAppID && !this.appID ) { + const tokenAppID = + this.getAppIDFromAuthToken(selectedAuthToken); + if (!tokenAppID && !this.appID) { // if appID is already set in localStorage, then we don't need to show the dialog - const storedAppID = localStorage.getItem('puter.app.id'); - if ( storedAppID ) { + const storedAppID = + localStorage.getItem('puter.app.id'); + if (storedAppID) { this.setAppID(storedAppID); } } - } catch ( error ) { + } catch (error) { // Handle the error here console.error('Error accessing localStorage:', error); } @@ -523,7 +549,7 @@ const puterInit = (function () { // SDK was loaded in a 3rd-party website. // When SDK is loaded in GUI the initiation process should start when the DOM is ready. This is because // the SDK needs to show a dialog to the user to ask for permission to access their Puter account. - else if ( this.env === 'web' ) { + else if (this.env === 'web') { // initialize submodules this.initSubmodules(); try { @@ -532,13 +558,13 @@ const puterInit = (function () { const v2 = this.normalizeAuthTokenCandidate( localStorage.getItem(STORAGE_KEY_V2), ); - if ( v2 ) { + if (v2) { this.setAuthToken(v2); } else { const v1 = this.normalizeAuthTokenCandidate( localStorage.getItem(STORAGE_KEY_V1), ); - if ( v1 ) { + if (v1) { this.setAuthToken(v1); // For kind='access_token' the backend will swap // this for a v2 token. For kind='web' it'll @@ -548,10 +574,10 @@ const puterInit = (function () { } } // if appID is already set in localStorage, then we don't need to show the dialog - if ( !this.appID && localStorage.getItem('puter.app.id') ) { + if (!this.appID && localStorage.getItem('puter.app.id')) { this.setAppID(localStorage.getItem('puter.app.id')); } - } catch ( error ) { + } catch (error) { // Handle the error here console.error('Error accessing localStorage:', error); } @@ -563,7 +589,11 @@ const puterInit = (function () { // Puter.js cannot function. Warn the developer immediately on // load rather than waiting for an action that triggers auth. this.warnUnsupportedProtocol(); - } else if ( this.env === 'web-worker' || this.env === 'service-worker' || this.env === 'nodejs' ) { + } else if ( + this.env === 'web-worker' || + this.env === 'service-worker' || + this.env === 'nodejs' + ) { this.initSubmodules(); } @@ -576,9 +606,12 @@ const puterInit = (function () { }]`; logger = logger.fields({ prefix }); this.logger = logger; - } catch ( error ) { - if ( this.debugMode ) { - console.error('Failed to initialize prefix logger', error); + } catch (error) { + if (this.debugMode) { + console.error( + 'Failed to initialize prefix logger', + error, + ); } } })(); @@ -592,14 +625,19 @@ const puterInit = (function () { this.net = { generateWispV1URL: async () => { - const { token: wispToken, server: wispServer } = (await (await fetch(`${this.APIOrigin }/wisp/relay-token/create`, { - method: 'POST', - headers: { - Authorization: `Bearer ${this.authToken}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({}), - })).json()); + const { token: wispToken, server: wispServer } = await ( + await fetch( + `${this.APIOrigin}/wisp/relay-token/create`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${this.authToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({}), + }, + ) + ).json(); return `${wispServer}/${wispToken}/`; }, Socket: PSocket, @@ -620,24 +658,24 @@ const puterInit = (function () { * Makes a request to `/rao`. This method aquires a lock to prevent * multiple requests, and is effectively idempotent. */ - async request_rao_ () { + async request_rao_() { await this.p_can_request_rao_; - if ( this.env === 'gui' ) { + if (this.env === 'gui') { return; } // setAuthToken is called more than once when auth completes, which // causes multiple requests to /rao. This lock prevents that. await this.lock_rao_.acquire(); - if ( this.rao_requested_ ) { + if (this.rao_requested_) { this.lock_rao_.release(); return; } let had_error = false; try { - const resp = await fetch(`${this.APIOrigin }/rao`, { + const resp = await fetch(`${this.APIOrigin}/rao`, { method: 'POST', headers: { Authorization: `Bearer ${this.authToken}`, @@ -645,29 +683,29 @@ const puterInit = (function () { }, }); return await resp.json(); - } catch ( e ) { + } catch (e) { had_error = true; console.error(e); } finally { this.lock_rao_.release(); } - if ( ! had_error ) { + if (!had_error) { this.rao_requested_ = true; } } - registerModule (name, cls, parameters = {}) { + registerModule(name, cls, parameters = {}) { const instance = new cls(this, parameters); instance.puter = this; this.modules_.push(name); this[name] = instance; - if ( instance._init ) instance._init({ puter: this }); + if (instance._init) instance._init({ puter: this }); } - updateSubmodules () { + updateSubmodules() { // Update submodules with new auth token and API origin - for ( const name of this.modules_ ) { - if ( ! this[name] ) continue; + for (const name of this.modules_) { + if (!this[name]) continue; this[name]?.setAuthToken?.(this.authToken); this[name]?.setAPIOrigin?.(this.APIOrigin); } @@ -677,7 +715,7 @@ const puterInit = (function () { // save to localStorage try { localStorage.setItem('puter.app.id', appID); - } catch ( error ) { + } catch (error) { // Handle the error here console.error('Error accessing localStorage:', error); } @@ -686,20 +724,24 @@ const puterInit = (function () { }; setAuthToken = function (authToken) { - const normalizedAuthToken = this.normalizeAuthTokenCandidate(authToken); + const normalizedAuthToken = + this.normalizeAuthTokenCandidate(authToken); this.authToken = normalizedAuthToken; // Keep app identity consistent with token claims whenever available. const tokenAppID = this.getAppIDFromAuthToken(normalizedAuthToken); - if ( tokenAppID ) { + if (tokenAppID) { this.setAppID(tokenAppID); } // If the SDK is running on a 3rd-party site or an app, then save the authToken in localStorage - if ( this.env === 'web' || this.env === 'app' ) { + if (this.env === 'web' || this.env === 'app') { try { - if ( normalizedAuthToken ) { - localStorage.setItem(STORAGE_KEY_V2, normalizedAuthToken); + if (normalizedAuthToken) { + localStorage.setItem( + STORAGE_KEY_V2, + normalizedAuthToken, + ); } else { localStorage.removeItem(STORAGE_KEY_V2); } @@ -707,13 +749,13 @@ const puterInit = (function () { // have a v2 token, the v1 one must not linger or it // would be picked up by older code paths. localStorage.removeItem(STORAGE_KEY_V1); - } catch ( error ) { + } catch (error) { // Handle the error here console.error('Error accessing localStorage:', error); } } // initialize loop for updating caches for major directories - if ( this.env === 'gui' ) { + if (this.env === 'gui') { // check and update gui fs cache regularly setInterval(puter.checkAndUpdateGUIFScache, 10000); } @@ -736,35 +778,40 @@ const puterInit = (function () { }; runWhenPuterHappensCallbacks = function () { - if ( this.env !== 'gui' ) return; - if ( ! globalThis.when_puter_happens ) return; + if (this.env !== 'gui') return; + if (!globalThis.when_puter_happens) return; const callbacks = Array.isArray(globalThis.when_puter_happens) ? globalThis.when_puter_happens : [globalThis.when_puter_happens]; - for ( const fn of callbacks ) { + for (const fn of callbacks) { try { fn({ puter: this }); - } catch ( error ) { - if ( this.debugMode ) { - console.error('when_puter_happens callback failed', error); + } catch (error) { + if (this.debugMode) { + console.error( + 'when_puter_happens callback failed', + error, + ); } } } }; resetAuthToken = function () { - if ( this.env === 'worker' || this.env === 'service-worker' ) { - throw new Error('Sign out is not permitted from WebWorkers or ServiceWorkers'); + if (this.env === 'worker' || this.env === 'service-worker') { + throw new Error( + 'Sign out is not permitted from WebWorkers or ServiceWorkers', + ); } this.authToken = null; // If the SDK is running on a 3rd-party site or an app, then save the authToken in localStorage - if ( this.env === 'web' || this.env === 'app' ) { + if (this.env === 'web' || this.env === 'app') { try { localStorage.removeItem(STORAGE_KEY_V2); localStorage.removeItem(STORAGE_KEY_V1); - } catch ( error ) { + } catch (error) { // Handle the error here console.error('Error accessing localStorage:', error); } @@ -793,7 +840,7 @@ const puterInit = (function () { */ triggerReauth = async function (signal = {}) { const { reason, auth_id } = signal; - if ( this._reauthInflight ) return this._reauthInflight; + if (this._reauthInflight) return this._reauthInflight; // Emit before clearing so listeners can read state if needed. this._emitReauthEvent({ reason, auth_id }); @@ -802,18 +849,18 @@ const puterInit = (function () { // doesn't leave a poisoned value in localStorage. The new token // (if reauth succeeds) is written by setAuthToken downstream. this.authToken = null; - if ( this.env === 'web' || this.env === 'app' ) { + if (this.env === 'web' || this.env === 'app') { try { localStorage.removeItem(STORAGE_KEY_V2); localStorage.removeItem(STORAGE_KEY_V1); - } catch ( e ) { + } catch (e) { console.error('Error accessing localStorage:', e); } } this.updateSubmodules(); this._reauthInflight = (async () => { - if ( this.env === 'gui' ) { + if (this.env === 'gui') { // GUI handles its own modal at the layer above puter-js. return; } @@ -828,14 +875,14 @@ const puterInit = (function () { err.auth_id = auth_id; throw err; } - if ( this.env === 'web' ) { + if (this.env === 'web') { // Drives the puter.com login popup. On success, the // postMessage handler at the bottom of this file calls // setAuthToken() and updates this.authToken. await this.ui.authenticateWithPuter({ auth_id, reason }); return; } - if ( this.env === 'app' ) { + if (this.env === 'app') { // We're inside an iframe in the GUI. Ask the parent to // surface the reauth modal; once the user signs in there, // the existing `puter.token` postMessage delivers the @@ -846,13 +893,16 @@ const puterInit = (function () { // to any embedding parent (including a malicious one), // and the message body is only meaningful to the GUI. try { - globalThis.parent?.postMessage?.({ - msg: 'reauth_required', - appInstanceID: this.appInstanceID, - reason, - auth_id, - }, this.defaultGUIOrigin); - } catch ( e ) { + globalThis.parent?.postMessage?.( + { + msg: 'reauth_required', + appInstanceID: this.appInstanceID, + reason, + auth_id, + }, + this.defaultGUIOrigin, + ); + } catch (e) { // Best-effort: if postMessage isn't available // (sandboxed iframe), fall through to error. } @@ -864,18 +914,28 @@ const puterInit = (function () { await new Promise((resolve, reject) => { const expectedSource = globalThis.parent; const onToken = (event) => { - if ( event.origin !== this.defaultGUIOrigin ) return; - if ( expectedSource && event.source !== expectedSource ) return; - if ( event.data?.msg !== 'puter.token' ) return; + if (event.origin !== this.defaultGUIOrigin) return; + if ( + expectedSource && + event.source !== expectedSource + ) + return; + if (event.data?.msg !== 'puter.token') return; globalThis.removeEventListener('message', onToken); resolve(); }; globalThis.addEventListener?.('message', onToken); // Give the user a generous window to re-auth. - setTimeout(() => { - globalThis.removeEventListener?.('message', onToken); - reject(new Error('reauth_timeout')); - }, 5 * 60 * 1000); + setTimeout( + () => { + globalThis.removeEventListener?.( + 'message', + onToken, + ); + reject(new Error('reauth_timeout')); + }, + 5 * 60 * 1000, + ); }); } })(); @@ -889,13 +949,18 @@ const puterInit = (function () { _emitReauthEvent = function ({ reason, auth_id }) { try { - const handlers = this.eventHandlers?.['puter.auth.reauth_required']; - if ( Array.isArray(handlers) ) { - for ( const h of handlers ) { - try { h({ reason, auth_id }); } catch ( e ) { /* swallow per-handler errors */ } + const handlers = + this.eventHandlers?.['puter.auth.reauth_required']; + if (Array.isArray(handlers)) { + for (const h of handlers) { + try { + h({ reason, auth_id }); + } catch (e) { + /* swallow per-handler errors */ + } } } - } catch ( e ) { + } catch (e) { // Never let event delivery break the reauth flow itself. } }; @@ -905,16 +970,17 @@ const puterInit = (function () { * to `puter.auth.reauth_required`. */ on = function (eventName, handler) { - if ( ! this.eventHandlers[eventName] ) this.eventHandlers[eventName] = []; + if (!this.eventHandlers[eventName]) + this.eventHandlers[eventName] = []; this.eventHandlers[eventName].push(handler); return () => this.off(eventName, handler); }; off = function (eventName, handler) { const handlers = this.eventHandlers[eventName]; - if ( ! handlers ) return; + if (!handlers) return; const idx = handlers.indexOf(handler); - if ( idx >= 0 ) handlers.splice(idx, 1); + if (idx >= 0) handlers.splice(idx, 1); }; /** @@ -929,18 +995,21 @@ const puterInit = (function () { * `reauth_required` and route through triggerReauth. */ _silentMigrateV1Token = async function (v1Token) { - if ( ! v1Token ) return false; + if (!v1Token) return false; try { - const resp = await fetch(`${this.APIOrigin}/auth/migrate-token`, { - method: 'POST', - headers: { - Authorization: `Bearer ${v1Token}`, - 'Content-Type': 'application/json', + const resp = await fetch( + `${this.defaultGUIOrigin}/auth/migrate-token`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${v1Token}`, + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify({}), }, - credentials: 'include', - body: JSON.stringify({}), - }); - if ( ! resp.ok ) { + ); + if (!resp.ok) { // 409 = backend refuses silent migration for this kind // (web sessions). Any caller will then 401 reauth_required // and the interactive flow takes over. @@ -948,28 +1017,33 @@ const puterInit = (function () { } const data = await resp.json().catch(() => null); const token = data?.token; - if ( typeof token === 'string' && token.length > 0 ) { + if (typeof token === 'string' && token.length > 0) { this.setAuthToken(token); return true; } return false; - } catch ( e ) { + } catch (e) { // Network errors etc. — fall back to reauth on next 401. return false; } }; exit = function (statusCode = 0) { - if ( statusCode && (typeof statusCode !== 'number') ) { - console.warn('puter.exit() requires status code to be a number. Treating it as 1'); + if (statusCode && typeof statusCode !== 'number') { + console.warn( + 'puter.exit() requires status code to be a number. Treating it as 1', + ); statusCode = 1; } - globalThis.parent.postMessage({ - msg: 'exit', - appInstanceID: this.appInstanceID, - statusCode, - }, '*'); + globalThis.parent.postMessage( + { + msg: 'exit', + appInstanceID: this.appInstanceID, + statusCode, + }, + '*', + ); }; /** @@ -982,29 +1056,160 @@ const puterInit = (function () { * */ randName = function (separateWith = '-') { - const first_adj = ['helpful', 'sensible', 'loyal', 'honest', 'clever', 'capable', 'calm', 'smart', 'genius', 'bright', 'charming', 'creative', 'diligent', 'elegant', 'fancy', - 'colorful', 'avid', 'active', 'gentle', 'happy', 'intelligent', 'jolly', 'kind', 'lively', 'merry', 'nice', 'optimistic', 'polite', - 'quiet', 'relaxed', 'silly', 'victorious', 'witty', 'young', 'zealous', 'strong', 'brave', 'agile', 'bold']; + const first_adj = [ + 'helpful', + 'sensible', + 'loyal', + 'honest', + 'clever', + 'capable', + 'calm', + 'smart', + 'genius', + 'bright', + 'charming', + 'creative', + 'diligent', + 'elegant', + 'fancy', + 'colorful', + 'avid', + 'active', + 'gentle', + 'happy', + 'intelligent', + 'jolly', + 'kind', + 'lively', + 'merry', + 'nice', + 'optimistic', + 'polite', + 'quiet', + 'relaxed', + 'silly', + 'victorious', + 'witty', + 'young', + 'zealous', + 'strong', + 'brave', + 'agile', + 'bold', + ]; - const nouns = ['street', 'roof', 'floor', 'tv', 'idea', 'morning', 'game', 'wheel', 'shoe', 'bag', 'clock', 'pencil', 'pen', - 'magnet', 'chair', 'table', 'house', 'dog', 'room', 'book', 'car', 'cat', 'tree', - 'flower', 'bird', 'fish', 'sun', 'moon', 'star', 'cloud', 'rain', 'snow', 'wind', 'mountain', - 'river', 'lake', 'sea', 'ocean', 'island', 'bridge', 'road', 'train', 'plane', 'ship', 'bicycle', - 'horse', 'elephant', 'lion', 'tiger', 'bear', 'zebra', 'giraffe', 'monkey', 'snake', 'rabbit', 'duck', - 'goose', 'penguin', 'frog', 'crab', 'shrimp', 'whale', 'octopus', 'spider', 'ant', 'bee', 'butterfly', 'dragonfly', - 'ladybug', 'snail', 'camel', 'kangaroo', 'koala', 'panda', 'piglet', 'sheep', 'wolf', 'fox', 'deer', 'mouse', 'seal', - 'chicken', 'cow', 'dinosaur', 'puppy', 'kitten', 'circle', 'square', 'garden', 'otter', 'bunny', 'meerkat', 'harp']; + const nouns = [ + 'street', + 'roof', + 'floor', + 'tv', + 'idea', + 'morning', + 'game', + 'wheel', + 'shoe', + 'bag', + 'clock', + 'pencil', + 'pen', + 'magnet', + 'chair', + 'table', + 'house', + 'dog', + 'room', + 'book', + 'car', + 'cat', + 'tree', + 'flower', + 'bird', + 'fish', + 'sun', + 'moon', + 'star', + 'cloud', + 'rain', + 'snow', + 'wind', + 'mountain', + 'river', + 'lake', + 'sea', + 'ocean', + 'island', + 'bridge', + 'road', + 'train', + 'plane', + 'ship', + 'bicycle', + 'horse', + 'elephant', + 'lion', + 'tiger', + 'bear', + 'zebra', + 'giraffe', + 'monkey', + 'snake', + 'rabbit', + 'duck', + 'goose', + 'penguin', + 'frog', + 'crab', + 'shrimp', + 'whale', + 'octopus', + 'spider', + 'ant', + 'bee', + 'butterfly', + 'dragonfly', + 'ladybug', + 'snail', + 'camel', + 'kangaroo', + 'koala', + 'panda', + 'piglet', + 'sheep', + 'wolf', + 'fox', + 'deer', + 'mouse', + 'seal', + 'chicken', + 'cow', + 'dinosaur', + 'puppy', + 'kitten', + 'circle', + 'square', + 'garden', + 'otter', + 'bunny', + 'meerkat', + 'harp', + ]; // return a random combination of first_adj + noun + number (between 0 and 9999) // e.g. clever-idea-123 - return first_adj[Math.floor(Math.random() * first_adj.length)] + separateWith + nouns[Math.floor(Math.random() * nouns.length)] + separateWith + Math.floor(Math.random() * 10000); + return ( + first_adj[Math.floor(Math.random() * first_adj.length)] + + separateWith + + nouns[Math.floor(Math.random() * nouns.length)] + + separateWith + + Math.floor(Math.random() * 10000) + ); }; getUser = function (...args) { let options; // If first argument is an object, it's the options - if ( typeof args[0] === 'object' && args[0] !== null ) { + if (typeof args[0] === 'object' && args[0] !== null) { options = args[0]; } else { // Otherwise, we assume separate arguments are provided @@ -1015,9 +1220,20 @@ const puterInit = (function () { } return new Promise((resolve, reject) => { - const xhr = utils.initXhr('/whoami', this.APIOrigin, this.authToken, 'get'); + const xhr = utils.initXhr( + '/whoami', + this.APIOrigin, + this.authToken, + 'get', + ); // set up event handlers for load and error events - utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject); + utils.setupXhrEventHandlers( + xhr, + options.success, + options.error, + resolve, + reject, + ); xhr.send(); }); @@ -1026,15 +1242,24 @@ const puterInit = (function () { print = function (...args) { // Check if the last argument is an options object with escapeHTML or code property let options = {}; - if ( args.length > 0 && typeof args[args.length - 1] === 'object' && args[args.length - 1] !== null && - ('escapeHTML' in args[args.length - 1] || 'code' in args[args.length - 1]) ) { + if ( + args.length > 0 && + typeof args[args.length - 1] === 'object' && + args[args.length - 1] !== null && + ('escapeHTML' in args[args.length - 1] || + 'code' in args[args.length - 1]) + ) { options = args.pop(); } - for ( let arg of args ) { + for (let arg of args) { // Escape HTML if the option is set to true or if code option is true - if ( (options.escapeHTML === true || options.code === true) && typeof arg === 'string' ) { - arg = arg.replace(/&/g, '&') + if ( + (options.escapeHTML === true || options.code === true) && + typeof arg === 'string' + ) { + arg = arg + .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') @@ -1042,7 +1267,7 @@ const puterInit = (function () { } // Wrap in code/pre tags if code option is true - if ( options.code === true ) { + if (options.code === true) { arg = `
${arg}
`; } @@ -1054,10 +1279,10 @@ const puterInit = (function () { * Configures API call logging settings * @param {Object} config - Configuration options for API call logging * @param {boolean} config.enabled - Enable/disable API call logging - * @param {boolean} config.enabled - Enable/disable API call logging + * @param {boolean} config.enabled - Enable/disable API call logging */ configureAPILogging = function (config = {}) { - if ( this.apiCallLogger ) { + if (this.apiCallLogger) { this.apiCallLogger.updateConfig(config); } return this; @@ -1068,7 +1293,7 @@ const puterInit = (function () { * @param {Object} config - Optional configuration to apply when enabling */ enableAPILogging = function (config = {}) { - if ( this.apiCallLogger ) { + if (this.apiCallLogger) { this.apiCallLogger.updateConfig({ ...config, enabled: true }); } return this; @@ -1078,7 +1303,7 @@ const puterInit = (function () { * Disables API call logging */ disableAPILogging = function () { - if ( this.apiCallLogger ) { + if (this.apiCallLogger) { this.apiCallLogger.disable(); } return this; @@ -1090,8 +1315,10 @@ const puterInit = (function () { */ initNetworkMonitoring = function () { // Only initialize in environments that support navigator.onLine and window events - if ( typeof globalThis.navigator === 'undefined' || - typeof globalThis.addEventListener !== 'function' ) { + if ( + typeof globalThis.navigator === 'undefined' || + typeof globalThis.addEventListener !== 'function' + ) { return; } @@ -1103,12 +1330,12 @@ const puterInit = (function () { const isOnline = navigator.onLine; // If we went from online to offline, purge the cache - if ( wasOnline && !isOnline ) { + if (wasOnline && !isOnline) { console.log('Network connection lost - purging cache'); try { this._cache.flushall(); console.log('Cache purged successfully'); - } catch ( error ) { + } catch (error) { console.error('Error purging cache:', error); } } @@ -1123,7 +1350,7 @@ const puterInit = (function () { // Also listen for visibility change as an additional indicator // (some browsers don't fire offline events reliably) - if ( typeof document !== 'undefined' ) { + if (typeof document !== 'undefined') { document.addEventListener('visibilitychange', () => { // Small delay to allow network state to update setTimeout(handleNetworkChange, 100); @@ -1137,31 +1364,33 @@ const puterInit = (function () { * @private */ printDevCTA = function () { - if ( this.quiet || globalThis.PUTER_QUIET ) return; - const isDark = globalThis.matchMedia && globalThis.matchMedia('(prefers-color-scheme: dark)').matches; + if (this.quiet || globalThis.PUTER_QUIET) return; + const isDark = + globalThis.matchMedia && + globalThis.matchMedia('(prefers-color-scheme: dark)').matches; const asciiColor = isDark ? '#7c8cff' : '#000fd8'; const headingColor = isDark ? '#cbd5f5' : 'rgb(0, 57, 137)'; const linkColor = isDark ? '#93c5fd' : '#3b82f6'; const mutedColor = isDark ? '#64748b' : '#94a3b8'; console.log( '%c' + - ' ____ _ _ _____ _____ ____ _ ____ \n' + - '| _ \\| | | |_ _| ____| _ \\ | / ___| \n' + - '| |_) | | | | | | | _| | |_) | _ | \\___ \\ \n' + - '| __/| |_| | | | | |___| _ < | |_| |___) |\n' + - '|_| \\___/ |_| |_____|_| \\_(_)___/|____/ ', - `color: ${asciiColor}; font-weight: bold; font-size: 14px; font-family: monospace;` + ' ____ _ _ _____ _____ ____ _ ____ \n' + + '| _ \\| | | |_ _| ____| _ \\ | / ___| \n' + + '| |_) | | | | | | | _| | |_) | _ | \\___ \\ \n' + + '| __/| |_| | | | | |___| _ < | |_| |___) |\n' + + '|_| \\___/ |_| |_____|_| \\_(_)___/|____/ ', + `color: ${asciiColor}; font-weight: bold; font-size: 14px; font-family: monospace;`, ); console.log( '%cSubmit this app to the Puter App Store:\n' + - '%chttps://apps.puter.com/', + '%chttps://apps.puter.com/', `color: ${headingColor}; font-size: 18px; font-weight: bold;`, - `color: ${linkColor}; font-size: 18px; font-weight: bold; text-decoration: underline;` + `color: ${linkColor}; font-size: 18px; font-weight: bold; text-decoration: underline;`, ); console.log( '%cTo disable this message: %cputer.quiet = true', `color: ${mutedColor}; font-size: 11px;`, - `color: ${mutedColor}; font-size: 11px; font-style: italic;` + `color: ${mutedColor}; font-size: 11px; font-style: italic;`, ); }; @@ -1173,20 +1402,25 @@ const puterInit = (function () { * @private */ warnUnsupportedProtocol = function () { - if ( globalThis.location?.protocol !== 'file:' ) return; - if ( this._fileProtocolWarned ) return; + if (globalThis.location?.protocol !== 'file:') return; + if (this._fileProtocolWarned) return; this._fileProtocolWarned = true; const showDialog = () => { // On file:// PuterDialog renders the "Unsupported Protocol" // warning instead of the auth consent content. - const dialog = new PuterDialog(() => {}, () => {}); + const dialog = new PuterDialog( + () => {}, + () => {}, + ); document.body.appendChild(dialog); dialog.open(); }; - if ( document.readyState === 'loading' ) { - document.addEventListener('DOMContentLoaded', showDialog, { once: true }); + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', showDialog, { + once: true, + }); } else { showDialog(); } @@ -1198,9 +1432,9 @@ const puterInit = (function () { */ checkAndUpdateGUIFScache = function () { // only run in gui environment - if ( puter.env !== 'gui' ) return; + if (puter.env !== 'gui') return; // only run if user is authenticated - if ( ! puter.whoami ) return; + if (!puter.whoami) return; let username = puter.whoami.username; @@ -1211,51 +1445,65 @@ const puterInit = (function () { let public_path = `/${username}/Public`; // item:Home - if ( ! puter._cache.get(`item:${ home_path}`) ) { - console.log(`/${username} item is not cached, refetching cache`); + if (!puter._cache.get(`item:${home_path}`)) { + console.log( + `/${username} item is not cached, refetching cache`, + ); // fetch home puter.fs.stat(home_path); } // item:Desktop - if ( ! puter._cache.get(`item:${ desktop_path}`) ) { - console.log(`/${username}/Desktop item is not cached, refetching cache`); + if (!puter._cache.get(`item:${desktop_path}`)) { + console.log( + `/${username}/Desktop item is not cached, refetching cache`, + ); // fetch desktop puter.fs.stat(desktop_path); } // item:Documents - if ( ! puter._cache.get(`item:${ documents_path}`) ) { - console.log(`/${username}/Documents item is not cached, refetching cache`); + if (!puter._cache.get(`item:${documents_path}`)) { + console.log( + `/${username}/Documents item is not cached, refetching cache`, + ); // fetch documents puter.fs.stat(documents_path); } // item:Public - if ( ! puter._cache.get(`item:${ public_path}`) ) { - console.log(`/${username}/Public item is not cached, refetching cache`); + if (!puter._cache.get(`item:${public_path}`)) { + console.log( + `/${username}/Public item is not cached, refetching cache`, + ); // fetch public puter.fs.stat(public_path); } // readdir:Home - if ( ! puter._cache.get(`readdir:${ home_path}`) ) { + if (!puter._cache.get(`readdir:${home_path}`)) { console.log(`/${username} is not cached, refetching cache`); // fetch home puter.fs.readdir(home_path); } // readdir:Desktop - if ( ! puter._cache.get(`readdir:${ desktop_path}`) ) { - console.log(`/${username}/Desktop is not cached, refetching cache`); + if (!puter._cache.get(`readdir:${desktop_path}`)) { + console.log( + `/${username}/Desktop is not cached, refetching cache`, + ); // fetch desktop puter.fs.readdir(desktop_path); } // readdir:Documents - if ( ! puter._cache.get(`readdir:${ documents_path}`) ) { - console.log(`/${username}/Documents is not cached, refetching cache`); + if (!puter._cache.get(`readdir:${documents_path}`)) { + console.log( + `/${username}/Documents is not cached, refetching cache`, + ); // fetch documents puter.fs.readdir(documents_path); } // readdir:Public - if ( ! puter._cache.get(`readdir:${ public_path}`) ) { - console.log(`/${username}/Public is not cached, refetching cache`); + if (!puter._cache.get(`readdir:${public_path}`)) { + console.log( + `/${username}/Public is not cached, refetching cache`, + ); // fetch public puter.fs.readdir(public_path); } @@ -1267,9 +1515,9 @@ const puterInit = (function () { // Return the Puter object return puterobj; -}); +}; -export const puter = puterInit(); +export const puter = puterInit(); export default puter; globalThis.puter = puter; puter.runWhenPuterHappensCallbacks(); @@ -1280,11 +1528,11 @@ puter.tools = []; */ const puterParent = puter.ui.parentApp(); globalThis.puterParent = puterParent; -if ( puterParent ) { +if (puterParent) { console.log('I have a parent, registering tools'); puterParent.on('message', async (event) => { console.log('Got tool req ', event); - if ( event.$ === 'requestTools' ) { + if (event.$ === 'requestTools') { console.log('Responding with tools'); puterParent.postMessage({ $: 'providedTools', @@ -1292,13 +1540,15 @@ if ( puterParent ) { }); } - if ( event.$ === 'executeTool' ) { + if (event.$ === 'executeTool') { console.log('xecuting tools'); /** * Puter tools format * @type {[{exec: Function, function: {description: string, name: string, parameters: {properties: any, required: Array}, type: string}}]} */ - const [tool] = puter.tools.filter(e => e.function.name === event.toolName); + const [tool] = puter.tools.filter( + (e) => e.function.name === event.toolName, + ); const response = await tool.exec(event.parameters); puterParent.postMessage({ @@ -1311,50 +1561,55 @@ if ( puterParent ) { puterParent.postMessage({ $: 'ready' }); } -globalThis.addEventListener && globalThis.addEventListener('message', async (event) => { - // if the message is not from Puter, then ignore it - if ( event.origin !== puter.defaultGUIOrigin ) return; +globalThis.addEventListener && + globalThis.addEventListener('message', async (event) => { + // if the message is not from Puter, then ignore it + if (event.origin !== puter.defaultGUIOrigin) return; - if ( event.data.msg && event.data.msg === 'requestOrigin' ) { - event.source.postMessage({ - msg: 'originResponse', - }, '*'); - } - else if ( event.data.msg === 'puter.token' ) { - // puterDialog.close(); - // Set the authToken property - puter.setAuthToken(event.data.token); - // update appID only when token does not include app identity - const tokenAppID = puter.getAppIDFromAuthToken(event.data.token); - if ( !tokenAppID && !puter.appID ) { - const fallbackAppID = puter.normalizeStringCandidate(event.data.app_uid); - if ( fallbackAppID ) { - puter.setAppID(fallbackAppID); + if (event.data.msg && event.data.msg === 'requestOrigin') { + event.source.postMessage( + { + msg: 'originResponse', + }, + '*', + ); + } else if (event.data.msg === 'puter.token') { + // puterDialog.close(); + // Set the authToken property + puter.setAuthToken(event.data.token); + // update appID only when token does not include app identity + const tokenAppID = puter.getAppIDFromAuthToken(event.data.token); + if (!tokenAppID && !puter.appID) { + const fallbackAppID = puter.normalizeStringCandidate( + event.data.app_uid, + ); + if (fallbackAppID) { + puter.setAppID(fallbackAppID); + } + } + // Remove the event listener to avoid memory leaks + // window.removeEventListener('message', messageListener); + + puter.puterAuthState.authGranted = true; + // Resolve the promise + // resolve(); + + // Call onAuth callback + if (puter.onAuth && typeof puter.onAuth === 'function') { + puter.getUser().then((user) => { + puter.onAuth(user); + }); + } + + puter.puterAuthState.isPromptOpen = false; + // Resolve or reject any waiting promises. + if (puter.puterAuthState.resolver) { + if (puter.puterAuthState.authGranted) { + puter.puterAuthState.resolver.resolve(); + } else { + puter.puterAuthState.resolver.reject(); + } + puter.puterAuthState.resolver = null; } } - // Remove the event listener to avoid memory leaks - // window.removeEventListener('message', messageListener); - - puter.puterAuthState.authGranted = true; - // Resolve the promise - // resolve(); - - // Call onAuth callback - if ( puter.onAuth && typeof puter.onAuth === 'function' ) { - puter.getUser().then((user) => { - puter.onAuth(user); - }); - } - - puter.puterAuthState.isPromptOpen = false; - // Resolve or reject any waiting promises. - if ( puter.puterAuthState.resolver ) { - if ( puter.puterAuthState.authGranted ) { - puter.puterAuthState.resolver.resolve(); - } else { - puter.puterAuthState.resolver.reject(); - } - puter.puterAuthState.resolver = null; - }; - } -}); + });