index.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. import OS from './modules/OS.js';
  2. import FileSystem from './modules/FileSystem/index.js';
  3. import Hosting from './modules/Hosting.js';
  4. import Apps from './modules/Apps.js';
  5. import UI from './modules/UI.js';
  6. import KV from './modules/KV.js';
  7. import AI from './modules/AI.js';
  8. import Auth from './modules/Auth.js';
  9. import FSItem from './modules/FSItem.js';
  10. import * as utils from './lib/utils.js';
  11. import path from './lib/path.js';
  12. window.puter = (function() {
  13. 'use strict';
  14. class Puter{
  15. // The environment that the SDK is running in. Can be 'gui', 'app' or 'web'.
  16. // 'gui' means the SDK is running in the Puter GUI, i.e. Puter.com.
  17. // 'app' means the SDK is running as a Puter app, i.e. within an iframe in the Puter GUI.
  18. // 'web' means the SDK is running in a 3rd-party website.
  19. env;
  20. defaultAPIOrigin = 'https://api.puter.com';
  21. defaultGUIOrigin = 'https://puter.com';
  22. // An optional callback when the user is authenticated. This can be set by the app using the SDK.
  23. onAuth;
  24. /**
  25. * State object to keep track of the authentication request status.
  26. * This is used to prevent multiple authentication popups from showing up by different parts of the app.
  27. */
  28. puterAuthState = {
  29. isPromptOpen: false,
  30. authGranted: null,
  31. resolver: null
  32. };
  33. // Holds the unique app instance ID that is provided by the host environment
  34. appInstanceID;
  35. // Holds the unique app instance ID for the parent (if any), which is provided by the host environment
  36. parentInstanceID;
  37. // Expose the FSItem class
  38. static FSItem = FSItem;
  39. // Event handling properties
  40. eventHandlers = {};
  41. // --------------------------------------------
  42. // Constructor
  43. // --------------------------------------------
  44. constructor(options) {
  45. options = options ?? {};
  46. // Holds the query parameters found in the current URL
  47. let URLParams = new URLSearchParams(window.location.search);
  48. // Figure out the environment in which the SDK is running
  49. if (URLParams.has('puter.app_instance_id'))
  50. this.env = 'app';
  51. else if(window.puter_gui_enabled === true)
  52. this.env = 'gui';
  53. else
  54. this.env = 'web';
  55. // there are some specific situations where puter is definitely loaded in GUI mode
  56. // we're going to check for those situations here so that we don't break anything unintentionally
  57. // if navigator URL's hostname is 'puter.com'
  58. if(window.location.hostname === 'puter.com'){
  59. this.env = 'gui';
  60. }
  61. // Get the 'args' from the URL. This is used to pass arguments to the app.
  62. if(URLParams.has('puter.args')){
  63. this.args = JSON.parse(decodeURIComponent(URLParams.get('puter.args')));
  64. }else{
  65. this.args = {};
  66. }
  67. // Try to extract appInstanceID from the URL. appInstanceID is included in every messaage
  68. // sent to the host environment. This is used to help host environment identify the app
  69. // instance that sent the message and communicate back to it.
  70. if(URLParams.has('puter.app_instance_id')){
  71. this.appInstanceID = decodeURIComponent(URLParams.get('puter.app_instance_id'));
  72. }
  73. // Try to extract parentInstanceID from the URL. If another app launched this app instance, parentInstanceID
  74. // holds its instance ID, and is used to communicate with that parent app.
  75. if(URLParams.has('puter.parent_instance_id')){
  76. this.parentInstanceID = decodeURIComponent(URLParams.get('puter.parent_instance_id'));
  77. }
  78. // Try to extract `puter.app.id` from the URL. `puter.app.id` is the unique ID of the app.
  79. // App ID is useful for identifying the app when communicating with the Puter API, among other things.
  80. if(URLParams.has('puter.app.id')){
  81. this.appID = decodeURIComponent(URLParams.get('puter.app.id'));
  82. }
  83. // Construct this App's AppData path based on the appID. AppData path is used to store files that are specific to this app.
  84. // The default AppData path is `~/AppData/<appID>`.
  85. if(this.appID){
  86. this.appDataPath = `~/AppData/${this.appID}`;
  87. }
  88. // Construct APIOrigin from the URL. APIOrigin is used to build the URLs for the Puter API endpoints.
  89. // The default APIOrigin is https://api.puter.com. However, if the URL contains a `puter.api_origin` query parameter,
  90. // then that value is used as the APIOrigin. If the URL contains a `puter.domain` query parameter, then the APIOrigin
  91. // is constructed as `https://api.<puter.domain>`.
  92. this.APIOrigin = this.defaultAPIOrigin;
  93. if(URLParams.has('puter.api_origin')){
  94. this.APIOrigin = decodeURIComponent(URLParams.get('puter.api_origin'));
  95. }else if(URLParams.has('puter.domain')){
  96. this.APIOrigin = 'https://api.' + URLParams.get('puter.domain');
  97. }
  98. // The SDK is running in the Puter GUI (i.e. 'gui')
  99. if(this.env === 'gui'){
  100. this.authToken = window.auth_token;
  101. // initialize submodules
  102. this.initSubmodules();
  103. }
  104. // Loaded in an iframe in the Puter GUI (i.e. 'app')
  105. // When SDK is loaded in App mode the initiation process should start when the DOM is ready
  106. else if (this.env === 'app') {
  107. this.authToken = decodeURIComponent(URLParams.get('puter.auth.token'));
  108. // initialize submodules
  109. this.initSubmodules();
  110. // If the authToken is already set in localStorage, then we don't need to show the dialog
  111. try {
  112. if(localStorage.getItem('puter.auth.token')){
  113. this.setAuthToken(localStorage.getItem('puter.auth.token'));
  114. }
  115. // if appID is already set in localStorage, then we don't need to show the dialog
  116. if(localStorage.getItem('puter.app.id')){
  117. this.setAppID(localStorage.getItem('puter.app.id'));
  118. }
  119. } catch (error) {
  120. // Handle the error here
  121. console.error('Error accessing localStorage:', error);
  122. }
  123. }
  124. // SDK was loaded in a 3rd-party website.
  125. // When SDK is loaded in GUI the initiation process should start when the DOM is ready. This is because
  126. // the SDK needs to show a dialog to the user to ask for permission to access their Puter account.
  127. else if(this.env === 'web') {
  128. // initialize submodules
  129. this.initSubmodules();
  130. try{
  131. // If the authToken is already set in localStorage, then we don't need to show the dialog
  132. if(localStorage.getItem('puter.auth.token')){
  133. this.setAuthToken(localStorage.getItem('puter.auth.token'));
  134. }
  135. // if appID is already set in localStorage, then we don't need to show the dialog
  136. if(localStorage.getItem('puter.app.id')){
  137. this.setAppID(localStorage.getItem('puter.app.id'));
  138. }
  139. } catch (error) {
  140. // Handle the error here
  141. console.error('Error accessing localStorage:', error);
  142. }
  143. }
  144. }
  145. // Initialize submodules
  146. initSubmodules = function(){
  147. // Auth
  148. this.auth = new Auth(this.authToken, this.APIOrigin, this.appID, this.env);
  149. // OS
  150. this.os = new OS(this.authToken, this.APIOrigin, this.appID, this.env);
  151. // FileSystem
  152. this.fs = new FileSystem(this.authToken, this.APIOrigin, this.appID, this.env);
  153. // UI
  154. this.ui = new UI(this.appInstanceID, this.parentInstanceID, this.appID, this.env);
  155. // Hosting
  156. this.hosting = new Hosting(this.authToken, this.APIOrigin, this.appID, this.env);
  157. // Apps
  158. this.apps = new Apps(this.authToken, this.APIOrigin, this.appID, this.env);
  159. // AI
  160. this.ai = new AI(this.authToken, this.APIOrigin, this.appID, this.env);
  161. // Key-Value Store
  162. this.kv = new KV(this.authToken, this.APIOrigin, this.appID, this.env);
  163. // Path
  164. this.path = path;
  165. }
  166. updateSubmodules() {
  167. // Update submodules with new auth token and API origin
  168. [this.os, this.fs, this.hosting, this.apps, this.ai, this.kv].forEach(module => {
  169. if(!module) return;
  170. module.setAuthToken(this.authToken);
  171. module.setAPIOrigin(this.APIOrigin);
  172. });
  173. }
  174. setAppID = function (appID) {
  175. // save to localStorage
  176. try{
  177. localStorage.setItem('puter.app.id', appID);
  178. } catch (error) {
  179. // Handle the error here
  180. console.error('Error accessing localStorage:', error);
  181. }
  182. this.appID = appID;
  183. }
  184. setAuthToken = function (authToken) {
  185. this.authToken = authToken;
  186. // If the SDK is running on a 3rd-party site or an app, then save the authToken in localStorage
  187. if(this.env === 'web' || this.env === 'app'){
  188. try{
  189. localStorage.setItem('puter.auth.token', authToken);
  190. } catch (error) {
  191. // Handle the error here
  192. console.error('Error accessing localStorage:', error);
  193. }
  194. }
  195. // reinitialize submodules
  196. this.updateSubmodules();
  197. }
  198. setAPIOrigin = function (APIOrigin) {
  199. this.APIOrigin = APIOrigin;
  200. // reinitialize submodules
  201. this.updateSubmodules();
  202. }
  203. resetAuthToken = function () {
  204. this.authToken = null;
  205. // If the SDK is running on a 3rd-party site or an app, then save the authToken in localStorage
  206. if(this.env === 'web' || this.env === 'app'){
  207. try{
  208. localStorage.removeItem('puter.auth.token');
  209. } catch (error) {
  210. // Handle the error here
  211. console.error('Error accessing localStorage:', error);
  212. }
  213. }
  214. // reinitialize submodules
  215. this.updateSubmodules();
  216. }
  217. exit = function() {
  218. window.parent.postMessage({
  219. msg: "exit",
  220. appInstanceID: this.appInstanceID,
  221. }, '*');
  222. }
  223. /**
  224. * A function that generates a domain-safe name by combining a random adjective, a random noun, and a random number (between 0 and 9999).
  225. * The result is returned as a string with components separated by hyphens.
  226. * It is useful when you need to create unique identifiers that are also human-friendly.
  227. *
  228. * @param {string} [separateWith='-'] - The character to use to separate the components of the generated name.
  229. * @returns {string} A unique, hyphen-separated string comprising of an adjective, a noun, and a number.
  230. *
  231. */
  232. randName = function(separateWith = '-'){
  233. const first_adj = ['helpful','sensible', 'loyal', 'honest', 'clever', 'capable','calm', 'smart', 'genius', 'bright', 'charming', 'creative', 'diligent', 'elegant', 'fancy',
  234. 'colorful', 'avid', 'active', 'gentle', 'happy', 'intelligent', 'jolly', 'kind', 'lively', 'merry', 'nice', 'optimistic', 'polite',
  235. 'quiet', 'relaxed', 'silly', 'victorious', 'witty', 'young', 'zealous', 'strong', 'brave', 'agile', 'bold'];
  236. const nouns = ['street', 'roof', 'floor', 'tv', 'idea', 'morning', 'game', 'wheel', 'shoe', 'bag', 'clock', 'pencil', 'pen',
  237. 'magnet', 'chair', 'table', 'house', 'dog', 'room', 'book', 'car', 'cat', 'tree',
  238. 'flower', 'bird', 'fish', 'sun', 'moon', 'star', 'cloud', 'rain', 'snow', 'wind', 'mountain',
  239. 'river', 'lake', 'sea', 'ocean', 'island', 'bridge', 'road', 'train', 'plane', 'ship', 'bicycle',
  240. 'horse', 'elephant', 'lion', 'tiger', 'bear', 'zebra', 'giraffe', 'monkey', 'snake', 'rabbit', 'duck',
  241. 'goose', 'penguin', 'frog', 'crab', 'shrimp', 'whale', 'octopus', 'spider', 'ant', 'bee', 'butterfly', 'dragonfly',
  242. 'ladybug', 'snail', 'camel', 'kangaroo', 'koala', 'panda', 'piglet', 'sheep', 'wolf', 'fox', 'deer', 'mouse', 'seal',
  243. 'chicken', 'cow', 'dinosaur', 'puppy', 'kitten', 'circle', 'square', 'garden', 'otter', 'bunny', 'meerkat', 'harp']
  244. // return a random combination of first_adj + noun + number (between 0 and 9999)
  245. // e.g. clever-idea-123
  246. return first_adj[Math.floor(Math.random() * first_adj.length)] + separateWith + nouns[Math.floor(Math.random() * nouns.length)] + separateWith + Math.floor(Math.random() * 10000);
  247. }
  248. getUser = function(...args){
  249. let options;
  250. // If first argument is an object, it's the options
  251. if (typeof args[0] === 'object' && args[0] !== null) {
  252. options = args[0];
  253. } else {
  254. // Otherwise, we assume separate arguments are provided
  255. options = {
  256. success: args[0],
  257. error: args[1],
  258. };
  259. }
  260. return new Promise((resolve, reject) => {
  261. const xhr = utils.initXhr('/whoami', this.APIOrigin, this.authToken, 'get');
  262. // set up event handlers for load and error events
  263. utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
  264. xhr.send();
  265. })
  266. }
  267. print = function(...args){
  268. for(let arg of args){
  269. document.getElementsByTagName('body')[0].append(arg);
  270. }
  271. }
  272. }
  273. // Create a new Puter object and return it
  274. const puterobj = new Puter();
  275. // Return the Puter object
  276. return puterobj;
  277. }());
  278. window.addEventListener('message', async (event) => {
  279. // if the message is not from Puter, then ignore it
  280. if(event.origin !== puter.defaultGUIOrigin) return;
  281. if(event.data.msg && event.data.msg === 'requestOrigin'){
  282. event.source.postMessage({
  283. msg: "originResponse",
  284. }, '*');
  285. }
  286. else if (event.data.msg === 'puter.token') {
  287. // puterDialog.close();
  288. // Set the authToken property
  289. puter.setAuthToken(event.data.token);
  290. // update appID
  291. puter.setAppID(event.data.app_uid);
  292. // Remove the event listener to avoid memory leaks
  293. // window.removeEventListener('message', messageListener);
  294. puter.puterAuthState.authGranted = true;
  295. // Resolve the promise
  296. // resolve();
  297. // Call onAuth callback
  298. if(puter.onAuth && typeof puter.onAuth === 'function'){
  299. puter.getUser().then((user) => {
  300. puter.onAuth(user)
  301. });
  302. }
  303. puter.puterAuthState.isPromptOpen = false;
  304. // Resolve or reject any waiting promises.
  305. if (puter.puterAuthState.resolver) {
  306. if (puter.puterAuthState.authGranted) {
  307. puter.puterAuthState.resolver.resolve();
  308. } else {
  309. puter.puterAuthState.resolver.reject();
  310. }
  311. puter.puterAuthState.resolver = null;
  312. };
  313. }
  314. })