Jelajahi Sumber

refactor: begin migrating utility code

I'm calling this approach a "re-core"; see src/backend-core-0/README.md
for more information about this.
KernelDeimos 1 bulan lalu
induk
melakukan
7a3365a25c

+ 9 - 0
package-lock.json

@@ -2049,6 +2049,10 @@
       "resolved": "src/backend",
       "link": true
     },
+    "node_modules/@heyputer/backend-core-0": {
+      "resolved": "src/backend-core-0",
+      "link": true
+    },
     "node_modules/@heyputer/gui": {
       "resolved": "src/gui",
       "link": true
@@ -15133,6 +15137,11 @@
         "typescript": "^5.1.6"
       }
     },
+    "src/backend-core-0": {
+      "name": "@heyputer/backend-core-0",
+      "version": "1.0.0",
+      "license": "AGPL-3.0-only"
+    },
     "src/backend/node_modules/lru-cache": {
       "version": "11.0.2",
       "license": "ISC",

+ 50 - 0
src/backend-core-0/README.md

@@ -0,0 +1,50 @@
+# What is `backend-core-0`?
+
+The ugly name is intentional. We prefer to refactor incrementally which
+means we need a way to "re-core" the backend, and we may do this more
+than once simultaneously (hence it's `0` right now).
+
+"re-core" is a term I just made up, and it means this:
+> To find the utility code that is not dependent on other utility code,
+> move that into a new package, and then continue this process in multiple
+> iterations until the problem being solved is solved.
+
+The purpose of `backend-core-0` is to move common dependencies for driver
+implementations into a new core so that existing driver implementations
+can be moved from backend modules (part of the `backend` package) to
+extensions (packages added to Puter at runtime).
+
+What will follow is a log of what was moved here and why.
+
+## 2025-03-31
+
+The AI/LLM driver module depends on constructs related to driver
+interfaces. The actual mechanism that facilitates these interfaces,
+as well as the interface format, both don't really have a name yet;
+I'll call it the "PDIM" (Puter Driver Interface Mechanism) in this log.
+
+The PDIM depends on some class definitions currently in
+`src/backend/src/services/drivers/meta` which are split into the categories
+of "Constructs" and "Runtime Entities". A construct is the class
+representation of something defined in an interface, including
+**Interface** itself, and a RuntimeEntity - well there's only one;
+it's a wrapper for runtime-typed values such as "jpeg stream".
+
+A construct called **Parameter**, which is the class represerntation
+of a parameter of an interface that a driver may implement, depends on
+a file called `types.js`. This file defines high-level types like String,
+URL, File, etc that can be used in Puter drivers.
+
+Some types depend on utilities in Puter's backend:
+- **File**
+  - filesystem/validation
+  - `is_valid_uuidv4` from helpers.js
+- **URL**
+  - `is_valid_url` from helpers.js
+
+These utilities do not have dependencies so they are good candidates
+to be moved into this package. Afterwards, it currently apperas that
+everything in `drivers/meta` can be moved here, allowing DriverService
+to finally be moved to a backend module (right now it's part of backend
+core), and driver modules like `puterai` will be closer to being able
+to be moved to extensions.

+ 20 - 0
src/backend-core-0/package.json

@@ -0,0 +1,20 @@
+{
+  "name": "@heyputer/backend-core-0",
+  "version": "1.0.0",
+  "description": "The ugly name is intentional. We prefer to refactor incrementally which means we need a way to \"re-core\" the backend, and we may do this more than once simultaneously (hence it's `0` right now).",
+  "type": "module",
+  "scripts": {
+    "build": "rollup -c",
+    "prepare": "npm run build",
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "exports": {
+    ".": {
+      "require": "./dist/cjs/exports.cjs",
+      "import": "./dist/esm/exports.js"
+    }
+  },
+  "keywords": [],
+  "author": "",
+  "license": "AGPL-3.0-only"
+}

+ 27 - 0
src/backend-core-0/rollup.config.js

@@ -0,0 +1,27 @@
+import { defineConfig } from 'rollup';
+import { nodeResolve } from '@rollup/plugin-node-resolve';
+import commonjs from '@rollup/plugin-commonjs';
+
+export default defineConfig([
+  // ESM build
+  {
+    input: 'src/exports.js',
+    output: {
+      dir: 'dist/esm',
+      format: 'es',
+      preserveModules: true
+    },
+    plugins: [nodeResolve()]
+  },
+  // CJS build
+  {
+    input: 'src/exports.js',
+    output: {
+      dir: 'dist/cjs',
+      format: 'cjs',
+      preserveModules: true,
+      entryFileNames: '[name].cjs',
+    },
+    plugins: [nodeResolve(), commonjs()]
+  }
+]);

+ 1 - 0
src/backend-core-0/src/exports.js

@@ -0,0 +1 @@
+export * as validation from './pdim/validation';

+ 73 - 0
src/backend-core-0/src/pdim/validation.js

@@ -0,0 +1,73 @@
+export const is_valid_uuid = ( uuid ) => {
+    let s = "" + uuid;
+    s = s.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
+    return !! s;
+}
+
+export const is_valid_uuid4 = ( uuid ) => {
+    return is_valid_uuid(uuid);
+}
+
+export const is_specifically_uuidv4 = ( uuid ) => {
+    let s = "" + uuid;
+
+    s = s.match(/^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i);
+    if (!s) {
+      return false;
+    }
+    return true;
+}
+
+export const is_valid_url = ( url ) => {
+    let s = "" + url;
+
+    try {
+        new URL(s);
+        return true;
+    } catch (e) {
+        return false;
+    }
+}
+
+const path_excludes = () => /[\x00-\x1F]/g;
+
+// this characters are not allowed in path names because
+// they might be used to trick the user into thinking
+// a filename is different from what it actually is.
+const safety_excludes = [
+    /[\u202A-\u202E]/, // RTL and LTR override
+    /[\u200E-\u200F]/, // RTL and LTR mark
+    /[\u2066-\u2069]/, // RTL and LTR isolate
+    /[\u2028-\u2029]/, // line and paragraph separator
+    /[\uFF01-\uFF5E]/, // fullwidth ASCII
+    /[\u2060]/,        // word joiner
+    /[\uFEFF]/,        // zero width no-break space
+    /[\uFFFE-\uFFFF]/, // non-characters
+];
+
+export const is_valid_path = (path, {
+    no_relative_components,
+    allow_path_fragment,
+} = {}) => {
+    if ( typeof path !== 'string' ) return false;
+    if ( path.length < 1 ) false;
+    if ( path_excludes().test(path) ) return false;
+    for ( const exclude of safety_excludes ) {
+        if ( exclude.test(path) ) return false;
+    }
+
+    if ( ! allow_path_fragment ) if ( path[0] !== '/' && path[0] !== '.' ) {
+        return false;
+    }
+
+    if ( no_relative_components ) {
+        const components = path.split('/');
+        for ( const component of components ) {
+            if ( component === '' ) continue;
+            const name_without_dots = component.replace(/\./g, '');
+            if ( name_without_dots.length < 1 ) return false;
+        }
+    }
+
+    return true;
+}

+ 1 - 35
src/backend/src/helpers.js

@@ -1194,37 +1194,6 @@ async function jwt_auth(req){
     return ancestors;
 }
 
-function is_valid_uuid ( uuid ) {
-    let s = "" + uuid;
-    s = s.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
-    return !! s;
-}
-
-function is_valid_uuid4 ( uuid ) {
-    return is_valid_uuid(uuid);
-}
-
-function is_specifically_uuidv4 ( uuid ) {
-    let s = "" + uuid;
-
-    s = s.match(/^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i);
-    if (!s) {
-      return false;
-    }
-    return true;
-}
-
-function is_valid_url ( url ) {
-    let s = "" + url;
-
-    try {
-        new URL(s);
-        return true;
-    } catch (e) {
-        return false;
-    }
-}
-
 function hyphenize_confirm_code(email_confirm_code){
     email_confirm_code = email_confirm_code.toString();
     email_confirm_code =
@@ -1679,12 +1648,9 @@ module.exports = {
     is_empty,
     is_shared_with,
     is_shared_with_anyone,
-    is_valid_uuid4,
-    is_valid_uuid,
-    is_specifically_uuidv4,
+    ...require('@heyputer/backend-core-0').validation,
     is_temp_users_disabled,
     is_user_signup_disabled,
-    is_valid_url,
     jwt_auth,
     mv,
     number_format,