index.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  1. import OS from './modules/OS.js';
  2. import { PuterJSFileSystemModule } from './modules/FileSystem/index.js';
  3. import Hosting from './modules/Hosting.js';
  4. import Email from './modules/Email.js';
  5. import Apps from './modules/Apps.js';
  6. import UI from './modules/UI.js';
  7. import KV from './modules/KV.js';
  8. import AI from './modules/AI.js';
  9. import Auth from './modules/Auth.js';
  10. import FSItem from './modules/FSItem.js';
  11. import * as utils from './lib/utils.js';
  12. import path from './lib/path.js';
  13. import Util from './modules/Util.js';
  14. import Drivers from './modules/Drivers.js';
  15. import putility from '@heyputer/putility';
  16. import { FSRelayService } from './services/FSRelay.js';
  17. import { FilesystemService } from './services/Filesystem.js';
  18. import { APIAccessService } from './services/APIAccess.js';
  19. import { XDIncomingService } from './services/XDIncoming.js';
  20. import { NoPuterYetService } from './services/NoPuterYet.js';
  21. import { Debug } from './modules/Debug.js';
  22. import { PSocket, wispInfo } from './modules/networking/PSocket.js';
  23. import { PTLSSocket } from "./modules/networking/PTLS.js"
  24. import { PWispHandler } from './modules/networking/PWispHandler.js';
  25. import { make_http_api } from './lib/http.js';
  26. import Exec from './modules/Exec.js';
  27. import Convert from './modules/Convert.js';
  28. import Threads from './modules/Threads.js';
  29. import Perms from './modules/Perms.js';
  30. // TODO: This is for a safe-guard below; we should check if we can
  31. // generalize this behavior rather than hard-coding it.
  32. // (using defaultGUIOrigin breaks locally-hosted apps)
  33. const PROD_ORIGIN = 'https://puter.com';
  34. export default window.puter = (function() {
  35. 'use strict';
  36. class Puter{
  37. // The environment that the SDK is running in. Can be 'gui', 'app' or 'web'.
  38. // 'gui' means the SDK is running in the Puter GUI, i.e. Puter.com.
  39. // 'app' means the SDK is running as a Puter app, i.e. within an iframe in the Puter GUI.
  40. // 'web' means the SDK is running in a 3rd-party website.
  41. env;
  42. defaultAPIOrigin = globalThis.PUTER_API_ORIGIN ?? 'https://api.puter.com';
  43. defaultGUIOrigin = globalThis.PUTER_ORIGIN ?? 'https://puter.com';
  44. // An optional callback when the user is authenticated. This can be set by the app using the SDK.
  45. onAuth;
  46. /**
  47. * State object to keep track of the authentication request status.
  48. * This is used to prevent multiple authentication popups from showing up by different parts of the app.
  49. */
  50. puterAuthState = {
  51. isPromptOpen: false,
  52. authGranted: null,
  53. resolver: null
  54. };
  55. // Holds the unique app instance ID that is provided by the host environment
  56. appInstanceID;
  57. // Holds the unique app instance ID for the parent (if any), which is provided by the host environment
  58. parentInstanceID;
  59. // Expose the FSItem class
  60. static FSItem = FSItem;
  61. // Event handling properties
  62. eventHandlers = {};
  63. // debug flag
  64. debugMode = false;
  65. /**
  66. * Puter.js Modules
  67. *
  68. * These are the modules you see on docs.puter.com; for example:
  69. * - puter.fs
  70. * - puter.kv
  71. * - puter.ui
  72. *
  73. * initSubmodules is called from the constructor of this class.
  74. */
  75. initSubmodules = function(){
  76. // Util
  77. this.util = new Util();
  78. this.registerModule('auth', Auth);
  79. this.registerModule('os', OS);
  80. this.registerModule('fs', PuterJSFileSystemModule);
  81. this.registerModule('ui', UI, {
  82. appInstanceID: this.appInstanceID,
  83. parentInstanceID: this.parentInstanceID,
  84. });
  85. this.registerModule('hosting', Hosting);
  86. this.registerModule('email', Email);
  87. this.registerModule('apps', Apps);
  88. this.registerModule('ai', AI);
  89. this.registerModule('kv', KV);
  90. this.registerModule('threads', Threads);
  91. this.registerModule('perms', Perms);
  92. this.registerModule('drivers', Drivers);
  93. this.registerModule('debug', Debug);
  94. this.registerModule('exec', Exec);
  95. this.registerModule('convert', Convert);
  96. // Path
  97. this.path = path;
  98. }
  99. // --------------------------------------------
  100. // Constructor
  101. // --------------------------------------------
  102. constructor(options) {
  103. options = options ?? {};
  104. // "modules" in puter.js are external interfaces for the developer
  105. this.modules_ = [];
  106. // "services" in puter.js are used by modules and may interact with each other
  107. const context = new putility.libs.context.Context()
  108. .follow(this, ['env', 'util', 'authToken', 'APIOrigin', 'appID']);
  109. context.puter = this;
  110. this.services = new putility.system.ServiceManager({ context });
  111. this.context = context;
  112. context.services = this.services;
  113. // Holds the query parameters found in the current URL
  114. let URLParams = new URLSearchParams(window.location.search);
  115. // Figure out the environment in which the SDK is running
  116. if (URLParams.has('puter.app_instance_id'))
  117. this.env = 'app';
  118. else if(window.puter_gui_enabled === true)
  119. this.env = 'gui';
  120. else
  121. this.env = 'web';
  122. // There are some specific situations where puter is definitely loaded in GUI mode
  123. // we're going to check for those situations here so that we don't break anything unintentionally
  124. // if navigator URL's hostname is 'puter.com'
  125. if(this.env !== 'gui'){
  126. // 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).
  127. // This is necessary because the trailing dot can cause the hostname to not match the expected value.
  128. let hostname = location.hostname.replace(/\.$/, '');
  129. // Create a new URL object with the URL string
  130. const url = new URL(PROD_ORIGIN);
  131. // Extract hostname from the URL object
  132. const gui_hostname = url.hostname;
  133. // If the hostname matches the GUI hostname, then the SDK is running in the GUI environment
  134. if(hostname === gui_hostname){
  135. this.env = 'gui';
  136. }
  137. }
  138. // Get the 'args' from the URL. This is used to pass arguments to the app.
  139. if(URLParams.has('puter.args')){
  140. this.args = JSON.parse(decodeURIComponent(URLParams.get('puter.args')));
  141. }else{
  142. this.args = {};
  143. }
  144. // Try to extract appInstanceID from the URL. appInstanceID is included in every messaage
  145. // sent to the host environment. This is used to help host environment identify the app
  146. // instance that sent the message and communicate back to it.
  147. if(URLParams.has('puter.app_instance_id')){
  148. this.appInstanceID = decodeURIComponent(URLParams.get('puter.app_instance_id'));
  149. }
  150. // Try to extract parentInstanceID from the URL. If another app launched this app instance, parentInstanceID
  151. // holds its instance ID, and is used to communicate with that parent app.
  152. if(URLParams.has('puter.parent_instance_id')){
  153. this.parentInstanceID = decodeURIComponent(URLParams.get('puter.parent_instance_id'));
  154. }
  155. // Try to extract `puter.app.id` from the URL. `puter.app.id` is the unique ID of the app.
  156. // App ID is useful for identifying the app when communicating with the Puter API, among other things.
  157. if(URLParams.has('puter.app.id')){
  158. this.appID = decodeURIComponent(URLParams.get('puter.app.id'));
  159. }
  160. // Construct this App's AppData path based on the appID. AppData path is used to store files that are specific to this app.
  161. // The default AppData path is `~/AppData/<appID>`.
  162. if(this.appID){
  163. this.appDataPath = `~/AppData/${this.appID}`;
  164. }
  165. // Construct APIOrigin from the URL. APIOrigin is used to build the URLs for the Puter API endpoints.
  166. // The default APIOrigin is https://api.puter.com. However, if the URL contains a `puter.api_origin` query parameter,
  167. // then that value is used as the APIOrigin. If the URL contains a `puter.domain` query parameter, then the APIOrigin
  168. // is constructed as `https://api.<puter.domain>`.
  169. // This should only be done when the SDK is running in 'app' mode.
  170. this.APIOrigin = this.defaultAPIOrigin;
  171. if(URLParams.has('puter.api_origin') && this.env === 'app'){
  172. this.APIOrigin = decodeURIComponent(URLParams.get('puter.api_origin'));
  173. }else if(URLParams.has('puter.domain') && this.env === 'app'){
  174. this.APIOrigin = 'https://api.' + URLParams.get('puter.domain');
  175. }
  176. // === START :: Logger ===
  177. // logger will log to console
  178. let logger = new putility.libs.log.ConsoleLogger();
  179. // logs can be toggled based on categories
  180. logger = new putility.libs.log.CategorizedToggleLogger(
  181. { delegate: logger });
  182. const cat_logger = logger;
  183. // create facade for easy logging
  184. this.log = new putility.libs.log.LoggerFacade({
  185. impl: logger,
  186. cat: cat_logger,
  187. });
  188. // === START :: Services === //
  189. this.services.register('no-puter-yet', NoPuterYetService);
  190. this.services.register('filesystem', FilesystemService);
  191. this.services.register('api-access', APIAccessService);
  192. this.services.register('xd-incoming', XDIncomingService);
  193. if ( this.env !== 'app' ) {
  194. this.services.register('fs-relay', FSRelayService);
  195. }
  196. // When api-access is initialized, bind `.authToken` and
  197. // `.APIOrigin` as a 1-1 mapping with the `puter` global
  198. (async () => {
  199. await this.services.wait_for_init(['api-access']);
  200. const svc_apiAccess = this.services.get('api-access');
  201. svc_apiAccess.auth_token = this.authToken;
  202. svc_apiAccess.api_origin = this.APIOrigin;
  203. [
  204. ['authToken','auth_token'],
  205. ['APIOrigin','api_origin'],
  206. ].forEach(([k1,k2]) => {
  207. Object.defineProperty(this, k1, {
  208. get () {
  209. return svc_apiAccess[k2];
  210. },
  211. set (v) {
  212. svc_apiAccess[k2] = v;
  213. return true;
  214. }
  215. });
  216. });
  217. })();
  218. // === Start :: Modules === //
  219. // The SDK is running in the Puter GUI (i.e. 'gui')
  220. if(this.env === 'gui'){
  221. this.authToken = window.auth_token;
  222. // initialize submodules
  223. this.initSubmodules();
  224. }
  225. // Loaded in an iframe in the Puter GUI (i.e. 'app')
  226. // When SDK is loaded in App mode the initiation process should start when the DOM is ready
  227. else if (this.env === 'app') {
  228. this.authToken = decodeURIComponent(URLParams.get('puter.auth.token'));
  229. // initialize submodules
  230. this.initSubmodules();
  231. // If the authToken is already set in localStorage, then we don't need to show the dialog
  232. try {
  233. if(localStorage.getItem('puter.auth.token')){
  234. this.setAuthToken(localStorage.getItem('puter.auth.token'));
  235. }
  236. // if appID is already set in localStorage, then we don't need to show the dialog
  237. if(localStorage.getItem('puter.app.id')){
  238. this.setAppID(localStorage.getItem('puter.app.id'));
  239. }
  240. } catch (error) {
  241. // Handle the error here
  242. console.error('Error accessing localStorage:', error);
  243. }
  244. }
  245. // SDK was loaded in a 3rd-party website.
  246. // When SDK is loaded in GUI the initiation process should start when the DOM is ready. This is because
  247. // the SDK needs to show a dialog to the user to ask for permission to access their Puter account.
  248. else if(this.env === 'web') {
  249. // initialize submodules
  250. this.initSubmodules();
  251. try{
  252. // If the authToken is already set in localStorage, then we don't need to show the dialog
  253. if(localStorage.getItem('puter.auth.token')){
  254. this.setAuthToken(localStorage.getItem('puter.auth.token'));
  255. }
  256. // if appID is already set in localStorage, then we don't need to show the dialog
  257. if(localStorage.getItem('puter.app.id')){
  258. this.setAppID(localStorage.getItem('puter.app.id'));
  259. }
  260. } catch (error) {
  261. // Handle the error here
  262. console.error('Error accessing localStorage:', error);
  263. }
  264. }
  265. // Add prefix logger (needed to happen after modules are initialized)
  266. (async () => {
  267. await this.services.wait_for_init(['api-access']);
  268. const whoami = await this.auth.whoami();
  269. logger = new putility.libs.log.PrefixLogger({
  270. delegate: logger,
  271. prefix: '[' +
  272. (whoami?.app_name ?? this.appInstanceID ?? 'HOST') +
  273. '] ',
  274. });
  275. this.log.impl = logger;
  276. })();
  277. // Lock to prevent multiple requests to `/rao`
  278. this.lock_rao_ = new putility.libs.promise.Lock();
  279. // Promise that resolves when it's okay to request `/rao`
  280. this.p_can_request_rao_ = new putility.libs.promise.TeePromise();
  281. // Flag that indicates if a request to `/rao` has been made
  282. this.rao_requested_ = false;
  283. // In case we're already auth'd, request `/rao`
  284. (async () => {
  285. await this.services.wait_for_init(['api-access']);
  286. this.p_can_request_rao_.resolve();
  287. })();
  288. // TODO: This should be separated into modules called "Net" and "Http".
  289. // Modules need to be refactored first because right now they
  290. // are too tightly-coupled with authentication state.
  291. (async () => {
  292. // === puter.net ===
  293. const { token: wispToken, server: wispServer } = (await (await fetch(this.APIOrigin + '/wisp/relay-token/create', {
  294. method: 'POST',
  295. headers: {
  296. Authorization: `Bearer ${this.authToken}`,
  297. 'Content-Type': 'application/json',
  298. },
  299. body: JSON.stringify({}),
  300. })).json());
  301. wispInfo.handler = new PWispHandler(wispServer, wispToken);
  302. this.net = {
  303. generateWispV1URL: async () => {
  304. const { token: wispToken, server: wispServer } = (await (await fetch(this.APIOrigin + '/wisp/relay-token/create', {
  305. method: 'POST',
  306. headers: {
  307. Authorization: `Bearer ${this.authToken}`,
  308. 'Content-Type': 'application/json',
  309. },
  310. body: JSON.stringify({}),
  311. })).json());
  312. return `${wispServer}/${wispToken}/`
  313. },
  314. Socket: PSocket,
  315. tls: {
  316. TLSSocket: PTLSSocket
  317. }
  318. }
  319. // === puter.http ===
  320. this.http = make_http_api(
  321. { Socket: this.net.Socket, DEFAULT_PORT: 80 });
  322. this.https = make_http_api(
  323. { Socket: this.net.tls.TLSSocket, DEFAULT_PORT: 443 });
  324. })();
  325. }
  326. /**
  327. * @internal
  328. * Makes a request to `/rao`. This method aquires a lock to prevent
  329. * multiple requests, and is effectively idempotent.
  330. */
  331. async request_rao_ () {
  332. await this.p_can_request_rao_;
  333. if ( this.env === 'gui' ) {
  334. return;
  335. }
  336. // setAuthToken is called more than once when auth completes, which
  337. // causes multiple requests to /rao. This lock prevents that.
  338. await this.lock_rao_.acquire();
  339. if ( this.rao_requested_ ) {
  340. this.lock_rao_.release();
  341. return;
  342. }
  343. let had_error = false;
  344. try {
  345. const resp = await fetch(this.APIOrigin + '/rao', {
  346. method: 'POST',
  347. headers: {
  348. Authorization: `Bearer ${this.authToken}`
  349. }
  350. });
  351. return await resp.json();
  352. } catch (e) {
  353. had_error = true;
  354. console.error(e);
  355. } finally {
  356. this.lock_rao_.release();
  357. }
  358. if ( ! had_error ) {
  359. this.rao_requested_ = true;
  360. }
  361. }
  362. registerModule (name, cls, parameters = {}) {
  363. const instance = new cls(this.context, parameters);
  364. this.modules_.push(name);
  365. this[name] = instance;
  366. if ( instance._init ) instance._init({ puter: this });
  367. }
  368. updateSubmodules() {
  369. // Update submodules with new auth token and API origin
  370. for ( const name of this.modules_ ) {
  371. if ( ! this[name] ) continue;
  372. this[name]?.setAuthToken?.(this.authToken);
  373. this[name]?.setAPIOrigin?.(this.APIOrigin);
  374. }
  375. }
  376. setAppID = function (appID) {
  377. // save to localStorage
  378. try{
  379. localStorage.setItem('puter.app.id', appID);
  380. } catch (error) {
  381. // Handle the error here
  382. console.error('Error accessing localStorage:', error);
  383. }
  384. this.appID = appID;
  385. }
  386. setAuthToken = function (authToken) {
  387. this.authToken = authToken;
  388. // If the SDK is running on a 3rd-party site or an app, then save the authToken in localStorage
  389. if(this.env === 'web' || this.env === 'app'){
  390. try{
  391. localStorage.setItem('puter.auth.token', authToken);
  392. } catch (error) {
  393. // Handle the error here
  394. console.error('Error accessing localStorage:', error);
  395. }
  396. }
  397. // reinitialize submodules
  398. this.updateSubmodules();
  399. // rao
  400. this.request_rao_();
  401. }
  402. setAPIOrigin = function (APIOrigin) {
  403. this.APIOrigin = APIOrigin;
  404. // reinitialize submodules
  405. this.updateSubmodules();
  406. }
  407. resetAuthToken = function () {
  408. this.authToken = null;
  409. // If the SDK is running on a 3rd-party site or an app, then save the authToken in localStorage
  410. if(this.env === 'web' || this.env === 'app'){
  411. try{
  412. localStorage.removeItem('puter.auth.token');
  413. } catch (error) {
  414. // Handle the error here
  415. console.error('Error accessing localStorage:', error);
  416. }
  417. }
  418. // reinitialize submodules
  419. this.updateSubmodules();
  420. }
  421. exit = function(statusCode = 0) {
  422. if (statusCode && (typeof statusCode !== 'number')) {
  423. console.warn('puter.exit() requires status code to be a number. Treating it as 1');
  424. statusCode = 1;
  425. }
  426. window.parent.postMessage({
  427. msg: "exit",
  428. appInstanceID: this.appInstanceID,
  429. statusCode,
  430. }, '*');
  431. }
  432. /**
  433. * A function that generates a domain-safe name by combining a random adjective, a random noun, and a random number (between 0 and 9999).
  434. * The result is returned as a string with components separated by hyphens.
  435. * It is useful when you need to create unique identifiers that are also human-friendly.
  436. *
  437. * @param {string} [separateWith='-'] - The character to use to separate the components of the generated name.
  438. * @returns {string} A unique, hyphen-separated string comprising of an adjective, a noun, and a number.
  439. *
  440. */
  441. randName = function(separateWith = '-'){
  442. const first_adj = ['helpful','sensible', 'loyal', 'honest', 'clever', 'capable','calm', 'smart', 'genius', 'bright', 'charming', 'creative', 'diligent', 'elegant', 'fancy',
  443. 'colorful', 'avid', 'active', 'gentle', 'happy', 'intelligent', 'jolly', 'kind', 'lively', 'merry', 'nice', 'optimistic', 'polite',
  444. 'quiet', 'relaxed', 'silly', 'victorious', 'witty', 'young', 'zealous', 'strong', 'brave', 'agile', 'bold'];
  445. const nouns = ['street', 'roof', 'floor', 'tv', 'idea', 'morning', 'game', 'wheel', 'shoe', 'bag', 'clock', 'pencil', 'pen',
  446. 'magnet', 'chair', 'table', 'house', 'dog', 'room', 'book', 'car', 'cat', 'tree',
  447. 'flower', 'bird', 'fish', 'sun', 'moon', 'star', 'cloud', 'rain', 'snow', 'wind', 'mountain',
  448. 'river', 'lake', 'sea', 'ocean', 'island', 'bridge', 'road', 'train', 'plane', 'ship', 'bicycle',
  449. 'horse', 'elephant', 'lion', 'tiger', 'bear', 'zebra', 'giraffe', 'monkey', 'snake', 'rabbit', 'duck',
  450. 'goose', 'penguin', 'frog', 'crab', 'shrimp', 'whale', 'octopus', 'spider', 'ant', 'bee', 'butterfly', 'dragonfly',
  451. 'ladybug', 'snail', 'camel', 'kangaroo', 'koala', 'panda', 'piglet', 'sheep', 'wolf', 'fox', 'deer', 'mouse', 'seal',
  452. 'chicken', 'cow', 'dinosaur', 'puppy', 'kitten', 'circle', 'square', 'garden', 'otter', 'bunny', 'meerkat', 'harp']
  453. // return a random combination of first_adj + noun + number (between 0 and 9999)
  454. // e.g. clever-idea-123
  455. return first_adj[Math.floor(Math.random() * first_adj.length)] + separateWith + nouns[Math.floor(Math.random() * nouns.length)] + separateWith + Math.floor(Math.random() * 10000);
  456. }
  457. getUser = function(...args){
  458. let options;
  459. // If first argument is an object, it's the options
  460. if (typeof args[0] === 'object' && args[0] !== null) {
  461. options = args[0];
  462. } else {
  463. // Otherwise, we assume separate arguments are provided
  464. options = {
  465. success: args[0],
  466. error: args[1],
  467. };
  468. }
  469. return new Promise((resolve, reject) => {
  470. const xhr = utils.initXhr('/whoami', this.APIOrigin, this.authToken, 'get');
  471. // set up event handlers for load and error events
  472. utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
  473. xhr.send();
  474. })
  475. }
  476. print = function(...args){
  477. for(let arg of args){
  478. document.body.innerHTML += arg;
  479. }
  480. }
  481. }
  482. // Create a new Puter object and return it
  483. const puterobj = new Puter();
  484. // Return the Puter object
  485. return puterobj;
  486. }());
  487. window.addEventListener('message', async (event) => {
  488. // if the message is not from Puter, then ignore it
  489. if(event.origin !== puter.defaultGUIOrigin) return;
  490. if(event.data.msg && event.data.msg === 'requestOrigin'){
  491. event.source.postMessage({
  492. msg: "originResponse",
  493. }, '*');
  494. }
  495. else if (event.data.msg === 'puter.token') {
  496. // puterDialog.close();
  497. // Set the authToken property
  498. puter.setAuthToken(event.data.token);
  499. // update appID
  500. puter.setAppID(event.data.app_uid);
  501. // Remove the event listener to avoid memory leaks
  502. // window.removeEventListener('message', messageListener);
  503. puter.puterAuthState.authGranted = true;
  504. // Resolve the promise
  505. // resolve();
  506. // Call onAuth callback
  507. if(puter.onAuth && typeof puter.onAuth === 'function'){
  508. puter.getUser().then((user) => {
  509. puter.onAuth(user)
  510. });
  511. }
  512. puter.puterAuthState.isPromptOpen = false;
  513. // Resolve or reject any waiting promises.
  514. if (puter.puterAuthState.resolver) {
  515. if (puter.puterAuthState.authGranted) {
  516. puter.puterAuthState.resolver.resolve();
  517. } else {
  518. puter.puterAuthState.resolver.reject();
  519. }
  520. puter.puterAuthState.resolver = null;
  521. };
  522. }
  523. })