diff --git a/src/puter-js/README.md b/src/puter-js/README.md index 8167449c..8d018d57 100644 --- a/src/puter-js/README.md +++ b/src/puter-js/README.md @@ -12,16 +12,37 @@ npm install @heyputer/puter.js ### Importing +### Node.js (with Auth Token) + +```js +const {init} = require("@heyputer/puter.js/src/init.cjs"); // NODE JS ONLY +// or +import {init} from "@heyputer/puter.js/src/init.cjs"; + +const puter = init(process.env.puterAuthToken); // uses your auth token +const puter2 = init(process.env.puterAuthToken2); // use some other auth token +``` + +### Browser (without Auth Token) + #### ES Modules ```js -import '@heyputer/puter.js'; +import {puter} from '@heyputer/puter.js'; +// or +import puter from '@heyputer/puter.js'; +// or +import '@heyputer/puter.js'; // puter will be available globally ``` #### CommonJS ```js +const {puter} = require('@heyputer/puter.js'); +// or const puter = require('@heyputer/puter.js'); +// or +require('@heyputer/puter.js'); // puter will be available globally ``` #### CDN diff --git a/src/puter-js/index.d.ts b/src/puter-js/index.d.ts index 72c452cc..b772c694 100644 --- a/src/puter-js/index.d.ts +++ b/src/puter-js/index.d.ts @@ -1,479 +1,496 @@ +// index.d.ts + declare global { interface Window { puter: Puter; } } -declare namespace Puter { - // Main Puter interface - interface Puter { - // Properties - appID: string; - env: 'app' | 'web' | 'gui'; +declare class Puter { + // Properties + appID: string; + env: 'app' | 'web' | 'gui'; - // Utility methods - print(text: string, options?: { code?: boolean }): void; - randName(separator?: string): string; - exit(statusCode?: number): void; + // Utility methods + print(text: string, options?: { code?: boolean }): void; + randName(separator?: string): string; + exit(statusCode?: number): void; - // Sub-modules - ai: AI; - apps: Apps; - auth: Auth; - drivers: Drivers; - fs: FileSystem; - hosting: Hosting; - kv: KeyValue; - net: Networking; - perms: Permissions; - ui: UI; - workers: Workers; - } - - // AI Module - interface AI { - chat(prompt: string, options?: ChatOptions): Promise; - chat(prompt: string, testMode?: boolean, options?: ChatOptions): Promise; - chat(prompt: string, imageURL?: string, testMode?: boolean, options?: ChatOptions): Promise; - chat(prompt: string, imageURLArray?: string[], testMode?: boolean, options?: ChatOptions): Promise; - chat(messages: ChatMessage[], testMode?: boolean, options?: ChatOptions): Promise; - - img2txt(image: string | File | Blob, testMode?: boolean): Promise; - - txt2img(prompt: string, testMode?: boolean): Promise; - txt2img(prompt: string, options?: Txt2ImgOptions): Promise; - - txt2speech(text: string): Promise; - txt2speech(text: string, options?: Txt2SpeechOptions): Promise; - txt2speech(text: string, language?: string): Promise; - txt2speech(text: string, language?: string, voice?: string): Promise; - txt2speech(text: string, language?: string, voice?: string, engine?: string): Promise; - } - - interface ChatOptions { - model?: string; - stream?: boolean; - max_tokens?: number; - temperature?: number; - tools?: ToolDefinition[]; - } - - interface ToolDefinition { - type: 'function'; - function: { - name: string; - description: string; - parameters: object; - strict?: boolean; - }; - } - - interface ChatMessage { - role: 'system' | 'assistant' | 'user' | 'function' | 'tool'; - content: string | ContentObject[]; - tool_call_id?: string; - } - - interface ContentObject { - type: 'text' | 'file'; - text?: string; - puter_path?: string; - } - - interface ChatResponse { - message: { - role: string; - content: string; - tool_calls?: ToolCall[]; - }; - } - - interface ToolCall { - id: string; - function: { - name: string; - arguments: string; - }; - } - - interface Txt2ImgOptions { - model?: 'gpt-image-1' | 'gemini-2.5-flash-image-preview' | 'dall-e-3'; - quality?: 'high' | 'medium' | 'low' | 'hd' | 'standard'; - input_image?: string; - input_image_mime_type?: string; - } - - interface Txt2SpeechOptions { - language?: string; - voice?: string; - engine?: 'standard' | 'neural' | 'generative'; - } - - // Apps Module - interface Apps { - create(name: string, indexURL: string): Promise; - create(name: string, indexURL: string, title?: string): Promise; - create(name: string, indexURL: string, title?: string, description?: string): Promise; - create(options: CreateAppOptions): Promise; - - delete(name: string): Promise; - get(name: string, options?: GetAppOptions): Promise; - list(options?: ListAppOptions): Promise; - update(name: string, attributes: UpdateAppAttributes): Promise; - } - - interface CreateAppOptions { - name: string; - indexURL: string; - title?: string; - description?: string; - icon?: string; - maximizeOnStart?: boolean; - filetypeAssociations?: string[]; - } - - interface GetAppOptions { - stats_period?: StatsPeriod; - icon_size?: null | 16 | 32 | 64 | 128 | 256 | 512; - } - - interface ListAppOptions extends GetAppOptions { } - - interface UpdateAppAttributes { - name?: string; - indexURL?: string; - title?: string; - description?: string; - icon?: string; - maximizeOnStart?: boolean; - filetypeAssociations?: string[]; - } - - type StatsPeriod = 'all' | 'today' | 'yesterday' | '7d' | '30d' | 'this_month' | 'last_month' | 'this_year' | 'last_year' | 'month_to_date' | 'year_to_date' | 'last_12_months'; - - interface App { - uid: string; - name: string; - icon: string; - description: string; - title: string; - maximize_on_start: boolean; - index_url: string; - created_at: string; - background: boolean; - filetype_associations: string[]; - open_count: number; - user_count: number; - } - - // Auth Module - interface Auth { - signIn(options?: { attempt_temp_user_creation?: boolean }): Promise; - signOut(): void; - isSignedIn(): boolean; - getUser(): Promise; - } - - interface User { - uuid: string; - username: string; - email_confirmed: boolean; - } - - // Drivers Module - interface Drivers { - call(interface: string, driver: string, method: string, args?: object): Promise; - } - - // FileSystem Module - interface FileSystem { - copy(source: string, destination: string, options?: CopyOptions): Promise; - delete(path: string, options?: DeleteOptions): Promise; - getReadURL(path: string, expiresIn?: number): Promise; - mkdir(path: string, options?: MkdirOptions): Promise; - move(source: string, destination: string, options?: MoveOptions): Promise; - read(path: string, options?: ReadOptions): Promise; - readdir(path: string, options?: ReaddirOptions): Promise; - readdir(options?: ReaddirOptions): Promise; - rename(path: string, newName: string): Promise; - space(): Promise; - stat(path: string): Promise; - upload(items: FileList | File[] | Blob[], dirPath?: string, options?: object): Promise; - write(path: string, data?: string | File | Blob, options?: WriteOptions): Promise; - } - - interface CopyOptions { - overwrite?: boolean; - dedupeName?: boolean; - newName?: string; - } - - interface DeleteOptions { - recursive?: boolean; - descendantsOnly?: boolean; - } - - interface MkdirOptions { - overwrite?: boolean; - dedupeName?: boolean; - createMissingParents?: boolean; - } - - interface MoveOptions extends CopyOptions { - createMissingParents?: boolean; - } - - interface ReadOptions { - offset?: number; - byte_count?: number; - } - - interface ReaddirOptions { - path?: string; - uid?: string; - } - - interface WriteOptions { - overwrite?: boolean; - dedupeName?: boolean; - createMissingParents?: boolean; - } - - interface SpaceInfo { - capacity: number; - used: number; - } - - interface FSItem { - id: string; - uid: string; - name: string; - path: string; - is_dir: boolean; - parent_id: string; - parent_uid: string; - created: number; - modified: number; - accessed: number; - size: number | null; - writable: boolean; - read(): Promise; - readdir(): Promise; - } - - // Hosting Module - interface Hosting { - create(subdomain: string, dirPath?: string): Promise; - delete(subdomain: string): Promise; - get(subdomain: string): Promise; - list(): Promise; - update(subdomain: string, dirPath?: string): Promise; - } - - interface Subdomain { - uid: string; - subdomain: string; - root_dir: FSItem; - } - - // KeyValue Module - interface KeyValue { - set(key: string, value: string | number | boolean | object | any[]): Promise; - get(key: string): Promise; - del(key: string): Promise; - incr(key: string, amount?: number): Promise; - decr(key: string, amount?: number): Promise; - list(pattern?: string, returnValues?: boolean): Promise; - list(returnValues?: boolean): Promise; - flush(): Promise; - } - - interface KeyValuePair { - key: string; - value: any; - } - - // Networking Module - interface Networking { - fetch(url: string, options?: RequestInit): Promise; - Socket: typeof Socket; - tls: { - TLSSocket: typeof TLSSocket; - }; - } - - class Socket { - constructor(hostname: string, port: number); - write(data: ArrayBuffer | Uint8Array | string): void; - close(): void; - on(event: 'open', callback: () => void): void; - on(event: 'data', callback: (buffer: Uint8Array) => void): void; - on(event: 'error', callback: (reason: string) => void): void; - on(event: 'close', callback: (hadError: boolean) => void): void; - } - - class TLSSocket extends Socket { - constructor(hostname: string, port: number); - } - - // Permissions Module - interface Permissions { - grantApp(app_uid: string, permissionString: string): Promise; - grantAppAnyUser(app_uid: string, permissionString: string): Promise; - grantGroup(group_uid: string, permissionString: string): Promise; - grantOrigin(origin: string, permissionString: string): Promise; - grantUser(username: string, permissionString: string): Promise; - revokeApp(app_uid: string, permissionString: string): Promise; - revokeAppAnyUser(app_uid: string, permissionString: string): Promise; - revokeGroup(group_uid: string, permissionString: string): Promise; - revokeOrigin(origin: string, permissionString: string): Promise; - revokeUser(username: string, permissionString: string): Promise; - } - - // UI Module - interface UI { - alert(message?: string, buttons?: AlertButton[]): Promise; - prompt(message?: string, defaultValue?: string): Promise; - authenticateWithPuter(): Promise; - contextMenu(options: ContextMenuOptions): void; - createWindow(options?: WindowOptions): void; - exit(statusCode?: number): void; - getLanguage(): Promise; - hideSpinner(): void; - launchApp(appName?: string, args?: object): Promise; - launchApp(options: LaunchAppOptions): Promise; - on(eventName: 'localeChanged', handler: (data: { language: string }) => void): void; - on(eventName: 'themeChanged', handler: (data: ThemeData) => void): void; - onItemsOpened(handler: (items: FSItem[]) => void): void; - onLaunchedWithItems(handler: (items: FSItem[]) => void): void; - onWindowClose(handler: () => void): void; - parentApp(): AppConnection | null; - setMenubar(options: MenubarOptions): void; - setWindowHeight(height: number): void; - setWindowPosition(x: number, y: number): void; - setWindowSize(width: number, height: number): void; - setWindowTitle(title: string): void; - setWindowWidth(width: number): void; - setWindowX(x: number): void; - setWindowY(y: number): void; - showColorPicker(defaultColor?: string): Promise; - showColorPicker(options?: object): Promise; - showDirectoryPicker(options?: { multiple?: boolean }): Promise; - showFontPicker(defaultFont?: string): Promise<{ fontFamily: string }>; - showFontPicker(options?: object): Promise<{ fontFamily: string }>; - showOpenFilePicker(options?: FilePickerOptions): Promise; - showSaveFilePicker(data?: any, defaultFileName?: string): Promise; - showSpinner(): void; - socialShare(url: string, message?: string, options?: { left?: number; top?: number }): void; - wasLaunchedWithItems(): boolean; - } - - interface AlertButton { - label: string; - value?: string; - type?: 'primary' | 'success' | 'info' | 'warning' | 'danger'; - } - - interface ContextMenuOptions { - items: (ContextMenuItem | '-')[]; - } - - interface ContextMenuItem { - label: string; - action?: () => void; - icon?: string; - icon_active?: string; - disabled?: boolean; - items?: (ContextMenuItem | '-')[]; - } - - interface WindowOptions { - center?: boolean; - content?: string; - disable_parent_window?: boolean; - has_head?: boolean; - height?: number; - is_resizable?: boolean; - show_in_taskbar?: boolean; - title?: string; - width?: number; - } - - interface LaunchAppOptions { - name?: string; - args?: object; - } - - interface ThemeData { - palette: { - primaryHue: number; - primarySaturation: string; - primaryLightness: string; - primaryAlpha: number; - primaryColor: string; - }; - } - - interface MenubarOptions { - items: MenuItem[]; - } - - interface MenuItem { - label: string; - action?: () => void; - items?: MenuItem[]; - } - - interface FilePickerOptions { - multiple?: boolean; - accept?: string | string[]; - } - - interface AppConnection { - usesSDK: boolean; - on(eventName: 'message', handler: (message: any) => void): void; - on(eventName: 'close', handler: (data: { appInstanceID: string }) => void): void; - off(eventName: string, handler: Function): void; - postMessage(message: any): void; - close(): void; - } - - // Workers Module - interface Workers { - create(workerName: string, filePath: string): Promise; - delete(workerName: string): Promise; - exec(workerURL: string, options?: WorkerExecOptions): Promise; - get(workerName: string): Promise; - list(): Promise; - } - - interface WorkerDeployment { - success: boolean; - url: string; - errors: any[]; - } - - interface WorkerExecOptions extends RequestInit { - method?: string; - headers?: object; - body?: string | object; - cache?: RequestCache; - credentials?: RequestCredentials; - mode?: RequestMode; - redirect?: RequestRedirect; - referrer?: string; - signal?: AbortSignal; - } - - interface WorkerInfo { - name: string; - url: string; - file_path: string; - file_uid: string; - created_at: string; - } + // Sub-modules + ai: AI; + apps: Apps; + auth: Auth; + drivers: Drivers; + fs: FileSystem; + hosting: Hosting; + kv: KV; + net: Networking; + perms: Permissions; + ui: UI; + workers: Workers; } -declare const puter: Puter.Puter; +// AI Module +interface AI { + chat(prompt: string, options?: ChatOptions): Promise; + chat(prompt: string, testMode?: boolean, options?: ChatOptions): Promise; + chat(prompt: string, imageURL?: string, testMode?: boolean, options?: ChatOptions): Promise; + chat(prompt: string, imageURLArray?: string[], testMode?: boolean, options?: ChatOptions): Promise; + chat(messages: ChatMessage[], testMode?: boolean, options?: ChatOptions): Promise; + + img2txt(image: string | File | Blob, testMode?: boolean): Promise; + + txt2img(prompt: string, testMode?: boolean): Promise; + txt2img(prompt: string, options?: Txt2ImgOptions): Promise; + + txt2speech(text: string): Promise; + txt2speech(text: string, options?: Txt2SpeechOptions): Promise; + txt2speech(text: string, language?: string): Promise; + txt2speech(text: string, language?: string, voice?: string): Promise; + txt2speech(text: string, language?: string, voice?: string, engine?: string): Promise; +} + +interface ChatOptions { + model?: string; + stream?: boolean; + max_tokens?: number; + temperature?: number; + tools?: ToolDefinition[]; +} + +interface ToolDefinition { + type: 'function'; + function: { + name: string; + description: string; + parameters: object; + strict?: boolean; + }; +} + +interface ChatMessage { + role: 'system' | 'assistant' | 'user' | 'function' | 'tool'; + content: string | ContentObject[]; + tool_call_id?: string; +} + +interface ContentObject { + type: 'text' | 'file'; + text?: string; + puter_path?: string; +} + +interface ChatResponse { + message: { + role: string; + content: string; + tool_calls?: ToolCall[]; + }; +} + +interface ToolCall { + id: string; + function: { + name: string; + arguments: string; + }; +} + +interface Txt2ImgOptions { + model?: 'gpt-image-1' | 'gemini-2.5-flash-image-preview' | 'dall-e-3'; + quality?: 'high' | 'medium' | 'low' | 'hd' | 'standard'; + input_image?: string; + input_image_mime_type?: string; +} + +interface Txt2SpeechOptions { + language?: string; + voice?: string; + engine?: 'standard' | 'neural' | 'generative'; +} + +// Apps Module +interface Apps { + create(name: string, indexURL: string): Promise; + create(name: string, indexURL: string, title?: string): Promise; + create(name: string, indexURL: string, title?: string, description?: string): Promise; + create(options: CreateAppOptions): Promise; + + delete(name: string): Promise; + get(name: string, options?: GetAppOptions): Promise; + list(options?: ListAppOptions): Promise; + update(name: string, attributes: UpdateAppAttributes): Promise; +} + +interface CreateAppOptions { + name: string; + indexURL: string; + title?: string; + description?: string; + icon?: string; + maximizeOnStart?: boolean; + filetypeAssociations?: string[]; +} + +interface GetAppOptions { + stats_period?: StatsPeriod; + icon_size?: null | 16 | 32 | 64 | 128 | 256 | 512; +} + +interface ListAppOptions extends GetAppOptions { } + +interface UpdateAppAttributes { + name?: string; + indexURL?: string; + title?: string; + description?: string; + icon?: string; + maximizeOnStart?: boolean; + filetypeAssociations?: string[]; +} + +type StatsPeriod = 'all' | 'today' | 'yesterday' | '7d' | '30d' | 'this_month' | 'last_month' | 'this_year' | 'last_year' | 'month_to_date' | 'year_to_date' | 'last_12_months'; + +interface App { + uid: string; + name: string; + icon: string; + description: string; + title: string; + maximize_on_start: boolean; + index_url: string; + created_at: string; + background: boolean; + filetype_associations: string[]; + open_count: number; + user_count: number; +} + +// Auth Module +interface Auth { + signIn(options?: { attempt_temp_user_creation?: boolean }): Promise; + signOut(): void; + isSignedIn(): boolean; + getUser(): Promise; +} + +interface User { + uuid: string; + username: string; + email_confirmed: boolean; +} + +// Drivers Module +interface Drivers { + call(interface: string, driver: string, method: string, args?: object): Promise; +} + +// FileSystem Module +interface FileSystem { + copy(source: string, destination: string, options?: CopyOptions): Promise; + delete(path: string, options?: DeleteOptions): Promise; + getReadURL(path: string, expiresIn?: number): Promise; + mkdir(path: string, options?: MkdirOptions): Promise; + move(source: string, destination: string, options?: MoveOptions): Promise; + read(path: string, options?: ReadOptions): Promise; + readdir(path: string, options?: ReaddirOptions): Promise; + readdir(options?: ReaddirOptions): Promise; + rename(path: string, newName: string): Promise; + space(): Promise; + stat(path: string): Promise; + upload(items: FileList | File[] | Blob[], dirPath?: string, options?: object): Promise; + write(path: string, data?: string | File | Blob, options?: WriteOptions): Promise; +} + +interface CopyOptions { + overwrite?: boolean; + dedupeName?: boolean; + newName?: string; +} + +interface DeleteOptions { + recursive?: boolean; + descendantsOnly?: boolean; +} + +interface MkdirOptions { + overwrite?: boolean; + dedupeName?: boolean; + createMissingParents?: boolean; +} + +interface MoveOptions extends CopyOptions { + createMissingParents?: boolean; +} + +interface ReadOptions { + offset?: number; + byte_count?: number; +} + +interface ReaddirOptions { + path?: string; + uid?: string; +} + +interface WriteOptions { + overwrite?: boolean; + dedupeName?: boolean; + createMissingParents?: boolean; +} + +interface SpaceInfo { + capacity: number; + used: number; +} + +interface FSItem { + id: string; + uid: string; + name: string; + path: string; + is_dir: boolean; + parent_id: string; + parent_uid: string; + created: number; + modified: number; + accessed: number; + size: number | null; + writable: boolean; + read(): Promise; + readdir(): Promise; +} + +// Hosting Module +interface Hosting { + create(subdomain: string, dirPath?: string): Promise; + delete(subdomain: string): Promise; + get(subdomain: string): Promise; + list(): Promise; + update(subdomain: string, dirPath?: string): Promise; +} + +interface Subdomain { + uid: string; + subdomain: string; + root_dir: FSItem; +} + +// Key-Value Store Module +interface KV { + set(key: string, value: string | number | boolean | object | any[]): Promise; + get(key: string): Promise; + del(key: string): Promise; + incr(key: string, amount?: number): Promise; + decr(key: string, amount?: number): Promise; + list(pattern?: string, returnValues?: boolean): Promise; + list(returnValues?: boolean): Promise; + flush(): Promise; +} + +interface KVPair { + key: string; + value: any; +} + +// Networking Module +interface Networking { + fetch(url: string, options?: RequestInit): Promise; + Socket: typeof Socket; + tls: { + TLSSocket: typeof TLSSocket; + }; +} + +declare class Socket { + constructor(hostname: string, port: number); + write(data: ArrayBuffer | Uint8Array | string): void; + close(): void; + on(event: 'open', callback: () => void): void; + on(event: 'data', callback: (buffer: Uint8Array) => void): void; + on(event: 'error', callback: (reason: string) => void): void; + on(event: 'close', callback: (hadError: boolean) => void): void; +} + +declare class TLSSocket extends Socket { + constructor(hostname: string, port: number); +} + +// Permissions Module +interface Permissions { + grantApp(app_uid: string, permissionString: string): Promise; + grantAppAnyUser(app_uid: string, permissionString: string): Promise; + grantGroup(group_uid: string, permissionString: string): Promise; + grantOrigin(origin: string, permissionString: string): Promise; + grantUser(username: string, permissionString: string): Promise; + revokeApp(app_uid: string, permissionString: string): Promise; + revokeAppAnyUser(app_uid: string, permissionString: string): Promise; + revokeGroup(group_uid: string, permissionString: string): Promise; + revokeOrigin(origin: string, permissionString: string): Promise; + revokeUser(username: string, permissionString: string): Promise; +} + +// UI Module +interface UI { + alert(message?: string, buttons?: AlertButton[]): Promise; + prompt(message?: string, defaultValue?: string): Promise; + authenticateWithPuter(): Promise; + contextMenu(options: ContextMenuOptions): void; + createWindow(options?: WindowOptions): void; + exit(statusCode?: number): void; + getLanguage(): Promise; + hideSpinner(): void; + launchApp(appName?: string, args?: object): Promise; + launchApp(options: LaunchAppOptions): Promise; + on(eventName: 'localeChanged', handler: (data: { language: string }) => void): void; + on(eventName: 'themeChanged', handler: (data: ThemeData) => void): void; + onItemsOpened(handler: (items: FSItem[]) => void): void; + onLaunchedWithItems(handler: (items: FSItem[]) => void): void; + onWindowClose(handler: () => void): void; + parentApp(): AppConnection | null; + setMenubar(options: MenubarOptions): void; + setWindowHeight(height: number): void; + setWindowPosition(x: number, y: number): void; + setWindowSize(width: number, height: number): void; + setWindowTitle(title: string): void; + setWindowWidth(width: number): void; + setWindowX(x: number): void; + setWindowY(y: number): void; + showColorPicker(defaultColor?: string): Promise; + showColorPicker(options?: object): Promise; + showDirectoryPicker(options?: { multiple?: boolean }): Promise; + showFontPicker(defaultFont?: string): Promise<{ fontFamily: string }>; + showFontPicker(options?: object): Promise<{ fontFamily: string }>; + showOpenFilePicker(options?: FilePickerOptions): Promise; + showSaveFilePicker(data?: any, defaultFileName?: string): Promise; + showSpinner(): void; + socialShare(url: string, message?: string, options?: { left?: number; top?: number }): void; + wasLaunchedWithItems(): boolean; +} + +interface AlertButton { + label: string; + value?: string; + type?: 'primary' | 'success' | 'info' | 'warning' | 'danger'; +} + +interface ContextMenuOptions { + items: (ContextMenuItem | '-')[]; +} + +interface ContextMenuItem { + label: string; + action?: () => void; + icon?: string; + icon_active?: string; + disabled?: boolean; + items?: (ContextMenuItem | '-')[]; +} + +interface WindowOptions { + center?: boolean; + content?: string; + disable_parent_window?: boolean; + has_head?: boolean; + height?: number; + is_resizable?: boolean; + show_in_taskbar?: boolean; + title?: string; + width?: number; +} + +interface LaunchAppOptions { + name?: string; + args?: object; +} + +interface ThemeData { + palette: { + primaryHue: number; + primarySaturation: string; + primaryLightness: string; + primaryAlpha: number; + primaryColor: string; + }; +} + +interface MenubarOptions { + items: MenuItem[]; +} + +interface MenuItem { + label: string; + action?: () => void; + items?: MenuItem[]; +} + +interface FilePickerOptions { + multiple?: boolean; + accept?: string | string[]; +} + +interface AppConnection { + usesSDK: boolean; + on(eventName: 'message', handler: (message: any) => void): void; + on(eventName: 'close', handler: (data: { appInstanceID: string }) => void): void; + off(eventName: string, handler: Function): void; + postMessage(message: any): void; + close(): void; +} + +// Workers Module +interface Workers { + create(workerName: string, filePath: string): Promise; + delete(workerName: string): Promise; + exec(workerURL: string, options?: WorkerExecOptions): Promise; + get(workerName: string): Promise; + list(): Promise; +} + +interface WorkerDeployment { + success: boolean; + url: string; + errors: any[]; +} + +interface WorkerExecOptions extends RequestInit { + method?: string; + headers?: object; + body?: string | object; + cache?: RequestCache; + credentials?: RequestCredentials; + mode?: RequestMode; + redirect?: RequestRedirect; + referrer?: string; + signal?: AbortSignal; +} + +interface WorkerInfo { + name: string; + url: string; + file_path: string; + file_uid: string; + created_at: string; +} + +// Global puter instance +declare const puter: Puter; + +// Export the Puter class as both default and named export +export default puter; +export { puter }; + +// Also export all the interfaces for users who want to use them +export { + AI, AlertButton, App, AppConnection, Apps, + Auth, ChatMessage, ChatOptions, ChatResponse, ContentObject, ContextMenuItem, ContextMenuOptions, CopyOptions, CreateAppOptions, DeleteOptions, Drivers, FilePickerOptions, FileSystem, FSItem, GetAppOptions, Hosting, + KV, + KVPair, LaunchAppOptions, MenubarOptions, + MenuItem, MkdirOptions, + MoveOptions, Networking, + Permissions, Puter, ReaddirOptions, ReadOptions, SpaceInfo, StatsPeriod, Subdomain, ThemeData, ToolCall, ToolDefinition, Txt2ImgOptions, + Txt2SpeechOptions, UI, UpdateAppAttributes, User, WindowOptions, WorkerDeployment, + WorkerExecOptions, + WorkerInfo, Workers, WriteOptions +}; -export = Puter; \ No newline at end of file diff --git a/src/puter-js/package.json b/src/puter-js/package.json index c3b7bfb1..1893c1e9 100644 --- a/src/puter-js/package.json +++ b/src/puter-js/package.json @@ -1,15 +1,16 @@ { "name": "@heyputer/puter.js", - "version": "2.0.1", + "version": "2.0.11", "description": "Puter.js - A JavaScript library for interacting with Puter services.", "main": "src/index.js", "types": "index.d.ts", "typings": "index.d.ts", + "type": "module", "files": [ - "src/index.js", + "dist/puter.js", + "src/", "index.d.ts" ], - "type": "module", "publishConfig": { "registry": "https://registry.npmjs.org/" }, diff --git a/src/puter-js/src/index.js b/src/puter-js/src/index.js index 64e1cce7..9228fe66 100644 --- a/src/puter-js/src/index.js +++ b/src/puter-js/src/index.js @@ -1,9 +1,10 @@ import putility from '@heyputer/putility'; +import kvjs from '@heyputer/kv.js'; import APICallLogger from './lib/APICallLogger.js'; import path from './lib/path.js'; -import localStorageMemory from './lib/polyfills/localStorage.js' -import xhrshim from './lib/polyfills/xhrshim.js' +import localStorageMemory from './lib/polyfills/localStorage.js'; +import xhrshim from './lib/polyfills/xhrshim.js'; import * as utils from './lib/utils.js'; import AI from './modules/AI.js'; import Apps from './modules/Apps.js'; @@ -15,7 +16,7 @@ import FSItem from './modules/FSItem.js'; import Hosting from './modules/Hosting.js'; import KV from './modules/KV.js'; import { PSocket } from './modules/networking/PSocket.js'; -import { PTLSSocket } from "./modules/networking/PTLS.js" +import { PTLSSocket } from './modules/networking/PTLS.js'; import { pFetch } from './modules/networking/requests.js'; import OS from './modules/OS.js'; import Perms from './modules/Perms.js'; @@ -28,14 +29,13 @@ import { FilesystemService } from './services/Filesystem.js'; import { FSRelayService } from './services/FSRelay.js'; import { NoPuterYetService } from './services/NoPuterYet.js'; import { XDIncomingService } from './services/XDIncoming.js'; -import kvjs from '@heyputer/kv.js'; // TODO: This is for a safe-guard below; we should check if we can // generalize this behavior rather than hard-coding it. // (using defaultGUIOrigin breaks locally-hosted apps) const PROD_ORIGIN = 'https://puter.com'; -export default globalThis.puter = (function() { +const puterInit = (function() { 'use strict'; class Puter{ @@ -58,7 +58,7 @@ export default globalThis.puter = (function() { puterAuthState = { isPromptOpen: false, authGranted: null, - resolver: null + resolver: null, }; // Holds the unique app instance ID that is provided by the host environment @@ -78,12 +78,12 @@ export default globalThis.puter = (function() { /** * Puter.js Modules - * + * * These are the modules you see on docs.puter.com; for example: * - puter.fs * - puter.kv * - puter.ui - * + * * initSubmodules is called from the constructor of this class. */ initSubmodules = function(){ @@ -108,14 +108,13 @@ export default globalThis.puter = (function() { // Path this.path = path; - } + }; // -------------------------------------------- // Constructor // -------------------------------------------- - constructor(options) { - options = options ?? {}; - + constructor() { + // Initialize the cache using kv.js this._cache = new kvjs(); @@ -131,57 +130,57 @@ export default globalThis.puter = (function() { this.context = context; context.services = this.services; - // Holds the query parameters found in the current URL let URLParams = new URLSearchParams(globalThis.location?.search); // Figure out the environment in which the SDK is running - if (URLParams.has('puter.app_instance_id')) { + if ( URLParams.has('puter.app_instance_id') ) { this.env = 'app'; - } else if(globalThis.puter_gui_enabled === true) + } else if ( globalThis.puter_gui_enabled === true ) + { this.env = 'gui'; - else if (globalThis.WorkerGlobalScope) { - if (globalThis.ServiceWorkerGlobalScope) { - this.env = 'service-worker' - if (!globalThis.XMLHttpRequest) { - globalThis.XMLHttpRequest = xhrshim + } + else if ( globalThis.WorkerGlobalScope ) { + if ( globalThis.ServiceWorkerGlobalScope ) { + this.env = 'service-worker'; + if ( !globalThis.XMLHttpRequest ) { + globalThis.XMLHttpRequest = xhrshim; } - if (!globalThis.location) { - globalThis.location = new URL("https://puter.site/"); + if ( !globalThis.location ) { + globalThis.location = new URL('https://puter.site/'); } // XHRShimGlobalize here } else { - this.env = 'web-worker' + this.env = 'web-worker'; } - if (!globalThis.localStorage) { + if ( !globalThis.localStorage ) { globalThis.localStorage = localStorageMemory; } - } else if (globalThis.process) { + } else if ( globalThis.process ) { this.env = 'nodejs'; - if (!globalThis.localStorage) { + if ( !globalThis.localStorage ) { globalThis.localStorage = localStorageMemory; } - if (!globalThis.XMLHttpRequest) { - globalThis.XMLHttpRequest = xhrshim + if ( !globalThis.XMLHttpRequest ) { + globalThis.XMLHttpRequest = xhrshim; } - if (!globalThis.location) { - globalThis.location = new URL("https://nodejs.puter.site/"); + if ( !globalThis.location ) { + globalThis.location = new URL('https://nodejs.puter.site/'); } - if (!globalThis.addEventListener) { - globalThis.addEventListener = () => {} // API Stub + if ( !globalThis.addEventListener ) { + globalThis.addEventListener = () => { + }; // API Stub } } else { this.env = 'web'; } - - // There are some specific situations where puter is definitely loaded in GUI mode // we're going to check for those situations here so that we don't break anything unintentionally // if navigator URL's hostname is 'puter.com' - if(this.env !== 'gui'){ + if ( this.env !== 'gui' ){ // Retrieve the hostname from the URL: Remove the trailing dot if it exists. This is to handle the case where the URL is, for example, `https://puter.com.` (note the trailing dot). - // This is necessary because the trailing dot can cause the hostname to not match the expected value. + // This is necessary because the trailing dot can cause the hostname to not match the expected value. let hostname = location.hostname.replace(/\.$/, ''); // Create a new URL object with the URL string @@ -191,45 +190,45 @@ export default globalThis.puter = (function() { const gui_hostname = url.hostname; // If the hostname matches the GUI hostname, then the SDK is running in the GUI environment - if(hostname === gui_hostname){ + if ( hostname === gui_hostname ){ this.env = 'gui'; } } // Get the 'args' from the URL. This is used to pass arguments to the app. - if(URLParams.has('puter.args')){ + if ( URLParams.has('puter.args') ){ this.args = JSON.parse(decodeURIComponent(URLParams.get('puter.args'))); - }else{ + } else { this.args = {}; } // Try to extract appInstanceID from the URL. appInstanceID is included in every messaage // sent to the host environment. This is used to help host environment identify the app // instance that sent the message and communicate back to it. - if(URLParams.has('puter.app_instance_id')){ + if ( URLParams.has('puter.app_instance_id') ){ this.appInstanceID = decodeURIComponent(URLParams.get('puter.app_instance_id')); } // Try to extract parentInstanceID from the URL. If another app launched this app instance, parentInstanceID // holds its instance ID, and is used to communicate with that parent app. - if(URLParams.has('puter.parent_instance_id')){ + if ( URLParams.has('puter.parent_instance_id') ){ this.parentInstanceID = decodeURIComponent(URLParams.get('puter.parent_instance_id')); } // Try to extract `puter.app.id` from the URL. `puter.app.id` is the unique ID of the app. // App ID is useful for identifying the app when communicating with the Puter API, among other things. - if(URLParams.has('puter.app.id')){ + if ( URLParams.has('puter.app.id') ){ this.appID = decodeURIComponent(URLParams.get('puter.app.id')); } - + // Extract app name (added later) - if(URLParams.has('puter.app.name')){ + if ( URLParams.has('puter.app.name') ){ this.appName = decodeURIComponent(URLParams.get('puter.app.name')); } // Construct this App's AppData path based on the appID. AppData path is used to store files that are specific to this app. // The default AppData path is `~/AppData/`. - if(this.appID){ + if ( this.appID ){ this.appDataPath = `~/AppData/${this.appID}`; } @@ -239,9 +238,9 @@ export default globalThis.puter = (function() { // is constructed as `https://api.`. // This should only be done when the SDK is running in 'app' mode. this.APIOrigin = this.defaultAPIOrigin; - if(URLParams.has('puter.api_origin') && this.env === 'app'){ + if ( URLParams.has('puter.api_origin') && this.env === 'app' ){ this.APIOrigin = decodeURIComponent(URLParams.get('puter.api_origin')); - }else if(URLParams.has('puter.domain') && this.env === 'app'){ + } else if ( URLParams.has('puter.domain') && this.env === 'app' ){ this.APIOrigin = 'https://api.' + URLParams.get('puter.domain'); } @@ -251,10 +250,9 @@ export default globalThis.puter = (function() { let logger = new putility.libs.log.ConsoleLogger(); // logs can be toggled based on categories - logger = new putility.libs.log.CategorizedToggleLogger( - { delegate: logger }); + logger = new putility.libs.log.CategorizedToggleLogger({ delegate: logger }); const cat_logger = logger; - + // create facade for easy logging this.logger = new putility.libs.log.LoggerFacade({ impl: logger, @@ -263,7 +261,7 @@ export default globalThis.puter = (function() { // Initialize API call logger this.apiCallLogger = new APICallLogger({ - enabled: false // Disabled by default + enabled: false, // Disabled by default }); // === START :: Services === // @@ -285,17 +283,16 @@ export default globalThis.puter = (function() { svc_apiAccess.auth_token = this.authToken; svc_apiAccess.api_origin = this.APIOrigin; [ - ['authToken','auth_token'], - ['APIOrigin','api_origin'], - ].forEach(([k1,k2]) => { + ['authToken', 'auth_token'], + ['APIOrigin', 'api_origin'], + ].forEach(([k1, k2]) => { Object.defineProperty(this, k1, { - get () { + get() { return svc_apiAccess[k2]; }, - set (v) { + set(v) { svc_apiAccess[k2] = v; - return true; - } + }, }); }); })(); @@ -303,27 +300,27 @@ export default globalThis.puter = (function() { // === Start :: Modules === // // The SDK is running in the Puter GUI (i.e. 'gui') - if(this.env === 'gui'){ + if ( this.env === 'gui' ){ this.authToken = window.auth_token; // initialize submodules this.initSubmodules(); } // Loaded in an iframe in the Puter GUI (i.e. 'app') // When SDK is loaded in App mode the initiation process should start when the DOM is ready - else if (this.env === 'app') { + else if ( this.env === 'app' ) { this.authToken = decodeURIComponent(URLParams.get('puter.auth.token')); // initialize submodules this.initSubmodules(); // If the authToken is already set in localStorage, then we don't need to show the dialog try { - if(localStorage.getItem('puter.auth.token')){ + if ( localStorage.getItem('puter.auth.token') ){ this.setAuthToken(localStorage.getItem('puter.auth.token')); } // if appID is already set in localStorage, then we don't need to show the dialog - if(localStorage.getItem('puter.app.id')){ + if ( localStorage.getItem('puter.app.id') ){ this.setAppID(localStorage.getItem('puter.app.id')); } - } catch (error) { + } catch( error ) { // Handle the error here console.error('Error accessing localStorage:', error); } @@ -331,23 +328,23 @@ export default globalThis.puter = (function() { // SDK was loaded in a 3rd-party website. // When SDK is loaded in GUI the initiation process should start when the DOM is ready. This is because // the SDK needs to show a dialog to the user to ask for permission to access their Puter account. - else if(this.env === 'web') { + else if ( this.env === 'web' ) { // initialize submodules this.initSubmodules(); - try{ + try { // If the authToken is already set in localStorage, then we don't need to show the dialog - if(localStorage.getItem('puter.auth.token')){ + if ( localStorage.getItem('puter.auth.token') ){ this.setAuthToken(localStorage.getItem('puter.auth.token')); } // if appID is already set in localStorage, then we don't need to show the dialog - if(localStorage.getItem('puter.app.id')){ + if ( localStorage.getItem('puter.app.id') ){ this.setAppID(localStorage.getItem('puter.app.id')); } - } catch (error) { + } catch( error ) { // Handle the error here console.error('Error accessing localStorage:', error); } - } else if (this.env === 'web-worker' || this.env === 'service-worker' || this.env === 'nodejs') { + } else if ( this.env === 'web-worker' || this.env === 'service-worker' || this.env === 'nodejs' ) { this.initSubmodules(); } @@ -364,7 +361,7 @@ export default globalThis.puter = (function() { this.logger.impl = logger; })(); - + // Lock to prevent multiple requests to `/rao` this.lock_rao_ = new putility.libs.promise.Lock(); // Promise that resolves when it's okay to request `/rao` @@ -388,14 +385,14 @@ export default globalThis.puter = (function() { }, body: JSON.stringify({}), })).json()); - return `${wispServer}/${wispToken}/` + return `${wispServer}/${wispToken}/`; }, Socket: PSocket, tls: { - TLSSocket: PTLSSocket + TLSSocket: PTLSSocket, }, - fetch: pFetch - } + fetch: pFetch, + }; this.workers = new WorkersHandler(this.authToken); @@ -408,13 +405,13 @@ export default globalThis.puter = (function() { * Makes a request to `/rao`. This method aquires a lock to prevent * multiple requests, and is effectively idempotent. */ - async request_rao_ () { + async request_rao_() { await this.p_can_request_rao_; - + if ( this.env === 'gui' ) { return; } - + // setAuthToken is called more than once when auth completes, which // causes multiple requests to /rao. This lock prevents that. await this.lock_rao_.acquire(); @@ -429,11 +426,11 @@ export default globalThis.puter = (function() { method: 'POST', headers: { Authorization: `Bearer ${this.authToken}`, - Origin: location.origin // This is ignored in the browser but needed for workers and nodejs - } + Origin: location.origin, // This is ignored in the browser but needed for workers and nodejs + }, }); return await resp.json(); - } catch (e) { + } catch( e ) { had_error = true; console.error(e); } finally { @@ -443,8 +440,8 @@ export default globalThis.puter = (function() { this.rao_requested_ = true; } } - - registerModule (name, cls, parameters = {}) { + + registerModule(name, cls, parameters = {}) { const instance = new cls(this.context, parameters); this.modules_.push(name); this[name] = instance; @@ -460,24 +457,24 @@ export default globalThis.puter = (function() { } } - setAppID = function (appID) { + setAppID = function(appID) { // save to localStorage - try{ + try { localStorage.setItem('puter.app.id', appID); - } catch (error) { + } catch( error ) { // Handle the error here console.error('Error accessing localStorage:', error); } this.appID = appID; - } + }; - setAuthToken = function (authToken) { + setAuthToken = function(authToken) { this.authToken = authToken; // If the SDK is running on a 3rd-party site or an app, then save the authToken in localStorage - if(this.env === 'web' || this.env === 'app'){ - try{ + if ( this.env === 'web' || this.env === 'app' ){ + try { localStorage.setItem('puter.auth.token', authToken); - } catch (error) { + } catch( error ) { // Handle the error here console.error('Error accessing localStorage:', error); } @@ -486,41 +483,41 @@ export default globalThis.puter = (function() { this.updateSubmodules(); // rao this.request_rao_(); - } + }; - setAPIOrigin = function (APIOrigin) { + setAPIOrigin = function(APIOrigin) { this.APIOrigin = APIOrigin; // reinitialize submodules this.updateSubmodules(); - } + }; - resetAuthToken = function () { + resetAuthToken = function() { this.authToken = null; // If the SDK is running on a 3rd-party site or an app, then save the authToken in localStorage - if(this.env === 'web' || this.env === 'app'){ - try{ + if ( this.env === 'web' || this.env === 'app' ){ + try { localStorage.removeItem('puter.auth.token'); - } catch (error) { + } catch( error ) { // Handle the error here console.error('Error accessing localStorage:', error); } } // reinitialize submodules this.updateSubmodules(); - } + }; exit = function(statusCode = 0) { - if (statusCode && (typeof statusCode !== 'number')) { + if ( statusCode && (typeof statusCode !== 'number') ) { console.warn('puter.exit() requires status code to be a number. Treating it as 1'); statusCode = 1; } globalThis.parent.postMessage({ - msg: "exit", + msg: 'exit', appInstanceID: this.appInstanceID, statusCode, }, '*'); - } + }; /** * A function that generates a domain-safe name by combining a random adjective, a random noun, and a random number (between 0 and 9999). @@ -532,29 +529,29 @@ export default globalThis.puter = (function() { * */ randName = function(separateWith = '-'){ - const first_adj = ['helpful','sensible', 'loyal', 'honest', 'clever', 'capable','calm', 'smart', 'genius', 'bright', 'charming', 'creative', 'diligent', 'elegant', 'fancy', - 'colorful', 'avid', 'active', 'gentle', 'happy', 'intelligent', 'jolly', 'kind', 'lively', 'merry', 'nice', 'optimistic', 'polite', - 'quiet', 'relaxed', 'silly', 'victorious', 'witty', 'young', 'zealous', 'strong', 'brave', 'agile', 'bold']; + const first_adj = ['helpful', 'sensible', 'loyal', 'honest', 'clever', 'capable', 'calm', 'smart', 'genius', 'bright', 'charming', 'creative', 'diligent', 'elegant', 'fancy', + 'colorful', 'avid', 'active', 'gentle', 'happy', 'intelligent', 'jolly', 'kind', 'lively', 'merry', 'nice', 'optimistic', 'polite', + 'quiet', 'relaxed', 'silly', 'victorious', 'witty', 'young', 'zealous', 'strong', 'brave', 'agile', 'bold']; - const nouns = ['street', 'roof', 'floor', 'tv', 'idea', 'morning', 'game', 'wheel', 'shoe', 'bag', 'clock', 'pencil', 'pen', - 'magnet', 'chair', 'table', 'house', 'dog', 'room', 'book', 'car', 'cat', 'tree', - 'flower', 'bird', 'fish', 'sun', 'moon', 'star', 'cloud', 'rain', 'snow', 'wind', 'mountain', - 'river', 'lake', 'sea', 'ocean', 'island', 'bridge', 'road', 'train', 'plane', 'ship', 'bicycle', - 'horse', 'elephant', 'lion', 'tiger', 'bear', 'zebra', 'giraffe', 'monkey', 'snake', 'rabbit', 'duck', - 'goose', 'penguin', 'frog', 'crab', 'shrimp', 'whale', 'octopus', 'spider', 'ant', 'bee', 'butterfly', 'dragonfly', - 'ladybug', 'snail', 'camel', 'kangaroo', 'koala', 'panda', 'piglet', 'sheep', 'wolf', 'fox', 'deer', 'mouse', 'seal', - 'chicken', 'cow', 'dinosaur', 'puppy', 'kitten', 'circle', 'square', 'garden', 'otter', 'bunny', 'meerkat', 'harp'] + const nouns = ['street', 'roof', 'floor', 'tv', 'idea', 'morning', 'game', 'wheel', 'shoe', 'bag', 'clock', 'pencil', 'pen', + 'magnet', 'chair', 'table', 'house', 'dog', 'room', 'book', 'car', 'cat', 'tree', + 'flower', 'bird', 'fish', 'sun', 'moon', 'star', 'cloud', 'rain', 'snow', 'wind', 'mountain', + 'river', 'lake', 'sea', 'ocean', 'island', 'bridge', 'road', 'train', 'plane', 'ship', 'bicycle', + 'horse', 'elephant', 'lion', 'tiger', 'bear', 'zebra', 'giraffe', 'monkey', 'snake', 'rabbit', 'duck', + 'goose', 'penguin', 'frog', 'crab', 'shrimp', 'whale', 'octopus', 'spider', 'ant', 'bee', 'butterfly', 'dragonfly', + 'ladybug', 'snail', 'camel', 'kangaroo', 'koala', 'panda', 'piglet', 'sheep', 'wolf', 'fox', 'deer', 'mouse', 'seal', + 'chicken', 'cow', 'dinosaur', 'puppy', 'kitten', 'circle', 'square', 'garden', 'otter', 'bunny', 'meerkat', 'harp']; // return a random combination of first_adj + noun + number (between 0 and 9999) // e.g. clever-idea-123 return first_adj[Math.floor(Math.random() * first_adj.length)] + separateWith + nouns[Math.floor(Math.random() * nouns.length)] + separateWith + Math.floor(Math.random() * 10000); - } + }; getUser = function(...args){ let options; - + // If first argument is an object, it's the options - if (typeof args[0] === 'object' && args[0] !== null) { + if ( typeof args[0] === 'object' && args[0] !== null ) { options = args[0]; } else { // Otherwise, we assume separate arguments are provided @@ -563,43 +560,43 @@ export default globalThis.puter = (function() { error: args[1], }; } - + return new Promise((resolve, reject) => { const xhr = utils.initXhr('/whoami', this.APIOrigin, this.authToken, 'get'); // set up event handlers for load and error events utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject); - + xhr.send(); - }) - } + }); + }; print = function(...args){ // Check if the last argument is an options object with escapeHTML or code property let options = {}; - if(args.length > 0 && typeof args[args.length - 1] === 'object' && args[args.length - 1] !== null && - ('escapeHTML' in args[args.length - 1] || 'code' in args[args.length - 1])) { + if ( args.length > 0 && typeof args[args.length - 1] === 'object' && args[args.length - 1] !== null && + ('escapeHTML' in args[args.length - 1] || 'code' in args[args.length - 1]) ) { options = args.pop(); } - - for(let arg of args){ + + for ( let arg of args ){ // Escape HTML if the option is set to true or if code option is true - if((options.escapeHTML === true || options.code === true) && typeof arg === 'string') { + if ( (options.escapeHTML === true || options.code === true) && typeof arg === 'string' ) { arg = arg.replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); } - + // Wrap in code/pre tags if code option is true - if(options.code === true) { + if ( options.code === true ) { arg = `
${arg}
`; } - + document.body.innerHTML += arg; } - } - + }; + /** * Configures API call logging settings * @param {Object} config - Configuration options for API call logging @@ -607,32 +604,32 @@ export default globalThis.puter = (function() { * @param {boolean} config.enabled - Enable/disable API call logging */ configureAPILogging = function(config = {}){ - if (this.apiCallLogger) { + if ( this.apiCallLogger ) { this.apiCallLogger.updateConfig(config); } return this; - } - + }; + /** * Enables API call logging with optional configuration * @param {Object} config - Optional configuration to apply when enabling */ enableAPILogging = function(config = {}) { - if (this.apiCallLogger) { + if ( this.apiCallLogger ) { this.apiCallLogger.updateConfig({ ...config, enabled: true }); } return this; - } - + }; + /** * Disables API call logging */ disableAPILogging = function() { - if (this.apiCallLogger) { + if ( this.apiCallLogger ) { this.apiCallLogger.disable(); } return this; - } + }; /** * Initializes network connectivity monitoring to purge cache when connection is lost @@ -640,8 +637,8 @@ export default globalThis.puter = (function() { */ initNetworkMonitoring = function() { // Only initialize in environments that support navigator.onLine and window events - if (typeof globalThis.navigator === 'undefined' || - typeof globalThis.addEventListener !== 'function') { + if ( typeof globalThis.navigator === 'undefined' || + typeof globalThis.addEventListener !== 'function' ) { return; } @@ -651,13 +648,13 @@ export default globalThis.puter = (function() { // Function to handle network state changes const handleNetworkChange = () => { const isOnline = navigator.onLine; - + // If we went from online to offline, purge the cache - if (wasOnline && !isOnline) { + if ( wasOnline && !isOnline ) { console.log('Network connection lost - purging cache'); this.purgeCache(); } - + // Update the previous state wasOnline = isOnline; }; @@ -668,13 +665,13 @@ export default globalThis.puter = (function() { // Also listen for visibility change as an additional indicator // (some browsers don't fire offline events reliably) - if (typeof document !== 'undefined') { + if ( typeof document !== 'undefined' ) { document.addEventListener('visibilitychange', () => { // Small delay to allow network state to update setTimeout(handleNetworkChange, 100); }); } - } + }; /** * Purges all cached data @@ -682,17 +679,17 @@ export default globalThis.puter = (function() { */ purgeCache = function() { try { - if (this._cache && typeof this._cache.flushall === 'function') { + if ( this._cache && typeof this._cache.flushall === 'function' ) { this._cache.flushall(); console.log('Cache purged successfully'); } else { console.warn('Cache purge failed: cache instance not available'); } - } catch (error) { + } catch( error ) { console.error('Error purging cache:', error); } - } - + }; + } // Create a new Puter object and return it @@ -700,18 +697,22 @@ export default globalThis.puter = (function() { // Return the Puter object return puterobj; -}()); +}); -globalThis.addEventListener('message', async (event) => { +export const puter = puterInit(); +export default puter; +globalThis.puter = puter; + +globalThis.addEventListener && globalThis.addEventListener('message', async (event) => { // if the message is not from Puter, then ignore it - if(event.origin !== puter.defaultGUIOrigin) return; + if ( event.origin !== puter.defaultGUIOrigin ) return; - if(event.data.msg && event.data.msg === 'requestOrigin'){ + if ( event.data.msg && event.data.msg === 'requestOrigin' ){ event.source.postMessage({ - msg: "originResponse", - }, '*'); + msg: 'originResponse', + }, '*'); } - else if (event.data.msg === 'puter.token') { + else if ( event.data.msg === 'puter.token' ) { // puterDialog.close(); // Set the authToken property puter.setAuthToken(event.data.token); @@ -725,16 +726,16 @@ globalThis.addEventListener('message', async (event) => { // resolve(); // Call onAuth callback - if(puter.onAuth && typeof puter.onAuth === 'function'){ + if ( puter.onAuth && typeof puter.onAuth === 'function' ){ puter.getUser().then((user) => { - puter.onAuth(user) + puter.onAuth(user); }); } puter.puterAuthState.isPromptOpen = false; // Resolve or reject any waiting promises. - if (puter.puterAuthState.resolver) { - if (puter.puterAuthState.authGranted) { + if ( puter.puterAuthState.resolver ) { + if ( puter.puterAuthState.authGranted ) { puter.puterAuthState.resolver.resolve(); } else { puter.puterAuthState.resolver.reject(); @@ -742,4 +743,4 @@ globalThis.addEventListener('message', async (event) => { puter.puterAuthState.resolver = null; }; } -}) +}); \ No newline at end of file diff --git a/src/puter-js/src/init.cjs b/src/puter-js/src/init.cjs new file mode 100644 index 00000000..fa081263 --- /dev/null +++ b/src/puter-js/src/init.cjs @@ -0,0 +1,28 @@ +const { readFileSync } = require('node:fs'); +const vm = require('node:vm'); +const { resolve } = require('node:path'); +/** + * Method for loading puter.js in Node.js environment with auth token + * @param {string} authToken - Optional auth token to initialize puter with + * @returns {import('../index').puter} The `puter` object from puter.js + */ +const init = (authToken) => { + const goodContext = {}; + Object.getOwnPropertyNames(globalThis).forEach(name => { + try { + goodContext[name] = globalThis[name]; + } catch { + // silent fail + } + }); + goodContext.globalThis = goodContext; + const code = readFileSync(`${resolve(__filename, '..')}/../dist/puter.js`, 'utf8'); + const context = vm.createContext(goodContext); + vm.runInNewContext(code, context); + if ( authToken ) { + goodContext.puter.setAuthToken(authToken); + } + return goodContext.puter; +}; + +module.exports = { init }; \ No newline at end of file diff --git a/src/puter-js/src/modules/FileSystem/index.js b/src/puter-js/src/modules/FileSystem/index.js index f5a34aae..85f54072 100644 --- a/src/puter-js/src/modules/FileSystem/index.js +++ b/src/puter-js/src/modules/FileSystem/index.js @@ -1,23 +1,23 @@ import io from '../../lib/socket.io/socket.io.esm.min.js'; // Operations -import space from "./operations/space.js"; -import mkdir from "./operations/mkdir.js"; -import copy from "./operations/copy.js"; -import rename from "./operations/rename.js"; -import upload from "./operations/upload.js"; -import read from "./operations/read.js"; -import move from "./operations/move.js"; -import write from "./operations/write.js"; -import sign from "./operations/sign.js"; -import symlink from './operations/symlink.js'; +import copy from './operations/copy.js'; +import mkdir from './operations/mkdir.js'; +import move from './operations/move.js'; +import read from './operations/read.js'; import readdir from './operations/readdir.js'; +import rename from './operations/rename.js'; +import sign from './operations/sign.js'; +import space from './operations/space.js'; import stat from './operations/stat.js'; -// Why is this called deleteFSEntry instead of just delete? because delete is +import symlink from './operations/symlink.js'; +import upload from './operations/upload.js'; +import write from './operations/write.js'; +// Why is this called deleteFSEntry instead of just delete? because delete is // a reserved keyword in javascript -import deleteFSEntry from "./operations/deleteFSEntry.js"; import { AdvancedBase } from '../../../../putility/index.js'; import FSItem from '../FSItem.js'; +import deleteFSEntry from './operations/deleteFSEntry.js'; import getReadURL from './operations/getReadUrl.js'; export class PuterJSFileSystemModule extends AdvancedBase { @@ -39,7 +39,7 @@ export class PuterJSFileSystemModule extends AdvancedBase { readdir = readdir; stat = stat; - FSItem = FSItem + FSItem = FSItem; static NARI_METHODS = { // stat: { @@ -50,7 +50,7 @@ export class PuterJSFileSystemModule extends AdvancedBase { // return svc_fs.filesystem.stat(parameters); // } // }, - } + }; /** * Creates a new instance with the given authentication token, API origin, and app ID, @@ -61,7 +61,7 @@ export class PuterJSFileSystemModule extends AdvancedBase { * @param {string} APIOrigin - Origin of the API server. Used to build the API endpoint URLs. * @param {string} appID - ID of the app to use. */ - constructor (context) { + constructor(context) { super(); this.authToken = context.authToken; this.APIOrigin = context.APIOrigin; @@ -81,7 +81,6 @@ export class PuterJSFileSystemModule extends AdvancedBase { }); } - /** * Initializes the socket connection to the server using the current API origin. * If a socket connection already exists, it disconnects it before creating a new one. @@ -92,17 +91,19 @@ export class PuterJSFileSystemModule extends AdvancedBase { * @returns {void} */ initializeSocket() { - if (this.socket) { + if ( this.socket ) { this.socket.disconnect(); } this.socket = io(this.APIOrigin, { auth: { auth_token: this.authToken, - } + }, + autoUnref: this.context.env === 'nodejs', }); this.bindSocketEvents(); + } bindSocketEvents() { @@ -117,16 +118,20 @@ export class PuterJSFileSystemModule extends AdvancedBase { this.socket.on('item.moved', (item) => { // todo: NAIVE PURGE puter._cache.flushall(); - }); + }); this.socket.on('connect', () => { - if(puter.debugMode) + if ( puter.debugMode ) + { console.log('FileSystem Socket: Connected', this.socket.id); + } }); this.socket.on('disconnect', () => { - if(puter.debugMode) + if ( puter.debugMode ) + { console.log('FileSystem Socket: Disconnected'); + } // todo: NAIVE PURGE // purge cache on disconnect since we may have become out of sync @@ -134,28 +139,38 @@ export class PuterJSFileSystemModule extends AdvancedBase { }); this.socket.on('reconnect', (attempt) => { - if(puter.debugMode) + if ( puter.debugMode ) + { console.log('FileSystem Socket: Reconnected', this.socket.id); + } }); this.socket.on('reconnect_attempt', (attempt) => { - if(puter.debugMode) + if ( puter.debugMode ) + { console.log('FileSystem Socket: Reconnection Attemps', attempt); + } }); this.socket.on('reconnect_error', (error) => { - if(puter.debugMode) + if ( puter.debugMode ) + { console.log('FileSystem Socket: Reconnection Error', error); + } }); this.socket.on('reconnect_failed', () => { - if(puter.debugMode) + if ( puter.debugMode ) + { console.log('FileSystem Socket: Reconnection Failed'); + } }); this.socket.on('error', (error) => { - if(puter.debugMode) + if ( puter.debugMode ) + { console.error('FileSystem Socket Error:', error); + } }); } @@ -166,7 +181,7 @@ export class PuterJSFileSystemModule extends AdvancedBase { * @memberof [FileSystem] * @returns {void} */ - setAuthToken (authToken) { + setAuthToken(authToken) { this.authToken = authToken; // reset socket this.initializeSocket(); @@ -174,12 +189,12 @@ export class PuterJSFileSystemModule extends AdvancedBase { /** * Sets the API origin and resets the socket connection with the updated API origin. - * + * * @param {string} APIOrigin - The new API origin. * @memberof [Apps] * @returns {void} */ - setAPIOrigin (APIOrigin) { + setAPIOrigin(APIOrigin) { this.APIOrigin = APIOrigin; // reset socket this.initializeSocket(); diff --git a/src/puter-js/src/services/Filesystem.js b/src/puter-js/src/services/Filesystem.js index f60d25b9..588b0591 100644 --- a/src/puter-js/src/services/Filesystem.js +++ b/src/puter-js/src/services/Filesystem.js @@ -1,13 +1,13 @@ -import putility from "@heyputer/putility"; -import { PuterAPIFilesystem } from "../lib/filesystem/APIFS.js"; -import { CachedFilesystem } from "../lib/filesystem/CacheFS.js"; -import { ProxyFilesystem, TFilesystem } from "../lib/filesystem/definitions.js"; +import putility from '@heyputer/putility'; +import { PuterAPIFilesystem } from '../lib/filesystem/APIFS.js'; +import { CachedFilesystem } from '../lib/filesystem/CacheFS.js'; +import { ProxyFilesystem, TFilesystem } from '../lib/filesystem/definitions.js'; +import { PostMessageFilesystem } from '../lib/filesystem/PostMessageFS.js'; import io from '../lib/socket.io/socket.io.esm.min.js'; -import { PostMessageFilesystem } from "../lib/filesystem/PostMessageFS.js"; export class FilesystemService extends putility.concepts.Service { static PROPERTIES = { - // filesystem: + // filesystem: }; static DEPENDS = ['api-access']; @@ -19,13 +19,13 @@ export class FilesystemService extends putility.concepts.Service { re-initialize the socket connection whenever the authentication token or API origin is changed. `, - async do () { + async do() { this.initializeSocket(); - } - } - ] + }, + }, + ]; - _init () { + _init() { const env = this._.context.env; if ( env === 'app' ) { @@ -40,14 +40,14 @@ export class FilesystemService extends putility.concepts.Service { this.initializeSocket(); } - init_app_fs_ () { + init_app_fs_() { this.fs_nocache_ = new PostMessageFilesystem({ messageTarget: globalThis.parent, rpc: this._.context.util.rpc, }).as(TFilesystem); this.filesystem = this.fs_nocache_; } - init_top_fs_ () { + init_top_fs_() { const api_info = this._.context.services.get('api-access').get_api_info(); this.fs_nocache_ = new PuterAPIFilesystem({ api_info }).as(TFilesystem); this.fs_cache_ = new CachedFilesystem({ delegate: this.fs_nocache_ }).as(TFilesystem); @@ -56,15 +56,15 @@ export class FilesystemService extends putility.concepts.Service { this.filesystem = this.fs_proxy_.as(TFilesystem); } - cache_on () { + cache_on() { this.fs_proxy_.delegate = this.fs_cache_; } - cache_off () { + cache_off() { this.fs_proxy_.delegate = this.fs_nocache_; } - async initializeSocket () { - if (this.socket) { + async initializeSocket() { + if ( this.socket ) { this.socket.disconnect(); } @@ -77,7 +77,8 @@ export class FilesystemService extends putility.concepts.Service { } this.socket = io(api_info.api_origin, { - auth: { auth_token: api_info.auth_token } + auth: { auth_token: api_info.auth_token }, + autoUnref: this._.context.env === 'nodejs', }); this.bindSocketEvents(); @@ -85,38 +86,52 @@ export class FilesystemService extends putility.concepts.Service { bindSocketEvents() { this.socket.on('connect', () => { - if(puter.debugMode) + if ( puter.debugMode ) + { console.log('FileSystem Socket: Connected', this.socket.id); + } }); this.socket.on('disconnect', () => { - if(puter.debugMode) + if ( puter.debugMode ) + { console.log('FileSystem Socket: Disconnected'); + } }); this.socket.on('reconnect', (attempt) => { - if(puter.debugMode) + if ( puter.debugMode ) + { console.log('FileSystem Socket: Reconnected', this.socket.id); + } }); this.socket.on('reconnect_attempt', (attempt) => { - if(puter.debugMode) + if ( puter.debugMode ) + { console.log('FileSystem Socket: Reconnection Attemps', attempt); + } }); this.socket.on('reconnect_error', (error) => { - if(puter.debugMode) + if ( puter.debugMode ) + { console.log('FileSystem Socket: Reconnection Error', error); + } }); this.socket.on('reconnect_failed', () => { - if(puter.debugMode) + if ( puter.debugMode ) + { console.log('FileSystem Socket: Reconnection Failed'); + } }); this.socket.on('error', (error) => { - if(puter.debugMode) + if ( puter.debugMode ) + { console.error('FileSystem Socket Error:', error); + } }); } }