commit f00381f2b605a9414c2aabd5a1066eb0681267ed Author: user Date: Sat Feb 28 14:50:04 2026 +0200 & so it begins diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2e2ad5c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,61 @@ +.zed + +# Dependencies +node_modules +.pnp +.pnp.js + +__pycache__ +.venv + +# ignore generated log files +**/logs/**.log +**/logs/**.log.gz +**/logs/**-audit.json + +**/data/credentials/** +**/testdocs/** + +ot_res.json +out.json +payload.json + +screenshots/*.jpeg +screenshots/*.png +screenshots/*.jpg + +# Local env files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Testing +coverage + +# Turbo +.turbo + +# Vercel +.vercel + +# Build Outputs +.next/ +out/ +build +dist +.svelte-kit + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Misc +.DS_Store +*.pem + +creds.md + +onlydevs diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..987f136 --- /dev/null +++ b/.env.example @@ -0,0 +1,49 @@ +APP_NAME=${{project.APP_NAME}} +NODE_ENV=${{project.NODE_ENV}} + +REDIS_URL=${{project.REDIS_URL}} +DATABASE_URL=${{project.DATABASE_URL}} + +COUCHBASE_CONNECTION_STRING=${{project.COUCHBASE_CONNECTION_STRING}} +COUCHBASE_USERNAME=${{project.COUCHBASE_USERNAME}} +COUCHBASE_PASSWORD=${{project.COUCHBASE_PASSWORD}} + +QDRANT_URL=${{project.QDRANT_URL}} +QDRANT_API_KEY=${{project.QDRANT_API_KEY}} + +BETTER_AUTH_SECRET=${{project.BETTER_AUTH_SECRET}} +BETTER_AUTH_URL=${{project.BETTER_AUTH_URL}} + +GOOGLE_CLIENT_ID=${{project.GOOGLE_CLIENT_ID}} +GOOGLE_CLIENT_SECRET=${{project.GOOGLE_CLIENT_SECRET}} +GOOGLE_OAUTH_SERVER_URL=${{project.GOOGLE_OAUTH_SERVER_URL}} +GOOGLE_OAUTH_SERVER_PORT=${{project.GOOGLE_OAUTH_SERVER_PORT}} + +GEMINI_API_KEY=${{project.GEMINI_API_KEY}} + +RESEND_API_KEY=${{project.RESEND_API_KEY}} +FROM_EMAIL=${{project.FROM_EMAIL}} + +INTERNAL_API_KEY="supersecretkey" + +PROCESSOR_API_URL=${{project.PROCESSOR_API_URL}} + +OPENROUTER_API_KEY=${{project.OPENROUTER_API_KEY}} +MODEL_NAME=${{project.MODEL_NAME}} + +OCR_SERVICE_URL=${{project.OCR_SERVICE_URL}} +OCR_SERVICE_TIMEOUT=${{project.OCR_SERVICE_TIMEOUT}} +OCR_SERVICE_MAX_RETRIES=${{project.OCR_SERVICE_MAX_RETRIES}} +OCR_FORCE_OCR=${{project.OCR_FORCE_OCR}} +OCR_DEBUG=${{project.OCR_DEBUG}} + +R2_BUCKET_NAME=${{project.R2_BUCKET_NAME}} +R2_REGION=${{project.R2_REGION}} +R2_ENDPOINT=${{project.R2_ENDPOINT}} +R2_ACCESS_KEY=${{project.R2_ACCESS_KEY}} +R2_SECRET_KEY=${{project.R2_SECRET_KEY}} +R2_PUBLIC_URL=${{project.R2_PUBLIC_URL}} + +MAX_FILE_SIZE=${{project.MAX_FILE_SIZE}} +ALLOWED_MIME_TYPES=${{project.ALLOWED_MIME_TYPES}} +ALLOWED_EXTENSIONS=${{project.ALLOWED_EXTENSIONS}} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e9e4835 --- /dev/null +++ b/.gitignore @@ -0,0 +1,63 @@ +.zed + +# Dependencies +node_modules +.pnp +.pnp.js + +__pycache__ +.venv + +# ignore generated log files +**/logs/**.log +**/logs/**.log.gz +**/logs/**-audit.json + +**/data/credentials/** +**/testdocs/** + +scripts/whatsapp.req.sh + +ot_res.json +out.json +payload.json + +screenshots/*.jpeg +screenshots/*.png +screenshots/*.jpg + +# Local env files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Testing +coverage + +# Turbo +.turbo + +# Vercel +.vercel + +# Build Outputs +.next/ +out/ +build +dist +.svelte-kit + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Misc +.DS_Store +*.pem + +creds.md + +onlydevs diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..6c10e55 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# bare web application monorepo + +for practice and showcase purpose + +--- diff --git a/apps/main/.gitignore b/apps/main/.gitignore new file mode 100644 index 0000000..3b462cb --- /dev/null +++ b/apps/main/.gitignore @@ -0,0 +1,23 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/apps/main/.npmrc b/apps/main/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/apps/main/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/apps/main/.prettierignore b/apps/main/.prettierignore new file mode 100644 index 0000000..7d74fe2 --- /dev/null +++ b/apps/main/.prettierignore @@ -0,0 +1,9 @@ +# Package Managers +package-lock.json +pnpm-lock.yaml +yarn.lock +bun.lock +bun.lockb + +# Miscellaneous +/static/ diff --git a/apps/main/README.md b/apps/main/README.md new file mode 100644 index 0000000..75842c4 --- /dev/null +++ b/apps/main/README.md @@ -0,0 +1,38 @@ +# sv + +Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```sh +# create a new project in the current directory +npx sv create + +# create a new project in my-app +npx sv create my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```sh +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```sh +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/apps/main/components.json b/apps/main/components.json new file mode 100644 index 0000000..1e6a638 --- /dev/null +++ b/apps/main/components.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://shadcn-svelte.com/schema.json", + "tailwind": { + "css": "src/routes/layout.css", + "baseColor": "neutral" + }, + "aliases": { + "components": "$lib/components", + "utils": "$lib/utils", + "ui": "$lib/components/ui", + "hooks": "$lib/hooks", + "lib": "$lib" + }, + "typescript": true, + "registry": "https://shadcn-svelte.com/registry" +} diff --git a/apps/main/package.json b/apps/main/package.json new file mode 100644 index 0000000..aa9f103 --- /dev/null +++ b/apps/main/package.json @@ -0,0 +1,71 @@ +{ + "name": "@apps/main", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev --port 5173", + "build": "NODE_ENV=build vite build", + "prod": "HOST=0.0.0.0 PORT=3000 bun ./build/index.js", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "format": "prettier --write .", + "lint": "prettier --check .", + "test:unit": "vitest", + "test": "npm run test:unit -- --run" + }, + "dependencies": { + "@pkg/db": "workspace:*", + "@pkg/logger": "workspace:*", + "@pkg/logic": "workspace:*", + "@pkg/result": "workspace:*", + "@tanstack/svelte-query": "^6.0.10", + "better-auth": "^1.4.7", + "date-fns": "^4.1.0", + "hono": "^4.11.1", + "marked": "^17.0.1", + "nanoid": "^5.1.6", + "neverthrow": "^8.2.0", + "qrcode": "^1.5.4", + "valibot": "^1.2.0" + }, + "devDependencies": { + "@iconify/json": "^2.2.434", + "@internationalized/date": "^3.10.0", + "@lucide/svelte": "^0.561.0", + "@sveltejs/adapter-auto": "^7.0.0", + "@sveltejs/kit": "^2.49.1", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "@tailwindcss/forms": "^0.5.10", + "@tailwindcss/typography": "^0.5.19", + "@tailwindcss/vite": "^4.1.18", + "@tanstack/table-core": "^8.21.3", + "@types/qrcode": "^1.5.6", + "bits-ui": "^2.14.4", + "clsx": "^2.1.1", + "embla-carousel-svelte": "^8.6.0", + "formsnap": "^2.0.1", + "layerchart": "2.0.0-next.43", + "mode-watcher": "^1.1.0", + "paneforge": "^1.0.2", + "prettier": "^3.7.4", + "prettier-plugin-svelte": "^3.4.0", + "prettier-plugin-tailwindcss": "^0.7.2", + "svelte": "^5.45.6", + "svelte-adapter-bun": "^1.0.1", + "svelte-check": "^4.3.4", + "svelte-sonner": "^1.0.7", + "sveltekit-superforms": "^2.28.1", + "tailwind-merge": "^3.4.0", + "tailwind-variants": "^3.2.2", + "tailwindcss": "^4.1.18", + "tw-animate-css": "^1.4.0", + "typescript": "^5.9.3", + "unplugin-icons": "^23.0.1", + "vaul-svelte": "^1.0.0-next.7", + "vite": "^7.2.6", + "vitest": "^4.0.15" + } +} diff --git a/apps/main/src/app.d.ts b/apps/main/src/app.d.ts new file mode 100644 index 0000000..7ded0fe --- /dev/null +++ b/apps/main/src/app.d.ts @@ -0,0 +1,20 @@ +import type { Session, User } from "@pkg/logic/domains/user/data"; +import "unplugin-icons/types/svelte"; + +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + interface Locals { + flowId?: string; + session?: Session; + user?: User; + } + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/apps/main/src/app.html b/apps/main/src/app.html new file mode 100644 index 0000000..f273cc5 --- /dev/null +++ b/apps/main/src/app.html @@ -0,0 +1,11 @@ + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/apps/main/src/demo.spec.ts b/apps/main/src/demo.spec.ts new file mode 100644 index 0000000..e07cbbd --- /dev/null +++ b/apps/main/src/demo.spec.ts @@ -0,0 +1,7 @@ +import { describe, it, expect } from 'vitest'; + +describe('sum test', () => { + it('adds 1 + 2 to equal 3', () => { + expect(1 + 2).toBe(3); + }); +}); diff --git a/apps/main/src/hooks.server.ts b/apps/main/src/hooks.server.ts new file mode 100644 index 0000000..705368f --- /dev/null +++ b/apps/main/src/hooks.server.ts @@ -0,0 +1,79 @@ +import { checkInitial2FaRequired } from "@pkg/logic/domains/2fa/sensitive-actions"; +import type { Handle, HandleServerError } from "@sveltejs/kit"; +import { auth } from "@pkg/logic/domains/auth/config.base"; +import { svelteKitHandler } from "better-auth/svelte-kit"; +import type { User } from "@pkg/logic/domains/user/data"; +import { sequence } from "@sveltejs/kit/hooks"; +import { building } from "$app/environment"; + +export const handleError: HandleServerError = async ({ error, event }) => { + console.log("[-] Running error middleware for : ", event.url.pathname); + console.log(error); + return { message: (error as Error).message ?? "Internal Server Error" }; +}; + +export const zero: Handle = async ({ event, resolve }) => { + return svelteKitHandler({ event, resolve, auth, building }); +}; + +export const first: Handle = async ({ event, resolve }) => { + if ( + event.url.pathname.includes("/api/v1") || + event.url.pathname.includes("/api/auth") || + event.url.pathname.includes("/api/debug") || + event.url.pathname.includes("/api/chat") || + event.url.pathname.includes("/legal") || + event.url.pathname.includes("/auth") + ) { + return await resolve(event); + } + console.log("[+] Running middleware for : ", event.url.pathname); + const baseUrl = event.url.origin; + const signInUrl = baseUrl + "/auth/login"; + const isSignInPage = event.url.pathname === "/auth/login"; + const redirectResponse = new Response(null, { + status: 302, + headers: { Location: signInUrl }, + }); + + const u = await auth.api.getSession({ headers: event.request.headers }); + if (!u || !u.user || !u.session) { + return redirectResponse; + } + if (u.user && isSignInPage) { + return new Response(null, { + status: 302, + headers: { Location: baseUrl }, + }); + } + + console.log("Setting user & session to locals"); + + const fid = crypto.randomUUID(); + + event.locals.flowId = fid; + event.locals.session = u.session; + event.locals.user = u.user as any as User; + + const needs2FA = await checkInitial2FaRequired( + { + flowId: fid, + userId: u.user.id, + sessionId: u.session.id, + }, + u.user as any, + u.session.id, + ); + if (needs2FA && !event.url.pathname.includes("/auth/2fa")) { + return new Response(null, { + status: 302, + headers: { + Location: `/auth/2fa?redirect=${encodeURIComponent(event.url.pathname)}`, + }, + }); + } + + return resolve(event); +}; + +export const handle = sequence(zero, first); diff --git a/apps/main/src/lib/api.ts b/apps/main/src/lib/api.ts new file mode 100644 index 0000000..4ec94cd --- /dev/null +++ b/apps/main/src/lib/api.ts @@ -0,0 +1,15 @@ +import { notificationsRouter } from "@pkg/logic/domains/notifications/router"; +import { usersRouter } from "@pkg/logic/domains/user/router"; +import { twofaRouter } from "@pkg/logic/domains/2fa/router"; +import { Hono } from "hono"; + +const baseRouter = new Hono() + .route("/users", usersRouter) + .route("/twofactor", twofaRouter) + .route("/notifications", notificationsRouter); + +export const apiBasePath = "/api/v1"; + +export const api = new Hono().route(apiBasePath, baseRouter); + +export type Router = typeof baseRouter; diff --git a/apps/main/src/lib/auth.client.ts b/apps/main/src/lib/auth.client.ts new file mode 100644 index 0000000..ddfc649 --- /dev/null +++ b/apps/main/src/lib/auth.client.ts @@ -0,0 +1,21 @@ +import { + adminClient, + customSessionClient, + inferAdditionalFields, + magicLinkClient, + multiSessionClient, + usernameClient, +} from "better-auth/client/plugins"; +import type { auth } from "@pkg/logic/domains/auth/config.base"; +import { createAuthClient } from "better-auth/svelte"; + +export const authClient = createAuthClient({ + plugins: [ + usernameClient(), + adminClient(), + magicLinkClient(), + multiSessionClient(), + customSessionClient(), + inferAdditionalFields(), + ], +}); diff --git a/apps/main/src/lib/components/app-sidebar.svelte b/apps/main/src/lib/components/app-sidebar.svelte new file mode 100644 index 0000000..f0ed540 --- /dev/null +++ b/apps/main/src/lib/components/app-sidebar.svelte @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + diff --git a/apps/main/src/lib/components/atoms/button-text.svelte b/apps/main/src/lib/components/atoms/button-text.svelte new file mode 100644 index 0000000..207d697 --- /dev/null +++ b/apps/main/src/lib/components/atoms/button-text.svelte @@ -0,0 +1,17 @@ + + +{#if loading} + + {loadingText} +{:else} + {text} +{/if} diff --git a/apps/main/src/lib/components/atoms/icon.svelte b/apps/main/src/lib/components/atoms/icon.svelte new file mode 100644 index 0000000..794c944 --- /dev/null +++ b/apps/main/src/lib/components/atoms/icon.svelte @@ -0,0 +1,11 @@ + + + diff --git a/apps/main/src/lib/components/atoms/title.svelte b/apps/main/src/lib/components/atoms/title.svelte new file mode 100644 index 0000000..ed17511 --- /dev/null +++ b/apps/main/src/lib/components/atoms/title.svelte @@ -0,0 +1,61 @@ + + + + {@render children?.()} + diff --git a/apps/main/src/lib/components/molecules/google-oauth-button.svelte b/apps/main/src/lib/components/molecules/google-oauth-button.svelte new file mode 100644 index 0000000..18aa308 --- /dev/null +++ b/apps/main/src/lib/components/molecules/google-oauth-button.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/main/src/lib/components/molecules/markdown-renderer.svelte b/apps/main/src/lib/components/molecules/markdown-renderer.svelte new file mode 100644 index 0000000..6459612 --- /dev/null +++ b/apps/main/src/lib/components/molecules/markdown-renderer.svelte @@ -0,0 +1,183 @@ + + + + + + + + +
+ {@html marked(content)} +
+ + diff --git a/apps/main/src/lib/components/molecules/max-width-wrapper.svelte b/apps/main/src/lib/components/molecules/max-width-wrapper.svelte new file mode 100644 index 0000000..e3ec428 --- /dev/null +++ b/apps/main/src/lib/components/molecules/max-width-wrapper.svelte @@ -0,0 +1,11 @@ + + +
+ {@render children()} +
diff --git a/apps/main/src/lib/components/nav-main.svelte b/apps/main/src/lib/components/nav-main.svelte new file mode 100644 index 0000000..5968981 --- /dev/null +++ b/apps/main/src/lib/components/nav-main.svelte @@ -0,0 +1,81 @@ + + + + + {#each items as mainItem (mainItem.title)} + + + +
+ + + {#snippet child({ props })} + + {#if mainItem.icon} + + {/if} + {mainItem.title} + + {/snippet} + + + + {#if mainItem.items && sidebar.open} + + {#snippet child({ props })} +
+ + Toggle {mainItem.title} submenu +
+ {/snippet} +
+ {/if} +
+ + + + {#if mainItem.items} + + {#each mainItem.items as subItem (subItem.title)} + + + {#snippet child({ props })} + + {subItem.title} + + {/snippet} + + + {/each} + + {/if} + +
+
+ {/each} +
+
diff --git a/apps/main/src/lib/components/nav-user.svelte b/apps/main/src/lib/components/nav-user.svelte new file mode 100644 index 0000000..0a9e96a --- /dev/null +++ b/apps/main/src/lib/components/nav-user.svelte @@ -0,0 +1,150 @@ + + + + + + + {#snippet child({ props })} + + + + + {user.name.slice(0, 2).toUpperCase()} + + +
+ {user.name} + {user.email} +
+ +
+ {/snippet} +
+ + +
+ + + + {user.name.slice(0, 2).toUpperCase()} + + +
+ {user.name} + {user.email} +
+
+
+ + + + {#each secondaryNavTree as item (item.title)} + + + + {item.title} + + + {/each} + + + + + + + + + + Theme + + + setMode("light")}> + + Light + + setMode("dark")}> + + Dark + + resetMode()}> + + System + + + + + + logoutUser()}> + + Log out + +
+
+
+
diff --git a/apps/main/src/lib/components/team-switcher.svelte b/apps/main/src/lib/components/team-switcher.svelte new file mode 100644 index 0000000..4981212 --- /dev/null +++ b/apps/main/src/lib/components/team-switcher.svelte @@ -0,0 +1,80 @@ + + + + + + + {#snippet child({ props })} + +
+ +
+
+ + {activeTeam.name} + + {activeTeam.plan} +
+ +
+ {/snippet} +
+ + Teams + {#each teams as team, index (team.name)} + (activeTeam = team)} + class="gap-2 p-2" + > +
+ +
+ {team.name} + > +
+ {/each} + + +
+ +
+
+ Coming soon +
+
+
+
+
+
diff --git a/apps/main/src/lib/components/ui/accordion/accordion-content.svelte b/apps/main/src/lib/components/ui/accordion/accordion-content.svelte new file mode 100644 index 0000000..559db3d --- /dev/null +++ b/apps/main/src/lib/components/ui/accordion/accordion-content.svelte @@ -0,0 +1,22 @@ + + + +
+ {@render children?.()} +
+
diff --git a/apps/main/src/lib/components/ui/accordion/accordion-item.svelte b/apps/main/src/lib/components/ui/accordion/accordion-item.svelte new file mode 100644 index 0000000..780545c --- /dev/null +++ b/apps/main/src/lib/components/ui/accordion/accordion-item.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/main/src/lib/components/ui/accordion/accordion-trigger.svelte b/apps/main/src/lib/components/ui/accordion/accordion-trigger.svelte new file mode 100644 index 0000000..c46c246 --- /dev/null +++ b/apps/main/src/lib/components/ui/accordion/accordion-trigger.svelte @@ -0,0 +1,32 @@ + + + + svg]:rotate-180", + className + )} + {...restProps} + > + {@render children?.()} + + + diff --git a/apps/main/src/lib/components/ui/accordion/accordion.svelte b/apps/main/src/lib/components/ui/accordion/accordion.svelte new file mode 100644 index 0000000..117ee37 --- /dev/null +++ b/apps/main/src/lib/components/ui/accordion/accordion.svelte @@ -0,0 +1,16 @@ + + + diff --git a/apps/main/src/lib/components/ui/accordion/index.ts b/apps/main/src/lib/components/ui/accordion/index.ts new file mode 100644 index 0000000..ac343a1 --- /dev/null +++ b/apps/main/src/lib/components/ui/accordion/index.ts @@ -0,0 +1,16 @@ +import Root from "./accordion.svelte"; +import Content from "./accordion-content.svelte"; +import Item from "./accordion-item.svelte"; +import Trigger from "./accordion-trigger.svelte"; + +export { + Root, + Content, + Item, + Trigger, + // + Root as Accordion, + Content as AccordionContent, + Item as AccordionItem, + Trigger as AccordionTrigger, +}; diff --git a/apps/main/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte b/apps/main/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte new file mode 100644 index 0000000..a005691 --- /dev/null +++ b/apps/main/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte @@ -0,0 +1,18 @@ + + + diff --git a/apps/main/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte b/apps/main/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte new file mode 100644 index 0000000..a7b0cf7 --- /dev/null +++ b/apps/main/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte @@ -0,0 +1,18 @@ + + + diff --git a/apps/main/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte b/apps/main/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte new file mode 100644 index 0000000..00bdd9c --- /dev/null +++ b/apps/main/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte @@ -0,0 +1,29 @@ + + + + + + diff --git a/apps/main/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte b/apps/main/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte new file mode 100644 index 0000000..2ec67dc --- /dev/null +++ b/apps/main/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/main/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte b/apps/main/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte new file mode 100644 index 0000000..f78b97a --- /dev/null +++ b/apps/main/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/apps/main/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte b/apps/main/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte new file mode 100644 index 0000000..1835d91 --- /dev/null +++ b/apps/main/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/apps/main/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte b/apps/main/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte new file mode 100644 index 0000000..a64ee76 --- /dev/null +++ b/apps/main/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/main/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte b/apps/main/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte new file mode 100644 index 0000000..f0a19a8 --- /dev/null +++ b/apps/main/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/main/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte b/apps/main/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte new file mode 100644 index 0000000..7ef2b5f --- /dev/null +++ b/apps/main/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/main/src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte b/apps/main/src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte new file mode 100644 index 0000000..b22d1d5 --- /dev/null +++ b/apps/main/src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/main/src/lib/components/ui/alert-dialog/alert-dialog.svelte b/apps/main/src/lib/components/ui/alert-dialog/alert-dialog.svelte new file mode 100644 index 0000000..7ea78bb --- /dev/null +++ b/apps/main/src/lib/components/ui/alert-dialog/alert-dialog.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/main/src/lib/components/ui/alert-dialog/index.ts b/apps/main/src/lib/components/ui/alert-dialog/index.ts new file mode 100644 index 0000000..269538e --- /dev/null +++ b/apps/main/src/lib/components/ui/alert-dialog/index.ts @@ -0,0 +1,37 @@ +import Root from "./alert-dialog.svelte"; +import Portal from "./alert-dialog-portal.svelte"; +import Trigger from "./alert-dialog-trigger.svelte"; +import Title from "./alert-dialog-title.svelte"; +import Action from "./alert-dialog-action.svelte"; +import Cancel from "./alert-dialog-cancel.svelte"; +import Footer from "./alert-dialog-footer.svelte"; +import Header from "./alert-dialog-header.svelte"; +import Overlay from "./alert-dialog-overlay.svelte"; +import Content from "./alert-dialog-content.svelte"; +import Description from "./alert-dialog-description.svelte"; + +export { + Root, + Title, + Action, + Cancel, + Portal, + Footer, + Header, + Trigger, + Overlay, + Content, + Description, + // + Root as AlertDialog, + Title as AlertDialogTitle, + Action as AlertDialogAction, + Cancel as AlertDialogCancel, + Portal as AlertDialogPortal, + Footer as AlertDialogFooter, + Header as AlertDialogHeader, + Trigger as AlertDialogTrigger, + Overlay as AlertDialogOverlay, + Content as AlertDialogContent, + Description as AlertDialogDescription, +}; diff --git a/apps/main/src/lib/components/ui/alert/alert-description.svelte b/apps/main/src/lib/components/ui/alert/alert-description.svelte new file mode 100644 index 0000000..8b56aed --- /dev/null +++ b/apps/main/src/lib/components/ui/alert/alert-description.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/apps/main/src/lib/components/ui/alert/alert-title.svelte b/apps/main/src/lib/components/ui/alert/alert-title.svelte new file mode 100644 index 0000000..77e45ad --- /dev/null +++ b/apps/main/src/lib/components/ui/alert/alert-title.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/apps/main/src/lib/components/ui/alert/alert.svelte b/apps/main/src/lib/components/ui/alert/alert.svelte new file mode 100644 index 0000000..2b2eff9 --- /dev/null +++ b/apps/main/src/lib/components/ui/alert/alert.svelte @@ -0,0 +1,44 @@ + + + + + diff --git a/apps/main/src/lib/components/ui/alert/index.ts b/apps/main/src/lib/components/ui/alert/index.ts new file mode 100644 index 0000000..97e21b4 --- /dev/null +++ b/apps/main/src/lib/components/ui/alert/index.ts @@ -0,0 +1,14 @@ +import Root from "./alert.svelte"; +import Description from "./alert-description.svelte"; +import Title from "./alert-title.svelte"; +export { alertVariants, type AlertVariant } from "./alert.svelte"; + +export { + Root, + Description, + Title, + // + Root as Alert, + Description as AlertDescription, + Title as AlertTitle, +}; diff --git a/apps/main/src/lib/components/ui/aspect-ratio/aspect-ratio.svelte b/apps/main/src/lib/components/ui/aspect-ratio/aspect-ratio.svelte new file mode 100644 index 0000000..815aab0 --- /dev/null +++ b/apps/main/src/lib/components/ui/aspect-ratio/aspect-ratio.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/main/src/lib/components/ui/aspect-ratio/index.ts b/apps/main/src/lib/components/ui/aspect-ratio/index.ts new file mode 100644 index 0000000..985c75f --- /dev/null +++ b/apps/main/src/lib/components/ui/aspect-ratio/index.ts @@ -0,0 +1,3 @@ +import Root from "./aspect-ratio.svelte"; + +export { Root, Root as AspectRatio }; diff --git a/apps/main/src/lib/components/ui/avatar/avatar-fallback.svelte b/apps/main/src/lib/components/ui/avatar/avatar-fallback.svelte new file mode 100644 index 0000000..249d4a4 --- /dev/null +++ b/apps/main/src/lib/components/ui/avatar/avatar-fallback.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/main/src/lib/components/ui/avatar/avatar-image.svelte b/apps/main/src/lib/components/ui/avatar/avatar-image.svelte new file mode 100644 index 0000000..2bb9db4 --- /dev/null +++ b/apps/main/src/lib/components/ui/avatar/avatar-image.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/main/src/lib/components/ui/avatar/avatar.svelte b/apps/main/src/lib/components/ui/avatar/avatar.svelte new file mode 100644 index 0000000..e37214d --- /dev/null +++ b/apps/main/src/lib/components/ui/avatar/avatar.svelte @@ -0,0 +1,19 @@ + + + diff --git a/apps/main/src/lib/components/ui/avatar/index.ts b/apps/main/src/lib/components/ui/avatar/index.ts new file mode 100644 index 0000000..d06457b --- /dev/null +++ b/apps/main/src/lib/components/ui/avatar/index.ts @@ -0,0 +1,13 @@ +import Root from "./avatar.svelte"; +import Image from "./avatar-image.svelte"; +import Fallback from "./avatar-fallback.svelte"; + +export { + Root, + Image, + Fallback, + // + Root as Avatar, + Image as AvatarImage, + Fallback as AvatarFallback, +}; diff --git a/apps/main/src/lib/components/ui/badge/badge.svelte b/apps/main/src/lib/components/ui/badge/badge.svelte new file mode 100644 index 0000000..e3164ba --- /dev/null +++ b/apps/main/src/lib/components/ui/badge/badge.svelte @@ -0,0 +1,50 @@ + + + + + + {@render children?.()} + diff --git a/apps/main/src/lib/components/ui/badge/index.ts b/apps/main/src/lib/components/ui/badge/index.ts new file mode 100644 index 0000000..64e0aa9 --- /dev/null +++ b/apps/main/src/lib/components/ui/badge/index.ts @@ -0,0 +1,2 @@ +export { default as Badge } from "./badge.svelte"; +export { badgeVariants, type BadgeVariant } from "./badge.svelte"; diff --git a/apps/main/src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte b/apps/main/src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte new file mode 100644 index 0000000..a178cf5 --- /dev/null +++ b/apps/main/src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte @@ -0,0 +1,23 @@ + + + diff --git a/apps/main/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte b/apps/main/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte new file mode 100644 index 0000000..1a84c4c --- /dev/null +++ b/apps/main/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte @@ -0,0 +1,20 @@ + + +
  • + {@render children?.()} +
  • diff --git a/apps/main/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte b/apps/main/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte new file mode 100644 index 0000000..e6bc17d --- /dev/null +++ b/apps/main/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte @@ -0,0 +1,31 @@ + + +{#if child} + {@render child({ props: attrs })} +{:else} + + {@render children?.()} + +{/if} diff --git a/apps/main/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte b/apps/main/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte new file mode 100644 index 0000000..1272a37 --- /dev/null +++ b/apps/main/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte @@ -0,0 +1,23 @@ + + +
      + {@render children?.()} +
    diff --git a/apps/main/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte b/apps/main/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte new file mode 100644 index 0000000..5fb6979 --- /dev/null +++ b/apps/main/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte @@ -0,0 +1,23 @@ + + + + {@render children?.()} + diff --git a/apps/main/src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte b/apps/main/src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte new file mode 100644 index 0000000..84106a1 --- /dev/null +++ b/apps/main/src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte @@ -0,0 +1,27 @@ + + + diff --git a/apps/main/src/lib/components/ui/breadcrumb/breadcrumb.svelte b/apps/main/src/lib/components/ui/breadcrumb/breadcrumb.svelte new file mode 100644 index 0000000..8f8a3e6 --- /dev/null +++ b/apps/main/src/lib/components/ui/breadcrumb/breadcrumb.svelte @@ -0,0 +1,21 @@ + + + diff --git a/apps/main/src/lib/components/ui/breadcrumb/index.ts b/apps/main/src/lib/components/ui/breadcrumb/index.ts new file mode 100644 index 0000000..dc914ec --- /dev/null +++ b/apps/main/src/lib/components/ui/breadcrumb/index.ts @@ -0,0 +1,25 @@ +import Root from "./breadcrumb.svelte"; +import Ellipsis from "./breadcrumb-ellipsis.svelte"; +import Item from "./breadcrumb-item.svelte"; +import Separator from "./breadcrumb-separator.svelte"; +import Link from "./breadcrumb-link.svelte"; +import List from "./breadcrumb-list.svelte"; +import Page from "./breadcrumb-page.svelte"; + +export { + Root, + Ellipsis, + Item, + Separator, + Link, + List, + Page, + // + Root as Breadcrumb, + Ellipsis as BreadcrumbEllipsis, + Item as BreadcrumbItem, + Separator as BreadcrumbSeparator, + Link as BreadcrumbLink, + List as BreadcrumbList, + Page as BreadcrumbPage, +}; diff --git a/apps/main/src/lib/components/ui/button-group/button-group-separator.svelte b/apps/main/src/lib/components/ui/button-group/button-group-separator.svelte new file mode 100644 index 0000000..86ff8ae --- /dev/null +++ b/apps/main/src/lib/components/ui/button-group/button-group-separator.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/main/src/lib/components/ui/button-group/button-group-text.svelte b/apps/main/src/lib/components/ui/button-group/button-group-text.svelte new file mode 100644 index 0000000..1be72bb --- /dev/null +++ b/apps/main/src/lib/components/ui/button-group/button-group-text.svelte @@ -0,0 +1,30 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} +
    + {@render mergedProps.children?.()} +
    +{/if} diff --git a/apps/main/src/lib/components/ui/button-group/button-group.svelte b/apps/main/src/lib/components/ui/button-group/button-group.svelte new file mode 100644 index 0000000..34c8d79 --- /dev/null +++ b/apps/main/src/lib/components/ui/button-group/button-group.svelte @@ -0,0 +1,46 @@ + + + + +
    + {@render children?.()} +
    diff --git a/apps/main/src/lib/components/ui/button-group/index.ts b/apps/main/src/lib/components/ui/button-group/index.ts new file mode 100644 index 0000000..7f706e3 --- /dev/null +++ b/apps/main/src/lib/components/ui/button-group/index.ts @@ -0,0 +1,13 @@ +import Root from "./button-group.svelte"; +import Text from "./button-group-text.svelte"; +import Separator from "./button-group-separator.svelte"; + +export { + Root, + Text, + Separator, + // + Root as ButtonGroup, + Text as ButtonGroupText, + Separator as ButtonGroupSeparator, +}; diff --git a/apps/main/src/lib/components/ui/button/button.svelte b/apps/main/src/lib/components/ui/button/button.svelte new file mode 100644 index 0000000..a8296ae --- /dev/null +++ b/apps/main/src/lib/components/ui/button/button.svelte @@ -0,0 +1,82 @@ + + + + +{#if href} + + {@render children?.()} + +{:else} + +{/if} diff --git a/apps/main/src/lib/components/ui/button/index.ts b/apps/main/src/lib/components/ui/button/index.ts new file mode 100644 index 0000000..fb585d7 --- /dev/null +++ b/apps/main/src/lib/components/ui/button/index.ts @@ -0,0 +1,17 @@ +import Root, { + type ButtonProps, + type ButtonSize, + type ButtonVariant, + buttonVariants, +} from "./button.svelte"; + +export { + Root, + type ButtonProps as Props, + // + Root as Button, + buttonVariants, + type ButtonProps, + type ButtonSize, + type ButtonVariant, +}; diff --git a/apps/main/src/lib/components/ui/calendar/calendar-caption.svelte b/apps/main/src/lib/components/ui/calendar/calendar-caption.svelte new file mode 100644 index 0000000..5c93037 --- /dev/null +++ b/apps/main/src/lib/components/ui/calendar/calendar-caption.svelte @@ -0,0 +1,76 @@ + + +{#snippet MonthSelect()} + { + if (!placeholder) return; + const v = Number.parseInt(e.currentTarget.value); + const newPlaceholder = placeholder.set({ month: v }); + placeholder = newPlaceholder.subtract({ months: monthIndex }); + }} + /> +{/snippet} + +{#snippet YearSelect()} + +{/snippet} + +{#if captionLayout === "dropdown"} + {@render MonthSelect()} + {@render YearSelect()} +{:else if captionLayout === "dropdown-months"} + {@render MonthSelect()} + {#if placeholder} + {formatYear(placeholder)} + {/if} +{:else if captionLayout === "dropdown-years"} + {#if placeholder} + {formatMonth(placeholder)} + {/if} + {@render YearSelect()} +{:else} + {formatMonth(month)} {formatYear(month)} +{/if} diff --git a/apps/main/src/lib/components/ui/calendar/calendar-cell.svelte b/apps/main/src/lib/components/ui/calendar/calendar-cell.svelte new file mode 100644 index 0000000..4cdb548 --- /dev/null +++ b/apps/main/src/lib/components/ui/calendar/calendar-cell.svelte @@ -0,0 +1,19 @@ + + + diff --git a/apps/main/src/lib/components/ui/calendar/calendar-day.svelte b/apps/main/src/lib/components/ui/calendar/calendar-day.svelte new file mode 100644 index 0000000..19d7bde --- /dev/null +++ b/apps/main/src/lib/components/ui/calendar/calendar-day.svelte @@ -0,0 +1,35 @@ + + +span]:text-xs [&>span]:opacity-70", + className + )} + {...restProps} +/> diff --git a/apps/main/src/lib/components/ui/calendar/calendar-grid-body.svelte b/apps/main/src/lib/components/ui/calendar/calendar-grid-body.svelte new file mode 100644 index 0000000..8cd86de --- /dev/null +++ b/apps/main/src/lib/components/ui/calendar/calendar-grid-body.svelte @@ -0,0 +1,12 @@ + + + diff --git a/apps/main/src/lib/components/ui/calendar/calendar-grid-head.svelte b/apps/main/src/lib/components/ui/calendar/calendar-grid-head.svelte new file mode 100644 index 0000000..333edc4 --- /dev/null +++ b/apps/main/src/lib/components/ui/calendar/calendar-grid-head.svelte @@ -0,0 +1,12 @@ + + + diff --git a/apps/main/src/lib/components/ui/calendar/calendar-grid-row.svelte b/apps/main/src/lib/components/ui/calendar/calendar-grid-row.svelte new file mode 100644 index 0000000..9032236 --- /dev/null +++ b/apps/main/src/lib/components/ui/calendar/calendar-grid-row.svelte @@ -0,0 +1,12 @@ + + + diff --git a/apps/main/src/lib/components/ui/calendar/calendar-grid.svelte b/apps/main/src/lib/components/ui/calendar/calendar-grid.svelte new file mode 100644 index 0000000..e0c8627 --- /dev/null +++ b/apps/main/src/lib/components/ui/calendar/calendar-grid.svelte @@ -0,0 +1,16 @@ + + + diff --git a/apps/main/src/lib/components/ui/calendar/calendar-head-cell.svelte b/apps/main/src/lib/components/ui/calendar/calendar-head-cell.svelte new file mode 100644 index 0000000..131807e --- /dev/null +++ b/apps/main/src/lib/components/ui/calendar/calendar-head-cell.svelte @@ -0,0 +1,19 @@ + + + diff --git a/apps/main/src/lib/components/ui/calendar/calendar-header.svelte b/apps/main/src/lib/components/ui/calendar/calendar-header.svelte new file mode 100644 index 0000000..c39e955 --- /dev/null +++ b/apps/main/src/lib/components/ui/calendar/calendar-header.svelte @@ -0,0 +1,19 @@ + + + diff --git a/apps/main/src/lib/components/ui/calendar/calendar-heading.svelte b/apps/main/src/lib/components/ui/calendar/calendar-heading.svelte new file mode 100644 index 0000000..a9b9810 --- /dev/null +++ b/apps/main/src/lib/components/ui/calendar/calendar-heading.svelte @@ -0,0 +1,16 @@ + + + diff --git a/apps/main/src/lib/components/ui/calendar/calendar-month-select.svelte b/apps/main/src/lib/components/ui/calendar/calendar-month-select.svelte new file mode 100644 index 0000000..8d88deb --- /dev/null +++ b/apps/main/src/lib/components/ui/calendar/calendar-month-select.svelte @@ -0,0 +1,44 @@ + + + + + {#snippet child({ props, monthItems, selectedMonthItem })} + + + {/snippet} + + diff --git a/apps/main/src/lib/components/ui/calendar/calendar-month.svelte b/apps/main/src/lib/components/ui/calendar/calendar-month.svelte new file mode 100644 index 0000000..e747fae --- /dev/null +++ b/apps/main/src/lib/components/ui/calendar/calendar-month.svelte @@ -0,0 +1,15 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/main/src/lib/components/ui/calendar/calendar-months.svelte b/apps/main/src/lib/components/ui/calendar/calendar-months.svelte new file mode 100644 index 0000000..f717a9d --- /dev/null +++ b/apps/main/src/lib/components/ui/calendar/calendar-months.svelte @@ -0,0 +1,19 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/main/src/lib/components/ui/calendar/calendar-nav.svelte b/apps/main/src/lib/components/ui/calendar/calendar-nav.svelte new file mode 100644 index 0000000..27f33d7 --- /dev/null +++ b/apps/main/src/lib/components/ui/calendar/calendar-nav.svelte @@ -0,0 +1,19 @@ + + + diff --git a/apps/main/src/lib/components/ui/calendar/calendar-next-button.svelte b/apps/main/src/lib/components/ui/calendar/calendar-next-button.svelte new file mode 100644 index 0000000..5c5a78d --- /dev/null +++ b/apps/main/src/lib/components/ui/calendar/calendar-next-button.svelte @@ -0,0 +1,31 @@ + + +{#snippet Fallback()} + +{/snippet} + + diff --git a/apps/main/src/lib/components/ui/calendar/calendar-prev-button.svelte b/apps/main/src/lib/components/ui/calendar/calendar-prev-button.svelte new file mode 100644 index 0000000..33cfd63 --- /dev/null +++ b/apps/main/src/lib/components/ui/calendar/calendar-prev-button.svelte @@ -0,0 +1,31 @@ + + +{#snippet Fallback()} + +{/snippet} + + diff --git a/apps/main/src/lib/components/ui/calendar/calendar-year-select.svelte b/apps/main/src/lib/components/ui/calendar/calendar-year-select.svelte new file mode 100644 index 0000000..226efdf --- /dev/null +++ b/apps/main/src/lib/components/ui/calendar/calendar-year-select.svelte @@ -0,0 +1,43 @@ + + + + + {#snippet child({ props, yearItems, selectedYearItem })} + + + {/snippet} + + diff --git a/apps/main/src/lib/components/ui/calendar/calendar.svelte b/apps/main/src/lib/components/ui/calendar/calendar.svelte new file mode 100644 index 0000000..29b6fff --- /dev/null +++ b/apps/main/src/lib/components/ui/calendar/calendar.svelte @@ -0,0 +1,115 @@ + + + + + {#snippet children({ months, weekdays })} + + + + + + {#each months as month, monthIndex (month)} + + + + + + + + {#each weekdays as weekday (weekday)} + + {weekday.slice(0, 2)} + + {/each} + + + + {#each month.weeks as weekDates (weekDates)} + + {#each weekDates as date (date)} + + {#if day} + {@render day({ + day: date, + outsideMonth: !isEqualMonth(date, month.value), + })} + {:else} + + {/if} + + {/each} + + {/each} + + + + {/each} + + {/snippet} + diff --git a/apps/main/src/lib/components/ui/calendar/index.ts b/apps/main/src/lib/components/ui/calendar/index.ts new file mode 100644 index 0000000..f3a16d2 --- /dev/null +++ b/apps/main/src/lib/components/ui/calendar/index.ts @@ -0,0 +1,40 @@ +import Root from "./calendar.svelte"; +import Cell from "./calendar-cell.svelte"; +import Day from "./calendar-day.svelte"; +import Grid from "./calendar-grid.svelte"; +import Header from "./calendar-header.svelte"; +import Months from "./calendar-months.svelte"; +import GridRow from "./calendar-grid-row.svelte"; +import Heading from "./calendar-heading.svelte"; +import GridBody from "./calendar-grid-body.svelte"; +import GridHead from "./calendar-grid-head.svelte"; +import HeadCell from "./calendar-head-cell.svelte"; +import NextButton from "./calendar-next-button.svelte"; +import PrevButton from "./calendar-prev-button.svelte"; +import MonthSelect from "./calendar-month-select.svelte"; +import YearSelect from "./calendar-year-select.svelte"; +import Month from "./calendar-month.svelte"; +import Nav from "./calendar-nav.svelte"; +import Caption from "./calendar-caption.svelte"; + +export { + Day, + Cell, + Grid, + Header, + Months, + GridRow, + Heading, + GridBody, + GridHead, + HeadCell, + NextButton, + PrevButton, + Nav, + Month, + YearSelect, + MonthSelect, + Caption, + // + Root as Calendar, +}; diff --git a/apps/main/src/lib/components/ui/card/card-action.svelte b/apps/main/src/lib/components/ui/card/card-action.svelte new file mode 100644 index 0000000..cc36c56 --- /dev/null +++ b/apps/main/src/lib/components/ui/card/card-action.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/main/src/lib/components/ui/card/card-content.svelte b/apps/main/src/lib/components/ui/card/card-content.svelte new file mode 100644 index 0000000..bc90b83 --- /dev/null +++ b/apps/main/src/lib/components/ui/card/card-content.svelte @@ -0,0 +1,15 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/main/src/lib/components/ui/card/card-description.svelte b/apps/main/src/lib/components/ui/card/card-description.svelte new file mode 100644 index 0000000..9b20ac7 --- /dev/null +++ b/apps/main/src/lib/components/ui/card/card-description.svelte @@ -0,0 +1,20 @@ + + +

    + {@render children?.()} +

    diff --git a/apps/main/src/lib/components/ui/card/card-footer.svelte b/apps/main/src/lib/components/ui/card/card-footer.svelte new file mode 100644 index 0000000..2d4d0f2 --- /dev/null +++ b/apps/main/src/lib/components/ui/card/card-footer.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/main/src/lib/components/ui/card/card-header.svelte b/apps/main/src/lib/components/ui/card/card-header.svelte new file mode 100644 index 0000000..2501788 --- /dev/null +++ b/apps/main/src/lib/components/ui/card/card-header.svelte @@ -0,0 +1,23 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/main/src/lib/components/ui/card/card-title.svelte b/apps/main/src/lib/components/ui/card/card-title.svelte new file mode 100644 index 0000000..7447231 --- /dev/null +++ b/apps/main/src/lib/components/ui/card/card-title.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/main/src/lib/components/ui/card/card.svelte b/apps/main/src/lib/components/ui/card/card.svelte new file mode 100644 index 0000000..99448cc --- /dev/null +++ b/apps/main/src/lib/components/ui/card/card.svelte @@ -0,0 +1,23 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/main/src/lib/components/ui/card/index.ts b/apps/main/src/lib/components/ui/card/index.ts new file mode 100644 index 0000000..4d3fce4 --- /dev/null +++ b/apps/main/src/lib/components/ui/card/index.ts @@ -0,0 +1,25 @@ +import Root from "./card.svelte"; +import Content from "./card-content.svelte"; +import Description from "./card-description.svelte"; +import Footer from "./card-footer.svelte"; +import Header from "./card-header.svelte"; +import Title from "./card-title.svelte"; +import Action from "./card-action.svelte"; + +export { + Root, + Content, + Description, + Footer, + Header, + Title, + Action, + // + Root as Card, + Content as CardContent, + Description as CardDescription, + Footer as CardFooter, + Header as CardHeader, + Title as CardTitle, + Action as CardAction, +}; diff --git a/apps/main/src/lib/components/ui/carousel/carousel-content.svelte b/apps/main/src/lib/components/ui/carousel/carousel-content.svelte new file mode 100644 index 0000000..84c71f8 --- /dev/null +++ b/apps/main/src/lib/components/ui/carousel/carousel-content.svelte @@ -0,0 +1,43 @@ + + +
    +
    + {@render children?.()} +
    +
    diff --git a/apps/main/src/lib/components/ui/carousel/carousel-item.svelte b/apps/main/src/lib/components/ui/carousel/carousel-item.svelte new file mode 100644 index 0000000..ebf1649 --- /dev/null +++ b/apps/main/src/lib/components/ui/carousel/carousel-item.svelte @@ -0,0 +1,30 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/main/src/lib/components/ui/carousel/carousel-next.svelte b/apps/main/src/lib/components/ui/carousel/carousel-next.svelte new file mode 100644 index 0000000..1aaa1f4 --- /dev/null +++ b/apps/main/src/lib/components/ui/carousel/carousel-next.svelte @@ -0,0 +1,38 @@ + + + diff --git a/apps/main/src/lib/components/ui/carousel/carousel-previous.svelte b/apps/main/src/lib/components/ui/carousel/carousel-previous.svelte new file mode 100644 index 0000000..dafe4fd --- /dev/null +++ b/apps/main/src/lib/components/ui/carousel/carousel-previous.svelte @@ -0,0 +1,38 @@ + + + diff --git a/apps/main/src/lib/components/ui/carousel/carousel.svelte b/apps/main/src/lib/components/ui/carousel/carousel.svelte new file mode 100644 index 0000000..0492805 --- /dev/null +++ b/apps/main/src/lib/components/ui/carousel/carousel.svelte @@ -0,0 +1,93 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/main/src/lib/components/ui/carousel/context.ts b/apps/main/src/lib/components/ui/carousel/context.ts new file mode 100644 index 0000000..a5fd74f --- /dev/null +++ b/apps/main/src/lib/components/ui/carousel/context.ts @@ -0,0 +1,58 @@ +import type { WithElementRef } from "$lib/utils.js"; +import type { + EmblaCarouselSvelteType, + default as emblaCarouselSvelte, +} from "embla-carousel-svelte"; +import { getContext, hasContext, setContext } from "svelte"; +import type { HTMLAttributes } from "svelte/elements"; + +export type CarouselAPI = + NonNullable["on:emblaInit"]> extends ( + evt: CustomEvent + ) => void + ? CarouselAPI + : never; + +type EmblaCarouselConfig = NonNullable[1]>; + +export type CarouselOptions = EmblaCarouselConfig["options"]; +export type CarouselPlugins = EmblaCarouselConfig["plugins"]; + +//// + +export type CarouselProps = { + opts?: CarouselOptions; + plugins?: CarouselPlugins; + setApi?: (api: CarouselAPI | undefined) => void; + orientation?: "horizontal" | "vertical"; +} & WithElementRef>; + +const EMBLA_CAROUSEL_CONTEXT = Symbol("EMBLA_CAROUSEL_CONTEXT"); + +export type EmblaContext = { + api: CarouselAPI | undefined; + orientation: "horizontal" | "vertical"; + scrollNext: () => void; + scrollPrev: () => void; + canScrollNext: boolean; + canScrollPrev: boolean; + handleKeyDown: (e: KeyboardEvent) => void; + options: CarouselOptions; + plugins: CarouselPlugins; + onInit: (e: CustomEvent) => void; + scrollTo: (index: number, jump?: boolean) => void; + scrollSnaps: number[]; + selectedIndex: number; +}; + +export function setEmblaContext(config: EmblaContext): EmblaContext { + setContext(EMBLA_CAROUSEL_CONTEXT, config); + return config; +} + +export function getEmblaContext(name = "This component") { + if (!hasContext(EMBLA_CAROUSEL_CONTEXT)) { + throw new Error(`${name} must be used within a component`); + } + return getContext>(EMBLA_CAROUSEL_CONTEXT); +} diff --git a/apps/main/src/lib/components/ui/carousel/index.ts b/apps/main/src/lib/components/ui/carousel/index.ts new file mode 100644 index 0000000..957fc74 --- /dev/null +++ b/apps/main/src/lib/components/ui/carousel/index.ts @@ -0,0 +1,19 @@ +import Root from "./carousel.svelte"; +import Content from "./carousel-content.svelte"; +import Item from "./carousel-item.svelte"; +import Previous from "./carousel-previous.svelte"; +import Next from "./carousel-next.svelte"; + +export { + Root, + Content, + Item, + Previous, + Next, + // + Root as Carousel, + Content as CarouselContent, + Item as CarouselItem, + Previous as CarouselPrevious, + Next as CarouselNext, +}; diff --git a/apps/main/src/lib/components/ui/chart/chart-container.svelte b/apps/main/src/lib/components/ui/chart/chart-container.svelte new file mode 100644 index 0000000..36c0000 --- /dev/null +++ b/apps/main/src/lib/components/ui/chart/chart-container.svelte @@ -0,0 +1,80 @@ + + +
    + + {@render children?.()} +
    diff --git a/apps/main/src/lib/components/ui/chart/chart-style.svelte b/apps/main/src/lib/components/ui/chart/chart-style.svelte new file mode 100644 index 0000000..864ecc3 --- /dev/null +++ b/apps/main/src/lib/components/ui/chart/chart-style.svelte @@ -0,0 +1,37 @@ + + +{#if themeContents} + {#key id} + + {themeContents} + + {/key} +{/if} diff --git a/apps/main/src/lib/components/ui/chart/chart-tooltip.svelte b/apps/main/src/lib/components/ui/chart/chart-tooltip.svelte new file mode 100644 index 0000000..6eb66ff --- /dev/null +++ b/apps/main/src/lib/components/ui/chart/chart-tooltip.svelte @@ -0,0 +1,159 @@ + + +{#snippet TooltipLabel()} + {#if formattedLabel} +
    + {#if typeof formattedLabel === "function"} + {@render formattedLabel()} + {:else} + {formattedLabel} + {/if} +
    + {/if} +{/snippet} + + +
    + {#if !nestLabel} + {@render TooltipLabel()} + {/if} +
    + {#each tooltipCtx.payload as item, i (item.key + i)} + {@const key = `${nameKey || item.key || item.name || "value"}`} + {@const itemConfig = getPayloadConfigFromPayload(chart.config, item, key)} + {@const indicatorColor = color || item.payload?.color || item.color} +
    svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:size-2.5", + indicator === "dot" && "items-center" + )} + > + {#if formatter && item.value !== undefined && item.name} + {@render formatter({ + value: item.value, + name: item.name, + item, + index: i, + payload: tooltipCtx.payload, + })} + {:else} + {#if itemConfig?.icon} + + {:else if !hideIndicator} +
    + {/if} +
    +
    + {#if nestLabel} + {@render TooltipLabel()} + {/if} + + {itemConfig?.label || item.name} + +
    + {#if item.value !== undefined} + + {item.value.toLocaleString()} + + {/if} +
    + {/if} +
    + {/each} +
    +
    +
    diff --git a/apps/main/src/lib/components/ui/chart/chart-utils.ts b/apps/main/src/lib/components/ui/chart/chart-utils.ts new file mode 100644 index 0000000..2decbbf --- /dev/null +++ b/apps/main/src/lib/components/ui/chart/chart-utils.ts @@ -0,0 +1,66 @@ +import type { Tooltip } from "layerchart"; +import { getContext, setContext, type Component, type ComponentProps, type Snippet } from "svelte"; + +export const THEMES = { light: "", dark: ".dark" } as const; + +export type ChartConfig = { + [k in string]: { + label?: string; + icon?: Component; + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ); +}; + +export type ExtractSnippetParams = T extends Snippet<[infer P]> ? P : never; + +export type TooltipPayload = ExtractSnippetParams< + ComponentProps["children"] +>["payload"][number]; + +// Helper to extract item config from a payload. +export function getPayloadConfigFromPayload( + config: ChartConfig, + payload: TooltipPayload, + key: string +) { + if (typeof payload !== "object" || payload === null) return undefined; + + const payloadPayload = + "payload" in payload && typeof payload.payload === "object" && payload.payload !== null + ? payload.payload + : undefined; + + let configLabelKey: string = key; + + if (payload.key === key) { + configLabelKey = payload.key; + } else if (payload.name === key) { + configLabelKey = payload.name; + } else if (key in payload && typeof payload[key as keyof typeof payload] === "string") { + configLabelKey = payload[key as keyof typeof payload] as string; + } else if ( + payloadPayload !== undefined && + key in payloadPayload && + typeof payloadPayload[key as keyof typeof payloadPayload] === "string" + ) { + configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string; + } + + return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config]; +} + +type ChartContextValue = { + config: ChartConfig; +}; + +const chartContextKey = Symbol("chart-context"); + +export function setChartContext(value: ChartContextValue) { + return setContext(chartContextKey, value); +} + +export function useChart() { + return getContext(chartContextKey); +} diff --git a/apps/main/src/lib/components/ui/chart/index.ts b/apps/main/src/lib/components/ui/chart/index.ts new file mode 100644 index 0000000..f22375e --- /dev/null +++ b/apps/main/src/lib/components/ui/chart/index.ts @@ -0,0 +1,6 @@ +import ChartContainer from "./chart-container.svelte"; +import ChartTooltip from "./chart-tooltip.svelte"; + +export { getPayloadConfigFromPayload, type ChartConfig } from "./chart-utils.js"; + +export { ChartContainer, ChartTooltip, ChartContainer as Container, ChartTooltip as Tooltip }; diff --git a/apps/main/src/lib/components/ui/checkbox/checkbox.svelte b/apps/main/src/lib/components/ui/checkbox/checkbox.svelte new file mode 100644 index 0000000..0a2b010 --- /dev/null +++ b/apps/main/src/lib/components/ui/checkbox/checkbox.svelte @@ -0,0 +1,36 @@ + + + + {#snippet children({ checked, indeterminate })} +
    + {#if checked} + + {:else if indeterminate} + + {/if} +
    + {/snippet} +
    diff --git a/apps/main/src/lib/components/ui/checkbox/index.ts b/apps/main/src/lib/components/ui/checkbox/index.ts new file mode 100644 index 0000000..6d92d94 --- /dev/null +++ b/apps/main/src/lib/components/ui/checkbox/index.ts @@ -0,0 +1,6 @@ +import Root from "./checkbox.svelte"; +export { + Root, + // + Root as Checkbox, +}; diff --git a/apps/main/src/lib/components/ui/collapsible/collapsible-content.svelte b/apps/main/src/lib/components/ui/collapsible/collapsible-content.svelte new file mode 100644 index 0000000..bdabb55 --- /dev/null +++ b/apps/main/src/lib/components/ui/collapsible/collapsible-content.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/main/src/lib/components/ui/collapsible/collapsible-trigger.svelte b/apps/main/src/lib/components/ui/collapsible/collapsible-trigger.svelte new file mode 100644 index 0000000..ece7ad6 --- /dev/null +++ b/apps/main/src/lib/components/ui/collapsible/collapsible-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/main/src/lib/components/ui/collapsible/collapsible.svelte b/apps/main/src/lib/components/ui/collapsible/collapsible.svelte new file mode 100644 index 0000000..39cdd4e --- /dev/null +++ b/apps/main/src/lib/components/ui/collapsible/collapsible.svelte @@ -0,0 +1,11 @@ + + + diff --git a/apps/main/src/lib/components/ui/collapsible/index.ts b/apps/main/src/lib/components/ui/collapsible/index.ts new file mode 100644 index 0000000..169b479 --- /dev/null +++ b/apps/main/src/lib/components/ui/collapsible/index.ts @@ -0,0 +1,13 @@ +import Root from "./collapsible.svelte"; +import Trigger from "./collapsible-trigger.svelte"; +import Content from "./collapsible-content.svelte"; + +export { + Root, + Content, + Trigger, + // + Root as Collapsible, + Content as CollapsibleContent, + Trigger as CollapsibleTrigger, +}; diff --git a/apps/main/src/lib/components/ui/command/command-dialog.svelte b/apps/main/src/lib/components/ui/command/command-dialog.svelte new file mode 100644 index 0000000..4bdb740 --- /dev/null +++ b/apps/main/src/lib/components/ui/command/command-dialog.svelte @@ -0,0 +1,40 @@ + + + + + {title} + {description} + + + + + diff --git a/apps/main/src/lib/components/ui/command/command-empty.svelte b/apps/main/src/lib/components/ui/command/command-empty.svelte new file mode 100644 index 0000000..6726cd8 --- /dev/null +++ b/apps/main/src/lib/components/ui/command/command-empty.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/main/src/lib/components/ui/command/command-group.svelte b/apps/main/src/lib/components/ui/command/command-group.svelte new file mode 100644 index 0000000..104f817 --- /dev/null +++ b/apps/main/src/lib/components/ui/command/command-group.svelte @@ -0,0 +1,32 @@ + + + + {#if heading} + + {heading} + + {/if} + + diff --git a/apps/main/src/lib/components/ui/command/command-input.svelte b/apps/main/src/lib/components/ui/command/command-input.svelte new file mode 100644 index 0000000..28d3dcf --- /dev/null +++ b/apps/main/src/lib/components/ui/command/command-input.svelte @@ -0,0 +1,26 @@ + + +
    + + +
    diff --git a/apps/main/src/lib/components/ui/command/command-item.svelte b/apps/main/src/lib/components/ui/command/command-item.svelte new file mode 100644 index 0000000..5833416 --- /dev/null +++ b/apps/main/src/lib/components/ui/command/command-item.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/main/src/lib/components/ui/command/command-link-item.svelte b/apps/main/src/lib/components/ui/command/command-link-item.svelte new file mode 100644 index 0000000..ada6d2c --- /dev/null +++ b/apps/main/src/lib/components/ui/command/command-link-item.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/main/src/lib/components/ui/command/command-list.svelte b/apps/main/src/lib/components/ui/command/command-list.svelte new file mode 100644 index 0000000..2d3a01a --- /dev/null +++ b/apps/main/src/lib/components/ui/command/command-list.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/main/src/lib/components/ui/command/command-loading.svelte b/apps/main/src/lib/components/ui/command/command-loading.svelte new file mode 100644 index 0000000..19dd298 --- /dev/null +++ b/apps/main/src/lib/components/ui/command/command-loading.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/main/src/lib/components/ui/command/command-separator.svelte b/apps/main/src/lib/components/ui/command/command-separator.svelte new file mode 100644 index 0000000..35c4c95 --- /dev/null +++ b/apps/main/src/lib/components/ui/command/command-separator.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/main/src/lib/components/ui/command/command-shortcut.svelte b/apps/main/src/lib/components/ui/command/command-shortcut.svelte new file mode 100644 index 0000000..f3d6928 --- /dev/null +++ b/apps/main/src/lib/components/ui/command/command-shortcut.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/apps/main/src/lib/components/ui/command/command.svelte b/apps/main/src/lib/components/ui/command/command.svelte new file mode 100644 index 0000000..a1581f1 --- /dev/null +++ b/apps/main/src/lib/components/ui/command/command.svelte @@ -0,0 +1,28 @@ + + + diff --git a/apps/main/src/lib/components/ui/command/index.ts b/apps/main/src/lib/components/ui/command/index.ts new file mode 100644 index 0000000..5435fbe --- /dev/null +++ b/apps/main/src/lib/components/ui/command/index.ts @@ -0,0 +1,37 @@ +import Root from "./command.svelte"; +import Loading from "./command-loading.svelte"; +import Dialog from "./command-dialog.svelte"; +import Empty from "./command-empty.svelte"; +import Group from "./command-group.svelte"; +import Item from "./command-item.svelte"; +import Input from "./command-input.svelte"; +import List from "./command-list.svelte"; +import Separator from "./command-separator.svelte"; +import Shortcut from "./command-shortcut.svelte"; +import LinkItem from "./command-link-item.svelte"; + +export { + Root, + Dialog, + Empty, + Group, + Item, + LinkItem, + Input, + List, + Separator, + Shortcut, + Loading, + // + Root as Command, + Dialog as CommandDialog, + Empty as CommandEmpty, + Group as CommandGroup, + Item as CommandItem, + LinkItem as CommandLinkItem, + Input as CommandInput, + List as CommandList, + Separator as CommandSeparator, + Shortcut as CommandShortcut, + Loading as CommandLoading, +}; diff --git a/apps/main/src/lib/components/ui/context-menu/context-menu-checkbox-item.svelte b/apps/main/src/lib/components/ui/context-menu/context-menu-checkbox-item.svelte new file mode 100644 index 0000000..f3b6db3 --- /dev/null +++ b/apps/main/src/lib/components/ui/context-menu/context-menu-checkbox-item.svelte @@ -0,0 +1,40 @@ + + + + {#snippet children({ checked })} + + {#if checked} + + {/if} + + {@render childrenProp?.()} + {/snippet} + diff --git a/apps/main/src/lib/components/ui/context-menu/context-menu-content.svelte b/apps/main/src/lib/components/ui/context-menu/context-menu-content.svelte new file mode 100644 index 0000000..20b516d --- /dev/null +++ b/apps/main/src/lib/components/ui/context-menu/context-menu-content.svelte @@ -0,0 +1,28 @@ + + + + + diff --git a/apps/main/src/lib/components/ui/context-menu/context-menu-group-heading.svelte b/apps/main/src/lib/components/ui/context-menu/context-menu-group-heading.svelte new file mode 100644 index 0000000..2cb6207 --- /dev/null +++ b/apps/main/src/lib/components/ui/context-menu/context-menu-group-heading.svelte @@ -0,0 +1,21 @@ + + + diff --git a/apps/main/src/lib/components/ui/context-menu/context-menu-group.svelte b/apps/main/src/lib/components/ui/context-menu/context-menu-group.svelte new file mode 100644 index 0000000..c7c1e06 --- /dev/null +++ b/apps/main/src/lib/components/ui/context-menu/context-menu-group.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/main/src/lib/components/ui/context-menu/context-menu-item.svelte b/apps/main/src/lib/components/ui/context-menu/context-menu-item.svelte new file mode 100644 index 0000000..4e8d224 --- /dev/null +++ b/apps/main/src/lib/components/ui/context-menu/context-menu-item.svelte @@ -0,0 +1,27 @@ + + + diff --git a/apps/main/src/lib/components/ui/context-menu/context-menu-label.svelte b/apps/main/src/lib/components/ui/context-menu/context-menu-label.svelte new file mode 100644 index 0000000..5e96107 --- /dev/null +++ b/apps/main/src/lib/components/ui/context-menu/context-menu-label.svelte @@ -0,0 +1,24 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/main/src/lib/components/ui/context-menu/context-menu-portal.svelte b/apps/main/src/lib/components/ui/context-menu/context-menu-portal.svelte new file mode 100644 index 0000000..96b1e3e --- /dev/null +++ b/apps/main/src/lib/components/ui/context-menu/context-menu-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/main/src/lib/components/ui/context-menu/context-menu-radio-group.svelte b/apps/main/src/lib/components/ui/context-menu/context-menu-radio-group.svelte new file mode 100644 index 0000000..964cb55 --- /dev/null +++ b/apps/main/src/lib/components/ui/context-menu/context-menu-radio-group.svelte @@ -0,0 +1,16 @@ + + + diff --git a/apps/main/src/lib/components/ui/context-menu/context-menu-radio-item.svelte b/apps/main/src/lib/components/ui/context-menu/context-menu-radio-item.svelte new file mode 100644 index 0000000..0141b14 --- /dev/null +++ b/apps/main/src/lib/components/ui/context-menu/context-menu-radio-item.svelte @@ -0,0 +1,33 @@ + + + + {#snippet children({ checked })} + + {#if checked} + + {/if} + + {@render childrenProp?.({ checked })} + {/snippet} + diff --git a/apps/main/src/lib/components/ui/context-menu/context-menu-separator.svelte b/apps/main/src/lib/components/ui/context-menu/context-menu-separator.svelte new file mode 100644 index 0000000..7f5b237 --- /dev/null +++ b/apps/main/src/lib/components/ui/context-menu/context-menu-separator.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/main/src/lib/components/ui/context-menu/context-menu-shortcut.svelte b/apps/main/src/lib/components/ui/context-menu/context-menu-shortcut.svelte new file mode 100644 index 0000000..6181881 --- /dev/null +++ b/apps/main/src/lib/components/ui/context-menu/context-menu-shortcut.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/apps/main/src/lib/components/ui/context-menu/context-menu-sub-content.svelte b/apps/main/src/lib/components/ui/context-menu/context-menu-sub-content.svelte new file mode 100644 index 0000000..2b6ca47 --- /dev/null +++ b/apps/main/src/lib/components/ui/context-menu/context-menu-sub-content.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/main/src/lib/components/ui/context-menu/context-menu-sub-trigger.svelte b/apps/main/src/lib/components/ui/context-menu/context-menu-sub-trigger.svelte new file mode 100644 index 0000000..38d74eb --- /dev/null +++ b/apps/main/src/lib/components/ui/context-menu/context-menu-sub-trigger.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + + diff --git a/apps/main/src/lib/components/ui/context-menu/context-menu-sub.svelte b/apps/main/src/lib/components/ui/context-menu/context-menu-sub.svelte new file mode 100644 index 0000000..a03827b --- /dev/null +++ b/apps/main/src/lib/components/ui/context-menu/context-menu-sub.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/main/src/lib/components/ui/context-menu/context-menu-trigger.svelte b/apps/main/src/lib/components/ui/context-menu/context-menu-trigger.svelte new file mode 100644 index 0000000..3efa857 --- /dev/null +++ b/apps/main/src/lib/components/ui/context-menu/context-menu-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/main/src/lib/components/ui/context-menu/context-menu.svelte b/apps/main/src/lib/components/ui/context-menu/context-menu.svelte new file mode 100644 index 0000000..cfaefb3 --- /dev/null +++ b/apps/main/src/lib/components/ui/context-menu/context-menu.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/main/src/lib/components/ui/context-menu/index.ts b/apps/main/src/lib/components/ui/context-menu/index.ts new file mode 100644 index 0000000..cbeaee1 --- /dev/null +++ b/apps/main/src/lib/components/ui/context-menu/index.ts @@ -0,0 +1,52 @@ +import Root from "./context-menu.svelte"; +import Sub from "./context-menu-sub.svelte"; +import Portal from "./context-menu-portal.svelte"; +import Trigger from "./context-menu-trigger.svelte"; +import Group from "./context-menu-group.svelte"; +import RadioGroup from "./context-menu-radio-group.svelte"; +import Item from "./context-menu-item.svelte"; +import GroupHeading from "./context-menu-group-heading.svelte"; +import Content from "./context-menu-content.svelte"; +import Shortcut from "./context-menu-shortcut.svelte"; +import RadioItem from "./context-menu-radio-item.svelte"; +import Separator from "./context-menu-separator.svelte"; +import SubContent from "./context-menu-sub-content.svelte"; +import SubTrigger from "./context-menu-sub-trigger.svelte"; +import CheckboxItem from "./context-menu-checkbox-item.svelte"; +import Label from "./context-menu-label.svelte"; + +export { + Root, + Sub, + Portal, + Item, + GroupHeading, + Label, + Group, + Trigger, + Content, + Shortcut, + Separator, + RadioItem, + SubContent, + SubTrigger, + RadioGroup, + CheckboxItem, + // + Root as ContextMenu, + Sub as ContextMenuSub, + Portal as ContextMenuPortal, + Item as ContextMenuItem, + GroupHeading as ContextMenuGroupHeading, + Group as ContextMenuGroup, + Content as ContextMenuContent, + Trigger as ContextMenuTrigger, + Shortcut as ContextMenuShortcut, + RadioItem as ContextMenuRadioItem, + Separator as ContextMenuSeparator, + RadioGroup as ContextMenuRadioGroup, + SubContent as ContextMenuSubContent, + SubTrigger as ContextMenuSubTrigger, + CheckboxItem as ContextMenuCheckboxItem, + Label as ContextMenuLabel, +}; diff --git a/apps/main/src/lib/components/ui/data-table/data-table.svelte.ts b/apps/main/src/lib/components/ui/data-table/data-table.svelte.ts new file mode 100644 index 0000000..5b7985e --- /dev/null +++ b/apps/main/src/lib/components/ui/data-table/data-table.svelte.ts @@ -0,0 +1,142 @@ +import { + type RowData, + type TableOptions, + type TableOptionsResolved, + type TableState, + createTable, +} from "@tanstack/table-core"; + +/** + * Creates a reactive TanStack table object for Svelte. + * @param options Table options to create the table with. + * @returns A reactive table object. + * @example + * ```svelte + * + * + * + * + * {#each table.getHeaderGroups() as headerGroup} + * + * {#each headerGroup.headers as header} + * + * {/each} + * + * {/each} + * + * + *
    + * + *
    + * ``` + */ +export function createSvelteTable(options: TableOptions) { + const resolvedOptions: TableOptionsResolved = mergeObjects( + { + state: {}, + onStateChange() {}, + renderFallbackValue: null, + mergeOptions: ( + defaultOptions: TableOptions, + options: Partial> + ) => { + return mergeObjects(defaultOptions, options); + }, + }, + options + ); + + const table = createTable(resolvedOptions); + let state = $state>(table.initialState); + + function updateOptions() { + table.setOptions((prev) => { + return mergeObjects(prev, options, { + state: mergeObjects(state, options.state || {}), + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onStateChange: (updater: any) => { + if (updater instanceof Function) state = updater(state); + else state = mergeObjects(state, updater); + + options.onStateChange?.(updater); + }, + }); + }); + } + + updateOptions(); + + $effect.pre(() => { + updateOptions(); + }); + + return table; +} + +type MaybeThunk = T | (() => T | null | undefined); +type Intersection = (T extends [infer H, ...infer R] + ? H & Intersection + : unknown) & {}; + +/** + * Lazily merges several objects (or thunks) while preserving + * getter semantics from every source. + * + * Proxy-based to avoid known WebKit recursion issue. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function mergeObjects[]>( + ...sources: Sources +): Intersection<{ [K in keyof Sources]: Sources[K] }> { + const resolve = (src: MaybeThunk): T | undefined => + typeof src === "function" ? (src() ?? undefined) : src; + + const findSourceWithKey = (key: PropertyKey) => { + for (let i = sources.length - 1; i >= 0; i--) { + const obj = resolve(sources[i]); + if (obj && key in obj) return obj; + } + return undefined; + }; + + return new Proxy(Object.create(null), { + get(_, key) { + const src = findSourceWithKey(key); + + return src?.[key as never]; + }, + + has(_, key) { + return !!findSourceWithKey(key); + }, + + ownKeys(): (string | symbol)[] { + // eslint-disable-next-line svelte/prefer-svelte-reactivity + const all = new Set(); + for (const s of sources) { + const obj = resolve(s); + if (obj) { + for (const k of Reflect.ownKeys(obj) as (string | symbol)[]) { + all.add(k); + } + } + } + return [...all]; + }, + + getOwnPropertyDescriptor(_, key) { + const src = findSourceWithKey(key); + if (!src) return undefined; + return { + configurable: true, + enumerable: true, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + value: (src as any)[key], + writable: true, + }; + }, + }) as Intersection<{ [K in keyof Sources]: Sources[K] }>; +} diff --git a/apps/main/src/lib/components/ui/data-table/flex-render.svelte b/apps/main/src/lib/components/ui/data-table/flex-render.svelte new file mode 100644 index 0000000..ac82a58 --- /dev/null +++ b/apps/main/src/lib/components/ui/data-table/flex-render.svelte @@ -0,0 +1,40 @@ + + +{#if typeof content === "string"} + {content} +{:else if content instanceof Function} + + + {@const result = content(context as any)} + {#if result instanceof RenderComponentConfig} + {@const { component: Component, props } = result} + + {:else if result instanceof RenderSnippetConfig} + {@const { snippet, params } = result} + {@render snippet({ ...params, attach })} + {:else} + {result} + {/if} +{/if} diff --git a/apps/main/src/lib/components/ui/data-table/index.ts b/apps/main/src/lib/components/ui/data-table/index.ts new file mode 100644 index 0000000..5f4e77e --- /dev/null +++ b/apps/main/src/lib/components/ui/data-table/index.ts @@ -0,0 +1,3 @@ +export { default as FlexRender } from "./flex-render.svelte"; +export { renderComponent, renderSnippet } from "./render-helpers.js"; +export { createSvelteTable } from "./data-table.svelte.js"; diff --git a/apps/main/src/lib/components/ui/data-table/render-helpers.ts b/apps/main/src/lib/components/ui/data-table/render-helpers.ts new file mode 100644 index 0000000..fa036d6 --- /dev/null +++ b/apps/main/src/lib/components/ui/data-table/render-helpers.ts @@ -0,0 +1,111 @@ +import type { Component, ComponentProps, Snippet } from "svelte"; + +/** + * A helper class to make it easy to identify Svelte components in + * `columnDef.cell` and `columnDef.header` properties. + * + * > NOTE: This class should only be used internally by the adapter. If you're + * reading this and you don't know what this is for, you probably don't need it. + * + * @example + * ```svelte + * {@const result = content(context as any)} + * {#if result instanceof RenderComponentConfig} + * {@const { component: Component, props } = result} + * + * {/if} + * ``` + */ +export class RenderComponentConfig { + component: TComponent; + props: ComponentProps | Record; + constructor( + component: TComponent, + props: ComponentProps | Record = {} + ) { + this.component = component; + this.props = props; + } +} + +/** + * A helper class to make it easy to identify Svelte Snippets in `columnDef.cell` and `columnDef.header` properties. + * + * > NOTE: This class should only be used internally by the adapter. If you're + * reading this and you don't know what this is for, you probably don't need it. + * + * @example + * ```svelte + * {@const result = content(context as any)} + * {#if result instanceof RenderSnippetConfig} + * {@const { snippet, params } = result} + * {@render snippet(params)} + * {/if} + * ``` + */ +export class RenderSnippetConfig { + snippet: Snippet<[TProps]>; + params: TProps; + constructor(snippet: Snippet<[TProps]>, params: TProps) { + this.snippet = snippet; + this.params = params; + } +} + +/** + * A helper function to help create cells from Svelte components through ColumnDef's `cell` and `header` properties. + * + * This is only to be used with Svelte Components - use `renderSnippet` for Svelte Snippets. + * + * @param component A Svelte component + * @param props The props to pass to `component` + * @returns A `RenderComponentConfig` object that helps svelte-table know how to render the header/cell component. + * @example + * ```ts + * // +page.svelte + * const defaultColumns = [ + * columnHelper.accessor('name', { + * header: header => renderComponent(SortHeader, { label: 'Name', header }), + * }), + * columnHelper.accessor('state', { + * header: header => renderComponent(SortHeader, { label: 'State', header }), + * }), + * ] + * ``` + * @see {@link https://tanstack.com/table/latest/docs/guide/column-defs} + */ +export function renderComponent< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + T extends Component, + Props extends ComponentProps, +>(component: T, props: Props = {} as Props) { + return new RenderComponentConfig(component, props); +} + +/** + * A helper function to help create cells from Svelte Snippets through ColumnDef's `cell` and `header` properties. + * + * The snippet must only take one parameter. + * + * This is only to be used with Snippets - use `renderComponent` for Svelte Components. + * + * @param snippet + * @param params + * @returns - A `RenderSnippetConfig` object that helps svelte-table know how to render the header/cell snippet. + * @example + * ```ts + * // +page.svelte + * const defaultColumns = [ + * columnHelper.accessor('name', { + * cell: cell => renderSnippet(nameSnippet, { name: cell.row.name }), + * }), + * columnHelper.accessor('state', { + * cell: cell => renderSnippet(stateSnippet, { state: cell.row.state }), + * }), + * ] + * ``` + * @see {@link https://tanstack.com/table/latest/docs/guide/column-defs} + */ +export function renderSnippet(snippet: Snippet<[TProps]>, params: TProps = {} as TProps) { + return new RenderSnippetConfig(snippet, params); +} diff --git a/apps/main/src/lib/components/ui/dialog/dialog-close.svelte b/apps/main/src/lib/components/ui/dialog/dialog-close.svelte new file mode 100644 index 0000000..840b2f6 --- /dev/null +++ b/apps/main/src/lib/components/ui/dialog/dialog-close.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/main/src/lib/components/ui/dialog/dialog-content.svelte b/apps/main/src/lib/components/ui/dialog/dialog-content.svelte new file mode 100644 index 0000000..ae1a03f --- /dev/null +++ b/apps/main/src/lib/components/ui/dialog/dialog-content.svelte @@ -0,0 +1,45 @@ + + + + + + {@render children?.()} + {#if showCloseButton} + + + Close + + {/if} + + diff --git a/apps/main/src/lib/components/ui/dialog/dialog-description.svelte b/apps/main/src/lib/components/ui/dialog/dialog-description.svelte new file mode 100644 index 0000000..3845023 --- /dev/null +++ b/apps/main/src/lib/components/ui/dialog/dialog-description.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/main/src/lib/components/ui/dialog/dialog-footer.svelte b/apps/main/src/lib/components/ui/dialog/dialog-footer.svelte new file mode 100644 index 0000000..e7ff446 --- /dev/null +++ b/apps/main/src/lib/components/ui/dialog/dialog-footer.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/main/src/lib/components/ui/dialog/dialog-header.svelte b/apps/main/src/lib/components/ui/dialog/dialog-header.svelte new file mode 100644 index 0000000..4e5c447 --- /dev/null +++ b/apps/main/src/lib/components/ui/dialog/dialog-header.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/main/src/lib/components/ui/dialog/dialog-overlay.svelte b/apps/main/src/lib/components/ui/dialog/dialog-overlay.svelte new file mode 100644 index 0000000..f81ad83 --- /dev/null +++ b/apps/main/src/lib/components/ui/dialog/dialog-overlay.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/main/src/lib/components/ui/dialog/dialog-portal.svelte b/apps/main/src/lib/components/ui/dialog/dialog-portal.svelte new file mode 100644 index 0000000..ccfa79c --- /dev/null +++ b/apps/main/src/lib/components/ui/dialog/dialog-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/main/src/lib/components/ui/dialog/dialog-title.svelte b/apps/main/src/lib/components/ui/dialog/dialog-title.svelte new file mode 100644 index 0000000..e4d4b34 --- /dev/null +++ b/apps/main/src/lib/components/ui/dialog/dialog-title.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/main/src/lib/components/ui/dialog/dialog-trigger.svelte b/apps/main/src/lib/components/ui/dialog/dialog-trigger.svelte new file mode 100644 index 0000000..9d1e801 --- /dev/null +++ b/apps/main/src/lib/components/ui/dialog/dialog-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/main/src/lib/components/ui/dialog/dialog.svelte b/apps/main/src/lib/components/ui/dialog/dialog.svelte new file mode 100644 index 0000000..211672c --- /dev/null +++ b/apps/main/src/lib/components/ui/dialog/dialog.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/main/src/lib/components/ui/dialog/index.ts b/apps/main/src/lib/components/ui/dialog/index.ts new file mode 100644 index 0000000..076cef5 --- /dev/null +++ b/apps/main/src/lib/components/ui/dialog/index.ts @@ -0,0 +1,34 @@ +import Root from "./dialog.svelte"; +import Portal from "./dialog-portal.svelte"; +import Title from "./dialog-title.svelte"; +import Footer from "./dialog-footer.svelte"; +import Header from "./dialog-header.svelte"; +import Overlay from "./dialog-overlay.svelte"; +import Content from "./dialog-content.svelte"; +import Description from "./dialog-description.svelte"; +import Trigger from "./dialog-trigger.svelte"; +import Close from "./dialog-close.svelte"; + +export { + Root, + Title, + Portal, + Footer, + Header, + Trigger, + Overlay, + Content, + Description, + Close, + // + Root as Dialog, + Title as DialogTitle, + Portal as DialogPortal, + Footer as DialogFooter, + Header as DialogHeader, + Trigger as DialogTrigger, + Overlay as DialogOverlay, + Content as DialogContent, + Description as DialogDescription, + Close as DialogClose, +}; diff --git a/apps/main/src/lib/components/ui/drawer/drawer-close.svelte b/apps/main/src/lib/components/ui/drawer/drawer-close.svelte new file mode 100644 index 0000000..95c2479 --- /dev/null +++ b/apps/main/src/lib/components/ui/drawer/drawer-close.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/main/src/lib/components/ui/drawer/drawer-content.svelte b/apps/main/src/lib/components/ui/drawer/drawer-content.svelte new file mode 100644 index 0000000..6bb01db --- /dev/null +++ b/apps/main/src/lib/components/ui/drawer/drawer-content.svelte @@ -0,0 +1,40 @@ + + + + + + + {@render children?.()} + + diff --git a/apps/main/src/lib/components/ui/drawer/drawer-description.svelte b/apps/main/src/lib/components/ui/drawer/drawer-description.svelte new file mode 100644 index 0000000..2763a1a --- /dev/null +++ b/apps/main/src/lib/components/ui/drawer/drawer-description.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/main/src/lib/components/ui/drawer/drawer-footer.svelte b/apps/main/src/lib/components/ui/drawer/drawer-footer.svelte new file mode 100644 index 0000000..1691f58 --- /dev/null +++ b/apps/main/src/lib/components/ui/drawer/drawer-footer.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/main/src/lib/components/ui/drawer/drawer-header.svelte b/apps/main/src/lib/components/ui/drawer/drawer-header.svelte new file mode 100644 index 0000000..65d2de5 --- /dev/null +++ b/apps/main/src/lib/components/ui/drawer/drawer-header.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/main/src/lib/components/ui/drawer/drawer-nested.svelte b/apps/main/src/lib/components/ui/drawer/drawer-nested.svelte new file mode 100644 index 0000000..834af94 --- /dev/null +++ b/apps/main/src/lib/components/ui/drawer/drawer-nested.svelte @@ -0,0 +1,12 @@ + + + diff --git a/apps/main/src/lib/components/ui/drawer/drawer-overlay.svelte b/apps/main/src/lib/components/ui/drawer/drawer-overlay.svelte new file mode 100644 index 0000000..53f78a2 --- /dev/null +++ b/apps/main/src/lib/components/ui/drawer/drawer-overlay.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/main/src/lib/components/ui/drawer/drawer-portal.svelte b/apps/main/src/lib/components/ui/drawer/drawer-portal.svelte new file mode 100644 index 0000000..5a0dd74 --- /dev/null +++ b/apps/main/src/lib/components/ui/drawer/drawer-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/main/src/lib/components/ui/drawer/drawer-title.svelte b/apps/main/src/lib/components/ui/drawer/drawer-title.svelte new file mode 100644 index 0000000..a2e5761 --- /dev/null +++ b/apps/main/src/lib/components/ui/drawer/drawer-title.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/main/src/lib/components/ui/drawer/drawer-trigger.svelte b/apps/main/src/lib/components/ui/drawer/drawer-trigger.svelte new file mode 100644 index 0000000..f1877d8 --- /dev/null +++ b/apps/main/src/lib/components/ui/drawer/drawer-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/main/src/lib/components/ui/drawer/drawer.svelte b/apps/main/src/lib/components/ui/drawer/drawer.svelte new file mode 100644 index 0000000..0cb57ff --- /dev/null +++ b/apps/main/src/lib/components/ui/drawer/drawer.svelte @@ -0,0 +1,12 @@ + + + diff --git a/apps/main/src/lib/components/ui/drawer/index.ts b/apps/main/src/lib/components/ui/drawer/index.ts new file mode 100644 index 0000000..1656cac --- /dev/null +++ b/apps/main/src/lib/components/ui/drawer/index.ts @@ -0,0 +1,38 @@ +import Root from "./drawer.svelte"; +import Content from "./drawer-content.svelte"; +import Description from "./drawer-description.svelte"; +import Overlay from "./drawer-overlay.svelte"; +import Footer from "./drawer-footer.svelte"; +import Header from "./drawer-header.svelte"; +import Title from "./drawer-title.svelte"; +import NestedRoot from "./drawer-nested.svelte"; +import Close from "./drawer-close.svelte"; +import Trigger from "./drawer-trigger.svelte"; +import Portal from "./drawer-portal.svelte"; + +export { + Root, + NestedRoot, + Content, + Description, + Overlay, + Footer, + Header, + Title, + Trigger, + Portal, + Close, + + // + Root as Drawer, + NestedRoot as DrawerNestedRoot, + Content as DrawerContent, + Description as DrawerDescription, + Overlay as DrawerOverlay, + Footer as DrawerFooter, + Header as DrawerHeader, + Title as DrawerTitle, + Trigger as DrawerTrigger, + Portal as DrawerPortal, + Close as DrawerClose, +}; diff --git a/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte new file mode 100644 index 0000000..e0e1971 --- /dev/null +++ b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte @@ -0,0 +1,16 @@ + + + diff --git a/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte new file mode 100644 index 0000000..6d9ef85 --- /dev/null +++ b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte @@ -0,0 +1,43 @@ + + + + {#snippet children({ checked, indeterminate })} + + {#if indeterminate} + + {:else} + + {/if} + + {@render childrenProp?.()} + {/snippet} + diff --git a/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte new file mode 100644 index 0000000..1e96782 --- /dev/null +++ b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte @@ -0,0 +1,29 @@ + + + + + diff --git a/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte new file mode 100644 index 0000000..433540f --- /dev/null +++ b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte @@ -0,0 +1,22 @@ + + + diff --git a/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte new file mode 100644 index 0000000..aca1f7b --- /dev/null +++ b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte new file mode 100644 index 0000000..04cd110 --- /dev/null +++ b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte @@ -0,0 +1,27 @@ + + + diff --git a/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte new file mode 100644 index 0000000..9681c2b --- /dev/null +++ b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte @@ -0,0 +1,24 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte new file mode 100644 index 0000000..274cfef --- /dev/null +++ b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte new file mode 100644 index 0000000..189aef4 --- /dev/null +++ b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte @@ -0,0 +1,16 @@ + + + diff --git a/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte new file mode 100644 index 0000000..ce2ad09 --- /dev/null +++ b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte @@ -0,0 +1,33 @@ + + + + {#snippet children({ checked })} + + {#if checked} + + {/if} + + {@render childrenProp?.({ checked })} + {/snippet} + diff --git a/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte new file mode 100644 index 0000000..90f1b6f --- /dev/null +++ b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte new file mode 100644 index 0000000..7c6e9c6 --- /dev/null +++ b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte new file mode 100644 index 0000000..3f06dc4 --- /dev/null +++ b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte new file mode 100644 index 0000000..5f49d01 --- /dev/null +++ b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + + diff --git a/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte new file mode 100644 index 0000000..f044581 --- /dev/null +++ b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte new file mode 100644 index 0000000..cb05344 --- /dev/null +++ b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte new file mode 100644 index 0000000..cb4bc62 --- /dev/null +++ b/apps/main/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/main/src/lib/components/ui/dropdown-menu/index.ts b/apps/main/src/lib/components/ui/dropdown-menu/index.ts new file mode 100644 index 0000000..7850c6a --- /dev/null +++ b/apps/main/src/lib/components/ui/dropdown-menu/index.ts @@ -0,0 +1,54 @@ +import Root from "./dropdown-menu.svelte"; +import Sub from "./dropdown-menu-sub.svelte"; +import CheckboxGroup from "./dropdown-menu-checkbox-group.svelte"; +import CheckboxItem from "./dropdown-menu-checkbox-item.svelte"; +import Content from "./dropdown-menu-content.svelte"; +import Group from "./dropdown-menu-group.svelte"; +import Item from "./dropdown-menu-item.svelte"; +import Label from "./dropdown-menu-label.svelte"; +import RadioGroup from "./dropdown-menu-radio-group.svelte"; +import RadioItem from "./dropdown-menu-radio-item.svelte"; +import Separator from "./dropdown-menu-separator.svelte"; +import Shortcut from "./dropdown-menu-shortcut.svelte"; +import Trigger from "./dropdown-menu-trigger.svelte"; +import SubContent from "./dropdown-menu-sub-content.svelte"; +import SubTrigger from "./dropdown-menu-sub-trigger.svelte"; +import GroupHeading from "./dropdown-menu-group-heading.svelte"; +import Portal from "./dropdown-menu-portal.svelte"; + +export { + CheckboxGroup, + CheckboxItem, + Content, + Portal, + Root as DropdownMenu, + CheckboxGroup as DropdownMenuCheckboxGroup, + CheckboxItem as DropdownMenuCheckboxItem, + Content as DropdownMenuContent, + Portal as DropdownMenuPortal, + Group as DropdownMenuGroup, + Item as DropdownMenuItem, + Label as DropdownMenuLabel, + RadioGroup as DropdownMenuRadioGroup, + RadioItem as DropdownMenuRadioItem, + Separator as DropdownMenuSeparator, + Shortcut as DropdownMenuShortcut, + Sub as DropdownMenuSub, + SubContent as DropdownMenuSubContent, + SubTrigger as DropdownMenuSubTrigger, + Trigger as DropdownMenuTrigger, + GroupHeading as DropdownMenuGroupHeading, + Group, + GroupHeading, + Item, + Label, + RadioGroup, + RadioItem, + Root, + Separator, + Shortcut, + Sub, + SubContent, + SubTrigger, + Trigger, +}; diff --git a/apps/main/src/lib/components/ui/empty/empty-content.svelte b/apps/main/src/lib/components/ui/empty/empty-content.svelte new file mode 100644 index 0000000..f5a9c68 --- /dev/null +++ b/apps/main/src/lib/components/ui/empty/empty-content.svelte @@ -0,0 +1,23 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/main/src/lib/components/ui/empty/empty-description.svelte b/apps/main/src/lib/components/ui/empty/empty-description.svelte new file mode 100644 index 0000000..85a866c --- /dev/null +++ b/apps/main/src/lib/components/ui/empty/empty-description.svelte @@ -0,0 +1,23 @@ + + +
    a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4", + className + )} + {...restProps} +> + {@render children?.()} +
    diff --git a/apps/main/src/lib/components/ui/empty/empty-header.svelte b/apps/main/src/lib/components/ui/empty/empty-header.svelte new file mode 100644 index 0000000..296eaf8 --- /dev/null +++ b/apps/main/src/lib/components/ui/empty/empty-header.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/main/src/lib/components/ui/empty/empty-media.svelte b/apps/main/src/lib/components/ui/empty/empty-media.svelte new file mode 100644 index 0000000..0b4e45d --- /dev/null +++ b/apps/main/src/lib/components/ui/empty/empty-media.svelte @@ -0,0 +1,41 @@ + + + + +
    + {@render children?.()} +
    diff --git a/apps/main/src/lib/components/ui/empty/empty-title.svelte b/apps/main/src/lib/components/ui/empty/empty-title.svelte new file mode 100644 index 0000000..8c237aa --- /dev/null +++ b/apps/main/src/lib/components/ui/empty/empty-title.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/main/src/lib/components/ui/empty/empty.svelte b/apps/main/src/lib/components/ui/empty/empty.svelte new file mode 100644 index 0000000..4ccf060 --- /dev/null +++ b/apps/main/src/lib/components/ui/empty/empty.svelte @@ -0,0 +1,23 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/main/src/lib/components/ui/empty/index.ts b/apps/main/src/lib/components/ui/empty/index.ts new file mode 100644 index 0000000..ae4c106 --- /dev/null +++ b/apps/main/src/lib/components/ui/empty/index.ts @@ -0,0 +1,22 @@ +import Root from "./empty.svelte"; +import Header from "./empty-header.svelte"; +import Media from "./empty-media.svelte"; +import Title from "./empty-title.svelte"; +import Description from "./empty-description.svelte"; +import Content from "./empty-content.svelte"; + +export { + Root, + Header, + Media, + Title, + Description, + Content, + // + Root as Empty, + Header as EmptyHeader, + Media as EmptyMedia, + Title as EmptyTitle, + Description as EmptyDescription, + Content as EmptyContent, +}; diff --git a/apps/main/src/lib/components/ui/field/field-content.svelte b/apps/main/src/lib/components/ui/field/field-content.svelte new file mode 100644 index 0000000..1b6535b --- /dev/null +++ b/apps/main/src/lib/components/ui/field/field-content.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/main/src/lib/components/ui/field/field-description.svelte b/apps/main/src/lib/components/ui/field/field-description.svelte new file mode 100644 index 0000000..a0c9f06 --- /dev/null +++ b/apps/main/src/lib/components/ui/field/field-description.svelte @@ -0,0 +1,25 @@ + + +

    a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4", + className + )} + {...restProps} +> + {@render children?.()} +

    diff --git a/apps/main/src/lib/components/ui/field/field-error.svelte b/apps/main/src/lib/components/ui/field/field-error.svelte new file mode 100644 index 0000000..1d5cc5f --- /dev/null +++ b/apps/main/src/lib/components/ui/field/field-error.svelte @@ -0,0 +1,58 @@ + + +{#if hasContent} + +{/if} diff --git a/apps/main/src/lib/components/ui/field/field-group.svelte b/apps/main/src/lib/components/ui/field/field-group.svelte new file mode 100644 index 0000000..e685427 --- /dev/null +++ b/apps/main/src/lib/components/ui/field/field-group.svelte @@ -0,0 +1,23 @@ + + +
    [data-slot=field-group]]:gap-4", + className + )} + {...restProps} +> + {@render children?.()} +
    diff --git a/apps/main/src/lib/components/ui/field/field-label.svelte b/apps/main/src/lib/components/ui/field/field-label.svelte new file mode 100644 index 0000000..2ee431a --- /dev/null +++ b/apps/main/src/lib/components/ui/field/field-label.svelte @@ -0,0 +1,26 @@ + + + diff --git a/apps/main/src/lib/components/ui/field/field-legend.svelte b/apps/main/src/lib/components/ui/field/field-legend.svelte new file mode 100644 index 0000000..3f1c50f --- /dev/null +++ b/apps/main/src/lib/components/ui/field/field-legend.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + diff --git a/apps/main/src/lib/components/ui/field/field-separator.svelte b/apps/main/src/lib/components/ui/field/field-separator.svelte new file mode 100644 index 0000000..12bcb77 --- /dev/null +++ b/apps/main/src/lib/components/ui/field/field-separator.svelte @@ -0,0 +1,38 @@ + + +
    + + {#if children} + + {@render children()} + + {/if} +
    diff --git a/apps/main/src/lib/components/ui/field/field-set.svelte b/apps/main/src/lib/components/ui/field/field-set.svelte new file mode 100644 index 0000000..1d8e233 --- /dev/null +++ b/apps/main/src/lib/components/ui/field/field-set.svelte @@ -0,0 +1,24 @@ + + +
    [data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3", + className + )} + {...restProps} +> + {@render children?.()} +
    diff --git a/apps/main/src/lib/components/ui/field/field-title.svelte b/apps/main/src/lib/components/ui/field/field-title.svelte new file mode 100644 index 0000000..5906044 --- /dev/null +++ b/apps/main/src/lib/components/ui/field/field-title.svelte @@ -0,0 +1,23 @@ + + +
    + {@render children?.()} +
    diff --git a/apps/main/src/lib/components/ui/field/field.svelte b/apps/main/src/lib/components/ui/field/field.svelte new file mode 100644 index 0000000..3284203 --- /dev/null +++ b/apps/main/src/lib/components/ui/field/field.svelte @@ -0,0 +1,53 @@ + + + + +
    + {@render children?.()} +
    diff --git a/apps/main/src/lib/components/ui/field/index.ts b/apps/main/src/lib/components/ui/field/index.ts new file mode 100644 index 0000000..a644a95 --- /dev/null +++ b/apps/main/src/lib/components/ui/field/index.ts @@ -0,0 +1,33 @@ +import Field from "./field.svelte"; +import Set from "./field-set.svelte"; +import Legend from "./field-legend.svelte"; +import Group from "./field-group.svelte"; +import Content from "./field-content.svelte"; +import Label from "./field-label.svelte"; +import Title from "./field-title.svelte"; +import Description from "./field-description.svelte"; +import Separator from "./field-separator.svelte"; +import Error from "./field-error.svelte"; + +export { + Field, + Set, + Legend, + Group, + Content, + Label, + Title, + Description, + Separator, + Error, + // + Set as FieldSet, + Legend as FieldLegend, + Group as FieldGroup, + Content as FieldContent, + Label as FieldLabel, + Title as FieldTitle, + Description as FieldDescription, + Separator as FieldSeparator, + Error as FieldError, +}; diff --git a/apps/main/src/lib/components/ui/form/form-button.svelte b/apps/main/src/lib/components/ui/form/form-button.svelte new file mode 100644 index 0000000..48d3936 --- /dev/null +++ b/apps/main/src/lib/components/ui/form/form-button.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/main/src/lib/components/ui/input-group/input-group-input.svelte b/apps/main/src/lib/components/ui/input-group/input-group-input.svelte new file mode 100644 index 0000000..ded2655 --- /dev/null +++ b/apps/main/src/lib/components/ui/input-group/input-group-input.svelte @@ -0,0 +1,23 @@ + + + diff --git a/apps/main/src/lib/components/ui/input-group/input-group-text.svelte b/apps/main/src/lib/components/ui/input-group/input-group-text.svelte new file mode 100644 index 0000000..9c43dc4 --- /dev/null +++ b/apps/main/src/lib/components/ui/input-group/input-group-text.svelte @@ -0,0 +1,22 @@ + + + + {@render children?.()} + diff --git a/apps/main/src/lib/components/ui/input-group/input-group-textarea.svelte b/apps/main/src/lib/components/ui/input-group/input-group-textarea.svelte new file mode 100644 index 0000000..91850ff --- /dev/null +++ b/apps/main/src/lib/components/ui/input-group/input-group-textarea.svelte @@ -0,0 +1,23 @@ + + + diff --git a/apps/main/src/lib/components/ui/toggle-group/index.ts b/apps/main/src/lib/components/ui/toggle-group/index.ts new file mode 100644 index 0000000..12b14b9 --- /dev/null +++ b/apps/main/src/lib/components/ui/toggle-group/index.ts @@ -0,0 +1,10 @@ +import Root from "./toggle-group.svelte"; +import Item from "./toggle-group-item.svelte"; + +export { + Root, + Item, + // + Root as ToggleGroup, + Item as ToggleGroupItem, +}; diff --git a/apps/main/src/lib/components/ui/toggle-group/toggle-group-item.svelte b/apps/main/src/lib/components/ui/toggle-group/toggle-group-item.svelte new file mode 100644 index 0000000..6d60b52 --- /dev/null +++ b/apps/main/src/lib/components/ui/toggle-group/toggle-group-item.svelte @@ -0,0 +1,35 @@ + + + diff --git a/apps/main/src/lib/components/ui/toggle-group/toggle-group.svelte b/apps/main/src/lib/components/ui/toggle-group/toggle-group.svelte new file mode 100644 index 0000000..106561c --- /dev/null +++ b/apps/main/src/lib/components/ui/toggle-group/toggle-group.svelte @@ -0,0 +1,59 @@ + + + + + + diff --git a/apps/main/src/lib/components/ui/toggle/index.ts b/apps/main/src/lib/components/ui/toggle/index.ts new file mode 100644 index 0000000..8cb2936 --- /dev/null +++ b/apps/main/src/lib/components/ui/toggle/index.ts @@ -0,0 +1,13 @@ +import Root from "./toggle.svelte"; +export { + toggleVariants, + type ToggleSize, + type ToggleVariant, + type ToggleVariants, +} from "./toggle.svelte"; + +export { + Root, + // + Root as Toggle, +}; diff --git a/apps/main/src/lib/components/ui/toggle/toggle.svelte b/apps/main/src/lib/components/ui/toggle/toggle.svelte new file mode 100644 index 0000000..56eb86b --- /dev/null +++ b/apps/main/src/lib/components/ui/toggle/toggle.svelte @@ -0,0 +1,52 @@ + + + + + diff --git a/apps/main/src/lib/components/ui/tooltip/index.ts b/apps/main/src/lib/components/ui/tooltip/index.ts new file mode 100644 index 0000000..1718604 --- /dev/null +++ b/apps/main/src/lib/components/ui/tooltip/index.ts @@ -0,0 +1,19 @@ +import Root from "./tooltip.svelte"; +import Trigger from "./tooltip-trigger.svelte"; +import Content from "./tooltip-content.svelte"; +import Provider from "./tooltip-provider.svelte"; +import Portal from "./tooltip-portal.svelte"; + +export { + Root, + Trigger, + Content, + Provider, + Portal, + // + Root as Tooltip, + Content as TooltipContent, + Trigger as TooltipTrigger, + Provider as TooltipProvider, + Portal as TooltipPortal, +}; diff --git a/apps/main/src/lib/components/ui/tooltip/tooltip-content.svelte b/apps/main/src/lib/components/ui/tooltip/tooltip-content.svelte new file mode 100644 index 0000000..788ec34 --- /dev/null +++ b/apps/main/src/lib/components/ui/tooltip/tooltip-content.svelte @@ -0,0 +1,52 @@ + + + + + {@render children?.()} + + {#snippet child({ props })} +
    + {/snippet} +
    +
    +
    diff --git a/apps/main/src/lib/components/ui/tooltip/tooltip-portal.svelte b/apps/main/src/lib/components/ui/tooltip/tooltip-portal.svelte new file mode 100644 index 0000000..d234f7d --- /dev/null +++ b/apps/main/src/lib/components/ui/tooltip/tooltip-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/main/src/lib/components/ui/tooltip/tooltip-provider.svelte b/apps/main/src/lib/components/ui/tooltip/tooltip-provider.svelte new file mode 100644 index 0000000..8150bef --- /dev/null +++ b/apps/main/src/lib/components/ui/tooltip/tooltip-provider.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/main/src/lib/components/ui/tooltip/tooltip-trigger.svelte b/apps/main/src/lib/components/ui/tooltip/tooltip-trigger.svelte new file mode 100644 index 0000000..1acdaa4 --- /dev/null +++ b/apps/main/src/lib/components/ui/tooltip/tooltip-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/main/src/lib/components/ui/tooltip/tooltip.svelte b/apps/main/src/lib/components/ui/tooltip/tooltip.svelte new file mode 100644 index 0000000..0b0f9ce --- /dev/null +++ b/apps/main/src/lib/components/ui/tooltip/tooltip.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/main/src/lib/core/constants.ts b/apps/main/src/lib/core/constants.ts new file mode 100644 index 0000000..dad2799 --- /dev/null +++ b/apps/main/src/lib/core/constants.ts @@ -0,0 +1,50 @@ +import LayoutDashboard from "@lucide/svelte/icons/layout-dashboard"; +import { BellRingIcon, UsersIcon } from "@lucide/svelte"; +import UserCircle from "~icons/lucide/user-circle"; + +export type AppSidebarItem = { + title: string; + url: string; + icon?: any; + isActive?: boolean; + items?: { + title: string; + url: string; + }[]; +}; + +export const mainNavTree = [ + { + title: "Dashboard", + url: "/dashboard", + icon: LayoutDashboard, + isActive: true, + }, + { + title: "Users", + url: "/users", + icon: UsersIcon, + }, +] as AppSidebarItem[]; + +export const secondaryNavTree = [ + { + title: "Account", + url: "/account", + icon: UserCircle, + }, + { + title: "Notifications", + url: "/notifications", + icon: BellRingIcon, + }, +] as AppSidebarItem[]; + +export const COMPANY_NAME = "SaaS Template"; +export const WEBSITE_URL = "https://company.com"; + +export const CONTACT_EMAIL = "contact@ahmadrehan.com"; +export const CONTACT_INFO = { email: CONTACT_EMAIL }; + +export const TRANSITION_COLORS = "transition-colors duration-150 ease-in-out"; +export const TRANSITION_ALL = "transition-all duration-150 ease-in-out"; diff --git a/apps/main/src/lib/currency.utils.ts b/apps/main/src/lib/currency.utils.ts new file mode 100644 index 0000000..1dc9712 --- /dev/null +++ b/apps/main/src/lib/currency.utils.ts @@ -0,0 +1,5 @@ +export function formatCurrency(amount: number, currency = "EUR"): string { + return new Intl.NumberFormat("en-US", { style: "currency", currency }).format( + amount, + ); +} diff --git a/apps/main/src/lib/domains/account/account.vm.svelte.ts b/apps/main/src/lib/domains/account/account.vm.svelte.ts new file mode 100644 index 0000000..4c0fa77 --- /dev/null +++ b/apps/main/src/lib/domains/account/account.vm.svelte.ts @@ -0,0 +1,157 @@ +import type { User } from "@pkg/logic/domains/user/data"; +import { authClient } from "$lib/auth.client"; +import { toast } from "svelte-sonner"; +import { ResultAsync, errAsync, okAsync } from "neverthrow"; +import type { Err } from "@pkg/result"; + +class AccountViewModel { + loading = $state(false); + emailLoading = $state(false); + errorMessage = $state(null); + + async updateProfilePicture(imagePath: string): Promise { + const result = await ResultAsync.fromPromise( + authClient.updateUser({ image: imagePath }), + (error): Err => ({ + code: "NETWORK_ERROR", + message: "Failed to update profile picture", + description: "Network request failed", + detail: error instanceof Error ? error.message : String(error), + }), + ) + .andThen((response) => { + if (response.error) { + return errAsync({ + code: "API_ERROR", + message: + response.error.message ?? + "Failed to update profile picture", + description: + response.error.statusText ?? + "Please try again later", + detail: response.error.statusText ?? "Unknown error", + }); + } + return okAsync(response.data); + }); + + return result.match( + () => { + toast.success("Profile picture updated"); + return true; + }, + (error) => { + this.errorMessage = error.message ?? "Failed to update profile picture"; + toast.error(this.errorMessage, { + description: error.description, + }); + return false; + }, + ); + } + + async updateProfile(userData: { + name: string; + username: string; + }): Promise { + this.loading = true; + this.errorMessage = null; + + const result = await ResultAsync.fromPromise( + authClient.updateUser({ + displayUsername: userData.username, + username: userData.username, + name: userData.name, + }), + (error): Err => ({ + code: "NETWORK_ERROR", + message: "Failed to update profile", + description: "Network request failed", + detail: error instanceof Error ? error.message : String(error), + }), + ) + .andThen((response) => { + if (response.error) { + return errAsync({ + code: "API_ERROR", + message: + response.error.message ?? "Failed to update profile", + description: + response.error.statusText ?? + "Please try again later", + detail: response.error.statusText ?? "Unknown error", + }); + } + return okAsync(response.data); + }); + + const user = result.match( + (data) => { + toast.success("Profile updated successfully"); + window.location.reload(); + return (data as any)?.user as User | null; + }, + (error) => { + this.errorMessage = error.message ?? "Failed to update profile"; + toast.error(this.errorMessage, { + description: error.description, + }); + return null; + }, + ); + + this.loading = false; + return user; + } + + async changeEmail(email: string): Promise { + this.emailLoading = true; + this.errorMessage = null; + + const result = await ResultAsync.fromPromise( + authClient.changeEmail({ newEmail: email }), + (error): Err => ({ + code: "NETWORK_ERROR", + message: "Failed to change email", + description: "Network request failed", + detail: error instanceof Error ? error.message : String(error), + }), + ) + .andThen((response) => { + if (response.error) { + return errAsync({ + code: "API_ERROR", + message: + response.error.message ?? "Failed to change email", + description: + response.error.statusText ?? + "Please try again later", + detail: response.error.statusText ?? "Unknown error", + }); + } + return okAsync(response.data); + }); + + const success = result.match( + () => { + toast.success("Verification email sent!", { + description: + "Please check your inbox and verify your new email address.", + }); + return true; + }, + (error) => { + this.errorMessage = error.message ?? "Failed to change email"; + toast.error(this.errorMessage, { + description: error.description, + }); + return false; + }, + ); + + this.emailLoading = false; + return success; + } +} + +export const accountVM = new AccountViewModel(); diff --git a/apps/main/src/lib/domains/account/sessions/sessions-card.svelte b/apps/main/src/lib/domains/account/sessions/sessions-card.svelte new file mode 100644 index 0000000..36c64a1 --- /dev/null +++ b/apps/main/src/lib/domains/account/sessions/sessions-card.svelte @@ -0,0 +1,182 @@ + + + + +
    + + Active Sessions +
    + + Manage and monitor your active login sessions across devices. + +
    + +
    + {#if sessionsVM.isLoading && sessionsVM.activeSessions.length === 0} +
    + +
    + {:else if sessionsVM.activeSessions.length > 0} +
    + {#each sessionsVM.activeSessions as session} + {@const { os, browser } = extractInfoFromUA( + session.userAgent ?? "", + )} +
    +
    +
    + +
    +
    +
    +

    + {browser || "Unknown Browser"} + on + {os || "Unknown OS"} +

    + {#if session.isCurrent} + + Current Session + + {/if} +
    +
    + {session.ipAddress || "Unknown IP"} +
    +
    + + + Created{" "} + {sessionsVM.formatRelativeTime( + session.createdAt.getTime(), + )} + +
    +
    +
    + {#if !session.isCurrent} + + {/if} +
    + {/each} +
    + + {#if sessionsVM.activeSessions.filter((s) => !s.isCurrent).length > 0} + + {/if} + {:else} +
    +

    + No active sessions found. This is unusual and might + indicate a problem. +

    +
    + {/if} +
    +
    +
    diff --git a/apps/main/src/lib/domains/account/sessions/sessions.vm.svelte.ts b/apps/main/src/lib/domains/account/sessions/sessions.vm.svelte.ts new file mode 100644 index 0000000..d6553cf --- /dev/null +++ b/apps/main/src/lib/domains/account/sessions/sessions.vm.svelte.ts @@ -0,0 +1,209 @@ +import type { ModifiedSession } from "@pkg/logic/domains/user/data"; +import { authClient } from "$lib/auth.client"; +import { toast } from "svelte-sonner"; +import { ResultAsync, errAsync, okAsync } from "neverthrow"; +import type { Err } from "@pkg/result"; + +class SessionsViewModel { + session: ModifiedSession | undefined = $state(undefined); + activeSessions = $state([]); + isLoading = $state(false); + errorMessage = $state(null); + + async setCurrentSession(s: ModifiedSession) { + this.session = s; + } + + async fetchActiveSessions() { + this.isLoading = true; + this.errorMessage = null; + + const result = await ResultAsync.fromPromise( + authClient.listSessions(), + (error): Err => ({ + code: "NETWORK_ERROR", + message: "Failed to fetch active sessions", + description: "Network request failed", + detail: error instanceof Error ? error.message : String(error), + }), + ) + .andThen((response) => { + if (response.error) { + return errAsync({ + code: "API_ERROR", + message: "Failed to fetch active sessions", + description: response.error.message ?? "Unknown error", + detail: response.error.statusText ?? "Unknown error", + }); + } + return okAsync(response.data ?? []); + }); + + const sessions = result.match( + (data) => { + this.activeSessions = data.map((session: ModifiedSession) => ({ + ...session, + isCurrent: session.id === this.session?.id, + })); + return this.activeSessions; + }, + (error) => { + this.errorMessage = error.message ?? "Failed to fetch active sessions"; + toast.error("Failed to fetch active sessions", { + description: error.description, + }); + return []; + }, + ); + + this.isLoading = false; + return sessions; + } + + async terminateSession(sessionId: string) { + this.isLoading = true; + this.errorMessage = null; + + const result = await ResultAsync.fromPromise( + authClient.revokeSession({ + token: sessionId, + }), + (error): Err => ({ + code: "NETWORK_ERROR", + message: "Failed to terminate session", + description: "Network request failed", + detail: error instanceof Error ? error.message : String(error), + }), + ) + .andThen((response) => { + if (response.error) { + return errAsync({ + code: "API_ERROR", + message: "Failed to terminate session", + description: response.error.message ?? "Unknown error", + detail: response.error.statusText ?? "Unknown error", + }); + } + return okAsync(response.data); + }); + + result.match( + () => { + this.activeSessions = this.activeSessions.filter( + (session) => session.id !== sessionId, + ); + toast.success("Session terminated"); + }, + (error) => { + this.errorMessage = error.message ?? "Failed to terminate session"; + toast.error("Failed to terminate session", { + description: error.description, + }); + }, + ); + + this.isLoading = false; + } + + async terminateAllOtherSessions() { + this.isLoading = true; + this.errorMessage = null; + + const result = await ResultAsync.fromPromise( + authClient.revokeOtherSessions(), + (error): Err => ({ + code: "NETWORK_ERROR", + message: "Failed to terminate other sessions", + description: "Network request failed", + detail: error instanceof Error ? error.message : String(error), + }), + ) + .andThen((response) => { + if (response.error) { + return errAsync({ + code: "API_ERROR", + message: "Failed to terminate other sessions", + description: response.error.message ?? "Unknown error", + detail: response.error.statusText ?? "Unknown error", + }); + } + return okAsync(response.data); + }); + + result.match( + () => { + this.activeSessions = this.activeSessions.filter( + // @ts-ignore + (session) => session.isCurrent, + ); + toast.success("All other sessions terminated"); + }, + (error) => { + this.errorMessage = + error.message ?? "Failed to terminate other sessions"; + toast.error("Failed to terminate other sessions", { + description: error.description, + }); + }, + ); + + this.isLoading = false; + } + + async logout(skipToast = false) { + const result = await ResultAsync.fromPromise( + authClient.signOut(), + (error): Err => ({ + code: "NETWORK_ERROR", + message: "Failed to log out", + description: "Network request failed", + detail: error instanceof Error ? error.message : String(error), + }), + ); + + result.match( + () => { + if (!skipToast) { + toast("Logged out successfully, redirecting..."); + } + setTimeout(() => { + window.location.href = "/auth/login"; + }, 500); + }, + (error) => { + toast.error("Failed to log out", { + description: error.description, + }); + }, + ); + } + + formatRelativeTime(timestamp: string | number): string { + const date = new Date(timestamp); + const now = new Date(); + const diffInSeconds = Math.floor( + (now.getTime() - date.getTime()) / 1000, + ); + + if (diffInSeconds < 60) { + return "just now"; + } else if (diffInSeconds < 3600) { + const minutes = Math.floor(diffInSeconds / 60); + return `${minutes} minute${minutes > 1 ? "s" : ""} ago`; + } else if (diffInSeconds < 86400) { + const hours = Math.floor(diffInSeconds / 3600); + return `${hours} hour${hours > 1 ? "s" : ""} ago`; + } else { + const days = Math.floor(diffInSeconds / 86400); + return `${days} day${days > 1 ? "s" : ""} ago`; + } + } + + reset() { + this.activeSessions = []; + this.isLoading = false; + this.errorMessage = null; + } +} + +export const sessionsVM = new SessionsViewModel(); diff --git a/apps/main/src/lib/domains/notifications/notification.vm.svelte.ts b/apps/main/src/lib/domains/notifications/notification.vm.svelte.ts new file mode 100644 index 0000000..0ad8ca9 --- /dev/null +++ b/apps/main/src/lib/domains/notifications/notification.vm.svelte.ts @@ -0,0 +1,536 @@ +import type { + ClientNotificationFilters, + ClientPaginationState, + Notifications, +} from "@pkg/logic/domains/notifications/data"; +import { apiClient, user } from "$lib/global.stores"; +import { toast } from "svelte-sonner"; +import { get } from "svelte/store"; +import { ResultAsync, errAsync, okAsync } from "neverthrow"; +import type { Err } from "@pkg/result"; + +class NotificationViewModel { + notifications = $state([] as Notifications); + loading = $state(false); + selectedIds = $state(new Set()); + + // Pagination state + pagination = $state({ + page: 1, + pageSize: 20, + total: 0, + totalPages: 0, + sortBy: "createdAt", + sortOrder: "desc", + }); + + // Filter state + filters = $state({ + userId: get(user)?.id!, + isArchived: false, // Default to showing non-archived + }); + + // Stats + unreadCount = $state(0); + + async fetchNotifications() { + this.loading = true; + + const params = new URLSearchParams(); + + // Add pagination params + params.append("page", this.pagination.page.toString()); + params.append("pageSize", this.pagination.pageSize.toString()); + params.append("sortBy", this.pagination.sortBy); + params.append("sortOrder", this.pagination.sortOrder); + + // Add filter params + if (this.filters.isRead !== undefined) { + params.append("isRead", this.filters.isRead.toString()); + } + if (this.filters.isArchived !== undefined) { + params.append("isArchived", this.filters.isArchived.toString()); + } + if (this.filters.type) { + params.append("type", this.filters.type); + } + if (this.filters.category) { + params.append("category", this.filters.category); + } + if (this.filters.priority) { + params.append("priority", this.filters.priority); + } + if (this.filters.search) { + params.append("search", this.filters.search); + } + + const result = await ResultAsync.fromPromise( + get(apiClient).notifications.$get({ + query: Object.fromEntries(params.entries()), + }), + (error): Err => ({ + code: "NETWORK_ERROR", + message: "Failed to fetch notifications", + description: "Network request failed", + detail: error instanceof Error ? error.message : String(error), + }), + ) + .andThen((response) => { + if (!response.ok) { + return errAsync({ + code: "API_ERROR", + message: "Failed to fetch notifications", + description: `Response failed with status ${response.status}`, + detail: `HTTP ${response.status}`, + }); + } + return ResultAsync.fromPromise( + response.json(), + (error): Err => ({ + code: "PARSING_ERROR", + message: "Failed to parse response", + description: "Invalid response format", + detail: error instanceof Error ? error.message : String(error), + }), + ); + }) + .andThen((apiResult: any) => { + if (apiResult.error || !apiResult.data) { + return errAsync( + apiResult.error || { + code: "API_ERROR", + message: "Failed to fetch notifications", + description: "Invalid response data", + detail: "Missing data in response", + }, + ); + } + return okAsync(apiResult.data); + }); + + result.match( + (data) => { + this.notifications = data.data as any; + this.pagination.total = data.total; + this.pagination.totalPages = data.totalPages; + }, + (error) => { + const errorMessage = error.message || "Failed to fetch notifications"; + toast.error(errorMessage, { + description: error.description || "Please try again later", + }); + }, + ); + + this.loading = false; + } + + async markAsRead(notificationIds: number[]) { + const result = await ResultAsync.fromPromise( + get(apiClient).notifications["mark-read"].$put({ + json: { notificationIds }, + }), + (error): Err => ({ + code: "NETWORK_ERROR", + message: "Failed to mark as read", + description: "Network request failed", + detail: error instanceof Error ? error.message : String(error), + }), + ) + .andThen((response) => { + if (!response.ok) { + return errAsync({ + code: "API_ERROR", + message: "Failed to mark as read", + description: `Response failed with status ${response.status}`, + detail: `HTTP ${response.status}`, + }); + } + return ResultAsync.fromPromise( + response.json(), + (error): Err => ({ + code: "PARSING_ERROR", + message: "Failed to parse response", + description: "Invalid response format", + detail: error instanceof Error ? error.message : String(error), + }), + ); + }) + .andThen((apiResult: any) => { + if (apiResult.error) { + return errAsync(apiResult.error); + } + return okAsync(apiResult.data); + }); + + result.match( + async () => { + await this.fetchNotifications(); + await this.fetchUnreadCount(); + }, + (error) => { + const errorMessage = error.message || "Failed to mark as read"; + toast.error(errorMessage, { + description: error.description || "Please try again later", + }); + }, + ); + } + + async markAsUnread(notificationIds: number[]) { + const result = await ResultAsync.fromPromise( + get(apiClient).notifications["mark-unread"].$put({ + json: { notificationIds }, + }), + (error): Err => ({ + code: "NETWORK_ERROR", + message: "Failed to mark as unread", + description: "Network request failed", + detail: error instanceof Error ? error.message : String(error), + }), + ) + .andThen((response) => { + if (!response.ok) { + return errAsync({ + code: "API_ERROR", + message: "Failed to mark as unread", + description: `Response failed with status ${response.status}`, + detail: `HTTP ${response.status}`, + }); + } + return ResultAsync.fromPromise( + response.json(), + (error): Err => ({ + code: "PARSING_ERROR", + message: "Failed to parse response", + description: "Invalid response format", + detail: error instanceof Error ? error.message : String(error), + }), + ); + }) + .andThen((apiResult: any) => { + if (apiResult.error) { + return errAsync(apiResult.error); + } + return okAsync(apiResult.data); + }); + + result.match( + async () => { + await this.fetchNotifications(); + await this.fetchUnreadCount(); + }, + (error) => { + const errorMessage = error.message || "Failed to mark as unread"; + toast.error(errorMessage, { + description: error.description || "Please try again later", + }); + }, + ); + } + + async archive(notificationIds: number[]) { + const result = await ResultAsync.fromPromise( + get(apiClient).notifications.archive.$put({ + json: { notificationIds }, + }), + (error): Err => ({ + code: "NETWORK_ERROR", + message: "Failed to archive", + description: "Network request failed", + detail: error instanceof Error ? error.message : String(error), + }), + ) + .andThen((response) => { + if (!response.ok) { + return errAsync({ + code: "API_ERROR", + message: "Failed to archive", + description: `Response failed with status ${response.status}`, + detail: `HTTP ${response.status}`, + }); + } + return ResultAsync.fromPromise( + response.json(), + (error): Err => ({ + code: "PARSING_ERROR", + message: "Failed to parse response", + description: "Invalid response format", + detail: error instanceof Error ? error.message : String(error), + }), + ); + }) + .andThen((apiResult: any) => { + if (apiResult.error) { + return errAsync(apiResult.error); + } + return okAsync(apiResult.data); + }); + + result.match( + async () => { + await this.fetchNotifications(); + }, + (error) => { + const errorMessage = error.message || "Failed to archive"; + toast.error(errorMessage, { + description: error.description || "Please try again later", + }); + }, + ); + } + + async unarchive(notificationIds: number[]) { + const result = await ResultAsync.fromPromise( + get(apiClient).notifications.unarchive.$put({ + json: { notificationIds }, + }), + (error): Err => ({ + code: "NETWORK_ERROR", + message: "Failed to unarchive", + description: "Network request failed", + detail: error instanceof Error ? error.message : String(error), + }), + ) + .andThen((response) => { + if (!response.ok) { + return errAsync({ + code: "API_ERROR", + message: "Failed to unarchive", + description: `Response failed with status ${response.status}`, + detail: `HTTP ${response.status}`, + }); + } + return ResultAsync.fromPromise( + response.json(), + (error): Err => ({ + code: "PARSING_ERROR", + message: "Failed to parse response", + description: "Invalid response format", + detail: error instanceof Error ? error.message : String(error), + }), + ); + }) + .andThen((apiResult: any) => { + if (apiResult.error) { + return errAsync(apiResult.error); + } + return okAsync(apiResult.data); + }); + + result.match( + async () => { + await this.fetchNotifications(); + }, + (error) => { + const errorMessage = error.message || "Failed to unarchive"; + toast.error(errorMessage, { + description: error.description || "Please try again later", + }); + }, + ); + } + + async deleteNotifications(notificationIds: number[]) { + const result = await ResultAsync.fromPromise( + get(apiClient).notifications.delete.$delete({ + json: { notificationIds }, + }), + (error): Err => ({ + code: "NETWORK_ERROR", + message: "Failed to delete", + description: "Network request failed", + detail: error instanceof Error ? error.message : String(error), + }), + ) + .andThen((response) => { + if (!response.ok) { + return errAsync({ + code: "API_ERROR", + message: "Failed to delete", + description: `Response failed with status ${response.status}`, + detail: `HTTP ${response.status}`, + }); + } + return ResultAsync.fromPromise( + response.json(), + (error): Err => ({ + code: "PARSING_ERROR", + message: "Failed to parse response", + description: "Invalid response format", + detail: error instanceof Error ? error.message : String(error), + }), + ); + }) + .andThen((apiResult: any) => { + if (apiResult.error) { + return errAsync(apiResult.error); + } + return okAsync(apiResult.data); + }); + + result.match( + async () => { + await this.fetchNotifications(); + await this.fetchUnreadCount(); + }, + (error) => { + const errorMessage = error.message || "Failed to delete"; + toast.error(errorMessage, { + description: error.description || "Please try again later", + }); + }, + ); + } + + async markAllAsRead() { + const result = await ResultAsync.fromPromise( + get(apiClient).notifications["mark-all-read"].$put(), + (error): Err => ({ + code: "NETWORK_ERROR", + message: "Failed to mark all as read", + description: "Network request failed", + detail: error instanceof Error ? error.message : String(error), + }), + ) + .andThen((response) => { + if (!response.ok) { + return errAsync({ + code: "API_ERROR", + message: "Failed to mark all as read", + description: `Response failed with status ${response.status}`, + detail: `HTTP ${response.status}`, + }); + } + return ResultAsync.fromPromise( + response.json(), + (error): Err => ({ + code: "PARSING_ERROR", + message: "Failed to parse response", + description: "Invalid response format", + detail: error instanceof Error ? error.message : String(error), + }), + ); + }) + .andThen((apiResult: any) => { + if (apiResult.error) { + return errAsync(apiResult.error); + } + return okAsync(apiResult.data); + }); + + result.match( + async () => { + await this.fetchNotifications(); + await this.fetchUnreadCount(); + }, + (error) => { + const errorMessage = error.message || "Failed to mark all as read"; + toast.error(errorMessage, { + description: error.description || "Please try again later", + }); + }, + ); + } + + async fetchUnreadCount() { + const result = await ResultAsync.fromPromise( + get(apiClient).notifications["unread-count"].$get(), + (error): Err => ({ + code: "NETWORK_ERROR", + message: "Failed to fetch unread count", + description: "Network request failed", + detail: error instanceof Error ? error.message : String(error), + }), + ) + .andThen((response) => { + if (!response.ok) { + return errAsync({ + code: "API_ERROR", + message: "Failed to fetch unread count", + description: `Response failed with status ${response.status}`, + detail: `HTTP ${response.status}`, + }); + } + return ResultAsync.fromPromise( + response.json(), + (error): Err => ({ + code: "PARSING_ERROR", + message: "Failed to parse response", + description: "Invalid response format", + detail: error instanceof Error ? error.message : String(error), + }), + ); + }) + .andThen((apiResult: any) => { + if (apiResult.error) { + return errAsync(apiResult.error); + } + return okAsync(apiResult.data); + }); + + result.match( + (data) => { + if (data !== undefined && data !== null) { + this.unreadCount = data as number; + } + }, + (error) => { + // Silently fail for unread count - don't show toast as it's not critical + console.error("Failed to fetch unread count:", error); + }, + ); + } + + // Helper methods + toggleSelection(id: number) { + if (this.selectedIds.has(id)) { + this.selectedIds.delete(id); + } else { + this.selectedIds.add(id); + } + } + + selectAll() { + this.notifications.forEach((n) => this.selectedIds.add(n.id)); + } + + clearSelection() { + this.selectedIds.clear(); + } + + goToPage(page: number) { + this.pagination.page = page; + this.fetchNotifications(); + } + + setPageSize(pageSize: number) { + this.pagination.pageSize = pageSize; + this.pagination.page = 1; // Reset to first page + this.fetchNotifications(); + } + + setSorting( + sortBy: ClientPaginationState["sortBy"], + sortOrder: ClientPaginationState["sortOrder"], + ) { + this.pagination.sortBy = sortBy; + this.pagination.sortOrder = sortOrder; + this.pagination.page = 1; // Reset to first page + this.fetchNotifications(); + } + + setFilters(newFilters: Partial) { + this.filters = { ...this.filters, ...newFilters }; + this.pagination.page = 1; // Reset to first page + this.fetchNotifications(); + } + + clearFilters() { + this.filters = { userId: get(user)?.id!, isArchived: false }; + this.pagination.page = 1; + this.fetchNotifications(); + } +} + +export const notificationViewModel = new NotificationViewModel(); diff --git a/apps/main/src/lib/domains/notifications/notifications-table.svelte b/apps/main/src/lib/domains/notifications/notifications-table.svelte new file mode 100644 index 0000000..cc263a1 --- /dev/null +++ b/apps/main/src/lib/domains/notifications/notifications-table.svelte @@ -0,0 +1,566 @@ + + + + +
    +
    + + Notifications + {#if notificationViewModel.unreadCount > 0} + + {notificationViewModel.unreadCount} unread + + {/if} +
    + + {#if hasSelection} +
    + + {notificationViewModel.selectedIds.size} selected + + + + +
    + {:else} + + {/if} +
    + +
    + +
    + + +
    + + +
    + + + + Filters + + + + handleFilterChange("isRead", undefined)} + > + All + + handleFilterChange("isRead", false)} + > + Unread Only + + handleFilterChange("isRead", true)} + > + Read Only + + + + handleFilterChange("isArchived", false)} + > + Active + + handleFilterChange("isArchived", true)} + > + Archived + + + +
    +
    +
    + + + {#if notificationViewModel.loading && notificationViewModel.notifications.length === 0} +
    + +
    + {:else if notificationViewModel.notifications.length === 0} +
    + +

    No notifications

    +

    + {notificationViewModel.filters.search + ? "No notifications match your search." + : "You're all caught up!"} +

    +
    + {:else} +
    + + + + + + + + + Notification + Priority + Time + + + + + {#each notificationViewModel.notifications as notification (notification.id)} + + + + handleRowSelect( + notification.id, + checked, + )} + /> + + +
    + +
    +
    + +
    +

    + {notification.title} +

    +

    + {notification.body} +

    + {#if notification.category} + + {notification.category} + + {/if} +
    +
    + + + {notification.priority} + + + +
    + + {formatRelativeTime( + notification.sentAt, + )} +
    +
    + + + + + + + {#if notification.isRead} + + handleSingleAction( + notification.id, + "mark-unread", + )} + > + Mark as Unread + + {:else} + + handleSingleAction( + notification.id, + "mark-read", + )} + > + Mark as Read + + {/if} + + handleSingleAction( + notification.id, + "archive", + )} + > + Archive + + + + handleSingleAction( + notification.id, + "delete", + )} + class="text-destructive" + > + Delete + + + + +
    + {/each} +
    +
    + + + {#if notificationViewModel.pagination.totalPages > 1} +
    +
    + Showing {(notificationViewModel.pagination.page - 1) * + notificationViewModel.pagination.pageSize + + 1} to {Math.min( + notificationViewModel.pagination.page * + notificationViewModel.pagination.pageSize, + notificationViewModel.pagination.total, + )} of {notificationViewModel.pagination.total} notifications +
    + +
    + + +
    + {#each Array.from( { length: Math.min(5, notificationViewModel.pagination.totalPages) }, (_, i) => { + const startPage = Math.max(1, notificationViewModel.pagination.page - 2); + return startPage + i; + }, ) as page} + {#if page <= notificationViewModel.pagination.totalPages} + + {/if} + {/each} +
    + + +
    +
    + {/if} +
    + {/if} +
    +
    + + diff --git a/apps/main/src/lib/domains/security/auth.vm.svelte.ts b/apps/main/src/lib/domains/security/auth.vm.svelte.ts new file mode 100644 index 0000000..1ef7e88 --- /dev/null +++ b/apps/main/src/lib/domains/security/auth.vm.svelte.ts @@ -0,0 +1,233 @@ +import { apiClient } from "$lib/global.stores"; +import { authClient } from "$lib/auth.client"; +import { toast } from "svelte-sonner"; +import { get } from "svelte/store"; +import { page } from "$app/state"; +import { ResultAsync, errAsync, okAsync } from "neverthrow"; +import type { Err } from "@pkg/result"; + +class AuthViewModel { + loggingIn = $state(false); + loginError = $state(null); + + async handleGoogleOAuthLogin() { + const result = await ResultAsync.fromPromise( + authClient.signIn.social({ provider: "google" }), + (error): Err => ({ + code: "NETWORK_ERROR", + message: "Failed to initiate Google OAuth login", + description: "Network request failed", + detail: error instanceof Error ? error.message : String(error), + }), + ); + + result.match( + () => { + // OAuth flow will redirect, no action needed + }, + (error) => { + toast.error("Failed to initiate Google login", { + description: error.description, + }); + }, + ); + } + + async loginWithEmail(data: FormData): Promise { + const email = data.get("email")?.toString(); + + if (!email || email.length < 5) { + toast.error("Please enter a valid email"); + return false; + } + + this.loggingIn = true; + this.loginError = null; + + const result = await ResultAsync.fromPromise( + authClient.signIn.magicLink({ email }), + (error): Err => ({ + code: "NETWORK_ERROR", + message: "Failed to send magic link", + description: "Network request failed", + detail: error instanceof Error ? error.message : String(error), + }), + ) + .andThen((response) => { + if (response.error) { + return errAsync({ + code: "API_ERROR", + message: response.error.message ?? "Something went wrong", + description: + response.error.statusText ?? + "Please try another login method or try again later", + detail: response.error.statusText ?? "Unknown error", + }); + } + return okAsync(response.data); + }); + + const success = result.match( + () => { + this.loginError = null; + toast.success("Login request sent", { + description: "Check your email for a magic link to log in", + }); + return true; + }, + (error) => { + // Set error message for UI display + const errorMessage = error.message ?? "Something went wrong"; + this.loginError = errorMessage; + + // Don't show toast for known error cases that we handle in the UI + if ( + !errorMessage.includes("not valid to login") && + !errorMessage.includes("not allowed") + ) { + toast.error(errorMessage, { + description: error.description, + }); + } + return false; + }, + ); + + this.loggingIn = false; + return success; + } + + clearLoginError() { + this.loginError = null; + } + + async verifyMagicLink() { + const token = page.url.searchParams.get("token"); + if (!token) { + throw new Error("No token provided"); + } + + const result = await ResultAsync.fromPromise( + authClient.magicLink.verify({ query: { token } }), + (error): Err => ({ + code: "NETWORK_ERROR", + message: "Failed to verify magic link", + description: "Network request failed", + detail: error instanceof Error ? error.message : String(error), + }), + ) + .andThen((response) => { + if (response.error) { + return errAsync({ + code: "API_ERROR", + message: "Could not verify magic link", + description: response.error.message ?? "Unknown error", + detail: response.error.statusText ?? "Unknown error", + }); + } + if (!response.data?.user?.id) { + return errAsync({ + code: "API_ERROR", + message: "Invalid verification result", + description: "User ID not found in verification response", + detail: "Missing user.id in response", + }); + } + return okAsync(response.data); + }) + .andThen((verificationResult) => { + return ResultAsync.fromPromise( + get(apiClient).users["ensure-account-exists"].$put({ + json: { userId: verificationResult.user.id }, + }), + (error): Err => ({ + code: "NETWORK_ERROR", + message: "Failed to ensure account exists", + description: "Network request failed", + detail: + error instanceof Error + ? error.message + : String(error), + }), + ) + .andThen((response) => { + if (!response.ok) { + return errAsync({ + code: "API_ERROR", + message: "Failed to ensure account exists", + description: `Response failed with status ${response.status}`, + detail: `HTTP ${response.status}`, + }); + } + return ResultAsync.fromPromise( + response.json(), + (error): Err => ({ + code: "PARSING_ERROR", + message: "Failed to parse response", + description: "Invalid response format", + detail: + error instanceof Error + ? error.message + : String(error), + }), + ); + }) + .andThen((apiResult: any) => { + if (apiResult.error) { + return errAsync(apiResult.error); + } + return okAsync(apiResult.data); + }); + }); + + result.match( + () => { + // Success - account ensured + }, + (error: Err) => { + throw new Error(error.message ?? "Failed to verify magic link"); + }, + ); + } + + async verifyEmailChange() { + const token = page.url.searchParams.get("token"); + if (!token) { + throw new Error("No verification token provided"); + } + + const result = await ResultAsync.fromPromise( + authClient.verifyEmail({ + query: { token }, + }), + (error): Err => ({ + code: "NETWORK_ERROR", + message: "Failed to verify email change", + description: "Network request failed", + detail: error instanceof Error ? error.message : String(error), + }), + ) + .andThen((response) => { + if (response.error) { + return errAsync({ + code: "API_ERROR", + message: "Could not verify email change", + description: response.error.message ?? "Unknown error", + detail: response.error.statusText ?? "Unknown error", + }); + } + return okAsync(response.data); + }); + + result.match( + () => { + // Success - email verified + }, + (error: Err) => { + throw new Error(error.message ?? "Could not verify email change"); + }, + ); + } +} + +export const authVM = new AuthViewModel(); diff --git a/apps/main/src/lib/domains/security/email-login-form.svelte b/apps/main/src/lib/domains/security/email-login-form.svelte new file mode 100644 index 0000000..ab67c11 --- /dev/null +++ b/apps/main/src/lib/domains/security/email-login-form.svelte @@ -0,0 +1,154 @@ + + +
    + +
    + +
    + (emailFocused = true)} + onblur={() => (emailFocused = false)} + class="h-12 pr-12" + aria-invalid={!isEmailValid && email.length > 0} + /> + +
    + {#if authVM.loggingIn} + + {:else if isEmailValid && email.length > 0} + + {:else if email.length > 0} + + + {/if} +
    +
    + + {#if emailFocused && email.length > 0 && !isEmailValid} +

    + Please enter a valid email address. +

    + {/if} +
    + + + + + +
    + {#if status === "sent"} +
    +
    + +

    + We’ve sent a magic link to {submittedEmail}. Check your inbox. +

    +
    +
    + {:else if status === "failed" && authVM.loginError} +
    +
    + +

    {authVM.loginError}

    +
    + +
    + {/if} +
    +
    diff --git a/apps/main/src/lib/domains/security/magic-link-verification.svelte b/apps/main/src/lib/domains/security/magic-link-verification.svelte new file mode 100644 index 0000000..5f58a66 --- /dev/null +++ b/apps/main/src/lib/domains/security/magic-link-verification.svelte @@ -0,0 +1,228 @@ + + + + +
    + {#if verificationState === "loading"} + +
    +
    +
    + +
    +
    + {/if} + + + {#if verificationState === "loading"} +
    + + Verifying Your Login + + +
    + + Please wait while we verify your magic + link... +
    + + +
    +
    +
    + + +
    +
    +
    + +
    + Secure encrypted connection +
    +
    +
    + +
    + Verifying your identity +
    +
    +
    + +
    + Processing authentication +
    +
    +
    + {:else if verificationState === "success"} +
    +
    +
    +
    + +
    +
    + + + Login Successful! + + +

    + You've been successfully authenticated. + Redirecting you now... +

    + +
    + Redirecting + +
    +
    + {:else if verificationState === "error"} +
    +
    +
    +
    + +
    +
    + + + Verification Failed + + +

    + {errorMessage || + "We couldn't verify your magic link. It may have expired or been used already."} +

    + +
    + + +

    + Need help? Contact our support team if this + problem persists. +

    +
    +
    + {/if} + + +
    +

    + Magic links are secure, one-time use authentication + tokens that expire after 10 minutes. +

    +
    +
    +
    +
    diff --git a/apps/main/src/lib/domains/todo/list.svelte b/apps/main/src/lib/domains/todo/list.svelte new file mode 100644 index 0000000..e69de29 diff --git a/apps/main/src/lib/global.stores.ts b/apps/main/src/lib/global.stores.ts new file mode 100644 index 0000000..88b4e38 --- /dev/null +++ b/apps/main/src/lib/global.stores.ts @@ -0,0 +1,14 @@ +import type { Session, User } from "@pkg/logic/domains/user/data"; +import type { AppSidebarItem } from "./core/constants"; +import { writable } from "svelte/store"; +import type { Router } from "$lib/api"; +import type { hc } from "hono/client"; + +export const breadcrumbs = writable>([ + { title: "Dashboard", url: "/dashboard" }, +]); + +export const apiClient = writable>>(undefined); + +export const user = writable(undefined); +export const session = writable(undefined); diff --git a/apps/main/src/lib/hooks/is-mobile.svelte.ts b/apps/main/src/lib/hooks/is-mobile.svelte.ts new file mode 100644 index 0000000..4829c00 --- /dev/null +++ b/apps/main/src/lib/hooks/is-mobile.svelte.ts @@ -0,0 +1,9 @@ +import { MediaQuery } from "svelte/reactivity"; + +const DEFAULT_MOBILE_BREAKPOINT = 768; + +export class IsMobile extends MediaQuery { + constructor(breakpoint: number = DEFAULT_MOBILE_BREAKPOINT) { + super(`max-width: ${breakpoint - 1}px`); + } +} diff --git a/apps/main/src/lib/make-client.ts b/apps/main/src/lib/make-client.ts new file mode 100644 index 0000000..999475d --- /dev/null +++ b/apps/main/src/lib/make-client.ts @@ -0,0 +1,21 @@ +import type { Router } from "$lib/api"; +import { hc } from "hono/client"; + +let browserClient: ReturnType>; + +export const makeClient = (fetch: Window["fetch"]) => { + const isBrowser = typeof window !== "undefined"; + const origin = isBrowser ? window.location.origin : ""; + + if (isBrowser && browserClient) { + return browserClient; + } + + const client = hc(origin + "/api/v1", { fetch }); + + if (isBrowser) { + browserClient = client; + } + + return client; +}; diff --git a/apps/main/src/lib/utils.ts b/apps/main/src/lib/utils.ts new file mode 100644 index 0000000..55b3a91 --- /dev/null +++ b/apps/main/src/lib/utils.ts @@ -0,0 +1,13 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type WithoutChild = T extends { child?: any } ? Omit : T; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type WithoutChildren = T extends { children?: any } ? Omit : T; +export type WithoutChildrenOrChild = WithoutChildren>; +export type WithElementRef = T & { ref?: U | null }; diff --git a/apps/main/src/routes/(main)/+layout.server.ts b/apps/main/src/routes/(main)/+layout.server.ts new file mode 100644 index 0000000..93ad849 --- /dev/null +++ b/apps/main/src/routes/(main)/+layout.server.ts @@ -0,0 +1,13 @@ +import { auth } from "@pkg/logic/domains/auth/config.base"; +import { redirect } from "@sveltejs/kit"; +import type { LayoutServerLoad } from "./$types"; + +export const load = (async (c) => { + const sess = await auth.api.getSession({ + headers: c.request.headers, + }); + if ((!sess?.user || !sess?.session) && c.url.pathname !== "/auth/login") { + return redirect(302, "/auth/login"); + } + return { user: c.locals.user, session: c.locals.session }; +}) satisfies LayoutServerLoad; diff --git a/apps/main/src/routes/(main)/+layout.svelte b/apps/main/src/routes/(main)/+layout.svelte new file mode 100644 index 0000000..f39d355 --- /dev/null +++ b/apps/main/src/routes/(main)/+layout.svelte @@ -0,0 +1,58 @@ + + + + + +
    +
    + + + + + {#if $breadcrumbs.length > 0} + {#each $breadcrumbs as breadcrumb, i} + + {#if i < $breadcrumbs.length - 1} + + +
    +
    +
    + {@render children()} +
    +
    +
    diff --git a/apps/main/src/routes/(main)/+page.server.ts b/apps/main/src/routes/(main)/+page.server.ts new file mode 100644 index 0000000..f2058f3 --- /dev/null +++ b/apps/main/src/routes/(main)/+page.server.ts @@ -0,0 +1,6 @@ +import { redirect } from "@sveltejs/kit"; +import type { PageServerLoad } from "./$types"; + +export const load = (async (c) => { + throw redirect(302, "/dashboard"); +}) satisfies PageServerLoad; diff --git a/apps/main/src/routes/(main)/+page.svelte b/apps/main/src/routes/(main)/+page.svelte new file mode 100644 index 0000000..93b9d02 --- /dev/null +++ b/apps/main/src/routes/(main)/+page.svelte @@ -0,0 +1,18 @@ + diff --git a/apps/main/src/routes/(main)/account/+layout.svelte b/apps/main/src/routes/(main)/account/+layout.svelte new file mode 100644 index 0000000..fb25929 --- /dev/null +++ b/apps/main/src/routes/(main)/account/+layout.svelte @@ -0,0 +1,134 @@ + + + + + + +
    + {@render children()} +
    +
    + + +{#if isMobile} + + + + + +
    + + + {#each secondaryNavTree as each} + { + handleNavigation(each.url); + }} + > + +

    + {each.title} +

    +
    + {/each} +
    +
    +
    +
    +
    +{/if} diff --git a/apps/main/src/routes/(main)/account/+page.svelte b/apps/main/src/routes/(main)/account/+page.svelte new file mode 100644 index 0000000..3031c4d --- /dev/null +++ b/apps/main/src/routes/(main)/account/+page.svelte @@ -0,0 +1,247 @@ + + +
    + + + +
    +
    +
    + + {#if user.image} + + {:else} + + {(user.name || "User") + .substring(0, 2) + .toUpperCase()} + + {/if} + + +
    +
    +

    + {user.name} +

    +

    + Member since {new Date( + user.createdAt.toString(), + ).toLocaleDateString()} +

    +
    +
    +
    + + + + Personal Information + + Update your personal information and how others see you on the + platform. + +
    + + +
    + +
    + + +
    + + +
    + + +

    + This is your public username visible to other users. +

    +
    + +
    + +
    +
    +
    + +

    + Last updated: {new Date( + user.updatedAt.toString(), + ).toLocaleString()} +

    +
    +
    + + + + + Email Settings + + Manage your email address and verification status. + + + +
    + +
    + + +
    + + {user.emailVerified + ? "Email verified" + : "Email not verified"} + + {#if !user.emailVerified} + + {/if} +
    +
    + +
    + +
    +
    +
    + +

    + Changing your email will require verification of the new + address. +

    +
    +
    +
    diff --git a/apps/main/src/routes/(main)/account/verify-email/+page.svelte b/apps/main/src/routes/(main)/account/verify-email/+page.svelte new file mode 100644 index 0000000..f128cee --- /dev/null +++ b/apps/main/src/routes/(main)/account/verify-email/+page.svelte @@ -0,0 +1,240 @@ + + +
    +
    + + +
    + {#if verificationState === "loading"} + +
    +
    +
    + +
    +
    + {/if} + + + {#if verificationState === "loading"} +
    + + Verifying Your Email + + +
    + + Please wait while we verify your email + change... +
    + + +
    +
    +
    + + +
    +
    +
    + +
    + Secure encrypted connection +
    +
    +
    + +
    + Validating email ownership +
    +
    +
    + +
    + Updating account settings +
    +
    +
    + {:else if verificationState === "success"} +
    +
    +
    +
    + +
    +
    + + + Email Verified Successfully! + + +

    + Your email address has been updated and + verified. Redirecting you to your account... +

    + +
    + Redirecting + +
    +
    + {:else if verificationState === "error"} +
    +
    +
    +
    + +
    +
    + + + Email Verification Failed + + +

    + {errorMessage || + "We couldn't verify your email change. The verification link may have expired or been used already."} +

    + +
    + + +

    + You can try changing your email again from + your account settings. +

    +
    +
    + {/if} + + +
    +

    + Email verification links are secure, one-time use + tokens that expire after 10 minutes. +

    +
    +
    +
    +
    +
    +
    diff --git a/apps/main/src/routes/(main)/dashboard/+page.svelte b/apps/main/src/routes/(main)/dashboard/+page.svelte new file mode 100644 index 0000000..797b356 --- /dev/null +++ b/apps/main/src/routes/(main)/dashboard/+page.svelte @@ -0,0 +1,18 @@ + + + +
    +

    + Dashboard Not Yet Implemented +

    +

    + This is where your implementation will go +

    +
    +
    diff --git a/apps/main/src/routes/(main)/notifications/+page.svelte b/apps/main/src/routes/(main)/notifications/+page.svelte new file mode 100644 index 0000000..1d5bd87 --- /dev/null +++ b/apps/main/src/routes/(main)/notifications/+page.svelte @@ -0,0 +1,27 @@ + + + + + diff --git a/apps/main/src/routes/(main)/users/+page.server.ts b/apps/main/src/routes/(main)/users/+page.server.ts new file mode 100644 index 0000000..cc76a46 --- /dev/null +++ b/apps/main/src/routes/(main)/users/+page.server.ts @@ -0,0 +1,13 @@ +import { UserRoleMap } from "@pkg/logic/domains/user/data"; +import type { PageServerLoad } from "./$types"; +import { redirect } from "@sveltejs/kit"; + +// Info: this ensures only admin type users can access this page + +export const load = (async (c) => { + const user = c.locals.user; + if (!user || user.role !== UserRoleMap.admin) { + return redirect(307, "/dashboard"); + } + return {}; +}) satisfies PageServerLoad; diff --git a/apps/main/src/routes/(main)/users/+page.svelte b/apps/main/src/routes/(main)/users/+page.svelte new file mode 100644 index 0000000..2fb8dad --- /dev/null +++ b/apps/main/src/routes/(main)/users/+page.svelte @@ -0,0 +1,73 @@ + + + + + +
    + Users +
    + + +
    +
    + +
    + +
    + + { + toast.info("Searching...", { + description: "simulating API call", + }); + }} + /> +
    +
    + +
    + INFO: Normally would show all the users table here + +
    +
    +
    +
    diff --git a/apps/main/src/routes/+layout.svelte b/apps/main/src/routes/+layout.svelte new file mode 100644 index 0000000..345a779 --- /dev/null +++ b/apps/main/src/routes/+layout.svelte @@ -0,0 +1,36 @@ + + + + {$breadcrumbs[$breadcrumbs.length - 1]?.title ?? "Dashboard"} + + + + + + + + + {@render children()} + diff --git a/apps/main/src/routes/api/debug/users/+server.ts b/apps/main/src/routes/api/debug/users/+server.ts new file mode 100644 index 0000000..725c0de --- /dev/null +++ b/apps/main/src/routes/api/debug/users/+server.ts @@ -0,0 +1,92 @@ +import { UserRoleMap } from "@pkg/logic/domains/user/data"; +import { user } from "@pkg/db/schema/better.auth.schema"; +import type { RequestHandler } from "./$types"; +import { env } from "$env/dynamic/private"; +import { logger } from "@pkg/logger"; +import { db, eq } from "@pkg/db"; +import { nanoid } from "nanoid"; +import * as v from "valibot"; + +function isAuthorized(authHeader?: string | null) { + if (!authHeader) return false; + const authToken = authHeader.toString().replace("Bearer ", ""); + return authToken === env.DEBUG_API_KEY; +} + +export const GET: RequestHandler = async ({ request }) => { + if (!isAuthorized(request.headers.get("Authorization"))) { + return new Response("Unauthorized", { status: 401 }); + } + const users = await db.query.user.findMany({}); + return new Response(JSON.stringify({ users }), { status: 200 }); +}; + +export const PUT: RequestHandler = async ({ request }) => { + if (!isAuthorized(request.headers.get("Authorization"))) { + return new Response("Unauthorized", { status: 401 }); + } + + const data = await request.json(); + + if (!data.username) { + return new Response("Invalid data", { status: 400 }); + } + + await db + .update(user) + .set({ role: UserRoleMap.admin }) + .where(eq(user.username, data.username)) + .execute(); + + return new Response("Not implemented", { status: 200 }); +}; + +export const POST: RequestHandler = async ({ request }) => { + if (!isAuthorized(request.headers.get("Authorization"))) { + return new Response("Unauthorized", { status: 401 }); + } + const data = await request.json(); + const _schema = v.object({ + username: v.string(), + email: v.string(), + usertype: v.enum(UserRoleMap), + }); + const res = v.safeParse(_schema, data); + if (!res.success) { + return new Response("Invalid data", { status: 400 }); + } + + if ( + !!( + await db.query.user + .findFirst({ + where: eq(user.role, UserRoleMap.user), + columns: { id: true }, + }) + .execute() + )?.id + ) { + return new Response("Admin already exists", { status: 400 }); + } + + const resData = res.output; + + logger.info( + `Creating ${resData.username} | ${resData.email} (${resData.usertype})`, + ); + + const out = await db.insert(user).values({ + id: nanoid(), + username: resData.username, + email: resData.email, + emailVerified: false, + name: resData.username, + role: resData.usertype, + createdAt: new Date(), + updatedAt: new Date(), + }); + + logger.debug(out); + + return new Response(JSON.stringify({ ...out }), { status: 200 }); +}; diff --git a/apps/main/src/routes/api/v1/[...paths]/+server.ts b/apps/main/src/routes/api/v1/[...paths]/+server.ts new file mode 100644 index 0000000..3613780 --- /dev/null +++ b/apps/main/src/routes/api/v1/[...paths]/+server.ts @@ -0,0 +1,93 @@ +import { getUserController } from "@pkg/logic/domains/user/controller"; +import type { Session, User } from "@pkg/logic/domains/user/data"; +import { auth } from "@pkg/logic/domains/auth/config.base"; +import type { RequestHandler } from "@sveltejs/kit"; +import { env } from "$env/dynamic/private"; +import { api } from "$lib/api"; + +async function createContext(locals: App.Locals) { + return { ...env, locals }; +} + +async function getExecutionContext(sess: Session, user: User) { + const flowId = crypto.randomUUID(); + return { + flowId: flowId, + userId: user.id, + sessionId: sess.id, + }; +} + +async function getContext(headers: Headers) { + const sess = await auth.api.getSession({ headers }); + if (!sess?.session) { + return false; + } + + // @ts-ignore + const fCtx = getExecutionContext(sess.session, sess.user); + + return await getUserController() + .getUserInfo(fCtx, sess.user.id) + .match( + (user) => { + return { + user: user, + session: sess.session, + fCtx: fCtx, + }; + }, + (error) => { + console.error(error); + return false; + }, + ); +} + +export const GET: RequestHandler = async ({ request }) => { + const context = await getContext(request.headers); + if (!context || typeof context === "boolean") { + return new Response("Unauthorized", { status: 401 }); + } + return api.fetch(request, await createContext(context)); +}; + +export const HEAD: RequestHandler = async ({ request }) => { + const context = await getContext(request.headers); + if (!context || typeof context === "boolean") { + return new Response("Unauthorized", { status: 401 }); + } + return api.fetch(request, await createContext(context)); +}; + +export const POST: RequestHandler = async ({ request }) => { + const context = await getContext(request.headers); + if (!context || typeof context === "boolean") { + return new Response("Unauthorized", { status: 401 }); + } + return api.fetch(request, await createContext(context)); +}; + +export const PUT: RequestHandler = async ({ request }) => { + const context = await getContext(request.headers); + if (!context || typeof context === "boolean") { + return new Response("Unauthorized", { status: 401 }); + } + return api.fetch(request, await createContext(context)); +}; + +export const DELETE: RequestHandler = async ({ request }) => { + const context = await getContext(request.headers); + if (!context || typeof context === "boolean") { + return new Response("Unauthorized", { status: 401 }); + } + return api.fetch(request, await createContext(context)); +}; + +export const OPTIONS: RequestHandler = async ({ request }) => { + const context = await getContext(request.headers); + if (!context || typeof context === "boolean") { + return new Response("Unauthorized", { status: 401 }); + } + return api.fetch(request, await createContext(context)); +}; diff --git a/apps/main/src/routes/auth/2fa/+layout.server.ts b/apps/main/src/routes/auth/2fa/+layout.server.ts new file mode 100644 index 0000000..e01db46 --- /dev/null +++ b/apps/main/src/routes/auth/2fa/+layout.server.ts @@ -0,0 +1,14 @@ +import { auth } from "@pkg/logic/domains/auth/config.base"; +import { redirect } from "@sveltejs/kit"; +import type { LayoutServerLoad } from "./$types"; + +export const load = (async ({ request }) => { + const sess = await auth.api.getSession({ + headers: request.headers, + }); + if (!sess || !sess.user || !sess.session) { + return redirect(302, "/auth/login"); + } + // sess.session.id = + return { user: sess.user as any, session: sess.session as any }; +}) satisfies LayoutServerLoad; diff --git a/apps/main/src/routes/auth/2fa/+page.svelte b/apps/main/src/routes/auth/2fa/+page.svelte new file mode 100644 index 0000000..18d6259 --- /dev/null +++ b/apps/main/src/routes/auth/2fa/+page.svelte @@ -0,0 +1,285 @@ + + +
    +
    + + {#if !mounted || twoFactorVerifyVM.startingVerification} + + +
    +
    +
    +
    + +
    +
    + +
    + + Preparing Verification + + +
    + + Setting up secure verification... +
    + + +
    +
    +
    +
    +
    +
    + {:else if twoFactorVerifyVM.verificationToken} + + +
    + + + + Two-Factor Authentication + + +
    + + Enter the 6-digit code from your authenticator app to + continue + +
    + + + +
    +
    + + {#snippet children({ cells })} + + {#each cells.slice(0, 3) as cell (cell)} + + {/each} + + + + {#each cells.slice(3, 6) as cell (cell)} + + {/each} + + {/snippet} + +
    + + {#if twoFactorVerifyVM.errorMessage} +
    + + {twoFactorVerifyVM.errorMessage} +
    + {/if} + + +
    + + +
    +
    + +

    OR

    + +
    + + +
    + + +
    +
    +

    + Having trouble? +

    +
    + + +
    +
    +
    + + +
    +
    + + + This verification expires in 10 minutes for your + security + +
    +
    +
    + {:else} + + +
    +
    +
    +
    + +
    +
    + +
    + + Verification Setup Failed + + +

    + {twoFactorVerifyVM.errorMessage || + "We couldn't set up your verification session. Please try again."} +

    + +
    + + + +
    +
    +
    +
    + {/if} +
    +
    +
    diff --git a/apps/main/src/routes/auth/login/+page.server.ts b/apps/main/src/routes/auth/login/+page.server.ts new file mode 100644 index 0000000..cee74fb --- /dev/null +++ b/apps/main/src/routes/auth/login/+page.server.ts @@ -0,0 +1,13 @@ +import { redirect } from "@sveltejs/kit"; +import type { PageServerLoad } from "./$types"; +import { auth } from "@pkg/logic/domains/auth/config.base"; + +export const load = (async (c) => { + const sess = await auth.api.getSession({ + headers: c.request.headers, + }); + + if (!!sess && !!sess.user && !!sess.session) { + return redirect(302, "/"); + } +}) satisfies PageServerLoad; diff --git a/apps/main/src/routes/auth/login/+page.svelte b/apps/main/src/routes/auth/login/+page.svelte new file mode 100644 index 0000000..e4c5e5c --- /dev/null +++ b/apps/main/src/routes/auth/login/+page.svelte @@ -0,0 +1,77 @@ + + +
    + +
    +
    +
    +
    +
    + +
    + + +
    +
    +
    +
    + +
    +
    + +
    + + + Welcome Back + + + + Sign in to your account to continue + +
    +
    +
    + + + + +
    +
    +

    OR

    +
    +
    +
    + + +

    + By signing in, you agree to our + + and + +

    +
    +
    +
    +
    diff --git a/apps/main/src/routes/auth/magic-link/+page.svelte b/apps/main/src/routes/auth/magic-link/+page.svelte new file mode 100644 index 0000000..57a39b0 --- /dev/null +++ b/apps/main/src/routes/auth/magic-link/+page.svelte @@ -0,0 +1,11 @@ + + +
    +
    + +
    +
    diff --git a/apps/main/src/routes/layout.css b/apps/main/src/routes/layout.css new file mode 100644 index 0000000..5860334 --- /dev/null +++ b/apps/main/src/routes/layout.css @@ -0,0 +1,196 @@ +@import "tailwindcss"; +@import "tw-animate-css"; +@plugin "@tailwindcss/forms"; +@plugin "@tailwindcss/typography"; + +@font-face { + font-family: "Manrope"; + src: url("/fonts/manrope-variable.ttf") format("truetype"); +} + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: oklch(0.994 0 0); + --foreground: oklch(0 0 0); + + --card: oklch(0.994 0 0); + --card-foreground: oklch(0 0 0); + + --popover: oklch(0.991 0 0); + --popover-foreground: oklch(0 0 0); + + /* --- main theme: lavender/royal purple --- */ + --primary: oklch(0.6 0.2 280); /* medium lavender purple */ + --primary-foreground: oklch(0.99 0 0); + + --secondary: oklch(0.93 0.05 285); /* soft pale lavender */ + --secondary-foreground: oklch(0.25 0.03 285); + + --muted: oklch(0.96 0.01 275); + --muted-foreground: oklch(0.4 0.01 278); + + --accent: oklch(0.86 0.08 275); /* lavender accent */ + --accent-foreground: oklch(0.5 0.15 280); + + --destructive: oklch(0.63 0.18 25); + --destructive-foreground: oklch(1 0 0); + + --border: oklch(0.92 0.02 284); + --input: oklch(0.94 0 0); + --ring: oklch(0.6 0.2 280); + + /* charts — more variety but still within lavender spectrum */ + --chart-1: oklch(0.7 0.16 275); + --chart-2: oklch(0.6 0.2 280); + --chart-3: oklch(0.72 0.18 295); /* slightly more magenta */ + --chart-4: oklch(0.65 0.15 265); /* slightly bluer lavender */ + --chart-5: oklch(0.76 0.1 285); + + --sidebar: oklch(0.97 0.01 280); + --sidebar-foreground: oklch(0 0 0); + --sidebar-primary: oklch(0.6 0.2 280); + --sidebar-primary-foreground: oklch(1 0 0); + --sidebar-accent: oklch(0.92 0.02 284); + --sidebar-accent-foreground: oklch(0.2 0.02 280); + --sidebar-border: oklch(0.92 0.02 284); + --sidebar-ring: oklch(0.6 0.2 280); + + --font-sans: Plus Jakarta Sans, sans-serif; + --font-serif: Lora, serif; + --font-mono: IBM Plex Mono, monospace; + + --radius: 0.69rem; + + --shadow-2xs: 0px 2px 3px 0px hsl(0 0% 0% / 0.08); + --shadow-xs: 0px 2px 3px 0px hsl(0 0% 0% / 0.08); + --shadow-sm: + 0px 2px 3px 0px hsl(0 0% 0% / 0.16), + 0px 1px 2px -1px hsl(0 0% 0% / 0.16); + --shadow: + 0px 2px 3px 0px hsl(0 0% 0% / 0.16), + 0px 1px 2px -1px hsl(0 0% 0% / 0.16); + --shadow-md: + 0px 2px 3px 0px hsl(0 0% 0% / 0.16), + 0px 2px 4px -1px hsl(0 0% 0% / 0.16); + --shadow-lg: + 0px 2px 3px 0px hsl(0 0% 0% / 0.16), + 0px 4px 6px -1px hsl(0 0% 0% / 0.16); + --shadow-xl: + 0px 2px 3px 0px hsl(0 0% 0% / 0.16), + 0px 8px 10px -1px hsl(0 0% 0% / 0.16); + --shadow-2xl: 0px 2px 3px 0px hsl(0 0% 0% / 0.4); + + --tracking-normal: -0.025em; + --spacing: 0.27rem; +} + +.dark { + --background: oklch(0.23 0.01 278); + --foreground: oklch(0.95 0 0); + + --card: oklch(0.25 0.015 278); + --card-foreground: oklch(0.95 0 0); + + --popover: oklch(0.25 0.015 278); + --popover-foreground: oklch(0.95 0 0); + + --primary: oklch(0.56 0.17 280); + --primary-foreground: oklch(0.97 0 0); + + --secondary: oklch(0.35 0.03 280); + --secondary-foreground: oklch(0.92 0 0); + + --muted: oklch(0.33 0.02 280); + --muted-foreground: oklch(0.7 0.01 280); + + --accent: oklch(0.44 0.1 278); + --accent-foreground: oklch(0.88 0.09 280); + + --destructive: oklch(0.7 0.17 25); + --destructive-foreground: oklch(1 0 0); + + --border: oklch(0.34 0.02 278); + --input: oklch(0.34 0.02 278); + --ring: oklch(0.65 0.22 280); + --ring: oklch(0.56 0.17 280); + + --chart-1: oklch(0.68 0.15 275); + --chart-2: oklch(0.62 0.2 280); + --chart-3: oklch(0.7 0.14 292); + --chart-4: oklch(0.65 0.16 265); + --chart-5: oklch(0.72 0.1 285); + + --sidebar: oklch(0.2 0.01 278); + --sidebar-foreground: oklch(0.95 0 0); + --sidebar-primary: oklch(0.56 0.17 280); + --sidebar-primary-foreground: oklch(0.97 0 0); + --sidebar-accent: oklch(0.35 0.03 280); + --sidebar-accent-foreground: oklch(0.65 0.22 280); + --sidebar-border: oklch(0.34 0.02 278); + --sidebar-ring: oklch(0.65 0.22 280); +} + +@theme inline { + --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-destructive-foreground: var(--destructive-foreground); + --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); + + --font-sans: var(--font-sans); + --font-mono: var(--font-mono); + --font-serif: var(--font-serif); + + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + + --shadow-2xs: var(--shadow-2xs); + --shadow-xs: var(--shadow-xs); + --shadow-sm: var(--shadow-sm); + --shadow: var(--shadow); + --shadow-md: var(--shadow-md); + --shadow-lg: var(--shadow-lg); + --shadow-xl: var(--shadow-xl); + --shadow-2xl: var(--shadow-2xl); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + font-family: "Manrope", sans-serif; + letter-spacing: var(--tracking-normal); + } +} diff --git a/apps/main/static/favicon.png b/apps/main/static/favicon.png new file mode 100644 index 0000000..822f477 Binary files /dev/null and b/apps/main/static/favicon.png differ diff --git a/apps/main/static/fonts/manrope-variable.ttf b/apps/main/static/fonts/manrope-variable.ttf new file mode 100644 index 0000000..f39ca39 Binary files /dev/null and b/apps/main/static/fonts/manrope-variable.ttf differ diff --git a/apps/main/static/images/avatar.png b/apps/main/static/images/avatar.png new file mode 100644 index 0000000..0989209 Binary files /dev/null and b/apps/main/static/images/avatar.png differ diff --git a/apps/main/static/robots.txt b/apps/main/static/robots.txt new file mode 100644 index 0000000..b6dd667 --- /dev/null +++ b/apps/main/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/apps/main/svelte.config.js b/apps/main/svelte.config.js new file mode 100644 index 0000000..f989453 --- /dev/null +++ b/apps/main/svelte.config.js @@ -0,0 +1,18 @@ +import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; +import adapter from "svelte-adapter-bun"; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://svelte.dev/docs/kit/integrations + // for more information about preprocessors + preprocess: vitePreprocess(), + + kit: { + // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. + // If your environment is not supported, or you settled on a specific environment, switch out the adapter. + // See https://svelte.dev/docs/kit/adapters for more information about adapters. + adapter: adapter(), + }, +}; + +export default config; diff --git a/apps/main/tsconfig.json b/apps/main/tsconfig.json new file mode 100644 index 0000000..2c2ed3c --- /dev/null +++ b/apps/main/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "rewriteRelativeImportExtensions": true, + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // To make changes to top-level options such as include and exclude, we recommend extending + // the generated config; see https://svelte.dev/docs/kit/configuration#typescript +} diff --git a/apps/main/vite.config.ts b/apps/main/vite.config.ts new file mode 100644 index 0000000..e164584 --- /dev/null +++ b/apps/main/vite.config.ts @@ -0,0 +1,35 @@ +import { sveltekit } from "@sveltejs/kit/vite"; +import { defineConfig } from "vitest/config"; +import tailwindcss from "@tailwindcss/vite"; +import Icons from "unplugin-icons/vite"; +import { resolve } from "path"; + +export default defineConfig({ + plugins: [sveltekit(), tailwindcss(), Icons({ compiler: "svelte" })], + + resolve: { + alias: { + "@core": resolve(__dirname, "../../packages/logic/core"), + "@domains": resolve(__dirname, "../../packages/logic/domains"), + "@/core": resolve(__dirname, "../../packages/logic/core"), + "@/domains": resolve(__dirname, "../../packages/logic/domains"), + }, + }, + + test: { + expect: { requireAssertions: true }, + + projects: [ + { + extends: "./vite.config.ts", + + test: { + name: "server", + environment: "node", + include: ["src/**/*.{test,spec}.{js,ts}"], + exclude: ["src/**/*.svelte.{test,spec}.{js,ts}"], + }, + }, + ], + }, +}); diff --git a/apps/processor/.gitignore b/apps/processor/.gitignore new file mode 100644 index 0000000..506e4c3 --- /dev/null +++ b/apps/processor/.gitignore @@ -0,0 +1,2 @@ +# deps +node_modules/ diff --git a/apps/processor/README.md b/apps/processor/README.md new file mode 100644 index 0000000..6dd13e7 --- /dev/null +++ b/apps/processor/README.md @@ -0,0 +1,11 @@ +To install dependencies: +```sh +bun install +``` + +To run: +```sh +bun run dev +``` + +open http://localhost:3000 diff --git a/apps/processor/package.json b/apps/processor/package.json new file mode 100644 index 0000000..45056f9 --- /dev/null +++ b/apps/processor/package.json @@ -0,0 +1,13 @@ +{ + "name": "@app/processor", + "scripts": { + "dev": "bun run --hot src/index.ts", + "prod": "bun run src/index.ts" + }, + "dependencies": { + "hono": "^4.12.3" + }, + "devDependencies": { + "@types/bun": "latest" + } +} diff --git a/apps/processor/src/index.ts b/apps/processor/src/index.ts new file mode 100644 index 0000000..3191383 --- /dev/null +++ b/apps/processor/src/index.ts @@ -0,0 +1,9 @@ +import { Hono } from 'hono' + +const app = new Hono() + +app.get('/', (c) => { + return c.text('Hello Hono!') +}) + +export default app diff --git a/apps/processor/tsconfig.json b/apps/processor/tsconfig.json new file mode 100644 index 0000000..c442b33 --- /dev/null +++ b/apps/processor/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "strict": true, + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx" + } +} \ No newline at end of file diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..c398850 --- /dev/null +++ b/bun.lock @@ -0,0 +1,1513 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "@becometaxpayer/invoicing-system", + "devDependencies": { + "prettier": "^3.8.1", + "prettier-plugin-sort-imports": "^1.8.11", + "prettier-plugin-svelte": "^3.5.0", + "prettier-plugin-tailwindcss": "^0.7.2", + "turbo": "^2.7.0", + "typescript": "^5.9.3", + }, + }, + "apps/main": { + "name": "@apps/main", + "version": "0.0.1", + "dependencies": { + "@pkg/db": "workspace:*", + "@pkg/logger": "workspace:*", + "@pkg/logic": "workspace:*", + "@pkg/result": "workspace:*", + "@tanstack/svelte-query": "^6.0.10", + "better-auth": "^1.4.7", + "date-fns": "^4.1.0", + "hono": "^4.11.1", + "marked": "^17.0.1", + "nanoid": "^5.1.6", + "neverthrow": "^8.2.0", + "qrcode": "^1.5.4", + "valibot": "^1.2.0", + }, + "devDependencies": { + "@iconify/json": "^2.2.434", + "@internationalized/date": "^3.10.0", + "@lucide/svelte": "^0.561.0", + "@sveltejs/adapter-auto": "^7.0.0", + "@sveltejs/kit": "^2.49.1", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "@tailwindcss/forms": "^0.5.10", + "@tailwindcss/typography": "^0.5.19", + "@tailwindcss/vite": "^4.1.18", + "@tanstack/table-core": "^8.21.3", + "@types/qrcode": "^1.5.6", + "bits-ui": "^2.14.4", + "clsx": "^2.1.1", + "embla-carousel-svelte": "^8.6.0", + "formsnap": "^2.0.1", + "layerchart": "2.0.0-next.43", + "mode-watcher": "^1.1.0", + "paneforge": "^1.0.2", + "prettier": "^3.7.4", + "prettier-plugin-svelte": "^3.4.0", + "prettier-plugin-tailwindcss": "^0.7.2", + "svelte": "^5.45.6", + "svelte-adapter-bun": "^1.0.1", + "svelte-check": "^4.3.4", + "svelte-sonner": "^1.0.7", + "sveltekit-superforms": "^2.28.1", + "tailwind-merge": "^3.4.0", + "tailwind-variants": "^3.2.2", + "tailwindcss": "^4.1.18", + "tw-animate-css": "^1.4.0", + "typescript": "^5.9.3", + "unplugin-icons": "^23.0.1", + "vaul-svelte": "^1.0.0-next.7", + "vite": "^7.2.6", + "vitest": "^4.0.15", + }, + }, + "apps/processor": { + "name": "@app/processor", + "dependencies": { + "hono": "^4.12.3", + }, + "devDependencies": { + "@types/bun": "latest", + }, + }, + "packages/db": { + "name": "@pkg/db", + "dependencies": { + "@pkg/settings": "workspace:*", + "dotenv": "^16.4.7", + "drizzle-orm": "^0.36.1", + "postgres": "^3.4.8", + }, + "devDependencies": { + "@types/bun": "latest", + "@types/pg": "^8.11.10", + "drizzle-kit": "^0.28.0", + }, + "peerDependencies": { + "typescript": "^5.0.0", + }, + }, + "packages/logger": { + "name": "@pkg/logger", + "dependencies": { + "@axiomhq/winston": "^1.3.1", + "@pkg/result": "workspace:*", + "@pkg/settings": "workspace:*", + "winston": "^3.17.0", + "winston-daily-rotate-file": "^5.0.0", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5.0.0", + }, + }, + "packages/logic": { + "name": "@pkg/logic", + "dependencies": { + "@hono/standard-validator": "^0.2.1", + "@pkg/db": "workspace:*", + "@pkg/logger": "workspace:*", + "@pkg/redis": "workspace:*", + "@pkg/result": "workspace:*", + "@pkg/settings": "workspace:*", + "@types/pdfkit": "^0.14.0", + "argon2": "^0.43.0", + "better-auth": "^1.4.7", + "date-fns-tz": "^3.2.0", + "dotenv": "^16.5.0", + "hono": "^4.11.1", + "imapflow": "^1.0.188", + "mailparser": "^3.7.3", + "nanoid": "^5.1.5", + "neverthrow": "^8.2.0", + "otplib": "^12.0.1", + "pdfkit": "^0.17.1", + "tmp": "^0.2.3", + "uuid": "^11.1.0", + "valibot": "^1.2.0", + "xlsx": "^0.18.5", + }, + "devDependencies": { + "@types/bun": "latest", + "@types/imapflow": "^1.0.22", + "@types/mailparser": "^3.4.6", + "@types/tmp": "^0.2.6", + "@types/uuid": "^10.0.0", + }, + "peerDependencies": { + "typescript": "^5.0.0", + }, + }, + "packages/redis": { + "name": "@pkg/redis", + "dependencies": { + "ioredis": "^5.6.1", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + "packages/result": { + "name": "@pkg/result", + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5.0.0", + }, + }, + "packages/settings": { + "name": "@pkg/settings", + "dependencies": { + "dotenv": "^17.2.3", + "valibot": "^1.2.0", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5.0.0", + }, + }, + }, + "packages": { + "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], + + "@app/processor": ["@app/processor@workspace:apps/processor"], + + "@apps/main": ["@apps/main@workspace:apps/main"], + + "@ark/schema": ["@ark/schema@0.56.0", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA=="], + + "@ark/util": ["@ark/util@0.56.0", "", {}, "sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA=="], + + "@axiomhq/js": ["@axiomhq/js@1.3.1", "", { "dependencies": { "fetch-retry": "^6.0.0", "uuid": "^11.0.2" } }, "sha512-Ytf5V3wKz8FKNiqJxnqZmUhjgJ7TItKUoyHVNE/H2V9dN1ozD6NNnsueenOjKdA48cm2sGRyP432nworst18aA=="], + + "@axiomhq/winston": ["@axiomhq/winston@1.3.1", "", { "dependencies": { "@axiomhq/js": "1.3.1", "winston-transport": "^4.5.0" } }, "sha512-Nad/PkMIkFp7VxJHnZQTg6co9QpTPQDRPeC+/G7BfvXjISQ5+AY+BxNV3h8St7Bs9tLLAufx+gtyG+fgS47x0Q=="], + + "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], + + "@better-auth/core": ["@better-auth/core@1.4.7", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.1.12" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-rNfj8aNFwPwAMYo+ahoWDsqKrV7svD3jhHSC6+A77xxKodbgV0UgH+RO21GMaZ0PPAibEl851nw5e3bsNslW/w=="], + + "@better-auth/telemetry": ["@better-auth/telemetry@1.4.7", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.7" } }, "sha512-k07C/FWnX6m+IxLruNkCweIxuaIwVTB2X40EqwamRVhYNBAhOYZFGLHH+PtQyM+Yf1Z4+8H6MugLOXSreXNAjQ=="], + + "@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="], + + "@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], + + "@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="], + + "@dabh/diagnostics": ["@dabh/diagnostics@2.0.8", "", { "dependencies": { "@so-ric/colorspace": "^1.1.6", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q=="], + + "@dagrejs/dagre": ["@dagrejs/dagre@1.1.8", "", { "dependencies": { "@dagrejs/graphlib": "2.2.4" } }, "sha512-5SEDlndt4W/LaVzPYJW+bSmSEZc9EzTf8rJ20WCKvjS5EAZAN0b+x0Yww7VMT4R3Wootkg+X9bUfUxazYw6Blw=="], + + "@dagrejs/graphlib": ["@dagrejs/graphlib@2.2.4", "", {}, "sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw=="], + + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + + "@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + + "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], + + "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], + + "@exodus/schemasafe": ["@exodus/schemasafe@1.3.0", "", {}, "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw=="], + + "@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], + + "@hapi/hoek": ["@hapi/hoek@9.3.0", "", {}, "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ=="], + + "@hapi/topo": ["@hapi/topo@5.1.0", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg=="], + + "@hono/standard-validator": ["@hono/standard-validator@0.2.1", "", { "peerDependencies": { "@standard-schema/spec": "1.0.0", "hono": ">=3.9.0" } }, "sha512-uF1W7/iSWi0r5Mugj5ZzdlCVp/KeGatS6JYt+Eht9xKO0IDtdZbdaLApgjrzHINQQa+Wnxw2pcX6EfO+vgB+Wg=="], + + "@iconify/json": ["@iconify/json@2.2.434", "", { "dependencies": { "@iconify/types": "*", "pathe": "^2.0.3" } }, "sha512-iTG1M1OCzHnfW+VZbsyZyLfttR0EB5SurUq92zoPkmI+QAsh23tp8ocR4umUMsRMFkz448MoC5iabA+EYDU46w=="], + + "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], + + "@iconify/utils": ["@iconify/utils@3.1.0", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "mlly": "^1.8.0" } }, "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw=="], + + "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], + + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], + + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + + "@internationalized/date": ["@internationalized/date@3.10.1", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-oJrXtQiAXLvT9clCf1K4kxp3eKsQhIaZqxEyowkBcsvZDdZkbWrVmnGknxs5flTD0VGsxrxKgBCZty1EzoiMzA=="], + + "@ioredis/commands": ["@ioredis/commands@1.4.0", "", {}, "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@layerstack/svelte-actions": ["@layerstack/svelte-actions@1.0.1-next.14", "", { "dependencies": { "@floating-ui/dom": "^1.7.0", "@layerstack/utils": "2.0.0-next.14", "d3-scale": "^4.0.2" } }, "sha512-MPBmVaB+GfNHvBkg5nJkPG18smoXKvsvJRpsdWnrUBfca+TieZLoaEzNxDH+9LG11dIXP9gghsXt1mUqbbyAsA=="], + + "@layerstack/svelte-state": ["@layerstack/svelte-state@0.1.0-next.19", "", { "dependencies": { "@layerstack/utils": "2.0.0-next.14" } }, "sha512-yCYoQAIbeP8y1xmOB/r0+UundgP4JFnpNURgMki+26TotzoqrZ5oLpHvhPSVm60ks+buR3ebDBTeUFdHzxwzQQ=="], + + "@layerstack/tailwind": ["@layerstack/tailwind@2.0.0-next.17", "", { "dependencies": { "@layerstack/utils": "^2.0.0-next.14", "clsx": "^2.1.1", "d3-array": "^3.2.4", "lodash-es": "^4.17.21", "tailwind-merge": "^3.2.0" } }, "sha512-ZSn6ouqpnzB6DKzSKLVwrUBOQsrzpDA/By2/ba9ApxgTGnaD1nyqNwrvmZ+kswdAwB4YnrGEAE4VZkKrB2+DaQ=="], + + "@layerstack/utils": ["@layerstack/utils@2.0.0-next.14", "", { "dependencies": { "d3-array": "^3.2.4", "d3-time": "^3.1.0", "d3-time-format": "^4.1.0", "lodash-es": "^4.17.21" } }, "sha512-1I2CS0Cwgs53W35qVg1eBdYhB/CiPvL3s0XE61b8jWkTHxgjBF65yYNgXjW74kv7WI7GsJcWMNBufPd0rnu9kA=="], + + "@lucide/svelte": ["@lucide/svelte@0.561.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-vofKV2UFVrKE6I4ewKJ3dfCXSV6iP6nWVmiM83MLjsU91EeJcEg7LoWUABLp/aOTxj1HQNbJD1f3g3L0JQgH9A=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], + + "@neondatabase/serverless": ["@neondatabase/serverless@1.0.2", "", { "dependencies": { "@types/node": "^22.15.30", "@types/pg": "^8.8.0" } }, "sha512-I5sbpSIAHiB+b6UttofhrN/UJXII+4tZPAq1qugzwCwLIL8EZLV7F/JyHUrEIiGgQpEXzpnjlJ+zwcEhheGvCw=="], + + "@next/env": ["@next/env@15.5.9", "", {}, "sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg=="], + + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.5.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw=="], + + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.5.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg=="], + + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.5.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA=="], + + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.5.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw=="], + + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.5.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw=="], + + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.5.7", "", { "os": "linux", "cpu": "x64" }, "sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA=="], + + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.5.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ=="], + + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.5.7", "", { "os": "win32", "cpu": "x64" }, "sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw=="], + + "@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="], + + "@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], + + "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + + "@otplib/core": ["@otplib/core@12.0.1", "", {}, "sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA=="], + + "@otplib/plugin-crypto": ["@otplib/plugin-crypto@12.0.1", "", { "dependencies": { "@otplib/core": "^12.0.1" } }, "sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g=="], + + "@otplib/plugin-thirty-two": ["@otplib/plugin-thirty-two@12.0.1", "", { "dependencies": { "@otplib/core": "^12.0.1", "thirty-two": "^1.0.2" } }, "sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA=="], + + "@otplib/preset-default": ["@otplib/preset-default@12.0.1", "", { "dependencies": { "@otplib/core": "^12.0.1", "@otplib/plugin-crypto": "^12.0.1", "@otplib/plugin-thirty-two": "^12.0.1" } }, "sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ=="], + + "@otplib/preset-v11": ["@otplib/preset-v11@12.0.1", "", { "dependencies": { "@otplib/core": "^12.0.1", "@otplib/plugin-crypto": "^12.0.1", "@otplib/plugin-thirty-two": "^12.0.1" } }, "sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg=="], + + "@oxc-project/runtime": ["@oxc-project/runtime@0.71.0", "", {}, "sha512-QwoF5WUXIGFQ+hSxWEib4U/aeLoiDN9JlP18MnBgx9LLPRDfn1iICtcow7Jgey6HLH4XFceWXQD5WBJ39dyJcw=="], + + "@oxc-project/types": ["@oxc-project/types@0.71.0", "", {}, "sha512-5CwQ4MI+P4MQbjLWXgNurA+igGwu/opNetIE13LBs9+V93R64MLvDKOOLZIXSzEfovU3Zef3q3GjPnMTgJTn2w=="], + + "@phc/format": ["@phc/format@1.0.0", "", {}, "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ=="], + + "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], + + "@pkg/db": ["@pkg/db@workspace:packages/db"], + + "@pkg/logger": ["@pkg/logger@workspace:packages/logger"], + + "@pkg/logic": ["@pkg/logic@workspace:packages/logic"], + + "@pkg/redis": ["@pkg/redis@workspace:packages/redis"], + + "@pkg/result": ["@pkg/result@workspace:packages/result"], + + "@pkg/settings": ["@pkg/settings@workspace:packages/settings"], + + "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], + + "@poppinss/macroable": ["@poppinss/macroable@1.1.0", "", {}, "sha512-y/YKzZDuG8XrpXpM7Z1RdQpiIc0MAKyva24Ux1PB4aI7RiSI/79K8JVDcdyubriTm7vJ1LhFs8CrZpmPnx/8Pw=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.9-commit.d91dfb5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Mp0/gqiPdepHjjVm7e0yL1acWvI0rJVVFQEADSezvAjon9sjQ7CEg9JnXICD4B1YrPmN9qV/e7cQZCp87tTV4w=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-beta.9-commit.d91dfb5", "", { "os": "darwin", "cpu": "x64" }, "sha512-40re4rMNrsi57oavRzIOpRGmg3QRlW6Ea8Q3znaqgOuJuKVrrm2bIQInTfkZJG7a4/5YMX7T951d0+toGLTdCA=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-beta.9-commit.d91dfb5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-8BDM939bbMariZupiHp3OmP5N+LXPT4mULA0hZjDaq970PCxv4krZOSMG+HkWUUwmuQROtV+/00xw39EO0P+8g=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.9-commit.d91dfb5", "", { "os": "linux", "cpu": "arm" }, "sha512-sntsPaPgrECpBB/+2xrQzVUt0r493TMPI+4kWRMhvMsmrxOqH1Ep5lM0Wua/ZdbfZNwm1aVa5pcESQfNfM4Fhw=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-beta.9-commit.d91dfb5", "", { "os": "linux", "cpu": "arm64" }, "sha512-5clBW/I+er9F2uM1OFjJFWX86y7Lcy0M+NqsN4s3o07W+8467Zk8oQa4B45vdaXoNUF/yqIAgKkA/OEdQDxZqA=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-beta.9-commit.d91dfb5", "", { "os": "linux", "cpu": "arm64" }, "sha512-wv+rnAfQDk9p/CheX8/Kmqk2o1WaFa4xhWI9gOyDMk/ljvOX0u0ubeM8nI1Qfox7Tnh71eV5AjzSePXUhFOyOg=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-beta.9-commit.d91dfb5", "", { "os": "linux", "cpu": "x64" }, "sha512-gxD0/xhU4Py47IH3bKZbWtvB99tMkUPGPJFRfSc5UB9Osoje0l0j1PPbxpUtXIELurYCqwLBKXIMTQGifox1BQ=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-beta.9-commit.d91dfb5", "", { "os": "linux", "cpu": "x64" }, "sha512-HotuVe3XUjDwqqEMbm3o3IRkP9gdm8raY/btd/6KE3JGLF/cv4+3ff1l6nOhAZI8wulWDPEXPtE7v+HQEaTXnA=="], + + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-beta.9-commit.d91dfb5", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.4" }, "cpu": "none" }, "sha512-8Cx+ucbd8n2dIr21FqBh6rUvTVL0uTgEtKR7l+MUZ5BgY4dFh1e4mPVX8oqmoYwOxBiXrsD2JIOCz4AyKLKxWA=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-beta.9-commit.d91dfb5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Vhq5vikrVDxAa75fxsyqj0c0Y/uti/TwshXI71Xb8IeUQJOBnmLUsn5dgYf5ljpYYkNa0z9BPAvUDIDMmyDi+w=="], + + "@rolldown/binding-win32-ia32-msvc": ["@rolldown/binding-win32-ia32-msvc@1.0.0-beta.9-commit.d91dfb5", "", { "os": "win32", "cpu": "ia32" }, "sha512-lN7RIg9Iugn08zP2aZN9y/MIdG8iOOCE93M1UrFlrxMTqPf8X+fDzmR/OKhTSd1A2pYNipZHjyTcb5H8kyQSow=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-beta.9-commit.d91dfb5", "", { "os": "win32", "cpu": "x64" }, "sha512-7/7cLIn48Y+EpQ4CePvf8reFl63F15yPUlg4ZAhl+RXJIfydkdak1WD8Ir3AwAO+bJBXzrfNL+XQbxm0mcQZmw=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.9-commit.d91dfb5", "", {}, "sha512-8sExkWRK+zVybw3+2/kBkYBFeLnEUWz1fT7BLHplpzmtqkOfTbAQ9gkt4pzwGIIZmg4Qn5US5ACjUBenrhezwQ=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.54.0", "", { "os": "android", "cpu": "arm" }, "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.54.0", "", { "os": "android", "cpu": "arm64" }, "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.54.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.54.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.54.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.54.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.54.0", "", { "os": "linux", "cpu": "arm" }, "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.54.0", "", { "os": "linux", "cpu": "arm" }, "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.54.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.54.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.54.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.54.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.54.0", "", { "os": "linux", "cpu": "x64" }, "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.54.0", "", { "os": "linux", "cpu": "x64" }, "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.54.0", "", { "os": "none", "cpu": "arm64" }, "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.54.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.54.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.54.0", "", { "os": "win32", "cpu": "x64" }, "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.54.0", "", { "os": "win32", "cpu": "x64" }, "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg=="], + + "@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="], + + "@sideway/address": ["@sideway/address@4.1.5", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q=="], + + "@sideway/formula": ["@sideway/formula@3.0.1", "", {}, "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg=="], + + "@sideway/pinpoint": ["@sideway/pinpoint@2.0.0", "", {}, "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="], + + "@so-ric/colorspace": ["@so-ric/colorspace@1.1.6", "", { "dependencies": { "color": "^5.0.2", "text-hex": "1.0.x" } }, "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.8", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA=="], + + "@sveltejs/adapter-auto": ["@sveltejs/adapter-auto@7.0.0", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-ImDWaErTOCkRS4Gt+5gZuymKFBobnhChXUZ9lhUZLahUgvA4OOvRzi3sahzYgbxGj5nkA6OV0GAW378+dl/gyw=="], + + "@sveltejs/kit": ["@sveltejs/kit@2.49.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.3.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["@opentelemetry/api"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-Vp3zX/qlwerQmHMP6x0Ry1oY7eKKRcOWGc2P59srOp4zcqyn+etJyQpELgOi4+ZSUgteX8Y387NuwruLgGXLUQ=="], + + "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.1", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", "deepmerge": "^4.3.1", "magic-string": "^0.30.17", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ=="], + + "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.1", "", { "dependencies": { "debug": "^4.4.1" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA=="], + + "@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="], + + "@tailwindcss/forms": ["@tailwindcss/forms@0.5.11", "", { "dependencies": { "mini-svg-data-uri": "^1.2.3" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1" } }, "sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.18", "", { "os": "android", "cpu": "arm64" }, "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18", "", { "os": "linux", "cpu": "arm" }, "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.18", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.18", "", { "os": "win32", "cpu": "x64" }, "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q=="], + + "@tailwindcss/typography": ["@tailwindcss/typography@0.5.19", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg=="], + + "@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="], + + "@tanstack/query-core": ["@tanstack/query-core@5.90.12", "", {}, "sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg=="], + + "@tanstack/svelte-query": ["@tanstack/svelte-query@6.0.10", "", { "dependencies": { "@tanstack/query-core": "5.90.12" }, "peerDependencies": { "svelte": "^5.25.0" } }, "sha512-J0kM3JNvRcRCM6cbHLeICs73aLp98N/nsihdVEtiNo3MEN4pAnO45qZ2yxX70MrEZ9vffXaCXMCChwgXs1lZ/Q=="], + + "@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], + + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/imapflow": ["@types/imapflow@1.0.189", "", { "dependencies": { "imapflow": "*" } }, "sha512-3I/9/rKYd4+p4tAkQ3sRtTu2EazHzvruzwgpjpM6ZLx8YHjUyqPv3rBEg5wabVWuuX7/+yHULYROQz5cgALX0g=="], + + "@types/mailparser": ["@types/mailparser@3.4.6", "", { "dependencies": { "@types/node": "*", "iconv-lite": "^0.6.3" } }, "sha512-wVV3cnIKzxTffaPH8iRnddX1zahbYB1ZEoAxyhoBo3TBCBuK6nZ8M8JYO/RhsCuuBVOw/DEN/t/ENbruwlxn6Q=="], + + "@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + + "@types/pdfkit": ["@types/pdfkit@0.14.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-X94hoZVr9dNfV23roeXRm57AWS+AOMak3gq2wZvn4TXiLvXE8+TrYaM5IkMyZbGRw49jEqI49rP/UVL3+C3Svg=="], + + "@types/pg": ["@types/pg@8.16.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ=="], + + "@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="], + + "@types/react": ["@types/react@19.2.10", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw=="], + + "@types/tmp": ["@types/tmp@0.2.6", "", {}, "sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA=="], + + "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], + + "@types/uuid": ["@types/uuid@10.0.0", "", {}, "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ=="], + + "@types/validator": ["@types/validator@13.15.10", "", {}, "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA=="], + + "@typeschema/class-validator": ["@typeschema/class-validator@0.3.0", "", { "dependencies": { "@typeschema/core": "0.14.0" }, "peerDependencies": { "class-validator": "^0.14.1" }, "optionalPeers": ["class-validator"] }, "sha512-OJSFeZDIQ8EK1HTljKLT5CItM2wsbgczLN8tMEfz3I1Lmhc5TBfkZ0eikFzUC16tI3d1Nag7um6TfCgp2I2Bww=="], + + "@typeschema/core": ["@typeschema/core@0.14.0", "", { "peerDependencies": { "@types/json-schema": "^7.0.15" }, "optionalPeers": ["@types/json-schema"] }, "sha512-Ia6PtZHcL3KqsAWXjMi5xIyZ7XMH4aSnOQes8mfMLx+wGFGtGRNlwe6Y7cYvX+WfNK67OL0/HSe9t8QDygV0/w=="], + + "@valibot/to-json-schema": ["@valibot/to-json-schema@1.5.0", "", { "peerDependencies": { "valibot": "^1.2.0" } }, "sha512-GE7DmSr1C2UCWPiV0upRH6mv0cCPsqYGs819fb6srCS1tWhyXrkGGe+zxUiwzn/L1BOfADH4sNjY/YHCuP8phQ=="], + + "@vinejs/compiler": ["@vinejs/compiler@3.0.0", "", {}, "sha512-v9Lsv59nR56+bmy2p0+czjZxsLHwaibJ+SV5iK9JJfehlJMa501jUJQqqz4X/OqKXrxtE3uTQmSqjUqzF3B2mw=="], + + "@vinejs/vine": ["@vinejs/vine@3.0.1", "", { "dependencies": { "@poppinss/macroable": "^1.0.4", "@types/validator": "^13.12.2", "@vinejs/compiler": "^3.0.0", "camelcase": "^8.0.0", "dayjs": "^1.11.13", "dlv": "^1.1.3", "normalize-url": "^8.0.1", "validator": "^13.12.0" } }, "sha512-ZtvYkYpZOYdvbws3uaOAvTFuvFXoQGAtmzeiXu+XSMGxi5GVsODpoI9Xu9TplEMuD/5fmAtBbKb9cQHkWkLXDQ=="], + + "@vitest/expect": ["@vitest/expect@4.0.16", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.16", "@vitest/utils": "4.0.16", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA=="], + + "@vitest/mocker": ["@vitest/mocker@4.0.16", "", { "dependencies": { "@vitest/spy": "4.0.16", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.0.16", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA=="], + + "@vitest/runner": ["@vitest/runner@4.0.16", "", { "dependencies": { "@vitest/utils": "4.0.16", "pathe": "^2.0.3" } }, "sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA=="], + + "@vitest/spy": ["@vitest/spy@4.0.16", "", {}, "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw=="], + + "@vitest/utils": ["@vitest/utils@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "tinyrainbow": "^3.0.3" } }, "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA=="], + + "@zone-eu/mailsplit": ["@zone-eu/mailsplit@5.4.8", "", { "dependencies": { "libbase64": "1.3.0", "libmime": "5.3.7", "libqp": "2.1.1" } }, "sha512-eEyACj4JZ7sjzRvy26QhLgKEMWwQbsw1+QZnlLX+/gihcNH07lVPOcnwf5U6UAL7gkc//J3jVd76o/WS+taUiA=="], + + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "adler-32": ["adler-32@1.3.1", "", {}, "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="], + + "argon2": ["argon2@0.43.1", "", { "dependencies": { "@phc/format": "^1.0.0", "node-addon-api": "^8.4.0", "node-gyp-build": "^4.8.4" } }, "sha512-TfOzvDWUaQPurCT1hOwIeFNkgrAJDpbBGBGWDgzDsm11nNhImc13WhdGdCU6K7brkp8VpeY07oGtSex0Wmhg8w=="], + + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + + "arkregex": ["arkregex@0.0.5", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw=="], + + "arktype": ["arktype@2.1.29", "", { "dependencies": { "@ark/schema": "0.56.0", "@ark/util": "0.56.0", "arkregex": "0.0.5" } }, "sha512-jyfKk4xIOzvYNayqnD8ZJQqOwcrTOUbIU4293yrzAjA3O1dWh61j71ArMQ6tS/u4pD7vabSPe7nG3RCyoXW6RQ=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + + "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], + + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + + "base64-js": ["base64-js@0.0.8", "", {}, "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw=="], + + "better-auth": ["better-auth@1.4.7", "", { "dependencies": { "@better-auth/core": "1.4.7", "@better-auth/telemetry": "1.4.7", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.5", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.12" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.22.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "better-sqlite3": "^12.4.1", "drizzle-kit": "^0.31.4", "drizzle-orm": "^0.41.0", "mongodb": "^6.18.0", "mysql2": "^3.14.4", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.16.3", "prisma": "^5.22.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^4.0.15", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-kVmDQxzqGwP4FFMOYpS5I7oAaoFW3hwooUAAtcbb2DrOYv5EUvRUDJbTMaPoMTj7URjNDQ6vG9gcCS1Q+0aVBw=="], + + "better-call": ["better-call@1.1.5", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-nQJ3S87v6wApbDwbZ++FrQiSiVxWvZdjaO+2v6lZJAG2WWggkB2CziUDjPciz3eAt9TqfRursIQMZIcpkBnvlw=="], + + "bits-ui": ["bits-ui@2.14.4", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/dom": "^1.7.1", "esm-env": "^1.1.2", "runed": "^0.35.1", "svelte-toolbelt": "^0.10.6", "tabbable": "^6.2.0" }, "peerDependencies": { "@internationalized/date": "^3.8.1", "svelte": "^5.33.0" } }, "sha512-W6kenhnbd/YVvur+DKkaVJ6GldE53eLewur5AhUCqslYQ0vjZr8eWlOfwZnMiPB+PF5HMVqf61vXBvmyrAmPWg=="], + + "brotli": ["brotli@1.3.3", "", { "dependencies": { "base64-js": "^1.1.2" } }, "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg=="], + + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + + "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], + + "camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001761", "", {}, "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g=="], + + "cfb": ["cfb@1.2.2", "", { "dependencies": { "adler-32": "~1.3.0", "crc-32": "~1.2.0" } }, "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA=="], + + "chai": ["chai@6.2.1", "", {}, "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg=="], + + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "class-validator": ["class-validator@0.14.3", "", { "dependencies": { "@types/validator": "^13.15.3", "libphonenumber-js": "^1.11.1", "validator": "^13.15.20" } }, "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA=="], + + "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], + + "cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], + + "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="], + + "codepage": ["codepage@1.15.0", "", {}, "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA=="], + + "color": ["color@5.0.3", "", { "dependencies": { "color-convert": "^3.1.3", "color-string": "^2.1.3" } }, "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA=="], + + "color-convert": ["color-convert@3.1.3", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg=="], + + "color-name": ["color-name@2.1.0", "", {}, "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg=="], + + "color-string": ["color-string@2.1.4", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg=="], + + "commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], + + "confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="], + + "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], + + "crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="], + + "crypto-js": ["crypto-js@4.2.0", "", {}, "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], + + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + + "d3-delaunay": ["d3-delaunay@6.0.4", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="], + + "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="], + + "d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="], + + "d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="], + + "d3-format": ["d3-format@3.1.0", "", {}, "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA=="], + + "d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="], + + "d3-geo-voronoi": ["d3-geo-voronoi@2.1.0", "", { "dependencies": { "d3-array": "3", "d3-delaunay": "6", "d3-geo": "3", "d3-tricontour": "1" } }, "sha512-kqE4yYuOjPbKdBXG0xztCacPwkVSK2REF1opSNrnqqtXJmNcM++UbwQ8SxvwP6IQTj9RvIjjK4qeiVsEfj0Z2Q=="], + + "d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="], + + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + + "d3-interpolate-path": ["d3-interpolate-path@2.3.0", "", {}, "sha512-tZYtGXxBmbgHsIc9Wms6LS5u4w6KbP8C09a4/ZYc4KLMYYqub57rRBUgpUr2CIarIrJEpdAWWxWQvofgaMpbKQ=="], + + "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], + + "d3-quadtree": ["d3-quadtree@3.0.1", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="], + + "d3-random": ["d3-random@3.0.1", "", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="], + + "d3-sankey": ["d3-sankey@0.12.3", "", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="], + + "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], + + "d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="], + + "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], + + "d3-tile": ["d3-tile@1.0.0", "", {}, "sha512-79fnTKpPMPDS5xQ0xuS9ir0165NEwwkFpe/DSOmc2Gl9ldYzKKRDWogmTTE8wAJ8NA7PMapNfEcyKhI9Lxdu5Q=="], + + "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], + + "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], + + "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + + "d3-tricontour": ["d3-tricontour@1.1.0", "", { "dependencies": { "d3-delaunay": "6", "d3-scale": "4" } }, "sha512-G7gHKj89n2owmkGb6WX6ixcnQ0Kf/0wpa9VIh9DGdbHu8wdrlaHU4ir3/bFNERl8N8nn4G7e7qbtBG8N9caihQ=="], + + "date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], + + "date-fns-tz": ["date-fns-tz@3.2.0", "", { "peerDependencies": { "date-fns": "^3.0.0 || ^4.0.0" } }, "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ=="], + + "dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], + + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + + "delaunator": ["delaunator@5.0.1", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw=="], + + "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "devalue": ["devalue@5.6.1", "", {}, "sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A=="], + + "dfa": ["dfa@1.2.0", "", {}, "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q=="], + + "dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="], + + "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], + + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], + + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], + + "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], + + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], + + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + + "drizzle-kit": ["drizzle-kit@0.28.1", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-JimOV+ystXTWMgZkLHYHf2w3oS28hxiH1FR0dkmJLc7GHzdGJoJAQtQS5DRppnabsRZwE2U1F6CuezVBgmsBBQ=="], + + "drizzle-orm": ["drizzle-orm@0.36.4", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=3", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/react": ">=18", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "react": ">=18", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/react", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "knex", "kysely", "mysql2", "pg", "postgres", "react", "sql.js", "sqlite3"] }, "sha512-1OZY3PXD7BR00Gl61UUOFihslDldfH4NFRH2MbP54Yxi0G/PKn4HfO65JYZ7c16DeP3SpM3Aw+VXVG9j6CRSXA=="], + + "effect": ["effect@3.19.13", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-8MZ783YuHRwHZX2Mmm+bpGxq+7XPd88sWwYAz2Ysry80sEKpftDZXs2Hg9ZyjESi1IBTNHF0oDKe0zJRkUlyew=="], + + "embla-carousel": ["embla-carousel@8.6.0", "", {}, "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA=="], + + "embla-carousel-reactive-utils": ["embla-carousel-reactive-utils@8.6.0", "", { "peerDependencies": { "embla-carousel": "8.6.0" } }, "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A=="], + + "embla-carousel-svelte": ["embla-carousel-svelte@8.6.0", "", { "dependencies": { "embla-carousel": "8.6.0", "embla-carousel-reactive-utils": "8.6.0" }, "peerDependencies": { "svelte": "^3.49.0 || ^4.0.0 || ^5.0.0" } }, "sha512-ZDsKk8Sdv+AUTygMYcwZjfRd1DTh+JSUzxkOo8b9iKAkYjg+39mzbY/lwHsE3jXSpKxdKWS69hPSNuzlOGtR2Q=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="], + + "encoding-japanese": ["encoding-japanese@2.2.0", "", {}, "sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A=="], + + "enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="], + + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + + "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + + "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], + + "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], + + "esrap": ["esrap@2.2.1", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-GiYWG34AN/4CUyaWAgunGt0Rxvr1PTMlGC0vvEov/uOQYWne2bpN03Um+k8jT+q3op33mKouP2zeJ6OlM+qeUg=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + + "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], + + "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], + + "fetch-retry": ["fetch-retry@6.0.0", "", {}, "sha512-BUFj1aMubgib37I3v4q78fYo63Po7t4HUPTpQ6/QE6yK6cIQrP+W43FYToeTEyg5m2Y7eFUtijUuAv/PDlWuag=="], + + "file-stream-rotator": ["file-stream-rotator@0.6.1", "", { "dependencies": { "moment": "^2.29.1" } }, "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ=="], + + "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + + "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], + + "fontkit": ["fontkit@2.0.4", "", { "dependencies": { "@swc/helpers": "^0.5.12", "brotli": "^1.3.2", "clone": "^2.1.2", "dfa": "^1.2.0", "fast-deep-equal": "^3.1.3", "restructure": "^3.0.0", "tiny-inflate": "^1.0.3", "unicode-properties": "^1.4.0", "unicode-trie": "^2.0.0" } }, "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g=="], + + "formsnap": ["formsnap@2.0.1", "", { "dependencies": { "svelte-toolbelt": "^0.5.0" }, "peerDependencies": { "svelte": "^5.0.0", "sveltekit-superforms": "^2.19.0" } }, "sha512-iJSe4YKd/W6WhLwKDVJU9FQeaJRpEFuolhju7ZXlRpUVyDdqFdMP8AUBICgnVvQPyP41IPAlBa/v0Eo35iE6wQ=="], + + "frac": ["frac@1.1.2", "", {}, "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], + + "hono": ["hono@4.12.3", "", {}, "sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg=="], + + "html-to-text": ["html-to-text@9.0.5", "", { "dependencies": { "@selderee/plugin-htmlparser2": "^0.11.0", "deepmerge": "^4.3.1", "dom-serializer": "^2.0.0", "htmlparser2": "^8.0.2", "selderee": "^0.11.0" } }, "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg=="], + + "htmlparser2": ["htmlparser2@8.0.2", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1", "entities": "^4.4.0" } }, "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA=="], + + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "imapflow": ["imapflow@1.2.1", "", { "dependencies": { "@zone-eu/mailsplit": "5.4.8", "encoding-japanese": "2.2.0", "iconv-lite": "0.7.0", "libbase64": "1.3.0", "libmime": "5.3.7", "libqp": "2.1.1", "nodemailer": "7.0.11", "pino": "10.1.0", "socks": "2.8.7" } }, "sha512-zoi2hYAtAw/ZRaSZzq5Fu0vxJ8LIGi444YBNN5VJFR0jTi8Y2etwMGO3yacea9XT9qvVSe6FceGtGGTxeqrvpQ=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], + + "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], + + "ioredis": ["ioredis@5.8.2", "", { "dependencies": { "@ioredis/commands": "1.4.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q=="], + + "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], + + "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "joi": ["joi@17.13.3", "", { "dependencies": { "@hapi/hoek": "^9.3.0", "@hapi/topo": "^5.1.0", "@sideway/address": "^4.1.5", "@sideway/formula": "^3.0.1", "@sideway/pinpoint": "^2.0.0" } }, "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA=="], + + "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], + + "jpeg-exif": ["jpeg-exif@1.1.4", "", {}, "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ=="], + + "json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="], + + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + + "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], + + "kysely": ["kysely@0.28.9", "", {}, "sha512-3BeXMoiOhpOwu62CiVpO6lxfq4eS6KMYfQdMsN/2kUCRNuF2YiEr7u0HLHaQU+O4Xu8YXE3bHVkwaQ85i72EuA=="], + + "layerchart": ["layerchart@2.0.0-next.43", "", { "dependencies": { "@dagrejs/dagre": "^1.1.5", "@layerstack/svelte-actions": "1.0.1-next.14", "@layerstack/svelte-state": "0.1.0-next.19", "@layerstack/tailwind": "2.0.0-next.17", "@layerstack/utils": "2.0.0-next.14", "d3-array": "^3.2.4", "d3-color": "^3.1.0", "d3-delaunay": "^6.0.4", "d3-dsv": "^3.0.1", "d3-force": "^3.0.0", "d3-geo": "^3.1.1", "d3-geo-voronoi": "^2.1.0", "d3-hierarchy": "^3.1.2", "d3-interpolate": "^3.0.1", "d3-interpolate-path": "^2.3.0", "d3-path": "^3.1.0", "d3-quadtree": "^3.0.1", "d3-random": "^3.0.1", "d3-sankey": "^0.12.3", "d3-scale": "^4.0.2", "d3-scale-chromatic": "^3.1.0", "d3-shape": "^3.2.0", "d3-tile": "^1.0.0", "d3-time": "^3.1.0", "lodash-es": "^4.17.21", "memoize": "^10.1.0", "runed": "^0.31.1" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-1Ywm38NdzHWKwgaAHq3EcqshIgsq+pylntSnVWAVazXUk/NsxPcxdpR3tMt3ySjWV0ZPBBgLs78sdVf7FTgd+g=="], + + "leac": ["leac@0.6.0", "", {}, "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="], + + "libbase64": ["libbase64@1.3.0", "", {}, "sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg=="], + + "libmime": ["libmime@5.3.7", "", { "dependencies": { "encoding-japanese": "2.2.0", "iconv-lite": "0.6.3", "libbase64": "1.3.0", "libqp": "2.1.1" } }, "sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw=="], + + "libphonenumber-js": ["libphonenumber-js@1.12.33", "", {}, "sha512-r9kw4OA6oDO4dPXkOrXTkArQAafIKAU71hChInV4FxZ69dxCfbwQGDPzqR5/vea94wU705/3AZroEbSoeVWrQw=="], + + "libqp": ["libqp@2.1.1", "", {}, "sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow=="], + + "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], + + "linebreak": ["linebreak@1.1.0", "", { "dependencies": { "base64-js": "0.0.8", "unicode-trie": "^2.0.0" } }, "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ=="], + + "linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="], + + "local-pkg": ["local-pkg@1.1.2", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.3.0", "quansync": "^0.2.11" } }, "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A=="], + + "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="], + + "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "lodash-es": ["lodash-es@4.17.22", "", {}, "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q=="], + + "lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="], + + "lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="], + + "logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="], + + "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "mailparser": ["mailparser@3.9.1", "", { "dependencies": { "@zone-eu/mailsplit": "5.4.8", "encoding-japanese": "2.2.0", "he": "1.2.0", "html-to-text": "9.0.5", "iconv-lite": "0.7.0", "libmime": "5.3.7", "linkify-it": "5.0.0", "nodemailer": "7.0.11", "punycode.js": "2.3.1", "tlds": "1.261.0" } }, "sha512-6vHZcco3fWsDMkf4Vz9iAfxvwrKNGbHx0dV1RKVphQ/zaNY34Buc7D37LSa09jeSeybWzYcTPjhiZFxzVRJedA=="], + + "marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="], + + "memoize": ["memoize@10.2.0", "", { "dependencies": { "mimic-function": "^5.0.1" } }, "sha512-DeC6b7QBrZsRs3Y02A6A7lQyzFbsQbqgjI6UW0GigGWV+u1s25TycMr0XHZE4cJce7rY/vyw2ctMQqfDkIhUEA=="], + + "memoize-weak": ["memoize-weak@1.0.2", "", {}, "sha512-gj39xkrjEw7nCn4nJ1M5ms6+MyMlyiGmttzsqAUsAKn6bYKwuTHh/AO3cKPF8IBrTIYTxb0wWXFs3E//Y8VoWQ=="], + + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + + "mini-svg-data-uri": ["mini-svg-data-uri@1.4.4", "", { "bin": { "mini-svg-data-uri": "cli.js" } }, "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg=="], + + "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], + + "mode-watcher": ["mode-watcher@1.1.0", "", { "dependencies": { "runed": "^0.25.0", "svelte-toolbelt": "^0.7.1" }, "peerDependencies": { "svelte": "^5.27.0" } }, "sha512-mUT9RRGPDYenk59qJauN1rhsIMKBmWA3xMF+uRwE8MW/tjhaDSCCARqkSuDTq8vr4/2KcAxIGVjACxTjdk5C3g=="], + + "moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="], + + "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], + + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], + + "nanostores": ["nanostores@1.1.0", "", {}, "sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA=="], + + "neverthrow": ["neverthrow@8.2.0", "", { "optionalDependencies": { "@rollup/rollup-linux-x64-gnu": "^4.24.0" } }, "sha512-kOCT/1MCPAxY5iUV3wytNFUMUolzuwd/VF/1KCx7kf6CutrOsTie+84zTGTpgQycjvfLdBBdvBvFLqFD2c0wkQ=="], + + "next": ["next@15.5.9", "", { "dependencies": { "@next/env": "15.5.9", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.5.7", "@next/swc-darwin-x64": "15.5.7", "@next/swc-linux-arm64-gnu": "15.5.7", "@next/swc-linux-arm64-musl": "15.5.7", "@next/swc-linux-x64-gnu": "15.5.7", "@next/swc-linux-x64-musl": "15.5.7", "@next/swc-win32-arm64-msvc": "15.5.7", "@next/swc-win32-x64-msvc": "15.5.7", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg=="], + + "node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="], + + "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], + + "nodemailer": ["nodemailer@7.0.11", "", {}, "sha512-gnXhNRE0FNhD7wPSCGhdNh46Hs6nm+uTyg+Kq0cZukNQiYdnCsoQjodNP9BQVG9XrcK/v6/MgpAPBUFyzh9pvw=="], + + "normalize-url": ["normalize-url@8.1.0", "", {}, "sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w=="], + + "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], + + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + + "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="], + + "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], + + "otplib": ["otplib@12.0.1", "", { "dependencies": { "@otplib/core": "^12.0.1", "@otplib/preset-default": "^12.0.1", "@otplib/preset-v11": "^12.0.1" } }, "sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg=="], + + "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + + "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], + + "pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="], + + "paneforge": ["paneforge@1.0.2", "", { "dependencies": { "runed": "^0.23.4", "svelte-toolbelt": "^0.9.2" }, "peerDependencies": { "svelte": "^5.29.0" } }, "sha512-KzmIXQH1wCfwZ4RsMohD/IUtEjVhteR+c+ulb/CHYJHX8SuDXoJmChtsc/Xs5Wl8NHS4L5Q7cxL8MG40gSU1bA=="], + + "parseley": ["parseley@0.12.1", "", { "dependencies": { "leac": "^0.6.0", "peberminta": "^0.9.0" } }, "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "pdfkit": ["pdfkit@0.17.2", "", { "dependencies": { "crypto-js": "^4.2.0", "fontkit": "^2.0.4", "jpeg-exif": "^1.1.4", "linebreak": "^1.1.0", "png-js": "^1.0.0" } }, "sha512-UnwF5fXy08f0dnp4jchFYAROKMNTaPqb/xgR8GtCzIcqoTnbOqtp3bwKvO4688oHI6vzEEs8Q6vqqEnC5IUELw=="], + + "peberminta": ["peberminta@0.9.0", "", {}, "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ=="], + + "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], + + "pg-protocol": ["pg-protocol@1.10.3", "", {}, "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="], + + "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "pino": ["pino@10.1.0", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w=="], + + "pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="], + + "pino-std-serializers": ["pino-std-serializers@7.0.0", "", {}, "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA=="], + + "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], + + "png-js": ["png-js@1.0.0", "", {}, "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g=="], + + "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="], + + "postgres": ["postgres@3.4.8", "", {}, "sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg=="], + + "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], + + "postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="], + + "postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="], + + "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], + + "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], + + "prettier-plugin-sort-imports": ["prettier-plugin-sort-imports@1.8.11", "", { "dependencies": { "prettier": "^3.1.1" }, "peerDependencies": { "typescript": ">4.0.0" } }, "sha512-ApmZkEmNh2Fi6TAcYNgUR15rsP1+omX43+8neY+MXfNZ7JQwrqSdjzKhLUYTtaLo52aaC9gCs+lJaYSU8oSJJA=="], + + "prettier-plugin-svelte": ["prettier-plugin-svelte@3.5.0", "", { "peerDependencies": { "prettier": "^3.0.0", "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" } }, "sha512-2lLO/7EupnjO/95t+XZesXs8Bf3nYLIDfCo270h5QWbj/vjLqmrQ1LiRk9LPggxSDsnVYfehamZNf+rgQYApZg=="], + + "prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.7.2", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-svelte"] }, "sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA=="], + + "process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="], + + "property-expr": ["property-expr@2.0.6", "", {}, "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA=="], + + "punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="], + + "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], + + "qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="], + + "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], + + "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], + + "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], + + "react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="], + + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], + + "redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="], + + "redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "restructure": ["restructure@3.0.2", "", {}, "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="], + + "robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="], + + "rolldown": ["rolldown@1.0.0-beta.9-commit.d91dfb5", "", { "dependencies": { "@oxc-project/runtime": "0.71.0", "@oxc-project/types": "0.71.0", "@rolldown/pluginutils": "1.0.0-beta.9-commit.d91dfb5", "ansis": "^4.0.0" }, "optionalDependencies": { "@rolldown/binding-darwin-arm64": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-darwin-x64": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-freebsd-x64": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.9-commit.d91dfb5" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-FHkj6gGEiEgmAXQchglofvUUdwj2Oiw603Rs+zgFAnn9Cb7T7z3fiaEc0DbN3ja4wYkW6sF2rzMEtC1V4BGx/g=="], + + "rollup": ["rollup@4.54.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.54.0", "@rollup/rollup-android-arm64": "4.54.0", "@rollup/rollup-darwin-arm64": "4.54.0", "@rollup/rollup-darwin-x64": "4.54.0", "@rollup/rollup-freebsd-arm64": "4.54.0", "@rollup/rollup-freebsd-x64": "4.54.0", "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", "@rollup/rollup-linux-arm-musleabihf": "4.54.0", "@rollup/rollup-linux-arm64-gnu": "4.54.0", "@rollup/rollup-linux-arm64-musl": "4.54.0", "@rollup/rollup-linux-loong64-gnu": "4.54.0", "@rollup/rollup-linux-ppc64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-musl": "4.54.0", "@rollup/rollup-linux-s390x-gnu": "4.54.0", "@rollup/rollup-linux-x64-gnu": "4.54.0", "@rollup/rollup-linux-x64-musl": "4.54.0", "@rollup/rollup-openharmony-arm64": "4.54.0", "@rollup/rollup-win32-arm64-msvc": "4.54.0", "@rollup/rollup-win32-ia32-msvc": "4.54.0", "@rollup/rollup-win32-x64-gnu": "4.54.0", "@rollup/rollup-win32-x64-msvc": "4.54.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw=="], + + "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], + + "runed": ["runed@0.35.1", "", { "dependencies": { "dequal": "^2.0.3", "esm-env": "^1.0.0", "lz-string": "^1.5.0" }, "peerDependencies": { "@sveltejs/kit": "^2.21.0", "svelte": "^5.7.0" }, "optionalPeers": ["@sveltejs/kit"] }, "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q=="], + + "rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="], + + "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], + + "selderee": ["selderee@0.11.0", "", { "dependencies": { "parseley": "^0.12.0" } }, "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA=="], + + "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], + + "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], + + "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], + + "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], + + "socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="], + + "sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "ssf": ["ssf@0.11.2", "", { "dependencies": { "frac": "~1.1.2" } }, "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g=="], + + "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="], + + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], + + "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], + + "superstruct": ["superstruct@2.0.2", "", {}, "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A=="], + + "svelte": ["svelte@5.46.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.5.0", "esm-env": "^1.2.1", "esrap": "^2.2.1", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-ZhLtvroYxUxr+HQJfMZEDRsGsmU46x12RvAv/zi9584f5KOX7bUrEbhPJ7cKFmUvZTJXi/CFZUYwDC6M1FigPw=="], + + "svelte-adapter-bun": ["svelte-adapter-bun@1.0.1", "", { "dependencies": { "rolldown": "^1.0.0-beta.38" }, "peerDependencies": { "@sveltejs/kit": "^2.4.0", "typescript": "^5" } }, "sha512-tNOvfm8BGgG+rmEA7hkmqtq07v7zoo4skLQc+hIoQ79J+1fkEMpJEA2RzCIe3aPc8JdrsMJkv3mpiZPMsgahjA=="], + + "svelte-check": ["svelte-check@4.3.5", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q=="], + + "svelte-sonner": ["svelte-sonner@1.0.7", "", { "dependencies": { "runed": "^0.28.0" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-1EUFYmd7q/xfs2qCHwJzGPh9n5VJ3X6QjBN10fof2vxgy8fYE7kVfZ7uGnd7i6fQaWIr5KvXcwYXE/cmTEjk5A=="], + + "svelte-toolbelt": ["svelte-toolbelt@0.10.6", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.35.1", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.30.2" } }, "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ=="], + + "sveltekit-superforms": ["sveltekit-superforms@2.29.1", "", { "dependencies": { "devalue": "^5.6.1", "memoize-weak": "^1.0.2", "ts-deepmerge": "^7.0.3" }, "optionalDependencies": { "@exodus/schemasafe": "^1.3.0", "@typeschema/class-validator": "^0.3.0", "@valibot/to-json-schema": "^1.5.0", "@vinejs/vine": "^3.0.1", "arktype": "^2.1.29", "class-validator": "^0.14.3", "effect": "^3.19.12", "joi": "^17.13.3", "json-schema-to-ts": "^3.1.1", "superstruct": "^2.0.2", "typebox": "^1.0.62", "valibot": "^1.2.0", "yup": "^1.7.1", "zod": "^4.1.13", "zod-v3-to-json-schema": "^4.0.0" }, "peerDependencies": { "@sveltejs/kit": "1.x || 2.x", "svelte": "3.x || 4.x || >=5.0.0-next.51" } }, "sha512-9Cv1beOVPgm8rb8NZBqLdlZ9cBqRBTk0+6/oHn7DWvHQoAFie1EPjh1e4NHO3Qouv1Zq9QTGrZNDbYcetkuOVw=="], + + "tabbable": ["tabbable@6.3.0", "", {}, "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ=="], + + "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], + + "tailwind-variants": ["tailwind-variants@3.2.2", "", { "peerDependencies": { "tailwind-merge": ">=3.0.0", "tailwindcss": "*" }, "optionalPeers": ["tailwind-merge"] }, "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg=="], + + "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], + + "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + + "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], + + "thirty-two": ["thirty-two@1.0.2", "", {}, "sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA=="], + + "thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="], + + "tiny-case": ["tiny-case@1.0.3", "", {}, "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q=="], + + "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="], + + "tlds": ["tlds@1.261.0", "", { "bin": { "tlds": "bin.js" } }, "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA=="], + + "tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="], + + "toposort": ["toposort@2.0.2", "", {}, "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg=="], + + "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + + "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], + + "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="], + + "ts-deepmerge": ["ts-deepmerge@7.0.3", "", {}, "sha512-Du/ZW2RfwV/D4cmA5rXafYjBQVuvu4qGiEEla4EmEHVHgRdx68Gftx7i66jn2bzHPwSVZY36Ae6OuDn9el4ZKA=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "turbo": ["turbo@2.7.0", "", { "optionalDependencies": { "turbo-darwin-64": "2.7.0", "turbo-darwin-arm64": "2.7.0", "turbo-linux-64": "2.7.0", "turbo-linux-arm64": "2.7.0", "turbo-windows-64": "2.7.0", "turbo-windows-arm64": "2.7.0" }, "bin": { "turbo": "bin/turbo" } }, "sha512-1dUGwi6cSSVZts1BwJa/Gh7w5dPNNGsNWZEAuRKxXWME44hTKWpQZrgiPnqMc5jJJOovzPK5N6tL+PHYRYL5Wg=="], + + "turbo-darwin-64": ["turbo-darwin-64@2.7.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-gwqL7cJOSYrV/jNmhXM8a2uzSFn7GcUASOuen6OgmUsafUj9SSWcgXZ/q0w9hRoL917hpidkdI//UpbxbZbwwg=="], + + "turbo-darwin-arm64": ["turbo-darwin-arm64@2.7.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-f3F5DYOnfE6lR6v/rSld7QGZgartKsnlIYY7jcF/AA7Wz27za9XjxMHzb+3i4pvRhAkryFgf2TNq7eCFrzyTpg=="], + + "turbo-linux-64": ["turbo-linux-64@2.7.0", "", { "os": "linux", "cpu": "x64" }, "sha512-KsC+UuKlhjCL+lom10/IYoxUsdhJOsuEki72YSr7WGYUSRihcdJQnaUyIDTlm0nPOb+gVihVNBuVP4KsNg1UnA=="], + + "turbo-linux-arm64": ["turbo-linux-arm64@2.7.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-1tjIYULeJtpmE/ovoI9qPBFJCtUEM7mYfeIMOIs4bXR6t/8u+rHPwr3j+vRHcXanIc42V1n3Pz52VqmJtIAviw=="], + + "turbo-windows-64": ["turbo-windows-64@2.7.0", "", { "os": "win32", "cpu": "x64" }, "sha512-KThkAeax46XiH+qICCQm7R8V2pPdeTTP7ArCSRrSLqnlO75ftNm8Ljx4VAllwIZkILrq/GDM8PlyhZdPeUdDxQ=="], + + "turbo-windows-arm64": ["turbo-windows-arm64@2.7.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-kzI6rsQ3Ejs+CkM9HEEP3Z4h5YMCRxwIlQXFQmgXSG3BIgorCkRF2Xr7iQ2i9AGwY/6jbiAYeJbvi3yCp+noFw=="], + + "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], + + "type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], + + "typebox": ["typebox@1.0.65", "", {}, "sha512-3WaZ4QmfAxmelhi0dwusYDoZ+DLDoVrsc3aORzgtk1I8JfIf4wn+F8i1TtrnU2jJKM/hZgjJGfzXrwS4B31zZw=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="], + + "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "unicode-properties": ["unicode-properties@1.4.1", "", { "dependencies": { "base64-js": "^1.3.0", "unicode-trie": "^2.0.0" } }, "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg=="], + + "unicode-trie": ["unicode-trie@2.0.0", "", { "dependencies": { "pako": "^0.2.5", "tiny-inflate": "^1.0.0" } }, "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ=="], + + "unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], + + "unplugin-icons": ["unplugin-icons@23.0.1", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/utils": "^3.1.0", "local-pkg": "^1.1.2", "obug": "^2.1.1", "unplugin": "^2.3.11" }, "peerDependencies": { "@svgr/core": ">=7.0.0", "@svgx/core": "^1.0.1", "@vue/compiler-sfc": "^3.0.2", "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["@svgr/core", "@svgx/core", "@vue/compiler-sfc", "svelte"] }, "sha512-rv0XEJepajKzDLvRUWASM8K+8+/CCfZn2jtogXqg6RIp7kpatRc/aFrVJn8ANQA09e++lPEEv9yX8cC9enc+QQ=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + + "valibot": ["valibot@1.2.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg=="], + + "validator": ["validator@13.15.26", "", {}, "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA=="], + + "vaul-svelte": ["vaul-svelte@1.0.0-next.7", "", { "dependencies": { "runed": "^0.23.2", "svelte-toolbelt": "^0.7.1" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-7zN7Bi3dFQixvvbUJY9uGDe7Ws/dGZeBQR2pXdXmzQiakjrxBvWo0QrmsX3HK+VH+SZOltz378cmgmCS9f9rSg=="], + + "vite": ["vite@7.3.0", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="], + + "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], + + "vitest": ["vitest@4.0.16", "", { "dependencies": { "@vitest/expect": "4.0.16", "@vitest/mocker": "4.0.16", "@vitest/pretty-format": "4.0.16", "@vitest/runner": "4.0.16", "@vitest/snapshot": "4.0.16", "@vitest/spy": "4.0.16", "@vitest/utils": "4.0.16", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.16", "@vitest/browser-preview": "4.0.16", "@vitest/browser-webdriverio": "4.0.16", "@vitest/ui": "4.0.16", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q=="], + + "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], + + "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "winston": ["winston@3.19.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.8", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA=="], + + "winston-daily-rotate-file": ["winston-daily-rotate-file@5.0.0", "", { "dependencies": { "file-stream-rotator": "^0.6.1", "object-hash": "^3.0.0", "triple-beam": "^1.4.1", "winston-transport": "^4.7.0" }, "peerDependencies": { "winston": "^3" } }, "sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw=="], + + "winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="], + + "wmf": ["wmf@1.0.2", "", {}, "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw=="], + + "word": ["word@0.3.0", "", {}, "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA=="], + + "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], + + "xlsx": ["xlsx@0.18.5", "", { "dependencies": { "adler-32": "~1.3.0", "cfb": "~1.2.1", "codepage": "~1.15.0", "crc-32": "~1.2.1", "ssf": "~0.11.2", "wmf": "~1.0.1", "word": "~0.3.0" }, "bin": { "xlsx": "bin/xlsx.njs" } }, "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ=="], + + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + + "y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="], + + "yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], + + "yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], + + "yup": ["yup@1.7.1", "", { "dependencies": { "property-expr": "^2.0.5", "tiny-case": "^1.0.3", "toposort": "^2.0.2", "type-fest": "^2.19.0" } }, "sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw=="], + + "zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="], + + "zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="], + + "zod-v3-to-json-schema": ["zod-v3-to-json-schema@4.0.0", "", { "peerDependencies": { "zod": "^3.25 || ^4.0.14" } }, "sha512-KixLrhX/uPmRFnDgsZrzrk4x5SSJA+PmaE5adbfID9+3KPJcdxqRobaHU397EfWBqfQircrjKqvEqZ/mW5QH6w=="], + + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + + "@neondatabase/serverless/@types/node": ["@types/node@22.19.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA=="], + + "@pkg/settings/dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.0", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "brotli/base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="], + + "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], + + "drizzle-kit/esbuild": ["esbuild@0.19.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", "@esbuild/android-arm": "0.19.12", "@esbuild/android-arm64": "0.19.12", "@esbuild/android-x64": "0.19.12", "@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-x64": "0.19.12", "@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-x64": "0.19.12", "@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-x64": "0.19.12", "@esbuild/netbsd-x64": "0.19.12", "@esbuild/openbsd-x64": "0.19.12", "@esbuild/sunos-x64": "0.19.12", "@esbuild/win32-arm64": "0.19.12", "@esbuild/win32-ia32": "0.19.12", "@esbuild/win32-x64": "0.19.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg=="], + + "formsnap/svelte-toolbelt": ["svelte-toolbelt@0.5.0", "", { "dependencies": { "clsx": "^2.1.1", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.0.0-next.126" } }, "sha512-t3tenZcnfQoIeRuQf/jBU7bvTeT3TGkcEE+1EUr5orp0lR7NEpprflpuie3x9Dn0W9nOKqs3HwKGJeeN5Ok1sQ=="], + + "imapflow/iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="], + + "layerchart/runed": ["runed@0.31.1", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-v3czcTnO+EJjiPvD4dwIqfTdHLZ8oH0zJheKqAHh9QMViY7Qb29UlAMRpX7ZtHh7AFqV60KmfxaJ9QMy+L1igQ=="], + + "mailparser/iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="], + + "mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + + "mode-watcher/runed": ["runed@0.25.0", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg=="], + + "mode-watcher/svelte-toolbelt": ["svelte-toolbelt@0.7.1", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.23.2", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ=="], + + "next/@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], + + "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + + "paneforge/runed": ["runed@0.23.4", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA=="], + + "paneforge/svelte-toolbelt": ["svelte-toolbelt@0.9.3", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.29.0", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.30.2" } }, "sha512-HCSWxCtVmv+c6g1ACb8LTwHVbDqLKJvHpo6J8TaqwUme2hj9ATJCpjCPNISR1OCq2Q4U1KT41if9ON0isINQZw=="], + + "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "svelte-sonner/runed": ["runed@0.28.0", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ=="], + + "unicode-properties/base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "vaul-svelte/runed": ["runed@0.23.4", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA=="], + + "vaul-svelte/svelte-toolbelt": ["svelte-toolbelt@0.7.1", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.23.2", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ=="], + + "yargs-parser/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + + "@neondatabase/serverless/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "d3-sankey/d3-array/internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], + + "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], + + "drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="], + + "drizzle-kit/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="], + + "drizzle-kit/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.19.12", "", { "os": "android", "cpu": "arm64" }, "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA=="], + + "drizzle-kit/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.19.12", "", { "os": "android", "cpu": "x64" }, "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew=="], + + "drizzle-kit/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.19.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g=="], + + "drizzle-kit/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.19.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A=="], + + "drizzle-kit/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.19.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA=="], + + "drizzle-kit/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.19.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg=="], + + "drizzle-kit/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.19.12", "", { "os": "linux", "cpu": "arm" }, "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w=="], + + "drizzle-kit/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.19.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA=="], + + "drizzle-kit/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.19.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA=="], + + "drizzle-kit/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA=="], + + "drizzle-kit/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w=="], + + "drizzle-kit/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.19.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg=="], + + "drizzle-kit/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg=="], + + "drizzle-kit/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.19.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg=="], + + "drizzle-kit/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.19.12", "", { "os": "linux", "cpu": "x64" }, "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg=="], + + "drizzle-kit/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.19.12", "", { "os": "none", "cpu": "x64" }, "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA=="], + + "drizzle-kit/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.19.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw=="], + + "drizzle-kit/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.19.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA=="], + + "drizzle-kit/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.19.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A=="], + + "drizzle-kit/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.19.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ=="], + + "drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.12", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="], + + "mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + + "mode-watcher/svelte-toolbelt/runed": ["runed@0.23.4", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA=="], + + "next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "paneforge/svelte-toolbelt/runed": ["runed@0.29.2", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-0cq6cA6sYGZwl/FvVqjx9YN+1xEBu9sDDyuWdDW1yWX7JF2wmvmVKfH+hVCZs+csW+P3ARH92MjI3H9QTagOQA=="], + } +} diff --git a/dev/README.md b/dev/README.md new file mode 100644 index 0000000..23b0c33 --- /dev/null +++ b/dev/README.md @@ -0,0 +1,42 @@ +# Dev + +Self-contained local development stack. Spin up all shared infrastructure on a per-project basis. + +## Services + +| Service | Description | Port(s) | +| ------------------ | ---------------------------------------- | -------------- | +| **PostgreSQL** | Primary relational database | `5432` | +| **Valkey** | Redis-compatible cache / message broker | `6379` | +| **SigNoz** | Observability UI (traces, metrics, logs) | `8080` | +| **OTel Collector** | OpenTelemetry ingest (gRPC / HTTP) | `4317`, `4318` | +| **ClickHouse** | Telemetry storage backend for SigNoz | — | + +## Run + +```sh +cd dev +docker compose -f docker-compose.dev.yaml up -d +``` + +## Stop + +```sh +docker compose -f docker-compose.dev.yaml down +``` + +To also remove all persisted data volumes: + +```sh +docker compose -f docker-compose.dev.yaml down -v +``` + +## Connection strings + +| Resource | Default value | +| ---------- | --------------------------------------------------------- | +| PostgreSQL | `postgresql://postgres:postgres@localhost:5432/primarydb` | +| Valkey | `redis://localhost:6379` | +| SigNoz UI | `http://localhost:8080` | +| OTLP gRPC | `localhost:4317` | +| OTLP HTTP | `localhost:4318` | diff --git a/dev/clickhouse-cluster.xml b/dev/clickhouse-cluster.xml new file mode 100644 index 0000000..8b475ff --- /dev/null +++ b/dev/clickhouse-cluster.xml @@ -0,0 +1,75 @@ + + + + + + zookeeper-1 + 2181 + + + + + + + + + + + + + + + + clickhouse + 9000 + + + + + + + + diff --git a/dev/clickhouse-config.xml b/dev/clickhouse-config.xml new file mode 100644 index 0000000..1965ac3 --- /dev/null +++ b/dev/clickhouse-config.xml @@ -0,0 +1,1142 @@ + + + + + + information + + json + + /var/log/clickhouse-server/clickhouse-server.log + /var/log/clickhouse-server/clickhouse-server.err.log + + 1000M + 10 + + + + + + + + + + + + + + + + + + 8123 + + + 9000 + + + 9004 + + + 9005 + + + + + + + + + + + + 9009 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 4096 + + + 3 + + + + + false + + + /path/to/ssl_cert_file + /path/to/ssl_key_file + + + false + + + /path/to/ssl_ca_cert_file + + + none + + + 0 + + + -1 + -1 + + + false + + + + + + + + + + + none + true + true + sslv2,sslv3 + true + + + + true + true + sslv2,sslv3 + true + + + + RejectCertificateHandler + + + + + + + + + 100 + + + 0 + + + + 10000 + + + + + + 0.9 + + + 4194304 + + + 0 + + + + + + 8589934592 + + + 5368709120 + + + + 1000 + + + 134217728 + + + 10000 + + + /var/lib/clickhouse/ + + + /var/lib/clickhouse/tmp/ + + + + ` + + + + + + /var/lib/clickhouse/user_files/ + + + + + + + + + + + + + users.xml + + + + /var/lib/clickhouse/access/ + + + + + + + default + + + + + + + + + + + + default + + + + + + + + + true + + + false + + ' | sed -e 's|.*>\(.*\)<.*|\1|') + wget https://github.com/ClickHouse/clickhouse-jdbc-bridge/releases/download/v$PKG_VER/clickhouse-jdbc-bridge_$PKG_VER-1_all.deb + apt install --no-install-recommends -f ./clickhouse-jdbc-bridge_$PKG_VER-1_all.deb + clickhouse-jdbc-bridge & + + * [CentOS/RHEL] + export MVN_URL=https://repo1.maven.org/maven2/ru/yandex/clickhouse/clickhouse-jdbc-bridge + export PKG_VER=$(curl -sL $MVN_URL/maven-metadata.xml | grep '' | sed -e 's|.*>\(.*\)<.*|\1|') + wget https://github.com/ClickHouse/clickhouse-jdbc-bridge/releases/download/v$PKG_VER/clickhouse-jdbc-bridge-$PKG_VER-1.noarch.rpm + yum localinstall -y clickhouse-jdbc-bridge-$PKG_VER-1.noarch.rpm + clickhouse-jdbc-bridge & + + Please refer to https://github.com/ClickHouse/clickhouse-jdbc-bridge#usage for more information. + ]]> + + + + + + + + + + + + + + + 01 + example01-01-1 + + + + + + 3600 + + + + 3600 + + + 60 + + + + + + + + + + /metrics + 9363 + + true + true + true + true + + + + + + system + query_log
    + + toYYYYMM(event_date) + + + + + + 7500 +
    + + + + system + trace_log
    + + toYYYYMM(event_date) + 7500 +
    + + + + system + query_thread_log
    + toYYYYMM(event_date) + 7500 +
    + + + + system + query_views_log
    + toYYYYMM(event_date) + 7500 +
    + + + + system + part_log
    + toYYYYMM(event_date) + 7500 +
    + + + + + + system + metric_log
    + 7500 + 1000 +
    + + + + system + asynchronous_metric_log
    + + 7000 +
    + + + + + + engine MergeTree + partition by toYYYYMM(finish_date) + order by (finish_date, finish_time_us, trace_id) + + system + opentelemetry_span_log
    + 7500 +
    + + + + + system + crash_log
    + + + 1000 +
    + + + + + + + system + processors_profile_log
    + + toYYYYMM(event_date) + 7500 +
    + + + + + + + + + *_dictionary.xml + + + *function.xml + /var/lib/clickhouse/user_scripts/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + /clickhouse/task_queue/ddl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + click_cost + any + + 0 + 3600 + + + 86400 + 60 + + + + max + + 0 + 60 + + + 3600 + 300 + + + 86400 + 3600 + + + + + + /var/lib/clickhouse/format_schemas/ + + + + + hide encrypt/decrypt arguments + ((?:aes_)?(?:encrypt|decrypt)(?:_mysql)?)\s*\(\s*(?:'(?:\\'|.)+'|.*?)\s*\) + + \1(???) + + + + + + + + + + false + + false + + + https://6f33034cfe684dd7a3ab9875e57b1c8d@o388870.ingest.sentry.io/5226277 + + + + + + + + + + + 268435456 + true + +
    diff --git a/dev/clickhouse-custom-function.xml b/dev/clickhouse-custom-function.xml new file mode 100644 index 0000000..b2b3f91 --- /dev/null +++ b/dev/clickhouse-custom-function.xml @@ -0,0 +1,21 @@ + + + executable + histogramQuantile + Float64 + + Array(Float64) + buckets + + + Array(Float64) + counts + + + Float64 + quantile + + CSV + ./histogramQuantile + + diff --git a/dev/clickhouse-users.xml b/dev/clickhouse-users.xml new file mode 100644 index 0000000..f185620 --- /dev/null +++ b/dev/clickhouse-users.xml @@ -0,0 +1,123 @@ + + + + + + + + + + 10000000000 + + + random + + + + + 1 + + + + + + + + + + + + + ::/0 + + + + default + + + default + + + + + + + + + + + + + + 3600 + + + 0 + 0 + 0 + 0 + 0 + + + + diff --git a/dev/docker-compose.dev.yaml b/dev/docker-compose.dev.yaml new file mode 100644 index 0000000..3b3a752 --- /dev/null +++ b/dev/docker-compose.dev.yaml @@ -0,0 +1,218 @@ +x-common: &common + networks: + - signoz-net + restart: unless-stopped + logging: + options: + max-size: 50m + max-file: "3" + +x-clickhouse-defaults: &clickhouse-defaults + !!merge <<: *common + image: clickhouse/clickhouse-server:25.5.6 + tty: true + labels: + signoz.io/scrape: "true" + signoz.io/port: "9363" + signoz.io/path: "/metrics" + depends_on: + init-clickhouse: + condition: service_completed_successfully + zookeeper-1: + condition: service_healthy + healthcheck: + test: + - CMD + - wget + - --spider + - -q + - 0.0.0.0:8123/ping + interval: 30s + timeout: 5s + retries: 3 + ulimits: + nproc: 65535 + nofile: + soft: 262144 + hard: 262144 + environment: + - CLICKHOUSE_SKIP_USER_SETUP=1 +x-zookeeper-defaults: &zookeeper-defaults + !!merge <<: *common + image: signoz/zookeeper:3.7.1 + user: root + labels: + signoz.io/scrape: "true" + signoz.io/port: "9141" + signoz.io/path: "/metrics" + healthcheck: + test: + - CMD-SHELL + - curl -s -m 2 http://localhost:8080/commands/ruok | grep error | grep null + interval: 30s + timeout: 5s + retries: 3 +x-db-depend: &db-depend + !!merge <<: *common + depends_on: + clickhouse: + condition: service_healthy +# ====== +# Main +# ====== +services: + valkey: + restart: always + image: valkey/valkey:9.0.3 + networks: + - signoz-net + ports: + - 6379:6379 + volumes: + - dev_valkey_data:/data + postgresql: + restart: always + image: postgres:18.3 + networks: + - signoz-net + ports: + - 5432:5432 + environment: + POSTGRES_DB: primarydb + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + volumes: + - dev_postgresql_data:/var/lib/postgresql + + init-clickhouse: + !!merge <<: *common + image: clickhouse/clickhouse-server:25.5.6 + container_name: signoz-init-clickhouse + command: + - bash + - -c + - | + version="v0.0.1" + node_os=$$(uname -s | tr '[:upper:]' '[:lower:]') + node_arch=$$(uname -m | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) + echo "Fetching histogram-binary for $${node_os}/$${node_arch}" + cd /tmp + wget -O histogram-quantile.tar.gz "https://github.com/SigNoz/signoz/releases/download/histogram-quantile%2F$${version}/histogram-quantile_$${node_os}_$${node_arch}.tar.gz" + tar -xvzf histogram-quantile.tar.gz + mv histogram-quantile /var/lib/clickhouse/user_scripts/histogramQuantile + restart: on-failure + volumes: + - clickhouse-user-scripts:/var/lib/clickhouse/user_scripts/ + zookeeper-1: + !!merge <<: *zookeeper-defaults + container_name: signoz-zookeeper-1 + # ports: + # - "2181:2181" + # - "2888:2888" + # - "3888:3888" + volumes: + - zookeeper-1:/bitnami/zookeeper + environment: + - ZOO_SERVER_ID=1 + - ALLOW_ANONYMOUS_LOGIN=yes + - ZOO_AUTOPURGE_INTERVAL=1 + - ZOO_ENABLE_PROMETHEUS_METRICS=yes + - ZOO_PROMETHEUS_METRICS_PORT_NUMBER=9141 + clickhouse: + !!merge <<: *clickhouse-defaults + container_name: signoz-clickhouse + # ports: + # - "9000:9000" + # - "8123:8123" + # - "9181:9181" + volumes: + - ./clickhouse-config.xml:/etc/clickhouse-server/config.xml + - ./clickhouse-users.xml:/etc/clickhouse-server/users.xml + - ./clickhouse-custom-function.xml:/etc/clickhouse-server/custom-function.xml + - clickhouse-user-scripts:/var/lib/clickhouse/user_scripts/ + - ./clickhouse-cluster.xml:/etc/clickhouse-server/config.d/cluster.xml + - clickhouse:/var/lib/clickhouse/ + # - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml + signoz: + !!merge <<: *db-depend + image: signoz/signoz:${VERSION:-v0.113.0} + container_name: signoz + ports: + - "8080:8080" # signoz port + volumes: + - sqlite:/var/lib/signoz/ + environment: + - SIGNOZ_ALERTMANAGER_PROVIDER=signoz + - SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://clickhouse:9000 + - SIGNOZ_SQLSTORE_SQLITE_PATH=/var/lib/signoz/signoz.db + - SIGNOZ_TOKENIZER_JWT_SECRET=secret + healthcheck: + test: + - CMD + - wget + - --spider + - -q + - localhost:8080/api/v1/health + interval: 30s + timeout: 5s + retries: 3 + otel-collector: + !!merge <<: *db-depend + image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1} + container_name: signoz-otel-collector + entrypoint: + - /bin/sh + command: + - -c + - | + /signoz-otel-collector migrate sync check && + /signoz-otel-collector --config=/etc/otel-collector-config.yaml --manager-config=/etc/manager-config.yaml --copy-path=/var/tmp/collector-config.yaml + volumes: + - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml + - ./otel-collector-opamp-config.yaml:/etc/manager-config.yaml + environment: + - OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux + - LOW_CARDINAL_EXCEPTION_GROUPING=false + - SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000 + - SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster + - SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_REPLICATION=true + - SIGNOZ_OTEL_COLLECTOR_TIMEOUT=10m + ports: + # - "1777:1777" # pprof extension + - "4317:4317" # OTLP gRPC receiver + - "4318:4318" # OTLP HTTP receiver + signoz-telemetrystore-migrator: + !!merge <<: *db-depend + image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1} + container_name: signoz-telemetrystore-migrator + environment: + - SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000 + - SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster + - SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_REPLICATION=true + - SIGNOZ_OTEL_COLLECTOR_TIMEOUT=10m + entrypoint: + - /bin/sh + command: + - -c + - | + /signoz-otel-collector migrate bootstrap && + /signoz-otel-collector migrate sync up && + /signoz-otel-collector migrate async up + restart: on-failure +# Peripherals +networks: + signoz-net: + name: signoz-net +volumes: + dev_valkey_data: + name: dev-valkey-data + dev_postgresql_data: + name: dev-postgresql-data + clickhouse: + name: signoz-clickhouse + clickhouse-user-scripts: + name: signoz-clickhouse-user-scripts + sqlite: + name: signoz-sqlite + zookeeper-1: + name: signoz-zookeeper-1 diff --git a/dev/otel-collector-config.yaml b/dev/otel-collector-config.yaml new file mode 100644 index 0000000..88d395b --- /dev/null +++ b/dev/otel-collector-config.yaml @@ -0,0 +1,124 @@ +connectors: + signozmeter: + metrics_flush_interval: 1h + dimensions: + - name: service.name + - name: deployment.environment + - name: host.name +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + +processors: + batch: + send_batch_size: 10000 + send_batch_max_size: 11000 + timeout: 10s + batch/meter: + send_batch_max_size: 25000 + send_batch_size: 20000 + timeout: 1s + resourcedetection: + # Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels. + detectors: [env, system] + timeout: 2s + signozspanmetrics/delta: + metrics_exporter: signozclickhousemetrics + metrics_flush_interval: 60s + latency_histogram_buckets: + [ + 100us, + 1ms, + 2ms, + 6ms, + 10ms, + 50ms, + 100ms, + 250ms, + 500ms, + 1000ms, + 1400ms, + 2000ms, + 5s, + 10s, + 20s, + 40s, + 60s, + ] + dimensions_cache_size: 100000 + aggregation_temporality: AGGREGATION_TEMPORALITY_DELTA + enable_exp_histogram: true + dimensions: + - name: service.namespace + default: default + - name: deployment.environment + default: default + # This is added to ensure the uniqueness of the timeseries + # Otherwise, identical timeseries produced by multiple replicas of + # collectors result in incorrect APM metrics + - name: signoz.collector.id + - name: service.version + - name: browser.platform + - name: browser.mobile + - name: k8s.cluster.name + - name: k8s.node.name + - name: k8s.namespace.name + - name: host.name + - name: host.type + - name: container.name +extensions: + health_check: + endpoint: 0.0.0.0:13133 + pprof: + endpoint: 0.0.0.0:1777 +exporters: + clickhousetraces: + datasource: tcp://clickhouse:9000/signoz_traces + low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING} + use_new_schema: true + signozclickhousemetrics: + dsn: tcp://clickhouse:9000/signoz_metrics + clickhouselogsexporter: + dsn: tcp://clickhouse:9000/signoz_logs + timeout: 10s + use_new_schema: true + signozclickhousemeter: + dsn: tcp://clickhouse:9000/signoz_meter + timeout: 45s + sending_queue: + enabled: false + metadataexporter: + cache: + provider: in_memory + dsn: tcp://clickhouse:9000/signoz_metadata + enabled: true + timeout: 45s +service: + telemetry: + logs: + encoding: json + extensions: + - health_check + - pprof + pipelines: + traces: + receivers: [otlp] + processors: [signozspanmetrics/delta, batch] + exporters: [clickhousetraces, metadataexporter, signozmeter] + metrics: + receivers: [otlp] + processors: [batch] + exporters: [signozclickhousemetrics, metadataexporter, signozmeter] + + logs: + receivers: [otlp] + processors: [batch] + exporters: [clickhouselogsexporter, metadataexporter, signozmeter] + metrics/meter: + receivers: [signozmeter] + processors: [batch/meter] + exporters: [signozclickhousemeter] diff --git a/dev/otel-collector-opamp-config.yaml b/dev/otel-collector-opamp-config.yaml new file mode 100644 index 0000000..7267607 --- /dev/null +++ b/dev/otel-collector-opamp-config.yaml @@ -0,0 +1 @@ +server_endpoint: ws://signoz:4320/v1/opamp diff --git a/dockerfiles/main.Dockerfile b/dockerfiles/main.Dockerfile new file mode 100644 index 0000000..14843cb --- /dev/null +++ b/dockerfiles/main.Dockerfile @@ -0,0 +1,27 @@ +FROM node:25.6.1 AS production + +RUN npm i -g bun + +WORKDIR /app + +COPY package.json bun.lock turbo.json ./ + +COPY apps/main/package.json ./apps/main/package.json + +COPY packages ./packages + +RUN bun install + +COPY apps/main ./apps/main + +RUN bun install + +RUN bun run build + +COPY scripts ./scripts + +EXPOSE 3000 + +RUN chmod +x scripts/prod.start.sh + +CMD ["/bin/sh", "scripts/prod.start.sh", "apps/main"] diff --git a/dockerfiles/migrator.Dockerfile b/dockerfiles/migrator.Dockerfile new file mode 100644 index 0000000..ad9e319 --- /dev/null +++ b/dockerfiles/migrator.Dockerfile @@ -0,0 +1,17 @@ +FROM node:23.11.0 AS base + +RUN npm i -g bun + +FROM base AS primary + +WORKDIR /app + +COPY package.json bun.lock turbo.json ./ + +COPY packages/db packages/db + +RUN bun install + +COPY scripts scripts + +CMD ["/bin/sh", "/app/scripts/migrate.sh"] diff --git a/dockerfiles/processor.Dockerfile b/dockerfiles/processor.Dockerfile new file mode 100644 index 0000000..a968026 --- /dev/null +++ b/dockerfiles/processor.Dockerfile @@ -0,0 +1,26 @@ +FROM oven/bun:1.3.5-alpine + +RUN apk add --no-cache xh + +WORKDIR /app + +COPY package.json bun.lock turbo.json ./ + +COPY apps/processor/package.json ./apps/processor/package.json + +COPY packages ./packages + +RUN bun install + +COPY apps/processor ./apps/processor + +RUN bun install + +COPY scripts ./scripts + +EXPOSE 3000 +EXPOSE 9001 + +RUN chmod +x scripts/*.sh + +CMD ["/bin/sh", "scripts/prod.start.sh", "apps/processor"] diff --git a/package.json b/package.json new file mode 100644 index 0000000..ec27cab --- /dev/null +++ b/package.json @@ -0,0 +1,42 @@ +{ + "name": "@illusory/mapp", + "version": "1.0.0", + "private": true, + "engines": { + "node": ">=24" + }, + "packageManager": "bun@1.3.5", + "workspaces": [ + "apps/*", + "packages/*" + ], + "scripts": { + "build": "turbo build", + "dev": "turbo dev --concurrency=8", + "auth:schemagen": "source .env && cd packages/logic && bun run auth:schemagen", + "db:migrate": "./scripts/migrate.sh", + "prod": "turbo prod", + "test": "turbo test" + }, + "devDependencies": { + "prettier": "^3.8.1", + "prettier-plugin-sort-imports": "^1.8.11", + "prettier-plugin-svelte": "^3.5.0", + "prettier-plugin-tailwindcss": "^0.7.2", + "turbo": "^2.7.0", + "typescript": "^5.9.3" + }, + "prettier": { + "arrowParens": "always", + "singleQuote": false, + "jsxSingleQuote": false, + "semi": true, + "trailingComma": "all", + "tabWidth": 4, + "plugins": [ + "prettier-plugin-sort-imports", + "prettier-plugin-tailwindcss", + "prettier-plugin-svelte" + ] + } +} diff --git a/packages/db/drizzle.config.ts b/packages/db/drizzle.config.ts new file mode 100644 index 0000000..98dd550 --- /dev/null +++ b/packages/db/drizzle.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from "drizzle-kit"; +import { settings } from "@pkg/settings"; + +export default defineConfig({ + schema: "./schema", + out: "./migrations", + dialect: "postgresql", + verbose: true, + strict: false, + dbCredentials: { + url: settings.databaseUrl, + }, +}); diff --git a/packages/db/index.ts b/packages/db/index.ts new file mode 100644 index 0000000..7bf9e59 --- /dev/null +++ b/packages/db/index.ts @@ -0,0 +1,11 @@ +import { drizzle } from "drizzle-orm/postgres-js"; +import { settings } from "@pkg/settings"; +import * as schema from "./schema"; + +const db = drizzle(settings.databaseUrl, { schema }); + +export type Database = typeof db; + +export * from "drizzle-orm"; + +export { db, schema }; diff --git a/packages/db/migrations/0000_woozy_mother_askani.sql b/packages/db/migrations/0000_woozy_mother_askani.sql new file mode 100644 index 0000000..330fc99 --- /dev/null +++ b/packages/db/migrations/0000_woozy_mother_askani.sql @@ -0,0 +1,119 @@ +CREATE TABLE IF NOT EXISTS "two_factor" ( + "id" text PRIMARY KEY NOT NULL, + "secret" text NOT NULL, + "backup_codes" json, + "user_id" text NOT NULL, + "created_at" timestamp NOT NULL, + "updated_at" timestamp NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "twofa_sessions" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "session_id" text NOT NULL, + "verification_token" text NOT NULL, + "code_used" text, + "status" varchar(16) NOT NULL, + "attempts" integer DEFAULT 0 NOT NULL, + "max_attempts" integer DEFAULT 5 NOT NULL, + "verified_at" timestamp, + "expires_at" timestamp NOT NULL, + "created_at" timestamp NOT NULL, + "ip_address" text DEFAULT '', + "user_agent" text DEFAULT '', + CONSTRAINT "twofa_sessions_verification_token_unique" UNIQUE("verification_token") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "account" ( + "id" text PRIMARY KEY NOT NULL, + "account_id" text NOT NULL, + "provider_id" text NOT NULL, + "user_id" text NOT NULL, + "access_token" text, + "refresh_token" text, + "id_token" text, + "access_token_expires_at" timestamp, + "refresh_token_expires_at" timestamp, + "scope" text, + "password" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "user" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "email" text NOT NULL, + "email_verified" boolean DEFAULT false NOT NULL, + "image" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + "username" text, + "display_username" text, + "role" text, + "banned" boolean DEFAULT false, + "ban_reason" text, + "ban_expires" timestamp, + "onboarding_done" boolean DEFAULT false, + "last2_fa_verified_at" timestamp, + "parent_id" text, + CONSTRAINT "user_email_unique" UNIQUE("email"), + CONSTRAINT "user_username_unique" UNIQUE("username") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "verification" ( + "id" text PRIMARY KEY NOT NULL, + "identifier" text NOT NULL, + "value" text NOT NULL, + "expires_at" timestamp NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "notifications" ( + "id" serial PRIMARY KEY NOT NULL, + "title" text NOT NULL, + "body" text NOT NULL, + "priority" varchar(12) DEFAULT 'normal' NOT NULL, + "type" varchar(12) NOT NULL, + "category" varchar(64), + "is_read" boolean DEFAULT false NOT NULL, + "is_archived" boolean DEFAULT false NOT NULL, + "action_url" text, + "action_type" varchar(16), + "action_data" json, + "icon" varchar(64), + "user_id" text NOT NULL, + "sent_at" timestamp NOT NULL, + "read_at" timestamp, + "expires_at" timestamp, + "created_at" timestamp NOT NULL, + "updated_at" timestamp NOT NULL +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "two_factor" ADD CONSTRAINT "two_factor_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "twofa_sessions" ADD CONSTRAINT "twofa_sessions_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "notifications" ADD CONSTRAINT "notifications_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "account_userId_idx" ON "account" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "verification_identifier_idx" ON "verification" USING btree ("identifier"); \ No newline at end of file diff --git a/packages/db/migrations/meta/0000_snapshot.json b/packages/db/migrations/meta/0000_snapshot.json new file mode 100644 index 0000000..86da0f3 --- /dev/null +++ b/packages/db/migrations/meta/0000_snapshot.json @@ -0,0 +1,655 @@ +{ + "id": "1bb75845-f9cf-41a0-96ec-10c66fdc34d6", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "two_factor_user_id_user_id_fk": { + "name": "two_factor_user_id_user_id_fk", + "tableFrom": "two_factor", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.twofa_sessions": { + "name": "twofa_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verification_token": { + "name": "verification_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code_used": { + "name": "code_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": { + "twofa_sessions_user_id_user_id_fk": { + "name": "twofa_sessions_user_id_user_id_fk", + "tableFrom": "twofa_sessions", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "twofa_sessions_verification_token_unique": { + "name": "twofa_sessions_verification_token_unique", + "nullsNotDistinct": false, + "columns": [ + "verification_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_username": { + "name": "display_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "onboarding_done": { + "name": "onboarding_done", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "last2_fa_verified_at": { + "name": "last2_fa_verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "user_username_unique": { + "name": "user_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true, + "default": "'normal'" + }, + "type": { + "name": "type", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "is_read": { + "name": "is_read", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_archived": { + "name": "is_archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "action_url": { + "name": "action_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action_type": { + "name": "action_type", + "type": "varchar(16)", + "primaryKey": false, + "notNull": false + }, + "action_data": { + "name": "action_data", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "read_at": { + "name": "read_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "notifications_user_id_user_id_fk": { + "name": "notifications_user_id_user_id_fk", + "tableFrom": "notifications", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json new file mode 100644 index 0000000..ee55497 --- /dev/null +++ b/packages/db/migrations/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1769954723767, + "tag": "0000_woozy_mother_askani", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/packages/db/package.json b/packages/db/package.json new file mode 100644 index 0000000..593d43f --- /dev/null +++ b/packages/db/package.json @@ -0,0 +1,27 @@ +{ + "name": "@pkg/db", + "module": "index.ts", + "type": "module", + "scripts": { + "db:gen": "drizzle-kit generate --config=drizzle.config.ts", + "db:drop": "drizzle-kit drop --config=drizzle.config.ts", + "db:push": "drizzle-kit push --config=drizzle.config.ts", + "db:migrate": "drizzle-kit generate --config=drizzle.config.ts && drizzle-kit push --config=drizzle.config.ts", + "db:forcemigrate": "drizzle-kit generate --config=drizzle.config.ts && drizzle-kit push --config=drizzle.config.ts --force", + "dev": "drizzle-kit studio --host=0.0.0.0 --port=5420 --config=drizzle.config.ts --verbose" + }, + "dependencies": { + "@pkg/settings": "workspace:*", + "dotenv": "^16.4.7", + "drizzle-orm": "^0.36.1", + "postgres": "^3.4.8" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/pg": "^8.11.10", + "drizzle-kit": "^0.28.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } +} diff --git a/packages/db/schema/auth.schema.ts b/packages/db/schema/auth.schema.ts new file mode 100644 index 0000000..0001481 --- /dev/null +++ b/packages/db/schema/auth.schema.ts @@ -0,0 +1,52 @@ +import { + integer, + json, + pgTable, + text, + timestamp, + varchar, +} from "drizzle-orm/pg-core"; +import { user } from "./better.auth.schema"; +import { relations } from "drizzle-orm"; + +export const twoFactor = pgTable("two_factor", { + id: text("id").primaryKey(), + secret: text("secret").notNull(), + backupCodes: json("backup_codes").$type(), + userId: text("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + createdAt: timestamp("created_at").notNull(), + updatedAt: timestamp("updated_at").notNull(), +}); + +export const twofaSessions = pgTable("twofa_sessions", { + id: text("id").primaryKey(), + userId: text("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + sessionId: text("session_id").notNull(), // Better Auth session ID + + // Verification Tracking + verificationToken: text("verification_token").notNull().unique(), // Unique nonce for this attempt + codeUsed: text("code_used"), // The TOTP code submitted (prevent replay) + status: varchar("status", { length: 16 }).notNull(), // "pending" | "verified" | "failed" | "expired" + + attempts: integer("attempts").default(0).notNull(), + maxAttempts: integer("max_attempts").default(5).notNull(), + + verifiedAt: timestamp("verified_at"), + expiresAt: timestamp("expires_at").notNull(), + createdAt: timestamp("created_at").notNull(), + + // Security Audit + ipAddress: text("ip_address").default(""), + userAgent: text("user_agent").default(""), +}); + +export const twofaSessionsRelations = relations(twofaSessions, ({ one }) => ({ + userAccount: one(user, { + fields: [twofaSessions.userId], + references: [user.id], + }), +})); diff --git a/packages/db/schema/better.auth.schema.ts b/packages/db/schema/better.auth.schema.ts new file mode 100644 index 0000000..d162dc1 --- /dev/null +++ b/packages/db/schema/better.auth.schema.ts @@ -0,0 +1,75 @@ +import { boolean, index, pgTable, text, timestamp } from "drizzle-orm/pg-core"; +import { relations } from "drizzle-orm"; + +export const user = pgTable("user", { + id: text("id").primaryKey(), + name: text("name").notNull(), + email: text("email").notNull().unique(), + emailVerified: boolean("email_verified").default(false).notNull(), + image: text("image"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .defaultNow() + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + username: text("username").unique(), + displayUsername: text("display_username"), + role: text("role"), + banned: boolean("banned").default(false), + banReason: text("ban_reason"), + banExpires: timestamp("ban_expires"), + onboardingDone: boolean("onboarding_done").default(false), + last2FAVerifiedAt: timestamp("last2_fa_verified_at"), + parentId: text("parent_id"), +}); + +export const account = pgTable( + "account", + { + id: text("id").primaryKey(), + accountId: text("account_id").notNull(), + providerId: text("provider_id").notNull(), + userId: text("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + accessToken: text("access_token"), + refreshToken: text("refresh_token"), + idToken: text("id_token"), + accessTokenExpiresAt: timestamp("access_token_expires_at"), + refreshTokenExpiresAt: timestamp("refresh_token_expires_at"), + scope: text("scope"), + password: text("password"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + }, + (table) => [index("account_userId_idx").on(table.userId)], +); + +export const verification = pgTable( + "verification", + { + id: text("id").primaryKey(), + identifier: text("identifier").notNull(), + value: text("value").notNull(), + expiresAt: timestamp("expires_at").notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .defaultNow() + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + }, + (table) => [index("verification_identifier_idx").on(table.identifier)], +); + +export const userRelations = relations(user, ({ many }) => ({ + accounts: many(account), +})); + +export const accountRelations = relations(account, ({ one }) => ({ + user: one(user, { + fields: [account.userId], + references: [user.id], + }), +})); diff --git a/packages/db/schema/general.schema.ts b/packages/db/schema/general.schema.ts new file mode 100644 index 0000000..b8cf6ab --- /dev/null +++ b/packages/db/schema/general.schema.ts @@ -0,0 +1,49 @@ +import { + boolean, + json, + pgTable, + serial, + text, + timestamp, + varchar, +} from "drizzle-orm/pg-core"; +import { user } from "./better.auth.schema"; +import { relations } from "drizzle-orm"; + +export const notifications = pgTable("notifications", { + id: serial("id").primaryKey(), + + title: text("title").notNull(), + body: text("body").notNull(), + priority: varchar("priority", { length: 12 }).default("normal").notNull(), // "low", "normal", "high", "urgent" + + type: varchar("type", { length: 12 }).notNull(), + category: varchar("category", { length: 64 }), + + isRead: boolean("is_read").default(false).notNull(), + isArchived: boolean("is_archived").default(false).notNull(), + + actionUrl: text("action_url"), // URL to navigate to when clicked + actionType: varchar("action_type", { length: 16 }), // Type of action ("link", "function", etc.) + actionData: json("action_data"), // Any additional data for the action + + icon: varchar("icon", { length: 64 }), // Optional icon identifier + + userId: text("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + + // Lifecycle management + sentAt: timestamp("sent_at").notNull(), + readAt: timestamp("read_at"), + expiresAt: timestamp("expires_at"), + createdAt: timestamp("created_at").notNull(), + updatedAt: timestamp("updated_at").notNull(), +}); + +export const notificationsRelations = relations(notifications, ({ one }) => ({ + userAccount: one(user, { + fields: [notifications.userId], + references: [user.id], + }), +})); diff --git a/packages/db/schema/index.ts b/packages/db/schema/index.ts new file mode 100644 index 0000000..2171de4 --- /dev/null +++ b/packages/db/schema/index.ts @@ -0,0 +1,3 @@ +export * from "./auth.schema"; +export * from "./better.auth.schema"; +export * from "./general.schema"; diff --git a/packages/logger/client.ts b/packages/logger/client.ts new file mode 100644 index 0000000..b246456 --- /dev/null +++ b/packages/logger/client.ts @@ -0,0 +1,222 @@ +type LogLevel = "error" | "warn" | "info" | "http" | "debug"; + +export interface LogEntry { + level: LogLevel; + timestamp: string; + message: any; + metadata?: any; +} + +interface Error { + code: string; + message: string; + description?: string; + detail?: string; + error?: any; + actionable?: boolean; +} + +class BrowserLogger { + private queue: LogEntry[] = []; + private timer: ReturnType | null = null; + private readonly BATCH_INTERVAL = 5000; // 5 seconds + private readonly BATCH_SIZE_LIMIT = 50; + private readonly isDev: boolean; + + constructor(isDev: boolean) { + this.isDev = isDev; + if (!this.isDev) { + this.startBatchTimer(); + this.setupBeforeUnloadHandler(); + } + } + + private startBatchTimer() { + this.timer = setInterval(() => this.flush(), this.BATCH_INTERVAL); + } + + private setupBeforeUnloadHandler() { + // Flush logs before page unload to avoid losing them + if (typeof window !== "undefined") { + window.addEventListener("beforeunload", () => { + this.flushSync(); + }); + } + } + + private async flush() { + if (this.queue.length === 0) return; + + const batch = [...this.queue]; + this.queue = []; + + try { + // Forward batch to Hono route + await fetch("/api/logs", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ logs: batch }), + }); + } catch (err) { + console.error("Axiom batch upload failed", err); + // Re-add failed logs back to queue (up to a limit to avoid memory issues) + if (this.queue.length < this.BATCH_SIZE_LIMIT * 2) { + this.queue.push(...batch); + } + } + } + + private flushSync() { + // Synchronous flush for beforeunload using sendBeacon + if (this.queue.length === 0) return; + + const batch = [...this.queue]; + this.queue = []; + + try { + const blob = new Blob([JSON.stringify({ logs: batch })], { + type: "application/json", + }); + navigator.sendBeacon("/api/logs", blob); + } catch (err) { + console.error("Failed to send logs via sendBeacon", err); + } + } + + private serializeError(error: unknown): any { + if (error instanceof Error) { + return { + name: error.name, + message: error.message, + stack: error.stack, + ...(error.cause && { cause: this.serializeError(error.cause) }), + }; + } + return error; + } + + private createLogEntry( + level: LogLevel, + message: any, + metadata?: any, + ): LogEntry { + // Handle Error serialization for message + const cleanMessage = + message instanceof Error ? this.serializeError(message) : message; + + // Handle Error serialization for metadata + const cleanMetadata = + metadata instanceof Error + ? this.serializeError(metadata) + : metadata; + + return { + level, + timestamp: new Date().toISOString(), + message: cleanMessage, + metadata: cleanMetadata || {}, + }; + } + + private async sendLog(entry: LogEntry) { + // Always log errors to console, even in production (for user debugging) + // In dev, log everything to console + const shouldConsoleLog = this.isDev || entry.level === "error"; + + if (shouldConsoleLog) { + const consoleMethod = + entry.level === "http" ? "debug" : entry.level; + console[consoleMethod]( + `[client-${entry.level}] ${entry.timestamp}:`, + entry.message, + entry.metadata && Object.keys(entry.metadata).length > 0 + ? entry.metadata + : "", + ); + } + + // In production, add to queue for batching + if (!this.isDev) { + this.queue.push(entry); + + // Safety flush if queue gets too large + if (this.queue.length >= this.BATCH_SIZE_LIMIT) { + await this.flush(); + } + } + } + + error(message: any, metadata?: any) { + this.sendLog(this.createLogEntry("error", message, metadata)); + } + + warn(message: any, metadata?: any) { + this.sendLog(this.createLogEntry("warn", message, metadata)); + } + + info(message: any, metadata?: any) { + this.sendLog(this.createLogEntry("info", message, metadata)); + } + + http(message: any, metadata?: any) { + this.sendLog(this.createLogEntry("http", message, metadata)); + } + + debug(message: any, metadata?: any) { + this.sendLog(this.createLogEntry("debug", message, metadata)); + } + + // Manual flush method for advanced use cases + async forceFlush() { + await this.flush(); + } + + // Cleanup method + destroy() { + if (this.timer) { + clearInterval(this.timer); + this.timer = null; + } + this.flushSync(); + } +} + +/** + * Factory function to create a BrowserLogger instance + * + * @param isDev - Whether the app is running in development mode + * @returns A new BrowserLogger instance + * + * @example + * // SvelteKit + * import { dev } from '$app/environment'; + * const logger = getLoggerInstance(dev); + * + * @example + * // Next.js + * const logger = getLoggerInstance(process.env.NODE_ENV === 'development'); + * + * @example + * // Vite + * const logger = getLoggerInstance(import.meta.env.DEV); + */ +function getLoggerInstance(isDev: boolean): BrowserLogger { + return new BrowserLogger(isDev); +} + +function getError(logger: BrowserLogger, payload: Error, error?: any) { + logger.error(payload); + if (error) { + logger.error(error); + } + return { + code: payload.code, + message: payload.message, + description: payload.description, + detail: payload.detail, + error: error, + actionable: payload.actionable, + } as Error; +} + +export { getError, getLoggerInstance }; diff --git a/packages/logger/index.ts b/packages/logger/index.ts new file mode 100644 index 0000000..6a07c24 --- /dev/null +++ b/packages/logger/index.ts @@ -0,0 +1,143 @@ +import DailyRotateFile from "winston-daily-rotate-file"; +import { settings } from "@pkg/settings"; +import type { Err } from "@pkg/result"; +import winston from "winston"; +import util from "util"; +import path from "path"; + +process.on("warning", (warning) => { + const msg = String(warning?.message || ""); + const name = String((warning as any)?.name || ""); + + // Ignore the noisy timer warning from Node/kafkajs interplay + if ( + name === "TimeoutNegativeWarning" || + msg.includes("TimeoutNegativeWarning") || + msg.includes("Timeout duration was set to 1") + ) { + return; + } + + // Keep other warnings visible + console.warn(warning); +}); + +const levels = { + error: 0, + warn: 1, + info: 2, + http: 3, + debug: 4, +}; + +const colors = { + error: "red", + warn: "yellow", + info: "green", + http: "magenta", + debug: "white", +}; + +const level = () => { + const envLevel = process.env.LOG_LEVEL?.toLowerCase(); + if (envLevel && envLevel in levels) { + return envLevel; + } + return settings.isDevelopment ? "debug" : "warn"; +}; + +// Console format with colors +const consoleFormat = winston.format.combine( + winston.format.errors({ stack: true }), + winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss:ms" }), + winston.format.colorize({ all: true }), + winston.format.printf((info) => { + const { level, message, timestamp, ...extra } = info; + + let formattedMessage = ""; + if (message instanceof Error) { + formattedMessage = message.stack || message.message; + } else if (typeof message === "object") { + formattedMessage = util.inspect(message, { + depth: null, + colors: true, + }); + } else { + formattedMessage = message as any as string; + } + + // Handle extra fields (if any) + const formattedExtra = + Object.keys(extra).length > 0 + ? `\n${util.inspect(extra, { depth: null, colors: true })}` + : ""; + + return `[${level}] ${timestamp}: ${formattedMessage}${formattedExtra}`; + }), +); + +// JSON format for file logging +const fileFormat = winston.format.combine( + winston.format.errors({ stack: true }), + winston.format.timestamp(), + winston.format.json(), +); + +// File transport with daily rotation +const fileTransport = new DailyRotateFile({ + filename: path.join("logs", "app-%DATE%.log"), + datePattern: "YYYY-MM-DD", + maxSize: "20m", + maxFiles: "14d", + format: fileFormat, +}); + +// Error file transport with daily rotation +const errorFileTransport = new DailyRotateFile({ + filename: path.join("logs", "error-%DATE%.log"), + datePattern: "YYYY-MM-DD", + maxSize: "20m", + maxFiles: "14d", + level: "error", + format: fileFormat, +}); + +const transports: winston.transport[] = [ + new winston.transports.Console({ format: consoleFormat }), + fileTransport, + errorFileTransport, +]; + +winston.addColors(colors); + +const logger = winston.createLogger({ + level: level(), + levels, + transports, + format: fileFormat, + exceptionHandlers: [ + new winston.transports.Console({ format: consoleFormat }), + fileTransport, + ], + rejectionHandlers: [ + new winston.transports.Console({ format: consoleFormat }), + fileTransport, + ], +}); + +const stream = { write: (message: string) => logger.http(message.trim()) }; + +function getError(payload: Err, error?: any) { + logger.error(JSON.stringify({ payload, error }, null, 2)); + console.error(error); + return { + code: payload.code, + message: payload.message, + description: payload.description, + detail: payload.detail, + error: error instanceof Error ? error.message : error, + actionable: payload.actionable, + } as Err; +} + +export { getError, logger, stream }; diff --git a/packages/logger/package.json b/packages/logger/package.json new file mode 100644 index 0000000..d9bdcbe --- /dev/null +++ b/packages/logger/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pkg/logger", + "module": "index.ts", + "type": "module", + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "@axiomhq/winston": "^1.3.1", + "@pkg/result": "workspace:*", + "@pkg/settings": "workspace:*", + "winston": "^3.17.0", + "winston-daily-rotate-file": "^5.0.0" + } +} diff --git a/packages/logger/tsconfig.json b/packages/logger/tsconfig.json new file mode 100644 index 0000000..0f8d8dd --- /dev/null +++ b/packages/logger/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + }, +} diff --git a/packages/logic/core/array.utils.ts b/packages/logic/core/array.utils.ts new file mode 100644 index 0000000..237c4a5 --- /dev/null +++ b/packages/logic/core/array.utils.ts @@ -0,0 +1,7 @@ +export function chunk(arr: T[], size: number): T[][] { + const result = []; + for (let i = 0; i < arr.length; i += size) { + result.push(arr.slice(i, i + size)); + } + return result; +} diff --git a/packages/logic/core/data/countries.ts b/packages/logic/core/data/countries.ts new file mode 100644 index 0000000..be0a9e0 --- /dev/null +++ b/packages/logic/core/data/countries.ts @@ -0,0 +1,264 @@ +export const COUNTRIES = [ + { id: "1", name: "Afghanistan", code: "AF" }, + { id: "2", name: "Albania", code: "AL" }, + { id: "3", name: "Algeria", code: "DZ" }, + { id: "4", name: "American Samoa", code: "AS" }, + { id: "5", name: "Andorra", code: "AD" }, + { id: "6", name: "Angola", code: "AO" }, + { id: "7", name: "Anguilla", code: "AI" }, + { id: "8", name: "Antarctica", code: "AQ" }, + { id: "9", name: "Antigua and Barbuda", code: "AG" }, + { id: "10", name: "Argentina", code: "AR" }, + { id: "11", name: "Armenia", code: "AM" }, + { id: "12", name: "Aruba", code: "AW" }, + { id: "13", name: "Australia", code: "AU" }, + { id: "14", name: "Austria", code: "AT" }, + { id: "15", name: "Azerbaijan", code: "AZ" }, + { id: "16", name: "Bahamas", code: "BS" }, + { id: "17", name: "Bahrain", code: "BH" }, + { id: "18", name: "Bangladesh", code: "BD" }, + { id: "19", name: "Barbados", code: "BB" }, + { id: "20", name: "Belarus", code: "BY" }, + { id: "21", name: "Belgium", code: "BE" }, + { id: "22", name: "Belize", code: "BZ" }, + { id: "23", name: "Benin", code: "BJ" }, + { id: "24", name: "Bermuda", code: "BM" }, + { id: "25", name: "Bhutan", code: "BT" }, + { id: "26", name: "Bolivia", code: "BO" }, + { id: "27", name: "Bosnia and Herzegovina", code: "BA" }, + { id: "28", name: "Botswana", code: "BW" }, + { id: "29", name: "Bouvet Island", code: "BV" }, + { id: "30", name: "Brazil", code: "BR" }, + { id: "31", name: "British Indian Ocean Territory", code: "IO" }, + { id: "32", name: "British Virgin Islands", code: "VG" }, + { id: "33", name: "Brunei", code: "BN" }, + { id: "34", name: "Bulgaria", code: "BG" }, + { id: "35", name: "Burkina Faso", code: "BF" }, + { id: "36", name: "Burundi", code: "BI" }, + { id: "37", name: "Cambodia", code: "KH" }, + { id: "38", name: "Cameroon", code: "CM" }, + { id: "39", name: "Canada", code: "CA" }, + { id: "40", name: "Cape Verde", code: "CV" }, + { id: "41", name: "Caribbean Netherlands", code: "BQ" }, + { id: "42", name: "Cayman Islands", code: "KY" }, + { id: "43", name: "Central African Republic", code: "CF" }, + { id: "44", name: "Chad", code: "TD" }, + { id: "45", name: "Chile", code: "CL" }, + { id: "46", name: "China", code: "CN" }, + { id: "47", name: "Christmas Island", code: "CX" }, + { id: "48", name: "Cocos (Keeling) Islands", code: "CC" }, + { id: "49", name: "Colombia", code: "CO" }, + { id: "50", name: "Comoros", code: "KM" }, + { id: "51", name: "Cook Islands", code: "CK" }, + { id: "52", name: "Costa Rica", code: "CR" }, + { id: "53", name: "Croatia", code: "HR" }, + { id: "54", name: "Cuba", code: "CU" }, + { id: "55", name: "Curaçao", code: "CW" }, + { id: "56", name: "Cyprus", code: "CY" }, + { id: "57", name: "Czechia", code: "CZ" }, + { id: "58", name: "DR Congo", code: "CD" }, + { id: "59", name: "Denmark", code: "DK" }, + { id: "60", name: "Djibouti", code: "DJ" }, + { id: "61", name: "Dominica", code: "DM" }, + { id: "62", name: "Dominican Republic", code: "DO" }, + { id: "63", name: "Ecuador", code: "EC" }, + { id: "64", name: "Egypt", code: "EG" }, + { id: "65", name: "El Salvador", code: "SV" }, + { id: "66", name: "Equatorial Guinea", code: "GQ" }, + { id: "67", name: "Eritrea", code: "ER" }, + { id: "68", name: "Estonia", code: "EE" }, + { id: "69", name: "Eswatini", code: "SZ" }, + { id: "70", name: "Ethiopia", code: "ET" }, + { id: "71", name: "Falkland Islands", code: "FK" }, + { id: "72", name: "Faroe Islands", code: "FO" }, + { id: "73", name: "Fiji", code: "FJ" }, + { id: "74", name: "Finland", code: "FI" }, + { id: "75", name: "France", code: "FR" }, + { id: "76", name: "French Guiana", code: "GF" }, + { id: "77", name: "French Polynesia", code: "PF" }, + { id: "78", name: "French Southern and Antarctic Lands", code: "TF" }, + { id: "79", name: "Gabon", code: "GA" }, + { id: "80", name: "Gambia", code: "GM" }, + { id: "81", name: "Georgia", code: "GE" }, + { id: "82", name: "Germany", code: "DE" }, + { id: "83", name: "Ghana", code: "GH" }, + { id: "84", name: "Gibraltar", code: "GI" }, + { id: "85", name: "Greece", code: "GR" }, + { id: "86", name: "Greenland", code: "GL" }, + { id: "87", name: "Grenada", code: "GD" }, + { id: "88", name: "Guadeloupe", code: "GP" }, + { id: "89", name: "Guam", code: "GU" }, + { id: "90", name: "Guatemala", code: "GT" }, + { id: "91", name: "Guernsey", code: "GG" }, + { id: "92", name: "Guinea", code: "GN" }, + { id: "93", name: "Guinea-Bissau", code: "GW" }, + { id: "94", name: "Guyana", code: "GY" }, + { id: "95", name: "Haiti", code: "HT" }, + { id: "96", name: "Heard Island and McDonald Islands", code: "HM" }, + { id: "97", name: "Honduras", code: "HN" }, + { id: "98", name: "Hong Kong", code: "HK" }, + { id: "99", name: "Hungary", code: "HU" }, + { id: "100", name: "Iceland", code: "IS" }, + { id: "101", name: "India", code: "IN" }, + { id: "102", name: "Indonesia", code: "ID" }, + { id: "103", name: "Iran", code: "IR" }, + { id: "104", name: "Iraq", code: "IQ" }, + { id: "105", name: "Ireland", code: "IE" }, + { id: "106", name: "Isle of Man", code: "IM" }, + { id: "107", name: "Israel", code: "IL" }, + { id: "108", name: "Italy", code: "IT" }, + { id: "109", name: "Ivory Coast", code: "CI" }, + { id: "110", name: "Jamaica", code: "JM" }, + { id: "111", name: "Japan", code: "JP" }, + { id: "112", name: "Jersey", code: "JE" }, + { id: "113", name: "Jordan", code: "JO" }, + { id: "114", name: "Kazakhstan", code: "KZ" }, + { id: "115", name: "Kenya", code: "KE" }, + { id: "116", name: "Kiribati", code: "KI" }, + { id: "117", name: "Kosovo", code: "XK" }, + { id: "118", name: "Kuwait", code: "KW" }, + { id: "119", name: "Kyrgyzstan", code: "KG" }, + { id: "120", name: "Laos", code: "LA" }, + { id: "121", name: "Latvia", code: "LV" }, + { id: "122", name: "Lebanon", code: "LB" }, + { id: "123", name: "Lesotho", code: "LS" }, + { id: "124", name: "Liberia", code: "LR" }, + { id: "125", name: "Libya", code: "LY" }, + { id: "126", name: "Liechtenstein", code: "LI" }, + { id: "127", name: "Lithuania", code: "LT" }, + { id: "128", name: "Luxembourg", code: "LU" }, + { id: "129", name: "Macau", code: "MO" }, + { id: "130", name: "Madagascar", code: "MG" }, + { id: "131", name: "Malawi", code: "MW" }, + { id: "132", name: "Malaysia", code: "MY" }, + { id: "133", name: "Maldives", code: "MV" }, + { id: "134", name: "Mali", code: "ML" }, + { id: "135", name: "Malta", code: "MT" }, + { id: "136", name: "Marshall Islands", code: "MH" }, + { id: "137", name: "Martinique", code: "MQ" }, + { id: "138", name: "Mauritania", code: "MR" }, + { id: "139", name: "Mauritius", code: "MU" }, + { id: "140", name: "Mayotte", code: "YT" }, + { id: "141", name: "Mexico", code: "MX" }, + { id: "142", name: "Micronesia", code: "FM" }, + { id: "143", name: "Moldova", code: "MD" }, + { id: "144", name: "Monaco", code: "MC" }, + { id: "145", name: "Mongolia", code: "MN" }, + { id: "146", name: "Montenegro", code: "ME" }, + { id: "147", name: "Montserrat", code: "MS" }, + { id: "148", name: "Morocco", code: "MA" }, + { id: "149", name: "Mozambique", code: "MZ" }, + { id: "150", name: "Myanmar", code: "MM" }, + { id: "151", name: "Namibia", code: "NA" }, + { id: "152", name: "Nauru", code: "NR" }, + { id: "153", name: "Nepal", code: "NP" }, + { id: "154", name: "Netherlands", code: "NL" }, + { id: "155", name: "New Caledonia", code: "NC" }, + { id: "156", name: "New Zealand", code: "NZ" }, + { id: "157", name: "Nicaragua", code: "NI" }, + { id: "158", name: "Niger", code: "NE" }, + { id: "159", name: "Nigeria", code: "NG" }, + { id: "160", name: "Niue", code: "NU" }, + { id: "161", name: "Norfolk Island", code: "NF" }, + { id: "162", name: "North Korea", code: "KP" }, + { id: "163", name: "North Macedonia", code: "MK" }, + { id: "164", name: "Northern Mariana Islands", code: "MP" }, + { id: "165", name: "Norway", code: "NO" }, + { id: "166", name: "Oman", code: "OM" }, + { id: "167", name: "Pakistan", code: "PK" }, + { id: "168", name: "Palau", code: "PW" }, + { id: "169", name: "Palestine", code: "PS" }, + { id: "170", name: "Panama", code: "PA" }, + { id: "171", name: "Papua New Guinea", code: "PG" }, + { id: "172", name: "Paraguay", code: "PY" }, + { id: "173", name: "Peru", code: "PE" }, + { id: "174", name: "Philippines", code: "PH" }, + { id: "175", name: "Pitcairn Islands", code: "PN" }, + { id: "176", name: "Poland", code: "PL" }, + { id: "177", name: "Portugal", code: "PT" }, + { id: "178", name: "Puerto Rico", code: "PR" }, + { id: "179", name: "Qatar", code: "QA" }, + { id: "180", name: "Republic of the Congo", code: "CG" }, + { id: "181", name: "Romania", code: "RO" }, + { id: "182", name: "Russia", code: "RU" }, + { id: "183", name: "Rwanda", code: "RW" }, + { id: "184", name: "Réunion", code: "RE" }, + { id: "185", name: "Saint Barthélemy", code: "BL" }, + { + id: "186", + name: "Saint Helena, Ascension and Tristan da Cunha", + code: "SH", + }, + { id: "187", name: "Saint Kitts and Nevis", code: "KN" }, + { id: "188", name: "Saint Lucia", code: "LC" }, + { id: "189", name: "Saint Martin", code: "MF" }, + { id: "190", name: "Saint Pierre and Miquelon", code: "PM" }, + { id: "191", name: "Saint Vincent and the Grenadines", code: "VC" }, + { id: "192", name: "Samoa", code: "WS" }, + { id: "193", name: "San Marino", code: "SM" }, + { id: "194", name: "Saudi Arabia", code: "SA" }, + { id: "195", name: "Senegal", code: "SN" }, + { id: "196", name: "Serbia", code: "RS" }, + { id: "197", name: "Seychelles", code: "SC" }, + { id: "198", name: "Sierra Leone", code: "SL" }, + { id: "199", name: "Singapore", code: "SG" }, + { id: "200", name: "Sint Maarten", code: "SX" }, + { id: "201", name: "Slovakia", code: "SK" }, + { id: "202", name: "Slovenia", code: "SI" }, + { id: "203", name: "Solomon Islands", code: "SB" }, + { id: "204", name: "Somalia", code: "SO" }, + { id: "205", name: "South Africa", code: "ZA" }, + { id: "206", name: "South Georgia", code: "GS" }, + { id: "207", name: "South Korea", code: "KR" }, + { id: "208", name: "South Sudan", code: "SS" }, + { id: "209", name: "Spain", code: "ES" }, + { id: "210", name: "Sri Lanka", code: "LK" }, + { id: "211", name: "Sudan", code: "SD" }, + { id: "212", name: "Suriname", code: "SR" }, + { id: "213", name: "Svalbard and Jan Mayen", code: "SJ" }, + { id: "214", name: "Sweden", code: "SE" }, + { id: "215", name: "Switzerland", code: "CH" }, + { id: "216", name: "Syria", code: "SY" }, + { id: "217", name: "São Tomé and Príncipe", code: "ST" }, + { id: "218", name: "Taiwan", code: "TW" }, + { id: "219", name: "Tajikistan", code: "TJ" }, + { id: "220", name: "Tanzania", code: "TZ" }, + { id: "221", name: "Thailand", code: "TH" }, + { id: "222", name: "Timor-Leste", code: "TL" }, + { id: "223", name: "Togo", code: "TG" }, + { id: "224", name: "Tokelau", code: "TK" }, + { id: "225", name: "Tonga", code: "TO" }, + { id: "226", name: "Trinidad and Tobago", code: "TT" }, + { id: "227", name: "Tunisia", code: "TN" }, + { id: "228", name: "Turkey", code: "TR" }, + { id: "229", name: "Turkmenistan", code: "TM" }, + { id: "230", name: "Turks and Caicos Islands", code: "TC" }, + { id: "231", name: "Tuvalu", code: "TV" }, + { id: "232", name: "Uganda", code: "UG" }, + { id: "233", name: "Ukraine", code: "UA" }, + { id: "234", name: "United Arab Emirates", code: "AE" }, + { id: "235", name: "United Kingdom", code: "GB" }, + { id: "236", name: "United States", code: "US" }, + { id: "237", name: "United States Minor Outlying Islands", code: "UM" }, + { id: "238", name: "United States Virgin Islands", code: "VI" }, + { id: "239", name: "Uruguay", code: "UY" }, + { id: "240", name: "Uzbekistan", code: "UZ" }, + { id: "241", name: "Vanuatu", code: "VU" }, + { id: "242", name: "Vatican City", code: "VA" }, + { id: "243", name: "Venezuela", code: "VE" }, + { id: "244", name: "Vietnam", code: "VN" }, + { id: "245", name: "Wallis and Futuna", code: "WF" }, + { id: "246", name: "Western Sahara", code: "EH" }, + { id: "247", name: "Yemen", code: "YE" }, + { id: "248", name: "Zambia", code: "ZM" }, + { id: "249", name: "Zimbabwe", code: "ZW" }, + { id: "250", name: "Åland Islands", code: "AX" }, +]; + +export const COUNTRIES_SELECT = COUNTRIES.map((c) => { + return { + id: c.id, + label: `${c.code} (${c.name})`, + value: c.name.toLowerCase(), + }; +}); diff --git a/packages/logic/core/data/phonecc.ts b/packages/logic/core/data/phonecc.ts new file mode 100644 index 0000000..87d4c86 --- /dev/null +++ b/packages/logic/core/data/phonecc.ts @@ -0,0 +1,1227 @@ +export const PHONE_COUNTRY_CODES = [ + { + countryCode: "af", + country: "Afghanistan", + phoneCode: "+93", + }, + { + countryCode: "ax", + country: "Åland Islands", + phoneCode: "+358", + }, + { + countryCode: "al", + country: "Albania", + phoneCode: "+355", + }, + { + countryCode: "dz", + country: "Algeria", + phoneCode: "+213", + }, + { + countryCode: "as", + country: "American Samoa", + phoneCode: "+1", + }, + { + countryCode: "ad", + country: "Andorra", + phoneCode: "+376", + }, + { + countryCode: "ao", + country: "Angola", + phoneCode: "+244", + }, + { + countryCode: "ai", + country: "Anguilla", + phoneCode: "+1", + }, + { + countryCode: "aq", + country: "Antarctica", + phoneCode: "+672", + }, + { + countryCode: "ag", + country: "Antigua and Barbuda", + phoneCode: "+1", + }, + { + countryCode: "ar", + country: "Argentina", + phoneCode: "+54", + }, + { + countryCode: "am", + country: "Armenia", + phoneCode: "+374", + }, + { + countryCode: "aw", + country: "Aruba", + phoneCode: "+297", + }, + { + countryCode: "au", + country: "Australia", + phoneCode: "+61", + }, + { + countryCode: "at", + country: "Austria", + phoneCode: "+43", + }, + { + countryCode: "az", + country: "Azerbaijan", + phoneCode: "+994", + }, + { + countryCode: "bs", + country: "Bahamas", + phoneCode: "+1", + }, + { + countryCode: "bh", + country: "Bahrain", + phoneCode: "+973", + }, + { + countryCode: "bd", + country: "Bangladesh", + phoneCode: "+880", + }, + { + countryCode: "bb", + country: "Barbados", + phoneCode: "+1", + }, + { + countryCode: "by", + country: "Belarus", + phoneCode: "+375", + }, + { + countryCode: "be", + country: "Belgium", + phoneCode: "+32", + }, + { + countryCode: "bz", + country: "Belize", + phoneCode: "+501", + }, + { + countryCode: "bj", + country: "Benin", + phoneCode: "+229", + }, + { + countryCode: "bm", + country: "Bermuda", + phoneCode: "+1", + }, + { + countryCode: "bt", + country: "Bhutan", + phoneCode: "+975", + }, + { + countryCode: "bo", + country: "Bolivia", + phoneCode: "+591", + }, + { + countryCode: "ba", + country: "Bosnia and Herzegovina", + phoneCode: "+387", + }, + { + countryCode: "bw", + country: "Botswana", + phoneCode: "+267", + }, + { + countryCode: "br", + country: "Brazil", + phoneCode: "+55", + }, + { + countryCode: "io", + country: "British Indian Ocean Territory", + phoneCode: "+246", + }, + { + countryCode: "vg", + country: "British Virgin Islands", + phoneCode: "+1", + }, + { + countryCode: "bn", + country: "Brunei", + phoneCode: "+673", + }, + { + countryCode: "bg", + country: "Bulgaria", + phoneCode: "+359", + }, + { + countryCode: "bf", + country: "Burkina Faso", + phoneCode: "+226", + }, + { + countryCode: "bi", + country: "Burundi", + phoneCode: "+257", + }, + { + countryCode: "kh", + country: "Cambodia", + phoneCode: "+855", + }, + { + countryCode: "cm", + country: "Cameroon", + phoneCode: "+237", + }, + { + countryCode: "ca", + country: "Canada", + phoneCode: "+1", + }, + { + countryCode: "cv", + country: "Cape Verde", + phoneCode: "+238", + }, + { + countryCode: "ky", + country: "Cayman Islands", + phoneCode: "+1", + }, + { + countryCode: "cf", + country: "Central African Republic", + phoneCode: "+236", + }, + { + countryCode: "td", + country: "Chad", + phoneCode: "+235", + }, + { + countryCode: "cl", + country: "Chile", + phoneCode: "+56", + }, + { + countryCode: "cn", + country: "China", + phoneCode: "+86", + }, + { + countryCode: "cx", + country: "Christmas Island", + phoneCode: "+61", + }, + { + countryCode: "cc", + country: "Cocos [Keeling] Islands", + phoneCode: "+61", + }, + { + countryCode: "co", + country: "Colombia", + phoneCode: "+57", + }, + { + countryCode: "km", + country: "Comoros", + phoneCode: "+269", + }, + { + countryCode: "cg", + country: "Congo - Brazzaville", + phoneCode: "+242", + }, + { + countryCode: "cd", + country: "Congo - Kinshasa", + phoneCode: "+243", + }, + { + countryCode: "ck", + country: "Cook Islands", + phoneCode: "+682", + }, + { + countryCode: "cr", + country: "Costa Rica", + phoneCode: "+506", + }, + { + countryCode: "ci", + country: "Côte d’Ivoire", + phoneCode: "+225", + }, + { + countryCode: "hr", + country: "Croatia", + phoneCode: "+385", + }, + { + countryCode: "cy", + country: "Cyprus", + phoneCode: "+357", + }, + { + countryCode: "cz", + country: "Czech Republic", + phoneCode: "+420", + }, + { + countryCode: "dk", + country: "Denmark", + phoneCode: "+45", + }, + { + countryCode: "dj", + country: "Djibouti", + phoneCode: "+253", + }, + { + countryCode: "dm", + country: "Dominica", + phoneCode: "+1", + }, + { + countryCode: "do", + country: "Dominican Republic", + phoneCode: "+1", + }, + { + countryCode: "ec", + country: "Ecuador", + phoneCode: "+593", + }, + { + countryCode: "eg", + country: "Egypt", + phoneCode: "+20", + }, + { + countryCode: "sv", + country: "El Salvador", + phoneCode: "+503", + }, + { + countryCode: "gq", + country: "Equatorial Guinea", + phoneCode: "+240", + }, + { + countryCode: "er", + country: "Eritrea", + phoneCode: "+291", + }, + { + countryCode: "ee", + country: "Estonia", + phoneCode: "+372", + }, + { + countryCode: "et", + country: "Ethiopia", + phoneCode: "+251", + }, + { + countryCode: "fk", + country: "Falkland Islands", + phoneCode: "+500", + }, + { + countryCode: "fo", + country: "Faroe Islands", + phoneCode: "+298", + }, + { + countryCode: "fj", + country: "Fiji", + phoneCode: "+679", + }, + { + countryCode: "fi", + country: "Finland", + phoneCode: "+358", + }, + { + countryCode: "fr", + country: "France", + phoneCode: "+33", + }, + { + countryCode: "gf", + country: "French Guiana", + phoneCode: "+594", + }, + { + countryCode: "pf", + country: "French Polynesia", + phoneCode: "+689", + }, + { + countryCode: "tf", + country: "French Southern Territories", + phoneCode: "+262", + }, + { + countryCode: "ga", + country: "Gabon", + phoneCode: "+241", + }, + { + countryCode: "gm", + country: "Gambia", + phoneCode: "+220", + }, + { + countryCode: "ge", + country: "Georgia", + phoneCode: "+995", + }, + { + countryCode: "de", + country: "Germany", + phoneCode: "+49", + }, + { + countryCode: "gh", + country: "Ghana", + phoneCode: "+233", + }, + { + countryCode: "gi", + country: "Gibraltar", + phoneCode: "+350", + }, + { + countryCode: "gr", + country: "Greece", + phoneCode: "+30", + }, + { + countryCode: "gl", + country: "Greenland", + phoneCode: "+299", + }, + { + countryCode: "gd", + country: "Grenada", + phoneCode: "+1", + }, + { + countryCode: "gp", + country: "Guadeloupe", + phoneCode: "+590", + }, + { + countryCode: "gu", + country: "Guam", + phoneCode: "+1", + }, + { + countryCode: "gt", + country: "Guatemala", + phoneCode: "+502", + }, + { + countryCode: "gg", + country: "Guernsey", + phoneCode: "+44", + }, + { + countryCode: "gn", + country: "Guinea", + phoneCode: "+224", + }, + { + countryCode: "gw", + country: "Guinea-Bissau", + phoneCode: "+245", + }, + { + countryCode: "gy", + country: "Guyana", + phoneCode: "+592", + }, + { + countryCode: "ht", + country: "Haiti", + phoneCode: "+509", + }, + { + countryCode: "hn", + country: "Honduras", + phoneCode: "+504", + }, + { + countryCode: "hk", + country: "Hong Kong SAR China", + phoneCode: "+852", + }, + { + countryCode: "hu", + country: "Hungary", + phoneCode: "+36", + }, + { + countryCode: "is", + country: "Iceland", + phoneCode: "+354", + }, + { + countryCode: "in", + country: "India", + phoneCode: "+91", + }, + { + countryCode: "id", + country: "Indonesia", + phoneCode: "+62", + }, + { + countryCode: "ir", + country: "Iran", + phoneCode: "+98", + }, + { + countryCode: "iq", + country: "Iraq", + phoneCode: "+964", + }, + { + countryCode: "ie", + country: "Ireland", + phoneCode: "+353", + }, + { + countryCode: "im", + country: "Isle of Man", + phoneCode: "+44", + }, + { + countryCode: "il", + country: "Israel", + phoneCode: "+972", + }, + { + countryCode: "it", + country: "Italy", + phoneCode: "+39", + }, + { + countryCode: "jm", + country: "Jamaica", + phoneCode: "+1", + }, + { + countryCode: "jp", + country: "Japan", + phoneCode: "+81", + }, + { + countryCode: "je", + country: "Jersey", + phoneCode: "+44", + }, + { + countryCode: "jo", + country: "Jordan", + phoneCode: "+962", + }, + { + countryCode: "kz", + country: "Kazakhstan", + phoneCode: "+7", + }, + { + countryCode: "ke", + country: "Kenya", + phoneCode: "+254", + }, + { + countryCode: "ki", + country: "Kiribati", + phoneCode: "+686", + }, + { + countryCode: "xk", + country: "Kosovo", + phoneCode: "+383", + }, + { + countryCode: "kw", + country: "Kuwait", + phoneCode: "+965", + }, + { + countryCode: "kg", + country: "Kyrgyzstan", + phoneCode: "+996", + }, + { + countryCode: "la", + country: "Laos", + phoneCode: "+856", + }, + { + countryCode: "lv", + country: "Latvia", + phoneCode: "+371", + }, + { + countryCode: "lb", + country: "Lebanon", + phoneCode: "+961", + }, + { + countryCode: "ls", + country: "Lesotho", + phoneCode: "+266", + }, + { + countryCode: "lr", + country: "Liberia", + phoneCode: "+231", + }, + { + countryCode: "ly", + country: "Libya", + phoneCode: "+218", + }, + { + countryCode: "li", + country: "Liechtenstein", + phoneCode: "+423", + }, + { + countryCode: "lt", + country: "Lithuania", + phoneCode: "+370", + }, + { + countryCode: "lu", + country: "Luxembourg", + phoneCode: "+352", + }, + { + countryCode: "mo", + country: "Macau SAR China", + phoneCode: "+853", + }, + { + countryCode: "mk", + country: "Macedonia", + phoneCode: "+389", + }, + { + countryCode: "mg", + country: "Madagascar", + phoneCode: "+261", + }, + { + countryCode: "mw", + country: "Malawi", + phoneCode: "+265", + }, + { + countryCode: "my", + country: "Malaysia", + phoneCode: "+60", + }, + { + countryCode: "mv", + country: "Maldives", + phoneCode: "+960", + }, + { + countryCode: "ml", + country: "Mali", + phoneCode: "+223", + }, + { + countryCode: "mt", + country: "Malta", + phoneCode: "+356", + }, + { + countryCode: "mh", + country: "Marshall Islands", + phoneCode: "+692", + }, + { + countryCode: "mq", + country: "Martinique", + phoneCode: "+596", + }, + { + countryCode: "mr", + country: "Mauritania", + phoneCode: "+222", + }, + { + countryCode: "mu", + country: "Mauritius", + phoneCode: "+230", + }, + { + countryCode: "yt", + country: "Mayotte", + phoneCode: "+262", + }, + { + countryCode: "mx", + country: "Mexico", + phoneCode: "+52", + }, + { + countryCode: "fm", + country: "Micronesia", + phoneCode: "+691", + }, + { + countryCode: "md", + country: "Moldova", + phoneCode: "+373", + }, + { + countryCode: "mc", + country: "Monaco", + phoneCode: "+377", + }, + { + countryCode: "mn", + country: "Mongolia", + phoneCode: "+976", + }, + { + countryCode: "me", + country: "Montenegro", + phoneCode: "+382", + }, + { + countryCode: "ms", + country: "Montserrat", + phoneCode: "+1", + }, + { + countryCode: "ma", + country: "Morocco", + phoneCode: "+212", + }, + { + countryCode: "mz", + country: "Mozambique", + phoneCode: "+258", + }, + { + countryCode: "mm", + country: "Myanmar [Burma]", + phoneCode: "+95", + }, + { + countryCode: "na", + country: "Namibia", + phoneCode: "+264", + }, + { + countryCode: "nr", + country: "Nauru", + phoneCode: "+674", + }, + { + countryCode: "np", + country: "Nepal", + phoneCode: "+977", + }, + { + countryCode: "nl", + country: "Netherlands", + phoneCode: "+31", + }, + { + countryCode: "an", + country: "Netherlands Antilles", + phoneCode: "+599", + }, + { + countryCode: "nc", + country: "New Caledonia", + phoneCode: "+687", + }, + { + countryCode: "nz", + country: "New Zealand", + phoneCode: "+64", + }, + { + countryCode: "ni", + country: "Nicaragua", + phoneCode: "+505", + }, + { + countryCode: "ne", + country: "Niger", + phoneCode: "+227", + }, + { + countryCode: "ng", + country: "Nigeria", + phoneCode: "+234", + }, + { + countryCode: "nu", + country: "Niue", + phoneCode: "+683", + }, + { + countryCode: "nf", + country: "Norfolk Island", + phoneCode: "+672", + }, + { + countryCode: "kp", + country: "North Korea", + phoneCode: "+850", + }, + { + countryCode: "mp", + country: "Northern Mariana Islands", + phoneCode: "+1", + }, + { + countryCode: "no", + country: "Norway", + phoneCode: "+47", + }, + { + countryCode: "om", + country: "Oman", + phoneCode: "+968", + }, + { + countryCode: "pk", + country: "Pakistan", + phoneCode: "+92", + }, + { + countryCode: "pw", + country: "Palau", + phoneCode: "+680", + }, + { + countryCode: "ps", + country: "Palestinian Territories", + phoneCode: "+970", + }, + { + countryCode: "pa", + country: "Panama", + phoneCode: "+507", + }, + { + countryCode: "pg", + country: "Papua New Guinea", + phoneCode: "+675", + }, + { + countryCode: "py", + country: "Paraguay", + phoneCode: "+595", + }, + { + countryCode: "pe", + country: "Peru", + phoneCode: "+51", + }, + { + countryCode: "ph", + country: "Philippines", + phoneCode: "+63", + }, + { + countryCode: "pn", + country: "Pitcairn Islands", + phoneCode: "+64", + }, + { + countryCode: "pl", + country: "Poland", + phoneCode: "+48", + }, + { + countryCode: "pt", + country: "Portugal", + phoneCode: "+351", + }, + { + countryCode: "pr", + country: "Puerto Rico", + phoneCode: "+1", + }, + { + countryCode: "qa", + country: "Qatar", + phoneCode: "+974", + }, + { + countryCode: "re", + country: "Réunion", + phoneCode: "+262", + }, + { + countryCode: "ro", + country: "Romania", + phoneCode: "+40", + }, + { + countryCode: "ru", + country: "Russia", + phoneCode: "+7", + }, + { + countryCode: "rw", + country: "Rwanda", + phoneCode: "+250", + }, + { + countryCode: "bl", + country: "Saint Barthélemy", + phoneCode: "+590", + }, + { + countryCode: "sh", + country: "Saint Helena", + phoneCode: "+290", + }, + { + countryCode: "kn", + country: "Saint Kitts and Nevis", + phoneCode: "+1", + }, + { + countryCode: "lc", + country: "Saint Lucia", + phoneCode: "+1", + }, + { + countryCode: "mf", + country: "Saint Martin", + phoneCode: "+590", + }, + { + countryCode: "sx", + country: "Saint Martin", + phoneCode: "+1721", + }, + { + countryCode: "pm", + country: "Saint Pierre and Miquelon", + phoneCode: "+508", + }, + { + countryCode: "vc", + country: "Saint Vincent and the Grenadines", + phoneCode: "+1", + }, + { + countryCode: "ws", + country: "Samoa", + phoneCode: "+685", + }, + { + countryCode: "sm", + country: "San Marino", + phoneCode: "+378", + }, + { + countryCode: "st", + country: "São Tomé and Príncipe", + phoneCode: "+239", + }, + { + countryCode: "sa", + country: "Saudi Arabia", + phoneCode: "+966", + }, + { + countryCode: "sn", + country: "Senegal", + phoneCode: "+221", + }, + { + countryCode: "rs", + country: "Serbia", + phoneCode: "+381", + }, + { + countryCode: "sc", + country: "Seychelles", + phoneCode: "+248", + }, + { + countryCode: "sl", + country: "Sierra Leone", + phoneCode: "+232", + }, + { + countryCode: "sg", + country: "Singapore", + phoneCode: "+65", + }, + { + countryCode: "sk", + country: "Slovakia", + phoneCode: "+421", + }, + { + countryCode: "si", + country: "Slovenia", + phoneCode: "+386", + }, + { + countryCode: "sb", + country: "Solomon Islands", + phoneCode: "+677", + }, + { + countryCode: "so", + country: "Somalia", + phoneCode: "+252", + }, + { + countryCode: "za", + country: "South Africa", + phoneCode: "+27", + }, + { + countryCode: "gs", + country: "South Georgia and the South Sandwich Islands", + phoneCode: "+500", + }, + { + countryCode: "kr", + country: "South Korea", + phoneCode: "+82", + }, + { + countryCode: "ss", + country: "South Sudan", + phoneCode: "+211", + }, + { + countryCode: "es", + country: "Spain", + phoneCode: "+34", + }, + { + countryCode: "lk", + country: "Sri Lanka", + phoneCode: "+94", + }, + { + countryCode: "sd", + country: "Sudan", + phoneCode: "+249", + }, + { + countryCode: "sr", + country: "Suriname", + phoneCode: "+597", + }, + { + countryCode: "sj", + country: "Svalbard and Jan Mayen", + phoneCode: "+47", + }, + { + countryCode: "sz", + country: "Swaziland", + phoneCode: "+268", + }, + { + countryCode: "se", + country: "Sweden", + phoneCode: "+46", + }, + { + countryCode: "ch", + country: "Switzerland", + phoneCode: "+41", + }, + { + countryCode: "sy", + country: "Syria", + phoneCode: "+963", + }, + { + countryCode: "tw", + country: "Taiwan", + phoneCode: "+886", + }, + { + countryCode: "tj", + country: "Tajikistan", + phoneCode: "+992", + }, + { + countryCode: "tz", + country: "Tanzania", + phoneCode: "+255", + }, + { + countryCode: "th", + country: "Thailand", + phoneCode: "+66", + }, + { + countryCode: "tl", + country: "Timor-Leste", + phoneCode: "+670", + }, + { + countryCode: "tg", + country: "Togo", + phoneCode: "+228", + }, + { + countryCode: "tk", + country: "Tokelau", + phoneCode: "+690", + }, + { + countryCode: "to", + country: "Tonga", + phoneCode: "+676", + }, + { + countryCode: "tt", + country: "Trinidad and Tobago", + phoneCode: "+1", + }, + { + countryCode: "tn", + country: "Tunisia", + phoneCode: "+216", + }, + { + countryCode: "tr", + country: "Turkey", + phoneCode: "+90", + }, + { + countryCode: "tm", + country: "Turkmenistan", + phoneCode: "+993", + }, + { + countryCode: "tc", + country: "Turks and Caicos Islands", + phoneCode: "+1", + }, + { + countryCode: "tv", + country: "Tuvalu", + phoneCode: "+688", + }, + { + countryCode: "vi", + country: "U.S. Virgin Islands", + phoneCode: "+1", + }, + { + countryCode: "ug", + country: "Uganda", + phoneCode: "+256", + }, + { + countryCode: "ua", + country: "Ukraine", + phoneCode: "+380", + }, + { + countryCode: "ae", + country: "United Arab Emirates", + phoneCode: "+971", + }, + { + countryCode: "gb", + country: "United Kingdom", + phoneCode: "+44", + }, + { + countryCode: "us", + country: "United States", + phoneCode: "+1", + }, + { + countryCode: "uy", + country: "Uruguay", + phoneCode: "+598", + }, + { + countryCode: "uz", + country: "Uzbekistan", + phoneCode: "+998", + }, + { + countryCode: "vu", + country: "Vanuatu", + phoneCode: "+678", + }, + { + countryCode: "va", + country: "Vatican City", + phoneCode: "+379", + }, + { + countryCode: "ve", + country: "Venezuela", + phoneCode: "+58", + }, + { + countryCode: "vn", + country: "Vietnam", + phoneCode: "+84", + }, + { + countryCode: "wf", + country: "Wallis and Futuna", + phoneCode: "+681", + }, + { + countryCode: "eh", + country: "Western Sahara", + phoneCode: "+212", + }, + { + countryCode: "ye", + country: "Yemen", + phoneCode: "+967", + }, + { + countryCode: "zm", + country: "Zambia", + phoneCode: "+260", + }, + { + countryCode: "zw", + country: "Zimbabwe", + phoneCode: "+263", + }, +]; diff --git a/packages/logic/core/date.utils.ts b/packages/logic/core/date.utils.ts new file mode 100644 index 0000000..15bed65 --- /dev/null +++ b/packages/logic/core/date.utils.ts @@ -0,0 +1,83 @@ +import type { CalendarDate } from "@internationalized/date"; + +export function formatDuration(ms: number): string { + const seconds = Math.floor(ms / 1000); + if (seconds < 60) return `${seconds}s`; + + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + return `${minutes}m ${remainingSeconds}s`; +} + +export function formatDateTimeFromIsoString(isoString: string): string { + try { + const date = new Date(isoString); + return new Intl.DateTimeFormat("en-US", { + dateStyle: "medium", + timeStyle: "short", + }).format(date); + } catch (e) { + return "Invalid date"; + } +} + +export function getJustDateString(d: Date): string { + return d.toISOString().split("T")[0]; +} + +export function formatDateTime(dateTimeStr: string) { + const date = new Date(dateTimeStr); + return { + time: date.toLocaleTimeString("en-US", { + hour: "2-digit", + minute: "2-digit", + hour12: false, + }), + date: date.toLocaleDateString("en-US", { + weekday: "short", + day: "2-digit", + month: "short", + }), + }; +} + +export function formatDate(dateStr: string) { + return new Date(dateStr).toLocaleDateString("en-US", { + weekday: "short", + day: "2-digit", + month: "short", + }); +} + +export function isTimestampMoreThan1MinAgo(ts: string): boolean { + const lastPingedDate = new Date(ts); + const now = new Date(); + const diff = now.getTime() - lastPingedDate.getTime(); + return diff > 60000; +} + +export function isTimestampOlderThan(ts: string, seconds: number): boolean { + const lastPingedDate = new Date(ts); + const now = new Date(); + const diff = now.getTime() - lastPingedDate.getTime(); + return diff > seconds * 1000; +} + +export function makeDateStringISO(ds: string): string { + if (ds.includes("T")) { + return `${ds.split("T")[0]}T00:00:00.000Z`; + } + return `${ds}T00:00:00.000Z`; +} + +export function parseCalDateToDateString(v: CalendarDate) { + let month: string | number = v.month; + if (month < 10) { + month = `0${month}`; + } + let day: string | number = v.day; + if (day < 10) { + day = `0${day}`; + } + return `${v.year}-${month}-${day}`; +} diff --git a/packages/logic/core/error.ts b/packages/logic/core/error.ts new file mode 100644 index 0000000..83d8c49 --- /dev/null +++ b/packages/logic/core/error.ts @@ -0,0 +1,8 @@ +export type Err = { + code: string; + message: string; + description: string; + detail: string; + actionable?: boolean; + error?: any; +}; diff --git a/packages/logic/core/flow.execution.context.ts b/packages/logic/core/flow.execution.context.ts new file mode 100644 index 0000000..6c84e29 --- /dev/null +++ b/packages/logic/core/flow.execution.context.ts @@ -0,0 +1,5 @@ +export type FlowExecCtx = { + flowId: string; + userId?: string; + sessionId?: string; +}; diff --git a/packages/logic/core/hash.utils.ts b/packages/logic/core/hash.utils.ts new file mode 100644 index 0000000..b0da8fa --- /dev/null +++ b/packages/logic/core/hash.utils.ts @@ -0,0 +1,31 @@ +import { argon2id, hash as argonHash, verify as argonVerify } from "argon2"; + +export async function hashString(target: string): Promise { + const salt = Buffer.from(crypto.getRandomValues(new Uint8Array(16))).toString( + "hex", + ); + const hash = await argonHash(target, { + type: argon2id, + salt: Buffer.from(salt, "hex"), + hashLength: 32, + timeCost: 3, + memoryCost: 65536, + parallelism: 1, + }); + return hash; +} + +export async function verifyHash({ + hash, + target, +}: { + hash: string; + target: string; +}): Promise { + try { + const isValid = await argonVerify(hash, `${target}`); + return isValid; + } catch (err) { + return false; + } +} diff --git a/packages/logic/core/hono.helpers.ts b/packages/logic/core/hono.helpers.ts new file mode 100644 index 0000000..fd229c9 --- /dev/null +++ b/packages/logic/core/hono.helpers.ts @@ -0,0 +1,9 @@ +import type { Session, User } from "@/domains/user/data"; +import { FlowExecCtx } from "./flow.execution.context"; +import { Env } from "hono"; + +export interface HonoContext extends Env { + Bindings: { + locals: { user: User; session: Session; fCtx: FlowExecCtx }; + }; +} diff --git a/packages/logic/core/pagination.utils.ts b/packages/logic/core/pagination.utils.ts new file mode 100644 index 0000000..dd11b81 --- /dev/null +++ b/packages/logic/core/pagination.utils.ts @@ -0,0 +1,12 @@ +import * as v from "valibot"; + +export const paginationModel = v.object({ + cursor: v.optional(v.string()), + limit: v.pipe(v.number(), v.integer(), v.maxValue(100)), + asc: v.optional(v.boolean(), true), + totalItemCount: v.optional(v.pipe(v.number(), v.integer()), 0), + totalPages: v.pipe(v.number(), v.integer()), + page: v.pipe(v.number(), v.integer()), +}); + +export type PaginationModel = v.InferOutput; diff --git a/packages/logic/core/rate.limiter.ts b/packages/logic/core/rate.limiter.ts new file mode 100644 index 0000000..5f8b3bb --- /dev/null +++ b/packages/logic/core/rate.limiter.ts @@ -0,0 +1,40 @@ +import { logger } from "@pkg/logger"; + +export class RateLimiter { + private requestTimestamps: number[] = []; + private readonly callsPerMinute: number; + + constructor(callsPerMinute: number = 60) { + this.callsPerMinute = Math.min(callsPerMinute, 60); + } + + async checkRateLimit(): Promise { + const currentTime = Date.now(); + const oneMinuteAgo = currentTime - 60000; // 60 seconds in milliseconds + + // Remove timestamps older than 1 minute + this.requestTimestamps = this.requestTimestamps.filter( + (timestamp) => timestamp > oneMinuteAgo, + ); + + // If we're approaching the limit, wait until we have capacity + if (this.requestTimestamps.length >= this.callsPerMinute) { + const oldestRequest = this.requestTimestamps[0]; + const waitTime = oldestRequest + 60000 - currentTime; + + if (waitTime > 0) { + logger.warn( + `Rate limit approaching (${this.requestTimestamps.length} requests in last minute). Sleeping for ${waitTime}ms`, + ); + await new Promise((resolve) => setTimeout(resolve, waitTime)); + // After waiting, some timestamps may have expired + this.requestTimestamps = this.requestTimestamps.filter( + (timestamp) => timestamp > Date.now() - 60000, + ); + } + } + + // Add current request to timestamps + this.requestTimestamps.push(Date.now()); + } +} diff --git a/packages/logic/core/settings.ts b/packages/logic/core/settings.ts new file mode 100644 index 0000000..33b2d76 --- /dev/null +++ b/packages/logic/core/settings.ts @@ -0,0 +1 @@ +export { getSetting, settings } from "@pkg/settings"; diff --git a/packages/logic/core/string.utils/index.ts b/packages/logic/core/string.utils/index.ts new file mode 100644 index 0000000..7531a50 --- /dev/null +++ b/packages/logic/core/string.utils/index.ts @@ -0,0 +1,106 @@ +import * as v from "valibot"; + +export function capitalize(input: string, firstOfAllWords?: boolean): string { + // capitalize first letter of input + if (!firstOfAllWords) { + return input.charAt(0).toUpperCase() + input.slice(1); + } + let out = ""; + for (const word of input.split(" ")) { + out += word.charAt(0).toUpperCase() + word.slice(1) + " "; + } + return out.slice(0, -1); +} + +export function camelToSpacedPascal(input: string): string { + let result = ""; + let previousChar = ""; + for (const char of input) { + if (char === char.toUpperCase() && previousChar !== " ") { + result += " "; + } + result += char; + previousChar = char; + } + return result.charAt(0).toUpperCase() + result.slice(1); +} + +export function snakeToCamel(input: string): string { + if (!input) { + return input; + } + // also account for numbers and kebab-case + const splits = input.split(/[-_]/); + let result = splits[0]; + for (const split of splits.slice(1)) { + result += capitalize(split, true); + } + return result ?? ""; +} + +export function snakeToSpacedPascal(input: string): string { + return camelToSpacedPascal(snakeToCamel(input)); +} + +export function spacedPascalToSnake(input: string): string { + return input.split(" ").join("_").toLowerCase(); +} + +export function convertDashedLowerToTitleCase(input: string): string { + return input + .split("-") + .map( + (word) => + word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(), + ) + .join(" "); // Join the words with a space +} + +export function encodeCursor(cursor: T): string { + try { + // Convert the object to a JSON string + const jsonString = JSON.stringify(cursor); + // Convert to UTF-8 bytes, then base64 + return btoa( + encodeURIComponent(jsonString).replace(/%([0-9A-F]{2})/g, (_, p1) => + String.fromCharCode(parseInt(p1, 16)), + ), + ); + } catch (error) { + console.error("Error encoding cursor:", error); + throw new Error("Failed to encode cursor"); + } +} + +export function decodeCursor( + cursor: string, + parser: v.BaseSchema, +) { + try { + // Decode base64 back to UTF-8 string + const decoded = decodeURIComponent( + Array.prototype.map + .call(atob(cursor), (c) => { + return ( + "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2) + ); + }) + .join(""), + ); + // Parse back to object + const parsedData = JSON.parse(decoded); + const result = v.safeParse(parser, parsedData); + return result.success + ? { success: true, data: result.output as T } + : { + success: false, + error: new Error( + result.issues.map((i) => i.message).join(", "), + ), + data: undefined, + }; + } catch (error) { + console.error("Error decoding cursor:", error); + return { error: new Error("Failed to decode cursor"), data: undefined }; + } +} diff --git a/packages/logic/core/string.utils/sequence.matcher.ts b/packages/logic/core/string.utils/sequence.matcher.ts new file mode 100644 index 0000000..1196510 --- /dev/null +++ b/packages/logic/core/string.utils/sequence.matcher.ts @@ -0,0 +1,555 @@ +/** + * Similar to Python's difflib.SequenceMatcher + * + * A flexible class for comparing pairs of sequences of any type. + * Uses the Ratcliff-Obershelp algorithm with "gestalt pattern matching" + * to find the longest contiguous matching subsequences. + */ + +export interface Match { + /** Starting position in sequence a */ + a: number; + /** Starting position in sequence b */ + b: number; + /** Length of the matching block */ + size: number; +} + +export type OpCode = "replace" | "delete" | "insert" | "equal"; + +export interface OpCodeTuple { + /** Operation type */ + tag: OpCode; + /** Start index in sequence a */ + i1: number; + /** End index in sequence a */ + i2: number; + /** Start index in sequence b */ + j1: number; + /** End index in sequence b */ + j2: number; +} + +export type JunkFunction = (element: T) => boolean; + +export class SequenceMatcher { + private isjunk: JunkFunction | null; + private a: T[]; + private b: T[]; + private autojunk: boolean; + + // Cached data structures for sequence b + private bjunk: Set; + private bpopular: Set; + private b2j: Map; + + // Cached results + private fullbcount: Map | null = null; + private matchingBlocks: Match[] | null = null; + private opcodes: OpCodeTuple[] | null = null; + + constructor( + isjunk: JunkFunction | null = null, + a: T[] = [], + b: T[] = [], + autojunk: boolean = true, + ) { + this.isjunk = isjunk; + this.a = []; + this.b = []; + this.autojunk = autojunk; + this.bjunk = new Set(); + this.bpopular = new Set(); + this.b2j = new Map(); + + this.setSeqs(a, b); + } + + /** + * Set both sequences to be compared + */ + setSeqs(a: T[], b: T[]): void { + this.setSeq1(a); + this.setSeq2(b); + } + + /** + * Set the first sequence to be compared + */ + setSeq1(a: T[]): void { + if (a === this.a) return; + this.a = [...a]; + this.matchingBlocks = null; + this.opcodes = null; + } + + /** + * Set the second sequence to be compared + */ + setSeq2(b: T[]): void { + if (b === this.b) return; + this.b = [...b]; + this.matchingBlocks = null; + this.opcodes = null; + this.fullbcount = null; + this.chainB(); + } + + /** + * Analyze sequence b and build lookup structures + */ + private chainB(): void { + const b = this.b; + this.bjunk = new Set(); + this.bpopular = new Set(); + this.b2j = new Map(); + + // Count occurrences of each element + const elementCounts = new Map(); + for (const element of b) { + elementCounts.set(element, (elementCounts.get(element) || 0) + 1); + } + + // Determine junk and popular elements + const n = b.length; + const popularThreshold = Math.floor(n / 100) + 1; // > 1% of sequence + + for (const [element, count] of elementCounts) { + if (this.isjunk && this.isjunk(element)) { + this.bjunk.add(element); + } else if (this.autojunk && n >= 200 && count > popularThreshold) { + this.bpopular.add(element); + } + } + + // Build position mapping for non-junk, non-popular elements + for (let i = 0; i < b.length; i++) { + const element = b[i]; + if (!this.bjunk.has(element) && !this.bpopular.has(element)) { + if (!this.b2j.has(element)) { + this.b2j.set(element, []); + } + this.b2j.get(element)!.push(i); + } + } + } + + /** + * Find the longest matching block in a[alo:ahi] and b[blo:bhi] + */ + findLongestMatch( + alo: number = 0, + ahi: number | null = null, + blo: number = 0, + bhi: number | null = null, + ): Match { + if (ahi === null) ahi = this.a.length; + if (bhi === null) bhi = this.b.length; + + let besti = alo; + let bestj = blo; + let bestsize = 0; + + // Find all positions where a[i] appears in b + const j2len = new Map(); + + for (let i = alo; i < ahi; i++) { + const element = this.a[i]; + const positions = this.b2j.get(element) || []; + const newj2len = new Map(); + + for (const j of positions) { + if (j < blo) continue; + if (j >= bhi) break; + + const prevLen = j2len.get(j - 1) || 0; + const k = prevLen + 1; + newj2len.set(j, k); + + if (k > bestsize) { + besti = i - k + 1; + bestj = j - k + 1; + bestsize = k; + } + } + + j2len.clear(); + for (const [key, value] of newj2len) { + j2len.set(key, value); + } + } + + // Extend match with junk elements + while ( + besti > alo && + bestj > blo && + !this.isBJunk(this.b[bestj - 1]) && + this.elementsEqual(this.a[besti - 1], this.b[bestj - 1]) + ) { + besti--; + bestj--; + bestsize++; + } + + while ( + besti + bestsize < ahi && + bestj + bestsize < bhi && + !this.isBJunk(this.b[bestj + bestsize]) && + this.elementsEqual(this.a[besti + bestsize], this.b[bestj + bestsize]) + ) { + bestsize++; + } + + // Extend match with junk elements at the beginning + while (besti > alo && bestj > blo && this.isBJunk(this.b[bestj - 1])) { + besti--; + bestj--; + bestsize++; + } + + // Extend match with junk elements at the end + while ( + besti + bestsize < ahi && + bestj + bestsize < bhi && + this.isBJunk(this.b[bestj + bestsize]) + ) { + bestsize++; + } + + return { a: besti, b: bestj, size: bestsize }; + } + + /** + * Return list of non-overlapping matching blocks + */ + getMatchingBlocks(): Match[] { + if (this.matchingBlocks !== null) { + return this.matchingBlocks; + } + + const matches: Match[] = []; + this.getMatchingBlocksRecursive( + 0, + this.a.length, + 0, + this.b.length, + matches, + ); + + // Add sentinel + matches.push({ a: this.a.length, b: this.b.length, size: 0 }); + + this.matchingBlocks = matches; + return matches; + } + + /** + * Recursively find matching blocks + */ + private getMatchingBlocksRecursive( + alo: number, + ahi: number, + blo: number, + bhi: number, + matches: Match[], + ): void { + const match = this.findLongestMatch(alo, ahi, blo, bhi); + + if (match.size > 0) { + // Recurse on the pieces before and after the match + if (alo < match.a && blo < match.b) { + this.getMatchingBlocksRecursive( + alo, + match.a, + blo, + match.b, + matches, + ); + } + + matches.push(match); + + if (match.a + match.size < ahi && match.b + match.size < bhi) { + this.getMatchingBlocksRecursive( + match.a + match.size, + ahi, + match.b + match.size, + bhi, + matches, + ); + } + } + } + + /** + * Return list of 5-tuples describing how to turn a into b + */ + getOpcodes(): OpCodeTuple[] { + if (this.opcodes !== null) { + return this.opcodes; + } + + let i = 0; + let j = 0; + const opcodes: OpCodeTuple[] = []; + + for (const match of this.getMatchingBlocks()) { + let tag: OpCode = "equal"; + + if (i < match.a && j < match.b) { + tag = "replace"; + } else if (i < match.a) { + tag = "delete"; + } else if (j < match.b) { + tag = "insert"; + } + + if (tag !== "equal") { + opcodes.push({ + tag, + i1: i, + i2: match.a, + j1: j, + j2: match.b, + }); + } + + i = match.a + match.size; + j = match.b + match.size; + + // Don't add the sentinel match + if (match.size > 0) { + opcodes.push({ + tag: "equal", + i1: match.a, + i2: i, + j1: match.b, + j2: j, + }); + } + } + + this.opcodes = opcodes; + return opcodes; + } + + /** + * Return a measure of sequences' similarity (0.0-1.0) + */ + ratio(): number { + const matches = this.getMatchingBlocks() + .slice(0, -1) // Exclude sentinel + .reduce((sum, match) => sum + match.size, 0); + + const total = this.a.length + this.b.length; + return total === 0 ? 1.0 : (2.0 * matches) / total; + } + + /** + * Return an upper bound on ratio() relatively quickly + */ + quickRatio(): number { + if (this.fullbcount === null) { + this.fullbcount = new Map(); + for (const element of this.b) { + this.fullbcount.set( + element, + (this.fullbcount.get(element) || 0) + 1, + ); + } + } + + let matches = 0; + const tempCounts = new Map(this.fullbcount); + + for (const element of this.a) { + const count = tempCounts.get(element); + if (count && count > 0) { + matches++; + tempCounts.set(element, count - 1); + } + } + + const total = this.a.length + this.b.length; + return total === 0 ? 1.0 : (2.0 * matches) / total; + } + + /** + * Return an upper bound on ratio() very quickly + */ + realQuickRatio(): number { + const total = this.a.length + this.b.length; + return total === 0 + ? 1.0 + : (2.0 * Math.min(this.a.length, this.b.length)) / total; + } + + /** + * Check if element is junk in sequence b + */ + private isBJunk(element: T): boolean { + return this.bjunk.has(element); + } + + /** + * Check if two elements are equal + */ + private elementsEqual(a: T, b: T): boolean { + return a === b; + } +} + +/** + * Utility function to get close matches similar to Python's get_close_matches + */ +export function getCloseMatches( + word: T[], + possibilities: T[][], + n: number = 3, + cutoff: number = 0.6, +): T[][] { + if (n <= 0) { + throw new Error("n must be greater than 0"); + } + + const matches: Array<{ sequence: T[]; ratio: number }> = []; + + for (const possibility of possibilities) { + const matcher = new SequenceMatcher(null, word, possibility); + const ratio = matcher.ratio(); + + if (ratio >= cutoff) { + matches.push({ sequence: possibility, ratio }); + } + } + + // Sort by ratio (descending) and take top n + matches.sort((a, b) => b.ratio - a.ratio); + return matches.slice(0, n).map((match) => match.sequence); +} + +/** + * String-specific version of SequenceMatcher for character-by-character comparison. + * This class treats strings as sequences of characters while providing a string-friendly API. + */ +export class StringSequenceMatcher { + private matcher: SequenceMatcher; + + constructor( + isjunk: JunkFunction | null = null, + a: string = "", + b: string = "", + autojunk: boolean = true, + ) { + this.matcher = new SequenceMatcher( + isjunk, + Array.from(a), + Array.from(b), + autojunk, + ); + } + + /** + * Set both sequences to be compared + */ + setSeqs(a: string, b: string): void { + this.matcher.setSeqs(Array.from(a), Array.from(b)); + } + + /** + * Set the first sequence to be compared + */ + setSeq1(a: string): void { + this.matcher.setSeq1(Array.from(a)); + } + + /** + * Set the second sequence to be compared + */ + setSeq2(b: string): void { + this.matcher.setSeq2(Array.from(b)); + } + + /** + * Find the longest matching block in a[alo:ahi] and b[blo:bhi] + */ + findLongestMatch( + alo: number = 0, + ahi: number | null = null, + blo: number = 0, + bhi: number | null = null, + ): Match { + return this.matcher.findLongestMatch(alo, ahi, blo, bhi); + } + + /** + * Return list of non-overlapping matching blocks + */ + getMatchingBlocks(): Match[] { + return this.matcher.getMatchingBlocks(); + } + + /** + * Return list of 5-tuples describing how to turn a into b + */ + getOpcodes(): OpCodeTuple[] { + return this.matcher.getOpcodes(); + } + + /** + * Return a measure of sequences' similarity (0.0-1.0) + */ + ratio(): number { + return this.matcher.ratio(); + } + + /** + * Return an upper bound on ratio() relatively quickly + */ + quickRatio(): number { + return this.matcher.quickRatio(); + } + + /** + * Return an upper bound on ratio() very quickly + */ + realQuickRatio(): number { + return this.matcher.realQuickRatio(); + } +} + +/** + * Utility function for string similarity + */ +export function getStringSimilarity(a: string, b: string): number { + const matcher = new StringSequenceMatcher(null, a, b); + return matcher.ratio(); +} + +/** + * Get close string matches + */ +export function getCloseStringMatches( + word: string, + possibilities: string[], + n: number = 3, + cutoff: number = 0.6, +): string[] { + if (n <= 0) { + throw new Error("n must be greater than 0"); + } + + const matches: Array<{ string: string; ratio: number }> = []; + + for (const possibility of possibilities) { + const ratio = getStringSimilarity(word, possibility); + + if (ratio >= cutoff) { + matches.push({ string: possibility, ratio }); + } + } + + // Sort by ratio (descending) and take top n + matches.sort((a, b) => b.ratio - a.ratio); + return matches.slice(0, n).map((match) => match.string); +} diff --git a/packages/logic/domains/2fa/controller.ts b/packages/logic/domains/2fa/controller.ts new file mode 100644 index 0000000..191482f --- /dev/null +++ b/packages/logic/domains/2fa/controller.ts @@ -0,0 +1,349 @@ +import { errAsync, okAsync, ResultAsync } from "neverthrow"; +import { FlowExecCtx } from "@core/flow.execution.context"; +import { UserRepository } from "@domains/user/repository"; +import { getRedisInstance, Redis } from "@pkg/redis"; +import { TwofaRepository } from "./repository"; +import { auth } from "../auth/config.base"; +import type { TwoFaSession } from "./data"; +import { User } from "@domains/user/data"; +import { settings } from "@core/settings"; +import { type Err } from "@pkg/result"; +import { twofaErrors } from "./errors"; +import { logger } from "@pkg/logger"; +import { db } from "@pkg/db"; + +export class TwofaController { + constructor( + private twofaRepo: TwofaRepository, + private userRepo: UserRepository, + private store: Redis, + ) {} + + checkTotp(secret: string, code: string) { + return this.twofaRepo.checkTotp(secret, code); + } + + is2faEnabled(fctx: FlowExecCtx, userId: string) { + return this.twofaRepo + .getUsers2FAInfo(fctx, userId, true) + .map((data) => !!data) + .orElse(() => okAsync(false)); + } + + isUserBanned(fctx: FlowExecCtx, userId: string) { + return this.userRepo.isUserBanned(fctx, userId).orElse((error) => { + logger.error("Error checking user ban status:", error); + return okAsync(false); + }); + } + + setup2FA(fctx: FlowExecCtx, user: User) { + return this.is2faEnabled(fctx, user.id) + .andThen((enabled) => + enabled + ? errAsync(twofaErrors.alreadyEnabled(fctx)) + : this.twofaRepo.setup(fctx, user.id), + ) + .map((secret) => { + const appName = settings.appName; + const totpUri = `otpauth://totp/${appName}:${user.email}?secret=${secret}&issuer=${appName}`; + return { totpURI: totpUri, secret }; + }); + } + + verifyAndEnable2FA( + fctx: FlowExecCtx, + user: User, + code: string, + headers: Headers, + ) { + return this.is2faEnabled(fctx, user.id) + .andThen((enabled) => { + if (enabled) { + return errAsync(twofaErrors.alreadyEnabled(fctx)); + } + return okAsync(undefined); + }) + .andThen(() => { + logger.info(`Verifying 2fa for ${user.id} : ${code}`, { + flowId: fctx.flowId, + }); + return this.twofaRepo.verifyAndEnable2FA(fctx, user.id, code); + }) + .andThen((verified) => { + if (verified) { + return ResultAsync.combine([ + ResultAsync.fromPromise( + auth.api.revokeOtherSessions({ headers }), + () => twofaErrors.revokeSessionsFailed(fctx), + ), + this.userRepo.updateLastVerified2FaAtToNow( + fctx, + user.id, + ), + ]).map(() => true); + } + return okAsync(verified); + }); + } + + disable(fctx: FlowExecCtx, user: User, code: string) { + return this.is2faEnabled(fctx, user.id) + .andThen((enabled) => { + if (!enabled) { + return errAsync(twofaErrors.notEnabled(fctx)); + } + return okAsync(undefined); + }) + .andThen(() => this.twofaRepo.get2FASecret(fctx, user.id)) + .andThen((secret) => { + if (!secret) { + return errAsync(twofaErrors.invalidSetup(fctx)); + } + if (!this.checkTotp(secret, code)) { + return errAsync(twofaErrors.invalidCode(fctx)); + } + return okAsync(undefined); + }) + .andThen(() => this.twofaRepo.disable(fctx, user.id)); + } + + generateBackupCodes(fctx: FlowExecCtx, user: User) { + return this.is2faEnabled(fctx, user.id) + .andThen((enabled) => { + if (!enabled) { + return errAsync(twofaErrors.notEnabled(fctx)); + } + return okAsync(undefined); + }) + .andThen(() => this.twofaRepo.generateBackupCodes(fctx, user.id)); + } + + requiresInitialVerification( + fctx: FlowExecCtx, + user: User, + sessionId: string, + ) { + return this.is2faEnabled(fctx, user.id).andThen((enabled) => { + if (!enabled) { + return okAsync(false); + } + + return ResultAsync.fromPromise( + this.store.get(`initial_2fa_completed:${sessionId}`), + () => null, + ) + .map((completed) => !completed && completed !== "0") + .orElse(() => okAsync(true)); + }); + } + + requiresSensitiveActionVerification(fctx: FlowExecCtx, user: User) { + return this.is2faEnabled(fctx, user.id).andThen((enabled) => { + if (!enabled) { + return okAsync(false); + } + + if (!user.last2FAVerifiedAt) { + return okAsync(true); + } + + const requiredHours = settings.twofaRequiredHours || 24; + const verificationAge = + Date.now() - user.last2FAVerifiedAt.getTime(); + const maxAge = requiredHours * 60 * 60 * 1000; + + return okAsync(verificationAge > maxAge); + }); + } + + markInitialVerificationComplete(sessionId: string) { + return ResultAsync.fromPromise( + this.store.setex( + `initial_2fa_completed:${sessionId}`, + 60 * 60 * 24 * 7, + "true", + ), + () => null, + ) + .map(() => undefined) + .orElse((error) => { + logger.error("Error marking initial 2FA as complete:", error); + return okAsync(undefined); + }); + } + + startVerification( + fctx: FlowExecCtx, + params: { + userId: string; + sessionId: string; + ipAddress?: string; + userAgent?: string; + }, + ) { + return this.twofaRepo.createSession(fctx, params).map((session) => ({ + verificationToken: session.verificationToken, + })); + } + + private validateSession(fctx: FlowExecCtx, session: TwoFaSession) { + if (session.status !== "pending") { + return errAsync(twofaErrors.sessionNotActive(fctx)); + } + + if (session.expiresAt < new Date()) { + return this.twofaRepo + .updateSession(fctx, session.id, { status: "expired" }) + .andThen(() => errAsync(twofaErrors.sessionExpired(fctx))); + } + + return okAsync(session); + } + + private handleMaxAttempts( + fctx: FlowExecCtx, + session: TwoFaSession, + userId: string, + ) { + const banExpiresAt = new Date(); + banExpiresAt.setHours(banExpiresAt.getHours() + 1); + + return this.twofaRepo + .updateSession(fctx, session.id, { status: "failed" }) + .andThen(() => + this.userRepo.banUser( + fctx, + userId, + "Too many failed 2FA verification attempts", + banExpiresAt, + ), + ) + .andThen(() => errAsync(twofaErrors.tooManyAttempts(fctx))); + } + + private checkAttemptsLimit( + fctx: FlowExecCtx, + session: TwoFaSession, + userId: string, + ) { + if (session.attempts >= session.maxAttempts) { + return this.handleMaxAttempts(fctx, session, userId); + } + return okAsync(session); + } + + private checkCodeReplay( + fctx: FlowExecCtx, + session: TwoFaSession, + code: string, + ): ResultAsync { + if (session.codeUsed === code) { + return this.twofaRepo + .incrementAttempts(fctx, session.id) + .andThen(() => errAsync(twofaErrors.codeReplay(fctx))); + } + return okAsync(session); + } + + private verifyTotpCode( + fctx: FlowExecCtx, + session: TwoFaSession, + userId: string, + code: string, + ) { + return this.twofaRepo.get2FASecret(fctx, userId).andThen((secret) => { + if (!secret) { + return errAsync(twofaErrors.invalidSetup(fctx)); + } + + if (!this.checkTotp(secret, code)) { + return this.twofaRepo + .incrementAttempts(fctx, session.id) + .andThen(() => errAsync(twofaErrors.invalidCode(fctx))); + } + + return okAsync(session); + }); + } + + private completeVerification( + fctx: FlowExecCtx, + session: TwoFaSession, + userId: string, + code: string, + ) { + return this.twofaRepo + .updateSession(fctx, session.id, { + status: "verified", + verifiedAt: new Date(), + codeUsed: code, + }) + .andThen(() => + ResultAsync.combine([ + this.userRepo.updateLastVerified2FaAtToNow(fctx, userId), + this.markInitialVerificationComplete(session.sessionId), + ]), + ) + .map(() => undefined); + } + + verifyCode( + fctx: FlowExecCtx, + params: { verificationSessToken: string; code: string }, + user?: User, + ) { + if (!user) { + return errAsync(twofaErrors.userNotFound(fctx)); + } + + return this.is2faEnabled(fctx, user.id) + .andThen((enabled) => { + if (!enabled) { + return errAsync( + twofaErrors.notEnabledForVerification(fctx), + ); + } + return okAsync(undefined); + }) + .andThen(() => + this.twofaRepo.getSessionByToken( + fctx, + params.verificationSessToken, + ), + ) + .andThen((session) => { + if (!session) { + return errAsync(twofaErrors.sessionNotFound(fctx)); + } + return okAsync(session); + }) + .andThen((session) => this.validateSession(fctx, session)) + .andThen((session) => + this.checkAttemptsLimit(fctx, session, user.id), + ) + .andThen((session) => + this.checkCodeReplay(fctx, session, params.code), + ) + .andThen((session) => + this.verifyTotpCode(fctx, session, user.id, params.code), + ) + .andThen((session) => + this.completeVerification(fctx, session, user.id, params.code), + ) + .map(() => ({ success: true })); + } + + cleanupExpiredSessions(fctx: FlowExecCtx) { + return this.twofaRepo.cleanupExpiredSessions(fctx); + } +} + +export function getTwofaController() { + const _redis = getRedisInstance(); + return new TwofaController( + new TwofaRepository(db, _redis), + new UserRepository(db), + _redis, + ); +} diff --git a/packages/logic/domains/2fa/data.ts b/packages/logic/domains/2fa/data.ts new file mode 100644 index 0000000..7e19e0a --- /dev/null +++ b/packages/logic/domains/2fa/data.ts @@ -0,0 +1,48 @@ +import * as v from "valibot"; + +export const startVerificationSchema = v.object({ + userId: v.string(), + sessionId: v.string(), +}); + +export const verifyCodeSchema = v.object({ + verificationToken: v.string(), + code: v.string(), +}); + +export const enable2FACodeSchema = v.object({ + code: v.string(), +}); + +export const disable2FASchema = v.object({ + code: v.string(), +}); + +export const twoFactorSchema = v.object({ + id: v.string(), + secret: v.string(), + backupCodes: v.array(v.string()), + userId: v.string(), + createdAt: v.date(), + updatedAt: v.date(), +}); +export type TwoFactor = v.InferOutput; + +export type TwoFaSessionStatus = "pending" | "verified" | "failed" | "expired"; + +export const twoFaSessionSchema = v.object({ + id: v.string(), + userId: v.string(), + sessionId: v.string(), + verificationToken: v.string(), + codeUsed: v.optional(v.string()), + status: v.picklist(["pending", "verified", "failed", "expired"]), + attempts: v.number(), + maxAttempts: v.number(), + verifiedAt: v.optional(v.date()), + expiresAt: v.date(), + createdAt: v.date(), + ipAddress: v.string(), + userAgent: v.string(), +}); +export type TwoFaSession = v.InferOutput; diff --git a/packages/logic/domains/2fa/errors.ts b/packages/logic/domains/2fa/errors.ts new file mode 100644 index 0000000..5777acf --- /dev/null +++ b/packages/logic/domains/2fa/errors.ts @@ -0,0 +1,180 @@ +import { FlowExecCtx } from "@/core/flow.execution.context"; +import { ERROR_CODES, type Err } from "@pkg/result"; +import { getError } from "@pkg/logger"; + +export const twofaErrors = { + dbError: (fctx: FlowExecCtx, detail: string): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.DATABASE_ERROR, + message: "Database operation failed", + description: "Please try again later", + detail, + }), + + alreadyEnabled: (fctx: FlowExecCtx): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.AUTH_ERROR, + message: "2FA already enabled", + description: "Disable it first if you want to re-enable it", + detail: "2FA already enabled", + }), + + notEnabled: (fctx: FlowExecCtx): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.AUTH_ERROR, + message: "2FA not enabled for this user", + description: "Enable 2FA to perform this action", + detail: "2FA not enabled for this user", + }), + + userNotFound: (fctx: FlowExecCtx): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.AUTH_ERROR, + message: "User not found", + description: "Session is invalid or expired", + detail: "User ID not found in database", + }), + + sessionNotActive: (fctx: FlowExecCtx): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.AUTH_ERROR, + message: "Verification session is no longer active", + description: "Please request a new verification code", + detail: "Session status is not 'pending'", + }), + + sessionExpired: (fctx: FlowExecCtx): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.AUTH_ERROR, + message: "Verification session has expired", + description: "Please request a new verification code", + detail: "Session expired timestamp passed", + }), + + sessionNotFound: (fctx: FlowExecCtx): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.NOT_FOUND, + message: "Invalid or expired verification session", + description: "Your verification session has expired or is invalid", + detail: "Session not found by verification token", + }), + + tooManyAttempts: (fctx: FlowExecCtx): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.BANNED, + message: "Too many failed attempts", + description: + "Your account has been banned, contact us to resolve this issue", + detail: "Max attempts reached for 2FA verification", + }), + + codeReplay: (fctx: FlowExecCtx): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.AUTH_ERROR, + message: "This code has already been used", + description: "Please request a new verification code", + detail: "Code replay attempt detected", + }), + + invalidSetup: (fctx: FlowExecCtx): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.AUTH_ERROR, + message: "Invalid 2FA setup found", + description: "Please contact us to resolve this issue", + detail: "Invalid 2FA data found", + }), + + invalidCode: (fctx: FlowExecCtx): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.AUTH_ERROR, + message: "Invalid verification code", + description: "Please try again with the correct code", + detail: "Code is invalid", + }), + + notEnabledForVerification: (fctx: FlowExecCtx): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.AUTH_ERROR, + message: "2FA not enabled for this user", + description: + "Two-factor authentication is not enabled on your account", + detail: "User has 2FA disabled but verification attempted", + }), + + revokeSessionsFailed: (fctx: FlowExecCtx): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.AUTH_ERROR, + message: "Failed to revoke sessions", + description: "Please try again later", + detail: "Failed to revoke other sessions", + }), + + // Repository errors + notFound: (fctx: FlowExecCtx): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.NOT_FOUND, + message: "2FA not found", + description: "Likely not enabled, otherwise please contact us :)", + detail: "2FA not found", + }), + + setupNotFound: (fctx: FlowExecCtx): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.VALIDATION_ERROR, + message: "Cannot perform action", + description: "If 2FA is not enabled, please refresh and try again", + detail: "2FA setup not found", + }), + + maxAttemptsReached: (fctx: FlowExecCtx): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.AUTH_ERROR, + message: "Too many failed attempts", + description: "Please refresh and try again", + detail: "Max attempts reached for session", + }), + + backupCodesNotFound: (fctx: FlowExecCtx): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.NOT_FOUND, + message: "2FA info not found", + description: "Please setup 2FA or contact us if this is unexpected", + detail: "2FA info not found for user", + }), + + backupCodesAlreadyGenerated: (fctx: FlowExecCtx): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.AUTH_ERROR, + message: "Backup codes already generated", + description: + "Can only generate if not already present, or all are used up", + detail: "Backup codes already generated", + }), + + sessionNotFoundById: (fctx: FlowExecCtx): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.NOT_FOUND, + message: "2FA session not found", + description: "The verification session may have expired", + detail: "Session ID not found in database", + }), +}; diff --git a/packages/logic/domains/2fa/repository.ts b/packages/logic/domains/2fa/repository.ts new file mode 100644 index 0000000..eb10b1e --- /dev/null +++ b/packages/logic/domains/2fa/repository.ts @@ -0,0 +1,554 @@ +import { errAsync, okAsync, ResultAsync } from "neverthrow"; +import { FlowExecCtx } from "@core/flow.execution.context"; +import { hashString, verifyHash } from "@/core/hash.utils"; +import { twoFactor, twofaSessions } from "@pkg/db/schema"; +import { TwoFactor, type TwoFaSession } from "./data"; +import { and, Database, eq, gt, lt } from "@pkg/db"; +import { settings } from "@core/settings"; +import type { Err } from "@pkg/result"; +import { twofaErrors } from "./errors"; +import { authenticator } from "otplib"; +import { logger } from "@pkg/logger"; +import { Redis } from "@pkg/redis"; +import { nanoid } from "nanoid"; + +type TwoFaSetup = { + secret: string; + lastUsedCode: string; + tries: number; +}; + +export class TwofaRepository { + private PENDING_KEY_PREFIX = "pending_enabling_2fa:"; + private EXPIRY_TIME = 60 * 20; // 20 mins + private DEFAULT_BACKUP_CODES_AMT = 8; + private MAX_SETUP_ATTEMPTS = 3; + + constructor( + private db: Database, + private store: Redis, + ) {} + + checkTotp(secret: string, code: string) { + const checked = authenticator.verify({ secret, token: code }); + logger.debug("TOTP check result", { checked }); + return checked; + } + + async checkBackupCode(hash: string, code: string) { + return verifyHash({ hash, target: code }); + } + + private getKey(userId: string) { + if (userId.includes(this.PENDING_KEY_PREFIX)) { + return userId; + } + return `${this.PENDING_KEY_PREFIX}${userId}`; + } + + getUsers2FAInfo( + fctx: FlowExecCtx, + userId: string, + returnUndefined?: boolean, + ): ResultAsync { + logger.info("Getting user 2FA info", { ...fctx, userId }); + + return ResultAsync.fromPromise( + this.db.query.twoFactor.findFirst({ + where: eq(twoFactor.userId, userId), + }), + () => twofaErrors.dbError(fctx, "Failed to query 2FA info"), + ).andThen((found) => { + if (!found) { + logger.debug("2FA info not found for user", { + ...fctx, + userId, + }); + if (returnUndefined) { + return okAsync(undefined); + } + return errAsync(twofaErrors.notFound(fctx)); + } + logger.info("2FA info retrieved successfully", { ...fctx, userId }); + return okAsync(found as TwoFactor); + }); + } + + isSetupPending( + fctx: FlowExecCtx, + userId: string, + ): ResultAsync { + logger.debug("Checking if 2FA setup is pending", { ...fctx, userId }); + + return ResultAsync.fromPromise( + this.store.get(this.getKey(userId)), + () => + twofaErrors.dbError( + fctx, + "Failed to check setup pending status", + ), + ).map((found) => { + const isPending = !!found; + logger.debug("Setup pending status checked", { + ...fctx, + userId, + isPending, + }); + return isPending; + }); + } + + setup(fctx: FlowExecCtx, userId: string): ResultAsync { + logger.info("Starting 2FA setup", { ...fctx, userId }); + + return ResultAsync.fromSafePromise( + (async () => { + const secret = authenticator.generateSecret(); + const payload = { + secret, + lastUsedCode: "", + tries: 0, + } as TwoFaSetup; + await this.store.setex( + this.getKey(userId), + this.EXPIRY_TIME, + JSON.stringify(payload), + ); + logger.info("Created temp 2FA session", { + ...fctx, + userId, + expiresIn: this.EXPIRY_TIME, + }); + return secret; + })(), + ).mapErr(() => + twofaErrors.dbError(fctx, "Setting to data store failed"), + ); + } + + verifyAndEnable2FA( + fctx: FlowExecCtx, + userId: string, + code: string, + ): ResultAsync { + logger.info("Verifying and enabling 2FA", { ...fctx, userId }); + + return ResultAsync.fromPromise( + this.store.get(this.getKey(userId)), + () => twofaErrors.dbError(fctx, "Failed to get setup session"), + ) + .andThen((payload) => { + if (!payload) { + logger.error("Setup session not found", { + ...fctx, + userId, + }); + return errAsync(twofaErrors.setupNotFound(fctx)); + } + return okAsync(JSON.parse(payload) as TwoFaSetup); + }) + .andThen((payloadObj) => { + const key = this.getKey(userId); + + if (payloadObj.tries >= this.MAX_SETUP_ATTEMPTS) { + logger.warn("Max setup attempts reached", { + ...fctx, + userId, + tries: payloadObj.tries, + }); + return ResultAsync.fromPromise(this.store.del(key), () => + twofaErrors.dbError( + fctx, + "Failed to delete setup session", + ), + ).andThen(() => + errAsync(twofaErrors.maxAttemptsReached(fctx)), + ); + } + + if ( + !this.checkTotp(payloadObj.secret, code) || + code === payloadObj.lastUsedCode + ) { + logger.warn("Invalid 2FA code during setup", { + ...fctx, + userId, + tries: payloadObj.tries + 1, + codeReused: code === payloadObj.lastUsedCode, + }); + return ResultAsync.fromPromise( + this.store.setex( + key, + this.EXPIRY_TIME, + JSON.stringify({ + secret: payloadObj.secret, + lastUsedCode: code, + tries: payloadObj.tries + 1, + }), + ), + () => + twofaErrors.dbError( + fctx, + "Failed to update setup session", + ), + ).map(() => false); + } + + logger.info("2FA code verified successfully, enabling 2FA", { + ...fctx, + userId, + }); + + return ResultAsync.fromPromise(this.store.del(key), () => + twofaErrors.dbError(fctx, "Failed to delete setup session"), + ) + .andThen(() => + ResultAsync.fromPromise( + this.db + .insert(twoFactor) + .values({ + id: nanoid(), + secret: payloadObj.secret, + userId: userId, + createdAt: new Date(), + updatedAt: new Date(), + }) + .execute(), + () => + twofaErrors.dbError( + fctx, + "Failed to insert 2FA record", + ), + ), + ) + .map(() => { + logger.info("2FA enabled successfully", { + ...fctx, + userId, + }); + return true; + }); + }); + } + + disable(fctx: FlowExecCtx, userId: string): ResultAsync { + logger.info("Disabling 2FA", { ...fctx, userId }); + + return ResultAsync.fromPromise( + this.db + .delete(twoFactor) + .where(eq(twoFactor.userId, userId)) + .execute(), + () => twofaErrors.dbError(fctx, "Failed to delete 2FA record"), + ).map((result) => { + logger.info("2FA disabled successfully", { ...fctx, userId }); + return true; + }); + } + + generateBackupCodes( + fctx: FlowExecCtx, + userId: string, + ): ResultAsync { + logger.info("Generating backup codes", { ...fctx, userId }); + + return ResultAsync.fromPromise( + this.db.query.twoFactor.findFirst({ + where: eq(twoFactor.userId, userId), + }), + () => twofaErrors.dbError(fctx, "Failed to query 2FA info"), + ) + .andThen((found) => { + if (!found) { + logger.error("2FA not enabled for user", { + ...fctx, + userId, + }); + return errAsync(twofaErrors.backupCodesNotFound(fctx)); + } + if (found.backupCodes && found.backupCodes.length) { + logger.warn("Backup codes already generated", { + ...fctx, + userId, + }); + return errAsync( + twofaErrors.backupCodesAlreadyGenerated(fctx), + ); + } + return okAsync(found); + }) + .andThen(() => { + const codes = Array.from( + { length: this.DEFAULT_BACKUP_CODES_AMT }, + () => nanoid(12), + ); + + logger.debug("Backup codes generated, hashing", { + ...fctx, + userId, + count: codes.length, + }); + + return ResultAsync.fromPromise( + (async () => { + const hashed = []; + for (const code of codes) { + const hash = await hashString(code); + hashed.push(hash); + } + return { codes, hashed }; + })(), + () => + twofaErrors.dbError( + fctx, + "Failed to hash backup codes", + ), + ).andThen(({ codes, hashed }) => + ResultAsync.fromPromise( + this.db + .update(twoFactor) + .set({ backupCodes: hashed }) + .where(eq(twoFactor.userId, userId)) + .returning(), + () => + twofaErrors.dbError( + fctx, + "Failed to update backup codes", + ), + ).map(() => { + logger.info("Backup codes generated successfully", { + ...fctx, + userId, + }); + return codes; + }), + ); + }); + } + + get2FASecret( + fctx: FlowExecCtx, + userId: string, + ): ResultAsync { + logger.debug("Getting 2FA secret", { ...fctx, userId }); + + return ResultAsync.fromPromise( + this.db + .select() + .from(twoFactor) + .where(eq(twoFactor.userId, userId)) + .limit(1), + () => twofaErrors.dbError(fctx, "Failed to query 2FA secret"), + ).map((result) => { + if (!result.length) { + logger.debug("No 2FA secret found", { ...fctx, userId }); + return null; + } + logger.debug("2FA secret retrieved", { ...fctx, userId }); + return result[0].secret; + }); + } + + createSession( + fctx: FlowExecCtx, + params: { + userId: string; + sessionId: string; + ipAddress?: string; + userAgent?: string; + }, + ): ResultAsync { + logger.info("Creating 2FA verification session", { + ...fctx, + userId: params.userId, + sessionId: params.sessionId, + }); + + return ResultAsync.fromSafePromise( + (async () => { + const expiryMinutes = settings.twofaSessionExpiryMinutes || 10; + const now = new Date(); + const expiresAt = new Date( + now.getTime() + expiryMinutes * 60 * 1000, + ); + + return { expiresAt, now, params }; + })(), + ).andThen(({ expiresAt, now, params }) => + ResultAsync.fromPromise( + this.db + .insert(twofaSessions) + .values({ + id: nanoid(), + userId: params.userId, + sessionId: params.sessionId, + verificationToken: nanoid(32), + status: "pending", + attempts: 0, + maxAttempts: 5, + expiresAt, + createdAt: now, + ipAddress: params.ipAddress, + userAgent: params.userAgent, + }) + .returning(), + () => twofaErrors.dbError(fctx, "Failed to create 2FA session"), + ).map(([session]) => { + logger.info("2FA verification session created", { + ...fctx, + sessionId: session.id, + userId: params.userId, + }); + return session as TwoFaSession; + }), + ); + } + + getSessionByToken( + fctx: FlowExecCtx, + token: string, + ): ResultAsync { + logger.debug("Getting 2FA session by token", { ...fctx }); + + return ResultAsync.fromPromise( + this.db + .select() + .from(twofaSessions) + .where( + and( + eq(twofaSessions.verificationToken, token), + gt(twofaSessions.expiresAt, new Date()), + ), + ) + .limit(1), + () => twofaErrors.dbError(fctx, "Failed to query 2FA session"), + ).map((result) => { + if (!result.length) { + logger.warn("2FA session not found or expired", { ...fctx }); + return null; + } + logger.debug("2FA session found", { + ...fctx, + sessionId: result[0].id, + }); + return result[0] as TwoFaSession; + }); + } + + updateSession( + fctx: FlowExecCtx, + id: string, + updates: Partial< + Pick< + TwoFaSession, + "status" | "attempts" | "verifiedAt" | "codeUsed" + > + >, + ): ResultAsync { + logger.debug("Updating 2FA session", { + ...fctx, + sessionId: id, + updates, + }); + + return ResultAsync.fromPromise( + this.db + .update(twofaSessions) + .set(updates) + .where(eq(twofaSessions.id, id)) + .returning(), + () => twofaErrors.dbError(fctx, "Failed to update 2FA session"), + ).andThen(([session]) => { + if (!session) { + logger.error("2FA session not found for update", { + ...fctx, + sessionId: id, + }); + return errAsync(twofaErrors.sessionNotFoundById(fctx)); + } + logger.debug("2FA session updated successfully", { + ...fctx, + sessionId: id, + }); + return okAsync(session as TwoFaSession); + }); + } + + incrementAttempts( + fctx: FlowExecCtx, + id: string, + ): ResultAsync { + logger.debug("Incrementing session attempts", { + ...fctx, + sessionId: id, + }); + + return ResultAsync.fromPromise( + this.db.query.twofaSessions.findFirst({ + where: eq(twofaSessions.id, id), + columns: { id: true, attempts: true }, + }), + () => + twofaErrors.dbError( + fctx, + "Failed to query session for increment", + ), + ) + .andThen((s) => { + if (!s) { + logger.error("Session not found for increment", { + ...fctx, + sessionId: id, + }); + return errAsync(twofaErrors.sessionNotFoundById(fctx)); + } + return okAsync(s); + }) + .andThen((s) => + ResultAsync.fromPromise( + this.db + .update(twofaSessions) + .set({ attempts: s.attempts + 1 }) + .where(eq(twofaSessions.id, id)) + .returning(), + () => + twofaErrors.dbError( + fctx, + "Failed to increment attempts", + ), + ).andThen(([session]) => { + if (!session) { + logger.error("Session not found after increment", { + ...fctx, + sessionId: id, + }); + return errAsync(twofaErrors.sessionNotFoundById(fctx)); + } + + logger.warn("Failed verification attempt", { + ...fctx, + sessionId: session.id, + attempts: session.attempts, + }); + + return okAsync(session as TwoFaSession); + }), + ); + } + + cleanupExpiredSessions(fctx: FlowExecCtx): ResultAsync { + logger.info("Cleaning up expired 2FA sessions", { ...fctx }); + + return ResultAsync.fromPromise( + this.db + .delete(twofaSessions) + .where(lt(twofaSessions.expiresAt, new Date())), + () => + twofaErrors.dbError(fctx, "Failed to cleanup expired sessions"), + ).map((result) => { + const count = result.rowCount || 0; + logger.info("Expired sessions cleaned up", { ...fctx, count }); + return count; + }); + } +} diff --git a/packages/logic/domains/2fa/router.ts b/packages/logic/domains/2fa/router.ts new file mode 100644 index 0000000..d682399 --- /dev/null +++ b/packages/logic/domains/2fa/router.ts @@ -0,0 +1,170 @@ +import { + disable2FASchema, + enable2FACodeSchema, + startVerificationSchema, + verifyCodeSchema, +} from "./data"; +import { sValidator } from "@hono/standard-validator"; +import { HonoContext } from "@/core/hono.helpers"; +import { getTwofaController } from "./controller"; +import { auth } from "@domains/auth/config.base"; +import { Hono } from "hono"; + +const twofaController = getTwofaController(); + +export const twofaRouter = new Hono() + .post("/setup", async (c) => { + const res = await twofaController.setup2FA( + c.env.locals.fCtx, + c.env.locals.user, + ); + return c.json( + res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }, + res.isOk() ? 200 : 400, + ); + }) + .post( + "/verify-and-enable", + sValidator("json", enable2FACodeSchema), + async (c) => { + const data = c.req.valid("json"); + const res = await twofaController.verifyAndEnable2FA( + c.env.locals.fCtx, + c.env.locals.user, + data.code, + c.req.raw.headers, + ); + return c.json( + res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }, + res.isOk() ? 200 : 400, + ); + }, + ) + .get("/generate-backup-codes", async (c) => { + const res = await twofaController.generateBackupCodes( + c.env.locals.fCtx, + c.env.locals.user, + ); + return c.json( + res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }, + res.isOk() ? 200 : 400, + ); + }) + .delete("/disable", sValidator("json", disable2FASchema), async (c) => { + const data = c.req.valid("json"); + const res = await twofaController.disable( + c.env.locals.fCtx, + c.env.locals.user, + data.code, + ); + return c.json( + res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }, + res.isOk() ? 200 : 400, + ); + }) + .get("/requires-verification", async (c) => { + const user = c.env.locals.user; + const sessionId = c.req.query("sessionId")?.toString() ?? ""; + const res = await twofaController.requiresInitialVerification( + c.env.locals.fCtx, + user, + sessionId, + ); + return c.json( + res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }, + res.isOk() ? 200 : 400, + ); + }) + .get("/requires-sensitive-action", async (c) => { + const res = await twofaController.requiresSensitiveActionVerification( + c.env.locals.fCtx, + c.env.locals.user, + ); + return c.json( + res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }, + res.isOk() ? 200 : 400, + ); + }) + .post( + "/start-verification-session", + sValidator("json", startVerificationSchema), + async (c) => { + const data = c.req.valid("json"); + + const ipAddress = + c.req.header("x-forwarded-for") || + c.req.header("x-real-ip") || + "unknown"; + const userAgent = c.req.header("user-agent") || "unknown"; + + const res = await twofaController.startVerification( + c.env.locals.fCtx, + { + userId: data.userId, + sessionId: data.sessionId, + ipAddress, + userAgent, + }, + ); + + return c.json( + res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }, + ); + }, + ) + .post( + "/verify-session-code", + sValidator("json", verifyCodeSchema), + async (c) => { + const data = c.req.valid("json"); + + let user = c.env.locals.user; + if (!user) { + const out = await auth.api.getSession({ + headers: c.req.raw.headers, + }); + user = out?.user as any; + } + + const res = await twofaController.verifyCode( + c.env.locals.fCtx, + { + verificationSessToken: data.verificationToken, + code: data.code, + }, + user, + ); + + return c.json( + res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }, + res.isOk() ? 200 : 400, + ); + }, + ) + .post("/cleanup-expired-sessions", async (c) => { + const res = await twofaController.cleanupExpiredSessions( + c.env.locals.fCtx, + ); + return c.json( + res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }, + res.isOk() ? 200 : 400, + ); + }); diff --git a/packages/logic/domains/2fa/sensitive-actions.ts b/packages/logic/domains/2fa/sensitive-actions.ts new file mode 100644 index 0000000..54d4315 --- /dev/null +++ b/packages/logic/domains/2fa/sensitive-actions.ts @@ -0,0 +1,43 @@ +import { FlowExecCtx } from "@core/flow.execution.context"; +import { getTwofaController } from "./controller"; +import type { User } from "@/domains/user/data"; + +const twofaController = getTwofaController(); + +/** + * Check if user needs 2FA verification for sensitive actions + * Call this before executing sensitive operations like: + * - Changing password + * - Viewing billing info + * - Deleting account + * - etc. + */ +export async function requiresSensitiveAction2FA( + fctx: FlowExecCtx, + user: User, +): Promise { + const result = await twofaController.requiresSensitiveActionVerification( + fctx, + user, + ); + return result.match( + (data) => data, + () => true, // On error, require verification for security + ); +} + +export async function checkInitial2FaRequired( + fctx: FlowExecCtx, + user: User, + sessionId: string, +): Promise { + const result = await twofaController.requiresInitialVerification( + fctx, + user, + sessionId, + ); + return result.match( + (data) => data, + () => true, + ); +} diff --git a/packages/logic/domains/auth/config.base.ts b/packages/logic/domains/auth/config.base.ts new file mode 100644 index 0000000..9bf35d2 --- /dev/null +++ b/packages/logic/domains/auth/config.base.ts @@ -0,0 +1,205 @@ +import { + admin, + customSession, + magicLink, + multiSession, + username, +} from "better-auth/plugins"; +import { getUserController, UserController } from "../user/controller"; +import { AuthController, getAuthController } from "./controller"; +import { drizzleAdapter } from "better-auth/adapters/drizzle"; +import { FlowExecCtx } from "@/core/flow.execution.context"; +import { UserRoleMap } from "@domains/user/data"; +import { getRedisInstance } from "@pkg/redis"; +import { APIError } from "better-auth/api"; +import { settings } from "@core/settings"; +import { betterAuth } from "better-auth"; +import { logger } from "@pkg/logger"; +import { db, schema } from "@pkg/db"; +import { nanoid } from "nanoid"; + +// Constants +const EMAIL_EXPIRES_IN_MINS = 10; +const EMAIL_EXPIRES_IN_SECONDS = 60 * EMAIL_EXPIRES_IN_MINS; +const COOKIE_CACHE_MAX_AGE = 60 * 5; + +// Helper to create flow context for better-auth callbacks +function createAuthFlowContext(contextLabel: string): FlowExecCtx { + return { + flowId: `auth:${contextLabel}:${nanoid(10)}`, + }; +} + +// Singleton controller instances +let authControllerInstance: AuthController | null = null; +let userControllerInstance: UserController | null = null; + +function getAuthControllerInstance(): AuthController { + if (!authControllerInstance) { + authControllerInstance = getAuthController(); + } + return authControllerInstance; +} + +function getUserControllerInstance(): UserController { + if (!userControllerInstance) { + userControllerInstance = getUserController(); + } + return userControllerInstance; +} + +export const auth = betterAuth({ + trustedOrigins: ["http://localhost:5173", settings.betterAuthUrl], + advanced: { useSecureCookies: settings.nodeEnv === "production" }, + appName: settings.appName, + emailAndPassword: { + enabled: true, + disableSignUp: true, + requireEmailVerification: false, + }, + plugins: [ + customSession(async ({ user, session }) => { + session.id = session.token; + return { user, session }; + }), + username({ + minUsernameLength: 5, + maxUsernameLength: 20, + usernameValidator: async (username) => { + const fctx = createAuthFlowContext("username-check"); + const uc = getUserControllerInstance(); + + const result = await uc + .isUsernameAvailable(fctx, username) + .match( + (isAvailable) => ({ success: true, isAvailable }), + (error) => { + logger.error( + `[${fctx.flowId}] Failed to check username availability`, + error, + ); + return { success: false, isAvailable: false }; + }, + ); + + return result.isAvailable; + }, + }), + magicLink({ + expiresIn: EMAIL_EXPIRES_IN_SECONDS, + rateLimit: { window: 60, max: 4 }, + sendMagicLink: async ({ email, token, url }, request) => { + const fctx = createAuthFlowContext("magic-link"); + const ac = getAuthControllerInstance(); + + const result = await ac + .sendMagicLink(fctx, email, token, url) + .match( + () => ({ success: true, error: undefined }), + (error) => ({ success: false, error }), + ); + + if (!result.success || result?.error) { + logger.error( + `[${fctx.flowId}] Failed to send magic link`, + result.error, + ); + throw new APIError("INTERNAL_SERVER_ERROR", { + message: result.error?.message, + }); + } + }, + }), + admin({ + defaultRole: UserRoleMap.admin, + defaultBanReason: + "Stop fanum taxing the server bub, losing aura points fr", + defaultBanExpiresIn: 60 * 60 * 24, + }), + multiSession({ maximumSessions: 5 }), + ], + logger: { + log: (level, message, metadata) => { + logger.log(level, message, metadata); + }, + level: settings.isDevelopment ? "debug" : "info", + }, + database: drizzleAdapter(db, { provider: "pg", schema: { ...schema } }), + secondaryStorage: { + get: async (key) => { + const redis = getRedisInstance(); + return await redis.get(key); + }, + set: async (key, value, ttl) => { + const redis = getRedisInstance(); + if (ttl) { + await redis.setex(key, ttl, value); + } else { + await redis.set(key, value); + } + }, + delete: async (key) => { + const redis = getRedisInstance(); + const out = await redis.del(key); + if (!out && out !== 0) { + return null; + } + return out.toString() as any; + }, + }, + session: { + modelName: "session", + expiresIn: 60 * 60 * 24 * 7, + updateAge: 60 * 60 * 24, + cookieCache: { + enabled: true, + maxAge: COOKIE_CACHE_MAX_AGE, + }, + }, + user: { + changeEmail: { + enabled: true, + sendChangeEmailVerification: async ( + { user, newEmail, url, token }, + request, + ) => { + const fctx = createAuthFlowContext("email-change"); + const ac = getAuthControllerInstance(); + + const result = await ac + .sendEmailChangeVerificationEmail( + fctx, + newEmail, + token, + url, + ) + .match( + () => ({ success: true, error: undefined }), + (error) => ({ success: false, error }), + ); + + if (!result.success || result?.error) { + logger.error( + `[${fctx.flowId}] Failed to send email change verification`, + result.error, + ); + throw new APIError("INTERNAL_SERVER_ERROR", { + message: result.error?.message, + }); + } + }, + }, + modelName: "user", + additionalFields: { + onboardingDone: { + type: "boolean", + defaultValue: false, + required: false, + }, + last2FAVerifiedAt: { type: "date", required: false }, + parentId: { required: false, type: "string" }, + }, + }, +}); + +// - - - diff --git a/packages/logic/domains/auth/controller.ts b/packages/logic/domains/auth/controller.ts new file mode 100644 index 0000000..e42602a --- /dev/null +++ b/packages/logic/domains/auth/controller.ts @@ -0,0 +1,148 @@ +import { AuthContext, MiddlewareContext, MiddlewareOptions } from "better-auth"; +import { AccountRepository } from "../user/account.repository"; +import { FlowExecCtx } from "@/core/flow.execution.context"; +import { ResultAsync } from "neverthrow"; +import type { Err } from "@pkg/result"; +import { authErrors } from "./errors"; +import { logger } from "@pkg/logger"; +import { nanoid } from "nanoid"; +import { db } from "@pkg/db"; + +export class AuthController { + private readonly mins = 10; + + constructor(private accountRepo: AccountRepository) {} + + sendEmailChangeVerificationEmail( + fctx: FlowExecCtx, + newEmail: string, + token: string, + url: string, + ): ResultAsync { + logger.info("Sending email change verification link", { + ...fctx, + newEmail, + }); + logger.debug("Original URL", { ...fctx, url }); + + const transformedUrl = url + .replace("/api/auth/verify-email", "/account/verify-email") + .replace("/api/", "/"); + + logger.debug("Transformed URL", { ...fctx, transformedUrl }); + + // Simulate email sending with 90/10 success/failure + const success = Math.random() > 0.1; + + if (!success) { + logger.error("Failed to send email change verification link", { + ...fctx, + error: "Simulated email service failure", + }); + return ResultAsync.fromPromise( + Promise.reject( + authErrors.emailChangeVerificationFailed( + fctx, + "Simulated email service failure", + ), + ), + (error) => error as Err, + ); + } + + logger.info("Email change verification sent successfully", { + ...fctx, + newEmail, + }); + return ResultAsync.fromSafePromise(Promise.resolve(undefined)); + } + + swapAccountPasswordForTwoFactor( + fctx: FlowExecCtx, + ctx: MiddlewareContext< + MiddlewareOptions, + AuthContext & { returned?: unknown; responseHeaders?: Headers } + >, + ) { + logger.info("Swapping account password for 2FA", { + ...fctx, + }); + + if (!ctx.path.includes("two-factor")) { + return ResultAsync.fromSafePromise(Promise.resolve(ctx)); + } + + if (!ctx.body.password || ctx.body.password.length === 0) { + return ResultAsync.fromSafePromise(Promise.resolve(ctx)); + } + + logger.info("Rotating password for 2FA setup for user", { + ...fctx, + userId: ctx.body.userId, + }); + + return this.accountRepo + .rotatePassword(fctx, ctx.body.userId, nanoid()) + .mapErr((err) => { + logger.error("Failed to rotate password for 2FA", { + ...fctx, + error: err, + }); + return authErrors.passwordRotationFailed(fctx, err.detail); + }) + .map((newPassword) => { + logger.info("Password rotated successfully for 2FA setup", { + ...fctx, + }); + return { + ...ctx, + body: { ...ctx.body, password: newPassword }, + }; + }); + } + + sendMagicLink( + fctx: FlowExecCtx, + email: string, + token: string, + url: string, + ): ResultAsync { + logger.info("Sending magic link", { ...fctx, email }); + logger.debug("Original URL", { ...fctx, url }); + + const transformedUrl = url + .replace("/api/auth/magic-link/verify", "/auth/magic-link") + .replace("/api/", "/"); + + logger.debug("Transformed URL", { ...fctx, transformedUrl }); + + // Simulate email sending with 90/10 success/failure + const success = Math.random() > 0.1; + + if (!success) { + logger.error("Failed to send magic link email", { + ...fctx, + error: "Simulated email service failure", + }); + return ResultAsync.fromPromise( + Promise.reject( + authErrors.magicLinkEmailFailed( + fctx, + "Simulated email service failure", + ), + ), + (error) => error as Err, + ); + } + + logger.info("Magic link email sent successfully", { + ...fctx, + email, + }); + return ResultAsync.fromSafePromise(Promise.resolve(undefined)); + } +} + +export function getAuthController(): AuthController { + return new AuthController(new AccountRepository(db)); +} diff --git a/packages/logic/domains/auth/errors.ts b/packages/logic/domains/auth/errors.ts new file mode 100644 index 0000000..33c3bb1 --- /dev/null +++ b/packages/logic/domains/auth/errors.ts @@ -0,0 +1,59 @@ +import { FlowExecCtx } from "@/core/flow.execution.context"; +import { getError } from "@pkg/logger"; +import { ERROR_CODES, type Err } from "@pkg/result"; + +export const authErrors = { + emailSendFailed: (fctx: FlowExecCtx, detail: string): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.INTERNAL_SERVER_ERROR, + message: "Failed to send email", + description: "An error occurred while sending the email", + detail, + }), + + magicLinkEmailFailed: (fctx: FlowExecCtx, detail: string): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.INTERNAL_SERVER_ERROR, + message: "Failed to send magic link email", + description: "An error occurred while sending the magic link", + detail, + }), + + emailChangeVerificationFailed: (fctx: FlowExecCtx, detail: string): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.INTERNAL_SERVER_ERROR, + message: "Failed to send email change verification link", + description: "An error occurred while sending the verification email", + detail, + }), + + passwordRotationFailed: (fctx: FlowExecCtx, detail: string): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.INTERNAL_SERVER_ERROR, + message: "Failed to begin 2FA setup", + description: "An error occurred while rotating the password for 2FA", + detail, + }), + + dbError: (fctx: FlowExecCtx, detail: string): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.DATABASE_ERROR, + message: "Database operation failed", + description: "Please try again later", + detail, + }), + + accountNotFound: (fctx: FlowExecCtx): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.NOT_FOUND, + message: "Account not found", + description: "Please try again later", + detail: "Account not found for user", + }), +}; diff --git a/packages/logic/domains/notifications/controller.ts b/packages/logic/domains/notifications/controller.ts new file mode 100644 index 0000000..82c5bb9 --- /dev/null +++ b/packages/logic/domains/notifications/controller.ts @@ -0,0 +1,96 @@ +import { FlowExecCtx } from "@/core/flow.execution.context"; +import { okAsync } from "neverthrow"; +import { + NotificationFilters, + PaginationOptions, +} from "./data"; +import { NotificationRepository } from "./repository"; +import { db } from "@pkg/db"; + +export class NotificationController { + constructor(private notifsRepo: NotificationRepository) {} + + getNotifications( + fctx: FlowExecCtx, + filters: NotificationFilters, + pagination: PaginationOptions, + ) { + return this.notifsRepo.getNotifications(fctx, filters, pagination); + } + + markAsRead( + fctx: FlowExecCtx, + notificationIds: number[], + userId: string, + ) { + return this.notifsRepo.markAsRead(fctx, notificationIds, userId); + } + + markAsUnread( + fctx: FlowExecCtx, + notificationIds: number[], + userId: string, + ) { + return this.notifsRepo.markAsUnread(fctx, notificationIds, userId); + } + + archive( + fctx: FlowExecCtx, + notificationIds: number[], + userId: string, + ) { + return this.notifsRepo.archive(fctx, notificationIds, userId); + } + + unarchive( + fctx: FlowExecCtx, + notificationIds: number[], + userId: string, + ) { + return this.notifsRepo.unarchive(fctx, notificationIds, userId); + } + + deleteNotifications( + fctx: FlowExecCtx, + notificationIds: number[], + userId: string, + ) { + return this.notifsRepo.deleteNotifications(fctx, notificationIds, userId); + } + + getUnreadCount( + fctx: FlowExecCtx, + userId: string, + ) { + return this.notifsRepo.getUnreadCount(fctx, userId); + } + + markAllAsRead( + fctx: FlowExecCtx, + userId: string, + ) { + // Get all unread notification IDs for this user + const filters: NotificationFilters = { + userId, + isRead: false, + isArchived: false, + }; + + // Get a large number to handle bulk operations + const pagination: PaginationOptions = { page: 1, pageSize: 1000 }; + + return this.notifsRepo + .getNotifications(fctx, filters, pagination) + .map((paginated) => paginated.data.map((n) => n.id)) + .andThen((notificationIds) => { + if (notificationIds.length === 0) { + return okAsync(true); + } + return this.notifsRepo.markAsRead(fctx, notificationIds, userId); + }); + } +} + +export function getNotificationController(): NotificationController { + return new NotificationController(new NotificationRepository(db)); +} diff --git a/packages/logic/domains/notifications/data.ts b/packages/logic/domains/notifications/data.ts new file mode 100644 index 0000000..b95eefd --- /dev/null +++ b/packages/logic/domains/notifications/data.ts @@ -0,0 +1,102 @@ +import * as v from "valibot"; + +// Notification schema +export const notificationSchema = v.object({ + id: v.pipe(v.number(), v.integer()), + title: v.string(), + body: v.string(), + priority: v.string(), + type: v.string(), + category: v.string(), + isRead: v.boolean(), + isArchived: v.boolean(), + actionUrl: v.string(), + actionType: v.string(), + actionData: v.string(), + icon: v.string(), + userId: v.string(), + sentAt: v.date(), + readAt: v.nullable(v.date()), + expiresAt: v.nullable(v.date()), + createdAt: v.date(), + updatedAt: v.date(), +}); + +export type Notification = v.InferOutput; +export type Notifications = Notification[]; + +// Notification filters schema +export const notificationFiltersSchema = v.object({ + userId: v.string(), + isRead: v.optional(v.boolean()), + isArchived: v.optional(v.boolean()), + type: v.optional(v.string()), + category: v.optional(v.string()), + priority: v.optional(v.string()), + search: v.optional(v.string()), +}); +export type NotificationFilters = v.InferOutput< + typeof notificationFiltersSchema +>; + +// Pagination options schema +export const paginationOptionsSchema = v.object({ + page: v.pipe(v.number(), v.integer()), + pageSize: v.pipe(v.number(), v.integer()), + sortBy: v.optional(v.string()), + sortOrder: v.optional(v.string()), +}); +export type PaginationOptions = v.InferOutput; + +// Paginated notifications schema +export const paginatedNotificationsSchema = v.object({ + data: v.array(notificationSchema), + total: v.pipe(v.number(), v.integer()), + page: v.pipe(v.number(), v.integer()), + pageSize: v.pipe(v.number(), v.integer()), + totalPages: v.pipe(v.number(), v.integer()), +}); +export type PaginatedNotifications = v.InferOutput< + typeof paginatedNotificationsSchema +>; + +// Get notifications schema +export const getNotificationsSchema = v.object({ + filters: notificationFiltersSchema, + pagination: paginationOptionsSchema, +}); +export type GetNotifications = v.InferOutput; + +// Bulk notification IDs schema +export const bulkNotificationIdsSchema = v.object({ + notificationIds: v.array(v.pipe(v.number(), v.integer())), +}); +export type BulkNotificationIds = v.InferOutput< + typeof bulkNotificationIdsSchema +>; + +// View Model specific types +export const clientNotificationFiltersSchema = v.object({ + userId: v.string(), + isRead: v.optional(v.boolean()), + isArchived: v.optional(v.boolean()), + type: v.optional(v.string()), + category: v.optional(v.string()), + priority: v.optional(v.string()), + search: v.optional(v.string()), +}); +export type ClientNotificationFilters = v.InferOutput< + typeof clientNotificationFiltersSchema +>; + +export const clientPaginationStateSchema = v.object({ + page: v.pipe(v.number(), v.integer()), + pageSize: v.pipe(v.number(), v.integer()), + total: v.pipe(v.number(), v.integer()), + totalPages: v.pipe(v.number(), v.integer()), + sortBy: v.picklist(["createdAt", "sentAt", "readAt", "priority"]), + sortOrder: v.picklist(["asc", "desc"]), +}); +export type ClientPaginationState = v.InferOutput< + typeof clientPaginationStateSchema +>; diff --git a/packages/logic/domains/notifications/errors.ts b/packages/logic/domains/notifications/errors.ts new file mode 100644 index 0000000..d924146 --- /dev/null +++ b/packages/logic/domains/notifications/errors.ts @@ -0,0 +1,78 @@ +import { FlowExecCtx } from "@/core/flow.execution.context"; +import { ERROR_CODES, type Err } from "@pkg/result"; +import { getError } from "@pkg/logger"; + +export const notificationErrors = { + dbError: (fctx: FlowExecCtx, detail: string): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.DATABASE_ERROR, + message: "Database operation failed", + description: "Please try again later", + detail, + }), + + getNotificationsFailed: (fctx: FlowExecCtx, detail: string): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.DATABASE_ERROR, + message: "Failed to fetch notifications", + description: "Please try again later", + detail, + }), + + markAsReadFailed: (fctx: FlowExecCtx, detail: string): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.DATABASE_ERROR, + message: "Failed to mark notifications as read", + description: "Please try again later", + detail, + }), + + markAsUnreadFailed: (fctx: FlowExecCtx, detail: string): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.DATABASE_ERROR, + message: "Failed to mark notifications as unread", + description: "Please try again later", + detail, + }), + + archiveFailed: (fctx: FlowExecCtx, detail: string): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.DATABASE_ERROR, + message: "Failed to archive notifications", + description: "Please try again later", + detail, + }), + + unarchiveFailed: (fctx: FlowExecCtx, detail: string): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.DATABASE_ERROR, + message: "Failed to unarchive notifications", + description: "Please try again later", + detail, + }), + + deleteNotificationsFailed: (fctx: FlowExecCtx, detail: string): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.DATABASE_ERROR, + message: "Failed to delete notifications", + description: "Please try again later", + detail, + }), + + getUnreadCountFailed: (fctx: FlowExecCtx, detail: string): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.DATABASE_ERROR, + message: "Failed to get unread count", + description: "Please try again later", + detail, + }), +}; + diff --git a/packages/logic/domains/notifications/repository.ts b/packages/logic/domains/notifications/repository.ts new file mode 100644 index 0000000..e121267 --- /dev/null +++ b/packages/logic/domains/notifications/repository.ts @@ -0,0 +1,384 @@ +import { and, asc, count, Database, desc, eq, like, or, sql } from "@pkg/db"; +import { notifications } from "@pkg/db/schema"; +import { ResultAsync } from "neverthrow"; +import { FlowExecCtx } from "@core/flow.execution.context"; +import type { + Notification, + NotificationFilters, + PaginatedNotifications, + PaginationOptions, +} from "./data"; +import { type Err } from "@pkg/result"; +import { notificationErrors } from "./errors"; +import { logger } from "@pkg/logger"; + +export class NotificationRepository { + constructor(private db: Database) {} + + getNotifications( + fctx: FlowExecCtx, + filters: NotificationFilters, + pagination: PaginationOptions, + ): ResultAsync { + logger.info("Getting notifications with filters", { ...fctx, filters }); + + const { userId, isRead, isArchived, type, category, priority, search } = + filters; + const { + page, + pageSize, + sortBy = "createdAt", + sortOrder = "desc", + } = pagination; + + // Build WHERE conditions + const conditions = [eq(notifications.userId, userId)]; + + if (isRead !== undefined) { + conditions.push(eq(notifications.isRead, isRead)); + } + + if (isArchived !== undefined) { + conditions.push(eq(notifications.isArchived, isArchived)); + } + + if (type) { + conditions.push(eq(notifications.type, type)); + } + + if (category) { + conditions.push(eq(notifications.category, category)); + } + + if (priority) { + conditions.push(eq(notifications.priority, priority)); + } + + if (search) { + conditions.push( + or( + like(notifications.title, `%${search}%`), + like(notifications.body, `%${search}%`), + )!, + ); + } + + const whereClause = and(...conditions); + + return ResultAsync.fromPromise( + this.db.select({ count: count() }).from(notifications).where(whereClause), + (error) => { + logger.error("Failed to get notifications count", { + ...fctx, + error, + }); + return notificationErrors.getNotificationsFailed( + fctx, + error instanceof Error ? error.message : String(error), + ); + }, + ).andThen((totalResult) => { + const total = totalResult[0]?.count || 0; + const offset = (page - 1) * pageSize; + + // Map sortBy to proper column + const getOrderColumn = (sortBy: string) => { + switch (sortBy) { + case "createdAt": + return notifications.createdAt; + case "sentAt": + return notifications.sentAt; + case "readAt": + return notifications.readAt; + case "priority": + return notifications.priority; + default: + return notifications.createdAt; + } + }; + + const orderColumn = getOrderColumn(sortBy); + const orderFunc = sortOrder === "asc" ? asc : desc; + + return ResultAsync.fromPromise( + this.db + .select() + .from(notifications) + .where(whereClause) + .orderBy(orderFunc(orderColumn)) + .limit(pageSize) + .offset(offset), + (error) => { + logger.error("Failed to get notifications data", { + ...fctx, + error, + }); + return notificationErrors.getNotificationsFailed( + fctx, + error instanceof Error ? error.message : String(error), + ); + }, + ).map((data) => { + const totalPages = Math.ceil(total / pageSize); + logger.info("Retrieved notifications", { + ...fctx, + count: data.length, + page, + totalPages, + }); + + return { + data: data as Notification[], + total, + page, + pageSize, + totalPages, + }; + }); + }); + } + + markAsRead( + fctx: FlowExecCtx, + notificationIds: number[], + userId: string, + ): ResultAsync { + logger.info("Marking notifications as read", { + ...fctx, + notificationIds, + userId, + }); + + return ResultAsync.fromPromise( + this.db + .update(notifications) + .set({ + isRead: true, + readAt: new Date(), + updatedAt: new Date(), + }) + .where( + and( + eq(notifications.userId, userId), + sql`${notifications.id} = ANY(${notificationIds})`, + ), + ), + (error) => { + logger.error("Failed to mark notifications as read", { + ...fctx, + error, + }); + return notificationErrors.markAsReadFailed( + fctx, + error instanceof Error ? error.message : String(error), + ); + }, + ).map(() => { + logger.info("Notifications marked as read successfully", { + ...fctx, + notificationIds, + }); + return true; + }); + } + + markAsUnread( + fctx: FlowExecCtx, + notificationIds: number[], + userId: string, + ): ResultAsync { + logger.info("Marking notifications as unread", { + ...fctx, + notificationIds, + userId, + }); + + return ResultAsync.fromPromise( + this.db + .update(notifications) + .set({ + isRead: false, + readAt: null, + updatedAt: new Date(), + }) + .where( + and( + eq(notifications.userId, userId), + sql`${notifications.id} = ANY(${notificationIds})`, + ), + ), + (error) => { + logger.error("Failed to mark notifications as unread", { + ...fctx, + error, + }); + return notificationErrors.markAsUnreadFailed( + fctx, + error instanceof Error ? error.message : String(error), + ); + }, + ).map(() => { + logger.info("Notifications marked as unread successfully", { + ...fctx, + notificationIds, + }); + return true; + }); + } + + archive( + fctx: FlowExecCtx, + notificationIds: number[], + userId: string, + ): ResultAsync { + logger.info("Archiving notifications", { + ...fctx, + notificationIds, + userId, + }); + + return ResultAsync.fromPromise( + this.db + .update(notifications) + .set({ + isArchived: true, + updatedAt: new Date(), + }) + .where( + and( + eq(notifications.userId, userId), + sql`${notifications.id} = ANY(${notificationIds})`, + ), + ), + (error) => { + logger.error("Failed to archive notifications", { + ...fctx, + error, + }); + return notificationErrors.archiveFailed( + fctx, + error instanceof Error ? error.message : String(error), + ); + }, + ).map(() => { + logger.info("Notifications archived successfully", { + ...fctx, + notificationIds, + }); + return true; + }); + } + + unarchive( + fctx: FlowExecCtx, + notificationIds: number[], + userId: string, + ): ResultAsync { + logger.info("Unarchiving notifications", { + ...fctx, + notificationIds, + userId, + }); + + return ResultAsync.fromPromise( + this.db + .update(notifications) + .set({ + isArchived: false, + updatedAt: new Date(), + }) + .where( + and( + eq(notifications.userId, userId), + sql`${notifications.id} = ANY(${notificationIds})`, + ), + ), + (error) => { + logger.error("Failed to unarchive notifications", { + ...fctx, + error, + }); + return notificationErrors.unarchiveFailed( + fctx, + error instanceof Error ? error.message : String(error), + ); + }, + ).map(() => { + logger.info("Notifications unarchived successfully", { + ...fctx, + notificationIds, + }); + return true; + }); + } + + deleteNotifications( + fctx: FlowExecCtx, + notificationIds: number[], + userId: string, + ): ResultAsync { + logger.info("Deleting notifications", { + ...fctx, + notificationIds, + userId, + }); + + return ResultAsync.fromPromise( + this.db + .delete(notifications) + .where( + and( + eq(notifications.userId, userId), + sql`${notifications.id} = ANY(${notificationIds})`, + ), + ), + (error) => { + logger.error("Failed to delete notifications", { + ...fctx, + error, + }); + return notificationErrors.deleteNotificationsFailed( + fctx, + error instanceof Error ? error.message : String(error), + ); + }, + ).map(() => { + logger.info("Notifications deleted successfully", { + ...fctx, + notificationIds, + }); + return true; + }); + } + + getUnreadCount( + fctx: FlowExecCtx, + userId: string, + ): ResultAsync { + logger.info("Getting unread count", { ...fctx, userId }); + + return ResultAsync.fromPromise( + this.db + .select({ count: count() }) + .from(notifications) + .where( + and( + eq(notifications.userId, userId), + eq(notifications.isRead, false), + eq(notifications.isArchived, false), + ), + ), + (error) => { + logger.error("Failed to get unread count", { ...fctx, error }); + return notificationErrors.getUnreadCountFailed( + fctx, + error instanceof Error ? error.message : String(error), + ); + }, + ).map((result) => { + const count = result[0]?.count || 0; + logger.info("Retrieved unread count", { ...fctx, count }); + return count; + }); + } +} diff --git a/packages/logic/domains/notifications/router.ts b/packages/logic/domains/notifications/router.ts new file mode 100644 index 0000000..0e2781e --- /dev/null +++ b/packages/logic/domains/notifications/router.ts @@ -0,0 +1,160 @@ +import { bulkNotificationIdsSchema, getNotificationsSchema } from "./data"; +import { getNotificationController } from "./controller"; +import { sValidator } from "@hono/standard-validator"; +import { HonoContext } from "@core/hono.helpers"; +import { Hono } from "hono"; + +const nc = getNotificationController(); + +export const notificationsRouter = new Hono() + .get("/", async (c) => { + const fctx = c.env.locals.fCtx; + const userId = c.env.locals.user.id; + const url = new URL(c.req.url); + + const filters = { + userId, + isRead: url.searchParams.get("isRead") + ? url.searchParams.get("isRead") === "true" + : undefined, + isArchived: url.searchParams.get("isArchived") + ? url.searchParams.get("isArchived") === "true" + : undefined, + type: url.searchParams.get("type") || undefined, + category: url.searchParams.get("category") || undefined, + priority: url.searchParams.get("priority") || undefined, + search: url.searchParams.get("search") || undefined, + }; + + const pagination = { + page: parseInt(url.searchParams.get("page") || "1"), + pageSize: parseInt(url.searchParams.get("pageSize") || "20"), + sortBy: url.searchParams.get("sortBy") || "createdAt", + sortOrder: url.searchParams.get("sortOrder") || "desc", + }; + + const res = await nc.getNotifications(fctx, filters, pagination); + return c.json( + res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }, + res.isOk() ? 200 : 400, + ); + }) + .post( + "/get-notifications", + sValidator("json", getNotificationsSchema), + async (c) => { + const fctx = c.env.locals.fCtx; + const data = c.req.valid("json"); + const res = await nc.getNotifications(fctx, data.filters, data.pagination); + return c.json( + res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }, + res.isOk() ? 200 : 400, + ); + }, + ) + .put( + "/mark-read", + sValidator("json", bulkNotificationIdsSchema), + async (c) => { + const fctx = c.env.locals.fCtx; + const data = c.req.valid("json"); + const userId = c.env.locals.user.id; + const res = await nc.markAsRead(fctx, [...data.notificationIds], userId); + return c.json( + res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }, + res.isOk() ? 200 : 400, + ); + }, + ) + .put( + "/mark-unread", + sValidator("json", bulkNotificationIdsSchema), + async (c) => { + const fctx = c.env.locals.fCtx; + const data = c.req.valid("json"); + const userId = c.env.locals.user.id; + const res = await nc.markAsUnread(fctx, [...data.notificationIds], userId); + return c.json( + res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }, + res.isOk() ? 200 : 400, + ); + }, + ) + .put( + "/archive", + sValidator("json", bulkNotificationIdsSchema), + async (c) => { + const fctx = c.env.locals.fCtx; + const data = c.req.valid("json"); + const userId = c.env.locals.user.id; + const res = await nc.archive(fctx, [...data.notificationIds], userId); + return c.json( + res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }, + res.isOk() ? 200 : 400, + ); + }, + ) + .put( + "/unarchive", + sValidator("json", bulkNotificationIdsSchema), + async (c) => { + const fctx = c.env.locals.fCtx; + const data = c.req.valid("json"); + const userId = c.env.locals.user.id; + const res = await nc.unarchive(fctx, [...data.notificationIds], userId); + return c.json( + res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }, + res.isOk() ? 200 : 400, + ); + }, + ) + .delete( + "/delete", + sValidator("json", bulkNotificationIdsSchema), + async (c) => { + const fctx = c.env.locals.fCtx; + const data = c.req.valid("json"); + const userId = c.env.locals.user.id; + const res = await nc.deleteNotifications(fctx, [...data.notificationIds], userId); + return c.json( + res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }, + res.isOk() ? 200 : 400, + ); + }, + ) + .put("/mark-all-read", async (c) => { + const fctx = c.env.locals.fCtx; + const userId = c.env.locals.user.id; + const res = await nc.markAllAsRead(fctx, userId); + return c.json( + res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }, + res.isOk() ? 200 : 400, + ); + }) + .get("/unread-count", async (c) => { + const fctx = c.env.locals.fCtx; + const userId = c.env.locals.user.id; + const res = await nc.getUnreadCount(fctx, userId); + return c.json( + res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }, + res.isOk() ? 200 : 400, + ); + }); diff --git a/packages/logic/domains/user/account.repository.ts b/packages/logic/domains/user/account.repository.ts new file mode 100644 index 0000000..7a05071 --- /dev/null +++ b/packages/logic/domains/user/account.repository.ts @@ -0,0 +1,213 @@ +import { FlowExecCtx } from "@/core/flow.execution.context"; +import { ERROR_CODES, type Err } from "@pkg/result"; +import { getError, logger } from "@pkg/logger"; +import { auth } from "../auth/config.base"; +import { account } from "@pkg/db/schema"; +import { ResultAsync } from "neverthrow"; +import { Database, eq } from "@pkg/db"; +import { nanoid } from "nanoid"; + +export class AccountRepository { + constructor(private db: Database) {} + + private dbError(fctx: FlowExecCtx, detail: string): Err { + return getError({ + flowId: fctx.flowId, + code: ERROR_CODES.DATABASE_ERROR, + message: "Database operation failed", + description: "Please try again later", + detail, + }); + } + + private accountNotFound(fctx: FlowExecCtx): Err { + return getError({ + flowId: fctx.flowId, + code: ERROR_CODES.NOT_FOUND, + message: "Account not found", + description: "Please try again later", + detail: "Account not found for user", + }); + } + + ensureAccountExists( + fctx: FlowExecCtx, + userId: string, + ): ResultAsync { + logger.info("Checking if account exists for user", { + ...fctx, + userId, + }); + + return ResultAsync.fromPromise( + this.db.query.account.findFirst({ + where: eq(account.userId, userId), + }), + (error) => { + logger.error("Failed to check account existence", { + ...fctx, + error, + }); + return this.dbError( + fctx, + error instanceof Error ? error.message : String(error), + ); + }, + ).andThen((existingAccount) => { + if (existingAccount) { + logger.info("Account already exists for user", { + ...fctx, + userId, + }); + return ResultAsync.fromSafePromise(Promise.resolve(true)); + } + + logger.info( + "Account does not exist, creating new account for user", + { + ...fctx, + userId, + }, + ); + + return ResultAsync.fromPromise( + auth.$context.then((ctx) => ctx.password.hash(nanoid())), + (error) => { + logger.error("Failed to hash password", { + ...fctx, + error, + }); + return this.dbError( + fctx, + error instanceof Error ? error.message : String(error), + ); + }, + ).andThen((password) => { + const aid = nanoid(); + + return ResultAsync.fromPromise( + this.db + .insert(account) + .values({ + id: aid, + accountId: userId, + providerId: "credential", + userId: userId, + password, + createdAt: new Date(), + updatedAt: new Date(), + }) + .execute(), + (error) => { + logger.error("Failed to create account", { + ...fctx, + error, + }); + return this.dbError( + fctx, + error instanceof Error + ? error.message + : String(error), + ); + }, + ).map(() => { + logger.info("Account created successfully for user", { + ...fctx, + userId, + }); + return false; + }); + }); + }); + } + + rotatePassword( + fctx: FlowExecCtx, + userId: string, + password: string, + ): ResultAsync { + logger.info("Starting password rotation for user", { + ...fctx, + userId, + }); + + return ResultAsync.fromPromise( + this.db.query.account.findFirst({ + where: eq(account.userId, userId), + }), + (error) => { + logger.error( + "Failed to check account existence for password rotation", + { + ...fctx, + error, + }, + ); + return this.dbError( + fctx, + error instanceof Error ? error.message : String(error), + ); + }, + ).andThen((existingAccount) => { + if (!existingAccount) { + logger.error("Account not found for user", { + ...fctx, + userId, + }); + return ResultAsync.fromSafePromise( + Promise.resolve(this.accountNotFound(fctx)), + ).andThen((err) => + ResultAsync.fromSafePromise(Promise.reject(err)), + ); + } + + return ResultAsync.fromPromise( + auth.$context.then((ctx) => ctx.password.hash(password)), + (error) => { + logger.error("Failed to hash password for rotation", { + ...fctx, + error, + }); + return this.dbError( + fctx, + error instanceof Error ? error.message : String(error), + ); + }, + ).andThen((hashed) => { + logger.info("Updating user's password in database", { + ...fctx, + }); + + return ResultAsync.fromPromise( + this.db + .update(account) + .set({ password: hashed }) + .where(eq(account.userId, userId)) + .returning() + .execute(), + (error) => { + logger.error("Failed to update password", { + ...fctx, + error, + }); + return this.dbError( + fctx, + error instanceof Error + ? error.message + : String(error), + ); + }, + ).map((result) => { + logger.info("User's password updated successfully", { + ...fctx, + }); + logger.debug("Password rotation result", { + ...fctx, + result, + }); + return password; + }); + }); + }); + } +} diff --git a/packages/logic/domains/user/controller.ts b/packages/logic/domains/user/controller.ts new file mode 100644 index 0000000..b432ff0 --- /dev/null +++ b/packages/logic/domains/user/controller.ts @@ -0,0 +1,55 @@ +import { FlowExecCtx } from "@/core/flow.execution.context"; +import { AccountRepository } from "./account.repository"; +import { UserRepository } from "./repository"; +import { db } from "@pkg/db"; + +export class UserController { + constructor( + private userRepository: UserRepository, + private accountRepo: AccountRepository, + ) {} + + getUserInfo(fctx: FlowExecCtx, userId: string) { + return this.userRepository.getUserInfo(fctx, userId); + } + + ensureAccountExists(fctx: FlowExecCtx, userId: string) { + return this.accountRepo.ensureAccountExists(fctx, userId); + } + + isUsernameAvailable(fctx: FlowExecCtx, username: string) { + return this.userRepository.isUsernameAvailable(fctx, username); + } + + updateLastVerified2FaAtToNow(fctx: FlowExecCtx, userId: string) { + return this.userRepository.updateLastVerified2FaAtToNow(fctx, userId); + } + + banUser( + fctx: FlowExecCtx, + userId: string, + reason: string, + banExpiresAt: Date, + ) { + return this.userRepository.banUser(fctx, userId, reason, banExpiresAt); + } + + isUserBanned(fctx: FlowExecCtx, userId: string) { + return this.userRepository.isUserBanned(fctx, userId); + } + + getBanInfo(fctx: FlowExecCtx, userId: string) { + return this.userRepository.getBanInfo(fctx, userId); + } + + rotatePassword(fctx: FlowExecCtx, userId: string, password: string) { + return this.accountRepo.rotatePassword(fctx, userId, password); + } +} + +export function getUserController(): UserController { + return new UserController( + new UserRepository(db), + new AccountRepository(db), + ); +} diff --git a/packages/logic/domains/user/data.ts b/packages/logic/domains/user/data.ts new file mode 100644 index 0000000..70e660c --- /dev/null +++ b/packages/logic/domains/user/data.ts @@ -0,0 +1,159 @@ +import { Session } from "better-auth"; +import * as v from "valibot"; + +export type { Session } from "better-auth"; + +export type ModifiedSession = Session & { isCurrent?: boolean }; + +// User role enum +export enum UserRoleMap { + user = "user", + admin = "admin", +} + +// User role schema +export const userRoleSchema = v.picklist(["user", "admin"]); +export type UserRole = v.InferOutput; + +// User schema +export const userSchema = v.object({ + id: v.string(), + name: v.string(), + email: v.string(), + emailVerified: v.boolean(), + image: v.optional(v.string()), + createdAt: v.date(), + updatedAt: v.date(), + username: v.optional(v.string()), + displayUsername: v.optional(v.string()), + role: v.optional(v.string()), + banned: v.optional(v.boolean()), + banReason: v.optional(v.string()), + banExpires: v.optional(v.date()), + onboardingDone: v.optional(v.boolean()), + last2FAVerifiedAt: v.optional(v.date()), + parentId: v.optional(v.string()), +}); +export type User = v.InferOutput; + +// Account schema +export const accountSchema = v.object({ + id: v.string(), + accountId: v.string(), + providerId: v.string(), + userId: v.string(), + accessToken: v.string(), + refreshToken: v.string(), + idToken: v.string(), + accessTokenExpiresAt: v.date(), + refreshTokenExpiresAt: v.date(), + scope: v.string(), + password: v.string(), + createdAt: v.date(), + updatedAt: v.date(), +}); +export type Account = v.InferOutput; + +// Ensure account exists schema +export const ensureAccountExistsSchema = v.object({ + userId: v.string(), +}); +export type EnsureAccountExists = v.InferOutput< + typeof ensureAccountExistsSchema +>; + +// Ban info schema +export const banInfoSchema = v.object({ + banned: v.boolean(), + reason: v.optional(v.string()), + expires: v.optional(v.date()), +}); +export type BanInfo = v.InferOutput; + +// Ban user schema +export const banUserSchema = v.object({ + userId: v.string(), + reason: v.string(), + banExpiresAt: v.date(), +}); +export type BanUser = v.InferOutput; + +// Check username availability schema +export const checkUsernameSchema = v.object({ + username: v.string(), +}); +export type CheckUsername = v.InferOutput; + +// Rotate password schema +export const rotatePasswordSchema = v.object({ + userId: v.string(), + password: v.string(), +}); +export type RotatePassword = v.InferOutput; + +// View Model specific types + +// Search and filter types +export const searchFieldSchema = v.picklist(["email", "name", "username"]); +export type SearchField = v.InferOutput; + +export const searchOperatorSchema = v.picklist([ + "contains", + "starts_with", + "ends_with", +]); +export type SearchOperator = v.InferOutput; + +export const filterOperatorSchema = v.picklist([ + "eq", + "ne", + "lt", + "lte", + "gt", + "gte", +]); +export type FilterOperator = v.InferOutput; + +export const sortDirectionSchema = v.picklist(["asc", "desc"]); +export type SortDirection = v.InferOutput; + +// Users query state +export const usersQueryStateSchema = v.object({ + // searching + searchValue: v.optional(v.string()), + searchField: v.optional(searchFieldSchema), + searchOperator: v.optional(searchOperatorSchema), + + // pagination + limit: v.pipe(v.number(), v.integer()), + offset: v.pipe(v.number(), v.integer()), + + // sorting + sortBy: v.optional(v.string()), + sortDirection: v.optional(sortDirectionSchema), + + // filtering + filterField: v.optional(v.string()), + filterValue: v.optional(v.union([v.string(), v.number(), v.boolean()])), + filterOperator: v.optional(filterOperatorSchema), +}); +export type UsersQueryState = v.InferOutput; + +// UI View Model types + +export const banExpiryModeSchema = v.picklist([ + "never", + "1d", + "7d", + "30d", + "custom", +]); +export type BanExpiryMode = v.InferOutput; + +export const createUserFormSchema = v.object({ + email: v.string(), + password: v.string(), + name: v.string(), + role: v.union([userRoleSchema, v.array(userRoleSchema)]), +}); +export type CreateUserForm = v.InferOutput; diff --git a/packages/logic/domains/user/errors.ts b/packages/logic/domains/user/errors.ts new file mode 100644 index 0000000..ad178b8 --- /dev/null +++ b/packages/logic/domains/user/errors.ts @@ -0,0 +1,77 @@ +import { FlowExecCtx } from "@/core/flow.execution.context"; +import { ERROR_CODES, type Err } from "@pkg/result"; +import { getError } from "@pkg/logger"; + +export const userErrors = { + dbError: (fctx: FlowExecCtx, detail: string): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.DATABASE_ERROR, + message: "Database operation failed", + description: "Please try again later", + detail, + }), + + userNotFound: (fctx: FlowExecCtx): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.NOT_FOUND, + message: "User not found", + description: "Try with a different user id", + detail: "User not found in database", + }), + + usernameCheckFailed: (fctx: FlowExecCtx, detail: string): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.DATABASE_ERROR, + message: "An error occurred while checking username availability", + description: "Try again later", + detail, + }), + + banOperationFailed: (fctx: FlowExecCtx, detail: string): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.DATABASE_ERROR, + message: "Failed to perform ban operation", + description: "Please try again later", + detail, + }), + + unbanFailed: (fctx: FlowExecCtx, detail: string): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.DATABASE_ERROR, + message: "Failed to unban user", + description: "Please try again later", + detail, + }), + + updateFailed: (fctx: FlowExecCtx, detail: string): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.DATABASE_ERROR, + message: "Failed to update user", + description: "Please try again later", + detail, + }), + + getUserInfoFailed: (fctx: FlowExecCtx, detail: string): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.DATABASE_ERROR, + message: "An error occurred while getting user info", + description: "Try again later", + detail, + }), + + getBanInfoFailed: (fctx: FlowExecCtx, detail: string): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.DATABASE_ERROR, + message: "An error occurred while getting ban info", + description: "Try again later", + detail, + }), +}; diff --git a/packages/logic/domains/user/repository.ts b/packages/logic/domains/user/repository.ts new file mode 100644 index 0000000..240badb --- /dev/null +++ b/packages/logic/domains/user/repository.ts @@ -0,0 +1,289 @@ +import { ResultAsync, errAsync, okAsync } from "neverthrow"; +import { FlowExecCtx } from "@core/flow.execution.context"; +import { type Err } from "@pkg/result"; +import { Database, eq } from "@pkg/db"; +import { BanInfo, User } from "./data"; +import { user } from "@pkg/db/schema"; +import { userErrors } from "./errors"; +import { logger } from "@pkg/logger"; + +export class UserRepository { + constructor(private db: Database) {} + + getUserInfo(fctx: FlowExecCtx, userId: string): ResultAsync { + logger.info("Getting user info for user", { + flowId: fctx.flowId, + userId, + }); + + return ResultAsync.fromPromise( + this.db.query.user.findFirst({ + where: eq(user.id, userId), + }), + (error) => { + logger.error("Failed to get user info", { + flowId: fctx.flowId, + error, + }); + return userErrors.getUserInfoFailed( + fctx, + error instanceof Error ? error.message : String(error), + ); + }, + ).andThen((userData) => { + if (!userData) { + logger.error("User not found with id", { + flowId: fctx.flowId, + userId, + }); + return errAsync(userErrors.userNotFound(fctx)); + } + + logger.info("User info retrieved successfully for user", { + flowId: fctx.flowId, + userId, + }); + return okAsync(userData as User); + }); + } + + updateLastVerified2FaAtToNow( + fctx: FlowExecCtx, + userId: string, + ): ResultAsync { + logger.info("Updating last 2FA verified timestamp for user", { + flowId: fctx.flowId, + userId, + }); + + return ResultAsync.fromPromise( + this.db + .update(user) + .set({ last2FAVerifiedAt: new Date() }) + .where(eq(user.id, userId)) + .execute(), + (error) => { + logger.error("Failed to update last 2FA verified timestamp", { + ...fctx, + error, + }); + return userErrors.updateFailed( + fctx, + error instanceof Error ? error.message : String(error), + ); + }, + ).map(() => { + logger.info("Last 2FA verified timestamp updated successfully", { + ...fctx, + }); + return true; + }); + } + + isUsernameAvailable( + fctx: FlowExecCtx, + username: string, + ): ResultAsync { + logger.info("Checking username availability", { + ...fctx, + username, + }); + + return ResultAsync.fromPromise( + this.db.query.user.findFirst({ + where: eq(user.username, username), + }), + (error) => { + logger.error("Failed to check username availability", { + ...fctx, + error, + }); + return userErrors.usernameCheckFailed( + fctx, + error instanceof Error ? error.message : String(error), + ); + }, + ).map((existingUser) => { + const isAvailable = !existingUser?.id; + logger.info("Username availability checked", { + ...fctx, + username, + isAvailable, + }); + return isAvailable; + }); + } + + banUser( + fctx: FlowExecCtx, + userId: string, + reason: string, + banExpiresAt: Date, + ): ResultAsync { + logger.info("Banning user", { + ...fctx, + userId, + banExpiresAt: banExpiresAt.toISOString(), + reason, + }); + + return ResultAsync.fromPromise( + this.db + .update(user) + .set({ + banned: true, + banReason: reason, + banExpires: banExpiresAt, + }) + .where(eq(user.id, userId)) + .execute(), + (error) => { + logger.error("Failed to ban user", { ...fctx, error }); + return userErrors.banOperationFailed( + fctx, + error instanceof Error ? error.message : String(error), + ); + }, + ).map(() => { + logger.info("User has been banned", { + ...fctx, + userId, + banExpiresAt: banExpiresAt.toISOString(), + }); + return true; + }); + } + + isUserBanned(fctx: FlowExecCtx, userId: string): ResultAsync { + logger.info("Checking ban status for user", { ...fctx, userId }); + + return ResultAsync.fromPromise( + this.db.query.user.findFirst({ + where: eq(user.id, userId), + columns: { + banned: true, + banExpires: true, + }, + }), + (error) => { + logger.error("Failed to check ban status", { + ...fctx, + error, + }); + return userErrors.dbError( + fctx, + error instanceof Error ? error.message : String(error), + ); + }, + ).andThen((userData) => { + if (!userData) { + logger.error("User not found when checking ban status", { + ...fctx, + }); + return errAsync(userErrors.userNotFound(fctx)); + } + + // If not banned, return false + if (!userData.banned) { + logger.info("User is not banned", { ...fctx, userId }); + return okAsync(false); + } + + // If banned but no expiry date, consider permanently banned + if (!userData.banExpires) { + logger.info("User is permanently banned", { ...fctx, userId }); + return okAsync(true); + } + + const now = new Date(); + if (userData.banExpires <= now) { + logger.info("User ban has expired, removing ban status", { + ...fctx, + userId, + }); + + return ResultAsync.fromPromise( + this.db + .update(user) + .set({ + banned: false, + banReason: null, + banExpires: null, + }) + .where(eq(user.id, userId)) + .execute(), + (error) => { + logger.error("Failed to unban user after expiry", { + ...fctx, + error, + }); + return userErrors.unbanFailed( + fctx, + error instanceof Error + ? error.message + : String(error), + ); + }, + ) + .map(() => { + logger.info("User has been unbanned after expiry", { + ...fctx, + userId, + }); + return false; + }) + .orElse((error) => { + logger.error( + "Failed to unban user after expiry, still returning banned status", + { ...fctx, userId, error }, + ); + // Still return banned status since we couldn't update + return okAsync(true); + }); + } + + logger.info("User is banned", { + ...fctx, + userId, + banExpires: userData.banExpires.toISOString(), + }); + return okAsync(true); + }); + } + + getBanInfo(fctx: FlowExecCtx, userId: string): ResultAsync { + logger.info("Getting ban info for user", { ...fctx, userId }); + + return ResultAsync.fromPromise( + this.db.query.user.findFirst({ + where: eq(user.id, userId), + columns: { banned: true, banReason: true, banExpires: true }, + }), + (error) => { + logger.error("Failed to get ban info", { ...fctx, error }); + return userErrors.getBanInfoFailed( + fctx, + error instanceof Error ? error.message : String(error), + ); + }, + ).andThen((userData) => { + if (!userData) { + logger.error("User not found when getting ban info", { + ...fctx, + }); + return errAsync(userErrors.userNotFound(fctx)); + } + + logger.info("Ban info retrieved successfully for user", { + ...fctx, + userId, + }); + + return okAsync({ + banned: userData.banned || false, + reason: userData.banReason || undefined, + expires: userData.banExpires || undefined, + }); + }); + } +} diff --git a/packages/logic/domains/user/router.ts b/packages/logic/domains/user/router.ts new file mode 100644 index 0000000..6e9e91e --- /dev/null +++ b/packages/logic/domains/user/router.ts @@ -0,0 +1,165 @@ +import { + banUserSchema, + checkUsernameSchema, + ensureAccountExistsSchema, + rotatePasswordSchema, +} from "./data"; +import { HonoContext } from "@core/hono.helpers"; +import { sValidator } from "@hono/standard-validator"; +import { getUserController } from "./controller"; +import { Hono } from "hono"; + +const uc = getUserController(); + +export const usersRouter = new Hono() + // Get current user info + .get("/me", async (c) => { + const fctx = c.env.locals.fCtx; + const userId = c.env.locals.user?.id; + + if (!userId) { + return c.json( + { + error: { + code: "UNAUTHORIZED", + message: "User not authenticated", + description: "Please log in", + detail: "No user ID found in session", + }, + }, + 401, + ); + } + + const res = await uc.getUserInfo(fctx, userId); + return c.json( + res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }, + res.isOk() ? 200 : 400, + ); + }) + + // Get user info by ID + .get("/:userId", async (c) => { + const fctx = c.env.locals.fCtx; + const userId = c.req.param("userId"); + + const res = await uc.getUserInfo(fctx, userId); + return c.json( + res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }, + res.isOk() ? 200 : 400, + ); + }) + + // Ensure account exists + .put( + "/ensure-account-exists", + sValidator("json", ensureAccountExistsSchema), + async (c) => { + const fctx = c.env.locals.fCtx; + const data = c.req.valid("json"); + + const res = await uc.ensureAccountExists(fctx, data.userId); + return c.json( + res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }, + res.isOk() ? 200 : 400, + ); + }, + ) + + // Check username availability + .post( + "/check-username", + sValidator("json", checkUsernameSchema), + async (c) => { + const fctx = c.env.locals.fCtx; + const data = c.req.valid("json"); + + const res = await uc.isUsernameAvailable(fctx, data.username); + return c.json( + res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }, + res.isOk() ? 200 : 400, + ); + }, + ) + + // Update last 2FA verification time + .put("/update-2fa-verified/:userId", async (c) => { + const fctx = c.env.locals.fCtx; + const userId = c.req.param("userId"); + + const res = await uc.updateLastVerified2FaAtToNow(fctx, userId); + return c.json( + res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }, + res.isOk() ? 200 : 400, + ); + }) + + // Ban user + .post("/ban", sValidator("json", banUserSchema), async (c) => { + const fctx = c.env.locals.fCtx; + const data = c.req.valid("json"); + + const res = await uc.banUser(fctx, data.userId, data.reason, data.banExpiresAt); + return c.json( + res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }, + res.isOk() ? 200 : 400, + ); + }) + + // Check if user is banned + .get("/:userId/is-banned", async (c) => { + const fctx = c.env.locals.fCtx; + const userId = c.req.param("userId"); + + const res = await uc.isUserBanned(fctx, userId); + return c.json( + res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }, + res.isOk() ? 200 : 400, + ); + }) + + // Get ban info + .get("/:userId/ban-info", async (c) => { + const fctx = c.env.locals.fCtx; + const userId = c.req.param("userId"); + + const res = await uc.getBanInfo(fctx, userId); + return c.json( + res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }, + res.isOk() ? 200 : 400, + ); + }) + + // Rotate password + .put( + "/rotate-password", + sValidator("json", rotatePasswordSchema), + async (c) => { + const fctx = c.env.locals.fCtx; + const data = c.req.valid("json"); + + const res = await uc.rotatePassword(fctx, data.userId, data.password); + return c.json( + res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }, + res.isOk() ? 200 : 400, + ); + }, + ); diff --git a/packages/logic/package.json b/packages/logic/package.json new file mode 100644 index 0000000..194bacf --- /dev/null +++ b/packages/logic/package.json @@ -0,0 +1,40 @@ +{ + "name": "@pkg/logic", + "scripts": { + "auth:schemagen": "bun x @better-auth/cli generate --config ./domains/auth/config.base.ts --output ../../packages/db/schema/better.auth.schema.ts" + }, + "dependencies": { + "@hono/standard-validator": "^0.2.1", + "@pkg/db": "workspace:*", + "@pkg/logger": "workspace:*", + "@pkg/redis": "workspace:*", + "@pkg/result": "workspace:*", + "@pkg/settings": "workspace:*", + "@types/pdfkit": "^0.14.0", + "argon2": "^0.43.0", + "better-auth": "^1.4.7", + "date-fns-tz": "^3.2.0", + "dotenv": "^16.5.0", + "hono": "^4.11.1", + "imapflow": "^1.0.188", + "mailparser": "^3.7.3", + "nanoid": "^5.1.5", + "neverthrow": "^8.2.0", + "otplib": "^12.0.1", + "pdfkit": "^0.17.1", + "tmp": "^0.2.3", + "uuid": "^11.1.0", + "valibot": "^1.2.0", + "xlsx": "^0.18.5" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/imapflow": "^1.0.22", + "@types/mailparser": "^3.4.6", + "@types/tmp": "^0.2.6", + "@types/uuid": "^10.0.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } +} diff --git a/packages/logic/tsconfig.json b/packages/logic/tsconfig.json new file mode 100644 index 0000000..3c8de5f --- /dev/null +++ b/packages/logic/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "strict": true, + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx", + "baseUrl": ".", + "paths": { + "@/*": ["./*"], + "@domains/*": ["./domains/*"], + "@core/*": ["./core/*"] + }, + "moduleResolution": "bundler", + "module": "esnext", + "target": "esnext" + } +} diff --git a/packages/redis/index.ts b/packages/redis/index.ts new file mode 100644 index 0000000..ce48f0c --- /dev/null +++ b/packages/redis/index.ts @@ -0,0 +1,18 @@ +import { Redis } from "ioredis"; +export * from "ioredis"; + +let redis: Redis | undefined; + +let defaultRedisUrl = process.env.REDIS_URL ?? ""; + +export function getRedisInstance(url: string = defaultRedisUrl) { + if (redis) { + return redis; + } + redis = new Redis(url, { + lazyConnect: true, + connectTimeout: 5000, + commandTimeout: 5000, + }); + return redis; +} diff --git a/packages/redis/package.json b/packages/redis/package.json new file mode 100644 index 0000000..c134105 --- /dev/null +++ b/packages/redis/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pkg/redis", + "module": "index.ts", + "type": "module", + "private": true, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "ioredis": "^5.6.1" + } +} diff --git a/packages/result/index.ts b/packages/result/index.ts new file mode 100644 index 0000000..4de398e --- /dev/null +++ b/packages/result/index.ts @@ -0,0 +1,81 @@ +export const ERROR_CODES = { + API_ERROR: "API_ERROR", + EXTERNAL_API_ERROR: "EXTERNAL_API_ERROR", + RATE_LIMIT_ERROR: "RATE_LIMIT_ERROR", + DATABASE_ERROR: "DATABASE_ERROR", + NETWORK_ERROR: "NETWORK_ERROR", + BANNED: "BANNED", + AUTH_ERROR: "AUTH_ERROR", + PERMISSION_ERROR: "PERMISSION_ERROR", + VALIDATION_ERROR: "VALIDATION_ERROR", + UNKNOWN_ERROR: "UNKNOWN_ERROR", + NOT_FOUND_ERROR: "NOT_FOUND_ERROR", + NOT_FOUND: "NOT_FOUND", + INPUT_ERROR: "INPUT_ERROR", + INTERNAL_SERVER_ERROR: "INTERNAL_SERVER_ERROR", + EXTERNAL_SERVICE_ERROR: "EXTERNAL_SERVICE_ERROR", + FILE_SYSTEM_ERROR: "FILE_SYSTEM_ERROR", + STORAGE_ERROR: "STORAGE_ERROR", + NOT_ALLOWED: "NOT_ALLOWED", + NOT_IMPLEMENTED: "NOT_IMPLEMENTED", + PROCESSING_ERROR: "PROCESSING_ERROR", + PARSING_ERROR: "PARSING_ERROR", +} as const; + +export const errorStatusMap = { + [ERROR_CODES.VALIDATION_ERROR]: 400, + [ERROR_CODES.AUTH_ERROR]: 403, + [ERROR_CODES.BANNED]: 403, + [ERROR_CODES.NOT_FOUND]: 404, + [ERROR_CODES.NOT_ALLOWED]: 405, + [ERROR_CODES.RATE_LIMIT_ERROR]: 429, + [ERROR_CODES.DATABASE_ERROR]: 500, + [ERROR_CODES.NETWORK_ERROR]: 500, + [ERROR_CODES.EXTERNAL_API_ERROR]: 500, + [ERROR_CODES.API_ERROR]: 500, + [ERROR_CODES.INTERNAL_SERVER_ERROR]: 500, + [ERROR_CODES.EXTERNAL_SERVICE_ERROR]: 500, + [ERROR_CODES.FILE_SYSTEM_ERROR]: 500, + [ERROR_CODES.STORAGE_ERROR]: 500, + [ERROR_CODES.PROCESSING_ERROR]: 500, + [ERROR_CODES.PARSING_ERROR]: 500, + [ERROR_CODES.NOT_IMPLEMENTED]: 501, +} as Record; + +export type Err = { + flowId?: string; + code: string; + message: string; + description: string; + detail: string; + actionable?: boolean; + error?: any; +}; + +type Success = { data: T; error?: undefined | null }; +type Failure = { data?: undefined | null; error: E }; + +// Legacy now, making use of Effect throughout the project +export type Result = Success | Failure; + +export async function tryCatch( + promise: Promise, + err?: E, +): Promise> { + try { + const data = await promise; + return { data }; + } catch (e) { + return { + // @ts-ignore + error: !!err + ? err + : { + code: "UNKNOWN_ERROR", + message: "An unknown error occurred", + description: "An unknown error occurred", + detail: "An unknown error occurred", + }, + }; + } +} diff --git a/packages/result/package.json b/packages/result/package.json new file mode 100644 index 0000000..051f65c --- /dev/null +++ b/packages/result/package.json @@ -0,0 +1,9 @@ +{ + "name": "@pkg/result", + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } +} diff --git a/packages/settings/index.ts b/packages/settings/index.ts new file mode 100644 index 0000000..d6f361f --- /dev/null +++ b/packages/settings/index.ts @@ -0,0 +1,178 @@ +import * as v from "valibot"; + +import "dotenv/config"; + +/** + * Settings schema using Valibot for validation + */ +export const settingsSchema = v.object({ + appName: v.string(), + nodeEnv: v.string(), + logLevel: v.string(), + + isDevelopment: v.optional(v.boolean()), + + redisUrl: v.string(), + databaseUrl: v.string(), + + internalApiKey: v.string(), + + processorApiUrl: v.string(), + + betterAuthUrl: v.string(), + betterAuthSecret: v.string(), + + twofaSessionExpiryMinutes: v.optional(v.number()), + twofaRequiredHours: v.optional(v.number()), + + defaultAdminEmail: v.string(), + + googleClientId: v.string(), + googleClientSecret: v.string(), + + resendApiKey: v.string(), + fromEmail: v.string(), + + qstashUrl: v.string(), + qstashToken: v.string(), + qstashCurrentSigningKey: v.string(), + qstashNextSigningKey: v.string(), + + axiomDatasetName: v.string(), + axiomApiToken: v.string(), + + // R2/Object Storage settings + r2BucketName: v.string(), + r2Region: v.string(), + r2Endpoint: v.string(), + r2AccessKey: v.string(), + r2SecretKey: v.string(), + r2PublicUrl: v.optional(v.string()), + + // File upload settings + maxFileSize: v.number(), + allowedMimeTypes: v.array(v.string()), + allowedExtensions: v.array(v.string()), +}); + +export type Settings = v.InferOutput; + +/** + * Helper to get environment variable with default value + */ +function getEnv(key: string, defaultValue: string = ""): string { + return process.env[key] ?? defaultValue; +} + +/** + * Helper to get environment variable as number with default value + */ +function getEnvNumber(key: string, defaultValue: number): number { + const value = process.env[key]; + if (!value) return defaultValue; + const parsed = Number(value); + return Number.isNaN(parsed) ? defaultValue : parsed; +} + +/** + * Parse comma-separated string into array + */ +function parseCommaSeparated(value: string): string[] { + return value + .split(",") + .map((item) => item.trim()) + .filter((item) => item.length > 0); +} + +/** + * Load and validate settings from environment variables + */ +function loadSettings(): Settings { + const nodeEnv = getEnv("NODE_ENV", "development"); + + const rawSettings = { + appName: getEnv("APP_NAME", "App"), + nodeEnv, + logLevel: getEnv("LOG_LEVEL", "info"), + + isDevelopment: nodeEnv === "development", + + redisUrl: getEnv("REDIS_URL", "redis://localhost:6379"), + databaseUrl: getEnv("DATABASE_URL"), + + internalApiKey: getEnv("INTERNAL_API_KEY"), + + processorApiUrl: getEnv("PROCESSOR_API_URL", "http://localhost:3000"), + + betterAuthUrl: getEnv("BETTER_AUTH_URL"), + betterAuthSecret: getEnv("BETTER_AUTH_SECRET"), + + twofaSessionExpiryMinutes: getEnvNumber( + "TWOFA_SESSION_EXPIRY_MINUTES", + 10, + ), + twofaRequiredHours: getEnvNumber("TWOFA_REQUIRED_HOURS", 24), + + defaultAdminEmail: getEnv("DEFAULT_ADMIN_EMAIL"), + + googleClientId: getEnv("GOOGLE_CLIENT_ID"), + googleClientSecret: getEnv("GOOGLE_CLIENT_SECRET"), + + resendApiKey: getEnv("RESEND_API_KEY"), + fromEmail: getEnv("FROM_EMAIL"), + + qstashUrl: getEnv("QSTASH_URL"), + qstashToken: getEnv("QSTASH_TOKEN"), + qstashCurrentSigningKey: getEnv("QSTASH_CURRENT_SIGNING_KEY"), + qstashNextSigningKey: getEnv("QSTASH_NEXT_SIGNING_KEY"), + + axiomDatasetName: getEnv("AXIOM_DATASET_NAME"), + axiomApiToken: getEnv("AXIOM_API_TOKEN"), + + // R2/Object Storage settings + r2BucketName: getEnv("R2_BUCKET_NAME"), + r2Region: getEnv("R2_REGION", "auto"), + r2Endpoint: getEnv("R2_ENDPOINT"), + r2AccessKey: getEnv("R2_ACCESS_KEY"), + r2SecretKey: getEnv("R2_SECRET_KEY"), + r2PublicUrl: getEnv("R2_PUBLIC_URL") || undefined, + + // File upload settings + maxFileSize: getEnvNumber("MAX_FILE_SIZE", 10485760), // 10MB default + allowedMimeTypes: parseCommaSeparated( + getEnv( + "ALLOWED_MIME_TYPES", + "image/jpeg,image/png,image/webp,image/gif,application/pdf,text/plain", + ), + ), + allowedExtensions: parseCommaSeparated( + getEnv("ALLOWED_EXTENSIONS", "jpg,jpeg,png,webp,gif,pdf,txt"), + ), + }; + + try { + return v.parse(settingsSchema, rawSettings); + } catch (error) { + console.error("❌ Settings validation failed:"); + if (error instanceof v.ValiError) { + for (const issue of error.issues) { + console.error( + ` - ${issue.path?.map((p: any) => p.key).join(".")}: ${issue.message}`, + ); + } + } else { + console.error(error); + } + throw new Error( + "Failed to load settings. Check environment variables.", + ); + } +} + +export const settings = loadSettings(); + +export const getSetting = (key: K): Settings[K] => { + return settings[key]; +}; + +console.log(`✅ Settings loaded | ${settings.appName} (${settings.nodeEnv})`); diff --git a/packages/settings/package.json b/packages/settings/package.json new file mode 100644 index 0000000..c2f357e --- /dev/null +++ b/packages/settings/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pkg/settings", + "module": "index.ts", + "type": "module", + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "dotenv": "^17.2.3", + "valibot": "^1.2.0" + } +} diff --git a/scripts/migrate.sh b/scripts/migrate.sh new file mode 100755 index 0000000..14ee203 --- /dev/null +++ b/scripts/migrate.sh @@ -0,0 +1,7 @@ +echo "🔄 Migrating Primary DB" + +cd packages/db + +bun run db:migrate + +cd ../../ diff --git a/scripts/populate.env.sh b/scripts/populate.env.sh new file mode 100755 index 0000000..0879bb6 --- /dev/null +++ b/scripts/populate.env.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +# copy over the environment variables from the base /.env file to all other apps & packages in apps/* and packages/* + +for dir in apps/*; do + if [ -d "$dir" ]; then + cp .env $dir/.env + fi +done + +for dir in packages/*; do + if [ -d "$dir" ]; then + cp .env $dir/.env + fi +done diff --git a/scripts/prod.start.sh b/scripts/prod.start.sh new file mode 100755 index 0000000..622cd27 --- /dev/null +++ b/scripts/prod.start.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +APP_PATH=$1 + +if [ -z "$APP_PATH" ]; then + echo "Usage: prod.start.sh " + exit 1 +fi + +echo "Starting $APP_PATH" + +cd $APP_PATH + +bun run prod diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6360d8f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "strictNullChecks": true + } +} \ No newline at end of file diff --git a/turbo.json b/turbo.json new file mode 100644 index 0000000..321813c --- /dev/null +++ b/turbo.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://turbo.build/schema.json", + "ui": "tui", + "tasks": { + "build": { + "dependsOn": ["^build"], + "inputs": ["$TURBO_DEFAULT$", ".env*"], + "outputs": ["build/**"] + }, + "dev": { + "cache": false, + "persistent": true + }, + "prod": { + "cache": false + }, + "db:migrate": { + "cache": false + }, + "test": { + "cache": false + } + } +}