diff --git a/apps/react/package.json b/apps/react/package.json
index 5fe6378..a5284bf 100644
--- a/apps/react/package.json
+++ b/apps/react/package.json
@@ -10,16 +10,20 @@
"preview": "vite preview"
},
"dependencies": {
+ "@spec/relay": "workspace:*",
+ "@spec/schemas": "workspace:*",
"@tanstack/react-query": "5",
"@tanstack/react-router": "1",
"@valkyr/db": "1",
"@valkyr/event-emitter": "npm:@jsr/valkyr__event-emitter@1",
"fast-equals": "5",
"react": "19",
- "react-dom": "19"
+ "react-dom": "19",
+ "zod": "4"
},
"devDependencies": {
"@eslint/js": "9",
+ "@tanstack/react-router-devtools": "1",
"@types/react": "19",
"@types/react-dom": "19",
"@vitejs/plugin-react": "4",
diff --git a/apps/react/src/App.css b/apps/react/src/App.css
deleted file mode 100644
index b9d355d..0000000
--- a/apps/react/src/App.css
+++ /dev/null
@@ -1,42 +0,0 @@
-#root {
- max-width: 1280px;
- margin: 0 auto;
- padding: 2rem;
- text-align: center;
-}
-
-.logo {
- height: 6em;
- padding: 1.5em;
- will-change: filter;
- transition: filter 300ms;
-}
-.logo:hover {
- filter: drop-shadow(0 0 2em #646cffaa);
-}
-.logo.react:hover {
- filter: drop-shadow(0 0 2em #61dafbaa);
-}
-
-@keyframes logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
-
-@media (prefers-reduced-motion: no-preference) {
- a:nth-of-type(2) .logo {
- animation: logo-spin infinite 20s linear;
- }
-}
-
-.card {
- padding: 2em;
-}
-
-.read-the-docs {
- color: #888;
-}
diff --git a/apps/react/src/App.tsx b/apps/react/src/App.tsx
deleted file mode 100644
index cfff5c9..0000000
--- a/apps/react/src/App.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import "./App.css";
-
-import { useState } from "react";
-
-import viteLogo from "/vite.svg";
-
-import reactLogo from "./assets/react.svg";
-import { Session } from "./components/Session.tsx";
-
-function App() {
- const [count, setCount] = useState(0);
-
- return (
- <>
-
- Vite + React
-
-
-
- Edit src/App.tsx and save to test HMR
-
-
- Click on the Vite and React logos to learn more
-
- >
- );
-}
-
-export default App;
diff --git a/apps/react/src/index.css b/apps/react/src/index.css
deleted file mode 100644
index 08a3ac9..0000000
--- a/apps/react/src/index.css
+++ /dev/null
@@ -1,68 +0,0 @@
-:root {
- font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
- line-height: 1.5;
- font-weight: 400;
-
- color-scheme: light dark;
- color: rgba(255, 255, 255, 0.87);
- background-color: #242424;
-
- font-synthesis: none;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-a {
- font-weight: 500;
- color: #646cff;
- text-decoration: inherit;
-}
-a:hover {
- color: #535bf2;
-}
-
-body {
- margin: 0;
- display: flex;
- place-items: center;
- min-width: 320px;
- min-height: 100vh;
-}
-
-h1 {
- font-size: 3.2em;
- line-height: 1.1;
-}
-
-button {
- border-radius: 8px;
- border: 1px solid transparent;
- padding: 0.6em 1.2em;
- font-size: 1em;
- font-weight: 500;
- font-family: inherit;
- background-color: #1a1a1a;
- cursor: pointer;
- transition: border-color 0.25s;
-}
-button:hover {
- border-color: #646cff;
-}
-button:focus,
-button:focus-visible {
- outline: 4px auto -webkit-focus-ring-color;
-}
-
-@media (prefers-color-scheme: light) {
- :root {
- color: #213547;
- background-color: #ffffff;
- }
- a:hover {
- color: #747bff;
- }
- button {
- background-color: #f9f9f9;
- }
-}
diff --git a/apps/react/src/libraries/form.ts b/apps/react/src/libraries/form.ts
new file mode 100644
index 0000000..665f595
--- /dev/null
+++ b/apps/react/src/libraries/form.ts
@@ -0,0 +1,269 @@
+import z, { type ZodObject, type ZodRawShape } from "zod";
+
+export class Form>> {
+ readonly schema: ZodObject;
+
+ readonly inputs: Partial = {};
+
+ #debounce: FormDebounce = {
+ validate: {},
+ };
+
+ #defaults: Partial;
+ #errors: FormErrors = {};
+ #elements: Record = {};
+
+ #onChange?: OnChangeCallback;
+ #onProcessing?: OnProcessingCallback;
+ #onError?: OnErrorCallback;
+ #onSubmit?: OnSubmitCallback;
+ #onResponse?: OnResponseCallback;
+
+ /*
+ |--------------------------------------------------------------------------------
+ | Constructor
+ |--------------------------------------------------------------------------------
+ */
+
+ constructor(schema: TSchema, defaults: Partial = {}) {
+ this.schema = z.object(schema);
+ this.#defaults = defaults;
+ this.#bindMethods();
+ this.#setDefaults();
+ this.#setSubmit();
+ }
+
+ #bindMethods() {
+ this.register = this.register.bind(this);
+ this.set = this.set.bind(this);
+ this.get = this.get.bind(this);
+ this.validate = this.validate.bind(this);
+ this.submit = this.submit.bind(this);
+ }
+
+ #setDefaults() {
+ for (const key in this.#defaults) {
+ this.inputs[key] = this.#defaults[key] ?? ("" as any);
+ }
+ }
+
+ #setSubmit() {
+ if ((this.constructor as any).submit !== undefined) {
+ this.onSubmit((this.constructor as any).submit);
+ }
+ }
+
+ /*
+ |--------------------------------------------------------------------------------
+ | Accessors
+ |--------------------------------------------------------------------------------
+ */
+
+ get isValid(): boolean {
+ return Object.keys(this.#getFormErrors()).length === 0;
+ }
+
+ get hasError() {
+ return Object.keys(this.errors).length !== 0;
+ }
+
+ get errors(): FormErrors {
+ return this.#errors;
+ }
+
+ set errors(value: FormErrors) {
+ this.#errors = value;
+ this.#onError?.(value);
+ }
+
+ /**
+ * Register a input element with the form. This registers form related methods and a
+ * reference to the element itself that can be utilized by the form.
+ *
+ * @param name - Name of the input field.
+ */
+ register(name: TKey) {
+ return {
+ name,
+ ref: (element: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | null) => {
+ if (element !== null) {
+ this.#elements[name as string] = element;
+ }
+ },
+ defaultValue: this.get(name),
+ onChange: ({ target: { value } }: any) => {
+ this.set(name, value);
+ },
+ };
+ }
+
+ /*
+ |--------------------------------------------------------------------------------
+ | Registrars
+ |--------------------------------------------------------------------------------
+ */
+
+ onChange(callback: OnChangeCallback): this {
+ this.#onChange = callback;
+ return this;
+ }
+
+ onProcessing(callback: OnProcessingCallback): this {
+ this.#onProcessing = callback;
+ return this;
+ }
+
+ onError(callback: OnErrorCallback): this {
+ this.#onError = callback;
+ return this;
+ }
+
+ onSubmit(callback: OnSubmitCallback): this {
+ this.#onSubmit = callback;
+ return this;
+ }
+
+ onResponse(callback: OnResponseCallback): this {
+ this.#onResponse = callback;
+ return this;
+ }
+
+ /*
+ |--------------------------------------------------------------------------------
+ | Data
+ |--------------------------------------------------------------------------------
+ */
+
+ /**
+ * Set the value of an input field.
+ *
+ * @param name - Name of the input field.
+ * @param value - Value to set.
+ */
+ set(name: TKey, value: TInputs[TKey]): void {
+ this.inputs[name] = value;
+ this.#onChange?.(name, value);
+ clearTimeout(this.#debounce.validate[name]);
+ this.#debounce.validate[name] = setTimeout(() => {
+ this.validate(name);
+ }, 200);
+ }
+
+ /**
+ * Get the current input values or a specific input value.
+ *
+ * @param name - Name of the input field. _(Optional)_
+ */
+ get(): Partial;
+ get(name: TKey): TInputs[TKey] | undefined;
+ get(name?: TKey): Partial | TInputs[TKey] | undefined {
+ if (name === undefined) {
+ return { ...this.inputs };
+ }
+ return this.inputs[name];
+ }
+
+ /**
+ * Reset form back to its default values.
+ */
+ reset() {
+ for (const key in this.inputs) {
+ const value = this.#defaults[key] ?? "";
+ (this.inputs as any)[key] = value;
+ if (this.#elements[key] !== undefined) {
+ (this.#elements as any)[key].value = value;
+ }
+ }
+ }
+
+ /*
+ |--------------------------------------------------------------------------------
+ | Submission
+ |--------------------------------------------------------------------------------
+ */
+
+ async submit(event: any) {
+ event.preventDefault?.();
+ this.#onProcessing?.(true);
+ this.validate();
+ if (this.hasError === false) {
+ try {
+ const response = await this.#onSubmit?.(this.schema.parse(this.inputs) as TInputs);
+ this.#onResponse?.(undefined, response);
+ } catch (error) {
+ this.#onResponse?.(error, undefined as any);
+ }
+ }
+ this.#onProcessing?.(false);
+ this.reset();
+ }
+
+ validate(name?: keyof TInputs) {
+ if (name !== undefined) {
+ this.#validateInput(name);
+ } else {
+ this.#validateForm();
+ }
+ }
+
+ #validateForm(): void {
+ this.errors = this.#getFormErrors();
+ }
+
+ #validateInput(name: keyof TInputs): void {
+ const errors = this.#getFormErrors();
+ let hasChanges = false;
+ if (errors[name] === undefined && this.errors[name] !== undefined) {
+ delete this.errors[name];
+ hasChanges = true;
+ }
+ if (errors[name] !== undefined && this.errors[name] !== errors[name]) {
+ this.errors[name] = errors[name];
+ hasChanges = true;
+ }
+ if (hasChanges === true) {
+ this.#onError?.({ ...this.errors });
+ }
+ }
+
+ #getFormErrors(): FormErrors {
+ const result = this.schema.safeParse(this.inputs);
+ if (result.success === false) {
+ throw result.error.flatten;
+ // return result.error.details.reduce>(
+ // (error, next) => ({
+ // ...error,
+ // [next.path[0]]: next.message,
+ // }),
+ // {},
+ // );
+ }
+ return {};
+ }
+}
+
+/*
+ |--------------------------------------------------------------------------------
+ | Types
+ |--------------------------------------------------------------------------------
+ */
+
+type OnChangeCallback = (name: TKey, value: TInputs[TKey]) => void;
+
+type OnProcessingCallback = (value: boolean) => void;
+
+type OnErrorCallback = (errors: FormErrors) => void;
+
+type OnSubmitCallback = (inputs: TInputs) => Promise;
+
+type OnResponseCallback = (err: Error, res: Response) => void;
+
+type FormDebounce = {
+ validate: {
+ [TKey in keyof TInputs]?: any;
+ };
+};
+
+type FormErrors = {
+ [TKey in keyof TInputs]?: string;
+};
diff --git a/apps/react/src/main.tsx b/apps/react/src/main.tsx
index 33bb9dd..e927e5c 100644
--- a/apps/react/src/main.tsx
+++ b/apps/react/src/main.tsx
@@ -1,14 +1,9 @@
-import "./index.css";
-
-import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createRouter, RouterProvider } from "@tanstack/react-router";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { routeTree } from "./routes.tsx";
-const queryClient = new QueryClient();
-
const router = createRouter({ routeTree });
declare module "@tanstack/react-router" {
@@ -19,8 +14,6 @@ declare module "@tanstack/react-router" {
createRoot(document.getElementById("root")!).render(
-
-
-
+
,
);
diff --git a/apps/react/src/routes.tsx b/apps/react/src/routes.tsx
index a3356be..830fe4b 100644
--- a/apps/react/src/routes.tsx
+++ b/apps/react/src/routes.tsx
@@ -1,9 +1,29 @@
-import { createRootRoute } from "@tanstack/react-router";
+import { createRootRoute, createRoute, Outlet } from "@tanstack/react-router";
+import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
-import App from "./App.tsx";
+import { CreateAccountView } from "./views/account/create.view.tsx";
const rootRoute = createRootRoute({
- component: App,
+ component: () => (
+ <>
+
+
+ >
+ ),
});
-export const routeTree = rootRoute.addChildren([]);
+const homeRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: "/",
+ component: function Index() {
+ return Welcome Home!
;
+ },
+});
+
+const createAccountRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: "/accounts",
+ component: CreateAccountView,
+});
+
+export const routeTree = rootRoute.addChildren([homeRoute, createAccountRoute]);
diff --git a/apps/react/src/services/api.ts b/apps/react/src/services/api.ts
index 24d9bed..2f23a83 100644
--- a/apps/react/src/services/api.ts
+++ b/apps/react/src/services/api.ts
@@ -9,7 +9,7 @@ export const api = makeClient(
}),
},
{
- account: (await import("@spec/modules/account/mod.ts")).routes,
- auth: (await import("@spec/modules/auth/mod.ts")).routes,
+ account: (await import("@spec/schemas/account/routes.ts")).routes,
+ auth: (await import("@spec/schemas/auth/routes.ts")).routes,
},
);
diff --git a/apps/react/src/views/account/create.controller.ts b/apps/react/src/views/account/create.controller.ts
new file mode 100644
index 0000000..0f082b4
--- /dev/null
+++ b/apps/react/src/views/account/create.controller.ts
@@ -0,0 +1,36 @@
+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
new file mode 100644
index 0000000..fcae7ea
--- /dev/null
+++ b/apps/react/src/views/account/create.view.tsx
@@ -0,0 +1,13 @@
+import { makeControllerView } from "../../libraries/view.ts";
+import { CreateController } from "./create.controller.ts";
+
+export const CreateAccountView = makeControllerView(CreateController, ({ state: { form } }) => {
+ return (
+
+ );
+});
diff --git a/deno.lock b/deno.lock
index 45a7c2e..d6730d8 100644
--- a/deno.lock
+++ b/deno.lock
@@ -14,11 +14,13 @@
"npm:@jsr/valkyr__event-store@2.0.0-beta.6": "2.0.0-beta.6",
"npm:@jsr/valkyr__inverse@1": "1.0.1",
"npm:@tanstack/react-query@5": "5.84.2_react@19.1.1",
+ "npm:@tanstack/react-router-devtools@1": "1.131.7_@tanstack+react-router@1.131.5__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",
"npm:@tanstack/react-router@1": "1.131.5_react@19.1.1_react-dom@19.1.1__react@19.1.1",
+ "npm:@types/node@*": "22.15.15",
"npm:@types/react-dom@19": "19.1.7_@types+react@19.1.9",
"npm:@types/react@19": "19.1.9",
"npm:@valkyr/db@1": "1.0.1",
- "npm:@vitejs/plugin-react@4": "4.7.0_vite@7.1.2__picomatch@4.0.3_@babel+core@7.28.0",
+ "npm:@vitejs/plugin-react@4": "4.7.0_vite@7.1.2__picomatch@4.0.3_@babel+core@7.28.0_@types+node@22.15.15",
"npm:cookie@1": "1.0.2",
"npm:eslint-plugin-react-hooks@5": "5.2.0_eslint@9.33.0",
"npm:eslint-plugin-react-refresh@0.4": "0.4.20_eslint@9.33.0",
@@ -33,7 +35,8 @@
"npm:react@19": "19.1.1",
"npm:typescript-eslint@8": "8.39.1_eslint@9.33.0_typescript@5.9.2_@typescript-eslint+parser@8.39.1__eslint@9.33.0__typescript@5.9.2",
"npm:typescript@5": "5.9.2",
- "npm:vite@7": "7.1.2_picomatch@4.0.3",
+ "npm:vite@7": "7.1.2_picomatch@4.0.3_@types+node@22.15.15",
+ "npm:vite@7.1.2": "7.1.2_picomatch@4.0.3_@types+node@22.15.15",
"npm:zod@4": "4.0.17"
},
"npm": {
@@ -670,12 +673,21 @@
"react"
]
},
+ "@tanstack/react-router-devtools@1.131.7_@tanstack+react-router@1.131.5__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": {
+ "integrity": "sha512-RLxjwsD8A9iavGtMA1RhQ+j/gfAdQcEf9pygGk9RZuWV7XJ4RXZeeKQHDKyJ/Rry5NkYbO+eJzeToq/szuQbuw==",
+ "dependencies": [
+ "@tanstack/react-router",
+ "@tanstack/router-devtools-core",
+ "react",
+ "react-dom"
+ ]
+ },
"@tanstack/react-router@1.131.5_react@19.1.1_react-dom@19.1.1__react@19.1.1": {
"integrity": "sha512-71suJGuCmrHN9PLLRUDB3CGnW5RNcEEfgfX616TOpKamHs977H8P4/75BgWPRWcLHCga/1kkA6c7bddCwZ35Fw==",
"dependencies": [
"@tanstack/history",
"@tanstack/react-store",
- "@tanstack/router-core",
+ "@tanstack/router-core@1.131.5_seroval@1.3.2",
"isbot",
"react",
"react-dom",
@@ -704,6 +716,28 @@
"tiny-warning"
]
},
+ "@tanstack/router-core@1.131.7_seroval@1.3.2": {
+ "integrity": "sha512-NpFfAG1muv4abrCij6sEtRrVzlU+xYpY30NAgquHNhMMMNIiN7djzsaGV+vCJdR4u5mi13+f0c3f+f9MdekY5A==",
+ "dependencies": [
+ "@tanstack/history",
+ "@tanstack/store",
+ "cookie-es",
+ "seroval",
+ "seroval-plugins",
+ "tiny-invariant",
+ "tiny-warning"
+ ]
+ },
+ "@tanstack/router-devtools-core@1.131.7_@tanstack+router-core@1.131.7__seroval@1.3.2_solid-js@1.9.9__seroval@1.3.2_tiny-invariant@1.3.3": {
+ "integrity": "sha512-1GHWILJr69Ej/c8UUMhT7Srx392FbsDqRrPhCWWtrjmYOv6Fdx3HdKDJt/YdJGBc8z6x+V7EE41j+LZggD+70Q==",
+ "dependencies": [
+ "@tanstack/router-core@1.131.7_seroval@1.3.2",
+ "clsx",
+ "goober",
+ "solid-js",
+ "tiny-invariant"
+ ]
+ },
"@tanstack/store@0.7.2": {
"integrity": "sha512-RP80Z30BYiPX2Pyo0Nyw4s1SJFH2jyM6f9i3HfX4pA+gm5jsnYryscdq2aIQLnL4TaGuQMO+zXmN9nh1Qck+Pg=="
},
@@ -742,6 +776,12 @@
"@types/json-schema@7.0.15": {
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="
},
+ "@types/node@22.15.15": {
+ "integrity": "sha512-R5muMcZob3/Jjchn5LcO8jdKwSCbzqmPB6ruBxMcf9kbxtniZHP327s6C37iOfuw8mbKK3cAQa7sEl7afLrQ8A==",
+ "dependencies": [
+ "undici-types"
+ ]
+ },
"@types/react-dom@19.1.7_@types+react@19.1.9": {
"integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==",
"dependencies": [
@@ -884,7 +924,19 @@
"@rolldown/pluginutils",
"@types/babel__core",
"react-refresh",
- "vite"
+ "vite@7.1.2_picomatch@4.0.3"
+ ]
+ },
+ "@vitejs/plugin-react@4.7.0_vite@7.1.2__picomatch@4.0.3_@babel+core@7.28.0_@types+node@22.15.15": {
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dependencies": [
+ "@babel/core",
+ "@babel/plugin-transform-react-jsx-self",
+ "@babel/plugin-transform-react-jsx-source",
+ "@rolldown/pluginutils",
+ "@types/babel__core",
+ "react-refresh",
+ "vite@7.1.2_picomatch@4.0.3_@types+node@22.15.15"
]
},
"acorn-jsx@5.3.2_acorn@8.15.0": {
@@ -963,6 +1015,9 @@
"supports-color"
]
},
+ "clsx@2.1.1": {
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="
+ },
"color-convert@2.0.1": {
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dependencies": [
@@ -1248,6 +1303,12 @@
"globals@16.3.0": {
"integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ=="
},
+ "goober@2.1.16_csstype@3.1.3": {
+ "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==",
+ "dependencies": [
+ "csstype"
+ ]
+ },
"graphemer@1.4.0": {
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="
},
@@ -1573,6 +1634,14 @@
"shebang-regex@3.0.0": {
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
},
+ "solid-js@1.9.9_seroval@1.3.2": {
+ "integrity": "sha512-A0ZBPJQldAeGCTW0YRYJmt7RCeh5rbFfPZ2aOttgYnctHE7HgKeHCBB/PVc2P7eOfmNXqMFFFoYYdm3S4dcbkA==",
+ "dependencies": [
+ "csstype",
+ "seroval",
+ "seroval-plugins"
+ ]
+ },
"source-map-js@1.2.1": {
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="
},
@@ -1649,6 +1718,9 @@
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"bin": true
},
+ "undici-types@6.21.0": {
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
+ },
"update-browserslist-db@1.1.3_browserslist@4.25.2": {
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
"dependencies": [
@@ -1685,6 +1757,25 @@
],
"bin": true
},
+ "vite@7.1.2_picomatch@4.0.3_@types+node@22.15.15": {
+ "integrity": "sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ==",
+ "dependencies": [
+ "@types/node",
+ "esbuild",
+ "fdir",
+ "picomatch@4.0.3",
+ "postcss",
+ "rollup",
+ "tinyglobby"
+ ],
+ "optionalDependencies": [
+ "fsevents"
+ ],
+ "optionalPeers": [
+ "@types/node"
+ ],
+ "bin": true
+ },
"webidl-conversions@7.0.0": {
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="
},
@@ -1753,6 +1844,7 @@
"npm:@eslint/js@9",
"npm:@jsr/valkyr__event-emitter@1",
"npm:@tanstack/react-query@5",
+ "npm:@tanstack/react-router-devtools@1",
"npm:@tanstack/react-router@1",
"npm:@types/react-dom@19",
"npm:@types/react@19",
@@ -1767,7 +1859,8 @@
"npm:react@19",
"npm:typescript-eslint@8",
"npm:typescript@5",
- "npm:vite@7"
+ "npm:vite@7",
+ "npm:zod@4"
]
}
},
diff --git a/spec/schemas/account/errors.ts b/spec/schemas/account/errors.ts
index 445f722..96419fe 100644
--- a/spec/schemas/account/errors.ts
+++ b/spec/schemas/account/errors.ts
@@ -1,4 +1,4 @@
-import { ConflictError } from "@spec/relay/mod.ts";
+import { ConflictError } from "@spec/relay";
export class AccountEmailClaimedError extends ConflictError {
constructor(email: string) {