@heyputer/puter.js 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/APACHE_LICENSE.txt +201 -0
  2. package/README.md +88 -0
  3. package/doc/devlog.md +49 -0
  4. package/package.json +31 -0
  5. package/src/bg.png +0 -0
  6. package/src/bg.webp +0 -0
  7. package/src/index.js +745 -0
  8. package/src/lib/APICallLogger.js +110 -0
  9. package/src/lib/EventListener.js +51 -0
  10. package/src/lib/RequestError.js +6 -0
  11. package/src/lib/filesystem/APIFS.js +73 -0
  12. package/src/lib/filesystem/CacheFS.js +243 -0
  13. package/src/lib/filesystem/PostMessageFS.js +40 -0
  14. package/src/lib/filesystem/definitions.js +39 -0
  15. package/src/lib/path.js +509 -0
  16. package/src/lib/polyfills/localStorage.js +92 -0
  17. package/src/lib/polyfills/xhrshim.js +233 -0
  18. package/src/lib/socket.io/socket.io.esm.min.js +7 -0
  19. package/src/lib/socket.io/socket.io.esm.min.js.map +1 -0
  20. package/src/lib/socket.io/socket.io.js +4385 -0
  21. package/src/lib/socket.io/socket.io.js.map +1 -0
  22. package/src/lib/socket.io/socket.io.min.js +7 -0
  23. package/src/lib/socket.io/socket.io.min.js.map +1 -0
  24. package/src/lib/socket.io/socket.io.msgpack.min.js +7 -0
  25. package/src/lib/socket.io/socket.io.msgpack.min.js.map +1 -0
  26. package/src/lib/utils.js +620 -0
  27. package/src/lib/xdrpc.js +104 -0
  28. package/src/modules/AI.js +680 -0
  29. package/src/modules/Apps.js +215 -0
  30. package/src/modules/Auth.js +171 -0
  31. package/src/modules/Debug.js +39 -0
  32. package/src/modules/Drivers.js +278 -0
  33. package/src/modules/FSItem.js +139 -0
  34. package/src/modules/FileSystem/index.js +187 -0
  35. package/src/modules/FileSystem/operations/copy.js +64 -0
  36. package/src/modules/FileSystem/operations/deleteFSEntry.js +59 -0
  37. package/src/modules/FileSystem/operations/getReadUrl.js +42 -0
  38. package/src/modules/FileSystem/operations/mkdir.js +62 -0
  39. package/src/modules/FileSystem/operations/move.js +75 -0
  40. package/src/modules/FileSystem/operations/read.js +46 -0
  41. package/src/modules/FileSystem/operations/readdir.js +102 -0
  42. package/src/modules/FileSystem/operations/rename.js +58 -0
  43. package/src/modules/FileSystem/operations/sign.js +103 -0
  44. package/src/modules/FileSystem/operations/space.js +40 -0
  45. package/src/modules/FileSystem/operations/stat.js +95 -0
  46. package/src/modules/FileSystem/operations/symlink.js +55 -0
  47. package/src/modules/FileSystem/operations/upload.js +440 -0
  48. package/src/modules/FileSystem/operations/write.js +65 -0
  49. package/src/modules/FileSystem/utils/getAbsolutePathForApp.js +21 -0
  50. package/src/modules/Hosting.js +138 -0
  51. package/src/modules/KV.js +301 -0
  52. package/src/modules/OS.js +95 -0
  53. package/src/modules/Perms.js +109 -0
  54. package/src/modules/PuterDialog.js +481 -0
  55. package/src/modules/Threads.js +75 -0
  56. package/src/modules/UI.js +1555 -0
  57. package/src/modules/Util.js +38 -0
  58. package/src/modules/Workers.js +120 -0
  59. package/src/modules/networking/PSocket.js +87 -0
  60. package/src/modules/networking/PTLS.js +100 -0
  61. package/src/modules/networking/PWispHandler.js +89 -0
  62. package/src/modules/networking/parsers.js +157 -0
  63. package/src/modules/networking/requests.js +282 -0
  64. package/src/services/APIAccess.js +46 -0
  65. package/src/services/FSRelay.js +20 -0
  66. package/src/services/Filesystem.js +122 -0
  67. package/src/services/NoPuterYet.js +20 -0
  68. package/src/services/XDIncoming.js +44 -0
  69. package/test/ai.test.js +214 -0
  70. package/test/fs.test.js +798 -0
  71. package/test/index.html +1183 -0
  72. package/test/kv.test.js +548 -0
  73. package/test/txt2speech.test.js +178 -0
  74. package/webpack.config.js +25 -0
package/src/index.js ADDED
@@ -0,0 +1,745 @@
1
+ import putility from '@heyputer/putility';
2
+
3
+ import APICallLogger from './lib/APICallLogger.js';
4
+ import path from './lib/path.js';
5
+ import localStorageMemory from './lib/polyfills/localStorage.js'
6
+ import xhrshim from './lib/polyfills/xhrshim.js'
7
+ import * as utils from './lib/utils.js';
8
+ import AI from './modules/AI.js';
9
+ import Apps from './modules/Apps.js';
10
+ import Auth from './modules/Auth.js';
11
+ import { Debug } from './modules/Debug.js';
12
+ import Drivers from './modules/Drivers.js';
13
+ import { PuterJSFileSystemModule } from './modules/FileSystem/index.js';
14
+ import FSItem from './modules/FSItem.js';
15
+ import Hosting from './modules/Hosting.js';
16
+ import KV from './modules/KV.js';
17
+ import { PSocket } from './modules/networking/PSocket.js';
18
+ import { PTLSSocket } from "./modules/networking/PTLS.js"
19
+ import { pFetch } from './modules/networking/requests.js';
20
+ import OS from './modules/OS.js';
21
+ import Perms from './modules/Perms.js';
22
+ import Threads from './modules/Threads.js';
23
+ import UI from './modules/UI.js';
24
+ import Util from './modules/Util.js';
25
+ import { WorkersHandler } from './modules/Workers.js';
26
+ import { APIAccessService } from './services/APIAccess.js';
27
+ import { FilesystemService } from './services/Filesystem.js';
28
+ import { FSRelayService } from './services/FSRelay.js';
29
+ import { NoPuterYetService } from './services/NoPuterYet.js';
30
+ import { XDIncomingService } from './services/XDIncoming.js';
31
+ import kvjs from '@heyputer/kv.js';
32
+
33
+ // TODO: This is for a safe-guard below; we should check if we can
34
+ // generalize this behavior rather than hard-coding it.
35
+ // (using defaultGUIOrigin breaks locally-hosted apps)
36
+ const PROD_ORIGIN = 'https://puter.com';
37
+
38
+ export default globalThis.puter = (function() {
39
+ 'use strict';
40
+
41
+ class Puter{
42
+ // The environment that the SDK is running in. Can be 'gui', 'app' or 'web'.
43
+ // 'gui' means the SDK is running in the Puter GUI, i.e. Puter.com.
44
+ // 'app' means the SDK is running as a Puter app, i.e. within an iframe in the Puter GUI.
45
+ // 'web' means the SDK is running in a 3rd-party website.
46
+ env;
47
+
48
+ defaultAPIOrigin = globalThis.PUTER_API_ORIGIN ?? 'https://api.puter.com';
49
+ defaultGUIOrigin = globalThis.PUTER_ORIGIN ?? 'https://puter.com';
50
+
51
+ // An optional callback when the user is authenticated. This can be set by the app using the SDK.
52
+ onAuth;
53
+
54
+ /**
55
+ * State object to keep track of the authentication request status.
56
+ * This is used to prevent multiple authentication popups from showing up by different parts of the app.
57
+ */
58
+ puterAuthState = {
59
+ isPromptOpen: false,
60
+ authGranted: null,
61
+ resolver: null
62
+ };
63
+
64
+ // Holds the unique app instance ID that is provided by the host environment
65
+ appInstanceID;
66
+
67
+ // Holds the unique app instance ID for the parent (if any), which is provided by the host environment
68
+ parentInstanceID;
69
+
70
+ // Expose the FSItem class
71
+ static FSItem = FSItem;
72
+
73
+ // Event handling properties
74
+ eventHandlers = {};
75
+
76
+ // debug flag
77
+ debugMode = false;
78
+
79
+ /**
80
+ * Puter.js Modules
81
+ *
82
+ * These are the modules you see on docs.puter.com; for example:
83
+ * - puter.fs
84
+ * - puter.kv
85
+ * - puter.ui
86
+ *
87
+ * initSubmodules is called from the constructor of this class.
88
+ */
89
+ initSubmodules = function(){
90
+ // Util
91
+ this.util = new Util();
92
+
93
+ this.registerModule('auth', Auth);
94
+ this.registerModule('os', OS);
95
+ this.registerModule('fs', PuterJSFileSystemModule);
96
+ this.registerModule('ui', UI, {
97
+ appInstanceID: this.appInstanceID,
98
+ parentInstanceID: this.parentInstanceID,
99
+ });
100
+ this.registerModule('hosting', Hosting);
101
+ this.registerModule('apps', Apps);
102
+ this.registerModule('ai', AI);
103
+ this.registerModule('kv', KV);
104
+ this.registerModule('threads', Threads);
105
+ this.registerModule('perms', Perms);
106
+ this.registerModule('drivers', Drivers);
107
+ this.registerModule('debug', Debug);
108
+
109
+ // Path
110
+ this.path = path;
111
+ }
112
+
113
+ // --------------------------------------------
114
+ // Constructor
115
+ // --------------------------------------------
116
+ constructor(options) {
117
+ options = options ?? {};
118
+
119
+ // Initialize the cache using kv.js
120
+ this._cache = new kvjs();
121
+
122
+ // "modules" in puter.js are external interfaces for the developer
123
+ this.modules_ = [];
124
+ // "services" in puter.js are used by modules and may interact with each other
125
+ const context = new putility.libs.context.Context()
126
+ .follow(this, ['env', 'util', 'authToken', 'APIOrigin', 'appID']);
127
+
128
+ context.puter = this;
129
+
130
+ this.services = new putility.system.ServiceManager({ context });
131
+ this.context = context;
132
+ context.services = this.services;
133
+
134
+
135
+ // Holds the query parameters found in the current URL
136
+ let URLParams = new URLSearchParams(globalThis.location?.search);
137
+
138
+ // Figure out the environment in which the SDK is running
139
+ if (URLParams.has('puter.app_instance_id')) {
140
+ this.env = 'app';
141
+ } else if(globalThis.puter_gui_enabled === true)
142
+ this.env = 'gui';
143
+ else if (globalThis.WorkerGlobalScope) {
144
+ if (globalThis.ServiceWorkerGlobalScope) {
145
+ this.env = 'service-worker'
146
+ if (!globalThis.XMLHttpRequest) {
147
+ globalThis.XMLHttpRequest = xhrshim
148
+ }
149
+ if (!globalThis.location) {
150
+ globalThis.location = new URL("https://puter.site/");
151
+ }
152
+ // XHRShimGlobalize here
153
+ } else {
154
+ this.env = 'web-worker'
155
+ }
156
+ if (!globalThis.localStorage) {
157
+ globalThis.localStorage = localStorageMemory;
158
+ }
159
+ } else if (globalThis.process) {
160
+ this.env = 'nodejs';
161
+ if (!globalThis.localStorage) {
162
+ globalThis.localStorage = localStorageMemory;
163
+ }
164
+ if (!globalThis.XMLHttpRequest) {
165
+ globalThis.XMLHttpRequest = xhrshim
166
+ }
167
+ if (!globalThis.location) {
168
+ globalThis.location = new URL("https://nodejs.puter.site/");
169
+ }
170
+ if (!globalThis.addEventListener) {
171
+ globalThis.addEventListener = () => {} // API Stub
172
+ }
173
+ } else {
174
+ this.env = 'web';
175
+ }
176
+
177
+
178
+
179
+ // There are some specific situations where puter is definitely loaded in GUI mode
180
+ // we're going to check for those situations here so that we don't break anything unintentionally
181
+ // if navigator URL's hostname is 'puter.com'
182
+ if(this.env !== 'gui'){
183
+ // 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).
184
+ // This is necessary because the trailing dot can cause the hostname to not match the expected value.
185
+ let hostname = location.hostname.replace(/\.$/, '');
186
+
187
+ // Create a new URL object with the URL string
188
+ const url = new URL(PROD_ORIGIN);
189
+
190
+ // Extract hostname from the URL object
191
+ const gui_hostname = url.hostname;
192
+
193
+ // If the hostname matches the GUI hostname, then the SDK is running in the GUI environment
194
+ if(hostname === gui_hostname){
195
+ this.env = 'gui';
196
+ }
197
+ }
198
+
199
+ // Get the 'args' from the URL. This is used to pass arguments to the app.
200
+ if(URLParams.has('puter.args')){
201
+ this.args = JSON.parse(decodeURIComponent(URLParams.get('puter.args')));
202
+ }else{
203
+ this.args = {};
204
+ }
205
+
206
+ // Try to extract appInstanceID from the URL. appInstanceID is included in every messaage
207
+ // sent to the host environment. This is used to help host environment identify the app
208
+ // instance that sent the message and communicate back to it.
209
+ if(URLParams.has('puter.app_instance_id')){
210
+ this.appInstanceID = decodeURIComponent(URLParams.get('puter.app_instance_id'));
211
+ }
212
+
213
+ // Try to extract parentInstanceID from the URL. If another app launched this app instance, parentInstanceID
214
+ // holds its instance ID, and is used to communicate with that parent app.
215
+ if(URLParams.has('puter.parent_instance_id')){
216
+ this.parentInstanceID = decodeURIComponent(URLParams.get('puter.parent_instance_id'));
217
+ }
218
+
219
+ // Try to extract `puter.app.id` from the URL. `puter.app.id` is the unique ID of the app.
220
+ // App ID is useful for identifying the app when communicating with the Puter API, among other things.
221
+ if(URLParams.has('puter.app.id')){
222
+ this.appID = decodeURIComponent(URLParams.get('puter.app.id'));
223
+ }
224
+
225
+ // Extract app name (added later)
226
+ if(URLParams.has('puter.app.name')){
227
+ this.appName = decodeURIComponent(URLParams.get('puter.app.name'));
228
+ }
229
+
230
+ // Construct this App's AppData path based on the appID. AppData path is used to store files that are specific to this app.
231
+ // The default AppData path is `~/AppData/<appID>`.
232
+ if(this.appID){
233
+ this.appDataPath = `~/AppData/${this.appID}`;
234
+ }
235
+
236
+ // Construct APIOrigin from the URL. APIOrigin is used to build the URLs for the Puter API endpoints.
237
+ // The default APIOrigin is https://api.puter.com. However, if the URL contains a `puter.api_origin` query parameter,
238
+ // then that value is used as the APIOrigin. If the URL contains a `puter.domain` query parameter, then the APIOrigin
239
+ // is constructed as `https://api.<puter.domain>`.
240
+ // This should only be done when the SDK is running in 'app' mode.
241
+ this.APIOrigin = this.defaultAPIOrigin;
242
+ if(URLParams.has('puter.api_origin') && this.env === 'app'){
243
+ this.APIOrigin = decodeURIComponent(URLParams.get('puter.api_origin'));
244
+ }else if(URLParams.has('puter.domain') && this.env === 'app'){
245
+ this.APIOrigin = 'https://api.' + URLParams.get('puter.domain');
246
+ }
247
+
248
+ // === START :: Logger ===
249
+
250
+ // logger will log to console
251
+ let logger = new putility.libs.log.ConsoleLogger();
252
+
253
+ // logs can be toggled based on categories
254
+ logger = new putility.libs.log.CategorizedToggleLogger(
255
+ { delegate: logger });
256
+ const cat_logger = logger;
257
+
258
+ // create facade for easy logging
259
+ this.logger = new putility.libs.log.LoggerFacade({
260
+ impl: logger,
261
+ cat: cat_logger,
262
+ });
263
+
264
+ // Initialize API call logger
265
+ this.apiCallLogger = new APICallLogger({
266
+ enabled: false // Disabled by default
267
+ });
268
+
269
+ // === START :: Services === //
270
+
271
+ this.services.register('no-puter-yet', NoPuterYetService);
272
+ this.services.register('filesystem', FilesystemService);
273
+ this.services.register('api-access', APIAccessService);
274
+ this.services.register('xd-incoming', XDIncomingService);
275
+ if ( this.env !== 'app' ) {
276
+ this.services.register('fs-relay', FSRelayService);
277
+ }
278
+
279
+ // When api-access is initialized, bind `.authToken` and
280
+ // `.APIOrigin` as a 1-1 mapping with the `puter` global
281
+ (async () => {
282
+ await this.services.wait_for_init(['api-access']);
283
+ const svc_apiAccess = this.services.get('api-access');
284
+
285
+ svc_apiAccess.auth_token = this.authToken;
286
+ svc_apiAccess.api_origin = this.APIOrigin;
287
+ [
288
+ ['authToken','auth_token'],
289
+ ['APIOrigin','api_origin'],
290
+ ].forEach(([k1,k2]) => {
291
+ Object.defineProperty(this, k1, {
292
+ get () {
293
+ return svc_apiAccess[k2];
294
+ },
295
+ set (v) {
296
+ svc_apiAccess[k2] = v;
297
+ return true;
298
+ }
299
+ });
300
+ });
301
+ })();
302
+
303
+ // === Start :: Modules === //
304
+
305
+ // The SDK is running in the Puter GUI (i.e. 'gui')
306
+ if(this.env === 'gui'){
307
+ this.authToken = window.auth_token;
308
+ // initialize submodules
309
+ this.initSubmodules();
310
+ }
311
+ // Loaded in an iframe in the Puter GUI (i.e. 'app')
312
+ // When SDK is loaded in App mode the initiation process should start when the DOM is ready
313
+ else if (this.env === 'app') {
314
+ this.authToken = decodeURIComponent(URLParams.get('puter.auth.token'));
315
+ // initialize submodules
316
+ this.initSubmodules();
317
+ // If the authToken is already set in localStorage, then we don't need to show the dialog
318
+ try {
319
+ if(localStorage.getItem('puter.auth.token')){
320
+ this.setAuthToken(localStorage.getItem('puter.auth.token'));
321
+ }
322
+ // if appID is already set in localStorage, then we don't need to show the dialog
323
+ if(localStorage.getItem('puter.app.id')){
324
+ this.setAppID(localStorage.getItem('puter.app.id'));
325
+ }
326
+ } catch (error) {
327
+ // Handle the error here
328
+ console.error('Error accessing localStorage:', error);
329
+ }
330
+ }
331
+ // SDK was loaded in a 3rd-party website.
332
+ // When SDK is loaded in GUI the initiation process should start when the DOM is ready. This is because
333
+ // the SDK needs to show a dialog to the user to ask for permission to access their Puter account.
334
+ else if(this.env === 'web') {
335
+ // initialize submodules
336
+ this.initSubmodules();
337
+ try{
338
+ // If the authToken is already set in localStorage, then we don't need to show the dialog
339
+ if(localStorage.getItem('puter.auth.token')){
340
+ this.setAuthToken(localStorage.getItem('puter.auth.token'));
341
+ }
342
+ // if appID is already set in localStorage, then we don't need to show the dialog
343
+ if(localStorage.getItem('puter.app.id')){
344
+ this.setAppID(localStorage.getItem('puter.app.id'));
345
+ }
346
+ } catch (error) {
347
+ // Handle the error here
348
+ console.error('Error accessing localStorage:', error);
349
+ }
350
+ } else if (this.env === 'web-worker' || this.env === 'service-worker' || this.env === 'nodejs') {
351
+ this.initSubmodules();
352
+ }
353
+
354
+ // Add prefix logger (needed to happen after modules are initialized)
355
+ (async () => {
356
+ await this.services.wait_for_init(['api-access']);
357
+ const whoami = await this.auth.whoami();
358
+ logger = new putility.libs.log.PrefixLogger({
359
+ delegate: logger,
360
+ prefix: '[' +
361
+ (whoami?.app_name ?? this.appInstanceID ?? 'HOST') +
362
+ '] ',
363
+ });
364
+
365
+ this.logger.impl = logger;
366
+ })();
367
+
368
+ // Lock to prevent multiple requests to `/rao`
369
+ this.lock_rao_ = new putility.libs.promise.Lock();
370
+ // Promise that resolves when it's okay to request `/rao`
371
+ this.p_can_request_rao_ = new putility.libs.promise.TeePromise();
372
+ // Flag that indicates if a request to `/rao` has been made
373
+ this.rao_requested_ = false;
374
+
375
+ // In case we're already auth'd, request `/rao`
376
+ (async () => {
377
+ await this.services.wait_for_init(['api-access']);
378
+ this.p_can_request_rao_.resolve();
379
+ })();
380
+
381
+ this.net = {
382
+ generateWispV1URL: async () => {
383
+ const { token: wispToken, server: wispServer } = (await (await fetch(this.APIOrigin + '/wisp/relay-token/create', {
384
+ method: 'POST',
385
+ headers: {
386
+ Authorization: `Bearer ${this.authToken}`,
387
+ 'Content-Type': 'application/json',
388
+ },
389
+ body: JSON.stringify({}),
390
+ })).json());
391
+ return `${wispServer}/${wispToken}/`
392
+ },
393
+ Socket: PSocket,
394
+ tls: {
395
+ TLSSocket: PTLSSocket
396
+ },
397
+ fetch: pFetch
398
+ }
399
+
400
+ this.workers = new WorkersHandler(this.authToken);
401
+
402
+ // Initialize network connectivity monitoring and cache purging
403
+ this.initNetworkMonitoring();
404
+ }
405
+
406
+ /**
407
+ * @internal
408
+ * Makes a request to `/rao`. This method aquires a lock to prevent
409
+ * multiple requests, and is effectively idempotent.
410
+ */
411
+ async request_rao_ () {
412
+ await this.p_can_request_rao_;
413
+
414
+ if ( this.env === 'gui' ) {
415
+ return;
416
+ }
417
+
418
+ // setAuthToken is called more than once when auth completes, which
419
+ // causes multiple requests to /rao. This lock prevents that.
420
+ await this.lock_rao_.acquire();
421
+ if ( this.rao_requested_ ) {
422
+ this.lock_rao_.release();
423
+ return;
424
+ }
425
+
426
+ let had_error = false;
427
+ try {
428
+ const resp = await fetch(this.APIOrigin + '/rao', {
429
+ method: 'POST',
430
+ headers: {
431
+ Authorization: `Bearer ${this.authToken}`,
432
+ Origin: location.origin // This is ignored in the browser but needed for workers and nodejs
433
+ }
434
+ });
435
+ return await resp.json();
436
+ } catch (e) {
437
+ had_error = true;
438
+ console.error(e);
439
+ } finally {
440
+ this.lock_rao_.release();
441
+ }
442
+ if ( ! had_error ) {
443
+ this.rao_requested_ = true;
444
+ }
445
+ }
446
+
447
+ registerModule (name, cls, parameters = {}) {
448
+ const instance = new cls(this.context, parameters);
449
+ this.modules_.push(name);
450
+ this[name] = instance;
451
+ if ( instance._init ) instance._init({ puter: this });
452
+ }
453
+
454
+ updateSubmodules() {
455
+ // Update submodules with new auth token and API origin
456
+ for ( const name of this.modules_ ) {
457
+ if ( ! this[name] ) continue;
458
+ this[name]?.setAuthToken?.(this.authToken);
459
+ this[name]?.setAPIOrigin?.(this.APIOrigin);
460
+ }
461
+ }
462
+
463
+ setAppID = function (appID) {
464
+ // save to localStorage
465
+ try{
466
+ localStorage.setItem('puter.app.id', appID);
467
+ } catch (error) {
468
+ // Handle the error here
469
+ console.error('Error accessing localStorage:', error);
470
+ }
471
+ this.appID = appID;
472
+ }
473
+
474
+ setAuthToken = function (authToken) {
475
+ this.authToken = authToken;
476
+ // If the SDK is running on a 3rd-party site or an app, then save the authToken in localStorage
477
+ if(this.env === 'web' || this.env === 'app'){
478
+ try{
479
+ localStorage.setItem('puter.auth.token', authToken);
480
+ } catch (error) {
481
+ // Handle the error here
482
+ console.error('Error accessing localStorage:', error);
483
+ }
484
+ }
485
+ // reinitialize submodules
486
+ this.updateSubmodules();
487
+ // rao
488
+ this.request_rao_();
489
+ }
490
+
491
+ setAPIOrigin = function (APIOrigin) {
492
+ this.APIOrigin = APIOrigin;
493
+ // reinitialize submodules
494
+ this.updateSubmodules();
495
+ }
496
+
497
+ resetAuthToken = function () {
498
+ this.authToken = null;
499
+ // If the SDK is running on a 3rd-party site or an app, then save the authToken in localStorage
500
+ if(this.env === 'web' || this.env === 'app'){
501
+ try{
502
+ localStorage.removeItem('puter.auth.token');
503
+ } catch (error) {
504
+ // Handle the error here
505
+ console.error('Error accessing localStorage:', error);
506
+ }
507
+ }
508
+ // reinitialize submodules
509
+ this.updateSubmodules();
510
+ }
511
+
512
+ exit = function(statusCode = 0) {
513
+ if (statusCode && (typeof statusCode !== 'number')) {
514
+ console.warn('puter.exit() requires status code to be a number. Treating it as 1');
515
+ statusCode = 1;
516
+ }
517
+
518
+ globalThis.parent.postMessage({
519
+ msg: "exit",
520
+ appInstanceID: this.appInstanceID,
521
+ statusCode,
522
+ }, '*');
523
+ }
524
+
525
+ /**
526
+ * A function that generates a domain-safe name by combining a random adjective, a random noun, and a random number (between 0 and 9999).
527
+ * The result is returned as a string with components separated by hyphens.
528
+ * It is useful when you need to create unique identifiers that are also human-friendly.
529
+ *
530
+ * @param {string} [separateWith='-'] - The character to use to separate the components of the generated name.
531
+ * @returns {string} A unique, hyphen-separated string comprising of an adjective, a noun, and a number.
532
+ *
533
+ */
534
+ randName = function(separateWith = '-'){
535
+ const first_adj = ['helpful','sensible', 'loyal', 'honest', 'clever', 'capable','calm', 'smart', 'genius', 'bright', 'charming', 'creative', 'diligent', 'elegant', 'fancy',
536
+ 'colorful', 'avid', 'active', 'gentle', 'happy', 'intelligent', 'jolly', 'kind', 'lively', 'merry', 'nice', 'optimistic', 'polite',
537
+ 'quiet', 'relaxed', 'silly', 'victorious', 'witty', 'young', 'zealous', 'strong', 'brave', 'agile', 'bold'];
538
+
539
+ const nouns = ['street', 'roof', 'floor', 'tv', 'idea', 'morning', 'game', 'wheel', 'shoe', 'bag', 'clock', 'pencil', 'pen',
540
+ 'magnet', 'chair', 'table', 'house', 'dog', 'room', 'book', 'car', 'cat', 'tree',
541
+ 'flower', 'bird', 'fish', 'sun', 'moon', 'star', 'cloud', 'rain', 'snow', 'wind', 'mountain',
542
+ 'river', 'lake', 'sea', 'ocean', 'island', 'bridge', 'road', 'train', 'plane', 'ship', 'bicycle',
543
+ 'horse', 'elephant', 'lion', 'tiger', 'bear', 'zebra', 'giraffe', 'monkey', 'snake', 'rabbit', 'duck',
544
+ 'goose', 'penguin', 'frog', 'crab', 'shrimp', 'whale', 'octopus', 'spider', 'ant', 'bee', 'butterfly', 'dragonfly',
545
+ 'ladybug', 'snail', 'camel', 'kangaroo', 'koala', 'panda', 'piglet', 'sheep', 'wolf', 'fox', 'deer', 'mouse', 'seal',
546
+ 'chicken', 'cow', 'dinosaur', 'puppy', 'kitten', 'circle', 'square', 'garden', 'otter', 'bunny', 'meerkat', 'harp']
547
+
548
+ // return a random combination of first_adj + noun + number (between 0 and 9999)
549
+ // e.g. clever-idea-123
550
+ return first_adj[Math.floor(Math.random() * first_adj.length)] + separateWith + nouns[Math.floor(Math.random() * nouns.length)] + separateWith + Math.floor(Math.random() * 10000);
551
+ }
552
+
553
+ getUser = function(...args){
554
+ let options;
555
+
556
+ // If first argument is an object, it's the options
557
+ if (typeof args[0] === 'object' && args[0] !== null) {
558
+ options = args[0];
559
+ } else {
560
+ // Otherwise, we assume separate arguments are provided
561
+ options = {
562
+ success: args[0],
563
+ error: args[1],
564
+ };
565
+ }
566
+
567
+ return new Promise((resolve, reject) => {
568
+ const xhr = utils.initXhr('/whoami', this.APIOrigin, this.authToken, 'get');
569
+ // set up event handlers for load and error events
570
+ utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
571
+
572
+ xhr.send();
573
+ })
574
+ }
575
+
576
+ print = function(...args){
577
+ // Check if the last argument is an options object with escapeHTML or code property
578
+ let options = {};
579
+ if(args.length > 0 && typeof args[args.length - 1] === 'object' && args[args.length - 1] !== null &&
580
+ ('escapeHTML' in args[args.length - 1] || 'code' in args[args.length - 1])) {
581
+ options = args.pop();
582
+ }
583
+
584
+ for(let arg of args){
585
+ // Escape HTML if the option is set to true or if code option is true
586
+ if((options.escapeHTML === true || options.code === true) && typeof arg === 'string') {
587
+ arg = arg.replace(/&/g, '&amp;')
588
+ .replace(/</g, '&lt;')
589
+ .replace(/>/g, '&gt;')
590
+ .replace(/"/g, '&quot;')
591
+ .replace(/'/g, '&#039;');
592
+ }
593
+
594
+ // Wrap in code/pre tags if code option is true
595
+ if(options.code === true) {
596
+ arg = `<code><pre>${arg}</pre></code>`;
597
+ }
598
+
599
+ document.body.innerHTML += arg;
600
+ }
601
+ }
602
+
603
+ /**
604
+ * Configures API call logging settings
605
+ * @param {Object} config - Configuration options for API call logging
606
+ * @param {boolean} config.enabled - Enable/disable API call logging
607
+ * @param {boolean} config.enabled - Enable/disable API call logging
608
+ */
609
+ configureAPILogging = function(config = {}){
610
+ if (this.apiCallLogger) {
611
+ this.apiCallLogger.updateConfig(config);
612
+ }
613
+ return this;
614
+ }
615
+
616
+ /**
617
+ * Enables API call logging with optional configuration
618
+ * @param {Object} config - Optional configuration to apply when enabling
619
+ */
620
+ enableAPILogging = function(config = {}) {
621
+ if (this.apiCallLogger) {
622
+ this.apiCallLogger.updateConfig({ ...config, enabled: true });
623
+ }
624
+ return this;
625
+ }
626
+
627
+ /**
628
+ * Disables API call logging
629
+ */
630
+ disableAPILogging = function() {
631
+ if (this.apiCallLogger) {
632
+ this.apiCallLogger.disable();
633
+ }
634
+ return this;
635
+ }
636
+
637
+ /**
638
+ * Initializes network connectivity monitoring to purge cache when connection is lost
639
+ * @private
640
+ */
641
+ initNetworkMonitoring = function() {
642
+ // Only initialize in environments that support navigator.onLine and window events
643
+ if (typeof globalThis.navigator === 'undefined' ||
644
+ typeof globalThis.addEventListener !== 'function') {
645
+ return;
646
+ }
647
+
648
+ // Track previous online state
649
+ let wasOnline = navigator.onLine;
650
+
651
+ // Function to handle network state changes
652
+ const handleNetworkChange = () => {
653
+ const isOnline = navigator.onLine;
654
+
655
+ // If we went from online to offline, purge the cache
656
+ if (wasOnline && !isOnline) {
657
+ console.log('Network connection lost - purging cache');
658
+ this.purgeCache();
659
+ }
660
+
661
+ // Update the previous state
662
+ wasOnline = isOnline;
663
+ };
664
+
665
+ // Listen for online/offline events
666
+ globalThis.addEventListener('online', handleNetworkChange);
667
+ globalThis.addEventListener('offline', handleNetworkChange);
668
+
669
+ // Also listen for visibility change as an additional indicator
670
+ // (some browsers don't fire offline events reliably)
671
+ if (typeof document !== 'undefined') {
672
+ document.addEventListener('visibilitychange', () => {
673
+ // Small delay to allow network state to update
674
+ setTimeout(handleNetworkChange, 100);
675
+ });
676
+ }
677
+ }
678
+
679
+ /**
680
+ * Purges all cached data
681
+ * @public
682
+ */
683
+ purgeCache = function() {
684
+ try {
685
+ if (this._cache && typeof this._cache.flushall === 'function') {
686
+ this._cache.flushall();
687
+ console.log('Cache purged successfully');
688
+ } else {
689
+ console.warn('Cache purge failed: cache instance not available');
690
+ }
691
+ } catch (error) {
692
+ console.error('Error purging cache:', error);
693
+ }
694
+ }
695
+
696
+ }
697
+
698
+ // Create a new Puter object and return it
699
+ const puterobj = new Puter();
700
+
701
+ // Return the Puter object
702
+ return puterobj;
703
+ }());
704
+
705
+ globalThis.addEventListener('message', async (event) => {
706
+ // if the message is not from Puter, then ignore it
707
+ if(event.origin !== puter.defaultGUIOrigin) return;
708
+
709
+ if(event.data.msg && event.data.msg === 'requestOrigin'){
710
+ event.source.postMessage({
711
+ msg: "originResponse",
712
+ }, '*');
713
+ }
714
+ else if (event.data.msg === 'puter.token') {
715
+ // puterDialog.close();
716
+ // Set the authToken property
717
+ puter.setAuthToken(event.data.token);
718
+ // update appID
719
+ puter.setAppID(event.data.app_uid);
720
+ // Remove the event listener to avoid memory leaks
721
+ // window.removeEventListener('message', messageListener);
722
+
723
+ puter.puterAuthState.authGranted = true;
724
+ // Resolve the promise
725
+ // resolve();
726
+
727
+ // Call onAuth callback
728
+ if(puter.onAuth && typeof puter.onAuth === 'function'){
729
+ puter.getUser().then((user) => {
730
+ puter.onAuth(user)
731
+ });
732
+ }
733
+
734
+ puter.puterAuthState.isPromptOpen = false;
735
+ // Resolve or reject any waiting promises.
736
+ if (puter.puterAuthState.resolver) {
737
+ if (puter.puterAuthState.authGranted) {
738
+ puter.puterAuthState.resolver.resolve();
739
+ } else {
740
+ puter.puterAuthState.resolver.reject();
741
+ }
742
+ puter.puterAuthState.resolver = null;
743
+ };
744
+ }
745
+ })