Restructure plugin API, implement basic minimal shell prototype
diff --git a/.vscode/launch.json b/.vscode/launch.json
deleted file mode 100644
index 197b39f..0000000
--- a/.vscode/launch.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
-  // Use IntelliSense to learn about possible attributes.
-  // Hover to view descriptions of existing attributes.
-  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
-  "version": "0.2.0",
-  "configurations": [
-    {
-      "name": "iris-server [wrangler]",
-      "preLaunchTask": "iris-server/wrangler",
-      "type": "node",
-      "request": "attach",
-      "port": 2501,
-      "resolveSourceMapLocations": null,
-      "attachExistingChildren": false,
-      "autoAttachChildProcesses": false,
-      "cwd": "${workspaceFolder}/packages/server"
-    }
-  ]
-}
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 0a31d2c..fb273e4 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -4,42 +4,6 @@
   "version": "2.0.0",
   "tasks": [
     {
-      "label": "iris-server/wrangler",
-      "type": "shell",
-      "command": "npm start",
-      "options": {
-        "cwd": "${workspaceFolder}/packages/server"
-      },
-      "isBackground": true,
-      "problemMatcher": {
-        "owner": "wrangler",
-        "source": "wrangler",
-        "pattern": {
-          "regexp": "\\b\\B",
-          "file": 1,
-          "line": 2,
-          "column": 3,
-          "message": 4
-        },
-        "background": {
-          "activeOnStart": true,
-          "beginsPattern": "15f8cd96-2ee2-4dcc-a757-5d8fc00ed307",
-          "endsPattern": "\\[wrangler:info\\] Ready on"
-        }
-      },
-      "group": {
-        "kind": "build",
-        "isDefault": true
-      },
-      "icon": {
-        "id": "cloud"
-      },
-      "dependsOn": [
-        "iris-sdk/tsc",
-        "iris-plugins/tsc"
-      ]
-    },
-    {
       "label": "iris-webui/vite",
       "type": "shell",
       "command": "npm run start",
@@ -71,29 +35,6 @@
         "id": "record"
       },
       "dependsOn": [
-        "iris-sdk/tsc",
-        "iris-plugins/tsc"
-      ]
-    },
-    {
-      "label": "iris-plugins/tsc",
-      "type": "shell",
-      "command": "npm start",
-      "options": {
-        "cwd": "${workspaceFolder}/packages/plugins"
-      },
-      "isBackground": true,
-      "problemMatcher": {
-        "base": "$tsc-watch"
-      },
-      "group": {
-        "kind": "build",
-        "isDefault": true
-      },
-      "icon": {
-        "id": "library"
-      },
-      "dependsOn": [
         "iris-sdk/tsc"
       ]
     },
diff --git a/package.json b/package.json
index 1d0a62e..d6e5b0a 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,8 @@
     "@react-router/remix-routes-option-adapter": "^7.7.0",
     "@remix-run/v1-route-convention": "patch:@remix-run/v1-route-convention@npm%3A0.1.4#~/.yarn/patches/@remix-run-v1-route-convention-npm-0.1.4-d336e45418.patch",
     "chroma-js": "^3.1.2",
+    "jose": "^6.0.12",
+    "lodash": "^4.17.21",
     "react": "^19.1.0",
     "react-dom": "^19.1.0",
     "react-router": "^7.5.3"
@@ -34,13 +36,16 @@
     "@react-router/dev": "^7.5.3",
     "@tailwindcss/vite": "^4.1.4",
     "@types/chroma-js": "^3.1.1",
+    "@types/lodash": "^4.17.20",
     "@types/node": "^20",
     "@types/react": "^19.1.2",
     "@types/react-dom": "^19.1.2",
     "cross-env": "^7.0.3",
+    "npm-run-all": "^4.1.5",
     "nx": "^21.2.4",
     "rimraf": "^6.0.1",
     "tailwindcss": "^4.1.4",
+    "tsc-alias": "^1.8.16",
     "typescript": "^5.8.3",
     "vite": "^6.3.3",
     "vite-tsconfig-paths": "^5.1.4",
diff --git a/packages/plugins/.gitignore b/packages/plugins/.gitignore
deleted file mode 100644
index 206df3e..0000000
--- a/packages/plugins/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-lib
-node_modules
-*.tsbuildinfo
\ No newline at end of file
diff --git a/packages/plugins/package.json b/packages/plugins/package.json
deleted file mode 100644
index 3406ac4..0000000
--- a/packages/plugins/package.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
-  "name": "iris-plugins",
-  "private": true,
-  "license": "LGPL-2.1-only",
-  "author": "jottocraft",
-  "scripts": {
-    "build": "tsc --build",
-    "clean": "rimraf lib",
-    "start": "tsc --build --watch",
-    "validate": "tsc --noEmit"
-  },
-  "dependencies": {
-    "@jottocraft/iris-sdk": "workspace:*"
-  }
-}
diff --git a/packages/plugins/src/index.ts b/packages/plugins/src/index.ts
deleted file mode 100644
index 7f535b5..0000000
--- a/packages/plugins/src/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-// Iris
-// Copyright (c) jottocraft 2025
-
-export const DUMMY_EXPORT = 1;
\ No newline at end of file
diff --git a/packages/plugins/tsconfig.json b/packages/plugins/tsconfig.json
deleted file mode 100644
index 599fdc0..0000000
--- a/packages/plugins/tsconfig.json
+++ /dev/null
@@ -1,113 +0,0 @@
-{
-  "include": [
-    "src"
-  ],
-  "compilerOptions": {
-    /* Visit https://aka.ms/tsconfig to read more about this file */
-
-    /* Projects */
-    "incremental": true,                                 /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
-    "composite": true,                                   /* Enable constraints that allow a TypeScript project to be used with project references. */
-    // "tsBuildInfoFile": "./.tsbuildinfo",              /* Specify the path to .tsbuildinfo incremental compilation file. */
-    // "disableSourceOfProjectReferenceRedirect": true,  /* Disable preferring source files instead of declaration files when referencing composite projects. */
-    // "disableSolutionSearching": true,                 /* Opt a project out of multi-project reference checking when editing. */
-    // "disableReferencedProjectLoad": true,             /* Reduce the number of projects loaded automatically by TypeScript. */
-
-    /* Language and Environment */
-    "target": "ESNext",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
-    // "lib": [],                                        /* Specify a set of bundled library declaration files that describe the target runtime environment. */
-    // "jsx": "preserve",                                /* Specify what JSX code is generated. */
-    // "experimentalDecorators": true,                   /* Enable experimental support for legacy experimental decorators. */
-    // "emitDecoratorMetadata": true,                    /* Emit design-type metadata for decorated declarations in source files. */
-    // "jsxFactory": "",                                 /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
-    // "jsxFragmentFactory": "",                         /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
-    // "jsxImportSource": "",                            /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
-    // "reactNamespace": "",                             /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
-    // "noLib": true,                                    /* Disable including any library files, including the default lib.d.ts. */
-    // "useDefineForClassFields": true,                  /* Emit ECMAScript-standard-compliant class fields. */
-    // "moduleDetection": "auto",                        /* Control what method is used to detect module-format JS files. */
-
-    /* Modules */
-    "module": "commonjs",                                /* Specify what module code is generated. */
-    "rootDir": "src",                                    /* Specify the root folder within your source files. */
-    "moduleResolution": "node",                          /* Specify how TypeScript looks up a file from a given module specifier. */
-    // "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */
-    "paths": {                                           /* Specify a set of entries that re-map imports to additional lookup locations. */
-      "~/*": ["./src/*"]
-    },
-    // "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */
-    // "typeRoots": [],                                  /* Specify multiple folders that act like './node_modules/@types'. */
-    // "types": [],                                      /* Specify type package names to be included without being referenced in a source file. */
-    // "allowUmdGlobalAccess": true,                     /* Allow accessing UMD globals from modules. */
-    // "moduleSuffixes": [],                             /* List of file name suffixes to search when resolving a module. */
-    // "allowImportingTsExtensions": true,               /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
-    // "resolvePackageJsonExports": true,                /* Use the package.json 'exports' field when resolving package imports. */
-    // "resolvePackageJsonImports": true,                /* Use the package.json 'imports' field when resolving imports. */
-    // "customConditions": [],                           /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
-    // "resolveJsonModule": true,                        /* Enable importing .json files. */
-    // "allowArbitraryExtensions": true,                 /* Enable importing files with any extension, provided a declaration file is present. */
-    // "noResolve": true,                                /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
-
-    /* JavaScript Support */
-    // "allowJs": true,                                  /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
-    // "checkJs": true,                                  /* Enable error reporting in type-checked JavaScript files. */
-    // "maxNodeModuleJsDepth": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
-
-    /* Emit */
-    "declaration": true,                                 /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
-    "declarationMap": true,                              /* Create sourcemaps for d.ts files. */
-    // "emitDeclarationOnly": true,                      /* Only output d.ts files and not JavaScript files. */
-    "sourceMap": true,                                   /* Create source map files for emitted JavaScript files. */
-    // "inlineSourceMap": true,                          /* Include sourcemap files inside the emitted JavaScript. */
-    // "outFile": "./",                                  /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
-    "outDir": "lib",                                     /* Specify an output folder for all emitted files. */
-    // "removeComments": true,                           /* Disable emitting comments. */
-    // "noEmit": true,                                   /* Disable emitting files from a compilation. */
-    // "importHelpers": true,                            /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
-    // "downlevelIteration": true,                       /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
-    // "sourceRoot": "",                                 /* Specify the root path for debuggers to find the reference source code. */
-    // "mapRoot": "",                                    /* Specify the location where debugger should locate map files instead of generated locations. */
-    // "inlineSources": true,                            /* Include source code in the sourcemaps inside the emitted JavaScript. */
-    // "emitBOM": true,                                  /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
-    // "newLine": "crlf",                                /* Set the newline character for emitting files. */
-    // "stripInternal": true,                            /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
-    // "noEmitHelpers": true,                            /* Disable generating custom helper functions like '__extends' in compiled output. */
-    // "noEmitOnError": true,                            /* Disable emitting files if any type checking errors are reported. */
-    // "preserveConstEnums": true,                       /* Disable erasing 'const enum' declarations in generated code. */
-    // "declarationDir": "./",                           /* Specify the output directory for generated declaration files. */
-
-    /* Interop Constraints */
-    "isolatedModules": true,                             /* Ensure that each file can be safely transpiled without relying on other imports. */
-    // "verbatimModuleSyntax": true,                     /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
-    // "isolatedDeclarations": true,                     /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
-    // "allowSyntheticDefaultImports": true,             /* Allow 'import x from y' when a module doesn't have a default export. */
-    "esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
-    // "preserveSymlinks": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
-    "forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */
-
-    /* Type Checking */
-    "strict": true,                                      /* Enable all strict type-checking options. */
-    // "noImplicitAny": true,                            /* Enable error reporting for expressions and declarations with an implied 'any' type. */
-    // "strictNullChecks": true,                         /* When type checking, take into account 'null' and 'undefined'. */
-    // "strictFunctionTypes": true,                      /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
-    // "strictBindCallApply": true,                      /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
-    // "strictPropertyInitialization": true,             /* Check for class properties that are declared but not set in the constructor. */
-    // "noImplicitThis": true,                           /* Enable error reporting when 'this' is given the type 'any'. */
-    // "useUnknownInCatchVariables": true,               /* Default catch clause variables as 'unknown' instead of 'any'. */
-    // "alwaysStrict": true,                             /* Ensure 'use strict' is always emitted. */
-    // "noUnusedLocals": true,                           /* Enable error reporting when local variables aren't read. */
-    // "noUnusedParameters": true,                       /* Raise an error when a function parameter isn't read. */
-    // "exactOptionalPropertyTypes": true,               /* Interpret optional property types as written, rather than adding 'undefined'. */
-    // "noImplicitReturns": true,                        /* Enable error reporting for codepaths that do not explicitly return in a function. */
-    // "noFallthroughCasesInSwitch": true,               /* Enable error reporting for fallthrough cases in switch statements. */
-    // "noUncheckedIndexedAccess": true,                 /* Add 'undefined' to a type when accessed using an index. */
-    // "noImplicitOverride": true,                       /* Ensure overriding members in derived classes are marked with an override modifier. */
-    // "noPropertyAccessFromIndexSignature": true,       /* Enforces using indexed accessors for keys declared using an indexed type. */
-    // "allowUnusedLabels": true,                        /* Disable error reporting for unused labels. */
-    // "allowUnreachableCode": true,                     /* Disable error reporting for unreachable code. */
-
-    /* Completeness */
-    // "skipDefaultLibCheck": true,                      /* Skip type checking .d.ts files that are included with TypeScript. */
-    "skipLibCheck": true                                 /* Skip type checking all .d.ts files. */
-  }
-}
\ No newline at end of file
diff --git a/packages/sdk/package.json b/packages/sdk/package.json
index 4303336..e77cbdc 100644
--- a/packages/sdk/package.json
+++ b/packages/sdk/package.json
@@ -18,9 +18,11 @@
     "README.md"
   ],
   "scripts": {
-    "build": "tsc --build",
+    "build": "tsc --build && tsc-alias",
     "clean": "rimraf lib",
-    "start": "tsc --build --watch",
-    "validate": "tsc --noEmit"
+    "start": "run-p dev:*",
+    "validate": "tsc --noEmit",
+    "dev:tsc": "tsc --build --watch",
+    "dev:tsc-alias": "tsc-alias --watch"
   }
 }
diff --git a/packages/sdk/src/conduit.ts b/packages/sdk/src/conduit.ts
new file mode 100644
index 0000000..ac735b2
--- /dev/null
+++ b/packages/sdk/src/conduit.ts
@@ -0,0 +1,218 @@
+// Iris
+// Copyright (c) jottocraft 2025
+
+export type IrisPluginConfigConduit = {
+    type: "conduit";
+    id: string;
+    baseUrl: string;
+};
+
+export type IrisPluginCtxConduit<PluginType extends IrisPluginConduit> = {
+    type: "conduit";
+    plugin: PluginType;
+    client: import("@jottocraft/yggdrasil").YggdrasilConduitApiClient<IrisPluginConduitConduitServerTypes<PluginType>>;
+};
+
+export type IrisPluginConduitConduitServerTypes<PluginType extends IrisPluginConduit> = {
+    client: {
+        plugin: () => Promise<PluginType>;
+        panels: {
+            [Property in keyof PluginType["panels"]]: (args: IrisPluginConduitPanelArgs<PluginType["panels"][Property]["requiredArgs"]>) => Promise<IrisPluginConduitPanelLayout<PluginType>>;
+        };
+        forms: {
+            [Property in keyof PluginType["forms"]]: (data: IrisPluginConduitFieldData<PluginType["forms"][Property]["data"]>) => Promise<string>;
+        };
+    };
+    apiCtx: {
+        jwt: string;
+    };
+    conduitCtx: {
+        jwt: string;
+    };
+};
+
+export type IrisPluginConduit = {
+    displayName: string;
+    icon: string;
+    panels: {
+        [id: string]: IrisPluginConduitPanel;
+    };
+    forms: {
+        [id: string]: IrisPluginConduitForm;
+    };
+    navigation: {
+        panel: IrisPluginConduitPanelRef<IrisPluginConduit>;
+        displayName: string;
+        icon?: string;
+    }[];
+};
+
+export type IrisPluginConduitPanel = {
+    requiredArgs: string[];
+};
+
+export type IrisPluginConduitPanelArgs<RequiredArgsType extends string[]> = {
+    [Property in RequiredArgsType[number]]: string;
+} & {
+    [arg: string]: string;
+};
+
+export type IrisPluginConduitForm = {
+    displayName: string;
+    icon?: string;
+    data?: IrisPluginConduitField;
+};
+
+export type IrisPluginConduitField = {
+    displayName?: string;
+    icon?: string;
+    helpText?: string;
+    optional?: boolean;
+} & ({
+    type: "string" | "number" | "date" | "datetime" | "boolean";
+} | {
+    type: "dropdown";
+    options: { label: string; icon?: string; value: string; }[];
+} | {
+    type: "array";
+    of: IrisPluginConduitField;
+} | {
+    type: "object";
+    struct: {
+        [key: string]: IrisPluginConduitField;
+    };
+});
+
+export type _IrisPluginConduitFieldDeepOptional<FieldSpec extends IrisPluginConduitField | undefined> = FieldSpec extends { type: "string" | "date" | "datetime" | "number" | "boolean" | "dropdown" } ? (
+    Omit<FieldSpec, "optional"> & { optional: true; }
+) : FieldSpec extends { type: "array" } ? (
+    Omit<FieldSpec, "of"> & { of: _IrisPluginConduitFieldDeepOptional<FieldSpec["of"]>; }
+) : FieldSpec extends { type: "object" } ? (
+    Omit<FieldSpec, "struct"> & {
+        struct: {
+            [Property in keyof FieldSpec["struct"]]: _IrisPluginConduitFieldDeepOptional<FieldSpec["struct"][Property]>;
+        };
+    }
+) : FieldSpec extends undefined ? (
+    undefined
+) : never;
+
+// https://github.com/microsoft/TypeScript/issues/12400#issuecomment-758523767
+
+export type _IrisUKeysOfType<T, SelectedType> = {
+    [key in keyof T]: SelectedType extends T[key] ? key : never
+}[keyof T];
+
+export type _IrisUOptional<T> = Partial<Pick<T, _IrisUKeysOfType<T, undefined>>>;
+
+export type _IrisURequired<T> = Omit<T, _IrisUKeysOfType<T, undefined>>;
+
+export type _IrisUOptionalUndefined<T> = _IrisUOptional<T> & _IrisURequired<T>;
+
+export type _IrisPluginConduitFieldDataCore<FieldSpec extends IrisPluginConduitField | undefined> = FieldSpec extends { type: "string" | "date" | "datetime" } ? (
+    string
+) : FieldSpec extends { type: "number" } ? (
+    number
+) : FieldSpec extends { type: "boolean" } ? (
+    boolean
+) : FieldSpec extends { type: "dropdown" } ? (
+    FieldSpec["options"][number]
+) : FieldSpec extends { type: "array" } ? (
+    IrisPluginConduitFieldData<FieldSpec["of"]>[]
+) : FieldSpec extends { type: "object" } ? (
+    _IrisUOptionalUndefined<{
+        [Property in keyof FieldSpec["struct"]]: IrisPluginConduitFieldData<FieldSpec["struct"][Property]>;
+    }>
+) : FieldSpec extends undefined ? (
+    undefined
+) : never;
+
+export type IrisPluginConduitFieldData<FieldSpec extends IrisPluginConduitField | undefined> = FieldSpec extends { optional: true; } ? (
+    _IrisPluginConduitFieldDataCore<FieldSpec> | undefined
+) : (
+        _IrisPluginConduitFieldDataCore<FieldSpec>
+    );
+
+export type IrisPluginConduitPanelLayout<PluginType extends IrisPluginConduit> = {
+    displayName: string;
+    icon?: string;
+    forms?: IrisPluginConduitFormRef<PluginType>[];
+    main?: IrisPluginConduitPanelLayoutField[];
+    sections?: IrisPluginConduitPanelLayoutSection<PluginType>[];
+};
+
+export type IrisPluginConduitPanelLayoutSection<PluginType extends IrisPluginConduit> = IrisPluginConduitPanelLayoutSectionProperties<PluginType> | IrisPluginConduitPanelLayoutSectionTable<PluginType> | IrisPluginConduitPanelLayoutSectionRef<PluginType>;
+
+export type IrisPluginConduitPanelLayoutSectionCommon = {
+    id: string;
+    displayName: string;
+    icon?: string;
+};
+
+export type IrisPluginConduitPanelLayoutSectionProperties<PluginType extends IrisPluginConduit> = IrisPluginConduitPanelLayoutSectionCommon & {
+    type: "properties";
+    properties: IrisPluginConduitPanelLayoutField[];
+};
+
+export type IrisPluginConduitPanelLayoutSectionTable<PluginType extends IrisPluginConduit> = IrisPluginConduitPanelLayoutSectionCommon & {
+    type: "table";
+    columns: {
+        key: string;
+        displayName: string;
+        icon?: string;
+    }[];
+    rows: {
+        vals: {
+            [key: string]: string;
+        };
+        links?: {
+            [key: string]: IrisPluginConduitPanelRef<PluginType>;
+        };
+    }[];
+    firstRow: number;
+    totalRows: number;
+};
+
+export type IrisPluginConduitPanelLayoutSectionRef<PluginType extends IrisPluginConduit> = IrisPluginConduitPanelLayoutSectionCommon & {
+    type: "ref";
+    panel: IrisPluginConduitPanelRef<PluginType>;
+};
+
+export type IrisPluginConduitPanelRef<PluginType extends IrisPluginConduit> = {
+    id: string;
+    args?: unknown;
+};
+
+export type IrisPluginConduitFormRef<PluginType extends IrisPluginConduit> = {
+    id: string;
+    data?: unknown;
+};
+
+export type IrisPluginConduitPanelLayoutField = {
+    displayName: string;
+    icon?: string;
+    value: string;
+};
+
+/**
+ * Helper utility for implementing an Iris Conduit server with strong type safety.
+ */
+export function createIrisConduitServerImpl<PluginType extends IrisPluginConduit>(): {
+    panelRef: <PanelId extends keyof PluginType["panels"]>(id: PanelId, args?: Partial<IrisPluginConduitPanelArgs<PluginType["panels"][PanelId]["requiredArgs"]>>) => IrisPluginConduitPanelRef<PluginType>;
+    formRef: <FormId extends keyof PluginType["forms"]>(id: FormId, args?: IrisPluginConduitFieldData<_IrisPluginConduitFieldDeepOptional<PluginType["forms"][FormId]["data"]>>) => IrisPluginConduitFormRef<PluginType>;
+} {
+    return {
+        panelRef(id, args) {
+            return {
+                id: id as string,
+                args
+            }
+        },
+        formRef(id, data) {
+            return {
+                id: id as string,
+                data
+            }
+        }
+    };
+}
\ No newline at end of file
diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts
index 7f535b5..1750cff 100644
--- a/packages/sdk/src/index.ts
+++ b/packages/sdk/src/index.ts
@@ -1,4 +1,5 @@
 // Iris
 // Copyright (c) jottocraft 2025
 
-export const DUMMY_EXPORT = 1;
\ No newline at end of file
+export * from "~/conduit";
+export * from "~/site";
\ No newline at end of file
diff --git a/packages/sdk/src/site.ts b/packages/sdk/src/site.ts
new file mode 100644
index 0000000..c545a3d
--- /dev/null
+++ b/packages/sdk/src/site.ts
@@ -0,0 +1,42 @@
+// Iris
+// Copyright (c) jottocraft 2025
+
+import { IrisPluginConduit, IrisPluginConfigConduit, IrisPluginCtxConduit } from "~/conduit";
+
+export type IrisSiteConfig = {
+    brand: {
+        name: string;
+        logo?: {
+            dark: string;
+            light: string;
+        };
+    };
+    auth: IrisAuthConfig;
+    plugins: IrisPluginConfig[];
+};
+
+// Auth:
+
+export type IrisAuthConfig = IrisAuthConfigOidc;
+
+export type IrisAuthConfigOidc = {
+    type: "oidc";
+    method: "implicit";
+    iss: string;
+    authUrl: string;
+    clientId: string;
+    scope?: string;
+};
+
+export type IrisAuthCtx = {
+    jwt: string;
+    claims: import("jose").JWTPayload;
+};
+
+// Plugins:
+
+export type IrisPluginConfig = IrisPluginConfigConduit;
+
+export type IrisPluginCtx = {
+    [id: string]: IrisPluginCtxConduit<IrisPluginConduit>;
+};
\ No newline at end of file
diff --git a/packages/server/.gitignore b/packages/server/.gitignore
deleted file mode 100644
index 1b0d95d..0000000
--- a/packages/server/.gitignore
+++ /dev/null
@@ -1,4 +0,0 @@
-node_modules
-build
-.wrangler
-*.tsbuildinfo
\ No newline at end of file
diff --git a/packages/server/package.json b/packages/server/package.json
deleted file mode 100644
index 522b845..0000000
--- a/packages/server/package.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
-  "name": "iris-server",
-  "private": true,
-  "license": "LGPL-2.1-only",
-  "author": "jottocraft",
-  "scripts": {
-    "build": "wrangler deploy --dry-run --outdir build",
-    "clean": "rimraf build",
-    "deploy": "wrangler deploy",
-    "start": "wrangler dev --local --ip 127.0.0.1 --port 2500 --inspector-port 2501",
-    "validate": "tsc --noEmit"
-  },
-  "dependencies": {
-    "@jottocraft/iris-sdk": "workspace:*",
-    "iris-plugins": "workspace:*"
-  }
-}
diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts
deleted file mode 100644
index c1698e8..0000000
--- a/packages/server/src/index.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-// Iris
-// Copyright (c) jottocraft 2025
-
-import { YggdrasilRouter } from "@jottocraft/yggdrasil";
-
-import { IrisServer } from "~/server";
-
-export default {
-    async fetch(request: Request, env: IrisServerEnv, ctx: ExecutionContext): Promise<Response> {
-        return IrisServer.handleRequest(request, async (ygg) => {
-            return new Response("Hello world!", { status: 200 });
-        }, {
-            ctx: {
-                env,
-                exec: ctx
-            }
-        }, {
-            // TODO: Put these in environment variables
-            baseURL: "https://iris.hq.jottocraft.com/api",
-            debug: true,
-            generateHtmlStatusPage: async (ygg, e) => {
-                return new Response([
-                    "Error " + e.status,
-                    e.statusCode,
-                    e.statusMessage,
-                    "",
-                    "",
-                    "",
-                    "",
-                    "Iris",
-                    "Copyright (c) jottocraft 2025"
-                ].join("\n"), {
-                    status: 200
-                });
-            }
-        });
-    }
-}
\ No newline at end of file
diff --git a/packages/server/src/iris-server.d.ts b/packages/server/src/iris-server.d.ts
deleted file mode 100644
index 2e859ba..0000000
--- a/packages/server/src/iris-server.d.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-// Iris
-// Copyright (c) jottocraft 2025
-
-declare type IrisServerEnv = {
-
-};
-
-declare type IrisServerContext = {
-    env: IrisServerEnv;
-    exec: ExecutionContext;
-};
-
-declare type IrisServerAppTypes = {
-    ctx: IrisServerContext;
-};
\ No newline at end of file
diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts
deleted file mode 100644
index f52d620..0000000
--- a/packages/server/src/server.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-// Iris
-// Copyright (c) jottocraft 2025
-
-import { YggdrasilServer } from "@jottocraft/yggdrasil";
-
-export const IrisServer = new YggdrasilServer<IrisServerAppTypes>();
\ No newline at end of file
diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json
deleted file mode 100644
index 141ac4e..0000000
--- a/packages/server/tsconfig.json
+++ /dev/null
@@ -1,107 +0,0 @@
-{
-  "include": [
-    "src"
-  ],
-  "compilerOptions": {
-    /* Visit https://aka.ms/tsconfig to read more about this file */
-    /* Projects */
-    // "incremental": true,                              /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
-    // "composite": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */
-    // "tsBuildInfoFile": "./.tsbuildinfo",              /* Specify the path to .tsbuildinfo incremental compilation file. */
-    // "disableSourceOfProjectReferenceRedirect": true,  /* Disable preferring source files instead of declaration files when referencing composite projects. */
-    // "disableSolutionSearching": true,                 /* Opt a project out of multi-project reference checking when editing. */
-    // "disableReferencedProjectLoad": true,             /* Reduce the number of projects loaded automatically by TypeScript. */
-    /* Language and Environment */
-    "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
-    "lib": [
-      "ESNext"
-    ], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
-    // "jsx": "react-jsx",                                  /* Specify what JSX code is generated. */
-    // "experimentalDecorators": true,                   /* Enable experimental support for TC39 stage 2 draft decorators. */
-    // "emitDecoratorMetadata": true,                    /* Emit design-type metadata for decorated declarations in source files. */
-    // "jsxFactory": "",                                 /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
-    // "jsxFragmentFactory": "",                         /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
-    // "jsxImportSource": "",                            /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
-    // "reactNamespace": "",                             /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
-    // "noLib": true,                                    /* Disable including any library files, including the default lib.d.ts. */
-    // "useDefineForClassFields": true,                  /* Emit ECMAScript-standard-compliant class fields. */
-    // "moduleDetection": "auto",                        /* Control what method is used to detect module-format JS files. */
-    /* Modules */
-    // "module": "commonjs",                             /* Specify what module code is generated. */
-    "rootDir": "src",
-    "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
-    // "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */
-    "paths": {
-      "~/*": [
-        "./src/*"
-      ]
-    },
-    // "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */
-    // "typeRoots": [],                                  /* Specify multiple folders that act like './node_modules/@types'. */
-    "types": [
-      "node",
-      "@cloudflare/workers-types"
-    ], /* Specify type package names to be included without being referenced in a source file. */
-    // "allowUmdGlobalAccess": true,                     /* Allow accessing UMD globals from modules. */
-    // "moduleSuffixes": [],                             /* List of file name suffixes to search when resolving a module. */
-    "resolveJsonModule": true, /* Enable importing .json files. */
-    // "noResolve": true,                                /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
-    /* JavaScript Support */
-    "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
-    // "checkJs": true,                                  /* Enable error reporting in type-checked JavaScript files. */
-    // "maxNodeModuleJsDepth": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
-    /* Emit */
-    "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
-    "declarationMap": true, /* Create sourcemaps for d.ts files. */
-    // "emitDeclarationOnly": true,                      /* Only output d.ts files and not JavaScript files. */
-    "sourceMap": true, /* Create source map files for emitted JavaScript files. */
-    // "outFile": "./",                                  /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
-    // "outDir": "./",                                   /* Specify an output folder for all emitted files. */
-    // "removeComments": true,                           /* Disable emitting comments. */
-    "noEmit": true, /* Disable emitting files from a compilation. */
-    // "importHelpers": true,                            /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
-    // "importsNotUsedAsValues": "remove",               /* Specify emit/checking behavior for imports that are only used for types. */
-    // "downlevelIteration": true,                       /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
-    // "sourceRoot": "",                                 /* Specify the root path for debuggers to find the reference source code. */
-    // "mapRoot": "",                                    /* Specify the location where debugger should locate map files instead of generated locations. */
-    // "inlineSourceMap": true,                          /* Include sourcemap files inside the emitted JavaScript. */
-    // "inlineSources": true,                            /* Include source code in the sourcemaps inside the emitted JavaScript. */
-    // "emitBOM": true,                                  /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
-    // "newLine": "crlf",                                /* Set the newline character for emitting files. */
-    // "stripInternal": true,                            /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
-    // "noEmitHelpers": true,                            /* Disable generating custom helper functions like '__extends' in compiled output. */
-    // "noEmitOnError": true,                            /* Disable emitting files if any type checking errors are reported. */
-    // "preserveConstEnums": true,                       /* Disable erasing 'const enum' declarations in generated code. */
-    // "declarationDir": "./",                           /* Specify the output directory for generated declaration files. */
-    // "preserveValueImports": true,                     /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
-    /* Interop Constraints */
-    // "isolatedModules": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */
-    // "allowSyntheticDefaultImports": true,             /* Allow 'import x from y' when a module doesn't have a default export. */
-    "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
-    // "preserveSymlinks": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
-    "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
-    /* Type Checking */
-    "strict": true, /* Enable all strict type-checking options. */
-    // "noImplicitAny": true,                            /* Enable error reporting for expressions and declarations with an implied 'any' type. */
-    // "strictNullChecks": true,                         /* When type checking, take into account 'null' and 'undefined'. */
-    // "strictFunctionTypes": true,                      /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
-    // "strictBindCallApply": true,                      /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
-    // "strictPropertyInitialization": true,             /* Check for class properties that are declared but not set in the constructor. */
-    // "noImplicitThis": true,                           /* Enable error reporting when 'this' is given the type 'any'. */
-    // "useUnknownInCatchVariables": true,               /* Default catch clause variables as 'unknown' instead of 'any'. */
-    // "alwaysStrict": true,                             /* Ensure 'use strict' is always emitted. */
-    "noUnusedLocals": false, /* Enable error reporting when local variables aren't read. */
-    "noUnusedParameters": false, /* Raise an error when a function parameter isn't read. */
-    // "exactOptionalPropertyTypes": true,               /* Interpret optional property types as written, rather than adding 'undefined'. */
-    // "noImplicitReturns": true,                        /* Enable error reporting for codepaths that do not explicitly return in a function. */
-    // "noFallthroughCasesInSwitch": true,               /* Enable error reporting for fallthrough cases in switch statements. */
-    // "noUncheckedIndexedAccess": true,                 /* Add 'undefined' to a type when accessed using an index. */
-    // "noImplicitOverride": true,                       /* Ensure overriding members in derived classes are marked with an override modifier. */
-    // "noPropertyAccessFromIndexSignature": true,       /* Enforces using indexed accessors for keys declared using an indexed type. */
-    // "allowUnusedLabels": true,                        /* Disable error reporting for unused labels. */
-    // "allowUnreachableCode": true,                     /* Disable error reporting for unreachable code. */
-    /* Completeness */
-    "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
-    "skipLibCheck": true /* Skip type checking all .d.ts files. */
-  }
-}
\ No newline at end of file
diff --git a/packages/server/wrangler.toml b/packages/server/wrangler.toml
deleted file mode 100644
index 32dbdf4..0000000
--- a/packages/server/wrangler.toml
+++ /dev/null
@@ -1,7 +0,0 @@
-name = "iris-server"
-main = "src/index.ts"
-compatibility_date = "2025-07-12"
-compatibility_flags = [ "nodejs_compat" ]
-
-account_id = "17174d3fc7098a664b04fb3c4c4ec65c"
-workers_dev = false
\ No newline at end of file
diff --git a/packages/webui/.gitignore b/packages/webui/.gitignore
index 36cd0db..7bd7f37 100644
--- a/packages/webui/.gitignore
+++ b/packages/webui/.gitignore
@@ -11,3 +11,4 @@
 .wrangler
 .dev.vars*
 
+src/IRIS_SITE.ts
\ No newline at end of file
diff --git a/packages/webui/package.json b/packages/webui/package.json
index b7f06b6..74f6b9e 100644
--- a/packages/webui/package.json
+++ b/packages/webui/package.json
@@ -13,7 +13,6 @@
     "validate": "tsc --noEmit"
   },
   "dependencies": {
-    "@jottocraft/iris-sdk": "workspace:*",
-    "iris-plugins": "workspace:*"
+    "@jottocraft/iris-sdk": "workspace:*"
   }
 }
diff --git a/packages/webui/src/app/components/Form/Field.tsx b/packages/webui/src/app/components/Form/Field.tsx
new file mode 100644
index 0000000..122654c
--- /dev/null
+++ b/packages/webui/src/app/components/Form/Field.tsx
@@ -0,0 +1,162 @@
+// Iris
+// Copyright (c) jottocraft 2025
+
+import { Button, Dropdown, DropdownItem, Icon, Switch, TextInput } from "@ffxp/react-dom";
+import type { IrisPluginConduitField } from "@jottocraft/iris-sdk";
+
+export function createDefaultFieldData(spec: IrisPluginConduitField, refData?: any, forceRequired?: boolean) {
+    if (spec.optional && !forceRequired) return undefined;
+
+    if (spec.type === "string") {
+        return refData ?? "";
+    } else if (spec.type === "number") {
+        return refData ?? 0;
+    } else if (spec.type === "date") {
+        return refData ?? new Date().toISOString().split("T")[0];
+    } else if (spec.type === "datetime") {
+        return refData ?? new Date().toISOString();
+    } else if (spec.type === "boolean") {
+        return refData ?? false;
+    } else if (spec.type === "dropdown") {
+        return refData ?? spec.options[0].value;
+    } else if (spec.type === "array") {
+        return refData ? refData.map((refItem: any) => createDefaultFieldData(spec.of, refItem)) : [];
+    } else if (spec.type === "object") {
+        const defaultData: any = {};
+        for (const key in spec.struct) {
+            defaultData[key] = createDefaultFieldData(spec.struct[key], refData ? refData[key] : undefined);
+        }
+        return defaultData;
+    }
+}
+
+export default function Field({ path, data, refData, spec, patchFormData, controls }: { path: string; data: any; refData: any; spec: IrisPluginConduitField; patchFormData: (path: string, value: unknown) => void; controls?: React.ReactNode; }) {
+    let fieldInput: React.ReactNode = null;
+
+    if ((refData !== undefined) && (spec.type !== "object")) return null;
+
+    if (data === undefined) {
+        fieldInput = (
+            <Button onClick={() => patchFormData(path, createDefaultFieldData(spec, undefined, true))} iconLeading="add">Set value</Button>
+        );
+    } else if ((spec.type === "string") || (spec.type === "number") || (spec.type === "date") || (spec.type === "datetime")) {
+        fieldInput = (
+            <TextInput
+                onChange={e => {
+                    if (spec.type === "string") {
+                        patchFormData(path, e.target.value);
+                    } else if (spec.type === "number") {
+                        patchFormData(path, Number(e.target.value));
+                    } else if (spec.type === "date") {
+                        patchFormData(path, e.target.value);
+                    } else if (spec.type === "datetime") {
+                        patchFormData(path, e.target.value + "Z");
+                    }
+                }}
+                value={spec.type === "datetime" ? new Date(data).toISOString().split(":").slice(0, -1).join(":") : data}
+                type={spec.type === "string" ? (
+                    "text"
+                ) : spec.type === "number" ? (
+                    "number"
+                ) : spec.type === "date" ? (
+                    "date"
+                ) : spec.type === "datetime" ? (
+                    "datetime-local"
+                ) : undefined}
+            />
+        );
+    } else if (spec.type === "boolean") {
+        fieldInput = (
+            <Switch onClick={() => patchFormData(path, !data)} active={data} />
+        );
+    } else if (spec.type === "dropdown") {
+        const selectedOption = spec.options.find(option => option.value === data)!;
+        fieldInput = (
+            <Dropdown menu={(
+                <>
+                    {spec.options.map(option => (
+                        <DropdownItem onClick={() => patchFormData(path, option.value)} icon={option.icon}>{option.label}</DropdownItem>
+                    ))}
+                </>
+            )}>
+                <Button variant="outline" iconLeading={selectedOption.icon} iconTrailing="arrow_drop_down">{selectedOption.label}</Button>
+            </Dropdown>
+        );
+    } else if (spec.type === "array") {
+        fieldInput = (
+            <div className="space-y-6 border border-ui-20 p-4 roundness-xl">
+                {data.map((value: any, i: number) => {
+                    return (
+                        <Field controls={(
+                            <div className="space-x-2">
+                                <Button onClick={() => {
+                                    if (i === 0) return;
+                                    const newValue = [...data];
+                                    newValue.splice(i, 1);
+                                    newValue.splice(i - 1, 0, value);
+                                    patchFormData(path, newValue);
+                                }} iconLeading="arrow_upward" size="small" />
+                                <Button onClick={() => {
+                                    if (i === (data.length - 1)) return;
+                                    const newValue = [...data];
+                                    newValue.splice(i, 1);
+                                    newValue.splice(i + 1, 0, value);
+                                    patchFormData(path, newValue);
+                                }} iconLeading="arrow_downward" size="small" />
+                                <Button onClick={() => {
+                                    const newValue = [...data];
+                                    newValue.splice(i, 1);
+                                    patchFormData(path, newValue);
+                                }} iconLeading="delete" size="small" />
+                            </div>
+                        )} path={`${path}[${i}]`} data={value} refData={refData !== undefined ? refData[i] : undefined} spec={spec.of} patchFormData={patchFormData} />
+                    );
+                })}
+                <Button iconLeading="add" onClick={() => patchFormData(path, [...data, createDefaultFieldData(spec.of)])}>Add item</Button>
+            </div>
+        );
+    } else if (spec.type === "object") {
+        fieldInput = (
+            <div className="space-y-6 border border-ui-20 p-4 roundness-xl">
+                {Object.keys(spec.struct).map(key => {
+                    return (
+                        <Field path={`${path}.${key}`} data={data[key]} refData={refData !== undefined ? refData[key] : undefined} spec={spec.struct[key]} patchFormData={patchFormData} />
+                    );
+                })}
+            </div>
+        );
+    } else {
+        fieldInput = (
+            <div>
+                <p>Unrecognized field type {spec.type}</p>
+            </div>
+        );
+    }
+
+    return (
+        <div className="space-y-2">
+            {spec.icon || spec.displayName || spec.optional || controls ? (
+                <div className="flex flex-row items-center justify-between">
+                    <div className="flex flex-row items-center space-x-2">
+                        {spec.icon ? <Icon className="text-lg">{spec.icon}</Icon> : null}
+                        {spec.displayName ? <div>{spec.displayName}</div> : null}
+                    </div>
+                    <div className="flex flex-row items-center space-x-8">
+                        {spec.optional && (data !== undefined) ? (
+                            <Button onClick={() => patchFormData(path, undefined)} iconLeading="delete" size="small">Clear value</Button>
+                        ) : spec.optional ? (
+                            <div className="text-xs px-2 py-1 bg-ui-10 roundness">
+                                Optional
+                            </div>
+                        ) : null}
+                        {controls}
+                    </div>
+                </div>
+            ) : null}
+            {fieldInput}
+            {spec.helpText ? (
+                <div className="text-type-30 text-xs">{spec.helpText}</div>
+            ) : null}
+        </div>
+    );
+}
\ No newline at end of file
diff --git a/packages/webui/src/app/components/Form/index.tsx b/packages/webui/src/app/components/Form/index.tsx
new file mode 100644
index 0000000..b323de0
--- /dev/null
+++ b/packages/webui/src/app/components/Form/index.tsx
@@ -0,0 +1,81 @@
+// Iris
+// Copyright (c) jottocraft 2025
+
+import type { IrisPluginConduitField } from "@jottocraft/iris-sdk";
+import { Button, Icon } from "@ffxp/react-dom";
+import { useCallback, useMemo, useState } from "react";
+import { createPortal } from "react-dom";
+import set from "lodash/set";
+import clone from "lodash/clone";
+
+import Field, { createDefaultFieldData } from "~/app/components/Form/Field";
+import { usePanelNavigation } from "~/app/data/panels";
+import { usePlugins } from "~/app/data/root";
+import { YggdrasilStatus } from "@jottocraft/yggdrasil";
+
+export default function Form({ ref, onFinished }: { ref: IrisWebuiFormRef; onFinished: () => void; }) {
+    const plugins = usePlugins()!;
+    const panelNavigation = usePanelNavigation() as IrisWebuiPanelNavigationCtxActive;
+    const form = plugins[panelNavigation.pluginId].plugin.forms[ref.id];
+
+    const defaultData = useMemo(() => form.data ? createDefaultFieldData(form.data, ref.data) : undefined, [form, ref]);
+
+    const [formData, setFormData] = useState<{ data: unknown; }>({
+        data: clone(defaultData)
+    });
+
+    const patchFormData = useCallback((path: string, value: unknown) => {
+        set(formData, path, value);
+
+        setFormData({
+            data: formData.data
+        });
+    }, [formData]);
+
+    const [formResult, setFormResult] = useState<YggdrasilStatus<unknown> | string | null>(null);
+    const submitForm = useCallback(() => {
+        (async () => {
+            try {
+                const data = await plugins[panelNavigation.pluginId].client().forms[ref.id](formData.data as any);
+                setFormResult(data);
+            } catch(e) {
+                if (!(e instanceof YggdrasilStatus)) throw e;
+                setFormResult(e);
+            }
+        })();
+    }, [panelNavigation, ref, formData]);
+
+    return createPortal((
+        <div className="w-full h-full py-10 absolute top-0 left-0 z-99 bg-black/90 flex items-center justify-center">
+            <div className="max-h-full overflow-auto w-2xl bg-ui-0 p-4 roundness-xl shadow-xl space-y-8">
+                <div className="flex flex-row items-center justify-between">
+                    <div className="flex flex-row items-center text-xl space-x-2 font-medium">
+                        {form.icon ? <Icon className="text-2xl">{form.icon}</Icon> : null}
+                        <h1>{form.displayName}</h1>
+                    </div>
+                    <div>
+                        <Button variant="borderless" onClick={() => onFinished()} iconLeading="close" size="small" />
+                    </div>
+                </div>
+                {formResult ? (
+                    <div>
+                        <p>{formResult instanceof YggdrasilStatus ? formResult.statusCode : formResult}</p>
+                    </div>
+                ) : (
+                    <>
+                        <div>
+                            {form.data ? (
+                                <Field path="data" data={formData.data} refData={ref.data} spec={form.data} patchFormData={patchFormData} />
+                            ) : (
+                                <p>This form doesn't take any input.</p>
+                            )}
+                        </div>
+                        <div>
+                            <Button onClick={submitForm} variant="primary">Submit</Button>
+                        </div>
+                    </>
+                )}
+            </div >
+        </div >
+    ), document.body);
+}
\ No newline at end of file
diff --git a/packages/webui/src/app/components/Panel/index.tsx b/packages/webui/src/app/components/Panel/index.tsx
new file mode 100644
index 0000000..bafc717
--- /dev/null
+++ b/packages/webui/src/app/components/Panel/index.tsx
@@ -0,0 +1,82 @@
+// Iris
+// Copyright (c) jottocraft 2025
+
+import { Button, Icon } from "@ffxp/react-dom";
+import type { IrisPluginConduit, IrisPluginConduitPanelArgs, IrisPluginConduitPanelLayout } from "@jottocraft/iris-sdk";
+import { useEffect, useState } from "react";
+import { useSearchParams } from "react-router";
+
+import { usePlugins } from "~/app/data/root";
+import PanelSection from "~/app/components/PanelSection";
+import { usePanelNavigation } from "~/app/data/panels";
+import Form from "~/app/components/Form";
+
+export default function Panel() {
+    const panelNavigation = usePanelNavigation()!;
+    const plugins = usePlugins()!;
+    const [activeForm, setActiveForm] = useState<IrisWebuiFormRef | null>(null);
+
+    if (!panelNavigation.activeRef) {
+        return (
+            <p>Please select a panel to open from the sidebar.</p>
+        );
+    }
+
+    if (!panelNavigation.layout) {
+        return (
+            <p>Loading panel {panelNavigation.activeRef.id}...</p>
+        );
+    }
+
+    const plugin = plugins[panelNavigation.pluginId];
+    const layout = panelNavigation.layout;
+
+    return (
+        <>
+            {activeForm ? (
+                <Form ref={activeForm} onFinished={() => setActiveForm(null)} />
+            ) : null}
+            <div className="space-y-8">
+                <div className="flex flex-row items-center space-x-3 text-4xl font-semibold">
+                    {layout.icon ? <Icon>{layout.icon}</Icon> : null}
+                    <h1>{layout.displayName}</h1>
+                </div>
+                {layout.forms ? (
+                    <div>
+                        {layout.forms?.map((ref) => {
+                            const form = plugin.plugin.forms[ref.id];
+
+                            return (
+                                <div className="inline-block m-1">
+                                    <Button onClick={() => setActiveForm(ref)} iconLeading={form.icon}>{form.displayName}</Button>
+                                </div>
+                            );
+                        })}
+                    </div>
+                ) : null}
+                {layout.main ? (
+                    <div>
+                        {layout.main.map(item => (
+                            <div className="inline-block space-y-2 m-1 p-4 bg-ui-5 roundness-xl">
+                                <div className="flex flex-row items-center space-x-2 text-xl font-medium">
+                                    {item.icon ? <Icon>{item.icon}</Icon> : null}
+                                    <div>{item.displayName}</div>
+                                </div>
+                                <div>
+                                    <div>{item.value}</div>
+                                </div>
+                            </div>
+                        ))}
+                    </div>
+                ) : null}
+                {layout.sections ? (
+                    <div>
+                        {layout.sections.map(section => (
+                            <PanelSection section={section} />
+                        ))}
+                    </div>
+                ) : null}
+            </div>
+        </>
+    );
+}
\ No newline at end of file
diff --git a/packages/webui/src/app/components/PanelBreadcrumbs.tsx b/packages/webui/src/app/components/PanelBreadcrumbs.tsx
new file mode 100644
index 0000000..1e824a2
--- /dev/null
+++ b/packages/webui/src/app/components/PanelBreadcrumbs.tsx
@@ -0,0 +1,38 @@
+// Iris
+// Copyright (c) jottocraft 2025
+
+import { Button, Icon, Link } from "@ffxp/react-dom";
+import { Link as RouterLink } from "react-router";
+import { panelRefHref, usePanelNavigation } from "~/app/data/panels";
+
+export default function PanelBreadcrumbs() {
+    const panelNavigation = usePanelNavigation()!;
+
+    if (!panelNavigation.activeRef) return;
+
+    return (
+        <div className="flex-grow flex flex-row items-center border-ui-20 border-2 roundness-lg p-1">
+            <div className="mr-4 space-x-1">
+                <Button onClick={() => panelNavigation.back()} iconLeading="arrow_back" size="small" />
+                <Button onClick={() => panelNavigation.refreshLayout()} iconLeading="refresh" size="small" />
+            </div>
+            {panelNavigation.breadcrumbs.map((breadcrumb, i) => (
+                <div className="flex flex-row items-center space-x-1 text-sm text-type-20">
+                    {breadcrumb.icon ? <Icon className="text-base">{breadcrumb.icon}</Icon> : null}
+                    <div>
+                        <RouterLink to={panelRefHref(panelNavigation.pluginId, breadcrumb.ref)} onClick={(e) => panelNavigation.goToBreadcrumb(i, e)}>
+                            <Link>{breadcrumb.displayName}</Link>
+                        </RouterLink>
+                    </div>
+                    <Icon className="text-xl mx-1">keyboard_arrow_right</Icon>
+                </div>
+            ))}
+            {panelNavigation.layout ? (
+                <div className="flex flex-row items-center space-x-1 text-base text-type-0 mr-2">
+                    {panelNavigation.layout.icon ? <Icon className="text-lg">{panelNavigation.layout.icon}</Icon> : null}
+                    <div className="font-medium">{panelNavigation.layout.displayName}</div>
+                </div>
+            ) : null}
+        </div>
+    );
+}
\ No newline at end of file
diff --git a/packages/webui/src/app/components/PanelNavigator.tsx b/packages/webui/src/app/components/PanelNavigator.tsx
new file mode 100644
index 0000000..16bcb25
--- /dev/null
+++ b/packages/webui/src/app/components/PanelNavigator.tsx
@@ -0,0 +1,94 @@
+// Iris
+// Copyright (c) jottocraft 2025
+
+import type { IrisPluginConduit, IrisPluginConduitPanelLayout } from "@jottocraft/iris-sdk";
+import { useCallback, useEffect, useState } from "react";
+import { useNavigate } from "react-router";
+
+import { panelNavigationCtx, panelRefHref } from "~/app/data/panels";
+import { usePlugins } from "~/app/data/root";
+
+export default function PanelNavigator({ children }: { children?: React.ReactNode; }) {
+    const routerNavigate = useNavigate();
+    const plugins = usePlugins()!;
+
+    const [pluginId, setPluginId] = useState<string | null>(null);
+    const [activeRef, setActiveRef] = useState<IrisWebuiPanelRef | null>(null);
+    const [layout, setLayout] = useState<IrisPluginConduitPanelLayout<IrisPluginConduit> | null>(null);
+    const [breadcrumbs, setBreadcrumbs] = useState<IrisWebuiPanelBreadcrumb[]>([]);
+
+    useEffect(() => {
+        if (!activeRef) return;
+        routerNavigate(panelRefHref(pluginId!, activeRef));
+    }, [pluginId, activeRef, routerNavigate]);
+
+    const fetchLayout = useCallback(() => {
+        (async () => {
+            const data = await plugins[pluginId!].client().panels[activeRef!.id]((activeRef!.args ?? {}) as { [key: string]: string; });
+            setLayout(data);
+        })();
+    }, [plugins, pluginId, activeRef]);
+
+    useEffect(() => {
+        if (!activeRef) return;
+
+        fetchLayout();
+    }, [activeRef, fetchLayout]);
+
+    const navigate = useCallback((ref: IrisWebuiPanelRef, e?: React.MouseEvent) => {
+        e?.preventDefault();
+
+        setBreadcrumbs([...breadcrumbs, {
+            displayName: layout?.displayName ?? activeRef!.id,
+            icon: layout?.icon ?? undefined,
+            ref: activeRef!
+        }]);
+        setActiveRef(ref);
+    }, [activeRef, layout, breadcrumbs]);
+
+    const back = useCallback((e?: React.MouseEvent) => {
+        e?.preventDefault();
+        if (!breadcrumbs.length) return;
+
+        setActiveRef(breadcrumbs[breadcrumbs.length - 1].ref);
+        setBreadcrumbs(breadcrumbs.slice(0, -1));
+    }, [breadcrumbs]);
+
+    const goToBreadcrumb = useCallback((index: number, e?: React.MouseEvent) => {
+        e?.preventDefault();
+        if (!breadcrumbs.length) return;
+
+        setActiveRef(breadcrumbs[index].ref);
+        setBreadcrumbs(breadcrumbs.slice(0, index));
+    }, [breadcrumbs]);
+
+    const startNavigation = useCallback((pluginId: string, ref: IrisWebuiPanelRef, e?: React.MouseEvent) => {
+        setPluginId(pluginId);
+        setActiveRef(ref);
+        setBreadcrumbs([]);
+    }, []);
+
+    const refreshLayout = useCallback(() => {
+        setLayout(null);
+        fetchLayout();
+    }, [fetchLayout]);
+
+    return (
+        <panelNavigationCtx.Provider value={activeRef ? {
+            activeRef,
+            layout,
+            pluginId: pluginId!,
+            breadcrumbs,
+            navigate,
+            back,
+            goToBreadcrumb,
+            startNavigation,
+            refreshLayout
+        } : {
+            activeRef,
+            startNavigation
+        }}>
+            {children}
+        </panelNavigationCtx.Provider>
+    );
+}
\ No newline at end of file
diff --git a/packages/webui/src/app/components/PanelSection/PanelSectionProperties.tsx b/packages/webui/src/app/components/PanelSection/PanelSectionProperties.tsx
new file mode 100644
index 0000000..1668d1c
--- /dev/null
+++ b/packages/webui/src/app/components/PanelSection/PanelSectionProperties.tsx
@@ -0,0 +1,10 @@
+// Iris
+// Copyright (c) jottocraft 2025
+
+import type { IrisPluginConduit, IrisPluginConduitPanelLayoutSectionProperties } from "@jottocraft/iris-sdk";
+
+export default function PanelSectionProperties({ section }: { section: IrisPluginConduitPanelLayoutSectionProperties<IrisPluginConduit>; }) {
+    return (
+        <p>TODO</p>
+    );
+}
\ No newline at end of file
diff --git a/packages/webui/src/app/components/PanelSection/PanelSectionRef.tsx b/packages/webui/src/app/components/PanelSection/PanelSectionRef.tsx
new file mode 100644
index 0000000..c889178
--- /dev/null
+++ b/packages/webui/src/app/components/PanelSection/PanelSectionRef.tsx
@@ -0,0 +1,10 @@
+// Iris
+// Copyright (c) jottocraft 2025
+
+import type { IrisPluginConduit, IrisPluginConduitPanelLayoutSectionRef } from "@jottocraft/iris-sdk";
+
+export default function PanelSectionRef({ section }: { section: IrisPluginConduitPanelLayoutSectionRef<IrisPluginConduit>; }) {
+    return (
+        <p>TODO</p>
+    );
+}
\ No newline at end of file
diff --git a/packages/webui/src/app/components/PanelSection/PanelSectionTable.tsx b/packages/webui/src/app/components/PanelSection/PanelSectionTable.tsx
new file mode 100644
index 0000000..26229e4
--- /dev/null
+++ b/packages/webui/src/app/components/PanelSection/PanelSectionTable.tsx
@@ -0,0 +1,46 @@
+// Iris
+// Copyright (c) jottocraft 2025
+
+import type { IrisPluginConduit, IrisPluginConduitPanelLayoutSectionTable } from "@jottocraft/iris-sdk";
+import { Link, useParams } from "react-router";
+
+import { panelRefHref, usePanelNavigation } from "~/app/data/panels";
+
+export default function PanelSectionTable({ section }: { section: IrisPluginConduitPanelLayoutSectionTable<IrisPluginConduit>; }) {
+    const params = useParams();
+    const panelNavigation = usePanelNavigation() as IrisWebuiPanelNavigationCtxActive;
+
+    return (
+        <div>
+            <table>
+                <thead>
+                    <tr>
+                        {section.columns.map(column => (
+                            <th>
+                                {column.displayName}
+                            </th>
+                        ))}
+                    </tr>
+                </thead>
+                <tbody>
+                    {section.rows.map(row => (
+                        <tr>
+                            {section.columns.map(column => {
+                                const link = row.links?.[column.key];
+                                return (
+                                    <td>
+                                        {link ? (
+                                            <Link to={panelRefHref(panelNavigation.pluginId, link)} onClick={(e) => panelNavigation.navigate(link, e)}>{row.vals[column.key] ?? null}</Link>
+                                        ) : (
+                                            row.vals[column.key] ?? null
+                                        )}
+                                    </td>
+                                );
+                            })}
+                        </tr>
+                    ))}
+                </tbody>
+            </table>
+        </div>
+    );
+}
\ No newline at end of file
diff --git a/packages/webui/src/app/components/PanelSection/index.tsx b/packages/webui/src/app/components/PanelSection/index.tsx
new file mode 100644
index 0000000..af483f0
--- /dev/null
+++ b/packages/webui/src/app/components/PanelSection/index.tsx
@@ -0,0 +1,26 @@
+// Iris
+// Copyright (c) jottocraft 2025
+
+import type { IrisPluginConduit, IrisPluginConduitPanelLayoutSection } from "@jottocraft/iris-sdk";
+
+import PanelSectionProperties from "~/app/components/PanelSection/PanelSectionProperties";
+import PanelSectionTable from "~/app/components/PanelSection/PanelSectionTable";
+import PanelSectionRef from "~/app/components/PanelSection/PanelSectionRef";
+
+export default function PanelSection({ section }: { section: IrisPluginConduitPanelLayoutSection<IrisPluginConduit>; }) {
+    if (section.type === "properties") {
+        return (
+            <PanelSectionProperties section={section} />
+        );
+    } else if (section.type === "table") {
+        return (
+            <PanelSectionTable section={section} />
+        );
+    } else if (section.type === "ref") {
+        return (
+            <PanelSectionRef section={section} />
+        );
+    } else {
+        throw new Error("Unrecognized panel layout section type " + (section as any).type);
+    }
+}
\ No newline at end of file
diff --git a/packages/webui/src/app/components/Shell.tsx b/packages/webui/src/app/components/Shell.tsx
new file mode 100644
index 0000000..62267db
--- /dev/null
+++ b/packages/webui/src/app/components/Shell.tsx
@@ -0,0 +1,111 @@
+// Iris
+// Copyright (c) jottocraft 2025
+
+import { Button, NavbarLayout, NavbarTitle, SidebarHeader, SidebarItem, SidebarItemGroup, SidebarItems, SidebarLayout, SidebarTitle, useThemeEngine } from "@ffxp/react-dom";
+import { useEffect } from "react";
+import { Link, Outlet, useLocation, useParams, useSearchParams } from "react-router";
+
+import IRIS_SITE from "~/IRIS_SITE";
+import { useAuth, usePlugins } from "~/app/data/root";
+import { isAtPanel, panelRefHref, usePanelNavigation } from "~/app/data/panels";
+import PanelNavigator from "~/app/components/PanelNavigator";
+import PanelBreadcrumbs from "~/app/components/PanelBreadcrumbs";
+
+function ShellLayout({ children }: { children?: React.ReactNode; }) {
+    const theme = useThemeEngine();
+    const plugins = usePlugins()!;
+    const auth = useAuth();
+    const panelNavigation = usePanelNavigation()!;
+    const routerParams = useParams();
+    const [searchParams] = useSearchParams();
+
+    useEffect(() => {
+        if (!panelNavigation.activeRef && (routerParams.pluginId && routerParams.panelId)) {
+            const args: { [key: string]: string } = {};
+            for (const [k, v] of searchParams.entries()) args[k] = v;
+
+            panelNavigation.startNavigation(routerParams.pluginId, {
+                id: routerParams.panelId,
+                args
+            });
+        }
+    }, [routerParams, searchParams, panelNavigation]);
+
+    return (
+        <NavbarLayout left={(
+            <div className="flex-grow flex flex-row items-center space-x-8">
+                <Link to="/">
+                    <NavbarTitle logo="/webui/assets/favicon.png">
+                        Iris
+                    </NavbarTitle>
+                </Link>
+                <PanelBreadcrumbs />
+            </div>
+        )}
+            right={(
+                <>
+                    <Button onClick={() => {
+                        if (window.confirm("Do you want to sign out?")) {
+                            window.localStorage.removeItem("jwt");
+                            window.location.reload();
+                        }
+                    }}>
+                        Signed in as {auth?.claims.email as string}
+                    </Button>
+                </>
+            )}>
+            <SidebarLayout sidebar={(
+                <>
+                    <SidebarHeader>
+                        {IRIS_SITE.brand.logo ? (
+                            <img className="w-full h-10 object-contain" src={theme.config.colors.dark ? IRIS_SITE.brand.logo.light : IRIS_SITE.brand.logo.dark} />
+                        ) : (
+                            <SidebarTitle>{IRIS_SITE.brand.name}</SidebarTitle>
+                        )}
+                    </SidebarHeader>
+                    <SidebarItems>
+                        {IRIS_SITE.plugins.map(({ id }) => {
+                            const plugin = plugins[id];
+                            return (
+                                <SidebarItemGroup title={plugin.plugin.displayName} icon={plugin.plugin.icon}>
+                                    {plugin.plugin.navigation.map(item => (
+                                        <Link to={panelRefHref(id, item.panel)} onClick={e => panelNavigation.startNavigation(id, item.panel, e)}>
+                                            <SidebarItem active={(panelNavigation.activeRef && (panelNavigation.pluginId === id)) ? isAtPanel(panelNavigation.breadcrumbs[0]?.ref ?? panelNavigation.activeRef, item.panel) : false} icon={item.icon}>{item.displayName}</SidebarItem>
+                                        </Link>
+                                    ))}
+                                </SidebarItemGroup>
+                            );
+                        })}
+                    </SidebarItems>
+                </>
+            )}>
+                {children}
+            </SidebarLayout>
+        </NavbarLayout>
+    );
+}
+
+export default function Shell({ children }: { children?: React.ReactNode; }) {
+    const location = useLocation();
+    const plugins = usePlugins();
+
+    if (location.pathname === "/login") {
+        return (
+            <Outlet />
+        );
+    }
+
+    if (!plugins) {
+        return (
+            <p>Loading plugins...</p>
+        );
+    }
+
+    return (
+        <PanelNavigator>
+            <ShellLayout>
+                {children}
+            </ShellLayout>
+        </PanelNavigator>
+    );
+}
\ No newline at end of file
diff --git a/packages/webui/src/app/data/panels.ts b/packages/webui/src/app/data/panels.ts
new file mode 100644
index 0000000..26e99f0
--- /dev/null
+++ b/packages/webui/src/app/data/panels.ts
@@ -0,0 +1,28 @@
+// Iris
+// Copyright (c) jottocraft 2025
+
+import { createContext, useContext } from "react";
+import isEqual from "lodash/isEqual";
+
+export const panelNavigationCtx = createContext<IrisWebuiPanelNavigationCtx | null>(null);
+export function usePanelNavigation() {
+    return useContext(panelNavigationCtx);
+}
+
+export function panelRefHref(pluginId: string, ref: IrisWebuiPanelRef) {
+    let linkHref: string = `/p/${pluginId}/${ref.id}`;
+    if (ref.args) {
+        const linkParams = new URLSearchParams();
+        for (const arg in ref.args) {
+            if (ref.args[arg]) linkParams.set(arg, ref.args[arg]);
+        }
+        linkHref = linkHref + "?" + linkParams.toString();
+    }
+
+    return linkHref;
+}
+
+export function isAtPanel(activeRef: IrisWebuiPanelRef, desiredRef: IrisWebuiPanelRef) {
+    if (activeRef.id !== desiredRef.id) return false;
+    return isEqual(activeRef.args ?? {}, desiredRef.args ?? {});
+}
\ No newline at end of file
diff --git a/packages/webui/src/app/data/root.ts b/packages/webui/src/app/data/root.ts
new file mode 100644
index 0000000..0697087
--- /dev/null
+++ b/packages/webui/src/app/data/root.ts
@@ -0,0 +1,15 @@
+// Iris
+// Copyright (c) jottocraft 2025
+
+import type { IrisAuthCtx, IrisPluginCtx } from "@jottocraft/iris-sdk";
+import { createContext, useContext } from "react";
+
+export const pluginContext = createContext<IrisPluginCtx | null>(null);
+export function usePlugins() {
+    return useContext(pluginContext);
+}
+
+export const authContext = createContext<IrisAuthCtx | null>(null);
+export function useAuth() {
+    return useContext(authContext);
+}
\ No newline at end of file
diff --git a/packages/webui/src/app/root.tsx b/packages/webui/src/app/root.tsx
index abab211..c96602f 100644
--- a/packages/webui/src/app/root.tsx
+++ b/packages/webui/src/app/root.tsx
@@ -2,6 +2,9 @@
 // Copyright (c) jottocraft 2025
 
 import { BodyWrapper, Container, HeadWrapper, HTMLWrapper, Link, Root, ThemeEngine } from "@ffxp/react-dom";
+import type { IrisAuthCtx, IrisPluginConduit, IrisPluginConduitConduitServerTypes, IrisPluginCtx } from "@jottocraft/iris-sdk";
+import { createConduitClientSideClient } from "@jottocraft/yggdrasil";
+import { useEffect, useState } from "react";
 import {
     isRouteErrorResponse,
     Links,
@@ -9,12 +12,88 @@
     Scripts,
     ScrollRestoration,
     useLoaderData,
+    useLocation,
     type LoaderFunction,
 } from "react-router";
 
 import "~/app/app.css";
+import Shell from "~/app/components/Shell";
+import { authContext, pluginContext } from "~/app/data/root";
+import IRIS_SITE from "~/IRIS_SITE";
 
 export default function App() {
+    const location = useLocation();
+
+    const [auth, setAuth] = useState<IrisAuthCtx | null>(null);
+    useEffect(() => {
+        if (location.pathname === "/login") {
+            return;
+        }
+
+        if (!auth) {
+            let jwt = window.localStorage.getItem("jwt");
+            if (jwt) {
+                const jwtData = JSON.parse(atob(jwt.split(".")[1]));
+                if (!jwtData.exp || (new Date().getTime() > (jwtData.exp * 1000))) {
+                    jwt = null;
+                } else {
+                    setAuth({
+                        jwt,
+                        claims: jwtData
+                    });
+                }
+            }
+
+            if (!jwt) {
+                const redirectUrl = new URL(window.location.origin);
+                redirectUrl.pathname = "/webui/login";
+
+                const nonce = crypto.randomUUID();
+                window.localStorage.setItem("nonce", JSON.stringify({
+                    nonce,
+                    // Expires in 30 minutes
+                    useBefore: new Date().getTime() + 30 * 60 * 1000
+                } satisfies IrisWebuiNonceStorage));
+
+                const authUrl = new URL(IRIS_SITE.auth.authUrl);
+                authUrl.searchParams.set("client_id", IRIS_SITE.auth.clientId);
+                authUrl.searchParams.set("redirect_uri", redirectUrl.href);
+                authUrl.searchParams.set("response_type", "id_token");
+                authUrl.searchParams.set("scope", IRIS_SITE.auth.scope ?? "openid email profile");
+                authUrl.searchParams.set("nonce", nonce);
+                window.location.href = authUrl.href;
+            }
+        }
+    }, [auth, location.pathname]);
+
+    const [plugins, setPlugins] = useState<null | IrisPluginCtx>(null);
+    useEffect(() => {
+        if (!auth) return;
+
+        const plugins: IrisPluginCtx = {};
+        Promise.all(IRIS_SITE.plugins.map(async (sitePlugin) => {
+            if (sitePlugin.type === "conduit") {
+                const client = createConduitClientSideClient<IrisPluginConduitConduitServerTypes<IrisPluginConduit>>({
+                    jwt: auth.jwt
+                }, {
+                    baseURL: sitePlugin.baseUrl
+                });
+
+                const plugin = await client().plugin();
+
+                plugins[sitePlugin.id] = {
+                    type: "conduit",
+                    plugin,
+                    client
+                };
+            } else {
+                throw new Error("Unexpected plugin type " + sitePlugin.type);
+            }
+        })).then(() => {
+            setPlugins(plugins);
+        }).catch(e => { throw e; });
+    }, [auth]);
+
     return (
         <Root>
             <ThemeEngine plugins={[
@@ -53,7 +132,13 @@
                         <Links />
                     </HeadWrapper>
                     <BodyWrapper>
-                        <Outlet />
+                        <authContext.Provider value={auth}>
+                            <pluginContext.Provider value={plugins}>
+                                <Shell>
+                                    <Outlet />
+                                </Shell>
+                            </pluginContext.Provider>
+                        </authContext.Provider>
                         <ScrollRestoration />
                         <Scripts />
                     </BodyWrapper>
diff --git a/packages/webui/src/app/routes/login.tsx b/packages/webui/src/app/routes/login.tsx
new file mode 100644
index 0000000..f63ce03
--- /dev/null
+++ b/packages/webui/src/app/routes/login.tsx
@@ -0,0 +1,68 @@
+// Iris
+// Copyright (c) jottocraft 2025
+
+import type { JWTPayload } from "jose";
+import { useEffect, useState } from "react";
+
+import IRIS_SITE from "~/IRIS_SITE";
+
+export default function Login() {
+    const [error, setError] = useState<null | string>(null);
+    useEffect(() => {
+        const nonce = window.localStorage.getItem("nonce");
+        if (!nonce) {
+            setError("Missing auth state");
+            return;
+        }
+
+        const nonceData = JSON.parse(nonce) as IrisWebuiNonceStorage;
+        if (new Date().getTime() > nonceData.useBefore) {
+            setError("Auth state has expired");
+            return;
+        }
+
+        if (!window.location.hash.slice(1)) {
+            setError("Missing auth parameters");
+            return;
+        }
+
+        const hashParams = new URLSearchParams(window.location.hash.slice(1));
+        const jwt = hashParams.get("id_token");
+        if (!jwt) {
+            setError("Missing required id_token parameter");
+            return;
+        }
+
+        const jwtData = JSON.parse(atob(jwt.split(".")[1])) as JWTPayload;
+        if ((jwtData.aud !== IRIS_SITE.auth.clientId) ||
+            (jwtData.iss !== IRIS_SITE.auth.iss) ||
+            !jwtData.nbf ||
+            ((jwtData.nbf * 1000) > new Date().getTime()) ||
+            !jwtData.exp ||
+            ((jwtData.exp * 1000) < new Date().getTime()) ||
+            (jwtData.nonce !== nonceData.nonce)) {
+            setError("Invalid or expired id_token");
+            return;
+        }
+
+        const webuiUrl = new URL(window.location.origin);
+        webuiUrl.pathname = "/webui";
+
+        window.localStorage.removeItem("nonce");
+        window.localStorage.setItem("jwt", jwt);
+        window.location.href = webuiUrl.href;
+    }, []);
+
+    return (
+        <div>
+            {error ? (
+                <div>
+                    <h1>Authentication error</h1>
+                    <p>{error}</p>
+                </div>
+            ) : (
+                <p>Signing in...</p>
+            )}
+        </div>
+    );
+}
\ No newline at end of file
diff --git a/packages/webui/src/app/routes/p/$.tsx b/packages/webui/src/app/routes/p/$.tsx
new file mode 100644
index 0000000..1837210
--- /dev/null
+++ b/packages/webui/src/app/routes/p/$.tsx
@@ -0,0 +1,10 @@
+// Iris
+// Copyright (c) jottocraft 2025
+
+export default function Home() {
+    return (
+        <div>
+            <p>Hello plugin!</p>
+        </div>
+    );
+}
\ No newline at end of file
diff --git a/packages/webui/src/app/routes/p/$pluginId/$panelId.tsx b/packages/webui/src/app/routes/p/$pluginId/$panelId.tsx
new file mode 100644
index 0000000..76e2f75
--- /dev/null
+++ b/packages/webui/src/app/routes/p/$pluginId/$panelId.tsx
@@ -0,0 +1,15 @@
+// Iris
+// Copyright (c) jottocraft 2025
+
+import { useState } from "react";
+import { useParams } from "react-router";
+
+import Panel from "~/app/components/Panel";
+
+export default function PanelRoute() {
+    return (
+        <div className="py-12">
+            <Panel />
+        </div >
+    );
+}
\ No newline at end of file
diff --git a/packages/webui/src/webui.d.ts b/packages/webui/src/webui.d.ts
index dc05a7e..869b1fc 100644
--- a/packages/webui/src/webui.d.ts
+++ b/packages/webui/src/webui.d.ts
@@ -1,2 +1,42 @@
 // Iris
-// Copyright (c) jottocraft 2025
\ No newline at end of file
+// Copyright (c) jottocraft 2025
+
+declare type IrisWebuiConfig = {
+    origin: string;
+    site: import("@jottocraft/iris-sdk").IrisSiteConfig;
+};
+
+declare type IrisWebuiNonceStorage = {
+    nonce: string;
+    useBefore: number;
+};
+
+declare type IrisWebuiPanelRef = import("@jottocraft/iris-sdk").IrisPluginConduitPanelRef<import("@jottocraft/iris-sdk").IrisPluginConduit>;
+declare type IrisWebuiFormRef = import("@jottocraft/iris-sdk").IrisPluginConduitFormRef<import("@jottocraft/iris-sdk").IrisPluginConduit>;
+
+declare type IrisWebuiPanelNavigationCtx = IrisWebuiPanelNavigationCtxActive | IrisWebuiPanelNavigationCtxInactive;
+
+declare type IrisWebuiPanelNavigationCtxCommon = {
+    startNavigation: (pluginId: string, ref: IrisWebuiPanelRef, e?: React.MouseEvent) => void;
+};
+
+declare type IrisWebuiPanelNavigationCtxActive = IrisWebuiPanelNavigationCtxCommon & {
+    activeRef: IrisWebuiPanelRef;
+    layout: null | import("@jottocraft/iris-sdk").IrisPluginConduitPanelLayout<import("@jottocraft/iris-sdk").IrisPluginConduit>;
+    pluginId: string;
+    breadcrumbs: IrisWebuiPanelBreadcrumb[];
+    navigate: (ref: IrisWebuiPanelRef, e?: React.MouseEvent) => void;
+    back: (e?: React.MouseEvent) => void;
+    goToBreadcrumb: (index: number, e?: React.MouseEvent) => void;
+    refreshLayout: () => void;
+};
+
+declare type IrisWebuiPanelNavigationCtxInactive = IrisWebuiPanelNavigationCtxCommon & {
+    activeRef: null;
+};
+
+declare type IrisWebuiPanelBreadcrumb = {
+    displayName: string;
+    icon?: string;
+    ref: IrisWebuiPanelRef;
+};
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index 5da2495..4e04f2d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1157,6 +1157,33 @@
   languageName: node
   linkType: hard
 
+"@nodelib/fs.scandir@npm:2.1.5":
+  version: 2.1.5
+  resolution: "@nodelib/fs.scandir@npm:2.1.5"
+  dependencies:
+    "@nodelib/fs.stat": "npm:2.0.5"
+    run-parallel: "npm:^1.1.9"
+  checksum: 10c0/732c3b6d1b1e967440e65f284bd06e5821fedf10a1bea9ed2bb75956ea1f30e08c44d3def9d6a230666574edbaf136f8cfd319c14fd1f87c66e6a44449afb2eb
+  languageName: node
+  linkType: hard
+
+"@nodelib/fs.stat@npm:2.0.5, @nodelib/fs.stat@npm:^2.0.2":
+  version: 2.0.5
+  resolution: "@nodelib/fs.stat@npm:2.0.5"
+  checksum: 10c0/88dafe5e3e29a388b07264680dc996c17f4bda48d163a9d4f5c1112979f0ce8ec72aa7116122c350b4e7976bc5566dc3ddb579be1ceaacc727872eb4ed93926d
+  languageName: node
+  linkType: hard
+
+"@nodelib/fs.walk@npm:^1.2.3":
+  version: 1.2.8
+  resolution: "@nodelib/fs.walk@npm:1.2.8"
+  dependencies:
+    "@nodelib/fs.scandir": "npm:2.1.5"
+    fastq: "npm:^1.6.0"
+  checksum: 10c0/db9de047c3bb9b51f9335a7bb46f4fcfb6829fb628318c12115fbaf7d369bfce71c15b103d1fc3b464812d936220ee9bc1c8f762d032c9f6be9acc99249095b1
+  languageName: node
+  linkType: hard
+
 "@npmcli/agent@npm:^3.0.0":
   version: 3.0.0
   resolution: "@npmcli/agent@npm:3.0.0"
@@ -1782,6 +1809,13 @@
   languageName: node
   linkType: hard
 
+"@types/lodash@npm:^4.17.20":
+  version: 4.17.20
+  resolution: "@types/lodash@npm:4.17.20"
+  checksum: 10c0/98cdd0faae22cbb8079a01a3bb65aa8f8c41143367486c1cbf5adc83f16c9272a2a5d2c1f541f61d0d73da543c16ee1d21cf2ef86cb93cd0cc0ac3bced6dd88f
+  languageName: node
+  linkType: hard
+
 "@types/node@npm:^20":
   version: 20.19.9
   resolution: "@types/node@npm:20.19.9"
@@ -1888,6 +1922,15 @@
   languageName: node
   linkType: hard
 
+"ansi-styles@npm:^3.2.1":
+  version: 3.2.1
+  resolution: "ansi-styles@npm:3.2.1"
+  dependencies:
+    color-convert: "npm:^1.9.0"
+  checksum: 10c0/ece5a8ef069fcc5298f67e3f4771a663129abd174ea2dfa87923a2be2abf6cd367ef72ac87942da00ce85bd1d651d4cd8595aebdb1b385889b89b205860e977b
+  languageName: node
+  linkType: hard
+
 "ansi-styles@npm:^4.0.0, ansi-styles@npm:^4.1.0":
   version: 4.3.0
   resolution: "ansi-styles@npm:4.3.0"
@@ -1911,6 +1954,16 @@
   languageName: node
   linkType: hard
 
+"anymatch@npm:~3.1.2":
+  version: 3.1.3
+  resolution: "anymatch@npm:3.1.3"
+  dependencies:
+    normalize-path: "npm:^3.0.0"
+    picomatch: "npm:^2.0.4"
+  checksum: 10c0/57b06ae984bc32a0d22592c87384cd88fe4511b1dd7581497831c56d41939c8a001b28e7b853e1450f2bf61992dfcaa8ae2d0d161a0a90c4fb631ef07098fbac
+  languageName: node
+  linkType: hard
+
 "arg@npm:^5.0.1":
   version: 5.0.2
   resolution: "arg@npm:5.0.2"
@@ -1934,6 +1987,45 @@
   languageName: node
   linkType: hard
 
+"array-buffer-byte-length@npm:^1.0.1, array-buffer-byte-length@npm:^1.0.2":
+  version: 1.0.2
+  resolution: "array-buffer-byte-length@npm:1.0.2"
+  dependencies:
+    call-bound: "npm:^1.0.3"
+    is-array-buffer: "npm:^3.0.5"
+  checksum: 10c0/74e1d2d996941c7a1badda9cabb7caab8c449db9086407cad8a1b71d2604cc8abf105db8ca4e02c04579ec58b7be40279ddb09aea4784832984485499f48432d
+  languageName: node
+  linkType: hard
+
+"array-union@npm:^2.1.0":
+  version: 2.1.0
+  resolution: "array-union@npm:2.1.0"
+  checksum: 10c0/429897e68110374f39b771ec47a7161fc6a8fc33e196857c0a396dc75df0b5f65e4d046674db764330b6bb66b39ef48dd7c53b6a2ee75cfb0681e0c1a7033962
+  languageName: node
+  linkType: hard
+
+"arraybuffer.prototype.slice@npm:^1.0.4":
+  version: 1.0.4
+  resolution: "arraybuffer.prototype.slice@npm:1.0.4"
+  dependencies:
+    array-buffer-byte-length: "npm:^1.0.1"
+    call-bind: "npm:^1.0.8"
+    define-properties: "npm:^1.2.1"
+    es-abstract: "npm:^1.23.5"
+    es-errors: "npm:^1.3.0"
+    get-intrinsic: "npm:^1.2.6"
+    is-array-buffer: "npm:^3.0.4"
+  checksum: 10c0/2f2459caa06ae0f7f615003f9104b01f6435cc803e11bd2a655107d52a1781dc040532dc44d93026b694cc18793993246237423e13a5337e86b43ed604932c06
+  languageName: node
+  linkType: hard
+
+"async-function@npm:^1.0.0":
+  version: 1.0.0
+  resolution: "async-function@npm:1.0.0"
+  checksum: 10c0/669a32c2cb7e45091330c680e92eaeb791bc1d4132d827591e499cd1f776ff5a873e77e5f92d0ce795a8d60f10761dec9ddfe7225a5de680f5d357f67b1aac73
+  languageName: node
+  linkType: hard
+
 "asynckit@npm:^0.4.0":
   version: 0.4.0
   resolution: "asynckit@npm:0.4.0"
@@ -1941,6 +2033,15 @@
   languageName: node
   linkType: hard
 
+"available-typed-arrays@npm:^1.0.7":
+  version: 1.0.7
+  resolution: "available-typed-arrays@npm:1.0.7"
+  dependencies:
+    possible-typed-array-names: "npm:^1.0.0"
+  checksum: 10c0/d07226ef4f87daa01bd0fe80f8f310982e345f372926da2e5296aecc25c41cab440916bbaa4c5e1034b453af3392f67df5961124e4b586df1e99793a1374bdb2
+  languageName: node
+  linkType: hard
+
 "axios@npm:^1.8.3":
   version: 1.10.0
   resolution: "axios@npm:1.10.0"
@@ -1978,6 +2079,13 @@
   languageName: node
   linkType: hard
 
+"binary-extensions@npm:^2.0.0":
+  version: 2.3.0
+  resolution: "binary-extensions@npm:2.3.0"
+  checksum: 10c0/75a59cafc10fb12a11d510e77110c6c7ae3f4ca22463d52487709ca7f18f69d886aa387557cc9864fbdb10153d0bdb4caacabf11541f55e89ed6e18d12ece2b5
+  languageName: node
+  linkType: hard
+
 "bl@npm:^4.0.3":
   version: 4.1.0
   resolution: "bl@npm:4.1.0"
@@ -1996,6 +2104,16 @@
   languageName: node
   linkType: hard
 
+"brace-expansion@npm:^1.1.7":
+  version: 1.1.12
+  resolution: "brace-expansion@npm:1.1.12"
+  dependencies:
+    balanced-match: "npm:^1.0.0"
+    concat-map: "npm:0.0.1"
+  checksum: 10c0/975fecac2bb7758c062c20d0b3b6288c7cc895219ee25f0a64a9de662dbac981ff0b6e89909c3897c1f84fa353113a721923afdec5f8b2350255b097f12b1f73
+  languageName: node
+  linkType: hard
+
 "brace-expansion@npm:^2.0.1":
   version: 2.0.2
   resolution: "brace-expansion@npm:2.0.2"
@@ -2005,6 +2123,15 @@
   languageName: node
   linkType: hard
 
+"braces@npm:^3.0.3, braces@npm:~3.0.2":
+  version: 3.0.3
+  resolution: "braces@npm:3.0.3"
+  dependencies:
+    fill-range: "npm:^7.1.1"
+  checksum: 10c0/7c6dfd30c338d2997ba77500539227b9d1f85e388a5f43220865201e407e076783d0881f2d297b9f80951b4c957fcf0b51c1d2d24227631643c3f7c284b0aa04
+  languageName: node
+  linkType: hard
+
 "browserslist@npm:^4.24.0":
   version: 4.25.1
   resolution: "browserslist@npm:4.25.1"
@@ -2056,7 +2183,7 @@
   languageName: node
   linkType: hard
 
-"call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2":
+"call-bind-apply-helpers@npm:^1.0.0, call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2":
   version: 1.0.2
   resolution: "call-bind-apply-helpers@npm:1.0.2"
   dependencies:
@@ -2066,6 +2193,28 @@
   languageName: node
   linkType: hard
 
+"call-bind@npm:^1.0.7, call-bind@npm:^1.0.8":
+  version: 1.0.8
+  resolution: "call-bind@npm:1.0.8"
+  dependencies:
+    call-bind-apply-helpers: "npm:^1.0.0"
+    es-define-property: "npm:^1.0.0"
+    get-intrinsic: "npm:^1.2.4"
+    set-function-length: "npm:^1.2.2"
+  checksum: 10c0/a13819be0681d915144467741b69875ae5f4eba8961eb0bf322aab63ec87f8250eb6d6b0dcbb2e1349876412a56129ca338592b3829ef4343527f5f18a0752d4
+  languageName: node
+  linkType: hard
+
+"call-bound@npm:^1.0.2, call-bound@npm:^1.0.3, call-bound@npm:^1.0.4":
+  version: 1.0.4
+  resolution: "call-bound@npm:1.0.4"
+  dependencies:
+    call-bind-apply-helpers: "npm:^1.0.2"
+    get-intrinsic: "npm:^1.3.0"
+  checksum: 10c0/f4796a6a0941e71c766aea672f63b72bc61234c4f4964dc6d7606e3664c307e7d77845328a8f3359ce39ddb377fed67318f9ee203dea1d47e46165dcf2917644
+  languageName: node
+  linkType: hard
+
 "caniuse-lite@npm:^1.0.30001726":
   version: 1.0.30001727
   resolution: "caniuse-lite@npm:1.0.30001727"
@@ -2073,6 +2222,17 @@
   languageName: node
   linkType: hard
 
+"chalk@npm:^2.4.1":
+  version: 2.4.2
+  resolution: "chalk@npm:2.4.2"
+  dependencies:
+    ansi-styles: "npm:^3.2.1"
+    escape-string-regexp: "npm:^1.0.5"
+    supports-color: "npm:^5.3.0"
+  checksum: 10c0/e6543f02ec877732e3a2d1c3c3323ddb4d39fbab687c23f526e25bd4c6a9bf3b83a696e8c769d078e04e5754921648f7821b2a2acfd16c550435fd630026e073
+  languageName: node
+  linkType: hard
+
 "chalk@npm:^4.1.0, chalk@npm:^4.1.2":
   version: 4.1.2
   resolution: "chalk@npm:4.1.2"
@@ -2083,6 +2243,25 @@
   languageName: node
   linkType: hard
 
+"chokidar@npm:^3.5.3":
+  version: 3.6.0
+  resolution: "chokidar@npm:3.6.0"
+  dependencies:
+    anymatch: "npm:~3.1.2"
+    braces: "npm:~3.0.2"
+    fsevents: "npm:~2.3.2"
+    glob-parent: "npm:~5.1.2"
+    is-binary-path: "npm:~2.1.0"
+    is-glob: "npm:~4.0.1"
+    normalize-path: "npm:~3.0.0"
+    readdirp: "npm:~3.6.0"
+  dependenciesMeta:
+    fsevents:
+      optional: true
+  checksum: 10c0/8361dcd013f2ddbe260eacb1f3cb2f2c6f2b0ad118708a343a5ed8158941a39cb8fb1d272e0f389712e74ee90ce8ba864eece9e0e62b9705cb468a2f6d917462
+  languageName: node
+  linkType: hard
+
 "chokidar@npm:^4.0.0":
   version: 4.0.3
   resolution: "chokidar@npm:4.0.3"
@@ -2161,6 +2340,15 @@
   languageName: node
   linkType: hard
 
+"color-convert@npm:^1.9.0":
+  version: 1.9.3
+  resolution: "color-convert@npm:1.9.3"
+  dependencies:
+    color-name: "npm:1.1.3"
+  checksum: 10c0/5ad3c534949a8c68fca8fbc6f09068f435f0ad290ab8b2f76841b9e6af7e0bb57b98cb05b0e19fe33f5d91e5a8611ad457e5f69e0a484caad1f7487fd0e8253c
+  languageName: node
+  linkType: hard
+
 "color-convert@npm:^2.0.1":
   version: 2.0.1
   resolution: "color-convert@npm:2.0.1"
@@ -2170,6 +2358,13 @@
   languageName: node
   linkType: hard
 
+"color-name@npm:1.1.3":
+  version: 1.1.3
+  resolution: "color-name@npm:1.1.3"
+  checksum: 10c0/566a3d42cca25b9b3cd5528cd7754b8e89c0eb646b7f214e8e2eaddb69994ac5f0557d9c175eb5d8f0ad73531140d9c47525085ee752a91a2ab15ab459caf6d6
+  languageName: node
+  linkType: hard
+
 "color-name@npm:^1.0.0, color-name@npm:~1.1.4":
   version: 1.1.4
   resolution: "color-name@npm:1.1.4"
@@ -2206,6 +2401,20 @@
   languageName: node
   linkType: hard
 
+"commander@npm:^9.0.0":
+  version: 9.5.0
+  resolution: "commander@npm:9.5.0"
+  checksum: 10c0/5f7784fbda2aaec39e89eb46f06a999e00224b3763dc65976e05929ec486e174fe9aac2655f03ba6a5e83875bd173be5283dc19309b7c65954701c02025b3c1d
+  languageName: node
+  linkType: hard
+
+"concat-map@npm:0.0.1":
+  version: 0.0.1
+  resolution: "concat-map@npm:0.0.1"
+  checksum: 10c0/c996b1cfdf95b6c90fee4dae37e332c8b6eb7d106430c17d538034c0ad9a1630cb194d2ab37293b1bdd4d779494beee7786d586a50bd9376fd6f7bcc2bd4c98f
+  languageName: node
+  linkType: hard
+
 "convert-source-map@npm:^2.0.0":
   version: 2.0.0
   resolution: "convert-source-map@npm:2.0.0"
@@ -2232,6 +2441,19 @@
   languageName: node
   linkType: hard
 
+"cross-spawn@npm:^6.0.5":
+  version: 6.0.6
+  resolution: "cross-spawn@npm:6.0.6"
+  dependencies:
+    nice-try: "npm:^1.0.4"
+    path-key: "npm:^2.0.1"
+    semver: "npm:^5.5.0"
+    shebang-command: "npm:^1.2.0"
+    which: "npm:^1.2.9"
+  checksum: 10c0/bf61fb890e8635102ea9bce050515cf915ff6a50ccaa0b37a17dc82fded0fb3ed7af5478b9367b86baee19127ad86af4be51d209f64fd6638c0862dca185fe1d
+  languageName: node
+  linkType: hard
+
 "cross-spawn@npm:^7.0.1, cross-spawn@npm:^7.0.6":
   version: 7.0.6
   resolution: "cross-spawn@npm:7.0.6"
@@ -2250,6 +2472,39 @@
   languageName: node
   linkType: hard
 
+"data-view-buffer@npm:^1.0.2":
+  version: 1.0.2
+  resolution: "data-view-buffer@npm:1.0.2"
+  dependencies:
+    call-bound: "npm:^1.0.3"
+    es-errors: "npm:^1.3.0"
+    is-data-view: "npm:^1.0.2"
+  checksum: 10c0/7986d40fc7979e9e6241f85db8d17060dd9a71bd53c894fa29d126061715e322a4cd47a00b0b8c710394854183d4120462b980b8554012acc1c0fa49df7ad38c
+  languageName: node
+  linkType: hard
+
+"data-view-byte-length@npm:^1.0.2":
+  version: 1.0.2
+  resolution: "data-view-byte-length@npm:1.0.2"
+  dependencies:
+    call-bound: "npm:^1.0.3"
+    es-errors: "npm:^1.3.0"
+    is-data-view: "npm:^1.0.2"
+  checksum: 10c0/f8a4534b5c69384d95ac18137d381f18a5cfae1f0fc1df0ef6feef51ef0d568606d970b69e02ea186c6c0f0eac77fe4e6ad96fec2569cc86c3afcc7475068c55
+  languageName: node
+  linkType: hard
+
+"data-view-byte-offset@npm:^1.0.1":
+  version: 1.0.1
+  resolution: "data-view-byte-offset@npm:1.0.1"
+  dependencies:
+    call-bound: "npm:^1.0.2"
+    es-errors: "npm:^1.3.0"
+    is-data-view: "npm:^1.0.1"
+  checksum: 10c0/fa7aa40078025b7810dcffc16df02c480573b7b53ef1205aa6a61533011005c1890e5ba17018c692ce7c900212b547262d33279fde801ad9843edc0863bf78c4
+  languageName: node
+  linkType: hard
+
 "debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.4, debug@npm:^4.4.1":
   version: 4.4.1
   resolution: "debug@npm:4.4.1"
@@ -2283,6 +2538,17 @@
   languageName: node
   linkType: hard
 
+"define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.4":
+  version: 1.1.4
+  resolution: "define-data-property@npm:1.1.4"
+  dependencies:
+    es-define-property: "npm:^1.0.0"
+    es-errors: "npm:^1.3.0"
+    gopd: "npm:^1.0.1"
+  checksum: 10c0/dea0606d1483eb9db8d930d4eac62ca0fa16738b0b3e07046cddfacf7d8c868bbe13fa0cb263eb91c7d0d527960dc3f2f2471a69ed7816210307f6744fe62e37
+  languageName: node
+  linkType: hard
+
 "define-lazy-prop@npm:^2.0.0":
   version: 2.0.0
   resolution: "define-lazy-prop@npm:2.0.0"
@@ -2290,6 +2556,17 @@
   languageName: node
   linkType: hard
 
+"define-properties@npm:^1.2.1":
+  version: 1.2.1
+  resolution: "define-properties@npm:1.2.1"
+  dependencies:
+    define-data-property: "npm:^1.0.1"
+    has-property-descriptors: "npm:^1.0.0"
+    object-keys: "npm:^1.1.1"
+  checksum: 10c0/88a152319ffe1396ccc6ded510a3896e77efac7a1bfbaa174a7b00414a1747377e0bb525d303794a47cf30e805c2ec84e575758512c6e44a993076d29fd4e6c3
+  languageName: node
+  linkType: hard
+
 "defu@npm:^6.1.4":
   version: 6.1.4
   resolution: "defu@npm:6.1.4"
@@ -2311,6 +2588,15 @@
   languageName: node
   linkType: hard
 
+"dir-glob@npm:^3.0.1":
+  version: 3.0.1
+  resolution: "dir-glob@npm:3.0.1"
+  dependencies:
+    path-type: "npm:^4.0.0"
+  checksum: 10c0/dcac00920a4d503e38bb64001acb19df4efc14536ada475725e12f52c16777afdee4db827f55f13a908ee7efc0cb282e2e3dbaeeb98c0993dd93d1802d3bf00c
+  languageName: node
+  linkType: hard
+
 "dotenv-expand@npm:~11.0.6":
   version: 11.0.7
   resolution: "dotenv-expand@npm:11.0.7"
@@ -2334,7 +2620,7 @@
   languageName: node
   linkType: hard
 
-"dunder-proto@npm:^1.0.1":
+"dunder-proto@npm:^1.0.0, dunder-proto@npm:^1.0.1":
   version: 1.0.1
   resolution: "dunder-proto@npm:1.0.1"
   dependencies:
@@ -2424,6 +2710,15 @@
   languageName: node
   linkType: hard
 
+"error-ex@npm:^1.3.1":
+  version: 1.3.2
+  resolution: "error-ex@npm:1.3.2"
+  dependencies:
+    is-arrayish: "npm:^0.2.1"
+  checksum: 10c0/ba827f89369b4c93382cfca5a264d059dfefdaa56ecc5e338ffa58a6471f5ed93b71a20add1d52290a4873d92381174382658c885ac1a2305f7baca363ce9cce
+  languageName: node
+  linkType: hard
+
 "error-stack-parser-es@npm:^1.0.5":
   version: 1.0.5
   resolution: "error-stack-parser-es@npm:1.0.5"
@@ -2431,7 +2726,69 @@
   languageName: node
   linkType: hard
 
-"es-define-property@npm:^1.0.1":
+"es-abstract@npm:^1.23.2, es-abstract@npm:^1.23.5, es-abstract@npm:^1.23.9":
+  version: 1.24.0
+  resolution: "es-abstract@npm:1.24.0"
+  dependencies:
+    array-buffer-byte-length: "npm:^1.0.2"
+    arraybuffer.prototype.slice: "npm:^1.0.4"
+    available-typed-arrays: "npm:^1.0.7"
+    call-bind: "npm:^1.0.8"
+    call-bound: "npm:^1.0.4"
+    data-view-buffer: "npm:^1.0.2"
+    data-view-byte-length: "npm:^1.0.2"
+    data-view-byte-offset: "npm:^1.0.1"
+    es-define-property: "npm:^1.0.1"
+    es-errors: "npm:^1.3.0"
+    es-object-atoms: "npm:^1.1.1"
+    es-set-tostringtag: "npm:^2.1.0"
+    es-to-primitive: "npm:^1.3.0"
+    function.prototype.name: "npm:^1.1.8"
+    get-intrinsic: "npm:^1.3.0"
+    get-proto: "npm:^1.0.1"
+    get-symbol-description: "npm:^1.1.0"
+    globalthis: "npm:^1.0.4"
+    gopd: "npm:^1.2.0"
+    has-property-descriptors: "npm:^1.0.2"
+    has-proto: "npm:^1.2.0"
+    has-symbols: "npm:^1.1.0"
+    hasown: "npm:^2.0.2"
+    internal-slot: "npm:^1.1.0"
+    is-array-buffer: "npm:^3.0.5"
+    is-callable: "npm:^1.2.7"
+    is-data-view: "npm:^1.0.2"
+    is-negative-zero: "npm:^2.0.3"
+    is-regex: "npm:^1.2.1"
+    is-set: "npm:^2.0.3"
+    is-shared-array-buffer: "npm:^1.0.4"
+    is-string: "npm:^1.1.1"
+    is-typed-array: "npm:^1.1.15"
+    is-weakref: "npm:^1.1.1"
+    math-intrinsics: "npm:^1.1.0"
+    object-inspect: "npm:^1.13.4"
+    object-keys: "npm:^1.1.1"
+    object.assign: "npm:^4.1.7"
+    own-keys: "npm:^1.0.1"
+    regexp.prototype.flags: "npm:^1.5.4"
+    safe-array-concat: "npm:^1.1.3"
+    safe-push-apply: "npm:^1.0.0"
+    safe-regex-test: "npm:^1.1.0"
+    set-proto: "npm:^1.0.0"
+    stop-iteration-iterator: "npm:^1.1.0"
+    string.prototype.trim: "npm:^1.2.10"
+    string.prototype.trimend: "npm:^1.0.9"
+    string.prototype.trimstart: "npm:^1.0.8"
+    typed-array-buffer: "npm:^1.0.3"
+    typed-array-byte-length: "npm:^1.0.3"
+    typed-array-byte-offset: "npm:^1.0.4"
+    typed-array-length: "npm:^1.0.7"
+    unbox-primitive: "npm:^1.1.0"
+    which-typed-array: "npm:^1.1.19"
+  checksum: 10c0/b256e897be32df5d382786ce8cce29a1dd8c97efbab77a26609bd70f2ed29fbcfc7a31758cb07488d532e7ccccdfca76c1118f2afe5a424cdc05ca007867c318
+  languageName: node
+  linkType: hard
+
+"es-define-property@npm:^1.0.0, es-define-property@npm:^1.0.1":
   version: 1.0.1
   resolution: "es-define-property@npm:1.0.1"
   checksum: 10c0/3f54eb49c16c18707949ff25a1456728c883e81259f045003499efba399c08bad00deebf65cccde8c0e07908c1a225c9d472b7107e558f2a48e28d530e34527c
@@ -2473,6 +2830,17 @@
   languageName: node
   linkType: hard
 
+"es-to-primitive@npm:^1.3.0":
+  version: 1.3.0
+  resolution: "es-to-primitive@npm:1.3.0"
+  dependencies:
+    is-callable: "npm:^1.2.7"
+    is-date-object: "npm:^1.0.5"
+    is-symbol: "npm:^1.0.4"
+  checksum: 10c0/c7e87467abb0b438639baa8139f701a06537d2b9bc758f23e8622c3b42fd0fdb5bde0f535686119e446dd9d5e4c0f238af4e14960f4771877cf818d023f6730b
+  languageName: node
+  linkType: hard
+
 "esbuild@npm:0.25.4":
   version: 0.25.4
   resolution: "esbuild@npm:0.25.4"
@@ -2693,6 +3061,28 @@
   languageName: node
   linkType: hard
 
+"fast-glob@npm:^3.2.9":
+  version: 3.3.3
+  resolution: "fast-glob@npm:3.3.3"
+  dependencies:
+    "@nodelib/fs.stat": "npm:^2.0.2"
+    "@nodelib/fs.walk": "npm:^1.2.3"
+    glob-parent: "npm:^5.1.2"
+    merge2: "npm:^1.3.0"
+    micromatch: "npm:^4.0.8"
+  checksum: 10c0/f6aaa141d0d3384cf73cbcdfc52f475ed293f6d5b65bfc5def368b09163a9f7e5ec2b3014d80f733c405f58e470ee0cc451c2937685045cddcdeaa24199c43fe
+  languageName: node
+  linkType: hard
+
+"fastq@npm:^1.6.0":
+  version: 1.19.1
+  resolution: "fastq@npm:1.19.1"
+  dependencies:
+    reusify: "npm:^1.0.4"
+  checksum: 10c0/ebc6e50ac7048daaeb8e64522a1ea7a26e92b3cee5cd1c7f2316cdca81ba543aa40a136b53891446ea5c3a67ec215fbaca87ad405f102dd97012f62916905630
+  languageName: node
+  linkType: hard
+
 "fdir@npm:^6.4.4, fdir@npm:^6.4.6":
   version: 6.4.6
   resolution: "fdir@npm:6.4.6"
@@ -2714,6 +3104,15 @@
   languageName: node
   linkType: hard
 
+"fill-range@npm:^7.1.1":
+  version: 7.1.1
+  resolution: "fill-range@npm:7.1.1"
+  dependencies:
+    to-regex-range: "npm:^5.0.1"
+  checksum: 10c0/b75b691bbe065472f38824f694c2f7449d7f5004aa950426a2c28f0306c60db9b880c0b0e4ed819997ffb882d1da02cfcfc819bddc94d71627f5269682edf018
+  languageName: node
+  linkType: hard
+
 "flat@npm:^5.0.2":
   version: 5.0.2
   resolution: "flat@npm:5.0.2"
@@ -2733,6 +3132,15 @@
   languageName: node
   linkType: hard
 
+"for-each@npm:^0.3.3, for-each@npm:^0.3.5":
+  version: 0.3.5
+  resolution: "for-each@npm:0.3.5"
+  dependencies:
+    is-callable: "npm:^1.2.7"
+  checksum: 10c0/0e0b50f6a843a282637d43674d1fb278dda1dd85f4f99b640024cfb10b85058aac0cc781bf689d5fe50b4b7f638e91e548560723a4e76e04fe96ae35ef039cee
+  languageName: node
+  linkType: hard
+
 "foreground-child@npm:^3.1.0, foreground-child@npm:^3.3.1":
   version: 3.3.1
   resolution: "foreground-child@npm:3.3.1"
@@ -2807,6 +3215,27 @@
   languageName: node
   linkType: hard
 
+"function.prototype.name@npm:^1.1.6, function.prototype.name@npm:^1.1.8":
+  version: 1.1.8
+  resolution: "function.prototype.name@npm:1.1.8"
+  dependencies:
+    call-bind: "npm:^1.0.8"
+    call-bound: "npm:^1.0.3"
+    define-properties: "npm:^1.2.1"
+    functions-have-names: "npm:^1.2.3"
+    hasown: "npm:^2.0.2"
+    is-callable: "npm:^1.2.7"
+  checksum: 10c0/e920a2ab52663005f3cbe7ee3373e3c71c1fb5558b0b0548648cdf3e51961085032458e26c71ff1a8c8c20e7ee7caeb03d43a5d1fa8610c459333323a2e71253
+  languageName: node
+  linkType: hard
+
+"functions-have-names@npm:^1.2.3":
+  version: 1.2.3
+  resolution: "functions-have-names@npm:1.2.3"
+  checksum: 10c0/33e77fd29bddc2d9bb78ab3eb854c165909201f88c75faa8272e35899e2d35a8a642a15e7420ef945e1f64a9670d6aa3ec744106b2aa42be68ca5114025954ca
+  languageName: node
+  linkType: hard
+
 "gensync@npm:^1.0.0-beta.2":
   version: 1.0.0-beta.2
   resolution: "gensync@npm:1.0.0-beta.2"
@@ -2821,7 +3250,7 @@
   languageName: node
   linkType: hard
 
-"get-intrinsic@npm:^1.2.6":
+"get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.2.7, get-intrinsic@npm:^1.3.0":
   version: 1.3.0
   resolution: "get-intrinsic@npm:1.3.0"
   dependencies:
@@ -2839,7 +3268,7 @@
   languageName: node
   linkType: hard
 
-"get-proto@npm:^1.0.1":
+"get-proto@npm:^1.0.0, get-proto@npm:^1.0.1":
   version: 1.0.1
   resolution: "get-proto@npm:1.0.1"
   dependencies:
@@ -2849,6 +3278,35 @@
   languageName: node
   linkType: hard
 
+"get-symbol-description@npm:^1.1.0":
+  version: 1.1.0
+  resolution: "get-symbol-description@npm:1.1.0"
+  dependencies:
+    call-bound: "npm:^1.0.3"
+    es-errors: "npm:^1.3.0"
+    get-intrinsic: "npm:^1.2.6"
+  checksum: 10c0/d6a7d6afca375779a4b307738c9e80dbf7afc0bdbe5948768d54ab9653c865523d8920e670991a925936eb524b7cb6a6361d199a760b21d0ca7620194455aa4b
+  languageName: node
+  linkType: hard
+
+"get-tsconfig@npm:^4.10.0":
+  version: 4.10.1
+  resolution: "get-tsconfig@npm:4.10.1"
+  dependencies:
+    resolve-pkg-maps: "npm:^1.0.0"
+  checksum: 10c0/7f8e3dabc6a49b747920a800fb88e1952fef871cdf51b79e98db48275a5de6cdaf499c55ee67df5fa6fe7ce65f0063e26de0f2e53049b408c585aa74d39ffa21
+  languageName: node
+  linkType: hard
+
+"glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2":
+  version: 5.1.2
+  resolution: "glob-parent@npm:5.1.2"
+  dependencies:
+    is-glob: "npm:^4.0.1"
+  checksum: 10c0/cab87638e2112bee3f839ef5f6e0765057163d39c66be8ec1602f3823da4692297ad4e972de876ea17c44d652978638d2fd583c6713d0eb6591706825020c9ee
+  languageName: node
+  linkType: hard
+
 "glob-to-regexp@npm:0.4.1":
   version: 0.4.1
   resolution: "glob-to-regexp@npm:0.4.1"
@@ -2888,6 +3346,30 @@
   languageName: node
   linkType: hard
 
+"globalthis@npm:^1.0.4":
+  version: 1.0.4
+  resolution: "globalthis@npm:1.0.4"
+  dependencies:
+    define-properties: "npm:^1.2.1"
+    gopd: "npm:^1.0.1"
+  checksum: 10c0/9d156f313af79d80b1566b93e19285f481c591ad6d0d319b4be5e03750d004dde40a39a0f26f7e635f9007a3600802f53ecd85a759b86f109e80a5f705e01846
+  languageName: node
+  linkType: hard
+
+"globby@npm:^11.0.4":
+  version: 11.1.0
+  resolution: "globby@npm:11.1.0"
+  dependencies:
+    array-union: "npm:^2.1.0"
+    dir-glob: "npm:^3.0.1"
+    fast-glob: "npm:^3.2.9"
+    ignore: "npm:^5.2.0"
+    merge2: "npm:^1.4.1"
+    slash: "npm:^3.0.0"
+  checksum: 10c0/b39511b4afe4bd8a7aead3a27c4ade2b9968649abab0a6c28b1a90141b96ca68ca5db1302f7c7bd29eab66bf51e13916b8e0a3d0ac08f75e1e84a39b35691189
+  languageName: node
+  linkType: hard
+
 "globrex@npm:^0.1.2":
   version: 0.1.2
   resolution: "globrex@npm:0.1.2"
@@ -2895,20 +3377,34 @@
   languageName: node
   linkType: hard
 
-"gopd@npm:^1.2.0":
+"gopd@npm:^1.0.1, gopd@npm:^1.2.0":
   version: 1.2.0
   resolution: "gopd@npm:1.2.0"
   checksum: 10c0/50fff1e04ba2b7737c097358534eacadad1e68d24cccee3272e04e007bed008e68d2614f3987788428fd192a5ae3889d08fb2331417e4fc4a9ab366b2043cead
   languageName: node
   linkType: hard
 
-"graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6":
+"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6":
   version: 4.2.11
   resolution: "graceful-fs@npm:4.2.11"
   checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2
   languageName: node
   linkType: hard
 
+"has-bigints@npm:^1.0.2":
+  version: 1.1.0
+  resolution: "has-bigints@npm:1.1.0"
+  checksum: 10c0/2de0cdc4a1ccf7a1e75ffede1876994525ac03cc6f5ae7392d3415dd475cd9eee5bceec63669ab61aa997ff6cceebb50ef75561c7002bed8988de2b9d1b40788
+  languageName: node
+  linkType: hard
+
+"has-flag@npm:^3.0.0":
+  version: 3.0.0
+  resolution: "has-flag@npm:3.0.0"
+  checksum: 10c0/1c6c83b14b8b1b3c25b0727b8ba3e3b647f99e9e6e13eb7322107261de07a4c1be56fc0d45678fc376e09772a3a1642ccdaf8fc69bdf123b6c086598397ce473
+  languageName: node
+  linkType: hard
+
 "has-flag@npm:^4.0.0":
   version: 4.0.0
   resolution: "has-flag@npm:4.0.0"
@@ -2916,6 +3412,24 @@
   languageName: node
   linkType: hard
 
+"has-property-descriptors@npm:^1.0.0, has-property-descriptors@npm:^1.0.2":
+  version: 1.0.2
+  resolution: "has-property-descriptors@npm:1.0.2"
+  dependencies:
+    es-define-property: "npm:^1.0.0"
+  checksum: 10c0/253c1f59e80bb476cf0dde8ff5284505d90c3bdb762983c3514d36414290475fe3fd6f574929d84de2a8eec00d35cf07cb6776205ff32efd7c50719125f00236
+  languageName: node
+  linkType: hard
+
+"has-proto@npm:^1.2.0":
+  version: 1.2.0
+  resolution: "has-proto@npm:1.2.0"
+  dependencies:
+    dunder-proto: "npm:^1.0.0"
+  checksum: 10c0/46538dddab297ec2f43923c3d35237df45d8c55a6fc1067031e04c13ed8a9a8f94954460632fd4da84c31a1721eefee16d901cbb1ae9602bab93bb6e08f93b95
+  languageName: node
+  linkType: hard
+
 "has-symbols@npm:^1.0.3, has-symbols@npm:^1.1.0":
   version: 1.1.0
   resolution: "has-symbols@npm:1.1.0"
@@ -2941,6 +3455,13 @@
   languageName: node
   linkType: hard
 
+"hosted-git-info@npm:^2.1.4":
+  version: 2.8.9
+  resolution: "hosted-git-info@npm:2.8.9"
+  checksum: 10c0/317cbc6b1bbbe23c2a40ae23f3dafe9fa349ce42a89a36f930e3f9c0530c179a3882d2ef1e4141a4c3674d6faaea862138ec55b43ad6f75e387fda2483a13c70
+  languageName: node
+  linkType: hard
+
 "hosted-git-info@npm:^6.0.0, hosted-git-info@npm:^6.1.1":
   version: 6.1.3
   resolution: "hosted-git-info@npm:6.1.3"
@@ -2993,7 +3514,7 @@
   languageName: node
   linkType: hard
 
-"ignore@npm:^5.0.4":
+"ignore@npm:^5.0.4, ignore@npm:^5.2.0":
   version: 5.3.2
   resolution: "ignore@npm:5.3.2"
   checksum: 10c0/f9f652c957983634ded1e7f02da3b559a0d4cc210fca3792cb67f1b153623c9c42efdc1c4121af171e295444459fc4a9201101fb041b1104a3c000bccb188337
@@ -3014,6 +3535,17 @@
   languageName: node
   linkType: hard
 
+"internal-slot@npm:^1.1.0":
+  version: 1.1.0
+  resolution: "internal-slot@npm:1.1.0"
+  dependencies:
+    es-errors: "npm:^1.3.0"
+    hasown: "npm:^2.0.2"
+    side-channel: "npm:^1.1.0"
+  checksum: 10c0/03966f5e259b009a9bf1a78d60da920df198af4318ec004f57b8aef1dd3fe377fbc8cce63a96e8c810010302654de89f9e19de1cd8ad0061d15be28a695465c7
+  languageName: node
+  linkType: hard
+
 "ip-address@npm:^9.0.5":
   version: 9.0.5
   resolution: "ip-address@npm:9.0.5"
@@ -3024,29 +3556,11 @@
   languageName: node
   linkType: hard
 
-"iris-plugins@workspace:*, iris-plugins@workspace:packages/plugins":
-  version: 0.0.0-use.local
-  resolution: "iris-plugins@workspace:packages/plugins"
-  dependencies:
-    "@jottocraft/iris-sdk": "workspace:*"
-  languageName: unknown
-  linkType: soft
-
-"iris-server@workspace:packages/server":
-  version: 0.0.0-use.local
-  resolution: "iris-server@workspace:packages/server"
-  dependencies:
-    "@jottocraft/iris-sdk": "workspace:*"
-    iris-plugins: "workspace:*"
-  languageName: unknown
-  linkType: soft
-
 "iris-webui@workspace:packages/webui":
   version: 0.0.0-use.local
   resolution: "iris-webui@workspace:packages/webui"
   dependencies:
     "@jottocraft/iris-sdk": "workspace:*"
-    iris-plugins: "workspace:*"
   languageName: unknown
   linkType: soft
 
@@ -3067,17 +3581,22 @@
     "@remix-run/v1-route-convention": "patch:@remix-run/v1-route-convention@npm%3A0.1.4#~/.yarn/patches/@remix-run-v1-route-convention-npm-0.1.4-d336e45418.patch"
     "@tailwindcss/vite": "npm:^4.1.4"
     "@types/chroma-js": "npm:^3.1.1"
+    "@types/lodash": "npm:^4.17.20"
     "@types/node": "npm:^20"
     "@types/react": "npm:^19.1.2"
     "@types/react-dom": "npm:^19.1.2"
     chroma-js: "npm:^3.1.2"
     cross-env: "npm:^7.0.3"
+    jose: "npm:^6.0.12"
+    lodash: "npm:^4.17.21"
+    npm-run-all: "npm:^4.1.5"
     nx: "npm:^21.2.4"
     react: "npm:^19.1.0"
     react-dom: "npm:^19.1.0"
     react-router: "npm:^7.5.3"
     rimraf: "npm:^6.0.1"
     tailwindcss: "npm:^4.1.4"
+    tsc-alias: "npm:^1.8.16"
     typescript: "npm:^5.8.3"
     vite: "npm:^6.3.3"
     vite-tsconfig-paths: "npm:^5.1.4"
@@ -3085,6 +3604,24 @@
   languageName: unknown
   linkType: soft
 
+"is-array-buffer@npm:^3.0.4, is-array-buffer@npm:^3.0.5":
+  version: 3.0.5
+  resolution: "is-array-buffer@npm:3.0.5"
+  dependencies:
+    call-bind: "npm:^1.0.8"
+    call-bound: "npm:^1.0.3"
+    get-intrinsic: "npm:^1.2.6"
+  checksum: 10c0/c5c9f25606e86dbb12e756694afbbff64bc8b348d1bc989324c037e1068695131930199d6ad381952715dad3a9569333817f0b1a72ce5af7f883ce802e49c83d
+  languageName: node
+  linkType: hard
+
+"is-arrayish@npm:^0.2.1":
+  version: 0.2.1
+  resolution: "is-arrayish@npm:0.2.1"
+  checksum: 10c0/e7fb686a739068bb70f860b39b67afc62acc62e36bb61c5f965768abce1873b379c563e61dd2adad96ebb7edf6651111b385e490cf508378959b0ed4cac4e729
+  languageName: node
+  linkType: hard
+
 "is-arrayish@npm:^0.3.1":
   version: 0.3.2
   resolution: "is-arrayish@npm:0.3.2"
@@ -3092,7 +3629,55 @@
   languageName: node
   linkType: hard
 
-"is-core-module@npm:^2.8.1":
+"is-async-function@npm:^2.0.0":
+  version: 2.1.1
+  resolution: "is-async-function@npm:2.1.1"
+  dependencies:
+    async-function: "npm:^1.0.0"
+    call-bound: "npm:^1.0.3"
+    get-proto: "npm:^1.0.1"
+    has-tostringtag: "npm:^1.0.2"
+    safe-regex-test: "npm:^1.1.0"
+  checksum: 10c0/d70c236a5e82de6fc4d44368ffd0c2fee2b088b893511ce21e679da275a5ecc6015ff59a7d7e1bdd7ca39f71a8dbdd253cf8cce5c6b3c91cdd5b42b5ce677298
+  languageName: node
+  linkType: hard
+
+"is-bigint@npm:^1.1.0":
+  version: 1.1.0
+  resolution: "is-bigint@npm:1.1.0"
+  dependencies:
+    has-bigints: "npm:^1.0.2"
+  checksum: 10c0/f4f4b905ceb195be90a6ea7f34323bf1c18e3793f18922e3e9a73c684c29eeeeff5175605c3a3a74cc38185fe27758f07efba3dbae812e5c5afbc0d2316b40e4
+  languageName: node
+  linkType: hard
+
+"is-binary-path@npm:~2.1.0":
+  version: 2.1.0
+  resolution: "is-binary-path@npm:2.1.0"
+  dependencies:
+    binary-extensions: "npm:^2.0.0"
+  checksum: 10c0/a16eaee59ae2b315ba36fad5c5dcaf8e49c3e27318f8ab8fa3cdb8772bf559c8d1ba750a589c2ccb096113bb64497084361a25960899cb6172a6925ab6123d38
+  languageName: node
+  linkType: hard
+
+"is-boolean-object@npm:^1.2.1":
+  version: 1.2.2
+  resolution: "is-boolean-object@npm:1.2.2"
+  dependencies:
+    call-bound: "npm:^1.0.3"
+    has-tostringtag: "npm:^1.0.2"
+  checksum: 10c0/36ff6baf6bd18b3130186990026f5a95c709345c39cd368468e6c1b6ab52201e9fd26d8e1f4c066357b4938b0f0401e1a5000e08257787c1a02f3a719457001e
+  languageName: node
+  linkType: hard
+
+"is-callable@npm:^1.2.7":
+  version: 1.2.7
+  resolution: "is-callable@npm:1.2.7"
+  checksum: 10c0/ceebaeb9d92e8adee604076971dd6000d38d6afc40bb843ea8e45c5579b57671c3f3b50d7f04869618242c6cee08d1b67806a8cb8edaaaf7c0748b3720d6066f
+  languageName: node
+  linkType: hard
+
+"is-core-module@npm:^2.16.0, is-core-module@npm:^2.8.1":
   version: 2.16.1
   resolution: "is-core-module@npm:2.16.1"
   dependencies:
@@ -3101,6 +3686,27 @@
   languageName: node
   linkType: hard
 
+"is-data-view@npm:^1.0.1, is-data-view@npm:^1.0.2":
+  version: 1.0.2
+  resolution: "is-data-view@npm:1.0.2"
+  dependencies:
+    call-bound: "npm:^1.0.2"
+    get-intrinsic: "npm:^1.2.6"
+    is-typed-array: "npm:^1.1.13"
+  checksum: 10c0/ef3548a99d7e7f1370ce21006baca6d40c73e9f15c941f89f0049c79714c873d03b02dae1c64b3f861f55163ecc16da06506c5b8a1d4f16650b3d9351c380153
+  languageName: node
+  linkType: hard
+
+"is-date-object@npm:^1.0.5, is-date-object@npm:^1.1.0":
+  version: 1.1.0
+  resolution: "is-date-object@npm:1.1.0"
+  dependencies:
+    call-bound: "npm:^1.0.2"
+    has-tostringtag: "npm:^1.0.2"
+  checksum: 10c0/1a4d199c8e9e9cac5128d32e6626fa7805175af9df015620ac0d5d45854ccf348ba494679d872d37301032e35a54fc7978fba1687e8721b2139aea7870cafa2f
+  languageName: node
+  linkType: hard
+
 "is-docker@npm:^2.0.0, is-docker@npm:^2.1.1":
   version: 2.2.1
   resolution: "is-docker@npm:2.2.1"
@@ -3110,6 +3716,22 @@
   languageName: node
   linkType: hard
 
+"is-extglob@npm:^2.1.1":
+  version: 2.1.1
+  resolution: "is-extglob@npm:2.1.1"
+  checksum: 10c0/5487da35691fbc339700bbb2730430b07777a3c21b9ebaecb3072512dfd7b4ba78ac2381a87e8d78d20ea08affb3f1971b4af629173a6bf435ff8a4c47747912
+  languageName: node
+  linkType: hard
+
+"is-finalizationregistry@npm:^1.1.0":
+  version: 1.1.1
+  resolution: "is-finalizationregistry@npm:1.1.1"
+  dependencies:
+    call-bound: "npm:^1.0.3"
+  checksum: 10c0/818dff679b64f19e228a8205a1e2d09989a98e98def3a817f889208cfcbf918d321b251aadf2c05918194803ebd2eb01b14fc9d0b2bea53d984f4137bfca5e97
+  languageName: node
+  linkType: hard
+
 "is-fullwidth-code-point@npm:^3.0.0":
   version: 3.0.0
   resolution: "is-fullwidth-code-point@npm:3.0.0"
@@ -3117,6 +3739,27 @@
   languageName: node
   linkType: hard
 
+"is-generator-function@npm:^1.0.10":
+  version: 1.1.0
+  resolution: "is-generator-function@npm:1.1.0"
+  dependencies:
+    call-bound: "npm:^1.0.3"
+    get-proto: "npm:^1.0.0"
+    has-tostringtag: "npm:^1.0.2"
+    safe-regex-test: "npm:^1.1.0"
+  checksum: 10c0/fdfa96c8087bf36fc4cd514b474ba2ff404219a4dd4cfa6cf5426404a1eed259bdcdb98f082a71029a48d01f27733e3436ecc6690129a7ec09cb0434bee03a2a
+  languageName: node
+  linkType: hard
+
+"is-glob@npm:^4.0.1, is-glob@npm:~4.0.1":
+  version: 4.0.3
+  resolution: "is-glob@npm:4.0.3"
+  dependencies:
+    is-extglob: "npm:^2.1.1"
+  checksum: 10c0/17fb4014e22be3bbecea9b2e3a76e9e34ff645466be702f1693e8f1ee1adac84710d0be0bd9f967d6354036fd51ab7c2741d954d6e91dae6bb69714de92c197a
+  languageName: node
+  linkType: hard
+
 "is-interactive@npm:^1.0.0":
   version: 1.0.0
   resolution: "is-interactive@npm:1.0.0"
@@ -3124,6 +3767,95 @@
   languageName: node
   linkType: hard
 
+"is-map@npm:^2.0.3":
+  version: 2.0.3
+  resolution: "is-map@npm:2.0.3"
+  checksum: 10c0/2c4d431b74e00fdda7162cd8e4b763d6f6f217edf97d4f8538b94b8702b150610e2c64961340015fe8df5b1fcee33ccd2e9b62619c4a8a3a155f8de6d6d355fc
+  languageName: node
+  linkType: hard
+
+"is-negative-zero@npm:^2.0.3":
+  version: 2.0.3
+  resolution: "is-negative-zero@npm:2.0.3"
+  checksum: 10c0/bcdcf6b8b9714063ffcfa9929c575ac69bfdabb8f4574ff557dfc086df2836cf07e3906f5bbc4f2a5c12f8f3ba56af640c843cdfc74da8caed86c7c7d66fd08e
+  languageName: node
+  linkType: hard
+
+"is-number-object@npm:^1.1.1":
+  version: 1.1.1
+  resolution: "is-number-object@npm:1.1.1"
+  dependencies:
+    call-bound: "npm:^1.0.3"
+    has-tostringtag: "npm:^1.0.2"
+  checksum: 10c0/97b451b41f25135ff021d85c436ff0100d84a039bb87ffd799cbcdbea81ef30c464ced38258cdd34f080be08fc3b076ca1f472086286d2aa43521d6ec6a79f53
+  languageName: node
+  linkType: hard
+
+"is-number@npm:^7.0.0":
+  version: 7.0.0
+  resolution: "is-number@npm:7.0.0"
+  checksum: 10c0/b4686d0d3053146095ccd45346461bc8e53b80aeb7671cc52a4de02dbbf7dc0d1d2a986e2fe4ae206984b4d34ef37e8b795ebc4f4295c978373e6575e295d811
+  languageName: node
+  linkType: hard
+
+"is-regex@npm:^1.2.1":
+  version: 1.2.1
+  resolution: "is-regex@npm:1.2.1"
+  dependencies:
+    call-bound: "npm:^1.0.2"
+    gopd: "npm:^1.2.0"
+    has-tostringtag: "npm:^1.0.2"
+    hasown: "npm:^2.0.2"
+  checksum: 10c0/1d3715d2b7889932349241680032e85d0b492cfcb045acb75ffc2c3085e8d561184f1f7e84b6f8321935b4aea39bc9c6ba74ed595b57ce4881a51dfdbc214e04
+  languageName: node
+  linkType: hard
+
+"is-set@npm:^2.0.3":
+  version: 2.0.3
+  resolution: "is-set@npm:2.0.3"
+  checksum: 10c0/f73732e13f099b2dc879c2a12341cfc22ccaca8dd504e6edae26484bd5707a35d503fba5b4daad530a9b088ced1ae6c9d8200fd92e09b428fe14ea79ce8080b7
+  languageName: node
+  linkType: hard
+
+"is-shared-array-buffer@npm:^1.0.4":
+  version: 1.0.4
+  resolution: "is-shared-array-buffer@npm:1.0.4"
+  dependencies:
+    call-bound: "npm:^1.0.3"
+  checksum: 10c0/65158c2feb41ff1edd6bbd6fd8403a69861cf273ff36077982b5d4d68e1d59278c71691216a4a64632bd76d4792d4d1d2553901b6666d84ade13bba5ea7bc7db
+  languageName: node
+  linkType: hard
+
+"is-string@npm:^1.1.1":
+  version: 1.1.1
+  resolution: "is-string@npm:1.1.1"
+  dependencies:
+    call-bound: "npm:^1.0.3"
+    has-tostringtag: "npm:^1.0.2"
+  checksum: 10c0/2f518b4e47886bb81567faba6ffd0d8a8333cf84336e2e78bf160693972e32ad00fe84b0926491cc598dee576fdc55642c92e62d0cbe96bf36f643b6f956f94d
+  languageName: node
+  linkType: hard
+
+"is-symbol@npm:^1.0.4, is-symbol@npm:^1.1.1":
+  version: 1.1.1
+  resolution: "is-symbol@npm:1.1.1"
+  dependencies:
+    call-bound: "npm:^1.0.2"
+    has-symbols: "npm:^1.1.0"
+    safe-regex-test: "npm:^1.1.0"
+  checksum: 10c0/f08f3e255c12442e833f75a9e2b84b2d4882fdfd920513cf2a4a2324f0a5b076c8fd913778e3ea5d258d5183e9d92c0cd20e04b03ab3df05316b049b2670af1e
+  languageName: node
+  linkType: hard
+
+"is-typed-array@npm:^1.1.13, is-typed-array@npm:^1.1.14, is-typed-array@npm:^1.1.15":
+  version: 1.1.15
+  resolution: "is-typed-array@npm:1.1.15"
+  dependencies:
+    which-typed-array: "npm:^1.1.16"
+  checksum: 10c0/415511da3669e36e002820584e264997ffe277ff136643a3126cc949197e6ca3334d0f12d084e83b1994af2e9c8141275c741cf2b7da5a2ff62dd0cac26f76c4
+  languageName: node
+  linkType: hard
+
 "is-unicode-supported@npm:^0.1.0":
   version: 0.1.0
   resolution: "is-unicode-supported@npm:0.1.0"
@@ -3131,6 +3863,32 @@
   languageName: node
   linkType: hard
 
+"is-weakmap@npm:^2.0.2":
+  version: 2.0.2
+  resolution: "is-weakmap@npm:2.0.2"
+  checksum: 10c0/443c35bb86d5e6cc5929cd9c75a4024bb0fff9586ed50b092f94e700b89c43a33b186b76dbc6d54f3d3d09ece689ab38dcdc1af6a482cbe79c0f2da0a17f1299
+  languageName: node
+  linkType: hard
+
+"is-weakref@npm:^1.0.2, is-weakref@npm:^1.1.1":
+  version: 1.1.1
+  resolution: "is-weakref@npm:1.1.1"
+  dependencies:
+    call-bound: "npm:^1.0.3"
+  checksum: 10c0/8e0a9c07b0c780949a100e2cab2b5560a48ecd4c61726923c1a9b77b6ab0aa0046c9e7fb2206042296817045376dee2c8ab1dabe08c7c3dfbf195b01275a085b
+  languageName: node
+  linkType: hard
+
+"is-weakset@npm:^2.0.3":
+  version: 2.0.4
+  resolution: "is-weakset@npm:2.0.4"
+  dependencies:
+    call-bound: "npm:^1.0.3"
+    get-intrinsic: "npm:^1.2.6"
+  checksum: 10c0/6491eba08acb8dc9532da23cb226b7d0192ede0b88f16199e592e4769db0a077119c1f5d2283d1e0d16d739115f70046e887e477eb0e66cd90e1bb29f28ba647
+  languageName: node
+  linkType: hard
+
 "is-wsl@npm:^2.2.0":
   version: 2.2.0
   resolution: "is-wsl@npm:2.2.0"
@@ -3140,6 +3898,13 @@
   languageName: node
   linkType: hard
 
+"isarray@npm:^2.0.5":
+  version: 2.0.5
+  resolution: "isarray@npm:2.0.5"
+  checksum: 10c0/4199f14a7a13da2177c66c31080008b7124331956f47bca57dd0b6ea9f11687aa25e565a2c7a2b519bc86988d10398e3049a1f5df13c9f6b7664154690ae79fd
+  languageName: node
+  linkType: hard
+
 "isbot@npm:^5.1.11":
   version: 5.1.28
   resolution: "isbot@npm:5.1.28"
@@ -3204,6 +3969,13 @@
   languageName: node
   linkType: hard
 
+"jose@npm:^6.0.12":
+  version: 6.0.12
+  resolution: "jose@npm:6.0.12"
+  checksum: 10c0/e5ca51b078b2443f6ca671e14d72e0ffd21b760dac0d77cabd7af649a127376ec90665c8b25f34dd88bb31094915ee662daf76e0b33a025d28dbc2bc17413dec
+  languageName: node
+  linkType: hard
+
 "js-tokens@npm:^4.0.0":
   version: 4.0.0
   resolution: "js-tokens@npm:4.0.0"
@@ -3248,6 +4020,13 @@
   languageName: node
   linkType: hard
 
+"json-parse-better-errors@npm:^1.0.1":
+  version: 1.0.2
+  resolution: "json-parse-better-errors@npm:1.0.2"
+  checksum: 10c0/2f1287a7c833e397c9ddd361a78638e828fc523038bb3441fd4fc144cfd2c6cd4963ffb9e207e648cf7b692600f1e1e524e965c32df5152120910e4903a47dcb
+  languageName: node
+  linkType: hard
+
 "json-parse-even-better-errors@npm:^3.0.0":
   version: 3.0.2
   resolution: "json-parse-even-better-errors@npm:3.0.2"
@@ -3395,6 +4174,18 @@
   languageName: node
   linkType: hard
 
+"load-json-file@npm:^4.0.0":
+  version: 4.0.0
+  resolution: "load-json-file@npm:4.0.0"
+  dependencies:
+    graceful-fs: "npm:^4.1.2"
+    parse-json: "npm:^4.0.0"
+    pify: "npm:^3.0.0"
+    strip-bom: "npm:^3.0.0"
+  checksum: 10c0/6b48f6a0256bdfcc8970be2c57f68f10acb2ee7e63709b386b2febb6ad3c86198f840889cdbe71d28f741cbaa2f23a7771206b138cd1bdd159564511ca37c1d5
+  languageName: node
+  linkType: hard
+
 "lodash@npm:^4.17.21":
   version: 4.17.21
   resolution: "lodash@npm:4.17.21"
@@ -3477,6 +4268,30 @@
   languageName: node
   linkType: hard
 
+"memorystream@npm:^0.3.1":
+  version: 0.3.1
+  resolution: "memorystream@npm:0.3.1"
+  checksum: 10c0/4bd164657711d9747ff5edb0508b2944414da3464b7fe21ac5c67cf35bba975c4b446a0124bd0f9a8be54cfc18faf92e92bd77563a20328b1ccf2ff04e9f39b9
+  languageName: node
+  linkType: hard
+
+"merge2@npm:^1.3.0, merge2@npm:^1.4.1":
+  version: 1.4.1
+  resolution: "merge2@npm:1.4.1"
+  checksum: 10c0/254a8a4605b58f450308fc474c82ac9a094848081bf4c06778200207820e5193726dc563a0d2c16468810516a5c97d9d3ea0ca6585d23c58ccfff2403e8dbbeb
+  languageName: node
+  linkType: hard
+
+"micromatch@npm:^4.0.8":
+  version: 4.0.8
+  resolution: "micromatch@npm:4.0.8"
+  dependencies:
+    braces: "npm:^3.0.3"
+    picomatch: "npm:^2.3.1"
+  checksum: 10c0/166fa6eb926b9553f32ef81f5f531d27b4ce7da60e5baf8c021d043b27a388fb95e46a8038d5045877881e673f8134122b59624d5cecbd16eb50a42e7a6b5ca8
+  languageName: node
+  linkType: hard
+
 "mime-db@npm:1.52.0":
   version: 1.52.0
   resolution: "mime-db@npm:1.52.0"
@@ -3549,6 +4364,15 @@
   languageName: node
   linkType: hard
 
+"minimatch@npm:^3.0.4":
+  version: 3.1.2
+  resolution: "minimatch@npm:3.1.2"
+  dependencies:
+    brace-expansion: "npm:^1.1.7"
+  checksum: 10c0/0262810a8fc2e72cca45d6fd86bd349eee435eb95ac6aa45c9ea2180e7ee875ef44c32b55b5973ceabe95ea12682f6e3725cbb63d7a2d1da3ae1163c8b210311
+  languageName: node
+  linkType: hard
+
 "minimatch@npm:^7.4.3":
   version: 7.4.6
   resolution: "minimatch@npm:7.4.6"
@@ -3666,6 +4490,13 @@
   languageName: node
   linkType: hard
 
+"mylas@npm:^2.1.9":
+  version: 2.1.13
+  resolution: "mylas@npm:2.1.13"
+  checksum: 10c0/093dfaf88f444d9da956c68a61ddcfe05ab9411c0024b0c7f4d721639ba7d311ea8f9c052ce617344e67d40982f67614cd634b525b923048bf9a191234968c9c
+  languageName: node
+  linkType: hard
+
 "nanoid@npm:^3.3.11":
   version: 3.3.11
   resolution: "nanoid@npm:3.3.11"
@@ -3682,6 +4513,13 @@
   languageName: node
   linkType: hard
 
+"nice-try@npm:^1.0.4":
+  version: 1.0.5
+  resolution: "nice-try@npm:1.0.5"
+  checksum: 10c0/95568c1b73e1d0d4069a3e3061a2102d854513d37bcfda73300015b7ba4868d3b27c198d1dbbd8ebdef4112fc2ed9e895d4a0f2e1cce0bd334f2a1346dc9205f
+  languageName: node
+  linkType: hard
+
 "node-gyp@npm:latest":
   version: 11.2.0
   resolution: "node-gyp@npm:11.2.0"
@@ -3727,6 +4565,18 @@
   languageName: node
   linkType: hard
 
+"normalize-package-data@npm:^2.3.2":
+  version: 2.5.0
+  resolution: "normalize-package-data@npm:2.5.0"
+  dependencies:
+    hosted-git-info: "npm:^2.1.4"
+    resolve: "npm:^1.10.0"
+    semver: "npm:2 || 3 || 4 || 5"
+    validate-npm-package-license: "npm:^3.0.1"
+  checksum: 10c0/357cb1646deb42f8eb4c7d42c4edf0eec312f3628c2ef98501963cc4bbe7277021b2b1d977f982b2edce78f5a1014613ce9cf38085c3df2d76730481357ca504
+  languageName: node
+  linkType: hard
+
 "normalize-package-data@npm:^5.0.0":
   version: 5.0.0
   resolution: "normalize-package-data@npm:5.0.0"
@@ -3739,6 +4589,13 @@
   languageName: node
   linkType: hard
 
+"normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0":
+  version: 3.0.0
+  resolution: "normalize-path@npm:3.0.0"
+  checksum: 10c0/e008c8142bcc335b5e38cf0d63cfd39d6cf2d97480af9abdbe9a439221fd4d749763bab492a8ee708ce7a194bb00c9da6d0a115018672310850489137b3da046
+  languageName: node
+  linkType: hard
+
 "npm-install-checks@npm:^6.0.0":
   version: 6.3.0
   resolution: "npm-install-checks@npm:6.3.0"
@@ -3779,6 +4636,27 @@
   languageName: node
   linkType: hard
 
+"npm-run-all@npm:^4.1.5":
+  version: 4.1.5
+  resolution: "npm-run-all@npm:4.1.5"
+  dependencies:
+    ansi-styles: "npm:^3.2.1"
+    chalk: "npm:^2.4.1"
+    cross-spawn: "npm:^6.0.5"
+    memorystream: "npm:^0.3.1"
+    minimatch: "npm:^3.0.4"
+    pidtree: "npm:^0.3.0"
+    read-pkg: "npm:^3.0.0"
+    shell-quote: "npm:^1.6.1"
+    string.prototype.padend: "npm:^3.0.0"
+  bin:
+    npm-run-all: bin/npm-run-all/index.js
+    run-p: bin/run-p/index.js
+    run-s: bin/run-s/index.js
+  checksum: 10c0/736ee39bd35454d3efaa4a2e53eba6c523e2e17fba21a18edcce6b221f5cab62000bef16bb6ae8aff9e615831e6b0eb25ab51d52d60e6fa6f4ea880e4c6d31f4
+  languageName: node
+  linkType: hard
+
 "npm-run-path@npm:^4.0.1":
   version: 4.0.1
   resolution: "npm-run-path@npm:4.0.1"
@@ -3873,6 +4751,34 @@
   languageName: node
   linkType: hard
 
+"object-inspect@npm:^1.13.3, object-inspect@npm:^1.13.4":
+  version: 1.13.4
+  resolution: "object-inspect@npm:1.13.4"
+  checksum: 10c0/d7f8711e803b96ea3191c745d6f8056ce1f2496e530e6a19a0e92d89b0fa3c76d910c31f0aa270432db6bd3b2f85500a376a83aaba849a8d518c8845b3211692
+  languageName: node
+  linkType: hard
+
+"object-keys@npm:^1.1.1":
+  version: 1.1.1
+  resolution: "object-keys@npm:1.1.1"
+  checksum: 10c0/b11f7ccdbc6d406d1f186cdadb9d54738e347b2692a14439ca5ac70c225fa6db46db809711b78589866d47b25fc3e8dee0b4c722ac751e11180f9380e3d8601d
+  languageName: node
+  linkType: hard
+
+"object.assign@npm:^4.1.7":
+  version: 4.1.7
+  resolution: "object.assign@npm:4.1.7"
+  dependencies:
+    call-bind: "npm:^1.0.8"
+    call-bound: "npm:^1.0.3"
+    define-properties: "npm:^1.2.1"
+    es-object-atoms: "npm:^1.0.0"
+    has-symbols: "npm:^1.1.0"
+    object-keys: "npm:^1.1.1"
+  checksum: 10c0/3b2732bd860567ea2579d1567525168de925a8d852638612846bd8082b3a1602b7b89b67b09913cbb5b9bd6e95923b2ae73580baa9d99cb4e990564e8cbf5ddc
+  languageName: node
+  linkType: hard
+
 "ohash@npm:^2.0.11":
   version: 2.0.11
   resolution: "ohash@npm:2.0.11"
@@ -3925,6 +4831,17 @@
   languageName: node
   linkType: hard
 
+"own-keys@npm:^1.0.1":
+  version: 1.0.1
+  resolution: "own-keys@npm:1.0.1"
+  dependencies:
+    get-intrinsic: "npm:^1.2.6"
+    object-keys: "npm:^1.1.1"
+    safe-push-apply: "npm:^1.0.0"
+  checksum: 10c0/6dfeb3455bff92ec3f16a982d4e3e65676345f6902d9f5ded1d8265a6318d0200ce461956d6d1c70053c7fe9f9fe65e552faac03f8140d37ef0fdd108e67013a
+  languageName: node
+  linkType: hard
+
 "p-map@npm:^7.0.2":
   version: 7.0.3
   resolution: "p-map@npm:7.0.3"
@@ -3939,6 +4856,23 @@
   languageName: node
   linkType: hard
 
+"parse-json@npm:^4.0.0":
+  version: 4.0.0
+  resolution: "parse-json@npm:4.0.0"
+  dependencies:
+    error-ex: "npm:^1.3.1"
+    json-parse-better-errors: "npm:^1.0.1"
+  checksum: 10c0/8d80790b772ccb1bcea4e09e2697555e519d83d04a77c2b4237389b813f82898943a93ffff7d0d2406203bdd0c30dcf95b1661e3a53f83d0e417f053957bef32
+  languageName: node
+  linkType: hard
+
+"path-key@npm:^2.0.1":
+  version: 2.0.1
+  resolution: "path-key@npm:2.0.1"
+  checksum: 10c0/dd2044f029a8e58ac31d2bf34c34b93c3095c1481942960e84dd2faa95bbb71b9b762a106aead0646695330936414b31ca0bd862bf488a937ad17c8c5d73b32b
+  languageName: node
+  linkType: hard
+
 "path-key@npm:^3.0.0, path-key@npm:^3.1.0":
   version: 3.1.1
   resolution: "path-key@npm:3.1.1"
@@ -3946,6 +4880,13 @@
   languageName: node
   linkType: hard
 
+"path-parse@npm:^1.0.7":
+  version: 1.0.7
+  resolution: "path-parse@npm:1.0.7"
+  checksum: 10c0/11ce261f9d294cc7a58d6a574b7f1b935842355ec66fba3c3fd79e0f036462eaf07d0aa95bb74ff432f9afef97ce1926c720988c6a7451d8a584930ae7de86e1
+  languageName: node
+  linkType: hard
+
 "path-scurry@npm:^1.11.1":
   version: 1.11.1
   resolution: "path-scurry@npm:1.11.1"
@@ -3973,6 +4914,22 @@
   languageName: node
   linkType: hard
 
+"path-type@npm:^3.0.0":
+  version: 3.0.0
+  resolution: "path-type@npm:3.0.0"
+  dependencies:
+    pify: "npm:^3.0.0"
+  checksum: 10c0/1332c632f1cac15790ebab8dd729b67ba04fc96f81647496feb1c2975d862d046f41e4b975dbd893048999b2cc90721f72924ad820acc58c78507ba7141a8e56
+  languageName: node
+  linkType: hard
+
+"path-type@npm:^4.0.0":
+  version: 4.0.0
+  resolution: "path-type@npm:4.0.0"
+  checksum: 10c0/666f6973f332f27581371efaf303fd6c272cc43c2057b37aa99e3643158c7e4b2626549555d88626e99ea9e046f82f32e41bbde5f1508547e9a11b149b52387c
+  languageName: node
+  linkType: hard
+
 "pathe@npm:^1.1.2":
   version: 1.1.2
   resolution: "pathe@npm:1.1.2"
@@ -3994,6 +4951,13 @@
   languageName: node
   linkType: hard
 
+"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.3.1":
+  version: 2.3.1
+  resolution: "picomatch@npm:2.3.1"
+  checksum: 10c0/26c02b8d06f03206fc2ab8d16f19960f2ff9e81a658f831ecb656d8f17d9edc799e8364b1f4a7873e89d9702dff96204be0fa26fe4181f6843f040f819dac4be
+  languageName: node
+  linkType: hard
+
 "picomatch@npm:^4.0.2":
   version: 4.0.3
   resolution: "picomatch@npm:4.0.3"
@@ -4001,6 +4965,38 @@
   languageName: node
   linkType: hard
 
+"pidtree@npm:^0.3.0":
+  version: 0.3.1
+  resolution: "pidtree@npm:0.3.1"
+  bin:
+    pidtree: bin/pidtree.js
+  checksum: 10c0/cd69b0182f749f45ab48584e3442c48c5dc4512502c18d5b0147a33b042c41a4db4269b9ce2f7c48f11833ee5e79d81f5ebc6f7bf8372d4ea55726f60dc505a1
+  languageName: node
+  linkType: hard
+
+"pify@npm:^3.0.0":
+  version: 3.0.0
+  resolution: "pify@npm:3.0.0"
+  checksum: 10c0/fead19ed9d801f1b1fcd0638a1ac53eabbb0945bf615f2f8806a8b646565a04a1b0e7ef115c951d225f042cca388fdc1cd3add46d10d1ed6951c20bd2998af10
+  languageName: node
+  linkType: hard
+
+"plimit-lit@npm:^1.2.6":
+  version: 1.6.1
+  resolution: "plimit-lit@npm:1.6.1"
+  dependencies:
+    queue-lit: "npm:^1.5.1"
+  checksum: 10c0/af5d351bb55afe1eaa84b27c2b329699e150e4cf70464f3d474f5eabe9bdb9f48ed378ada1498d3b893f68ee7da2423ba6d9a4d88b1429d3b0aea22afcf5292b
+  languageName: node
+  linkType: hard
+
+"possible-typed-array-names@npm:^1.0.0":
+  version: 1.1.0
+  resolution: "possible-typed-array-names@npm:1.1.0"
+  checksum: 10c0/c810983414142071da1d644662ce4caebce890203eb2bc7bf119f37f3fe5796226e117e6cca146b521921fa6531072674174a3325066ac66fce089a53e1e5196
+  languageName: node
+  linkType: hard
+
 "postcss@npm:^8.5.3, postcss@npm:^8.5.6":
   version: 8.5.6
   resolution: "postcss@npm:8.5.6"
@@ -4070,6 +5066,20 @@
   languageName: node
   linkType: hard
 
+"queue-lit@npm:^1.5.1":
+  version: 1.5.2
+  resolution: "queue-lit@npm:1.5.2"
+  checksum: 10c0/8aa838b2c939aeaa6cd272b5b6b172379a3fa1d9193b2a3e687643c68c0efee3cd3493af4f1f8a11ff79b8207e4d00cc5d0b072f6e4bbeaaa27ee01f567ec4ac
+  languageName: node
+  linkType: hard
+
+"queue-microtask@npm:^1.2.2":
+  version: 1.2.3
+  resolution: "queue-microtask@npm:1.2.3"
+  checksum: 10c0/900a93d3cdae3acd7d16f642c29a642aea32c2026446151f0778c62ac089d4b8e6c986811076e1ae180a694cedf077d453a11b58ff0a865629a4f82ab558e102
+  languageName: node
+  linkType: hard
+
 "react-dom@npm:^19.1.0":
   version: 19.1.0
   resolution: "react-dom@npm:19.1.0"
@@ -4118,6 +5128,17 @@
   languageName: node
   linkType: hard
 
+"read-pkg@npm:^3.0.0":
+  version: 3.0.0
+  resolution: "read-pkg@npm:3.0.0"
+  dependencies:
+    load-json-file: "npm:^4.0.0"
+    normalize-package-data: "npm:^2.3.2"
+    path-type: "npm:^3.0.0"
+  checksum: 10c0/65acf2df89fbcd506b48b7ced56a255ba00adf7ecaa2db759c86cc58212f6fd80f1f0b7a85c848551a5d0685232e9b64f45c1fd5b48d85df2761a160767eeb93
+  languageName: node
+  linkType: hard
+
 "readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0":
   version: 3.6.2
   resolution: "readable-stream@npm:3.6.2"
@@ -4136,6 +5157,45 @@
   languageName: node
   linkType: hard
 
+"readdirp@npm:~3.6.0":
+  version: 3.6.0
+  resolution: "readdirp@npm:3.6.0"
+  dependencies:
+    picomatch: "npm:^2.2.1"
+  checksum: 10c0/6fa848cf63d1b82ab4e985f4cf72bd55b7dcfd8e0a376905804e48c3634b7e749170940ba77b32804d5fe93b3cc521aa95a8d7e7d725f830da6d93f3669ce66b
+  languageName: node
+  linkType: hard
+
+"reflect.getprototypeof@npm:^1.0.6, reflect.getprototypeof@npm:^1.0.9":
+  version: 1.0.10
+  resolution: "reflect.getprototypeof@npm:1.0.10"
+  dependencies:
+    call-bind: "npm:^1.0.8"
+    define-properties: "npm:^1.2.1"
+    es-abstract: "npm:^1.23.9"
+    es-errors: "npm:^1.3.0"
+    es-object-atoms: "npm:^1.0.0"
+    get-intrinsic: "npm:^1.2.7"
+    get-proto: "npm:^1.0.1"
+    which-builtin-type: "npm:^1.2.1"
+  checksum: 10c0/7facec28c8008876f8ab98e80b7b9cb4b1e9224353fd4756dda5f2a4ab0d30fa0a5074777c6df24e1e0af463a2697513b0a11e548d99cf52f21f7bc6ba48d3ac
+  languageName: node
+  linkType: hard
+
+"regexp.prototype.flags@npm:^1.5.4":
+  version: 1.5.4
+  resolution: "regexp.prototype.flags@npm:1.5.4"
+  dependencies:
+    call-bind: "npm:^1.0.8"
+    define-properties: "npm:^1.2.1"
+    es-errors: "npm:^1.3.0"
+    get-proto: "npm:^1.0.1"
+    gopd: "npm:^1.2.0"
+    set-function-name: "npm:^2.0.2"
+  checksum: 10c0/83b88e6115b4af1c537f8dabf5c3744032cb875d63bc05c288b1b8c0ef37cbe55353f95d8ca817e8843806e3e150b118bc624e4279b24b4776b4198232735a77
+  languageName: node
+  linkType: hard
+
 "require-directory@npm:^2.1.1":
   version: 2.1.1
   resolution: "require-directory@npm:2.1.1"
@@ -4143,6 +5203,13 @@
   languageName: node
   linkType: hard
 
+"resolve-pkg-maps@npm:^1.0.0":
+  version: 1.0.0
+  resolution: "resolve-pkg-maps@npm:1.0.0"
+  checksum: 10c0/fb8f7bbe2ca281a73b7ef423a1cbc786fb244bd7a95cbe5c3fba25b27d327150beca8ba02f622baea65919a57e061eb5005204daa5f93ed590d9b77463a567ab
+  languageName: node
+  linkType: hard
+
 "resolve.exports@npm:2.0.3":
   version: 2.0.3
   resolution: "resolve.exports@npm:2.0.3"
@@ -4150,6 +5217,32 @@
   languageName: node
   linkType: hard
 
+"resolve@npm:^1.10.0":
+  version: 1.22.10
+  resolution: "resolve@npm:1.22.10"
+  dependencies:
+    is-core-module: "npm:^2.16.0"
+    path-parse: "npm:^1.0.7"
+    supports-preserve-symlinks-flag: "npm:^1.0.0"
+  bin:
+    resolve: bin/resolve
+  checksum: 10c0/8967e1f4e2cc40f79b7e080b4582b9a8c5ee36ffb46041dccb20e6461161adf69f843b43067b4a375de926a2cd669157e29a29578191def399dd5ef89a1b5203
+  languageName: node
+  linkType: hard
+
+"resolve@patch:resolve@npm%3A^1.10.0#optional!builtin<compat/resolve>":
+  version: 1.22.10
+  resolution: "resolve@patch:resolve@npm%3A1.22.10#optional!builtin<compat/resolve>::version=1.22.10&hash=c3c19d"
+  dependencies:
+    is-core-module: "npm:^2.16.0"
+    path-parse: "npm:^1.0.7"
+    supports-preserve-symlinks-flag: "npm:^1.0.0"
+  bin:
+    resolve: bin/resolve
+  checksum: 10c0/52a4e505bbfc7925ac8f4cd91fd8c4e096b6a89728b9f46861d3b405ac9a1ccf4dcbf8befb4e89a2e11370dacd0160918163885cbc669369590f2f31f4c58939
+  languageName: node
+  linkType: hard
+
 "restore-cursor@npm:^3.1.0":
   version: 3.1.0
   resolution: "restore-cursor@npm:3.1.0"
@@ -4167,6 +5260,13 @@
   languageName: node
   linkType: hard
 
+"reusify@npm:^1.0.4":
+  version: 1.1.0
+  resolution: "reusify@npm:1.1.0"
+  checksum: 10c0/4eff0d4a5f9383566c7d7ec437b671cc51b25963bd61bf127c3f3d3f68e44a026d99b8d2f1ad344afff8d278a8fe70a8ea092650a716d22287e8bef7126bb2fa
+  languageName: node
+  linkType: hard
+
 "rimraf@npm:^6.0.1":
   version: 6.0.1
   resolution: "rimraf@npm:6.0.1"
@@ -4254,6 +5354,28 @@
   languageName: node
   linkType: hard
 
+"run-parallel@npm:^1.1.9":
+  version: 1.2.0
+  resolution: "run-parallel@npm:1.2.0"
+  dependencies:
+    queue-microtask: "npm:^1.2.2"
+  checksum: 10c0/200b5ab25b5b8b7113f9901bfe3afc347e19bb7475b267d55ad0eb86a62a46d77510cb0f232507c9e5d497ebda569a08a9867d0d14f57a82ad5564d991588b39
+  languageName: node
+  linkType: hard
+
+"safe-array-concat@npm:^1.1.3":
+  version: 1.1.3
+  resolution: "safe-array-concat@npm:1.1.3"
+  dependencies:
+    call-bind: "npm:^1.0.8"
+    call-bound: "npm:^1.0.2"
+    get-intrinsic: "npm:^1.2.6"
+    has-symbols: "npm:^1.1.0"
+    isarray: "npm:^2.0.5"
+  checksum: 10c0/43c86ffdddc461fb17ff8a17c5324f392f4868f3c7dd2c6a5d9f5971713bc5fd755667212c80eab9567595f9a7509cc2f83e590ddaebd1bd19b780f9c79f9a8d
+  languageName: node
+  linkType: hard
+
 "safe-buffer@npm:~5.2.0":
   version: 5.2.1
   resolution: "safe-buffer@npm:5.2.1"
@@ -4261,6 +5383,27 @@
   languageName: node
   linkType: hard
 
+"safe-push-apply@npm:^1.0.0":
+  version: 1.0.0
+  resolution: "safe-push-apply@npm:1.0.0"
+  dependencies:
+    es-errors: "npm:^1.3.0"
+    isarray: "npm:^2.0.5"
+  checksum: 10c0/831f1c9aae7436429e7862c7e46f847dfe490afac20d0ee61bae06108dbf5c745a0de3568ada30ccdd3eeb0864ca8331b2eef703abd69bfea0745b21fd320750
+  languageName: node
+  linkType: hard
+
+"safe-regex-test@npm:^1.1.0":
+  version: 1.1.0
+  resolution: "safe-regex-test@npm:1.1.0"
+  dependencies:
+    call-bound: "npm:^1.0.2"
+    es-errors: "npm:^1.3.0"
+    is-regex: "npm:^1.2.1"
+  checksum: 10c0/f2c25281bbe5d39cddbbce7f86fca5ea9b3ce3354ea6cd7c81c31b006a5a9fff4286acc5450a3b9122c56c33eba69c56b9131ad751457b2b4a585825e6a10665
+  languageName: node
+  linkType: hard
+
 "safer-buffer@npm:>= 2.1.2 < 3.0.0":
   version: 2.1.2
   resolution: "safer-buffer@npm:2.1.2"
@@ -4275,6 +5418,15 @@
   languageName: node
   linkType: hard
 
+"semver@npm:2 || 3 || 4 || 5, semver@npm:^5.5.0":
+  version: 5.7.2
+  resolution: "semver@npm:5.7.2"
+  bin:
+    semver: bin/semver
+  checksum: 10c0/e4cf10f86f168db772ae95d86ba65b3fd6c5967c94d97c708ccb463b778c2ee53b914cd7167620950fc07faf5a564e6efe903836639e512a1aa15fbc9667fa25
+  languageName: node
+  linkType: hard
+
 "semver@npm:^6.3.1":
   version: 6.3.1
   resolution: "semver@npm:6.3.1"
@@ -4300,6 +5452,43 @@
   languageName: node
   linkType: hard
 
+"set-function-length@npm:^1.2.2":
+  version: 1.2.2
+  resolution: "set-function-length@npm:1.2.2"
+  dependencies:
+    define-data-property: "npm:^1.1.4"
+    es-errors: "npm:^1.3.0"
+    function-bind: "npm:^1.1.2"
+    get-intrinsic: "npm:^1.2.4"
+    gopd: "npm:^1.0.1"
+    has-property-descriptors: "npm:^1.0.2"
+  checksum: 10c0/82850e62f412a258b71e123d4ed3873fa9377c216809551192bb6769329340176f109c2eeae8c22a8d386c76739855f78e8716515c818bcaef384b51110f0f3c
+  languageName: node
+  linkType: hard
+
+"set-function-name@npm:^2.0.2":
+  version: 2.0.2
+  resolution: "set-function-name@npm:2.0.2"
+  dependencies:
+    define-data-property: "npm:^1.1.4"
+    es-errors: "npm:^1.3.0"
+    functions-have-names: "npm:^1.2.3"
+    has-property-descriptors: "npm:^1.0.2"
+  checksum: 10c0/fce59f90696c450a8523e754abb305e2b8c73586452619c2bad5f7bf38c7b6b4651895c9db895679c5bef9554339cf3ef1c329b66ece3eda7255785fbe299316
+  languageName: node
+  linkType: hard
+
+"set-proto@npm:^1.0.0":
+  version: 1.0.0
+  resolution: "set-proto@npm:1.0.0"
+  dependencies:
+    dunder-proto: "npm:^1.0.1"
+    es-errors: "npm:^1.3.0"
+    es-object-atoms: "npm:^1.0.0"
+  checksum: 10c0/ca5c3ccbba479d07c30460e367e66337cec825560b11e8ba9c5ebe13a2a0d6021ae34eddf94ff3dfe17a3104dc1f191519cb6c48378b503e5c3f36393938776a
+  languageName: node
+  linkType: hard
+
 "sharp@npm:^0.33.5":
   version: 0.33.5
   resolution: "sharp@npm:0.33.5"
@@ -4369,6 +5558,15 @@
   languageName: node
   linkType: hard
 
+"shebang-command@npm:^1.2.0":
+  version: 1.2.0
+  resolution: "shebang-command@npm:1.2.0"
+  dependencies:
+    shebang-regex: "npm:^1.0.0"
+  checksum: 10c0/7b20dbf04112c456b7fc258622dafd566553184ac9b6938dd30b943b065b21dabd3776460df534cc02480db5e1b6aec44700d985153a3da46e7db7f9bd21326d
+  languageName: node
+  linkType: hard
+
 "shebang-command@npm:^2.0.0":
   version: 2.0.0
   resolution: "shebang-command@npm:2.0.0"
@@ -4378,6 +5576,13 @@
   languageName: node
   linkType: hard
 
+"shebang-regex@npm:^1.0.0":
+  version: 1.0.0
+  resolution: "shebang-regex@npm:1.0.0"
+  checksum: 10c0/9abc45dee35f554ae9453098a13fdc2f1730e525a5eb33c51f096cc31f6f10a4b38074c1ebf354ae7bffa7229506083844008dfc3bb7818228568c0b2dc1fff2
+  languageName: node
+  linkType: hard
+
 "shebang-regex@npm:^3.0.0":
   version: 3.0.0
   resolution: "shebang-regex@npm:3.0.0"
@@ -4385,6 +5590,61 @@
   languageName: node
   linkType: hard
 
+"shell-quote@npm:^1.6.1":
+  version: 1.8.3
+  resolution: "shell-quote@npm:1.8.3"
+  checksum: 10c0/bee87c34e1e986cfb4c30846b8e6327d18874f10b535699866f368ade11ea4ee45433d97bf5eada22c4320c27df79c3a6a7eb1bf3ecfc47f2c997d9e5e2672fd
+  languageName: node
+  linkType: hard
+
+"side-channel-list@npm:^1.0.0":
+  version: 1.0.0
+  resolution: "side-channel-list@npm:1.0.0"
+  dependencies:
+    es-errors: "npm:^1.3.0"
+    object-inspect: "npm:^1.13.3"
+  checksum: 10c0/644f4ac893456c9490ff388bf78aea9d333d5e5bfc64cfb84be8f04bf31ddc111a8d4b83b85d7e7e8a7b845bc185a9ad02c052d20e086983cf59f0be517d9b3d
+  languageName: node
+  linkType: hard
+
+"side-channel-map@npm:^1.0.1":
+  version: 1.0.1
+  resolution: "side-channel-map@npm:1.0.1"
+  dependencies:
+    call-bound: "npm:^1.0.2"
+    es-errors: "npm:^1.3.0"
+    get-intrinsic: "npm:^1.2.5"
+    object-inspect: "npm:^1.13.3"
+  checksum: 10c0/010584e6444dd8a20b85bc926d934424bd809e1a3af941cace229f7fdcb751aada0fb7164f60c2e22292b7fa3c0ff0bce237081fd4cdbc80de1dc68e95430672
+  languageName: node
+  linkType: hard
+
+"side-channel-weakmap@npm:^1.0.2":
+  version: 1.0.2
+  resolution: "side-channel-weakmap@npm:1.0.2"
+  dependencies:
+    call-bound: "npm:^1.0.2"
+    es-errors: "npm:^1.3.0"
+    get-intrinsic: "npm:^1.2.5"
+    object-inspect: "npm:^1.13.3"
+    side-channel-map: "npm:^1.0.1"
+  checksum: 10c0/71362709ac233e08807ccd980101c3e2d7efe849edc51455030327b059f6c4d292c237f94dc0685031dd11c07dd17a68afde235d6cf2102d949567f98ab58185
+  languageName: node
+  linkType: hard
+
+"side-channel@npm:^1.1.0":
+  version: 1.1.0
+  resolution: "side-channel@npm:1.1.0"
+  dependencies:
+    es-errors: "npm:^1.3.0"
+    object-inspect: "npm:^1.13.3"
+    side-channel-list: "npm:^1.0.0"
+    side-channel-map: "npm:^1.0.1"
+    side-channel-weakmap: "npm:^1.0.2"
+  checksum: 10c0/cb20dad41eb032e6c24c0982e1e5a24963a28aa6122b4f05b3f3d6bf8ae7fd5474ef382c8f54a6a3ab86e0cac4d41a23bd64ede3970e5bfb50326ba02a7996e6
+  languageName: node
+  linkType: hard
+
 "signal-exit@npm:^3.0.2":
   version: 3.0.7
   resolution: "signal-exit@npm:3.0.7"
@@ -4408,6 +5668,13 @@
   languageName: node
   linkType: hard
 
+"slash@npm:^3.0.0":
+  version: 3.0.0
+  resolution: "slash@npm:3.0.0"
+  checksum: 10c0/e18488c6a42bdfd4ac5be85b2ced3ccd0224773baae6ad42cfbb9ec74fc07f9fa8396bd35ee638084ead7a2a0818eb5e7151111544d4731ce843019dab4be47b
+  languageName: node
+  linkType: hard
+
 "smart-buffer@npm:^4.2.0":
   version: 4.2.0
   resolution: "smart-buffer@npm:4.2.0"
@@ -4500,6 +5767,16 @@
   languageName: node
   linkType: hard
 
+"stop-iteration-iterator@npm:^1.1.0":
+  version: 1.1.0
+  resolution: "stop-iteration-iterator@npm:1.1.0"
+  dependencies:
+    es-errors: "npm:^1.3.0"
+    internal-slot: "npm:^1.1.0"
+  checksum: 10c0/de4e45706bb4c0354a4b1122a2b8cc45a639e86206807ce0baf390ee9218d3ef181923fa4d2b67443367c491aa255c5fbaa64bb74648e3c5b48299928af86c09
+  languageName: node
+  linkType: hard
+
 "stoppable@npm:1.1.0":
   version: 1.1.0
   resolution: "stoppable@npm:1.1.0"
@@ -4529,6 +5806,56 @@
   languageName: node
   linkType: hard
 
+"string.prototype.padend@npm:^3.0.0":
+  version: 3.1.6
+  resolution: "string.prototype.padend@npm:3.1.6"
+  dependencies:
+    call-bind: "npm:^1.0.7"
+    define-properties: "npm:^1.2.1"
+    es-abstract: "npm:^1.23.2"
+    es-object-atoms: "npm:^1.0.0"
+  checksum: 10c0/8f2c8c1f3db1efcdc210668c80c87f2cea1253d6029ff296a172b5e13edc9adebeed4942d023de8d31f9b13b69f3f5d73de7141959b1f09817fba5f527e83be1
+  languageName: node
+  linkType: hard
+
+"string.prototype.trim@npm:^1.2.10":
+  version: 1.2.10
+  resolution: "string.prototype.trim@npm:1.2.10"
+  dependencies:
+    call-bind: "npm:^1.0.8"
+    call-bound: "npm:^1.0.2"
+    define-data-property: "npm:^1.1.4"
+    define-properties: "npm:^1.2.1"
+    es-abstract: "npm:^1.23.5"
+    es-object-atoms: "npm:^1.0.0"
+    has-property-descriptors: "npm:^1.0.2"
+  checksum: 10c0/8a8854241c4b54a948e992eb7dd6b8b3a97185112deb0037a134f5ba57541d8248dd610c966311887b6c2fd1181a3877bffb14d873ce937a344535dabcc648f8
+  languageName: node
+  linkType: hard
+
+"string.prototype.trimend@npm:^1.0.9":
+  version: 1.0.9
+  resolution: "string.prototype.trimend@npm:1.0.9"
+  dependencies:
+    call-bind: "npm:^1.0.8"
+    call-bound: "npm:^1.0.2"
+    define-properties: "npm:^1.2.1"
+    es-object-atoms: "npm:^1.0.0"
+  checksum: 10c0/59e1a70bf9414cb4c536a6e31bef5553c8ceb0cf44d8b4d0ed65c9653358d1c64dd0ec203b100df83d0413bbcde38b8c5d49e14bc4b86737d74adc593a0d35b6
+  languageName: node
+  linkType: hard
+
+"string.prototype.trimstart@npm:^1.0.8":
+  version: 1.0.8
+  resolution: "string.prototype.trimstart@npm:1.0.8"
+  dependencies:
+    call-bind: "npm:^1.0.7"
+    define-properties: "npm:^1.2.1"
+    es-object-atoms: "npm:^1.0.0"
+  checksum: 10c0/d53af1899959e53c83b64a5fd120be93e067da740e7e75acb433849aa640782fb6c7d4cd5b84c954c84413745a3764df135a8afeb22908b86a835290788d8366
+  languageName: node
+  linkType: hard
+
 "string_decoder@npm:^1.1.1":
   version: 1.3.0
   resolution: "string_decoder@npm:1.3.0"
@@ -4570,6 +5897,15 @@
   languageName: node
   linkType: hard
 
+"supports-color@npm:^5.3.0":
+  version: 5.5.0
+  resolution: "supports-color@npm:5.5.0"
+  dependencies:
+    has-flag: "npm:^3.0.0"
+  checksum: 10c0/6ae5ff319bfbb021f8a86da8ea1f8db52fac8bd4d499492e30ec17095b58af11f0c55f8577390a749b1c4dde691b6a0315dab78f5f54c9b3d83f8fb5905c1c05
+  languageName: node
+  linkType: hard
+
 "supports-color@npm:^7.1.0":
   version: 7.2.0
   resolution: "supports-color@npm:7.2.0"
@@ -4579,6 +5915,13 @@
   languageName: node
   linkType: hard
 
+"supports-preserve-symlinks-flag@npm:^1.0.0":
+  version: 1.0.0
+  resolution: "supports-preserve-symlinks-flag@npm:1.0.0"
+  checksum: 10c0/6c4032340701a9950865f7ae8ef38578d8d7053f5e10518076e6554a9381fa91bd9c6850193695c141f32b21f979c985db07265a758867bac95de05f7d8aeb39
+  languageName: node
+  linkType: hard
+
 "tailwindcss@npm:4.1.11, tailwindcss@npm:^4.1.4":
   version: 4.1.11
   resolution: "tailwindcss@npm:4.1.11"
@@ -4637,6 +5980,15 @@
   languageName: node
   linkType: hard
 
+"to-regex-range@npm:^5.0.1":
+  version: 5.0.1
+  resolution: "to-regex-range@npm:5.0.1"
+  dependencies:
+    is-number: "npm:^7.0.0"
+  checksum: 10c0/487988b0a19c654ff3e1961b87f471702e708fa8a8dd02a298ef16da7206692e8552a0250e8b3e8759270f62e9d8314616f6da274734d3b558b1fc7b7724e892
+  languageName: node
+  linkType: hard
+
 "tree-kill@npm:^1.2.2":
   version: 1.2.2
   resolution: "tree-kill@npm:1.2.2"
@@ -4646,6 +5998,23 @@
   languageName: node
   linkType: hard
 
+"tsc-alias@npm:^1.8.16":
+  version: 1.8.16
+  resolution: "tsc-alias@npm:1.8.16"
+  dependencies:
+    chokidar: "npm:^3.5.3"
+    commander: "npm:^9.0.0"
+    get-tsconfig: "npm:^4.10.0"
+    globby: "npm:^11.0.4"
+    mylas: "npm:^2.1.9"
+    normalize-path: "npm:^3.0.0"
+    plimit-lit: "npm:^1.2.6"
+  bin:
+    tsc-alias: dist/bin/index.js
+  checksum: 10c0/5775a6044bd5b6e94efdf1902493aa959270def65e7915edad78023fac7f42f25724842bd98f38a5d00e01f7395dca102a6615933bec3bdd887617d00419f66a
+  languageName: node
+  linkType: hard
+
 "tsconfck@npm:^3.0.3":
   version: 3.1.6
   resolution: "tsconfck@npm:3.1.6"
@@ -4678,6 +6047,59 @@
   languageName: node
   linkType: hard
 
+"typed-array-buffer@npm:^1.0.3":
+  version: 1.0.3
+  resolution: "typed-array-buffer@npm:1.0.3"
+  dependencies:
+    call-bound: "npm:^1.0.3"
+    es-errors: "npm:^1.3.0"
+    is-typed-array: "npm:^1.1.14"
+  checksum: 10c0/1105071756eb248774bc71646bfe45b682efcad93b55532c6ffa4518969fb6241354e4aa62af679ae83899ec296d69ef88f1f3763657cdb3a4d29321f7b83079
+  languageName: node
+  linkType: hard
+
+"typed-array-byte-length@npm:^1.0.3":
+  version: 1.0.3
+  resolution: "typed-array-byte-length@npm:1.0.3"
+  dependencies:
+    call-bind: "npm:^1.0.8"
+    for-each: "npm:^0.3.3"
+    gopd: "npm:^1.2.0"
+    has-proto: "npm:^1.2.0"
+    is-typed-array: "npm:^1.1.14"
+  checksum: 10c0/6ae083c6f0354f1fce18b90b243343b9982affd8d839c57bbd2c174a5d5dc71be9eb7019ffd12628a96a4815e7afa85d718d6f1e758615151d5f35df841ffb3e
+  languageName: node
+  linkType: hard
+
+"typed-array-byte-offset@npm:^1.0.4":
+  version: 1.0.4
+  resolution: "typed-array-byte-offset@npm:1.0.4"
+  dependencies:
+    available-typed-arrays: "npm:^1.0.7"
+    call-bind: "npm:^1.0.8"
+    for-each: "npm:^0.3.3"
+    gopd: "npm:^1.2.0"
+    has-proto: "npm:^1.2.0"
+    is-typed-array: "npm:^1.1.15"
+    reflect.getprototypeof: "npm:^1.0.9"
+  checksum: 10c0/3d805b050c0c33b51719ee52de17c1cd8e6a571abdf0fffb110e45e8dd87a657e8b56eee94b776b13006d3d347a0c18a730b903cf05293ab6d92e99ff8f77e53
+  languageName: node
+  linkType: hard
+
+"typed-array-length@npm:^1.0.7":
+  version: 1.0.7
+  resolution: "typed-array-length@npm:1.0.7"
+  dependencies:
+    call-bind: "npm:^1.0.7"
+    for-each: "npm:^0.3.3"
+    gopd: "npm:^1.0.1"
+    is-typed-array: "npm:^1.1.13"
+    possible-typed-array-names: "npm:^1.0.0"
+    reflect.getprototypeof: "npm:^1.0.6"
+  checksum: 10c0/e38f2ae3779584c138a2d8adfa8ecf749f494af3cd3cdafe4e688ce51418c7d2c5c88df1bd6be2bbea099c3f7cea58c02ca02ed438119e91f162a9de23f61295
+  languageName: node
+  linkType: hard
+
 "typescript@npm:^5.8.3":
   version: 5.8.3
   resolution: "typescript@npm:5.8.3"
@@ -4705,6 +6127,18 @@
   languageName: node
   linkType: hard
 
+"unbox-primitive@npm:^1.1.0":
+  version: 1.1.0
+  resolution: "unbox-primitive@npm:1.1.0"
+  dependencies:
+    call-bound: "npm:^1.0.3"
+    has-bigints: "npm:^1.0.2"
+    has-symbols: "npm:^1.1.0"
+    which-boxed-primitive: "npm:^1.1.1"
+  checksum: 10c0/7dbd35ab02b0e05fe07136c72cb9355091242455473ec15057c11430129bab38b7b3624019b8778d02a881c13de44d63cd02d122ee782fb519e1de7775b5b982
+  languageName: node
+  linkType: hard
+
 "undici-types@npm:~6.21.0":
   version: 6.21.0
   resolution: "undici-types@npm:6.21.0"
@@ -4783,7 +6217,7 @@
   languageName: node
   linkType: hard
 
-"validate-npm-package-license@npm:^3.0.4":
+"validate-npm-package-license@npm:^3.0.1, validate-npm-package-license@npm:^3.0.4":
   version: 3.0.4
   resolution: "validate-npm-package-license@npm:3.0.4"
   dependencies:
@@ -4950,6 +6384,78 @@
   languageName: node
   linkType: hard
 
+"which-boxed-primitive@npm:^1.1.0, which-boxed-primitive@npm:^1.1.1":
+  version: 1.1.1
+  resolution: "which-boxed-primitive@npm:1.1.1"
+  dependencies:
+    is-bigint: "npm:^1.1.0"
+    is-boolean-object: "npm:^1.2.1"
+    is-number-object: "npm:^1.1.1"
+    is-string: "npm:^1.1.1"
+    is-symbol: "npm:^1.1.1"
+  checksum: 10c0/aceea8ede3b08dede7dce168f3883323f7c62272b49801716e8332ff750e7ae59a511ae088840bc6874f16c1b7fd296c05c949b0e5b357bfe3c431b98c417abe
+  languageName: node
+  linkType: hard
+
+"which-builtin-type@npm:^1.2.1":
+  version: 1.2.1
+  resolution: "which-builtin-type@npm:1.2.1"
+  dependencies:
+    call-bound: "npm:^1.0.2"
+    function.prototype.name: "npm:^1.1.6"
+    has-tostringtag: "npm:^1.0.2"
+    is-async-function: "npm:^2.0.0"
+    is-date-object: "npm:^1.1.0"
+    is-finalizationregistry: "npm:^1.1.0"
+    is-generator-function: "npm:^1.0.10"
+    is-regex: "npm:^1.2.1"
+    is-weakref: "npm:^1.0.2"
+    isarray: "npm:^2.0.5"
+    which-boxed-primitive: "npm:^1.1.0"
+    which-collection: "npm:^1.0.2"
+    which-typed-array: "npm:^1.1.16"
+  checksum: 10c0/8dcf323c45e5c27887800df42fbe0431d0b66b1163849bb7d46b5a730ad6a96ee8bfe827d078303f825537844ebf20c02459de41239a0a9805e2fcb3cae0d471
+  languageName: node
+  linkType: hard
+
+"which-collection@npm:^1.0.2":
+  version: 1.0.2
+  resolution: "which-collection@npm:1.0.2"
+  dependencies:
+    is-map: "npm:^2.0.3"
+    is-set: "npm:^2.0.3"
+    is-weakmap: "npm:^2.0.2"
+    is-weakset: "npm:^2.0.3"
+  checksum: 10c0/3345fde20964525a04cdf7c4a96821f85f0cc198f1b2ecb4576e08096746d129eb133571998fe121c77782ac8f21cbd67745a3d35ce100d26d4e684c142ea1f2
+  languageName: node
+  linkType: hard
+
+"which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.19":
+  version: 1.1.19
+  resolution: "which-typed-array@npm:1.1.19"
+  dependencies:
+    available-typed-arrays: "npm:^1.0.7"
+    call-bind: "npm:^1.0.8"
+    call-bound: "npm:^1.0.4"
+    for-each: "npm:^0.3.5"
+    get-proto: "npm:^1.0.1"
+    gopd: "npm:^1.2.0"
+    has-tostringtag: "npm:^1.0.2"
+  checksum: 10c0/702b5dc878addafe6c6300c3d0af5983b175c75fcb4f2a72dfc3dd38d93cf9e89581e4b29c854b16ea37e50a7d7fca5ae42ece5c273d8060dcd603b2404bbb3f
+  languageName: node
+  linkType: hard
+
+"which@npm:^1.2.9":
+  version: 1.3.1
+  resolution: "which@npm:1.3.1"
+  dependencies:
+    isexe: "npm:^2.0.0"
+  bin:
+    which: ./bin/which
+  checksum: 10c0/e945a8b6bbf6821aaaef7f6e0c309d4b615ef35699576d5489b4261da9539f70393c6b2ce700ee4321c18f914ebe5644bc4631b15466ffbaad37d83151f6af59
+  languageName: node
+  linkType: hard
+
 "which@npm:^2.0.1":
   version: 2.0.2
   resolution: "which@npm:2.0.2"