@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.
- package/APACHE_LICENSE.txt +201 -0
- package/README.md +88 -0
- package/doc/devlog.md +49 -0
- package/package.json +31 -0
- package/src/bg.png +0 -0
- package/src/bg.webp +0 -0
- package/src/index.js +745 -0
- package/src/lib/APICallLogger.js +110 -0
- package/src/lib/EventListener.js +51 -0
- package/src/lib/RequestError.js +6 -0
- package/src/lib/filesystem/APIFS.js +73 -0
- package/src/lib/filesystem/CacheFS.js +243 -0
- package/src/lib/filesystem/PostMessageFS.js +40 -0
- package/src/lib/filesystem/definitions.js +39 -0
- package/src/lib/path.js +509 -0
- package/src/lib/polyfills/localStorage.js +92 -0
- package/src/lib/polyfills/xhrshim.js +233 -0
- package/src/lib/socket.io/socket.io.esm.min.js +7 -0
- package/src/lib/socket.io/socket.io.esm.min.js.map +1 -0
- package/src/lib/socket.io/socket.io.js +4385 -0
- package/src/lib/socket.io/socket.io.js.map +1 -0
- package/src/lib/socket.io/socket.io.min.js +7 -0
- package/src/lib/socket.io/socket.io.min.js.map +1 -0
- package/src/lib/socket.io/socket.io.msgpack.min.js +7 -0
- package/src/lib/socket.io/socket.io.msgpack.min.js.map +1 -0
- package/src/lib/utils.js +620 -0
- package/src/lib/xdrpc.js +104 -0
- package/src/modules/AI.js +680 -0
- package/src/modules/Apps.js +215 -0
- package/src/modules/Auth.js +171 -0
- package/src/modules/Debug.js +39 -0
- package/src/modules/Drivers.js +278 -0
- package/src/modules/FSItem.js +139 -0
- package/src/modules/FileSystem/index.js +187 -0
- package/src/modules/FileSystem/operations/copy.js +64 -0
- package/src/modules/FileSystem/operations/deleteFSEntry.js +59 -0
- package/src/modules/FileSystem/operations/getReadUrl.js +42 -0
- package/src/modules/FileSystem/operations/mkdir.js +62 -0
- package/src/modules/FileSystem/operations/move.js +75 -0
- package/src/modules/FileSystem/operations/read.js +46 -0
- package/src/modules/FileSystem/operations/readdir.js +102 -0
- package/src/modules/FileSystem/operations/rename.js +58 -0
- package/src/modules/FileSystem/operations/sign.js +103 -0
- package/src/modules/FileSystem/operations/space.js +40 -0
- package/src/modules/FileSystem/operations/stat.js +95 -0
- package/src/modules/FileSystem/operations/symlink.js +55 -0
- package/src/modules/FileSystem/operations/upload.js +440 -0
- package/src/modules/FileSystem/operations/write.js +65 -0
- package/src/modules/FileSystem/utils/getAbsolutePathForApp.js +21 -0
- package/src/modules/Hosting.js +138 -0
- package/src/modules/KV.js +301 -0
- package/src/modules/OS.js +95 -0
- package/src/modules/Perms.js +109 -0
- package/src/modules/PuterDialog.js +481 -0
- package/src/modules/Threads.js +75 -0
- package/src/modules/UI.js +1555 -0
- package/src/modules/Util.js +38 -0
- package/src/modules/Workers.js +120 -0
- package/src/modules/networking/PSocket.js +87 -0
- package/src/modules/networking/PTLS.js +100 -0
- package/src/modules/networking/PWispHandler.js +89 -0
- package/src/modules/networking/parsers.js +157 -0
- package/src/modules/networking/requests.js +282 -0
- package/src/services/APIAccess.js +46 -0
- package/src/services/FSRelay.js +20 -0
- package/src/services/Filesystem.js +122 -0
- package/src/services/NoPuterYet.js +20 -0
- package/src/services/XDIncoming.js +44 -0
- package/test/ai.test.js +214 -0
- package/test/fs.test.js +798 -0
- package/test/index.html +1183 -0
- package/test/kv.test.js +548 -0
- package/test/txt2speech.test.js +178 -0
- 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, '&')
|
|
588
|
+
.replace(/</g, '<')
|
|
589
|
+
.replace(/>/g, '>')
|
|
590
|
+
.replace(/"/g, '"')
|
|
591
|
+
.replace(/'/g, ''');
|
|
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
|
+
})
|