diff --git a/.gitignore b/.gitignore index 6d1a318cf..0ec8f93c1 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,7 @@ jsconfig.json # node js # ====================================================================== # the exact tree installed in the node_modules folder -package-lock.json \ No newline at end of file +package-lock.json + +AGENTS.md +.roo \ No newline at end of file diff --git a/src/puter-js/src/index.js b/src/puter-js/src/index.js index aacc34fad..76ecaaa52 100644 --- a/src/puter-js/src/index.js +++ b/src/puter-js/src/index.js @@ -1,31 +1,33 @@ -import OS from './modules/OS.js'; -import { PuterJSFileSystemModule } from './modules/FileSystem/index.js'; -import Hosting from './modules/Hosting.js'; -import Apps from './modules/Apps.js'; -import UI from './modules/UI.js'; -import KV from './modules/KV.js'; -import AI from './modules/AI.js'; -import Auth from './modules/Auth.js'; -import FSItem from './modules/FSItem.js'; -import * as utils from './lib/utils.js'; -import path from './lib/path.js'; -import Util from './modules/Util.js'; -import Drivers from './modules/Drivers.js'; import putility from '@heyputer/putility'; -import { FSRelayService } from './services/FSRelay.js'; -import { FilesystemService } from './services/Filesystem.js'; -import { APIAccessService } from './services/APIAccess.js'; -import { XDIncomingService } from './services/XDIncoming.js'; -import { NoPuterYetService } from './services/NoPuterYet.js'; -import { Debug } from './modules/Debug.js'; -import { PSocket } from './modules/networking/PSocket.js'; -import { PTLSSocket } from "./modules/networking/PTLS.js" -import Threads from './modules/Threads.js'; -import Perms from './modules/Perms.js'; -import { pFetch } from './modules/networking/requests.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 * as utils from './lib/utils.js'; +import AI from './modules/AI.js'; +import Apps from './modules/Apps.js'; +import Auth from './modules/Auth.js'; +import { Debug } from './modules/Debug.js'; +import Drivers from './modules/Drivers.js'; +import { PuterJSFileSystemModule } from './modules/FileSystem/index.js'; +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 { pFetch } from './modules/networking/requests.js'; +import OS from './modules/OS.js'; +import Perms from './modules/Perms.js'; +import Threads from './modules/Threads.js'; +import UI from './modules/UI.js'; +import Util from './modules/Util.js'; import { WorkersHandler } from './modules/Workers.js'; +import { APIAccessService } from './services/APIAccess.js'; +import { FilesystemService } from './services/Filesystem.js'; +import { FSRelayService } from './services/FSRelay.js'; +import { NoPuterYetService } from './services/NoPuterYet.js'; +import { XDIncomingService } from './services/XDIncoming.js'; // TODO: This is for a safe-guard below; we should check if we can // generalize this behavior rather than hard-coding it. @@ -255,6 +257,11 @@ export default globalThis.puter = (function() { cat: cat_logger, }); + // Initialize API call logger + this.apiCallLogger = new APICallLogger({ + enabled: false // Disabled by default + }); + // === START :: Services === // this.services.register('no-puter-yet', NoPuterYetService); @@ -585,6 +592,41 @@ export default globalThis.puter = (function() { document.body.innerHTML += arg; } } + + /** + * Configures API call logging settings + * @param {Object} config - Configuration options for API call logging + * @param {boolean} config.enabled - Enable/disable API call logging + * @param {boolean} config.enabled - Enable/disable API call logging + */ + configureAPILogging = function(config = {}){ + 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) { + this.apiCallLogger.updateConfig({ ...config, enabled: true }); + } + return this; + } + + /** + * Disables API call logging + */ + disableAPILogging = function() { + if (this.apiCallLogger) { + this.apiCallLogger.disable(); + } + return this; + } + } // Create a new Puter object and return it diff --git a/src/puter-js/src/lib/APICallLogger.js b/src/puter-js/src/lib/APICallLogger.js new file mode 100644 index 000000000..bedbb9ccf --- /dev/null +++ b/src/puter-js/src/lib/APICallLogger.js @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2024-present Puter Technologies Inc. + * + * This file is part of Puter. + * + * Puter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +/** + * APICallLogger provides centralized logging for all API calls made by the puter-js SDK. + * It logs API calls in a simple format: service - operation - params - result + */ +class APICallLogger { + constructor(config = {}) { + this.config = { + enabled: config.enabled ?? false, + ...config + }; + } + + /** + * Updates the logger configuration + * @param {Object} newConfig - New configuration options + */ + updateConfig(newConfig) { + this.config = { ...this.config, ...newConfig }; + } + + /** + * Enables API call logging + */ + enable() { + this.config.enabled = true; + } + + /** + * Disables API call logging + */ + disable() { + this.config.enabled = false; + } + + /** + * Checks if logging is enabled for the current configuration + * @returns {boolean} + */ + isEnabled() { + return this.config.enabled; + } + + /** + * Logs the completion of an API request in a simple format + * @param {Object} options - Request completion options + */ + logRequest(options = {}) { + if (!this.isEnabled()) return; + + const { + service = 'unknown', + operation = 'unknown', + params = {}, + result = null, + error = null + } = options; + + // Format params as a readable string + let paramsStr = '{}'; + if (params && Object.keys(params).length > 0) { + try { + paramsStr = JSON.stringify(params); + } catch (e) { + paramsStr = '[Unable to serialize params]'; + } + } + + // Format the log message with bold params + const logMessage = `${service} - ${operation} - \x1b[1m${paramsStr}\x1b[22m`; + + if (error) { + console.error(logMessage, { error: error.message || error, result }); + } else { + console.log(logMessage, result); + } + } + + /** + * Gets current logging statistics + * @returns {Object} + */ + getStats() { + return { + enabled: this.config.enabled, + config: { ...this.config } + }; + } +} + +export default APICallLogger; \ No newline at end of file diff --git a/src/puter-js/src/lib/utils.js b/src/puter-js/src/lib/utils.js index f03392333..781300d73 100644 --- a/src/puter-js/src/lib/utils.js +++ b/src/puter-js/src/lib/utils.js @@ -83,6 +83,17 @@ function initXhr(endpoint, APIOrigin, authToken, method= "post", contentType = " xhr.setRequestHeader("Authorization", "Bearer " + authToken); xhr.setRequestHeader("Content-Type", contentType); xhr.responseType = responseType ?? ''; + + // Add API call logging if available + if (globalThis.puter?.apiCallLogger?.isEnabled()) { + xhr._puterRequestId = { + method, + service: 'xhr', + operation: endpoint.replace(/^\//, ''), + params: { endpoint, contentType, responseType } + }; + } + return xhr; } @@ -160,12 +171,35 @@ function handle_error(error_cb, reject_func, error){ function setupXhrEventHandlers(xhr, success_cb, error_cb, resolve_func, reject_func) { // load: success or error - xhr.addEventListener('load', function(e){ + xhr.addEventListener('load', async function(e){ + // Log the response if API logging is enabled + if (globalThis.puter?.apiCallLogger?.isEnabled() && this._puterRequestId) { + const response = await parseResponse(this).catch(() => null); + globalThis.puter.apiCallLogger.logRequest({ + service: this._puterRequestId.service, + operation: this._puterRequestId.operation, + params: this._puterRequestId.params, + result: this.status >= 400 ? null : response, + error: this.status >= 400 ? { message: this.statusText, status: this.status } : null + }); + } return handle_resp(success_cb, error_cb, resolve_func, reject_func, this, xhr); }); // error xhr.addEventListener('error', function(e){ + // Log the error if API logging is enabled + if (globalThis.puter?.apiCallLogger?.isEnabled() && this._puterRequestId) { + globalThis.puter.apiCallLogger.logRequest({ + service: this._puterRequestId.service, + operation: this._puterRequestId.operation, + params: this._puterRequestId.params, + error: { + message: 'Network error occurred', + event: e.type + } + }); + } return handle_error(error_cb, reject_func, this); }) } @@ -247,11 +281,32 @@ async function driverCall_( contentType = 'application/json;charset=UTF-8', settings = {}, ) { + // Generate request ID for logging + // Store request info for logging + let requestInfo = null; + if (globalThis.puter?.apiCallLogger?.isEnabled()) { + requestInfo = { + interface: driverInterface, + driver: driverName, + method: driverMethod, + args: driverArgs + }; + } + // If there is no authToken and the environment is web, try authenticating with Puter if(!puter.authToken && puter.env === 'web'){ try{ await puter.ui.authenticateWithPuter(); }catch(e){ + // Log authentication error + if (requestInfo && globalThis.puter?.apiCallLogger?.isEnabled()) { + globalThis.puter.apiCallLogger.logRequest({ + service: 'drivers', + operation: `${driverInterface}::${driverMethod}`, + params: { interface: driverInterface, driver: driverName, method: driverMethod, args: driverArgs }, + error: { code: 'auth_canceled', message: 'Authentication canceled' } + }); + } return reject_func({ error: { code: 'auth_canceled', message: 'Authentication canceled' @@ -265,6 +320,11 @@ async function driverCall_( // create xhr object const xhr = initXhr('/drivers/call', puter.APIOrigin, puter.authToken, 'POST', contentType); + // Store request info for later logging + if (requestInfo) { + xhr._puterDriverRequestInfo = requestInfo; + } + if ( settings.responseType ) { xhr.responseType = settings.responseType; } @@ -370,6 +430,18 @@ async function driverCall_( return; } const resp = await parseResponse(response.target); + + // Log driver call response + if (this._puterDriverRequestInfo && globalThis.puter?.apiCallLogger?.isEnabled()) { + globalThis.puter.apiCallLogger.logRequest({ + service: 'drivers', + operation: `${this._puterDriverRequestInfo.interface}::${this._puterDriverRequestInfo.method}`, + params: { interface: this._puterDriverRequestInfo.interface, driver: this._puterDriverRequestInfo.driver, method: this._puterDriverRequestInfo.method, args: this._puterDriverRequestInfo.args }, + result: response.status >= 400 || resp?.success === false ? null : resp, + error: response.status >= 400 || resp?.success === false ? resp : null + }); + } + // HTTP Error - unauthorized if(response.status === 401 || resp?.code === "token_auth_failed"){ if(resp?.code === "token_auth_failed" && puter.env === 'web'){ @@ -436,6 +508,15 @@ async function driverCall_( // error xhr.addEventListener('error', function(e){ + // Log driver call error + if (this._puterDriverRequestInfo && globalThis.puter?.apiCallLogger?.isEnabled()) { + globalThis.puter.apiCallLogger.logRequest({ + service: 'drivers', + operation: `${this._puterDriverRequestInfo.interface}::${this._puterDriverRequestInfo.method}`, + params: { interface: this._puterDriverRequestInfo.interface, driver: this._puterDriverRequestInfo.driver, method: this._puterDriverRequestInfo.method, args: this._puterDriverRequestInfo.args }, + error: { message: 'Network error occurred', event: e.type } + }); + } return handle_error(error_cb, reject_func, this); }) diff --git a/src/puter-js/src/modules/Auth.js b/src/puter-js/src/modules/Auth.js index 0c9148f2f..2f85f8df1 100644 --- a/src/puter-js/src/modules/Auth.js +++ b/src/puter-js/src/modules/Auth.js @@ -130,12 +130,41 @@ class Auth{ } async whoami () { - const resp = await fetch(this.APIOrigin + '/whoami', { - headers: { - Authorization: `Bearer ${this.authToken}` + try { + const resp = await fetch(this.APIOrigin + '/whoami', { + headers: { + Authorization: `Bearer ${this.authToken}` + } + }); + + const result = await resp.json(); + + // Log the response + if (globalThis.puter?.apiCallLogger?.isEnabled()) { + globalThis.puter.apiCallLogger.logRequest({ + service: 'auth', + operation: 'whoami', + params: {}, + result: result + }); } - }); - return await resp.json(); + + return result; + } catch (error) { + // Log the error + if (globalThis.puter?.apiCallLogger?.isEnabled()) { + globalThis.puter.apiCallLogger.logRequest({ + service: 'auth', + operation: 'whoami', + params: {}, + error: { + message: error.message || error.toString(), + stack: error.stack + } + }); + } + throw error; + } } } diff --git a/src/puter-js/src/modules/Drivers.js b/src/puter-js/src/modules/Drivers.js index 529a482f7..d81d3cfb6 100644 --- a/src/puter-js/src/modules/Drivers.js +++ b/src/puter-js/src/modules/Drivers.js @@ -31,34 +31,73 @@ class FetchDriverCallBackend { } async call ({ driver, method_name, parameters }) { - const resp = await fetch(`${this.context.APIOrigin}/drivers/call`, { - headers: { - Authorization: `Bearer ${this.context.authToken}`, - 'Content-Type': 'application/json', - }, - method: 'POST', - body: JSON.stringify({ - 'interface': driver.iface_name, - ...(driver.service_name - ? { service: driver.service_name } - : {}), - method: method_name, - args: parameters, - }), - }); - - const content_type = resp.headers.get('content-type') - .split(';')[0].trim(); // TODO: parser for Content-Type - const handler = this.response_handlers[content_type]; - if ( ! handler ) { - const msg = `unrecognized content type: ${content_type}`; - console.error(msg); - console.error('creating blob so dev tools shows response...'); - await resp.blob(); - throw new Error(msg); + try { + const resp = await fetch(`${this.context.APIOrigin}/drivers/call`, { + headers: { + Authorization: `Bearer ${this.context.authToken}`, + 'Content-Type': 'application/json', + }, + method: 'POST', + body: JSON.stringify({ + 'interface': driver.iface_name, + ...(driver.service_name + ? { service: driver.service_name } + : {}), + method: method_name, + args: parameters, + }), + }); + + const content_type = resp.headers.get('content-type') + .split(';')[0].trim(); // TODO: parser for Content-Type + const handler = this.response_handlers[content_type]; + if ( ! handler ) { + const msg = `unrecognized content type: ${content_type}`; + console.error(msg); + console.error('creating blob so dev tools shows response...'); + await resp.blob(); + + // Log the error + if (globalThis.puter?.apiCallLogger?.isEnabled()) { + globalThis.puter.apiCallLogger.logRequest({ + service: 'drivers', + operation: `${driver.iface_name}::${method_name}`, + params: { interface: driver.iface_name, driver: driver.service_name || driver.iface_name, method: method_name, args: parameters }, + error: { message: msg } + }); + } + + throw new Error(msg); + } + + const result = await handler(resp); + + // Log the successful response + if (globalThis.puter?.apiCallLogger?.isEnabled()) { + globalThis.puter.apiCallLogger.logRequest({ + service: 'drivers', + operation: `${driver.iface_name}::${method_name}`, + params: { interface: driver.iface_name, driver: driver.service_name || driver.iface_name, method: method_name, args: parameters }, + result: result + }); + } + + return result; + } catch (error) { + // Log unexpected errors + if (globalThis.puter?.apiCallLogger?.isEnabled()) { + globalThis.puter.apiCallLogger.logRequest({ + service: 'drivers', + operation: `${driver.iface_name}::${method_name}`, + params: { interface: driver.iface_name, driver: driver.service_name || driver.iface_name, method: method_name, args: parameters }, + error: { + message: error.message || error.toString(), + stack: error.stack + } + }); + } + throw error; } - - return await handler(resp); } } @@ -137,14 +176,42 @@ class Drivers { } async list () { - const resp = await fetch(`${this.APIOrigin}/lsmod`, { - headers: { - Authorization: 'Bearer ' + this.authToken, - }, - method: 'POST' - }); - const list = await resp.json(); - return list.interfaces; + try { + const resp = await fetch(`${this.APIOrigin}/lsmod`, { + headers: { + Authorization: 'Bearer ' + this.authToken, + }, + method: 'POST' + }); + + const list = await resp.json(); + + // Log the response + if (globalThis.puter?.apiCallLogger?.isEnabled()) { + globalThis.puter.apiCallLogger.logRequest({ + service: 'drivers', + operation: 'list', + params: {}, + result: list.interfaces + }); + } + + return list.interfaces; + } catch (error) { + // Log the error + if (globalThis.puter?.apiCallLogger?.isEnabled()) { + globalThis.puter.apiCallLogger.logRequest({ + service: 'drivers', + operation: 'list', + params: {}, + error: { + message: error.message || error.toString(), + stack: error.stack + } + }); + } + throw error; + } } async get (iface_name, service_name) { diff --git a/src/puter-js/src/modules/networking/requests.js b/src/puter-js/src/modules/networking/requests.js index d98a6200c..6bbf81fce 100644 --- a/src/puter-js/src/modules/networking/requests.js +++ b/src/puter-js/src/modules/networking/requests.js @@ -52,9 +52,20 @@ export function pFetch(...args) { parsedURL.port || 443, ); } else { - rej( - `Failed to fetch. URL scheme "${parsedURL.protocol}" is not supported.`, - ); + const errorMsg = `Failed to fetch. URL scheme "${parsedURL.protocol}" is not supported.`; + + // Log the error + if (globalThis.puter?.apiCallLogger?.isEnabled()) { + globalThis.puter.apiCallLogger.logRequest({ + service: 'network', + operation: 'pFetch', + params: { url: reqObj.url, method: reqObj.method }, + error: { message: errorMsg } + }); + } + + rej(errorMsg); + return; } // Sending default UA @@ -198,6 +209,17 @@ export function pFetch(...args) { chunkedTransfer = parsedHead.headers.get("transfer-encoding") === "chunked"; + + // Log the response + if (globalThis.puter?.apiCallLogger?.isEnabled()) { + globalThis.puter.apiCallLogger.logRequest({ + service: 'network', + operation: 'pFetch', + params: { url: reqObj.url, method: reqObj.method }, + result: { status: parsedHead.status, statusText: parsedHead.statusText } + }); + } + // Return initial response object res(new Response(outStream, parsedHead)); @@ -232,11 +254,29 @@ export function pFetch(...args) { } }); socket.on("error", (reason) => { + // Log the error + if (globalThis.puter?.apiCallLogger?.isEnabled()) { + globalThis.puter.apiCallLogger.logRequest({ + service: 'network', + operation: 'pFetch', + params: { url: reqObj.url, method: reqObj.method }, + error: { message: "Socket errored with the following reason: " + reason } + }); + } rej("Socket errored with the following reason: " + reason); }); }, }); } catch (e) { + // Log unexpected errors + if (globalThis.puter?.apiCallLogger?.isEnabled()) { + globalThis.puter.apiCallLogger.logRequest({ + service: 'network', + operation: 'pFetch', + params: { url: reqObj.url, method: reqObj.method }, + error: { message: e.message || e.toString(), stack: e.stack } + }); + } rej(e); }}); }