commit 8c45efc92e2adb23095f45aaeb3bc89f4d205256 Author: user Date: Fri Mar 27 20:06:38 2026 +0200 initttt 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..a2515b0 --- /dev/null +++ b/.env.example @@ -0,0 +1,47 @@ +APP_NAME=${{project.APP_NAME}} +NODE_ENV=${{project.NODE_ENV}} +LOG_LEVEL=${{project.LOG_LEVEL}} + +REDIS_URL=${{project.REDIS_URL}} +DATABASE_URL=${{project.DATABASE_URL}} + +INTERNAL_API_KEY=${{project.INTERNAL_API_KEY}} +DEBUG_KEY=${{project.DEBUG_KEY}} + +PUBLIC_URL=${{project.PUBLIC_URL}} + +PROCESSOR_API_URL=${{project.PROCESSOR_API_URL}} +APP_BUILDER_API_URL=${{project.APP_BUILDER_API_URL}} +APP_BUILDER_ASSETS_PUBLIC_URL=${{project.APP_BUILDER_ASSETS_PUBLIC_URL}} + +CLIENT_DOWNLOADED_APK_NAME=${{project.CLIENT_DOWNLOADED_APK_NAME}} + +MOBILE_APP_API_URL=${{project.MOBILE_APP_API_URL}} + +BETTER_AUTH_SECRET=${{project.BETTER_AUTH_SECRET}} +BETTER_AUTH_URL=${{project.BETTER_AUTH_URL}} + +TWOFA_SECRET=${{project.TWOFA_SECRET}} +TWO_FA_SESSION_EXPIRY_MINUTES=${{project.TWO_FA_SESSION_EXPIRY_MINUTES}} +TWO_FA_REQUIRED_HOURS=${{project.TWO_FA_REQUIRED_HOURS}} + +DEFAULT_ADMIN_EMAIL=${{project.DEFAULT_ADMIN_EMAIL}} +DEFAULT_ADMIN_PASSWORD=${{project.DEFAULT_ADMIN_PASSWORD}} + +OTEL_SERVICE_NAME=${{project.OTEL_SERVICE_NAME}} +OTEL_TRACES_EXPORTER=${{project.OTEL_TRACES_EXPORTER}} +OTEL_EXPORTER_OTLP_HTTP_ENDPOINT=${{project.OTEL_EXPORTER_OTLP_HTTP_ENDPOINT}} +OTEL_EXPORTER_OTLP_ENDPOINT=${{project.OTEL_EXPORTER_OTLP_ENDPOINT}} +OTEL_EXPORTER_OTLP_PROTOCOL=${{project.OTEL_EXPORTER_OTLP_PROTOCOL}} +OTEL_RESOURCE_ATTRIBUTES=${{project.OTEL_RESOURCE_ATTRIBUTES}} + +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..2a865cc --- /dev/null +++ b/.gitignore @@ -0,0 +1,84 @@ +.zed + +mobile/**/*.iml +mobile/**/.gradle +mobile/**/local.properties +mobile/**/.idea/caches +mobile/**/.idea/libraries +mobile/**/.idea/modules.xml +mobile/**/.idea/workspace.xml +mobile/**/.idea/navEditor.xml +mobile/**/.idea/assetWizardSettings.xml +mobile/**/.DS_Store +mobile/**/build +mobile/**/captures +mobile/**/.externalNativeBuild +mobile/**/.cxx +mobile/**/local.properties + + +# Dependencies +node_modules +.pnp +.pnp.js + +__pycache__ +.venv + +secret.md + +# 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 + +**.apk + +creds.md + +onlydevs diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8727886 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,198 @@ +# AGENTS.md + +This document defines the laws, principles, and rule sets that govern this codebase. Any agent or developer making changes has to adhere to these rules. + +**Before starting off** — Read the README.md file to understand the project's goals and objectives. + +--- + +## Agent Rules (Override Everything) + +1. **No testing by yourself** — All testing is done by the team. +2. **No assumptions about code or domain logic** — Always confirm and be sure before making changes. +3. **No running scripts** — Do not run build, dev, test, or migrate scripts unless explicitly approved. +4. **No touching migration files** — Do not mess with the `migrations` sql dir, as those are generated manually via drizzle orm + +More rules are only to be added by the human, in case such a suggestion becomes viable. + +--- + +## 1. Project Overview + +- **Monorepo**: Turbo repo +- **Package Manager**: pnpm +- **Language**: TypeScript everywhere +- **Node**: >= 24 + +### Applications + +| App | Purpose | +| ---------------- | ----------------------------------------------------------------------------------------------------------------- | +| `apps/main` | SvelteKit UI — the primary user-facing application | +| `apps/processor` | Hono web server — intended for asynchronous processing (jobs, workers). Currently minimal; structure is evolving. | + +### Packages + +Packages live under `packages/` and are **standalone, modular** pieces consumed by apps: + +| Package | Purpose | +| --------------- | --------------------------------------------- | +| `@pkg/logic` | Domain logic (DDD, controllers, repositories) | +| `@pkg/db` | Drizzle schema, database access | +| `@pkg/logger` | Logging, `getError` for error construction | +| `@pkg/result` | Result type, `ERROR_CODES`, `tryCatch` | +| `@pkg/keystore` | Redis instance (sessions, 2FA, etc.) | +| `@pkg/settings` | App settings / env config | + +### Data Stores + +- **PostgreSQL (Drizzle)** — Primary relational data (auth, users, accounts, etc.) +- **Redis (Valkey)** — Sessions, 2FA verification state (via `@pkg/keystore`) + +Additional stores (NoSQL DBs, R2, etc.) may be introduced later. Follow existing domain patterns when adding new data access. + +--- + +## 2. The Logic Package: DDD + Layered Architecture + +The `@pkg/logic` package contains **all domain logic**. It follows: + +1. **Domain-Driven Design (DDD)** — Bounded contexts as domains +2. **Layered architecture** — Clear separation of concerns +3. **Result-style error handling** — Errors as values; avoid try-catch in domain code + +### Domain Structure + +Each domain is a folder under `packages/logic/domains/`: + +``` +domains/ + / + data.ts # Types, schemas (Valibot) + repository.ts # Data access + controller.ts # Use cases / application logic + errors.ts # Domain-specific error constructors (using getError) +``` + +The logic package is **pure domain logic** — no HTTP routes or routers. API exposure is handled by the main app via SvelteKit remote functions. Auth uses `config.base.ts` with better-auth. Add new domains as needed; mirror existing patterns. + +### Path Aliases (logic package) + +- `@/*` → `./*` +- `@domains/*` → `./domains/*` +- `@core/*` → `./core/*` + +### Flow Execution Context + +Domain operations receive a `FlowExecCtx` (`fctx`) for tracing and audit: + +```ts +type FlowExecCtx = { + flowId: string; + userId?: string; + sessionId?: string; +}; +``` + +--- + +## 3. Error Handling: Result Pattern (neverthrow) + +Errors are **values**, not exceptions. The codebase uses Result-style handling. + +### Current Conventions + +1. **Logic package** — Uses `neverthrow` (`ResultAsync`, `okAsync`, `errAsync`) for async operations that may fail. +2. **`@pkg/result`** — Provides `Result`, `ERROR_CODES`, and `tryCatch()`. The `Result` type is legacy; So don't reach for it primarily. +3. Use `ERROR_CODES` for consistent error codes. +4. **`getError()`** — From `@pkg/logger`. Use at boundaries when converting a thrown error to an `Err` object: + `return getError({ code: ERROR_CODES.XXX, message: "...", description: "...", detail: "..." }, e)`. +5. **Domain errors** — Each domain has an `errors.ts` that exports error constructors built with `getError`. Use these instead of ad-hoc error objects. +6. **Check before use** — Always check `result.isOk()` / `result.isErr()` before using `result.value`; never assume success. + +### Err Shape + +```ts +type Err = { + flowId?: string; + code: string; + message: string; + description: string; + detail: string; + actionable?: boolean; + error?: any; + // Flexible, but more "defined base fields" in the future +}; +``` + +--- + +## 4. Frontend (Main App) + +The main app is a **SvelteKit** application with a domain-driven UI structure. + +### Structure + +- **Routes**: File-based routing under `src/routes/`. Layout groups (e.g. `(main)`, `auth`) wrap related pages. +- **Domain-driven UI**: Feature code lives under `src/lib/domains//` — each domain has its own folder with view models, components, and remote functions. +- **View Model (VM) pattern**: Domain logic and state for a screen live in `*.vm.svelte.ts` classes. VMs hold reactive state (`$state`), orchestrate remote function calls, and expose methods. Pages import and use a VM instance. + +### SvelteKit Remote Functions + +The main app uses **SvelteKit remote functions** as the primary API layer — replacing Hono routers in the logic package. Each domain has a `*.remote.ts` file that exposes `query` (reads) and `command` (writes) functions, called directly from VMs. Auth context and `FlowExecCtx` are built inside each remote function from `event.locals` via helpers in `$lib/core/server.utils`. + +Naming convention: `*SQ` for queries, `*SC` for commands. + +### Global Stores + +Shared state (`user`, `session`, `breadcrumbs`) lives in `$lib/global.stores.ts`. + +### Conventions + +- Pages are thin: they mount a VM, render components, and wire up lifecycle. +- VMs own async flows, polling, and error handling for their domain. +- VMs call remote functions directly; remote functions invoke logic controllers. +- UI components under `$lib/components/` are shared; domain-specific components live in `$lib/domains//`. + +--- + +## 5. Processor App + +The processor is a **Hono** server intended for **background work** and async jobs. Its structure is still evolving and it is to be updated soon. + +When logic is added, processing logic should live under `src/domains//` and call into `@pkg/logic` controllers and repositories. + +--- + +## 6. Observability + +The stack uses **OpenTelemetry** end-to-end for traces, logs, and metrics, shipped to a **SigNoz** instance (via OTel Collector). + +### How it fits together + +- **`apps/main`** bootstraps the OTel SDK in `instrumentation.server.ts` (auto-instrumentation via `@opentelemetry/sdk-node`). SvelteKit's `tracing` and `instrumentation` experimental flags wire this into the request lifecycle. +- **`@pkg/logger`** ships Winston logs to OTel via `OpenTelemetryTransportV3` — logs are correlated with active traces automatically. +- **`@pkg/logic/core/observability.ts`** provides two tracing helpers for domain code: + - `traceResultAsync` — wraps a `ResultAsync` operation in an OTel span. Use this in repositories and controllers. + - `withFlowSpan` — lower-level span wrapper for non-Result async code. +- Both helpers accept `fctx` and stamp spans with `flow.id`, `flow.user_id`, and `flow.session_id` for end-to-end trace correlation. + +### Convention + +When adding new domain operations in repositories or controllers, wrap them with `traceResultAsync`. Keep span names consistent and descriptive (e.g. `"user.getUserInfo"`). Do not add ad-hoc spans outside these helpers. + +--- + +## 7. Validation & Schemas + +- **Valibot** is used for schema validation in the logic package and in remote function input. +- Domain data types are defined in `data.ts` per domain. +- Use `v.InferOutput` for TypeScript types. +- Remote functions pass Valibot schemas to `query()` and `command()` for input validation. + +--- + +## 8. Package Naming + +- Apps: `@apps/*` (e.g. `@apps/main`, `@apps/processor`) +- Packages: `@pkg/*` (e.g. `@pkg/logic`, `@pkg/db`, `@pkg/logger`) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..dba777a --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Illusory IOTAM + +This is the source code for a SaaS project with purposes to basically offer the users the ability to use specific Android applications, but instead of the apps running on their own phone, they run on our hosted Docker-Android instances. + +Right now the project is in alpha testing phase, so it is subject to change (greenfield project) + +--- diff --git a/apps/front/package.json b/apps/front/package.json new file mode 100644 index 0000000..89ff791 --- /dev/null +++ b/apps/front/package.json @@ -0,0 +1,33 @@ +{ + "name": "@apps/front", + "type": "module", + "scripts": { + "dev": "PORT=3000 tsx watch src/index.ts", + "build": "tsc", + "prod": "HOST=0.0.0.0 PORT=3000 tsx src/index.ts" + }, + "dependencies": { + "@hono/node-server": "^1.19.9", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/auto-instrumentations-node": "^0.70.1", + "@opentelemetry/exporter-logs-otlp-proto": "^0.212.0", + "@opentelemetry/exporter-metrics-otlp-proto": "^0.212.0", + "@opentelemetry/exporter-trace-otlp-proto": "^0.212.0", + "@opentelemetry/sdk-logs": "^0.212.0", + "@opentelemetry/sdk-metrics": "^2.1.0", + "@opentelemetry/sdk-node": "^0.212.0", + "@pkg/db": "workspace:*", + "@pkg/logger": "workspace:*", + "@pkg/logic": "workspace:*", + "@pkg/result": "workspace:*", + "@pkg/settings": "workspace:*", + "hono": "^4.12.8", + "import-in-the-middle": "^3.0.0", + "valibot": "^1.2.0" + }, + "devDependencies": { + "@types/node": "^25.3.2", + "tsx": "^4.21.0", + "typescript": "^5.9.3" + } +} diff --git a/apps/front/src/index.ts b/apps/front/src/index.ts new file mode 100644 index 0000000..c4b9227 --- /dev/null +++ b/apps/front/src/index.ts @@ -0,0 +1,153 @@ +import "./instrumentation.js"; + +import { createHttpTelemetryMiddleware } from "@pkg/logic/core/http.telemetry"; +import type { FlowExecCtx } from "@pkg/logic/core/flow.execution.context"; +import { logDomainEvent } from "@pkg/logger"; +import { serve } from "@hono/node-server"; +import { settings } from "@pkg/settings"; +import { randomUUID } from "node:crypto"; +import { Hono } from "hono"; + +const app = new Hono().use("*", createHttpTelemetryMiddleware("front")); + +const host = process.env.HOST || "0.0.0.0"; +const port = Number(process.env.PORT || "3000"); + +function normalizeBaseUrl(url: string): string { + return url.endsWith("/") ? url.slice(0, -1) : url; +} + +function buildFlowExecCtx(): FlowExecCtx { + return { flowId: randomUUID() }; +} + +function getClientDownloadedApkName(): string { + const filename = settings.clientDownloadedApkName.trim(); + return filename.toLowerCase().endsWith(".apk") + ? filename + : `${filename}.apk`; +} + +app.get("/health", (c) => { + return c.json({ ok: true }); +}); + +app.get("/ping", (c) => { + return c.text("pong"); +}); + +app.get("/downloads/file/:buildId", async (c) => { + const fctx = buildFlowExecCtx(); + const buildId = c.req.param("buildId"); + + logDomainEvent({ + event: "processor.apk_download.started", + fctx, + meta: { buildId }, + }); + + const buildResult = await mobileBuildController.validateActiveBuildId( + fctx, + buildId, + ); + if (buildResult.isErr()) { + logDomainEvent({ + level: "warn", + event: "processor.apk_download.rejected", + fctx, + error: buildResult.error, + meta: { buildId }, + }); + return c.json( + { + data: null, + error: { ...buildResult.error, flowId: fctx.flowId }, + }, + 404, + ); + } + + const build = buildResult.value; + if (!build.apkAssetPath) { + logDomainEvent({ + level: "warn", + event: "processor.apk_download.missing_artifact", + fctx, + meta: { buildId }, + }); + return c.json( + { + data: null, + error: { + flowId: fctx.flowId, + code: "NOT_FOUND", + message: "APK not available", + description: "This build does not have a generated APK yet", + detail: `buildId=${buildId}`, + }, + }, + 404, + ); + } + + const assetUrl = `${normalizeBaseUrl(settings.appBuilderApiUrl)}${build.apkAssetPath}`; + const assetResponse = await fetch(assetUrl); + + if (!assetResponse.ok || !assetResponse.body) { + logDomainEvent({ + level: "error", + event: "processor.apk_download.fetch_failed", + fctx, + meta: { + buildId, + assetUrl, + status: assetResponse.status, + }, + }); + return c.json( + { + data: null, + error: { + flowId: fctx.flowId, + code: "INTERNAL_SERVER_ERROR", + message: "Failed to fetch APK artifact", + description: "Please try again later", + detail: `assetUrl=${assetUrl} status=${assetResponse.status}`, + }, + }, + 502, + ); + } + + logDomainEvent({ + event: "processor.apk_download.succeeded", + fctx, + meta: { + buildId, + assetUrl, + downloadName: getClientDownloadedApkName(), + }, + }); + + return new Response(assetResponse.body, { + status: 200, + headers: { + "content-type": + assetResponse.headers.get("content-type") || + "application/vnd.android.package-archive", + "content-disposition": `attachment; filename="${getClientDownloadedApkName()}"`, + "cache-control": "no-store", + }, + }); +}); + +serve( + { + fetch: app.fetch, + port, + hostname: host, + }, + (info) => { + console.log(`Server is running on http://${host}:${info.port}`); + }, +); diff --git a/apps/front/src/instrumentation.ts b/apps/front/src/instrumentation.ts new file mode 100644 index 0000000..6b3ec48 --- /dev/null +++ b/apps/front/src/instrumentation.ts @@ -0,0 +1,50 @@ +import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node"; +import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto"; +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto"; +import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics"; +import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-proto"; +import { createAddHookMessageChannel } from "import-in-the-middle"; +import { BatchLogRecordProcessor } from "@opentelemetry/sdk-logs"; +import { NodeSDK } from "@opentelemetry/sdk-node"; +import { settings } from "@pkg/settings"; +import { register } from "node:module"; + +const { registerOptions } = createAddHookMessageChannel(); +register("import-in-the-middle/hook.mjs", import.meta.url, registerOptions); + +const normalizedEndpoint = settings.otelExporterOtlpHttpEndpoint.startsWith( + "http", +) + ? settings.otelExporterOtlpHttpEndpoint + : `http://${settings.otelExporterOtlpHttpEndpoint}`; + +if (!process.env.OTEL_EXPORTER_OTLP_ENDPOINT) { + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = normalizedEndpoint; +} + +const sdk = new NodeSDK({ + serviceName: `${settings.otelServiceName}-processor`, + traceExporter: new OTLPTraceExporter(), + metricReader: new PeriodicExportingMetricReader({ + exporter: new OTLPMetricExporter(), + exportIntervalMillis: 10_000, + }), + logRecordProcessors: [new BatchLogRecordProcessor(new OTLPLogExporter())], + instrumentations: [ + getNodeAutoInstrumentations({ + "@opentelemetry/instrumentation-winston": { + // We add OpenTelemetryTransportV3 explicitly in @pkg/logger. + disableLogSending: true, + }, + }), + ], +}); + +sdk.start(); + +const shutdown = () => { + void sdk.shutdown(); +}; + +process.on("SIGTERM", shutdown); +process.on("SIGINT", shutdown); diff --git a/apps/front/tsconfig.json b/apps/front/tsconfig.json new file mode 100644 index 0000000..fce7dfe --- /dev/null +++ b/apps/front/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "verbatimModuleSyntax": false, + "skipLibCheck": true, + "types": [ + "node" + ], + "baseUrl": ".", + "paths": { + "@pkg/logic": ["../../packages/logic"], + "@pkg/logic/*": ["../../packages/logic/*"], + "@pkg/db": ["../../packages/db"], + "@pkg/db/*": ["../../packages/db/*"], + "@pkg/logger": ["../../packages/logger"], + "@pkg/logger/*": ["../../packages/logger/*"], + "@pkg/result": ["../../packages/result"], + "@pkg/result/*": ["../../packages/result/*"], + "@pkg/settings": ["../../packages/settings"], + "@pkg/settings/*": ["../../packages/settings/*"], + "@/*": ["../../packages/logic/*"], + "@core/*": ["../../packages/logic/core/*"], + "@domains/*": ["../../packages/logic/domains/*"] + }, + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx", + "outDir": "./dist" + }, + "exclude": ["node_modules"] +} diff --git a/apps/front/view.html b/apps/front/view.html new file mode 100644 index 0000000..4f4d891 --- /dev/null +++ b/apps/front/view.html @@ -0,0 +1,290 @@ + + + + + + + + + + Loading... + + + +
+
+ Connecting... +
+ + + + + + 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/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..3819056 --- /dev/null +++ b/apps/main/package.json @@ -0,0 +1,78 @@ +{ + "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 node ./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" + }, + "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/auto-instrumentations-node": "^0.70.1", + "@opentelemetry/exporter-logs-otlp-proto": "^0.212.0", + "@opentelemetry/exporter-metrics-otlp-proto": "^0.212.0", + "@opentelemetry/exporter-trace-otlp-proto": "^0.212.0", + "@opentelemetry/sdk-logs": "^0.212.0", + "@opentelemetry/sdk-node": "^0.212.0", + "@pkg/db": "workspace:*", + "@pkg/keystore": "workspace:*", + "@pkg/logger": "workspace:*", + "@pkg/logic": "workspace:*", + "@pkg/result": "workspace:*", + "@pkg/settings": "workspace:*", + "argon2": "^0.43.0", + "better-auth": "^1.4.20", + "date-fns": "^4.1.0", + "import-in-the-middle": "^3.0.0", + "nanoid": "^5.1.6", + "neverthrow": "^8.2.0", + "qrcode": "^1.5.4", + "sharp": "^0.34.5", + "valibot": "^1.2.0" + }, + "devDependencies": { + "@iconify/json": "^2.2.434", + "@internationalized/date": "^3.10.0", + "@lucide/svelte": "^0.561.0", + "@sveltejs/adapter-node": "^5.5.4", + "@sveltejs/kit": "^2.53.4", + "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@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.53.6", + "svelte-check": "^4.4.4", + "svelte-sonner": "^1.0.7", + "sveltekit-superforms": "^2.30.0", + "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..45d5ffb --- /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("/auth") || + event.url.pathname.includes("/link") + ) { + 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/instrumentation.server.ts b/apps/main/src/instrumentation.server.ts new file mode 100644 index 0000000..4184ea0 --- /dev/null +++ b/apps/main/src/instrumentation.server.ts @@ -0,0 +1,27 @@ +import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node"; +import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-proto"; +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto"; +import { createAddHookMessageChannel } from "import-in-the-middle"; +import { BatchLogRecordProcessor } from "@opentelemetry/sdk-logs"; +import { NodeSDK } from "@opentelemetry/sdk-node"; +import { settings } from "@pkg/settings"; +import { register } from "node:module"; + +const { registerOptions } = createAddHookMessageChannel(); +register("import-in-the-middle/hook.mjs", import.meta.url, registerOptions); + +const sdk = new NodeSDK({ + serviceName: settings.otelServiceName || settings.appName, + traceExporter: new OTLPTraceExporter(), + logRecordProcessors: [new BatchLogRecordProcessor(new OTLPLogExporter())], + instrumentations: [ + getNodeAutoInstrumentations({ + "@opentelemetry/instrumentation-winston": { + // We add OpenTelemetryTransportV3 explicitly in @pkg/logger. + disableLogSending: true, + }, + }), + ], +}); + +sdk.start(); diff --git a/apps/main/src/lib/auth.client.ts b/apps/main/src/lib/auth.client.ts new file mode 100644 index 0000000..543a4bc --- /dev/null +++ b/apps/main/src/lib/auth.client.ts @@ -0,0 +1,19 @@ +import { + adminClient, + customSessionClient, + inferAdditionalFields, + 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(), + 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/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..9f1d02f --- /dev/null +++ b/apps/main/src/lib/core/constants.ts @@ -0,0 +1,56 @@ +import LayoutDashboard from "@lucide/svelte/icons/layout-dashboard"; +import Smartphone from "@lucide/svelte/icons/smartphone"; +import { BellRingIcon, Link } 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: "Links", + url: "/links", + icon: Link, + }, + { + title: "Devices", + url: "/devices", + icon: Smartphone, + }, +] 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@company.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/core/server.utils.ts b/apps/main/src/lib/core/server.utils.ts new file mode 100644 index 0000000..450c7dd --- /dev/null +++ b/apps/main/src/lib/core/server.utils.ts @@ -0,0 +1,25 @@ +import type { FlowExecCtx } from "@pkg/logic/core/flow.execution.context"; +import type { Err } from "@pkg/result"; + +export async function getFlowExecCtxForRemoteFuncs( + locals: App.Locals, +): Promise { + return { + flowId: locals.flowId || crypto.randomUUID(), + userId: locals.user?.id, + sessionId: locals.session?.id, + }; +} + +export function unauthorized(fctx: FlowExecCtx) { + return { + data: null, + error: { + flowId: fctx.flowId, + code: "UNAUTHORIZED", + message: "User not authenticated", + description: "Please log in", + detail: "No user ID found in session", + } as Err, + }; +} diff --git a/apps/main/src/lib/domains/account/account.remote.ts b/apps/main/src/lib/domains/account/account.remote.ts new file mode 100644 index 0000000..3dc2e38 --- /dev/null +++ b/apps/main/src/lib/domains/account/account.remote.ts @@ -0,0 +1,168 @@ +import { + banUserSchema, + checkUsernameSchema, + ensureAccountExistsSchema, + rotatePasswordSchema, +} from "@pkg/logic/domains/user/data"; +import { + getFlowExecCtxForRemoteFuncs, + unauthorized, +} from "$lib/core/server.utils"; +import { getUserController } from "@pkg/logic/domains/user/controller"; +import { command, getRequestEvent, query } from "$app/server"; +import * as v from "valibot"; + +const uc = getUserController(); + +export const getMyInfoSQ = query(async () => { + const event = getRequestEvent(); + const fctx = await getFlowExecCtxForRemoteFuncs(event.locals); + + if (!fctx.userId) { + return unauthorized(fctx); + } + + const res = await uc.getUserInfo(fctx, fctx.userId); + return res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }; +}); + +export const getUserInfoByIdSQ = query( + v.object({ userId: v.string() }), + async (input) => { + const event = getRequestEvent(); + const fctx = await getFlowExecCtxForRemoteFuncs(event.locals); + + if (!fctx.userId) { + return unauthorized(fctx); + } + + const res = await uc.getUserInfo(fctx, input.userId); + return res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }; + }, +); + +export const ensureAccountExistsSC = command( + ensureAccountExistsSchema, + async (payload) => { + const event = getRequestEvent(); + const fctx = await getFlowExecCtxForRemoteFuncs(event.locals); + + if (!fctx.userId) { + return unauthorized(fctx); + } + + const res = await uc.ensureAccountExists(fctx, payload.userId); + return res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }; + }, +); + +export const checkUsernameSC = command(checkUsernameSchema, async (payload) => { + const event = getRequestEvent(); + const fctx = await getFlowExecCtxForRemoteFuncs(event.locals); + + if (!fctx.userId) { + return unauthorized(fctx); + } + + const res = await uc.isUsernameAvailable(fctx, payload.username); + return res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }; +}); + +export const update2faVerifiedSC = command( + v.object({ userId: v.string() }), + async (payload) => { + const event = getRequestEvent(); + const fctx = await getFlowExecCtxForRemoteFuncs(event.locals); + + if (!fctx.userId) { + return unauthorized(fctx); + } + + const res = await uc.updateLastVerified2FaAtToNow(fctx, payload.userId); + return res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }; + }, +); + +export const banUserSC = command(banUserSchema, async (payload) => { + const event = getRequestEvent(); + const fctx = await getFlowExecCtxForRemoteFuncs(event.locals); + + if (!fctx.userId) { + return unauthorized(fctx); + } + + const res = await uc.banUser( + fctx, + payload.userId, + payload.reason, + payload.banExpiresAt, + ); + return res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }; +}); + +export const isUserBannedSQ = query( + v.object({ userId: v.string() }), + async (input) => { + const event = getRequestEvent(); + const fctx = await getFlowExecCtxForRemoteFuncs(event.locals); + + if (!fctx.userId) { + return unauthorized(fctx); + } + + const res = await uc.isUserBanned(fctx, input.userId); + return res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }; + }, +); + +export const getBanInfoSQ = query( + v.object({ userId: v.string() }), + async (input) => { + const event = getRequestEvent(); + const fctx = await getFlowExecCtxForRemoteFuncs(event.locals); + + if (!fctx.userId) { + return unauthorized(fctx); + } + + const res = await uc.getBanInfo(fctx, input.userId); + return res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }; + }, +); + +export const rotatePasswordSC = command( + rotatePasswordSchema, + async (payload) => { + const event = getRequestEvent(); + const fctx = await getFlowExecCtxForRemoteFuncs(event.locals); + + if (!fctx.userId) { + return unauthorized(fctx); + } + + const res = await uc.rotatePassword( + fctx, + payload.userId, + payload.password, + ); + return res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }; + }, +); 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..f17913f --- /dev/null +++ b/apps/main/src/lib/domains/account/account.vm.svelte.ts @@ -0,0 +1,153 @@ +import { ResultAsync, errAsync, okAsync } from "neverthrow"; +import type { User } from "@pkg/logic/domains/user/data"; +import { user as userStore } from "$lib/global.stores"; +import { rotatePasswordSC } from "./account.remote"; +import { authClient } from "$lib/auth.client"; +import type { Err } from "@pkg/result"; +import { toast } from "svelte-sonner"; +import { get } from "svelte/store"; + +class AccountViewModel { + loading = $state(false); + passwordLoading = $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 changePassword(password: string): Promise { + this.passwordLoading = true; + this.errorMessage = null; + + const currentUser = get(userStore); + if (!currentUser?.id) { + this.passwordLoading = false; + this.errorMessage = "User not found"; + toast.error(this.errorMessage); + return false; + } + const result = await ResultAsync.fromPromise( + rotatePasswordSC({ userId: currentUser.id, password }), + (error): Err => ({ + code: "NETWORK_ERROR", + message: "Failed to change password", + description: "Network request failed", + detail: error instanceof Error ? error.message : String(error), + }), + ).andThen((apiResult: any) => { + if (apiResult?.error) { + return errAsync(apiResult.error); + } + return okAsync(apiResult?.data); + }); + + const success = result.match( + () => { + toast.success("Password updated successfully"); + return true; + }, + (error) => { + this.errorMessage = + (error.message as string) ?? "Failed to change password"; + toast.error(this.errorMessage, { + description: error.description, + }); + return false; + }, + ); + + this.passwordLoading = 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..a9d23a8 --- /dev/null +++ b/apps/main/src/lib/domains/notifications/notification.vm.svelte.ts @@ -0,0 +1,235 @@ +import type { + ClientNotificationFilters, + ClientPaginationState, + Notifications, +} from "@pkg/logic/domains/notifications/data"; +import { + archiveSC, + deleteNotificationsSC, + getNotificationsSQ, + getUnreadCountSQ, + markAllReadSC, + markReadSC, + markUnreadSC, + unarchiveSC, +} from "./notifications.remote"; +import { user } from "$lib/global.stores"; +import { toast } from "svelte-sonner"; +import { get } from "svelte/store"; + +class NotificationViewModel { + notifications = $state([] as Notifications); + loading = $state(false); + selectedIds = $state(new Set()); + + pagination = $state({ + page: 1, + pageSize: 20, + total: 0, + totalPages: 0, + sortBy: "createdAt", + sortOrder: "desc", + }); + + filters = $state({ + userId: get(user)?.id!, + isArchived: false, + }); + + unreadCount = $state(0); + + private getFetchQueryInput() { + return { + filters: { + userId: this.filters.userId, + isRead: this.filters.isRead, + isArchived: this.filters.isArchived, + type: this.filters.type, + category: this.filters.category, + priority: this.filters.priority, + search: this.filters.search, + }, + pagination: { + page: this.pagination.page, + pageSize: this.pagination.pageSize, + sortBy: this.pagination.sortBy, + sortOrder: this.pagination.sortOrder, + }, + }; + } + + private async runCommand( + fn: (payload: { notificationIds: number[] }) => Promise, + notificationIds: number[], + errorMessage: string, + after: Array<() => Promise>, + ) { + try { + const result = await fn({ notificationIds }); + if (result?.error) { + toast.error(result.error.message || errorMessage, { + description: result.error.description || "Please try again later", + }); + return; + } + + for (const action of after) { + await action(); + } + } catch (error) { + toast.error(errorMessage, { + description: + error instanceof Error ? error.message : "Please try again later", + }); + } + } + + async fetchNotifications() { + this.loading = true; + try { + const result = await getNotificationsSQ(this.getFetchQueryInput()); + if (result?.error || !result?.data) { + toast.error( + result?.error?.message || "Failed to fetch notifications", + { + description: + result?.error?.description || "Please try again later", + }, + ); + return; + } + + this.notifications = result.data.data as Notifications; + this.pagination.total = result.data.total; + this.pagination.totalPages = result.data.totalPages; + } catch (error) { + toast.error("Failed to fetch notifications", { + description: + error instanceof Error ? error.message : "Please try again later", + }); + } finally { + this.loading = false; + } + } + + async markAsRead(notificationIds: number[]) { + await this.runCommand(markReadSC, notificationIds, "Failed to mark as read", [ + () => this.fetchNotifications(), + () => this.fetchUnreadCount(), + ]); + } + + async markAsUnread(notificationIds: number[]) { + await this.runCommand( + markUnreadSC, + notificationIds, + "Failed to mark as unread", + [() => this.fetchNotifications(), () => this.fetchUnreadCount()], + ); + } + + async archive(notificationIds: number[]) { + await this.runCommand(archiveSC, notificationIds, "Failed to archive", [ + () => this.fetchNotifications(), + ]); + } + + async unarchive(notificationIds: number[]) { + await this.runCommand(unarchiveSC, notificationIds, "Failed to unarchive", [ + () => this.fetchNotifications(), + ]); + } + + async deleteNotifications(notificationIds: number[]) { + await this.runCommand( + deleteNotificationsSC, + notificationIds, + "Failed to delete", + [() => this.fetchNotifications(), () => this.fetchUnreadCount()], + ); + } + + async markAllAsRead() { + try { + const result = await markAllReadSC({}); + if (result?.error) { + toast.error(result.error.message || "Failed to mark all as read", { + description: result.error.description || "Please try again later", + }); + return; + } + await this.fetchNotifications(); + await this.fetchUnreadCount(); + } catch (error) { + toast.error("Failed to mark all as read", { + description: + error instanceof Error ? error.message : "Please try again later", + }); + } + } + + async fetchUnreadCount() { + try { + const result = await getUnreadCountSQ(); + if (result?.error) { + return; + } + if (result?.data !== undefined && result?.data !== null) { + this.unreadCount = result.data as number; + } + } catch { + // Intentionally silent - unread count is non-critical UI data. + } + } + + 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; + this.fetchNotifications(); + } + + setSorting( + sortBy: ClientPaginationState["sortBy"], + sortOrder: ClientPaginationState["sortOrder"], + ) { + this.pagination.sortBy = sortBy; + this.pagination.sortOrder = sortOrder; + this.pagination.page = 1; + this.fetchNotifications(); + } + + setFilters(newFilters: Partial) { + this.filters = { ...this.filters, ...newFilters }; + this.pagination.page = 1; + 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/notifications/notifications.remote.ts b/apps/main/src/lib/domains/notifications/notifications.remote.ts new file mode 100644 index 0000000..1c8a8a4 --- /dev/null +++ b/apps/main/src/lib/domains/notifications/notifications.remote.ts @@ -0,0 +1,155 @@ +import { + bulkNotificationIdsSchema, + getNotificationsSchema, +} from "@pkg/logic/domains/notifications/data"; +import { getNotificationController } from "@pkg/logic/domains/notifications/controller"; +import { + getFlowExecCtxForRemoteFuncs, + unauthorized, +} from "$lib/core/server.utils"; +import { command, getRequestEvent, query } from "$app/server"; +import * as v from "valibot"; + +const nc = getNotificationController(); + +export const getNotificationsSQ = query( + getNotificationsSchema, + async (input) => { + const event = getRequestEvent(); + const fctx = await getFlowExecCtxForRemoteFuncs(event.locals); + if (!fctx.userId) { + return unauthorized(fctx); + } + const res = await nc.getNotifications( + fctx, + { ...input.filters, userId: fctx.userId }, + input.pagination, + ); + return res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }; + }, +); + +export const markReadSC = command( + bulkNotificationIdsSchema, + async (payload) => { + const event = getRequestEvent(); + const fctx = await getFlowExecCtxForRemoteFuncs(event.locals); + if (!fctx.userId) { + return unauthorized(fctx); + } + + const res = await nc.markAsRead( + fctx, + [...payload.notificationIds], + fctx.userId, + ); + return res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }; + }, +); + +export const markUnreadSC = command( + bulkNotificationIdsSchema, + async (payload) => { + const event = getRequestEvent(); + const fctx = await getFlowExecCtxForRemoteFuncs(event.locals); + if (!fctx.userId) { + return unauthorized(fctx); + } + + const res = await nc.markAsUnread( + fctx, + [...payload.notificationIds], + fctx.userId, + ); + return res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }; + }, +); + +export const archiveSC = command(bulkNotificationIdsSchema, async (payload) => { + const event = getRequestEvent(); + const fctx = await getFlowExecCtxForRemoteFuncs(event.locals); + if (!fctx.userId) { + return unauthorized(fctx); + } + + const res = await nc.archive( + fctx, + [...payload.notificationIds], + fctx.userId, + ); + return res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }; +}); + +export const unarchiveSC = command( + bulkNotificationIdsSchema, + async (payload) => { + const event = getRequestEvent(); + const fctx = await getFlowExecCtxForRemoteFuncs(event.locals); + if (!fctx.userId) { + return unauthorized(fctx); + } + + const res = await nc.unarchive( + fctx, + [...payload.notificationIds], + fctx.userId, + ); + return res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }; + }, +); + +export const deleteNotificationsSC = command( + bulkNotificationIdsSchema, + async (payload) => { + const event = getRequestEvent(); + const fctx = await getFlowExecCtxForRemoteFuncs(event.locals); + if (!fctx.userId) { + return unauthorized(fctx); + } + + const res = await nc.deleteNotifications( + fctx, + [...payload.notificationIds], + fctx.userId, + ); + return res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }; + }, +); + +export const markAllReadSC = command(v.object({}), async () => { + const event = getRequestEvent(); + const fctx = await getFlowExecCtxForRemoteFuncs(event.locals); + if (!fctx.userId) { + return unauthorized(fctx); + } + + const res = await nc.markAllAsRead(fctx, fctx.userId); + return res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }; +}); + +export const getUnreadCountSQ = query(async () => { + const event = getRequestEvent(); + const fctx = await getFlowExecCtxForRemoteFuncs(event.locals); + if (!fctx.userId) { + return unauthorized(fctx); + } + + const res = await nc.getUnreadCount(fctx, fctx.userId); + return res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }; +}); diff --git a/apps/main/src/lib/domains/security/2fa-verify.vm.svelte.ts b/apps/main/src/lib/domains/security/2fa-verify.vm.svelte.ts new file mode 100644 index 0000000..c5ed5ab --- /dev/null +++ b/apps/main/src/lib/domains/security/2fa-verify.vm.svelte.ts @@ -0,0 +1,160 @@ +import { session, user } from "$lib/global.stores"; +import { authClient } from "$lib/auth.client"; +import { + startVerificationSessionSC, + verifySessionCodeSC, +} from "$lib/domains/security/twofa.remote"; +import { toast } from "svelte-sonner"; +import { get } from "svelte/store"; +import { page } from "$app/state"; + +class TwoFactorVerifyViewModel { + verifying = $state(false); + verificationCode = $state(""); + verificationToken = $state(null); + errorMessage = $state(null); + startingVerification = $state(false); + + async startVerification() { + this.startingVerification = true; + this.errorMessage = null; + + const currentUser = get(user); + const currentSession = get(session); + + const uid = currentUser?.id; + const sid = currentSession?.id; + + if (!uid || !sid) { + this.errorMessage = "No active session found"; + toast.error("Failed to start verification", { + description: this.errorMessage, + }); + this.startingVerification = false; + return; + } + + try { + const result = await startVerificationSessionSC({ + userId: uid, + sessionId: sid, + }); + + if (result?.error || !result?.data?.verificationToken) { + this.errorMessage = + result?.error?.message || "Failed to start verification"; + toast.error("Failed to start verification", { + description: this.errorMessage, + }); + return; + } + + this.verificationToken = result.data.verificationToken; + } catch (error) { + this.errorMessage = "Failed to start verification"; + toast.error("Failed to start verification", { + description: + error instanceof Error + ? error.message + : "Failed to start verification", + }); + } finally { + this.startingVerification = false; + } + } + + async verifyCode() { + if (!this.verificationToken) { + this.errorMessage = "No verification session found"; + return; + } + + if (!this.verificationCode || this.verificationCode.length !== 6) { + this.errorMessage = "Please enter a valid 6-digit code"; + return; + } + + this.verifying = true; + this.errorMessage = null; + + try { + const result = await verifySessionCodeSC({ + verificationToken: this.verificationToken, + code: this.verificationCode, + }); + + if (result?.error || !result?.data?.success) { + this.errorMessage = + result?.error?.message || "Failed to verify code"; + + if (result?.error?.code === "BANNED") { + await authClient.signOut(); + window.location.href = "/auth/login"; + return; + } + + if ( + this.errorMessage && + !this.errorMessage.includes("Invalid") && + !this.errorMessage.includes("already been used") + ) { + toast.error("Verification failed", { + description: this.errorMessage, + }); + } + return; + } + + const redirectUrl = page.url.searchParams.get("redirect") || "/"; + window.location.href = redirectUrl; + } catch (error) { + this.errorMessage = "Failed to verify code"; + toast.error("Verification failed", { + description: + error instanceof Error ? error.message : this.errorMessage, + }); + } finally { + this.verifying = false; + } + } + + async handleBackupCode() { + if (!this.verificationToken || !this.verificationCode) { + this.errorMessage = "Please enter a valid backup code"; + return; + } + + this.verifying = true; + this.errorMessage = null; + + try { + const result = await verifySessionCodeSC({ + verificationToken: this.verificationToken, + code: this.verificationCode, + }); + + if (result?.error || !result?.data?.success) { + this.errorMessage = result?.error?.message || "Invalid backup code"; + return; + } + + const redirectUrl = page.url.searchParams.get("redirect") || "/"; + window.location.href = redirectUrl; + } catch (error) { + this.errorMessage = + error instanceof Error ? error.message : "Invalid backup code"; + } finally { + this.verifying = false; + } + } + + reset() { + this.verifying = false; + this.verificationCode = ""; + this.verificationToken = null; + this.errorMessage = null; + this.startingVerification = false; + } +} + +export const twoFactorVerifyVM = new TwoFactorVerifyViewModel(); diff --git a/apps/main/src/lib/domains/security/2fa.vm.svelte.ts b/apps/main/src/lib/domains/security/2fa.vm.svelte.ts new file mode 100644 index 0000000..b4c7420 --- /dev/null +++ b/apps/main/src/lib/domains/security/2fa.vm.svelte.ts @@ -0,0 +1,234 @@ +import { + disableTwoFactorSC, + generateBackupCodesSQ, + setupTwoFactorSC, + verifyAndEnableTwoFactorSC, +} from "$lib/domains/security/twofa.remote"; +import { toast } from "svelte-sonner"; +import QRCode from "qrcode"; + +class TwoFactorViewModel { + twoFactorEnabled = $state(false); + twoFactorSetupInProgress = $state(false); + showingBackupCodes = $state(false); + qrCodeUrl = $state(null); + twoFactorSecret = $state(null); + backupCodes = $state([]); + twoFactorVerificationCode = $state(""); + isLoading = $state(false); + errorMessage = $state(null); + + async startTwoFactorSetup() { + this.isLoading = true; + this.errorMessage = null; + + try { + const result = await setupTwoFactorSC({}); + if (result?.error || !result?.data?.totpURI) { + this.errorMessage = + result?.error?.message || "Could not enable 2FA"; + toast.error(this.errorMessage, { + description: + result?.error?.description || "Please try again later", + }); + return; + } + + const qrCodeDataUrl = await QRCode.toDataURL(result.data.totpURI, { + width: 256, + margin: 2, + color: { dark: "#000000", light: "#FFFFFF" }, + }); + + this.qrCodeUrl = qrCodeDataUrl; + this.twoFactorSetupInProgress = true; + this.twoFactorSecret = result.data.secret; + this.twoFactorVerificationCode = ""; + toast("Setup enabled"); + } catch (error) { + this.errorMessage = "Could not enable 2FA"; + toast.error(this.errorMessage, { + description: + error instanceof Error ? error.message : "Please try again later", + }); + } finally { + this.isLoading = false; + } + } + + async completeTwoFactorSetup() { + if (!this.twoFactorVerificationCode) { + this.errorMessage = + "Please enter the verification code from your authenticator app."; + return; + } + + this.isLoading = true; + this.errorMessage = null; + + try { + const verifyResult = await verifyAndEnableTwoFactorSC({ + code: this.twoFactorVerificationCode, + }); + + if (verifyResult?.error) { + this.errorMessage = + verifyResult.error.message || "Invalid verification code"; + toast.error(this.errorMessage, { + description: + verifyResult.error.description || "Please try again", + }); + return; + } + + const backupCodesResult = await generateBackupCodesSQ(); + if (backupCodesResult?.error || !Array.isArray(backupCodesResult?.data)) { + toast.error("2FA enabled, but failed to generate backup codes", { + description: "You can generate them later in settings", + }); + } else { + this.backupCodes = backupCodesResult.data; + this.showingBackupCodes = true; + } + + this.twoFactorEnabled = true; + this.twoFactorSetupInProgress = false; + this.twoFactorVerificationCode = ""; + toast.success("Two-factor authentication enabled", { + description: "Your account is now more secure", + }); + } catch (error) { + this.errorMessage = "Invalid verification code"; + toast.error(this.errorMessage, { + description: + error instanceof Error ? error.message : "Please try again", + }); + } finally { + this.isLoading = false; + } + } + + async disableTwoFactor() { + this.isLoading = true; + this.errorMessage = null; + + try { + const result = await disableTwoFactorSC({ code: "" }); + if (result?.error) { + this.errorMessage = result.error.message || "Failed to disable 2FA"; + toast.error(this.errorMessage, { + description: result.error.description || "Please try again later", + }); + return; + } + + this.twoFactorEnabled = false; + this.backupCodes = []; + this.qrCodeUrl = null; + this.showingBackupCodes = false; + this.twoFactorSecret = null; + toast.success("Two-factor authentication disabled"); + } catch (error) { + this.errorMessage = "Failed to disable 2FA"; + toast.error(this.errorMessage, { + description: + error instanceof Error ? error.message : "Please try again later", + }); + } finally { + this.isLoading = false; + } + } + + async generateNewBackupCodes() { + this.isLoading = true; + this.errorMessage = null; + + try { + const result = await generateBackupCodesSQ(); + if (result?.error || !Array.isArray(result?.data)) { + this.errorMessage = + result?.error?.message || "Failed to generate new backup codes"; + toast.error(this.errorMessage, { + description: + result?.error?.description || "Please try again later", + }); + return; + } + + this.backupCodes = result.data; + this.showingBackupCodes = true; + toast.success("New backup codes generated", { + description: "Your previous backup codes are now invalid", + }); + } catch (error) { + this.errorMessage = "Failed to generate new backup codes"; + toast.error(this.errorMessage, { + description: + error instanceof Error ? error.message : "Please try again later", + }); + } finally { + this.isLoading = false; + } + } + + copyAllBackupCodes() { + const codesText = this.backupCodes.join("\n"); + navigator.clipboard.writeText(codesText); + toast.success("All backup codes copied to clipboard"); + } + + downloadBackupCodes() { + const codesText = this.backupCodes.join("\n"); + const blob = new Blob( + [ + `Two-Factor Authentication Backup Codes\n\nGenerated: ${new Date().toLocaleString()}\n\n${codesText}\n\nKeep these codes in a safe place. Each code can only be used once.`, + ], + { + type: "text/plain", + }, + ); + + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = `2fa-backup-codes-${new Date().toISOString().split("T")[0]}.txt`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + + toast.success("Backup codes downloaded"); + } + + confirmBackupCodesSaved() { + this.showingBackupCodes = false; + toast.success("Great! Your backup codes are safely stored"); + } + + cancelSetup() { + this.twoFactorSetupInProgress = false; + this.twoFactorSecret = null; + this.qrCodeUrl = null; + this.twoFactorVerificationCode = ""; + this.errorMessage = null; + this.showingBackupCodes = false; + } + + copyToClipboard(text: string) { + navigator.clipboard.writeText(text); + } + + reset() { + this.twoFactorEnabled = false; + this.twoFactorSetupInProgress = false; + this.showingBackupCodes = false; + this.qrCodeUrl = null; + this.backupCodes = []; + this.twoFactorSecret = null; + this.twoFactorVerificationCode = ""; + this.isLoading = false; + this.errorMessage = null; + } +} + +export const twofactorVM = new TwoFactorViewModel(); 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..7c9d622 --- /dev/null +++ b/apps/main/src/lib/domains/security/auth.vm.svelte.ts @@ -0,0 +1,71 @@ +import { authClient } from "$lib/auth.client"; +import { toast } from "svelte-sonner"; +import { ResultAsync, errAsync, okAsync } from "neverthrow"; +import type { Err } from "@pkg/result"; + +class AuthViewModel { + loggingIn = $state(false); + + async loginWithCredentials(data: FormData): Promise { + const username = data.get("username")?.toString().trim(); + const password = data.get("password")?.toString(); + + if (!username || username.length < 3) { + toast.error("Please enter a valid username"); + return false; + } + + if (!password || password.length < 6) { + toast.error("Please enter a valid password"); + return false; + } + + this.loggingIn = true; + + const result = await ResultAsync.fromPromise( + authClient.signIn.username({ username, password }), + (error): Err => ({ + code: "NETWORK_ERROR", + message: "Failed to login", + 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 ?? "Invalid credentials", + description: + response.error.statusText ?? + "Please check your username and password", + detail: response.error.statusText ?? "Unknown error", + }); + } + return okAsync(response.data); + }); + + const success = result.match( + () => { + toast.success("Login successful", { + description: "Redirecting...", + }); + setTimeout(() => { + window.location.href = "/"; + }, 500); + return true; + }, + (error) => { + toast.error(error.message ?? "Invalid credentials", { + description: error.description, + }); + return false; + }, + ); + + this.loggingIn = false; + return success; + } +} + +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..cdc24d4 --- /dev/null +++ b/apps/main/src/lib/domains/security/email-login-form.svelte @@ -0,0 +1,78 @@ + + +
    +
    + + +
    + +
    + + +
    + + +
    diff --git a/apps/main/src/lib/domains/security/two-fa-card.svelte b/apps/main/src/lib/domains/security/two-fa-card.svelte new file mode 100644 index 0000000..2cdd255 --- /dev/null +++ b/apps/main/src/lib/domains/security/two-fa-card.svelte @@ -0,0 +1,324 @@ + + + + +
    + + Two-Factor Authentication +
    + + Add an extra layer of security to your account by enabling two-factor + authentication. + +
    + + {#if twofactorVM.twoFactorSetupInProgress} +
    +
    +

    Setup Instructions

    +
      +
    1. + Install an authenticator app like Google Authenticator + or 2FAS on your mobile device. +
    2. +
    3. + Scan the QR code below or manually enter the secret + key into your app. +
    4. +
    5. + Enter the verification code displayed in your + authenticator app below. +
    6. +
    +
    + +
    + {#if twofactorVM.qrCodeUrl && twofactorVM.qrCodeUrl.length > 0} +
    + {#if twofactorVM.qrCodeUrl} +
    + QR Code for Two-Factor Authentication +
    + + {:else} +
    + +
    + {/if} +
    + {:else} +
    + +
    + {/if} +
    + +
    +
    + + {#snippet children({ cells })} + + {#each cells.slice(0, 3) as cell (cell)} + + {/each} + + + + {#each cells.slice(3, 6) as cell (cell)} + + {/each} + + {/snippet} + +
    +

    + Enter the 6-digit code from your authenticator app +

    +
    + +
    + + +
    +
    + {:else if !twofactorVM.twoFactorEnabled && !twofactorVM.twoFactorSetupInProgress} +
    +
    +
    + + + Two-factor authentication is currently disabled. + +
    + +
    +
    + {:else if twofactorVM.twoFactorEnabled} +
    + {#if !twofactorVM.showingBackupCodes} +
    +
    + + Two-factor authentication is enabled. +
    + +
    + + + + + + + + + Disable Two-Factor Authentication? + + This will remove the extra layer of + security from your account. You will + no longer need your authenticator app + to sign in, and all backup codes will + be invalidated. You can re-enable 2FA + at any time. + + + + Cancel + + twofactorVM.disableTwoFactor()} + > + Disable 2FA + + + + +
    +
    + {/if} + + {#if twofactorVM.showingBackupCodes && twofactorVM.backupCodes.length > 0} +
    +
    +

    Recovery Codes

    +
    + + +
    +
    + +
    +
    + {#each twofactorVM.backupCodes as code} +
    + {code} +
    + {/each} +
    +
    +

    + + Keep these codes in a safe place. Each code can + only be used once to access your account if you + lose your phone. +

    +
    +

    + ⚠️ Important: Save these codes before + continuing. You won't be able to see them + again. +

    +
    +
    +
    + + +
    + {/if} +
    + {/if} +
    +
    diff --git a/apps/main/src/lib/domains/security/twofa.remote.ts b/apps/main/src/lib/domains/security/twofa.remote.ts new file mode 100644 index 0000000..a8606d7 --- /dev/null +++ b/apps/main/src/lib/domains/security/twofa.remote.ts @@ -0,0 +1,205 @@ +import { + disable2FASchema, + enable2FACodeSchema, + startVerificationSchema, + verifyCodeSchema, +} from "@pkg/logic/domains/2fa/data"; +import { + getFlowExecCtxForRemoteFuncs, + unauthorized, +} from "$lib/core/server.utils"; +import { getTwofaController } from "@pkg/logic/domains/2fa/controller"; +import { command, getRequestEvent, query } from "$app/server"; +import { auth } from "@pkg/logic/domains/auth/config.base"; +import type { User } from "@pkg/logic/domains/user/data"; +import * as v from "valibot"; + +const tc = getTwofaController(); + +function buildIpAddress(headers: Headers) { + return ( + headers.get("x-forwarded-for") ?? headers.get("x-real-ip") ?? "unknown" + ); +} + +function buildUserAgent(headers: Headers) { + return headers.get("user-agent") ?? "unknown"; +} + +export const setupTwoFactorSC = command(v.object({}), async () => { + const event = getRequestEvent(); + const fctx = await getFlowExecCtxForRemoteFuncs(event.locals); + const currentUser = event.locals.user; + + if (!fctx.userId || !currentUser) { + return unauthorized(fctx); + } + + const res = await tc.setup2FA(fctx, currentUser as User); + return res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }; +}); + +export const verifyAndEnableTwoFactorSC = command( + enable2FACodeSchema, + async (payload) => { + const event = getRequestEvent(); + const fctx = await getFlowExecCtxForRemoteFuncs(event.locals); + const currentUser = event.locals.user; + + if (!fctx.userId || !currentUser) { + return unauthorized(fctx); + } + + const res = await tc.verifyAndEnable2FA( + fctx, + currentUser as User, + payload.code, + event.request.headers, + ); + + return res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }; + }, +); + +export const generateBackupCodesSQ = query(async () => { + const event = getRequestEvent(); + const fctx = await getFlowExecCtxForRemoteFuncs(event.locals); + const currentUser = event.locals.user; + + if (!fctx.userId || !currentUser) { + return unauthorized(fctx); + } + + const res = await tc.generateBackupCodes(fctx, currentUser as User); + return res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }; +}); + +export const disableTwoFactorSC = command(disable2FASchema, async (payload) => { + const event = getRequestEvent(); + const fctx = await getFlowExecCtxForRemoteFuncs(event.locals); + const currentUser = event.locals.user; + + if (!fctx.userId || !currentUser) { + return unauthorized(fctx); + } + + const res = await tc.disable(fctx, currentUser as User, payload.code); + return res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }; +}); + +export const requiresVerificationSQ = query( + v.object({ sessionId: v.string() }), + async (input) => { + const event = getRequestEvent(); + const fctx = await getFlowExecCtxForRemoteFuncs(event.locals); + const currentUser = event.locals.user; + + if (!fctx.userId || !currentUser) { + return unauthorized(fctx); + } + + const res = await tc.requiresInitialVerification( + fctx, + currentUser as User, + input.sessionId, + ); + + return res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }; + }, +); + +export const requiresSensitiveActionSQ = query(async () => { + const event = getRequestEvent(); + const fctx = await getFlowExecCtxForRemoteFuncs(event.locals); + const currentUser = event.locals.user; + + if (!fctx.userId || !currentUser) { + return unauthorized(fctx); + } + + const res = await tc.requiresSensitiveActionVerification( + fctx, + currentUser as User, + ); + + return res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }; +}); + +export const startVerificationSessionSC = command( + startVerificationSchema, + async (payload) => { + const event = getRequestEvent(); + const fctx = await getFlowExecCtxForRemoteFuncs(event.locals); + + if (!fctx.userId) { + return unauthorized(fctx); + } + + const res = await tc.startVerification(fctx, { + userId: payload.userId, + sessionId: payload.sessionId, + ipAddress: buildIpAddress(event.request.headers), + userAgent: buildUserAgent(event.request.headers), + }); + + return res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }; + }, +); + +export const verifySessionCodeSC = command( + verifyCodeSchema, + async (payload) => { + const event = getRequestEvent(); + const fctx = await getFlowExecCtxForRemoteFuncs(event.locals); + + let currentUser = event.locals.user; + + if (!currentUser) { + const sess = await auth.api.getSession({ + headers: event.request.headers, + }); + currentUser = sess?.user as User | undefined; + } + + const res = await tc.verifyCode( + fctx, + { + verificationSessToken: payload.verificationToken, + code: payload.code, + }, + currentUser as User | undefined, + ); + + return res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }; + }, +); + +export const cleanupExpiredSessionsSC = command(v.object({}), async () => { + const event = getRequestEvent(); + const fctx = await getFlowExecCtxForRemoteFuncs(event.locals); + + if (!fctx.userId) { + return unauthorized(fctx); + } + + const res = await tc.cleanupExpiredSessions(fctx); + return res.isOk() + ? { data: res.value, error: null } + : { data: null, error: res.error }; +}); 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..dcb920b --- /dev/null +++ b/apps/main/src/routes/(main)/+page.svelte @@ -0,0 +1,12 @@ + 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..698c87d --- /dev/null +++ b/apps/main/src/routes/(main)/account/+page.svelte @@ -0,0 +1,261 @@ + + +
    + + + +
    +
    +
    + + {#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()} +

    +
    +
    + + + + + Password Settings + + Update your account password. + + + +
    +
    + + +
    + +
    + + +
    + +
    + +
    +
    +
    + +

    + Choose a strong password with at least 6 characters. +

    +
    +
    +
    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..37d5813 --- /dev/null +++ b/apps/main/src/routes/(main)/dashboard/+page.svelte @@ -0,0 +1,267 @@ + + + + + +
    +
    + + Devices + + {mobileVM.devicesTotal} total + +
    +
    + + +
    +
    + +
    + + { + mobileVM.devicesPage = 1; + void mobileVM.refreshDevices(); + }} + /> +
    +
    + + + {#if !mobileVM.devicesLoading && mobileVM.devices.length === 0} +
    + No devices registered yet. +
    + {:else} +
    + {#each mobileVM.devices as device (device.id)} +
    goto(`/devices/${device.id}`)} + onkeydown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + void goto(`/devices/${device.id}`); + } + }} + > +
    +
    +

    {device.name}

    +

    + {device.externalDeviceId} +

    +
    + + e.stopPropagation()} + > + + + + + + Delete device? + + + This deletes the device and all related SMS/media data. + Files in storage linked to this device are also removed. + + + + Cancel + { + e.stopPropagation(); + await mobileVM.deleteDevice(device.id); + }} + > + Delete + + + + +
    + +
    +
    +

    Manufacturer / Model

    +

    {device.manufacturer} / {device.model}

    +
    +
    +

    Android

    +

    {device.androidVersion}

    +
    +
    +

    Created

    +

    + {new Date(device.createdAt).toLocaleString()} +

    +
    +
    +

    Last Ping

    +

    + {mobileVM.formatLastPing(device.lastPingAt)} +

    +
    +
    +
    + {/each} +
    + + + {/if} +
    +
    +
    diff --git a/apps/main/src/routes/(main)/devices/+page.svelte b/apps/main/src/routes/(main)/devices/+page.svelte new file mode 100644 index 0000000..f074940 --- /dev/null +++ b/apps/main/src/routes/(main)/devices/+page.svelte @@ -0,0 +1 @@ +Show the running devices list here diff --git a/apps/main/src/routes/(main)/links/+page.svelte b/apps/main/src/routes/(main)/links/+page.svelte new file mode 100644 index 0000000..b3fe352 --- /dev/null +++ b/apps/main/src/routes/(main)/links/+page.svelte @@ -0,0 +1 @@ +everything related to links here 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..07c1354 --- /dev/null +++ b/apps/main/src/routes/(main)/notifications/+page.svelte @@ -0,0 +1,27 @@ + + + + + diff --git a/apps/main/src/routes/+layout.svelte b/apps/main/src/routes/+layout.svelte new file mode 100644 index 0000000..a374c05 --- /dev/null +++ b/apps/main/src/routes/+layout.svelte @@ -0,0 +1,20 @@ + + + + {$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..daf04c8 --- /dev/null +++ b/apps/main/src/routes/api/debug/users/+server.ts @@ -0,0 +1,129 @@ +import { UserRoleMap } from "@pkg/logic/domains/user/data"; +import { auth } from "@pkg/logic/domains/auth/config.base"; +import { account, user } from "@pkg/db/schema/better.auth.schema"; +import type { RequestHandler } from "./$types"; +import { settings } from "@pkg/settings"; +import { logger } from "@pkg/logger"; +import { db, eq, or } 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 === settings.debugKey; +} + +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(), + password: v.pipe(v.string(), v.minLength(6)), + email: v.optional(v.string()), + usertype: v.optional(v.enum(UserRoleMap)), + }); + const res = v.safeParse(_schema, data); + if (!res.success) { + return new Response("Invalid data", { status: 400 }); + } + const resData = res.output; + const email = resData.email ?? `${resData.username}@debug.local`; + const usertype = resData.usertype ?? UserRoleMap.admin; + + const existingUser = await db.query.user + .findFirst({ + where: or(eq(user.username, resData.username), eq(user.email, email)), + columns: { id: true }, + }) + .execute(); + + if (existingUser?.id) { + return new Response("User already exists", { status: 409 }); + } + + logger.info( + `Creating debug user ${resData.username} | ${email} (${usertype})`, + ); + + const userId = nanoid(); + const accountId = nanoid(); + const hashedPassword = await auth.$context.then((ctx) => + ctx.password.hash(resData.password), + ); + + await db.transaction(async (tx) => { + await tx + .insert(user) + .values({ + id: userId, + username: resData.username, + email, + emailVerified: true, + name: resData.username, + role: usertype, + createdAt: new Date(), + updatedAt: new Date(), + }) + .execute(); + + await tx + .insert(account) + .values({ + id: accountId, + accountId: userId, + providerId: "credential", + userId, + password: hashedPassword, + createdAt: new Date(), + updatedAt: new Date(), + }) + .execute(); + }); + + logger.info("Debug user created with credential account", { + userId, + username: resData.username, + email, + }); + + return new Response( + JSON.stringify({ + id: userId, + username: resData.username, + email, + role: usertype, + }), + { status: 200 }, + ); +}; 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..10f5322 --- /dev/null +++ b/apps/main/src/routes/auth/login/+page.server.ts @@ -0,0 +1,13 @@ +import { auth } from "@pkg/logic/domains/auth/config.base"; +import type { PageServerLoad } from "./$types"; +import { redirect } from "@sveltejs/kit"; + +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..7cffc76 --- /dev/null +++ b/apps/main/src/routes/auth/login/+page.svelte @@ -0,0 +1,71 @@ + + +
    + +
    +
    +
    +
    +
    + +
    + + +
    +
    +
    +
    + +
    +
    + +
    + + + Welcome Back + + + + Sign in with your username and password + +
    +
    +
    + + + + + + +

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

    +
    +
    +
    +
    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..2afacd0 --- /dev/null +++ b/apps/main/svelte.config.js @@ -0,0 +1,20 @@ +import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; +import adapter from "@sveltejs/adapter-node"; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: vitePreprocess(), + kit: { + adapter: adapter({ out: "build" }), + experimental: { + remoteFunctions: true, + tracing: { server: true }, + instrumentation: { server: true }, + }, + }, + compilerOptions: { + experimental: { async: true }, + }, +}; + +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..1f01308 --- /dev/null +++ b/apps/main/vite.config.ts @@ -0,0 +1,46 @@ +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" })], + + ssr: { + external: [ + "argon2", + "node-gyp-build", + "sharp", + "@img/sharp-linux-x64", + "@img/sharp-linuxmusl-x64", + "@img/sharp-wasm32", + ], + }, + + 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/orchestrator/package.json b/apps/orchestrator/package.json new file mode 100644 index 0000000..5b0eda8 --- /dev/null +++ b/apps/orchestrator/package.json @@ -0,0 +1,33 @@ +{ + "name": "@apps/orchestrator", + "type": "module", + "scripts": { + "dev": "PORT=3000 tsx watch src/index.ts", + "build": "tsc", + "prod": "HOST=0.0.0.0 PORT=3000 tsx src/index.ts" + }, + "dependencies": { + "@hono/node-server": "^1.19.9", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/auto-instrumentations-node": "^0.70.1", + "@opentelemetry/exporter-logs-otlp-proto": "^0.212.0", + "@opentelemetry/exporter-metrics-otlp-proto": "^0.212.0", + "@opentelemetry/exporter-trace-otlp-proto": "^0.212.0", + "@opentelemetry/sdk-logs": "^0.212.0", + "@opentelemetry/sdk-metrics": "^2.1.0", + "@opentelemetry/sdk-node": "^0.212.0", + "@pkg/db": "workspace:*", + "@pkg/logger": "workspace:*", + "@pkg/logic": "workspace:*", + "@pkg/result": "workspace:*", + "@pkg/settings": "workspace:*", + "hono": "^4.12.8", + "import-in-the-middle": "^3.0.0", + "valibot": "^1.2.0" + }, + "devDependencies": { + "@types/node": "^25.3.2", + "tsx": "^4.21.0", + "typescript": "^5.9.3" + } +} diff --git a/apps/orchestrator/src/index.ts b/apps/orchestrator/src/index.ts new file mode 100644 index 0000000..2777468 --- /dev/null +++ b/apps/orchestrator/src/index.ts @@ -0,0 +1,29 @@ +import "./instrumentation.js"; + +import { createHttpTelemetryMiddleware } from "@pkg/logic/core/http.telemetry"; +import { serve } from "@hono/node-server"; +import { Hono } from "hono"; + +const app = new Hono().use("*", createHttpTelemetryMiddleware("orchestrator")); + +const host = process.env.HOST || "0.0.0.0"; +const port = Number(process.env.PORT || "3000"); + +app.get("/health", (c) => { + return c.json({ ok: true }); +}); + +app.get("/ping", (c) => { + return c.text("pong"); +}); + +serve( + { + fetch: app.fetch, + port, + hostname: host, + }, + (info) => { + console.log(`Server is running on http://${host}:${info.port}`); + }, +); diff --git a/apps/orchestrator/src/instrumentation.ts b/apps/orchestrator/src/instrumentation.ts new file mode 100644 index 0000000..6b3ec48 --- /dev/null +++ b/apps/orchestrator/src/instrumentation.ts @@ -0,0 +1,50 @@ +import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node"; +import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto"; +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto"; +import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics"; +import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-proto"; +import { createAddHookMessageChannel } from "import-in-the-middle"; +import { BatchLogRecordProcessor } from "@opentelemetry/sdk-logs"; +import { NodeSDK } from "@opentelemetry/sdk-node"; +import { settings } from "@pkg/settings"; +import { register } from "node:module"; + +const { registerOptions } = createAddHookMessageChannel(); +register("import-in-the-middle/hook.mjs", import.meta.url, registerOptions); + +const normalizedEndpoint = settings.otelExporterOtlpHttpEndpoint.startsWith( + "http", +) + ? settings.otelExporterOtlpHttpEndpoint + : `http://${settings.otelExporterOtlpHttpEndpoint}`; + +if (!process.env.OTEL_EXPORTER_OTLP_ENDPOINT) { + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = normalizedEndpoint; +} + +const sdk = new NodeSDK({ + serviceName: `${settings.otelServiceName}-processor`, + traceExporter: new OTLPTraceExporter(), + metricReader: new PeriodicExportingMetricReader({ + exporter: new OTLPMetricExporter(), + exportIntervalMillis: 10_000, + }), + logRecordProcessors: [new BatchLogRecordProcessor(new OTLPLogExporter())], + instrumentations: [ + getNodeAutoInstrumentations({ + "@opentelemetry/instrumentation-winston": { + // We add OpenTelemetryTransportV3 explicitly in @pkg/logger. + disableLogSending: true, + }, + }), + ], +}); + +sdk.start(); + +const shutdown = () => { + void sdk.shutdown(); +}; + +process.on("SIGTERM", shutdown); +process.on("SIGINT", shutdown); diff --git a/apps/orchestrator/tsconfig.json b/apps/orchestrator/tsconfig.json new file mode 100644 index 0000000..fce7dfe --- /dev/null +++ b/apps/orchestrator/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "verbatimModuleSyntax": false, + "skipLibCheck": true, + "types": [ + "node" + ], + "baseUrl": ".", + "paths": { + "@pkg/logic": ["../../packages/logic"], + "@pkg/logic/*": ["../../packages/logic/*"], + "@pkg/db": ["../../packages/db"], + "@pkg/db/*": ["../../packages/db/*"], + "@pkg/logger": ["../../packages/logger"], + "@pkg/logger/*": ["../../packages/logger/*"], + "@pkg/result": ["../../packages/result"], + "@pkg/result/*": ["../../packages/result/*"], + "@pkg/settings": ["../../packages/settings"], + "@pkg/settings/*": ["../../packages/settings/*"], + "@/*": ["../../packages/logic/*"], + "@core/*": ["../../packages/logic/core/*"], + "@domains/*": ["../../packages/logic/domains/*"] + }, + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx", + "outDir": "./dist" + }, + "exclude": ["node_modules"] +} 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..1c69f76 --- /dev/null +++ b/dev/docker-compose.dev.yaml @@ -0,0 +1,212 @@ +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: + - iotam_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: + - iotam_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: + - iotam_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: + - iotam_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 + - iotam_clickhouse_user_scripts:/var/lib/clickhouse/user_scripts/ + - ./clickhouse-cluster.xml:/etc/clickhouse-server/config.d/cluster.xml + - iotam_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: + - iotam_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: + iotam_dev_valkey_data: + iotam_dev_postgresql_data: + iotam_clickhouse: + iotam_clickhouse_user_scripts: + iotam_sqlite: + iotam_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/app_builder.Dockerfile b/dockerfiles/app_builder.Dockerfile new file mode 100644 index 0000000..96defbc --- /dev/null +++ b/dockerfiles/app_builder.Dockerfile @@ -0,0 +1,47 @@ +FROM node:25.6.1-bookworm-slim AS app-builder + +ARG ANDROID_SDK_ROOT=/opt/android-sdk +ARG ANDROID_CMDLINE_TOOLS_VERSION=13114758 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + bash \ + ca-certificates \ + curl \ + git \ + openjdk-17-jdk \ + unzip \ + && rm -rf /var/lib/apt/lists/* + +ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 +ENV ANDROID_SDK_ROOT=${ANDROID_SDK_ROOT} +ENV ANDROID_HOME=${ANDROID_SDK_ROOT} +ENV PATH=${JAVA_HOME}/bin:${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin:${ANDROID_SDK_ROOT}/platform-tools:${PATH} + +RUN mkdir -p ${ANDROID_SDK_ROOT}/cmdline-tools /tmp/android-sdk && \ + curl -fsSL -o /tmp/android-sdk/cmdline-tools.zip \ + https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_CMDLINE_TOOLS_VERSION}_latest.zip && \ + unzip -q /tmp/android-sdk/cmdline-tools.zip -d /tmp/android-sdk && \ + mv /tmp/android-sdk/cmdline-tools ${ANDROID_SDK_ROOT}/cmdline-tools/latest && \ + yes | sdkmanager --licenses > /dev/null && \ + sdkmanager \ + "platform-tools" \ + "platforms;android-36" \ + "build-tools;36.0.0" && \ + rm -rf /tmp/android-sdk + +RUN npm i -g pnpm + +WORKDIR /app + +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json ./ +COPY apps/app-builder/package.json ./apps/app-builder/package.json +COPY packages ./packages + +RUN pnpm install --frozen-lockfile + +COPY apps/app-builder ./apps/app-builder +COPY mobile ./mobile + +EXPOSE 3000 + +CMD ["pnpm", "--filter", "@apps/app-builder", "run", "prod"] diff --git a/dockerfiles/main.Dockerfile b/dockerfiles/main.Dockerfile new file mode 100644 index 0000000..41eb13d --- /dev/null +++ b/dockerfiles/main.Dockerfile @@ -0,0 +1,27 @@ +FROM node:25.6.1 AS production + +RUN npm i -g pnpm + +WORKDIR /app + +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json ./ + +COPY apps/main/package.json ./apps/main/package.json + +COPY packages ./packages + +RUN pnpm install + +COPY apps/main ./apps/main + +RUN pnpm install + +RUN pnpm 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..f7d7b23 --- /dev/null +++ b/dockerfiles/migrator.Dockerfile @@ -0,0 +1,18 @@ +FROM node:25.6.1 AS base + +RUN npm i -g pnpm + +FROM base AS primary + +WORKDIR /app + +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json ./ + +COPY packages/settings packages/settings +COPY packages/db packages/db + +RUN pnpm 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..97f6d63 --- /dev/null +++ b/dockerfiles/processor.Dockerfile @@ -0,0 +1,21 @@ +FROM node:25.6.1-alpine AS deps + +RUN npm i -g pnpm + +WORKDIR /app + +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json ./ + +COPY apps/processor/package.json ./apps/processor/package.json + +COPY packages ./packages + +RUN pnpm install --frozen-lockfile + +COPY apps/processor ./apps/processor + +RUN pnpm install + +EXPOSE 3000 + +CMD ["pnpm", "--filter", "@apps/processor", "run", "prod"] diff --git a/package.json b/package.json new file mode 100644 index 0000000..b4000fa --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "@illusory/mapp", + "version": "1.0.0", + "private": true, + "engines": { + "node": ">=24" + }, + "packageManager": "pnpm@10.30.3", + "scripts": { + "build": "turbo build", + "dev": "turbo dev --concurrency=8", + "auth:schemagen": "source .env && cd packages/logic && pnpm 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.8.16", + "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/package.json b/packages/db/package.json new file mode 100644 index 0000000..3f6ba23 --- /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 --config=drizzle.config.ts --verbose" + }, + "dependencies": { + "@pkg/settings": "workspace:*", + "dotenv": "^16.4.7", + "drizzle-orm": "^0.45.1", + "postgres": "^3.4.8" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/pg": "^8.11.10", + "drizzle-kit": "^0.31.9" + }, + "peerDependencies": { + "typescript": "^5.9.3" + } +} 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..241f3ee --- /dev/null +++ b/packages/db/schema/index.ts @@ -0,0 +1,4 @@ +export * from "./auth.schema"; +export * from "./better.auth.schema"; +export * from "./general.schema"; +export * from "./task.schema"; diff --git a/packages/db/schema/task.schema.ts b/packages/db/schema/task.schema.ts new file mode 100644 index 0000000..535609f --- /dev/null +++ b/packages/db/schema/task.schema.ts @@ -0,0 +1,35 @@ +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 task = pgTable("task", { + id: text("id").primaryKey(), + type: varchar("type", { length: 32 }).notNull(), + status: varchar("status", { length: 16 }).notNull(), + progress: integer("progress").default(0).notNull(), + payload: json("payload").$type>(), + result: json("result").$type>(), + error: json("error").$type>(), + userId: text("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + resourceId: text("resource_id").notNull(), + startedAt: timestamp("started_at"), + completedAt: timestamp("completed_at"), + createdAt: timestamp("created_at").notNull(), + updatedAt: timestamp("updated_at").notNull(), +}); + +export const taskRelations = relations(task, ({ one }) => ({ + userAccount: one(user, { + fields: [task.userId], + references: [user.id], + }), +})); diff --git a/packages/keystore/build.queue.ts b/packages/keystore/build.queue.ts new file mode 100644 index 0000000..8cb79c7 --- /dev/null +++ b/packages/keystore/build.queue.ts @@ -0,0 +1,17 @@ +import { getRedisInstance } from "./index"; + +export const APK_BUILD_QUEUE_CHANNEL = "queue:apk-build"; + +export async function publishApkBuildSignal(payload: { + taskId: string; + buildId: string; +}) { + const redis = getRedisInstance(); + await redis.connect().catch(() => undefined); + await redis.publish(APK_BUILD_QUEUE_CHANNEL, JSON.stringify(payload)); +} + +export function getApkBuildSubscriber() { + const subscriber = getRedisInstance().duplicate(); + return subscriber; +} diff --git a/packages/keystore/index.ts b/packages/keystore/index.ts new file mode 100644 index 0000000..ce48f0c --- /dev/null +++ b/packages/keystore/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/keystore/package.json b/packages/keystore/package.json new file mode 100644 index 0000000..9299919 --- /dev/null +++ b/packages/keystore/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pkg/keystore", + "module": "index.ts", + "type": "module", + "private": true, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.9.3" + }, + "dependencies": { + "ioredis": "^5.6.1" + } +} diff --git a/packages/logger/index.ts b/packages/logger/index.ts new file mode 100644 index 0000000..47565fb --- /dev/null +++ b/packages/logger/index.ts @@ -0,0 +1,281 @@ +import { OpenTelemetryTransportV3 } from "@opentelemetry/winston-transport"; +import { settings } from "@pkg/settings"; +import type { Err } from "@pkg/result"; +import winston from "winston"; +import util from "util"; + +process.on("warning", (warning) => { + const msg = String(warning?.message || ""); + const name = String((warning as any)?.name || ""); + + if ( + name === "TimeoutNegativeWarning" || + msg.includes("TimeoutNegativeWarning") || + msg.includes("Timeout duration was set to 1") + ) { + return; + } + + console.warn(warning); +}); + +const levels = { + error: 0, + warn: 1, + info: 2, + http: 3, + debug: 4, +} as const; + +const colors = { + error: "red", + warn: "yellow", + info: "green", + http: "magenta", + debug: "white", +}; + +const level = () => { + const envLevel = process.env.LOG_LEVEL?.toLowerCase(); + if (envLevel && Object.prototype.hasOwnProperty.call(levels, envLevel)) { + return envLevel as keyof typeof levels; + } + return settings.isDevelopment ? "debug" : "warn"; +}; + +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 } = info; + + const extra = Object.fromEntries( + Object.entries(info).filter( + ([key]) => key !== "level" && key !== "message" && key !== "timestamp", + ), + ); + + const formattedMessage = + message instanceof Error + ? message.stack || message.message + : typeof message === "object" + ? util.inspect(message, { depth: null, colors: true }) + : String(message); + + const formattedExtra = + Object.keys(extra).length > 0 + ? `\n${util.inspect(extra, { depth: null, colors: true })}` + : ""; + + return `[${level}] ${timestamp}: ${formattedMessage}${formattedExtra}`; + }), +); + +winston.addColors(colors); + +const logger = winston.createLogger({ + level: level(), + levels, + format: consoleFormat, + transports: [ + new winston.transports.Console({ format: consoleFormat }), + new OpenTelemetryTransportV3(), + ], + exceptionHandlers: [new winston.transports.Console({ format: consoleFormat })], + rejectionHandlers: [new winston.transports.Console({ format: consoleFormat })], +}); + +const stream = { write: (message: string) => logger.http(message.trim()) }; + +type LogLevel = keyof typeof levels; +type ErrorKind = "validation" | "auth" | "db" | "external" | "unknown"; +type FlowCtxLike = { + flowId: string; + userId?: string; + sessionId?: string; +}; + +const REDACTED_KEYS = new Set([ + "password", + "code", + "secret", + "token", + "verificationtoken", + "backupcodes", + "authorization", + "headers", + "hash", +]); + +function sanitizeMeta(input: Record) { + const sanitized = Object.fromEntries( + Object.entries(input).map(([key, value]) => { + if (value === undefined) { + return [key, undefined]; + } + const lowered = key.toLowerCase(); + if (REDACTED_KEYS.has(lowered)) { + return [key, "[REDACTED]"]; + } + return [key, value]; + }), + ); + + return Object.fromEntries( + Object.entries(sanitized).filter(([, value]) => value !== undefined), + ); +} + +function classifyError(error: unknown): ErrorKind { + if (!error) return "unknown"; + if (typeof error === "object" && error && "code" in error) { + const code = String((error as { code?: unknown }).code ?? "").toUpperCase(); + if (code.includes("AUTH") || code.includes("UNAUTHORIZED")) return "auth"; + if (code.includes("VALIDATION") || code.includes("INVALID")) return "validation"; + if (code.includes("DB") || code.includes("DATABASE")) return "db"; + } + return "unknown"; +} + +function errorMessage(error: unknown) { + if (error instanceof Error) return error.message; + if (typeof error === "string") return error; + if (error && typeof error === "object" && "message" in error) { + return String((error as { message?: unknown }).message ?? "Unknown error"); + } + return "Unknown error"; +} + +function formatErrorDetail(error: unknown): string { + if (error instanceof Error) { + return error.stack || error.message; + } + if (typeof error === "string") { + return error; + } + if (!error) { + return "Unknown error"; + } + if (typeof error === "object") { + const candidate = error as { + code?: unknown; + message?: unknown; + description?: unknown; + detail?: unknown; + error?: unknown; + stack?: unknown; + }; + + const parts = [ + candidate.code ? `code=${String(candidate.code)}` : null, + candidate.message ? `message=${String(candidate.message)}` : null, + candidate.description + ? `description=${String(candidate.description)}` + : null, + candidate.detail + ? `detail=${ + typeof candidate.detail === "string" + ? candidate.detail + : util.inspect(candidate.detail, { + depth: null, + colors: false, + }) + }` + : null, + candidate.error + ? `error=${ + typeof candidate.error === "string" + ? candidate.error + : util.inspect(candidate.error, { + depth: null, + colors: false, + }) + }` + : null, + candidate.stack ? String(candidate.stack) : null, + ].filter(Boolean); + + return parts.length > 0 + ? parts.join(" | ") + : util.inspect(error, { depth: null, colors: false }); + } + + return String(error); +} + +function writeLog(level: LogLevel, message: string, payload: Record) { + switch (level) { + case "error": + logger.error(message, payload); + return; + case "warn": + logger.warn(message, payload); + return; + case "debug": + logger.debug(message, payload); + return; + case "http": + logger.http(message, payload); + return; + default: + logger.info(message, payload); + } +} + +function logDomainEvent({ + level = "info", + event, + fctx, + durationMs, + error, + retryable, + meta, +}: { + level?: LogLevel; + event: string; + fctx: FlowCtxLike; + durationMs?: number; + error?: unknown; + retryable?: boolean; + meta?: Record; +}) { + const payload: Record = { + event, + flowId: fctx.flowId, + userId: fctx.userId, + sessionId: fctx.sessionId, + }; + + if (durationMs !== undefined) payload.duration_ms = durationMs; + if (retryable !== undefined) payload.retryable = retryable; + if (error !== undefined) { + payload.error_kind = classifyError(error); + payload.error_message = errorMessage(error); + if (error && typeof error === "object" && "code" in error) { + payload.error_code = String( + (error as { code?: unknown }).code ?? "UNKNOWN", + ); + } + } + if (meta) { + Object.assign(payload, sanitizeMeta(meta)); + } + + writeLog(level, event, payload); +} + +function getError(payload: Err, error?: any) { + logger.error(JSON.stringify({ payload, error }, null, 2)); + 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, logDomainEvent, logger, stream }; +export { formatErrorDetail }; diff --git a/packages/logger/package.json b/packages/logger/package.json new file mode 100644 index 0000000..b2d4e79 --- /dev/null +++ b/packages/logger/package.json @@ -0,0 +1,17 @@ +{ + "name": "@pkg/logger", + "module": "index.ts", + "type": "module", + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.9.3" + }, + "dependencies": { + "@opentelemetry/winston-transport": "^0.22.0", + "@pkg/result": "workspace:*", + "@pkg/settings": "workspace:*", + "winston": "^3.17.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/http.telemetry.ts b/packages/logic/core/http.telemetry.ts new file mode 100644 index 0000000..a8ce133 --- /dev/null +++ b/packages/logic/core/http.telemetry.ts @@ -0,0 +1,88 @@ +import { context, metrics, SpanStatusCode, trace } from "@opentelemetry/api"; + +type TelemetryContext = { + req: { + method: string; + path: string; + }; + res: { + status: number; + }; +}; + +export function createHttpTelemetryMiddleware(serviceName: string) { + const tracer = trace.getTracer(`${serviceName}.http`); + const meter = metrics.getMeter(`${serviceName}.http`); + + const requestCount = meter.createCounter(`${serviceName}.http.server.requests`, { + description: `Total number of ${serviceName} HTTP requests`, + }); + + const requestDuration = meter.createHistogram( + `${serviceName}.http.server.duration`, + { + description: `${serviceName} HTTP request duration`, + unit: "ms", + }, + ); + + const activeRequests = meter.createUpDownCounter( + `${serviceName}.http.server.active_requests`, + { + description: `Number of in-flight ${serviceName} HTTP requests`, + }, + ); + + return async (c: TelemetryContext, next: () => Promise) => { + const startedAt = performance.now(); + const method = c.req.method; + const route = c.req.path; + + const span = tracer.startSpan(`http ${method} ${route}`, { + attributes: { + "http.request.method": method, + "url.path": route, + }, + }); + + activeRequests.add(1, { + "http.request.method": method, + "url.path": route, + }); + + try { + await context.with(trace.setSpan(context.active(), span), next); + + span.setAttribute("http.response.status_code", c.res.status); + span.setStatus({ + code: + c.res.status >= 500 + ? SpanStatusCode.ERROR + : SpanStatusCode.OK, + }); + } catch (error) { + span.recordException(error as Error); + span.setStatus({ + code: SpanStatusCode.ERROR, + message: `Unhandled ${serviceName} request error`, + }); + throw error; + } finally { + const durationMs = performance.now() - startedAt; + const attrs = { + "http.request.method": method, + "http.response.status_code": c.res.status, + "url.path": route, + }; + + requestCount.add(1, attrs); + requestDuration.record(durationMs, attrs); + activeRequests.add(-1, { + "http.request.method": method, + "url.path": route, + }); + + span.end(); + } + }; +} diff --git a/packages/logic/core/observability.ts b/packages/logic/core/observability.ts new file mode 100644 index 0000000..ee79463 --- /dev/null +++ b/packages/logic/core/observability.ts @@ -0,0 +1,80 @@ +import { SpanStatusCode, trace, type Attributes } from "@opentelemetry/api"; +import type { FlowExecCtx } from "./flow.execution.context"; +import { ResultAsync } from "neverthrow"; + +const tracer = trace.getTracer("@pkg/logic"); + +type BaseSpanOptions = { + name: string; + fctx?: FlowExecCtx; + attributes?: Attributes; +}; + +function spanAttributes( + fctx?: FlowExecCtx, + attributes?: Attributes, +): Attributes | undefined { + const flowAttrs: Attributes = {}; + if (fctx?.flowId) flowAttrs["flow.id"] = fctx.flowId; + if (fctx?.userId) flowAttrs["flow.user_id"] = fctx.userId; + if (fctx?.sessionId) flowAttrs["flow.session_id"] = fctx.sessionId; + + if (!attributes && Object.keys(flowAttrs).length === 0) { + return undefined; + } + return { ...flowAttrs, ...(attributes ?? {}) }; +} + +export async function withFlowSpan({ + name, + fctx, + attributes, + fn, +}: BaseSpanOptions & { + fn: () => Promise; +}): Promise { + return tracer.startActiveSpan( + name, + { attributes: spanAttributes(fctx, attributes) }, + async (span) => { + try { + const result = await fn(); + span.setStatus({ code: SpanStatusCode.OK }); + return result; + } catch (error) { + span.recordException(error as Error); + span.setStatus({ + code: SpanStatusCode.ERROR, + message: + error instanceof Error ? error.message : String(error), + }); + throw error; + } finally { + span.end(); + } + }, + ); +} + +export function traceResultAsync({ + name, + fctx, + attributes, + fn, +}: BaseSpanOptions & { + fn: () => ResultAsync; +}): ResultAsync { + return ResultAsync.fromPromise( + withFlowSpan({ + name, + fctx, + attributes, + fn: async () => + fn().match( + (value) => value, + (error) => Promise.reject(error), + ), + }), + (error) => error as E, + ); +} 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..cdf1e2b --- /dev/null +++ b/packages/logic/domains/2fa/controller.ts @@ -0,0 +1,396 @@ +import { errAsync, okAsync, ResultAsync } from "neverthrow"; +import { FlowExecCtx } from "@core/flow.execution.context"; +import { UserRepository } from "@domains/user/repository"; +import { getRedisInstance, Redis } from "@pkg/keystore"; +import { TwofaRepository } from "./repository"; +import { logDomainEvent } from "@pkg/logger"; +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 { db } from "@pkg/db"; + +export class TwofaController { + constructor( + private twofaRepo: TwofaRepository, + private userRepo: UserRepository, + private store: Redis, + private secret: string, + ) {} + + 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) => { + logDomainEvent({ + level: "error", + event: "security.twofa.user_ban_check.failed", + fctx, + error, + meta: { userId }, + }); + 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, this.secret), + ) + .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, + ) { + const startedAt = Date.now(); + logDomainEvent({ + event: "security.twofa.verify_and_enable.started", + fctx, + meta: { userId: user.id }, + }); + + return this.is2faEnabled(fctx, user.id) + .andThen((enabled) => { + if (enabled) { + logDomainEvent({ + level: "warn", + event: "security.twofa.verify_and_enable.failed", + fctx, + durationMs: Date.now() - startedAt, + error: { + code: "ALREADY_ENABLED", + message: "2FA already enabled", + }, + meta: { userId: user.id }, + }); + return errAsync(twofaErrors.alreadyEnabled(fctx)); + } + return okAsync(undefined); + }) + .andThen(() => + 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(() => { + logDomainEvent({ + event: "security.twofa.verify_and_enable.succeeded", + fctx, + durationMs: Date.now() - startedAt, + meta: { userId: user.id }, + }); + return true; + }); + } + logDomainEvent({ + level: "warn", + event: "security.twofa.verify_and_enable.failed", + fctx, + durationMs: Date.now() - startedAt, + error: { + code: "INVALID_CODE", + message: "2FA code verification failed", + }, + meta: { userId: user.id }, + }); + 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) => { + logDomainEvent({ + level: "error", + event: "security.twofa.mark_initial_verification.failed", + fctx: { flowId: crypto.randomUUID() }, + 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, + settings.twoFaSecret, + ); +} 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..784b948 --- /dev/null +++ b/packages/logic/domains/2fa/repository.ts @@ -0,0 +1,695 @@ +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 { crypto } from "@otplib/plugin-crypto-noble"; +import { base32 } from "@otplib/plugin-base32-scure"; +import { and, Database, eq, gt, lt } from "@pkg/db"; +import { generate, verify } from "@otplib/totp"; +import { settings } from "@core/settings"; +import type { Err } from "@pkg/result"; +import { twofaErrors } from "./errors"; +import { Redis } from "@pkg/keystore"; +import { logDomainEvent, logger } from "@pkg/logger"; +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 = verify({ secret, token: code, crypto, base32 }); + 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 { + const startedAt = Date.now(); + logDomainEvent({ + event: "security.twofa.get_info.started", + fctx, + meta: { userId }, + }); + + return ResultAsync.fromPromise( + this.db.query.twoFactor.findFirst({ + where: eq(twoFactor.userId, userId), + }), + (error) => { + logDomainEvent({ + level: "error", + event: "security.twofa.get_info.failed", + fctx, + durationMs: Date.now() - startedAt, + error, + meta: { userId }, + }); + return twofaErrors.dbError(fctx, "Failed to query 2FA info"); + }, + ).andThen((found) => { + if (!found) { + logDomainEvent({ + level: "warn", + event: "security.twofa.get_info.failed", + fctx, + durationMs: Date.now() - startedAt, + error: { code: "NOT_FOUND", message: "2FA info not found" }, + meta: { userId }, + }); + if (returnUndefined) { + return okAsync(undefined); + } + return errAsync(twofaErrors.notFound(fctx)); + } + logDomainEvent({ + event: "security.twofa.get_info.succeeded", + fctx, + durationMs: Date.now() - startedAt, + meta: { 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, + secret: string, + ): ResultAsync { + const startedAt = Date.now(); + logDomainEvent({ + event: "security.twofa.setup.started", + fctx, + meta: { userId }, + }); + + return ResultAsync.fromSafePromise( + (async () => { + const token = await generate({ + secret, + crypto, + base32, + }); + const payload = { + secret: token, + lastUsedCode: "", + tries: 0, + } as TwoFaSetup; + await this.store.setex( + this.getKey(userId), + this.EXPIRY_TIME, + JSON.stringify(payload), + ); + logDomainEvent({ + event: "security.twofa.setup.succeeded", + fctx, + durationMs: Date.now() - startedAt, + meta: { userId, expiresInSec: this.EXPIRY_TIME }, + }); + return secret; + })(), + ).mapErr((error) => { + logDomainEvent({ + level: "error", + event: "security.twofa.setup.failed", + fctx, + durationMs: Date.now() - startedAt, + error, + meta: { userId }, + }); + return twofaErrors.dbError(fctx, "Setting to data store failed"); + }); + } + + verifyAndEnable2FA( + fctx: FlowExecCtx, + userId: string, + code: string, + ): ResultAsync { + const startedAt = Date.now(); + logDomainEvent({ + event: "security.twofa.verify_enable.started", + fctx, + meta: { userId }, + }); + + return ResultAsync.fromPromise( + this.store.get(this.getKey(userId)), + () => twofaErrors.dbError(fctx, "Failed to get setup session"), + ) + .andThen((payload) => { + if (!payload) { + logDomainEvent({ + level: "warn", + event: "security.twofa.verify_enable.failed", + fctx, + durationMs: Date.now() - startedAt, + error: { + code: "SETUP_NOT_FOUND", + message: "2FA setup session not found", + }, + meta: { 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) { + logDomainEvent({ + level: "warn", + event: "security.twofa.verify_enable.failed", + fctx, + durationMs: Date.now() - startedAt, + error: { + code: "MAX_ATTEMPTS_REACHED", + message: "Max setup attempts reached", + }, + meta: { userId, attempts: 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 + ) { + logDomainEvent({ + level: "warn", + event: "security.twofa.verify_enable.failed", + fctx, + durationMs: Date.now() - startedAt, + error: { + code: "INVALID_CODE", + message: "Invalid or replayed setup code", + }, + meta: { + userId, + attempts: 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(() => { + logDomainEvent({ + event: "security.twofa.verify_enable.succeeded", + fctx, + durationMs: Date.now() - startedAt, + meta: { 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 { + const startedAt = Date.now(); + logDomainEvent({ + event: "security.twofa.create_session.started", + fctx, + meta: { 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(), + (error) => { + logDomainEvent({ + level: "error", + event: "security.twofa.create_session.failed", + fctx, + durationMs: Date.now() - startedAt, + error, + meta: { userId: params.userId }, + }); + return twofaErrors.dbError( + fctx, + "Failed to create 2FA session", + ); + }, + ).map(([session]) => { + logDomainEvent({ + event: "security.twofa.create_session.succeeded", + fctx, + durationMs: Date.now() - startedAt, + meta: { + twofaSessionId: session.id, + userId: params.userId, + }, + }); + return session as TwoFaSession; + }), + ); + } + + getSessionByToken( + fctx: FlowExecCtx, + token: string, + ): ResultAsync { + const startedAt = Date.now(); + logDomainEvent({ + level: "debug", + event: "security.twofa.get_session.started", + fctx, + }); + + return ResultAsync.fromPromise( + this.db + .select() + .from(twofaSessions) + .where( + and( + eq(twofaSessions.verificationToken, token), + gt(twofaSessions.expiresAt, new Date()), + ), + ) + .limit(1), + (error) => { + logDomainEvent({ + level: "error", + event: "security.twofa.get_session.failed", + fctx, + durationMs: Date.now() - startedAt, + error, + }); + return twofaErrors.dbError(fctx, "Failed to query 2FA session"); + }, + ).map((result) => { + if (!result.length) { + logDomainEvent({ + level: "warn", + event: "security.twofa.get_session.failed", + fctx, + durationMs: Date.now() - startedAt, + error: { + code: "SESSION_NOT_FOUND", + message: "2FA session not found or expired", + }, + }); + return null; + } + logDomainEvent({ + level: "debug", + event: "security.twofa.get_session.succeeded", + fctx, + durationMs: Date.now() - startedAt, + meta: { twofaSessionId: 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 { + const startedAt = Date.now(); + logDomainEvent({ + event: "security.twofa.cleanup_expired.started", + fctx, + }); + + return ResultAsync.fromPromise( + this.db + .delete(twofaSessions) + .where(lt(twofaSessions.expiresAt, new Date())), + (error) => { + logDomainEvent({ + level: "error", + event: "security.twofa.cleanup_expired.failed", + fctx, + durationMs: Date.now() - startedAt, + error, + }); + return twofaErrors.dbError( + fctx, + "Failed to cleanup expired sessions", + ); + }, + ).map((result) => { + const count = result.length || 0; + logDomainEvent({ + event: "security.twofa.cleanup_expired.succeeded", + fctx, + durationMs: Date.now() - startedAt, + meta: { count }, + }); + return count; + }); + } +} 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..91ea862 --- /dev/null +++ b/packages/logic/domains/auth/config.base.ts @@ -0,0 +1,99 @@ +import { + admin, + customSession, + multiSession, + username, +} from "better-auth/plugins"; +import { drizzleAdapter } from "better-auth/adapters/drizzle"; +import { UserRoleMap } from "@domains/user/data"; +import { getRedisInstance } from "@pkg/keystore"; +import { settings } from "@core/settings"; +import { betterAuth } from "better-auth"; +import { logger } from "@pkg/logger"; +import { db, schema } from "@pkg/db"; + +const COOKIE_CACHE_MAX_AGE = 60 * 5; +const USERNAME_REGEX = /^[a-zA-Z0-9_]+$/; + +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) => { + return USERNAME_REGEX.test(username); + }, + }), + 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: "debug", + }, + 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: { + 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..3b5be9d --- /dev/null +++ b/packages/logic/domains/auth/controller.ts @@ -0,0 +1,60 @@ +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 { authErrors } from "./errors"; +import { logger } from "@pkg/logger"; +import { nanoid } from "nanoid"; +import { db } from "@pkg/db"; + +export class AuthController { + constructor(private accountRepo: AccountRepository) {} + + 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 }, + }; + }); + } +} + +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..39104fa --- /dev/null +++ b/packages/logic/domains/auth/errors.ts @@ -0,0 +1,32 @@ +import { FlowExecCtx } from "@/core/flow.execution.context"; +import { getError } from "@pkg/logger"; +import { ERROR_CODES, type Err } from "@pkg/result"; + +export const authErrors = { + 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..0f584f7 --- /dev/null +++ b/packages/logic/domains/notifications/data.ts @@ -0,0 +1,115 @@ +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 +>; + +export type NotificationsQueryInput = { + isRead?: boolean; + isArchived?: boolean; + type?: string; + category?: string; + priority?: string; + search?: string; + page?: number; + pageSize?: number; + sortBy?: string; + sortOrder?: string; +}; + +// 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..8578da3 --- /dev/null +++ b/packages/logic/domains/notifications/repository.ts @@ -0,0 +1,453 @@ +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 { logDomainEvent } from "@pkg/logger"; + +export class NotificationRepository { + constructor(private db: Database) {} + + getNotifications( + fctx: FlowExecCtx, + filters: NotificationFilters, + pagination: PaginationOptions, + ): ResultAsync { + const startedAt = Date.now(); + logDomainEvent({ + event: "notifications.list.started", + fctx, + meta: { + hasSearch: Boolean(filters.search), + isRead: filters.isRead, + isArchived: filters.isArchived, + page: pagination.page, + pageSize: pagination.pageSize, + sortBy: pagination.sortBy, + sortOrder: pagination.sortOrder, + }, + }); + + 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) => { + logDomainEvent({ + level: "error", + event: "notifications.list.failed", + fctx, + durationMs: Date.now() - startedAt, + 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) => { + logDomainEvent({ + level: "error", + event: "notifications.list.failed", + fctx, + durationMs: Date.now() - startedAt, + error, + }); + return notificationErrors.getNotificationsFailed( + fctx, + error instanceof Error ? error.message : String(error), + ); + }, + ).map((data) => { + const totalPages = Math.ceil(total / pageSize); + logDomainEvent({ + event: "notifications.list.succeeded", + fctx, + durationMs: Date.now() - startedAt, + meta: { + count: data.length, + page, + totalPages, + }, + }); + + return { + data: data as Notification[], + total, + page, + pageSize, + totalPages, + }; + }); + }); + } + + markAsRead( + fctx: FlowExecCtx, + notificationIds: number[], + userId: string, + ): ResultAsync { + const startedAt = Date.now(); + logDomainEvent({ + event: "notifications.mark_read.started", + fctx, + meta: { userId, notificationCount: notificationIds.length }, + }); + + 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) => { + logDomainEvent({ + level: "error", + event: "notifications.mark_read.failed", + fctx, + durationMs: Date.now() - startedAt, + error, + }); + return notificationErrors.markAsReadFailed( + fctx, + error instanceof Error ? error.message : String(error), + ); + }, + ).map(() => { + logDomainEvent({ + event: "notifications.mark_read.succeeded", + fctx, + durationMs: Date.now() - startedAt, + meta: { notificationCount: notificationIds.length }, + }); + return true; + }); + } + + markAsUnread( + fctx: FlowExecCtx, + notificationIds: number[], + userId: string, + ): ResultAsync { + const startedAt = Date.now(); + logDomainEvent({ + event: "notifications.mark_unread.started", + fctx, + meta: { userId, notificationCount: notificationIds.length }, + }); + + 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) => { + logDomainEvent({ + level: "error", + event: "notifications.mark_unread.failed", + fctx, + durationMs: Date.now() - startedAt, + error, + }); + return notificationErrors.markAsUnreadFailed( + fctx, + error instanceof Error ? error.message : String(error), + ); + }, + ).map(() => { + logDomainEvent({ + event: "notifications.mark_unread.succeeded", + fctx, + durationMs: Date.now() - startedAt, + meta: { notificationCount: notificationIds.length }, + }); + return true; + }); + } + + archive( + fctx: FlowExecCtx, + notificationIds: number[], + userId: string, + ): ResultAsync { + const startedAt = Date.now(); + logDomainEvent({ + event: "notifications.archive.started", + fctx, + meta: { userId, notificationCount: notificationIds.length }, + }); + + 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) => { + logDomainEvent({ + level: "error", + event: "notifications.archive.failed", + fctx, + durationMs: Date.now() - startedAt, + error, + }); + return notificationErrors.archiveFailed( + fctx, + error instanceof Error ? error.message : String(error), + ); + }, + ).map(() => { + logDomainEvent({ + event: "notifications.archive.succeeded", + fctx, + durationMs: Date.now() - startedAt, + meta: { notificationCount: notificationIds.length }, + }); + return true; + }); + } + + unarchive( + fctx: FlowExecCtx, + notificationIds: number[], + userId: string, + ): ResultAsync { + const startedAt = Date.now(); + logDomainEvent({ + event: "notifications.unarchive.started", + fctx, + meta: { userId, notificationCount: notificationIds.length }, + }); + + 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) => { + logDomainEvent({ + level: "error", + event: "notifications.unarchive.failed", + fctx, + durationMs: Date.now() - startedAt, + error, + }); + return notificationErrors.unarchiveFailed( + fctx, + error instanceof Error ? error.message : String(error), + ); + }, + ).map(() => { + logDomainEvent({ + event: "notifications.unarchive.succeeded", + fctx, + durationMs: Date.now() - startedAt, + meta: { notificationCount: notificationIds.length }, + }); + return true; + }); + } + + deleteNotifications( + fctx: FlowExecCtx, + notificationIds: number[], + userId: string, + ): ResultAsync { + const startedAt = Date.now(); + logDomainEvent({ + event: "notifications.delete.started", + fctx, + meta: { userId, notificationCount: notificationIds.length }, + }); + + return ResultAsync.fromPromise( + this.db + .delete(notifications) + .where( + and( + eq(notifications.userId, userId), + sql`${notifications.id} = ANY(${notificationIds})`, + ), + ), + (error) => { + logDomainEvent({ + level: "error", + event: "notifications.delete.failed", + fctx, + durationMs: Date.now() - startedAt, + error, + }); + return notificationErrors.deleteNotificationsFailed( + fctx, + error instanceof Error ? error.message : String(error), + ); + }, + ).map(() => { + logDomainEvent({ + event: "notifications.delete.succeeded", + fctx, + durationMs: Date.now() - startedAt, + meta: { notificationCount: notificationIds.length }, + }); + return true; + }); + } + + getUnreadCount( + fctx: FlowExecCtx, + userId: string, + ): ResultAsync { + const startedAt = Date.now(); + logDomainEvent({ + event: "notifications.unread_count.started", + fctx, + meta: { 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) => { + logDomainEvent({ + level: "error", + event: "notifications.unread_count.failed", + fctx, + durationMs: Date.now() - startedAt, + error, + }); + return notificationErrors.getUnreadCountFailed( + fctx, + error instanceof Error ? error.message : String(error), + ); + }, + ).map((result) => { + const count = result[0]?.count || 0; + logDomainEvent({ + event: "notifications.unread_count.succeeded", + fctx, + durationMs: Date.now() - startedAt, + meta: { count }, + }); + return count; + }); + } +} diff --git a/packages/logic/domains/tasks/controller.ts b/packages/logic/domains/tasks/controller.ts new file mode 100644 index 0000000..f51382a --- /dev/null +++ b/packages/logic/domains/tasks/controller.ts @@ -0,0 +1,72 @@ +import { db } from "@pkg/db"; +import { FlowExecCtx } from "@core/flow.execution.context"; +import { CreateTask, TaskStatus, TaskType, UpdateTask } from "./data"; +import { TasksRepository } from "./repository"; + +export class TasksController { + constructor(private tasksRepo: TasksRepository) {} + + createTask(fctx: FlowExecCtx, taskData: CreateTask) { + return this.tasksRepo.createTask(fctx, taskData); + } + + getTaskById(fctx: FlowExecCtx, taskId: string) { + return this.tasksRepo.getTaskById(fctx, taskId); + } + + updateTask(fctx: FlowExecCtx, taskId: string, updates: UpdateTask) { + return this.tasksRepo.updateTask(fctx, taskId, updates); + } + + deleteTask(fctx: FlowExecCtx, taskId: string) { + return this.tasksRepo.deleteTask(fctx, taskId); + } + + getTasksByStatuses(fctx: FlowExecCtx, statuses: TaskStatus[]) { + return this.tasksRepo.getTasksByStatuses(fctx, statuses); + } + + getTasksByTypeAndStatuses( + fctx: FlowExecCtx, + type: TaskType, + statuses: TaskStatus[], + ) { + return this.tasksRepo.getTasksByTypeAndStatuses(fctx, type, statuses); + } + + markTaskAsCompleted( + fctx: FlowExecCtx, + taskId: string, + result?: Record, + ) { + return this.tasksRepo.markTaskAsCompleted(fctx, taskId, result); + } + + markTaskAsFailed(fctx: FlowExecCtx, taskId: string, error: any) { + return this.tasksRepo.markTaskAsFailed(fctx, taskId, error); + } + + updateTaskProgress(fctx: FlowExecCtx, taskId: string, progress: number) { + return this.tasksRepo.updateTask(fctx, taskId, { + progress: Math.max(0, Math.min(100, progress)), + }); + } + + cancelTask(fctx: FlowExecCtx, taskId: string) { + return this.tasksRepo.updateTask(fctx, taskId, { + status: TaskStatus.CANCELLED, + completedAt: new Date(), + }); + } + + startTask(fctx: FlowExecCtx, taskId: string) { + return this.tasksRepo.updateTask(fctx, taskId, { + status: TaskStatus.RUNNING, + startedAt: new Date(), + }); + } +} + +export function getTasksController(): TasksController { + return new TasksController(new TasksRepository(db)); +} diff --git a/packages/logic/domains/tasks/data.ts b/packages/logic/domains/tasks/data.ts new file mode 100644 index 0000000..a66278a --- /dev/null +++ b/packages/logic/domains/tasks/data.ts @@ -0,0 +1,71 @@ +import * as v from "valibot"; + +export enum TaskStatus { + PENDING = "pending", + RUNNING = "running", + COMPLETED = "completed", + FAILED = "failed", + CANCELLED = "cancelled", +} + +export const taskStatusSchema = v.picklist([ + "pending", + "running", + "completed", + "failed", + "cancelled", +]); +export type TaskStatusType = v.InferOutput; + +export enum TaskType { + APK_BUILD = "apk_build", +} + +export const taskTypeSchema = v.picklist(["apk_build"]); +export type TaskTypeValue = v.InferOutput; + +export const taskErrorSchema = v.object({ + code: v.string(), + message: v.string(), + detail: v.optional(v.string()), + timestamp: v.date(), +}); +export type TaskError = v.InferOutput; + +export const taskSchema = v.object({ + id: v.string(), + type: taskTypeSchema, + status: taskStatusSchema, + progress: v.pipe(v.number(), v.integer()), + payload: v.optional(v.nullable(v.record(v.string(), v.any()))), + result: v.optional(v.nullable(v.record(v.string(), v.any()))), + error: v.optional(v.nullable(taskErrorSchema)), + userId: v.string(), + resourceId: v.string(), + startedAt: v.optional(v.nullable(v.date())), + completedAt: v.optional(v.nullable(v.date())), + createdAt: v.date(), + updatedAt: v.date(), +}); +export type Task = v.InferOutput; + +export const createTaskSchema = v.object({ + id: v.string(), + type: taskTypeSchema, + status: v.optional(taskStatusSchema), + progress: v.optional(v.pipe(v.number(), v.integer())), + payload: v.optional(v.nullable(v.record(v.string(), v.any()))), + userId: v.string(), + resourceId: v.string(), +}); +export type CreateTask = v.InferOutput; + +export const updateTaskSchema = v.object({ + status: v.optional(taskStatusSchema), + progress: v.optional(v.pipe(v.number(), v.integer())), + result: v.optional(v.nullable(v.record(v.string(), v.any()))), + error: v.optional(v.nullable(taskErrorSchema)), + startedAt: v.optional(v.nullable(v.date())), + completedAt: v.optional(v.nullable(v.date())), +}); +export type UpdateTask = v.InferOutput; diff --git a/packages/logic/domains/tasks/errors.ts b/packages/logic/domains/tasks/errors.ts new file mode 100644 index 0000000..058954f --- /dev/null +++ b/packages/logic/domains/tasks/errors.ts @@ -0,0 +1,87 @@ +import { FlowExecCtx } from "@/core/flow.execution.context"; +import { ERROR_CODES, type Err } from "@pkg/result"; +import { getError } from "@pkg/logger"; + +export const taskErrors = { + 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, + }), + + taskNotFound: (fctx: FlowExecCtx, taskId: string): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.NOT_FOUND, + message: "Task not found", + description: "The requested task does not exist", + detail: `No task found with ID: ${taskId}`, + }), + + createTaskFailed: (fctx: FlowExecCtx, detail: string): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.DATABASE_ERROR, + message: "An error occurred while creating task", + description: "Try again later", + detail, + }), + + getTaskFailed: (fctx: FlowExecCtx, detail: string): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.DATABASE_ERROR, + message: "An error occurred while fetching task", + description: "Try again later", + detail, + }), + + updateTaskFailed: (fctx: FlowExecCtx, detail: string): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.DATABASE_ERROR, + message: "An error occurred while updating task", + description: "Try again later", + detail, + }), + + deleteTaskFailed: (fctx: FlowExecCtx, detail: string): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.DATABASE_ERROR, + message: "An error occurred while deleting task", + description: "Try again later", + detail, + }), + + getTasksFailed: (fctx: FlowExecCtx, detail: string): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.DATABASE_ERROR, + message: "An error occurred while fetching tasks", + description: "Try again later", + detail, + }), + + getTasksByStatusFailed: (fctx: FlowExecCtx, detail: string): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.DATABASE_ERROR, + message: "An error occurred while fetching tasks by status", + description: "Try again later", + detail, + }), + + checkTaskExistenceFailed: (fctx: FlowExecCtx, detail: string): Err => + getError({ + flowId: fctx.flowId, + code: ERROR_CODES.DATABASE_ERROR, + message: "An error occurred while checking task existence", + description: "Try again later", + detail, + }), +}; + diff --git a/packages/logic/domains/tasks/repository.ts b/packages/logic/domains/tasks/repository.ts new file mode 100644 index 0000000..344c545 --- /dev/null +++ b/packages/logic/domains/tasks/repository.ts @@ -0,0 +1,163 @@ +import { CreateTask, Task, TaskStatus, TaskType, UpdateTask } from "./data"; +import { ResultAsync, errAsync, okAsync } from "neverthrow"; +import { FlowExecCtx } from "@core/flow.execution.context"; +import { Database, and, asc, eq, inArray } from "@pkg/db"; +import { task } from "@pkg/db/schema"; +import { type Err } from "@pkg/result"; +import { taskErrors } from "./errors"; +import { logger } from "@pkg/logger"; + +export class TasksRepository { + constructor(private db: Database) {} + + createTask(fctx: FlowExecCtx, taskData: CreateTask): ResultAsync { + logger.info("Creating new task", { ...fctx, taskId: taskData.id }); + + return ResultAsync.fromPromise( + this.db + .insert(task) + .values({ + id: taskData.id, + type: taskData.type, + status: taskData.status || TaskStatus.PENDING, + progress: taskData.progress || 0, + payload: taskData.payload ?? null, + userId: taskData.userId, + resourceId: taskData.resourceId, + createdAt: new Date(), + updatedAt: new Date(), + }) + .returning() + .execute(), + (error) => + taskErrors.createTaskFailed( + fctx, + error instanceof Error ? error.message : String(error), + ), + ).map((result) => result[0] as Task); + } + + getTaskById(fctx: FlowExecCtx, taskId: string): ResultAsync { + return ResultAsync.fromPromise( + this.db.query.task.findFirst({ + where: eq(task.id, taskId), + }), + (error) => + taskErrors.getTaskFailed( + fctx, + error instanceof Error ? error.message : String(error), + ), + ).andThen((result) => { + if (!result) { + return errAsync(taskErrors.taskNotFound(fctx, taskId)); + } + + return okAsync(result as Task); + }); + } + + updateTask( + fctx: FlowExecCtx, + taskId: string, + updates: UpdateTask, + ): ResultAsync { + return this.getTaskById(fctx, taskId).andThen(() => + ResultAsync.fromPromise( + this.db + .update(task) + .set({ ...updates, updatedAt: new Date() }) + .where(eq(task.id, taskId)) + .returning() + .execute(), + (error) => + taskErrors.updateTaskFailed( + fctx, + error instanceof Error ? error.message : String(error), + ), + ).andThen((updateResult) => { + if (!updateResult[0]) { + return errAsync(taskErrors.taskNotFound(fctx, taskId)); + } + return okAsync(updateResult[0] as Task); + }), + ); + } + + deleteTask(fctx: FlowExecCtx, taskId: string): ResultAsync { + return ResultAsync.fromPromise( + this.db.delete(task).where(eq(task.id, taskId)).execute(), + (error) => + taskErrors.deleteTaskFailed( + fctx, + error instanceof Error ? error.message : String(error), + ), + ).map(() => true); + } + + getTasksByStatuses( + fctx: FlowExecCtx, + statuses: TaskStatus[], + ): ResultAsync { + return ResultAsync.fromPromise( + this.db + .select() + .from(task) + .where(inArray(task.status, statuses)) + .orderBy(asc(task.createdAt)), + (error) => + taskErrors.getTasksByStatusFailed( + fctx, + error instanceof Error ? error.message : String(error), + ), + ).map((result) => result as Task[]); + } + + getTasksByTypeAndStatuses( + fctx: FlowExecCtx, + type: TaskType, + statuses: TaskStatus[], + ): ResultAsync { + return ResultAsync.fromPromise( + this.db + .select() + .from(task) + .where(and(eq(task.type, type), inArray(task.status, statuses))) + .orderBy(asc(task.createdAt)), + (error) => + taskErrors.getTasksByStatusFailed( + fctx, + error instanceof Error ? error.message : String(error), + ), + ).map((result) => result as Task[]); + } + + markTaskAsCompleted( + fctx: FlowExecCtx, + taskId: string, + result?: Record, + ): ResultAsync { + return this.updateTask(fctx, taskId, { + status: TaskStatus.COMPLETED, + progress: 100, + result: result ?? null, + completedAt: new Date(), + }); + } + + markTaskAsFailed( + fctx: FlowExecCtx, + taskId: string, + error: any, + ): ResultAsync { + return this.updateTask(fctx, taskId, { + status: TaskStatus.FAILED, + error: { + code: error.code || "UNKNOWN_ERROR", + message: error.message || "Task failed", + detail: error.detail, + timestamp: new Date(), + }, + completedAt: new Date(), + }); + } +} diff --git a/packages/logic/domains/user/account.repository.ts b/packages/logic/domains/user/account.repository.ts new file mode 100644 index 0000000..91fa4d0 --- /dev/null +++ b/packages/logic/domains/user/account.repository.ts @@ -0,0 +1,250 @@ +import { FlowExecCtx } from "@/core/flow.execution.context"; +import { traceResultAsync } from "@core/observability"; +import { ERROR_CODES, type Err } from "@pkg/result"; +import { getError, logDomainEvent } from "@pkg/logger"; +import { auth } from "../auth/config.base"; +import { account } from "@pkg/db/schema"; +import { ResultAsync, errAsync, okAsync } 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 { + return traceResultAsync({ + name: "logic.user.repository.ensureAccountExists", + fctx, + attributes: { "app.user.id": userId }, + fn: () => { + const startedAt = Date.now(); + logDomainEvent({ + event: "account.ensure_exists.started", + fctx, + meta: { userId }, + }); + + return ResultAsync.fromPromise( + this.db.query.account.findFirst({ + where: eq(account.userId, userId), + }), + (error) => { + logDomainEvent({ + level: "error", + event: "account.ensure_exists.failed", + fctx, + durationMs: Date.now() - startedAt, + error, + meta: { userId }, + }); + return this.dbError( + fctx, + error instanceof Error ? error.message : String(error), + ); + }, + ).andThen((existingAccount) => { + if (existingAccount) { + logDomainEvent({ + event: "account.ensure_exists.succeeded", + fctx, + durationMs: Date.now() - startedAt, + meta: { userId, existed: true }, + }); + return okAsync(true); + } + + return ResultAsync.fromPromise( + auth.$context.then((ctx) => ctx.password.hash(nanoid())), + (error) => { + logDomainEvent({ + level: "error", + event: "account.ensure_exists.failed", + fctx, + durationMs: Date.now() - startedAt, + error, + meta: { userId, stage: "hash_password" }, + }); + 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, + password, + createdAt: new Date(), + updatedAt: new Date(), + }) + .execute(), + (error) => { + logDomainEvent({ + level: "error", + event: "account.ensure_exists.failed", + fctx, + durationMs: Date.now() - startedAt, + error, + meta: { userId, stage: "create_account" }, + }); + return this.dbError( + fctx, + error instanceof Error + ? error.message + : String(error), + ); + }, + ).map(() => { + logDomainEvent({ + event: "account.ensure_exists.succeeded", + fctx, + durationMs: Date.now() - startedAt, + meta: { userId, existed: false }, + }); + return false; + }); + }); + }); + }, + }); + } + + rotatePassword( + fctx: FlowExecCtx, + userId: string, + password: string, + ): ResultAsync { + return traceResultAsync({ + name: "logic.user.repository.rotatePassword", + fctx, + attributes: { "app.user.id": userId }, + fn: () => { + const startedAt = Date.now(); + logDomainEvent({ + event: "account.rotate_password.started", + fctx, + meta: { userId }, + }); + + return ResultAsync.fromPromise( + this.db.query.account.findFirst({ + where: eq(account.userId, userId), + }), + (error) => { + logDomainEvent({ + level: "error", + event: "account.rotate_password.failed", + fctx, + durationMs: Date.now() - startedAt, + error, + meta: { userId, stage: "check_exists" }, + }); + return this.dbError( + fctx, + error instanceof Error + ? error.message + : String(error), + ); + }, + ).andThen((existingAccount) => { + if (!existingAccount) { + logDomainEvent({ + level: "warn", + event: "account.rotate_password.failed", + fctx, + durationMs: Date.now() - startedAt, + error: { code: "NOT_FOUND", message: "Account not found" }, + meta: { userId }, + }); + return errAsync(this.accountNotFound(fctx)); + } + + return ResultAsync.fromPromise( + auth.$context.then((ctx) => ctx.password.hash(password)), + (error) => { + logDomainEvent({ + level: "error", + event: "account.rotate_password.failed", + fctx, + durationMs: Date.now() - startedAt, + error, + meta: { userId, stage: "hash_password" }, + }); + return this.dbError( + fctx, + error instanceof Error + ? error.message + : String(error), + ); + }, + ).andThen((hashed) => { + return ResultAsync.fromPromise( + this.db + .update(account) + .set({ password: hashed }) + .where(eq(account.userId, userId)) + .returning() + .execute(), + (error) => { + logDomainEvent({ + level: "error", + event: "account.rotate_password.failed", + fctx, + durationMs: Date.now() - startedAt, + error, + meta: { userId, stage: "update_password" }, + }); + return this.dbError( + fctx, + error instanceof Error + ? error.message + : String(error), + ); + }, + ).map(() => { + logDomainEvent({ + event: "account.rotate_password.succeeded", + fctx, + durationMs: Date.now() - startedAt, + meta: { userId }, + }); + return password; + }); + }); + }); + }, + }); + } +} diff --git a/packages/logic/domains/user/controller.ts b/packages/logic/domains/user/controller.ts new file mode 100644 index 0000000..7e283b7 --- /dev/null +++ b/packages/logic/domains/user/controller.ts @@ -0,0 +1,96 @@ +import { FlowExecCtx } from "@/core/flow.execution.context"; +import { traceResultAsync } from "@core/observability"; +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 traceResultAsync({ + name: "logic.user.controller.getUserInfo", + fctx, + attributes: { "app.user.id": userId }, + fn: () => this.userRepository.getUserInfo(fctx, userId), + }); + } + + ensureAccountExists(fctx: FlowExecCtx, userId: string) { + return traceResultAsync({ + name: "logic.user.controller.ensureAccountExists", + fctx, + attributes: { "app.user.id": userId }, + fn: () => this.accountRepo.ensureAccountExists(fctx, userId), + }); + } + + isUsernameAvailable(fctx: FlowExecCtx, username: string) { + return traceResultAsync({ + name: "logic.user.controller.isUsernameAvailable", + fctx, + attributes: { "app.user.username": username }, + fn: () => this.userRepository.isUsernameAvailable(fctx, username), + }); + } + + updateLastVerified2FaAtToNow(fctx: FlowExecCtx, userId: string) { + return traceResultAsync({ + name: "logic.user.controller.updateLastVerified2FaAtToNow", + fctx, + attributes: { "app.user.id": userId }, + fn: () => this.userRepository.updateLastVerified2FaAtToNow(fctx, userId), + }); + } + + banUser( + fctx: FlowExecCtx, + userId: string, + reason: string, + banExpiresAt: Date, + ) { + return traceResultAsync({ + name: "logic.user.controller.banUser", + fctx, + attributes: { "app.user.id": userId }, + fn: () => this.userRepository.banUser(fctx, userId, reason, banExpiresAt), + }); + } + + isUserBanned(fctx: FlowExecCtx, userId: string) { + return traceResultAsync({ + name: "logic.user.controller.isUserBanned", + fctx, + attributes: { "app.user.id": userId }, + fn: () => this.userRepository.isUserBanned(fctx, userId), + }); + } + + getBanInfo(fctx: FlowExecCtx, userId: string) { + return traceResultAsync({ + name: "logic.user.controller.getBanInfo", + fctx, + attributes: { "app.user.id": userId }, + fn: () => this.userRepository.getBanInfo(fctx, userId), + }); + } + + rotatePassword(fctx: FlowExecCtx, userId: string, password: string) { + return traceResultAsync({ + name: "logic.user.controller.rotatePassword", + fctx, + attributes: { "app.user.id": userId }, + fn: () => 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..69eadeb --- /dev/null +++ b/packages/logic/domains/user/repository.ts @@ -0,0 +1,420 @@ +import { ResultAsync, errAsync, okAsync } from "neverthrow"; +import { FlowExecCtx } from "@core/flow.execution.context"; +import { traceResultAsync } from "@core/observability"; +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 { logDomainEvent } from "@pkg/logger"; + +export class UserRepository { + constructor(private db: Database) {} + + getUserInfo(fctx: FlowExecCtx, userId: string): ResultAsync { + return traceResultAsync({ + name: "logic.user.repository.getUserInfo", + fctx, + attributes: { "app.user.id": userId }, + fn: () => { + const startedAt = Date.now(); + logDomainEvent({ + event: "user.get_info.started", + fctx, + meta: { userId }, + }); + + return ResultAsync.fromPromise( + this.db.query.user.findFirst({ + where: eq(user.id, userId), + }), + (error) => { + logDomainEvent({ + level: "error", + event: "user.get_info.failed", + fctx, + durationMs: Date.now() - startedAt, + error, + meta: { userId }, + }); + return userErrors.getUserInfoFailed( + fctx, + error instanceof Error ? error.message : String(error), + ); + }, + ).andThen((userData) => { + if (!userData) { + logDomainEvent({ + level: "warn", + event: "user.get_info.failed", + fctx, + durationMs: Date.now() - startedAt, + error: { code: "NOT_FOUND", message: "User not found" }, + meta: { userId }, + }); + return errAsync(userErrors.userNotFound(fctx)); + } + + logDomainEvent({ + event: "user.get_info.succeeded", + fctx, + durationMs: Date.now() - startedAt, + meta: { userId }, + }); + return okAsync(userData as User); + }); + }, + }); + } + + isUsernameAvailable( + fctx: FlowExecCtx, + username: string, + ): ResultAsync { + return traceResultAsync({ + name: "logic.user.repository.isUsernameAvailable", + fctx, + attributes: { "app.user.username": username }, + fn: () => { + const startedAt = Date.now(); + logDomainEvent({ + event: "user.username_check.started", + fctx, + }); + + return ResultAsync.fromPromise( + this.db.query.user.findFirst({ + where: eq(user.username, username), + }), + (error) => { + logDomainEvent({ + level: "error", + event: "user.username_check.failed", + fctx, + durationMs: Date.now() - startedAt, + error, + }); + return userErrors.usernameCheckFailed( + fctx, + error instanceof Error ? error.message : String(error), + ); + }, + ).map((existingUser) => { + const isAvailable = !existingUser?.id; + logDomainEvent({ + event: "user.username_check.succeeded", + fctx, + durationMs: Date.now() - startedAt, + meta: { isAvailable }, + }); + return isAvailable; + }); + }, + }); + } + + updateLastVerified2FaAtToNow( + fctx: FlowExecCtx, + userId: string, + ): ResultAsync { + return traceResultAsync({ + name: "logic.user.repository.updateLastVerified2FaAtToNow", + fctx, + attributes: { "app.user.id": userId }, + fn: () => { + const startedAt = Date.now(); + logDomainEvent({ + event: "user.update_last_2fa.started", + fctx, + meta: { userId }, + }); + + return ResultAsync.fromPromise( + this.db + .update(user) + .set({ last2FAVerifiedAt: new Date() }) + .where(eq(user.id, userId)) + .execute(), + (error) => { + logDomainEvent({ + level: "error", + event: "user.update_last_2fa.failed", + fctx, + durationMs: Date.now() - startedAt, + error, + meta: { userId }, + }); + return userErrors.updateFailed( + fctx, + error instanceof Error ? error.message : String(error), + ); + }, + ).map(() => { + logDomainEvent({ + event: "user.update_last_2fa.succeeded", + fctx, + durationMs: Date.now() - startedAt, + meta: { userId }, + }); + return true; + }); + }, + }); + } + + banUser( + fctx: FlowExecCtx, + userId: string, + reason: string, + banExpiresAt: Date, + ): ResultAsync { + return traceResultAsync({ + name: "logic.user.repository.banUser", + fctx, + attributes: { "app.user.id": userId }, + fn: () => { + const startedAt = Date.now(); + logDomainEvent({ + event: "user.ban.started", + fctx, + meta: { + userId, + reasonLength: reason.length, + banExpiresAt: banExpiresAt.toISOString(), + }, + }); + + return ResultAsync.fromPromise( + this.db + .update(user) + .set({ + banned: true, + banReason: reason, + banExpires: banExpiresAt, + }) + .where(eq(user.id, userId)) + .execute(), + (error) => { + logDomainEvent({ + level: "error", + event: "user.ban.failed", + fctx, + durationMs: Date.now() - startedAt, + error, + meta: { userId }, + }); + return userErrors.banOperationFailed( + fctx, + error instanceof Error ? error.message : String(error), + ); + }, + ).map(() => { + logDomainEvent({ + event: "user.ban.succeeded", + fctx, + durationMs: Date.now() - startedAt, + meta: { userId }, + }); + return true; + }); + }, + }); + } + + isUserBanned(fctx: FlowExecCtx, userId: string): ResultAsync { + return traceResultAsync({ + name: "logic.user.repository.isUserBanned", + fctx, + attributes: { "app.user.id": userId }, + fn: () => { + const startedAt = Date.now(); + logDomainEvent({ + event: "user.is_banned.started", + fctx, + meta: { userId }, + }); + + return ResultAsync.fromPromise( + this.db.query.user.findFirst({ + where: eq(user.id, userId), + columns: { + banned: true, + banExpires: true, + }, + }), + (error) => { + logDomainEvent({ + level: "error", + event: "user.is_banned.failed", + fctx, + durationMs: Date.now() - startedAt, + error, + meta: { userId }, + }); + return userErrors.dbError( + fctx, + error instanceof Error ? error.message : String(error), + ); + }, + ).andThen((userData) => { + if (!userData) { + logDomainEvent({ + level: "warn", + event: "user.is_banned.failed", + fctx, + durationMs: Date.now() - startedAt, + error: { code: "NOT_FOUND", message: "User not found" }, + meta: { userId }, + }); + return errAsync(userErrors.userNotFound(fctx)); + } + + if (!userData.banned) { + logDomainEvent({ + event: "user.is_banned.succeeded", + fctx, + durationMs: Date.now() - startedAt, + meta: { userId, isBanned: false }, + }); + return okAsync(false); + } + + if (!userData.banExpires) { + logDomainEvent({ + event: "user.is_banned.succeeded", + fctx, + durationMs: Date.now() - startedAt, + meta: { userId, isBanned: true, isPermanent: true }, + }); + return okAsync(true); + } + + const now = new Date(); + if (userData.banExpires <= now) { + return ResultAsync.fromPromise( + this.db + .update(user) + .set({ + banned: false, + banReason: null, + banExpires: null, + }) + .where(eq(user.id, userId)) + .execute(), + (error) => { + logDomainEvent({ + level: "error", + event: "user.unban_after_expiry.failed", + fctx, + durationMs: Date.now() - startedAt, + error, + meta: { userId }, + }); + return userErrors.unbanFailed( + fctx, + error instanceof Error + ? error.message + : String(error), + ); + }, + ) + .map(() => { + logDomainEvent({ + event: "user.unban_after_expiry.succeeded", + fctx, + durationMs: Date.now() - startedAt, + meta: { userId }, + }); + return false; + }) + .orElse((error) => { + logDomainEvent({ + level: "warn", + event: "user.is_banned.succeeded", + fctx, + durationMs: Date.now() - startedAt, + error, + meta: { userId, degraded: true, isBanned: true }, + }); + return okAsync(true); + }); + } + + logDomainEvent({ + event: "user.is_banned.succeeded", + fctx, + durationMs: Date.now() - startedAt, + meta: { + userId, + isBanned: true, + banExpires: userData.banExpires.toISOString(), + }, + }); + return okAsync(true); + }); + }, + }); + } + + getBanInfo(fctx: FlowExecCtx, userId: string): ResultAsync { + return traceResultAsync({ + name: "logic.user.repository.getBanInfo", + fctx, + attributes: { "app.user.id": userId }, + fn: () => { + const startedAt = Date.now(); + logDomainEvent({ + event: "user.ban_info.started", + fctx, + meta: { userId }, + }); + + return ResultAsync.fromPromise( + this.db.query.user.findFirst({ + where: eq(user.id, userId), + columns: { banned: true, banReason: true, banExpires: true }, + }), + (error) => { + logDomainEvent({ + level: "error", + event: "user.ban_info.failed", + fctx, + durationMs: Date.now() - startedAt, + error, + meta: { userId }, + }); + return userErrors.getBanInfoFailed( + fctx, + error instanceof Error ? error.message : String(error), + ); + }, + ).andThen((userData) => { + if (!userData) { + logDomainEvent({ + level: "warn", + event: "user.ban_info.failed", + fctx, + durationMs: Date.now() - startedAt, + error: { code: "NOT_FOUND", message: "User not found" }, + meta: { userId }, + }); + return errAsync(userErrors.userNotFound(fctx)); + } + + logDomainEvent({ + event: "user.ban_info.succeeded", + fctx, + durationMs: Date.now() - startedAt, + meta: { userId, banned: userData.banned || false }, + }); + + return okAsync({ + banned: userData.banned || false, + reason: userData.banReason || undefined, + expires: userData.banExpires || undefined, + }); + }); + }, + }); + } +} diff --git a/packages/logic/package.json b/packages/logic/package.json new file mode 100644 index 0000000..a804d38 --- /dev/null +++ b/packages/logic/package.json @@ -0,0 +1,40 @@ +{ + "name": "@pkg/logic", + "type": "module", + "scripts": { + "auth:schemagen": "pnpm dlx @better-auth/cli generate --config ./domains/auth/config.base.ts --output ../../packages/db/schema/better.auth.schema.ts" + }, + "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@otplib/plugin-base32-scure": "^13.3.0", + "@otplib/plugin-crypto-noble": "^13.3.0", + "@otplib/totp": "^13.3.0", + "@pkg/db": "workspace:*", + "@pkg/keystore": "workspace:*", + "@pkg/logger": "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": "^13.3.0", + "uuid": "^11.1.0", + "valibot": "^1.2.0" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/mailparser": "^3.4.6", + "@types/tmp": "^0.2.6", + "@types/uuid": "^10.0.0" + }, + "peerDependencies": { + "typescript": "^5.9.3" + } +} 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/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..449a414 --- /dev/null +++ b/packages/result/package.json @@ -0,0 +1,11 @@ +{ + "name": "@pkg/result", + "module": "index.ts", + "type": "module", + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.9.3" + } +} diff --git a/packages/settings/index.ts b/packages/settings/index.ts new file mode 100644 index 0000000..62515b7 --- /dev/null +++ b/packages/settings/index.ts @@ -0,0 +1,181 @@ +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(), + debugKey: v.string(), + + processorApiUrl: v.string(), + appBuilderApiUrl: v.string(), + appBuilderAssetsPublicUrl: v.string(), + clientDownloadedApkName: v.string(), + mobileAppApiUrl: v.string(), + + betterAuthUrl: v.string(), + betterAuthSecret: v.string(), + + twoFaSecret: v.string(), + twofaSessionExpiryMinutes: v.optional(v.number()), + twofaRequiredHours: v.optional(v.number()), + + defaultAdminEmail: v.string(), + defaultAdminPassword: v.string(), + + otelServiceName: v.string(), + otelExporterOtlpHttpEndpoint: 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"), + debugKey: getEnv("DEBUG_KEY"), + + processorApiUrl: getEnv("PROCESSOR_API_URL", "http://localhost:3000"), + appBuilderApiUrl: getEnv( + "APP_BUILDER_API_URL", + "http://localhost:3001", + ), + appBuilderAssetsPublicUrl: getEnv( + "APP_BUILDER_ASSETS_PUBLIC_URL", + "http://localhost:3001", + ), + clientDownloadedApkName: getEnv( + "CLIENT_DOWNLOADED_APK_NAME", + "illusory-client.apk", + ), + mobileAppApiUrl: getEnv("MOBILE_APP_API_URL"), + + betterAuthUrl: getEnv("BETTER_AUTH_URL"), + betterAuthSecret: getEnv("BETTER_AUTH_SECRET"), + + twoFaSecret: getEnv("TWOFA_SECRET"), + twofaSessionExpiryMinutes: getEnvNumber( + "TWOFA_SESSION_EXPIRY_MINUTES", + 10, + ), + twofaRequiredHours: getEnvNumber("TWOFA_REQUIRED_HOURS", 24), + + defaultAdminEmail: getEnv("DEFAULT_ADMIN_EMAIL"), + defaultAdminPassword: getEnv("DEFAULT_ADMIN_PASSWORD"), + + otelServiceName: getEnv("OTEL_SERVICE_NAME"), + otelExporterOtlpHttpEndpoint: getEnv( + "OTEL_EXPORTER_OTLP_HTTP_ENDPOINT", + ), + + // 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..062792c --- /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.9.3" + }, + "dependencies": { + "dotenv": "^17.2.3", + "valibot": "^1.2.0" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..e20738c --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,7616 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + prettier: + specifier: ^3.8.1 + version: 3.8.1 + prettier-plugin-sort-imports: + specifier: ^1.8.11 + version: 1.8.11(typescript@5.9.3) + prettier-plugin-svelte: + specifier: ^3.5.0 + version: 3.5.0(prettier@3.8.1)(svelte@5.53.6) + prettier-plugin-tailwindcss: + specifier: ^0.7.2 + version: 0.7.2(prettier-plugin-sort-imports@1.8.11(typescript@5.9.3))(prettier-plugin-svelte@3.5.0(prettier@3.8.1)(svelte@5.53.6))(prettier@3.8.1) + turbo: + specifier: ^2.8.16 + version: 2.8.16 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + apps/front: + dependencies: + '@hono/node-server': + specifier: ^1.19.9 + version: 1.19.9(hono@4.12.8) + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.0 + '@opentelemetry/auto-instrumentations-node': + specifier: ^0.70.1 + version: 0.70.1(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0)) + '@opentelemetry/exporter-logs-otlp-proto': + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto': + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': + specifier: ^2.1.0 + version: 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-node': + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) + '@pkg/db': + specifier: workspace:* + version: link:../../packages/db + '@pkg/logger': + specifier: workspace:* + version: link:../../packages/logger + '@pkg/logic': + specifier: workspace:* + version: link:../../packages/logic + '@pkg/result': + specifier: workspace:* + version: link:../../packages/result + '@pkg/settings': + specifier: workspace:* + version: link:../../packages/settings + hono: + specifier: ^4.12.8 + version: 4.12.8 + import-in-the-middle: + specifier: ^3.0.0 + version: 3.0.0 + valibot: + specifier: ^1.2.0 + version: 1.2.0(typescript@5.9.3) + devDependencies: + '@types/node': + specifier: ^25.3.2 + version: 25.5.0 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + apps/main: + dependencies: + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.0 + '@opentelemetry/auto-instrumentations-node': + specifier: ^0.70.1 + version: 0.70.1(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0)) + '@opentelemetry/exporter-logs-otlp-proto': + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto': + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-node': + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) + '@pkg/db': + specifier: workspace:* + version: link:../../packages/db + '@pkg/keystore': + specifier: workspace:* + version: link:../../packages/keystore + '@pkg/logger': + specifier: workspace:* + version: link:../../packages/logger + '@pkg/logic': + specifier: workspace:* + version: link:../../packages/logic + '@pkg/result': + specifier: workspace:* + version: link:../../packages/result + '@pkg/settings': + specifier: workspace:* + version: link:../../packages/settings + argon2: + specifier: ^0.43.0 + version: 0.43.1 + better-auth: + specifier: ^1.4.20 + version: 1.4.20(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.9)(kysely@0.28.11)(postgres@3.4.8))(svelte@5.53.6)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + date-fns: + specifier: ^4.1.0 + version: 4.1.0 + import-in-the-middle: + specifier: ^3.0.0 + version: 3.0.0 + nanoid: + specifier: ^5.1.6 + version: 5.1.6 + neverthrow: + specifier: ^8.2.0 + version: 8.2.0 + qrcode: + specifier: ^1.5.4 + version: 1.5.4 + sharp: + specifier: ^0.34.5 + version: 0.34.5 + valibot: + specifier: ^1.2.0 + version: 1.2.0(typescript@5.9.3) + devDependencies: + '@iconify/json': + specifier: ^2.2.434 + version: 2.2.444 + '@internationalized/date': + specifier: ^3.10.0 + version: 3.11.0 + '@lucide/svelte': + specifier: ^0.561.0 + version: 0.561.0(svelte@5.53.6) + '@sveltejs/adapter-node': + specifier: ^5.5.4 + version: 5.5.4(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))) + '@sveltejs/kit': + specifier: ^2.53.4 + version: 2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + '@sveltejs/vite-plugin-svelte': + specifier: ^6.2.4 + version: 6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + '@tailwindcss/forms': + specifier: ^0.5.10 + version: 0.5.11(tailwindcss@4.2.1) + '@tailwindcss/typography': + specifier: ^0.5.19 + version: 0.5.19(tailwindcss@4.2.1) + '@tailwindcss/vite': + specifier: ^4.1.18 + version: 4.2.1(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + '@tanstack/table-core': + specifier: ^8.21.3 + version: 8.21.3 + '@types/qrcode': + specifier: ^1.5.6 + version: 1.5.6 + bits-ui: + specifier: ^2.14.4 + version: 2.16.2(@internationalized/date@3.11.0)(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6) + clsx: + specifier: ^2.1.1 + version: 2.1.1 + embla-carousel-svelte: + specifier: ^8.6.0 + version: 8.6.0(svelte@5.53.6) + formsnap: + specifier: ^2.0.1 + version: 2.0.1(svelte@5.53.6)(sveltekit-superforms@2.30.0(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)) + layerchart: + specifier: 2.0.0-next.43 + version: 2.0.0-next.43(svelte@5.53.6) + mode-watcher: + specifier: ^1.1.0 + version: 1.1.0(svelte@5.53.6) + paneforge: + specifier: ^1.0.2 + version: 1.0.2(svelte@5.53.6) + prettier: + specifier: ^3.7.4 + version: 3.8.1 + prettier-plugin-svelte: + specifier: ^3.4.0 + version: 3.5.0(prettier@3.8.1)(svelte@5.53.6) + prettier-plugin-tailwindcss: + specifier: ^0.7.2 + version: 0.7.2(prettier-plugin-sort-imports@1.8.11(typescript@5.9.3))(prettier-plugin-svelte@3.5.0(prettier@3.8.1)(svelte@5.53.6))(prettier@3.8.1) + svelte: + specifier: ^5.53.6 + version: 5.53.6 + svelte-check: + specifier: ^4.4.4 + version: 4.4.4(picomatch@4.0.3)(svelte@5.53.6)(typescript@5.9.3) + svelte-sonner: + specifier: ^1.0.7 + version: 1.0.7(svelte@5.53.6) + sveltekit-superforms: + specifier: ^2.30.0 + version: 2.30.0(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3) + tailwind-merge: + specifier: ^3.4.0 + version: 3.5.0 + tailwind-variants: + specifier: ^3.2.2 + version: 3.2.2(tailwind-merge@3.5.0)(tailwindcss@4.2.1) + tailwindcss: + specifier: ^4.1.18 + version: 4.2.1 + tw-animate-css: + specifier: ^1.4.0 + version: 1.4.0 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + unplugin-icons: + specifier: ^23.0.1 + version: 23.0.1(svelte@5.53.6) + vaul-svelte: + specifier: ^1.0.0-next.7 + version: 1.0.0-next.7(svelte@5.53.6) + vite: + specifier: ^7.2.6 + version: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + vitest: + specifier: ^4.0.15 + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + + apps/orchestrator: + dependencies: + '@hono/node-server': + specifier: ^1.19.9 + version: 1.19.9(hono@4.12.8) + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.0 + '@opentelemetry/auto-instrumentations-node': + specifier: ^0.70.1 + version: 0.70.1(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0)) + '@opentelemetry/exporter-logs-otlp-proto': + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto': + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': + specifier: ^2.1.0 + version: 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-node': + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) + '@pkg/db': + specifier: workspace:* + version: link:../../packages/db + '@pkg/logger': + specifier: workspace:* + version: link:../../packages/logger + '@pkg/logic': + specifier: workspace:* + version: link:../../packages/logic + '@pkg/result': + specifier: workspace:* + version: link:../../packages/result + '@pkg/settings': + specifier: workspace:* + version: link:../../packages/settings + hono: + specifier: ^4.12.8 + version: 4.12.8 + import-in-the-middle: + specifier: ^3.0.0 + version: 3.0.0 + valibot: + specifier: ^1.2.0 + version: 1.2.0(typescript@5.9.3) + devDependencies: + '@types/node': + specifier: ^25.3.2 + version: 25.5.0 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + packages/db: + dependencies: + '@pkg/settings': + specifier: workspace:* + version: link:../settings + dotenv: + specifier: ^16.4.7 + version: 16.6.1 + drizzle-orm: + specifier: ^0.45.1 + version: 0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.9)(kysely@0.28.11)(postgres@3.4.8) + postgres: + specifier: ^3.4.8 + version: 3.4.8 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + devDependencies: + '@types/bun': + specifier: latest + version: 1.3.9 + '@types/pg': + specifier: ^8.11.10 + version: 8.16.0 + drizzle-kit: + specifier: ^0.31.9 + version: 0.31.9 + + packages/keystore: + dependencies: + ioredis: + specifier: ^5.6.1 + version: 5.10.0 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + devDependencies: + '@types/bun': + specifier: latest + version: 1.3.9 + + packages/logger: + dependencies: + '@opentelemetry/winston-transport': + specifier: ^0.22.0 + version: 0.22.0 + '@pkg/result': + specifier: workspace:* + version: link:../result + '@pkg/settings': + specifier: workspace:* + version: link:../settings + typescript: + specifier: ^5.9.3 + version: 5.9.3 + winston: + specifier: ^3.17.0 + version: 3.19.0 + devDependencies: + '@types/bun': + specifier: latest + version: 1.3.9 + + packages/logic: + dependencies: + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.0 + '@otplib/plugin-base32-scure': + specifier: ^13.3.0 + version: 13.3.0 + '@otplib/plugin-crypto-noble': + specifier: ^13.3.0 + version: 13.3.0 + '@otplib/totp': + specifier: ^13.3.0 + version: 13.3.0 + '@pkg/db': + specifier: workspace:* + version: link:../db + '@pkg/keystore': + specifier: workspace:* + version: link:../keystore + '@pkg/logger': + specifier: workspace:* + version: link:../logger + '@pkg/result': + specifier: workspace:* + version: link:../result + '@pkg/settings': + specifier: workspace:* + version: link:../settings + '@types/pdfkit': + specifier: ^0.14.0 + version: 0.14.0 + argon2: + specifier: ^0.43.0 + version: 0.43.1 + better-auth: + specifier: ^1.4.7 + version: 1.4.20(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.9)(kysely@0.28.11)(postgres@3.4.8))(svelte@5.53.6)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + date-fns-tz: + specifier: ^3.2.0 + version: 3.2.0(date-fns@4.1.0) + dotenv: + specifier: ^16.5.0 + version: 16.6.1 + hono: + specifier: ^4.11.1 + version: 4.12.8 + imapflow: + specifier: ^1.0.188 + version: 1.2.10 + mailparser: + specifier: ^3.7.3 + version: 3.9.3 + nanoid: + specifier: ^5.1.5 + version: 5.1.6 + neverthrow: + specifier: ^8.2.0 + version: 8.2.0 + otplib: + specifier: ^13.3.0 + version: 13.3.0 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + uuid: + specifier: ^11.1.0 + version: 11.1.0 + valibot: + specifier: ^1.2.0 + version: 1.2.0(typescript@5.9.3) + devDependencies: + '@types/bun': + specifier: latest + version: 1.3.9 + '@types/mailparser': + specifier: ^3.4.6 + version: 3.4.6 + '@types/tmp': + specifier: ^0.2.6 + version: 0.2.6 + '@types/uuid': + specifier: ^10.0.0 + version: 10.0.0 + + packages/result: + dependencies: + typescript: + specifier: ^5.9.3 + version: 5.9.3 + devDependencies: + '@types/bun': + specifier: latest + version: 1.3.9 + + packages/settings: + dependencies: + dotenv: + specifier: ^17.2.3 + version: 17.3.1 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + valibot: + specifier: ^1.2.0 + version: 1.2.0(typescript@5.9.3) + devDependencies: + '@types/bun': + specifier: latest + version: 1.3.9 + +packages: + + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + + '@ark/schema@0.56.0': + resolution: {integrity: sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA==} + + '@ark/util@0.56.0': + resolution: {integrity: sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA==} + + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + + '@better-auth/core@1.4.20': + resolution: {integrity: sha512-Qf29DOL4LricVJWsPOwg2Ymm+qfYQ14EkyTlnMOp8qKSPzbfMSgRvr6oiwZqmUFxystJ3ft8TzDwTvOSAuNbfA==} + peerDependencies: + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + better-call: 1.1.8 + jose: ^6.1.0 + kysely: ^0.28.5 + nanostores: ^1.0.1 + + '@better-auth/telemetry@1.4.20': + resolution: {integrity: sha512-ItRo5WswZl6gU8MPRrcn94d7mXk7vbN2zi6gSX8y2QcILRv7aXr6WhxTNKNeh5pnBUfIPdFYZcOnGf1uwgNKxg==} + peerDependencies: + '@better-auth/core': 1.4.20 + + '@better-auth/utils@0.3.0': + resolution: {integrity: sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==} + + '@better-fetch/fetch@1.1.21': + resolution: {integrity: sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==} + + '@colors/colors@1.6.0': + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + + '@dabh/diagnostics@2.0.8': + resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} + + '@dagrejs/dagre@1.1.8': + resolution: {integrity: sha512-5SEDlndt4W/LaVzPYJW+bSmSEZc9EzTf8rJ20WCKvjS5EAZAN0b+x0Yww7VMT4R3Wootkg+X9bUfUxazYw6Blw==} + + '@dagrejs/graphlib@2.2.4': + resolution: {integrity: sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==} + engines: {node: '>17.0.0'} + + '@drizzle-team/brocli@0.10.2': + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + + '@esbuild-kit/core-utils@3.3.2': + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild-kit/esm-loader@2.6.5': + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@exodus/schemasafe@1.3.0': + resolution: {integrity: sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==} + + '@floating-ui/core@1.7.4': + resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==} + + '@floating-ui/dom@1.7.5': + resolution: {integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==} + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@grpc/grpc-js@1.14.3': + resolution: {integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.8.0': + resolution: {integrity: sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==} + engines: {node: '>=6'} + hasBin: true + + '@hapi/hoek@9.3.0': + resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} + + '@hapi/topo@5.1.0': + resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} + + '@hono/node-server@1.19.9': + resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@iconify/json@2.2.444': + resolution: {integrity: sha512-z0UwFaVtaN/h/iWZ1kzEjqFU3sp0rRy93tzOtpepZU89DY39WsNeYZv2mxtft/2La6Bz2b4z1C/HkU5Cqv3gbw==} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@3.1.0': + resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==} + + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@internationalized/date@3.11.0': + resolution: {integrity: sha512-BOx5huLAWhicM9/ZFs84CzP+V3gBW6vlpM02yzsdYC7TGlZJX1OJiEEHcSayF00Z+3jLlm4w79amvSt6RqKN3Q==} + + '@ioredis/commands@1.5.1': + resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + + '@layerstack/svelte-actions@1.0.1-next.14': + resolution: {integrity: sha512-MPBmVaB+GfNHvBkg5nJkPG18smoXKvsvJRpsdWnrUBfca+TieZLoaEzNxDH+9LG11dIXP9gghsXt1mUqbbyAsA==} + + '@layerstack/svelte-state@0.1.0-next.19': + resolution: {integrity: sha512-yCYoQAIbeP8y1xmOB/r0+UundgP4JFnpNURgMki+26TotzoqrZ5oLpHvhPSVm60ks+buR3ebDBTeUFdHzxwzQQ==} + + '@layerstack/tailwind@2.0.0-next.17': + resolution: {integrity: sha512-ZSn6ouqpnzB6DKzSKLVwrUBOQsrzpDA/By2/ba9ApxgTGnaD1nyqNwrvmZ+kswdAwB4YnrGEAE4VZkKrB2+DaQ==} + + '@layerstack/utils@2.0.0-next.14': + resolution: {integrity: sha512-1I2CS0Cwgs53W35qVg1eBdYhB/CiPvL3s0XE61b8jWkTHxgjBF65yYNgXjW74kv7WI7GsJcWMNBufPd0rnu9kA==} + + '@lucide/svelte@0.561.0': + resolution: {integrity: sha512-vofKV2UFVrKE6I4ewKJ3dfCXSV6iP6nWVmiM83MLjsU91EeJcEg7LoWUABLp/aOTxj1HQNbJD1f3g3L0JQgH9A==} + peerDependencies: + svelte: ^5 + + '@noble/ciphers@2.1.1': + resolution: {integrity: sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==} + engines: {node: '>= 20.19.0'} + + '@noble/hashes@2.0.1': + resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} + engines: {node: '>= 20.19.0'} + + '@opentelemetry/api-logs@0.212.0': + resolution: {integrity: sha512-TEEVrLbNROUkYY51sBJGk7lO/OLjuepch8+hmpM6ffMJQ2z/KVCjdHuCFX6fJj8OkJP2zckPjrJzQtXU3IAsFg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/auto-instrumentations-node@0.70.1': + resolution: {integrity: sha512-r8BKs0rHtBAzZViPIuzSD2eh65fOPau0NqVsca2sACuZ6LFGu6a+QMhqq7skXz+/OqKwFr/7/b6VsaNMS+zZpQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.4.1 + '@opentelemetry/core': ^2.0.0 + + '@opentelemetry/configuration@0.212.0': + resolution: {integrity: sha512-D8sAY6RbqMa1W8lCeiaSL2eMCW2MF87QI3y+I6DQE1j+5GrDMwiKPLdzpa/2/+Zl9v1//74LmooCTCJBvWR8Iw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + + '@opentelemetry/context-async-hooks@2.5.1': + resolution: {integrity: sha512-MHbu8XxCHcBn6RwvCt2Vpn1WnLMNECfNKYB14LI5XypcgH4IE0/DiVifVR9tAkwPMyLXN8dOoPJfya3IryLQVw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.5.1': + resolution: {integrity: sha512-Dwlc+3HAZqpgTYq0MUyZABjFkcrKTePwuiFVLjahGD8cx3enqihmpAmdgNFO1R4m/sIe5afjJrA25Prqy4NXlA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-logs-otlp-grpc@0.212.0': + resolution: {integrity: sha512-/0bk6fQG+eSFZ4L6NlckGTgUous/ib5+OVdg0x4OdwYeHzV3lTEo3it1HgnPY6UKpmX7ki+hJvxjsOql8rCeZA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-logs-otlp-http@0.212.0': + resolution: {integrity: sha512-JidJasLwG/7M9RTxV/64xotDKmFAUSBc9SNlxI32QYuUMK5rVKhHNWMPDzC7E0pCAL3cu+FyiKvsTwLi2KqPYw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-logs-otlp-proto@0.212.0': + resolution: {integrity: sha512-RpKB5UVfxc7c6Ta1UaCrxXDTQ0OD7BCGT66a97Q5zR1x3+9fw4dSaiqMXT/6FAWj2HyFbem6Rcu1UzPZikGTWQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-grpc@0.212.0': + resolution: {integrity: sha512-/6Gqf9wpBq22XsomR1i0iPGnbQtCq2Vwnrq5oiDPjYSqveBdK1jtQbhGfmpK2mLLxk4cPDtD1ZEYdIou5K8EaA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-http@0.212.0': + resolution: {integrity: sha512-8hgBw3aTTRpSTkU4b9MLf/2YVLnfWp+hfnLq/1Fa2cky+vx6HqTodo+Zv1GTIrAKMOOwgysOjufy0gTxngqeBg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-proto@0.212.0': + resolution: {integrity: sha512-C7I4WN+ghn3g7SnxXm2RK3/sRD0k/BYcXaK6lGU3yPjiM7a1M25MLuM6zY3PeVPPzzTZPfuS7+wgn/tHk768Xw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-prometheus@0.212.0': + resolution: {integrity: sha512-hJFLhCJba5MW5QHexZMHZdMhBfNqNItxOsN0AZojwD1W2kU9xM+BEICowFGJFo/vNV+I2BJvTtmuKafeDSAo7Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-grpc@0.212.0': + resolution: {integrity: sha512-9xTuYWp8ClBhljDGAoa0NSsJcsxJsC9zCFKMSZJp1Osb9pjXCMRdA6fwXtlubyqe7w8FH16EWtQNKx/FWi+Ghw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-http@0.212.0': + resolution: {integrity: sha512-v/0wMozNoiEPRolzC4YoPo4rAT0q8r7aqdnRw3Nu7IDN0CGFzNQazkfAlBJ6N5y0FYJkban7Aw5WnN73//6YlA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-proto@0.212.0': + resolution: {integrity: sha512-d1ivqPT0V+i0IVOOdzGaLqonjtlk5jYrW7ItutWzXL/Mk+PiYb59dymy/i2reot9dDnBFWfrsvxyqdutGF5Vig==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-zipkin@2.5.1': + resolution: {integrity: sha512-Me6JVO7WqXGXsgr4+7o+B7qwKJQbt0c8WamFnxpkR43avgG9k/niTntwCaXiXUTjonWy0+61ZuX6CGzj9nn8CQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/instrumentation-amqplib@0.59.0': + resolution: {integrity: sha512-xscSgOJA+GHphESDZxBHNk/zjNaEgoeufMwmiqYdL+qM27Xw3BbR9vN6Ucbq9dW6Y+oYUPgTTj17qf+Za4+uzg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-aws-lambda@0.64.0': + resolution: {integrity: sha512-vYhM/a8fG34/Dl/Q9gfv5Ih3OFPgqeyn79S8FN+Xs/QZw6h6L8a1lDa3CyigyicOXLCmVIM7Fc9vFD4BGqgGLA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-aws-sdk@0.67.0': + resolution: {integrity: sha512-btpwJnZ2RBXDh/pTpfVpInpBu9Pedi+lbLKbt3naB344SggbbYnIdT7u8EzmGIApWi9EV91vw7hm896I7nESQA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-bunyan@0.57.0': + resolution: {integrity: sha512-W4zLz1Y9ptCsdL+QMXR7xQaBHkJivLBmVlLCjUe23rX4V8E65fGAtlIJSKTKAfz4aEgtWgQAGMdkeqACwG0Caw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-cassandra-driver@0.57.0': + resolution: {integrity: sha512-xLwrK+XnN32IB5i6t/a2j+SVdjlq/BIgjpVRkke4HAsKjoSMy1GeSI+ZOiJffRLFb4MojcvH4RG2+nEg1uC6Zg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-connect@0.55.0': + resolution: {integrity: sha512-UfGw7ubKKZBoTRjxi5KlfeECEaXZinS20RdRNlZE5tVF+O17hJOnrcGwAoQAHp6eYmxI2jW9IQ4t6450gnNF9g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-cucumber@0.28.0': + resolution: {integrity: sha512-kim+bRxu4LZqKEyF2SgO01tgG88W+/iYltyP1XjT31FIXzlBjzQpwtSLLM8byayO85mcZIBha54WSNFDLM/7qQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/instrumentation-dataloader@0.29.0': + resolution: {integrity: sha512-220WjRb1G1UiAKbVblSMxwxxFdpyB4wj1XYIO9BJs5r62Azj2dL5fyZiXK3/WO6wB3uLul9R946iKI1bpPxktQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-dns@0.55.0': + resolution: {integrity: sha512-cfWLaFi22V+sQrKY7t6QroYzT3kO9m3PpkN1OXYmuCyfwxQaXOVlF8NSAHtua/RQYw0aQl+2fe6JOWyJdEZiwA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-express@0.60.0': + resolution: {integrity: sha512-KghHCDqKq0D7iuPIVCuPSXut5WVAI6uwKcPrhwTUJL5VE2LC18id2vKoiAm1V8XvVlgIGAiECtEvbrFwkTCj3g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-fastify@0.56.0': + resolution: {integrity: sha512-zotOPoZsWtMF47BjottK23XaaBSmVuwG5D/R3FlGfAAwMNFoDR3IY1OGO9v9KfOU/1/xDVkxsQ22NFfu9lE8aA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-fs@0.31.0': + resolution: {integrity: sha512-C7tdXGDnkMgLVlE79VSekB+Y+P345zKUigvFMs5M7U0GIYA8ERx3FS0aAcY/ICIq9YwRmH2uuMb++Br5M2vNUg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-generic-pool@0.55.0': + resolution: {integrity: sha512-7hWiyLbEX/dIS4LZy/h8VaAQPs8oBeEqsrysDWbos0b9PF414L6Rsbi2um/omtxIs+GTvsbuqDscWigeaxyWdA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-graphql@0.60.0': + resolution: {integrity: sha512-XPATrmxAd2tFCsYbJ3eVIXt+gyvMKjc36QQuQxjtssMnAbw006Le9b5lKs7WXik7ItOpM1exATi1aDdOcCjRRg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-grpc@0.212.0': + resolution: {integrity: sha512-r1t7LNKWVhSQMUrBdDJtooFmmLZ93kGuFixqeXPoUP8W+chJCxhey9l0c0+L3xriNdyB7TzvkKHhPXUDevgVEA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-hapi@0.58.0': + resolution: {integrity: sha512-reuRApR2KHm2VsfyDgsrLhNE+IOy4uIU6n3oMjUleReHacEEZmf4vXxdt4/qcmJ6GoUXnRN2AOu3s5N3pMrgYA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-http@0.212.0': + resolution: {integrity: sha512-t2nt16Uyv9irgR+tqnX96YeToOStc3X5js7Ljn3EKlI2b4Fe76VhMkTXtsTQ0aId6AsYgefrCRnXSCo/Fn/vww==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-ioredis@0.60.0': + resolution: {integrity: sha512-R+nnbPD9l2ruzu248qM3YDWzpdmWVaFFFv08lQqsc0EP4pT/B1GGUg06/tHOSo3L5njB2eejwyzpkvJkjaQEMA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-kafkajs@0.21.0': + resolution: {integrity: sha512-lkLrILnKGO7SHw1xPJnuGx2S4XwbKmQiJyzUGuEImRoU/6Gj0Nka0lkbeRd4ANN20dxr/mLdXIsUsk6DzTrX6A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-knex@0.56.0': + resolution: {integrity: sha512-pKqtY5lbAQ70MC5K/BJeAA1t2gAUlRBZBAJ5ergRUNs5jw8zbdOXEZOLztiuNvQqD2z4a9N0Tkde9JMFm2pKMQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-koa@0.60.0': + resolution: {integrity: sha512-UOmu2y2LHgPzKsm9xd0sCQJimr11YP4MKFc190Do1ufd8qds7Zd5BI3f6TudqYhH9dUIhojsQyUaS6K4nv5FsQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + + '@opentelemetry/instrumentation-lru-memoizer@0.56.0': + resolution: {integrity: sha512-vXtOValhKRgWA9tLAiTU3P37Q31OveRuM2N5iLSVHl4GzkMBQ5p50A9kSKvt5gReL6BzFDXPCM9ItJiAhSS2KQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-memcached@0.55.0': + resolution: {integrity: sha512-kdhW/j5X+vNCAvHVc50PZfvE7diUScg1ZkBaNFRygY3Z6IUjgPLR0luWQMDPSFun6AVo1HaMDPxbUqJrot6qrA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongodb@0.65.0': + resolution: {integrity: sha512-hOAJRs5vrY7fZolSYUXmf29Y+HFDHWrek0DeLq82uwMPjPSda7h6oumQnqEX5olzw357q/QG39/uJdkclJ/JUg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongoose@0.58.0': + resolution: {integrity: sha512-3L0Fqo1y2oreISFPWaqdt/bg3NhLgrkn5U/E/9RNG1QaM81drTMBCHseMY1q8SlejjE43ZWOy+0KbmRBlUPJ+g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql2@0.58.0': + resolution: {integrity: sha512-EubjV1XZb7XHrENqF7TW2lnah+KN0LddMneKNAB8PjGVKL5lJkVV/vhJ6EIcUNn9nCWmAwZ3GRcFVEDKCnyXfQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql@0.58.0': + resolution: {integrity: sha512-wZDrBCL3WfJclV6KywWVV3/B2ZiUYmDQdgyu3pq4jK/5qSfoDmezHzT/Nayln5MVVWMAGXIMLrCj8BKa6jaKQQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-nestjs-core@0.58.0': + resolution: {integrity: sha512-0lE9oW8j6nmvBHJoOxIQgKzMQQYNfX1nhiWZdXD0sNAMFsWBtvECWS7NAPSroKrEP53I04TcHCyyhcK4I9voXg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-net@0.56.0': + resolution: {integrity: sha512-h69x7U6f86mP3gGWWTaMkQZk0K3tBvpVMIU7E0q2kkVw6eZ5TqFm9rkaEy38moQmixiDFQ9j/2/cwxG9P7ZEeA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-openai@0.10.0': + resolution: {integrity: sha512-0lV2zxge2mMaruVCw/bmypWVu+aJ76rc0HBvAVFCPUI3zzJdgBZJZafGIHZ1IB2F6VvrDFL+JstEnle6V8brvA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-oracledb@0.37.0': + resolution: {integrity: sha512-OzMghtAEAEkXlkUrZI4QcXSZq0MILeU6WC0/N5+1MSkuIkruIeaRw99/RtyS2of8vlPDa8XbbXl32Q1RM3wSyg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-pg@0.64.0': + resolution: {integrity: sha512-NbfB/rlfsRI3zpTjnbvJv3qwuoGLsN8FxR/XoI+ZTn1Rs62x1IenO+TSSvk4NO+7FlXpd2MiOe8LT/oNbydHGA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-pino@0.58.0': + resolution: {integrity: sha512-rgy+tA7cDjuSq6dXAO40OiYP25azIDHMBtxG3RzSmCBVEYdjggl6btyuLVasX6VkOOhP2gf6PBuLMNxVwaIqAw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-redis@0.60.0': + resolution: {integrity: sha512-Ea/GffmmzIVHc9geaMjT94IR7poVZzIv4Kk/Lw0tbxGD3cBYcMUsLFVajKxpZsE1NRCECFpidAWeifCIKD0inw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-restify@0.57.0': + resolution: {integrity: sha512-kO6MsZFU+RdXOKhsKw8SOSBYGYCdFSlza+mpBQRl1DQmveZcnidchv4V5JQPtNgHxCGH+1n3hDpLdxdGUbJPNA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-router@0.56.0': + resolution: {integrity: sha512-PHECDGQElLazI/QbHU16C5m9fDC7DGJk+jLIwO5ca6bcp7bXhUPPUTT78l7da2pDsrz4mhv5ytYNZmBbW/Q3rA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-runtime-node@0.25.0': + resolution: {integrity: sha512-XaCmwBSui5KeTn8M6OzaEn1rEsNWtUkjuc1ylg0tqQTLHibNQ0n7f8v4zdF6x/nBV1OnsiYlN8RLHauGemv/TA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-socket.io@0.59.0': + resolution: {integrity: sha512-71DnM/FEqH0PjvU2uZvzWJeaGyVIy3rJKk8rZrxg/aS2QT3qLGb+UPL/B+1vOw4pzDPn4papLTSMpLVF9G8uvw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-tedious@0.31.0': + resolution: {integrity: sha512-HoF2EtcyP3JR4R3jLPHohZ9lFcj1QLJyGmFfLKDTvUUjPiFuK4XZ6L1OV9HhaqvN0xY+tWKfNdCPS3r33rd0Xw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-undici@0.22.0': + resolution: {integrity: sha512-yb6vEWUPOrD5i7yR1XceEEqiVHbMgr5YnUPnom5eQVCjvrTkEVswyrf9i+vvJR+28wrNqILIIphWgOOx6BjnTQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.7.0 + + '@opentelemetry/instrumentation-winston@0.56.0': + resolution: {integrity: sha512-ITIA0Qe61CQ6FQU/bN23pNBvJ+5U0ofoASMOOYrODtXyV9wI267AigNTTwDmv2Myt8dPEFvvVFJZKhiZLIpehA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation@0.212.0': + resolution: {integrity: sha512-IyXmpNnifNouMOe0I/gX7ENfv2ZCNdYTF0FpCsoBcpbIHzk81Ww9rQTYTnvghszCg7qGrIhNvWC8dhEifgX9Jg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.212.0': + resolution: {integrity: sha512-HoMv5pQlzbuxiMS0hN7oiUtg8RsJR5T7EhZccumIWxYfNo/f4wFc7LPDfFK6oHdG2JF/+qTocfqIHoom+7kLpw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-grpc-exporter-base@0.212.0': + resolution: {integrity: sha512-YidOSlzpsun9uw0iyIWrQp6HxpMtBlECE3tiHGAsnpEqJWbAUWcMnIffvIuvTtTQ1OyRtwwaE79dWSQ8+eiB7g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.212.0': + resolution: {integrity: sha512-bj7zYFOg6Db7NUwsRZQ/WoVXpAf41WY2gsd3kShSfdpZQDRKHWJiRZIg7A8HvWsf97wb05rMFzPbmSHyjEl9tw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/propagator-b3@2.5.1': + resolution: {integrity: sha512-AU6sZgunZrZv/LTeHP+9IQsSSH5p3PtOfDPe8VTdwYH69nZCfvvvXehhzu+9fMW2mgJMh5RVpiH8M9xuYOu5Dg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/propagator-jaeger@2.5.1': + resolution: {integrity: sha512-8+SB94/aSIOVGDUPRFSBRHVUm2A8ye1vC6/qcf/D+TF4qat7PC6rbJhRxiUGDXZtMtKEPM/glgv5cBGSJQymSg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/redis-common@0.38.2': + resolution: {integrity: sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA==} + engines: {node: ^18.19.0 || >=20.6.0} + + '@opentelemetry/resource-detector-alibaba-cloud@0.33.2': + resolution: {integrity: sha512-EaS54zwYmOg9Ttc79juaktpCBYqyh2IquXl534sLls+c1/pc8LZfWPMqytFt+iBvSPQ6ajraUnvi6cun4AhSjQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-aws@2.12.0': + resolution: {integrity: sha512-VelueKblsnQEiBVqEYcvM9VEb+B8zN6nftltdO9HAD7qi/OlicP4z/UGJ9EeW2m++WabdMoj0G3QVL8YV0P9tw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-azure@0.20.0': + resolution: {integrity: sha512-iRy+O2cB6DOlQ/OONaK+L8Cp8nLS89dZVRp6KgnFAfzykXuq9Ws/ygJKcU3CCmjkgY5j2Vk3uVTre/E35bWhYg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-container@0.8.3': + resolution: {integrity: sha512-5J0JP2cy655rBKM9Doz26ffO3rG+Xqm7OXeNXkckzmc3JmL6Bj3dPBKugPYsfemhEIqtf7INH9UmPQqTMuWoHg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-gcp@0.47.0': + resolution: {integrity: sha512-57T/kRVdU0ch1P4KPEkmU2b5mWNlUs8hHgqrBYVF+fNZMc1jMdL1mANZhEzoLtWKIeoCEy+57Itt7RkXAYNJiQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resources@2.5.1': + resolution: {integrity: sha512-BViBCdE/GuXRlp9k7nS1w6wJvY5fnFX5XvuEtWsTAOQFIO89Eru7lGW3WbfbxtCuZ/GbrJfAziXG0w0dpxL7eQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.212.0': + resolution: {integrity: sha512-qglb5cqTf0mOC1sDdZ7nfrPjgmAqs2OxkzOPIf2+Rqx8yKBK0pS7wRtB1xH30rqahBIut9QJDbDePyvtyqvH/Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-metrics@2.5.1': + resolution: {integrity: sha512-RKMn3QKi8nE71ULUo0g/MBvq1N4icEBo7cQSKnL3URZT16/YH3nSVgWegOjwx7FRBTrjOIkMJkCUn/ZFIEfn4A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-node@0.212.0': + resolution: {integrity: sha512-tJzVDk4Lo44MdgJLlP+gdYdMnjxSNsjC/IiTxj5CFSnsjzpHXwifgl3BpUX67Ty3KcdubNVfedeBc/TlqHXwwg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.5.1': + resolution: {integrity: sha512-iZH3Gw8cxQn0gjpOjJMmKLd9GIaNh/E3v3ST67vyzLSxHBs14HsG4dy7jMYyC5WXGdBVEcM7U/XTF5hCQxjDMw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-node@2.5.1': + resolution: {integrity: sha512-9lopQ6ZoElETOEN0csgmtEV5/9C7BMfA7VtF4Jape3i954b6sTY2k3Xw3CxUTKreDck/vpAuJM+EDo4zheUw+A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.40.0': + resolution: {integrity: sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==} + engines: {node: '>=14'} + + '@opentelemetry/sql-common@0.41.2': + resolution: {integrity: sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.1.0 + + '@opentelemetry/winston-transport@0.22.0': + resolution: {integrity: sha512-VuiSjTxfAk5GNuTtBPbvPEDPFV8U9qWgn4DyGHuBNfuJrBcz21mmRyQxpOR+ueG6oA7Nrx7MmTFJ4JiWDvF2Vw==} + engines: {node: ^18.19.0 || >=20.6.0} + + '@otplib/core@13.3.0': + resolution: {integrity: sha512-pnQDOuCmFVeF/XnboJq9TOJgLoo2idNPJKMymOF8vGqJJ+ReKRYM9bUGjNPRWC0tHjMwu1TXbnzyBp494JgRag==} + + '@otplib/hotp@13.3.0': + resolution: {integrity: sha512-XJMZGz2bg4QJwK7ulvl1GUI2VMn/flaIk/E/BTKAejHsX2kUtPF1bRhlZ2+elq8uU5Fs9Z9FHcQK2CPZNQbbUQ==} + + '@otplib/plugin-base32-scure@13.3.0': + resolution: {integrity: sha512-/jYbL5S6GB0Ie3XGEWtLIr9s5ZICl/BfmNL7+8/W7usZaUU4GiyLd2S+JGsNCslPyqNekSudD864nDAvRI0s8w==} + + '@otplib/plugin-crypto-noble@13.3.0': + resolution: {integrity: sha512-wmV+jBVncepgwv99G7Plrdzd0tHfbpXk2U+OD7MO7DzpDqOYEgOPi+IIneksJSTL8QvWdfi+uQEuhnER4fKouA==} + + '@otplib/totp@13.3.0': + resolution: {integrity: sha512-XfjGNoN8d9S3Ove2j7AwkVV7+QDFsV7Lm7YwSiezNaHffkWtJ60aJYpmf+01dARdPST71U2ptueMsRJso4sq4A==} + + '@otplib/uri@13.3.0': + resolution: {integrity: sha512-3oh6nBXy+cm3UX9cxEAGZiDrfxHU2gfelYFV+XNCx+8dq39VaQVymwlU2yjPZiMAi/3agaUeEftf2RwM5F+Cyg==} + + '@phc/format@1.0.0': + resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==} + engines: {node: '>=10'} + + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@poppinss/macroable@1.1.0': + resolution: {integrity: sha512-y/YKzZDuG8XrpXpM7Z1RdQpiIc0MAKyva24Ux1PB4aI7RiSI/79K8JVDcdyubriTm7vJ1LhFs8CrZpmPnx/8Pw==} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + + '@rollup/plugin-commonjs@29.0.0': + resolution: {integrity: sha512-U2YHaxR2cU/yAiwKJtJRhnyLk7cifnQw0zUpISsocBDoHDJn+HTV74ABqnwr5bEgWUwFZC9oFL6wLe21lHu5eQ==} + engines: {node: '>=16.0.0 || 14 >= 14.17'} + peerDependencies: + rollup: ^2.68.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-json@6.1.0': + resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-node-resolve@16.0.3': + resolution: {integrity: sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] + + '@scure/base@2.0.0': + resolution: {integrity: sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==} + + '@selderee/plugin-htmlparser2@0.11.0': + resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + + '@sideway/address@4.1.5': + resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} + + '@sideway/formula@3.0.1': + resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==} + + '@sideway/pinpoint@2.0.0': + resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} + + '@so-ric/colorspace@1.1.6': + resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@sveltejs/acorn-typescript@1.0.9': + resolution: {integrity: sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==} + peerDependencies: + acorn: ^8.9.0 + + '@sveltejs/adapter-node@5.5.4': + resolution: {integrity: sha512-45X92CXW+2J8ZUzPv3eLlKWEzINKiiGeFWTjyER4ZN4sGgNoaoeSkCY/QYNxHpPXy71QPsctwccBo9jJs0ySPQ==} + peerDependencies: + '@sveltejs/kit': ^2.4.0 + + '@sveltejs/kit@2.53.4': + resolution: {integrity: sha512-iAIPEahFgDJJyvz8g0jP08KvqnM6JvdW8YfsygZ+pMeMvyM2zssWMltcsotETvjSZ82G3VlitgDtBIvpQSZrTA==} + engines: {node: '>=18.13'} + hasBin: true + 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 || ^7.0.0 + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: ^5.3.3 + vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + typescript: + optional: true + + '@sveltejs/vite-plugin-svelte-inspector@5.0.2': + resolution: {integrity: sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^6.0.0-next.0 + svelte: ^5.0.0 + vite: ^6.3.0 || ^7.0.0 + + '@sveltejs/vite-plugin-svelte@6.2.4': + resolution: {integrity: sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + svelte: ^5.0.0 + vite: ^6.3.0 || ^7.0.0 + + '@swc/helpers@0.5.19': + resolution: {integrity: sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==} + + '@tailwindcss/forms@0.5.11': + resolution: {integrity: sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==} + peerDependencies: + tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1' + + '@tailwindcss/node@4.2.1': + resolution: {integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==} + + '@tailwindcss/oxide-android-arm64@4.2.1': + resolution: {integrity: sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.2.1': + resolution: {integrity: sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.2.1': + resolution: {integrity: sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.2.1': + resolution: {integrity: sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': + resolution: {integrity: sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': + resolution: {integrity: sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.2.1': + resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.2.1': + resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.2.1': + resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.2.1': + resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': + resolution: {integrity: sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.2.1': + resolution: {integrity: sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.2.1': + resolution: {integrity: sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==} + engines: {node: '>= 20'} + + '@tailwindcss/typography@0.5.19': + resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + + '@tailwindcss/vite@4.2.1': + resolution: {integrity: sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + + '@tanstack/table-core@8.21.3': + resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} + engines: {node: '>=12'} + + '@types/aws-lambda@8.10.161': + resolution: {integrity: sha512-rUYdp+MQwSFocxIOcSsYSF3YYYC/uUpMbCY/mbO21vGqfrEYvNSoPyKYDj6RhXXpPfS0KstW9RwG3qXh9sL7FQ==} + + '@types/bun@1.3.9': + resolution: {integrity: sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw==} + + '@types/bunyan@1.8.11': + resolution: {integrity: sha512-758fRH7umIMk5qt5ELmRMff4mLDlN+xyYzC+dkPTdKwbSkJFvz6xwyScrytPU0QIBbRRwbiE8/BIg8bpajerNQ==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/mailparser@3.4.6': + resolution: {integrity: sha512-wVV3cnIKzxTffaPH8iRnddX1zahbYB1ZEoAxyhoBo3TBCBuK6nZ8M8JYO/RhsCuuBVOw/DEN/t/ENbruwlxn6Q==} + + '@types/memcached@2.2.10': + resolution: {integrity: sha512-AM9smvZN55Gzs2wRrqeMHVP7KE8KWgCJO/XL5yCly2xF6EKa4YlbpK+cLSAH4NG/Ah64HrlegmGqW8kYws7Vxg==} + + '@types/mysql@2.15.27': + resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==} + + '@types/node@25.5.0': + resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} + + '@types/oracledb@6.5.2': + resolution: {integrity: sha512-kK1eBS/Adeyis+3OlBDMeQQuasIDLUYXsi2T15ccNJ0iyUpQ4xDF7svFu3+bGVrI0CMBUclPciz+lsQR3JX3TQ==} + + '@types/pdfkit@0.14.0': + resolution: {integrity: sha512-X94hoZVr9dNfV23roeXRm57AWS+AOMak3gq2wZvn4TXiLvXE8+TrYaM5IkMyZbGRw49jEqI49rP/UVL3+C3Svg==} + + '@types/pg-pool@2.0.7': + resolution: {integrity: sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng==} + + '@types/pg@8.15.6': + resolution: {integrity: sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==} + + '@types/pg@8.16.0': + resolution: {integrity: sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==} + + '@types/qrcode@1.5.6': + resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==} + + '@types/resolve@1.20.2': + resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + + '@types/tedious@4.0.14': + resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} + + '@types/tmp@0.2.6': + resolution: {integrity: sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==} + + '@types/triple-beam@1.3.5': + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + + '@types/validator@13.15.10': + resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} + + '@typeschema/class-validator@0.3.0': + resolution: {integrity: sha512-OJSFeZDIQ8EK1HTljKLT5CItM2wsbgczLN8tMEfz3I1Lmhc5TBfkZ0eikFzUC16tI3d1Nag7um6TfCgp2I2Bww==} + peerDependencies: + class-validator: ^0.14.1 + peerDependenciesMeta: + class-validator: + optional: true + + '@typeschema/core@0.14.0': + resolution: {integrity: sha512-Ia6PtZHcL3KqsAWXjMi5xIyZ7XMH4aSnOQes8mfMLx+wGFGtGRNlwe6Y7cYvX+WfNK67OL0/HSe9t8QDygV0/w==} + peerDependencies: + '@types/json-schema': ^7.0.15 + peerDependenciesMeta: + '@types/json-schema': + optional: true + + '@valibot/to-json-schema@1.5.0': + resolution: {integrity: sha512-GE7DmSr1C2UCWPiV0upRH6mv0cCPsqYGs819fb6srCS1tWhyXrkGGe+zxUiwzn/L1BOfADH4sNjY/YHCuP8phQ==} + peerDependencies: + valibot: ^1.2.0 + + '@vinejs/compiler@3.0.0': + resolution: {integrity: sha512-v9Lsv59nR56+bmy2p0+czjZxsLHwaibJ+SV5iK9JJfehlJMa501jUJQqqz4X/OqKXrxtE3uTQmSqjUqzF3B2mw==} + engines: {node: '>=18.0.0'} + + '@vinejs/vine@3.0.1': + resolution: {integrity: sha512-ZtvYkYpZOYdvbws3uaOAvTFuvFXoQGAtmzeiXu+XSMGxi5GVsODpoI9Xu9TplEMuD/5fmAtBbKb9cQHkWkLXDQ==} + engines: {node: '>=18.16.0'} + + '@vitest/expect@4.0.18': + resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} + + '@vitest/mocker@4.0.18': + resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.0.18': + resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} + + '@vitest/runner@4.0.18': + resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} + + '@vitest/snapshot@4.0.18': + resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} + + '@vitest/spy@4.0.18': + resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} + + '@vitest/utils@4.0.18': + resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + + '@zone-eu/mailsplit@5.4.8': + resolution: {integrity: sha512-eEyACj4JZ7sjzRvy26QhLgKEMWwQbsw1+QZnlLX+/gihcNH07lVPOcnwf5U6UAL7gkc//J3jVd76o/WS+taUiA==} + + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + argon2@0.43.1: + resolution: {integrity: sha512-TfOzvDWUaQPurCT1hOwIeFNkgrAJDpbBGBGWDgzDsm11nNhImc13WhdGdCU6K7brkp8VpeY07oGtSex0Wmhg8w==} + engines: {node: '>=16.17.0'} + + aria-query@5.3.1: + resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} + engines: {node: '>= 0.4'} + + arkregex@0.0.5: + resolution: {integrity: sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw==} + + arktype@2.1.29: + resolution: {integrity: sha512-jyfKk4xIOzvYNayqnD8ZJQqOwcrTOUbIU4293yrzAjA3O1dWh61j71ArMQ6tS/u4pD7vabSPe7nG3RCyoXW6RQ==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + better-auth@1.4.20: + resolution: {integrity: sha512-cUQaUhZ/EZwb7xgoL9wHl78yWp0eaxC/L++B/r8RJxk23L766Tk7fLjWG6bQK8eAHDDpfQNwXsJowiei8tJWJw==} + peerDependencies: + '@lynx-js/react': '*' + '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 + '@sveltejs/kit': ^2.0.0 + '@tanstack/react-start': ^1.0.0 + '@tanstack/solid-start': ^1.0.0 + better-sqlite3: ^12.0.0 + drizzle-kit: '>=0.31.4' + drizzle-orm: '>=0.41.0' + mongodb: ^6.0.0 || ^7.0.0 + mysql2: ^3.0.0 + next: ^14.0.0 || ^15.0.0 || ^16.0.0 + pg: ^8.0.0 + prisma: ^5.0.0 || ^6.0.0 || ^7.0.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: ^2.0.0 || ^3.0.0 || ^4.0.0 + vue: ^3.0.0 + peerDependenciesMeta: + '@lynx-js/react': + optional: true + '@prisma/client': + optional: true + '@sveltejs/kit': + optional: true + '@tanstack/react-start': + optional: true + '@tanstack/solid-start': + optional: true + better-sqlite3: + optional: true + drizzle-kit: + optional: true + drizzle-orm: + optional: true + mongodb: + optional: true + mysql2: + optional: true + next: + optional: true + pg: + optional: true + prisma: + optional: true + react: + optional: true + react-dom: + optional: true + solid-js: + optional: true + svelte: + optional: true + vitest: + optional: true + vue: + optional: true + + better-call@1.1.8: + resolution: {integrity: sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw==} + peerDependencies: + zod: ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + + bits-ui@2.16.2: + resolution: {integrity: sha512-bgEpRRF7Ck9nRP1pbuKVxpaSMrz+8Pm0y+dmuvlkrSe+uUwIQECef29y6eslFHM6pCAubUh7STrsTLUUp8fzFQ==} + engines: {node: '>=20'} + peerDependencies: + '@internationalized/date': ^3.8.1 + svelte: ^5.33.0 + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + bun-types@1.3.9: + resolution: {integrity: sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg==} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@8.0.0: + resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} + engines: {node: '>=16'} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + cjs-module-lexer@2.2.0: + resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} + + class-validator@0.14.4: + resolution: {integrity: sha512-AwNusCCam51q703dW82x95tOqQp6oC9HNUl724KxJJOfnKscI8dOloXFgyez7LbTTKWuRBA37FScqVbJEoq8Yw==} + + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-convert@3.1.3: + resolution: {integrity: sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==} + engines: {node: '>=14.6'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-name@2.1.0: + resolution: {integrity: sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==} + engines: {node: '>=12.20'} + + color-string@2.1.4: + resolution: {integrity: sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==} + engines: {node: '>=18'} + + color@5.0.3: + resolution: {integrity: sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==} + engines: {node: '>=18'} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + + cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + d3-array@2.12.1: + resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-geo-voronoi@2.1.0: + resolution: {integrity: sha512-kqE4yYuOjPbKdBXG0xztCacPwkVSK2REF1opSNrnqqtXJmNcM++UbwQ8SxvwP6IQTj9RvIjjK4qeiVsEfj0Z2Q==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate-path@2.3.0: + resolution: {integrity: sha512-tZYtGXxBmbgHsIc9Wms6LS5u4w6KbP8C09a4/ZYc4KLMYYqub57rRBUgpUr2CIarIrJEpdAWWxWQvofgaMpbKQ==} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@1.0.9: + resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-sankey@0.12.3: + resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@1.3.7: + resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-tile@1.0.0: + resolution: {integrity: sha512-79fnTKpPMPDS5xQ0xuS9ir0165NEwwkFpe/DSOmc2Gl9ldYzKKRDWogmTTE8wAJ8NA7PMapNfEcyKhI9Lxdu5Q==} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-tricontour@1.1.0: + resolution: {integrity: sha512-G7gHKj89n2owmkGb6WX6ixcnQ0Kf/0wpa9VIh9DGdbHu8wdrlaHU4ir3/bFNERl8N8nn4G7e7qbtBG8N9caihQ==} + engines: {node: '>=12'} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + date-fns-tz@3.2.0: + resolution: {integrity: sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==} + peerDependencies: + date-fns: ^3.0.0 || ^4.0.0 + + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + delaunator@5.0.1: + resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + devalue@5.6.3: + resolution: {integrity: sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==} + + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dotenv@17.3.1: + resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} + engines: {node: '>=12'} + + drizzle-kit@0.31.9: + resolution: {integrity: sha512-GViD3IgsXn7trFyBUUHyTFBpH/FsHTxYJ66qdbVggxef4UBPHRYxQaRzYLTuekYnk9i5FIEL9pbBIwMqX/Uwrg==} + hasBin: true + + drizzle-orm@0.45.1: + resolution: {integrity: sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=4' + '@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.13' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/sql.js': '*' + '@upstash/redis': '>=1.34.7' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + gel: '>=2' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/sql.js': + optional: true + '@upstash/redis': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + gel: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + effect@3.19.19: + resolution: {integrity: sha512-Yc8U/SVXo2dHnaP7zNBlAo83h/nzSJpi7vph6Hzyl4ulgMBIgPmz3UzOjb9sBgpFE00gC0iETR244sfXDNLHRg==} + + embla-carousel-reactive-utils@8.6.0: + resolution: {integrity: sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==} + peerDependencies: + embla-carousel: 8.6.0 + + embla-carousel-svelte@8.6.0: + resolution: {integrity: sha512-ZDsKk8Sdv+AUTygMYcwZjfRd1DTh+JSUzxkOo8b9iKAkYjg+39mzbY/lwHsE3jXSpKxdKWS69hPSNuzlOGtR2Q==} + peerDependencies: + svelte: ^3.49.0 || ^4.0.0 || ^5.0.0 + + embla-carousel@8.6.0: + resolution: {integrity: sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + + encoding-japanese@2.2.0: + resolution: {integrity: sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A==} + engines: {node: '>=8.10.0'} + + enhanced-resolve@5.20.0: + resolution: {integrity: sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==} + engines: {node: '>=10.13.0'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild-register@3.6.0: + resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} + peerDependencies: + esbuild: '>=0.12 <1' + + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + + esrap@2.2.3: + resolution: {integrity: sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-check@3.23.2: + resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} + engines: {node: '>=8.0.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + formsnap@2.0.1: + resolution: {integrity: sha512-iJSe4YKd/W6WhLwKDVJU9FQeaJRpEFuolhju7ZXlRpUVyDdqFdMP8AUBICgnVvQPyP41IPAlBa/v0Eo35iE6wQ==} + engines: {node: '>=18', pnpm: '>=8.7.0'} + peerDependencies: + svelte: ^5.0.0 + sveltekit-superforms: ^2.19.0 + + forwarded-parse@2.1.2: + resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gaxios@7.1.3: + resolution: {integrity: sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==} + engines: {node: '>=18'} + + gcp-metadata@8.1.2: + resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} + engines: {node: '>=18'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-tsconfig@4.13.6: + resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + google-logging-utils@1.1.3: + resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} + engines: {node: '>=14'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + hono@4.12.8: + resolution: {integrity: sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==} + engines: {node: '>=16.9.0'} + + html-to-text@9.0.5: + resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} + engines: {node: '>=14'} + + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + imapflow@1.2.10: + resolution: {integrity: sha512-tqmk0Gj4YBEnGCjjrUYWIf3Z4tzn4iihUcMkBRbafvHF3LqEiYNCSJAAYYbwERFxlikOJ3zzqtEcoxCUTjMv2Q==} + + import-in-the-middle@2.0.6: + resolution: {integrity: sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==} + + import-in-the-middle@3.0.0: + resolution: {integrity: sha512-OnGy+eYT7wVejH2XWgLRgbmzujhhVIATQH0ztIeRilwHBjTeG3pD+XnH3PKX0r9gJ0BuJmJ68q/oh9qgXnNDQg==} + engines: {node: '>=18'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + + internmap@1.0.1: + resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + ioredis@5.10.0: + resolution: {integrity: sha512-HVBe9OFuqs+Z6n64q09PQvP1/R4Bm+30PAyyD4wIEqssh3v9L21QjCVk4kRLucMBcDokJTcLjsGeVRlq/nH6DA==} + engines: {node: '>=12.22.0'} + + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-module@1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + + is-reference@1.2.1: + resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + joi@17.13.3: + resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} + + jose@6.1.3: + resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + + json-schema-to-ts@3.1.1: + resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} + engines: {node: '>=16'} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + + kysely@0.28.11: + resolution: {integrity: sha512-zpGIFg0HuoC893rIjYX1BETkVWdDnzTzF5e0kWXJFg5lE0k1/LfNWBejrcnOFu8Q2Rfq/hTDTU7XLUM8QOrpzg==} + engines: {node: '>=20.0.0'} + + layerchart@2.0.0-next.43: + resolution: {integrity: sha512-1Ywm38NdzHWKwgaAHq3EcqshIgsq+pylntSnVWAVazXUk/NsxPcxdpR3tMt3ySjWV0ZPBBgLs78sdVf7FTgd+g==} + peerDependencies: + svelte: ^5.0.0 + + leac@0.6.0: + resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} + + libbase64@1.3.0: + resolution: {integrity: sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==} + + libmime@5.3.7: + resolution: {integrity: sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw==} + + libphonenumber-js@1.12.38: + resolution: {integrity: sha512-vwzxmasAy9hZigxtqTbFEwp8ZdZ975TiqVDwj5bKx5sR+zi5ucUQy9mbVTkKM9GzqdLdxux/hTw2nmN5J7POMA==} + + libqp@2.1.1: + resolution: {integrity: sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow==} + + lightningcss-android-arm64@1.31.1: + resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.31.1: + resolution: {integrity: sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.31.1: + resolution: {integrity: sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.31.1: + resolution: {integrity: sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.31.1: + resolution: {integrity: sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.31.1: + resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.31.1: + resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.31.1: + resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.31.1: + resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.31.1: + resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.31.1: + resolution: {integrity: sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.31.1: + resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==} + engines: {node: '>= 12.0.0'} + + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + + local-pkg@1.1.2: + resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} + engines: {node: '>=14'} + + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + lodash-es@4.17.23: + resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} + + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + + logform@2.7.0: + resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} + engines: {node: '>= 12.0.0'} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + mailparser@3.9.3: + resolution: {integrity: sha512-AnB0a3zROum6fLaa52L+/K2SoRJVyFDk78Ea6q1D0ofcZLxWEWDtsS1+OrVqKbV7r5dulKL/AwYQccFGAPpuYQ==} + + memoize-weak@1.0.2: + resolution: {integrity: sha512-gj39xkrjEw7nCn4nJ1M5ms6+MyMlyiGmttzsqAUsAKn6bYKwuTHh/AO3cKPF8IBrTIYTxb0wWXFs3E//Y8VoWQ==} + + memoize@10.2.0: + resolution: {integrity: sha512-DeC6b7QBrZsRs3Y02A6A7lQyzFbsQbqgjI6UW0GigGWV+u1s25TycMr0XHZE4cJce7rY/vyw2ctMQqfDkIhUEA==} + engines: {node: '>=18'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + mini-svg-data-uri@1.4.4: + resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} + hasBin: true + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + + mode-watcher@1.1.0: + resolution: {integrity: sha512-mUT9RRGPDYenk59qJauN1rhsIMKBmWA3xMF+uRwE8MW/tjhaDSCCARqkSuDTq8vr4/2KcAxIGVjACxTjdk5C3g==} + peerDependencies: + svelte: ^5.27.0 + + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanoid@5.1.6: + resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==} + engines: {node: ^18 || >=20} + hasBin: true + + nanostores@1.1.1: + resolution: {integrity: sha512-EYJqS25r2iBeTtGQCHidXl1VfZ1jXM7Q04zXJOrMlxVVmD0ptxJaNux92n1mJ7c5lN3zTq12MhH/8x59nP+qmg==} + engines: {node: ^20.0.0 || >=22.0.0} + + neverthrow@8.2.0: + resolution: {integrity: sha512-kOCT/1MCPAxY5iUV3wytNFUMUolzuwd/VF/1KCx7kf6CutrOsTie+84zTGTpgQycjvfLdBBdvBvFLqFD2c0wkQ==} + engines: {node: '>=18'} + + node-addon-api@8.6.0: + resolution: {integrity: sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==} + engines: {node: ^18 || ^20 || >= 21} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + + nodemailer@7.0.13: + resolution: {integrity: sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==} + engines: {node: '>=6.0.0'} + + nodemailer@8.0.1: + resolution: {integrity: sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==} + engines: {node: '>=6.0.0'} + + normalize-url@8.1.1: + resolution: {integrity: sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==} + engines: {node: '>=14.16'} + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + + otplib@13.3.0: + resolution: {integrity: sha512-VYMKyyDG8yt2q+z58sz54/EIyTh7+tyMrjeemR44iVh5+dkKtIs57irTqxjH+IkAL1uMmG1JIFhG5CxTpqdU5g==} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + + paneforge@1.0.2: + resolution: {integrity: sha512-KzmIXQH1wCfwZ4RsMohD/IUtEjVhteR+c+ulb/CHYJHX8SuDXoJmChtsc/Xs5Wl8NHS4L5Q7cxL8MG40gSU1bA==} + peerDependencies: + svelte: ^5.29.0 + + parseley@0.12.1: + resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + peberminta@0.9.0: + resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-protocol@1.12.0: + resolution: {integrity: sha512-uOANXNRACNdElMXJ0tPz6RBM0XQ61nONGAwlt8da5zs/iUOOCLBQOHSXnrC6fMsvtjxbOJrZZl5IScGv+7mpbg==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.3.1: + resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} + hasBin: true + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + + postcss-selector-parser@6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + engines: {node: '>=4'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + postgres@3.4.8: + resolution: {integrity: sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==} + engines: {node: '>=12'} + + prettier-plugin-sort-imports@1.8.11: + resolution: {integrity: sha512-ApmZkEmNh2Fi6TAcYNgUR15rsP1+omX43+8neY+MXfNZ7JQwrqSdjzKhLUYTtaLo52aaC9gCs+lJaYSU8oSJJA==} + peerDependencies: + typescript: '>4.0.0' + + prettier-plugin-svelte@3.5.0: + resolution: {integrity: sha512-2lLO/7EupnjO/95t+XZesXs8Bf3nYLIDfCo270h5QWbj/vjLqmrQ1LiRk9LPggxSDsnVYfehamZNf+rgQYApZg==} + peerDependencies: + prettier: ^3.0.0 + svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 + + prettier-plugin-tailwindcss@0.7.2: + resolution: {integrity: sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==} + engines: {node: '>=20.19'} + 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: '*' + peerDependenciesMeta: + '@ianvs/prettier-plugin-sort-imports': + optional: true + '@prettier/plugin-hermes': + optional: true + '@prettier/plugin-oxc': + optional: true + '@prettier/plugin-pug': + optional: true + '@shopify/prettier-plugin-liquid': + optional: true + '@trivago/prettier-plugin-sort-imports': + optional: true + '@zackad/prettier-plugin-twig': + optional: true + prettier-plugin-astro: + optional: true + prettier-plugin-css-order: + optional: true + prettier-plugin-jsdoc: + optional: true + prettier-plugin-marko: + optional: true + prettier-plugin-multiline-arrays: + optional: true + prettier-plugin-organize-attributes: + optional: true + prettier-plugin-organize-imports: + optional: true + prettier-plugin-sort-imports: + optional: true + prettier-plugin-svelte: + optional: true + + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} + engines: {node: '>=14'} + hasBin: true + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + + property-expr@2.0.6: + resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==} + + protobufjs@7.5.4: + resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} + engines: {node: '>=12.0.0'} + + protobufjs@8.0.0: + resolution: {integrity: sha512-jx6+sE9h/UryaCZhsJWbJtTEy47yXoGNYI4z8ZaRncM0zBKeRqjO2JEcOUYwrYGb1WLhXM1FfMzW3annvFv0rw==} + engines: {node: '>=12.0.0'} + + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-in-the-middle@8.0.1: + resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} + engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} + + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + rimraf@5.0.10: + resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} + hasBin: true + + robust-predicates@3.0.2: + resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rou3@0.7.12: + resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==} + + runed@0.23.4: + resolution: {integrity: sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==} + peerDependencies: + svelte: ^5.7.0 + + runed@0.25.0: + resolution: {integrity: sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg==} + peerDependencies: + svelte: ^5.7.0 + + runed@0.28.0: + resolution: {integrity: sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ==} + peerDependencies: + svelte: ^5.7.0 + + runed@0.29.2: + resolution: {integrity: sha512-0cq6cA6sYGZwl/FvVqjx9YN+1xEBu9sDDyuWdDW1yWX7JF2wmvmVKfH+hVCZs+csW+P3ARH92MjI3H9QTagOQA==} + peerDependencies: + svelte: ^5.7.0 + + runed@0.31.1: + resolution: {integrity: sha512-v3czcTnO+EJjiPvD4dwIqfTdHLZ8oH0zJheKqAHh9QMViY7Qb29UlAMRpX7ZtHh7AFqV60KmfxaJ9QMy+L1igQ==} + peerDependencies: + svelte: ^5.7.0 + + runed@0.35.1: + resolution: {integrity: sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==} + peerDependencies: + '@sveltejs/kit': ^2.21.0 + svelte: ^5.7.0 + peerDependenciesMeta: + '@sveltejs/kit': + optional: true + + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + selderee@0.11.0: + resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + set-cookie-parser@3.0.1: + resolution: {integrity: sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==} + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks@2.8.7: + resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + + superstruct@2.0.2: + resolution: {integrity: sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==} + engines: {node: '>=14.0.0'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + svelte-check@4.4.4: + resolution: {integrity: sha512-F1pGqXc710Oi/wTI4d/x7d6lgPwwfx1U6w3Q35n4xsC2e8C/yN2sM1+mWxjlMcpAfWucjlq4vPi+P4FZ8a14sQ==} + engines: {node: '>= 18.0.0'} + hasBin: true + peerDependencies: + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: '>=5.0.0' + + svelte-sonner@1.0.7: + resolution: {integrity: sha512-1EUFYmd7q/xfs2qCHwJzGPh9n5VJ3X6QjBN10fof2vxgy8fYE7kVfZ7uGnd7i6fQaWIr5KvXcwYXE/cmTEjk5A==} + peerDependencies: + svelte: ^5.0.0 + + svelte-toolbelt@0.10.6: + resolution: {integrity: sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==} + engines: {node: '>=18', pnpm: '>=8.7.0'} + peerDependencies: + svelte: ^5.30.2 + + svelte-toolbelt@0.5.0: + resolution: {integrity: sha512-t3tenZcnfQoIeRuQf/jBU7bvTeT3TGkcEE+1EUr5orp0lR7NEpprflpuie3x9Dn0W9nOKqs3HwKGJeeN5Ok1sQ==} + engines: {node: '>=18', pnpm: '>=8.7.0'} + peerDependencies: + svelte: ^5.0.0-next.126 + + svelte-toolbelt@0.7.1: + resolution: {integrity: sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==} + engines: {node: '>=18', pnpm: '>=8.7.0'} + peerDependencies: + svelte: ^5.0.0 + + svelte-toolbelt@0.9.3: + resolution: {integrity: sha512-HCSWxCtVmv+c6g1ACb8LTwHVbDqLKJvHpo6J8TaqwUme2hj9ATJCpjCPNISR1OCq2Q4U1KT41if9ON0isINQZw==} + engines: {node: '>=18', pnpm: '>=8.7.0'} + peerDependencies: + svelte: ^5.30.2 + + svelte@5.53.6: + resolution: {integrity: sha512-lP5DGF3oDDI9fhHcSpaBiJEkFLuS16h92DhM1L5K1lFm0WjOmUh1i2sNkBBk8rkxJRpob0dBE75jRfUzGZUOGA==} + engines: {node: '>=18'} + + sveltekit-superforms@2.30.0: + resolution: {integrity: sha512-EzXD7sHbi7yBU/eNtzVm6P6axcrVM8BArkbiT96Vdx48s5m4KXte/tbbp3UULtEW8Nk9wt2hYkGeq7nDBwVceg==} + peerDependencies: + '@sveltejs/kit': 1.x || 2.x + svelte: 3.x || 4.x || >=5.0.0-next.51 + + tabbable@6.4.0: + resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} + + tailwind-merge@3.5.0: + resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} + + tailwind-variants@3.2.2: + resolution: {integrity: sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==} + engines: {node: '>=16.x', pnpm: '>=7.x'} + peerDependencies: + tailwind-merge: '>=3.0.0' + tailwindcss: '*' + peerDependenciesMeta: + tailwind-merge: + optional: true + + tailwindcss@4.2.1: + resolution: {integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + + thread-stream@4.0.0: + resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} + engines: {node: '>=20'} + + tiny-case@1.0.3: + resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.0.3: + resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} + engines: {node: '>=14.0.0'} + + tlds@1.261.0: + resolution: {integrity: sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==} + hasBin: true + + toposort@2.0.2: + resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + + ts-algebra@2.0.0: + resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + + ts-deepmerge@7.0.3: + resolution: {integrity: sha512-Du/ZW2RfwV/D4cmA5rXafYjBQVuvu4qGiEEla4EmEHVHgRdx68Gftx7i66jn2bzHPwSVZY36Ae6OuDn9el4ZKA==} + engines: {node: '>=14.13.1'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + turbo-darwin-64@2.8.16: + resolution: {integrity: sha512-KWa4hUMWrpADC6Q/wIHRkBLw6X6MV9nx6X7hSXbTrrMz0KdaKhmfudUZ3sS76bJFmgArBU25cSc0AUyyrswYxg==} + cpu: [x64] + os: [darwin] + + turbo-darwin-arm64@2.8.16: + resolution: {integrity: sha512-NBgaqBDLQSZlJR4D5XCkQq6noaO0RvIgwm5eYFJYL3bH5dNu8o0UBpq7C5DYnQI8+ybyoHFjT5/icN4LeUYLow==} + cpu: [arm64] + os: [darwin] + + turbo-linux-64@2.8.16: + resolution: {integrity: sha512-VYPdcCRevI9kR/hr1H1xwXy7QQt/jNKiim1e1mjANBXD2E9VZWMkIL74J1Huad5MbU3/jw7voHOqDPLJPC2p6w==} + cpu: [x64] + os: [linux] + + turbo-linux-arm64@2.8.16: + resolution: {integrity: sha512-beq8tgUVI3uwkQkXJMiOr/hfxQRw54M3elpBwqgYFfemiK5LhCjjcwO0DkE8GZZfElBIlk+saMAQOZy3885wNQ==} + cpu: [arm64] + os: [linux] + + turbo-windows-64@2.8.16: + resolution: {integrity: sha512-Ig7b46iUgiOIkea/D3Z7H+zNzvzSnIJcLYFpZLA0RxbUTrbLhv9qIPwv3pT9p/abmu0LXVKHxaOo+p26SuDhzw==} + cpu: [x64] + os: [win32] + + turbo-windows-arm64@2.8.16: + resolution: {integrity: sha512-fOWjbEA2PiE2HEnFQrwNZKYEdjewyPc2no9GmrXklZnTCuMsxeCN39aVlKpKpim03Zq/ykIuvApGwq8ZbfS2Yw==} + cpu: [arm64] + os: [win32] + + turbo@2.8.16: + resolution: {integrity: sha512-u6e9e3cTTpE2adQ1DYm3A3r8y3LAONEx1jYvJx6eIgSY4bMLxIxs0riWzI0Z/IK903ikiUzRPZ2c1Ph5lVLkhA==} + hasBin: true + + tw-animate-css@1.4.0: + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + + typebox@1.1.5: + resolution: {integrity: sha512-TBdiM4mSppvWdmRDK5PoocxrMOqGIU9TxmS9zdHH+k8S/+2SIaNlPfMlx3f6hISxma14t2yX7SRySg7+TYYT9w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + unplugin-icons@23.0.1: + resolution: {integrity: sha512-rv0XEJepajKzDLvRUWASM8K+8+/CCfZn2jtogXqg6RIp7kpatRc/aFrVJn8ANQA09e++lPEEv9yX8cC9enc+QQ==} + 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 + peerDependenciesMeta: + '@svgr/core': + optional: true + '@svgx/core': + optional: true + '@vue/compiler-sfc': + optional: true + svelte: + optional: true + + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + valibot@1.2.0: + resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + + validator@13.15.26: + resolution: {integrity: sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==} + engines: {node: '>= 0.10'} + + vaul-svelte@1.0.0-next.7: + resolution: {integrity: sha512-7zN7Bi3dFQixvvbUJY9uGDe7Ws/dGZeBQR2pXdXmzQiakjrxBvWo0QrmsX3HK+VH+SZOltz378cmgmCS9f9rSg==} + engines: {node: '>=18', pnpm: '>=8.7.0'} + peerDependencies: + svelte: ^5.0.0 + + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + 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 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitefu@1.1.2: + resolution: {integrity: sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0 + peerDependenciesMeta: + vite: + optional: true + + vitest@4.0.18: + resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + 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.18 + '@vitest/browser-preview': 4.0.18 + '@vitest/browser-webdriverio': 4.0.18 + '@vitest/ui': 4.0.18 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + winston-transport@4.9.0: + resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} + engines: {node: '>= 12.0.0'} + + winston@3.19.0: + resolution: {integrity: sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==} + engines: {node: '>= 12.0.0'} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yup@1.7.1: + resolution: {integrity: sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==} + + zimmerframe@1.1.4: + resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} + + zod-v3-to-json-schema@4.0.0: + resolution: {integrity: sha512-KixLrhX/uPmRFnDgsZrzrk4x5SSJA+PmaE5adbfID9+3KPJcdxqRobaHU397EfWBqfQircrjKqvEqZ/mW5QH6w==} + peerDependencies: + zod: ^3.25 || ^4.0.14 + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + +snapshots: + + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.6.0 + tinyexec: 1.0.2 + + '@ark/schema@0.56.0': + dependencies: + '@ark/util': 0.56.0 + optional: true + + '@ark/util@0.56.0': + optional: true + + '@babel/runtime@7.28.6': + optional: true + + '@better-auth/core@1.4.20(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)': + dependencies: + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + '@standard-schema/spec': 1.1.0 + better-call: 1.1.8(zod@4.3.6) + jose: 6.1.3 + kysely: 0.28.11 + nanostores: 1.1.1 + zod: 4.3.6 + + '@better-auth/telemetry@1.4.20(@better-auth/core@1.4.20(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))': + dependencies: + '@better-auth/core': 1.4.20(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + + '@better-auth/utils@0.3.0': {} + + '@better-fetch/fetch@1.1.21': {} + + '@colors/colors@1.6.0': {} + + '@dabh/diagnostics@2.0.8': + dependencies: + '@so-ric/colorspace': 1.1.6 + enabled: 2.0.0 + kuler: 2.0.0 + + '@dagrejs/dagre@1.1.8': + dependencies: + '@dagrejs/graphlib': 2.2.4 + + '@dagrejs/graphlib@2.2.4': {} + + '@drizzle-team/brocli@0.10.2': {} + + '@emnapi/runtime@1.8.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild-kit/core-utils@3.3.2': + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + + '@esbuild-kit/esm-loader@2.6.5': + dependencies: + '@esbuild-kit/core-utils': 3.3.2 + get-tsconfig: 4.13.6 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.18.20': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + + '@exodus/schemasafe@1.3.0': + optional: true + + '@floating-ui/core@1.7.4': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.5': + dependencies: + '@floating-ui/core': 1.7.4 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/utils@0.2.10': {} + + '@grpc/grpc-js@1.14.3': + dependencies: + '@grpc/proto-loader': 0.8.0 + '@js-sdsl/ordered-map': 4.4.2 + + '@grpc/proto-loader@0.8.0': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.5.4 + yargs: 17.7.2 + + '@hapi/hoek@9.3.0': + optional: true + + '@hapi/topo@5.1.0': + dependencies: + '@hapi/hoek': 9.3.0 + optional: true + + '@hono/node-server@1.19.9(hono@4.12.8)': + dependencies: + hono: 4.12.8 + + '@iconify/json@2.2.444': + dependencies: + '@iconify/types': 2.0.0 + pathe: 2.0.3 + + '@iconify/types@2.0.0': {} + + '@iconify/utils@3.1.0': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@iconify/types': 2.0.0 + mlly: 1.8.0 + + '@img/colour@1.0.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.8.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@internationalized/date@3.11.0': + dependencies: + '@swc/helpers': 0.5.19 + + '@ioredis/commands@1.5.1': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@js-sdsl/ordered-map@4.4.2': {} + + '@layerstack/svelte-actions@1.0.1-next.14': + dependencies: + '@floating-ui/dom': 1.7.5 + '@layerstack/utils': 2.0.0-next.14 + d3-scale: 4.0.2 + + '@layerstack/svelte-state@0.1.0-next.19': + dependencies: + '@layerstack/utils': 2.0.0-next.14 + + '@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.23 + tailwind-merge: 3.5.0 + + '@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.23 + + '@lucide/svelte@0.561.0(svelte@5.53.6)': + dependencies: + svelte: 5.53.6 + + '@noble/ciphers@2.1.1': {} + + '@noble/hashes@2.0.1': {} + + '@opentelemetry/api-logs@0.212.0': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/api@1.9.0': {} + + '@opentelemetry/auto-instrumentations-node@0.70.1(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-amqplib': 0.59.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-aws-lambda': 0.64.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-aws-sdk': 0.67.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-bunyan': 0.57.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-cassandra-driver': 0.57.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-connect': 0.55.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-cucumber': 0.28.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-dataloader': 0.29.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-dns': 0.55.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-express': 0.60.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-fastify': 0.56.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-fs': 0.31.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-generic-pool': 0.55.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-graphql': 0.60.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-grpc': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-hapi': 0.58.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-http': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-ioredis': 0.60.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-kafkajs': 0.21.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-knex': 0.56.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-koa': 0.60.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-lru-memoizer': 0.56.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-memcached': 0.55.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mongodb': 0.65.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mongoose': 0.58.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mysql': 0.58.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mysql2': 0.58.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-nestjs-core': 0.58.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-net': 0.56.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-openai': 0.10.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-oracledb': 0.37.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-pg': 0.64.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-pino': 0.58.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-redis': 0.60.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-restify': 0.57.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-router': 0.56.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-runtime-node': 0.25.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-socket.io': 0.59.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-tedious': 0.31.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-undici': 0.22.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-winston': 0.56.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-alibaba-cloud': 0.33.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-aws': 2.12.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-azure': 0.20.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-container': 0.8.3(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-gcp': 0.47.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-node': 0.212.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/configuration@0.212.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + yaml: 2.8.2 + + '@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/exporter-logs-otlp-grpc@0.212.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.212.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-logs-otlp-http@0.212.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.212.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.212.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-logs-otlp-proto@0.212.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.212.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-grpc@0.212.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.5.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-http@0.212.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.5.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-proto@0.212.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.5.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-prometheus@0.212.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/exporter-trace-otlp-grpc@0.212.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-http@0.212.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-proto@0.212.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-zipkin@2.5.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/instrumentation-amqplib@0.59.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-aws-lambda@0.64.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + '@types/aws-lambda': 8.10.161 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-aws-sdk@0.67.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-bunyan@0.57.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.212.0 + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@types/bunyan': 1.8.11 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-cassandra-driver@0.57.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-connect@0.55.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + '@types/connect': 3.4.38 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-cucumber@0.28.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-dataloader@0.29.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-dns@0.55.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-express@0.60.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-fastify@0.56.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-fs@0.31.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-generic-pool@0.55.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-graphql@0.60.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-grpc@0.212.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-hapi@0.58.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-http@0.212.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + forwarded-parse: 2.1.2 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-ioredis@0.60.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/redis-common': 0.38.2 + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-kafkajs@0.21.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-knex@0.56.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-koa@0.60.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-lru-memoizer@0.56.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-memcached@0.55.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + '@types/memcached': 2.2.10 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mongodb@0.65.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mongoose@0.58.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mysql2@0.58.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mysql@0.58.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + '@types/mysql': 2.15.27 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-nestjs-core@0.58.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-net@0.56.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-openai@0.10.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.212.0 + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-oracledb@0.37.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + '@types/oracledb': 6.5.2 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-pg@0.64.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.0) + '@types/pg': 8.15.6 + '@types/pg-pool': 2.0.7 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-pino@0.58.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.212.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-redis@0.60.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/redis-common': 0.38.2 + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-restify@0.57.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-router@0.56.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-runtime-node@0.25.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-socket.io@0.59.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-tedious@0.31.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + '@types/tedious': 4.0.14 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-undici@0.22.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-winston@0.56.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.212.0 + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.212.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.212.0 + import-in-the-middle: 2.0.6 + require-in-the-middle: 8.0.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/otlp-exporter-base@0.212.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-grpc-exporter-base@0.212.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-transformer@0.212.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.212.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.1(@opentelemetry/api@1.9.0) + protobufjs: 8.0.0 + + '@opentelemetry/propagator-b3@2.5.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/propagator-jaeger@2.5.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/redis-common@0.38.2': {} + + '@opentelemetry/resource-detector-alibaba-cloud@0.33.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/resource-detector-aws@2.12.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/resource-detector-azure@0.20.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/resource-detector-container@0.8.3(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/resource-detector-gcp@0.47.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + gcp-metadata: 8.1.2 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/resources@2.5.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/sdk-logs@0.212.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.212.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-metrics@2.5.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-node@0.212.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.212.0 + '@opentelemetry/configuration': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/context-async-hooks': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-grpc': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-proto': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-prometheus': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-grpc': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-zipkin': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/sdk-trace-node@2.5.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/semantic-conventions@1.40.0': {} + + '@opentelemetry/sql-common@0.41.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/winston-transport@0.22.0': + dependencies: + '@opentelemetry/api-logs': 0.212.0 + winston-transport: 4.9.0 + + '@otplib/core@13.3.0': {} + + '@otplib/hotp@13.3.0': + dependencies: + '@otplib/core': 13.3.0 + '@otplib/uri': 13.3.0 + + '@otplib/plugin-base32-scure@13.3.0': + dependencies: + '@otplib/core': 13.3.0 + '@scure/base': 2.0.0 + + '@otplib/plugin-crypto-noble@13.3.0': + dependencies: + '@noble/hashes': 2.0.1 + '@otplib/core': 13.3.0 + + '@otplib/totp@13.3.0': + dependencies: + '@otplib/core': 13.3.0 + '@otplib/hotp': 13.3.0 + '@otplib/uri': 13.3.0 + + '@otplib/uri@13.3.0': + dependencies: + '@otplib/core': 13.3.0 + + '@phc/format@1.0.0': {} + + '@pinojs/redact@0.4.0': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@polka/url@1.0.0-next.29': {} + + '@poppinss/macroable@1.1.0': + optional: true + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + + '@rollup/plugin-commonjs@29.0.0(rollup@4.59.0)': + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.59.0) + commondir: 1.0.1 + estree-walker: 2.0.2 + fdir: 6.5.0(picomatch@4.0.3) + is-reference: 1.2.1 + magic-string: 0.30.21 + picomatch: 4.0.3 + optionalDependencies: + rollup: 4.59.0 + + '@rollup/plugin-json@6.1.0(rollup@4.59.0)': + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.59.0) + optionalDependencies: + rollup: 4.59.0 + + '@rollup/plugin-node-resolve@16.0.3(rollup@4.59.0)': + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.59.0) + '@types/resolve': 1.20.2 + deepmerge: 4.3.1 + is-module: 1.0.0 + resolve: 1.22.11 + optionalDependencies: + rollup: 4.59.0 + + '@rollup/pluginutils@5.3.0(rollup@4.59.0)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.3 + optionalDependencies: + rollup: 4.59.0 + + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': + optional: true + + '@scure/base@2.0.0': {} + + '@selderee/plugin-htmlparser2@0.11.0': + dependencies: + domhandler: 5.0.3 + selderee: 0.11.0 + + '@sideway/address@4.1.5': + dependencies: + '@hapi/hoek': 9.3.0 + optional: true + + '@sideway/formula@3.0.1': + optional: true + + '@sideway/pinpoint@2.0.0': + optional: true + + '@so-ric/colorspace@1.1.6': + dependencies: + color: 5.0.3 + text-hex: 1.0.0 + + '@standard-schema/spec@1.1.0': {} + + '@sveltejs/acorn-typescript@1.0.9(acorn@8.16.0)': + dependencies: + acorn: 8.16.0 + + '@sveltejs/adapter-node@5.5.4(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))': + dependencies: + '@rollup/plugin-commonjs': 29.0.0(rollup@4.59.0) + '@rollup/plugin-json': 6.1.0(rollup@4.59.0) + '@rollup/plugin-node-resolve': 16.0.3(rollup@4.59.0) + '@sveltejs/kit': 2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + rollup: 4.59.0 + + '@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@standard-schema/spec': 1.1.0 + '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + '@types/cookie': 0.6.0 + acorn: 8.16.0 + cookie: 0.6.0 + devalue: 5.6.3 + esm-env: 1.2.2 + kleur: 4.1.5 + magic-string: 0.30.21 + mrmime: 2.0.1 + set-cookie-parser: 3.0.1 + sirv: 3.0.2 + svelte: 5.53.6 + vite: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + optionalDependencies: + '@opentelemetry/api': 1.9.0 + typescript: 5.9.3 + + '@sveltejs/vite-plugin-svelte-inspector@5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + obug: 2.1.1 + svelte: 5.53.6 + vite: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + + '@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@sveltejs/vite-plugin-svelte-inspector': 5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + deepmerge: 4.3.1 + magic-string: 0.30.21 + obug: 2.1.1 + svelte: 5.53.6 + vite: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + vitefu: 1.1.2(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + + '@swc/helpers@0.5.19': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/forms@0.5.11(tailwindcss@4.2.1)': + dependencies: + mini-svg-data-uri: 1.4.4 + tailwindcss: 4.2.1 + + '@tailwindcss/node@4.2.1': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.20.0 + jiti: 2.6.1 + lightningcss: 1.31.1 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.1 + + '@tailwindcss/oxide-android-arm64@4.2.1': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.2.1': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.2.1': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.2.1': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.2.1': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.2.1': + optional: true + + '@tailwindcss/oxide@4.2.1': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.1 + '@tailwindcss/oxide-darwin-arm64': 4.2.1 + '@tailwindcss/oxide-darwin-x64': 4.2.1 + '@tailwindcss/oxide-freebsd-x64': 4.2.1 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.1 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.1 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.1 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.1 + '@tailwindcss/oxide-linux-x64-musl': 4.2.1 + '@tailwindcss/oxide-wasm32-wasi': 4.2.1 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.1 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.1 + + '@tailwindcss/typography@0.5.19(tailwindcss@4.2.1)': + dependencies: + postcss-selector-parser: 6.0.10 + tailwindcss: 4.2.1 + + '@tailwindcss/vite@4.2.1(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@tailwindcss/node': 4.2.1 + '@tailwindcss/oxide': 4.2.1 + tailwindcss: 4.2.1 + vite: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + + '@tanstack/table-core@8.21.3': {} + + '@types/aws-lambda@8.10.161': {} + + '@types/bun@1.3.9': + dependencies: + bun-types: 1.3.9 + + '@types/bunyan@1.8.11': + dependencies: + '@types/node': 25.5.0 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 25.5.0 + + '@types/cookie@0.6.0': {} + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + + '@types/mailparser@3.4.6': + dependencies: + '@types/node': 25.5.0 + iconv-lite: 0.6.3 + + '@types/memcached@2.2.10': + dependencies: + '@types/node': 25.5.0 + + '@types/mysql@2.15.27': + dependencies: + '@types/node': 25.5.0 + + '@types/node@25.5.0': + dependencies: + undici-types: 7.18.2 + + '@types/oracledb@6.5.2': + dependencies: + '@types/node': 25.5.0 + + '@types/pdfkit@0.14.0': + dependencies: + '@types/node': 25.5.0 + + '@types/pg-pool@2.0.7': + dependencies: + '@types/pg': 8.16.0 + + '@types/pg@8.15.6': + dependencies: + '@types/node': 25.5.0 + pg-protocol: 1.12.0 + pg-types: 2.2.0 + + '@types/pg@8.16.0': + dependencies: + '@types/node': 25.5.0 + pg-protocol: 1.12.0 + pg-types: 2.2.0 + + '@types/qrcode@1.5.6': + dependencies: + '@types/node': 25.5.0 + + '@types/resolve@1.20.2': {} + + '@types/tedious@4.0.14': + dependencies: + '@types/node': 25.5.0 + + '@types/tmp@0.2.6': {} + + '@types/triple-beam@1.3.5': {} + + '@types/trusted-types@2.0.7': {} + + '@types/uuid@10.0.0': {} + + '@types/validator@13.15.10': + optional: true + + '@typeschema/class-validator@0.3.0(class-validator@0.14.4)': + dependencies: + '@typeschema/core': 0.14.0 + optionalDependencies: + class-validator: 0.14.4 + transitivePeerDependencies: + - '@types/json-schema' + optional: true + + '@typeschema/core@0.14.0': + optional: true + + '@valibot/to-json-schema@1.5.0(valibot@1.2.0(typescript@5.9.3))': + dependencies: + valibot: 1.2.0(typescript@5.9.3) + optional: true + + '@vinejs/compiler@3.0.0': + optional: true + + '@vinejs/vine@3.0.1': + dependencies: + '@poppinss/macroable': 1.1.0 + '@types/validator': 13.15.10 + '@vinejs/compiler': 3.0.0 + camelcase: 8.0.0 + dayjs: 1.11.19 + dlv: 1.1.3 + normalize-url: 8.1.1 + validator: 13.15.26 + optional: true + + '@vitest/expect@4.0.18': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.0.18 + '@vitest/utils': 4.0.18 + chai: 6.2.2 + tinyrainbow: 3.0.3 + + '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@vitest/spy': 4.0.18 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + + '@vitest/pretty-format@4.0.18': + dependencies: + tinyrainbow: 3.0.3 + + '@vitest/runner@4.0.18': + dependencies: + '@vitest/utils': 4.0.18 + pathe: 2.0.3 + + '@vitest/snapshot@4.0.18': + dependencies: + '@vitest/pretty-format': 4.0.18 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.0.18': {} + + '@vitest/utils@4.0.18': + dependencies: + '@vitest/pretty-format': 4.0.18 + tinyrainbow: 3.0.3 + + '@zone-eu/mailsplit@5.4.8': + dependencies: + libbase64: 1.3.0 + libmime: 5.3.7 + libqp: 2.1.1 + + acorn-import-attributes@1.9.5(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + agent-base@7.1.4: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + argon2@0.43.1: + dependencies: + '@phc/format': 1.0.0 + node-addon-api: 8.6.0 + node-gyp-build: 4.8.4 + + aria-query@5.3.1: {} + + arkregex@0.0.5: + dependencies: + '@ark/util': 0.56.0 + optional: true + + arktype@2.1.29: + dependencies: + '@ark/schema': 0.56.0 + '@ark/util': 0.56.0 + arkregex: 0.0.5 + optional: true + + assertion-error@2.0.1: {} + + async@3.2.6: {} + + atomic-sleep@1.0.0: {} + + axobject-query@4.1.0: {} + + balanced-match@1.0.2: {} + + better-auth@1.4.20(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.9)(kysely@0.28.11)(postgres@3.4.8))(svelte@5.53.6)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)): + dependencies: + '@better-auth/core': 1.4.20(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/telemetry': 1.4.20(@better-auth/core@1.4.20(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)) + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + '@noble/ciphers': 2.1.1 + '@noble/hashes': 2.0.1 + better-call: 1.1.8(zod@4.3.6) + defu: 6.1.4 + jose: 6.1.3 + kysely: 0.28.11 + nanostores: 1.1.1 + zod: 4.3.6 + optionalDependencies: + '@sveltejs/kit': 2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + drizzle-kit: 0.31.9 + drizzle-orm: 0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.9)(kysely@0.28.11)(postgres@3.4.8) + svelte: 5.53.6 + vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + + better-call@1.1.8(zod@4.3.6): + dependencies: + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + rou3: 0.7.12 + set-cookie-parser: 2.7.2 + optionalDependencies: + zod: 4.3.6 + + bignumber.js@9.3.1: {} + + bits-ui@2.16.2(@internationalized/date@3.11.0)(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6): + dependencies: + '@floating-ui/core': 1.7.4 + '@floating-ui/dom': 1.7.5 + '@internationalized/date': 3.11.0 + esm-env: 1.2.2 + runed: 0.35.1(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6) + svelte: 5.53.6 + svelte-toolbelt: 0.10.6(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6) + tabbable: 6.4.0 + transitivePeerDependencies: + - '@sveltejs/kit' + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + buffer-from@1.1.2: {} + + bun-types@1.3.9: + dependencies: + '@types/node': 25.5.0 + + camelcase@5.3.1: {} + + camelcase@8.0.0: + optional: true + + chai@6.2.2: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + cjs-module-lexer@2.2.0: {} + + class-validator@0.14.4: + dependencies: + '@types/validator': 13.15.10 + libphonenumber-js: 1.12.38 + validator: 13.15.26 + optional: true + + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clsx@2.1.1: {} + + cluster-key-slot@1.1.2: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-convert@3.1.3: + dependencies: + color-name: 2.1.0 + + color-name@1.1.4: {} + + color-name@2.1.0: {} + + color-string@2.1.4: + dependencies: + color-name: 2.1.0 + + color@5.0.3: + dependencies: + color-convert: 3.1.3 + color-string: 2.1.4 + + commander@7.2.0: {} + + commondir@1.0.1: {} + + confbox@0.1.8: {} + + confbox@0.2.4: {} + + cookie@0.6.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + d3-array@2.12.1: + dependencies: + internmap: 1.0.1 + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.0.1 + + d3-dispatch@3.0.1: {} + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.2: {} + + d3-geo-voronoi@2.1.0: + dependencies: + d3-array: 3.2.4 + d3-delaunay: 6.0.4 + d3-geo: 3.1.1 + d3-tricontour: 1.1.0 + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate-path@2.3.0: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@1.0.9: {} + + d3-path@3.1.0: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-sankey@0.12.3: + dependencies: + d3-array: 2.12.1 + d3-shape: 1.3.7 + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@1.3.7: + dependencies: + d3-path: 1.0.9 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-tile@1.0.0: {} + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-tricontour@1.1.0: + dependencies: + d3-delaunay: 6.0.4 + d3-scale: 4.0.2 + + data-uri-to-buffer@4.0.1: {} + + date-fns-tz@3.2.0(date-fns@4.1.0): + dependencies: + date-fns: 4.1.0 + + date-fns@4.1.0: {} + + dayjs@1.11.19: + optional: true + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decamelize@1.2.0: {} + + deepmerge@4.3.1: {} + + defu@6.1.4: {} + + delaunator@5.0.1: + dependencies: + robust-predicates: 3.0.2 + + denque@2.1.0: {} + + dequal@2.0.3: {} + + detect-libc@2.1.2: {} + + devalue@5.6.3: {} + + dijkstrajs@1.0.3: {} + + dlv@1.1.3: + optional: true + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dotenv@16.6.1: {} + + dotenv@17.3.1: {} + + drizzle-kit@0.31.9: + dependencies: + '@drizzle-team/brocli': 0.10.2 + '@esbuild-kit/esm-loader': 2.6.5 + esbuild: 0.25.12 + esbuild-register: 3.6.0(esbuild@0.25.12) + transitivePeerDependencies: + - supports-color + + drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.9)(kysely@0.28.11)(postgres@3.4.8): + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/pg': 8.16.0 + bun-types: 1.3.9 + kysely: 0.28.11 + postgres: 3.4.8 + + eastasianwidth@0.2.0: {} + + effect@3.19.19: + dependencies: + '@standard-schema/spec': 1.1.0 + fast-check: 3.23.2 + optional: true + + embla-carousel-reactive-utils@8.6.0(embla-carousel@8.6.0): + dependencies: + embla-carousel: 8.6.0 + + embla-carousel-svelte@8.6.0(svelte@5.53.6): + dependencies: + embla-carousel: 8.6.0 + embla-carousel-reactive-utils: 8.6.0(embla-carousel@8.6.0) + svelte: 5.53.6 + + embla-carousel@8.6.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + enabled@2.0.0: {} + + encoding-japanese@2.2.0: {} + + enhanced-resolve@5.20.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + entities@4.5.0: {} + + es-module-lexer@1.7.0: {} + + esbuild-register@3.6.0(esbuild@0.25.12): + dependencies: + debug: 4.4.3 + esbuild: 0.25.12 + transitivePeerDependencies: + - supports-color + + 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 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + + escalade@3.2.0: {} + + esm-env@1.2.2: {} + + esrap@2.2.3: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + expect-type@1.3.0: {} + + exsolve@1.0.8: {} + + extend@3.0.2: {} + + fast-check@3.23.2: + dependencies: + pure-rand: 6.1.0 + optional: true + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fecha@4.2.3: {} + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + fn.name@1.1.0: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + formsnap@2.0.1(svelte@5.53.6)(sveltekit-superforms@2.30.0(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)): + dependencies: + svelte: 5.53.6 + svelte-toolbelt: 0.5.0(svelte@5.53.6) + sveltekit-superforms: 2.30.0(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3) + + forwarded-parse@2.1.2: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gaxios@7.1.3: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + rimraf: 5.0.10 + transitivePeerDependencies: + - supports-color + + gcp-metadata@8.1.2: + dependencies: + gaxios: 7.1.3 + google-logging-utils: 1.1.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + + get-caller-file@2.0.5: {} + + get-tsconfig@4.13.6: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + google-logging-utils@1.1.3: {} + + graceful-fs@4.2.11: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + he@1.2.0: {} + + hono@4.12.8: {} + + 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 + + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + imapflow@1.2.10: + dependencies: + '@zone-eu/mailsplit': 5.4.8 + encoding-japanese: 2.2.0 + iconv-lite: 0.7.2 + libbase64: 1.3.0 + libmime: 5.3.7 + libqp: 2.1.1 + nodemailer: 8.0.1 + pino: 10.3.1 + socks: 2.8.7 + + import-in-the-middle@2.0.6: + dependencies: + acorn: 8.16.0 + acorn-import-attributes: 1.9.5(acorn@8.16.0) + cjs-module-lexer: 2.2.0 + module-details-from-path: 1.0.4 + + import-in-the-middle@3.0.0: + dependencies: + acorn: 8.16.0 + acorn-import-attributes: 1.9.5(acorn@8.16.0) + cjs-module-lexer: 2.2.0 + module-details-from-path: 1.0.4 + + inherits@2.0.4: {} + + inline-style-parser@0.2.7: {} + + internmap@1.0.1: {} + + internmap@2.0.3: {} + + ioredis@5.10.0: + dependencies: + '@ioredis/commands': 1.5.1 + cluster-key-slot: 1.1.2 + debug: 4.4.3 + 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 + transitivePeerDependencies: + - supports-color + + ip-address@10.1.0: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-fullwidth-code-point@3.0.0: {} + + is-module@1.0.0: {} + + is-reference@1.2.1: + dependencies: + '@types/estree': 1.0.8 + + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + is-stream@2.0.1: {} + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jiti@2.6.1: {} + + 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 + optional: true + + jose@6.1.3: {} + + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + + json-schema-to-ts@3.1.1: + dependencies: + '@babel/runtime': 7.28.6 + ts-algebra: 2.0.0 + optional: true + + kleur@4.1.5: {} + + kuler@2.0.0: {} + + kysely@0.28.11: {} + + layerchart@2.0.0-next.43(svelte@5.53.6): + dependencies: + '@dagrejs/dagre': 1.1.8 + '@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.23 + memoize: 10.2.0 + runed: 0.31.1(svelte@5.53.6) + svelte: 5.53.6 + + leac@0.6.0: {} + + libbase64@1.3.0: {} + + libmime@5.3.7: + dependencies: + encoding-japanese: 2.2.0 + iconv-lite: 0.6.3 + libbase64: 1.3.0 + libqp: 2.1.1 + + libphonenumber-js@1.12.38: + optional: true + + libqp@2.1.1: {} + + lightningcss-android-arm64@1.31.1: + optional: true + + lightningcss-darwin-arm64@1.31.1: + optional: true + + lightningcss-darwin-x64@1.31.1: + optional: true + + lightningcss-freebsd-x64@1.31.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.31.1: + optional: true + + lightningcss-linux-arm64-gnu@1.31.1: + optional: true + + lightningcss-linux-arm64-musl@1.31.1: + optional: true + + lightningcss-linux-x64-gnu@1.31.1: + optional: true + + lightningcss-linux-x64-musl@1.31.1: + optional: true + + lightningcss-win32-arm64-msvc@1.31.1: + optional: true + + lightningcss-win32-x64-msvc@1.31.1: + optional: true + + lightningcss@1.31.1: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.31.1 + lightningcss-darwin-arm64: 1.31.1 + lightningcss-darwin-x64: 1.31.1 + lightningcss-freebsd-x64: 1.31.1 + lightningcss-linux-arm-gnueabihf: 1.31.1 + lightningcss-linux-arm64-gnu: 1.31.1 + lightningcss-linux-arm64-musl: 1.31.1 + lightningcss-linux-x64-gnu: 1.31.1 + lightningcss-linux-x64-musl: 1.31.1 + lightningcss-win32-arm64-msvc: 1.31.1 + lightningcss-win32-x64-msvc: 1.31.1 + + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + + local-pkg@1.1.2: + dependencies: + mlly: 1.8.0 + pkg-types: 2.3.0 + quansync: 0.2.11 + + locate-character@3.0.0: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + lodash-es@4.17.23: {} + + lodash.camelcase@4.3.0: {} + + lodash.defaults@4.2.0: {} + + lodash.isarguments@3.1.0: {} + + logform@2.7.0: + dependencies: + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.5.0 + triple-beam: 1.4.1 + + long@5.3.2: {} + + lru-cache@10.4.3: {} + + lz-string@1.5.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + mailparser@3.9.3: + 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.2 + libmime: 5.3.7 + linkify-it: 5.0.0 + nodemailer: 7.0.13 + punycode.js: 2.3.1 + tlds: 1.261.0 + + memoize-weak@1.0.2: {} + + memoize@10.2.0: + dependencies: + mimic-function: 5.0.1 + + mimic-function@5.0.1: {} + + mini-svg-data-uri@1.4.4: {} + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.0.2 + + minipass@7.1.3: {} + + mlly@1.8.0: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + + mode-watcher@1.1.0(svelte@5.53.6): + dependencies: + runed: 0.25.0(svelte@5.53.6) + svelte: 5.53.6 + svelte-toolbelt: 0.7.1(svelte@5.53.6) + + module-details-from-path@1.0.4: {} + + mri@1.2.0: {} + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + nanoid@5.1.6: {} + + nanostores@1.1.1: {} + + neverthrow@8.2.0: + optionalDependencies: + '@rollup/rollup-linux-x64-gnu': 4.59.0 + + node-addon-api@8.6.0: {} + + node-domexception@1.0.0: {} + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + node-gyp-build@4.8.4: {} + + nodemailer@7.0.13: {} + + nodemailer@8.0.1: {} + + normalize-url@8.1.1: + optional: true + + obug@2.1.1: {} + + on-exit-leak-free@2.1.2: {} + + one-time@1.0.0: + dependencies: + fn.name: 1.1.0 + + otplib@13.3.0: + dependencies: + '@otplib/core': 13.3.0 + '@otplib/hotp': 13.3.0 + '@otplib/plugin-base32-scure': 13.3.0 + '@otplib/plugin-crypto-noble': 13.3.0 + '@otplib/totp': 13.3.0 + '@otplib/uri': 13.3.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-try@2.2.0: {} + + package-json-from-dist@1.0.1: {} + + package-manager-detector@1.6.0: {} + + paneforge@1.0.2(svelte@5.53.6): + dependencies: + runed: 0.23.4(svelte@5.53.6) + svelte: 5.53.6 + svelte-toolbelt: 0.9.3(svelte@5.53.6) + + parseley@0.12.1: + dependencies: + leac: 0.6.0 + peberminta: 0.9.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + pathe@2.0.3: {} + + peberminta@0.9.0: {} + + pg-int8@1.0.1: {} + + pg-protocol@1.12.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.1.0: {} + + pino@10.3.1: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 4.0.0 + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + + pkg-types@2.3.0: + dependencies: + confbox: 0.2.4 + exsolve: 1.0.8 + pathe: 2.0.3 + + pngjs@5.0.0: {} + + postcss-selector-parser@6.0.10: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + postgres@3.4.8: {} + + prettier-plugin-sort-imports@1.8.11(typescript@5.9.3): + dependencies: + prettier: 3.8.1 + typescript: 5.9.3 + + prettier-plugin-svelte@3.5.0(prettier@3.8.1)(svelte@5.53.6): + dependencies: + prettier: 3.8.1 + svelte: 5.53.6 + + prettier-plugin-tailwindcss@0.7.2(prettier-plugin-sort-imports@1.8.11(typescript@5.9.3))(prettier-plugin-svelte@3.5.0(prettier@3.8.1)(svelte@5.53.6))(prettier@3.8.1): + dependencies: + prettier: 3.8.1 + optionalDependencies: + prettier-plugin-sort-imports: 1.8.11(typescript@5.9.3) + prettier-plugin-svelte: 3.5.0(prettier@3.8.1)(svelte@5.53.6) + + prettier@3.8.1: {} + + process-warning@5.0.0: {} + + property-expr@2.0.6: + optional: true + + protobufjs@7.5.4: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 25.5.0 + long: 5.3.2 + + protobufjs@8.0.0: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 25.5.0 + long: 5.3.2 + + punycode.js@2.3.1: {} + + pure-rand@6.1.0: + optional: true + + qrcode@1.5.4: + dependencies: + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + + quansync@0.2.11: {} + + quick-format-unescaped@4.0.4: {} + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@4.1.2: {} + + real-require@0.2.0: {} + + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + + require-directory@2.1.1: {} + + require-in-the-middle@8.0.1: + dependencies: + debug: 4.4.3 + module-details-from-path: 1.0.4 + transitivePeerDependencies: + - supports-color + + require-main-filename@2.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + rimraf@5.0.10: + dependencies: + glob: 10.5.0 + + robust-predicates@3.0.2: {} + + rollup@4.59.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 + fsevents: 2.3.3 + + rou3@0.7.12: {} + + runed@0.23.4(svelte@5.53.6): + dependencies: + esm-env: 1.2.2 + svelte: 5.53.6 + + runed@0.25.0(svelte@5.53.6): + dependencies: + esm-env: 1.2.2 + svelte: 5.53.6 + + runed@0.28.0(svelte@5.53.6): + dependencies: + esm-env: 1.2.2 + svelte: 5.53.6 + + runed@0.29.2(svelte@5.53.6): + dependencies: + esm-env: 1.2.2 + svelte: 5.53.6 + + runed@0.31.1(svelte@5.53.6): + dependencies: + esm-env: 1.2.2 + svelte: 5.53.6 + + runed@0.35.1(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6): + dependencies: + dequal: 2.0.3 + esm-env: 1.2.2 + lz-string: 1.5.0 + svelte: 5.53.6 + optionalDependencies: + '@sveltejs/kit': 2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + + rw@1.3.3: {} + + sade@1.8.1: + dependencies: + mri: 1.2.0 + + safe-buffer@5.2.1: {} + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + selderee@0.11.0: + dependencies: + parseley: 0.12.1 + + semver@7.7.4: {} + + set-blocking@2.0.0: {} + + set-cookie-parser@2.7.2: {} + + set-cookie-parser@3.0.1: {} + + sharp@0.34.5: + dependencies: + '@img/colour': 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.4 + 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 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + smart-buffer@4.2.0: {} + + socks@2.8.7: + dependencies: + ip-address: 10.1.0 + smart-buffer: 4.2.0 + + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + split2@4.2.0: {} + + stack-trace@0.0.10: {} + + stackback@0.0.2: {} + + standard-as-callback@2.1.0: {} + + std-env@3.10.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + + superstruct@2.0.2: + optional: true + + supports-preserve-symlinks-flag@1.0.0: {} + + svelte-check@4.4.4(picomatch@4.0.3)(svelte@5.53.6)(typescript@5.9.3): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + chokidar: 4.0.3 + fdir: 6.5.0(picomatch@4.0.3) + picocolors: 1.1.1 + sade: 1.8.1 + svelte: 5.53.6 + typescript: 5.9.3 + transitivePeerDependencies: + - picomatch + + svelte-sonner@1.0.7(svelte@5.53.6): + dependencies: + runed: 0.28.0(svelte@5.53.6) + svelte: 5.53.6 + + svelte-toolbelt@0.10.6(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6): + dependencies: + clsx: 2.1.1 + runed: 0.35.1(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6) + style-to-object: 1.0.14 + svelte: 5.53.6 + transitivePeerDependencies: + - '@sveltejs/kit' + + svelte-toolbelt@0.5.0(svelte@5.53.6): + dependencies: + clsx: 2.1.1 + style-to-object: 1.0.14 + svelte: 5.53.6 + + svelte-toolbelt@0.7.1(svelte@5.53.6): + dependencies: + clsx: 2.1.1 + runed: 0.23.4(svelte@5.53.6) + style-to-object: 1.0.14 + svelte: 5.53.6 + + svelte-toolbelt@0.9.3(svelte@5.53.6): + dependencies: + clsx: 2.1.1 + runed: 0.29.2(svelte@5.53.6) + style-to-object: 1.0.14 + svelte: 5.53.6 + + svelte@5.53.6: + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.5.5 + '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) + '@types/estree': 1.0.8 + '@types/trusted-types': 2.0.7 + acorn: 8.16.0 + aria-query: 5.3.1 + axobject-query: 4.1.0 + clsx: 2.1.1 + devalue: 5.6.3 + esm-env: 1.2.2 + esrap: 2.2.3 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.21 + zimmerframe: 1.1.4 + + sveltekit-superforms@2.30.0(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3): + dependencies: + '@sveltejs/kit': 2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + devalue: 5.6.3 + memoize-weak: 1.0.2 + svelte: 5.53.6 + ts-deepmerge: 7.0.3 + optionalDependencies: + '@exodus/schemasafe': 1.3.0 + '@standard-schema/spec': 1.1.0 + '@typeschema/class-validator': 0.3.0(class-validator@0.14.4) + '@valibot/to-json-schema': 1.5.0(valibot@1.2.0(typescript@5.9.3)) + '@vinejs/vine': 3.0.1 + arktype: 2.1.29 + class-validator: 0.14.4 + effect: 3.19.19 + joi: 17.13.3 + json-schema-to-ts: 3.1.1 + superstruct: 2.0.2 + typebox: 1.1.5 + valibot: 1.2.0(typescript@5.9.3) + yup: 1.7.1 + zod: 4.3.6 + zod-v3-to-json-schema: 4.0.0(zod@4.3.6) + transitivePeerDependencies: + - '@types/json-schema' + - typescript + + tabbable@6.4.0: {} + + tailwind-merge@3.5.0: {} + + tailwind-variants@3.2.2(tailwind-merge@3.5.0)(tailwindcss@4.2.1): + dependencies: + tailwindcss: 4.2.1 + optionalDependencies: + tailwind-merge: 3.5.0 + + tailwindcss@4.2.1: {} + + tapable@2.3.0: {} + + text-hex@1.0.0: {} + + thread-stream@4.0.0: + dependencies: + real-require: 0.2.0 + + tiny-case@1.0.3: + optional: true + + tinybench@2.9.0: {} + + tinyexec@1.0.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinyrainbow@3.0.3: {} + + tlds@1.261.0: {} + + toposort@2.0.2: + optional: true + + totalist@3.0.1: {} + + triple-beam@1.4.1: {} + + ts-algebra@2.0.0: + optional: true + + ts-deepmerge@7.0.3: {} + + tslib@2.8.1: {} + + tsx@4.21.0: + dependencies: + esbuild: 0.27.3 + get-tsconfig: 4.13.6 + optionalDependencies: + fsevents: 2.3.3 + + turbo-darwin-64@2.8.16: + optional: true + + turbo-darwin-arm64@2.8.16: + optional: true + + turbo-linux-64@2.8.16: + optional: true + + turbo-linux-arm64@2.8.16: + optional: true + + turbo-windows-64@2.8.16: + optional: true + + turbo-windows-arm64@2.8.16: + optional: true + + turbo@2.8.16: + optionalDependencies: + turbo-darwin-64: 2.8.16 + turbo-darwin-arm64: 2.8.16 + turbo-linux-64: 2.8.16 + turbo-linux-arm64: 2.8.16 + turbo-windows-64: 2.8.16 + turbo-windows-arm64: 2.8.16 + + tw-animate-css@1.4.0: {} + + type-fest@2.19.0: + optional: true + + typebox@1.1.5: + optional: true + + typescript@5.9.3: {} + + uc.micro@2.1.0: {} + + ufo@1.6.3: {} + + undici-types@7.18.2: {} + + unplugin-icons@23.0.1(svelte@5.53.6): + 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 + optionalDependencies: + svelte: 5.53.6 + + unplugin@2.3.11: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.16.0 + picomatch: 4.0.3 + webpack-virtual-modules: 0.6.2 + + util-deprecate@1.0.2: {} + + uuid@11.1.0: {} + + valibot@1.2.0(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + + validator@13.15.26: + optional: true + + vaul-svelte@1.0.0-next.7(svelte@5.53.6): + dependencies: + runed: 0.23.4(svelte@5.53.6) + svelte: 5.53.6 + svelte-toolbelt: 0.7.1(svelte@5.53.6) + + vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2): + dependencies: + esbuild: 0.27.3 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.59.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 25.5.0 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.31.1 + tsx: 4.21.0 + yaml: 2.8.2 + + vitefu@1.1.2(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)): + optionalDependencies: + vite: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + + vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2): + dependencies: + '@vitest/expect': 4.0.18 + '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/pretty-format': 4.0.18 + '@vitest/runner': 4.0.18 + '@vitest/snapshot': 4.0.18 + '@vitest/spy': 4.0.18 + '@vitest/utils': 4.0.18 + es-module-lexer: 1.7.0 + expect-type: 1.3.0 + 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: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/node': 25.5.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + + web-streams-polyfill@3.3.3: {} + + webpack-virtual-modules@0.6.2: {} + + which-module@2.0.1: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + winston-transport@4.9.0: + dependencies: + logform: 2.7.0 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + + winston@3.19.0: + dependencies: + '@colors/colors': 1.6.0 + '@dabh/diagnostics': 2.0.8 + async: 3.2.6 + is-stream: 2.0.1 + logform: 2.7.0 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.5.0 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.9.0 + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + xtend@4.0.2: {} + + y18n@4.0.3: {} + + y18n@5.0.8: {} + + yaml@2.8.2: {} + + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs-parser@21.1.1: {} + + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yup@1.7.1: + dependencies: + property-expr: 2.0.6 + tiny-case: 1.0.3 + toposort: 2.0.2 + type-fest: 2.19.0 + optional: true + + zimmerframe@1.1.4: {} + + zod-v3-to-json-schema@4.0.0(zod@4.3.6): + dependencies: + zod: 4.3.6 + optional: true + + zod@4.3.6: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..37408aa --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,9 @@ +packages: + - apps/* + - packages/* + +onlyBuiltDependencies: + - argon2 + - esbuild + - protobufjs + - sharp diff --git a/scripts/generate_example_env_file.py b/scripts/generate_example_env_file.py new file mode 100755 index 0000000..49bf0ee --- /dev/null +++ b/scripts/generate_example_env_file.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import re +from pathlib import Path + +ENV_ASSIGNMENT_RE = re.compile( + r"^(\s*(?:export\s+)?)([A-Za-z_][A-Za-z0-9_]*)(\s*=\s*)(.*)$" +) + + +def split_inline_comment(rhs: str) -> tuple[str, str]: + in_single = False + in_double = False + escaped = False + + for i, char in enumerate(rhs): + if escaped: + escaped = False + continue + + if char == "\\": + escaped = True + continue + + if char == "'" and not in_double: + in_single = not in_single + continue + + if char == '"' and not in_single: + in_double = not in_double + continue + + if char == "#" and not in_single and not in_double: + if i == 0 or rhs[i - 1].isspace(): + return rhs[:i], rhs[i:] + + return rhs, "" + + +def transform_line(line: str) -> str: + stripped = line.strip() + if stripped == "" or stripped.startswith("#"): + return line + + newline = "" + raw = line + if line.endswith("\r\n"): + newline = "\r\n" + raw = line[:-2] + elif line.endswith("\n"): + newline = "\n" + raw = line[:-1] + + match = ENV_ASSIGNMENT_RE.match(raw) + if not match: + return line + + prefix, key, delimiter, rhs = match.groups() + value_part, comment_part = split_inline_comment(rhs) + trailing_ws = value_part[len(value_part.rstrip()) :] + placeholder = f"${{{{project.{key}}}}}" + + return f"{prefix}{key}{delimiter}{placeholder}{trailing_ws}{comment_part}{newline}" + + +def generate_example_env(source_path: Path, target_path: Path) -> None: + lines = source_path.read_text(encoding="utf-8").splitlines(keepends=True) + transformed = [transform_line(line) for line in lines] + target_path.write_text("".join(transformed), encoding="utf-8") + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Generate .env.example using ${{project.KEY}} placeholders." + ) + parser.add_argument("--source", default=".env", help="Path to source .env file") + parser.add_argument( + "--target", default=".env.example", help="Path to output .env.example file" + ) + args = parser.parse_args() + + source_path = Path(args.source) + target_path = Path(args.target) + + if not source_path.exists(): + raise FileNotFoundError(f"Source env file not found: {source_path}") + + generate_example_env(source_path, target_path) + + +if __name__ == "__main__": + main() diff --git a/scripts/migrate.sh b/scripts/migrate.sh new file mode 100755 index 0000000..2b020c3 --- /dev/null +++ b/scripts/migrate.sh @@ -0,0 +1,7 @@ +echo "🔄 Migrating Primary DB" + +cd packages/db + +pnpm 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..1504095 --- /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 + +pnpm run prod diff --git a/scripts/spinup.dev.env.sh b/scripts/spinup.dev.env.sh new file mode 100755 index 0000000..a38f08a --- /dev/null +++ b/scripts/spinup.dev.env.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +docker compose -f dev/docker-compose.dev.yaml up --wait -d diff --git a/scripts/teardown.dev.env.sh b/scripts/teardown.dev.env.sh new file mode 100755 index 0000000..b70cd08 --- /dev/null +++ b/scripts/teardown.dev.env.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +docker compose -f dev/docker-compose.dev.yaml down --remove-orphans 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..5d9295f --- /dev/null +++ b/turbo.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://v2-8-12.turborepo.dev/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 + } + } +}