diff --git a/src/backend/src/services/FilesystemAPIService.js b/src/backend/src/services/FilesystemAPIService.js
index 2b054599..c18ec04e 100644
--- a/src/backend/src/services/FilesystemAPIService.js
+++ b/src/backend/src/services/FilesystemAPIService.js
@@ -61,7 +61,7 @@ class FilesystemAPIService extends BaseService {
app.use(require('../routers/filesystem_api/rename'))
app.use(require('../routers/filesystem_api/search'))
-
+
// v1
app.use(require('../routers/writeFile'))
app.use(require('../routers/file'))
diff --git a/src/gui/src/IPC.js b/src/gui/src/IPC.js
index ce1e360e..3b83927d 100644
--- a/src/gui/src/IPC.js
+++ b/src/gui/src/IPC.js
@@ -1078,12 +1078,15 @@ const ipc_listener = async (event, handled) => {
// disable parent window
event.data.options.window_options.disable_parent_window = true;
- let granted = await UIWindowRequestPermission({
- origin: event.origin,
- permission: event.data.options.permission,
- window_options: event.data.options.window_options,
- });
-
+ let granted = await UIWindowRequestPermission(
+ {
+ permission: event.data.options.permission,
+ window_options: event.data.options.window_options,
+ app_uid: app_uuid,
+ app_name: app_name,
+ }
+ );
+
// send selected font to requester window
target_iframe.contentWindow.postMessage({
msg: "permissionGranted",
diff --git a/src/gui/src/UI/UIWindowRequestPermission.js b/src/gui/src/UI/UIWindowRequestPermission.js
index 70e1f076..f34a37ee 100644
--- a/src/gui/src/UI/UIWindowRequestPermission.js
+++ b/src/gui/src/UI/UIWindowRequestPermission.js
@@ -17,127 +17,182 @@
* along with this program. If not, see .
*/
-import UIWindow from './UIWindow.js'
+import UIWindow from './UIWindow.js';
-async function UIWindowRequestPermission(options){
+async function UIWindowRequestPermission(options) {
options = options ?? {};
options.reload_on_success = options.reload_on_success ?? false;
- return new Promise(async (resolve) => {
- let drivers = [
- {
- name: 'puter-chat-completion',
- human_name: 'AI Chat Completion',
- description: 'This app wants to generate text using AI. This may incur costs on your behalf.',
- },
- {
- name: 'puter-image-generation',
- human_name: 'AI Image Generation',
- description: 'This app wants to generate images using AI. This may incur costs on your behalf.',
- },
- {
- name: 'puter-kvstore',
- human_name: 'Puter Storage',
- description: 'This app wants to securely store data in your Puter account. This app will not be able to access your personal data or data stored by other apps.',
+
+ return new Promise((resolve) => {
+ get_permission_description(options.permission).then((permission_description) => {
+ if (!permission_description) {
+ resolve(false);
+ return;
}
- ]
-
- let parts = options.permission.split(":");
- let driver_name = parts[1];
- let action_name = parts[2];
-
- function findDriverByName(driverName) {
- return drivers.find(driver => driver.name === driverName);
- }
-
- let driver = findDriverByName(driver_name);
-
- if(driver === undefined){
- resolve(false);
- return;
- }
-
- let h = ``;
- h += `
`;
- h += `
`;
- // title
- h += `
"${html_encode(options.app_uid ?? options.origin)}" would Like to use ${html_encode(driver.human_name)}
`;
- // todo show the real description of action
- h += `
${html_encode(driver.description)}
`;
- // Allow/Don't Allow
- h += `
`;
- h += `
`;
- h += `
`;
- h += `
`;
-
- const el_window = await UIWindow({
- title: null,
- app: 'request-authorization',
- single_instance: true,
- icon: null,
- uid: null,
- is_dir: false,
- body_content: h,
- has_head: true,
- selectable_body: false,
- draggable_body: true,
- allow_context_menu: false,
- is_draggable: true,
- is_droppable: false,
- is_resizable: false,
- stay_on_top: false,
- allow_native_ctxmenu: true,
- allow_user_select: true,
- ...options.window_options,
- width: 350,
- dominant: true,
- on_close: ()=>{
- resolve(false)
- },
- onAppend: function(this_window){
- },
- window_class: 'window-login',
- window_css:{
- height: 'initial',
- },
- body_css: {
- width: 'initial',
- padding: '0',
- 'background-color': 'rgba(231, 238, 245, .95)',
- 'backdrop-filter': 'blur(3px)',
- }
- })
-
- $(el_window).find('.app-auth-allow').on('click', async function(e){
- $(this).addClass('disabled');
-
- try{
- const res = await fetch( window.api_origin + "/auth/grant-user-app", {
- "headers": {
- "Content-Type": "application/json",
- "Authorization": "Bearer " + window.auth_token,
- },
- "body": JSON.stringify({
- app_uid: options.app_uid,
- origin: options.origin,
- permission: options.permission
- }),
- "method": "POST",
- });
- }catch(err){
- console.error(err);
- resolve(err);
- }
-
- resolve(true);
- })
-
- $(el_window).find('.app-auth-dont-allow').on('click', function(e){
- $(this).addClass('disabled');
- $(el_window).close();
- resolve(false);
- })
- })
+ create_permission_window(options, permission_description, resolve).then((el_window) => {
+ setup_window_events(el_window, options, resolve);
+ });
+ });
+ });
}
-export default UIWindowRequestPermission
\ No newline at end of file
+/**
+ * Creates the permission dialog
+ */
+async function create_permission_window(options, permission_description, resolve) {
+ const requestingEntity = options.app_name ?? options.origin;
+ const h = create_window_content(requestingEntity, permission_description);
+
+ return await UIWindow({
+ title: null,
+ app: 'request-authorization',
+ single_instance: true,
+ icon: null,
+ uid: null,
+ is_dir: false,
+ body_content: h,
+ has_head: true,
+ selectable_body: false,
+ draggable_body: true,
+ allow_context_menu: false,
+ is_draggable: true,
+ is_droppable: false,
+ is_resizable: false,
+ stay_on_top: false,
+ allow_native_ctxmenu: true,
+ allow_user_select: true,
+ ...options.window_options,
+ width: 350,
+ dominant: true,
+ on_close: () => resolve(false),
+ onAppend: function(this_window) {},
+ window_class: 'window-login',
+ window_css: {
+ height: 'initial',
+ },
+ body_css: {
+ width: 'initial',
+ padding: '0',
+ 'background-color': 'rgba(231, 238, 245, .95)',
+ 'backdrop-filter': 'blur(3px)',
+ }
+ });
+}
+
+/**
+ * Creates HTML content for permission dialog
+ */
+function create_window_content(requestingEntity, permission_description) {
+ let h = ``;
+ h += ``;
+ h += `
`;
+ // title
+ h += `
${html_encode(requestingEntity)}
`;
+
+ // show the real description of action
+ h += `
${html_encode(requestingEntity)} is requesting for permission to ${html_encode(permission_description)}
`;
+
+ // Allow/Don't Allow
+ h += `
`;
+ h += `
`;
+ h += `
`;
+ h += `
`;
+ return h;
+}
+
+/**
+ * Sets up event handlers for permission dialog
+ */
+async function setup_window_events(el_window, options, resolve) {
+ $(el_window).find('.app-auth-allow').on('click', async function(e) {
+ $(this).addClass('disabled');
+
+ try {
+ // register granted permission to app or website
+ const res = await fetch(window.api_origin + "/auth/grant-user-app", {
+ headers: {
+ "Content-Type": "application/json",
+ "Authorization": "Bearer " + window.auth_token,
+ },
+ body: JSON.stringify({
+ app_uid: options.app_uid,
+ origin: options.origin,
+ permission: options.permission
+ }),
+ method: "POST",
+ });
+
+ if (!res.ok) {
+ throw new Error(`HTTP error! Status: ${res.status}`);
+ }
+
+ $(el_window).close();
+ resolve(true);
+ } catch (err) {
+ console.error(err);
+ resolve(err);
+ }
+ });
+
+ $(el_window).find('.app-auth-dont-allow').on('click', function(e) {
+ $(this).addClass('disabled');
+ $(el_window).close();
+ resolve(false);
+ });
+}
+
+/**
+ * Generates user-friendly description of permission string. Currently handles:
+ * fs:UUID-OF-FILE:read, thread:UUID-OF-THREAD:post, service:name-of-service:ii:name-of-interface, driver:driver-name:action-name
+ */
+async function get_permission_description(permission) {
+ const parts = split_permission(permission);
+ const [resource_type, resource_id, action, interface_name = null] = parts;
+ let fsentry;
+
+ if (resource_type === "fs") {
+ fsentry = await puter.fs.stat({ uid: resource_id });
+ }
+
+ const permission_mappings = {
+ "fs": fsentry ? `use ${fsentry.name} located at ${fsentry.dirpath} with ${action} access.` : null,
+ "thread": action === "post" ? `post to thread ${resource_id}.` : null,
+ "service": action === "ii" ? `use ${resource_id} to invoke ${interface_name}.` : null,
+ "driver": `use ${resource_id} to ${action}.`,
+ };
+
+ return permission_mappings[resource_type];
+}
+
+function split_permission(permission) {
+ return permission
+ .split(':')
+ .map(unescape_permission_component);
+}
+
+function unescape_permission_component(component) {
+ let unescaped_str = '';
+ // Constant for unescaped permission component string
+ const STATE_NORMAL = {};
+ // Constant for escaping special characters in permission strings
+ const STATE_ESCAPE = {};
+ let state = STATE_NORMAL;
+ const const_escapes = { C: ':' };
+ for (let i = 0; i < component.length; i++) {
+ const c = component[i];
+ if (state === STATE_NORMAL) {
+ if (c === '\\') {
+ state = STATE_ESCAPE;
+ } else {
+ unescaped_str += c;
+ }
+ } else if (state === STATE_ESCAPE) {
+ unescaped_str += const_escapes.hasOwnProperty(c) ? const_escapes[c] : c;
+ state = STATE_NORMAL;
+ }
+ }
+ return unescaped_str;
+}
+
+export default UIWindowRequestPermission;
diff --git a/src/puter-js/src/modules/UI.js b/src/puter-js/src/modules/UI.js
index bcbc2222..5e25f40d 100644
--- a/src/puter-js/src/modules/UI.js
+++ b/src/puter-js/src/modules/UI.js
@@ -860,6 +860,19 @@ class UI extends EventListener {
this.#postMessageWithObject('setMenubar', spec);
}
+ requestPermission = function(options) {
+ return new Promise((resolve) => {
+ if (this.env === 'app') {
+ return new Promise((resolve) => {
+ this.#postMessageWithCallback('requestPermission', resolve, { options });
+ })
+ } else {
+ // TODO: Implement for web
+ resolve(false);
+ }
+ })
+ }
+
disableMenuItem = function(item_id) {
this.#postMessageWithObject('disableMenuItem', {id: item_id});
}