dev(puter.js): add support to request app dir

Add support for requesting access to the root directory of an app's
associated subdomain.
This commit is contained in:
KernelDeimos
2026-02-17 22:19:52 -05:00
committed by Eric Dubé
parent bfff2d20f9
commit 05cc4ad477
3 changed files with 94 additions and 7 deletions
+12 -2
View File
@@ -145,7 +145,7 @@ async function setup_window_events (el_window, options, resolve) {
/**
* Generates user-friendly description of permission string in HTML format.
*
*
* @param {string} permission - The permission string to describe
* @returns {string} The user-friendly description of the permission in HTML format
*/
@@ -231,7 +231,17 @@ async function get_permission_description (permission) {
}
}
return null
if ( parts[0] === 'app-root-dir' ) {
// Format: app-root-dir:resource_request_code:access
if ( parts[2] === 'read' ) {
return i18n('perm_app_root_dir_read');
}
if ( parts[2] === 'write' ) {
return i18n('perm_app_root_dir_write');
}
}
return null;
}
/**
+5 -2
View File
@@ -116,7 +116,6 @@ const en = {
desktop_background_fit: 'Fit',
developers: 'Developers',
dir_published_as_website: '%strong% has been published to:',
directory_depth_limit_exceeded: 'Cannot create folder. The maximum directory depth has been reached. Please create the folder in a higher-level directory.',
disable_2fa: 'Disable 2FA',
disable_2fa_confirm: 'Are you sure you want to disable 2FA?',
disable_2fa_instructions: 'Enter your password to disable 2FA.',
@@ -169,7 +168,6 @@ const en = {
item: 'item',
items_in_trash_cannot_be_renamed: 'This item can\'t be renamed because it\'s in the trash. To rename this item, first drag it out of the Trash.',
jpeg_image: 'JPEG image',
add_to_desktop: 'Add to Desktop',
keep_in_taskbar: 'Keep in Taskbar',
language: 'Language',
license: 'License',
@@ -203,6 +201,7 @@ const en = {
no_dir_associated_with_site: 'No directory associated with this address.',
no_websites_published: 'You have not published any websites yet. Right click on a folder to get started.',
ok: 'OK',
or: 'or',
open: 'Open',
new_window: 'New Window',
open_in_ai: 'Open in AI',
@@ -483,6 +482,8 @@ const en = {
// Signup Window
'signup_confirm_password': 'Confirm Password',
sign_in_with_google: 'Sign in with Google',
sign_up_with_google: 'Sign up with Google',
// Login Window
'login_email_username_required': 'Email or username is required',
@@ -550,6 +551,8 @@ const en = {
'perm_apps_write': 'manage your apps',
'perm_subdomains_read': 'see your subdomains',
'perm_subdomains_write': 'manage your subdomains',
'perm_app_root_dir_read': 'read the root directory of one of your apps',
'perm_app_root_dir_write': 'read and write to the root directory of one of your apps',
'error_user_or_path_not_found': 'User or path not found.',
'error_invalid_username': 'Invalid username.',
+77 -3
View File
@@ -21,11 +21,15 @@ export default class Perms {
...(body ? { body: JSON.stringify(body) } : {}),
});
if ( resp.headers.get('content-type')?.includes('application/json') ) {
return await resp.json();
const jsonResult = await resp.json();
if ( resp.status !== 200 ) {
jsonResult.error = true;
}
return jsonResult;
}
return { message: await resp.text(), code: 'unknown_error' };
return { error: true, message: await resp.text(), code: 'unknown_error' };
} catch (e) {
return { message: e.message, code: 'internal_error' };
return { error: true, message: e.message, code: 'internal_error' };
}
}
@@ -317,6 +321,76 @@ export default class Perms {
return granted;
}
/**
* Request read access to the root directory of one of the user's apps,
* identified by its resource request code (e.g. from app.resource_request_code).
* If the user grants permission, returns the filesystem item for that directory.
* If the user denies or an error occurs, returns undefined.
*
* @param {string} resourceRequestCode - The resource request code (e.g. `${app.uid}:root_dir`)
* @return {Promise<object|undefined>} The directory fs item (stat) or undefined
*/
async requestReadAppRootDir (app_uid) {
return await this.#requestAppRootDir('read', app_uid);
}
/**
* Request write access to the root directory of one of the user's apps,
* identified by its resource request code (e.g. from app.resource_request_code).
* If the user grants permission, returns the filesystem item for that directory.
* If the user denies or an error occurs, returns undefined.
*
* @param {string} resourceRequestCode - The resource request code (e.g. `${app.uid}:root_dir`)
* @return {Promise<object|undefined>} The directory fs item (stat) or undefined
*/
async requestWriteAppRootDir (app_uid) {
return await this.#requestAppRootDir('write', app_uid);
}
async #requestAppRootDir (access, app_uid) {
if ( typeof app_uid === 'object' && app_uid !== null ) {
app_uid = app_uid.uid;
}
if ( typeof app_uid !== 'string' ) {
throw new Error('parameter app_uid must be a strinkg');
}
let result;
const fetchIt = async () => result = await this.req_('/auth/request-app-root-dir', {
app_uid,
access: 'read',
});
await fetchIt();
if ( ! result.error ) return result;
// Request permission
app_uid = (typeof app_or_uuid === 'object') && app_uid !== null
? app_uid.uid : app_uid;
const readAppRootDirPermission = `app-root-dir:${app_uid}:read`;
const granted = await this.puter.ui.requestPermission({
permission: readAppRootDirPermission,
});
if ( granted ) {
await fetchIt();
// If the server has cache invalidation issues, we still want this
// to work anyway. This is a hack, but also a reasonable safeguard.
let delay = 100;
const maxTotalWait = 5000;
let totalWaited = 0;
while ( result.error && totalWaited < maxTotalWait ) {
await new Promise(r => setTimeout(r, delay));
totalWaited += delay;
await fetchIt();
if ( ! result.error ) break;
delay = Math.min(delay * 2, Math.max(100, maxTotalWait - totalWaited));
}
}
if ( ! result.error ) return result;
return undefined;
}
/**
* Internal helper to request access to a user's special folder.
* @private