Răsfoiți Sursa

dev: organize share sequence

KernelDeimos 5 luni în urmă
părinte
comite
168dbad295

+ 4 - 0
src/backend/src/codex/Sequence.js

@@ -141,6 +141,10 @@ class Sequence {
                     this.thisArg, this,
                 );
                 
+                if ( this.last_return_ instanceof Sequence.SequenceState ) {
+                    this.scope_ = this.last_return_.scope_;
+                }
+                
                 if ( this.sequence_.options_.after_each ) {
                     await this.sequence_.options_.after_each(this, step);
                 }

+ 7 - 430
src/backend/src/structured/sequence/share.js

@@ -21,14 +21,8 @@ const { Sequence } = require("../../codex/Sequence");
 const config = require("../../config");
 const { WorkList } = require("../../util/workutil");
 
-const validator = require('validator');
-const { get_user, get_app } = require("../../helpers");
-const { PermissionUtil } = require("../../services/auth/PermissionService");
-const FSNodeParam = require("../../api/filesystem/FSNodeParam");
-const { TYPE_DIRECTORY } = require("../../filesystem/FSNodeContext");
 const { UsernameNotifSelector } = require("../../services/NotificationService");
 const { quot } = require('@heyputer/putility').libs.string;
-const { whatis } = require("../../util/langutil");
 
 /*
     This code is optimized for editors supporting folding.
@@ -44,128 +38,12 @@ const { whatis } = require("../../util/langutil");
 
 
 module.exports = new Sequence([
-    function validate_metadata (a) {
-        const req = a.get('req');
-        const metadata = req.body.metadata;
-
-        if ( ! metadata ) return;
-        
-        if ( typeof metadata !== 'object' ) {
-            throw APIError.create('field_invalid', null, {
-                key: 'metadata',
-                expected: 'object',
-                got: whatis(metadata),
-            });
-        }
-
-        const MAX_KEYS = 20;
-        const MAX_STRING = 255;
-        const MAX_MESSAGE_STRING = 10*1024;
-
-        if ( Object.keys(metadata).length > MAX_KEYS ) {
-            throw APIError.create('field_invalid', null, {
-                key: 'metadata',
-                expected: `at most ${MAX_KEYS} keys`,
-                got: `${Object.keys(metadata).length} keys`,
-            });
-        }
-
-        for ( const key in metadata ) {
-            const value = metadata[key];
-            if ( typeof value !== 'string' && typeof value !== 'number' ) {
-                throw APIError.create('field_invalid', null, {
-                    key: `metadata.${key}`,
-                    expected: 'string or number',
-                    got: whatis(value),
-                });
-            }
-            if ( key === 'message' ) {
-                if ( typeof value !== 'string' ) {
-                    throw APIError.create('field_invalid', null, {
-                        key: `metadata.${key}`,
-                        expected: 'string',
-                        got: whatis(value),
-                    });
-                }
-                if ( value.length > MAX_MESSAGE_STRING ) {
-                    throw APIError.create('field_invalid', null, {
-                        key: `metadata.${key}`,
-                        expected: `at most ${MAX_MESSAGE_STRING} characters`,
-                        got: `${value.length} characters`,
-                    });
-                }
-                continue;
-            }
-            if ( typeof value === 'string' && value.length > MAX_STRING ) {
-                throw APIError.create('field_invalid', null, {
-                    key: `metadata.${key}`,
-                    expected: `at most ${MAX_STRING} characters`,
-                    got: `${value.length} characters`,
-                });
-            }
-        }
+    function testing_a_thing (a) {
+        a.set('thing', 'a thing');
     },
-    function validate_mode (a) {
-        const req = a.get('req');
-        const mode = req.body.mode;
-        
-        if ( mode === 'strict' ) {
-            a.set('strict_mode', true);
-            return;
-        }
-        if ( ! mode || mode === 'best-effort' ) {
-            a.set('strict_mode', false);
-            return;
-        }
-        throw APIError.create('field_invalid', null, {
-            key: 'mode',
-            expected: '`strict`, `best-effort`, or undefined',
-        });
-    },
-    function validate_recipients (a) {
-        const req = a.get('req');
-        let recipients = req.body.recipients;
-
-        // A string can be adapted to an array of one string
-        if ( typeof recipients === 'string' ) {
-            recipients = [recipients];
-        }
-        // Must be an array
-        if ( ! Array.isArray(recipients) ) {
-            throw APIError.create('field_invalid', null, {
-                key: 'recipients',
-                expected: 'array or string',
-                got: typeof recipients,
-            })
-        }
-        // At least one recipient
-        if ( recipients.length < 1 ) {
-            throw APIError.create('field_invalid', null, {
-                key: 'recipients',
-                expected: 'at least one',
-                got: 'none',
-            });
-        }
-        a.set('req_recipients', recipients);
-    },
-    function validate_shares (a) {
-        const req = a.get('req');
-        let shares = req.body.shares;
-
-        if ( ! Array.isArray(shares) ) {
-            shares = [shares];
-        }
-        
-        // At least one share
-        if ( shares.length < 1 ) {
-            throw APIError.create('field_invalid', null, {
-                key: 'shares',
-                expected: 'at least one',
-                got: 'none',
-            });
-        }
-        
-        a.set('req_shares', shares);
+    require('./share/validate.js'),
+    function testing_a_thing (a) {
+        console.log('ASDFASDFASDF', a.get('asdf'));
     },
     function initialize_result_object (a) {
         a.set('result', {
@@ -220,295 +98,8 @@ module.exports = new Sequence([
         
         a.values({ recipients_work, shares_work });
     },
-    new Sequence({ name: 'process recipients',
-        after_each (a) {
-            const { recipients_work } = a.values();
-            recipients_work.clear_invalid();
-        }
-    }, [
-        function valid_username_or_email (a) {
-            const { result, recipients_work } = a.values();
-            for ( const item of recipients_work.list() ) {
-                const { value, i } = item;
-                
-                if ( typeof value !== 'string' ) {
-                    item.invalid = true;
-                    result.recipients[i] =
-                        APIError.create('invalid_username_or_email', null, {
-                            value,
-                        });
-                    continue;
-                }
-
-                if ( value.match(config.username_regex) ) {
-                    item.type = 'username';
-                    continue;
-                }
-                if ( validator.isEmail(value) ) {
-                    item.type = 'email';
-                    continue;
-                }
-                
-                item.invalid = true;
-                result.recipients[i] =
-                    APIError.create('invalid_username_or_email', null, {
-                        value,
-                    });
-            }
-        },
-        async function check_existing_users_for_email_shares (a) {
-            const { recipients_work } = a.values();
-            for ( const recipient_item of recipients_work.list() ) {
-                if ( recipient_item.type !== 'email' ) continue;
-                const user = await get_user({
-                    email: recipient_item.value,
-                });
-                if ( ! user ) continue;
-                recipient_item.type = 'username';
-                recipient_item.value = user.username;
-            }
-        },
-        async function check_username_specified_users_exist (a) {
-            const { result, recipients_work } = a.values();
-            for ( const item of recipients_work.list() ) {
-                if ( item.type !== 'username' ) continue;
-
-                const user = await get_user({ username: item.value });
-                if ( ! user ) {
-                    item.invalid = true;
-                    result.recipients[item.i] =
-                        APIError.create('user_does_not_exist', null, {
-                            username: item.value,
-                        });
-                    continue;
-                }
-                item.user = user;
-            }
-        }
-    ]),
-    new Sequence({ name: 'process shares',
-        beforeEach (a) {
-            const { shares_work } = a.values();
-            shares_work.clear_invalid();
-        }
-    }, [
-        function validate_share_types (a) {
-            const { result, shares_work } = a.values();
-            
-            const lib_typeTagged = a.iget('services').get('lib-type-tagged');
-            
-            for ( const item of shares_work.list() ) {
-                const { i } = item;
-                let { value } = item;
-                
-                const thing = lib_typeTagged.process(value);
-                if ( thing.$ === 'error' ) {
-                    item.invalid = true;
-                    result.shares[i] =
-                        APIError.create('format_error', null, {
-                            message: thing.message
-                        });
-                    continue;
-                }
-                
-                const allowed_things = ['fs-share', 'app-share'];
-                if ( ! allowed_things.includes(thing.$) ) {
-                    item.invalid = true;
-                    result.shares[i] =
-                        APIError.create('disallowed_thing', null, {
-                            thing: thing.$,
-                            accepted: allowed_things,
-                        });
-                    continue;
-                }
-                
-                item.thing = thing;
-            }
-        },
-        function create_file_share_intents (a) {
-            const { result, shares_work } = a.values();
-            for ( const item of shares_work.list() ) {
-                const { thing } = item;
-                if ( thing.$ !== 'fs-share' ) continue;
-
-                item.type = 'fs';
-                const errors = [];
-                if ( ! thing.path ) {
-                    errors.push('`path` is required');
-                }
-                let access = thing.access;
-                if ( access ) {
-                    if ( ! ['read','write'].includes(access) ) {
-                        errors.push('`access` should be `read` or `write`');
-                    }
-                } else access = 'read';
-
-                if ( errors.length ) {
-                    item.invalid = true;
-                    result.shares[item.i] =
-                        APIError.create('field_errors', null, {
-                            key: `shares[${item.i}]`,
-                            errors
-                        });
-                    continue;
-                }
-                
-                item.path = thing.path;
-                item.share_intent = {
-                    $: 'share-intent:file',
-                    permissions: [PermissionUtil.join('fs', thing.path, access)],
-                };
-            }
-        },
-        function create_app_share_intents (a) {
-            const { result, shares_work } = a.values();
-            for ( const item of shares_work.list() ) {
-                const { thing } = item;
-                if ( thing.$ !== 'app-share' ) continue;
-
-                item.type = 'app';
-                const errors = [];
-                if ( ! thing.uid && ! thing.name ) {
-                    errors.push('`uid` or `name` is required');
-                }
-
-                if ( errors.length ) {
-                    item.invalid = true;
-                    result.shares[item.i] =
-                        APIError.create('field_errors', null, {
-                            key: `shares[${item.i}]`,
-                            errors
-                        });
-                    continue;
-                }
-                
-                const app_selector = thing.uid
-                    ? `uid#${thing.uid}` : thing.name;
-                
-                item.share_intent = {
-                    $: 'share-intent:app',
-                    permissions: [
-                        PermissionUtil.join('app', app_selector, 'access')
-                    ]
-                }
-                continue;
-            }
-        },
-        async function fetch_nodes_for_file_shares (a) {
-            const { req, result, shares_work } = a.values();
-            for ( const item of shares_work.list() ) {
-                if ( item.type !== 'fs' ) continue;
-                const node = await (new FSNodeParam('path')).consolidate({
-                    req, getParam: () => item.path
-                });
-                
-                if ( ! await node.exists() ) {
-                    item.invalid = true;
-                    result.shares[item.i] = APIError.create('subject_does_not_exist', {
-                        path: item.path,
-                    })
-                    continue;
-                }
-                
-                item.node = node;
-                let email_path = item.path;
-                let is_dir = true;
-                if ( await node.get('type') !== TYPE_DIRECTORY ) {
-                    is_dir = false;
-                    // remove last component
-                    email_path = email_path.slice(0, item.path.lastIndexOf('/')+1);
-                }
-
-                if ( email_path.startsWith('/') ) email_path = email_path.slice(1);
-                const email_link = `${config.origin}/show/${email_path}`;
-                item.is_dir = is_dir;
-                item.email_link = email_link;
-            }
-        },
-        async function fetch_apps_for_app_shares (a) {
-            const { result, shares_work } = a.values();
-            const db = a.iget('db');
-            
-            for ( const item of shares_work.list() ) {
-                if ( item.type !== 'app' ) continue;
-                const { thing } = item;
-
-                const app = await get_app(thing.uid ?
-                    { uid: thing.uid } : { name: thing.name });
-                if ( ! app ) {
-                    item.invalid = true;
-                    result.shares[item.i] =
-                        // note: since we're reporting `entity_not_found`
-                        // we will report the id as an entity-storage-compatible
-                        // identifier.
-                        APIError.create('entity_not_found', null, {
-                            identifier: thing.uid
-                                ? { uid: thing.uid }
-                                : { id: { name: thing.name } }
-                        });
-                }
-                
-                app.metadata = db.case({
-                    mysql: () => app.metadata,
-                    otherwise: () => JSON.parse(app.metadata ?? '{}')
-                })();
-                
-                item.app = app;
-            }
-        },
-        async function add_subdomain_permissions (a) {
-            const { shares_work } = a.values();
-            const actor = a.get('actor');
-            const db = a.iget('db');
-
-            for ( const item of shares_work.list() ) {
-                if ( item.type !== 'app' ) continue;
-                const [subdomain] = await db.read(
-                    `SELECT * FROM subdomains WHERE associated_app_id = ? ` +
-                    `AND user_id = ? LIMIT 1`,
-                    [item.app.id, actor.type.user.id]
-                );
-                if ( ! subdomain ) continue;
-                
-                // The subdomain is also owned by this user, so we'll
-                // add a permission for that as well
-                
-                const site_selector = `uid#${subdomain.uuid}`;
-                item.share_intent.permissions.push(
-                    PermissionUtil.join('site', site_selector, 'access')
-                )
-            }
-        },
-        async function add_appdata_permissions (a) {
-            const { result, shares_work } = a.values();
-            for ( const item of shares_work.list() ) {
-                if ( item.type !== 'app' ) continue;
-                if ( ! item.app.metadata?.shared_appdata ) continue;
-                
-                const app_owner = await get_user({ id: item.app.owner_user_id });
-                
-                const appdatadir =
-                    `/${app_owner.username}/AppData/${item.app.uid}`;
-                const appdatadir_perm =
-                    PermissionUtil.join('fs', appdatadir, 'write');
-
-                item.share_intent.permissions.push(appdatadir_perm);
-            }
-        },
-        function apply_success_status_to_shares (a) {
-            const { result, shares_work } = a.values();
-            for ( const item of shares_work.list() ) {
-                result.shares[item.i] =
-                    {
-                        $: 'api:status-report',
-                        status: 'success',
-                        fields: {
-                            permission: item.permission,
-                        }
-                    };
-            }
-        },
-    ]),
+    require('./share/process_recipients.js'),
+    require('./share/process_shares.js'),
     function abort_on_error_if_mode_is_strict (a) {
         const strict_mode = a.get('strict_mode');
         if ( ! strict_mode ) return;
@@ -587,20 +178,6 @@ module.exports = new Sequence([
                 }
             }
 
-            const apps = []; {
-                for ( const item of shares_work.list() ) {
-                    if ( item.thing.$ !== 'app' ) continue;
-                    // TODO: is there a general way to create a
-                    //       client-safe app right now without
-                    //       going through entity storage?
-                    // track: manual safe object
-                    apps.push(item.name
-                        ? item.name : await get_app({
-                            uid: item.uid,
-                        }));
-                }
-            }
-
             const metadata = a.get('req').body.metadata || {};
             
             svc_notification.notify(UsernameNotifSelector(username), {

+ 106 - 0
src/backend/src/structured/sequence/share/process_recipients.js

@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2024 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 A 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 <https://www.gnu.org/licenses/>.
+ */
+
+const APIError = require("../../../api/APIError");
+const { Sequence } = require("../../../codex/Sequence");
+const config = require("../../../config");
+
+const validator = require('validator');
+const { get_user } = require("../../../helpers");
+
+/*
+    This code is optimized for editors supporting folding.
+    Fold at Level 2 to conveniently browse sequence steps.
+    Fold at Level 3 after opening an inner-sequence.
+    
+    If you're using VSCode {
+        typically "Ctrl+K, Ctrl+2" or "⌘K, ⌘2";
+        to revert "Ctrl+K, Ctrl+J" or "⌘K, ⌘J";
+        https://stackoverflow.com/questions/30067767
+    }
+*/
+
+module.exports = new Sequence({
+    name: 'process recipients',
+    after_each (a) {
+        const { recipients_work } = a.values();
+        recipients_work.clear_invalid();
+    }
+}, [
+    function valid_username_or_email (a) {
+        const { result, recipients_work } = a.values();
+        for ( const item of recipients_work.list() ) {
+            const { value, i } = item;
+            
+            if ( typeof value !== 'string' ) {
+                item.invalid = true;
+                result.recipients[i] =
+                    APIError.create('invalid_username_or_email', null, {
+                        value,
+                    });
+                continue;
+            }
+
+            if ( value.match(config.username_regex) ) {
+                item.type = 'username';
+                continue;
+            }
+            if ( validator.isEmail(value) ) {
+                item.type = 'email';
+                continue;
+            }
+            
+            item.invalid = true;
+            result.recipients[i] =
+                APIError.create('invalid_username_or_email', null, {
+                    value,
+                });
+        }
+    },
+    async function check_existing_users_for_email_shares (a) {
+        const { recipients_work } = a.values();
+        for ( const recipient_item of recipients_work.list() ) {
+            if ( recipient_item.type !== 'email' ) continue;
+            const user = await get_user({
+                email: recipient_item.value,
+            });
+            if ( ! user ) continue;
+            recipient_item.type = 'username';
+            recipient_item.value = user.username;
+        }
+    },
+    async function check_username_specified_users_exist (a) {
+        const { result, recipients_work } = a.values();
+        for ( const item of recipients_work.list() ) {
+            if ( item.type !== 'username' ) continue;
+
+            const user = await get_user({ username: item.value });
+            if ( ! user ) {
+                item.invalid = true;
+                result.recipients[item.i] =
+                    APIError.create('user_does_not_exist', null, {
+                        username: item.value,
+                    });
+                continue;
+            }
+            item.user = user;
+        }
+    },
+    function return_state (a) { return a; }
+]);

+ 264 - 0
src/backend/src/structured/sequence/share/process_shares.js

@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2024 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 A 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 <https://www.gnu.org/licenses/>.
+ */
+
+const APIError = require("../../../api/APIError");
+const { Sequence } = require("../../../codex/Sequence");
+const config = require("../../../config");
+const { get_user, get_app } = require("../../../helpers");
+const { PermissionUtil } = require("../../../services/auth/PermissionService");
+const FSNodeParam = require("../../../api/filesystem/FSNodeParam");
+const { TYPE_DIRECTORY } = require("../../../filesystem/FSNodeContext");
+
+/*
+    This code is optimized for editors supporting folding.
+    Fold at Level 2 to conveniently browse sequence steps.
+    Fold at Level 3 after opening an inner-sequence.
+    
+    If you're using VSCode {
+        typically "Ctrl+K, Ctrl+2" or "⌘K, ⌘2";
+        to revert "Ctrl+K, Ctrl+J" or "⌘K, ⌘J";
+        https://stackoverflow.com/questions/30067767
+    }
+*/
+
+module.exports = new Sequence({
+    name: 'process shares',
+    beforeEach (a) {
+        const { shares_work } = a.values();
+        shares_work.clear_invalid();
+    }
+}, [
+    function validate_share_types (a) {
+        const { result, shares_work } = a.values();
+        
+        const lib_typeTagged = a.iget('services').get('lib-type-tagged');
+        
+        for ( const item of shares_work.list() ) {
+            const { i } = item;
+            let { value } = item;
+            
+            const thing = lib_typeTagged.process(value);
+            if ( thing.$ === 'error' ) {
+                item.invalid = true;
+                result.shares[i] =
+                    APIError.create('format_error', null, {
+                        message: thing.message
+                    });
+                continue;
+            }
+            
+            const allowed_things = ['fs-share', 'app-share'];
+            if ( ! allowed_things.includes(thing.$) ) {
+                item.invalid = true;
+                result.shares[i] =
+                    APIError.create('disallowed_thing', null, {
+                        thing: thing.$,
+                        accepted: allowed_things,
+                    });
+                continue;
+            }
+            
+            item.thing = thing;
+        }
+    },
+    function create_file_share_intents (a) {
+        const { result, shares_work } = a.values();
+        for ( const item of shares_work.list() ) {
+            const { thing } = item;
+            if ( thing.$ !== 'fs-share' ) continue;
+
+            item.type = 'fs';
+            const errors = [];
+            if ( ! thing.path ) {
+                errors.push('`path` is required');
+            }
+            let access = thing.access;
+            if ( access ) {
+                if ( ! ['read','write'].includes(access) ) {
+                    errors.push('`access` should be `read` or `write`');
+                }
+            } else access = 'read';
+
+            if ( errors.length ) {
+                item.invalid = true;
+                result.shares[item.i] =
+                    APIError.create('field_errors', null, {
+                        key: `shares[${item.i}]`,
+                        errors
+                    });
+                continue;
+            }
+            
+            item.path = thing.path;
+            item.share_intent = {
+                $: 'share-intent:file',
+                permissions: [PermissionUtil.join('fs', thing.path, access)],
+            };
+        }
+    },
+    function create_app_share_intents (a) {
+        const { result, shares_work } = a.values();
+        for ( const item of shares_work.list() ) {
+            const { thing } = item;
+            if ( thing.$ !== 'app-share' ) continue;
+
+            item.type = 'app';
+            const errors = [];
+            if ( ! thing.uid && ! thing.name ) {
+                errors.push('`uid` or `name` is required');
+            }
+
+            if ( errors.length ) {
+                item.invalid = true;
+                result.shares[item.i] =
+                    APIError.create('field_errors', null, {
+                        key: `shares[${item.i}]`,
+                        errors
+                    });
+                continue;
+            }
+            
+            const app_selector = thing.uid
+                ? `uid#${thing.uid}` : thing.name;
+            
+            item.share_intent = {
+                $: 'share-intent:app',
+                permissions: [
+                    PermissionUtil.join('app', app_selector, 'access')
+                ]
+            }
+            continue;
+        }
+    },
+    async function fetch_nodes_for_file_shares (a) {
+        const { req, result, shares_work } = a.values();
+        for ( const item of shares_work.list() ) {
+            if ( item.type !== 'fs' ) continue;
+            const node = await (new FSNodeParam('path')).consolidate({
+                req, getParam: () => item.path
+            });
+            
+            if ( ! await node.exists() ) {
+                item.invalid = true;
+                result.shares[item.i] = APIError.create('subject_does_not_exist', {
+                    path: item.path,
+                })
+                continue;
+            }
+            
+            item.node = node;
+            let email_path = item.path;
+            let is_dir = true;
+            if ( await node.get('type') !== TYPE_DIRECTORY ) {
+                is_dir = false;
+                // remove last component
+                email_path = email_path.slice(0, item.path.lastIndexOf('/')+1);
+            }
+
+            if ( email_path.startsWith('/') ) email_path = email_path.slice(1);
+            const email_link = `${config.origin}/show/${email_path}`;
+            item.is_dir = is_dir;
+            item.email_link = email_link;
+        }
+    },
+    async function fetch_apps_for_app_shares (a) {
+        const { result, shares_work } = a.values();
+        const db = a.iget('db');
+        
+        for ( const item of shares_work.list() ) {
+            if ( item.type !== 'app' ) continue;
+            const { thing } = item;
+
+            const app = await get_app(thing.uid ?
+                { uid: thing.uid } : { name: thing.name });
+            if ( ! app ) {
+                item.invalid = true;
+                result.shares[item.i] =
+                    // note: since we're reporting `entity_not_found`
+                    // we will report the id as an entity-storage-compatible
+                    // identifier.
+                    APIError.create('entity_not_found', null, {
+                        identifier: thing.uid
+                            ? { uid: thing.uid }
+                            : { id: { name: thing.name } }
+                    });
+            }
+            
+            app.metadata = db.case({
+                mysql: () => app.metadata,
+                otherwise: () => JSON.parse(app.metadata ?? '{}')
+            })();
+            
+            item.app = app;
+        }
+    },
+    async function add_subdomain_permissions (a) {
+        const { shares_work } = a.values();
+        const actor = a.get('actor');
+        const db = a.iget('db');
+
+        for ( const item of shares_work.list() ) {
+            if ( item.type !== 'app' ) continue;
+            const [subdomain] = await db.read(
+                `SELECT * FROM subdomains WHERE associated_app_id = ? ` +
+                `AND user_id = ? LIMIT 1`,
+                [item.app.id, actor.type.user.id]
+            );
+            if ( ! subdomain ) continue;
+            
+            // The subdomain is also owned by this user, so we'll
+            // add a permission for that as well
+            
+            const site_selector = `uid#${subdomain.uuid}`;
+            item.share_intent.permissions.push(
+                PermissionUtil.join('site', site_selector, 'access')
+            )
+        }
+    },
+    async function add_appdata_permissions (a) {
+        const { result, shares_work } = a.values();
+        for ( const item of shares_work.list() ) {
+            if ( item.type !== 'app' ) continue;
+            if ( ! item.app.metadata?.shared_appdata ) continue;
+            
+            const app_owner = await get_user({ id: item.app.owner_user_id });
+            
+            const appdatadir =
+                `/${app_owner.username}/AppData/${item.app.uid}`;
+            const appdatadir_perm =
+                PermissionUtil.join('fs', appdatadir, 'write');
+
+            item.share_intent.permissions.push(appdatadir_perm);
+        }
+    },
+    function apply_success_status_to_shares (a) {
+        const { result, shares_work } = a.values();
+        for ( const item of shares_work.list() ) {
+            result.shares[item.i] =
+                {
+                    $: 'api:status-report',
+                    status: 'success',
+                    fields: {
+                        permission: item.permission,
+                    }
+                };
+        }
+    },
+    function return_state (a) { return a; }
+]);

+ 165 - 0
src/backend/src/structured/sequence/share/validate.js

@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2024 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 A 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 <https://www.gnu.org/licenses/>.
+ */
+
+const APIError = require("../../../api/APIError");
+const { Sequence } = require("../../../codex/Sequence");
+const { whatis } = require("../../../util/langutil");
+
+/*
+    This code is optimized for editors supporting folding.
+    Fold at Level 2 to conveniently browse sequence steps.
+    Fold at Level 3 after opening an inner-sequence.
+    
+    If you're using VSCode {
+        typically "Ctrl+K, Ctrl+2" or "⌘K, ⌘2";
+        to revert "Ctrl+K, Ctrl+J" or "⌘K, ⌘J";
+        https://stackoverflow.com/questions/30067767
+    }
+*/
+
+module.exports = new Sequence({
+    name: 'validate request',
+}, [
+    function validate_metadata (a) {
+        console.log('thinngggggg', a.get('thing'));
+        a.set('asdf', 'zxcv');
+        const req = a.get('req');
+        const metadata = req.body.metadata;
+
+        if ( ! metadata ) return;
+        
+        if ( typeof metadata !== 'object' ) {
+            throw APIError.create('field_invalid', null, {
+                key: 'metadata',
+                expected: 'object',
+                got: whatis(metadata),
+            });
+        }
+
+        const MAX_KEYS = 20;
+        const MAX_STRING = 255;
+        const MAX_MESSAGE_STRING = 10*1024;
+
+        if ( Object.keys(metadata).length > MAX_KEYS ) {
+            throw APIError.create('field_invalid', null, {
+                key: 'metadata',
+                expected: `at most ${MAX_KEYS} keys`,
+                got: `${Object.keys(metadata).length} keys`,
+            });
+        }
+
+        for ( const key in metadata ) {
+            const value = metadata[key];
+            if ( typeof value !== 'string' && typeof value !== 'number' ) {
+                throw APIError.create('field_invalid', null, {
+                    key: `metadata.${key}`,
+                    expected: 'string or number',
+                    got: whatis(value),
+                });
+            }
+            if ( key === 'message' ) {
+                if ( typeof value !== 'string' ) {
+                    throw APIError.create('field_invalid', null, {
+                        key: `metadata.${key}`,
+                        expected: 'string',
+                        got: whatis(value),
+                    });
+                }
+                if ( value.length > MAX_MESSAGE_STRING ) {
+                    throw APIError.create('field_invalid', null, {
+                        key: `metadata.${key}`,
+                        expected: `at most ${MAX_MESSAGE_STRING} characters`,
+                        got: `${value.length} characters`,
+                    });
+                }
+                continue;
+            }
+            if ( typeof value === 'string' && value.length > MAX_STRING ) {
+                throw APIError.create('field_invalid', null, {
+                    key: `metadata.${key}`,
+                    expected: `at most ${MAX_STRING} characters`,
+                    got: `${value.length} characters`,
+                });
+            }
+        }
+    },
+    function validate_mode (a) {
+        const req = a.get('req');
+        const mode = req.body.mode;
+        
+        if ( mode === 'strict' ) {
+            a.set('strict_mode', true);
+            return;
+        }
+        if ( ! mode || mode === 'best-effort' ) {
+            a.set('strict_mode', false);
+            return;
+        }
+        throw APIError.create('field_invalid', null, {
+            key: 'mode',
+            expected: '`strict`, `best-effort`, or undefined',
+        });
+    },
+    function validate_recipients (a) {
+        const req = a.get('req');
+        let recipients = req.body.recipients;
+
+        // A string can be adapted to an array of one string
+        if ( typeof recipients === 'string' ) {
+            recipients = [recipients];
+        }
+        // Must be an array
+        if ( ! Array.isArray(recipients) ) {
+            throw APIError.create('field_invalid', null, {
+                key: 'recipients',
+                expected: 'array or string',
+                got: typeof recipients,
+            })
+        }
+        // At least one recipient
+        if ( recipients.length < 1 ) {
+            throw APIError.create('field_invalid', null, {
+                key: 'recipients',
+                expected: 'at least one',
+                got: 'none',
+            });
+        }
+        a.set('req_recipients', recipients);
+    },
+    function validate_shares (a) {
+        const req = a.get('req');
+        let shares = req.body.shares;
+
+        if ( ! Array.isArray(shares) ) {
+            shares = [shares];
+        }
+        
+        // At least one share
+        if ( shares.length < 1 ) {
+            throw APIError.create('field_invalid', null, {
+                key: 'shares',
+                expected: 'at least one',
+                got: 'none',
+            });
+        }
+        
+        a.set('req_shares', shares);
+    },
+    function return_state (a) { return a; }
+]);