diff --git a/api/config.ts b/api/config.ts
index 79604a5..7cfec2a 100644
--- a/api/config.ts
+++ b/api/config.ts
@@ -1,4 +1,4 @@
-import { getEnvironmentVariable } from "@platform/config/environment.ts";
+import { getEnvironmentVariable } from "@platform/config";
import z from "zod";
export const config = {
diff --git a/api/package.json b/api/package.json
index 4aa209c..9cd602a 100644
--- a/api/package.json
+++ b/api/package.json
@@ -6,6 +6,7 @@
"dependencies": {
"@modules/identity": "workspace:*",
"@module/workspace": "workspace:*",
- "zod": "4.1.11"
+ "@platform/config": "workspace:*",
+ "zod": "4.1.12"
}
}
diff --git a/api/server.ts b/api/server.ts
index 8c67b78..060ded3 100644
--- a/api/server.ts
+++ b/api/server.ts
@@ -1,5 +1,3 @@
-import identity from "@modules/identity/server.ts";
-import workspace from "@modules/workspace/server.ts";
import { logger } from "@platform/logger";
import { context } from "@platform/relay";
import { Api } from "@platform/server/api.ts";
@@ -26,7 +24,7 @@ await session.bootstrap();
// ### Modules
-await workspace.bootstrap();
+// await workspace.bootstrap();
/*
|--------------------------------------------------------------------------------
@@ -34,7 +32,9 @@ await workspace.bootstrap();
|--------------------------------------------------------------------------------
*/
-const api = new Api([...identity.routes, ...workspace.routes]);
+const api = new Api([
+ /*...identity.routes, ...workspace.routes*/
+]);
/*
|--------------------------------------------------------------------------------
diff --git a/api/session.ts b/api/session.ts
index 0c1f647..bba990d 100644
--- a/api/session.ts
+++ b/api/session.ts
@@ -1,5 +1,3 @@
-import { identity } from "@modules/iam/client.ts";
-import { getPrincipalSession } from "@modules/identity/server.ts";
import { context, UnauthorizedError } from "@platform/relay";
import { storage } from "@platform/storage";
diff --git a/apps/react/components.json b/apps/react/components.json
new file mode 100644
index 0000000..c3a0a52
--- /dev/null
+++ b/apps/react/components.json
@@ -0,0 +1,22 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.js",
+ "css": "src/index.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "iconLibrary": "lucide",
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/libraries/utils",
+ "ui": "@/components/ui",
+ "lib": "@/libraries",
+ "hooks": "@/hooks"
+ },
+ "registries": {}
+}
diff --git a/apps/react/package.json b/apps/react/package.json
index 256152c..396ec5d 100644
--- a/apps/react/package.json
+++ b/apps/react/package.json
@@ -10,17 +10,33 @@
"preview": "vite preview"
},
"dependencies": {
+ "@module/account": "workspace:*",
"@platform/relay": "workspace:*",
"@platform/spec": "workspace:*",
+ "@radix-ui/react-avatar": "^1.1.11",
+ "@radix-ui/react-dialog": "^1.1.15",
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
+ "@radix-ui/react-scroll-area": "^1.2.10",
+ "@radix-ui/react-separator": "^1.1.8",
+ "@radix-ui/react-slot": "^1.2.4",
+ "@radix-ui/react-tooltip": "^1.2.8",
+ "@tabler/icons-react": "3.35.0",
"@tanstack/react-query": "5.89.0",
"@tanstack/react-router": "1.131.47",
"@valkyr/db": "npm:@jsr/valkyr__db@2.0.0",
"@valkyr/event-emitter": "npm:@jsr/valkyr__event-emitter@1.0.1",
+ "@zitadel/react": "1.1.0",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
"fast-equals": "5.2.2",
+ "lucide-react": "^0.554.0",
"react": "19.1.1",
"react-dom": "19.1.1",
+ "tailwind-merge": "^3.4.0",
"tailwindcss": "4.1.13",
- "zod": "4.1.11"
+ "tailwindcss-animate": "^1.0.7",
+ "tw-animate-css": "1.4.0",
+ "zod": "4.1.12"
},
"devDependencies": {
"@eslint/js": "9.35.0",
diff --git a/apps/react/src/components/Session.tsx b/apps/react/src/components/Session.tsx
deleted file mode 100644
index efcf127..0000000
--- a/apps/react/src/components/Session.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import { makeControllerView } from "../libraries/view.ts";
-import { SessionController } from "./session.controller.ts";
-
-export const Session = makeControllerView(SessionController, ({ state: { error } }) => {
- if (error !== undefined) {
- return "Failed to fetch session";
- }
- return
Session OK!
;
-});
diff --git a/apps/react/src/components/app-sidebar.tsx b/apps/react/src/components/app-sidebar.tsx
new file mode 100644
index 0000000..3952ec0
--- /dev/null
+++ b/apps/react/src/components/app-sidebar.tsx
@@ -0,0 +1,178 @@
+"use client";
+
+import {
+ IconCamera,
+ IconChartBar,
+ IconDashboard,
+ IconDatabase,
+ IconFileAi,
+ IconFileDescription,
+ IconFileWord,
+ IconFolder,
+ IconHelp,
+ IconInnerShadowTop,
+ IconListDetails,
+ IconReport,
+ IconSearch,
+ IconSettings,
+ IconUsers,
+} from "@tabler/icons-react";
+import type * as React from "react";
+
+import { NavDocuments } from "@/components/nav-documents";
+import { NavMain } from "@/components/nav-main";
+import { NavSecondary } from "@/components/nav-secondary";
+import { NavUser } from "@/components/nav-user";
+import {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarHeader,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+} from "@/components/ui/sidebar";
+
+const data = {
+ user: {
+ name: "shadcn",
+ email: "m@example.com",
+ avatar: "/avatars/shadcn.jpg",
+ },
+ navMain: [
+ {
+ title: "Dashboard",
+ url: "#",
+ icon: IconDashboard,
+ },
+ {
+ title: "Lifecycle",
+ url: "#",
+ icon: IconListDetails,
+ },
+ {
+ title: "Analytics",
+ url: "#",
+ icon: IconChartBar,
+ },
+ {
+ title: "Projects",
+ url: "#",
+ icon: IconFolder,
+ },
+ {
+ title: "Team",
+ url: "#",
+ icon: IconUsers,
+ },
+ ],
+ navClouds: [
+ {
+ title: "Capture",
+ icon: IconCamera,
+ isActive: true,
+ url: "#",
+ items: [
+ {
+ title: "Active Proposals",
+ url: "#",
+ },
+ {
+ title: "Archived",
+ url: "#",
+ },
+ ],
+ },
+ {
+ title: "Proposal",
+ icon: IconFileDescription,
+ url: "#",
+ items: [
+ {
+ title: "Active Proposals",
+ url: "#",
+ },
+ {
+ title: "Archived",
+ url: "#",
+ },
+ ],
+ },
+ {
+ title: "Prompts",
+ icon: IconFileAi,
+ url: "#",
+ items: [
+ {
+ title: "Active Proposals",
+ url: "#",
+ },
+ {
+ title: "Archived",
+ url: "#",
+ },
+ ],
+ },
+ ],
+ navSecondary: [
+ {
+ title: "Settings",
+ url: "#",
+ icon: IconSettings,
+ },
+ {
+ title: "Get Help",
+ url: "#",
+ icon: IconHelp,
+ },
+ {
+ title: "Search",
+ url: "#",
+ icon: IconSearch,
+ },
+ ],
+ documents: [
+ {
+ name: "Data Library",
+ url: "#",
+ icon: IconDatabase,
+ },
+ {
+ name: "Reports",
+ url: "#",
+ icon: IconReport,
+ },
+ {
+ name: "Word Assistant",
+ url: "#",
+ icon: IconFileWord,
+ },
+ ],
+};
+
+export function AppSidebar({ ...props }: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+
+ Acme Inc.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/react/src/components/nav-documents.tsx b/apps/react/src/components/nav-documents.tsx
new file mode 100644
index 0000000..972a543
--- /dev/null
+++ b/apps/react/src/components/nav-documents.tsx
@@ -0,0 +1,83 @@
+"use client";
+
+import { type Icon, IconDots, IconFolder, IconShare3, IconTrash } from "@tabler/icons-react";
+
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import {
+ SidebarGroup,
+ SidebarGroupLabel,
+ SidebarMenu,
+ SidebarMenuAction,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ useSidebar,
+} from "@/components/ui/sidebar";
+
+export function NavDocuments({
+ items,
+}: {
+ items: {
+ name: string;
+ url: string;
+ icon: Icon;
+ }[];
+}) {
+ const { isMobile } = useSidebar();
+
+ return (
+
+ Documents
+
+ {items.map((item) => (
+
+
+
+
+ {item.name}
+
+
+
+
+
+
+ More
+
+
+
+
+
+ Open
+
+
+
+ Share
+
+
+
+
+ Delete
+
+
+
+
+ ))}
+
+
+
+ More
+
+
+
+
+ );
+}
diff --git a/apps/react/src/components/nav-main.tsx b/apps/react/src/components/nav-main.tsx
new file mode 100644
index 0000000..ac0332f
--- /dev/null
+++ b/apps/react/src/components/nav-main.tsx
@@ -0,0 +1,54 @@
+"use client";
+
+import { type Icon, IconCirclePlusFilled, IconMail } from "@tabler/icons-react";
+
+import { Button } from "@/components/ui/button";
+import {
+ SidebarGroup,
+ SidebarGroupContent,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+} from "@/components/ui/sidebar";
+
+export function NavMain({
+ items,
+}: {
+ items: {
+ title: string;
+ url: string;
+ icon?: Icon;
+ }[];
+}) {
+ return (
+
+
+
+
+
+
+ Quick Create
+
+
+
+
+
+ {items.map((item) => (
+
+
+ {item.icon && }
+ {item.title}
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/apps/react/src/components/nav-secondary.tsx b/apps/react/src/components/nav-secondary.tsx
new file mode 100644
index 0000000..7690c2d
--- /dev/null
+++ b/apps/react/src/components/nav-secondary.tsx
@@ -0,0 +1,42 @@
+"use client";
+
+import type { Icon } from "@tabler/icons-react";
+import type * as React from "react";
+
+import {
+ SidebarGroup,
+ SidebarGroupContent,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+} from "@/components/ui/sidebar";
+
+export function NavSecondary({
+ items,
+ ...props
+}: {
+ items: {
+ title: string;
+ url: string;
+ icon: Icon;
+ }[];
+} & React.ComponentPropsWithoutRef) {
+ return (
+
+
+
+ {items.map((item) => (
+
+
+
+
+ {item.title}
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/apps/react/src/components/nav-user.controller.ts b/apps/react/src/components/nav-user.controller.ts
new file mode 100644
index 0000000..9735a71
--- /dev/null
+++ b/apps/react/src/components/nav-user.controller.ts
@@ -0,0 +1,51 @@
+import { Controller } from "../libraries/controller.ts";
+import { type User as ZitadelUser, zitadel } from "../services/zitadel.ts";
+
+export class NavUserController extends Controller<{
+ user?: User;
+}> {
+ async onInit() {
+ return {
+ user: await this.#getAuthenticatedUser(),
+ };
+ }
+
+ async #getAuthenticatedUser(): Promise {
+ const user = await zitadel.userManager.getUser();
+ if (user !== null) {
+ return getUserProfile(user);
+ }
+ }
+
+ authorize() {
+ zitadel.authorize();
+ }
+
+ signout() {
+ zitadel.signout();
+ }
+}
+
+function getUserProfile({ profile }: ZitadelUser): User {
+ const user: User = { name: "Unknown", email: "unknown@acme.none", avatar: "" };
+ if (profile.name) {
+ user.name = profile.name;
+ } else if (profile.given_name && profile.family_name) {
+ user.name = `${profile.given_name} ${profile.family_name}`;
+ } else if (profile.given_name) {
+ user.name = profile.given_name;
+ }
+ if (profile.email) {
+ user.email = profile.email;
+ }
+ if (profile.picture !== undefined) {
+ user.avatar = profile.picture;
+ }
+ return user;
+}
+
+type User = {
+ name: string;
+ email: string;
+ avatar: string;
+};
diff --git a/apps/react/src/components/nav-user.tsx b/apps/react/src/components/nav-user.tsx
new file mode 100644
index 0000000..80fefc8
--- /dev/null
+++ b/apps/react/src/components/nav-user.tsx
@@ -0,0 +1,140 @@
+"use client";
+
+import { IconCreditCard, IconDotsVertical, IconLogout, IconNotification, IconUserCircle } from "@tabler/icons-react";
+
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from "@/components/ui/sidebar";
+import { useController } from "@/libraries/controller.ts";
+
+import { NavUserController } from "./nav-user.controller.ts";
+
+export function NavUser() {
+ const [{ user }, loading, { authorize, signout }] = useController(NavUserController);
+ const { isMobile } = useSidebar();
+
+ console.log({authorize})
+
+ if (loading === true || user === undefined) {
+ return (
+
+
+
+
+
+
+
+ CN
+
+
+ Guest
+ guest@fixture.none
+
+
+
+
+
+
+
+
+
+ CN
+
+
+ Guest
+ guest@fixture.none
+
+
+
+
+ authorize()}>
+
+ Sign in
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+ CN
+
+
+ {user.name}
+ {user.email}
+
+
+
+
+
+
+
+
+
+ CN
+
+
+ {user.name}
+ {user.email}
+
+
+
+
+
+
+
+ Account
+
+
+
+ Billing
+
+
+
+ Notifications
+
+
+
+ signout()}>
+
+ Log out
+
+
+
+
+
+ );
+}
diff --git a/apps/react/src/components/session.controller.ts b/apps/react/src/components/session.controller.ts
deleted file mode 100644
index 1de21e9..0000000
--- a/apps/react/src/components/session.controller.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { Controller } from "../libraries/controller.ts";
-import { api } from "../services/api.ts";
-
-export class SessionController extends Controller<{
- error?: string;
-}> {
- async onInit() {
- await this.getSessionCookie();
- }
-
- async getSessionCookie() {
- const response = await api.auth.authenticate({
- body: {
- type: "email",
- email: "john.doe@fixture.none",
- },
- });
- if ("error" in response) {
- this.setState("error", undefined);
- }
- }
-}
diff --git a/apps/react/src/components/site-header.tsx b/apps/react/src/components/site-header.tsx
new file mode 100644
index 0000000..c21931f
--- /dev/null
+++ b/apps/react/src/components/site-header.tsx
@@ -0,0 +1,27 @@
+import { Button } from "@/components/ui/button";
+import { Separator } from "@/components/ui/separator";
+import { SidebarTrigger } from "@/components/ui/sidebar";
+
+export function SiteHeader() {
+ return (
+
+ );
+}
diff --git a/apps/react/src/components/theme-provider.tsx b/apps/react/src/components/theme-provider.tsx
new file mode 100644
index 0000000..4d676bd
--- /dev/null
+++ b/apps/react/src/components/theme-provider.tsx
@@ -0,0 +1,67 @@
+import { createContext, useContext, useEffect, useState } from "react";
+
+type Theme = "dark" | "light" | "system";
+
+type ThemeProviderProps = {
+ children: React.ReactNode;
+ defaultTheme?: Theme;
+ storageKey?: string;
+};
+
+type ThemeProviderState = {
+ theme: Theme;
+ setTheme: (theme: Theme) => void;
+};
+
+const initialState: ThemeProviderState = {
+ theme: "system",
+ setTheme: () => null,
+};
+
+const ThemeProviderContext = createContext(initialState);
+
+export function ThemeProvider({
+ children,
+ defaultTheme = "system",
+ storageKey = "vite-ui-theme",
+ ...props
+}: ThemeProviderProps) {
+ const [theme, setTheme] = useState(() => (localStorage.getItem(storageKey) as Theme) || defaultTheme);
+
+ useEffect(() => {
+ const root = window.document.documentElement;
+
+ root.classList.remove("light", "dark");
+
+ if (theme === "system") {
+ const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
+
+ root.classList.add(systemTheme);
+ return;
+ }
+
+ root.classList.add(theme);
+ }, [theme]);
+
+ const value = {
+ theme,
+ setTheme: (theme: Theme) => {
+ localStorage.setItem(storageKey, theme);
+ setTheme(theme);
+ },
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export const useTheme = () => {
+ const context = useContext(ThemeProviderContext);
+
+ if (context === undefined) throw new Error("useTheme must be used within a ThemeProvider");
+
+ return context;
+};
diff --git a/apps/react/src/components/ui/avatar.tsx b/apps/react/src/components/ui/avatar.tsx
new file mode 100644
index 0000000..35db151
--- /dev/null
+++ b/apps/react/src/components/ui/avatar.tsx
@@ -0,0 +1,38 @@
+import * as AvatarPrimitive from "@radix-ui/react-avatar";
+import * as React from "react";
+
+import { cn } from "../../libraries/utils.ts";
+
+const Avatar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+Avatar.displayName = AvatarPrimitive.Root.displayName;
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AvatarImage.displayName = AvatarPrimitive.Image.displayName;
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
+
+export { Avatar, AvatarImage, AvatarFallback };
diff --git a/apps/react/src/components/ui/button.tsx b/apps/react/src/components/ui/button.tsx
new file mode 100644
index 0000000..f4f67f1
--- /dev/null
+++ b/apps/react/src/components/ui/button.tsx
@@ -0,0 +1,47 @@
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+import * as React from "react";
+
+import { cn } from "../../libraries/utils.ts";
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
+ destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
+ outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
+ secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-9 px-4 py-2",
+ sm: "h-8 rounded-md px-3 text-xs",
+ lg: "h-10 rounded-md px-8",
+ icon: "h-9 w-9",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
+);
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean;
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button";
+ return ;
+ },
+);
+Button.displayName = "Button";
+
+export { Button, buttonVariants };
diff --git a/apps/react/src/components/ui/dropdown-menu.tsx b/apps/react/src/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000..696a682
--- /dev/null
+++ b/apps/react/src/components/ui/dropdown-menu.tsx
@@ -0,0 +1,180 @@
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
+import { Check, ChevronRight, Circle } from "lucide-react";
+import * as React from "react";
+
+import { cn } from "../../libraries/utils.ts";
+
+const DropdownMenu = DropdownMenuPrimitive.Root;
+
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
+
+const DropdownMenuGroup = DropdownMenuPrimitive.Group;
+
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
+
+const DropdownMenuSub = DropdownMenuPrimitive.Sub;
+
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
+
+const DropdownMenuSubTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+
+));
+DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
+
+const DropdownMenuSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
+
+const DropdownMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+));
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
+
+const DropdownMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, ...props }, ref) => (
+ svg]:size-4 [&>svg]:shrink-0",
+ inset && "pl-8",
+ className,
+ )}
+ {...props}
+ />
+));
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
+
+const DropdownMenuCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+));
+DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
+
+const DropdownMenuRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+));
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
+
+const DropdownMenuLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, ...props }, ref) => (
+
+));
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
+
+const DropdownMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
+
+const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => {
+ return ;
+};
+DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
+
+export {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuGroup,
+ DropdownMenuPortal,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuRadioGroup,
+};
diff --git a/apps/react/src/components/ui/input.tsx b/apps/react/src/components/ui/input.tsx
new file mode 100644
index 0000000..22e9562
--- /dev/null
+++ b/apps/react/src/components/ui/input.tsx
@@ -0,0 +1,21 @@
+import type * as React from "react";
+
+import { cn } from "@/libraries/utils";
+
+function Input({ className, type, ...props }: React.ComponentProps<"input">) {
+ return (
+
+ );
+}
+
+export { Input };
diff --git a/apps/react/src/components/ui/scroll-area.tsx b/apps/react/src/components/ui/scroll-area.tsx
new file mode 100644
index 0000000..cade2ee
--- /dev/null
+++ b/apps/react/src/components/ui/scroll-area.tsx
@@ -0,0 +1,38 @@
+import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
+import * as React from "react";
+
+import { cn } from "@/libraries/utils";
+
+const ScrollArea = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+
+
+));
+ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
+
+const ScrollBar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, orientation = "vertical", ...props }, ref) => (
+
+
+
+));
+ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
+
+export { ScrollArea, ScrollBar };
diff --git a/apps/react/src/components/ui/separator.tsx b/apps/react/src/components/ui/separator.tsx
new file mode 100644
index 0000000..c1c3644
--- /dev/null
+++ b/apps/react/src/components/ui/separator.tsx
@@ -0,0 +1,26 @@
+import * as SeparatorPrimitive from "@radix-ui/react-separator";
+import type * as React from "react";
+
+import { cn } from "@/libraries/utils";
+
+function Separator({
+ className,
+ orientation = "horizontal",
+ decorative = true,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+export { Separator };
diff --git a/apps/react/src/components/ui/sheet.tsx b/apps/react/src/components/ui/sheet.tsx
new file mode 100644
index 0000000..f377113
--- /dev/null
+++ b/apps/react/src/components/ui/sheet.tsx
@@ -0,0 +1,103 @@
+"use client";
+
+import * as SheetPrimitive from "@radix-ui/react-dialog";
+import { XIcon } from "lucide-react";
+import type * as React from "react";
+
+import { cn } from "@/libraries/utils";
+
+function Sheet({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function SheetTrigger({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function SheetClose({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function SheetPortal({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function SheetOverlay({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function SheetContent({
+ className,
+ children,
+ side = "right",
+ ...props
+}: React.ComponentProps & {
+ side?: "top" | "right" | "bottom" | "left";
+}) {
+ return (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+ );
+}
+
+function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return ;
+}
+
+function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return ;
+}
+
+function SheetTitle({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function SheetDescription({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+export { Sheet, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription };
diff --git a/apps/react/src/components/ui/sidebar.tsx b/apps/react/src/components/ui/sidebar.tsx
new file mode 100644
index 0000000..ef84fe8
--- /dev/null
+++ b/apps/react/src/components/ui/sidebar.tsx
@@ -0,0 +1,643 @@
+"use client";
+
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+import { PanelLeft } from "lucide-react";
+import * as React from "react";
+
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Separator } from "@/components/ui/separator";
+import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet";
+import { Skeleton } from "@/components/ui/skeleton";
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
+import { useIsMobile } from "@/hooks/use-mobile";
+import { cn } from "@/libraries/utils";
+
+const SIDEBAR_COOKIE_NAME = "sidebar_state";
+const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
+const SIDEBAR_WIDTH = "16rem";
+const SIDEBAR_WIDTH_MOBILE = "18rem";
+const SIDEBAR_WIDTH_ICON = "3rem";
+const SIDEBAR_KEYBOARD_SHORTCUT = "b";
+
+type SidebarContextProps = {
+ state: "expanded" | "collapsed";
+ open: boolean;
+ setOpen: (open: boolean) => void;
+ openMobile: boolean;
+ setOpenMobile: (open: boolean) => void;
+ isMobile: boolean;
+ toggleSidebar: () => void;
+};
+
+const SidebarContext = React.createContext(null);
+
+function useSidebar() {
+ const context = React.useContext(SidebarContext);
+ if (!context) {
+ throw new Error("useSidebar must be used within a SidebarProvider.");
+ }
+
+ return context;
+}
+
+const SidebarProvider = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ defaultOpen?: boolean;
+ open?: boolean;
+ onOpenChange?: (open: boolean) => void;
+ }
+>(({ defaultOpen = true, open: openProp, onOpenChange: setOpenProp, className, style, children, ...props }, ref) => {
+ const isMobile = useIsMobile();
+ const [openMobile, setOpenMobile] = React.useState(false);
+
+ // This is the internal state of the sidebar.
+ // We use openProp and setOpenProp for control from outside the component.
+ const [_open, _setOpen] = React.useState(defaultOpen);
+ const open = openProp ?? _open;
+ const setOpen = React.useCallback(
+ (value: boolean | ((value: boolean) => boolean)) => {
+ const openState = typeof value === "function" ? value(open) : value;
+ if (setOpenProp) {
+ setOpenProp(openState);
+ } else {
+ _setOpen(openState);
+ }
+
+ // This sets the cookie to keep the sidebar state.
+ document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
+ },
+ [setOpenProp, open],
+ );
+
+ // Helper to toggle the sidebar.
+ const toggleSidebar = React.useCallback(() => {
+ return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
+ }, [isMobile, setOpen, setOpenMobile]);
+
+ // Adds a keyboard shortcut to toggle the sidebar.
+ React.useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
+ event.preventDefault();
+ toggleSidebar();
+ }
+ };
+
+ window.addEventListener("keydown", handleKeyDown);
+ return () => window.removeEventListener("keydown", handleKeyDown);
+ }, [toggleSidebar]);
+
+ // We add a state so that we can do data-state="expanded" or "collapsed".
+ // This makes it easier to style the sidebar with Tailwind classes.
+ const state = open ? "expanded" : "collapsed";
+
+ const contextValue = React.useMemo(
+ () => ({
+ state,
+ open,
+ setOpen,
+ isMobile,
+ openMobile,
+ setOpenMobile,
+ toggleSidebar,
+ }),
+ [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
+ );
+
+ return (
+
+
+
+ {children}
+
+
+
+ );
+});
+SidebarProvider.displayName = "SidebarProvider";
+
+const Sidebar = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ side?: "left" | "right";
+ variant?: "sidebar" | "floating" | "inset";
+ collapsible?: "offcanvas" | "icon" | "none";
+ }
+>(({ side = "left", variant = "sidebar", collapsible = "offcanvas", className, children, ...props }, ref) => {
+ const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
+
+ if (collapsible === "none") {
+ return (
+
+ {children}
+
+ );
+ }
+
+ if (isMobile) {
+ return (
+
+
+
+ Sidebar
+ Displays the mobile sidebar.
+
+ {children}
+
+
+ );
+ }
+
+ return (
+
+ {/* This is what handles the sidebar gap on desktop */}
+
+
+
+ );
+});
+Sidebar.displayName = "Sidebar";
+
+const SidebarTrigger = React.forwardRef, React.ComponentProps>(
+ ({ className, onClick, ...props }, ref) => {
+ const { toggleSidebar } = useSidebar();
+
+ return (
+
+ );
+ },
+);
+SidebarTrigger.displayName = "SidebarTrigger";
+
+const SidebarRail = React.forwardRef>(
+ ({ className, ...props }, ref) => {
+ const { toggleSidebar } = useSidebar();
+
+ return (
+
+ );
+ },
+);
+SidebarRail.displayName = "SidebarRail";
+
+const SidebarInset = React.forwardRef>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+SidebarInset.displayName = "SidebarInset";
+
+const SidebarInput = React.forwardRef, React.ComponentProps>(
+ ({ className, ...props }, ref) => {
+ return (
+
+ );
+ },
+);
+SidebarInput.displayName = "SidebarInput";
+
+const SidebarHeader = React.forwardRef>(({ className, ...props }, ref) => {
+ return ;
+});
+SidebarHeader.displayName = "SidebarHeader";
+
+const SidebarFooter = React.forwardRef>(({ className, ...props }, ref) => {
+ return ;
+});
+SidebarFooter.displayName = "SidebarFooter";
+
+const SidebarSeparator = React.forwardRef, React.ComponentProps>(
+ ({ className, ...props }, ref) => {
+ return (
+
+ );
+ },
+);
+SidebarSeparator.displayName = "SidebarSeparator";
+
+const SidebarContent = React.forwardRef>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+SidebarContent.displayName = "SidebarContent";
+
+const SidebarGroup = React.forwardRef>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+SidebarGroup.displayName = "SidebarGroup";
+
+const SidebarGroupLabel = React.forwardRef & { asChild?: boolean }>(
+ ({ className, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "div";
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
+ className,
+ )}
+ {...props}
+ />
+ );
+ },
+);
+SidebarGroupLabel.displayName = "SidebarGroupLabel";
+
+const SidebarGroupAction = React.forwardRef & { asChild?: boolean }>(
+ ({ className, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button";
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ // Increases the hit area of the button on mobile.
+ "after:absolute after:-inset-2 after:md:hidden",
+ "group-data-[collapsible=icon]:hidden",
+ className,
+ )}
+ {...props}
+ />
+ );
+ },
+);
+SidebarGroupAction.displayName = "SidebarGroupAction";
+
+const SidebarGroupContent = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ ),
+);
+SidebarGroupContent.displayName = "SidebarGroupContent";
+
+const SidebarMenu = React.forwardRef>(({ className, ...props }, ref) => (
+
+));
+SidebarMenu.displayName = "SidebarMenu";
+
+const SidebarMenuItem = React.forwardRef>(({ className, ...props }, ref) => (
+
+));
+SidebarMenuItem.displayName = "SidebarMenuItem";
+
+const sidebarMenuButtonVariants = cva(
+ "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
+ outline:
+ "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
+ },
+ size: {
+ default: "h-8 text-sm",
+ sm: "h-7 text-xs",
+ lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
+);
+
+const SidebarMenuButton = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button"> & {
+ asChild?: boolean;
+ isActive?: boolean;
+ tooltip?: string | React.ComponentProps;
+ } & VariantProps
+>(({ asChild = false, isActive = false, variant = "default", size = "default", tooltip, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button";
+ const { isMobile, state } = useSidebar();
+
+ const button = (
+
+ );
+
+ if (!tooltip) {
+ return button;
+ }
+
+ if (typeof tooltip === "string") {
+ tooltip = {
+ children: tooltip,
+ };
+ }
+
+ return (
+
+ {button}
+
+
+ );
+});
+SidebarMenuButton.displayName = "SidebarMenuButton";
+
+const SidebarMenuAction = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button"> & {
+ asChild?: boolean;
+ showOnHover?: boolean;
+ }
+>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button";
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ // Increases the hit area of the button on mobile.
+ "after:absolute after:-inset-2 after:md:hidden",
+ "peer-data-[size=sm]/menu-button:top-1",
+ "peer-data-[size=default]/menu-button:top-1.5",
+ "peer-data-[size=lg]/menu-button:top-2.5",
+ "group-data-[collapsible=icon]:hidden",
+ showOnHover &&
+ "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
+ className,
+ )}
+ {...props}
+ />
+ );
+});
+SidebarMenuAction.displayName = "SidebarMenuAction";
+
+const SidebarMenuBadge = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ ),
+);
+SidebarMenuBadge.displayName = "SidebarMenuBadge";
+
+const SidebarMenuSkeleton = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ showIcon?: boolean;
+ }
+>(({ className, showIcon = false, ...props }, ref) => {
+ // Random width between 50 to 90%.
+ const width = React.useMemo(() => {
+ return `${Math.floor(Math.random() * 40) + 50}%`;
+ }, []);
+
+ return (
+
+ {showIcon && }
+
+
+ );
+});
+SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton";
+
+const SidebarMenuSub = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ ),
+);
+SidebarMenuSub.displayName = "SidebarMenuSub";
+
+const SidebarMenuSubItem = React.forwardRef>(({ ...props }, ref) => (
+
+));
+SidebarMenuSubItem.displayName = "SidebarMenuSubItem";
+
+const SidebarMenuSubButton = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentProps<"a"> & {
+ asChild?: boolean;
+ size?: "sm" | "md";
+ isActive?: boolean;
+ }
+>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a";
+
+ return (
+ span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
+ "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
+ size === "sm" && "text-xs",
+ size === "md" && "text-sm",
+ "group-data-[collapsible=icon]:hidden",
+ className,
+ )}
+ {...props}
+ />
+ );
+});
+SidebarMenuSubButton.displayName = "SidebarMenuSubButton";
+
+export {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarGroup,
+ SidebarGroupAction,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarHeader,
+ SidebarInput,
+ SidebarInset,
+ SidebarMenu,
+ SidebarMenuAction,
+ SidebarMenuBadge,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarMenuSkeleton,
+ SidebarMenuSub,
+ SidebarMenuSubButton,
+ SidebarMenuSubItem,
+ SidebarProvider,
+ SidebarRail,
+ SidebarSeparator,
+ SidebarTrigger,
+ useSidebar,
+};
diff --git a/apps/react/src/components/ui/skeleton.tsx b/apps/react/src/components/ui/skeleton.tsx
new file mode 100644
index 0000000..bb99295
--- /dev/null
+++ b/apps/react/src/components/ui/skeleton.tsx
@@ -0,0 +1,7 @@
+import { cn } from "@/libraries/utils";
+
+function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
+ return ;
+}
+
+export { Skeleton };
diff --git a/apps/react/src/components/ui/tooltip.tsx b/apps/react/src/components/ui/tooltip.tsx
new file mode 100644
index 0000000..a00a370
--- /dev/null
+++ b/apps/react/src/components/ui/tooltip.tsx
@@ -0,0 +1,46 @@
+import * as TooltipPrimitive from "@radix-ui/react-tooltip";
+import type * as React from "react";
+
+import { cn } from "@/libraries/utils";
+
+function TooltipProvider({ delayDuration = 0, ...props }: React.ComponentProps) {
+ return ;
+}
+
+function Tooltip({ ...props }: React.ComponentProps) {
+ return (
+
+
+
+ );
+}
+
+function TooltipTrigger({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function TooltipContent({
+ className,
+ sideOffset = 0,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+ {children}
+
+
+
+ );
+}
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
diff --git a/apps/react/src/hooks/use-mobile.ts b/apps/react/src/hooks/use-mobile.ts
new file mode 100644
index 0000000..502fd32
--- /dev/null
+++ b/apps/react/src/hooks/use-mobile.ts
@@ -0,0 +1,19 @@
+import * as React from "react";
+
+const MOBILE_BREAKPOINT = 768;
+
+export function useIsMobile() {
+ const [isMobile, setIsMobile] = React.useState(undefined);
+
+ React.useEffect(() => {
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
+ const onChange = () => {
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
+ };
+ mql.addEventListener("change", onChange);
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
+ return () => mql.removeEventListener("change", onChange);
+ }, []);
+
+ return !!isMobile;
+}
diff --git a/apps/react/src/index.css b/apps/react/src/index.css
index f1d8c73..9702450 100644
--- a/apps/react/src/index.css
+++ b/apps/react/src/index.css
@@ -1 +1,122 @@
@import "tailwindcss";
+
+@plugin "tailwindcss-animate";
+@import "tw-animate-css";
+
+@custom-variant dark (&:is(.dark *));
+
+@theme inline {
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
+ --color-chart-1: var(--chart-1);
+ --color-chart-2: var(--chart-2);
+ --color-chart-3: var(--chart-3);
+ --color-chart-4: var(--chart-4);
+ --color-chart-5: var(--chart-5);
+ --color-sidebar: var(--sidebar);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-ring: var(--sidebar-ring);
+}
+
+:root {
+ --radius: 0.625rem;
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.145 0 0);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.145 0 0);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.145 0 0);
+ --primary: oklch(0.205 0 0);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.97 0 0);
+ --secondary-foreground: oklch(0.205 0 0);
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.556 0 0);
+ --accent: oklch(0.97 0 0);
+ --accent-foreground: oklch(0.205 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+ --chart-1: oklch(0.646 0.222 41.116);
+ --chart-2: oklch(0.6 0.118 184.704);
+ --chart-3: oklch(0.398 0.07 227.392);
+ --chart-4: oklch(0.828 0.189 84.429);
+ --chart-5: oklch(0.769 0.188 70.08);
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.145 0 0);
+ --sidebar-primary: oklch(0.205 0 0);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.97 0 0);
+ --sidebar-accent-foreground: oklch(0.205 0 0);
+ --sidebar-border: oklch(0.922 0 0);
+ --sidebar-ring: oklch(0.708 0 0);
+}
+
+.dark {
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.205 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.205 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.922 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.269 0 0);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.704 0.191 22.216);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.556 0 0);
+ --chart-1: oklch(0.488 0.243 264.376);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.556 0 0);
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
diff --git a/apps/react/src/libraries/controller.ts b/apps/react/src/libraries/controller.ts
index 7cabd16..762505b 100644
--- a/apps/react/src/libraries/controller.ts
+++ b/apps/react/src/libraries/controller.ts
@@ -1,372 +1,234 @@
-import type { ChangeEvent, Collection, SubscribeToMany, SubscribeToSingle, SubscriptionOptions } from "@valkyr/db";
-import type { Subscription } from "@valkyr/event-emitter";
+import { useEffect, useMemo, useRef, useState } from "react";
-import { Debounce } from "./debounce.ts";
-import { ControllerRefs } from "./refs.ts";
-import type { ControllerClass, Empty, ReactComponent, ReservedPropertyMembers, Unknown } from "./types.ts";
-
-export class Controller {
+/**
+ * Minimal Controller for managing component state and lifecycle.
+ */
+export class Controller = {}, TProps extends Record = {}> {
state: TState = {} as TState;
props: TProps = {} as TProps;
- /**
- * Stores a list of referenced elements identifies by a unique key.
- */
- readonly refs = new ControllerRefs();
-
- /**
- * Records of event emitter subscriptions. They are keyed to a subscription name
- * for easier identification when unsubscribing.
- */
- readonly subscriptions = new Map();
-
- /**
- * Has the controller fully resolved the .onInit lifecycle method?
- */
#resolved = false;
+ #destroyed = false;
- /**
- * Internal debounce instance used to ensure that we aren't triggering state
- * updates too frequently when updates are happening in quick succession.
- */
- #debounce = new Debounce();
+ #setState: (state: Partial) => void;
+ #setLoading: (state: boolean) => void;
- /**
- * Creates a new controller instance with given default state and pushState
- * handler method.
- *
- * @param state - Default state to assign to controller.
- * @param pushData - Push data handler method.
- */
- constructor(
- readonly view: ReactComponent,
- readonly setView: any,
- ) {
- this.query = this.query.bind(this);
- this.subscribe = this.subscribe.bind(this);
- this.setState = this.setState.bind(this);
+ constructor(setState: (state: Partial) => void, setLoading: (state: boolean) => void) {
+ this.#setState = setState;
+ this.#setLoading = setLoading;
}
- /*
- |--------------------------------------------------------------------------------
- | Factories
- |--------------------------------------------------------------------------------
- */
-
/**
- * Creates a new controller instance using the given component and setView handler.
- *
- * @param component - Component to render.
- * @param setView - Method to provide a resolved view component.
+ * Factory method to create a new controller instance.
*/
- static make(
+ static make(
this: TController,
- component: ReactComponent,
- setView: any,
+ setState: any,
+ setLoading: any,
): InstanceType {
- return new Controller(component, setView);
+ return new this(setState, setLoading) as InstanceType;
}
/*
|--------------------------------------------------------------------------------
- | Bootstrap & Teardown
+ | Lifecycle
|--------------------------------------------------------------------------------
*/
+ /**
+ * Resolves the controller with given props.
+ * - First time: Runs onInit() then onResolve()
+ * - Subsequent times: Runs only onResolve()
+ */
async $resolve(props: TProps): Promise {
+ if (this.#destroyed === true) {
+ return;
+ }
+
this.props = props;
let state: Partial = {};
+
try {
if (this.#resolved === false) {
- state = {
- ...state,
- ...((await this.onInit()) ?? {}),
- };
+ const initState = await this.onInit();
+ if (initState) {
+ state = { ...state, ...initState };
+ }
}
- state = {
- ...state,
- ...((await this.onResolve()) ?? {}),
- };
- } catch (err) {
- console.error(err);
- throw err;
+ const resolveState = await this.onResolve();
+ if (resolveState) {
+ state = { ...state, ...resolveState };
+ }
+ } catch (error) {
+ console.error("Controller resolve error:", error);
+ throw error;
}
+
this.#resolved = true;
- this.setState(state);
+
+ if (this.#destroyed === false) {
+ this.setState(state);
+ }
}
+ /**
+ * Destroys the controller and cleans up resources.
+ */
async $destroy(): Promise {
- for (const subscription of this.subscriptions.values()) {
- subscription.unsubscribe();
- }
+ this.#destroyed = true;
await this.onDestroy();
- this.refs.destroy();
}
/*
|--------------------------------------------------------------------------------
- | Lifecycle Methods
+ | Lifecycle Hooks
|--------------------------------------------------------------------------------
*/
/**
- * Method runs once per controller view lifecycle. This is where you should
- * subscribe to and return initial controller state. A component is kept in
- * loading state until the initial resolve is completed.
- *
- * Once the initial resolve is completed the controller will not run the onInit
- * method again unless the controller is destroyed and re-created.
- *
- * @example
- * ```ts
- * async onInit() {
- * return {
- * foos: this.query(foos, {}, "foos")
- * }
- * }
- * ```
+ * Called once when the controller is first initialized.
*/
async onInit(): Promise | void> {
return {};
}
/**
- * Method runs every time the controller is resolved. This is where you should
- * subscribe to and return state that is reflecting changes to the parent view
- * properties.
- *
- * @example
- * ```ts
- * async onResolve() {
- * return {
- * foos: this.query(foos, { tenantId: this.props.tenantId }, "foos")
- * }
- * }
- * ```
+ * Called every time props change (including first mount).
*/
async onResolve(): Promise | void> {
return {};
}
/**
- * Method runs when the controller parent view is destroyed.
+ * Called when the controller is destroyed.
*/
async onDestroy(): Promise {}
/*
|--------------------------------------------------------------------------------
- | Query Methods
+ | State Management
|--------------------------------------------------------------------------------
*/
/**
- * Executes a query on a given collection and returns the initial result. A
- * subsequent internal subscription is also created, which automatically updates
- * the controller state when changes are made to the data in which the query
- * subscribes.
- *
- * @param collection - Collection to query against.
- * @param query - Query to execute.
- * @param stateKey - State key to assign the results to, or state handler method.
- *
- * @example
- * ```ts
- * async onInit() {
- * return {
- * foo: await this.query(db.collection("foos"), { limit: 1 }, "foo")
- * }
- * }
- * ```
- */
- async query, TSchema = CollectionSchema, TStateKey = keyof TState>(
- collection: TCollection,
- query: QuerySingle,
- next: TStateKey | ((document: TSchema | undefined) => Promise>),
- ): Promise;
-
- /**
- * Executes a query on a given collection and returns the initial result. A
- * subsequent internal subscription is also created, which automatically updates
- * the controller state when changes are made to the data in which the query
- * subscribes.
- *
- * @param collection - Collection to query against.
- * @param query - Query to execute.
- * @param next - State key to assign the results to, or state handler method.
- *
- * @example
- * ```ts
- * async onInit() {
- * return {
- * foos: await this.query(db.collection("foos"), {}, "foos")
- * }
- * }
- * ```
- */
- async query, TSchema = CollectionSchema, TStateKey = keyof TState>(
- collection: TCollection,
- query: QueryMany,
- next:
- | TStateKey
- | ((documents: TSchema[], changed: TSchema[], type: ChangeEvent["type"]) => Promise>),
- ): Promise;
-
- /**
- * Executes a query on a given collection and returns the initial result. A
- * subsequent internal subscription is also created, which automatically updates
- * the controller state when changes are made to the data in which the query
- * subscribes.
- *
- * @param collection - Collection to query against.
- * @param query - Query to execute.
- * @param stateKey - State key to assign the results to, or state handler method.
- */
- async query, TSchema = CollectionSchema, TStateKey = keyof TState>(
- collection: TCollection,
- query: Query,
- next: TStateKey | ((...args: any[]) => Promise>),
- ): Promise {
- let resolved = false;
- this.subscriptions.get(collection.name)?.unsubscribe();
- return new Promise[] | CollectionSchema | undefined>((resolve) => {
- const { where, ...options } = query;
- this.subscriptions.set(
- collection.name,
- collection.subscribe(where, options, (...args: any[]) => {
- if (this.#isStateKey(next)) {
- if (resolved === true) {
- this.setState(next, args[0]);
- }
- } else {
- (next as any)(...args).then(this.setState);
- }
- setTimeout(() => {
- resolve(args[0]);
- resolved = true;
- }, 0);
- }),
- );
- });
- }
-
- /*
- |--------------------------------------------------------------------------------
- | Event Methods
- |--------------------------------------------------------------------------------
- */
-
- /**
- * Consumes a subscription under a given event key that is unsubscribed
- * automatically when the controller is unmounted.
- *
- * @param key - Unique identifier used to unsusbcribe duplicate subs.
- * @param sub - Subscription to unsubscribe on controller unmount.
- */
- subscribe(key: string, sub: { unsubscribe: () => void }): void {
- this.subscriptions.get(key)?.unsubscribe();
- this.subscriptions.set(key, sub);
- }
-
- /*
- |--------------------------------------------------------------------------------
- | State Methods
- |--------------------------------------------------------------------------------
- */
-
- /**
- * Updates the state of the controller and triggers a state update via the push
- * state handler. This method will debounce state updates to prevent excessive
- * state updates.
- *
- * @param key - State key to assign data to.
- * @param value - State value to assign.
+ * Updates the controller state.
*/
setState(state: Partial): void;
- setState(key: K): (state: TState[K]) => void;
setState(key: K, value: TState[K]): void;
- setState(...args: [K | TState, TState[K]?]): void | ((state: TState[K]) => void) {
+ setState(...args: [K | Partial, TState[K]?]): void {
+ if (this.#destroyed) {
+ return;
+ }
+
const [target, value] = args;
- if (this.#isStateKey(target) && args.length === 1) {
- return (value: TState[K]) => {
- this.setState(target, value);
- };
+ if (typeof target === "string") {
+ this.state = { ...this.state, [target]: value };
+ } else {
+ this.state = { ...this.state, ...(target as Partial) };
}
- this.state = this.#isStateKey(target)
- ? {
- ...this.state,
- [target]: value,
- }
- : {
- ...this.state,
- ...(target as Partial),
- };
-
- if (this.#resolved === true) {
- this.#debounce.run(() => {
- this.setView(
- this.view({
- props: this.props,
- state: this.state,
- actions: this.toActions(),
- refs: this.refs,
- }),
- );
- }, 0);
- }
+ this.#setState(this.state);
+ this.#setLoading(false);
}
/*
|--------------------------------------------------------------------------------
- | Resolvers
+ | Actions
|--------------------------------------------------------------------------------
*/
/**
- * Returns all the prototype methods defined on the controller as a list of
- * actions bound to the controller instance to be used in the view.
- *
- * @returns List of actions.
+ * Returns all public methods as bound actions.
*/
- toActions(): Omit {
+ toActions(): ControllerActions {
const actions: any = {};
- for (const name of Object.getOwnPropertyNames(this.constructor.prototype)) {
- if (name !== "constructor" && name !== "resolve") {
- const action = (this as any)[name];
- if (typeof action === "function") {
- actions[name] = action.bind(this);
+ const prototype = Object.getPrototypeOf(this);
+
+ for (const name of Object.getOwnPropertyNames(prototype)) {
+ if (this.#isAction(name)) {
+ const method = (this as any)[name];
+ if (typeof method === "function") {
+ actions[name] = method.bind(this);
}
}
}
+
return actions;
}
- /*
- |--------------------------------------------------------------------------------
- | Utilities
- |--------------------------------------------------------------------------------
- */
-
- #isStateKey(key: unknown): key is keyof TState {
- return typeof key === "string";
+ #isAction(name: string): boolean {
+ return name !== "constructor" && !name.startsWith("$") && !name.startsWith("_") && !name.startsWith("#");
}
}
+/*
+ |--------------------------------------------------------------------------------
+ | Hook
+ |--------------------------------------------------------------------------------
+ */
+
+/**
+ * React hook for using a controller.
+ *
+ * @example
+ * ```tsx
+ * function LoginView() {
+ * const [state, actions] = useController(LoginController);
+ *
+ * return (
+ *
+ * );
+ * }
+ * ```
+ */
+export function useController Controller>(
+ ControllerClass: TController,
+ props?: InstanceType["props"],
+): [InstanceType["state"], boolean, ControllerActions>] {
+ const [state, setState] = useState({});
+ const [loading, setLoading] = useState(true);
+
+ const controllerRef = useRef | null>(null);
+ const actionsRef = useRef> | null>(null);
+ const propsRef = useRef(props);
+
+ // Resolve only once after creation
+ useMemo(() => {
+ const instance = (ControllerClass as any).make(setState, setLoading);
+ controllerRef.current = instance;
+ actionsRef.current = instance.toActions();
+
+ instance.$resolve(props || {});
+
+ return () => {
+ instance.$destroy();
+ };
+ }, [controllerRef]);
+
+ // Resolve on props change
+ useEffect(() => {
+ if (propsRef.current !== props) {
+ propsRef.current = props;
+ controllerRef.current?.$resolve(props || {});
+ }
+ }, [props]);
+
+ return [state, loading, actionsRef.current!];
+}
+
/*
|--------------------------------------------------------------------------------
| Types
|--------------------------------------------------------------------------------
*/
-type Query = Where & SubscriptionOptions;
-
-type QuerySingle = Where & SubscribeToSingle;
-
-type QueryMany = Where & SubscribeToMany;
-
-type Where = {
- where?: Record;
+type ControllerActions = {
+ [K in keyof T]: T[K] extends (...args: any[]) => any
+ ? K extends `$${string}` | `_${string}` | `#${string}` | "constructor"
+ ? never
+ : T[K]
+ : never;
};
-
-type CollectionSchema = TCollection extends Collection ? TSchema : never;
diff --git a/apps/react/src/libraries/debounce.ts b/apps/react/src/libraries/debounce.ts
deleted file mode 100644
index 0aa2893..0000000
--- a/apps/react/src/libraries/debounce.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-export class Debounce {
- #timeout?: number;
-
- run(fn: (...args: any[]) => void, ms: number): void {
- this.#clear();
- this.#timeout = setTimeout(fn, ms);
- }
-
- #clear() {
- if (this.#timeout !== undefined) {
- clearTimeout(this.#timeout);
- }
- }
-}
diff --git a/apps/react/src/libraries/refs.ts b/apps/react/src/libraries/refs.ts
deleted file mode 100644
index 5b5b9d1..0000000
--- a/apps/react/src/libraries/refs.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-const refs = new Map();
-
-export class ControllerRefs {
- #refs = new Map();
-
- #forwarded: string[] = [];
-
- set(name: string) {
- return (element: HTMLElement | null) => {
- if (element !== null) {
- refs.set(name, element);
- }
- };
- }
-
- forward(name: string) {
- return (element: HTMLElement | null) => {
- if (element !== null) {
- refs.set(name, element);
- this.#forwarded.push(name);
- }
- };
- }
-
- get(name: string) {
- const element = this.#refs.get(name) ?? refs.get(name);
- if (element === undefined) {
- throw new Error(`Reference Exception: ${name} is not defined.`);
- }
- return element;
- }
-
- async on(name: string, count = 0): Promise {
- if (count > 20) {
- return undefined;
- }
- const element = this.#refs.get(name) ?? refs.get(name);
- if (element === undefined) {
- await new Promise((resolve) => setTimeout(resolve, 50));
- return this.on(name, count + 1);
- }
- return element;
- }
-
- destroy() {
- this.#forwarded.forEach((name) => {
- refs.delete(name);
- });
- }
-}
diff --git a/apps/react/src/libraries/types.ts b/apps/react/src/libraries/types.ts
deleted file mode 100644
index 9ba5e7d..0000000
--- a/apps/react/src/libraries/types.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import type React from "react";
-import type { FunctionComponent } from "react";
-
-import type { ControllerRefs } from "./refs.ts";
-
-export type ReactComponent = FunctionComponent<{
- props: TProps;
- state: InstanceType["state"];
- actions: Omit, ReservedPropertyMembers>;
- refs: ControllerRefs;
- component?: React.FC;
-}>;
-
-export type ControllerClass = {
- new (state: any, pushState: any): any;
- make(component: ReactComponent, pushState: any): any;
-};
-
-export type ReservedPropertyMembers = "state" | "pushState" | "init" | "destroy" | "setNext" | "setState" | "toActions";
-
-export type Unknown = Record;
-
-export type Empty = Record;
diff --git a/apps/react/src/libraries/utils.ts b/apps/react/src/libraries/utils.ts
new file mode 100644
index 0000000..365058c
--- /dev/null
+++ b/apps/react/src/libraries/utils.ts
@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/apps/react/src/libraries/view.ts b/apps/react/src/libraries/view.ts
deleted file mode 100644
index 5d86eae..0000000
--- a/apps/react/src/libraries/view.ts
+++ /dev/null
@@ -1,199 +0,0 @@
-import { deepEqual } from "fast-equals";
-import React, { createElement, type FunctionComponent, memo, type PropsWithChildren, useEffect, useState } from "react";
-
-import type { ControllerClass, ReactComponent, Unknown } from "./types.ts";
-
-/*
- |--------------------------------------------------------------------------------
- | Options
- |--------------------------------------------------------------------------------
- */
-
-const options: Partial> = {
- memoize: defaultMemoizeHandler,
-};
-
-/*
- |--------------------------------------------------------------------------------
- | Factory
- |--------------------------------------------------------------------------------
- */
-
-export function makeControllerView(
- controller: TController,
- component: ReactComponent,
- options?: Partial>,
-): FunctionComponent {
- const memoize = getMemoizeHandler(options?.memoize);
- const render = {
- loading: getLoadingComponent(options),
- error: getErrorComponent(options),
- };
-
- const container: FunctionComponent> = (props: any) => {
- const { error, view } = useView(controller, component, props);
- if (view === undefined) {
- return render.loading(props);
- }
- if (error !== undefined) {
- return render.error({ ...props, error });
- }
- return view;
- };
-
- container.displayName = component.displayName = options?.name ?? `${controller.name}View`;
-
- // ### Memoize
- // By default run component through react memoization using stringify
- // matching to determine changes to props.
-
- if (memoize !== false) {
- return memo(container, memoize);
- }
-
- return container;
-}
-
-/*
- |--------------------------------------------------------------------------------
- | Hooks
- |--------------------------------------------------------------------------------
- */
-
-function useView(
- instance: InstanceType | undefined,
- component: ReactComponent,
- props: any,
-) {
- const [view, setView] = useState();
-
- const error = useController(instance, component, props, setView);
-
- return { error, view };
-}
-
-function useController(controller: ControllerClass, component: any, props: any, setView: any) {
- const [instance, setInstance] = useState | undefined>(undefined);
- const error = useProps(instance, props);
-
- useEffect(() => {
- const instance = controller.make(component, setView);
- setInstance(instance);
- return () => {
- instance.$destroy();
- };
- }, [component, controller, setView]);
-
- return error;
-}
-
-function useProps(controller: InstanceType | undefined, props: any) {
- const [error, setError] = useState();
-
- useEffect(() => {
- if (controller === undefined) {
- return;
- }
- let isMounted = true;
- controller.$resolve(props).catch((error: Error) => {
- if (isMounted === true) {
- setError(error);
- }
- });
- return () => {
- isMounted = false;
- };
- }, [controller, props]);
-
- return error;
-}
-
-/*
- |--------------------------------------------------------------------------------
- | Components
- |--------------------------------------------------------------------------------
- */
-
-export function setLoadingComponent(component: React.FC) {
- options.loading = component;
-}
-
-function getLoadingComponent({ loading }: Partial> = {}) {
- const component = loading ?? options.loading;
- if (component === undefined) {
- return () => null;
- }
- return (props: TProps) => createElement(component, props);
-}
-
-export function setErrorComponent(component: React.FC) {
- options.error = component;
-}
-
-function getErrorComponent({ error }: Partial> = {}) {
- const component = error ?? options.loading;
- if (component === undefined) {
- return () => null;
- }
- return (props: TProps) => createElement(component, props);
-}
-
-/*
- |--------------------------------------------------------------------------------
- | Memoize
- |--------------------------------------------------------------------------------
- */
-
-export function setMemoizeHandler(value: boolean | Memoize) {
- if (typeof value === "function") {
- options.memoize = value;
- } else if (value === false) {
- options.memoize = false;
- } else {
- options.memoize = defaultMemoizeHandler;
- }
-}
-
-function getMemoizeHandler(memoize?: ViewOptions["memoize"]): false | Memoize | undefined {
- if (typeof memoize === "function") {
- return memoize;
- }
- if (memoize !== false) {
- return options.memoize;
- }
- return false;
-}
-
-/*
- |--------------------------------------------------------------------------------
- | Defaults
- |--------------------------------------------------------------------------------
- */
-
-function defaultMemoizeHandler(prev: any, next: any): boolean {
- if (prev.children !== undefined && next.children !== undefined) {
- if (prev.children.type.type.displayName !== next.children.type.type.displayName) {
- return false;
- }
- }
- return deepEqual(prev, next);
-}
-
-/*
- |--------------------------------------------------------------------------------
- | Types
- |--------------------------------------------------------------------------------
- */
-
-export type ViewOptions = {
- name?: string;
- loading: React.FC;
- error: React.FC;
- memoize: false | Memoize;
-};
-
-type Memoize = (prevProps: Readonly, nextProps: Readonly) => boolean;
-
-type Readonly = {
- readonly [P in keyof T]: T[P];
-};
diff --git a/apps/react/src/main.tsx b/apps/react/src/main.tsx
index 542b1fc..9f22d46 100644
--- a/apps/react/src/main.tsx
+++ b/apps/react/src/main.tsx
@@ -4,6 +4,7 @@ import { createRouter, RouterProvider } from "@tanstack/react-router";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
+import { ThemeProvider } from "./components/theme-provider.tsx";
import { routeTree } from "./routes.tsx";
const router = createRouter({ routeTree });
@@ -14,8 +15,15 @@ declare module "@tanstack/react-router" {
}
}
-createRoot(document.getElementById("root")!).render(
+const rootElement = document.getElementById("root");
+if (rootElement === null) {
+ throw new Error("Failed to retrieve root element");
+}
+
+createRoot(rootElement).render(
-
+
+
+
,
);
diff --git a/apps/react/src/routes.tsx b/apps/react/src/routes.tsx
index a9d2065..394bb73 100644
--- a/apps/react/src/routes.tsx
+++ b/apps/react/src/routes.tsx
@@ -1,36 +1,30 @@
-import { createRootRoute, createRoute, Outlet } from "@tanstack/react-router";
-import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
+import { createRootRoute, createRoute } from "@tanstack/react-router";
-import { CreateAccountView } from "./views/account/create.view.tsx";
-import { TodosView } from "./views/todo/todos.view.tsx";
+import { AppView } from "./views/app.view.tsx";
+import { CallbackView } from "./views/auth/callback.view.tsx";
+import { DashboardView } from "./views/dashboard/dashboard.view.tsx";
-const rootRoute = createRootRoute({
- component: () => (
- <>
-
-
- >
- ),
+const root = createRootRoute();
+
+const callback = createRoute({
+ getParentRoute: () => root,
+ path: "/callback",
+ component: CallbackView,
});
-const homeRoute = createRoute({
- getParentRoute: () => rootRoute,
+const app = createRoute({
+ id: "app",
+ getParentRoute: () => root,
+ component: AppView,
+});
+
+const dashboard = createRoute({
+ getParentRoute: () => app,
path: "/",
- component: function Index() {
- return Welcome Home!
;
- },
+ component: DashboardView,
});
-const createAccountRoute = createRoute({
- getParentRoute: () => rootRoute,
- path: "/accounts",
- component: CreateAccountView,
-});
+root.addChildren([app, callback]);
+app.addChildren([dashboard]);
-const todosRoute = createRoute({
- getParentRoute: () => rootRoute,
- path: "/todos",
- component: TodosView,
-});
-
-export const routeTree = rootRoute.addChildren([homeRoute, createAccountRoute, todosRoute]);
+export const routeTree = root;
diff --git a/apps/react/src/services/api.ts b/apps/react/src/services/api.ts
index 83ff98d..29060e7 100644
--- a/apps/react/src/services/api.ts
+++ b/apps/react/src/services/api.ts
@@ -1,3 +1,4 @@
+import { account } from "@module/account/client";
import { makeClient } from "@platform/relay";
import { HttpAdapter } from "../adapters/http.ts";
@@ -9,7 +10,6 @@ export const api = makeClient(
}),
},
{
- account: (await import("@platform/spec/account/routes.ts")).routes,
- auth: (await import("@platform/spec/auth/routes.ts")).routes,
+ account,
},
);
diff --git a/apps/react/src/services/zitadel.ts b/apps/react/src/services/zitadel.ts
new file mode 100644
index 0000000..50abd02
--- /dev/null
+++ b/apps/react/src/services/zitadel.ts
@@ -0,0 +1,14 @@
+import { createZitadelAuth, type ZitadelConfig } from "@zitadel/react";
+
+const config: ZitadelConfig = {
+ authority: "https://auth.valkyrjs.com",
+ client_id: "347982179092987909",
+ redirect_uri: "http://localhost:5173/callback",
+ post_logout_redirect_uri: "http://localhost:5173",
+ response_type: "code",
+ scope: "openid profile email",
+};
+
+export const zitadel = createZitadelAuth(config);
+
+export type User = NonNullable>>;
diff --git a/apps/react/src/theme.css b/apps/react/src/theme.css
new file mode 100644
index 0000000..fd549d6
--- /dev/null
+++ b/apps/react/src/theme.css
@@ -0,0 +1,105 @@
+body {
+ @apply overscroll-none bg-transparent;
+}
+
+:root {
+ --font-sans: var(--font-inter);
+ --header-height: calc(var(--spacing) * 12 + 1px);
+}
+
+.theme-scaled {
+ @media (min-width: 1024px) {
+ --radius: 0.6rem;
+ --text-lg: 1.05rem;
+ --text-base: 0.85rem;
+ --text-sm: 0.8rem;
+ --spacing: 0.222222rem;
+ }
+
+ [data-slot="card"] {
+ --spacing: 0.16rem;
+ }
+
+ [data-slot="select-trigger"],
+ [data-slot="toggle-group-item"] {
+ --spacing: 0.222222rem;
+ }
+}
+
+.theme-default,
+.theme-default-scaled {
+ --primary: var(--color-neutral-600);
+ --primary-foreground: var(--color-neutral-50);
+
+ @variant dark {
+ --primary: var(--color-neutral-500);
+ --primary-foreground: var(--color-neutral-50);
+ }
+}
+
+.theme-blue,
+.theme-blue-scaled {
+ --primary: var(--color-blue-600);
+ --primary-foreground: var(--color-blue-50);
+
+ @variant dark {
+ --primary: var(--color-blue-500);
+ --primary-foreground: var(--color-blue-50);
+ }
+}
+
+.theme-green,
+.theme-green-scaled {
+ --primary: var(--color-lime-600);
+ --primary-foreground: var(--color-lime-50);
+
+ @variant dark {
+ --primary: var(--color-lime-600);
+ --primary-foreground: var(--color-lime-50);
+ }
+}
+
+.theme-amber,
+.theme-amber-scaled {
+ --primary: var(--color-amber-600);
+ --primary-foreground: var(--color-amber-50);
+
+ @variant dark {
+ --primary: var(--color-amber-500);
+ --primary-foreground: var(--color-amber-50);
+ }
+}
+
+.theme-mono,
+.theme-mono-scaled {
+ --font-sans: var(--font-mono);
+ --primary: var(--color-neutral-600);
+ --primary-foreground: var(--color-neutral-50);
+
+ @variant dark {
+ --primary: var(--color-neutral-500);
+ --primary-foreground: var(--color-neutral-50);
+ }
+
+ .rounded-xs,
+ .rounded-sm,
+ .rounded-md,
+ .rounded-lg,
+ .rounded-xl {
+ @apply !rounded-none;
+ border-radius: 0;
+ }
+
+ .shadow-xs,
+ .shadow-sm,
+ .shadow-md,
+ .shadow-lg,
+ .shadow-xl {
+ @apply !shadow-none;
+ }
+
+ [data-slot="toggle-group"],
+ [data-slot="toggle-group-item"] {
+ @apply !rounded-none !shadow-none;
+ }
+}
diff --git a/apps/react/src/views/account/create.controller.ts b/apps/react/src/views/account/create.controller.ts
deleted file mode 100644
index 0f082b4..0000000
--- a/apps/react/src/views/account/create.controller.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import z from "zod";
-
-import { Controller } from "../../libraries/controller.ts";
-import { Form } from "../../libraries/form.ts";
-import { api } from "../../services/api.ts";
-
-const inputs = {
- givenName: z.string(),
- familyName: z.string(),
- email: z.string(),
-};
-
-export class CreateController extends Controller<{
- form: Form;
-}> {
- async onInit() {
- return {
- form: new Form(inputs).onSubmit(async ({ givenName, familyName, email }) => {
- const response = await api.account.create({
- body: {
- name: {
- given: givenName,
- family: familyName,
- },
- email,
- },
- });
- if ("error" in response) {
- console.log(response.error);
- } else {
- console.log(response.data);
- }
- }),
- };
- }
-}
diff --git a/apps/react/src/views/account/create.view.tsx b/apps/react/src/views/account/create.view.tsx
deleted file mode 100644
index fcae7ea..0000000
--- a/apps/react/src/views/account/create.view.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import { makeControllerView } from "../../libraries/view.ts";
-import { CreateController } from "./create.controller.ts";
-
-export const CreateAccountView = makeControllerView(CreateController, ({ state: { form } }) => {
- return (
-
- );
-});
diff --git a/apps/react/src/views/app.controller.ts b/apps/react/src/views/app.controller.ts
new file mode 100644
index 0000000..b5c1fc7
--- /dev/null
+++ b/apps/react/src/views/app.controller.ts
@@ -0,0 +1,25 @@
+import { Controller } from "../libraries/controller.ts";
+import { zitadel } from "../services/zitadel.ts";
+
+export class AppController extends Controller<{
+ authenticated: boolean;
+}> {
+ async onInit() {
+ return {
+ authenticated: await this.#getAuthenticatedState(),
+ };
+ }
+
+ async #getAuthenticatedState(): Promise {
+ const user = await zitadel.userManager.getUser();
+ if (user === null) {
+ zitadel.authorize();
+ return false;
+ }
+ return true;
+ }
+
+ signout() {
+ zitadel.signout();
+ }
+}
diff --git a/apps/react/src/views/app.view.tsx b/apps/react/src/views/app.view.tsx
new file mode 100644
index 0000000..8b83ee8
--- /dev/null
+++ b/apps/react/src/views/app.view.tsx
@@ -0,0 +1,40 @@
+import { Outlet } from "@tanstack/react-router";
+
+import { AppSidebar } from "@/components/app-sidebar.tsx";
+import { SiteHeader } from "@/components/site-header.tsx";
+import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar.tsx";
+import { useController } from "@/libraries/controller.ts";
+
+import { AppController } from "./app.controller.ts";
+
+export function AppView() {
+ const [{ authenticated }, loading] = useController(AppController);
+ if (loading === true) {
+ return Loading ...
;
+ }
+ if (authenticated === false) {
+ return Unauthenticated
;
+ }
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/react/src/views/auth/callback.view.tsx b/apps/react/src/views/auth/callback.view.tsx
new file mode 100644
index 0000000..2f1bec9
--- /dev/null
+++ b/apps/react/src/views/auth/callback.view.tsx
@@ -0,0 +1,26 @@
+import { useNavigate } from "@tanstack/react-router";
+import { useEffect } from "react";
+
+import { zitadel } from "../../services/zitadel.ts";
+
+export function CallbackView() {
+ const navigate = useNavigate();
+ useEffect(() => {
+ async function handleCallback() {
+ try {
+ const user = await zitadel.userManager.signinRedirectCallback();
+ if (user) {
+ navigate({ to: "/", replace: true });
+ } else {
+ navigate({ to: "/", replace: true });
+ }
+ } catch (error) {
+ console.error("Callback error", error);
+ navigate({ to: "/", replace: true });
+ }
+ }
+ handleCallback();
+ }, [navigate]);
+
+ return null;
+}
diff --git a/apps/react/src/views/auth/login.controller.ts b/apps/react/src/views/auth/login.controller.ts
new file mode 100644
index 0000000..b1d0a55
--- /dev/null
+++ b/apps/react/src/views/auth/login.controller.ts
@@ -0,0 +1,29 @@
+import { Controller } from "../../libraries/controller.ts";
+import { type User, zitadel } from "../../services/zitadel.ts";
+
+export class LoginController extends Controller<{
+ user?: User;
+}> {
+ async onInit() {
+ return {
+ user: await this.#getAuthenticationState(),
+ };
+ }
+
+ async #getAuthenticationState(): Promise {
+ return zitadel.userManager.getUser().then((user) => {
+ if (user === null) {
+ return undefined;
+ }
+ return user;
+ });
+ }
+
+ login() {
+ zitadel.authorize();
+ }
+
+ logout() {
+ zitadel.signout();
+ }
+}
diff --git a/apps/react/src/views/auth/login.view.tsx b/apps/react/src/views/auth/login.view.tsx
new file mode 100644
index 0000000..10c3d21
--- /dev/null
+++ b/apps/react/src/views/auth/login.view.tsx
@@ -0,0 +1,14 @@
+import { useController } from "../../libraries/controller.ts";
+import { LoginController } from "./login.controller.ts";
+
+export function LoginView() {
+ const [{ user }, { login, logout }] = useController(LoginController);
+ return (
+
+
+ {user !== undefined ?
{JSON.stringify(user, null, 2)} : null}
+
+ );
+}
diff --git a/apps/react/src/views/dashboard/dashboard.view.tsx b/apps/react/src/views/dashboard/dashboard.view.tsx
new file mode 100644
index 0000000..6d0cf26
--- /dev/null
+++ b/apps/react/src/views/dashboard/dashboard.view.tsx
@@ -0,0 +1,3 @@
+export function DashboardView() {
+ return Dashboard
;
+}
diff --git a/apps/react/src/views/todo/todos.controller.ts b/apps/react/src/views/todo/todos.controller.ts
deleted file mode 100644
index 0d67666..0000000
--- a/apps/react/src/views/todo/todos.controller.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import z from "zod";
-
-import { Controller } from "../../libraries/controller.ts";
-import { Form } from "../../libraries/form.ts";
-import { db } from "../../stores/database.ts";
-import type { Todo } from "../../stores/todo.ts";
-
-const inputs = {
- name: z.string(),
-};
-
-export class TodosController extends Controller<{
- form: Form;
- todos: Todo[];
-}> {
- override async onInit() {
- return {
- form: new Form(inputs).onSubmit(async ({ name }) => {
- db.collection("todos").insertOne({ name, items: [] });
- }),
- todos: await this.query(db.collection("todos"), { limit: 10 }, "todos"),
- };
- }
-
- remove(id: string) {
- db.collection("todos").remove({ id });
- }
-}
diff --git a/apps/react/src/views/todo/todos.view.tsx b/apps/react/src/views/todo/todos.view.tsx
deleted file mode 100644
index 9014a7b..0000000
--- a/apps/react/src/views/todo/todos.view.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import { Link } from "@tanstack/react-router";
-
-import { makeControllerView } from "../../libraries/view.ts";
-import { TodosController } from "./todos.controller.ts";
-
-export const TodosView = makeControllerView(
- TodosController,
- ({ state: { form, todos }, actions: { remove } }) => {
- return (
-
-
- {/* Heading */}
-
-
- {/* Create form */}
-
-
- {/* Todo list output */}
-
- Your Lists
- {todos?.length > 0 ? (
-
- ) : (
- No todo lists yet. Create one above!
- )}
-
-
-
- );
- },
-);
diff --git a/apps/react/tsconfig.app.json b/apps/react/tsconfig.app.json
index 126d126..b02fee3 100644
--- a/apps/react/tsconfig.app.json
+++ b/apps/react/tsconfig.app.json
@@ -21,7 +21,14 @@
"noUnusedParameters": true,
// "erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
- "noUncheckedSideEffectImports": true
+ "noUncheckedSideEffectImports": true,
+
+ "baseUrl": ".",
+ "paths": {
+ "@/*": [
+ "./src/*"
+ ]
+ }
},
"include": ["src"]
}
diff --git a/apps/react/tsconfig.json b/apps/react/tsconfig.json
index d32ff68..2b78387 100644
--- a/apps/react/tsconfig.json
+++ b/apps/react/tsconfig.json
@@ -1,4 +1,10 @@
{
"files": [],
- "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
+ "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }],
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ }
}
diff --git a/apps/react/vite.config.ts b/apps/react/vite.config.ts
index 09a88e3..366f799 100644
--- a/apps/react/vite.config.ts
+++ b/apps/react/vite.config.ts
@@ -1,9 +1,15 @@
+import path from "node:path"
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [react(), tailwindcss()],
+ resolve: {
+ alias: {
+ "@": path.resolve(__dirname, "./src"),
+ },
+ },
server: {
proxy: {
"/api/v1": {
diff --git a/deno.json b/deno.json
index cbd7970..5d147e6 100644
--- a/deno.json
+++ b/deno.json
@@ -4,8 +4,8 @@
"workspace": [
"api",
"apps/react",
- "modules/iam",
- "modules/workspace",
+ "modules/account",
+ "modules/tenant",
"platform/cerbos",
"platform/config",
"platform/database",
@@ -19,16 +19,16 @@
"platform/vault"
],
"tasks": {
- "start:api": {
+ "api": {
"command": "cd ./api && deno run start",
"description": "Start api server instance."
},
- "start:react": {
+ "react": {
"command": "cd ./apps/react && deno run dev",
"description": "Start react application instance."
},
"check": {
- "command": "deno run -A npm:@biomejs/biome check --write ./api ./modules ./platform",
+ "command": "deno run -A npm:@biomejs/biome check --write ./api ./apps/react/src ./modules ./platform",
"description": "Format, lint, and organize imports of the entire project."
},
"test": {
diff --git a/deno.lock b/deno.lock
index 0f53e2b..769b4a3 100644
--- a/deno.lock
+++ b/deno.lock
@@ -3,9 +3,7 @@
"specifiers": {
"npm:@biomejs/biome@*": "2.2.4",
"npm:@biomejs/biome@2.2.4": "2.2.4",
- "npm:@cerbos/core@0.24.1": "0.24.1",
"npm:@cerbos/core@0.25.1": "0.25.1_@bufbuild+protobuf@2.10.1",
- "npm:@cerbos/http@0.23.1": "0.23.1",
"npm:@cerbos/http@0.23.3": "0.23.3_@bufbuild+protobuf@2.10.1_@cerbos+api@0.2.0",
"npm:@eslint/js@9.35.0": "9.35.0",
"npm:@jsr/std__assert@1.0.14": "1.0.14",
@@ -14,8 +12,15 @@
"npm:@jsr/valkyr__db@2.0.0": "2.0.0",
"npm:@jsr/valkyr__event-emitter@1.0.1": "1.0.1",
"npm:@jsr/valkyr__event-store@2.0.1": "2.0.1",
- "npm:@jsr/valkyr__inverse@1.0.1": "1.0.1",
"npm:@jsr/valkyr__json-rpc@1.1.0": "1.1.0",
+ "npm:@radix-ui/react-avatar@^1.1.11": "1.1.11_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1",
+ "npm:@radix-ui/react-dialog@^1.1.15": "1.1.15_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1",
+ "npm:@radix-ui/react-dropdown-menu@^2.1.16": "2.1.16_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1",
+ "npm:@radix-ui/react-scroll-area@^1.2.10": "1.2.10_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1",
+ "npm:@radix-ui/react-separator@^1.1.8": "1.1.8_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1",
+ "npm:@radix-ui/react-slot@^1.2.4": "1.2.4_@types+react@19.1.13_react@19.1.1",
+ "npm:@radix-ui/react-tooltip@^1.2.8": "1.2.8_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1",
+ "npm:@tabler/icons-react@3.35.0": "3.35.0_react@19.1.1",
"npm:@tailwindcss/vite@4.1.13": "4.1.13_vite@7.1.6__@types+node@24.2.0__picomatch@4.0.3_@types+node@24.2.0",
"npm:@tanstack/react-query@5.89.0": "5.89.0_react@19.1.1",
"npm:@tanstack/react-router-devtools@1.131.47": "1.131.47_@tanstack+react-router@1.131.47__react@19.1.1__react-dom@19.1.1___react@19.1.1_react@19.1.1_react-dom@19.1.1__react@19.1.1",
@@ -24,24 +29,29 @@
"npm:@types/react-dom@19.1.9": "19.1.9_@types+react@19.1.13",
"npm:@types/react@19.1.13": "19.1.13",
"npm:@vitejs/plugin-react@4.7.0": "4.7.0_vite@7.1.6__@types+node@24.2.0__picomatch@4.0.3_@babel+core@7.28.4_@types+node@24.2.0",
- "npm:better-auth@1.3.16": "1.3.16_react@19.1.1_react-dom@19.1.1__react@19.1.1",
- "npm:cookie@1.0.2": "1.0.2",
+ "npm:@zitadel/react@1.1.0": "1.1.0",
+ "npm:class-variance-authority@~0.7.1": "0.7.1",
+ "npm:clsx@^2.1.1": "2.1.1",
"npm:eslint-plugin-react-hooks@5.2.0": "5.2.0_eslint@9.35.0",
"npm:eslint-plugin-react-refresh@0.4.20": "0.4.20_eslint@9.35.0",
"npm:eslint@9.35.0": "9.35.0",
"npm:fast-equals@5.2.2": "5.2.2",
"npm:globals@16.4.0": "16.4.0",
"npm:jose@6.1.0": "6.1.0",
- "npm:mongodb@6.20.0": "6.20.0",
+ "npm:lucide-react@0.554": "0.554.0_react@19.1.1",
"npm:nanoid@5.1.5": "5.1.5",
"npm:path-to-regexp@8": "8.3.0",
+ "npm:postgres@3.4.7": "3.4.7",
"npm:react-dom@19.1.1": "19.1.1_react@19.1.1",
"npm:react@19.1.1": "19.1.1",
+ "npm:tailwind-merge@^3.4.0": "3.4.0",
+ "npm:tailwindcss-animate@^1.0.7": "1.0.7_tailwindcss@4.1.13",
"npm:tailwindcss@4.1.13": "4.1.13",
+ "npm:tw-animate-css@1.4.0": "1.4.0",
"npm:typescript-eslint@8.44.0": "8.44.0_eslint@9.35.0_typescript@5.9.2_@typescript-eslint+parser@8.44.0__eslint@9.35.0__typescript@5.9.2",
"npm:typescript@5.9.2": "5.9.2",
"npm:vite@7.1.6": "7.1.6_@types+node@24.2.0_picomatch@4.0.3",
- "npm:zod@4.1.11": "4.1.11"
+ "npm:zod@4.1.12": "4.1.12"
},
"npm": {
"@babel/code-frame@7.27.1": {
@@ -181,12 +191,6 @@
"@babel/helper-validator-identifier"
]
},
- "@better-auth/utils@0.3.0": {
- "integrity": "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="
- },
- "@better-fetch/fetch@1.1.18": {
- "integrity": "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA=="
- },
"@biomejs/biome@2.2.4": {
"integrity": "sha512-TBHU5bUy/Ok6m8c0y3pZiuO/BZoY/OcGxoLlrfQof5s8ISVwbVBdFINPQZyFfKwil8XibYWb7JMwnT8wT4WVPg==",
"optionalDependencies": [
@@ -250,12 +254,6 @@
"@bufbuild/protobuf"
]
},
- "@cerbos/core@0.24.1": {
- "integrity": "sha512-Gt9ETQR3WDVcPlxN+HiGUDtNgWFulwS5ZjBgzJFsdb7e2GCw0tOPE9Ex1qHNZvG/0JHpFWJWIiYaSKyXcp35YQ==",
- "dependencies": [
- "uuid"
- ]
- },
"@cerbos/core@0.25.1_@bufbuild+protobuf@2.10.1": {
"integrity": "sha512-aPi8IqqgGHq9xyoBk6dYAKQ1U1athW0YZfI+7lzxxwpLlNFdZ9EwJLhaRSUFgYpUS2TBWDtX094Yn1kgB1esCQ==",
"dependencies": [
@@ -264,19 +262,12 @@
"uuid"
]
},
- "@cerbos/http@0.23.1": {
- "integrity": "sha512-XzWFS6L7M+oUnjGEFIoQygtlmZy3zOpUobN6spGp1MAaT6GQJMRFK8P8xhY2BQjTIhqYgnoiEFOAULTkbgNIjg==",
- "dependencies": [
- "@cerbos/core@0.24.1",
- "qs"
- ]
- },
"@cerbos/http@0.23.3_@bufbuild+protobuf@2.10.1_@cerbos+api@0.2.0": {
"integrity": "sha512-yf8s4v+T4sf/ZiLorHpXhdStOr+q5XEjF2m/yvpcR7E/7e5eGCr5yEov9NIgfRQg1HGW8h+B6CIFBl9amSsaGw==",
"dependencies": [
"@bufbuild/protobuf",
"@cerbos/api",
- "@cerbos/core@0.25.1_@bufbuild+protobuf@2.10.1",
+ "@cerbos/core",
"qs"
]
},
@@ -464,8 +455,29 @@
"levn"
]
},
- "@hexagon/base64@1.1.28": {
- "integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw=="
+ "@floating-ui/core@1.7.3": {
+ "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
+ "dependencies": [
+ "@floating-ui/utils"
+ ]
+ },
+ "@floating-ui/dom@1.7.4": {
+ "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
+ "dependencies": [
+ "@floating-ui/core",
+ "@floating-ui/utils"
+ ]
+ },
+ "@floating-ui/react-dom@2.1.6_react@19.1.1_react-dom@19.1.1__react@19.1.1": {
+ "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==",
+ "dependencies": [
+ "@floating-ui/dom",
+ "react",
+ "react-dom"
+ ]
+ },
+ "@floating-ui/utils@0.2.10": {
+ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="
},
"@humanfs/core@0.19.1": {
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="
@@ -601,10 +613,6 @@
],
"tarball": "https://npm.jsr.io/~/11/@jsr/valkyr__event-store/2.0.1.tgz"
},
- "@jsr/valkyr__inverse@1.0.1": {
- "integrity": "sha512-uZpzPct9FGobgl6H+iR3VJlzZbTFVmJSrB4z5In8zHgIJCkmgYj0diU3soU6MuiKR7SFBfD4PGSuUpTTJHNMlg==",
- "tarball": "https://npm.jsr.io/~/11/@jsr/valkyr__inverse/1.0.1.tgz"
- },
"@jsr/valkyr__json-rpc@1.1.0": {
"integrity": "sha512-i1dwWLI29i5mqRvS2NbI3jUyw8uZuO71hJRvT5+sGAexG8RmQJP4N+ETJkxq0RNwNAGGG1bocuzdqnawa2ahIA==",
"dependencies": [
@@ -623,21 +631,12 @@
],
"tarball": "https://npm.jsr.io/~/11/@jsr/valkyr__testcontainers/2.0.2.tgz"
},
- "@levischuck/tiny-cbor@0.2.11": {
- "integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="
- },
"@mongodb-js/saslprep@1.3.0": {
"integrity": "sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==",
"dependencies": [
"sparse-bitfield"
]
},
- "@noble/ciphers@2.0.1": {
- "integrity": "sha512-xHK3XHPUW8DTAobU+G0XT+/w+JLM7/8k1UFdB5xg/zTFPnFCobhftzw8wl4Lw2aq/Rvir5pxfZV5fEazmeCJ2g=="
- },
- "@noble/hashes@2.0.0": {
- "integrity": "sha512-h8VUBlE8R42+XIDO229cgisD287im3kdY6nbNZJFjc6ZvKIXPYXe6Vc/t+kyjFdMFyt5JpapzTsEg8n63w5/lw=="
- },
"@nodelib/fs.scandir@2.1.5": {
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
"dependencies": [
@@ -655,49 +654,524 @@
"fastq"
]
},
- "@peculiar/asn1-android@2.5.0": {
- "integrity": "sha512-t8A83hgghWQkcneRsgGs2ebAlRe54ns88p7ouv8PW2tzF1nAW4yHcL4uZKrFpIU+uszIRzTkcCuie37gpkId0A==",
+ "@radix-ui/number@1.1.1": {
+ "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="
+ },
+ "@radix-ui/primitive@1.1.3": {
+ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="
+ },
+ "@radix-ui/react-arrow@1.1.7_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1": {
+ "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
"dependencies": [
- "@peculiar/asn1-schema",
- "asn1js",
- "tslib"
+ "@radix-ui/react-primitive@2.1.3_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
]
},
- "@peculiar/asn1-ecc@2.5.0": {
- "integrity": "sha512-t4eYGNhXtLRxaP50h3sfO6aJebUCDGQACoeexcelL4roMFRRVgB20yBIu2LxsPh/tdW9I282gNgMOyg3ywg/mg==",
+ "@radix-ui/react-avatar@1.1.11_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1": {
+ "integrity": "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==",
"dependencies": [
- "@peculiar/asn1-schema",
- "@peculiar/asn1-x509",
- "asn1js",
- "tslib"
+ "@radix-ui/react-context@1.1.3_@types+react@19.1.13_react@19.1.1",
+ "@radix-ui/react-primitive@2.1.4_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1",
+ "@radix-ui/react-use-callback-ref",
+ "@radix-ui/react-use-is-hydrated",
+ "@radix-ui/react-use-layout-effect",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
]
},
- "@peculiar/asn1-rsa@2.5.0": {
- "integrity": "sha512-qMZ/vweiTHy9syrkkqWFvbT3eLoedvamcUdnnvwyyUNv5FgFXA3KP8td+ATibnlZ0EANW5PYRm8E6MJzEB/72Q==",
+ "@radix-ui/react-collection@1.1.7_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1": {
+ "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
"dependencies": [
- "@peculiar/asn1-schema",
- "@peculiar/asn1-x509",
- "asn1js",
- "tslib"
+ "@radix-ui/react-compose-refs",
+ "@radix-ui/react-context@1.1.2_@types+react@19.1.13_react@19.1.1",
+ "@radix-ui/react-primitive@2.1.3_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1",
+ "@radix-ui/react-slot@1.2.3_@types+react@19.1.13_react@19.1.1",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
]
},
- "@peculiar/asn1-schema@2.5.0": {
- "integrity": "sha512-YM/nFfskFJSlHqv59ed6dZlLZqtZQwjRVJ4bBAiWV08Oc+1rSd5lDZcBEx0lGDHfSoH3UziI2pXt2UM33KerPQ==",
+ "@radix-ui/react-compose-refs@1.1.2_@types+react@19.1.13_react@19.1.1": {
+ "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"dependencies": [
- "asn1js",
- "pvtsutils",
- "tslib"
+ "@types/react",
+ "react"
+ ],
+ "optionalPeers": [
+ "@types/react"
]
},
- "@peculiar/asn1-x509@2.5.0": {
- "integrity": "sha512-CpwtMCTJvfvYTFMuiME5IH+8qmDe3yEWzKHe7OOADbGfq7ohxeLaXwQo0q4du3qs0AII3UbLCvb9NF/6q0oTKQ==",
+ "@radix-ui/react-context@1.1.2_@types+react@19.1.13_react@19.1.1": {
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
"dependencies": [
- "@peculiar/asn1-schema",
- "asn1js",
- "pvtsutils",
- "tslib"
+ "@types/react",
+ "react"
+ ],
+ "optionalPeers": [
+ "@types/react"
]
},
+ "@radix-ui/react-context@1.1.3_@types+react@19.1.13_react@19.1.1": {
+ "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==",
+ "dependencies": [
+ "@types/react",
+ "react"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
+ "@radix-ui/react-dialog@1.1.15_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1": {
+ "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
+ "dependencies": [
+ "@radix-ui/primitive",
+ "@radix-ui/react-compose-refs",
+ "@radix-ui/react-context@1.1.2_@types+react@19.1.13_react@19.1.1",
+ "@radix-ui/react-dismissable-layer",
+ "@radix-ui/react-focus-guards",
+ "@radix-ui/react-focus-scope",
+ "@radix-ui/react-id",
+ "@radix-ui/react-portal",
+ "@radix-ui/react-presence",
+ "@radix-ui/react-primitive@2.1.3_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1",
+ "@radix-ui/react-slot@1.2.3_@types+react@19.1.13_react@19.1.1",
+ "@radix-ui/react-use-controllable-state",
+ "@types/react",
+ "@types/react-dom",
+ "aria-hidden",
+ "react",
+ "react-dom",
+ "react-remove-scroll"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
+ "@radix-ui/react-direction@1.1.1_@types+react@19.1.13_react@19.1.1": {
+ "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
+ "dependencies": [
+ "@types/react",
+ "react"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
+ "@radix-ui/react-dismissable-layer@1.1.11_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1": {
+ "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
+ "dependencies": [
+ "@radix-ui/primitive",
+ "@radix-ui/react-compose-refs",
+ "@radix-ui/react-primitive@2.1.3_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1",
+ "@radix-ui/react-use-callback-ref",
+ "@radix-ui/react-use-escape-keydown",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
+ "@radix-ui/react-dropdown-menu@2.1.16_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1": {
+ "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==",
+ "dependencies": [
+ "@radix-ui/primitive",
+ "@radix-ui/react-compose-refs",
+ "@radix-ui/react-context@1.1.2_@types+react@19.1.13_react@19.1.1",
+ "@radix-ui/react-id",
+ "@radix-ui/react-menu",
+ "@radix-ui/react-primitive@2.1.3_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1",
+ "@radix-ui/react-use-controllable-state",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
+ "@radix-ui/react-focus-guards@1.1.3_@types+react@19.1.13_react@19.1.1": {
+ "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
+ "dependencies": [
+ "@types/react",
+ "react"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
+ "@radix-ui/react-focus-scope@1.1.7_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1": {
+ "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
+ "dependencies": [
+ "@radix-ui/react-compose-refs",
+ "@radix-ui/react-primitive@2.1.3_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1",
+ "@radix-ui/react-use-callback-ref",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
+ "@radix-ui/react-id@1.1.1_@types+react@19.1.13_react@19.1.1": {
+ "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
+ "dependencies": [
+ "@radix-ui/react-use-layout-effect",
+ "@types/react",
+ "react"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
+ "@radix-ui/react-menu@2.1.16_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1": {
+ "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==",
+ "dependencies": [
+ "@radix-ui/primitive",
+ "@radix-ui/react-collection",
+ "@radix-ui/react-compose-refs",
+ "@radix-ui/react-context@1.1.2_@types+react@19.1.13_react@19.1.1",
+ "@radix-ui/react-direction",
+ "@radix-ui/react-dismissable-layer",
+ "@radix-ui/react-focus-guards",
+ "@radix-ui/react-focus-scope",
+ "@radix-ui/react-id",
+ "@radix-ui/react-popper",
+ "@radix-ui/react-portal",
+ "@radix-ui/react-presence",
+ "@radix-ui/react-primitive@2.1.3_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1",
+ "@radix-ui/react-roving-focus",
+ "@radix-ui/react-slot@1.2.3_@types+react@19.1.13_react@19.1.1",
+ "@radix-ui/react-use-callback-ref",
+ "@types/react",
+ "@types/react-dom",
+ "aria-hidden",
+ "react",
+ "react-dom",
+ "react-remove-scroll"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
+ "@radix-ui/react-popper@1.2.8_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1": {
+ "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
+ "dependencies": [
+ "@floating-ui/react-dom",
+ "@radix-ui/react-arrow",
+ "@radix-ui/react-compose-refs",
+ "@radix-ui/react-context@1.1.2_@types+react@19.1.13_react@19.1.1",
+ "@radix-ui/react-primitive@2.1.3_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1",
+ "@radix-ui/react-use-callback-ref",
+ "@radix-ui/react-use-layout-effect",
+ "@radix-ui/react-use-rect",
+ "@radix-ui/react-use-size",
+ "@radix-ui/rect",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
+ "@radix-ui/react-portal@1.1.9_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1": {
+ "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
+ "dependencies": [
+ "@radix-ui/react-primitive@2.1.3_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1",
+ "@radix-ui/react-use-layout-effect",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
+ "@radix-ui/react-presence@1.1.5_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1": {
+ "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
+ "dependencies": [
+ "@radix-ui/react-compose-refs",
+ "@radix-ui/react-use-layout-effect",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
+ "@radix-ui/react-primitive@2.1.3_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1": {
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "dependencies": [
+ "@radix-ui/react-slot@1.2.3_@types+react@19.1.13_react@19.1.1",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
+ "@radix-ui/react-primitive@2.1.4_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1": {
+ "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==",
+ "dependencies": [
+ "@radix-ui/react-slot@1.2.4_@types+react@19.1.13_react@19.1.1",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
+ "@radix-ui/react-roving-focus@1.1.11_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1": {
+ "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
+ "dependencies": [
+ "@radix-ui/primitive",
+ "@radix-ui/react-collection",
+ "@radix-ui/react-compose-refs",
+ "@radix-ui/react-context@1.1.2_@types+react@19.1.13_react@19.1.1",
+ "@radix-ui/react-direction",
+ "@radix-ui/react-id",
+ "@radix-ui/react-primitive@2.1.3_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1",
+ "@radix-ui/react-use-callback-ref",
+ "@radix-ui/react-use-controllable-state",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
+ "@radix-ui/react-scroll-area@1.2.10_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1": {
+ "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==",
+ "dependencies": [
+ "@radix-ui/number",
+ "@radix-ui/primitive",
+ "@radix-ui/react-compose-refs",
+ "@radix-ui/react-context@1.1.2_@types+react@19.1.13_react@19.1.1",
+ "@radix-ui/react-direction",
+ "@radix-ui/react-presence",
+ "@radix-ui/react-primitive@2.1.3_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1",
+ "@radix-ui/react-use-callback-ref",
+ "@radix-ui/react-use-layout-effect",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
+ "@radix-ui/react-separator@1.1.8_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1": {
+ "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==",
+ "dependencies": [
+ "@radix-ui/react-primitive@2.1.4_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
+ "@radix-ui/react-slot@1.2.3_@types+react@19.1.13_react@19.1.1": {
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "dependencies": [
+ "@radix-ui/react-compose-refs",
+ "@types/react",
+ "react"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
+ "@radix-ui/react-slot@1.2.4_@types+react@19.1.13_react@19.1.1": {
+ "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==",
+ "dependencies": [
+ "@radix-ui/react-compose-refs",
+ "@types/react",
+ "react"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
+ "@radix-ui/react-tooltip@1.2.8_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1": {
+ "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==",
+ "dependencies": [
+ "@radix-ui/primitive",
+ "@radix-ui/react-compose-refs",
+ "@radix-ui/react-context@1.1.2_@types+react@19.1.13_react@19.1.1",
+ "@radix-ui/react-dismissable-layer",
+ "@radix-ui/react-id",
+ "@radix-ui/react-popper",
+ "@radix-ui/react-portal",
+ "@radix-ui/react-presence",
+ "@radix-ui/react-primitive@2.1.3_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1",
+ "@radix-ui/react-slot@1.2.3_@types+react@19.1.13_react@19.1.1",
+ "@radix-ui/react-use-controllable-state",
+ "@radix-ui/react-visually-hidden",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
+ "@radix-ui/react-use-callback-ref@1.1.1_@types+react@19.1.13_react@19.1.1": {
+ "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
+ "dependencies": [
+ "@types/react",
+ "react"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
+ "@radix-ui/react-use-controllable-state@1.2.2_@types+react@19.1.13_react@19.1.1": {
+ "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+ "dependencies": [
+ "@radix-ui/react-use-effect-event",
+ "@radix-ui/react-use-layout-effect",
+ "@types/react",
+ "react"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
+ "@radix-ui/react-use-effect-event@0.0.2_@types+react@19.1.13_react@19.1.1": {
+ "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
+ "dependencies": [
+ "@radix-ui/react-use-layout-effect",
+ "@types/react",
+ "react"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
+ "@radix-ui/react-use-escape-keydown@1.1.1_@types+react@19.1.13_react@19.1.1": {
+ "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
+ "dependencies": [
+ "@radix-ui/react-use-callback-ref",
+ "@types/react",
+ "react"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
+ "@radix-ui/react-use-is-hydrated@0.1.0_@types+react@19.1.13_react@19.1.1": {
+ "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==",
+ "dependencies": [
+ "@types/react",
+ "react",
+ "use-sync-external-store"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
+ "@radix-ui/react-use-layout-effect@1.1.1_@types+react@19.1.13_react@19.1.1": {
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "dependencies": [
+ "@types/react",
+ "react"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
+ "@radix-ui/react-use-rect@1.1.1_@types+react@19.1.13_react@19.1.1": {
+ "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
+ "dependencies": [
+ "@radix-ui/rect",
+ "@types/react",
+ "react"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
+ "@radix-ui/react-use-size@1.1.1_@types+react@19.1.13_react@19.1.1": {
+ "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
+ "dependencies": [
+ "@radix-ui/react-use-layout-effect",
+ "@types/react",
+ "react"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
+ "@radix-ui/react-visually-hidden@1.2.3_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1": {
+ "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
+ "dependencies": [
+ "@radix-ui/react-primitive@2.1.3_@types+react@19.1.13_@types+react-dom@19.1.9__@types+react@19.1.13_react@19.1.1_react-dom@19.1.1__react@19.1.1",
+ "@types/react",
+ "@types/react-dom",
+ "react",
+ "react-dom"
+ ],
+ "optionalPeers": [
+ "@types/react",
+ "@types/react-dom"
+ ]
+ },
+ "@radix-ui/rect@1.1.1": {
+ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="
+ },
"@rolldown/pluginutils@1.0.0-beta.27": {
"integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="
},
@@ -811,21 +1285,16 @@
"os": ["win32"],
"cpu": ["x64"]
},
- "@simplewebauthn/browser@13.2.0": {
- "integrity": "sha512-N3fuA1AAnTo5gCStYoIoiasPccC+xPLx2YU88Dv0GeAmPQTWHETlZQq5xZ0DgUq1H9loXMWQH5qqUjcI7BHJ1A=="
- },
- "@simplewebauthn/server@13.1.2": {
- "integrity": "sha512-VwoDfvLXSCaRiD+xCIuyslU0HLxVggeE5BL06+GbsP2l1fGf5op8e0c3ZtKoi+vSg1q4ikjtAghC23ze2Q3H9g==",
+ "@tabler/icons-react@3.35.0_react@19.1.1": {
+ "integrity": "sha512-XG7t2DYf3DyHT5jxFNp5xyLVbL4hMJYJhiSdHADzAjLRYfL7AnjlRfiHDHeXxkb2N103rEIvTsBRazxXtAUz2g==",
"dependencies": [
- "@hexagon/base64",
- "@levischuck/tiny-cbor",
- "@peculiar/asn1-android",
- "@peculiar/asn1-ecc",
- "@peculiar/asn1-rsa",
- "@peculiar/asn1-schema",
- "@peculiar/asn1-x509"
+ "@tabler/icons",
+ "react"
]
},
+ "@tabler/icons@3.35.0": {
+ "integrity": "sha512-yYXe+gJ56xlZFiXwV9zVoe3FWCGuZ/D7/G4ZIlDtGxSx5CGQK110wrnT29gUj52kEZoxqF7oURTk97GQxELOFQ=="
+ },
"@tailwindcss/node@4.1.13": {
"integrity": "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==",
"dependencies": [
@@ -1171,6 +1640,12 @@
"vite"
]
},
+ "@zitadel/react@1.1.0": {
+ "integrity": "sha512-aMad1iZNpsZgEtUvSIyjCt1TdCzg++OMg3GwdPbFhnHTHwQMoSRB9IrYuD0grHK0TqU7yx283iJO5te2ToRWtA==",
+ "dependencies": [
+ "oidc-client-ts"
+ ]
+ },
"acorn-jsx@5.3.2_acorn@8.15.0": {
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
"dependencies": [
@@ -1199,11 +1674,9 @@
"argparse@2.0.1": {
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
- "asn1js@3.0.6": {
- "integrity": "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==",
+ "aria-hidden@1.2.6": {
+ "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
"dependencies": [
- "pvtsutils",
- "pvutils",
"tslib"
]
},
@@ -1214,39 +1687,6 @@
"integrity": "sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==",
"bin": true
},
- "better-auth@1.3.16_react@19.1.1_react-dom@19.1.1__react@19.1.1": {
- "integrity": "sha512-WHU3QTtkBdwMlzM4AbOSoti0aPVTw1IfdomNCcP9Mo0J03ENn7z2a6/Vz9fimHmjpVWMqRvW72cYcc30LLFFOw==",
- "dependencies": [
- "@better-auth/utils",
- "@better-fetch/fetch",
- "@noble/ciphers",
- "@noble/hashes",
- "@simplewebauthn/browser",
- "@simplewebauthn/server",
- "better-call",
- "defu",
- "jose",
- "kysely",
- "nanostores",
- "react",
- "react-dom",
- "zod"
- ],
- "optionalPeers": [
- "react",
- "react-dom"
- ]
- },
- "better-call@1.0.19": {
- "integrity": "sha512-sI3GcA1SCVa3H+CDHl8W8qzhlrckwXOTKhqq3OOPXjgn5aTOMIqGY34zLY/pHA6tRRMjTUC3lz5Mi7EbDA24Kw==",
- "dependencies": [
- "@better-auth/utils",
- "@better-fetch/fetch",
- "rou3",
- "set-cookie-parser",
- "uncrypto"
- ]
- },
"brace-expansion@1.1.12": {
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dependencies": [
@@ -1310,6 +1750,12 @@
"chownr@3.0.0": {
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="
},
+ "class-variance-authority@0.7.1": {
+ "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
+ "dependencies": [
+ "clsx"
+ ]
+ },
"clsx@2.1.1": {
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="
},
@@ -1331,9 +1777,6 @@
"cookie-es@1.2.2": {
"integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="
},
- "cookie@1.0.2": {
- "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="
- },
"cross-spawn@7.0.6": {
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dependencies": [
@@ -1342,6 +1785,9 @@
"which"
]
},
+ "crypto-js@4.2.0": {
+ "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
+ },
"csstype@3.1.3": {
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
@@ -1354,12 +1800,12 @@
"deep-is@0.1.4": {
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="
},
- "defu@6.1.4": {
- "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="
- },
"detect-libc@2.1.0": {
"integrity": "sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg=="
},
+ "detect-node-es@1.1.0": {
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="
+ },
"dunder-proto@1.0.1": {
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dependencies": [
@@ -1616,6 +2062,9 @@
"math-intrinsics"
]
},
+ "get-nonce@1.0.1": {
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="
+ },
"get-proto@1.0.1": {
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dependencies": [
@@ -1739,15 +2188,15 @@
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"bin": true
},
+ "jwt-decode@3.1.2": {
+ "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
+ },
"keyv@4.5.4": {
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
"dependencies": [
"json-buffer"
]
},
- "kysely@0.28.5": {
- "integrity": "sha512-rlB0I/c6FBDWPcQoDtkxi9zIvpmnV5xoIalfCMSMCa7nuA6VGA3F54TW9mEgX4DVf10sXAWCF5fDbamI/5ZpKA=="
- },
"levn@0.4.1": {
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
"dependencies": [
@@ -1838,6 +2287,12 @@
"yallist@3.1.1"
]
},
+ "lucide-react@0.554.0_react@19.1.1": {
+ "integrity": "sha512-St+z29uthEJVx0Is7ellNkgTEhaeSoA42I7JjOCBCrc5X6LYMGSv0P/2uS5HDLTExP5tpiqRD2PyUEOS6s9UXA==",
+ "dependencies": [
+ "react"
+ ]
+ },
"magic-string@0.30.19": {
"integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
"dependencies": [
@@ -1910,9 +2365,6 @@
"integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==",
"bin": true
},
- "nanostores@1.0.1": {
- "integrity": "sha512-kNZ9xnoJYKg/AfxjrVL4SS0fKX++4awQReGqWnwTRHxeHGZ1FJFVgTqr/eMrNQdp0Tz7M7tG/TDaX8QfHDwVCw=="
- },
"natural-compare@1.4.0": {
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="
},
@@ -1922,6 +2374,13 @@
"object-inspect@1.13.4": {
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="
},
+ "oidc-client-ts@2.4.1": {
+ "integrity": "sha512-IxlGMsbkZPsHJGCliWT3LxjUcYzmiN21656n/Zt2jDncZlBFc//cd8WqFF0Lt681UT3AImM57E6d4N53ziTCYA==",
+ "dependencies": [
+ "crypto-js",
+ "jwt-decode"
+ ]
+ },
"optionator@0.9.4": {
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
"dependencies": [
@@ -1986,15 +2445,6 @@
"punycode@2.3.1": {
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="
},
- "pvtsutils@1.3.6": {
- "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==",
- "dependencies": [
- "tslib"
- ]
- },
- "pvutils@1.1.3": {
- "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ=="
- },
"qs@6.14.0": {
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
"dependencies": [
@@ -2014,6 +2464,45 @@
"react-refresh@0.17.0": {
"integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="
},
+ "react-remove-scroll-bar@2.3.8_@types+react@19.1.13_react@19.1.1": {
+ "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
+ "dependencies": [
+ "@types/react",
+ "react",
+ "react-style-singleton",
+ "tslib"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
+ "react-remove-scroll@2.7.1_@types+react@19.1.13_react@19.1.1": {
+ "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==",
+ "dependencies": [
+ "@types/react",
+ "react",
+ "react-remove-scroll-bar",
+ "react-style-singleton",
+ "tslib",
+ "use-callback-ref",
+ "use-sidecar"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
+ "react-style-singleton@2.2.3_@types+react@19.1.13_react@19.1.1": {
+ "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
+ "dependencies": [
+ "@types/react",
+ "get-nonce",
+ "react",
+ "tslib"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
"react@19.1.1": {
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ=="
},
@@ -2055,9 +2544,6 @@
],
"bin": true
},
- "rou3@0.5.1": {
- "integrity": "sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ=="
- },
"run-parallel@1.2.0": {
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
"dependencies": [
@@ -2090,9 +2576,6 @@
"seroval@1.3.2": {
"integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ=="
},
- "set-cookie-parser@2.7.1": {
- "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="
- },
"shebang-command@2.0.0": {
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dependencies": [
@@ -2164,6 +2647,15 @@
"has-flag"
]
},
+ "tailwind-merge@3.4.0": {
+ "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="
+ },
+ "tailwindcss-animate@1.0.7_tailwindcss@4.1.13": {
+ "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==",
+ "dependencies": [
+ "tailwindcss"
+ ]
+ },
"tailwindcss@4.1.13": {
"integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w=="
},
@@ -2214,6 +2706,9 @@
"tslib@2.8.1": {
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
+ "tw-animate-css@1.4.0": {
+ "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="
+ },
"type-check@0.4.0": {
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
"dependencies": [
@@ -2235,9 +2730,6 @@
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"bin": true
},
- "uncrypto@0.1.3": {
- "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="
- },
"undici-types@7.10.0": {
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="
},
@@ -2256,6 +2748,29 @@
"punycode"
]
},
+ "use-callback-ref@1.3.3_@types+react@19.1.13_react@19.1.1": {
+ "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
+ "dependencies": [
+ "@types/react",
+ "react",
+ "tslib"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
+ "use-sidecar@1.1.3_@types+react@19.1.13_react@19.1.1": {
+ "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
+ "dependencies": [
+ "@types/react",
+ "detect-node-es",
+ "react",
+ "tslib"
+ ],
+ "optionalPeers": [
+ "@types/react"
+ ]
+ },
"use-sync-external-store@1.5.0_react@19.1.1": {
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
"dependencies": [
@@ -2314,8 +2829,8 @@
"yocto-queue@0.1.0": {
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="
},
- "zod@4.1.11": {
- "integrity": "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="
+ "zod@4.1.12": {
+ "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="
}
},
"workspace": {
@@ -2330,7 +2845,7 @@
"api": {
"packageJson": {
"dependencies": [
- "npm:zod@4.1.11"
+ "npm:zod@4.1.12"
]
}
},
@@ -2340,6 +2855,14 @@
"npm:@eslint/js@9.35.0",
"npm:@jsr/valkyr__db@2.0.0",
"npm:@jsr/valkyr__event-emitter@1.0.1",
+ "npm:@radix-ui/react-avatar@^1.1.11",
+ "npm:@radix-ui/react-dialog@^1.1.15",
+ "npm:@radix-ui/react-dropdown-menu@^2.1.16",
+ "npm:@radix-ui/react-scroll-area@^1.2.10",
+ "npm:@radix-ui/react-separator@^1.1.8",
+ "npm:@radix-ui/react-slot@^1.2.4",
+ "npm:@radix-ui/react-tooltip@^1.2.8",
+ "npm:@tabler/icons-react@3.35.0",
"npm:@tailwindcss/vite@4.1.13",
"npm:@tanstack/react-query@5.89.0",
"npm:@tanstack/react-router-devtools@1.131.47",
@@ -2347,38 +2870,32 @@
"npm:@types/react-dom@19.1.9",
"npm:@types/react@19.1.13",
"npm:@vitejs/plugin-react@4.7.0",
+ "npm:@zitadel/react@1.1.0",
+ "npm:class-variance-authority@~0.7.1",
+ "npm:clsx@^2.1.1",
"npm:eslint-plugin-react-hooks@5.2.0",
"npm:eslint-plugin-react-refresh@0.4.20",
"npm:eslint@9.35.0",
"npm:fast-equals@5.2.2",
"npm:globals@16.4.0",
+ "npm:lucide-react@0.554",
"npm:react-dom@19.1.1",
"npm:react@19.1.1",
+ "npm:tailwind-merge@^3.4.0",
+ "npm:tailwindcss-animate@^1.0.7",
"npm:tailwindcss@4.1.13",
+ "npm:tw-animate-css@1.4.0",
"npm:typescript-eslint@8.44.0",
"npm:typescript@5.9.2",
"npm:vite@7.1.6",
- "npm:zod@4.1.11"
+ "npm:zod@4.1.12"
]
}
},
- "modules/iam": {
+ "modules/account": {
"packageJson": {
"dependencies": [
- "npm:@cerbos/core@0.24.1",
- "npm:@cerbos/http@0.23.1",
- "npm:better-auth@1.3.16",
- "npm:cookie@1.0.2",
- "npm:zod@4.1.11"
- ]
- }
- },
- "modules/workspace": {
- "packageJson": {
- "dependencies": [
- "npm:@jsr/valkyr__event-store@2.0.1",
- "npm:cookie@1.0.2",
- "npm:zod@4.1.11"
+ "npm:zod@4.1.12"
]
}
},
@@ -2394,16 +2911,15 @@
"packageJson": {
"dependencies": [
"npm:@jsr/std__dotenv@0.225.5",
- "npm:zod@4.1.11"
+ "npm:zod@4.1.12"
]
}
},
"platform/database": {
"packageJson": {
"dependencies": [
- "npm:@jsr/valkyr__inverse@1.0.1",
- "npm:mongodb@6.20.0",
- "npm:zod@4.1.11"
+ "npm:postgres@3.4.7",
+ "npm:zod@4.1.12"
]
}
},
@@ -2411,7 +2927,7 @@
"packageJson": {
"dependencies": [
"npm:@jsr/valkyr__event-store@2.0.1",
- "npm:zod@4.1.11"
+ "npm:zod@4.1.12"
]
}
},
@@ -2419,14 +2935,14 @@
"packageJson": {
"dependencies": [
"npm:path-to-regexp@8",
- "npm:zod@4.1.11"
+ "npm:zod@4.1.12"
]
}
},
"platform/routes": {
"packageJson": {
"dependencies": [
- "npm:zod@4.1.11"
+ "npm:zod@4.1.12"
]
}
},
@@ -2434,7 +2950,7 @@
"packageJson": {
"dependencies": [
"npm:@jsr/valkyr__json-rpc@1.1.0",
- "npm:zod@4.1.11"
+ "npm:zod@4.1.12"
]
}
},
@@ -2448,7 +2964,7 @@
"platform/spec": {
"packageJson": {
"dependencies": [
- "npm:zod@4.1.11"
+ "npm:zod@4.1.12"
]
}
},
diff --git a/modules/account/client.ts b/modules/account/client.ts
new file mode 100644
index 0000000..8982b52
--- /dev/null
+++ b/modules/account/client.ts
@@ -0,0 +1,4 @@
+export const account = {
+ create: (await import("./routes/create/spec.ts")).default,
+ get: (await import("./routes/get/spec.ts")).default,
+};
diff --git a/modules/account/package.json b/modules/account/package.json
new file mode 100644
index 0000000..dd5612c
--- /dev/null
+++ b/modules/account/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "@module/account",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "exports": {
+ "./server": "./server.ts",
+ "./client": "./client.ts"
+ },
+ "dependencies": {
+ "@platform/relay": "workspace:*",
+ "zod": "4.1.12"
+ }
+}
diff --git a/modules/account/routes/create/handle.ts b/modules/account/routes/create/handle.ts
new file mode 100644
index 0000000..23a8dc8
--- /dev/null
+++ b/modules/account/routes/create/handle.ts
@@ -0,0 +1,5 @@
+import route from "./spec.ts";
+
+export default route.handle(async ({ body }) => {
+ console.log(body);
+});
diff --git a/modules/account/routes/create/spec.ts b/modules/account/routes/create/spec.ts
new file mode 100644
index 0000000..b58c38a
--- /dev/null
+++ b/modules/account/routes/create/spec.ts
@@ -0,0 +1,13 @@
+import { route } from "@platform/relay";
+import z from "zod";
+
+export default route.post("/api/v1/account").body(
+ z.strictObject({
+ tenantId: z.uuid().describe("Tenant identifier the account belongs to"),
+ userId: z.uuid().describe("User identifier the account belongs to"),
+ account: z.strictObject({
+ type: z.string().describe("Type of account being created"),
+ number: z.number().describe("Unique account identifier to create for the account"),
+ }),
+ }),
+);
diff --git a/modules/account/routes/get/handle.ts b/modules/account/routes/get/handle.ts
new file mode 100644
index 0000000..1cd205b
--- /dev/null
+++ b/modules/account/routes/get/handle.ts
@@ -0,0 +1,5 @@
+import route from "./spec.ts";
+
+export default route.handle(async ({ params }) => {
+ console.log(params);
+});
diff --git a/modules/account/routes/get/spec.ts b/modules/account/routes/get/spec.ts
new file mode 100644
index 0000000..f6ec5f1
--- /dev/null
+++ b/modules/account/routes/get/spec.ts
@@ -0,0 +1,6 @@
+import { route } from "@platform/relay";
+import z from "zod";
+
+export default route.get("/api/v1/account/:number").params({
+ number: z.number().describe("Account number to retrieve"),
+});
diff --git a/modules/workspace/client.ts b/modules/account/server.ts
similarity index 100%
rename from modules/workspace/client.ts
rename to modules/account/server.ts
diff --git a/modules/iam/cerbos/client.ts b/modules/iam/cerbos/client.ts
deleted file mode 100644
index 7248ad4..0000000
--- a/modules/iam/cerbos/client.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { HTTP } from "@cerbos/http";
-import { getEnvironmentVariable } from "@platform/config/environment.ts";
-import z from "zod";
-
-export const cerbos = new HTTP(
- getEnvironmentVariable({
- key: "CERBOS_URL",
- type: z.string(),
- fallback: "http://localhost:3592",
- }),
- {
- adminCredentials: {
- username: "cerbos",
- password: "cerbosAdmin",
- },
- },
-);
diff --git a/modules/iam/cerbos/policies/identity.yaml b/modules/iam/cerbos/policies/identity.yaml
deleted file mode 100644
index 57ff976..0000000
--- a/modules/iam/cerbos/policies/identity.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-# yaml-language-server: $schema=https://api.cerbos.dev/latest/cerbos/policy/v1/Policy.schema.json
-# docs: https://docs.cerbos.dev/cerbos/latest/policies/resource_policies
-
-apiVersion: api.cerbos.dev/v1
-resourcePolicy:
- resource: identity
- version: default
- rules:
-
- # Admins can read any identity with limited fields
-
- - actions: ["read", "update"]
- effect: EFFECT_ALLOW
- roles: ["admin"]
-
- # Users can fully read, update, or delete their own identity
-
- - actions: ["read", "update", "delete"]
- effect: EFFECT_ALLOW
- roles: ["user"]
- condition:
- match:
- expr: request.resource.id == request.principal.id
diff --git a/modules/iam/cerbos/policies/role.yaml b/modules/iam/cerbos/policies/role.yaml
deleted file mode 100644
index 72f3dad..0000000
--- a/modules/iam/cerbos/policies/role.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-# yaml-language-server: $schema=https://api.cerbos.dev/latest/cerbos/policy/v1/Policy.schema.json
-# docs: https://docs.cerbos.dev/cerbos/latest/policies/resource_policies
-
-apiVersion: api.cerbos.dev/v1
-resourcePolicy:
- resource: role
- version: default
- rules:
-
- # Admin can manage roles
-
- - actions: ["manage"]
- effect: EFFECT_ALLOW
- roles: ["super"]
diff --git a/modules/iam/cerbos/resources.ts b/modules/iam/cerbos/resources.ts
deleted file mode 100644
index 0e561d3..0000000
--- a/modules/iam/cerbos/resources.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-/*
-export const resources = new ResourceRegistry([
- {
- kind: "identity",
- actions: ["read", "update", "delete"],
- attr: {},
- },
-] as const);
-
-export type Resource = typeof resources.$resource;
-*/
diff --git a/modules/iam/client.ts b/modules/iam/client.ts
deleted file mode 100644
index 54043b2..0000000
--- a/modules/iam/client.ts
+++ /dev/null
@@ -1,163 +0,0 @@
-import { CheckResourcesResponse } from "@cerbos/core";
-import { HttpAdapter, makeClient } from "@platform/relay";
-
-import { config } from "./config.ts";
-import checkResource from "./routes/access/check-resource/spec.ts";
-import checkResources from "./routes/access/check-resources/spec.ts";
-import isAllowed from "./routes/access/is-allowed/spec.ts";
-import getById from "./routes/identities/get/spec.ts";
-import loginByPassword from "./routes/login/code/spec.ts";
-import loginByEmail from "./routes/login/email/spec.ts";
-import loginByCode from "./routes/login/password/spec.ts";
-import me from "./routes/me/spec.ts";
-
-const adapter = new HttpAdapter({
- url: config.url,
-});
-
-const access = makeClient(
- {
- adapter,
- },
- {
- isAllowed,
- checkResource,
- checkResources,
- },
-);
-
-export const identity = makeClient(
- {
- adapter,
- },
- {
- /**
- * TODO ...
- */
- getById,
-
- /**
- * TODO ...
- */
- me,
-
- /**
- * TODO ...
- */
- login: {
- /**
- * TODO ...
- */
- email: loginByEmail,
-
- /**
- * TODO ...
- */
- password: loginByPassword,
-
- /**
- * TODO ...
- */
- code: loginByCode,
- },
-
- access: {
- /**
- * Check if a principal is allowed to perform an action on a resource.
- *
- * @param resource - Resource which we are validating.
- * @param action - Action which we are validating.
- *
- * @example
- *
- * await access.isAllowed(
- * {
- * kind: "document",
- * id: "1",
- * attr: { owner: "user@example.com" },
- * },
- * "view"
- * ); // => true
- */
- isAllowed: async (resource: Resource, action: string) => {
- const response = await access.isAllowed({ body: { resource, action } });
- if ("error" in response) {
- throw response.error;
- }
- return response.data;
- },
-
- /**
- * Check a principal's permissions on a resource.
- *
- * @param resource - Resource which we are validating.
- * @param actions - Actions which we are validating.
- *
- * @example
- *
- * const decision = await access.checkResource(
- * {
- * kind: "document",
- * id: "1",
- * attr: { owner: "user@example.com" },
- * },
- * ["view", "edit"],
- * );
- *
- * decision.isAllowed("view"); // => true
- */
- checkResource: async (resource: Resource, actions: string[]) => {
- const response = await access.checkResource({ body: { resource, actions } });
- if ("error" in response) {
- throw response.error;
- }
- return new CheckResourcesResponse(response.data);
- },
-
- /**
- * Check a principal's permissions on a set of resources.
- *
- * @param resources - Resources which we are validating.
- *
- * @example
- *
- * const decision = await access.checkResources([
- * {
- * resource: {
- * kind: "document",
- * id: "1",
- * attr: { owner: "user@example.com" },
- * },
- * actions: ["view", "edit"],
- * },
- * {
- * resource: {
- * kind: "image",
- * id: "1",
- * attr: { owner: "user@example.com" },
- * },
- * actions: ["delete"],
- * },
- * ]);
- *
- * decision.isAllowed({
- * resource: { kind: "document", id: "1" },
- * action: "view",
- * }); // => true
- */
- checkResources: async (resources: { resource: Resource; actions: string[] }[]) => {
- const response = await access.checkResources({ body: resources });
- if ("error" in response) {
- throw response.error;
- }
- return new CheckResourcesResponse(response.data);
- },
- },
- },
-);
-
-type Resource = {
- kind: string;
- id: string;
- attr: Record;
-};
diff --git a/modules/iam/config.ts b/modules/iam/config.ts
deleted file mode 100644
index 1fe9c30..0000000
--- a/modules/iam/config.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { getEnvironmentVariable } from "@platform/config/environment.ts";
-import type { SerializeOptions } from "cookie";
-import z from "zod";
-
-export const config = {
- url: getEnvironmentVariable({
- key: "IDENTITY_SERVICE_URL",
- type: z.url(),
- fallback: "http://localhost:8370",
- }),
- internal: {
- privateKey: getEnvironmentVariable({
- key: "IDENTITY_PRIVATE_KEY",
- type: z.string(),
- fallback:
- "-----BEGIN PRIVATE KEY-----\n" +
- "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg2WYKMJZUWff5XOWC\n" +
- "XGuU+wmsRzhQGEIzfUoL6rrGoaehRANCAATCpiGiFQxTA76EIVG0cBbj+AFt6BuJ\n" +
- "t4q+zoInPUzkChCdwI+XfAYokrZwBjcyRGluC02HaN3cptrmjYSGSMSx\n" +
- "-----END PRIVATE KEY-----",
- }),
- publicKey: getEnvironmentVariable({
- key: "IDENTITY_PUBLIC_KEY",
- type: z.string(),
- fallback:
- "-----BEGIN PUBLIC KEY-----\n" +
- "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwqYhohUMUwO+hCFRtHAW4/gBbegb\n" +
- "ibeKvs6CJz1M5AoQncCPl3wGKJK2cAY3MkRpbgtNh2jd3Kba5o2EhkjEsQ==\n" +
- "-----END PUBLIC KEY-----",
- }),
- },
- cookie: (maxAge: number) =>
- ({
- httpOnly: true,
- secure: getEnvironmentVariable({
- key: "AUTH_COOKIE_SECURE",
- type: z.coerce.boolean(),
- fallback: "false",
- }), // Set to true for HTTPS in production
- maxAge,
- path: "/",
- sameSite: "strict",
- }) satisfies SerializeOptions,
-};
diff --git a/modules/iam/models/principal.ts b/modules/iam/models/principal.ts
deleted file mode 100644
index c0884f2..0000000
--- a/modules/iam/models/principal.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { makeDocumentParser } from "@platform/database/utilities.ts";
-import z from "zod";
-
-export enum PrincipalTypeId {
- User = 1,
- Group = 2,
- Other = 99,
-}
-
-export const PRINCIPAL_TYPE_NAMES = {
- [PrincipalTypeId.User]: "User",
- [PrincipalTypeId.Group]: "Group",
- [PrincipalTypeId.Other]: "Other",
-};
-
-/*
- |--------------------------------------------------------------------------------
- | Schema
- |--------------------------------------------------------------------------------
- */
-
-export const PrincipalSchema = z.object({
- id: z.string(),
- type: z.strictObject({
- id: z.enum(PrincipalTypeId),
- name: z.string(),
- }),
- roles: z.array(z.string()),
- attr: z.record(z.string(), z.any()),
-});
-
-/*
- |--------------------------------------------------------------------------------
- | Parsers
- |--------------------------------------------------------------------------------
- */
-
-export const parsePrincipal = makeDocumentParser(PrincipalSchema);
-
-/*
- |--------------------------------------------------------------------------------
- | Types
- |--------------------------------------------------------------------------------
- */
-
-export type Principal = z.infer;
diff --git a/modules/iam/models/session.ts b/modules/iam/models/session.ts
deleted file mode 100644
index 5fdc4f6..0000000
--- a/modules/iam/models/session.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import z from "zod";
-
-/*
- |--------------------------------------------------------------------------------
- | Schema
- |--------------------------------------------------------------------------------
- */
-
-export const SessionSchema = z.object({
- id: z.string(),
- userId: z.string(),
- token: z.string(),
- ipAddress: z.string().nullable().optional(),
- userAgent: z.string().nullable().optional(),
- createdAt: z.coerce.date(),
- updatedAt: z.coerce.date(),
- expiresAt: z.coerce.date(),
-});
-
-/*
- |--------------------------------------------------------------------------------
- | Types
- |--------------------------------------------------------------------------------
- */
-
-export type Session = z.infer;
diff --git a/modules/iam/package.json b/modules/iam/package.json
deleted file mode 100644
index a6565ed..0000000
--- a/modules/iam/package.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "name": "@modules/identity",
- "version": "0.0.0",
- "private": true,
- "type": "module",
- "exports": {
- "./client.ts": "./client.ts",
- "./server.ts": "./server.ts",
- "./types.ts": "./types.ts"
- },
- "dependencies": {
- "@cerbos/core": "0.24.1",
- "@cerbos/http": "0.23.1",
- "@platform/config": "workspace:*",
- "@platform/logger": "workspace:*",
- "@platform/relay": "workspace:*",
- "@platform/storage": "workspace:*",
- "@platform/vault": "workspace:*",
- "better-auth": "1.3.16",
- "cookie": "1.0.2",
- "zod": "4.1.11"
- }
-}
diff --git a/modules/iam/routes/access/check-resource/handle.ts b/modules/iam/routes/access/check-resource/handle.ts
deleted file mode 100644
index 4d67f6c..0000000
--- a/modules/iam/routes/access/check-resource/handle.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { cerbos } from "../../../cerbos/client.ts";
-import route from "./spec.ts";
-
-export default route.access("session").handle(async ({ body: { resource, actions } }, { principal }) => {
- return cerbos.checkResource({ principal, resource, actions });
-});
diff --git a/modules/iam/routes/access/check-resource/spec.ts b/modules/iam/routes/access/check-resource/spec.ts
deleted file mode 100644
index 3984730..0000000
--- a/modules/iam/routes/access/check-resource/spec.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { route } from "@platform/relay";
-import z from "zod";
-
-export default route
- .post("/api/v1/identity/access/check-resource")
- .body(
- z.strictObject({
- resource: z.strictObject({
- kind: z.string(),
- id: z.string(),
- attr: z.record(z.string(), z.any()),
- }),
- actions: z.array(z.string()),
- }),
- )
- .response(z.any());
diff --git a/modules/iam/routes/access/check-resources/handle.ts b/modules/iam/routes/access/check-resources/handle.ts
deleted file mode 100644
index 2d0d5d1..0000000
--- a/modules/iam/routes/access/check-resources/handle.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { cerbos } from "../../../cerbos/client.ts";
-import route from "./spec.ts";
-
-export default route.access("session").handle(async ({ body: resources }, { principal }) => {
- return cerbos.checkResources({ principal, resources });
-});
diff --git a/modules/iam/routes/access/check-resources/spec.ts b/modules/iam/routes/access/check-resources/spec.ts
deleted file mode 100644
index 60e1d90..0000000
--- a/modules/iam/routes/access/check-resources/spec.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { route } from "@platform/relay";
-import z from "zod";
-
-export default route
- .post("/api/v1/identity/access/check-resources")
- .body(
- z.array(
- z.strictObject({
- resource: z.strictObject({
- kind: z.string(),
- id: z.string(),
- attr: z.record(z.string(), z.any()),
- }),
- actions: z.array(z.string()),
- }),
- ),
- )
- .response(z.any());
diff --git a/modules/iam/routes/access/is-allowed/handle.ts b/modules/iam/routes/access/is-allowed/handle.ts
deleted file mode 100644
index c67af9c..0000000
--- a/modules/iam/routes/access/is-allowed/handle.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { cerbos } from "../../../cerbos/client.ts";
-import route from "./spec.ts";
-
-export default route.access("session").handle(async ({ body: { resource, action } }, { principal }) => {
- return cerbos.isAllowed({ principal, resource, action });
-});
diff --git a/modules/iam/routes/access/is-allowed/spec.ts b/modules/iam/routes/access/is-allowed/spec.ts
deleted file mode 100644
index 8728df5..0000000
--- a/modules/iam/routes/access/is-allowed/spec.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { route } from "@platform/relay";
-import z from "zod";
-
-export default route
- .post("/api/v1/identity/access/is-allowed")
- .body(
- z.strictObject({
- resource: z.strictObject({
- kind: z.string(),
- id: z.string(),
- attr: z.record(z.string(), z.any()),
- }),
- action: z.string(),
- }),
- )
- .response(z.boolean());
diff --git a/modules/iam/routes/identities/get/handle.ts b/modules/iam/routes/identities/get/handle.ts
deleted file mode 100644
index a0d09f2..0000000
--- a/modules/iam/routes/identities/get/handle.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { ForbiddenError, NotFoundError } from "@platform/relay";
-
-import { getPrincipalById } from "../../../services/database.ts";
-import route from "./spec.ts";
-
-export default route.access("session").handle(async ({ params: { id } }, { access }) => {
- const principal = await getPrincipalById(id);
- if (principal === undefined) {
- return new NotFoundError("Identity does not exist, or has been removed.");
- }
- const decision = await access.isAllowed({ kind: "identity", id, attr: {} }, "read");
- if (decision === false) {
- return new ForbiddenError("You do not have permission to view this identity.");
- }
- return principal;
-});
diff --git a/modules/iam/routes/identities/get/spec.ts b/modules/iam/routes/identities/get/spec.ts
deleted file mode 100644
index 828389e..0000000
--- a/modules/iam/routes/identities/get/spec.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { ForbiddenError, NotFoundError, route, UnauthorizedError } from "@platform/relay";
-import z from "zod";
-
-export default route
- .get("/api/v1/identity/:id")
- .params({
- id: z.string(),
- })
- .errors([UnauthorizedError, ForbiddenError, NotFoundError])
- .response(z.any());
diff --git a/modules/iam/routes/identities/update/handle.ts b/modules/iam/routes/identities/update/handle.ts
deleted file mode 100644
index bb21965..0000000
--- a/modules/iam/routes/identities/update/handle.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { ForbiddenError, NotFoundError } from "@platform/relay";
-
-import { getPrincipalById, setPrincipalAttributesById } from "../../../services/database.ts";
-import route from "./spec.ts";
-
-export default route.access("session").handle(async ({ params: { id }, body: ops }, { access }) => {
- const principal = await getPrincipalById(id);
- if (principal === undefined) {
- return new NotFoundError();
- }
- const decision = await access.isAllowed({ kind: "identity", id: principal.id, attr: principal.attr }, "update");
- if (decision === false) {
- return new ForbiddenError("You do not have permission to update this identity.");
- }
- const attr = principal.attr;
- for (const op of ops) {
- switch (op.type) {
- case "add": {
- attr[op.key] = op.value;
- break;
- }
- case "push": {
- if (attr[op.key] === undefined) {
- attr[op.key] = op.values;
- } else {
- attr[op.key] = [...attr[op.key], ...op.values];
- }
- break;
- }
- case "pop": {
- if (Array.isArray(attr[op.key])) {
- attr[op.key] = attr[op.key].filter((value: any) => op.values.includes(value) === false);
- }
- break;
- }
- case "remove": {
- delete attr[op.key];
- break;
- }
- }
- }
- await setPrincipalAttributesById(id, attr);
-});
diff --git a/modules/iam/routes/identities/update/spec.ts b/modules/iam/routes/identities/update/spec.ts
deleted file mode 100644
index 9d3a0f2..0000000
--- a/modules/iam/routes/identities/update/spec.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { ForbiddenError, NotFoundError, route, UnauthorizedError } from "@platform/relay";
-import z from "zod";
-
-export default route
- .put("/api/v1/identity/:id")
- .params({
- id: z.string(),
- })
- .body(
- z.array(
- z.union([
- z.strictObject({
- type: z.union([z.literal("add")]),
- key: z.string(),
- value: z.any(),
- }),
- z.strictObject({
- type: z.union([z.literal("push"), z.literal("pop")]),
- key: z.string(),
- values: z.array(z.any()),
- }),
- z.strictObject({
- type: z.union([z.literal("remove")]),
- key: z.string(),
- }),
- ]),
- ),
- )
- .errors([UnauthorizedError, ForbiddenError, NotFoundError]);
diff --git a/modules/iam/routes/login/code/handle.ts b/modules/iam/routes/login/code/handle.ts
deleted file mode 100644
index a867fd1..0000000
--- a/modules/iam/routes/login/code/handle.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { NotFoundError } from "@platform/relay";
-
-import { auth } from "../../../services/auth.ts";
-import { logger } from "../../../services/logger.ts";
-import route from "./spec.ts";
-
-export default route.access("public").handle(async ({ body: { email, otp } }) => {
- const response = await auth.api.signInEmailOTP({ body: { email, otp }, asResponse: true, returnHeaders: true });
- if (response.status !== 200) {
- logger.error("OTP Signin Failed", await response.json());
- return new NotFoundError();
- }
- return new Response(null, {
- headers: response.headers,
- });
-});
diff --git a/modules/iam/routes/login/code/spec.ts b/modules/iam/routes/login/code/spec.ts
deleted file mode 100644
index fdbafc4..0000000
--- a/modules/iam/routes/login/code/spec.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { route } from "@platform/relay";
-import z from "zod";
-
-export default route
- .post("/api/v1/identity/login/code")
- .body(
- z.strictObject({
- email: z.string(),
- otp: z.string(),
- }),
- )
- .query({
- next: z.string().optional(),
- });
diff --git a/modules/iam/routes/login/email/handle.ts b/modules/iam/routes/login/email/handle.ts
deleted file mode 100644
index 2af0aa3..0000000
--- a/modules/iam/routes/login/email/handle.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { auth } from "../../../services/auth.ts";
-import { logger } from "../../../services/logger.ts";
-import route from "./spec.ts";
-
-export default route.access("public").handle(async ({ body: { email } }) => {
- const response = await auth.api.sendVerificationOTP({ body: { email, type: "sign-in" } });
- if (response.success === false) {
- logger.info({
- type: "auth:passwordless",
- message: "OTP Email verification failed.",
- received: email,
- });
- }
-});
diff --git a/modules/iam/routes/login/email/spec.ts b/modules/iam/routes/login/email/spec.ts
deleted file mode 100644
index a075483..0000000
--- a/modules/iam/routes/login/email/spec.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { route } from "@platform/relay";
-import z from "zod";
-
-export default route.post("/api/v1/identity/login/email").body(
- z.object({
- email: z.email(),
- }),
-);
diff --git a/modules/iam/routes/login/password/handle.ts b/modules/iam/routes/login/password/handle.ts
deleted file mode 100644
index 978c264..0000000
--- a/modules/iam/routes/login/password/handle.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { logger } from "@platform/logger";
-import { BadRequestError } from "@platform/relay";
-import cookie from "cookie";
-
-import { auth } from "../../../auth.ts";
-import { config } from "../../../config.ts";
-import { password } from "../../../crypto/password.ts";
-import { getPasswordStrategyByAlias } from "../../../database.ts";
-import route from "./spec.ts";
-
-export default route.access("public").handle(async ({ body: { alias, password: userPassword } }) => {
- const strategy = await getPasswordStrategyByAlias(alias);
- if (strategy === undefined) {
- return logger.info({
- type: "auth:password",
- message: "Failed to get account with 'password' strategy.",
- alias,
- });
- }
-
- const isValidPassword = await password.verify(userPassword, strategy.password);
- if (isValidPassword === false) {
- return new BadRequestError("Invalid email/password provided.");
- }
-
- return new Response(null, {
- status: 204,
- headers: {
- "set-cookie": cookie.serialize(
- "token",
- await auth.generate({ id: strategy.accountId }, "1 week"),
- config.cookie(1000 * 60 * 60 * 24 * 7),
- ),
- },
- });
-});
diff --git a/modules/iam/routes/login/password/spec.ts b/modules/iam/routes/login/password/spec.ts
deleted file mode 100644
index 48a42d7..0000000
--- a/modules/iam/routes/login/password/spec.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { route } from "@platform/relay";
-import z from "zod";
-
-export default route.post("/api/v1/identities/login/password").body(
- z.object({
- alias: z.string(),
- password: z.string(),
- }),
-);
diff --git a/modules/iam/routes/login/sudo/handle.ts b/modules/iam/routes/login/sudo/handle.ts
deleted file mode 100644
index 7478252..0000000
--- a/modules/iam/routes/login/sudo/handle.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import route from "./spec.ts";
-
-export default route.access("public").handle(async () => {
- // const code = await Passwordless.createCode({ tenantId: "public", email });
- // if (code.status !== "OK") {
- // return logger.info({
- // type: "auth:passwordless",
- // message: "Create code failed.",
- // received: email,
- // });
- // }
- // logger.info({
- // type: "auth:passwordless",
- // data: {
- // deviceId: code.deviceId,
- // preAuthSessionId: code.preAuthSessionId,
- // userInputCode: code.userInputCode,
- // },
- // });
- // const response = await Passwordless.consumeCode({
- // tenantId: "public",
- // preAuthSessionId: code.preAuthSessionId,
- // deviceId: code.deviceId,
- // userInputCode: code.userInputCode,
- // });
- // if (response.status !== "OK") {
- // return new NotFoundError();
- // }
- // logger.info({
- // type: "code:claimed",
- // session: true,
- // message: "Identity resolved",
- // user: response.user.toJson(),
- // });
- // return new Response(null, {
- // status: 200,
- // headers: await getSessionHeaders("public", response.recipeUserId),
- // });
-});
diff --git a/modules/iam/routes/login/sudo/spec.ts b/modules/iam/routes/login/sudo/spec.ts
deleted file mode 100644
index b8fc493..0000000
--- a/modules/iam/routes/login/sudo/spec.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { route } from "@platform/relay";
-import z from "zod";
-
-export default route.post("/api/v1/identities/login/sudo").body(
- z.object({
- email: z.email(),
- }),
-);
diff --git a/modules/iam/routes/me/handle.ts b/modules/iam/routes/me/handle.ts
deleted file mode 100644
index ef1e7ee..0000000
--- a/modules/iam/routes/me/handle.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import route from "./spec.ts";
-
-export default route.access("session").handle(async ({ principal }) => {
- return principal;
-});
diff --git a/modules/iam/routes/me/spec.ts b/modules/iam/routes/me/spec.ts
deleted file mode 100644
index 6c2067d..0000000
--- a/modules/iam/routes/me/spec.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import { NotFoundError, route, UnauthorizedError } from "@platform/relay";
-import z from "zod";
-
-export default route.get("/api/v1/identity/me").errors([UnauthorizedError, NotFoundError]).response(z.any());
diff --git a/modules/iam/routes/roles/handle.ts b/modules/iam/routes/roles/handle.ts
deleted file mode 100644
index 51a097a..0000000
--- a/modules/iam/routes/roles/handle.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { ForbiddenError, NotFoundError } from "@platform/relay";
-
-import { getPrincipalById, setPrincipalRolesById } from "../../services/database.ts";
-import route from "./spec.ts";
-
-export default route.access("session").handle(async ({ params: { id }, body: ops }, { access }) => {
- const principal = await getPrincipalById(id);
- if (principal === undefined) {
- return new NotFoundError();
- }
- const decision = await access.isAllowed({ kind: "role", id: principal.id, attr: principal.attr }, "manage");
- if (decision === false) {
- return new ForbiddenError("You do not have permission to modify roles for this identity.");
- }
- const roles: Set = new Set(principal.roles);
- for (const op of ops) {
- switch (op.type) {
- case "add": {
- for (const role of op.roles) {
- roles.add(role);
- }
- break;
- }
- case "remove": {
- for (const role of op.roles) {
- roles.delete(role);
- }
- break;
- }
- }
- }
- await setPrincipalRolesById(id, Array.from(roles));
-});
diff --git a/modules/iam/routes/roles/spec.ts b/modules/iam/routes/roles/spec.ts
deleted file mode 100644
index d159b0c..0000000
--- a/modules/iam/routes/roles/spec.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { ForbiddenError, NotFoundError, route, UnauthorizedError } from "@platform/relay";
-import z from "zod";
-
-export default route
- .put("/api/v1/identity/:id/roles")
- .params({
- id: z.string(),
- })
- .body(
- z.array(
- z.union([
- z.strictObject({
- type: z.union([z.literal("add"), z.literal("remove")]),
- roles: z.array(z.any()),
- }),
- ]),
- ),
- )
- .errors([UnauthorizedError, ForbiddenError, NotFoundError]);
diff --git a/modules/iam/routes/session/resolve/handle.ts b/modules/iam/routes/session/resolve/handle.ts
deleted file mode 100644
index d431062..0000000
--- a/modules/iam/routes/session/resolve/handle.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { NotFoundError } from "@platform/relay";
-
-import { config } from "../../../config.ts";
-import { getPrincipalByUserId } from "../../../services/database.ts";
-import { getSessionByRequestHeader } from "../../../services/session.ts";
-import route from "./spec.ts";
-
-export default route.access(["internal:public", config.internal.privateKey]).handle(async ({ request }) => {
- const session = await getSessionByRequestHeader(request.headers);
- if (session === undefined) {
- return new NotFoundError();
- }
- return {
- session,
- principal: await getPrincipalByUserId(session.userId),
- };
-});
diff --git a/modules/iam/routes/session/resolve/spec.ts b/modules/iam/routes/session/resolve/spec.ts
deleted file mode 100644
index 01a748f..0000000
--- a/modules/iam/routes/session/resolve/spec.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { route } from "@platform/relay";
-import z from "zod";
-
-import { PrincipalSchema } from "../../../models/principal.ts";
-import { SessionSchema } from "../../../models/session.ts";
-
-export default route.get("/api/v1/identity/session").response(
- z.object({
- session: SessionSchema,
- principal: PrincipalSchema,
- }),
-);
diff --git a/modules/iam/server.ts b/modules/iam/server.ts
deleted file mode 100644
index 7f50a6d..0000000
--- a/modules/iam/server.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import { HttpAdapter, makeClient } from "@platform/relay";
-
-import { config } from "./config.ts";
-import resolve from "./routes/session/resolve/spec.ts";
-
-/*
- |--------------------------------------------------------------------------------
- | Internal Session Resolver
- |--------------------------------------------------------------------------------
- */
-
-const identity = makeClient(
- {
- adapter: new HttpAdapter({
- url: config.url,
- }),
- },
- {
- resolve: resolve.crypto({
- publicKey: config.internal.publicKey,
- }),
- },
-);
-
-export async function getPrincipalSession(payload: { headers: Headers }) {
- const response = await identity.resolve(payload);
- if ("data" in response) {
- return response.data;
- }
-}
-
-/*
- |--------------------------------------------------------------------------------
- | Server Exports
- |--------------------------------------------------------------------------------
- */
-
-export * from "./services/session.ts";
-export * from "./types.ts";
-
-/*
- |--------------------------------------------------------------------------------
- | Module Server
- |--------------------------------------------------------------------------------
- */
-
-export default {
- routes: [
- (await import("./routes/identities/get/handle.ts")).default,
- (await import("./routes/identities/update/handle.ts")).default,
- (await import("./routes/login/code/handle.ts")).default,
- (await import("./routes/login/email/handle.ts")).default,
- // (await import("./routes/login/password/handle.ts")).default,
- (await import("./routes/login/sudo/handle.ts")).default,
- (await import("./routes/me/handle.ts")).default,
- (await import("./routes/roles/handle.ts")).default,
- (await import("./routes/session/resolve/handle.ts")).default,
- (await import("./routes/access/is-allowed/handle.ts")).default,
- (await import("./routes/access/check-resource/handle.ts")).default,
- (await import("./routes/access/check-resources/handle.ts")).default,
- ],
-};
diff --git a/modules/iam/services/auth.ts b/modules/iam/services/auth.ts
deleted file mode 100644
index 4f243c0..0000000
--- a/modules/iam/services/auth.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { logger } from "@platform/logger";
-import { betterAuth } from "better-auth";
-import { mongodbAdapter } from "better-auth/adapters/mongodb";
-import { emailOTP } from "better-auth/plugins";
-
-import { db } from "./database.ts";
-
-export const auth = betterAuth({
- database: mongodbAdapter(db.db),
- session: {
- cookieCache: {
- enabled: true,
- maxAge: 5 * 60, // Cache duration in seconds
- },
- },
- plugins: [
- emailOTP({
- async sendVerificationOTP({ email, otp, type }) {
- if (type === "sign-in") {
- logger.info({ email, otp, type });
- } else if (type === "email-verification") {
- // Send the OTP for email verification
- } else {
- // Send the OTP for password reset
- }
- },
- }),
- ],
-});
diff --git a/modules/iam/services/database.ts b/modules/iam/services/database.ts
deleted file mode 100644
index d729510..0000000
--- a/modules/iam/services/database.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import { getDatabaseAccessor } from "@platform/database/accessor.ts";
-
-import {
- PRINCIPAL_TYPE_NAMES,
- type Principal,
- PrincipalSchema,
- PrincipalTypeId,
- parsePrincipal,
-} from "../models/principal.ts";
-
-export const db = getDatabaseAccessor<{
- principal: Principal;
-}>("auth");
-
-/*
- |--------------------------------------------------------------------------------
- | Methods
- |--------------------------------------------------------------------------------
- */
-
-export async function getPrincipalById(id: string): Promise {
- return db
- .collection("principal")
- .findOne({ id })
- .then((value) => parsePrincipal(value));
-}
-
-export async function setPrincipalRolesById(id: string, roles: string[]): Promise {
- await db.collection("principal").updateOne({ id }, { $set: { roles } });
-}
-
-export async function setPrincipalAttributesById(id: string, attr: Record): Promise {
- await db.collection("principal").updateOne({ id }, { $set: { attr } });
-}
-
-/**
- * Retrieve a principal for a better-auth user.
- *
- * @param userId - User id from better-auth user list.
- */
-export async function getPrincipalByUserId(userId: string): Promise {
- const principal = await db.collection("principal").findOneAndUpdate(
- { id: userId },
- {
- $setOnInsert: {
- id: userId,
- type: {
- id: PrincipalTypeId.User,
- name: PRINCIPAL_TYPE_NAMES[PrincipalTypeId.User],
- },
- roles: ["user"],
- attr: {},
- },
- },
- { upsert: true, returnDocument: "after" },
- );
- if (principal === null) {
- throw new Error("Failed to resolve Principal");
- }
- return PrincipalSchema.parse(principal);
-}
diff --git a/modules/iam/services/logger.ts b/modules/iam/services/logger.ts
deleted file mode 100644
index 965cd3d..0000000
--- a/modules/iam/services/logger.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { logger as platformLogger } from "@platform/logger";
-
-export const logger = platformLogger.prefix("Modules/Identity");
diff --git a/modules/iam/services/session.ts b/modules/iam/services/session.ts
deleted file mode 100644
index 7a1e806..0000000
--- a/modules/iam/services/session.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import cookie from "cookie";
-
-import { config } from "../config.ts";
-import { auth } from "./auth.ts";
-
-/**
- * Get session headers which can be applied on a Response object to apply
- * an authenticated session to the respondent.
- *
- * @param accessToken - Token to apply to the cookie.
- * @param maxAge - Max age of the token.
- */
-export async function getSessionHeaders(accessToken: string, maxAge: number): Promise {
- return new Headers({
- "set-cookie": cookie.serialize(
- "better-auth.session_token",
- encodeURIComponent(accessToken), // URL-encode the token
- config.cookie(maxAge),
- ),
- });
-}
-
-/**
- * Get session container from request headers.
- *
- * @param headers - Request headers to extract session from.
- */
-export async function getSessionByRequestHeader(headers: Headers) {
- const response = await auth.api.getSession({ headers });
- if (response === null) {
- return undefined;
- }
- return response.session;
-}
diff --git a/modules/iam/types.ts b/modules/iam/types.ts
deleted file mode 100644
index df4fd79..0000000
--- a/modules/iam/types.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import "@platform/relay";
-import "@platform/storage";
-
-import type { Session } from "better-auth";
-
-import type { identity } from "./client.ts";
-import type { Principal } from "./models/principal.ts";
-
-declare module "@platform/storage" {
- interface StorageContext {
- /**
- * TODO ...
- */
- session?: Session;
-
- /**
- * TODO ...
- */
- principal?: Principal;
-
- /**
- * TODO ...
- */
- access?: typeof identity.access;
- }
-}
-
-declare module "@platform/relay" {
- interface ServerContext {
- /**
- * TODO ...
- */
- isAuthenticated: boolean;
-
- /**
- * TODO ...
- */
- session: Session;
-
- /**
- * TODO ...
- */
- principal: Principal;
-
- /**
- * TODO ...
- */
- access: typeof identity.access;
- }
-}
diff --git a/modules/tenant/package.json b/modules/tenant/package.json
new file mode 100644
index 0000000..627d3fb
--- /dev/null
+++ b/modules/tenant/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@module/tenant",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module"
+}
diff --git a/modules/workspace/aggregates/workspace-user.ts b/modules/workspace/aggregates/workspace-user.ts
deleted file mode 100644
index 7f29851..0000000
--- a/modules/workspace/aggregates/workspace-user.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import { type AuditActor, auditors } from "@platform/spec/audit/actor.ts";
-import { AggregateRoot, getDate } from "@valkyr/event-store";
-
-import { db } from "../database.ts";
-import { type EventRecord, type EventStoreFactory, projector } from "../event-store.ts";
-
-export class WorkspaceUser extends AggregateRoot {
- static override readonly name = "workspace:user";
-
- workspaceId!: string;
- identityId!: string;
-
- createdAt!: Date;
- updatedAt?: Date;
-
- // -------------------------------------------------------------------------
- // Reducer
- // -------------------------------------------------------------------------
-
- with(event: EventRecord): void {
- switch (event.type) {
- case "workspace:user:created": {
- this.workspaceId = event.data.workspaceId;
- this.identityId = event.data.identityId;
- break;
- }
- }
- }
-
- // -------------------------------------------------------------------------
- // Actions
- // -------------------------------------------------------------------------
-
- create(workspaceId: string, identityId: string, meta: AuditActor = auditors.system) {
- return this.push({
- stream: this.id,
- type: "workspace:user:created",
- data: {
- workspaceId,
- identityId,
- },
- meta,
- });
- }
-}
-
-/*
- |--------------------------------------------------------------------------------
- | Projectors
- |--------------------------------------------------------------------------------
- */
-
-projector.on("workspace:user:created", async ({ stream: id, data: { workspaceId, identityId }, meta, created }) => {
- await db.collection("workspace:users").insertOne({
- id,
- workspaceId,
- identityId,
- name: {
- given: "",
- family: "",
- },
- contacts: [],
- createdAt: getDate(created),
- createdBy: meta.user.uid ?? "Unknown",
- });
-});
diff --git a/modules/workspace/aggregates/workspace.ts b/modules/workspace/aggregates/workspace.ts
deleted file mode 100644
index c76685d..0000000
--- a/modules/workspace/aggregates/workspace.ts
+++ /dev/null
@@ -1,120 +0,0 @@
-import { type AuditActor, auditors } from "@platform/spec/audit/actor.ts";
-import { AggregateRoot, getDate } from "@valkyr/event-store";
-
-import { db } from "../database.ts";
-import { type EventRecord, type EventStoreFactory, projector } from "../event-store.ts";
-
-export class Workspace extends AggregateRoot {
- static override readonly name = "workspace";
-
- ownerId!: string;
-
- name!: string;
- description?: string;
- archived = false;
-
- createdAt!: Date;
- updatedAt?: Date;
-
- // -------------------------------------------------------------------------
- // Reducer
- // -------------------------------------------------------------------------
-
- with(event: EventRecord): void {
- switch (event.type) {
- case "workspace:created": {
- this.id = event.stream;
- this.ownerId = event.data.ownerId;
- this.name = event.data.name;
- this.createdAt = getDate(event.created);
- break;
- }
- case "workspace:name:added": {
- this.name = event.data;
- this.updatedAt = getDate(event.created);
- break;
- }
- case "workspace:description:added": {
- this.description = event.data;
- this.updatedAt = getDate(event.created);
- break;
- }
- case "workspace:archived": {
- this.archived = true;
- this.updatedAt = getDate(event.created);
- break;
- }
- case "workspace:restored": {
- this.archived = false;
- this.updatedAt = getDate(event.created);
- break;
- }
- }
- }
-
- // -------------------------------------------------------------------------
- // Actions
- // -------------------------------------------------------------------------
-
- create(ownerId: string, name: string, meta: AuditActor = auditors.system) {
- return this.push({
- stream: this.id,
- type: "workspace:created",
- data: {
- ownerId,
- name,
- },
- meta,
- });
- }
-
- setName(name: string, meta: AuditActor = auditors.system) {
- return this.push({
- stream: this.id,
- type: "workspace:name:added",
- data: name,
- meta,
- });
- }
-
- setDescription(description: string, meta: AuditActor = auditors.system) {
- return this.push({
- stream: this.id,
- type: "workspace:description:added",
- data: description,
- meta,
- });
- }
-
- archive(meta: AuditActor = auditors.system) {
- return this.push({
- stream: this.id,
- type: "workspace:archived",
- meta,
- });
- }
-
- restore(meta: AuditActor = auditors.system) {
- return this.push({
- stream: this.id,
- type: "workspace:restored",
- meta,
- });
- }
-}
-
-/*
- |--------------------------------------------------------------------------------
- | Projectors
- |--------------------------------------------------------------------------------
- */
-
-projector.on("workspace:created", async ({ stream: id, data: { ownerId, name }, meta, created }) => {
- await db.collection("workspaces").insertOne({
- id,
- ownerId,
- name,
- createdAt: getDate(created),
- createdBy: meta.user.uid ?? "Unknown",
- });
-});
diff --git a/modules/workspace/database.ts b/modules/workspace/database.ts
deleted file mode 100644
index 1448cb2..0000000
--- a/modules/workspace/database.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { getDatabaseAccessor } from "@platform/database/accessor.ts";
-
-import { parseWorkspace, type Workspace } from "./models/workspace.ts";
-import type { WorkspaceUser } from "./models/workspace-user.ts";
-
-export const db = getDatabaseAccessor<{
- workspaces: Workspace;
- "workspace:users": WorkspaceUser;
-}>(`workspace:read-store`);
-
-/*
- |--------------------------------------------------------------------------------
- | Identity
- |--------------------------------------------------------------------------------
- */
-
-/**
- * Retrieve a single workspace by its primary identifier.
- *
- * @param id - Unique identity.
- */
-export async function getWorkspaceById(id: string): Promise {
- return db
- .collection("workspaces")
- .findOne({ id })
- .then((document) => parseWorkspace(document));
-}
diff --git a/modules/workspace/event-store.ts b/modules/workspace/event-store.ts
deleted file mode 100644
index 1c184d8..0000000
--- a/modules/workspace/event-store.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { mongo } from "@platform/database/client.ts";
-import { EventFactory, EventStore, type Prettify, Projector } from "@valkyr/event-store";
-import { MongoAdapter } from "@valkyr/event-store/mongo";
-
-/*
- |--------------------------------------------------------------------------------
- | Event Factory
- |--------------------------------------------------------------------------------
- */
-
-const eventFactory = new EventFactory([
- ...(await import("./events/workspace.ts")).default,
- ...(await import("./events/workspace-user.ts")).default,
-]);
-
-/*
- |--------------------------------------------------------------------------------
- | Event Store
- |--------------------------------------------------------------------------------
- */
-
-export const eventStore = new EventStore({
- adapter: new MongoAdapter(() => mongo, `workspace:event-store`),
- events: eventFactory,
- snapshot: "auto",
-});
-
-/*
- |--------------------------------------------------------------------------------
- | Projector
- |--------------------------------------------------------------------------------
- */
-
-export const projector = new Projector();
-
-eventStore.onEventsInserted(async (records, { batch }) => {
- if (batch !== undefined) {
- await projector.pushMany(batch, records);
- } else {
- for (const record of records) {
- await projector.push(record, { hydrated: false, outdated: false });
- }
- }
-});
-
-/*
- |--------------------------------------------------------------------------------
- | Events
- |--------------------------------------------------------------------------------
- */
-
-export type EventStoreFactory = typeof eventFactory;
-
-export type EventRecord = Prettify;
diff --git a/modules/workspace/events/workspace-user.ts b/modules/workspace/events/workspace-user.ts
deleted file mode 100644
index 225c7cf..0000000
--- a/modules/workspace/events/workspace-user.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { AuditActorSchema } from "@platform/spec/audit/actor.ts";
-import { event } from "@valkyr/event-store";
-import z from "zod";
-
-import { AvatarSchema } from "../value-objects/avatar.ts";
-import { ContactSchema } from "../value-objects/contact.ts";
-import { NameSchema } from "../value-objects/name.ts";
-
-export default [
- event
- .type("workspace:user:created")
- .data(
- z.strictObject({
- workspaceId: z.string(),
- identityId: z.string(),
- }),
- )
- .meta(AuditActorSchema),
- event.type("workspace:user:name-set").data(NameSchema).meta(AuditActorSchema),
- event.type("workspace:user:avatar-set").data(AvatarSchema).meta(AuditActorSchema),
- event.type("workspace:user:contacts-added").data(z.array(ContactSchema)).meta(AuditActorSchema),
- event.type("workspace:user:contacts-removed").data(z.array(z.string())).meta(AuditActorSchema),
-];
diff --git a/modules/workspace/events/workspace.ts b/modules/workspace/events/workspace.ts
deleted file mode 100644
index 7c5d984..0000000
--- a/modules/workspace/events/workspace.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { AuditActorSchema } from "@platform/spec/audit/actor.ts";
-import { event } from "@valkyr/event-store";
-import z from "zod";
-
-export default [
- event
- .type("workspace:created")
- .data(
- z.strictObject({
- ownerId: z.uuid(),
- name: z.string(),
- }),
- )
- .meta(AuditActorSchema),
- event.type("workspace:name:added").data(z.string()).meta(AuditActorSchema),
- event.type("workspace:description:added").data(z.string()).meta(AuditActorSchema),
- event.type("workspace:archived").meta(AuditActorSchema),
- event.type("workspace:restored").meta(AuditActorSchema),
-];
diff --git a/modules/workspace/models/workspace-user.ts b/modules/workspace/models/workspace-user.ts
deleted file mode 100644
index 4d368b2..0000000
--- a/modules/workspace/models/workspace-user.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { makeDocumentParser } from "@platform/database/utilities.ts";
-import { z } from "zod";
-
-import { AvatarSchema } from "../value-objects/avatar.ts";
-import { ContactSchema } from "../value-objects/contact.ts";
-import { NameSchema } from "../value-objects/name.ts";
-
-export const WorkspaceUserSchema = z.object({
- id: z.uuid(),
-
- workspaceId: z.uuid(),
- identityId: z.string(),
-
- name: NameSchema.optional(),
- avatar: AvatarSchema.optional(),
- contacts: z.array(ContactSchema).default([]),
-
- createdAt: z.coerce.date(),
- createdBy: z.string(),
- updatedAt: z.coerce.date().optional(),
- updatedBy: z.string().optional(),
-});
-
-/*
- |--------------------------------------------------------------------------------
- | Parsers
- |--------------------------------------------------------------------------------
- */
-
-export const parseWorkspaceUser = makeDocumentParser(WorkspaceUserSchema);
-
-/*
- |--------------------------------------------------------------------------------
- | Types
- |--------------------------------------------------------------------------------
- */
-
-export type WorkspaceUser = z.infer;
diff --git a/modules/workspace/models/workspace.ts b/modules/workspace/models/workspace.ts
deleted file mode 100644
index 7240c52..0000000
--- a/modules/workspace/models/workspace.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { makeDocumentParser } from "@platform/database/utilities.ts";
-import { z } from "zod";
-
-export const WorkspaceSchema = z.object({
- id: z.uuid(),
-
- ownerId: z.uuid(),
-
- name: z.string(),
- description: z.string().optional(),
-
- createdAt: z.coerce.date(),
- createdBy: z.string(),
- updatedAt: z.coerce.date().optional(),
- updatedBy: z.string().optional(),
-});
-
-/*
- |--------------------------------------------------------------------------------
- | Parsers
- |--------------------------------------------------------------------------------
- */
-
-export const parseWorkspace = makeDocumentParser(WorkspaceSchema);
-
-/*
- |--------------------------------------------------------------------------------
- | Types
- |--------------------------------------------------------------------------------
- */
-
-export type Workspace = z.infer;
diff --git a/modules/workspace/package.json b/modules/workspace/package.json
deleted file mode 100644
index 8f2c315..0000000
--- a/modules/workspace/package.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "name": "@modules/workspace",
- "version": "0.0.0",
- "private": true,
- "type": "module",
- "exports": {
- "./client.ts": "./client.ts",
- "./server.ts": "./server.ts"
- },
- "types": "types.d.ts",
- "dependencies": {
- "@modules/iam": "workspace:*",
- "@platform/database": "workspace:*",
- "@platform/relay": "workspace:*",
- "@platform/spec": "workspace:*",
- "@valkyr/event-store": "npm:@jsr/valkyr__event-store@2.0.1",
- "cookie": "1.0.2",
- "zod": "4.1.11"
- }
-}
diff --git a/modules/workspace/routes/workspaces/create/handle.ts b/modules/workspace/routes/workspaces/create/handle.ts
deleted file mode 100644
index d01ec59..0000000
--- a/modules/workspace/routes/workspaces/create/handle.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { ForbiddenError } from "@platform/relay";
-
-import { Workspace } from "../../../aggregates/workspace.ts";
-import { eventStore } from "../../../event-store.ts";
-import route from "./spec.ts";
-
-export default route.access("session").handle(async ({ body: { name } }, { access, principal }) => {
- const decision = await access.isAllowed({ kind: "workspace", id: "1", attr: {} }, "create");
- if (decision === false) {
- return new ForbiddenError("You do not have permission to create workspaces.");
- }
- const workspace = await eventStore.aggregate.from(Workspace).create(principal.id, name).save();
- return {
- id: workspace.id,
- ownerId: workspace.ownerId,
- name: workspace.name,
- createdAt: workspace.createdAt,
- createdBy: principal.id,
- };
-});
diff --git a/modules/workspace/routes/workspaces/create/spec.ts b/modules/workspace/routes/workspaces/create/spec.ts
deleted file mode 100644
index 2b4fd57..0000000
--- a/modules/workspace/routes/workspaces/create/spec.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { ForbiddenError, InternalServerError, route, UnauthorizedError, ValidationError } from "@platform/relay";
-import z from "zod";
-
-import { WorkspaceSchema } from "../../../models/workspace.ts";
-
-export default route
- .post("/api/v1/workspace")
- .body(
- z.strictObject({
- name: z.string(),
- }),
- )
- .errors([UnauthorizedError, ForbiddenError, ValidationError, InternalServerError])
- .response(WorkspaceSchema);
diff --git a/modules/workspace/server.ts b/modules/workspace/server.ts
deleted file mode 100644
index 7da962c..0000000
--- a/modules/workspace/server.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { idIndex } from "@platform/database/id.ts";
-import { register as registerReadStore } from "@platform/database/registrar.ts";
-import { register as registerEventStore } from "@valkyr/event-store/mongo";
-import "@modules/iam/types.ts";
-
-import { db } from "./database.ts";
-import { eventStore } from "./event-store.ts";
-
-export default {
- routes: [(await import("./routes/workspaces/create/handle.ts")).default],
-
- bootstrap: async (): Promise => {
- await registerReadStore(db.db, [
- {
- name: "workspaces",
- indexes: [
- idIndex,
- // [{ "strategies.type": 1, "strategies.alias": 1 }, { name: "strategy.password" }],
- ],
- },
- {
- name: "workspace:users",
- indexes: [
- idIndex,
- // [{ "strategies.type": 1, "strategies.alias": 1 }, { name: "strategy.password" }],
- ],
- },
- ]);
- await registerEventStore(eventStore.db.db, console.info);
- },
-};
diff --git a/modules/workspace/value-objects/avatar.ts b/modules/workspace/value-objects/avatar.ts
deleted file mode 100644
index 2a46586..0000000
--- a/modules/workspace/value-objects/avatar.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import z from "zod";
-
-export const AvatarSchema = z.object({
- url: z.string().describe("A valid URL pointing to the user's avatar image."),
-});
-
-export type Avatar = z.infer;
diff --git a/modules/workspace/value-objects/contact.ts b/modules/workspace/value-objects/contact.ts
deleted file mode 100644
index 384d09f..0000000
--- a/modules/workspace/value-objects/contact.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import z from "zod";
-
-import { EmailSchema } from "./email.ts";
-
-export const ContactSchema = z.union([
- z.object({
- id: z.string(),
- type: z.literal("email"),
- email: EmailSchema,
- }),
-]);
-
-export type Contact = z.infer;
diff --git a/modules/workspace/value-objects/email.ts b/modules/workspace/value-objects/email.ts
deleted file mode 100644
index 4e313aa..0000000
--- a/modules/workspace/value-objects/email.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import z from "zod";
-
-export const EmailSchema = z.object({
- type: z.enum(["personal", "work"]).describe("The context of the email address, e.g., personal or work."),
- value: z.email().describe("A valid email address string."),
- primary: z.boolean().describe("Indicates if this is the primary email."),
- verified: z.boolean().describe("True if the email address has been verified."),
- label: z.string().optional().describe("Optional display label for the email address."),
-});
-
-export type Email = z.infer;
diff --git a/modules/workspace/value-objects/name.ts b/modules/workspace/value-objects/name.ts
deleted file mode 100644
index 2a8bf12..0000000
--- a/modules/workspace/value-objects/name.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { z } from "zod";
-
-export const NameSchema = z.object({
- family: z.string().nullable().describe("Family name, also known as last name or surname."),
- given: z.string().nullable().describe("Given name, also known as first name."),
-});
-
-export type Name = z.infer;
diff --git a/platform/config/mod.ts b/platform/config/mod.ts
new file mode 100644
index 0000000..e538104
--- /dev/null
+++ b/platform/config/mod.ts
@@ -0,0 +1,4 @@
+export * from "./dotenv.ts";
+export * from "./environment.ts";
+export * from "./errors.ts";
+export * from "./service.ts";
diff --git a/platform/config/package.json b/platform/config/package.json
index 64231d6..7688e55 100644
--- a/platform/config/package.json
+++ b/platform/config/package.json
@@ -3,8 +3,12 @@
"version": "0.0.0",
"private": true,
"type": "module",
+ "main": "./mod.ts",
+ "exports": {
+ ".": "./mod.ts"
+ },
"dependencies": {
"@std/dotenv": "npm:@jsr/std__dotenv@0.225.5",
- "zod": "4.1.11"
+ "zod": "4.1.12"
}
}
diff --git a/platform/database/accessor.ts b/platform/database/accessor.ts
deleted file mode 100644
index e58d20b..0000000
--- a/platform/database/accessor.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import type { Collection, CollectionOptions, Db, Document, MongoClient } from "mongodb";
-
-import { mongo } from "./client.ts";
-
-export function getDatabaseAccessor>(
- database: string,
-): DatabaseAccessor {
- let instance: Db | undefined;
- return {
- get db(): Db {
- if (instance === undefined) {
- instance = this.client.db(database);
- }
- return instance;
- },
- get client(): MongoClient {
- return mongo;
- },
- collection(
- name: TSchema,
- options?: CollectionOptions,
- ): Collection {
- return this.db.collection(name.toString(), options);
- },
- };
-}
-
-export type DatabaseAccessor> = {
- /**
- * Database for given accessor.
- */
- db: Db;
-
- /**
- * Lazy loaded mongo client.
- */
- client: MongoClient;
-
- /**
- * Returns a reference to a MongoDB Collection. If it does not exist it will be created implicitly.
- *
- * Collection namespace validation is performed server-side.
- *
- * @param name - Collection name we wish to access.
- * @param options - Optional settings for the command.
- */
- collection(name: TSchema, options?: CollectionOptions): Collection;
-};
diff --git a/platform/database/client.ts b/platform/database/client.ts
index d8683f4..54c3ddb 100644
--- a/platform/database/client.ts
+++ b/platform/database/client.ts
@@ -1,4 +1,121 @@
-import { config } from "./config.ts";
-import { getMongoClient } from "./connection.ts";
+import { AsyncLocalStorage } from "node:async_hooks";
-export const mongo = getMongoClient(config.mongo);
+import postgres, { type Options, type Sql, type TransactionSql } from "postgres";
+import type { ZodType } from "zod";
+
+import { takeAll, takeOne } from "./parser.ts";
+
+const storage = new AsyncLocalStorage();
+
+/*
+ |--------------------------------------------------------------------------------
+ | Database
+ |--------------------------------------------------------------------------------
+ */
+
+export class Client {
+ /**
+ * Cached SQL instance.
+ */
+ #db?: Sql;
+
+ /**
+ * Instantiate a new Database accessor wrapper.
+ *
+ * @param db - Dependency container token to retrieve.
+ */
+ constructor(readonly config: Options<{}>) {}
+
+ /**
+ * SQL instance to perform queries against.
+ */
+ get sql(): Sql {
+ const tx = storage.getStore();
+ if (tx !== undefined) {
+ return tx;
+ }
+ return this.#getResolvedInstance();
+ }
+
+ /**
+ * SQL instance which ignores any potential transaction established
+ * in instance scope.
+ */
+ get direct(): Sql {
+ return this.#getResolvedInstance();
+ }
+
+ /**
+ * Retrieves cached SQL instance or attempts to create and return
+ * a new instance.
+ */
+ #getResolvedInstance(): Sql {
+ if (this.#db === undefined) {
+ this.#db = postgres(this.config);
+ }
+ return this.#db;
+ }
+
+ /**
+ * Initiates a SQL transaction by wrapping a new db instance with a
+ * new transaction instance.
+ *
+ * @example
+ * ```ts
+ * import { db } from "@optio/database/client.ts";
+ *
+ * db.begin(async (tx) => {
+ * tx`SELECT ...`
+ * });
+ * ```
+ */
+ begin(cb: (tx: TransactionSql) => TResponse | Promise): Promise> {
+ return this.direct.begin((tx) => storage.run(tx, () => cb(tx)));
+ }
+
+ /**
+ * Closes SQL connection if it has been instantiated.
+ */
+ async close(): Promise {
+ if (this.#db !== undefined) {
+ await this.#db.end();
+ this.#db = undefined;
+ }
+ }
+
+ /**
+ * Returns a schema pepared querying object allowing for a one or many
+ * response based on the query used.
+ *
+ * @param schema - Zod schema to parse.
+ */
+ schema(schema: TSchema) {
+ return {
+ /**
+ * Executes a sql query and parses the result with the given schema.
+ *
+ * @param sql - Template string SQL value.
+ */
+ one: (strings: TemplateStringsArray, ...values: any[]) => this.sql(strings, ...values).then(takeOne(schema)),
+
+ /**
+ * Executes a sql query and parses the resulting list with the given schema.
+ *
+ * @param sql - Template string SQL value.
+ */
+ many: (strings: TemplateStringsArray, ...values: any[]) => this.sql(strings, ...values).then(takeAll(schema)),
+ };
+ }
+}
+
+/*
+ |--------------------------------------------------------------------------------
+ | Types
+ |--------------------------------------------------------------------------------
+ */
+
+type UnwrapPromiseArray = T extends any[]
+ ? {
+ [k in keyof T]: T[k] extends Promise ? R : T[k];
+ }
+ : T;
diff --git a/platform/database/config.ts b/platform/database/config.ts
index 6113d59..250c17d 100644
--- a/platform/database/config.ts
+++ b/platform/database/config.ts
@@ -2,26 +2,26 @@ import { getEnvironmentVariable } from "@platform/config/environment.ts";
import z from "zod";
export const config = {
- mongo: {
+ xtdb: {
host: getEnvironmentVariable({
- key: "DB_MONGO_HOST",
+ key: "DB_XTDB_HOST",
type: z.string(),
fallback: "localhost",
}),
port: getEnvironmentVariable({
- key: "DB_MONGO_PORT",
+ key: "DB_XTDB_PORT",
type: z.coerce.number(),
- fallback: "67017",
+ fallback: "5432",
}),
user: getEnvironmentVariable({
- key: "DB_MONGO_USER",
+ key: "DB_XTDB_USER",
type: z.string(),
- fallback: "root",
+ fallback: "xtdb",
}),
pass: getEnvironmentVariable({
- key: "DB_MONGO_PASSWORD",
+ key: "DB_XTDB_PASSWORD",
type: z.string(),
- fallback: "password",
+ fallback: "xtdb",
}),
},
};
diff --git a/platform/database/connection.ts b/platform/database/connection.ts
deleted file mode 100644
index 13c523a..0000000
--- a/platform/database/connection.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { MongoClient } from "mongodb";
-
-export function getMongoClient(config: MongoConnectionInfo) {
- return new MongoClient(getConnectionUrl(config));
-}
-
-export function getConnectionUrl({ host, port, user, pass }: MongoConnectionInfo): MongoConnectionUrl {
- return `mongodb://${user}:${pass}@${host}:${port}`;
-}
-
-/*
- |--------------------------------------------------------------------------------
- | Types
- |--------------------------------------------------------------------------------
- */
-
-export type MongoConnectionUrl = `mongodb://${string}:${string}@${string}:${number}`;
-
-export type MongoConnectionInfo = {
- host: string;
- port: number;
- user: string;
- pass: string;
-};
diff --git a/platform/database/id.ts b/platform/database/id.ts
deleted file mode 100644
index 1912950..0000000
--- a/platform/database/id.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import type { CreateIndexesOptions, IndexSpecification } from "mongodb";
-
-export const idIndex: [IndexSpecification, CreateIndexesOptions] = [{ id: 1 }, { unique: true }];
diff --git a/platform/database/package.json b/platform/database/package.json
index 5d70f5d..1cf1d1c 100644
--- a/platform/database/package.json
+++ b/platform/database/package.json
@@ -5,8 +5,7 @@
"type": "module",
"dependencies": {
"@platform/config": "workspace:*",
- "@valkyr/inverse": "npm:@jsr/valkyr__inverse@1.0.1",
- "mongodb": "6.20.0",
- "zod": "4.1.11"
+ "postgres": "3.4.7",
+ "zod": "4.1.12"
}
}
diff --git a/platform/database/parser.ts b/platform/database/parser.ts
new file mode 100644
index 0000000..daebf75
--- /dev/null
+++ b/platform/database/parser.ts
@@ -0,0 +1,29 @@
+import type z from "zod";
+import type { ZodType } from "zod";
+
+/**
+ * Takes a single record from a list of database rows.
+ *
+ * @param rows - List of rows to retrieve record from.
+ */
+export function takeOne(
+ schema: TSchema,
+): (records: unknown[]) => z.output | undefined {
+ return (records: unknown[]) => {
+ if (records[0] === undefined) {
+ return undefined;
+ }
+ return schema.parse(records[0]);
+ };
+}
+
+/**
+ * Takes all records from a list of database rows and validates each one.
+ *
+ * @param schema - Zod schema to validate each record against.
+ */
+export function takeAll(schema: TSchema): (records: unknown[]) => z.output[] {
+ return (records: unknown[]) => {
+ return records.map((record) => schema.parse(record));
+ };
+}
diff --git a/platform/database/registrar.ts b/platform/database/registrar.ts
deleted file mode 100644
index ecf2c99..0000000
--- a/platform/database/registrar.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import type { CreateIndexesOptions, Db, IndexSpecification } from "mongodb";
-
-import { getCollectionsSet } from "./utilities.ts";
-
-/**
- * Takes a mongo database and registers the event store collections and
- * indexes defined internally.
- *
- * @param db - Mongo database to register event store collections against.
- * @param registrars - List of registrars to register with the database.
- * @param logger - Logger method to print internal logs.
- */
-export async function register(db: Db, registrars: Registrar[], logger?: (...args: any[]) => any) {
- const list = await getCollectionsSet(db);
- for (const { name, indexes } of registrars) {
- if (list.has(name) === false) {
- await db.createCollection(name);
- }
- for (const [indexSpec, options] of indexes) {
- await db.collection(name).createIndex(indexSpec, options);
- logger?.("Mongo Event Store > Collection '%s' is indexed [%O] with options %O", name, indexSpec, options ?? {});
- }
- logger?.("Mongo Event Store > Collection '%s' is registered", name);
- }
-}
-
-export type Registrar = {
- name: string;
- indexes: [IndexSpecification, CreateIndexesOptions?][];
-};
diff --git a/platform/database/utilities.ts b/platform/database/utilities.ts
deleted file mode 100644
index ffd3417..0000000
--- a/platform/database/utilities.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import type { Db } from "mongodb";
-import type { ZodObject, ZodType, z } from "zod";
-
-/**
- * TODO ...
- */
-export function takeOne(documents: TDocument[]): TDocument | undefined {
- return documents[0];
-}
-
-/**
- * TODO ...
- */
-export function makeDocumentParser(schema: TSchema): ModelParserFn {
- return ((value: unknown | unknown[]) => {
- if (Array.isArray(value)) {
- return value.map((value: unknown) => schema.parse(value));
- }
- if (value === undefined || value === null) {
- return undefined;
- }
- return schema.parse(value);
- }) as ModelParserFn;
-}
-
-/**
- * TODO ...
- */
-export function toParsedDocuments(
- schema: TSchema,
-): (documents: unknown[]) => Promise[]> {
- return async (documents: unknown[]) => {
- const parsed = [];
- for (const document of documents) {
- parsed.push(await schema.parseAsync(document));
- }
- return parsed;
- };
-}
-
-/**
- * TODO ...
- */
-export function toParsedDocument(
- schema: TSchema,
-): (document?: unknown) => Promise | undefined> {
- return async (document: unknown) => {
- if (document === undefined || document === null) {
- return undefined;
- }
- return schema.parseAsync(document);
- };
-}
-
-/**
- * Get a Set of collections that exists on a given mongo database instance.
- *
- * @param db - Mongo database to fetch collection list for.
- */
-export async function getCollectionsSet(db: Db) {
- return db
- .listCollections()
- .toArray()
- .then((collections) => new Set(collections.map((c) => c.name)));
-}
-
-type ModelParserFn = {
- (value: unknown): z.infer | undefined;
- (value: unknown[]): z.infer[];
-};
diff --git a/platform/logger/package.json b/platform/logger/package.json
index 67ccecd..6d9c1db 100644
--- a/platform/logger/package.json
+++ b/platform/logger/package.json
@@ -10,6 +10,6 @@
"dependencies": {
"@platform/config": "workspace:*",
"@valkyr/event-store": "npm:@jsr/valkyr__event-store@2.0.1",
- "zod": "4.1.11"
+ "zod": "4.1.12"
}
}
diff --git a/platform/relay/adapters/http.ts b/platform/relay/adapters/http.ts
index 379fa90..125b209 100644
--- a/platform/relay/adapters/http.ts
+++ b/platform/relay/adapters/http.ts
@@ -1,5 +1,3 @@
-import { encrypt } from "@platform/vault";
-
import {
assertServerErrorResponse,
type RelayAdapter,
@@ -69,10 +67,7 @@ export class HttpAdapter implements RelayAdapter {
return `${this.url}${endpoint}`;
}
- async send(
- { method, endpoint, query, body, headers = new Headers() }: RelayInput,
- publicKey: string,
- ): Promise {
+ async send({ method, endpoint, query, body, headers = new Headers() }: RelayInput): Promise {
const init: RequestInit = { method, headers };
// ### Before Request
@@ -95,14 +90,6 @@ export class HttpAdapter implements RelayAdapter {
}
}
- // ### Internal
- // If public key is present we create a encrypted token on the header that
- // is verified by the server before allowing the request through.
-
- if (publicKey !== undefined) {
- headers.set("x-internal", await encrypt("internal", publicKey));
- }
-
// ### Response
return this.request(`${endpoint}${query}`, init);
@@ -138,6 +125,9 @@ export class HttpAdapter implements RelayAdapter {
* @param body - Request body.
*/
#getRequestFormat(body: unknown): "form-data" | "json" {
+ if (body instanceof FormData) {
+ return "form-data";
+ }
if (containsFile(body) === true) {
return "form-data";
}
@@ -245,14 +235,30 @@ export class HttpAdapter implements RelayAdapter {
};
}
+ // ### Error
+ // If the 'content-type' is not a JSON response from the API then we check if the
+ // response status is an error code.
+
+ if (response.status >= 400) {
+ return {
+ result: "error",
+ headers: response.headers,
+ error: {
+ code: "SERVER_ERROR_RESPONSE",
+ status: response.status,
+ message: await response.text(),
+ },
+ };
+ }
+
+ // ### Success
+ // If the 'content-type' is not a JSON response from the API and the request is not
+ // an error we simply return the pure response in the data key.
+
return {
- result: "error",
+ result: "success",
headers: response.headers,
- error: {
- code: "UNSUPPORTED_CONTENT_TYPE",
- status: response.status,
- message: "Unsupported 'content-type' in header returned from server.",
- },
+ data: response,
};
}
diff --git a/platform/relay/libraries/client.ts b/platform/relay/libraries/client.ts
index 6b035e2..e53007a 100644
--- a/platform/relay/libraries/client.ts
+++ b/platform/relay/libraries/client.ts
@@ -114,7 +114,7 @@ function getRouteFn(route: Route, { adapter }: Config) {
// ### Fetch
- const response = await adapter.send(input, route.state.crypto?.publicKey);
+ const response = await adapter.send(input);
if ("data" in response && route.state.response !== undefined) {
response.data = route.state.response.parse(response.data);
diff --git a/platform/relay/libraries/hooks.ts b/platform/relay/libraries/hooks.ts
deleted file mode 100644
index 1356d17..0000000
--- a/platform/relay/libraries/hooks.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export type Hooks = {
- /**
- * Executes when any error is thrown before or during the lifetime
- * of the route. This allows for custom handling of errors if the
- * route has unique requirements to error handling.
- *
- * @param error - Error which has been thrown.
- */
- onError?: (error: unknown) => Response;
-};
diff --git a/platform/relay/libraries/route.ts b/platform/relay/libraries/route.ts
index 3b344bc..2ea687e 100644
--- a/platform/relay/libraries/route.ts
+++ b/platform/relay/libraries/route.ts
@@ -3,7 +3,6 @@ import z, { type ZodObject, type ZodRawShape, type ZodType } from "zod";
import type { ServerContext } from "./context.ts";
import { ServerError, type ServerErrorClass } from "./errors.ts";
-import type { Hooks } from "./hooks.ts";
export class Route {
readonly type = "route" as const;
@@ -85,23 +84,6 @@ export class Route {
return new Route({ ...this.state, meta });
}
- /**
- * Set cryptographic keys used to resolve cryptographic requests.
- *
- * @param crypto - Crypto configuration object.
- *
- * @examples
- *
- * ```ts
- * route.post("/foo").crypto({ publicKey: "..." });
- * ```
- */
- crypto(
- crypto: TCrypto,
- ): Route & { crypto: TCrypto }>> {
- return new Route({ ...this.state, crypto });
- }
-
/**
* Access level of the route which acts as the first barrier of entry
* to ensure that requests are valid.
@@ -307,19 +289,6 @@ export class Route {
): Route & { handle: THandleFn }> {
return new Route({ ...this.state, handle });
}
-
- /**
- * Assign lifetime hooks to a route allowing for custom handling of
- * events that can occur during a request or response.
- *
- * Can be used on both server and client with the appropriate
- * implementation.
- *
- * @param hooks - Hooks to register with the route.
- */
- hooks(hooks: THooks): Route & { hooks: THooks }>> {
- return new Route({ ...this.state, hooks });
- }
}
/*
@@ -451,9 +420,6 @@ export type RouteFn = (...args: any[]) => any;
type RouteState = {
method: RouteMethod;
path: string;
- crypto?: {
- publicKey: string;
- };
meta?: RouteMeta;
access?: RouteAccess;
params?: ZodObject;
@@ -462,7 +428,6 @@ type RouteState = {
errors: ServerErrorClass[];
response?: ZodType;
handle?: HandleFn;
- hooks?: Hooks;
};
export type RouteMeta = {
@@ -474,7 +439,7 @@ export type RouteMeta = {
export type RouteMethod = "POST" | "GET" | "PUT" | "PATCH" | "DELETE";
-export type RouteAccess = "public" | "session" | ["internal:public", string] | ["internal:session", string];
+export type RouteAccess = "public" | "authenticated";
type HandleFn = any[], TResponse = any> = (
...args: TArgs
diff --git a/platform/relay/mod.ts b/platform/relay/mod.ts
index 71a4bbe..a676d10 100644
--- a/platform/relay/mod.ts
+++ b/platform/relay/mod.ts
@@ -3,6 +3,5 @@ export * from "./libraries/adapter.ts";
export * from "./libraries/client.ts";
export * from "./libraries/context.ts";
export * from "./libraries/errors.ts";
-export * from "./libraries/hooks.ts";
export * from "./libraries/procedure.ts";
export * from "./libraries/route.ts";
diff --git a/platform/relay/package.json b/platform/relay/package.json
index a0b7a21..4699bcc 100644
--- a/platform/relay/package.json
+++ b/platform/relay/package.json
@@ -11,8 +11,7 @@
"@platform/auth": "workspace:*",
"@platform/socket": "workspace:*",
"@platform/supertokens": "workspace:*",
- "@platform/vault": "workspace:*",
"path-to-regexp": "8",
- "zod": "4.1.11"
+ "zod": "4.1.12"
}
}
diff --git a/platform/routes/package.json b/platform/routes/package.json
index 6f95686..adc419e 100644
--- a/platform/routes/package.json
+++ b/platform/routes/package.json
@@ -5,6 +5,6 @@
"type": "module",
"dependencies": {
"@platform/relay": "workspace:*",
- "zod": "4.1.11"
+ "zod": "4.1.12"
}
}
diff --git a/platform/server/api.ts b/platform/server/api.ts
index 41234ed..67dd055 100644
--- a/platform/server/api.ts
+++ b/platform/server/api.ts
@@ -2,7 +2,6 @@ import { logger } from "@platform/logger";
import {
BadRequestError,
context,
- ForbiddenError,
InternalServerError,
NotFoundError,
NotImplementedError,
@@ -13,7 +12,6 @@ import {
UnauthorizedError,
ValidationError,
} from "@platform/relay";
-import { decrypt } from "@platform/vault";
const SUPPORTED_MEHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
@@ -98,7 +96,7 @@ export class Api {
// Execute request and return a response.
const response = await this.#getRouteResponse(resolved, request).catch((error) =>
- this.#getErrorResponse(error, resolved.route, request),
+ this.#getErrorResponse(error, request),
);
return response;
@@ -164,31 +162,10 @@ export class Api {
);
}
- if (route.state.access === "session" && context.isAuthenticated === false) {
+ if (route.state.access === "authenticated" && context.isAuthenticated === false) {
return toResponse(new UnauthorizedError(), request);
}
- if (Array.isArray(route.state.access)) {
- const [access, privateKey] = route.state.access;
- const value = request.headers.get("x-internal");
- if (value === null) {
- return toResponse(
- new ForbiddenError(`Route '${route.method} ${route.path}' is missing 'x-internal' token.`),
- request,
- );
- }
- const decrypted = await decrypt(value, privateKey);
- if (decrypted !== "internal") {
- return toResponse(
- new ForbiddenError(`Route '${route.method} ${route.path}' has invalid 'x-internal' token.`),
- request,
- );
- }
- if (access === "internal:session" && context.isAuthenticated === false) {
- return toResponse(new UnauthorizedError(), request);
- }
- }
-
// ### Params
// If the route has params we want to coerce the values to the expected types.
@@ -242,10 +219,7 @@ export class Api {
return toResponse(await route.state.handle(...args), request);
}
- #getErrorResponse(error: unknown, route: Route, request: Request): Response {
- if (route?.state.hooks?.onError !== undefined) {
- return route.state.hooks.onError(error);
- }
+ #getErrorResponse(error: unknown, request: Request): Response {
if (error instanceof ServerError) {
return toResponse(error, request);
}
diff --git a/platform/server/package.json b/platform/server/package.json
index c41e985..f7a3345 100644
--- a/platform/server/package.json
+++ b/platform/server/package.json
@@ -11,6 +11,6 @@
"@platform/socket": "workspace:*",
"@platform/storage": "workspace:*",
"@valkyr/json-rpc": "npm:@jsr/valkyr__json-rpc@1.1.0",
- "zod": "4.1.11"
+ "zod": "4.1.12"
}
}
diff --git a/platform/spec/package.json b/platform/spec/package.json
index 92d8518..680b12a 100644
--- a/platform/spec/package.json
+++ b/platform/spec/package.json
@@ -6,6 +6,6 @@
"dependencies": {
"@platform/models": "workspace:*",
"@platform/relay": "workspace:*",
- "zod": "4.1.11"
+ "zod": "4.1.12"
}
}