kaneo (minimalist kanban) fork to experiment adding a tangled integration github.com/usekaneo/kaneo
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge origin/main into feat/add-user-notifications-integrations

Tin 3a574443 47224b39

+3575 -421
+113 -8
.github/workflows/ci.yml
··· 1 - name: Code quality 1 + name: CI 2 2 3 3 on: 4 4 push: 5 + pull_request: 6 + 7 + permissions: 8 + contents: read 5 9 6 10 jobs: 7 - quality: 11 + lint: 8 12 runs-on: ubuntu-24.04 9 13 steps: 10 - - name: Checkout 11 - uses: actions/checkout@v6 12 - - name: Setup Biome 13 - uses: biomejs/setup-biome@v2 14 + - name: Checkout repository 15 + uses: actions/checkout@v4 16 + 17 + - name: Setup pnpm 18 + uses: pnpm/action-setup@v4 19 + with: 20 + version: 10.32.1 21 + 22 + - name: Setup Node.js 23 + uses: actions/setup-node@v4 14 24 with: 15 - version: 2.4.8 25 + node-version: 20.19.0 26 + cache: pnpm 27 + 28 + - name: Install dependencies 29 + run: pnpm install --frozen-lockfile 30 + 16 31 - name: Run Biome 17 - run: biome ci . 32 + run: pnpm exec biome ci . 33 + 34 + unit: 35 + runs-on: ubuntu-24.04 36 + steps: 37 + - name: Checkout repository 38 + uses: actions/checkout@v4 39 + 40 + - name: Setup pnpm 41 + uses: pnpm/action-setup@v4 42 + with: 43 + version: 10.32.1 44 + 45 + - name: Setup Node.js 46 + uses: actions/setup-node@v4 47 + with: 48 + node-version: 20.19.0 49 + cache: pnpm 50 + 51 + - name: Install dependencies 52 + run: pnpm install --frozen-lockfile 53 + 54 + - name: Run unit tests 55 + run: pnpm test 56 + 57 + build: 58 + runs-on: ubuntu-24.04 59 + steps: 60 + - name: Checkout repository 61 + uses: actions/checkout@v4 62 + 63 + - name: Setup pnpm 64 + uses: pnpm/action-setup@v4 65 + with: 66 + version: 10.32.1 67 + 68 + - name: Setup Node.js 69 + uses: actions/setup-node@v4 70 + with: 71 + node-version: 20.19.0 72 + cache: pnpm 73 + 74 + - name: Install dependencies 75 + run: pnpm install --frozen-lockfile 76 + 77 + - name: Build monorepo 78 + run: pnpm build 79 + 80 + integration: 81 + runs-on: ubuntu-24.04 82 + services: 83 + postgres: 84 + image: postgres:16 85 + env: 86 + POSTGRES_DB: kaneo_test 87 + POSTGRES_USER: postgres 88 + POSTGRES_PASSWORD: postgres 89 + ports: 90 + - 5432:5432 91 + options: >- 92 + --health-cmd "pg_isready -U postgres -d kaneo_test" 93 + --health-interval 10s 94 + --health-timeout 5s 95 + --health-retries 5 96 + env: 97 + NODE_ENV: test 98 + AUTH_SECRET: test-secret-with-at-least-32-chars 99 + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/kaneo_test 100 + KANEO_API_URL: http://localhost:1337 101 + KANEO_CLIENT_URL: http://localhost:5173 102 + DISABLE_GUEST_ACCESS: "false" 103 + steps: 104 + - name: Checkout repository 105 + uses: actions/checkout@v4 106 + 107 + - name: Setup pnpm 108 + uses: pnpm/action-setup@v4 109 + with: 110 + version: 10.32.1 111 + 112 + - name: Setup Node.js 113 + uses: actions/setup-node@v4 114 + with: 115 + node-version: 20.19.0 116 + cache: pnpm 117 + 118 + - name: Install dependencies 119 + run: pnpm install --frozen-lockfile 120 + 121 + - name: Run API integration tests 122 + run: pnpm test:integration
+1 -1
.github/workflows/deploy-site.yml
··· 34 34 - name: Setup Node 35 35 uses: actions/setup-node@v4 36 36 with: 37 - node-version: "20" 37 + node-version: "20.19.0" 38 38 39 39 - name: Setup pnpm 40 40 uses: pnpm/action-setup@v4
+32
CHANGELOG.md
··· 1 + ## [2.5.2](https://github.com/usekaneo/kaneo/compare/v2.5.1...v2.5.2) (2026-04-02) 2 + 3 + 4 + ### Bug Fixes 5 + 6 + * accept dbOrTx, add subscribeToEvent, fix status lookup,error handling, error toast, french ([2ce9f59](https://github.com/usekaneo/kaneo/commit/2ce9f590523b8cdd910e638faeff25add6109e4c)) 7 + * **api:** create projects transactionally ([f15d34c](https://github.com/usekaneo/kaneo/commit/f15d34c6f2219c43a167043e677f9a6adf718e40)) 8 + * **api:** document health route and harden asset auth handling ([704376e](https://github.com/usekaneo/kaneo/commit/704376e440b8822b20fb18787dfa7f033ab723da)) 9 + * **ci,github:** tighten workflow token scope and preserve zero task numbers ([a56e8cd](https://github.com/usekaneo/kaneo/commit/a56e8cdd66d163365b79f7374ba46d384a676ded)) 10 + * **ci:** exclude coverage output from biome checks ([940f00f](https://github.com/usekaneo/kaneo/commit/940f00f34534746f8d3dd23ab4bcdc2e5c22bdd9)) 11 + * **ci:** mock email package in integration tests ([77646fc](https://github.com/usekaneo/kaneo/commit/77646fc456044a87731aac6cdddcf985cb636d9d)) 12 + * **ci:** use postgres admin db for tests ([be216d6](https://github.com/usekaneo/kaneo/commit/be216d6092f2f8e580bca5a516ad523ec35b6f61)) 13 + * **github:** only ignore missing label removals ([399c797](https://github.com/usekaneo/kaneo/commit/399c7977d72326955ed1f149d45c58991c374a2b)) 14 + * **github:** preserve zero-valued task numbers ([4f370aa](https://github.com/usekaneo/kaneo/commit/4f370aaca4eb4b339059408ffa78fe1c624f2600)) 15 + * **libs:** strip trailing slash before appending /api in resolveApiBaseUrl ([6edf7cf](https://github.com/usekaneo/kaneo/commit/6edf7cfaf1177dfcda2f8924fedb5f6913ec4283)) 16 + 17 + 18 + ### Features 19 + 20 + * **api:** set userEmail for API key auth and document public routes ([fedd40a](https://github.com/usekaneo/kaneo/commit/fedd40a0657313451042471e1dcd03943b45f2e0)) 21 + * **ci:** add api integration scaffolding ([9665488](https://github.com/usekaneo/kaneo/commit/9665488a1d55c080dcd09af56c97a2338ae99e18)) 22 + * **ci:** add api project integration harness ([8559555](https://github.com/usekaneo/kaneo/commit/8559555e5542c43c00599c085c07797b2a9e2bc0)) 23 + * **ci:** add api task integration tests ([831effd](https://github.com/usekaneo/kaneo/commit/831effdcc9f60d57e2bee6428139dd66272fa9b6)) 24 + * **ci:** add api vitest suite ([621c861](https://github.com/usekaneo/kaneo/commit/621c8613dbbb54233af0c8801418aef5cf5b2c7a)) 25 + * **ci:** add lint and unit workflow ([d83e642](https://github.com/usekaneo/kaneo/commit/d83e6428129eb388a333e577645dc48fdd134ee3)) 26 + * **ci:** disable default api unit coverage and add test:coverage script ([b773513](https://github.com/usekaneo/kaneo/commit/b773513cb89f71ddf7011399e8d81064da274fa8)) 27 + * **ci:** extract api app startup ([95b8c47](https://github.com/usekaneo/kaneo/commit/95b8c4730ee840d6bd9ec83f61f9efd921286cb3)) 28 + * **test:** add label api integration tests and readme ([528afc3](https://github.com/usekaneo/kaneo/commit/528afc3d16dceb611b7712defc5fb0eb767b23df)) 29 + * **test:** add otp email template render smoke test ([ec05672](https://github.com/usekaneo/kaneo/commit/ec05672ac9532133ce19245ce57484c0095b3d26)) 30 + * **test:** add vitest config and initial web unit tests ([05fe0fb](https://github.com/usekaneo/kaneo/commit/05fe0fb340cf0e1eafa2352f1c449438ea9109cf)) 31 + * **test:** extract resolveApiBaseUrl and add libs unit tests ([2476481](https://github.com/usekaneo/kaneo/commit/2476481f97f6b959cf87d9fb5f5ed4af6fe848bf)) 32 + * **web:** move-task popover on task toolbar with readable select labels ([7b747eb](https://github.com/usekaneo/kaneo/commit/7b747eb97b734c6330bc4c3c16164cff069fd0ff)) 1 33 ## [2.5.1](https://github.com/usekaneo/kaneo/compare/v2.5.0...v2.5.1) (2026-04-01) 2 34 3 35
+1 -1
CLAUDE.md
··· 229 229 - **Development Ports**: API runs on 1337, web runs on 5173 230 230 - **Hot Reload**: Both API and web have watch mode via `pnpm dev` 231 231 - **CORS**: Configured in API index.ts, controlled by `CORS_ORIGINS` env var 232 - - **No Tests Yet**: Test infrastructure not currently set up in this codebase 232 + - **Testing**: Run `pnpm test` at the repo root (Turbo runs `test` in packages that define it: API unit tests, web unit/component tests, shared packages). API integration tests: `pnpm test:integration` (requires PostgreSQL; env is set in `tests/api-integration/setup.ts`; CI uses `.github/workflows/ci.yml`). Vitest configs: `apps/api/vitest.config.ts` (unit), `apps/api/vitest.integration.config.ts` (integration), `apps/web/vitest.config.ts` (web). Integration tests live under `tests/api-integration/`; API unit tests under `tests/api/`. 233 233 - **Security**: Never commit secrets, always validate inputs, sanitize outputs 234 234 235 235 ## Common Patterns
+1 -1
CONTRIBUTING.md
··· 75 75 git checkout -b feat/cool-new-feature 76 76 ``` 77 77 78 - 2. **Make your changes** and test them locally 78 + 2. **Make your changes** and test them locally (`pnpm test` for unit tests; `pnpm test:integration` for API integration tests with PostgreSQL) 79 79 80 80 3. **Commit using conventional commits**: 81 81 ```bash
+6
apps/api/.env.test.example
··· 1 + NODE_ENV=test 2 + AUTH_SECRET=test-secret-with-at-least-32-chars 3 + DATABASE_URL=postgresql://postgres:postgres@localhost:5432/kaneo_test 4 + KANEO_API_URL=http://localhost:1337 5 + KANEO_CLIENT_URL=http://localhost:5173 6 + DISABLE_GUEST_ACCESS=false
+8 -1
apps/api/package.json
··· 16 16 "dev": "tsx watch src/index.ts", 17 17 "build": "esbuild src/index.ts --bundle --platform=node --outdir=dist --format=esm --packages=external --external:fs --external:path --external:crypto --external:os --external:util --external:stream --external:buffer --external:events --external:url --external:querystring --external:http --external:https --external:net --external:tls --external:zlib", 18 18 "lint": "biome check --write .", 19 + "test": "npm run test:unit", 20 + "test:coverage": "vitest run --config vitest.config.ts --coverage", 21 + "test:unit": "vitest run --config vitest.config.ts", 22 + "test:integration": "vitest run --config vitest.integration.config.ts", 23 + "test:watch": "vitest --config vitest.config.ts", 19 24 "db:generate": "drizzle-kit generate", 20 25 "db:migrate": "drizzle-kit migrate", 21 26 "db:studio": "drizzle-kit studio", ··· 50 55 "@types/bcrypt": "^6.0.0", 51 56 "@types/node": "^25.3.5", 52 57 "@types/pg": "^8.18.0", 58 + "@vitest/coverage-v8": "^4.1.2", 53 59 "esbuild": "0.27.4", 54 60 "tsx": "^4.21.0", 55 - "typescript": "^5.9.3" 61 + "typescript": "^5.9.3", 62 + "vitest": "^4.1.2" 56 63 } 57 64 }
+391 -309
apps/api/src/index.ts
··· 1 + import { pathToFileURL } from "node:url"; 1 2 import { serve } from "@hono/node-server"; 2 3 import type { Session, User } from "better-auth/types"; 3 4 import { eq } from "drizzle-orm"; ··· 66 67 enabled: boolean; 67 68 }; 68 69 70 + type AppVariables = { 71 + Variables: { 72 + user: User | null; 73 + session: Session | null; 74 + userId: string; 75 + apiKey?: ApiKey; 76 + }; 77 + }; 78 + 79 + type ApiVariables = { 80 + Variables: { 81 + user: User | null; 82 + session: Session | null; 83 + userId: string; 84 + userEmail: string; 85 + apiKey?: ApiKey; 86 + }; 87 + }; 88 + 69 89 function buildContentDisposition(filename: string) { 70 90 const normalized = filename 71 91 .normalize("NFC") ··· 77 97 .normalize("NFKD") 78 98 .replace(/[\u0300-\u036f]/g, "") 79 99 .replace(/[\\/]/g, "-") 80 - .replace(/[^\x20-\x7E]+/g, "_") 100 + .replace(/[^\x20-\u7E]+/g, "_") 81 101 .replace(/\s+/g, " ") 82 102 .trim() || "file"; 83 103 const encodedFilename = encodeURIComponent(safeFilename).replace( ··· 88 108 return `inline; filename="${asciiFallback}"; filename*=UTF-8''${encodedFilename}`; 89 109 } 90 110 91 - const app = new Hono<{ 92 - Variables: { 93 - user: User | null; 94 - session: Session | null; 95 - userId: string; 96 - apiKey?: ApiKey; 97 - }; 98 - }>(); 111 + export function createApp() { 112 + const app = new Hono<AppVariables>(); 113 + const corsOrigins = process.env.CORS_ORIGINS 114 + ? process.env.CORS_ORIGINS.split(",").map((origin) => origin.trim()) 115 + : undefined; 99 116 100 - const corsOrigins = process.env.CORS_ORIGINS 101 - ? process.env.CORS_ORIGINS.split(",").map((origin) => origin.trim()) 102 - : undefined; 117 + app.use( 118 + "*", 119 + cors({ 120 + credentials: true, 121 + origin: (origin) => { 122 + if (!corsOrigins) { 123 + return origin || "*"; 124 + } 103 125 104 - app.use( 105 - "*", 106 - cors({ 107 - credentials: true, 108 - origin: (origin) => { 109 - if (!corsOrigins) { 110 - return origin || "*"; 111 - } 126 + if (!origin) { 127 + return null; 128 + } 112 129 113 - if (!origin) { 114 - return null; 115 - } 130 + return corsOrigins.includes(origin) ? origin : null; 131 + }, 132 + }), 133 + ); 116 134 117 - return corsOrigins.includes(origin) ? origin : null; 118 - }, 119 - }), 120 - ); 135 + const api = new Hono<ApiVariables>(); 121 136 122 - const api = new Hono<{ 123 - Variables: { 124 - user: User | null; 125 - session: Session | null; 126 - userId: string; 127 - userEmail: string; 128 - apiKey?: ApiKey; 129 - }; 130 - }>(); 137 + api.get("/health", (c) => { 138 + return c.json({ status: "ok" }); 139 + }); 131 140 132 - api.get("/health", (c) => { 133 - return c.json({ status: "ok" }); 134 - }); 141 + const publicProjectApi = api.get("/public-project/:id", async (c) => { 142 + const { id } = c.req.param(); 143 + const project = await getPublicProject(id); 135 144 136 - const publicProjectApi = api.get("/public-project/:id", async (c) => { 137 - const { id } = c.req.param(); 138 - const project = await getPublicProject(id); 145 + return c.json(project); 146 + }); 139 147 140 - return c.json(project); 141 - }); 148 + api.post("/github-integration/webhook", handleGithubWebhookRoute); 142 149 143 - api.post("/github-integration/webhook", handleGithubWebhookRoute); 144 - 145 - api.post("/gitea-integration/webhook/:integrationId", handleGiteaWebhookRoute); 150 + api.post( 151 + "/gitea-integration/webhook/:integrationId", 152 + handleGiteaWebhookRoute, 153 + ); 146 154 147 - const invitationPublicApi = api.get("/invitation/public/:id", async (c) => { 148 - const { id } = c.req.param(); 149 - const result = await getInvitationDetails(id); 150 - return c.json(result); 151 - }); 155 + const invitationPublicApi = api.get("/invitation/public/:id", async (c) => { 156 + const { id } = c.req.param(); 157 + const result = await getInvitationDetails(id); 158 + return c.json(result); 159 + }); 152 160 153 - api.get( 154 - "/auth/get-session", 155 - describeRoute({ 156 - operationId: "getSession", 157 - tags: ["Authentication"], 158 - description: "Get the current authenticated session", 159 - security: [], 160 - responses: { 161 - 200: { 162 - description: "Current session details or null when unauthenticated", 163 - content: { 164 - "application/json": { schema: resolver(v.any()) }, 161 + api.get( 162 + "/auth/get-session", 163 + describeRoute({ 164 + operationId: "getSession", 165 + tags: ["Authentication"], 166 + description: "Get the current authenticated session", 167 + security: [], 168 + responses: { 169 + 200: { 170 + description: "Current session details or null when unauthenticated", 171 + content: { 172 + "application/json": { schema: resolver(v.any()) }, 173 + }, 165 174 }, 166 175 }, 176 + }), 177 + async (c) => { 178 + const session = await auth.api.getSession({ headers: c.req.raw.headers }); 179 + return c.json(session ?? null); 167 180 }, 168 - }), 169 - async (c) => { 170 - const session = await auth.api.getSession({ headers: c.req.raw.headers }); 171 - return c.json(session ?? null); 172 - }, 173 - ); 181 + ); 174 182 175 - api.get( 176 - "/asset/:id", 177 - describeRoute({ 178 - operationId: "getAsset", 179 - tags: ["Assets"], 180 - description: "Download an uploaded asset by ID", 181 - security: [], 182 - responses: { 183 - 200: { 184 - description: "The requested asset binary stream", 185 - content: { 186 - "*/*": { schema: resolver(v.any()) }, 183 + api.get( 184 + "/asset/:id", 185 + describeRoute({ 186 + operationId: "getAsset", 187 + tags: ["Assets"], 188 + description: "Download an uploaded asset by ID", 189 + security: [], 190 + responses: { 191 + 200: { 192 + description: "The requested asset binary stream", 193 + content: { 194 + "*/*": { schema: resolver(v.any()) }, 195 + }, 187 196 }, 188 197 }, 189 - }, 190 - }), 191 - validator("param", v.object({ id: v.string() })), 192 - async (c) => { 193 - const { id } = c.req.param(); 194 - const [asset] = await db 195 - .select({ 196 - id: schema.assetTable.id, 197 - objectKey: schema.assetTable.objectKey, 198 - mimeType: schema.assetTable.mimeType, 199 - filename: schema.assetTable.filename, 200 - workspaceId: schema.assetTable.workspaceId, 201 - isPublic: schema.projectTable.isPublic, 202 - }) 203 - .from(schema.assetTable) 204 - .innerJoin( 205 - schema.projectTable, 206 - eq(schema.assetTable.projectId, schema.projectTable.id), 207 - ) 208 - .where(eq(schema.assetTable.id, id)) 209 - .limit(1); 198 + }), 199 + validator("param", v.object({ id: v.string() })), 200 + async (c) => { 201 + const { id } = c.req.param(); 202 + const [asset] = await db 203 + .select({ 204 + id: schema.assetTable.id, 205 + objectKey: schema.assetTable.objectKey, 206 + mimeType: schema.assetTable.mimeType, 207 + filename: schema.assetTable.filename, 208 + workspaceId: schema.assetTable.workspaceId, 209 + isPublic: schema.projectTable.isPublic, 210 + }) 211 + .from(schema.assetTable) 212 + .innerJoin( 213 + schema.projectTable, 214 + eq(schema.assetTable.projectId, schema.projectTable.id), 215 + ) 216 + .where(eq(schema.assetTable.id, id)) 217 + .limit(1); 210 218 211 - if (!asset) { 212 - throw new HTTPException(404, { message: "Asset not found" }); 213 - } 219 + if (!asset) { 220 + throw new HTTPException(404, { message: "Asset not found" }); 221 + } 214 222 215 - const authHeader = c.req.header("Authorization"); 216 - const bearerToken = authHeader?.startsWith("Bearer ") 217 - ? authHeader.substring(7).replace(/\s+/g, "").trim() 218 - : null; 223 + const authHeader = c.req.header("Authorization"); 224 + const bearerToken = authHeader?.startsWith("Bearer ") 225 + ? authHeader.substring(7).replace(/\s+/g, "").trim() 226 + : null; 219 227 220 - let userId = ""; 221 - let apiKeyId: string | undefined; 228 + let userId = ""; 229 + let apiKeyId: string | undefined; 222 230 223 - if (bearerToken) { 224 - const result = await verifyApiKey(bearerToken); 231 + if (bearerToken) { 232 + const result = await verifyApiKey(bearerToken); 225 233 226 - if (result?.valid && result.key) { 227 - userId = result.key.userId; 228 - apiKeyId = result.key.id; 234 + if (result?.valid && result.key) { 235 + userId = result.key.userId; 236 + apiKeyId = result.key.id; 237 + } else { 238 + throw new HTTPException(401, { message: "Invalid API key" }); 239 + } 229 240 } else { 230 - throw new HTTPException(401, { message: "Invalid API key" }); 241 + const session = await auth.api.getSession({ 242 + headers: c.req.raw.headers, 243 + }); 244 + userId = session?.user?.id || ""; 231 245 } 232 - } else { 233 - const session = await auth.api.getSession({ headers: c.req.raw.headers }); 234 - userId = session?.user?.id || ""; 235 - } 236 246 237 - if (userId) { 238 - await validateWorkspaceAccess(userId, asset.workspaceId, apiKeyId); 239 - } else if (!asset.isPublic) { 240 - throw new HTTPException(401, { message: "Unauthorized" }); 241 - } 247 + if (userId) { 248 + await validateWorkspaceAccess(userId, asset.workspaceId, apiKeyId); 249 + } else if (!asset.isPublic) { 250 + throw new HTTPException(401, { message: "Unauthorized" }); 251 + } 242 252 243 - try { 244 - const object = await getPrivateObject(asset.objectKey); 253 + try { 254 + const object = await getPrivateObject(asset.objectKey); 245 255 246 - return new Response(object.body as BodyInit, { 247 - headers: { 248 - "Cache-Control": asset.isPublic 249 - ? "public, max-age=300" 250 - : "private, max-age=120", 251 - "Content-Disposition": buildContentDisposition(asset.filename), 252 - "Content-Length": object.contentLength?.toString() || "", 253 - "Content-Type": object.contentType || asset.mimeType, 254 - ETag: object.etag || "", 255 - "Last-Modified": object.lastModified?.toUTCString() || "", 256 - }, 257 - }); 258 - } catch (error) { 259 - console.error("Failed to stream asset:", error); 260 - throw new HTTPException(404, { message: "Asset object not found" }); 261 - } 262 - }, 263 - ); 256 + return new Response(object.body as BodyInit, { 257 + headers: { 258 + "Cache-Control": asset.isPublic 259 + ? "public, max-age=300" 260 + : "private, max-age=120", 261 + "Content-Disposition": buildContentDisposition(asset.filename), 262 + "Content-Length": object.contentLength?.toString() || "", 263 + "Content-Type": object.contentType || asset.mimeType, 264 + ETag: object.etag || "", 265 + "Last-Modified": object.lastModified?.toUTCString() || "", 266 + }, 267 + }); 268 + } catch (error) { 269 + console.error("Failed to stream asset:", error); 270 + throw new HTTPException(404, { message: "Asset object not found" }); 271 + } 272 + }, 273 + ); 264 274 265 - const configApi = api.route("/config", config); 275 + const configApi = api.route("/config", config); 266 276 267 - const honoOpenApiHandler = openAPIRouteHandler(api, { 268 - documentation: { 269 - openapi: "3.0.3", 270 - info: { 271 - title: "Kaneo API", 272 - version: "1.0.0", 273 - description: 274 - "Kaneo Project Management API - Manage projects, tasks, labels, and more", 275 - }, 276 - servers: [ 277 - { 278 - url: normalizeApiServerUrl( 279 - process.env.KANEO_API_URL || "https://cloud.kaneo.app", 280 - ), 281 - description: "Kaneo API Server", 277 + const honoOpenApiHandler = openAPIRouteHandler(api, { 278 + documentation: { 279 + openapi: "3.0.3", 280 + info: { 281 + title: "Kaneo API", 282 + version: "1.0.0", 283 + description: 284 + "Kaneo Project Management API - Manage projects, tasks, labels, and more", 282 285 }, 283 - ], 284 - components: { 285 - securitySchemes: { 286 - bearerAuth: { 287 - type: "http", 288 - scheme: "bearer", 289 - description: "API Key authentication", 286 + servers: [ 287 + { 288 + url: normalizeApiServerUrl( 289 + process.env.KANEO_API_URL || "https://cloud.kaneo.app", 290 + ), 291 + description: "Kaneo API Server", 292 + }, 293 + ], 294 + components: { 295 + securitySchemes: { 296 + bearerAuth: { 297 + type: "http", 298 + scheme: "bearer", 299 + description: "API Key authentication", 300 + }, 290 301 }, 291 302 }, 303 + security: [{ bearerAuth: [] }], 292 304 }, 293 - security: [{ bearerAuth: [] }], 294 - }, 295 - }); 305 + }); 296 306 297 - api.get("/openapi", async (c) => { 298 - const maybeResponse = await honoOpenApiHandler(c, async () => {}); 299 - const honoSpecResponse = maybeResponse ?? c.res; 300 - const honoSpec = (await honoSpecResponse.json()) as Record<string, unknown>; 307 + api.get("/openapi", async (c) => { 308 + const maybeResponse = await honoOpenApiHandler(c, async () => {}); 309 + const honoSpecResponse = maybeResponse ?? c.res; 310 + const honoSpec = (await honoSpecResponse.json()) as Record<string, unknown>; 301 311 302 - let authSpec: Record<string, unknown> = {}; 303 - try { 304 - authSpec = (await auth.api.generateOpenAPISchema()) as Record< 305 - string, 306 - unknown 307 - >; 308 - } catch (error) { 309 - console.error("Failed to generate Better Auth OpenAPI schema:", error); 310 - } 312 + let authSpec: Record<string, unknown> = {}; 313 + try { 314 + authSpec = (await auth.api.generateOpenAPISchema()) as Record< 315 + string, 316 + unknown 317 + >; 318 + } catch (error) { 319 + console.error("Failed to generate Better Auth OpenAPI schema:", error); 320 + } 311 321 312 - const normalizedAuthSpec = normalizeOrganizationAuthOperations(authSpec); 313 - return c.json( 314 - ensureOperationSummaries( 315 - dedupeOperationIds( 316 - markOptionalSchemaFieldsNullable( 317 - normalizeNullableSchemasForOpenApi30( 318 - normalizeEmptyRequiredArrays( 319 - mergeOpenApiSpecs(honoSpec, normalizedAuthSpec), 322 + const normalizedAuthSpec = normalizeOrganizationAuthOperations(authSpec); 323 + return c.json( 324 + ensureOperationSummaries( 325 + dedupeOperationIds( 326 + markOptionalSchemaFieldsNullable( 327 + normalizeNullableSchemasForOpenApi30( 328 + normalizeEmptyRequiredArrays( 329 + mergeOpenApiSpecs(honoSpec, normalizedAuthSpec), 330 + ), 320 331 ), 321 332 ), 322 333 ), 323 334 ), 324 - ), 325 - ); 326 - }); 335 + ); 336 + }); 327 337 328 - api.on(["POST", "GET", "PUT", "DELETE"], "/auth/*", (c) => { 329 - const authHeader = c.req.header("Authorization"); 338 + api.on(["POST", "GET", "PUT", "DELETE"], "/auth/*", (c) => { 339 + const authHeader = c.req.header("Authorization"); 330 340 331 - if (authHeader?.startsWith("Bearer ")) { 332 - const apiKey = authHeader.substring(7).replace(/\s+/g, "").trim(); 333 - const headers = new Headers(c.req.raw.headers); 341 + if (authHeader?.startsWith("Bearer ")) { 342 + const apiKey = authHeader.substring(7).replace(/\s+/g, "").trim(); 343 + const headers = new Headers(c.req.raw.headers); 334 344 335 - // Better Auth API key plugin validates from x-api-key by default. 336 - if (!headers.get("x-api-key")) { 337 - headers.set("x-api-key", apiKey); 345 + // Better Auth API key plugin validates from x-api-key by default. 346 + if (!headers.get("x-api-key")) { 347 + headers.set("x-api-key", apiKey); 348 + } 349 + 350 + return auth.handler( 351 + new Request(c.req.raw, { 352 + headers, 353 + }), 354 + ); 338 355 } 339 356 340 - return auth.handler( 341 - new Request(c.req.raw, { 342 - headers, 343 - }), 344 - ); 345 - } 357 + return auth.handler(c.req.raw); 358 + }); 346 359 347 - return auth.handler(c.req.raw); 348 - }); 360 + api.use("*", async (c, next) => { 361 + const authHeader = c.req.header("Authorization"); 362 + if (authHeader?.startsWith("Bearer ")) { 363 + const apiKey = authHeader.substring(7).replace(/\s+/g, "").trim(); 349 364 350 - api.use("*", async (c, next) => { 351 - const authHeader = c.req.header("Authorization"); 352 - if (authHeader?.startsWith("Bearer ")) { 353 - const apiKey = authHeader.substring(7).replace(/\s+/g, "").trim(); 354 - 355 - try { 356 - const result = await verifyApiKey(apiKey); 365 + try { 366 + const result = await verifyApiKey(apiKey); 367 + if (result?.valid && result.key) { 368 + c.set("userId", result.key.userId); 369 + c.set("user", null); 370 + c.set("session", null); 371 + c.set("apiKey", { 372 + id: result.key.id, 373 + userId: result.key.userId, 374 + enabled: result.key.enabled, 375 + }); 376 + return next(); 377 + } 357 378 358 - if (result?.valid && result.key) { 359 - c.set("userId", result.key.userId); 360 - c.set("user", null); 361 - c.set("session", null); 362 - c.set("apiKey", { 363 - id: result.key.id, 364 - userId: result.key.userId, 365 - enabled: result.key.enabled, 379 + throw new HTTPException(401, { message: "Invalid API key" }); 380 + } catch (error) { 381 + if (error instanceof HTTPException) { 382 + throw error; 383 + } 384 + console.error("API key verification failed:", error); 385 + throw new HTTPException(401, { 386 + message: "API key verification failed", 366 387 }); 367 - return next(); 368 388 } 389 + } 369 390 370 - throw new HTTPException(401, { message: "Invalid API key" }); 371 - } catch (error) { 372 - if (error instanceof HTTPException) { 373 - throw error; 374 - } 375 - console.error("API key verification failed:", error); 376 - throw new HTTPException(401, { message: "API key verification failed" }); 391 + const session = await auth.api.getSession({ headers: c.req.raw.headers }); 392 + c.set("user", session?.user || null); 393 + c.set("session", session?.session || null); 394 + c.set("userId", session?.user?.id || ""); 395 + c.set("userEmail", session?.user?.email || ""); 396 + 397 + if (!session?.user) { 398 + throw new HTTPException(401, { message: "Unauthorized" }); 377 399 } 378 - } 379 400 380 - const session = await auth.api.getSession({ headers: c.req.raw.headers }); 381 - c.set("user", session?.user || null); 382 - c.set("session", session?.session || null); 383 - c.set("userId", session?.user?.id || ""); 384 - c.set("userEmail", session?.user?.email || ""); 401 + return next(); 402 + }); 385 403 386 - if (!session?.user) { 387 - throw new HTTPException(401, { message: "Unauthorized" }); 388 - } 404 + const projectApi = api.route("/project", project); 405 + const taskApi = api.route("/task", task); 406 + const columnApi = api.route("/column", column); 407 + const activityApi = api.route("/activity", activity); 408 + const commentApi = api.route("/comment", comment); 409 + const timeEntryApi = api.route("/time-entry", timeEntry); 410 + const labelApi = api.route("/label", label); 411 + const notificationApi = api.route("/notification", notification); 412 + const notificationPreferencesApi = api.route( 413 + "/notification-preferences", 414 + notificationPreferences, 415 + ); 416 + const searchApi = api.route("/search", search); 417 + const githubIntegrationApi = api.route( 418 + "/github-integration", 419 + githubIntegration, 420 + ); 421 + const giteaIntegrationApi = api.route("/gitea-integration", giteaIntegration); 422 + const genericWebhookIntegrationApi = api.route( 423 + "/generic-webhook-integration", 424 + genericWebhookIntegration, 425 + ); 426 + const discordIntegrationApi = api.route( 427 + "/discord-integration", 428 + discordIntegration, 429 + ); 430 + const slackIntegrationApi = api.route("/slack-integration", slackIntegration); 431 + const telegramIntegrationApi = api.route( 432 + "/telegram-integration", 433 + telegramIntegration, 434 + ); 435 + const taskRelationApi = api.route("/task-relation", taskRelation); 436 + const externalLinkApi = api.route("/external-link", externalLink); 437 + const workflowRuleApi = api.route("/workflow-rule", workflowRule); 438 + const invitationApi = api.route("/invitation", invitation); 439 + const workspaceApi = api.route("/workspace", workspace); 389 440 390 - return next(); 391 - }); 441 + app.route("/api", api); 392 442 393 - const projectApi = api.route("/project", project); 394 - const taskApi = api.route("/task", task); 395 - const columnApi = api.route("/column", column); 396 - const activityApi = api.route("/activity", activity); 397 - const commentApi = api.route("/comment", comment); 398 - const timeEntryApi = api.route("/time-entry", timeEntry); 399 - const labelApi = api.route("/label", label); 400 - const notificationApi = api.route("/notification", notification); 401 - const notificationPreferencesApi = api.route( 402 - "/notification-preferences", 403 - notificationPreferences, 404 - ); 405 - const searchApi = api.route("/search", search); 406 - const githubIntegrationApi = api.route( 407 - "/github-integration", 408 - githubIntegration, 409 - ); 410 - const giteaIntegrationApi = api.route("/gitea-integration", giteaIntegration); 411 - const genericWebhookIntegrationApi = api.route( 412 - "/generic-webhook-integration", 413 - genericWebhookIntegration, 414 - ); 415 - const discordIntegrationApi = api.route( 416 - "/discord-integration", 417 - discordIntegration, 418 - ); 419 - const slackIntegrationApi = api.route("/slack-integration", slackIntegration); 420 - const telegramIntegrationApi = api.route( 421 - "/telegram-integration", 422 - telegramIntegration, 423 - ); 424 - const taskRelationApi = api.route("/task-relation", taskRelation); 425 - const externalLinkApi = api.route("/external-link", externalLink); 426 - const workflowRuleApi = api.route("/workflow-rule", workflowRule); 427 - const invitationApi = api.route("/invitation", invitation); 428 - const workspaceApi = api.route("/workspace", workspace); 443 + return { 444 + app, 445 + api, 446 + activityApi, 447 + columnApi, 448 + commentApi, 449 + configApi, 450 + discordIntegrationApi, 451 + externalLinkApi, 452 + genericWebhookIntegrationApi, 453 + githubIntegrationApi, 454 + giteaIntegrationApi, 455 + invitationApi, 456 + invitationPublicApi, 457 + labelApi, 458 + notificationApi, 459 + notificationPreferencesApi, 460 + projectApi, 461 + publicProjectApi, 462 + searchApi, 463 + slackIntegrationApi, 464 + taskApi, 465 + taskRelationApi, 466 + telegramIntegrationApi, 467 + timeEntryApi, 468 + workflowRuleApi, 469 + workspaceApi, 470 + }; 471 + } 429 472 430 - app.route("/api", api); 473 + export async function runStartupTasks() { 474 + await migrateWorkspaceUserEmail(); 475 + await migrateSessionColumn(); 476 + await migrateApiKeyReferenceId(); 431 477 432 - (async () => { 433 - try { 434 - await migrateWorkspaceUserEmail(); 435 - await migrateSessionColumn(); 436 - await migrateApiKeyReferenceId(); 478 + console.log("🔄 Migrating database..."); 479 + await migrate(db, { 480 + migrationsFolder: `${process.cwd()}/drizzle`, 481 + }); 482 + console.log("✅ Database migrated successfully!"); 437 483 438 - console.log("🔄 Migrating database..."); 439 - await migrate(db, { 440 - migrationsFolder: `${process.cwd()}/drizzle`, 441 - }); 442 - console.log("✅ Database migrated successfully!"); 484 + await migrateGitHubIntegration(); 485 + await migrateColumns(); 443 486 444 - await migrateGitHubIntegration(); 445 - await migrateColumns(); 487 + initializePlugins(); 488 + } 446 489 447 - initializePlugins(); 490 + export async function startServer(port = 1337) { 491 + try { 492 + await runStartupTasks(); 448 493 } catch (error) { 449 494 console.error("❌ Database migration failed!", error); 450 495 process.exit(1); 451 496 } 452 - })(); 497 + 498 + serve( 499 + { 500 + fetch: app.fetch, 501 + port, 502 + }, 503 + () => { 504 + console.log( 505 + `⚡ API is running at ${process.env.KANEO_API_URL || "http://localhost:1337"}`, 506 + ); 507 + }, 508 + ); 509 + } 510 + 511 + const createdApp = createApp(); 512 + const { 513 + app, 514 + activityApi, 515 + columnApi, 516 + commentApi, 517 + configApi, 518 + discordIntegrationApi, 519 + externalLinkApi, 520 + genericWebhookIntegrationApi, 521 + githubIntegrationApi, 522 + giteaIntegrationApi, 523 + invitationApi, 524 + invitationPublicApi, 525 + labelApi, 526 + notificationApi, 527 + notificationPreferencesApi, 528 + projectApi, 529 + publicProjectApi, 530 + searchApi, 531 + slackIntegrationApi, 532 + taskApi, 533 + taskRelationApi, 534 + telegramIntegrationApi, 535 + timeEntryApi, 536 + workflowRuleApi, 537 + workspaceApi, 538 + } = createdApp; 539 + 540 + const isMainModule = 541 + Boolean(process.argv[1]) && 542 + import.meta.url === pathToFileURL(process.argv[1]).href; 453 543 454 - serve( 455 - { 456 - fetch: app.fetch, 457 - port: 1337, 458 - }, 459 - () => { 460 - console.log( 461 - `⚡ API is running at ${process.env.KANEO_API_URL || "http://localhost:1337"}`, 462 - ); 463 - }, 464 - ); 544 + if (isMainModule) { 545 + void startServer(); 546 + } 465 547 466 548 export type AppType = 467 549 | typeof configApi
+3 -3
apps/api/src/plugins/github/utils/branch-matcher.ts
··· 117 117 config, 118 118 projectSlug, 119 119 ); 120 - if (fromBranch) return fromBranch; 120 + if (fromBranch !== null) return fromBranch; 121 121 122 122 if (prTitle) { 123 123 const fromTitle = extractTaskNumberFromPRTitle(prTitle); 124 - if (fromTitle) return fromTitle; 124 + if (fromTitle !== null) return fromTitle; 125 125 } 126 126 127 127 if (prBody) { 128 128 const fromBody = extractTaskNumberFromPRBody(prBody); 129 - if (fromBody) return fromBody; 129 + if (fromBody !== null) return fromBody; 130 130 } 131 131 132 132 return null;
+13 -1
apps/api/src/plugins/github/utils/labels.ts
··· 81 81 issue_number: issueNumber, 82 82 name: labelName, 83 83 }); 84 - } catch {} 84 + } catch (error) { 85 + if ( 86 + typeof error === "object" && 87 + error !== null && 88 + "status" in error && 89 + error.status === 404 90 + ) { 91 + return; 92 + } 93 + 94 + console.error(`Failed to remove label "${labelName}" from issue:`, error); 95 + throw error; 96 + } 85 97 }
+1 -1
apps/api/src/plugins/github/webhooks/pull-request-opened.ts
··· 55 55 projectSlug, 56 56 ); 57 57 58 - if (!taskNumber) { 58 + if (taskNumber === null) { 59 59 continue; 60 60 } 61 61
+24 -22
apps/api/src/project/controllers/create-project.ts
··· 1 1 import db from "../../database"; 2 2 import { columnTable, projectTable } from "../../database/schema"; 3 3 4 - const DEFAULT_COLUMNS = [ 4 + export const DEFAULT_PROJECT_COLUMNS = [ 5 5 { name: "To Do", slug: "to-do", position: 0, isFinal: false }, 6 6 { name: "In Progress", slug: "in-progress", position: 1, isFinal: false }, 7 7 { name: "In Review", slug: "in-review", position: 2, isFinal: false }, 8 8 { name: "Done", slug: "done", position: 3, isFinal: true }, 9 - ]; 9 + ] as const; 10 10 11 11 async function createProject( 12 12 workspaceId: string, ··· 14 14 icon: string, 15 15 slug: string, 16 16 ) { 17 - const [createdProject] = await db 18 - .insert(projectTable) 19 - .values({ 20 - workspaceId, 21 - name, 22 - icon, 23 - slug, 24 - }) 25 - .returning(); 17 + return db.transaction(async (tx) => { 18 + const [createdProject] = await tx 19 + .insert(projectTable) 20 + .values({ 21 + workspaceId, 22 + name, 23 + icon, 24 + slug, 25 + }) 26 + .returning(); 26 27 27 - if (createdProject) { 28 - for (const col of DEFAULT_COLUMNS) { 29 - await db.insert(columnTable).values({ 30 - projectId: createdProject.id, 31 - name: col.name, 32 - slug: col.slug, 33 - position: col.position, 34 - isFinal: col.isFinal, 35 - }); 28 + if (createdProject) { 29 + for (const col of DEFAULT_PROJECT_COLUMNS) { 30 + await tx.insert(columnTable).values({ 31 + projectId: createdProject.id, 32 + name: col.name, 33 + slug: col.slug, 34 + position: col.position, 35 + isFinal: col.isFinal, 36 + }); 37 + } 36 38 } 37 - } 38 39 39 - return createdProject; 40 + return createdProject; 41 + }); 40 42 } 41 43 42 44 export default createProject;
+2 -7
apps/api/src/schemas.ts
··· 109 109 webhookEnabled: v.boolean(), 110 110 projectMode: v.picklist(["all", "selected"] as const), 111 111 selectedProjectIds: v.array(v.string()), 112 - projectMode: v.picklist(["all", "selected"] as const), 113 - selectedProjectIds: v.array(v.string()), 114 - createdAt: v.date(), 115 - updatedAt: v.date(), 116 - }); 112 + createdAt: v.date(), 113 + updatedAt: v.date(), 117 114 }); 118 115 119 116 export const notificationPreferenceSchema = v.object({ ··· 136 133 webhookSecretConfigured: v.boolean(), 137 134 maskedWebhookSecret: v.nullable(v.string()), 138 135 workspaces: v.array(notificationPreferenceWorkspaceRuleSchema), 139 - workspaces: v.array(notificationPreferenceWorkspaceRuleSchema), 140 136 createdAt: v.nullable(v.date()), 141 137 updatedAt: v.nullable(v.date()), 142 - }); 143 138 }); 144 139 145 140 export const githubIntegrationSchema = v.object({
+6 -6
apps/api/src/storage/s3.ts
··· 79 79 return process.env[name]?.trim() || ""; 80 80 } 81 81 82 - function parseBoolean(value: string | undefined, fallback: boolean) { 82 + export function parseBoolean(value: string | undefined, fallback: boolean) { 83 83 if (value === undefined || value.trim() === "") return fallback; 84 84 return value.trim().toLowerCase() === "true"; 85 85 } 86 86 87 - function parsePositiveInt(value: string | undefined, fallback: number) { 87 + export function parsePositiveInt(value: string | undefined, fallback: number) { 88 88 const parsed = Number.parseInt(value?.trim() || "", 10); 89 89 if (!Number.isFinite(parsed) || parsed <= 0) return fallback; 90 90 return parsed; ··· 156 156 return client; 157 157 } 158 158 159 - function sanitizePathSegment(value: string) { 159 + export function sanitizePathSegment(value: string) { 160 160 return ( 161 161 value 162 162 .toLowerCase() ··· 166 166 ); 167 167 } 168 168 169 - function getFileExtension(filename: string) { 169 + export function getFileExtension(filename: string) { 170 170 const normalized = filename.trim(); 171 171 const extension = normalized.includes(".") 172 172 ? normalized.split(".").pop() || "" ··· 175 175 return sanitizePathSegment(extension).slice(0, 12); 176 176 } 177 177 178 - function buildObjectKeyPrefix( 178 + export function buildObjectKeyPrefix( 179 179 context: Omit<TaskImageUploadContext, "filename" | "contentType">, 180 180 ) { 181 181 const surfaceFolder = ··· 192 192 ].join("/"); 193 193 } 194 194 195 - function buildObjectKey(context: TaskImageUploadContext) { 195 + export function buildObjectKey(context: TaskImageUploadContext) { 196 196 const extension = getFileExtension(context.filename); 197 197 const objectKeyPrefix = buildObjectKeyPrefix(context); 198 198 const timestamp = Date.now();
+17
apps/api/vitest.config.ts
··· 1 + import { defineConfig } from "vitest/config"; 2 + 3 + export default defineConfig({ 4 + test: { 5 + environment: "node", 6 + include: ["../../tests/api/**/*.test.ts"], 7 + coverage: { 8 + enabled: false, 9 + provider: "v8", 10 + reporter: ["text", "html"], 11 + reportsDirectory: "./coverage", 12 + }, 13 + }, 14 + esbuild: { 15 + target: "node18", 16 + }, 17 + });
+27
apps/api/vitest.integration.config.ts
··· 1 + import { resolve } from "node:path"; 2 + import { defineConfig } from "vitest/config"; 3 + 4 + export default defineConfig({ 5 + test: { 6 + environment: "node", 7 + include: ["../../tests/api-integration/**/*.test.ts"], 8 + setupFiles: ["../../tests/api-integration/setup.ts"], 9 + fileParallelism: false, 10 + maxWorkers: 1, 11 + minWorkers: 1, 12 + coverage: { 13 + enabled: false, 14 + }, 15 + }, 16 + esbuild: { 17 + target: "node18", 18 + }, 19 + resolve: { 20 + alias: { 21 + "@kaneo/email": resolve( 22 + __dirname, 23 + "../../tests/api-integration/mocks/email.ts", 24 + ), 25 + }, 26 + }, 27 + });
+8 -2
apps/web/package.json
··· 8 8 "build": "vite build", 9 9 "preview": "vite preview", 10 10 "lint": "biome check --write .", 11 - "typecheck": "tsc --noEmit -p tsconfig.app.json" 11 + "typecheck": "tsc --noEmit -p tsconfig.app.json", 12 + "test": "vitest run --config vitest.config.ts", 13 + "test:watch": "vitest --config vitest.config.ts" 12 14 }, 13 15 "dependencies": { 14 16 "@base-ui/react": "^1.2.0", ··· 99 101 "devDependencies": { 100 102 "@kaneo/libs": "workspace:*", 101 103 "@tailwindcss/postcss": "^4.2.1", 104 + "@testing-library/jest-dom": "^6.9.1", 105 + "@testing-library/react": "^16.3.2", 102 106 "@types/node": "^25.5.0", 103 107 "@types/react": "^19.2.14", 104 108 "@types/react-dom": "^19.2.3", 105 109 "globals": "^17.4.0", 110 + "jsdom": "^29.0.1", 106 111 "postcss": "^8.5.8", 107 112 "tailwindcss": "^4.2.1", 108 113 "tw-animate-css": "^1.4.0", 109 114 "typescript": "~5.8.3", 110 - "vite": "^7.3.1" 115 + "vite": "^7.3.1", 116 + "vitest": "^4.1.2" 111 117 } 112 118 }
+15
apps/web/src/components/nav-projects.tsx
··· 5 5 Folder, 6 6 Forward, 7 7 MoreHorizontal, 8 + Settings, 8 9 Trash2, 9 10 } from "lucide-react"; 10 11 import { useState } from "react"; ··· 158 159 <Forward className="text-muted-foreground" /> 159 160 <span> 160 161 {t("navigation:projectList.shareProject")} 162 + </span> 163 + </DropdownMenuItem> 164 + <DropdownMenuItem 165 + className="h-7 items-start cursor-pointer text-sm" 166 + onClick={() => { 167 + navigate({ 168 + to: "/dashboard/settings/projects/$projectId/general", 169 + params: { projectId: project.id }, 170 + }); 171 + }} 172 + > 173 + <Settings className="text-muted-foreground" /> 174 + <span> 175 + {t("navigation:projectList.projectSettings")} 161 176 </span> 162 177 </DropdownMenuItem> 163 178 <DropdownMenuSeparator />
+4 -16
apps/web/src/components/public-project/kanban-view.tsx
··· 1 - import type { LucideIcon } from "lucide-react"; 2 - import { DEFAULT_COLUMNS } from "@/constants/columns"; 1 + import { getColumnIcon } from "@/lib/column"; 3 2 import type { ProjectWithTasks } from "@/types/project"; 4 3 import type Task from "@/types/task"; 5 4 import { PublicTaskCard } from "./task-card"; 6 - 7 - type Column = { 8 - id: string; 9 - name: string; 10 - icon: LucideIcon; 11 - tasks: Task[]; 12 - }; 13 5 14 6 type PublicKanbanViewProps = { 15 7 project: ProjectWithTasks; ··· 20 12 project, 21 13 onTaskClick, 22 14 }: PublicKanbanViewProps) { 23 - const columns: Column[] = DEFAULT_COLUMNS.map((column) => ({ 24 - ...column, 25 - tasks: project.columns?.find((col) => col.id === column.id)?.tasks || [], 26 - })); 15 + const columns = project.columns ?? []; 27 16 28 17 return ( 29 18 <div className="flex-1 min-h-0 overflow-x-auto [-webkit-overflow-scrolling:touch]"> 30 19 <div className="flex gap-3 p-3 h-full min-w-max transition-all duration-200 ease-out"> 31 20 {columns.map((column) => { 32 - const IconComponent = column.icon; 33 21 return ( 34 22 <div 35 23 key={column.id} ··· 39 27 <div className="p-2 shrink-0"> 40 28 <div className="flex items-center justify-between"> 41 29 <div className="flex items-center gap-2"> 42 - <IconComponent className="w-4 h-4 text-muted-foreground" /> 30 + {getColumnIcon(column.id, column.isFinal)} 43 31 <h3 className="font-medium text-foreground"> 44 32 {column.name} 45 33 </h3> ··· 65 53 {column.tasks.length === 0 && ( 66 54 <div className="text-center text-sm text-muted-foreground py-12 px-4"> 67 55 <div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center mx-auto mb-2"> 68 - <IconComponent className="w-4 h-4" /> 56 + {getColumnIcon(column.id, column.isFinal)} 69 57 </div> 70 58 No tasks in {column.name.toLowerCase()} 71 59 </div>
+6 -16
apps/web/src/components/public-project/list-view.tsx
··· 1 - import type { LucideIcon } from "lucide-react"; 2 - import { DEFAULT_COLUMNS } from "@/constants/columns"; 1 + import { getColumnIcon } from "@/lib/column"; 3 2 import type { ProjectWithTasks } from "@/types/project"; 4 3 import type Task from "@/types/task"; 5 4 import { PublicTaskRow } from "./task-row"; 6 5 7 - type Column = { 8 - id: string; 9 - name: string; 10 - icon: LucideIcon; 11 - tasks: Task[]; 12 - }; 13 - 14 6 type PublicListViewProps = { 15 7 project: ProjectWithTasks; 16 8 onTaskClick: (task: Task) => void; 17 9 }; 18 10 19 11 export function PublicListView({ project, onTaskClick }: PublicListViewProps) { 20 - const columns: Column[] = DEFAULT_COLUMNS.map((column) => ({ 21 - ...column, 22 - tasks: project.columns?.find((col) => col.id === column.id)?.tasks || [], 23 - })); 12 + const columns = project.columns ?? []; 24 13 25 14 return ( 26 15 <div className="flex-1 min-h-0 overflow-y-auto"> 27 16 <div className="p-6 space-y-8 max-w-5xl mx-auto"> 28 17 {columns.map((column) => { 29 - const IconComponent = column.icon; 30 18 return ( 31 19 <div key={column.id} className="space-y-4"> 32 20 <div className="flex items-center gap-3 px-2"> 33 - <IconComponent className="w-5 h-5 text-muted-foreground" /> 21 + <span className="flex [&_svg]:!h-5 [&_svg]:!w-5"> 22 + {getColumnIcon(column.id, column.isFinal)} 23 + </span> 34 24 <h3 className="font-semibold text-lg text-foreground"> 35 25 {column.name} 36 26 </h3> ··· 52 42 {column.tasks.length === 0 && ( 53 43 <div className="text-center text-sm text-muted-foreground py-8 bg-muted/50 rounded-lg border border-dashed border-border"> 54 44 <div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center mx-auto mb-2"> 55 - <IconComponent className="w-4 h-4" /> 45 + {getColumnIcon(column.id, column.isFinal)} 56 46 </div> 57 47 No tasks in {column.name.toLowerCase()} 58 48 </div>
+10
apps/web/src/components/ui/separator.test.tsx
··· 1 + import { render, screen } from "@testing-library/react"; 2 + import { describe, expect, it } from "vitest"; 3 + import { Separator } from "./separator"; 4 + 5 + describe("Separator", () => { 6 + it("renders a horizontal separator", () => { 7 + render(<Separator />); 8 + expect(screen.getByRole("separator")).toBeInTheDocument(); 9 + }); 10 + });
+8
apps/web/src/lib/cn.test.ts
··· 1 + import { describe, expect, it } from "vitest"; 2 + import { cn } from "./cn"; 3 + 4 + describe("cn", () => { 5 + it("merges tailwind classes and resolves conflicts", () => { 6 + expect(cn("px-2 py-1", "px-4")).toBe("py-1 px-4"); 7 + }); 8 + });
+1
apps/web/src/test/setup.ts
··· 1 + import "@testing-library/jest-dom/vitest";
+21
apps/web/vitest.config.ts
··· 1 + import path from "node:path"; 2 + import react from "@vitejs/plugin-react"; 3 + import { defineConfig } from "vitest/config"; 4 + 5 + export default defineConfig({ 6 + plugins: [react()], 7 + test: { 8 + environment: "jsdom", 9 + setupFiles: ["./src/test/setup.ts"], 10 + include: ["src/**/*.test.{ts,tsx}"], 11 + coverage: { 12 + enabled: false, 13 + }, 14 + }, 15 + resolve: { 16 + alias: { 17 + "@": path.resolve(__dirname, "./src"), 18 + "@i18n": path.resolve(__dirname, "../../i18n"), 19 + }, 20 + }, 21 + });
+1
biome.json
··· 21 21 "!**/.next", 22 22 "!**/out", 23 23 "!**/.cache", 24 + "!**/coverage", 24 25 "!**/.claude", 25 26 "!**/openapi.json" 26 27 ]
+1
i18n/de-DE.json
··· 1034 1034 "projectList": { 1035 1035 "viewProject": "Projekt ansehen", 1036 1036 "shareProject": "Projekt teilen", 1037 + "projectSettings": "Projekteinstellungen", 1037 1038 "linkCopied": "Projektlink in die Zwischenablage kopiert", 1038 1039 "addProject": "Projekt hinzufügen", 1039 1040 "deleteConfirmTitle": "Projekt löschen?",
+1
i18n/el-GR.json
··· 1032 1032 "projectList": { 1033 1033 "viewProject": "Προβολή έργου", 1034 1034 "shareProject": "Κοινοποίηση έργου", 1035 + "projectSettings": "Ρυθμίσεις έργου", 1035 1036 "linkCopied": "Ο σύνδεσμος του έργου αντιγράφηκε στο πρόχειρο", 1036 1037 "addProject": "Προσθήκη έργου", 1037 1038 "deleteConfirmTitle": "Διαγραφή έργου;",
+1
i18n/en-US.json
··· 1034 1034 "projectList": { 1035 1035 "viewProject": "View Project", 1036 1036 "shareProject": "Share Project", 1037 + "projectSettings": "Project settings", 1037 1038 "linkCopied": "Project link copied to clipboard", 1038 1039 "addProject": "Add project", 1039 1040 "deleteConfirmTitle": "Delete Project?",
+1
i18n/fr-FR.json
··· 1032 1032 "projectList": { 1033 1033 "viewProject": "Voir le projet", 1034 1034 "shareProject": "Partager le projet", 1035 + "projectSettings": "Paramètres du projet", 1035 1036 "linkCopied": "Lien du projet copié dans le presse-papiers", 1036 1037 "addProject": "Ajouter un projet", 1037 1038 "deleteConfirmTitle": "Supprimer le projet?",
+1
i18n/mk-MK.json
··· 1032 1032 "projectList": { 1033 1033 "viewProject": "Погледни проект", 1034 1034 "shareProject": "Сподели проект", 1035 + "projectSettings": "Поставки на проектот", 1035 1036 "linkCopied": "Линкот на проектот е копиран во клипборд", 1036 1037 "addProject": "Додади проект", 1037 1038 "deleteConfirmTitle": "Избриши проект?",
+1
i18n/nl-NL.json
··· 819 819 "projectList": { 820 820 "viewProject": "Project bekijken", 821 821 "shareProject": "Project delen", 822 + "projectSettings": "Projectinstellingen", 822 823 "linkCopied": "Projectlink gekopieerd naar klembord", 823 824 "addProject": "Project toevoegen", 824 825 "deleteConfirmTitle": "Project verwijderen?",
+4
i18n/schema.json
··· 3629 3629 "shareProject": { 3630 3630 "type": "string" 3631 3631 }, 3632 + "projectSettings": { 3633 + "type": "string" 3634 + }, 3632 3635 "linkCopied": { 3633 3636 "type": "string" 3634 3637 }, ··· 3651 3654 "required": [ 3652 3655 "viewProject", 3653 3656 "shareProject", 3657 + "projectSettings", 3654 3658 "linkCopied", 3655 3659 "addProject", 3656 3660 "deleteConfirmTitle",
+4 -2
package.json
··· 11 11 "i18n:report:fix": "node ./scripts/i18n/report.mjs --fix", 12 12 "i18n:schema": "node ./scripts/i18n/schema.mjs", 13 13 "lint": "turbo lint", 14 + "test": "turbo test", 15 + "test:integration": "pnpm --filter @kaneo/api test:integration", 14 16 "prepare": "husky" 15 17 }, 16 18 "devDependencies": { ··· 25 27 "typescript": "5.8.3" 26 28 }, 27 29 "engines": { 28 - "node": ">=18" 30 + "node": ">=20.19.0" 29 31 }, 30 32 "pnpm": { 31 33 "overrides": { ··· 51 53 } 52 54 }, 53 55 "packageManager": "pnpm@10.32.1", 54 - "version": "2.5.1", 56 + "version": "2.5.2", 55 57 "dependencies": { 56 58 "dotenv-mono": "^1.5.1" 57 59 }
+6 -2
packages/email/package.json
··· 15 15 "dev": "tsc && email dev --port 3002 --dir src/templates", 16 16 "export": "email export", 17 17 "lint": "biome check .", 18 - "format": "biome format --write ." 18 + "format": "biome format --write .", 19 + "test": "vitest run --config vitest.config.ts", 20 + "test:watch": "vitest --config vitest.config.ts" 19 21 }, 20 22 "dependencies": { 21 23 "@react-email/components": "1.0.10", ··· 27 29 }, 28 30 "devDependencies": { 29 31 "@react-email/preview-server": "5.2.10", 32 + "@react-email/render": "^2.0.4", 30 33 "@types/react": "^19.2.14", 31 34 "@types/react-dom": "^19.2.3", 32 - "react-email": "5.2.10" 35 + "react-email": "5.2.10", 36 + "vitest": "^4.1.2" 33 37 } 34 38 }
+12
packages/email/src/templates/otp.test.ts
··· 1 + import { render } from "@react-email/render"; 2 + import { createElement } from "react"; 3 + import { describe, expect, it } from "vitest"; 4 + import OtpEmail from "./otp"; 5 + 6 + describe("OtpEmail", () => { 7 + it("renders OTP and verification copy in HTML", async () => { 8 + const html = await render(createElement(OtpEmail, { otp: "123456" })); 9 + expect(html).toContain("123456"); 10 + expect(html).toContain("verification code"); 11 + }); 12 + });
+8
packages/email/vitest.config.ts
··· 1 + import { defineConfig } from "vitest/config"; 2 + 3 + export default defineConfig({ 4 + test: { 5 + environment: "node", 6 + include: ["src/**/*.test.ts"], 7 + }, 8 + });
+5 -2
packages/libs/package.json
··· 5 5 "types": "./src/index.ts", 6 6 "license": "MIT", 7 7 "scripts": { 8 - "lint": "biome check --write ." 8 + "lint": "biome check --write .", 9 + "test": "vitest run --config vitest.config.ts", 10 + "test:watch": "vitest --config vitest.config.ts" 9 11 }, 10 12 "devDependencies": { 11 13 "@types/react": "^19.2.14", ··· 15 17 "react": "^19.2.4", 16 18 "@kaneo/typescript-config": "workspace:*", 17 19 "typescript": "^5.9.2", 18 - "vite": "^7.3.1" 20 + "vite": "^7.3.1", 21 + "vitest": "^4.1.2" 19 22 } 20 23 }
+26
packages/libs/src/api-url.test.ts
··· 1 + import { describe, expect, it } from "vitest"; 2 + import { resolveApiBaseUrl } from "./api-url"; 3 + 4 + describe("resolveApiBaseUrl", () => { 5 + it("appends /api when the base has no api suffix", () => { 6 + expect(resolveApiBaseUrl(undefined)).toBe("http://localhost:1337/api"); 7 + expect(resolveApiBaseUrl("http://localhost:1337")).toBe( 8 + "http://localhost:1337/api", 9 + ); 10 + }); 11 + 12 + it("returns the URL unchanged when it already ends with /api", () => { 13 + expect(resolveApiBaseUrl("http://localhost:1337/api")).toBe( 14 + "http://localhost:1337/api", 15 + ); 16 + }); 17 + 18 + it("strips trailing slashes before appending /api", () => { 19 + expect(resolveApiBaseUrl("http://localhost:1337/")).toBe( 20 + "http://localhost:1337/api", 21 + ); 22 + expect(resolveApiBaseUrl("http://localhost:1337/api/")).toBe( 23 + "http://localhost:1337/api", 24 + ); 25 + }); 26 + });
+9
packages/libs/src/api-url.ts
··· 1 + /** 2 + * Resolves the Hono client base URL from `VITE_API_URL` (or default). 3 + * If the value already ends with `/api`, it is returned as-is; otherwise `/api` is appended. 4 + */ 5 + export function resolveApiBaseUrl(viteApiUrl: string | undefined): string { 6 + const raw = viteApiUrl || "http://localhost:1337"; 7 + const baseUrl = raw.replace(/\/+$/, ""); 8 + return baseUrl.endsWith("/api") ? baseUrl : `${baseUrl}/api`; 9 + }
+2 -2
packages/libs/src/hono.ts
··· 2 2 3 3 import type { AppType } from "@kaneo/api"; 4 4 import { hc } from "hono/client"; 5 + import { resolveApiBaseUrl } from "./api-url"; 5 6 6 - const baseUrl = import.meta.env.VITE_API_URL || "http://localhost:1337"; 7 - const apiUrl = baseUrl.endsWith("/api") ? baseUrl : `${baseUrl}/api`; 7 + const apiUrl = resolveApiBaseUrl(import.meta.env.VITE_API_URL); 8 8 9 9 export const client = hc<AppType>(apiUrl, { 10 10 fetch: (input: RequestInfo | URL, init?: RequestInit) => {
+1
packages/libs/src/index.ts
··· 1 + export { resolveApiBaseUrl } from "./api-url"; 1 2 export { client } from "./hono";
+8
packages/libs/vitest.config.ts
··· 1 + import { defineConfig } from "vitest/config"; 2 + 3 + export default defineConfig({ 4 + test: { 5 + environment: "node", 6 + include: ["src/**/*.test.ts"], 7 + }, 8 + });
+1033 -18
pnpm-lock.yaml
··· 71 71 version: 3.1006.0 72 72 '@better-auth/api-key': 73 73 specifier: ^1.5.6 74 - version: 1.5.6(b93c0fa4af19a53259adb7e86275d5df) 74 + version: 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.6(87ebdd32e322090e4445bd57d4349c23)) 75 75 '@better-auth/drizzle-adapter': 76 76 specifier: ^1.5.6 77 77 version: 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.18.0)(kysely@0.28.14)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))) ··· 104 104 version: 6.0.0 105 105 better-auth: 106 106 specifier: ^1.5.5 107 - version: 1.5.6(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.18.0)(kysely@0.28.14)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)))(mongodb@7.1.0)(mysql2@3.15.3)(next@16.1.7(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.20.0)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.10) 107 + version: 1.5.6(87ebdd32e322090e4445bd57d4349c23) 108 108 croner: 109 109 specifier: ^10.0.1 110 110 version: 10.0.1 ··· 142 142 '@types/pg': 143 143 specifier: ^8.18.0 144 144 version: 8.18.0 145 + '@vitest/coverage-v8': 146 + specifier: ^4.1.2 147 + version: 4.1.2(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.4.0)(jsdom@29.0.1(@noble/hashes@2.0.1))(msw@2.12.10(@types/node@25.4.0)(typescript@5.9.3))(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0))) 145 148 esbuild: 146 149 specifier: 0.27.3 147 150 version: 0.27.3 ··· 151 154 typescript: 152 155 specifier: ^5.9.3 153 156 version: 5.9.3 157 + vitest: 158 + specifier: ^4.1.2 159 + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.4.0)(jsdom@29.0.1(@noble/hashes@2.0.1))(msw@2.12.10(@types/node@25.4.0)(typescript@5.9.3))(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)) 154 160 155 161 apps/site: 156 162 dependencies: ··· 223 229 version: 1.2.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) 224 230 '@better-auth/api-key': 225 231 specifier: ^1.5.6 226 - version: 1.5.6(8c683fd870746335c7796e5beb9adb75) 232 + version: 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.6(1f32f5a0688eb2a2e4d7cf2aad15c406)) 227 233 '@dnd-kit/core': 228 234 specifier: ^6.3.1 229 235 version: 6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) ··· 373 379 version: 19.0.0-beta-ebf51a3-20250411 374 380 better-auth: 375 381 specifier: ^1.5.6 376 - version: 1.5.6(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.18.0)(kysely@0.28.14)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))(mongodb@7.1.0)(mysql2@3.15.3)(next@16.1.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-ebf51a3-20250411)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.20.0)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.10) 382 + version: 1.5.6(1f32f5a0688eb2a2e4d7cf2aad15c406) 377 383 class-variance-authority: 378 384 specifier: ^0.7.1 379 385 version: 0.7.1 ··· 477 483 '@tailwindcss/postcss': 478 484 specifier: ^4.2.1 479 485 version: 4.2.1 486 + '@testing-library/jest-dom': 487 + specifier: ^6.9.1 488 + version: 6.9.1 489 + '@testing-library/react': 490 + specifier: ^16.3.2 491 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) 480 492 '@types/node': 481 493 specifier: ^25.5.0 482 494 version: 25.5.0 ··· 489 501 globals: 490 502 specifier: ^17.4.0 491 503 version: 17.4.0 504 + jsdom: 505 + specifier: ^29.0.1 506 + version: 29.0.1(@noble/hashes@2.0.1) 492 507 postcss: 493 508 specifier: ^8.5.8 494 509 version: 8.5.8 ··· 504 519 vite: 505 520 specifier: ^7.3.1 506 521 version: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0) 522 + vitest: 523 + specifier: ^4.1.2 524 + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(jsdom@29.0.1(@noble/hashes@2.0.1))(msw@2.12.10(@types/node@25.5.0)(typescript@5.8.3))(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)) 507 525 508 526 packages/email: 509 527 dependencies: ··· 529 547 '@react-email/preview-server': 530 548 specifier: 5.2.10 531 549 version: 5.2.10(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) 550 + '@react-email/render': 551 + specifier: ^2.0.4 552 + version: 2.0.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) 532 553 '@types/react': 533 554 specifier: ^19.2.14 534 555 version: 19.2.14 ··· 538 559 react-email: 539 560 specifier: 5.2.10 540 561 version: 5.2.10 562 + vitest: 563 + specifier: ^4.1.2 564 + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(jsdom@29.0.1(@noble/hashes@2.0.1))(msw@2.12.10(@types/node@25.5.0)(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)) 541 565 542 566 packages/libs: 543 567 devDependencies: ··· 565 589 vite: 566 590 specifier: ^7.3.1 567 591 version: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0) 592 + vitest: 593 + specifier: ^4.1.2 594 + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(jsdom@29.0.1(@noble/hashes@2.0.1))(msw@2.12.10(@types/node@25.5.0)(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)) 568 595 569 596 packages/typescript-config: {} 570 597 571 598 packages: 599 + 600 + '@adobe/css-tools@4.4.4': 601 + resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} 572 602 573 603 '@alloc/quick-lru@5.2.0': 574 604 resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} ··· 577 607 '@antfu/ni@25.0.0': 578 608 resolution: {integrity: sha512-9q/yCljni37pkMr4sPrI3G4jqdIk074+iukc5aFJl7kmDCCsiJrbZ6zKxnES1Gwg+i9RcDZwvktl23puGslmvA==} 579 609 hasBin: true 610 + 611 + '@asamuzakjp/css-color@5.1.1': 612 + resolution: {integrity: sha512-iGWN8E45Ws0XWx3D44Q1t6vX2LqhCKcwfmwBYCDsFrYFS6m4q/Ks61L2veETaLv+ckDC6+dTETJoaAAb7VjLiw==} 613 + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} 614 + 615 + '@asamuzakjp/dom-selector@7.0.4': 616 + resolution: {integrity: sha512-jXR6x4AcT3eIrS2fSNAwJpwirOkGcd+E7F7CP3zjdTqz9B/2huHOL8YJZBgekKwLML+u7qB/6P1LXQuMScsx0w==} 617 + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} 618 + 619 + '@asamuzakjp/nwsapi@2.3.9': 620 + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} 580 621 581 622 '@aws-crypto/crc32@5.2.0': 582 623 resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} ··· 961 1002 '@types/react': 962 1003 optional: true 963 1004 1005 + '@bcoe/v8-coverage@1.0.2': 1006 + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} 1007 + engines: {node: '>=18'} 1008 + 964 1009 '@better-auth/api-key@1.5.6': 965 1010 resolution: {integrity: sha512-jr3m4/caFxn9BuY9pGDJ4B1HP1Qoqmyd7heBHm4KUFel+a9Whe/euROgZ/L+o7mbmUdZtreneaU15dpn0tJZ5g==} 966 1011 peerDependencies: ··· 1100 1145 cpu: [x64] 1101 1146 os: [win32] 1102 1147 1148 + '@bramus/specificity@2.4.2': 1149 + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} 1150 + hasBin: true 1151 + 1103 1152 '@chevrotain/cst-dts-gen@10.5.0': 1104 1153 resolution: {integrity: sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==} 1105 1154 ··· 1181 1230 resolution: {integrity: sha512-/yCrWGCoA1SVKOks25EGadP9Pnj0oAIHGpl2wH2M2Y46dPM2ueb8wyCVOD7O3WCTkaJ0IkKvzhl1JY7+uCT2Dw==} 1182 1231 engines: {node: '>=v18'} 1183 1232 1233 + '@csstools/color-helpers@6.0.2': 1234 + resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} 1235 + engines: {node: '>=20.19.0'} 1236 + 1237 + '@csstools/css-calc@3.1.1': 1238 + resolution: {integrity: sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==} 1239 + engines: {node: '>=20.19.0'} 1240 + peerDependencies: 1241 + '@csstools/css-parser-algorithms': ^4.0.0 1242 + '@csstools/css-tokenizer': ^4.0.0 1243 + 1244 + '@csstools/css-color-parser@4.0.2': 1245 + resolution: {integrity: sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==} 1246 + engines: {node: '>=20.19.0'} 1247 + peerDependencies: 1248 + '@csstools/css-parser-algorithms': ^4.0.0 1249 + '@csstools/css-tokenizer': ^4.0.0 1250 + 1251 + '@csstools/css-parser-algorithms@4.0.0': 1252 + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} 1253 + engines: {node: '>=20.19.0'} 1254 + peerDependencies: 1255 + '@csstools/css-tokenizer': ^4.0.0 1256 + 1257 + '@csstools/css-syntax-patches-for-csstree@1.1.2': 1258 + resolution: {integrity: sha512-5GkLzz4prTIpoyeUiIu3iV6CSG3Plo7xRVOFPKI7FVEJ3mZ0A8SwK0XU3Gl7xAkiQ+mDyam+NNp875/C5y+jSA==} 1259 + peerDependencies: 1260 + css-tree: ^3.2.1 1261 + peerDependenciesMeta: 1262 + css-tree: 1263 + optional: true 1264 + 1265 + '@csstools/css-tokenizer@4.0.0': 1266 + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} 1267 + engines: {node: '>=20.19.0'} 1268 + 1184 1269 '@date-fns/tz@1.4.1': 1185 1270 resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==} 1186 1271 ··· 1405 1490 engines: {node: '>=18'} 1406 1491 cpu: [x64] 1407 1492 os: [win32] 1493 + 1494 + '@exodus/bytes@1.15.0': 1495 + resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==} 1496 + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} 1497 + peerDependencies: 1498 + '@noble/hashes': ^1.8.0 || ^2.0.0 1499 + peerDependenciesMeta: 1500 + '@noble/hashes': 1501 + optional: true 1408 1502 1409 1503 '@floating-ui/core@1.7.3': 1410 1504 resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} ··· 3801 3895 resolution: {integrity: sha512-42WoRePf8v690qG8yGRe/YOh+oHni9vUaUUfoqlS91U2scd3a5rkLtVsc6b7z60w3RogH0I00vdrC5AaeiZ18w==} 3802 3896 engines: {node: '>=20.19'} 3803 3897 3898 + '@testing-library/dom@10.4.1': 3899 + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} 3900 + engines: {node: '>=18'} 3901 + 3902 + '@testing-library/jest-dom@6.9.1': 3903 + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} 3904 + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} 3905 + 3906 + '@testing-library/react@16.3.2': 3907 + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} 3908 + engines: {node: '>=18'} 3909 + peerDependencies: 3910 + '@testing-library/dom': ^10.0.0 3911 + '@types/react': ^18.0.0 || ^19.0.0 3912 + '@types/react-dom': ^18.0.0 || ^19.0.0 3913 + react: ^18.0.0 || ^19.0.0 3914 + react-dom: ^18.0.0 || ^19.0.0 3915 + peerDependenciesMeta: 3916 + '@types/react': 3917 + optional: true 3918 + '@types/react-dom': 3919 + optional: true 3920 + 3804 3921 '@tiptap/core@3.20.1': 3805 3922 resolution: {integrity: sha512-SwkPEWIfaDEZjC8SEIi4kZjqIYUbRgLUHUuQezo5GbphUNC8kM1pi3C3EtoOPtxXrEbY6e4pWEzW54Pcrd+rVA==} 3806 3923 peerDependencies: ··· 4009 4126 '@ts-morph/common@0.27.0': 4010 4127 resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==} 4011 4128 4129 + '@types/aria-query@5.0.4': 4130 + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} 4131 + 4012 4132 '@types/aws-lambda@8.10.159': 4013 4133 resolution: {integrity: sha512-SAP22WSGNN12OQ8PlCzGzRCZ7QDCwI85dQZbmpz7+mAk+L7j+wI7qnvmdKh+o7A5LaOp6QnOZ2NJphAZQTTHQg==} 4014 4134 ··· 4027 4147 '@types/bcrypt@6.0.0': 4028 4148 resolution: {integrity: sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==} 4029 4149 4150 + '@types/chai@5.2.3': 4151 + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} 4152 + 4030 4153 '@types/conventional-commits-parser@5.0.2': 4031 4154 resolution: {integrity: sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g==} 4032 4155 ··· 4036 4159 '@types/debug@4.1.12': 4037 4160 resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} 4038 4161 4162 + '@types/deep-eql@4.0.2': 4163 + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} 4164 + 4039 4165 '@types/estree-jsx@1.0.5': 4040 4166 resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} 4041 4167 ··· 4124 4250 peerDependencies: 4125 4251 vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 4126 4252 4253 + '@vitest/coverage-v8@4.1.2': 4254 + resolution: {integrity: sha512-sPK//PHO+kAkScb8XITeB1bf7fsk85Km7+rt4eeuRR3VS1/crD47cmV5wicisJmjNdfeokTZwjMk4Mj2d58Mgg==} 4255 + peerDependencies: 4256 + '@vitest/browser': 4.1.2 4257 + vitest: 4.1.2 4258 + peerDependenciesMeta: 4259 + '@vitest/browser': 4260 + optional: true 4261 + 4262 + '@vitest/expect@4.1.2': 4263 + resolution: {integrity: sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==} 4264 + 4265 + '@vitest/mocker@4.1.2': 4266 + resolution: {integrity: sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==} 4267 + peerDependencies: 4268 + msw: ^2.4.9 4269 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 4270 + peerDependenciesMeta: 4271 + msw: 4272 + optional: true 4273 + vite: 4274 + optional: true 4275 + 4276 + '@vitest/pretty-format@4.1.2': 4277 + resolution: {integrity: sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==} 4278 + 4279 + '@vitest/runner@4.1.2': 4280 + resolution: {integrity: sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==} 4281 + 4282 + '@vitest/snapshot@4.1.2': 4283 + resolution: {integrity: sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==} 4284 + 4285 + '@vitest/spy@4.1.2': 4286 + resolution: {integrity: sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==} 4287 + 4288 + '@vitest/utils@4.1.2': 4289 + resolution: {integrity: sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==} 4290 + 4127 4291 JSONStream@1.3.5: 4128 4292 resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} 4129 4293 hasBin: true ··· 4168 4332 resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 4169 4333 engines: {node: '>=8'} 4170 4334 4335 + ansi-styles@5.2.0: 4336 + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} 4337 + engines: {node: '>=10'} 4338 + 4171 4339 ansis@4.2.0: 4172 4340 resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} 4173 4341 engines: {node: '>=14'} ··· 4183 4351 resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} 4184 4352 engines: {node: '>=10'} 4185 4353 4354 + aria-query@5.3.0: 4355 + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} 4356 + 4357 + aria-query@5.3.2: 4358 + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} 4359 + engines: {node: '>= 0.4'} 4360 + 4186 4361 array-ify@1.0.0: 4187 4362 resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} 4188 4363 4364 + assertion-error@2.0.1: 4365 + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} 4366 + engines: {node: '>=12'} 4367 + 4189 4368 ast-types@0.16.1: 4190 4369 resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} 4191 4370 engines: {node: '>=4'} 4371 + 4372 + ast-v8-to-istanbul@1.0.0: 4373 + resolution: {integrity: sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==} 4192 4374 4193 4375 atomically@2.1.0: 4194 4376 resolution: {integrity: sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q==} ··· 4346 4528 zod: 4347 4529 optional: true 4348 4530 4531 + bidi-js@1.0.3: 4532 + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} 4533 + 4349 4534 bignumber.js@9.3.1: 4350 4535 resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} 4351 4536 ··· 4416 4601 4417 4602 ccount@2.0.1: 4418 4603 resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} 4604 + 4605 + chai@6.2.2: 4606 + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} 4607 + engines: {node: '>=18'} 4419 4608 4420 4609 chalk@5.6.2: 4421 4610 resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} ··· 4592 4781 resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} 4593 4782 engines: {node: '>= 8'} 4594 4783 4784 + css-tree@3.2.1: 4785 + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} 4786 + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} 4787 + 4788 + css.escape@1.5.1: 4789 + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} 4790 + 4595 4791 cssesc@3.0.0: 4596 4792 resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} 4597 4793 engines: {node: '>=4'} ··· 4607 4803 data-uri-to-buffer@4.0.1: 4608 4804 resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} 4609 4805 engines: {node: '>= 12'} 4806 + 4807 + data-urls@7.0.0: 4808 + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} 4809 + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} 4610 4810 4611 4811 date-fns-jalali@4.1.0-0: 4612 4812 resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==} ··· 4639 4839 peerDependenciesMeta: 4640 4840 supports-color: 4641 4841 optional: true 4842 + 4843 + decimal.js@10.6.0: 4844 + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} 4642 4845 4643 4846 decode-named-character-reference@1.2.0: 4644 4847 resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} ··· 4703 4906 resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} 4704 4907 engines: {node: '>=0.3.1'} 4705 4908 4909 + dom-accessibility-api@0.5.16: 4910 + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} 4911 + 4912 + dom-accessibility-api@0.6.3: 4913 + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} 4914 + 4706 4915 dom-serializer@2.0.0: 4707 4916 resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} 4708 4917 ··· 4890 5099 resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} 4891 5100 engines: {node: '>=0.12'} 4892 5101 5102 + entities@6.0.1: 5103 + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} 5104 + engines: {node: '>=0.12'} 5105 + 4893 5106 env-paths@2.2.1: 4894 5107 resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} 4895 5108 engines: {node: '>=6'} ··· 4911 5124 es-errors@1.3.0: 4912 5125 resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} 4913 5126 engines: {node: '>= 0.4'} 5127 + 5128 + es-module-lexer@2.0.0: 5129 + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} 4914 5130 4915 5131 es-object-atoms@1.1.1: 4916 5132 resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} ··· 4945 5161 estree-util-is-identifier-name@3.0.0: 4946 5162 resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} 4947 5163 5164 + estree-walker@3.0.3: 5165 + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} 5166 + 4948 5167 etag@1.8.1: 4949 5168 resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} 4950 5169 engines: {node: '>= 0.6'} ··· 4967 5186 execa@9.6.1: 4968 5187 resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} 4969 5188 engines: {node: ^18.19.0 || >=20.5.0} 5189 + 5190 + expect-type@1.3.0: 5191 + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} 5192 + engines: {node: '>=12.0.0'} 4970 5193 4971 5194 express-rate-limit@8.2.2: 4972 5195 resolution: {integrity: sha512-Ybv7bqtOgA914MLwaHWVFXMpMYeR1MQu/D+z2MaLYteqBsTIp9sY3AU7mGNLMJv8eLg8uQMpE20I+L2Lv49nSg==} ··· 5195 5418 resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} 5196 5419 engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} 5197 5420 5421 + has-flag@4.0.0: 5422 + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 5423 + engines: {node: '>=8'} 5424 + 5198 5425 has-symbols@1.1.0: 5199 5426 resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} 5200 5427 engines: {node: '>= 0.4'} ··· 5238 5465 resolution: {integrity: sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==} 5239 5466 engines: {node: '>=16.9.0'} 5240 5467 5468 + html-encoding-sniffer@6.0.0: 5469 + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} 5470 + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} 5471 + 5472 + html-escaper@2.0.2: 5473 + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} 5474 + 5241 5475 html-parse-stringify@3.0.1: 5242 5476 resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} 5243 5477 ··· 5303 5537 5304 5538 import-meta-resolve@4.2.0: 5305 5539 resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} 5540 + 5541 + indent-string@4.0.0: 5542 + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} 5543 + engines: {node: '>=8'} 5306 5544 5307 5545 inherits@2.0.4: 5308 5546 resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} ··· 5396 5634 resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} 5397 5635 engines: {node: '>=12'} 5398 5636 5637 + is-potential-custom-element-name@1.0.1: 5638 + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} 5639 + 5399 5640 is-promise@4.0.0: 5400 5641 resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} 5401 5642 ··· 5441 5682 resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} 5442 5683 engines: {node: '>=18'} 5443 5684 5685 + istanbul-lib-coverage@3.2.2: 5686 + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} 5687 + engines: {node: '>=8'} 5688 + 5689 + istanbul-lib-report@3.0.1: 5690 + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} 5691 + engines: {node: '>=10'} 5692 + 5693 + istanbul-reports@3.2.0: 5694 + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} 5695 + engines: {node: '>=8'} 5696 + 5444 5697 jiti@2.4.2: 5445 5698 resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} 5446 5699 hasBin: true ··· 5452 5705 jose@6.1.3: 5453 5706 resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} 5454 5707 5708 + js-tokens@10.0.0: 5709 + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} 5710 + 5455 5711 js-tokens@4.0.0: 5456 5712 resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 5457 5713 5458 5714 js-yaml@4.1.1: 5459 5715 resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} 5460 5716 hasBin: true 5717 + 5718 + jsdom@29.0.1: 5719 + resolution: {integrity: sha512-z6JOK5gRO7aMybVq/y/MlIpKh8JIi68FBKMUtKkK2KH/wMSRlCxQ682d08LB9fYXplyY/UXG8P4XXTScmdjApg==} 5720 + engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} 5721 + peerDependencies: 5722 + canvas: ^3.0.0 5723 + peerDependenciesMeta: 5724 + canvas: 5725 + optional: true 5461 5726 5462 5727 jsesc@3.1.0: 5463 5728 resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} ··· 5716 5981 resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} 5717 5982 engines: {node: 20 || >=22} 5718 5983 5984 + lru-cache@11.2.7: 5985 + resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==} 5986 + engines: {node: 20 || >=22} 5987 + 5719 5988 lru-cache@5.1.1: 5720 5989 resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} 5721 5990 ··· 5733 6002 peerDependencies: 5734 6003 react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 5735 6004 6005 + lz-string@1.5.0: 6006 + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} 6007 + hasBin: true 6008 + 5736 6009 magic-string@0.30.21: 5737 6010 resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} 6011 + 6012 + magicast@0.5.2: 6013 + resolution: {integrity: sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==} 6014 + 6015 + make-dir@4.0.0: 6016 + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} 6017 + engines: {node: '>=10'} 5738 6018 5739 6019 markdown-it@14.1.1: 5740 6020 resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==} ··· 5777 6057 5778 6058 mdast-util-to-string@4.0.0: 5779 6059 resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} 6060 + 6061 + mdn-data@2.27.1: 6062 + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} 5780 6063 5781 6064 mdurl@2.0.0: 5782 6065 resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} ··· 5893 6176 mimic-function@5.0.1: 5894 6177 resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} 5895 6178 engines: {node: '>=18'} 6179 + 6180 + min-indent@1.0.1: 6181 + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} 6182 + engines: {node: '>=4'} 5896 6183 5897 6184 minimatch@10.2.3: 5898 6185 resolution: {integrity: sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==} ··· 6087 6374 resolution: {integrity: sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==} 6088 6375 engines: {node: '>= 10'} 6089 6376 6377 + obug@2.1.1: 6378 + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} 6379 + 6090 6380 octokit@5.0.5: 6091 6381 resolution: {integrity: sha512-4+/OFSqOjoyULo7eN7EA97DE0Xydj/PW5aIckxqQIoFjFwqXKuFCvXUJObyJfBF9Khu4RL/jlDRI9FPaMGfPnw==} 6092 6382 engines: {node: '>= 20'} ··· 6157 6447 parse-ms@4.0.0: 6158 6448 resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} 6159 6449 engines: {node: '>=18'} 6450 + 6451 + parse5@8.0.0: 6452 + resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} 6160 6453 6161 6454 parseley@0.12.1: 6162 6455 resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} ··· 6295 6588 resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} 6296 6589 engines: {node: '>=14'} 6297 6590 hasBin: true 6591 + 6592 + pretty-format@27.5.1: 6593 + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} 6594 + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} 6298 6595 6299 6596 pretty-ms@9.3.0: 6300 6597 resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} ··· 6486 6783 typescript: 6487 6784 optional: true 6488 6785 6786 + react-is@17.0.2: 6787 + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} 6788 + 6489 6789 react-markdown@10.1.0: 6490 6790 resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} 6491 6791 peerDependencies: ··· 6548 6848 recast@0.23.11: 6549 6849 resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} 6550 6850 engines: {node: '>= 4'} 6851 + 6852 + redent@3.0.0: 6853 + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} 6854 + engines: {node: '>=8'} 6551 6855 6552 6856 regex-recursion@6.0.2: 6553 6857 resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} ··· 6632 6936 safer-buffer@2.1.2: 6633 6937 resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} 6634 6938 6939 + saxes@6.0.0: 6940 + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} 6941 + engines: {node: '>=v12.22.7'} 6942 + 6635 6943 scheduler@0.26.0: 6636 6944 resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} 6637 6945 ··· 6719 7027 resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} 6720 7028 engines: {node: '>= 0.4'} 6721 7029 7030 + siginfo@2.0.0: 7031 + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} 7032 + 6722 7033 signal-exit@3.0.7: 6723 7034 resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} 6724 7035 ··· 6778 7089 resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} 6779 7090 engines: {node: '>= 0.6'} 6780 7091 7092 + stackback@0.0.2: 7093 + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} 7094 + 6781 7095 statuses@2.0.2: 6782 7096 resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} 6783 7097 engines: {node: '>= 0.8'} 6784 7098 6785 7099 std-env@3.10.0: 6786 7100 resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} 7101 + 7102 + std-env@4.0.0: 7103 + resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} 6787 7104 6788 7105 stdin-discarder@0.2.2: 6789 7106 resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} ··· 6829 7146 strip-final-newline@4.0.0: 6830 7147 resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} 6831 7148 engines: {node: '>=18'} 7149 + 7150 + strip-indent@3.0.0: 7151 + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} 7152 + engines: {node: '>=8'} 6832 7153 6833 7154 strnum@2.2.0: 6834 7155 resolution: {integrity: sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==} ··· 6858 7179 babel-plugin-macros: 6859 7180 optional: true 6860 7181 7182 + supports-color@7.2.0: 7183 + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 7184 + engines: {node: '>=8'} 7185 + 7186 + symbol-tree@3.2.4: 7187 + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} 7188 + 6861 7189 tabbable@6.4.0: 6862 7190 resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} 6863 7191 ··· 6911 7239 tiny-warning@1.0.3: 6912 7240 resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} 6913 7241 7242 + tinybench@2.9.0: 7243 + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} 7244 + 6914 7245 tinyexec@1.0.2: 6915 7246 resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} 6916 7247 engines: {node: '>=18'} ··· 6922 7253 tinyglobby@0.2.15: 6923 7254 resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} 6924 7255 engines: {node: '>=12.0.0'} 7256 + 7257 + tinyrainbow@3.1.0: 7258 + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} 7259 + engines: {node: '>=14.0.0'} 6925 7260 6926 7261 tippy.js@6.3.7: 6927 7262 resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} ··· 6949 7284 resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} 6950 7285 engines: {node: '>=16'} 6951 7286 7287 + tough-cookie@6.0.1: 7288 + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} 7289 + engines: {node: '>=16'} 7290 + 6952 7291 tr46@5.1.1: 6953 7292 resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} 6954 7293 engines: {node: '>=18'} 7294 + 7295 + tr46@6.0.0: 7296 + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} 7297 + engines: {node: '>=20'} 6955 7298 6956 7299 trim-lines@3.0.1: 6957 7300 resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} ··· 7047 7390 7048 7391 undici-types@7.18.2: 7049 7392 resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} 7393 + 7394 + undici@7.24.6: 7395 + resolution: {integrity: sha512-Xi4agocCbRzt0yYMZGMA6ApD7gvtUFaxm4ZmeacWI4cZxaF6C+8I8QfofC20NAePiB/IcvZmzkJ7XPa471AEtA==} 7396 + engines: {node: '>=20.18.1'} 7050 7397 7051 7398 unicorn-magic@0.1.0: 7052 7399 resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} ··· 7191 7538 yaml: 7192 7539 optional: true 7193 7540 7541 + vitest@4.1.2: 7542 + resolution: {integrity: sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==} 7543 + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} 7544 + hasBin: true 7545 + peerDependencies: 7546 + '@edge-runtime/vm': '*' 7547 + '@opentelemetry/api': ^1.9.0 7548 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 7549 + '@vitest/browser-playwright': 4.1.2 7550 + '@vitest/browser-preview': 4.1.2 7551 + '@vitest/browser-webdriverio': 4.1.2 7552 + '@vitest/ui': 4.1.2 7553 + happy-dom: '*' 7554 + jsdom: '*' 7555 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 7556 + peerDependenciesMeta: 7557 + '@edge-runtime/vm': 7558 + optional: true 7559 + '@opentelemetry/api': 7560 + optional: true 7561 + '@types/node': 7562 + optional: true 7563 + '@vitest/browser-playwright': 7564 + optional: true 7565 + '@vitest/browser-preview': 7566 + optional: true 7567 + '@vitest/browser-webdriverio': 7568 + optional: true 7569 + '@vitest/ui': 7570 + optional: true 7571 + happy-dom: 7572 + optional: true 7573 + jsdom: 7574 + optional: true 7575 + 7194 7576 void-elements@3.1.0: 7195 7577 resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} 7196 7578 engines: {node: '>=0.10.0'} ··· 7198 7580 w3c-keyname@2.2.8: 7199 7581 resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} 7200 7582 7583 + w3c-xmlserializer@5.0.0: 7584 + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} 7585 + engines: {node: '>=18'} 7586 + 7201 7587 web-streams-polyfill@3.3.3: 7202 7588 resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} 7203 7589 engines: {node: '>= 8'} ··· 7206 7592 resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} 7207 7593 engines: {node: '>=12'} 7208 7594 7595 + webidl-conversions@8.0.1: 7596 + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} 7597 + engines: {node: '>=20'} 7598 + 7209 7599 webpack-virtual-modules@0.6.2: 7210 7600 resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} 7211 7601 7602 + whatwg-mimetype@5.0.0: 7603 + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} 7604 + engines: {node: '>=20'} 7605 + 7212 7606 whatwg-url@14.2.0: 7213 7607 resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} 7214 7608 engines: {node: '>=18'} 7609 + 7610 + whatwg-url@16.0.1: 7611 + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} 7612 + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} 7215 7613 7216 7614 when-exit@2.1.5: 7217 7615 resolution: {integrity: sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==} ··· 7226 7624 engines: {node: ^16.13.0 || >=18.0.0} 7227 7625 hasBin: true 7228 7626 7627 + why-is-node-running@2.3.0: 7628 + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} 7629 + engines: {node: '>=8'} 7630 + hasBin: true 7631 + 7229 7632 wrap-ansi@6.2.0: 7230 7633 resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} 7231 7634 engines: {node: '>=8'} ··· 7252 7655 wsl-utils@0.3.1: 7253 7656 resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} 7254 7657 engines: {node: '>=20'} 7658 + 7659 + xml-name-validator@5.0.0: 7660 + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} 7661 + engines: {node: '>=18'} 7662 + 7663 + xmlchars@2.2.0: 7664 + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} 7255 7665 7256 7666 xtend@4.0.2: 7257 7667 resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} ··· 7321 7731 7322 7732 snapshots: 7323 7733 7734 + '@adobe/css-tools@4.4.4': {} 7735 + 7324 7736 '@alloc/quick-lru@5.2.0': {} 7325 7737 7326 7738 '@antfu/ni@25.0.0': ··· 7328 7740 ansis: 4.2.0 7329 7741 fzf: 0.5.2 7330 7742 package-manager-detector: 1.6.0 7331 - tinyexec: 1.0.2 7743 + tinyexec: 1.0.4 7744 + 7745 + '@asamuzakjp/css-color@5.1.1': 7746 + dependencies: 7747 + '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) 7748 + '@csstools/css-color-parser': 4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) 7749 + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) 7750 + '@csstools/css-tokenizer': 4.0.0 7751 + lru-cache: 11.2.7 7752 + 7753 + '@asamuzakjp/dom-selector@7.0.4': 7754 + dependencies: 7755 + '@asamuzakjp/nwsapi': 2.3.9 7756 + bidi-js: 1.0.3 7757 + css-tree: 3.2.1 7758 + is-potential-custom-element-name: 1.0.1 7759 + lru-cache: 11.2.7 7760 + 7761 + '@asamuzakjp/nwsapi@2.3.9': {} 7332 7762 7333 7763 '@aws-crypto/crc32@5.2.0': 7334 7764 dependencies: ··· 7978 8408 7979 8409 '@babel/parser@7.28.5': 7980 8410 dependencies: 7981 - '@babel/types': 7.28.5 8411 + '@babel/types': 7.29.0 7982 8412 7983 8413 '@babel/parser@7.29.0': 7984 8414 dependencies: ··· 8126 8556 optionalDependencies: 8127 8557 '@types/react': 19.2.14 8128 8558 8129 - '@better-auth/api-key@1.5.6(8c683fd870746335c7796e5beb9adb75)': 8559 + '@bcoe/v8-coverage@1.0.2': {} 8560 + 8561 + '@better-auth/api-key@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.6(1f32f5a0688eb2a2e4d7cf2aad15c406))': 8130 8562 dependencies: 8131 8563 '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.1.1) 8132 8564 '@better-auth/utils': 0.3.1 8133 - better-auth: 1.5.6(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.18.0)(kysely@0.28.14)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))(mongodb@7.1.0)(mysql2@3.15.3)(next@16.1.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-ebf51a3-20250411)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.20.0)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.10) 8565 + better-auth: 1.5.6(1f32f5a0688eb2a2e4d7cf2aad15c406) 8134 8566 zod: 4.3.6 8135 8567 8136 - '@better-auth/api-key@1.5.6(b93c0fa4af19a53259adb7e86275d5df)': 8568 + '@better-auth/api-key@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.6(87ebdd32e322090e4445bd57d4349c23))': 8137 8569 dependencies: 8138 8570 '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.1.1) 8139 8571 '@better-auth/utils': 0.3.1 8140 - better-auth: 1.5.6(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.18.0)(kysely@0.28.14)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)))(mongodb@7.1.0)(mysql2@3.15.3)(next@16.1.7(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.20.0)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.10) 8572 + better-auth: 1.5.6(87ebdd32e322090e4445bd57d4349c23) 8141 8573 zod: 4.3.6 8142 8574 8143 8575 '@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.1.1)': ··· 8246 8678 8247 8679 '@biomejs/cli-win32-x64@2.4.8': 8248 8680 optional: true 8681 + 8682 + '@bramus/specificity@2.4.2': 8683 + dependencies: 8684 + css-tree: 3.2.1 8249 8685 8250 8686 '@chevrotain/cst-dts-gen@10.5.0': 8251 8687 dependencies: ··· 8376 8812 '@types/conventional-commits-parser': 5.0.2 8377 8813 chalk: 5.6.2 8378 8814 8815 + '@csstools/color-helpers@6.0.2': {} 8816 + 8817 + '@csstools/css-calc@3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': 8818 + dependencies: 8819 + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) 8820 + '@csstools/css-tokenizer': 4.0.0 8821 + 8822 + '@csstools/css-color-parser@4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': 8823 + dependencies: 8824 + '@csstools/color-helpers': 6.0.2 8825 + '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) 8826 + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) 8827 + '@csstools/css-tokenizer': 4.0.0 8828 + 8829 + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': 8830 + dependencies: 8831 + '@csstools/css-tokenizer': 4.0.0 8832 + 8833 + '@csstools/css-syntax-patches-for-csstree@1.1.2(css-tree@3.2.1)': 8834 + optionalDependencies: 8835 + css-tree: 3.2.1 8836 + 8837 + '@csstools/css-tokenizer@4.0.0': {} 8838 + 8379 8839 '@date-fns/tz@1.4.1': {} 8380 8840 8381 8841 '@dnd-kit/accessibility@3.1.1(react@19.2.4)': ··· 8534 8994 '@esbuild/win32-x64@0.27.3': 8535 8995 optional: true 8536 8996 8997 + '@exodus/bytes@1.15.0(@noble/hashes@2.0.1)': 8998 + optionalDependencies: 8999 + '@noble/hashes': 2.0.1 9000 + 8537 9001 '@floating-ui/core@1.7.3': 8538 9002 dependencies: 8539 9003 '@floating-ui/utils': 0.2.10 ··· 8677 9141 optionalDependencies: 8678 9142 '@types/node': 25.2.3 8679 9143 9144 + '@inquirer/confirm@5.1.21(@types/node@25.4.0)': 9145 + dependencies: 9146 + '@inquirer/core': 10.3.2(@types/node@25.4.0) 9147 + '@inquirer/type': 3.0.10(@types/node@25.4.0) 9148 + optionalDependencies: 9149 + '@types/node': 25.4.0 9150 + optional: true 9151 + 9152 + '@inquirer/confirm@5.1.21(@types/node@25.5.0)': 9153 + dependencies: 9154 + '@inquirer/core': 10.3.2(@types/node@25.5.0) 9155 + '@inquirer/type': 3.0.10(@types/node@25.5.0) 9156 + optionalDependencies: 9157 + '@types/node': 25.5.0 9158 + optional: true 9159 + 8680 9160 '@inquirer/core@10.3.2(@types/node@25.2.3)': 8681 9161 dependencies: 8682 9162 '@inquirer/ansi': 1.0.2 ··· 8690 9170 optionalDependencies: 8691 9171 '@types/node': 25.2.3 8692 9172 9173 + '@inquirer/core@10.3.2(@types/node@25.4.0)': 9174 + dependencies: 9175 + '@inquirer/ansi': 1.0.2 9176 + '@inquirer/figures': 1.0.15 9177 + '@inquirer/type': 3.0.10(@types/node@25.4.0) 9178 + cli-width: 4.1.0 9179 + mute-stream: 2.0.0 9180 + signal-exit: 4.1.0 9181 + wrap-ansi: 6.2.0 9182 + yoctocolors-cjs: 2.1.3 9183 + optionalDependencies: 9184 + '@types/node': 25.4.0 9185 + optional: true 9186 + 9187 + '@inquirer/core@10.3.2(@types/node@25.5.0)': 9188 + dependencies: 9189 + '@inquirer/ansi': 1.0.2 9190 + '@inquirer/figures': 1.0.15 9191 + '@inquirer/type': 3.0.10(@types/node@25.5.0) 9192 + cli-width: 4.1.0 9193 + mute-stream: 2.0.0 9194 + signal-exit: 4.1.0 9195 + wrap-ansi: 6.2.0 9196 + yoctocolors-cjs: 2.1.3 9197 + optionalDependencies: 9198 + '@types/node': 25.5.0 9199 + optional: true 9200 + 8693 9201 '@inquirer/figures@1.0.15': {} 8694 9202 8695 9203 '@inquirer/type@3.0.10(@types/node@25.2.3)': 8696 9204 optionalDependencies: 8697 9205 '@types/node': 25.2.3 9206 + 9207 + '@inquirer/type@3.0.10(@types/node@25.4.0)': 9208 + optionalDependencies: 9209 + '@types/node': 25.4.0 9210 + optional: true 9211 + 9212 + '@inquirer/type@3.0.10(@types/node@25.5.0)': 9213 + optionalDependencies: 9214 + '@types/node': 25.5.0 9215 + optional: true 8698 9216 8699 9217 '@jridgewell/gen-mapping@0.3.13': 8700 9218 dependencies: ··· 11745 12263 11746 12264 '@tanstack/virtual-file-routes@1.161.4': {} 11747 12265 12266 + '@testing-library/dom@10.4.1': 12267 + dependencies: 12268 + '@babel/code-frame': 7.29.0 12269 + '@babel/runtime': 7.29.2 12270 + '@types/aria-query': 5.0.4 12271 + aria-query: 5.3.0 12272 + dom-accessibility-api: 0.5.16 12273 + lz-string: 1.5.0 12274 + picocolors: 1.1.1 12275 + pretty-format: 27.5.1 12276 + 12277 + '@testing-library/jest-dom@6.9.1': 12278 + dependencies: 12279 + '@adobe/css-tools': 4.4.4 12280 + aria-query: 5.3.2 12281 + css.escape: 1.5.1 12282 + dom-accessibility-api: 0.6.3 12283 + picocolors: 1.1.1 12284 + redent: 3.0.0 12285 + 12286 + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': 12287 + dependencies: 12288 + '@babel/runtime': 7.29.2 12289 + '@testing-library/dom': 10.4.1 12290 + react: 19.2.4 12291 + react-dom: 19.2.4(react@19.2.4) 12292 + optionalDependencies: 12293 + '@types/react': 19.2.14 12294 + '@types/react-dom': 19.2.3(@types/react@19.2.14) 12295 + 11748 12296 '@tiptap/core@3.20.1(@tiptap/pm@3.20.1)': 11749 12297 dependencies: 11750 12298 '@tiptap/pm': 3.20.1 ··· 11975 12523 minimatch: 10.2.3 11976 12524 path-browserify: 1.0.1 11977 12525 12526 + '@types/aria-query@5.0.4': {} 12527 + 11978 12528 '@types/aws-lambda@8.10.159': {} 11979 12529 11980 12530 '@types/babel__core@7.20.5': ··· 11987 12537 11988 12538 '@types/babel__generator@7.27.0': 11989 12539 dependencies: 11990 - '@babel/types': 7.28.5 12540 + '@babel/types': 7.29.0 11991 12541 11992 12542 '@types/babel__template@7.4.4': 11993 12543 dependencies: 11994 - '@babel/parser': 7.28.5 11995 - '@babel/types': 7.28.5 12544 + '@babel/parser': 7.29.0 12545 + '@babel/types': 7.29.0 11996 12546 11997 12547 '@types/babel__traverse@7.28.0': 11998 12548 dependencies: 11999 - '@babel/types': 7.28.5 12549 + '@babel/types': 7.29.0 12000 12550 12001 12551 '@types/bcrypt@6.0.0': 12002 12552 dependencies: 12003 12553 '@types/node': 25.4.0 12554 + 12555 + '@types/chai@5.2.3': 12556 + dependencies: 12557 + '@types/deep-eql': 4.0.2 12558 + assertion-error: 2.0.1 12004 12559 12005 12560 '@types/conventional-commits-parser@5.0.2': 12006 12561 dependencies: ··· 12013 12568 '@types/debug@4.1.12': 12014 12569 dependencies: 12015 12570 '@types/ms': 2.1.0 12571 + 12572 + '@types/deep-eql@4.0.2': {} 12016 12573 12017 12574 '@types/estree-jsx@1.0.5': 12018 12575 dependencies: ··· 12109 12666 transitivePeerDependencies: 12110 12667 - supports-color 12111 12668 12669 + '@vitest/coverage-v8@4.1.2(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.4.0)(jsdom@29.0.1(@noble/hashes@2.0.1))(msw@2.12.10(@types/node@25.4.0)(typescript@5.9.3))(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)))': 12670 + dependencies: 12671 + '@bcoe/v8-coverage': 1.0.2 12672 + '@vitest/utils': 4.1.2 12673 + ast-v8-to-istanbul: 1.0.0 12674 + istanbul-lib-coverage: 3.2.2 12675 + istanbul-lib-report: 3.0.1 12676 + istanbul-reports: 3.2.0 12677 + magicast: 0.5.2 12678 + obug: 2.1.1 12679 + std-env: 4.0.0 12680 + tinyrainbow: 3.1.0 12681 + vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.4.0)(jsdom@29.0.1(@noble/hashes@2.0.1))(msw@2.12.10(@types/node@25.4.0)(typescript@5.9.3))(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)) 12682 + 12683 + '@vitest/expect@4.1.2': 12684 + dependencies: 12685 + '@standard-schema/spec': 1.1.0 12686 + '@types/chai': 5.2.3 12687 + '@vitest/spy': 4.1.2 12688 + '@vitest/utils': 4.1.2 12689 + chai: 6.2.2 12690 + tinyrainbow: 3.1.0 12691 + 12692 + '@vitest/mocker@4.1.2(msw@2.12.10(@types/node@25.4.0)(typescript@5.9.3))(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0))': 12693 + dependencies: 12694 + '@vitest/spy': 4.1.2 12695 + estree-walker: 3.0.3 12696 + magic-string: 0.30.21 12697 + optionalDependencies: 12698 + msw: 2.12.10(@types/node@25.4.0)(typescript@5.9.3) 12699 + vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0) 12700 + 12701 + '@vitest/mocker@4.1.2(msw@2.12.10(@types/node@25.5.0)(typescript@5.8.3))(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0))': 12702 + dependencies: 12703 + '@vitest/spy': 4.1.2 12704 + estree-walker: 3.0.3 12705 + magic-string: 0.30.21 12706 + optionalDependencies: 12707 + msw: 2.12.10(@types/node@25.5.0)(typescript@5.8.3) 12708 + vite: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0) 12709 + 12710 + '@vitest/mocker@4.1.2(msw@2.12.10(@types/node@25.5.0)(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))': 12711 + dependencies: 12712 + '@vitest/spy': 4.1.2 12713 + estree-walker: 3.0.3 12714 + magic-string: 0.30.21 12715 + optionalDependencies: 12716 + msw: 2.12.10(@types/node@25.5.0)(typescript@5.9.3) 12717 + vite: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0) 12718 + 12719 + '@vitest/pretty-format@4.1.2': 12720 + dependencies: 12721 + tinyrainbow: 3.1.0 12722 + 12723 + '@vitest/runner@4.1.2': 12724 + dependencies: 12725 + '@vitest/utils': 4.1.2 12726 + pathe: 2.0.3 12727 + 12728 + '@vitest/snapshot@4.1.2': 12729 + dependencies: 12730 + '@vitest/pretty-format': 4.1.2 12731 + '@vitest/utils': 4.1.2 12732 + magic-string: 0.30.21 12733 + pathe: 2.0.3 12734 + 12735 + '@vitest/spy@4.1.2': {} 12736 + 12737 + '@vitest/utils@4.1.2': 12738 + dependencies: 12739 + '@vitest/pretty-format': 4.1.2 12740 + convert-source-map: 2.0.0 12741 + tinyrainbow: 3.1.0 12742 + 12112 12743 JSONStream@1.3.5: 12113 12744 dependencies: 12114 12745 jsonparse: 1.3.1 ··· 12147 12778 dependencies: 12148 12779 color-convert: 2.0.1 12149 12780 12781 + ansi-styles@5.2.0: {} 12782 + 12150 12783 ansis@4.2.0: {} 12151 12784 12152 12785 anymatch@3.1.3: ··· 12160 12793 dependencies: 12161 12794 tslib: 2.8.1 12162 12795 12796 + aria-query@5.3.0: 12797 + dependencies: 12798 + dequal: 2.0.3 12799 + 12800 + aria-query@5.3.2: {} 12801 + 12163 12802 array-ify@1.0.0: {} 12164 12803 12804 + assertion-error@2.0.1: {} 12805 + 12165 12806 ast-types@0.16.1: 12166 12807 dependencies: 12167 12808 tslib: 2.8.1 12809 + 12810 + ast-v8-to-istanbul@1.0.0: 12811 + dependencies: 12812 + '@jridgewell/trace-mapping': 0.3.31 12813 + estree-walker: 3.0.3 12814 + js-tokens: 10.0.0 12168 12815 12169 12816 atomically@2.1.0: 12170 12817 dependencies: ··· 12243 12890 12244 12891 before-after-hook@4.0.0: {} 12245 12892 12246 - better-auth@1.5.6(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.18.0)(kysely@0.28.14)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))(mongodb@7.1.0)(mysql2@3.15.3)(next@16.1.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-ebf51a3-20250411)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.20.0)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.10): 12893 + better-auth@1.5.6(1f32f5a0688eb2a2e4d7cf2aad15c406): 12247 12894 dependencies: 12248 12895 '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.1.1) 12249 12896 '@better-auth/drizzle-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.18.0)(kysely@0.28.14)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))) ··· 12274 12921 react: 19.2.4 12275 12922 react-dom: 19.2.4(react@19.2.4) 12276 12923 solid-js: 1.9.10 12924 + vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(jsdom@29.0.1(@noble/hashes@2.0.1))(msw@2.12.10(@types/node@25.5.0)(typescript@5.8.3))(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)) 12277 12925 transitivePeerDependencies: 12278 12926 - '@cloudflare/workers-types' 12279 12927 - '@opentelemetry/api' 12280 12928 12281 - better-auth@1.5.6(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.18.0)(kysely@0.28.14)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)))(mongodb@7.1.0)(mysql2@3.15.3)(next@16.1.7(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.20.0)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.10): 12929 + better-auth@1.5.6(87ebdd32e322090e4445bd57d4349c23): 12282 12930 dependencies: 12283 12931 '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.1.1) 12284 12932 '@better-auth/drizzle-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.18.0)(kysely@0.28.14)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))) ··· 12309 12957 react: 19.2.4 12310 12958 react-dom: 19.2.4(react@19.2.4) 12311 12959 solid-js: 1.9.10 12960 + vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.4.0)(jsdom@29.0.1(@noble/hashes@2.0.1))(msw@2.12.10(@types/node@25.4.0)(typescript@5.9.3))(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)) 12312 12961 transitivePeerDependencies: 12313 12962 - '@cloudflare/workers-types' 12314 12963 - '@opentelemetry/api' ··· 12321 12970 set-cookie-parser: 3.0.1 12322 12971 optionalDependencies: 12323 12972 zod: 4.3.6 12973 + 12974 + bidi-js@1.0.3: 12975 + dependencies: 12976 + require-from-string: 2.0.2 12324 12977 12325 12978 bignumber.js@9.3.1: {} 12326 12979 ··· 12402 13055 caniuse-lite@1.0.30001780: {} 12403 13056 12404 13057 ccount@2.0.1: {} 13058 + 13059 + chai@6.2.2: {} 12405 13060 12406 13061 chalk@5.6.2: {} 12407 13062 ··· 12577 13232 shebang-command: 2.0.0 12578 13233 which: 2.0.2 12579 13234 13235 + css-tree@3.2.1: 13236 + dependencies: 13237 + mdn-data: 2.27.1 13238 + source-map-js: 1.2.1 13239 + 13240 + css.escape@1.5.1: {} 13241 + 12580 13242 cssesc@3.0.0: {} 12581 13243 12582 13244 csstype@3.2.3: {} ··· 12585 13247 12586 13248 data-uri-to-buffer@4.0.1: {} 12587 13249 13250 + data-urls@7.0.0(@noble/hashes@2.0.1): 13251 + dependencies: 13252 + whatwg-mimetype: 5.0.0 13253 + whatwg-url: 16.0.1(@noble/hashes@2.0.1) 13254 + transitivePeerDependencies: 13255 + - '@noble/hashes' 13256 + 12588 13257 date-fns-jalali@4.1.0-0: {} 12589 13258 12590 13259 date-fns@4.1.0: {} ··· 12602 13271 debug@4.4.3: 12603 13272 dependencies: 12604 13273 ms: 2.1.3 13274 + 13275 + decimal.js@10.6.0: {} 12605 13276 12606 13277 decode-named-character-reference@1.2.0: 12607 13278 dependencies: ··· 12644 13315 dequal: 2.0.3 12645 13316 12646 13317 diff@8.0.3: {} 13318 + 13319 + dom-accessibility-api@0.5.16: {} 13320 + 13321 + dom-accessibility-api@0.6.3: {} 12647 13322 12648 13323 dom-serializer@2.0.0: 12649 13324 dependencies: ··· 12786 13461 12787 13462 entities@4.5.0: {} 12788 13463 13464 + entities@6.0.1: {} 13465 + 12789 13466 env-paths@2.2.1: {} 12790 13467 12791 13468 env-paths@3.0.0: {} ··· 12799 13476 es-define-property@1.0.1: {} 12800 13477 12801 13478 es-errors@1.3.0: {} 13479 + 13480 + es-module-lexer@2.0.0: {} 12802 13481 12803 13482 es-object-atoms@1.1.1: 12804 13483 dependencies: ··· 12850 13529 12851 13530 estree-util-is-identifier-name@3.0.0: {} 12852 13531 13532 + estree-walker@3.0.3: 13533 + dependencies: 13534 + '@types/estree': 1.0.8 13535 + 12853 13536 etag@1.8.1: {} 12854 13537 12855 13538 events-universal@1.0.1: ··· 12890 13573 signal-exit: 4.1.0 12891 13574 strip-final-newline: 4.0.0 12892 13575 yoctocolors: 2.1.2 13576 + 13577 + expect-type@1.3.0: {} 12893 13578 12894 13579 express-rate-limit@8.2.2(express@5.2.1): 12895 13580 dependencies: ··· 13141 13826 13142 13827 graphql@16.12.0: {} 13143 13828 13829 + has-flag@4.0.0: {} 13830 + 13144 13831 has-symbols@1.1.0: {} 13145 13832 13146 13833 hasown@2.0.2: ··· 13200 13887 hono: 4.12.7 13201 13888 13202 13889 hono@4.12.7: {} 13890 + 13891 + html-encoding-sniffer@6.0.0(@noble/hashes@2.0.1): 13892 + dependencies: 13893 + '@exodus/bytes': 1.15.0(@noble/hashes@2.0.1) 13894 + transitivePeerDependencies: 13895 + - '@noble/hashes' 13896 + 13897 + html-escaper@2.0.2: {} 13203 13898 13204 13899 html-parse-stringify@3.0.1: 13205 13900 dependencies: ··· 13269 13964 13270 13965 import-meta-resolve@4.2.0: {} 13271 13966 13967 + indent-string@4.0.0: {} 13968 + 13272 13969 inherits@2.0.4: {} 13273 13970 13274 13971 ini@4.1.1: {} ··· 13329 14026 13330 14027 is-plain-obj@4.1.0: {} 13331 14028 14029 + is-potential-custom-element-name@1.0.1: {} 14030 + 13332 14031 is-promise@4.0.0: {} 13333 14032 13334 14033 is-property@1.0.2: ··· 13358 14057 13359 14058 isexe@3.1.5: {} 13360 14059 14060 + istanbul-lib-coverage@3.2.2: {} 14061 + 14062 + istanbul-lib-report@3.0.1: 14063 + dependencies: 14064 + istanbul-lib-coverage: 3.2.2 14065 + make-dir: 4.0.0 14066 + supports-color: 7.2.0 14067 + 14068 + istanbul-reports@3.2.0: 14069 + dependencies: 14070 + html-escaper: 2.0.2 14071 + istanbul-lib-report: 3.0.1 14072 + 13361 14073 jiti@2.4.2: {} 13362 14074 13363 14075 jiti@2.6.1: {} 13364 14076 13365 14077 jose@6.1.3: {} 13366 14078 14079 + js-tokens@10.0.0: {} 14080 + 13367 14081 js-tokens@4.0.0: {} 13368 14082 13369 14083 js-yaml@4.1.1: 13370 14084 dependencies: 13371 14085 argparse: 2.0.1 13372 14086 14087 + jsdom@29.0.1(@noble/hashes@2.0.1): 14088 + dependencies: 14089 + '@asamuzakjp/css-color': 5.1.1 14090 + '@asamuzakjp/dom-selector': 7.0.4 14091 + '@bramus/specificity': 2.4.2 14092 + '@csstools/css-syntax-patches-for-csstree': 1.1.2(css-tree@3.2.1) 14093 + '@exodus/bytes': 1.15.0(@noble/hashes@2.0.1) 14094 + css-tree: 3.2.1 14095 + data-urls: 7.0.0(@noble/hashes@2.0.1) 14096 + decimal.js: 10.6.0 14097 + html-encoding-sniffer: 6.0.0(@noble/hashes@2.0.1) 14098 + is-potential-custom-element-name: 1.0.1 14099 + lru-cache: 11.2.7 14100 + parse5: 8.0.0 14101 + saxes: 6.0.0 14102 + symbol-tree: 3.2.4 14103 + tough-cookie: 6.0.1 14104 + undici: 7.24.6 14105 + w3c-xmlserializer: 5.0.0 14106 + webidl-conversions: 8.0.1 14107 + whatwg-mimetype: 5.0.0 14108 + whatwg-url: 16.0.1(@noble/hashes@2.0.1) 14109 + xml-name-validator: 5.0.0 14110 + transitivePeerDependencies: 14111 + - '@noble/hashes' 14112 + 13373 14113 jsesc@3.1.0: {} 13374 14114 13375 14115 json-parse-even-better-errors@2.3.1: {} ··· 13552 14292 highlight.js: 11.11.1 13553 14293 13554 14294 lru-cache@11.2.4: {} 14295 + 14296 + lru-cache@11.2.7: {} 13555 14297 13556 14298 lru-cache@5.1.1: 13557 14299 dependencies: ··· 13568 14310 dependencies: 13569 14311 react: 19.2.4 13570 14312 14313 + lz-string@1.5.0: {} 14314 + 13571 14315 magic-string@0.30.21: 13572 14316 dependencies: 13573 14317 '@jridgewell/sourcemap-codec': 1.5.5 14318 + 14319 + magicast@0.5.2: 14320 + dependencies: 14321 + '@babel/parser': 7.29.0 14322 + '@babel/types': 7.29.0 14323 + source-map-js: 1.2.1 14324 + 14325 + make-dir@4.0.0: 14326 + dependencies: 14327 + semver: 7.7.4 13574 14328 13575 14329 markdown-it@14.1.1: 13576 14330 dependencies: ··· 13675 14429 mdast-util-to-string@4.0.0: 13676 14430 dependencies: 13677 14431 '@types/mdast': 4.0.4 14432 + 14433 + mdn-data@2.27.1: {} 13678 14434 13679 14435 mdurl@2.0.0: {} 13680 14436 ··· 13845 14601 13846 14602 mimic-function@5.0.1: {} 13847 14603 14604 + min-indent@1.0.1: {} 14605 + 13848 14606 minimatch@10.2.3: 13849 14607 dependencies: 13850 14608 brace-expansion: 5.0.5 ··· 13899 14657 transitivePeerDependencies: 13900 14658 - '@types/node' 13901 14659 14660 + msw@2.12.10(@types/node@25.4.0)(typescript@5.9.3): 14661 + dependencies: 14662 + '@inquirer/confirm': 5.1.21(@types/node@25.4.0) 14663 + '@mswjs/interceptors': 0.41.3 14664 + '@open-draft/deferred-promise': 2.2.0 14665 + '@types/statuses': 2.0.6 14666 + cookie: 1.1.1 14667 + graphql: 16.12.0 14668 + headers-polyfill: 4.0.3 14669 + is-node-process: 1.2.0 14670 + outvariant: 1.4.3 14671 + path-to-regexp: 8.4.0 14672 + picocolors: 1.1.1 14673 + rettime: 0.10.1 14674 + statuses: 2.0.2 14675 + strict-event-emitter: 0.5.1 14676 + tough-cookie: 6.0.0 14677 + type-fest: 5.3.1 14678 + until-async: 3.0.2 14679 + yargs: 17.7.2 14680 + optionalDependencies: 14681 + typescript: 5.9.3 14682 + transitivePeerDependencies: 14683 + - '@types/node' 14684 + optional: true 14685 + 14686 + msw@2.12.10(@types/node@25.5.0)(typescript@5.8.3): 14687 + dependencies: 14688 + '@inquirer/confirm': 5.1.21(@types/node@25.5.0) 14689 + '@mswjs/interceptors': 0.41.3 14690 + '@open-draft/deferred-promise': 2.2.0 14691 + '@types/statuses': 2.0.6 14692 + cookie: 1.1.1 14693 + graphql: 16.12.0 14694 + headers-polyfill: 4.0.3 14695 + is-node-process: 1.2.0 14696 + outvariant: 1.4.3 14697 + path-to-regexp: 8.4.0 14698 + picocolors: 1.1.1 14699 + rettime: 0.10.1 14700 + statuses: 2.0.2 14701 + strict-event-emitter: 0.5.1 14702 + tough-cookie: 6.0.0 14703 + type-fest: 5.3.1 14704 + until-async: 3.0.2 14705 + yargs: 17.7.2 14706 + optionalDependencies: 14707 + typescript: 5.8.3 14708 + transitivePeerDependencies: 14709 + - '@types/node' 14710 + optional: true 14711 + 14712 + msw@2.12.10(@types/node@25.5.0)(typescript@5.9.3): 14713 + dependencies: 14714 + '@inquirer/confirm': 5.1.21(@types/node@25.5.0) 14715 + '@mswjs/interceptors': 0.41.3 14716 + '@open-draft/deferred-promise': 2.2.0 14717 + '@types/statuses': 2.0.6 14718 + cookie: 1.1.1 14719 + graphql: 16.12.0 14720 + headers-polyfill: 4.0.3 14721 + is-node-process: 1.2.0 14722 + outvariant: 1.4.3 14723 + path-to-regexp: 8.4.0 14724 + picocolors: 1.1.1 14725 + rettime: 0.10.1 14726 + statuses: 2.0.2 14727 + strict-event-emitter: 0.5.1 14728 + tough-cookie: 6.0.0 14729 + type-fest: 5.3.1 14730 + until-async: 3.0.2 14731 + yargs: 17.7.2 14732 + optionalDependencies: 14733 + typescript: 5.9.3 14734 + transitivePeerDependencies: 14735 + - '@types/node' 14736 + optional: true 14737 + 13902 14738 mute-stream@2.0.0: {} 13903 14739 13904 14740 mysql2@3.15.3: ··· 14054 14890 14055 14891 object-treeify@1.1.33: {} 14056 14892 14893 + obug@2.1.1: {} 14894 + 14057 14895 octokit@5.0.5: 14058 14896 dependencies: 14059 14897 '@octokit/app': 16.1.2 ··· 14155 14993 14156 14994 parse-ms@4.0.0: {} 14157 14995 14996 + parse5@8.0.0: 14997 + dependencies: 14998 + entities: 6.0.1 14999 + 14158 15000 parseley@0.12.1: 14159 15001 dependencies: 14160 15002 leac: 0.6.0 ··· 14274 15116 powershell-utils@0.1.0: {} 14275 15117 14276 15118 prettier@3.7.4: {} 15119 + 15120 + pretty-format@27.5.1: 15121 + dependencies: 15122 + ansi-regex: 5.0.1 15123 + ansi-styles: 5.2.0 15124 + react-is: 17.0.2 14277 15125 14278 15126 pretty-ms@9.3.0: 14279 15127 dependencies: ··· 14444 15292 14445 15293 punycode.js@2.3.1: {} 14446 15294 14447 - punycode@2.3.1: 14448 - optional: true 15295 + punycode@2.3.1: {} 14449 15296 14450 15297 pure-rand@6.1.0: 14451 15298 optional: true ··· 14662 15509 react-dom: 19.2.4(react@19.2.4) 14663 15510 typescript: 5.8.3 14664 15511 15512 + react-is@17.0.2: {} 15513 + 14665 15514 react-markdown@10.1.0(@types/react@19.2.14)(react@19.2.4): 14666 15515 dependencies: 14667 15516 '@types/hast': 3.0.4 ··· 14756 15605 tiny-invariant: 1.3.3 14757 15606 tslib: 2.8.1 14758 15607 15608 + redent@3.0.0: 15609 + dependencies: 15610 + indent-string: 4.0.0 15611 + strip-indent: 3.0.0 15612 + 14759 15613 regex-recursion@6.0.2: 14760 15614 dependencies: 14761 15615 regex-utilities: 2.3.0 ··· 14865 15719 queue-microtask: 1.2.3 14866 15720 14867 15721 safer-buffer@2.1.2: {} 15722 + 15723 + saxes@6.0.0: 15724 + dependencies: 15725 + xmlchars: 2.2.0 14868 15726 14869 15727 scheduler@0.26.0: {} 14870 15728 ··· 15041 15899 side-channel-list: 1.0.0 15042 15900 side-channel-map: 1.0.1 15043 15901 side-channel-weakmap: 1.0.2 15902 + 15903 + siginfo@2.0.0: {} 15044 15904 15045 15905 signal-exit@3.0.7: {} 15046 15906 ··· 15113 15973 sqlstring@2.3.3: 15114 15974 optional: true 15115 15975 15976 + stackback@0.0.2: {} 15977 + 15116 15978 statuses@2.0.2: {} 15117 15979 15118 15980 std-env@3.10.0: 15119 15981 optional: true 15982 + 15983 + std-env@4.0.0: {} 15120 15984 15121 15985 stdin-discarder@0.2.2: {} 15122 15986 ··· 15167 16031 strip-final-newline@2.0.0: {} 15168 16032 15169 16033 strip-final-newline@4.0.0: {} 16034 + 16035 + strip-indent@3.0.0: 16036 + dependencies: 16037 + min-indent: 1.0.1 15170 16038 15171 16039 strnum@2.2.0: {} 15172 16040 ··· 15204 16072 client-only: 0.0.1 15205 16073 react: 19.2.4 15206 16074 16075 + supports-color@7.2.0: 16076 + dependencies: 16077 + has-flag: 4.0.0 16078 + 16079 + symbol-tree@3.2.4: {} 16080 + 15207 16081 tabbable@6.4.0: {} 15208 16082 15209 16083 tagged-tag@1.0.0: {} ··· 15259 16133 15260 16134 tiny-warning@1.0.3: {} 15261 16135 16136 + tinybench@2.9.0: {} 16137 + 15262 16138 tinyexec@1.0.2: {} 15263 16139 15264 16140 tinyexec@1.0.4: {} ··· 15267 16143 dependencies: 15268 16144 fdir: 6.5.0(picomatch@4.0.4) 15269 16145 picomatch: 4.0.4 16146 + 16147 + tinyrainbow@3.1.0: {} 15270 16148 15271 16149 tippy.js@6.3.7: 15272 16150 dependencies: ··· 15290 16168 dependencies: 15291 16169 tldts: 7.0.23 15292 16170 16171 + tough-cookie@6.0.1: 16172 + dependencies: 16173 + tldts: 7.0.23 16174 + 15293 16175 tr46@5.1.1: 15294 16176 dependencies: 15295 16177 punycode: 2.3.1 15296 16178 optional: true 15297 16179 16180 + tr46@6.0.0: 16181 + dependencies: 16182 + punycode: 2.3.1 16183 + 15298 16184 trim-lines@3.0.1: {} 15299 16185 15300 16186 trough@2.2.0: {} ··· 15375 16261 undici-types@7.16.0: {} 15376 16262 15377 16263 undici-types@7.18.2: {} 16264 + 16265 + undici@7.24.6: {} 15378 16266 15379 16267 unicorn-magic@0.1.0: {} 15380 16268 ··· 15499 16387 '@types/unist': 3.0.3 15500 16388 vfile-message: 4.0.3 15501 16389 16390 + vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0): 16391 + dependencies: 16392 + esbuild: 0.27.3 16393 + fdir: 6.5.0(picomatch@4.0.4) 16394 + picomatch: 4.0.4 16395 + postcss: 8.5.8 16396 + rollup: 4.59.0 16397 + tinyglobby: 0.2.15 16398 + optionalDependencies: 16399 + '@types/node': 25.4.0 16400 + fsevents: 2.3.3 16401 + jiti: 2.6.1 16402 + lightningcss: 1.31.1 16403 + tsx: 4.21.0 16404 + 15502 16405 vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0): 15503 16406 dependencies: 15504 16407 esbuild: 0.27.3 ··· 15514 16417 lightningcss: 1.31.1 15515 16418 tsx: 4.21.0 15516 16419 16420 + vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.4.0)(jsdom@29.0.1(@noble/hashes@2.0.1))(msw@2.12.10(@types/node@25.4.0)(typescript@5.9.3))(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)): 16421 + dependencies: 16422 + '@vitest/expect': 4.1.2 16423 + '@vitest/mocker': 4.1.2(msw@2.12.10(@types/node@25.4.0)(typescript@5.9.3))(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)) 16424 + '@vitest/pretty-format': 4.1.2 16425 + '@vitest/runner': 4.1.2 16426 + '@vitest/snapshot': 4.1.2 16427 + '@vitest/spy': 4.1.2 16428 + '@vitest/utils': 4.1.2 16429 + es-module-lexer: 2.0.0 16430 + expect-type: 1.3.0 16431 + magic-string: 0.30.21 16432 + obug: 2.1.1 16433 + pathe: 2.0.3 16434 + picomatch: 4.0.4 16435 + std-env: 4.0.0 16436 + tinybench: 2.9.0 16437 + tinyexec: 1.0.4 16438 + tinyglobby: 0.2.15 16439 + tinyrainbow: 3.1.0 16440 + vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0) 16441 + why-is-node-running: 2.3.0 16442 + optionalDependencies: 16443 + '@opentelemetry/api': 1.9.0 16444 + '@types/node': 25.4.0 16445 + jsdom: 29.0.1(@noble/hashes@2.0.1) 16446 + transitivePeerDependencies: 16447 + - msw 16448 + 16449 + vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(jsdom@29.0.1(@noble/hashes@2.0.1))(msw@2.12.10(@types/node@25.5.0)(typescript@5.8.3))(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)): 16450 + dependencies: 16451 + '@vitest/expect': 4.1.2 16452 + '@vitest/mocker': 4.1.2(msw@2.12.10(@types/node@25.5.0)(typescript@5.8.3))(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)) 16453 + '@vitest/pretty-format': 4.1.2 16454 + '@vitest/runner': 4.1.2 16455 + '@vitest/snapshot': 4.1.2 16456 + '@vitest/spy': 4.1.2 16457 + '@vitest/utils': 4.1.2 16458 + es-module-lexer: 2.0.0 16459 + expect-type: 1.3.0 16460 + magic-string: 0.30.21 16461 + obug: 2.1.1 16462 + pathe: 2.0.3 16463 + picomatch: 4.0.4 16464 + std-env: 4.0.0 16465 + tinybench: 2.9.0 16466 + tinyexec: 1.0.4 16467 + tinyglobby: 0.2.15 16468 + tinyrainbow: 3.1.0 16469 + vite: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0) 16470 + why-is-node-running: 2.3.0 16471 + optionalDependencies: 16472 + '@opentelemetry/api': 1.9.0 16473 + '@types/node': 25.5.0 16474 + jsdom: 29.0.1(@noble/hashes@2.0.1) 16475 + transitivePeerDependencies: 16476 + - msw 16477 + 16478 + vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(jsdom@29.0.1(@noble/hashes@2.0.1))(msw@2.12.10(@types/node@25.5.0)(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)): 16479 + dependencies: 16480 + '@vitest/expect': 4.1.2 16481 + '@vitest/mocker': 4.1.2(msw@2.12.10(@types/node@25.5.0)(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)) 16482 + '@vitest/pretty-format': 4.1.2 16483 + '@vitest/runner': 4.1.2 16484 + '@vitest/snapshot': 4.1.2 16485 + '@vitest/spy': 4.1.2 16486 + '@vitest/utils': 4.1.2 16487 + es-module-lexer: 2.0.0 16488 + expect-type: 1.3.0 16489 + magic-string: 0.30.21 16490 + obug: 2.1.1 16491 + pathe: 2.0.3 16492 + picomatch: 4.0.4 16493 + std-env: 4.0.0 16494 + tinybench: 2.9.0 16495 + tinyexec: 1.0.4 16496 + tinyglobby: 0.2.15 16497 + tinyrainbow: 3.1.0 16498 + vite: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0) 16499 + why-is-node-running: 2.3.0 16500 + optionalDependencies: 16501 + '@opentelemetry/api': 1.9.0 16502 + '@types/node': 25.5.0 16503 + jsdom: 29.0.1(@noble/hashes@2.0.1) 16504 + transitivePeerDependencies: 16505 + - msw 16506 + 15517 16507 void-elements@3.1.0: {} 15518 16508 15519 16509 w3c-keyname@2.2.8: {} 15520 16510 16511 + w3c-xmlserializer@5.0.0: 16512 + dependencies: 16513 + xml-name-validator: 5.0.0 16514 + 15521 16515 web-streams-polyfill@3.3.3: {} 15522 16516 15523 16517 webidl-conversions@7.0.0: 15524 16518 optional: true 15525 16519 16520 + webidl-conversions@8.0.1: {} 16521 + 15526 16522 webpack-virtual-modules@0.6.2: {} 16523 + 16524 + whatwg-mimetype@5.0.0: {} 15527 16525 15528 16526 whatwg-url@14.2.0: 15529 16527 dependencies: ··· 15531 16529 webidl-conversions: 7.0.0 15532 16530 optional: true 15533 16531 16532 + whatwg-url@16.0.1(@noble/hashes@2.0.1): 16533 + dependencies: 16534 + '@exodus/bytes': 1.15.0(@noble/hashes@2.0.1) 16535 + tr46: 6.0.0 16536 + webidl-conversions: 8.0.1 16537 + transitivePeerDependencies: 16538 + - '@noble/hashes' 16539 + 15534 16540 when-exit@2.1.5: {} 15535 16541 15536 16542 which@2.0.2: ··· 15541 16547 dependencies: 15542 16548 isexe: 3.1.5 15543 16549 16550 + why-is-node-running@2.3.0: 16551 + dependencies: 16552 + siginfo: 2.0.0 16553 + stackback: 0.0.2 16554 + 15544 16555 wrap-ansi@6.2.0: 15545 16556 dependencies: 15546 16557 ansi-styles: 4.3.0 ··· 15561 16572 dependencies: 15562 16573 is-wsl: 3.1.1 15563 16574 powershell-utils: 0.1.0 16575 + 16576 + xml-name-validator@5.0.0: {} 16577 + 16578 + xmlchars@2.2.0: {} 15564 16579 15565 16580 xtend@4.0.2: {} 15566 16581
+8
tests/api-integration/README.md
··· 1 + # API integration tests 2 + 3 + These tests boot the Hono app with a real PostgreSQL database (see `setup.ts` for env defaults). 4 + 5 + - Run: `pnpm test:integration` from the repo root (requires Postgres and `DATABASE_URL`). 6 + - CI: `.github/workflows/ci.yml` starts Postgres and runs the same command. 7 + 8 + Coverage is intentionally incremental: add new files under this directory for additional routes or behaviors, following existing helpers (`helpers/fixtures.ts`, `helpers/database.ts`, `helpers/auth.ts`).
+29
tests/api-integration/config.test.ts
··· 1 + import { describe, expect, it } from "vitest"; 2 + import { createApp } from "../../apps/api/src/index"; 3 + 4 + describe("API integration: config", () => { 5 + it("returns the public config shape", async () => { 6 + const { app } = createApp(); 7 + 8 + const response = await app.request("/api/config"); 9 + 10 + expect(response.status).toBe(200); 11 + const payload = (await response.json()) as Record<string, unknown>; 12 + 13 + expect(payload).toMatchObject({ 14 + disableRegistration: false, 15 + disablePasswordRegistration: false, 16 + isDemoMode: false, 17 + hasGuestAccess: true, 18 + }); 19 + expect(payload).toSatisfy((value: Record<string, unknown>) => 20 + [ 21 + "hasSmtp", 22 + "hasGithubSignIn", 23 + "hasGoogleSignIn", 24 + "hasDiscordSignIn", 25 + "hasCustomOAuth", 26 + ].every((key) => typeof value[key] === "boolean"), 27 + ); 28 + }); 29 + });
+13
tests/api-integration/health.test.ts
··· 1 + import { describe, expect, it } from "vitest"; 2 + import { createApp } from "../../apps/api/src/index"; 3 + 4 + describe("API integration: health", () => { 5 + it("responds with ok on /api/health", async () => { 6 + const { app } = createApp(); 7 + 8 + const response = await app.request("/api/health"); 9 + 10 + expect(response.status).toBe(200); 11 + await expect(response.json()).resolves.toEqual({ status: "ok" }); 12 + }); 13 + });
+29
tests/api-integration/helpers/auth.ts
··· 1 + import type { Session, User } from "better-auth/types"; 2 + import { vi } from "vitest"; 3 + import { auth } from "../../../apps/api/src/auth"; 4 + 5 + function createSession(userId: string): Session { 6 + const now = new Date(); 7 + 8 + return { 9 + id: `session-${userId}`, 10 + token: `token-${userId}`, 11 + userId, 12 + expiresAt: new Date(now.getTime() + 60 * 60 * 1000), 13 + createdAt: now, 14 + updatedAt: now, 15 + ipAddress: null, 16 + userAgent: null, 17 + }; 18 + } 19 + 20 + export function mockAuthenticatedSession(user: User) { 21 + return vi.spyOn(auth.api, "getSession").mockResolvedValue({ 22 + session: createSession(user.id), 23 + user, 24 + }); 25 + } 26 + 27 + export function mockAnonymousSession() { 28 + return vi.spyOn(auth.api, "getSession").mockResolvedValue(null); 29 + }
+115
tests/api-integration/helpers/database.ts
··· 1 + import { dirname, resolve } from "node:path"; 2 + import { fileURLToPath } from "node:url"; 3 + import { sql } from "drizzle-orm"; 4 + import { migrate } from "drizzle-orm/node-postgres/migrator"; 5 + import { Client } from "pg"; 6 + import db from "../../../apps/api/src/database"; 7 + 8 + const currentDir = dirname(fileURLToPath(import.meta.url)); 9 + const migrationsFolder = resolve(currentDir, "../../../apps/api/drizzle"); 10 + 11 + let migrationPromise: Promise<void> | null = null; 12 + 13 + function getDatabaseName(connectionString: string) { 14 + return new URL(connectionString).pathname.replace(/^\//, ""); 15 + } 16 + 17 + function getAdminDatabaseUrl(connectionString: string) { 18 + const url = new URL(connectionString); 19 + url.pathname = "/postgres"; 20 + return url.toString(); 21 + } 22 + 23 + function quoteIdentifier(identifier: string) { 24 + return `"${identifier.replaceAll('"', '""')}"`; 25 + } 26 + 27 + async function ensureTestDatabaseExists() { 28 + const connectionString = process.env.DATABASE_URL; 29 + 30 + if (!connectionString) { 31 + throw new Error("DATABASE_URL must be defined for integration tests"); 32 + } 33 + 34 + const databaseName = getDatabaseName(connectionString); 35 + 36 + if (!databaseName.endsWith("_test")) { 37 + throw new Error( 38 + `Refusing to manage non-test database "${databaseName}". DATABASE_URL must point to a test database.`, 39 + ); 40 + } 41 + 42 + const adminClient = new Client({ 43 + connectionString: getAdminDatabaseUrl(connectionString), 44 + }); 45 + 46 + await adminClient.connect(); 47 + 48 + try { 49 + const result = await adminClient.query( 50 + "SELECT 1 FROM pg_database WHERE datname = $1", 51 + [databaseName], 52 + ); 53 + 54 + if (result.rowCount === 0) { 55 + await adminClient.query( 56 + `CREATE DATABASE ${quoteIdentifier(databaseName)}`, 57 + ); 58 + } 59 + } finally { 60 + await adminClient.end(); 61 + } 62 + } 63 + 64 + export async function ensureTestDatabaseMigrated() { 65 + if (!migrationPromise) { 66 + migrationPromise = (async () => { 67 + await ensureTestDatabaseExists(); 68 + await migrate(db, { 69 + migrationsFolder, 70 + }); 71 + })(); 72 + } 73 + 74 + try { 75 + await migrationPromise; 76 + } catch (error) { 77 + migrationPromise = null; 78 + throw error; 79 + } 80 + } 81 + 82 + export async function resetTestDatabase() { 83 + await ensureTestDatabaseMigrated(); 84 + 85 + await db.execute( 86 + sql.raw(` 87 + TRUNCATE TABLE 88 + "activity", 89 + "account", 90 + "apikey", 91 + "asset", 92 + "column", 93 + "comment", 94 + "external_link", 95 + "github_integration", 96 + "integration", 97 + "invitation", 98 + "label", 99 + "notification", 100 + "project", 101 + "session", 102 + "task", 103 + "task_relation", 104 + "team", 105 + "team_member", 106 + "time_entry", 107 + "verification", 108 + "workflow_rule", 109 + "workspace", 110 + "workspace_member", 111 + "user" 112 + RESTART IDENTITY CASCADE 113 + `), 114 + ); 115 + }
+111
tests/api-integration/helpers/fixtures.ts
··· 1 + import { randomUUID } from "node:crypto"; 2 + import db, { schema } from "../../../apps/api/src/database"; 3 + import { DEFAULT_PROJECT_COLUMNS } from "../../../apps/api/src/project/controllers/create-project"; 4 + 5 + export type SeededMemberContext = { 6 + user: typeof schema.userTable.$inferSelect; 7 + workspace: typeof schema.workspaceTable.$inferSelect; 8 + }; 9 + 10 + export async function createWorkspaceMember( 11 + overrides?: Partial<{ 12 + userName: string; 13 + workspaceName: string; 14 + role: string; 15 + }>, 16 + ): Promise<SeededMemberContext> { 17 + const userId = `user-${randomUUID()}`; 18 + const workspaceId = `workspace-${randomUUID()}`; 19 + 20 + const [user] = await db 21 + .insert(schema.userTable) 22 + .values({ 23 + id: userId, 24 + email: `${userId}@example.com`, 25 + emailVerified: true, 26 + name: overrides?.userName || "Integration Test User", 27 + }) 28 + .returning(); 29 + 30 + const [workspace] = await db 31 + .insert(schema.workspaceTable) 32 + .values({ 33 + id: workspaceId, 34 + createdAt: new Date(), 35 + name: overrides?.workspaceName || "Integration Test Workspace", 36 + slug: `workspace-${randomUUID()}`, 37 + }) 38 + .returning(); 39 + 40 + await db.insert(schema.workspaceUserTable).values({ 41 + workspaceId: workspace.id, 42 + userId: user.id, 43 + role: overrides?.role ?? "member", 44 + joinedAt: new Date(), 45 + }); 46 + 47 + return { user, workspace }; 48 + } 49 + 50 + export async function createProjectFixture({ 51 + workspaceId, 52 + name = "Integration Project", 53 + icon = "Folder", 54 + slug = `project-${randomUUID()}`, 55 + }: { 56 + workspaceId: string; 57 + name?: string; 58 + icon?: string; 59 + slug?: string; 60 + }) { 61 + const [project] = await db 62 + .insert(schema.projectTable) 63 + .values({ 64 + workspaceId, 65 + name, 66 + icon, 67 + slug, 68 + }) 69 + .returning(); 70 + 71 + const insertedColumns: (typeof schema.columnTable.$inferSelect)[] = []; 72 + 73 + for (const col of DEFAULT_PROJECT_COLUMNS) { 74 + const [inserted] = await db 75 + .insert(schema.columnTable) 76 + .values({ 77 + projectId: project.id, 78 + name: col.name, 79 + slug: col.slug, 80 + position: col.position, 81 + isFinal: col.isFinal, 82 + }) 83 + .returning(); 84 + if (inserted) { 85 + insertedColumns.push(inserted); 86 + } 87 + } 88 + 89 + const columnsBySlug = new Map( 90 + insertedColumns.map((column) => [column.slug, column]), 91 + ); 92 + 93 + const todo = columnsBySlug.get("to-do"); 94 + const inProgress = columnsBySlug.get("in-progress"); 95 + const inReview = columnsBySlug.get("in-review"); 96 + const done = columnsBySlug.get("done"); 97 + 98 + if (!todo || !inProgress || !inReview || !done) { 99 + throw new Error("Failed to seed default project columns"); 100 + } 101 + 102 + return { 103 + project, 104 + columns: { 105 + todo, 106 + inProgress, 107 + inReview, 108 + done, 109 + }, 110 + }; 111 + }
+113
tests/api-integration/label.test.ts
··· 1 + import { eq } from "drizzle-orm"; 2 + import { beforeEach, describe, expect, it } from "vitest"; 3 + import db, { schema } from "../../apps/api/src/database"; 4 + import { createApp } from "../../apps/api/src/index"; 5 + import { mockAnonymousSession, mockAuthenticatedSession } from "./helpers/auth"; 6 + import { resetTestDatabase } from "./helpers/database"; 7 + import { createWorkspaceMember } from "./helpers/fixtures"; 8 + 9 + describe("API integration: labels", () => { 10 + beforeEach(async () => { 11 + await resetTestDatabase(); 12 + }); 13 + 14 + it("rejects unauthenticated label creation", async () => { 15 + mockAnonymousSession(); 16 + const { app } = createApp(); 17 + 18 + const response = await app.request("/api/label", { 19 + method: "POST", 20 + headers: { 21 + "content-type": "application/json", 22 + }, 23 + body: JSON.stringify({ 24 + name: "Bug", 25 + color: "#ff0000", 26 + workspaceId: "workspace-missing", 27 + }), 28 + }); 29 + 30 + expect(response.status).toBe(401); 31 + await expect(response.text()).resolves.toBe("Unauthorized"); 32 + }); 33 + 34 + it("creates a label in a workspace for a member", async () => { 35 + const member = await createWorkspaceMember(); 36 + mockAuthenticatedSession(member.user); 37 + const { app } = createApp(); 38 + 39 + const response = await app.request("/api/label", { 40 + method: "POST", 41 + headers: { 42 + "content-type": "application/json", 43 + }, 44 + body: JSON.stringify({ 45 + name: "Bug", 46 + color: "#ef4444", 47 + workspaceId: member.workspace.id, 48 + }), 49 + }); 50 + 51 + expect(response.status).toBe(200); 52 + const payload = 53 + (await response.json()) as typeof schema.labelTable.$inferSelect; 54 + 55 + expect(payload).toMatchObject({ 56 + workspaceId: member.workspace.id, 57 + name: "Bug", 58 + color: "#ef4444", 59 + }); 60 + 61 + const persisted = await db.query.labelTable.findFirst({ 62 + where: eq(schema.labelTable.id, payload.id), 63 + }); 64 + 65 + expect(persisted).toMatchObject({ 66 + id: payload.id, 67 + workspaceId: member.workspace.id, 68 + name: "Bug", 69 + color: "#ef4444", 70 + }); 71 + }); 72 + 73 + it("rejects label creation for users outside the workspace", async () => { 74 + const member = await createWorkspaceMember(); 75 + const outsiderId = "user-label-outsider"; 76 + 77 + const [outsider] = await db 78 + .insert(schema.userTable) 79 + .values({ 80 + id: outsiderId, 81 + email: `${outsiderId}@example.com`, 82 + emailVerified: true, 83 + name: "Label Outsider", 84 + }) 85 + .returning(); 86 + 87 + mockAuthenticatedSession(outsider); 88 + const { app } = createApp(); 89 + 90 + const response = await app.request("/api/label", { 91 + method: "POST", 92 + headers: { 93 + "content-type": "application/json", 94 + }, 95 + body: JSON.stringify({ 96 + name: "Blocked", 97 + color: "#6b7280", 98 + workspaceId: member.workspace.id, 99 + }), 100 + }); 101 + 102 + expect(response.status).toBe(403); 103 + await expect(response.text()).resolves.toBe( 104 + "You don't have access to this workspace", 105 + ); 106 + 107 + const persisted = await db.query.labelTable.findFirst({ 108 + where: eq(schema.labelTable.name, "Blocked"), 109 + }); 110 + 111 + expect(persisted).toBeUndefined(); 112 + }); 113 + });
+25
tests/api-integration/mocks/email.ts
··· 1 + import type { EmailResult } from "../../../packages/email/src/send-email"; 2 + 3 + export async function sendMagicLinkEmail( 4 + _to: string, 5 + _subject: string, 6 + _data: unknown, 7 + ): Promise<void> { 8 + return undefined; 9 + } 10 + 11 + export async function sendOtpEmail( 12 + _to: string, 13 + _subject: string, 14 + _data: unknown, 15 + ): Promise<void> { 16 + return undefined; 17 + } 18 + 19 + export async function sendWorkspaceInvitationEmail( 20 + _to: string, 21 + _subject: string, 22 + _data: unknown, 23 + ): Promise<EmailResult> { 24 + return { success: true }; 25 + }
+32
tests/api-integration/openapi.test.ts
··· 1 + import { describe, expect, it } from "vitest"; 2 + import { createApp } from "../../apps/api/src/index"; 3 + 4 + describe("API integration: openapi", () => { 5 + it("serves a merged OpenAPI document", async () => { 6 + const { app } = createApp(); 7 + 8 + const response = await app.request("/api/openapi"); 9 + 10 + expect(response.status).toBe(200); 11 + const payload = (await response.json()) as { 12 + openapi: string; 13 + info?: { title?: string }; 14 + paths?: Record<string, unknown>; 15 + components?: { 16 + securitySchemes?: Record<string, unknown>; 17 + }; 18 + }; 19 + 20 + expect(payload.openapi).toBe("3.0.3"); 21 + expect(payload.info?.title).toBe("Kaneo API"); 22 + expect(payload.paths?.["/config"]).toBeTruthy(); 23 + expect( 24 + Object.keys(payload.paths || {}).some((path) => 25 + path.startsWith("/auth/"), 26 + ), 27 + ).toBe(true); 28 + expect(payload.components?.securitySchemes).toMatchObject({ 29 + bearerAuth: expect.any(Object), 30 + }); 31 + }); 32 + });
+130
tests/api-integration/project.test.ts
··· 1 + import { eq } from "drizzle-orm"; 2 + import { beforeEach, describe, expect, it } from "vitest"; 3 + import db, { schema } from "../../apps/api/src/database"; 4 + import { createApp } from "../../apps/api/src/index"; 5 + import { mockAnonymousSession, mockAuthenticatedSession } from "./helpers/auth"; 6 + import { resetTestDatabase } from "./helpers/database"; 7 + import { createWorkspaceMember } from "./helpers/fixtures"; 8 + 9 + describe("API integration: project creation", () => { 10 + beforeEach(async () => { 11 + await resetTestDatabase(); 12 + }); 13 + 14 + it("rejects unauthenticated project creation requests", async () => { 15 + mockAnonymousSession(); 16 + const { app } = createApp(); 17 + 18 + const response = await app.request("/api/project", { 19 + method: "POST", 20 + headers: { 21 + "content-type": "application/json", 22 + }, 23 + body: JSON.stringify({ 24 + workspaceId: "workspace-missing", 25 + name: "Unauthorized Project", 26 + icon: "Folder", 27 + slug: "unauthorized-project", 28 + }), 29 + }); 30 + 31 + expect(response.status).toBe(401); 32 + await expect(response.text()).resolves.toBe("Unauthorized"); 33 + }); 34 + 35 + it("creates a project for a workspace member and seeds default columns", async () => { 36 + const member = await createWorkspaceMember(); 37 + mockAuthenticatedSession(member.user); 38 + const { app } = createApp(); 39 + 40 + const response = await app.request("/api/project", { 41 + method: "POST", 42 + headers: { 43 + "content-type": "application/json", 44 + }, 45 + body: JSON.stringify({ 46 + workspaceId: member.workspace.id, 47 + name: "Roadmap", 48 + icon: "FolderKanban", 49 + slug: "roadmap", 50 + }), 51 + }); 52 + 53 + expect(response.status).toBe(200); 54 + const payload = 55 + (await response.json()) as typeof schema.projectTable.$inferSelect; 56 + 57 + expect(payload).toMatchObject({ 58 + workspaceId: member.workspace.id, 59 + name: "Roadmap", 60 + icon: "FolderKanban", 61 + slug: "roadmap", 62 + }); 63 + 64 + const persistedProject = await db.query.projectTable.findFirst({ 65 + where: eq(schema.projectTable.id, payload.id), 66 + }); 67 + 68 + expect(persistedProject).toMatchObject({ 69 + id: payload.id, 70 + workspaceId: member.workspace.id, 71 + name: "Roadmap", 72 + slug: "roadmap", 73 + }); 74 + 75 + const columns = await db.query.columnTable.findMany({ 76 + where: eq(schema.columnTable.projectId, payload.id), 77 + orderBy: (column, { asc }) => [asc(column.position)], 78 + }); 79 + 80 + expect(columns).toHaveLength(4); 81 + expect(columns.map((column) => column.slug)).toEqual([ 82 + "to-do", 83 + "in-progress", 84 + "in-review", 85 + "done", 86 + ]); 87 + expect(columns.map((column) => column.isFinal)).toEqual([ 88 + false, 89 + false, 90 + false, 91 + true, 92 + ]); 93 + }); 94 + 95 + it("rejects project creation for users outside the workspace", async () => { 96 + const member = await createWorkspaceMember(); 97 + const outsiderId = "user-outsider"; 98 + 99 + const [outsider] = await db 100 + .insert(schema.userTable) 101 + .values({ 102 + id: outsiderId, 103 + email: `${outsiderId}@example.com`, 104 + emailVerified: true, 105 + name: "Outsider", 106 + }) 107 + .returning(); 108 + 109 + mockAuthenticatedSession(outsider); 110 + const { app } = createApp(); 111 + 112 + const response = await app.request("/api/project", { 113 + method: "POST", 114 + headers: { 115 + "content-type": "application/json", 116 + }, 117 + body: JSON.stringify({ 118 + workspaceId: member.workspace.id, 119 + name: "Forbidden Project", 120 + icon: "Folder", 121 + slug: "forbidden-project", 122 + }), 123 + }); 124 + 125 + expect(response.status).toBe(403); 126 + await expect(response.text()).resolves.toBe( 127 + "You don't have access to this workspace", 128 + ); 129 + }); 130 + });
+92
tests/api-integration/setup.ts
··· 1 + import { existsSync, readFileSync } from "node:fs"; 2 + import { dirname, resolve } from "node:path"; 3 + import { fileURLToPath } from "node:url"; 4 + import { afterEach, vi } from "vitest"; 5 + 6 + function stripEnvValueQuotes(value: string) { 7 + const trimmed = value.trim(); 8 + if ( 9 + (trimmed.startsWith('"') && trimmed.endsWith('"')) || 10 + (trimmed.startsWith("'") && trimmed.endsWith("'")) 11 + ) { 12 + return trimmed.slice(1, -1); 13 + } 14 + return trimmed; 15 + } 16 + 17 + function deriveTestDatabaseUrl(connectionString: string) { 18 + const url = new URL(connectionString); 19 + const databaseName = url.pathname.replace(/^\//, ""); 20 + 21 + if (!databaseName || databaseName.endsWith("_test")) { 22 + return connectionString; 23 + } 24 + 25 + url.pathname = `/${databaseName}_test`; 26 + return url.toString(); 27 + } 28 + 29 + function assertTestDatabaseUrl(connectionString: string) { 30 + const url = new URL(connectionString); 31 + const databaseName = url.pathname.replace(/^\//, ""); 32 + if (!databaseName.endsWith("_test")) { 33 + throw new Error( 34 + `Integration tests require DATABASE_URL to use a database name ending in _test (got "${databaseName}")`, 35 + ); 36 + } 37 + } 38 + 39 + function readDatabaseUrlFromEnvFile() { 40 + const currentDir = dirname(fileURLToPath(import.meta.url)); 41 + const envPath = resolve(currentDir, "../../.env"); 42 + 43 + if (!existsSync(envPath)) { 44 + return null; 45 + } 46 + 47 + const envFile = readFileSync(envPath, "utf8"); 48 + const match = envFile.match(/^DATABASE_URL=(.+)$/m); 49 + const raw = match?.[1]?.trim(); 50 + return raw ? stripEnvValueQuotes(raw) : null; 51 + } 52 + 53 + const defaultTestDatabaseUrl = 54 + "postgresql://postgres:postgres@localhost:5432/kaneo_test"; 55 + const envDatabaseUrl = process.env.DATABASE_URL?.trim(); 56 + const fromEnv = envDatabaseUrl ? stripEnvValueQuotes(envDatabaseUrl) : ""; 57 + const rawDatabaseUrl = 58 + fromEnv || readDatabaseUrlFromEnvFile() || defaultTestDatabaseUrl; 59 + process.env.DATABASE_URL = deriveTestDatabaseUrl(rawDatabaseUrl); 60 + assertTestDatabaseUrl(process.env.DATABASE_URL); 61 + 62 + process.env.NODE_ENV = "test"; 63 + process.env.AUTH_SECRET = "test-secret-with-at-least-32-chars"; 64 + process.env.KANEO_API_URL = "http://localhost:1337"; 65 + process.env.KANEO_CLIENT_URL = "http://localhost:5173"; 66 + process.env.DISABLE_GUEST_ACCESS = "false"; 67 + process.env.DISABLE_REGISTRATION = "false"; 68 + process.env.DISABLE_PASSWORD_REGISTRATION = "false"; 69 + process.env.DEMO_MODE = "false"; 70 + process.env.SMTP_HOST = ""; 71 + process.env.SMTP_PORT = ""; 72 + process.env.SMTP_SECURE = ""; 73 + process.env.SMTP_USER = ""; 74 + process.env.SMTP_PASSWORD = ""; 75 + process.env.GITHUB_CLIENT_ID = ""; 76 + process.env.GITHUB_CLIENT_SECRET = ""; 77 + process.env.GOOGLE_CLIENT_ID = ""; 78 + process.env.GOOGLE_CLIENT_SECRET = ""; 79 + process.env.DISCORD_CLIENT_ID = ""; 80 + process.env.DISCORD_CLIENT_SECRET = ""; 81 + process.env.CUSTOM_OAUTH_CLIENT_ID = ""; 82 + process.env.CUSTOM_OAUTH_CLIENT_SECRET = ""; 83 + process.env.CUSTOM_OAUTH_AUTHORIZATION_URL = ""; 84 + process.env.CUSTOM_OAUTH_TOKEN_URL = ""; 85 + process.env.CUSTOM_OAUTH_USER_INFO_URL = ""; 86 + process.env.CUSTOM_OAUTH_SCOPES = ""; 87 + process.env.CUSTOM_OAUTH_RESPONSE_TYPE = ""; 88 + process.env.CUSTOM_OAUTH_DISCOVERY_URL = ""; 89 + 90 + afterEach(() => { 91 + vi.restoreAllMocks(); 92 + });
+279
tests/api-integration/task.test.ts
··· 1 + import { randomUUID } from "node:crypto"; 2 + import { and, eq } from "drizzle-orm"; 3 + import { beforeEach, describe, expect, it } from "vitest"; 4 + import db, { schema } from "../../apps/api/src/database"; 5 + import { createApp } from "../../apps/api/src/index"; 6 + import { mockAnonymousSession, mockAuthenticatedSession } from "./helpers/auth"; 7 + import { resetTestDatabase } from "./helpers/database"; 8 + import { 9 + createProjectFixture, 10 + createWorkspaceMember, 11 + } from "./helpers/fixtures"; 12 + 13 + describe("API integration: task creation", () => { 14 + beforeEach(async () => { 15 + await resetTestDatabase(); 16 + }); 17 + 18 + it("rejects unauthenticated task creation requests", async () => { 19 + const member = await createWorkspaceMember(); 20 + const { project } = await createProjectFixture({ 21 + workspaceId: member.workspace.id, 22 + }); 23 + 24 + mockAnonymousSession(); 25 + const { app } = createApp(); 26 + 27 + const response = await app.request(`/api/task/${project.id}`, { 28 + method: "POST", 29 + headers: { 30 + "content-type": "application/json", 31 + }, 32 + body: JSON.stringify({ 33 + title: "Unauthorized task", 34 + description: "Should not be created", 35 + priority: "low", 36 + status: "to-do", 37 + }), 38 + }); 39 + 40 + expect(response.status).toBe(401); 41 + await expect(response.text()).resolves.toBe("Unauthorized"); 42 + }); 43 + 44 + it("creates a task with the matching column, assignee, and next number", async () => { 45 + const member = await createWorkspaceMember(); 46 + const { project, columns } = await createProjectFixture({ 47 + workspaceId: member.workspace.id, 48 + name: "Delivery", 49 + slug: "delivery", 50 + }); 51 + 52 + await db.insert(schema.taskTable).values({ 53 + projectId: project.id, 54 + userId: member.user.id, 55 + title: "Existing task", 56 + description: "Already there", 57 + status: "to-do", 58 + columnId: columns.todo.id, 59 + priority: "medium", 60 + number: 1, 61 + position: 1, 62 + }); 63 + 64 + mockAuthenticatedSession(member.user); 65 + const { app } = createApp(); 66 + 67 + const response = await app.request(`/api/task/${project.id}`, { 68 + method: "POST", 69 + headers: { 70 + "content-type": "application/json", 71 + }, 72 + body: JSON.stringify({ 73 + title: "Ship integration flow", 74 + description: "Cover the first create-task path", 75 + priority: "high", 76 + status: "to-do", 77 + userId: member.user.id, 78 + }), 79 + }); 80 + 81 + expect(response.status).toBe(200); 82 + const payload = (await response.json()) as { 83 + id: string; 84 + projectId: string; 85 + title: string; 86 + description: string; 87 + priority: string; 88 + status: string; 89 + userId: string | null; 90 + number: number | null; 91 + position: number | null; 92 + assigneeName?: string; 93 + }; 94 + 95 + expect(payload).toMatchObject({ 96 + projectId: project.id, 97 + title: "Ship integration flow", 98 + description: "Cover the first create-task path", 99 + priority: "high", 100 + status: "to-do", 101 + userId: member.user.id, 102 + number: 2, 103 + position: 2, 104 + assigneeName: member.user.name, 105 + }); 106 + 107 + const persistedTask = await db.query.taskTable.findFirst({ 108 + where: eq(schema.taskTable.id, payload.id), 109 + }); 110 + 111 + expect(persistedTask).toMatchObject({ 112 + id: payload.id, 113 + projectId: project.id, 114 + columnId: columns.todo.id, 115 + userId: member.user.id, 116 + title: "Ship integration flow", 117 + priority: "high", 118 + status: "to-do", 119 + number: 2, 120 + position: 2, 121 + }); 122 + }); 123 + 124 + it("rejects task creation for users outside the project workspace", async () => { 125 + const member = await createWorkspaceMember(); 126 + const outsiderId = `user-${randomUUID()}`; 127 + const { project } = await createProjectFixture({ 128 + workspaceId: member.workspace.id, 129 + }); 130 + 131 + const [outsider] = await db 132 + .insert(schema.userTable) 133 + .values({ 134 + id: outsiderId, 135 + email: `${outsiderId}@example.com`, 136 + emailVerified: true, 137 + name: "Task Outsider", 138 + }) 139 + .returning(); 140 + 141 + mockAuthenticatedSession(outsider); 142 + const { app } = createApp(); 143 + 144 + const response = await app.request(`/api/task/${project.id}`, { 145 + method: "POST", 146 + headers: { 147 + "content-type": "application/json", 148 + }, 149 + body: JSON.stringify({ 150 + title: "Forbidden task", 151 + description: "Should not be created", 152 + priority: "low", 153 + status: "to-do", 154 + }), 155 + }); 156 + 157 + expect(response.status).toBe(403); 158 + await expect(response.text()).resolves.toBe( 159 + "You don't have access to this workspace", 160 + ); 161 + 162 + const persistedTask = await db.query.taskTable.findFirst({ 163 + where: and( 164 + eq(schema.taskTable.projectId, project.id), 165 + eq(schema.taskTable.title, "Forbidden task"), 166 + ), 167 + }); 168 + 169 + expect(persistedTask).toBeUndefined(); 170 + }); 171 + 172 + it("creates an unassigned task with parsed dates when optional fields are provided", async () => { 173 + const member = await createWorkspaceMember(); 174 + const { project, columns } = await createProjectFixture({ 175 + workspaceId: member.workspace.id, 176 + }); 177 + 178 + mockAuthenticatedSession(member.user); 179 + const { app } = createApp(); 180 + 181 + const response = await app.request(`/api/task/${project.id}`, { 182 + method: "POST", 183 + headers: { 184 + "content-type": "application/json", 185 + }, 186 + body: JSON.stringify({ 187 + title: "Plan release cut", 188 + description: "Track optional fields too", 189 + priority: "medium", 190 + status: "in-progress", 191 + startDate: "2026-04-01T09:00:00.000Z", 192 + dueDate: "2026-04-05T17:00:00.000Z", 193 + }), 194 + }); 195 + 196 + expect(response.status).toBe(200); 197 + const payload = (await response.json()) as { 198 + id: string; 199 + userId: string | null; 200 + columnId: string | null; 201 + startDate: string | null; 202 + dueDate: string | null; 203 + assigneeName?: string; 204 + }; 205 + 206 + expect(payload).toMatchObject({ 207 + userId: null, 208 + columnId: columns.inProgress.id, 209 + startDate: "2026-04-01T09:00:00.000Z", 210 + dueDate: "2026-04-05T17:00:00.000Z", 211 + }); 212 + expect(payload.assigneeName).toBeUndefined(); 213 + 214 + const persistedTask = await db.query.taskTable.findFirst({ 215 + where: eq(schema.taskTable.id, payload.id), 216 + }); 217 + 218 + expect(persistedTask).toMatchObject({ 219 + id: payload.id, 220 + userId: null, 221 + columnId: columns.inProgress.id, 222 + status: "in-progress", 223 + }); 224 + expect(persistedTask?.startDate?.toISOString()).toBe( 225 + "2026-04-01T09:00:00.000Z", 226 + ); 227 + expect(persistedTask?.dueDate?.toISOString()).toBe( 228 + "2026-04-05T17:00:00.000Z", 229 + ); 230 + }); 231 + 232 + it("creates tasks without a column when the status has no matching project column", async () => { 233 + const member = await createWorkspaceMember(); 234 + const { project } = await createProjectFixture({ 235 + workspaceId: member.workspace.id, 236 + }); 237 + 238 + mockAuthenticatedSession(member.user); 239 + const { app } = createApp(); 240 + 241 + const response = await app.request(`/api/task/${project.id}`, { 242 + method: "POST", 243 + headers: { 244 + "content-type": "application/json", 245 + }, 246 + body: JSON.stringify({ 247 + title: "Future status task", 248 + description: "Status does not map to a seeded column", 249 + priority: "low", 250 + status: "planned", 251 + }), 252 + }); 253 + 254 + expect(response.status).toBe(200); 255 + const payload = (await response.json()) as { 256 + id: string; 257 + status: string; 258 + columnId: string | null; 259 + position: number | null; 260 + }; 261 + 262 + expect(payload).toMatchObject({ 263 + status: "planned", 264 + columnId: null, 265 + position: 1, 266 + }); 267 + 268 + const persistedTask = await db.query.taskTable.findFirst({ 269 + where: eq(schema.taskTable.id, payload.id), 270 + }); 271 + 272 + expect(persistedTask).toMatchObject({ 273 + id: payload.id, 274 + status: "planned", 275 + columnId: null, 276 + position: 1, 277 + }); 278 + }); 279 + });
+49
tests/api/plugins/github/config.test.ts
··· 1 + import { describe, expect, it } from "vitest"; 2 + import { 3 + branchPatterns, 4 + getDefaultConfig, 5 + validateGitHubConfig, 6 + } from "../../../../apps/api/src/plugins/github/config"; 7 + 8 + describe("github config", () => { 9 + it("exposes the supported branch patterns", () => { 10 + expect(branchPatterns).toContain("{slug}-{number}"); 11 + expect(branchPatterns).toContain("fix/{number}-{title}"); 12 + }); 13 + 14 + it("builds default config values", () => { 15 + expect(getDefaultConfig("usekaneo", "kaneo", 42)).toEqual({ 16 + repositoryOwner: "usekaneo", 17 + repositoryName: "kaneo", 18 + installationId: 42, 19 + branchPattern: "{slug}-{number}", 20 + commentTaskLinkOnGitHubIssue: true, 21 + statusTransitions: { 22 + onBranchPush: "in-progress", 23 + onPROpen: "in-review", 24 + onPRMerge: "done", 25 + }, 26 + }); 27 + }); 28 + 29 + it("validates valid config", async () => { 30 + await expect( 31 + validateGitHubConfig({ 32 + repositoryOwner: "usekaneo", 33 + repositoryName: "kaneo", 34 + installationId: 42, 35 + branchPattern: "{slug}-{number}", 36 + }), 37 + ).resolves.toEqual({ valid: true }); 38 + }); 39 + 40 + it("reports validation errors for invalid config", async () => { 41 + const result = await validateGitHubConfig({ 42 + repositoryOwner: "usekaneo", 43 + installationId: "bad", 44 + }); 45 + 46 + expect(result.valid).toBe(false); 47 + expect(result.errors?.length).toBeGreaterThan(0); 48 + }); 49 + });
+147
tests/api/plugins/github/utils/branch-matcher.test.ts
··· 1 + import { afterEach, describe, expect, it, vi } from "vitest"; 2 + import type { GitHubConfig } from "../../../../../apps/api/src/plugins/github/config"; 3 + import { 4 + createBranchRegex, 5 + extractTaskNumber, 6 + extractTaskNumberFromBranch, 7 + extractTaskNumberFromPRBody, 8 + extractTaskNumberFromPRTitle, 9 + generateBranchName, 10 + } from "../../../../../apps/api/src/plugins/github/utils/branch-matcher"; 11 + 12 + const baseConfig: GitHubConfig = { 13 + repositoryOwner: "kaneo", 14 + repositoryName: "api", 15 + installationId: 1, 16 + branchPattern: "{slug}-{number}", 17 + }; 18 + 19 + afterEach(() => { 20 + vi.restoreAllMocks(); 21 + }); 22 + 23 + describe("generateBranchName", () => { 24 + it("fills placeholders and slugifies the title", () => { 25 + expect( 26 + generateBranchName( 27 + "feature/{slug}-{number}-{title}", 28 + "KAN", 29 + 42, 30 + "Fix login: SSO + invites", 31 + ), 32 + ).toBe("feature/kan-42-fix-login-sso-invites"); 33 + }); 34 + }); 35 + 36 + describe("createBranchRegex", () => { 37 + it("matches default patterns and optional suffixes", () => { 38 + const regex = createBranchRegex("{slug}-{number}-{title}", "KAN"); 39 + 40 + expect(regex.test("kan-7-polish-sidebar")).toBe(true); 41 + expect(regex.test("kan-7-polish-sidebar-part-2")).toBe(true); 42 + expect(regex.test("ops-7-polish-sidebar")).toBe(false); 43 + }); 44 + }); 45 + 46 + describe("extractTaskNumberFromBranch", () => { 47 + it("uses the default branch pattern", () => { 48 + expect( 49 + extractTaskNumberFromBranch("kan-17-refine-search", baseConfig, "KAN"), 50 + ).toBe(17); 51 + }); 52 + 53 + it("supports custom regex patterns", () => { 54 + expect( 55 + extractTaskNumberFromBranch( 56 + "feature/TASK-33", 57 + { 58 + ...baseConfig, 59 + customBranchRegex: "TASK-(\\d+)", 60 + }, 61 + "KAN", 62 + ), 63 + ).toBe(33); 64 + }); 65 + 66 + it("returns null and logs when the custom regex is invalid", () => { 67 + const consoleError = vi 68 + .spyOn(console, "error") 69 + .mockImplementation(() => undefined); 70 + 71 + expect( 72 + extractTaskNumberFromBranch( 73 + "feature/TASK-33", 74 + { 75 + ...baseConfig, 76 + customBranchRegex: "(", 77 + }, 78 + "KAN", 79 + ), 80 + ).toBeNull(); 81 + expect(consoleError).toHaveBeenCalledOnce(); 82 + }); 83 + }); 84 + 85 + describe("extractTaskNumberFromPRTitle", () => { 86 + it("recognizes supported title formats", () => { 87 + expect(extractTaskNumberFromPRTitle("[12] Ship notifications")).toBe(12); 88 + expect(extractTaskNumberFromPRTitle("Fix sidebar (#34)")).toBe(34); 89 + expect(extractTaskNumberFromPRTitle("55: tidy auth flow")).toBe(55); 90 + }); 91 + }); 92 + 93 + describe("extractTaskNumberFromPRBody", () => { 94 + it("recognizes task references in the body", () => { 95 + expect(extractTaskNumberFromPRBody("Closes #21")).toBe(21); 96 + expect(extractTaskNumberFromPRBody("task: 77")).toBe(77); 97 + expect(extractTaskNumberFromPRBody("No linked task")).toBeNull(); 98 + }); 99 + }); 100 + 101 + describe("extractTaskNumber", () => { 102 + it("prefers the branch match before title and body", () => { 103 + expect( 104 + extractTaskNumber( 105 + "kan-88-polish-editor", 106 + "[12] Ship notifications", 107 + "Closes #21", 108 + baseConfig, 109 + "KAN", 110 + ), 111 + ).toBe(88); 112 + }); 113 + 114 + it("falls back to the title and then body", () => { 115 + expect( 116 + extractTaskNumber( 117 + "misc-branch", 118 + "[12] Ship notifications", 119 + "Closes #21", 120 + baseConfig, 121 + "KAN", 122 + ), 123 + ).toBe(12); 124 + 125 + expect( 126 + extractTaskNumber( 127 + "misc-branch", 128 + undefined, 129 + "Resolves task 21", 130 + baseConfig, 131 + "KAN", 132 + ), 133 + ).toBe(21); 134 + }); 135 + 136 + it("accepts task number 0 from the branch before other matches", () => { 137 + expect( 138 + extractTaskNumber( 139 + "kan-0-initial-setup", 140 + "[99] Other task", 141 + undefined, 142 + baseConfig, 143 + "KAN", 144 + ), 145 + ).toBe(0); 146 + }); 147 + });
+44
tests/api/plugins/github/utils/extract-priority.test.ts
··· 1 + import { describe, expect, it } from "vitest"; 2 + import { 3 + extractIssuePriority, 4 + extractIssueStatus, 5 + } from "../../../../../apps/api/src/plugins/github/utils/extract-priority"; 6 + 7 + describe("extractIssuePriority", () => { 8 + it("returns the first valid priority label", () => { 9 + expect( 10 + extractIssuePriority([ 11 + "type:bug", 12 + { name: "priority:high" }, 13 + "priority:low", 14 + ]), 15 + ).toBe("high"); 16 + }); 17 + 18 + it("returns null for missing or malformed priority labels", () => { 19 + expect(extractIssuePriority(undefined)).toBeNull(); 20 + expect(extractIssuePriority(["priority:critical"])).toBeNull(); 21 + expect(extractIssuePriority([{ name: "type:feature" }])).toBeNull(); 22 + }); 23 + 24 + it("treats case-sensitive malformed labels as invalid", () => { 25 + expect( 26 + extractIssuePriority(["Priority:high", "priority:URGENT"]), 27 + ).toBeNull(); 28 + }); 29 + }); 30 + 31 + describe("extractIssueStatus", () => { 32 + it("normalizes whitespace and casing", () => { 33 + expect( 34 + extractIssueStatus(["kind:feature", { name: "status: In-Review " }]), 35 + ).toBe("in-review"); 36 + }); 37 + 38 + it("returns null for missing labels or invalid slugs", () => { 39 + expect(extractIssueStatus(undefined)).toBeNull(); 40 + expect(extractIssueStatus([{ name: "priority:high" }])).toBeNull(); 41 + expect(extractIssueStatus(["status:Needs Review"])).toBeNull(); 42 + expect(extractIssueStatus(["status:review!"])).toBeNull(); 43 + }); 44 + });
+37
tests/api/plugins/github/utils/format.test.ts
··· 1 + import { describe, expect, it } from "vitest"; 2 + import { 3 + formatIssueBody, 4 + formatIssueTitle, 5 + formatSyncComment, 6 + formatTaskDescriptionFromIssue, 7 + getLabelsForIssue, 8 + } from "../../../../../apps/api/src/plugins/github/utils/format"; 9 + 10 + describe("github format helpers", () => { 11 + it("returns the title unchanged", () => { 12 + expect(formatIssueTitle("Ship notifications")).toBe("Ship notifications"); 13 + }); 14 + 15 + it("formats issue bodies with and without a description", () => { 16 + expect(formatIssueBody(null, "task_123")).toBe("<sub>Task: task_123</sub>"); 17 + expect(formatIssueBody("Body text", "task_123")).toBe(`Body text 18 + 19 + --- 20 + <sub>Task: task_123</sub>`); 21 + }); 22 + 23 + it("formats sync comments and task descriptions", () => { 24 + expect(formatSyncComment("task_123")).toBe("Task: task_123"); 25 + expect(formatTaskDescriptionFromIssue("Issue body")).toBe("Issue body"); 26 + expect(formatTaskDescriptionFromIssue(null)).toBe(""); 27 + }); 28 + 29 + it("builds labels while skipping no-priority", () => { 30 + expect(getLabelsForIssue("high", "in-review")).toEqual([ 31 + "priority:high", 32 + "status:in-review", 33 + ]); 34 + expect(getLabelsForIssue("no-priority", "done")).toEqual(["status:done"]); 35 + expect(getLabelsForIssue(null, "planned")).toEqual(["status:planned"]); 36 + }); 37 + });
+87
tests/api/plugins/github/utils/labels.test.ts
··· 1 + import { describe, expect, it, vi } from "vitest"; 2 + import { 3 + addLabelsToIssue, 4 + ensureLabelsExist, 5 + getLabelColor, 6 + removeLabel, 7 + } from "../../../../../apps/api/src/plugins/github/utils/labels"; 8 + 9 + function createOctokitMock() { 10 + return { 11 + rest: { 12 + issues: { 13 + getLabel: vi.fn(), 14 + createLabel: vi.fn(), 15 + addLabels: vi.fn(), 16 + removeLabel: vi.fn(), 17 + }, 18 + }, 19 + }; 20 + } 21 + 22 + describe("github labels helpers", () => { 23 + it("returns explicit and fallback colors", () => { 24 + expect(getLabelColor("priority:urgent")).toBe("EF4444"); 25 + expect(getLabelColor("status:done")).toBe("10B981"); 26 + expect(getLabelColor("custom:label")).toBe("6B7280"); 27 + }); 28 + 29 + it("creates missing labels only when needed", async () => { 30 + const octokit = createOctokitMock(); 31 + octokit.rest.issues.getLabel 32 + .mockResolvedValueOnce({}) 33 + .mockRejectedValueOnce(new Error("missing")); 34 + 35 + await ensureLabelsExist(octokit as never, "usekaneo", "kaneo", [ 36 + "status:done", 37 + "priority:high", 38 + ]); 39 + 40 + expect(octokit.rest.issues.getLabel).toHaveBeenCalledTimes(2); 41 + expect(octokit.rest.issues.createLabel).toHaveBeenCalledTimes(1); 42 + expect(octokit.rest.issues.createLabel).toHaveBeenCalledWith({ 43 + owner: "usekaneo", 44 + repo: "kaneo", 45 + name: "priority:high", 46 + color: "F97316", 47 + }); 48 + }); 49 + 50 + it("adds labels after ensuring they exist", async () => { 51 + const octokit = createOctokitMock(); 52 + octokit.rest.issues.getLabel.mockResolvedValue({}); 53 + 54 + await addLabelsToIssue(octokit as never, "usekaneo", "kaneo", 12, [ 55 + "priority:low", 56 + "status:done", 57 + ]); 58 + 59 + expect(octokit.rest.issues.getLabel).toHaveBeenCalledTimes(2); 60 + expect(octokit.rest.issues.createLabel).not.toHaveBeenCalled(); 61 + expect(octokit.rest.issues.addLabels).toHaveBeenCalledWith({ 62 + owner: "usekaneo", 63 + repo: "kaneo", 64 + issue_number: 12, 65 + labels: ["priority:low", "status:done"], 66 + }); 67 + }); 68 + 69 + it("ignores missing labels during removal", async () => { 70 + const octokit = createOctokitMock(); 71 + octokit.rest.issues.removeLabel.mockRejectedValue({ status: 404 }); 72 + 73 + await expect( 74 + removeLabel(octokit as never, "usekaneo", "kaneo", 21, "status:done"), 75 + ).resolves.toBeUndefined(); 76 + }); 77 + 78 + it("rethrows unexpected label removal errors", async () => { 79 + const octokit = createOctokitMock(); 80 + const error = new Error("gone"); 81 + octokit.rest.issues.removeLabel.mockRejectedValue(error); 82 + 83 + await expect( 84 + removeLabel(octokit as never, "usekaneo", "kaneo", 21, "status:done"), 85 + ).rejects.toThrow("gone"); 86 + }); 87 + });
+101
tests/api/storage/s3.test.ts
··· 1 + import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; 2 + import { 3 + assertTaskImageKeyMatchesContext, 4 + buildObjectKey, 5 + buildObjectKeyPrefix, 6 + getFileExtension, 7 + isImageContentType, 8 + parseBoolean, 9 + parsePositiveInt, 10 + sanitizePathSegment, 11 + validateTaskAssetUploadInput, 12 + } from "../../../apps/api/src/storage/s3"; 13 + 14 + describe("S3 helpers", () => { 15 + const originalMaxSize = process.env.S3_MAX_IMAGE_UPLOAD_BYTES; 16 + 17 + beforeEach(() => { 18 + delete process.env.S3_MAX_IMAGE_UPLOAD_BYTES; 19 + }); 20 + 21 + afterEach(() => { 22 + vi.restoreAllMocks(); 23 + if (originalMaxSize === undefined) { 24 + delete process.env.S3_MAX_IMAGE_UPLOAD_BYTES; 25 + return; 26 + } 27 + 28 + process.env.S3_MAX_IMAGE_UPLOAD_BYTES = originalMaxSize; 29 + }); 30 + 31 + it("recognizes allowed image content types case-insensitively", () => { 32 + expect(isImageContentType("IMAGE/PNG")).toBe(true); 33 + expect(isImageContentType("text/plain")).toBe(false); 34 + }); 35 + 36 + it("parses booleans and positive integers with fallbacks", () => { 37 + expect(parseBoolean(undefined, true)).toBe(true); 38 + expect(parseBoolean(" false ", true)).toBe(false); 39 + expect(parsePositiveInt("42", 10)).toBe(42); 40 + expect(parsePositiveInt("0", 10)).toBe(10); 41 + expect(parsePositiveInt("nope", 10)).toBe(10); 42 + }); 43 + 44 + it("sanitizes path segments and extracts normalized extensions", () => { 45 + expect(sanitizePathSegment(" Release Notes!!.PNG ")).toBe( 46 + "release-notes-.png", 47 + ); 48 + expect(sanitizePathSegment("")).toBe("file"); 49 + expect(getFileExtension("Screenshot.Final.PNG")).toBe("png"); 50 + expect(getFileExtension("README")).toBe("file"); 51 + }); 52 + 53 + it("builds stable key prefixes and keys", () => { 54 + vi.spyOn(Date, "now").mockReturnValue(1_717_171_717_000); 55 + 56 + const key = buildObjectKey({ 57 + workspaceId: "Workspace 1", 58 + projectId: "Project 2", 59 + taskId: "Task 3", 60 + surface: "comment", 61 + filename: "Sprint Plan Final!!.PNG", 62 + contentType: "image/png", 63 + }); 64 + 65 + expect( 66 + buildObjectKeyPrefix({ 67 + workspaceId: "Workspace 1", 68 + projectId: "Project 2", 69 + taskId: "Task 3", 70 + surface: "comment", 71 + }), 72 + ).toBe("workspace/workspace-1/project/project-2/task/task-3/comments"); 73 + 74 + expect(key).toMatch( 75 + /^workspace\/workspace-1\/project\/project-2\/task\/task-3\/comments\/sprint-plan-final-1717171717000-[a-z0-9]+\.png$/, 76 + ); 77 + expect( 78 + assertTaskImageKeyMatchesContext(key, { 79 + workspaceId: "Workspace 1", 80 + projectId: "Project 2", 81 + taskId: "Task 3", 82 + surface: "comment", 83 + }), 84 + ).toBe(true); 85 + }); 86 + 87 + it("validates upload size against the configured maximum", () => { 88 + process.env.S3_MAX_IMAGE_UPLOAD_BYTES = "1048576"; 89 + 90 + expect(() => validateTaskAssetUploadInput("", 10)).toThrow( 91 + "A valid content type is required.", 92 + ); 93 + expect(() => validateTaskAssetUploadInput("image/png", 0)).toThrow( 94 + "Upload size must be greater than zero.", 95 + ); 96 + expect(() => 97 + validateTaskAssetUploadInput("image/png", 2 * 1024 * 1024), 98 + ).toThrow("Upload exceeds the maximum upload size of 1MB."); 99 + expect(() => validateTaskAssetUploadInput("image/png", 512)).not.toThrow(); 100 + }); 101 + });
+22
tests/api/utils/generate-demo-name.test.ts
··· 1 + import { afterEach, describe, expect, it, vi } from "vitest"; 2 + import { generateDemoName } from "../../../apps/api/src/utils/generate-demo-name"; 3 + 4 + afterEach(() => { 5 + vi.restoreAllMocks(); 6 + }); 7 + 8 + describe("generateDemoName", () => { 9 + it("returns an adjective-animal slug", () => { 10 + vi.spyOn(Math, "random").mockReturnValueOnce(0).mockReturnValueOnce(0); 11 + 12 + expect(generateDemoName()).toBe("fractious-monkfish"); 13 + }); 14 + 15 + it("stays hyphenated for later values too", () => { 16 + vi.spyOn(Math, "random") 17 + .mockReturnValueOnce(0.9999) 18 + .mockReturnValueOnce(0.9999); 19 + 20 + expect(generateDemoName()).toBe("dynamic-lion"); 21 + }); 22 + });
+249
tests/api/utils/openapi-spec.test.ts
··· 1 + import { describe, expect, it } from "vitest"; 2 + import { 3 + dedupeOperationIds, 4 + ensureOperationSummaries, 5 + markOptionalSchemaFieldsNullable, 6 + mergeOpenApiSpecs, 7 + normalizeApiServerUrl, 8 + normalizeEmptyRequiredArrays, 9 + normalizeNullableSchemasForOpenApi30, 10 + normalizeOrganizationAuthOperations, 11 + } from "../../../apps/api/src/utils/openapi-spec"; 12 + 13 + describe("openapi spec helpers", () => { 14 + it("normalizes API server urls", () => { 15 + expect(normalizeApiServerUrl("https://api.kaneo.app")).toBe( 16 + "https://api.kaneo.app/api", 17 + ); 18 + expect(normalizeApiServerUrl("https://api.kaneo.app/api/")).toBe( 19 + "https://api.kaneo.app/api", 20 + ); 21 + }); 22 + 23 + it("normalizes organization auth operations and prunes components", () => { 24 + const authSpec = { 25 + paths: { 26 + "/organization/list-members": { 27 + get: { 28 + operationId: "oldId", 29 + summary: "Old summary", 30 + responses: { 31 + 200: { 32 + content: { 33 + "application/json": { 34 + schema: { $ref: "#/components/schemas/MemberList" }, 35 + }, 36 + }, 37 + }, 38 + }, 39 + }, 40 + }, 41 + "/session/get": { 42 + get: { 43 + operationId: "ignored", 44 + }, 45 + }, 46 + }, 47 + security: [{ bearerAuth: [] }], 48 + components: { 49 + securitySchemes: { 50 + bearerAuth: { type: "http", scheme: "bearer" }, 51 + }, 52 + schemas: { 53 + MemberList: { 54 + type: "object", 55 + properties: { 56 + data: { 57 + $ref: "#/components/schemas/Member", 58 + }, 59 + }, 60 + }, 61 + Member: { 62 + type: "object", 63 + properties: { 64 + id: { type: "string" }, 65 + }, 66 + }, 67 + Ignored: { 68 + type: "object", 69 + }, 70 + }, 71 + }, 72 + }; 73 + 74 + const normalized = normalizeOrganizationAuthOperations(authSpec); 75 + 76 + expect(Object.keys(normalized.paths as Record<string, unknown>)).toEqual([ 77 + "/auth/organization/list-members", 78 + ]); 79 + 80 + const operation = ( 81 + normalized.paths as Record< 82 + string, 83 + Record<string, Record<string, unknown>> 84 + > 85 + )["/auth/organization/list-members"].get; 86 + 87 + expect(operation.operationId).toBe("listOrganizationMembers"); 88 + expect(operation.summary).toBe("List Organization Members"); 89 + expect(operation.tags).toEqual(["Organization Management"]); 90 + 91 + const schemaNames = Object.keys( 92 + ( 93 + normalized.components as { 94 + schemas?: Record<string, unknown>; 95 + } 96 + ).schemas || {}, 97 + ); 98 + expect(schemaNames).toEqual(["MemberList", "Member"]); 99 + }); 100 + 101 + it("merges hono and auth specs", () => { 102 + const merged = mergeOpenApiSpecs( 103 + { 104 + openapi: "3.1.0", 105 + info: { title: "API" }, 106 + paths: { "/tasks": { get: { operationId: "getTasks" } } }, 107 + tags: [{ name: "Tasks" }], 108 + components: { 109 + schemas: { Task: { type: "object" } }, 110 + }, 111 + }, 112 + { 113 + paths: { "/auth/session": { get: { operationId: "getSession" } } }, 114 + tags: [{ name: "Auth" }], 115 + components: { 116 + securitySchemes: { bearerAuth: { type: "http" } }, 117 + schemas: { Session: { type: "object" } }, 118 + }, 119 + }, 120 + ); 121 + 122 + expect(merged.openapi).toBe("3.1.0"); 123 + expect(Object.keys(merged.paths)).toEqual(["/tasks", "/auth/session"]); 124 + expect(merged.tags).toEqual([{ name: "Tasks" }, { name: "Auth" }]); 125 + expect(merged.components.schemas).toEqual({ 126 + Task: { type: "object" }, 127 + Session: { type: "object" }, 128 + }); 129 + expect(merged.components.securitySchemes).toEqual({ 130 + bearerAuth: { type: "http" }, 131 + }); 132 + }); 133 + 134 + it("dedupes operation ids using method and path", () => { 135 + const spec = dedupeOperationIds({ 136 + paths: { 137 + "/tasks": { 138 + get: { operationId: "getTask" }, 139 + }, 140 + "/tasks/{id}": { 141 + get: { operationId: "getTask" }, 142 + }, 143 + }, 144 + }); 145 + 146 + expect( 147 + (spec.paths as Record<string, Record<string, { operationId: string }>>)[ 148 + "/tasks/{id}" 149 + ].get.operationId, 150 + ).toBe("getTask_get_tasks_id"); 151 + }); 152 + 153 + it("normalizes nullable schemas and empty required arrays", () => { 154 + const spec = normalizeEmptyRequiredArrays( 155 + normalizeNullableSchemasForOpenApi30({ 156 + components: { 157 + schemas: { 158 + Example: { 159 + type: ["string", "null"], 160 + required: [], 161 + }, 162 + ExampleAnyOf: { 163 + anyOf: [{ type: "null" }, { type: "number", minimum: 1 }], 164 + }, 165 + }, 166 + }, 167 + }), 168 + ); 169 + 170 + expect( 171 + ( 172 + spec.components as { 173 + schemas: Record<string, Record<string, unknown>>; 174 + } 175 + ).schemas.Example, 176 + ).toEqual({ 177 + type: "string", 178 + nullable: true, 179 + }); 180 + 181 + expect( 182 + ( 183 + spec.components as { 184 + schemas: Record<string, Record<string, unknown>>; 185 + } 186 + ).schemas.ExampleAnyOf, 187 + ).toEqual({ 188 + type: "number", 189 + minimum: 1, 190 + nullable: true, 191 + }); 192 + }); 193 + 194 + it("marks optional schema fields nullable and fills missing summaries", () => { 195 + const spec = ensureOperationSummaries( 196 + markOptionalSchemaFieldsNullable({ 197 + paths: { 198 + "/tasks": { 199 + get: { 200 + operationId: "listWorkspaceTasks", 201 + }, 202 + }, 203 + }, 204 + components: { 205 + schemas: { 206 + Task: { 207 + type: "object", 208 + required: ["id"], 209 + properties: { 210 + id: { type: "string" }, 211 + title: { type: "string" }, 212 + estimate: { type: "number", nullable: true }, 213 + }, 214 + }, 215 + }, 216 + }, 217 + }), 218 + ); 219 + 220 + expect( 221 + ( 222 + spec.components as { 223 + schemas: Record< 224 + string, 225 + { properties: Record<string, Record<string, unknown>> } 226 + >; 227 + } 228 + ).schemas.Task.properties.title.nullable, 229 + ).toBe(true); 230 + expect( 231 + ( 232 + spec.components as { 233 + schemas: Record< 234 + string, 235 + { properties: Record<string, Record<string, unknown>> } 236 + >; 237 + } 238 + ).schemas.Task.properties.id.nullable, 239 + ).toBeUndefined(); 240 + expect( 241 + ( 242 + spec.paths as Record< 243 + string, 244 + Record<string, { summary?: string; operationId: string }> 245 + > 246 + )["/tasks"].get.summary, 247 + ).toBe("List Workspace Tasks"); 248 + }); 249 + });
+14
tests/api/utils/to-normal-case.test.ts
··· 1 + import { describe, expect, it } from "vitest"; 2 + import toNormalCase from "../../../apps/api/src/utils/to-normal-case"; 3 + 4 + describe("toNormalCase", () => { 5 + it("normalizes kebab and snake case", () => { 6 + expect(toNormalCase("in-progress")).toBe("In Progress"); 7 + expect(toNormalCase("custom_oauth_status")).toBe("Custom Oauth Status"); 8 + }); 9 + 10 + it("returns falsy input as-is", () => { 11 + expect(toNormalCase(undefined)).toBeUndefined(); 12 + expect(toNormalCase("")).toBe(""); 13 + }); 14 + });
+5
turbo.json
··· 15 15 "lint": { 16 16 "cache": false, 17 17 "persistent": true 18 + }, 19 + "test": { 20 + "dependsOn": ["^test"], 21 + "cache": false, 22 + "outputs": ["coverage/**"] 18 23 } 19 24 } 20 25 }