From 1c02ce441bf090a82174880a01fe8596eed5b4a4 Mon Sep 17 00:00:00 2001 From: zen0 Date: Thu, 25 Dec 2025 21:55:33 +0800 Subject: [PATCH 1/3] chore: updating pages --- .env.example | 15 +- .prettierignore | 20 + .prettierrc | 12 + docker/docker-compose.yml | 6 +- eslint.config.mjs | 202 ++ package.json | 13 + packages/kal-backend/package.json | 7 + packages/kal-backend/src/index.ts | 35 +- packages/kal-backend/src/lib/context.ts | 92 +- packages/kal-backend/src/lib/db.ts | 3 +- packages/kal-backend/src/lib/logto.ts | 20 + packages/kal-backend/src/lib/trpc.ts | 3 +- .../kal-backend/src/middleware/rate-limit.ts | 150 + packages/kal-backend/src/routers/api.ts | 232 +- packages/kal-backend/src/routers/food.ts | 24 +- packages/kal-backend/src/routers/halal.ts | 135 + packages/kal-backend/src/routers/index.ts | 4 + .../20241225000001_add_user_tiers.js | 41 + .../20241225000002_rate_limit_indexes.js | 40 + packages/kal-db/package.json | 2 + packages/kal-db/scripts/seed-safe.ts | 81 +- packages/kal-db/scripts/seed.ts | 76 +- packages/kal-frontend/.env.example | 9 +- packages/kal-frontend/package.json | 14 +- .../kal-frontend/src/app/api-docs/client.tsx | 572 ++++ .../kal-frontend/src/app/api-docs/page.tsx | 346 +-- .../kal-frontend/src/app/callback/route.ts | 11 + packages/kal-frontend/src/app/globals.css | 65 + packages/kal-frontend/src/app/layout.tsx | 1 + packages/kal-frontend/src/app/page.tsx | 12 +- packages/kal-frontend/src/app/search/page.tsx | 382 ++- .../src/components/landing/CTA.tsx | 2 +- .../src/components/landing/FAQ.tsx | 1 + .../src/components/landing/Footer.tsx | 1 + .../src/components/landing/Navbar.tsx | 3 +- .../src/components/landing/SampleFoods.tsx | 2 +- .../kal-frontend/src/components/navbar.tsx | 37 + .../kal-frontend/src/components/sign-in.tsx | 16 + .../kal-frontend/src/components/sign-out.tsx | 16 + packages/kal-frontend/src/lib/logto.ts | 10 + .../kal-frontend/src/lib/trpc-provider.tsx | 3 +- packages/kal-frontend/src/lib/trpc.ts | 2 +- packages/kal-shared/package.json | 2 + packages/kal-shared/src/types/index.ts | 42 + pnpm-lock.yaml | 2617 ++++++++++++++++- turbo.json | 4 + 46 files changed, 4807 insertions(+), 576 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 eslint.config.mjs create mode 100644 packages/kal-backend/src/lib/logto.ts create mode 100644 packages/kal-backend/src/middleware/rate-limit.ts create mode 100644 packages/kal-backend/src/routers/halal.ts create mode 100644 packages/kal-db/migrations/20241225000001_add_user_tiers.js create mode 100644 packages/kal-db/migrations/20241225000002_rate_limit_indexes.js create mode 100644 packages/kal-frontend/src/app/api-docs/client.tsx create mode 100644 packages/kal-frontend/src/app/callback/route.ts create mode 100644 packages/kal-frontend/src/components/navbar.tsx create mode 100644 packages/kal-frontend/src/components/sign-in.tsx create mode 100644 packages/kal-frontend/src/components/sign-out.tsx create mode 100644 packages/kal-frontend/src/lib/logto.ts diff --git a/.env.example b/.env.example index c20fca2..b9e61a1 100644 --- a/.env.example +++ b/.env.example @@ -10,10 +10,12 @@ MONGODB_DATABASE=kalori_db # =================== # Logto Auth (Local Development) # =================== -LOGTO_ENDPOINT=http://localhost:3301 -LOGTO_ADMIN_ENDPOINT=http://localhost:3302 -LOGTO_PORT=3301 -LOGTO_ADMIN_PORT=3302 +LOGTO_ENDPOINT=http://localhost:3001 +LOGTO_ADMIN_ENDPOINT=http://localhost:3002 + +# Logto App Credentials (from Admin Console) +LOGTO_APP_ID=8hr723agl0aw142cbag0g +LOGTO_APP_SECRET=0mMmpWv4g3HklArGNM1XuSREJyRMjFyf # Logto Database (Postgres - Local) LOGTO_DB_USER=logto @@ -25,7 +27,12 @@ LOGTO_DB_NAME=logto # =================== NEXT_PUBLIC_API_URL=http://localhost:4000 BACKEND_PORT=4000 +BACKEND_BASE_URL=http://localhost:4000 FRONTEND_PORT=3000 +FRONTEND_URL=http://localhost:3000 + +# Session Secret (for express-session) +SESSION_SECRET=JdD5Kts7wv3xco9dHKPmYjhHaQmYM3sQ # ============================================ # PRODUCTION (VPS 72.62.74.47) - Copy to .env.production diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..67bb255 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,20 @@ +# Dependencies +node_modules + +# Build outputs +dist +.next +build +coverage + +# Turbo +.turbo + +# Lock files +pnpm-lock.yaml +package-lock.json +yarn.lock + +# Generated files +*.min.js +*.min.css diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..ffd0ca2 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,12 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": false, + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "always", + "endOfLine": "lf" +} diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index a8ab4bf..a9c62dc 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -37,13 +37,11 @@ services: container_name: kal-logto entrypoint: ["sh", "-c", "npm run cli db seed -- --swe && npm start"] ports: - - "${LOGTO_PORT:-3301}:3001" - - "${LOGTO_ADMIN_PORT:-3302}:3002" + - "3001:3001" + - "3002:3002" environment: - TRUST_PROXY_HEADER=1 - DB_URL=postgres://${LOGTO_DB_USER}:${LOGTO_DB_PASSWORD}@postgres:5432/${LOGTO_DB_NAME} - - ENDPOINT=${LOGTO_ENDPOINT} - - ADMIN_ENDPOINT=${LOGTO_ADMIN_ENDPOINT} depends_on: postgres: condition: service_healthy diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..b1fa5b6 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,202 @@ +import eslint from "@eslint/js"; +import tseslint from "@typescript-eslint/eslint-plugin"; +import tsparser from "@typescript-eslint/parser"; +import importPlugin from "eslint-plugin-import"; +import reactPlugin from "eslint-plugin-react"; +import reactHooksPlugin from "eslint-plugin-react-hooks"; +import prettierConfig from "eslint-config-prettier"; +import globals from "globals"; + +export default [ + // Global ignores + { + ignores: [ + "**/node_modules/**", + "**/dist/**", + "**/.next/**", + "**/build/**", + "**/.turbo/**", + "**/coverage/**", + "**/*.config.js", + "**/*.config.mjs", + "**/*.config.cjs", + "**/migrate-mongo-config.js", + "**/migrate-mongo-config.cjs", + "**/migrations/**/*.js", + "**/postcss.config.js", + "**/tailwind.config.ts", + ], + }, + + // Base ESLint recommended rules + eslint.configs.recommended, + + // TypeScript files configuration (backend/shared - Node.js) + { + files: [ + "packages/kal-backend/**/*.ts", + "packages/kal-db/**/*.ts", + "packages/kal-shared/**/*.ts", + ], + languageOptions: { + parser: tsparser, + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + }, + globals: { + ...globals.node, + ...globals.es2021, + }, + }, + plugins: { + "@typescript-eslint": tseslint, + import: importPlugin, + }, + rules: { + // TypeScript rules + "@typescript-eslint/no-unused-vars": [ + "warn", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/consistent-type-imports": [ + "warn", + { prefer: "type-imports" }, + ], + + // General rules + "no-console": "off", + "no-debugger": "warn", + "no-unused-vars": "off", + + // Import rules + "import/order": [ + "warn", + { + groups: [ + "builtin", + "external", + "internal", + "parent", + "sibling", + "index", + ], + "newlines-between": "always", + alphabetize: { order: "asc", caseInsensitive: true }, + }, + ], + "import/no-duplicates": "warn", + }, + }, + + // React/Next.js TypeScript files (frontend) + { + files: ["packages/kal-frontend/**/*.ts", "packages/kal-frontend/**/*.tsx"], + languageOptions: { + parser: tsparser, + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + ecmaFeatures: { + jsx: true, + }, + }, + globals: { + ...globals.browser, + ...globals.node, + ...globals.es2021, + React: "readonly", + }, + }, + plugins: { + "@typescript-eslint": tseslint, + import: importPlugin, + react: reactPlugin, + "react-hooks": reactHooksPlugin, + }, + settings: { + react: { + version: "detect", + }, + }, + rules: { + // TypeScript rules + "@typescript-eslint/no-unused-vars": [ + "warn", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/consistent-type-imports": [ + "warn", + { prefer: "type-imports" }, + ], + + // General rules + "no-console": ["warn", { allow: ["warn", "error", "info"] }], + "no-debugger": "warn", + "no-unused-vars": "off", + + // React rules + "react/react-in-jsx-scope": "off", + "react/prop-types": "off", + "react/jsx-uses-react": "off", + "react/jsx-uses-vars": "error", + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "warn", + + // Import rules + "import/order": [ + "warn", + { + groups: [ + "builtin", + "external", + "internal", + "parent", + "sibling", + "index", + ], + "newlines-between": "always", + alphabetize: { order: "asc", caseInsensitive: true }, + }, + ], + "import/no-duplicates": "warn", + }, + }, + + // JavaScript files configuration + { + files: ["**/*.js", "**/*.mjs", "**/*.cjs"], + languageOptions: { + ecmaVersion: "latest", + sourceType: "module", + globals: { + ...globals.node, + ...globals.browser, + ...globals.es2021, + }, + }, + rules: { + "no-unused-vars": [ + "warn", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + }, + ], + "no-console": "off", + }, + }, + + // Disable formatting rules (handled by Prettier) + prettierConfig, +]; diff --git a/package.json b/package.json index ce502d0..926f605 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,9 @@ "dev": "turbo dev", "build": "turbo build", "lint": "turbo lint", + "lint:fix": "turbo lint:fix", + "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md,css}\"", + "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md,css}\"", "typecheck": "turbo typecheck", "clean": "turbo clean", "docker:up": "docker compose --env-file .env -f docker/docker-compose.yml up -d", @@ -15,6 +18,16 @@ "db:seed": "pnpm --filter kal-db run seed" }, "devDependencies": { + "@eslint/js": "^9.39.2", + "@typescript-eslint/eslint-plugin": "^8.50.1", + "@typescript-eslint/parser": "^8.50.1", + "eslint": "^9.39.2", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.0.1", + "globals": "^16.5.0", + "prettier": "^3.7.4", "turbo": "^2.3.3", "typescript": "^5.7.2" } diff --git a/packages/kal-backend/package.json b/packages/kal-backend/package.json index 5b4b236..7fb24b2 100644 --- a/packages/kal-backend/package.json +++ b/packages/kal-backend/package.json @@ -8,22 +8,29 @@ "dev": "tsx watch src/index.ts", "build": "tsc", "start": "node dist/index.js", + "lint": "eslint .", + "lint:fix": "eslint . --fix", "typecheck": "tsc --noEmit", "clean": "rm -rf dist" }, "dependencies": { + "@logto/express": "^3.0.12", "@trpc/server": "^11.0.0-rc.682", + "cookie-parser": "^1.4.7", "cors": "^2.8.5", "dotenv": "^16.4.7", "express": "^4.21.2", + "express-session": "^1.18.2", "kal-shared": "workspace:*", "mongodb": "^6.12.0", "swagger-ui-express": "^5.0.1", "zod": "^3.24.1" }, "devDependencies": { + "@types/cookie-parser": "^1.4.10", "@types/cors": "^2.8.17", "@types/express": "^5.0.0", + "@types/express-session": "^1.18.2", "@types/node": "^22.10.2", "@types/swagger-ui-express": "^4.1.8", "tsx": "^4.19.2", diff --git a/packages/kal-backend/src/index.ts b/packages/kal-backend/src/index.ts index 3d6dfeb..a452580 100644 --- a/packages/kal-backend/src/index.ts +++ b/packages/kal-backend/src/index.ts @@ -1,11 +1,17 @@ import "dotenv/config"; -import express from "express"; -import cors from "cors"; +import { handleAuthRoutes } from "@logto/express"; import { createExpressMiddleware } from "@trpc/server/adapters/express"; -import { appRouter } from "./routers/index.js"; -import { apiRouter } from "./routers/api.js"; +import cookieParser from "cookie-parser"; +import cors from "cors"; +import express from "express"; +import session from "express-session"; + import { createContext } from "./lib/context.js"; -import { connectDB } from "./lib/db.js"; +import { connectDB, getDB } from "./lib/db.js"; +import { logtoConfig, validateLogtoConfig } from "./lib/logto.js"; +import { checkRateLimit, getRateLimitHeaders } from "./middleware/rate-limit.js"; +import { apiRouter } from "./routers/api.js"; +import { appRouter } from "./routers/index.js"; const PORT = process.env.BACKEND_PORT || 3000; @@ -87,9 +93,26 @@ async function main() { const app = express(); - // Middleware + // Core middleware app.use(cors()); app.use(express.json()); + app.use(cookieParser()); + + // Session middleware (required for Logto) + app.use( + session({ + secret: process.env.SESSION_SECRET || "dev-secret-change-in-production", + cookie: { maxAge: 14 * 24 * 60 * 60 * 1000 }, // 14 days + resave: false, + saveUninitialized: false, + }) + ); + + // Logto auth routes (if configured) + if (validateLogtoConfig()) { + app.use(handleAuthRoutes(logtoConfig)); + console.log("✅ Logto auth routes registered"); + } // Health check app.get("/health", (_, res) => { diff --git a/packages/kal-backend/src/lib/context.ts b/packages/kal-backend/src/lib/context.ts index 1cff00d..a4b16c7 100644 --- a/packages/kal-backend/src/lib/context.ts +++ b/packages/kal-backend/src/lib/context.ts @@ -1,15 +1,91 @@ -import { inferAsyncReturnType } from "@trpc/server"; -import { CreateExpressContextOptions } from "@trpc/server/adapters/express"; +import type { inferAsyncReturnType } from "@trpc/server"; +import type { CreateExpressContextOptions } from "@trpc/server/adapters/express"; +import type { Request } from "express"; +import type { User, UserTier, LogtoUserInfo } from "kal-shared"; +import type { Db } from "mongodb"; + import { getDB } from "./db.js"; -export async function createContext({ req }: CreateExpressContextOptions) { - // TODO: Add Logto authentication - // For now, return a mock user for development - const userId = req.headers["x-user-id"] as string | undefined; +// Logto user info from request (after session auth) +type LogtoRequestUser = { + claims?: LogtoUserInfo; +}; + +/** + * Sync user from Logto to MongoDB on first login or update + */ +async function syncUserFromLogto( + db: Db, + claims: LogtoUserInfo +): Promise { + if (!claims.sub) return null; + + const now = new Date(); + const usersCollection = db.collection("users"); + + // Upsert user - create if not exists, update if exists + const result = await usersCollection.findOneAndUpdate( + { logtoId: claims.sub }, + { + $set: { + email: claims.email || "", + name: claims.name, + updatedAt: now, + }, + $setOnInsert: { + logtoId: claims.sub, + tier: "free" as UserTier, + createdAt: now, + }, + }, + { upsert: true, returnDocument: "after" } + ); + + return result as User | null; +} + +/** + * Get user by ID from header (for API key / development) + */ +async function getUserById(db: Db, userId: string): Promise { + const { ObjectId } = await import("mongodb"); + try { + return await db + .collection("users") + .findOne({ _id: new ObjectId(userId) as any }); + } catch { + return null; + } +} + +export async function createContext({ + req, +}: CreateExpressContextOptions): Promise<{ + db: Db; + user: User | null; + userId: string | undefined; +}> { + const db = getDB(); + let user: User | null = null; + + // Try Logto session first (user is added by @logto/express middleware) + const logtoUser = (req as Request & { user?: LogtoRequestUser }).user; + if (logtoUser?.claims?.sub) { + user = await syncUserFromLogto(db, logtoUser.claims); + } + + // Fallback to header-based auth (for API keys or development) + if (!user) { + const headerUserId = req.headers["x-user-id"] as string | undefined; + if (headerUserId) { + user = await getUserById(db, headerUserId); + } + } return { - db: getDB(), - userId, // Will be populated after Logto auth + db, + user, + userId: user?._id?.toString(), }; } diff --git a/packages/kal-backend/src/lib/db.ts b/packages/kal-backend/src/lib/db.ts index 758d244..d97d56f 100644 --- a/packages/kal-backend/src/lib/db.ts +++ b/packages/kal-backend/src/lib/db.ts @@ -1,4 +1,5 @@ -import { MongoClient, Db } from "mongodb"; +import type { Db } from "mongodb"; +import { MongoClient } from "mongodb"; // Support both individual env vars and a full DATABASE_URL const getDatabaseUri = () => { diff --git a/packages/kal-backend/src/lib/logto.ts b/packages/kal-backend/src/lib/logto.ts new file mode 100644 index 0000000..083405c --- /dev/null +++ b/packages/kal-backend/src/lib/logto.ts @@ -0,0 +1,20 @@ +import type { LogtoExpressConfig } from "@logto/express"; + +// Logto configuration for Express integration +export const logtoConfig: LogtoExpressConfig = { + appId: process.env.LOGTO_APP_ID || "", + appSecret: process.env.LOGTO_APP_SECRET || "", + endpoint: process.env.LOGTO_ENDPOINT || "http://localhost:3001", + baseUrl: process.env.BACKEND_BASE_URL || "http://localhost:4000", +}; + +// Validate Logto config +export function validateLogtoConfig(): boolean { + if (!logtoConfig.appId || !logtoConfig.appSecret) { + console.warn( + "⚠️ Logto authentication not configured. Set LOGTO_APP_ID and LOGTO_APP_SECRET." + ); + return false; + } + return true; +} diff --git a/packages/kal-backend/src/lib/trpc.ts b/packages/kal-backend/src/lib/trpc.ts index 975d18a..2e53294 100644 --- a/packages/kal-backend/src/lib/trpc.ts +++ b/packages/kal-backend/src/lib/trpc.ts @@ -1,5 +1,6 @@ import { initTRPC, TRPCError } from "@trpc/server"; -import { Context } from "./context.js"; + +import type { Context } from "./context.js"; const t = initTRPC.context().create(); diff --git a/packages/kal-backend/src/middleware/rate-limit.ts b/packages/kal-backend/src/middleware/rate-limit.ts new file mode 100644 index 0000000..e15d61f --- /dev/null +++ b/packages/kal-backend/src/middleware/rate-limit.ts @@ -0,0 +1,150 @@ +import type { UserTier, RateLimitUsage } from "kal-shared"; +import { RATE_LIMITS } from "kal-shared"; +import type { Db } from "mongodb"; + +export interface RateLimitResult { + limited: boolean; + retryAfter?: number; // seconds until retry allowed + dailyCount?: number; + dailyLimit?: number; + minuteCount?: number; + burstLimit?: number; +} + +/** + * Check rate limits for a user based on their tier. + * Uses soft throttling - returns 429 with Retry-After, not permanent bans. + */ +export async function checkRateLimit( + db: Db, + userId: string, + tier: UserTier = "free" +): Promise { + const limits = RATE_LIMITS[tier]; + const now = new Date(); + const today = now.toISOString().split("T")[0]; // YYYY-MM-DD + const minuteStart = new Date(now); + minuteStart.setSeconds(0, 0); + + const collection = db.collection("rate_limit_usage"); + + // Get current usage + let usage = await collection.findOne({ userId, date: today }); + + // Check if we're in a new minute window + const isNewMinute = + !usage?.minuteWindow || + new Date(usage.minuteWindow).getTime() < minuteStart.getTime(); + + // Update or create usage record + if (!usage) { + // First request of the day + usage = { + _id: "" as any, + userId, + date: today, + dailyCount: 1, + minuteWindow: minuteStart, + minuteCount: 1, + updatedAt: now, + }; + await collection.insertOne(usage as any); + } else if (isNewMinute) { + // New minute - reset minute counter + await collection.updateOne( + { userId, date: today }, + { + $inc: { dailyCount: 1 }, + $set: { + minuteWindow: minuteStart, + minuteCount: 1, + updatedAt: now, + }, + } + ); + usage.dailyCount += 1; + usage.minuteCount = 1; + } else { + // Same minute - increment both counters + await collection.updateOne( + { userId, date: today }, + { + $inc: { dailyCount: 1, minuteCount: 1 }, + $set: { updatedAt: now }, + } + ); + usage.dailyCount += 1; + usage.minuteCount += 1; + } + + // Check daily limit (soft throttle until midnight) + if (usage.dailyCount > limits.dailyLimit) { + const secondsUntilMidnight = getSecondsUntilMidnight(); + return { + limited: true, + retryAfter: secondsUntilMidnight, + dailyCount: usage.dailyCount, + dailyLimit: limits.dailyLimit, + }; + } + + // Check burst limit (soft throttle for remaining seconds in minute) + if (usage.minuteCount > limits.burstLimit) { + const secondsInMinute = now.getSeconds(); + const retryAfter = 60 - secondsInMinute; + return { + limited: true, + retryAfter, + minuteCount: usage.minuteCount, + burstLimit: limits.burstLimit, + }; + } + + return { + limited: false, + dailyCount: usage.dailyCount, + dailyLimit: limits.dailyLimit, + minuteCount: usage.minuteCount, + burstLimit: limits.burstLimit, + }; +} + +/** + * Get seconds until midnight (UTC) + */ +function getSecondsUntilMidnight(): number { + const now = new Date(); + const midnight = new Date(now); + midnight.setUTCHours(24, 0, 0, 0); + return Math.ceil((midnight.getTime() - now.getTime()) / 1000); +} + +/** + * Format rate limit headers for response + */ +export function getRateLimitHeaders( + result: RateLimitResult, + tier: UserTier = "free" +): Record { + const limits = RATE_LIMITS[tier]; + const headers: Record = { + "X-RateLimit-Limit-Daily": String(limits.dailyLimit), + "X-RateLimit-Limit-Burst": String(limits.burstLimit), + }; + + if (result.dailyCount !== undefined) { + headers["X-RateLimit-Remaining-Daily"] = String( + Math.max(0, limits.dailyLimit - result.dailyCount) + ); + } + if (result.minuteCount !== undefined) { + headers["X-RateLimit-Remaining-Burst"] = String( + Math.max(0, limits.burstLimit - result.minuteCount) + ); + } + if (result.retryAfter !== undefined) { + headers["Retry-After"] = String(result.retryAfter); + } + + return headers; +} diff --git a/packages/kal-backend/src/routers/api.ts b/packages/kal-backend/src/routers/api.ts index 31cd1f0..73a69f6 100644 --- a/packages/kal-backend/src/routers/api.ts +++ b/packages/kal-backend/src/routers/api.ts @@ -1,15 +1,16 @@ import { Router, type Router as RouterType } from "express"; + import { getDB } from "../lib/db.js"; const router: RouterType = Router(); // ============================================ -// Malaysian Food Database - Public REST API +// Natural Foods - Public REST API // ============================================ /** * GET /api/foods/search - * Search foods by name + * Search natural foods by name * Query params: q (required) - search query */ router.get("/foods/search", async (req, res) => { @@ -25,7 +26,7 @@ router.get("/foods/search", async (req, res) => { const db = getDB(); const foods = await db - .collection("foods") + .collection("natural_foods") .find({ name: { $regex: q, $options: "i" } }) .limit(20) .toArray(); @@ -55,7 +56,7 @@ router.get("/foods/search", async (req, res) => { /** * GET /api/foods - * Get all foods or filter by category + * Get all natural foods or filter by category * Query params: * - category (optional) - filter by category * - limit (optional) - max results (default: 50, max: 200) @@ -73,12 +74,12 @@ router.get("/foods", async (req, res) => { const [foods, total] = await Promise.all([ db - .collection("foods") + .collection("natural_foods") .find(query) .skip(offsetNum) .limit(limitNum) .toArray(), - db.collection("foods").countDocuments(query), + db.collection("natural_foods").countDocuments(query), ]); return res.json({ @@ -111,7 +112,7 @@ router.get("/foods", async (req, res) => { /** * GET /api/foods/:id - * Get a single food by ID + * Get a single natural food by ID */ router.get("/foods/:id", async (req, res) => { try { @@ -126,7 +127,7 @@ router.get("/foods/:id", async (req, res) => { } const db = getDB(); - const food = await db.collection("foods").findOne({ _id: new ObjectId(id) }); + const food = await db.collection("natural_foods").findOne({ _id: new ObjectId(id) }); if (!food) { return res.status(404).json({ @@ -159,12 +160,12 @@ router.get("/foods/:id", async (req, res) => { /** * GET /api/categories - * Get all food categories + * Get all natural food categories */ router.get("/categories", async (_, res) => { try { const db = getDB(); - const categories = await db.collection("foods").distinct("category"); + const categories = await db.collection("natural_foods").distinct("category"); return res.json({ success: true, @@ -179,6 +180,198 @@ router.get("/categories", async (_, res) => { } }); +// ============================================ +// Halal Foods - Public REST API +// ============================================ + +/** + * GET /api/halal/search + * Search halal foods by name + * Query params: q (required) - search query + */ +router.get("/halal/search", async (req, res) => { + try { + const { q } = req.query; + + if (!q || typeof q !== "string") { + return res.status(400).json({ + success: false, + error: "Query parameter 'q' is required", + }); + } + + const db = getDB(); + const foods = await db + .collection("halal_foods") + .find({ name: { $regex: q, $options: "i" } }) + .limit(20) + .toArray(); + + return res.json({ + success: true, + data: foods.map((food) => ({ + id: food._id.toString(), + name: food.name, + calories: food.calories, + protein: food.protein ?? 0, + carbs: food.carbs ?? 0, + fat: food.fat ?? 0, + serving: food.serving, + category: food.category, + brand: food.brand, + halalCertifier: food.halalCertifier, + halalCertYear: food.halalCertYear, + })), + count: foods.length, + }); + } catch (error) { + console.error("API Error [/halal/search]:", error); + return res.status(500).json({ + success: false, + error: "Internal server error", + }); + } +}); + +/** + * GET /api/halal + * Get all halal foods or filter by brand/category + * Query params: + * - brand (optional) - filter by brand + * - category (optional) - filter by category + * - limit (optional) - max results (default: 50, max: 200) + * - offset (optional) - pagination offset (default: 0) + */ +router.get("/halal", async (req, res) => { + try { + const { brand, category, limit = "50", offset = "0" } = req.query; + + const limitNum = Math.min(parseInt(limit as string) || 50, 200); + const offsetNum = parseInt(offset as string) || 0; + + const db = getDB(); + const query: Record = {}; + if (brand) query.brand = brand; + if (category) query.category = category; + + const [foods, total] = await Promise.all([ + db + .collection("halal_foods") + .find(query) + .skip(offsetNum) + .limit(limitNum) + .toArray(), + db.collection("halal_foods").countDocuments(query), + ]); + + return res.json({ + success: true, + data: foods.map((food) => ({ + id: food._id.toString(), + name: food.name, + calories: food.calories, + protein: food.protein ?? 0, + carbs: food.carbs ?? 0, + fat: food.fat ?? 0, + serving: food.serving, + category: food.category, + brand: food.brand, + halalCertifier: food.halalCertifier, + halalCertYear: food.halalCertYear, + })), + pagination: { + total, + limit: limitNum, + offset: offsetNum, + hasMore: offsetNum + foods.length < total, + }, + }); + } catch (error) { + console.error("API Error [/halal]:", error); + return res.status(500).json({ + success: false, + error: "Internal server error", + }); + } +}); + +/** + * GET /api/halal/brands + * Get all halal food brands + */ +router.get("/halal/brands", async (_, res) => { + try { + const db = getDB(); + const brands = await db.collection("halal_foods").distinct("brand"); + + return res.json({ + success: true, + data: brands.filter(Boolean).sort(), + }); + } catch (error) { + console.error("API Error [/halal/brands]:", error); + return res.status(500).json({ + success: false, + error: "Internal server error", + }); + } +}); + +/** + * GET /api/halal/:id + * Get a single halal food by ID + */ +router.get("/halal/:id", async (req, res) => { + try { + const { id } = req.params; + const { ObjectId } = await import("mongodb"); + + if (!ObjectId.isValid(id)) { + return res.status(400).json({ + success: false, + error: "Invalid food ID", + }); + } + + const db = getDB(); + const food = await db.collection("halal_foods").findOne({ _id: new ObjectId(id) }); + + if (!food) { + return res.status(404).json({ + success: false, + error: "Food not found", + }); + } + + return res.json({ + success: true, + data: { + id: food._id.toString(), + name: food.name, + calories: food.calories, + protein: food.protein ?? 0, + carbs: food.carbs ?? 0, + fat: food.fat ?? 0, + serving: food.serving, + category: food.category, + brand: food.brand, + halalCertifier: food.halalCertifier, + halalCertYear: food.halalCertYear, + }, + }); + } catch (error) { + console.error("API Error [/halal/:id]:", error); + return res.status(500).json({ + success: false, + error: "Internal server error", + }); + } +}); + +// ============================================ +// Stats - Public REST API +// ============================================ + /** * GET /api/stats * Get database statistics @@ -187,17 +380,24 @@ router.get("/stats", async (_, res) => { try { const db = getDB(); - const [totalFoods, categories] = await Promise.all([ - db.collection("foods").countDocuments(), - db.collection("foods").distinct("category"), + const [naturalCount, naturalCategories, halalCount, halalBrands] = await Promise.all([ + db.collection("natural_foods").countDocuments(), + db.collection("natural_foods").distinct("category"), + db.collection("halal_foods").countDocuments(), + db.collection("halal_foods").distinct("brand"), ]); return res.json({ success: true, data: { - totalFoods, - totalCategories: categories.filter(Boolean).length, - categories: categories.filter(Boolean).sort(), + naturalFoods: { + total: naturalCount, + categories: naturalCategories.filter(Boolean).sort(), + }, + halalFoods: { + total: halalCount, + brands: halalBrands.filter(Boolean).sort(), + }, }, }); } catch (error) { diff --git a/packages/kal-backend/src/routers/food.ts b/packages/kal-backend/src/routers/food.ts index 8c212cd..74389d7 100644 --- a/packages/kal-backend/src/routers/food.ts +++ b/packages/kal-backend/src/routers/food.ts @@ -1,15 +1,17 @@ -import { z } from "zod"; +import { CreateFoodEntrySchema, GetEntriesSchema, DeleteEntrySchema } from "kal-shared"; import { ObjectId } from "mongodb"; +import { z } from "zod"; + import { router, publicProcedure, protectedProcedure } from "../lib/trpc.js"; -import { CreateFoodEntrySchema, GetEntriesSchema, DeleteEntrySchema } from "kal-shared"; + export const foodRouter = router({ - // Search foods database (public) + // Search natural foods database (public) search: publicProcedure .input(z.object({ query: z.string().min(1) })) .query(async ({ input, ctx }) => { const foods = await ctx.db - .collection("foods") + .collection("natural_foods") .find({ name: { $regex: input.query, $options: "i" } }) .limit(20) .toArray(); @@ -25,9 +27,9 @@ export const foodRouter = router({ })); }), - // Get all foods (public) + // Get all natural foods (public) all: publicProcedure.query(async ({ ctx }) => { - const foods = await ctx.db.collection("foods").find({}).toArray(); + const foods = await ctx.db.collection("natural_foods").find({}).toArray(); return foods.map((food) => ({ _id: food._id.toString(), name: food.name, @@ -40,7 +42,7 @@ export const foodRouter = router({ })); }), - // Get all foods with pagination (public) + // Get all natural foods with pagination (public) allPaginated: publicProcedure .input( z.object({ @@ -54,12 +56,12 @@ export const foodRouter = router({ const [foods, total] = await Promise.all([ ctx.db - .collection("foods") + .collection("natural_foods") .find(query) .skip(input.cursor) .limit(input.limit) .toArray(), - ctx.db.collection("foods").countDocuments(query), + ctx.db.collection("natural_foods").countDocuments(query), ]); return { @@ -81,10 +83,10 @@ export const foodRouter = router({ }; }), - // Get all categories (public) + // Get all categories for natural foods (public) categories: publicProcedure.query(async ({ ctx }) => { const categories = await ctx.db - .collection("foods") + .collection("natural_foods") .distinct("category"); return categories.filter(Boolean).sort(); }), diff --git a/packages/kal-backend/src/routers/halal.ts b/packages/kal-backend/src/routers/halal.ts new file mode 100644 index 0000000..066ddb4 --- /dev/null +++ b/packages/kal-backend/src/routers/halal.ts @@ -0,0 +1,135 @@ +import { z } from "zod"; + +import { router, publicProcedure } from "../lib/trpc.js"; + +export const halalRouter = router({ + // Search halal foods database (public) + search: publicProcedure + .input(z.object({ query: z.string().min(1) })) + .query(async ({ input, ctx }) => { + const foods = await ctx.db + .collection("halal_foods") + .find({ name: { $regex: input.query, $options: "i" } }) + .limit(20) + .toArray(); + + return foods.map((food) => ({ + _id: food._id.toString(), + name: food.name, + calories: food.calories, + protein: food.protein, + carbs: food.carbs, + fat: food.fat, + serving: food.serving, + category: food.category, + brand: food.brand, + halalCertifier: food.halalCertifier, + halalCertYear: food.halalCertYear, + })); + }), + + // Get all halal foods (public) + all: publicProcedure.query(async ({ ctx }) => { + const foods = await ctx.db.collection("halal_foods").find({}).toArray(); + return foods.map((food) => ({ + _id: food._id.toString(), + name: food.name, + calories: food.calories, + protein: food.protein, + carbs: food.carbs, + fat: food.fat, + serving: food.serving, + category: food.category, + brand: food.brand, + halalCertifier: food.halalCertifier, + halalCertYear: food.halalCertYear, + })); + }), + + // Get all halal foods with pagination (public) + allPaginated: publicProcedure + .input( + z.object({ + cursor: z.number().default(0), + limit: z.number().default(10), + brand: z.string().optional(), + category: z.string().optional(), + }) + ) + .query(async ({ input, ctx }) => { + const query: Record = {}; + if (input.brand) query.brand = input.brand; + if (input.category) query.category = input.category; + + const [foods, total] = await Promise.all([ + ctx.db + .collection("halal_foods") + .find(query) + .skip(input.cursor) + .limit(input.limit) + .toArray(), + ctx.db.collection("halal_foods").countDocuments(query), + ]); + + return { + items: foods.map((food) => ({ + _id: food._id.toString(), + name: food.name, + calories: food.calories, + protein: food.protein, + carbs: food.carbs, + fat: food.fat, + serving: food.serving, + category: food.category, + brand: food.brand, + halalCertifier: food.halalCertifier, + halalCertYear: food.halalCertYear, + })), + nextCursor: + input.cursor + foods.length < total + ? input.cursor + input.limit + : null, + total, + }; + }), + + // Get all brands (public) + brands: publicProcedure.query(async ({ ctx }) => { + const brands = await ctx.db + .collection("halal_foods") + .distinct("brand"); + return brands.filter(Boolean).sort(); + }), + + // Get all categories for halal foods (public) + categories: publicProcedure.query(async ({ ctx }) => { + const categories = await ctx.db + .collection("halal_foods") + .distinct("category"); + return categories.filter(Boolean).sort(); + }), + + // Get foods by brand (public) + byBrand: publicProcedure + .input(z.object({ brand: z.string() })) + .query(async ({ input, ctx }) => { + const foods = await ctx.db + .collection("halal_foods") + .find({ brand: input.brand }) + .toArray(); + + return foods.map((food) => ({ + _id: food._id.toString(), + name: food.name, + calories: food.calories, + protein: food.protein, + carbs: food.carbs, + fat: food.fat, + serving: food.serving, + category: food.category, + brand: food.brand, + halalCertifier: food.halalCertifier, + halalCertYear: food.halalCertYear, + })); + }), +}); diff --git a/packages/kal-backend/src/routers/index.ts b/packages/kal-backend/src/routers/index.ts index 5c48b2c..e1a65e1 100644 --- a/packages/kal-backend/src/routers/index.ts +++ b/packages/kal-backend/src/routers/index.ts @@ -1,9 +1,13 @@ import { router } from "../lib/trpc.js"; + import { foodRouter } from "./food.js"; +import { halalRouter } from "./halal.js"; export const appRouter = router({ food: foodRouter, + halal: halalRouter, }); // Export type for client export type AppRouter = typeof appRouter; + diff --git a/packages/kal-db/migrations/20241225000001_add_user_tiers.js b/packages/kal-db/migrations/20241225000001_add_user_tiers.js new file mode 100644 index 0000000..f55daed --- /dev/null +++ b/packages/kal-db/migrations/20241225000001_add_user_tiers.js @@ -0,0 +1,41 @@ +/** + * Migration: Add user tiers for rate limiting + * Adds tier field to users collection and creates rate_limit_usage collection + */ + +export const up = async (db, client) => { + // Add tier field to all existing users (default: 'free') + const usersCollection = db.collection("users"); + const result = await usersCollection.updateMany( + { tier: { $exists: false } }, + { $set: { tier: "free" } } + ); + console.log(`✅ Updated ${result.modifiedCount} users with default tier 'free'`); + + // Create rate_limit_usage collection if it doesn't exist + const collections = await db.listCollections({ name: "rate_limit_usage" }).toArray(); + if (collections.length === 0) { + await db.createCollection("rate_limit_usage"); + console.log("✅ Created 'rate_limit_usage' collection"); + } else { + console.log("ℹ️ 'rate_limit_usage' collection already exists"); + } +}; + +export const down = async (db, client) => { + // Remove tier field from users + const usersCollection = db.collection("users"); + await usersCollection.updateMany( + {}, + { $unset: { tier: "" } } + ); + console.log("✅ Removed tier field from users"); + + // Drop rate_limit_usage collection + try { + await db.collection("rate_limit_usage").drop(); + console.log("✅ Dropped 'rate_limit_usage' collection"); + } catch (e) { + console.log("ℹ️ Could not drop rate_limit_usage collection:", e.message); + } +}; diff --git a/packages/kal-db/migrations/20241225000002_rate_limit_indexes.js b/packages/kal-db/migrations/20241225000002_rate_limit_indexes.js new file mode 100644 index 0000000..fdc2145 --- /dev/null +++ b/packages/kal-db/migrations/20241225000002_rate_limit_indexes.js @@ -0,0 +1,40 @@ +/** + * Migration: Create indexes for rate limit tracking + * Adds compound index for fast lookups and TTL index for auto-cleanup + */ + +export const up = async (db, client) => { + const collection = db.collection("rate_limit_usage"); + + // Compound index for fast lookups (userId + date) + await collection.createIndex( + { userId: 1, date: 1 }, + { unique: true, name: "userId_date_unique" } + ); + console.log("✅ Created compound index on 'rate_limit_usage' (userId + date)"); + + // TTL index: auto-delete records after 7 days + await collection.createIndex( + { updatedAt: 1 }, + { expireAfterSeconds: 7 * 24 * 60 * 60, name: "updatedAt_ttl" } + ); + console.log("✅ Created TTL index on 'rate_limit_usage' (7 days expiry)"); +}; + +export const down = async (db, client) => { + const collection = db.collection("rate_limit_usage"); + + try { + await collection.dropIndex("userId_date_unique"); + console.log("✅ Dropped userId_date_unique index"); + } catch (e) { + console.log("ℹ️ Could not drop userId_date_unique index:", e.message); + } + + try { + await collection.dropIndex("updatedAt_ttl"); + console.log("✅ Dropped updatedAt_ttl index"); + } catch (e) { + console.log("ℹ️ Could not drop updatedAt_ttl index:", e.message); + } +}; diff --git a/packages/kal-db/package.json b/packages/kal-db/package.json index b51b759..d644d0c 100644 --- a/packages/kal-db/package.json +++ b/packages/kal-db/package.json @@ -10,6 +10,8 @@ "migrate:status": "migrate-mongo status", "seed": "tsx scripts/seed.ts", "seed:safe": "tsx scripts/seed-safe.ts", + "lint": "eslint .", + "lint:fix": "eslint . --fix", "typecheck": "tsc --noEmit" }, "dependencies": { diff --git a/packages/kal-db/scripts/seed-safe.ts b/packages/kal-db/scripts/seed-safe.ts index 4b57166..eaecf29 100644 --- a/packages/kal-db/scripts/seed-safe.ts +++ b/packages/kal-db/scripts/seed-safe.ts @@ -1,6 +1,6 @@ /** * Safe seed script for production - * Only seeds the foods collection if it's empty + * Only seeds collections if they're empty * This prevents data loss on redeployments */ import "dotenv/config"; @@ -35,8 +35,10 @@ const getDbName = () => { return process.env.MONGODB_DATABASE || "kal"; }; -// Malaysian food database with calories, macros, and categories -const foods = [ +// ============================================================================ +// NATURAL FOODS - Generic Malaysian dishes (street vendor, no formal halal cert) +// ============================================================================ +const naturalFoods = [ // === RICE DISHES === { name: "Nasi Lemak", calories: 644, protein: 18, carbs: 80, fat: 28, serving: "1 plate", category: "Rice" }, { name: "Nasi Lemak Ayam Goreng", calories: 850, protein: 32, carbs: 90, fat: 38, serving: "1 plate", category: "Rice" }, @@ -157,6 +159,26 @@ const foods = [ { name: "Tahu Goreng", calories: 180, protein: 12, carbs: 8, fat: 12, serving: "100g", category: "Basics" }, ]; +// ============================================================================ +// HALAL FOODS - Branded foods with official halal certification +// ============================================================================ +const halalFoods = [ + // === RAMLY (JAKIM Certified) === + { name: "Ramly Beef Burger", calories: 400, protein: 15, carbs: 35, fat: 20, serving: "1 burger (200g)", category: "Fast Food", brand: "Ramly", halalCertifier: "JAKIM", halalCertYear: 2024 }, + { name: "Ramly Beef Special", calories: 600, protein: 25, carbs: 45, fat: 35, serving: "1 burger (250g)", category: "Fast Food", brand: "Ramly", halalCertifier: "JAKIM", halalCertYear: 2024 }, + { name: "Ramly Beef Cheese", calories: 480, protein: 18, carbs: 38, fat: 28, serving: "1 burger (220g)", category: "Fast Food", brand: "Ramly", halalCertifier: "JAKIM", halalCertYear: 2024 }, + { name: "Ramly Double Special", calories: 800, protein: 40, carbs: 50, fat: 45, serving: "1 burger (350g)", category: "Fast Food", brand: "Ramly", halalCertifier: "JAKIM", halalCertYear: 2024 }, + { name: "Ramly Chicken Burger", calories: 480, protein: 20, carbs: 42, fat: 22, serving: "1 burger (250g)", category: "Fast Food", brand: "Ramly", halalCertifier: "JAKIM", halalCertYear: 2024 }, + { name: "Ramly Chicken Special", calories: 550, protein: 24, carbs: 45, fat: 28, serving: "1 burger (250g)", category: "Fast Food", brand: "Ramly", halalCertifier: "JAKIM", halalCertYear: 2024 }, + { name: "Ramly Chicken Cheese", calories: 530, protein: 22, carbs: 44, fat: 26, serving: "1 burger (250g)", category: "Fast Food", brand: "Ramly", halalCertifier: "JAKIM", halalCertYear: 2024 }, + { name: "Ramly Lamb Burger", calories: 450, protein: 22, carbs: 35, fat: 25, serving: "1 burger (200g)", category: "Fast Food", brand: "Ramly", halalCertifier: "JAKIM", halalCertYear: 2024 }, + { name: "Ramly Fish Burger", calories: 380, protein: 18, carbs: 38, fat: 18, serving: "1 burger (200g)", category: "Fast Food", brand: "Ramly", halalCertifier: "JAKIM", halalCertYear: 2024 }, + { name: "Ramly Beef Sausage", calories: 280, protein: 12, carbs: 8, fat: 22, serving: "2 sausages", category: "Fast Food", brand: "Ramly", halalCertifier: "JAKIM", halalCertYear: 2024 }, + { name: "Ramly Chicken Sausage", calories: 240, protein: 10, carbs: 10, fat: 18, serving: "2 sausages", category: "Fast Food", brand: "Ramly", halalCertifier: "JAKIM", halalCertYear: 2024 }, + { name: "Ramly Oblong Beef", calories: 420, protein: 16, carbs: 36, fat: 22, serving: "1 burger (220g)", category: "Fast Food", brand: "Ramly", halalCertifier: "JAKIM", halalCertYear: 2024 }, + { name: "Ramly Oblong Chicken", calories: 400, protein: 18, carbs: 38, fat: 20, serving: "1 burger (220g)", category: "Fast Food", brand: "Ramly", halalCertifier: "JAKIM", halalCertYear: 2024 }, +]; + async function seedSafe() { console.log("🌱 Starting safe seed (will skip if data exists)..."); @@ -169,26 +191,47 @@ async function seedSafe() { console.log("✅ Connected to MongoDB"); const db = client.db(dbName); - const collection = db.collection("foods"); - - // Check if collection already has data - const count = await collection.countDocuments(); - if (count > 0) { - console.log(`ℹ️ Foods collection already has ${count} documents - skipping seed`); - console.log("💡 To force reseed, run 'pnpm seed' instead"); - return; + // ======================================== + // Seed natural_foods collection (if empty) + // ======================================== + const naturalCollection = db.collection("natural_foods"); + const naturalCount = await naturalCollection.countDocuments(); + + if (naturalCount > 0) { + console.log(`ℹ️ natural_foods collection already has ${naturalCount} documents - skipping`); + } else { + await naturalCollection.insertMany(naturalFoods); + console.log(`✅ Inserted ${naturalFoods.length} items into 'natural_foods' collection`); + + // Create indexes + await naturalCollection.createIndex({ name: "text" }); + await naturalCollection.createIndex({ category: 1 }); + console.log("✅ Created indexes on 'natural_foods' collection"); } - // Collection is empty, proceed with seeding - await collection.insertMany(foods); - console.log(`✅ Inserted ${foods.length} Malaysian foods with categories`); - - // Create index on category for filtering - await collection.createIndex({ category: 1 }); - console.log("✅ Created index on category field"); + // ======================================== + // Seed halal_foods collection (if empty) + // ======================================== + const halalCollection = db.collection("halal_foods"); + const halalCount = await halalCollection.countDocuments(); + + if (halalCount > 0) { + console.log(`ℹ️ halal_foods collection already has ${halalCount} documents - skipping`); + } else { + await halalCollection.insertMany(halalFoods); + console.log(`✅ Inserted ${halalFoods.length} items into 'halal_foods' collection`); + + // Create indexes + await halalCollection.createIndex({ name: "text" }); + await halalCollection.createIndex({ category: 1 }); + await halalCollection.createIndex({ brand: 1 }); + await halalCollection.createIndex({ halalCertifier: 1 }); + console.log("✅ Created indexes on 'halal_foods' collection"); + } - console.log("🎉 Safe seeding completed successfully!"); + console.log("🎉 Safe seeding completed!"); + console.log("💡 To force reseed, run 'pnpm seed' instead"); } catch (error) { console.error("❌ Safe seeding failed:", error); process.exit(1); diff --git a/packages/kal-db/scripts/seed.ts b/packages/kal-db/scripts/seed.ts index e01d325..c786788 100644 --- a/packages/kal-db/scripts/seed.ts +++ b/packages/kal-db/scripts/seed.ts @@ -29,8 +29,10 @@ const getDbName = () => { return MONGODB_DATABASE || "kal"; }; -// Malaysian food database with calories, macros, and categories -const foods = [ +// ============================================================================ +// NATURAL FOODS - Generic Malaysian dishes (street vendor, no formal halal cert) +// ============================================================================ +const naturalFoods = [ // === RICE DISHES === { name: "Nasi Lemak", calories: 644, protein: 18, carbs: 80, fat: 28, serving: "1 plate", category: "Rice" }, { name: "Nasi Lemak Ayam Goreng", calories: 850, protein: 32, carbs: 90, fat: 38, serving: "1 plate", category: "Rice" }, @@ -151,6 +153,26 @@ const foods = [ { name: "Tahu Goreng", calories: 180, protein: 12, carbs: 8, fat: 12, serving: "100g", category: "Basics" }, ]; +// ============================================================================ +// HALAL FOODS - Branded foods with official halal certification +// ============================================================================ +const halalFoods = [ + // === RAMLY (JAKIM Certified) === + { name: "Ramly Beef Burger", calories: 400, protein: 15, carbs: 35, fat: 20, serving: "1 burger (200g)", category: "Fast Food", brand: "Ramly", halalCertifier: "JAKIM", halalCertYear: 2024 }, + { name: "Ramly Beef Special", calories: 600, protein: 25, carbs: 45, fat: 35, serving: "1 burger (250g)", category: "Fast Food", brand: "Ramly", halalCertifier: "JAKIM", halalCertYear: 2024 }, + { name: "Ramly Beef Cheese", calories: 480, protein: 18, carbs: 38, fat: 28, serving: "1 burger (220g)", category: "Fast Food", brand: "Ramly", halalCertifier: "JAKIM", halalCertYear: 2024 }, + { name: "Ramly Double Special", calories: 800, protein: 40, carbs: 50, fat: 45, serving: "1 burger (350g)", category: "Fast Food", brand: "Ramly", halalCertifier: "JAKIM", halalCertYear: 2024 }, + { name: "Ramly Chicken Burger", calories: 480, protein: 20, carbs: 42, fat: 22, serving: "1 burger (250g)", category: "Fast Food", brand: "Ramly", halalCertifier: "JAKIM", halalCertYear: 2024 }, + { name: "Ramly Chicken Special", calories: 550, protein: 24, carbs: 45, fat: 28, serving: "1 burger (250g)", category: "Fast Food", brand: "Ramly", halalCertifier: "JAKIM", halalCertYear: 2024 }, + { name: "Ramly Chicken Cheese", calories: 530, protein: 22, carbs: 44, fat: 26, serving: "1 burger (250g)", category: "Fast Food", brand: "Ramly", halalCertifier: "JAKIM", halalCertYear: 2024 }, + { name: "Ramly Lamb Burger", calories: 450, protein: 22, carbs: 35, fat: 25, serving: "1 burger (200g)", category: "Fast Food", brand: "Ramly", halalCertifier: "JAKIM", halalCertYear: 2024 }, + { name: "Ramly Fish Burger", calories: 380, protein: 18, carbs: 38, fat: 18, serving: "1 burger (200g)", category: "Fast Food", brand: "Ramly", halalCertifier: "JAKIM", halalCertYear: 2024 }, + { name: "Ramly Beef Sausage", calories: 280, protein: 12, carbs: 8, fat: 22, serving: "2 sausages", category: "Fast Food", brand: "Ramly", halalCertifier: "JAKIM", halalCertYear: 2024 }, + { name: "Ramly Chicken Sausage", calories: 240, protein: 10, carbs: 10, fat: 18, serving: "2 sausages", category: "Fast Food", brand: "Ramly", halalCertifier: "JAKIM", halalCertYear: 2024 }, + { name: "Ramly Oblong Beef", calories: 420, protein: 16, carbs: 36, fat: 22, serving: "1 burger (220g)", category: "Fast Food", brand: "Ramly", halalCertifier: "JAKIM", halalCertYear: 2024 }, + { name: "Ramly Oblong Chicken", calories: 400, protein: 18, carbs: 38, fat: 20, serving: "1 burger (220g)", category: "Fast Food", brand: "Ramly", halalCertifier: "JAKIM", halalCertYear: 2024 }, +]; + async function seed() { console.log("🌱 Starting seed (force mode)..."); @@ -164,20 +186,50 @@ async function seed() { const db = client.db(dbName); - // Clear existing and insert fresh - await db.collection("foods").deleteMany({}); - await db.collection("foods").insertMany(foods); - console.log(`✅ Inserted ${foods.length} Malaysian foods with categories`); + // ======================================== + // Migrate existing 'foods' to 'natural_foods' + // ======================================== + const oldFoodsCollection = db.collection("foods"); + const oldCount = await oldFoodsCollection.countDocuments(); + + if (oldCount > 0) { + console.log(`📦 Found ${oldCount} items in old 'foods' collection - will be replaced`); + } + + // Clear old 'foods' collection (if exists) + await oldFoodsCollection.deleteMany({}); + console.log("🗑️ Cleared old 'foods' collection"); + + // ======================================== + // Seed natural_foods collection + // ======================================== + const naturalCollection = db.collection("natural_foods"); + await naturalCollection.deleteMany({}); + await naturalCollection.insertMany(naturalFoods); + console.log(`✅ Inserted ${naturalFoods.length} items into 'natural_foods' collection`); + + // Create indexes for natural_foods + await naturalCollection.createIndex({ name: "text" }); + await naturalCollection.createIndex({ category: 1 }); + console.log("✅ Created indexes on 'natural_foods' collection"); - // Create text index for search - await db.collection("foods").createIndex({ name: "text" }); - console.log("✅ Created text index on foods collection"); + // ======================================== + // Seed halal_foods collection + // ======================================== + const halalCollection = db.collection("halal_foods"); + await halalCollection.deleteMany({}); + await halalCollection.insertMany(halalFoods); + console.log(`✅ Inserted ${halalFoods.length} items into 'halal_foods' collection`); - // Create index on category for filtering - await db.collection("foods").createIndex({ category: 1 }); - console.log("✅ Created index on category field"); + // Create indexes for halal_foods + await halalCollection.createIndex({ name: "text" }); + await halalCollection.createIndex({ category: 1 }); + await halalCollection.createIndex({ brand: 1 }); + await halalCollection.createIndex({ halalCertifier: 1 }); + console.log("✅ Created indexes on 'halal_foods' collection"); console.log("🎉 Seeding completed successfully!"); + console.log(` 📊 Total: ${naturalFoods.length} natural foods + ${halalFoods.length} halal certified foods`); } catch (error) { console.error("❌ Seeding failed:", error); process.exit(1); diff --git a/packages/kal-frontend/.env.example b/packages/kal-frontend/.env.example index 5ad43fe..ca73a93 100644 --- a/packages/kal-frontend/.env.example +++ b/packages/kal-frontend/.env.example @@ -1,6 +1,11 @@ # API NEXT_PUBLIC_API_URL=http://localhost:4000 -# Logto Auth -NEXT_PUBLIC_LOGTO_ENDPOINT=http://localhost:3301 +# Logto Auth (Client-side - NEXT_PUBLIC_ prefix) +NEXT_PUBLIC_LOGTO_ENDPOINT=http://localhost:3001 NEXT_PUBLIC_LOGTO_APP_ID=your-app-id +NEXT_PUBLIC_APP_URL=http://localhost:3000 + +# Logto Auth (Server-side - secrets) +LOGTO_APP_SECRET=your-app-secret +SESSION_SECRET=your-session-secret-min-32-chars diff --git a/packages/kal-frontend/package.json b/packages/kal-frontend/package.json index 6830194..12a87d4 100644 --- a/packages/kal-frontend/package.json +++ b/packages/kal-frontend/package.json @@ -6,27 +6,29 @@ "dev": "next dev --port 3000", "build": "next build", "start": "next start", - "lint": "next lint", + "lint": "eslint .", + "lint:fix": "eslint . --fix", "typecheck": "tsc --noEmit" }, "dependencies": { + "@logto/next": "^4.2.7", "@tanstack/react-query": "^5.62.11", "@trpc/client": "^11.0.0-rc.682", "@trpc/react-query": "^11.0.0-rc.682", + "kal-shared": "workspace:*", "next": "^15.1.2", "react": "^19.0.0", - "react-dom": "^19.0.0", - "kal-shared": "workspace:*" + "react-dom": "^19.0.0" }, "devDependencies": { - "kal-backend": "workspace:*", "@types/node": "^22.10.2", "@types/react": "^19.0.2", "@types/react-dom": "^19.0.2", "autoprefixer": "^10.4.20", + "kal-backend": "workspace:*", + "mongodb": "^6.12.0", "postcss": "^8.4.49", "tailwindcss": "^3.4.17", - "typescript": "^5.7.2", - "mongodb": "^6.12.0" + "typescript": "^5.7.2" } } diff --git a/packages/kal-frontend/src/app/api-docs/client.tsx b/packages/kal-frontend/src/app/api-docs/client.tsx new file mode 100644 index 0000000..aa8b487 --- /dev/null +++ b/packages/kal-frontend/src/app/api-docs/client.tsx @@ -0,0 +1,572 @@ +"use client"; + +import Link from "next/link"; +import { useState } from "react"; + +import { Button } from "@/components/ui/Button"; +import { Container } from "@/components/ui/Container"; + +// Get the API base URL from environment +const getApiUrl = () => { + if (typeof window !== "undefined") { + // In browser, use relative path or env + return process.env.NEXT_PUBLIC_API_URL || ""; + } + return ""; +}; + +interface EndpointExample { + id: string; + method: string; + path: string; + description: string; + params?: { name: string; type: string; required: boolean; description: string }[]; + example: string; + section: "natural" | "halal" | "general"; +} + +const endpoints: EndpointExample[] = [ + // Natural Foods + { + id: "search", + method: "GET", + path: "/api/foods/search", + description: "Search natural Malaysian foods by name. Returns up to 20 matching results.", + params: [ + { name: "q", type: "string", required: true, description: "Search query (e.g., 'nasi', 'mee')" }, + ], + example: '/api/foods/search?q=nasi', + section: "natural", + }, + { + id: "list", + method: "GET", + path: "/api/foods", + description: "List all natural foods with optional filtering and pagination.", + params: [ + { name: "category", type: "string", required: false, description: "Filter by category (e.g., 'Rice', 'Noodles')" }, + { name: "limit", type: "integer", required: false, description: "Max results (default: 50, max: 200)" }, + { name: "offset", type: "integer", required: false, description: "Pagination offset (default: 0)" }, + ], + example: '/api/foods?category=Rice&limit=5', + section: "natural", + }, + { + id: "single", + method: "GET", + path: "/api/foods/:id", + description: "Get a single natural food item by its ID.", + params: [ + { name: "id", type: "string", required: true, description: "Food ID (MongoDB ObjectId)" }, + ], + example: '/api/foods/694bf52dcf55787e6332e9c0', + section: "natural", + }, + { + id: "categories", + method: "GET", + path: "/api/categories", + description: "Get all available food categories for natural foods.", + example: '/api/categories', + section: "natural", + }, + // Halal Foods + { + id: "halal-search", + method: "GET", + path: "/api/halal/search", + description: "Search JAKIM certified halal foods by name. Returns up to 20 matching results.", + params: [ + { name: "q", type: "string", required: true, description: "Search query (e.g., 'ramly', 'burger')" }, + ], + example: '/api/halal/search?q=ramly', + section: "halal", + }, + { + id: "halal-list", + method: "GET", + path: "/api/halal", + description: "List all halal certified foods with optional filtering by brand.", + params: [ + { name: "brand", type: "string", required: false, description: "Filter by brand (e.g., 'Ramly')" }, + { name: "category", type: "string", required: false, description: "Filter by category (e.g., 'Fast Food')" }, + { name: "limit", type: "integer", required: false, description: "Max results (default: 50, max: 200)" }, + { name: "offset", type: "integer", required: false, description: "Pagination offset (default: 0)" }, + ], + example: '/api/halal?brand=Ramly', + section: "halal", + }, + { + id: "halal-brands", + method: "GET", + path: "/api/halal/brands", + description: "Get all available halal food brands.", + example: '/api/halal/brands', + section: "halal", + }, + { + id: "halal-single", + method: "GET", + path: "/api/halal/:id", + description: "Get a single halal food item by its ID.", + params: [ + { name: "id", type: "string", required: true, description: "Food ID (MongoDB ObjectId)" }, + ], + example: '/api/halal/694bf52dcf55787e6332e9c1', + section: "halal", + }, + // General + { + id: "stats", + method: "GET", + path: "/api/stats", + description: "Get database statistics including total foods, categories, and brands.", + example: '/api/stats', + section: "general", + }, +]; + +// cURL example type +interface CurlExample { + comment: string; + command: string; + type: "natural" | "halal"; +} + +interface APIDocsClientProps { + isAuthenticated: boolean; + userEmail?: string; + curlExamples: CurlExample[]; + onSignIn: () => Promise; + onSignOut: () => Promise; +} + +const navLinks = [ + { label: "Features", href: "/#features" }, + { label: "How It Works", href: "/#how-it-works" }, + { label: "FAQ", href: "/#faq" }, + { label: "API", href: "/api-docs" }, +]; + +export default function APIDocsClient({ isAuthenticated, userEmail, curlExamples, onSignIn, onSignOut }: APIDocsClientProps) { + const [activeEndpoint, setActiveEndpoint] = useState(null); + const [response, setResponse] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + + const tryEndpoint = async (example: string) => { + setLoading(true); + setError(null); + setResponse(null); + + try { + const apiUrl = getApiUrl(); + const fullUrl = apiUrl ? `${apiUrl}${example}` : example; + const res = await fetch(fullUrl); + const data = await res.json(); + setResponse(JSON.stringify(data, null, 2)); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to fetch"); + } finally { + setLoading(false); + } + }; + + const renderEndpoint = (endpoint: EndpointExample, accentColor: string) => ( +
+ {/* Header */} + + + {/* Expanded content */} + {activeEndpoint === endpoint.id && ( +
+

{endpoint.description}

+ + {/* Parameters */} + {endpoint.params && ( +
+

Parameters

+
+ + + + + + + + + + + {endpoint.params.map((param) => ( + + + + + + + ))} + +
NameTypeRequiredDescription
{param.name}{param.type} + {param.required ? ( + Yes + ) : ( + No + )} + {param.description}
+
+
+ )} + + {/* Try it */} +
+

Try it

+
+ + {endpoint.example} + + +
+
+ + {/* Response */} + {(response || error) && ( +
+

Response

+
+                {error || response}
+              
+
+ )} +
+ )} +
+ ); + + // cURL example component - secure: command is empty for unauthenticated users + const CurlExampleCard = ({ example }: { example: CurlExample }) => { + const borderColor = example.type === "halal" ? "border-emerald-500/30" : "border-[#262626]"; + const commentColor = example.type === "halal" ? "text-emerald-400" : "text-[#10b981]"; + const hasCommand = example.command && example.command.length > 0; + + if (hasCommand) { + // Authenticated - show full command + return ( +
+

{example.comment}

+ + {example.command} + +
+ ); + } + + // Not authenticated - show locked state (command is empty from server) + return ( +
+

{example.comment}

+
+
+ +
+
+ ); + }; + + const naturalEndpoints = endpoints.filter(e => e.section === "natural"); + const halalEndpoints = endpoints.filter(e => e.section === "halal"); + const generalEndpoints = endpoints.filter(e => e.section === "general"); + + return ( +
+ {/* Navbar (same style as landing page) */} + + + {/* Spacer for fixed navbar */} +
+ +
+ {/* Intro */} +
+

Malaysian Food API

+

+ Access our database of 100+ Malaysian foods with accurate nutritional information. + Now featuring halal certified foods with JAKIM verification. + All endpoints are public and require no authentication. +

+ + +
+ + {/* Base URL */} +
+

Base URL

+ + https://kalori-api.my + +
+ + {/* Natural Foods Endpoints */} +
+
+ 🍚 +

Natural Foods

+ Street food & traditional Malaysian dishes +
+
+ {naturalEndpoints.map((endpoint) => renderEndpoint(endpoint, "bg-[#10b981]"))} +
+
+ + {/* Halal Foods Endpoints */} +
+
+

Halal Certified Foods

+ JAKIM certified brands +
+
+

+ ✓ All items include brand name, halal certifier (JAKIM), and certification year +

+
+
+ {halalEndpoints.map((endpoint) => renderEndpoint(endpoint, "bg-emerald-500"))} +
+
+ + {/* General Endpoints */} +
+

General

+
+ {generalEndpoints.map((endpoint) => renderEndpoint(endpoint, "bg-[#10b981]"))} +
+
+ + {/* Response Format */} +
+

Response Format

+

All endpoints return JSON with this structure:

+
+{`{
+  "success": true,
+  "data": [...],       // The response data
+  "count": 12,         // For search results
+  "pagination": {      // For paginated endpoints
+    "total": 97,
+    "limit": 50,
+    "offset": 0,
+    "hasMore": true
+  }
+}`}
+          
+
+ + {/* Food Objects */} +
+

Food Objects

+ +
+ {/* Natural Food */} +
+

Natural Food

+
+{`{
+  "id": "694bf52dcf55787e6332e9c0",
+  "name": "Nasi Lemak",
+  "calories": 644,
+  "protein": 18,
+  "carbs": 80,
+  "fat": 28,
+  "serving": "1 plate",
+  "category": "Rice"
+}`}
+              
+
+ + {/* Halal Food */} +
+

Halal Certified Food

+
+{`{
+  "id": "694bf52dcf55787e6332e9c1",
+  "name": "Ramly Beef Burger",
+  "calories": 400,
+  "protein": 15,
+  "carbs": 35,
+  "fat": 20,
+  "serving": "1 burger (200g)",
+  "category": "Fast Food",
+  "brand": "Ramly",
+  "halalCertifier": "JAKIM",
+  "halalCertYear": 2024
+}`}
+              
+
+
+
+ + {/* cURL Examples - with blur for unauthenticated users */} +
+
+

cURL Examples

+ {!isAuthenticated && ( + + 🔒 Sign in to view full examples + + )} +
+
+ {curlExamples.map((example, index) => ( + + ))} +
+
+
+ + {/* Footer */} +
+
+

Made with ❤️ in Malaysia

+ ← Back to Kal +
+
+
+ ); +} diff --git a/packages/kal-frontend/src/app/api-docs/page.tsx b/packages/kal-frontend/src/app/api-docs/page.tsx index e676e5c..ddf2c2f 100644 --- a/packages/kal-frontend/src/app/api-docs/page.tsx +++ b/packages/kal-frontend/src/app/api-docs/page.tsx @@ -1,322 +1,60 @@ -"use client"; +import { getLogtoContext, signIn, signOut } from '@logto/next/server-actions'; -import { useState } from "react"; -import Link from "next/link"; +import APIDocsClient from './client'; -// Get the API base URL from environment -const getApiUrl = () => { - if (typeof window !== "undefined") { - // In browser, use relative path or env - return process.env.NEXT_PUBLIC_API_URL || ""; - } - return ""; -}; +import { logtoConfig } from '@/lib/logto'; -interface EndpointExample { - id: string; - method: string; - path: string; - description: string; - params?: { name: string; type: string; required: boolean; description: string }[]; - example: string; -} - -const endpoints: EndpointExample[] = [ +// cURL examples - only sent to client when authenticated +const curlExamples = [ { - id: "search", - method: "GET", - path: "/api/foods/search", - description: "Search foods by name. Returns up to 20 matching results.", - params: [ - { name: "q", type: "string", required: true, description: "Search query (e.g., 'nasi', 'mee')" }, - ], - example: '/api/foods/search?q=nasi', + comment: "# Search for nasi dishes", + command: 'curl "https://kalori-api.my/api/foods/search?q=nasi"', + type: "natural" as const, }, { - id: "list", - method: "GET", - path: "/api/foods", - description: "List all foods with optional filtering and pagination.", - params: [ - { name: "category", type: "string", required: false, description: "Filter by category (e.g., 'Rice', 'Noodles')" }, - { name: "limit", type: "integer", required: false, description: "Max results (default: 50, max: 200)" }, - { name: "offset", type: "integer", required: false, description: "Pagination offset (default: 0)" }, - ], - example: '/api/foods?category=Rice&limit=5', + comment: "# Get all rice dishes", + command: 'curl "https://kalori-api.my/api/foods?category=Rice"', + type: "natural" as const, }, { - id: "single", - method: "GET", - path: "/api/foods/:id", - description: "Get a single food item by its ID.", - params: [ - { name: "id", type: "string", required: true, description: "Food ID (MongoDB ObjectId)" }, - ], - example: '/api/foods/694bf52dcf55787e6332e9c0', + comment: "# Search halal foods", + command: 'curl "https://kalori-api.my/api/halal/search?q=ramly"', + type: "halal" as const, }, { - id: "categories", - method: "GET", - path: "/api/categories", - description: "Get all available food categories.", - example: '/api/categories', + comment: "# Get all Ramly brand items", + command: 'curl "https://kalori-api.my/api/halal?brand=Ramly"', + type: "halal" as const, }, { - id: "stats", - method: "GET", - path: "/api/stats", - description: "Get database statistics including total foods and categories.", - example: '/api/stats', + comment: "# Get database stats", + command: 'curl "https://kalori-api.my/api/stats"', + type: "natural" as const, }, ]; -export default function APIDocsPage() { - const [activeEndpoint, setActiveEndpoint] = useState(null); - const [response, setResponse] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const tryEndpoint = async (example: string) => { - setLoading(true); - setError(null); - setResponse(null); - - try { - const apiUrl = getApiUrl(); - const fullUrl = apiUrl ? `${apiUrl}${example}` : example; - const res = await fetch(fullUrl); - const data = await res.json(); - setResponse(JSON.stringify(data, null, 2)); - } catch (err) { - setError(err instanceof Error ? err.message : "Failed to fetch"); - } finally { - setLoading(false); - } - }; - +export default async function APIDocsPage() { + const { isAuthenticated, claims } = await getLogtoContext(logtoConfig); + + // Only pass curl examples when user is authenticated + // This prevents users from viewing the data via DevTools inspect + const safeCurlExamples = isAuthenticated + ? curlExamples + : curlExamples.map(ex => ({ ...ex, command: '' })); // Empty command for unauthenticated + return ( -
- {/* Header */} -
-
- - 🥗 - Kal - -

API Documentation

-
-
- -
- {/* Intro */} -
-

Malaysian Food API

-

- Access our database of 100+ Malaysian foods with accurate nutritional information. - All endpoints are public and require no authentication. -

- - -
- - {/* Base URL */} -
-

Base URL

- - https://kalori-api.my - -
- - {/* Endpoints */} -
-

Endpoints

- -
- {endpoints.map((endpoint) => ( -
- {/* Header */} - - - {/* Expanded content */} - {activeEndpoint === endpoint.id && ( -
-

{endpoint.description}

- - {/* Parameters */} - {endpoint.params && ( -
-

Parameters

-
- - - - - - - - - - - {endpoint.params.map((param) => ( - - - - - - - ))} - -
NameTypeRequiredDescription
{param.name}{param.type} - {param.required ? ( - Yes - ) : ( - No - )} - {param.description}
-
-
- )} - - {/* Try it */} -
-

Try it

-
- - {endpoint.example} - - -
-
- - {/* Response */} - {(response || error) && ( -
-

Response

-
-                          {error || response}
-                        
-
- )} -
- )} -
- ))} -
-
- - {/* Response Format */} -
-

Response Format

-

All endpoints return JSON with this structure:

-
-{`{
-  "success": true,
-  "data": [...],       // The response data
-  "count": 12,         // For search results
-  "pagination": {      // For paginated endpoints
-    "total": 97,
-    "limit": 50,
-    "offset": 0,
-    "hasMore": true
-  }
-}`}
-          
-
- - {/* Food Object */} -
-

Food Object

-

Each food item contains:

-
-{`{
-  "id": "694bf52dcf55787e6332e9c0",
-  "name": "Nasi Lemak",
-  "calories": 644,
-  "protein": 18,
-  "carbs": 80,
-  "fat": 28,
-  "serving": "1 plate",
-  "category": "Rice"
-}`}
-          
-
- - {/* cURL Examples */} -
-

cURL Examples

-
-
-

# Search for nasi dishes

- - curl "https://kalori-api.my/api/foods/search?q=nasi" - -
-
-

# Get all rice dishes

- - curl "https://kalori-api.my/api/foods?category=Rice" - -
-
-

# Get database stats

- - curl "https://kalori-api.my/api/stats" - -
-
-
-
- - {/* Footer */} -
-
-

Made with ❤️ in Malaysia

- ← Back to Kal -
-
-
+ { + 'use server'; + await signIn(logtoConfig); + }} + onSignOut={async () => { + 'use server'; + await signOut(logtoConfig); + }} + /> ); } diff --git a/packages/kal-frontend/src/app/callback/route.ts b/packages/kal-frontend/src/app/callback/route.ts new file mode 100644 index 0000000..a0269f5 --- /dev/null +++ b/packages/kal-frontend/src/app/callback/route.ts @@ -0,0 +1,11 @@ +import { handleSignIn } from '@logto/next/server-actions'; +import { redirect } from 'next/navigation'; +import type { NextRequest } from 'next/server'; + +import { logtoConfig } from '@/lib/logto'; + +export async function GET(request: NextRequest) { + const searchParams = request.nextUrl.searchParams; + await handleSignIn(logtoConfig, searchParams); + redirect('/'); +} diff --git a/packages/kal-frontend/src/app/globals.css b/packages/kal-frontend/src/app/globals.css index 2ddc6b4..0b81de1 100644 --- a/packages/kal-frontend/src/app/globals.css +++ b/packages/kal-frontend/src/app/globals.css @@ -42,3 +42,68 @@ html { background-color: rgba(16, 185, 129, 0.3); color: var(--text-primary); } + +/* Navbar */ +.navbar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 2rem; + background-color: var(--bg-surface); + border-bottom: 1px solid var(--border-color); +} + +.nav-brand a { + font-size: 1.5rem; + font-weight: 700; + color: var(--accent-green); + text-decoration: none; +} + +.nav-auth { + display: flex; + align-items: center; + gap: 1rem; +} + +.user-info { + display: flex; + align-items: center; + gap: 1rem; +} + +.user-email { + color: var(--text-secondary); + font-size: 0.875rem; +} + +/* Auth Buttons */ +.sign-in-btn, +.sign-out-btn { + padding: 0.5rem 1rem; + border-radius: 0.375rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} + +.sign-in-btn { + background-color: var(--accent-green); + color: white; + border: none; +} + +.sign-in-btn:hover { + background-color: var(--accent-green-hover); +} + +.sign-out-btn { + background-color: transparent; + color: var(--text-secondary); + border: 1px solid var(--border-color); +} + +.sign-out-btn:hover { + background-color: var(--bg-elevated); + color: var(--text-primary); +} diff --git a/packages/kal-frontend/src/app/layout.tsx b/packages/kal-frontend/src/app/layout.tsx index 799ffc3..7652df1 100644 --- a/packages/kal-frontend/src/app/layout.tsx +++ b/packages/kal-frontend/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { Inter } from "next/font/google"; + import "./globals.css"; import { TRPCProvider } from "@/lib/trpc-provider"; diff --git a/packages/kal-frontend/src/app/page.tsx b/packages/kal-frontend/src/app/page.tsx index f50b349..f4ac186 100644 --- a/packages/kal-frontend/src/app/page.tsx +++ b/packages/kal-frontend/src/app/page.tsx @@ -1,13 +1,13 @@ -import { Navbar } from "@/components/landing/Navbar"; +import { FinalCTA } from "@/components/landing/CTA"; +import { FAQ } from "@/components/landing/FAQ"; +import { Features } from "@/components/landing/Features"; +import { Footer } from "@/components/landing/Footer"; import { Hero } from "@/components/landing/Hero"; -import { ProblemSolution } from "@/components/landing/ProblemSolution"; import { HowItWorks } from "@/components/landing/HowItWorks"; -import { Features } from "@/components/landing/Features"; +import { Navbar } from "@/components/landing/Navbar"; +import { ProblemSolution } from "@/components/landing/ProblemSolution"; import { SampleFoods } from "@/components/landing/SampleFoods"; import { Testimonials } from "@/components/landing/Testimonials"; -import { FAQ } from "@/components/landing/FAQ"; -import { FinalCTA } from "@/components/landing/CTA"; -import { Footer } from "@/components/landing/Footer"; export default function LandingPage() { return ( diff --git a/packages/kal-frontend/src/app/search/page.tsx b/packages/kal-frontend/src/app/search/page.tsx index 15d5b2a..0fe3fe5 100644 --- a/packages/kal-frontend/src/app/search/page.tsx +++ b/packages/kal-frontend/src/app/search/page.tsx @@ -1,7 +1,8 @@ "use client"; -import { useState, useEffect, useCallback, useRef } from "react"; import Link from "next/link"; +import { useState, useEffect, useCallback, useRef } from "react"; + import { trpc } from "@/lib/trpc"; interface Food { @@ -15,11 +16,21 @@ interface Food { category?: string; } +interface HalalFood extends Food { + brand?: string; + halalCertifier?: string; + halalCertYear?: number; +} + +type TabType = "natural" | "halal"; + export default function SearchPage() { + const [activeTab, setActiveTab] = useState("natural"); const [inputValue, setInputValue] = useState(""); const [searchQuery, setSearchQuery] = useState(""); - const [selectedFood, setSelectedFood] = useState(null); + const [selectedFood, setSelectedFood] = useState(null); const [selectedCategory, setSelectedCategory] = useState(""); + const [selectedBrand, setSelectedBrand] = useState(""); const loadMoreRef = useRef(null); // Debounce effect - trigger search after 1.5s of no typing @@ -31,6 +42,13 @@ export default function SearchPage() { return () => clearTimeout(timer); }, [inputValue]); + // Reset filters when switching tabs + useEffect(() => { + setSelectedCategory(""); + setSelectedBrand(""); + setSelectedFood(null); + }, [activeTab]); + // Handle Enter key press const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { @@ -41,17 +59,19 @@ export default function SearchPage() { [inputValue] ); - // Get categories - const { data: categories } = trpc.food.categories.useQuery(); + // ============================================ + // Natural Foods Queries + // ============================================ + const { data: naturalCategories } = trpc.food.categories.useQuery(undefined, { + enabled: activeTab === "natural", + }); - // Search query (when searching) const { data: searchResults, isLoading: isSearching } = trpc.food.search.useQuery( { query: searchQuery }, - { enabled: searchQuery.length > 0 } + { enabled: searchQuery.length > 0 && activeTab === "natural" } ); - // Infinite query for browsing (when not searching) const { data: paginatedData, isLoading: isPaginating, @@ -61,7 +81,35 @@ export default function SearchPage() { } = trpc.food.allPaginated.useInfiniteQuery( { limit: 10, category: selectedCategory || undefined }, { - enabled: searchQuery.length === 0, + enabled: searchQuery.length === 0 && activeTab === "natural", + getNextPageParam: (lastPage) => lastPage.nextCursor, + initialCursor: 0, + } + ); + + // ============================================ + // Halal Foods Queries + // ============================================ + const { data: halalBrands } = trpc.halal.brands.useQuery(undefined, { + enabled: activeTab === "halal", + }); + + const { data: halalSearchResults, isLoading: isHalalSearching } = + trpc.halal.search.useQuery( + { query: searchQuery }, + { enabled: searchQuery.length > 0 && activeTab === "halal" } + ); + + const { + data: halalPaginatedData, + isLoading: isHalalPaginating, + fetchNextPage: fetchNextHalalPage, + hasNextPage: hasNextHalalPage, + isFetchingNextPage: isFetchingNextHalalPage, + } = trpc.halal.allPaginated.useInfiniteQuery( + { limit: 10, brand: selectedBrand || undefined }, + { + enabled: searchQuery.length === 0 && activeTab === "halal", getNextPageParam: (lastPage) => lastPage.nextCursor, initialCursor: 0, } @@ -73,8 +121,12 @@ export default function SearchPage() { const observer = new IntersectionObserver( (entries) => { - if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) { - fetchNextPage(); + if (entries[0].isIntersecting) { + if (activeTab === "natural" && hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } else if (activeTab === "halal" && hasNextHalalPage && !isFetchingNextHalalPage) { + fetchNextHalalPage(); + } } }, { threshold: 0.1 } @@ -82,19 +134,41 @@ export default function SearchPage() { observer.observe(loadMoreRef.current); return () => observer.disconnect(); - }, [hasNextPage, isFetchingNextPage, fetchNextPage, searchQuery]); + }, [ + activeTab, + hasNextPage, + isFetchingNextPage, + fetchNextPage, + hasNextHalalPage, + isFetchingNextHalalPage, + fetchNextHalalPage, + searchQuery, + ]); - // Determine which data to show + // Determine which data to show based on active tab const isSearchMode = searchQuery.length > 0; - const isLoading = isSearchMode ? isSearching : isPaginating; + const isLoading = activeTab === "natural" + ? (isSearchMode ? isSearching : isPaginating) + : (isSearchMode ? isHalalSearching : isHalalPaginating); + + const foods: (Food | HalalFood)[] = activeTab === "natural" + ? (isSearchMode + ? searchResults || [] + : paginatedData?.pages.flatMap((page) => page.items) || []) + : (isSearchMode + ? halalSearchResults || [] + : halalPaginatedData?.pages.flatMap((page) => page.items) || []); - const foods: Food[] = isSearchMode - ? searchResults || [] - : paginatedData?.pages.flatMap((page) => page.items) || []; + const totalCount = activeTab === "natural" + ? (isSearchMode + ? searchResults?.length || 0 + : paginatedData?.pages[0]?.total || 0) + : (isSearchMode + ? halalSearchResults?.length || 0 + : halalPaginatedData?.pages[0]?.total || 0); - const totalCount = isSearchMode - ? searchResults?.length || 0 - : paginatedData?.pages[0]?.total || 0; + const currentHasNextPage = activeTab === "natural" ? hasNextPage : hasNextHalalPage; + const currentIsFetchingNextPage = activeTab === "natural" ? isFetchingNextPage : isFetchingNextHalalPage; return (
@@ -119,6 +193,39 @@ export default function SearchPage() {

+ {/* Tab Switcher */} +
+ + +
+ + {/* Halal Tab Info Banner */} + {activeTab === "halal" && ( +
+

+ ✓ All items are JAKIM certified halal with verified brand information +

+
+ )} + {/* Search Bar */}
@@ -141,22 +248,30 @@ export default function SearchPage() { value={inputValue} onChange={(e) => setInputValue(e.target.value)} onKeyDown={handleKeyDown} - placeholder="Search foods... (press Enter or wait to search)" - className="w-full pl-12 pr-4 py-4 rounded-xl bg-dark-surface border border-dark-border - text-content-primary placeholder-content-muted - focus:ring-2 focus:ring-accent/50 focus:border-accent/50 - transition-all duration-200" + placeholder={activeTab === "natural" + ? "Search Malaysian foods..." + : "Search halal foods by name or brand..."} + className={`w-full pl-12 pr-4 py-4 rounded-xl bg-dark-surface border + text-content-primary placeholder-content-muted transition-all duration-200 + ${activeTab === "halal" + ? "border-emerald-500/30 focus:ring-2 focus:ring-emerald-500/50 focus:border-emerald-500/50" + : "border-dark-border focus:ring-2 focus:ring-accent/50 focus:border-accent/50" + }`} /> {/* Loading indicator when typing */} {inputValue !== searchQuery && inputValue.length > 0 && (
-
+
)}
- {/* Category Filter (only when not searching) */} - {!isSearchMode && categories && categories.length > 0 && ( + {/* Category Filter (Natural Foods) */} + {activeTab === "natural" && !isSearchMode && naturalCategories && naturalCategories.length > 0 && (
- {categories.map((cat) => ( + {naturalCategories.map((cat) => (
)} + {/* Brand Filter (Halal Foods) */} + {activeTab === "halal" && !isSearchMode && halalBrands && halalBrands.length > 0 && ( +
+ + {halalBrands.map((brand) => ( + + ))} +
+ )} + {/* Results count */} {!isLoading && foods.length > 0 && (

@@ -211,75 +357,105 @@ export default function SearchPage() { ) : foods.length > 0 ? ( <>

- {foods.map((food) => ( - - ))} + + ); + })}
{/* Load more trigger (only for pagination mode) */} {!isSearchMode && (
- {isFetchingNextPage ? ( + {currentIsFetchingNextPage ? (
-
+
Loading more...
- ) : hasNextPage ? ( + ) : currentHasNextPage ? (

Scroll for more

@@ -297,7 +473,9 @@ export default function SearchPage() {

{searchQuery ? `No foods found for "${searchQuery}"` - : "No food data"} + : activeTab === "halal" + ? "No halal certified foods available" + : "No food data"}

)} @@ -305,16 +483,32 @@ export default function SearchPage() { {/* Selected Food Detail Modal */} {selectedFood && (
-
+

{selectedFood.name}

- {selectedFood.category && ( - - {selectedFood.category} - +
+ {"brand" in selectedFood && (selectedFood as HalalFood).brand && ( + + {(selectedFood as HalalFood).brand} + + )} + {selectedFood.category && ( + + {selectedFood.category} + + )} +
+ {"brand" in selectedFood && (selectedFood as HalalFood).halalCertifier && ( +

+ {(selectedFood as HalalFood).halalCertifier} Certified + {(selectedFood as HalalFood).halalCertYear && ` (${(selectedFood as HalalFood).halalCertYear})`} +

)}
-
-

+

+

{selectedFood.calories}

Calories

diff --git a/packages/kal-frontend/src/components/landing/CTA.tsx b/packages/kal-frontend/src/components/landing/CTA.tsx index e7a8249..79a3141 100644 --- a/packages/kal-frontend/src/components/landing/CTA.tsx +++ b/packages/kal-frontend/src/components/landing/CTA.tsx @@ -1,5 +1,5 @@ -import { Container } from "@/components/ui/Container"; import { Button } from "@/components/ui/Button"; +import { Container } from "@/components/ui/Container"; export function FinalCTA() { return ( diff --git a/packages/kal-frontend/src/components/landing/FAQ.tsx b/packages/kal-frontend/src/components/landing/FAQ.tsx index 97d6c9a..7a75bda 100644 --- a/packages/kal-frontend/src/components/landing/FAQ.tsx +++ b/packages/kal-frontend/src/components/landing/FAQ.tsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; + import { Container } from "@/components/ui/Container"; import { SectionHeading } from "@/components/ui/SectionHeading"; diff --git a/packages/kal-frontend/src/components/landing/Footer.tsx b/packages/kal-frontend/src/components/landing/Footer.tsx index 5d7db1e..d1396aa 100644 --- a/packages/kal-frontend/src/components/landing/Footer.tsx +++ b/packages/kal-frontend/src/components/landing/Footer.tsx @@ -1,4 +1,5 @@ import Link from "next/link"; + import { Container } from "@/components/ui/Container"; export function Footer() { diff --git a/packages/kal-frontend/src/components/landing/Navbar.tsx b/packages/kal-frontend/src/components/landing/Navbar.tsx index 6bd79b9..ace3e4f 100644 --- a/packages/kal-frontend/src/components/landing/Navbar.tsx +++ b/packages/kal-frontend/src/components/landing/Navbar.tsx @@ -1,7 +1,8 @@ "use client"; -import { useState } from "react"; import Link from "next/link"; +import { useState } from "react"; + import { Button } from "@/components/ui/Button"; import { Container } from "@/components/ui/Container"; diff --git a/packages/kal-frontend/src/components/landing/SampleFoods.tsx b/packages/kal-frontend/src/components/landing/SampleFoods.tsx index 031953c..5b0948c 100644 --- a/packages/kal-frontend/src/components/landing/SampleFoods.tsx +++ b/packages/kal-frontend/src/components/landing/SampleFoods.tsx @@ -1,6 +1,6 @@ +import { Button } from "@/components/ui/Button"; import { Container } from "@/components/ui/Container"; import { SectionHeading } from "@/components/ui/SectionHeading"; -import { Button } from "@/components/ui/Button"; const sampleFoods = [ { name: "Grilled Chicken", calories: 165, protein: 31, carbs: 0, fat: 3.6, serving: "100g" }, diff --git a/packages/kal-frontend/src/components/navbar.tsx b/packages/kal-frontend/src/components/navbar.tsx new file mode 100644 index 0000000..512aaa9 --- /dev/null +++ b/packages/kal-frontend/src/components/navbar.tsx @@ -0,0 +1,37 @@ +import { getLogtoContext, signIn, signOut } from '@logto/next/server-actions'; + +import SignIn from '@/components/sign-in'; +import SignOut from '@/components/sign-out'; +import { logtoConfig } from '@/lib/logto'; + +export default async function Navbar() { + const { isAuthenticated, claims } = await getLogtoContext(logtoConfig); + + return ( + + ); +} diff --git a/packages/kal-frontend/src/components/sign-in.tsx b/packages/kal-frontend/src/components/sign-in.tsx new file mode 100644 index 0000000..39db2e8 --- /dev/null +++ b/packages/kal-frontend/src/components/sign-in.tsx @@ -0,0 +1,16 @@ +'use client'; + +type Props = { + onSignIn: () => Promise; +}; + +export default function SignIn({ onSignIn }: Props) { + return ( + + ); +} diff --git a/packages/kal-frontend/src/components/sign-out.tsx b/packages/kal-frontend/src/components/sign-out.tsx new file mode 100644 index 0000000..422fc82 --- /dev/null +++ b/packages/kal-frontend/src/components/sign-out.tsx @@ -0,0 +1,16 @@ +'use client'; + +type Props = { + onSignOut: () => Promise; +}; + +export default function SignOut({ onSignOut }: Props) { + return ( + + ); +} diff --git a/packages/kal-frontend/src/lib/logto.ts b/packages/kal-frontend/src/lib/logto.ts new file mode 100644 index 0000000..959b4eb --- /dev/null +++ b/packages/kal-frontend/src/lib/logto.ts @@ -0,0 +1,10 @@ +import type { LogtoNextConfig } from '@logto/next'; + +export const logtoConfig: LogtoNextConfig = { + endpoint: process.env.NEXT_PUBLIC_LOGTO_ENDPOINT || 'http://localhost:3001', + appId: process.env.NEXT_PUBLIC_LOGTO_APP_ID || '', + appSecret: process.env.LOGTO_APP_SECRET || '', + baseUrl: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000', + cookieSecret: process.env.SESSION_SECRET || 'dev-secret-please-change-in-production', + cookieSecure: process.env.NODE_ENV === 'production', +}; diff --git a/packages/kal-frontend/src/lib/trpc-provider.tsx b/packages/kal-frontend/src/lib/trpc-provider.tsx index 4525109..a9352ba 100644 --- a/packages/kal-frontend/src/lib/trpc-provider.tsx +++ b/packages/kal-frontend/src/lib/trpc-provider.tsx @@ -1,8 +1,9 @@ "use client"; -import { useState } from "react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { httpBatchLink } from "@trpc/client"; +import { useState } from "react"; + import { trpc } from "./trpc"; function getBaseUrl() { diff --git a/packages/kal-frontend/src/lib/trpc.ts b/packages/kal-frontend/src/lib/trpc.ts index f102614..4ce03a7 100644 --- a/packages/kal-frontend/src/lib/trpc.ts +++ b/packages/kal-frontend/src/lib/trpc.ts @@ -1,5 +1,5 @@ import { createTRPCReact } from "@trpc/react-query"; -import type { AppRouter } from "kal-backend/src/routers"; import type { CreateTRPCReact } from "@trpc/react-query"; +import type { AppRouter } from "kal-backend/src/routers"; export const trpc: CreateTRPCReact = createTRPCReact(); diff --git a/packages/kal-shared/package.json b/packages/kal-shared/package.json index d1d8c76..6df932e 100644 --- a/packages/kal-shared/package.json +++ b/packages/kal-shared/package.json @@ -7,6 +7,8 @@ "scripts": { "build": "tsc", "dev": "tsc --watch", + "lint": "eslint .", + "lint:fix": "eslint . --fix", "typecheck": "tsc --noEmit", "clean": "rm -rf dist" }, diff --git a/packages/kal-shared/src/types/index.ts b/packages/kal-shared/src/types/index.ts index 47360f7..4447885 100644 --- a/packages/kal-shared/src/types/index.ts +++ b/packages/kal-shared/src/types/index.ts @@ -1,12 +1,54 @@ +// =================== +// User Tiers +// =================== +export type UserTier = 'free' | 'tier_1' | 'tier_2'; + export interface User { _id: string; logtoId: string; email: string; name?: string; + tier: UserTier; createdAt: Date; updatedAt: Date; } +// =================== +// Rate Limiting +// =================== +export interface RateLimitConfig { + dailyLimit: number; + burstLimit: number; // per minute +} + +export const RATE_LIMITS: Record = { + free: { dailyLimit: 100, burstLimit: 10 }, + tier_1: { dailyLimit: 250, burstLimit: 30 }, + tier_2: { dailyLimit: 700, burstLimit: 50 }, +}; + +export interface RateLimitUsage { + _id: string; + userId: string; + date: string; // YYYY-MM-DD format + dailyCount: number; + minuteWindow: Date; + minuteCount: number; + updatedAt: Date; +} + +// =================== +// Logto Auth +// =================== +export interface LogtoUserInfo { + sub: string; // Logto user ID + email?: string; + name?: string; + picture?: string; +} + + + export interface FoodEntry { _id: string; userId: string; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8aaa40b..86f1bec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,36 @@ importers: .: devDependencies: + '@eslint/js': + specifier: ^9.39.2 + version: 9.39.2 + '@typescript-eslint/eslint-plugin': + specifier: ^8.50.1 + version: 8.50.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.50.1 + version: 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + eslint: + specifier: ^9.39.2 + version: 9.39.2(jiti@1.21.7) + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@9.39.2(jiti@1.21.7)) + eslint-plugin-import: + specifier: ^2.32.0 + version: 2.32.0(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7)) + eslint-plugin-react: + specifier: ^7.37.5 + version: 7.37.5(eslint@9.39.2(jiti@1.21.7)) + eslint-plugin-react-hooks: + specifier: ^7.0.1 + version: 7.0.1(eslint@9.39.2(jiti@1.21.7)) + globals: + specifier: ^16.5.0 + version: 16.5.0 + prettier: + specifier: ^3.7.4 + version: 3.7.4 turbo: specifier: ^2.3.3 version: 2.7.1 @@ -17,9 +47,15 @@ importers: packages/kal-backend: dependencies: + '@logto/express': + specifier: ^3.0.12 + version: 3.0.12(express@4.22.1) '@trpc/server': specifier: ^11.0.0-rc.682 version: 11.8.1(typescript@5.9.3) + cookie-parser: + specifier: ^1.4.7 + version: 1.4.7 cors: specifier: ^2.8.5 version: 2.8.5 @@ -29,6 +65,9 @@ importers: express: specifier: ^4.21.2 version: 4.22.1 + express-session: + specifier: ^1.18.2 + version: 1.18.2 kal-shared: specifier: workspace:* version: link:../kal-shared @@ -42,12 +81,18 @@ importers: specifier: ^3.24.1 version: 3.25.76 devDependencies: + '@types/cookie-parser': + specifier: ^1.4.10 + version: 1.4.10(@types/express@5.0.6) '@types/cors': specifier: ^2.8.17 version: 2.8.19 '@types/express': specifier: ^5.0.0 version: 5.0.6 + '@types/express-session': + specifier: ^1.18.2 + version: 1.18.2 '@types/node': specifier: ^22.10.2 version: 22.19.3 @@ -85,6 +130,9 @@ importers: packages/kal-frontend: dependencies: + '@logto/next': + specifier: ^4.2.7 + version: 4.2.7(next@15.5.9(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) '@tanstack/react-query': specifier: ^5.62.11 version: 5.90.12(react@19.2.3) @@ -99,7 +147,7 @@ importers: version: link:../kal-shared next: specifier: ^15.1.2 - version: 15.5.9(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 15.5.9(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: specifier: ^19.0.0 version: 19.2.3 @@ -151,10 +199,81 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.5': + resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.5': + resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.5': + resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.5': + resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + engines: {node: '>=6.9.0'} + '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} + '@edge-runtime/cookies@5.0.2': + resolution: {integrity: sha512-Sd8LcWpZk/SWEeKGE8LT6gMm5MGfX/wm+GPnh1eBEtCpya3vYqn37wYknwAHw92ONoyyREl1hJwxV/Qx2DWNOg==} + engines: {node: '>=16'} + '@emnapi/runtime@1.7.1': resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} @@ -314,6 +433,60 @@ packages: cpu: [x64] os: [win32] + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.1': + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.3': + resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.2': + resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + '@img/colour@1.0.0': resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} engines: {node: '>=18'} @@ -454,6 +627,9 @@ packages: '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -464,6 +640,25 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@logto/client@3.1.5': + resolution: {integrity: sha512-xitL6NT09HU41ebNlRfQhjeOL5Ct0nZOwQFqPaIWIvOD8QZ454xN+eSRodBP/xVGuh4znOX8Jrlz0fFQFhKqgQ==} + + '@logto/express@3.0.12': + resolution: {integrity: sha512-JpCzxWaz3t49R8nuR6J0hZjJl6zOVLYHquMoZULsmcewPGz1/+lz2UqV5I0ORvEj6YpIl5MIrgtLtccOOIQpIA==} + peerDependencies: + express: '>=4' + + '@logto/js@6.1.0': + resolution: {integrity: sha512-FGFDtqZqVP0mUR7ze7Uz488TsMSWQkMiU00PefJg/MMQJeI93WejPcpPkGd4JvoKAVj99wfsY/0AR1gMADCLyQ==} + + '@logto/next@4.2.7': + resolution: {integrity: sha512-VoG+tskWqtgDKurfBT9Iv0/FJd/j+D1JqqO2jbkZPJe1bTQaIDi8FrgjjzqrtoaXEvRehezplde968sgMolY2g==} + peerDependencies: + next: '>=12' + + '@logto/node@3.1.7': + resolution: {integrity: sha512-P75Q6lrIrHQu2M+stEA2Cykm+WIrDlKMKyLDOddPdoNBp5VeDGpP+H6b3GhY2UE8e0BpEsMqHGDp00NME3mb+g==} + '@mongodb-js/saslprep@1.4.4': resolution: {integrity: sha512-p7X/ytJDIdwUfFL/CLOhKgdfJe1Fa8uw9seJYvdOmnP9JBWGWHW69HkOixXS6Wy9yvGf1MbhcS6lVmrhy4jm2g==} @@ -530,9 +725,16 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@scarf/scarf@1.4.0': resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + '@silverhand/essentials@2.9.2': + resolution: {integrity: sha512-bD+82D9Dfa1F5xX1kfdR5ODIoJS41NOxTuHx4shVS5A4/ayEG+ZplpDDjB19fsa7kZXgSgD75R4sUCXjm88x6w==} + engines: {node: ^18.12.0 || ^20.9.0 || ^22.0.0, pnpm: ^9.0.0} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -571,18 +773,35 @@ packages: '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/cookie-parser@1.4.10': + resolution: {integrity: sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==} + peerDependencies: + '@types/express': '*' + '@types/cors@2.8.19': resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/express-serve-static-core@5.1.0': resolution: {integrity: sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==} + '@types/express-session@1.18.2': + resolution: {integrity: sha512-k+I0BxwVXsnEU2hV77cCobC08kIsn4y44C3gC0b46uxZVMaXA04lSPgRLR/bSL2w0t0ShJiG8o4jPzRG/nscFg==} + '@types/express@5.0.6': resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} '@types/http-errors@2.0.5': resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/node@22.19.3': resolution: {integrity: sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==} @@ -615,14 +834,90 @@ packages: '@types/whatwg-url@11.0.5': resolution: {integrity: sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==} + '@typescript-eslint/eslint-plugin@8.50.1': + resolution: {integrity: sha512-PKhLGDq3JAg0Jk/aK890knnqduuI/Qj+udH7wCf0217IGi4gt+acgCyPVe79qoT+qKUvHMDQkwJeKW9fwl8Cyw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.50.1 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.50.1': + resolution: {integrity: sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.50.1': + resolution: {integrity: sha512-E1ur1MCVf+YiP89+o4Les/oBAVzmSbeRB0MQLfSlYtbWU17HPxZ6Bhs5iYmKZRALvEuBoXIZMOIRRc/P++Ortg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.50.1': + resolution: {integrity: sha512-mfRx06Myt3T4vuoHaKi8ZWNTPdzKPNBhiblze5N50//TSHOAQQevl/aolqA/BcqqbJ88GUnLqjjcBc8EWdBcVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.50.1': + resolution: {integrity: sha512-ooHmotT/lCWLXi55G4mvaUF60aJa012QzvLK0Y+Mp4WdSt17QhMhWOaBWeGTFVkb2gDgBe19Cxy1elPXylslDw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.50.1': + resolution: {integrity: sha512-7J3bf022QZE42tYMO6SL+6lTPKFk/WphhRPe9Tw/el+cEwzLz1Jjz2PX3GtGQVxooLDKeMVmMt7fWpYRdG5Etg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.50.1': + resolution: {integrity: sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.50.1': + resolution: {integrity: sha512-woHPdW+0gj53aM+cxchymJCrh0cyS7BTIdcDxWUNsclr9VDkOSbqC13juHzxOmQ22dDkMZEpZB+3X1WpUvzgVQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.50.1': + resolution: {integrity: sha512-lCLp8H1T9T7gPbEuJSnHwnSuO9mDf8mfK/Nion5mZmiEaQD9sWf9W4dfeFqRyqRjF06/kBuTmAqcs9sewM2NbQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.50.1': + resolution: {integrity: sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -633,9 +928,48 @@ packages: arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + autoprefixer@10.4.23: resolution: {integrity: sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==} engines: {node: ^10 || ^12 || >=14} @@ -643,6 +977,13 @@ packages: peerDependencies: postcss: ^8.1.0 + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + baseline-browser-mapping@2.9.11: resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} hasBin: true @@ -655,6 +996,12 @@ packages: resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -676,17 +1023,37 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + call-bound@1.0.4: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + camelcase-css@2.0.1: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} + camelcase-keys@9.1.3: + resolution: {integrity: sha512-Rircqi9ch8AnZscQcsA1C47NFdaO3wukpmIRzYcDOrmvgt78hM/sj5pZhZNec2NM12uk5vTwRHZ4anGcrC4ZTg==} + engines: {node: '>=16'} + + camelcase@8.0.0: + resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} + engines: {node: '>=16'} + caniuse-lite@1.0.30001761: resolution: {integrity: sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==} + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -698,6 +1065,13 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + commander@14.0.2: resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} engines: {node: '>=20'} @@ -706,6 +1080,9 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -714,6 +1091,16 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-parser@1.4.7: + resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==} + engines: {node: '>= 0.8.0'} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + cookie-signature@1.0.7: resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} @@ -721,10 +1108,18 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + cors@2.8.5: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} engines: {node: '>= 0.10'} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -733,6 +1128,18 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -741,6 +1148,34 @@ packages: supports-color: optional: true + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -759,6 +1194,10 @@ packages: dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + dotenv@16.6.1: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} @@ -780,6 +1219,10 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + es-abstract@1.24.1: + resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} + engines: {node: '>= 0.4'} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -788,10 +1231,26 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-iterator-helpers@1.2.2: + resolution: {integrity: sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==} + engines: {node: '>= 0.4'} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + esbuild@0.27.2: resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} engines: {node: '>=18'} @@ -804,18 +1263,129 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-react-hooks@7.0.1: + resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.39.2: + resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + express-session@1.18.2: + resolution: {integrity: sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==} + engines: {node: '>= 0.8.0'} + express@4.22.1: resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} engines: {node: '>= 0.10.0'} + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -828,6 +1398,10 @@ packages: picomatch: optional: true + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -836,6 +1410,21 @@ packages: resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} engines: {node: '>= 0.8'} + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -855,6 +1444,21 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -863,6 +1467,10 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + get-tsconfig@4.13.0: resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} @@ -874,18 +1482,55 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@16.5.0: + resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -894,41 +1539,202 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + is-core-module@2.16.1: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} - is-number@7.0.0: + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + jiti@1.21.7: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true + jose@5.10.0: + resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} + + js-base64@3.7.8: + resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -936,6 +1742,24 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + map-obj@5.0.0: + resolution: {integrity: sha512-2L3MIgJynYrZ3TYMriLDLWocz15okFakV6J12HXvMXDHui2x/zgChzg1u9mFFGbbGWE+GsLpQByt4POb9Or+uA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -982,6 +1806,16 @@ packages: engines: {node: '>=4'} hasBin: true + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + mongodb-connection-string-url@3.0.2: resolution: {integrity: sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==} @@ -1026,6 +1860,9 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@0.6.3: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} @@ -1070,14 +1907,70 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} + on-headers@1.1.0: + resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} + engines: {node: '>= 0.8'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -1103,6 +1996,10 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + postcss-import@15.1.0: resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} @@ -1154,6 +2051,18 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.7.4: + resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} + engines: {node: '>=14'} + hasBin: true + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -1169,6 +2078,14 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quick-lru@6.1.2: + resolution: {integrity: sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==} + engines: {node: '>=12'} + + random-bytes@1.0.0: + resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==} + engines: {node: '>= 0.8'} + range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -1182,6 +2099,9 @@ packages: peerDependencies: react: ^19.2.3 + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react@19.2.3: resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} engines: {node: '>=0.10.0'} @@ -1193,6 +2113,18 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -1201,6 +2133,10 @@ packages: engines: {node: '>= 0.4'} hasBin: true + resolve@2.0.0-next.5: + resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} + hasBin: true + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -1208,15 +2144,31 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + semver@7.7.3: resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} @@ -1230,6 +2182,18 @@ packages: resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} engines: {node: '>= 0.8.0'} + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -1237,6 +2201,14 @@ packages: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + side-channel-list@1.0.0: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} engines: {node: '>= 0.4'} @@ -1264,14 +2236,45 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} @@ -1290,6 +2293,10 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -1331,9 +2338,18 @@ packages: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -1376,15 +2392,47 @@ packages: resolution: {integrity: sha512-zAj9jGc7VDvuAo/5Jbos4QTtWz9uUpkMhMKGyTjDJkx//hdL2bM31qQoJSAbU+7JyK5vb0LPzpwf6DUt3zayqg==} hasBin: true + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true + uid-safe@2.1.5: + resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==} + engines: {node: '>= 0.8'} + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -1398,6 +2446,9 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -1417,11 +2468,49 @@ packages: resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} engines: {node: '>=18'} + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.8.2: resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} engines: {node: '>= 14.6'} hasBin: true + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -1429,9 +2518,111 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.5': {} + + '@babel/core@7.28.5': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.5': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.5 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + + '@babel/parser@7.28.5': + dependencies: + '@babel/types': 7.28.5 + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + + '@babel/traverse@7.28.5': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@colors/colors@1.5.0': optional: true + '@edge-runtime/cookies@5.0.2': {} + '@emnapi/runtime@1.7.1': dependencies: tslib: 2.8.1 @@ -1515,6 +2706,63 @@ snapshots: '@esbuild/win32-x64@0.27.2': optional: true + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.2(jiti@1.21.7))': + dependencies: + eslint: 9.39.2(jiti@1.21.7) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.1': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.3': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.2': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + '@img/colour@1.0.0': optional: true @@ -1617,6 +2865,11 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.5': {} @@ -1626,13 +2879,43 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@mongodb-js/saslprep@1.4.4': + '@logto/client@3.1.5': dependencies: - sparse-bitfield: 3.0.3 + '@logto/js': 6.1.0 + '@silverhand/essentials': 2.9.2 + camelcase-keys: 9.1.3 + jose: 5.10.0 - '@next/env@15.5.9': {} + '@logto/express@3.0.12(express@4.22.1)': + dependencies: + '@logto/node': 3.1.7 + express: 4.22.1 - '@next/swc-darwin-arm64@15.5.7': + '@logto/js@6.1.0': + dependencies: + '@silverhand/essentials': 2.9.2 + camelcase-keys: 9.1.3 + + '@logto/next@4.2.7(next@15.5.9(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))': + dependencies: + '@edge-runtime/cookies': 5.0.2 + '@logto/node': 3.1.7 + cookie: 1.1.1 + next: 15.5.9(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + + '@logto/node@3.1.7': + dependencies: + '@logto/client': 3.1.5 + '@silverhand/essentials': 2.9.2 + js-base64: 3.7.8 + + '@mongodb-js/saslprep@1.4.4': + dependencies: + sparse-bitfield: 3.0.3 + + '@next/env@15.5.9': {} + + '@next/swc-darwin-arm64@15.5.7': optional: true '@next/swc-darwin-x64@15.5.7': @@ -1668,8 +2951,12 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 + '@rtsao/scc@1.1.0': {} + '@scarf/scarf@1.4.0': {} + '@silverhand/essentials@2.9.2': {} + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -1708,10 +2995,16 @@ snapshots: dependencies: '@types/node': 22.19.3 + '@types/cookie-parser@1.4.10(@types/express@5.0.6)': + dependencies: + '@types/express': 5.0.6 + '@types/cors@2.8.19': dependencies: '@types/node': 22.19.3 + '@types/estree@1.0.8': {} + '@types/express-serve-static-core@5.1.0': dependencies: '@types/node': 22.19.3 @@ -1719,6 +3012,10 @@ snapshots: '@types/range-parser': 1.2.7 '@types/send': 1.2.1 + '@types/express-session@1.18.2': + dependencies: + '@types/express': 5.0.6 + '@types/express@5.0.6': dependencies: '@types/body-parser': 1.19.6 @@ -1727,6 +3024,10 @@ snapshots: '@types/http-errors@2.0.5': {} + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + '@types/node@22.19.3': dependencies: undici-types: 6.21.0 @@ -1763,13 +3064,121 @@ snapshots: dependencies: '@types/webidl-conversions': 7.0.3 + '@typescript-eslint/eslint-plugin@8.50.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.50.1 + '@typescript-eslint/type-utils': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.50.1 + eslint: 9.39.2(jiti@1.21.7) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.50.1 + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.50.1 + debug: 4.4.3 + eslint: 9.39.2(jiti@1.21.7) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.50.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.50.1(typescript@5.9.3) + '@typescript-eslint/types': 8.50.1 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.50.1': + dependencies: + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/visitor-keys': 8.50.1 + + '@typescript-eslint/tsconfig-utils@8.50.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.2(jiti@1.21.7) + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.50.1': {} + + '@typescript-eslint/typescript-estree@8.50.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.50.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.50.1(typescript@5.9.3) + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/visitor-keys': 8.50.1 + debug: 4.4.3 + minimatch: 9.0.5 + semver: 7.7.3 + tinyglobby: 0.2.15 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@1.21.7)) + '@typescript-eslint/scope-manager': 8.50.1 + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3) + eslint: 9.39.2(jiti@1.21.7) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.50.1': + dependencies: + '@typescript-eslint/types': 8.50.1 + eslint-visitor-keys: 4.2.1 + accepts@1.3.8: dependencies: mime-types: 2.1.35 negotiator: 0.6.3 + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + ansi-regex@5.0.1: {} + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + any-promise@1.3.0: {} anymatch@3.1.3: @@ -1779,8 +3188,79 @@ snapshots: arg@5.0.2: {} + argparse@2.0.1: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + array-flatten@1.1.1: {} + array-includes@3.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + async-function@1.0.0: {} + autoprefixer@10.4.23(postcss@8.5.6): dependencies: browserslist: 4.28.1 @@ -1790,6 +3270,12 @@ snapshots: postcss: 8.5.6 postcss-value-parser: 4.2.0 + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + balanced-match@1.0.2: {} + baseline-browser-mapping@2.9.11: {} binary-extensions@2.3.0: {} @@ -1811,6 +3297,15 @@ snapshots: transitivePeerDependencies: - supports-color + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -1832,15 +3327,38 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + call-bound@1.0.4: dependencies: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + callsites@3.1.0: {} + camelcase-css@2.0.1: {} + camelcase-keys@9.1.3: + dependencies: + camelcase: 8.0.0 + map-obj: 5.0.0 + quick-lru: 6.1.2 + type-fest: 4.41.0 + + camelcase@8.0.0: {} + caniuse-lite@1.0.30001761: {} + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -1861,33 +3379,98 @@ snapshots: client-only@0.0.1: {} + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + commander@14.0.2: {} commander@4.1.1: {} + concat-map@0.0.1: {} + content-disposition@0.5.4: dependencies: safe-buffer: 5.2.1 content-type@1.0.5: {} + convert-source-map@2.0.0: {} + + cookie-parser@1.4.7: + dependencies: + cookie: 0.7.2 + cookie-signature: 1.0.6 + + cookie-signature@1.0.6: {} + cookie-signature@1.0.7: {} cookie@0.7.2: {} + cookie@1.1.1: {} + cors@2.8.5: dependencies: object-assign: 4.1.1 vary: 1.1.2 + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + cssesc@3.0.0: {} csstype@3.2.3: {} + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + debug@2.6.9: dependencies: ms: 2.0.0 + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + depd@2.0.0: {} destroy@1.2.0: {} @@ -1899,6 +3482,10 @@ snapshots: dlv@1.1.3: {} + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + dotenv@16.6.1: {} dunder-proto@1.0.1: @@ -1915,14 +3502,107 @@ snapshots: encodeurl@2.0.0: {} + es-abstract@1.24.1: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.19 + es-define-property@1.0.1: {} es-errors@1.3.0: {} + es-iterator-helpers@1.2.2: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + safe-array-concat: 1.1.3 + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + esbuild@0.27.2: optionalDependencies: '@esbuild/aix-ppc64': 0.27.2 @@ -1956,8 +3636,175 @@ snapshots: escape-html@1.0.3: {} + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@1.21.7)): + dependencies: + eslint: 9.39.2(jiti@1.21.7) + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.1 + resolve: 1.22.11 + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@1.21.7)): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + eslint: 9.39.2(jiti@1.21.7) + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7)): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.39.2(jiti@1.21.7) + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@1.21.7)) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-react-hooks@7.0.1(eslint@9.39.2(jiti@1.21.7)): + dependencies: + '@babel/core': 7.28.5 + '@babel/parser': 7.28.5 + eslint: 9.39.2(jiti@1.21.7) + hermes-parser: 0.25.1 + zod: 3.25.76 + zod-validation-error: 4.0.2(zod@3.25.76) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react@7.37.5(eslint@9.39.2(jiti@1.21.7)): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.2.2 + eslint: 9.39.2(jiti@1.21.7) + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.39.2(jiti@1.21.7): + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@1.21.7)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.3 + '@eslint/js': 9.39.2 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 1.21.7 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + etag@1.8.1: {} + express-session@1.18.2: + dependencies: + cookie: 0.7.2 + cookie-signature: 1.0.7 + debug: 2.6.9 + depd: 2.0.0 + on-headers: 1.1.0 + parseurl: 1.3.3 + safe-buffer: 5.2.1 + uid-safe: 2.1.5 + transitivePeerDependencies: + - supports-color + express@4.22.1: dependencies: accepts: 1.3.8 @@ -1994,6 +3841,8 @@ snapshots: transitivePeerDependencies: - supports-color + fast-deep-equal@3.1.3: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2002,6 +3851,10 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -2010,110 +3863,364 @@ snapshots: optionalDependencies: picomatch: 4.0.3 - fill-range@7.1.1: + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@1.3.2: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + forwarded@0.2.0: {} + + fraction.js@5.3.4: {} + + fresh@0.5.2: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + generator-function@2.0.1: {} + + gensync@1.0.0-beta.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-tsconfig@4.13.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globals@16.5.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inherits@2.0.4: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + ipaddr.js@1.9.1: {} + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-fullwidth-code-point@3.0.0: {} + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: dependencies: - to-regex-range: 5.0.1 + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 - finalhandler@1.3.2: + is-typed-array@1.1.15: dependencies: - debug: 2.6.9 - encodeurl: 2.0.0 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.2 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color + which-typed-array: 1.1.19 - forwarded@0.2.0: {} + is-weakmap@2.0.2: {} - fraction.js@5.3.4: {} + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 - fresh@0.5.2: {} + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 - fsevents@2.3.3: - optional: true + isarray@2.0.5: {} - function-bind@1.1.2: {} + isexe@2.0.0: {} - get-intrinsic@1.3.0: + iterator.prototype@1.1.5: dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 + define-data-property: 1.1.4 es-object-atoms: 1.1.1 - function-bind: 1.1.2 + get-intrinsic: 1.3.0 get-proto: 1.0.1 - gopd: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.2 - math-intrinsics: 1.1.0 + set-function-name: 2.0.2 - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 + jiti@1.21.7: {} - get-tsconfig@4.13.0: - dependencies: - resolve-pkg-maps: 1.0.0 + jose@5.10.0: {} - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 + js-base64@3.7.8: {} - glob-parent@6.0.2: + js-tokens@4.0.0: {} + + js-yaml@4.1.1: dependencies: - is-glob: 4.0.3 + argparse: 2.0.1 - gopd@1.2.0: {} + jsesc@3.1.0: {} - has-symbols@1.1.0: {} + json-buffer@3.0.1: {} - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 + json-schema-traverse@0.4.1: {} - http-errors@2.0.1: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.2 - toidentifier: 1.0.1 + json-stable-stringify-without-jsonify@1.0.1: {} - iconv-lite@0.4.24: + json5@1.0.2: dependencies: - safer-buffer: 2.1.2 + minimist: 1.2.8 - inherits@2.0.4: {} + json5@2.2.3: {} - ipaddr.js@1.9.1: {} + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 - is-binary-path@2.1.0: + keyv@4.5.4: dependencies: - binary-extensions: 2.3.0 + json-buffer: 3.0.1 - is-core-module@2.16.1: + levn@0.4.1: dependencies: - hasown: 2.0.2 + prelude-ls: 1.2.1 + type-check: 0.4.0 - is-extglob@2.1.1: {} + lilconfig@3.1.3: {} - is-fullwidth-code-point@3.0.0: {} + lines-and-columns@1.2.4: {} - is-glob@4.0.3: + locate-path@6.0.0: dependencies: - is-extglob: 2.1.1 + p-locate: 5.0.0 - is-number@7.0.0: {} + lodash.merge@4.6.2: {} - jiti@1.21.7: {} + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 - lilconfig@3.1.3: {} + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 - lines-and-columns@1.2.4: {} + map-obj@5.0.0: {} math-intrinsics@1.1.0: {} @@ -2146,6 +4253,16 @@ snapshots: mime@1.6.0: {} + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + mongodb-connection-string-url@3.0.2: dependencies: '@types/whatwg-url': 11.0.5 @@ -2169,9 +4286,11 @@ snapshots: nanoid@3.3.11: {} + natural-compare@1.4.0: {} + negotiator@0.6.3: {} - next@15.5.9(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + next@15.5.9(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: '@next/env': 15.5.9 '@swc/helpers': 0.5.15 @@ -2179,7 +4298,7 @@ snapshots: postcss: 8.4.31 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - styled-jsx: 5.1.6(react@19.2.3) + styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.3) optionalDependencies: '@next/swc-darwin-arm64': 15.5.7 '@next/swc-darwin-x64': 15.5.7 @@ -2204,12 +4323,83 @@ snapshots: object-inspect@1.13.4: {} + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + on-finished@2.4.1: dependencies: ee-first: 1.1.1 + on-headers@1.1.0: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + parseurl@1.3.3: {} + path-exists@4.0.0: {} + + path-key@3.1.1: {} + path-parse@1.0.7: {} path-to-regexp@0.1.12: {} @@ -2224,6 +4414,8 @@ snapshots: pirates@4.0.7: {} + possible-typed-array-names@1.1.0: {} + postcss-import@15.1.0(postcss@8.5.6): dependencies: postcss: 8.5.6 @@ -2269,6 +4461,16 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + prelude-ls@1.2.1: {} + + prettier@3.7.4: {} + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -2282,6 +4484,10 @@ snapshots: queue-microtask@1.2.3: {} + quick-lru@6.1.2: {} + + random-bytes@1.0.0: {} + range-parser@1.2.1: {} raw-body@2.5.3: @@ -2296,6 +4502,8 @@ snapshots: react: 19.2.3 scheduler: 0.27.0 + react-is@16.13.1: {} + react@19.2.3: {} read-cache@1.0.0: @@ -2306,6 +4514,28 @@ snapshots: dependencies: picomatch: 2.3.1 + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + resolve-from@4.0.0: {} + resolve-pkg-maps@1.0.0: {} resolve@1.22.11: @@ -2314,20 +4544,46 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + resolve@2.0.0-next.5: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + reusify@1.1.0: {} run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + safe-buffer@5.2.1: {} + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + safer-buffer@2.1.2: {} scheduler@0.27.0: {} - semver@7.7.3: - optional: true + semver@6.3.1: {} + + semver@7.7.3: {} send@0.19.2: dependencies: @@ -2356,6 +4612,28 @@ snapshots: transitivePeerDependencies: - supports-color + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + setprototypeof@1.2.0: {} sharp@0.34.5: @@ -2390,6 +4668,12 @@ snapshots: '@img/sharp-win32-x64': 0.34.5 optional: true + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + side-channel-list@1.0.0: dependencies: es-errors: 1.3.0 @@ -2426,20 +4710,75 @@ snapshots: statuses@2.0.2: {} + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.0 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.1 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 - styled-jsx@5.1.6(react@19.2.3): + strip-bom@3.0.0: {} + + strip-json-comments@3.1.1: {} + + styled-jsx@5.1.6(@babel/core@7.28.5)(react@19.2.3): dependencies: client-only: 0.0.1 react: 19.2.3 + optionalDependencies: + '@babel/core': 7.28.5 sucrase@3.35.1: dependencies: @@ -2451,6 +4790,10 @@ snapshots: tinyglobby: 0.2.15 ts-interface-checker: 0.1.13 + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + supports-preserve-symlinks-flag@1.0.0: {} swagger-ui-dist@5.31.0: @@ -2513,8 +4856,19 @@ snapshots: dependencies: punycode: 2.3.1 + ts-api-utils@2.1.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + ts-interface-checker@0.1.13: {} + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + tslib@2.8.1: {} tsx@4.21.0: @@ -2551,13 +4905,63 @@ snapshots: turbo-windows-64: 2.7.1 turbo-windows-arm64: 2.7.1 + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@4.41.0: {} + type-is@1.6.18: dependencies: media-typer: 0.3.0 mime-types: 2.1.35 + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + typescript@5.9.3: {} + uid-safe@2.1.5: + dependencies: + random-bytes: 1.0.0 + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + undici-types@6.21.0: {} unpipe@1.0.0: {} @@ -2568,6 +4972,10 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + util-deprecate@1.0.2: {} utils-merge@1.0.1: {} @@ -2581,7 +4989,62 @@ snapshots: tr46: 5.1.1 webidl-conversions: 7.0.0 + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.19 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + yallist@3.1.1: {} + yaml@2.8.2: optional: true + yocto-queue@0.1.0: {} + + zod-validation-error@4.0.2(zod@3.25.76): + dependencies: + zod: 3.25.76 + zod@3.25.76: {} diff --git a/turbo.json b/turbo.json index f32abf1..024ec2e 100644 --- a/turbo.json +++ b/turbo.json @@ -12,6 +12,10 @@ "lint": { "dependsOn": ["^lint"] }, + "lint:fix": { + "dependsOn": ["^lint:fix"], + "cache": false + }, "typecheck": { "dependsOn": ["^typecheck"] }, From 3f302bf419dd6cd972eef549145b978086fc536b Mon Sep 17 00:00:00 2001 From: zen0 Date: Thu, 25 Dec 2025 23:39:08 +0800 Subject: [PATCH 2/3] chore: add rate limit - add dashboard - add create api key --- README.md | 10 +- docs/linting.md | 81 +++ packages/kal-backend/src/index.ts | 14 +- packages/kal-backend/src/lib/context.ts | 43 +- .../src/middleware/api-key-middleware.ts | 146 +++++ packages/kal-backend/src/routers/api-keys.ts | 179 ++++++ packages/kal-backend/src/routers/api.ts | 6 +- packages/kal-backend/src/routers/index.ts | 2 + packages/kal-backend/tsconfig.json | 1 + .../20241225000003_create_api_keys.js | 49 ++ .../kal-frontend/src/app/callback/route.ts | 12 +- .../kal-frontend/src/app/dashboard/client.tsx | 325 ++++++++++ .../kal-frontend/src/app/dashboard/page.tsx | 45 ++ packages/kal-frontend/src/app/globals.css | 556 ++++++++++++++++++ packages/kal-frontend/src/app/layout.tsx | 5 +- .../src/app/login-success/client.tsx | 48 ++ .../src/app/login-success/page.tsx | 26 + .../kal-frontend/src/components/navbar.tsx | 1 + .../kal-frontend/src/lib/auth-context.tsx | 68 +++ .../kal-frontend/src/lib/trpc-provider.tsx | 27 +- packages/kal-shared/src/types/index.ts | 31 + 21 files changed, 1653 insertions(+), 22 deletions(-) create mode 100644 docs/linting.md create mode 100644 packages/kal-backend/src/middleware/api-key-middleware.ts create mode 100644 packages/kal-backend/src/routers/api-keys.ts create mode 100644 packages/kal-db/migrations/20241225000003_create_api_keys.js create mode 100644 packages/kal-frontend/src/app/dashboard/client.tsx create mode 100644 packages/kal-frontend/src/app/dashboard/page.tsx create mode 100644 packages/kal-frontend/src/app/login-success/client.tsx create mode 100644 packages/kal-frontend/src/app/login-success/page.tsx create mode 100644 packages/kal-frontend/src/lib/auth-context.tsx diff --git a/README.md b/README.md index 443cd68..2700a09 100644 --- a/README.md +++ b/README.md @@ -212,9 +212,13 @@ The app is deployed using [Coolify](https://coolify.io/) to a self-hosted server 1. Fork the repository 2. Create branch from `dev` (not `main`) 3. Make your changes -4. Run `pnpm typecheck` to verify -5. Submit PR to `dev` branch -6. Wait for review +4. **Run `pnpm lint:fix` to auto-fix issues** +5. Run `pnpm typecheck` to verify types +6. Submit PR to `dev` branch +7. Wait for review + +> [!TIP] +> See [docs/linting.md](docs/linting.md) for full linting guide. ### Commit Convention diff --git a/docs/linting.md b/docs/linting.md new file mode 100644 index 0000000..363a98d --- /dev/null +++ b/docs/linting.md @@ -0,0 +1,81 @@ +# Linting & Code Formatting + +This monorepo uses **ESLint** for linting and **Prettier** for code formatting. + +## Quick Commands + +```bash +# Check for lint issues +pnpm lint + +# Auto-fix lint issues +pnpm lint:fix + +# Format all files with Prettier +pnpm format + +# Check formatting without fixing +pnpm format:check +``` + +## Before Pushing Code + +> [!IMPORTANT] +> **Always run `pnpm lint:fix` before pushing your changes!** + +```bash +# 1. Auto-fix lint issues +pnpm lint:fix + +# 2. Format code +pnpm format + +# 3. Verify build +pnpm build +``` + +## What Gets Auto-Fixed + +| Issue | Auto-fixable | +| ----------------------- | ------------- | +| Import ordering | ✅ Yes | +| Duplicate imports | ✅ Yes | +| Consistent type imports | ✅ Yes | +| Code formatting | ✅ Yes | +| Unused variables | ❌ Manual fix | +| Type errors | ❌ Manual fix | + +## Configuration Files + +| File | Purpose | +| ------------------- | --------------------------- | +| `eslint.config.mjs` | ESLint rules (flat config) | +| `.prettierrc` | Prettier formatting options | +| `.prettierignore` | Files to skip formatting | + +## VS Code Integration + +For auto-fix on save, add to `.vscode/settings.json`: + +```json +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + } +} +``` + +Recommended extensions: + +- **ESLint** (`dbaeumer.vscode-eslint`) +- **Prettier** (`esbenp.prettier-vscode`) + +## Package-Specific Linting + +```bash +# Lint a specific package +pnpm --filter kal-frontend lint +pnpm --filter kal-backend lint:fix +``` diff --git a/packages/kal-backend/src/index.ts b/packages/kal-backend/src/index.ts index a452580..0869b45 100644 --- a/packages/kal-backend/src/index.ts +++ b/packages/kal-backend/src/index.ts @@ -7,9 +7,8 @@ import express from "express"; import session from "express-session"; import { createContext } from "./lib/context.js"; -import { connectDB, getDB } from "./lib/db.js"; +import { connectDB } from "./lib/db.js"; import { logtoConfig, validateLogtoConfig } from "./lib/logto.js"; -import { checkRateLimit, getRateLimitHeaders } from "./middleware/rate-limit.js"; import { apiRouter } from "./routers/api.js"; import { appRouter } from "./routers/index.js"; @@ -94,7 +93,16 @@ async function main() { const app = express(); // Core middleware - app.use(cors()); + app.use( + cors({ + origin: [ + "http://localhost:3000", + process.env.NEXT_PUBLIC_APP_URL, + process.env.FRONTEND_URL, + ].filter((url): url is string => !!url), + credentials: true, + }) + ); app.use(express.json()); app.use(cookieParser()); diff --git a/packages/kal-backend/src/lib/context.ts b/packages/kal-backend/src/lib/context.ts index a4b16c7..32ad951 100644 --- a/packages/kal-backend/src/lib/context.ts +++ b/packages/kal-backend/src/lib/context.ts @@ -16,12 +16,14 @@ type LogtoRequestUser = { */ async function syncUserFromLogto( db: Db, - claims: LogtoUserInfo + claims: LogtoUserInfo & { username?: string } ): Promise { if (!claims.sub) return null; const now = new Date(); const usersCollection = db.collection("users"); + + const displayName = claims.name || claims.username; // Upsert user - create if not exists, update if exists const result = await usersCollection.findOneAndUpdate( @@ -29,7 +31,7 @@ async function syncUserFromLogto( { $set: { email: claims.email || "", - name: claims.name, + name: displayName, updatedAt: now, }, $setOnInsert: { @@ -71,13 +73,46 @@ export async function createContext({ // Try Logto session first (user is added by @logto/express middleware) const logtoUser = (req as Request & { user?: LogtoRequestUser }).user; if (logtoUser?.claims?.sub) { - user = await syncUserFromLogto(db, logtoUser.claims); + user = await syncUserFromLogto(db, { + ...logtoUser.claims, + username: logtoUser.claims.username || undefined, + }); } // Fallback to header-based auth (for API keys or development) if (!user) { + // Try Logto ID from frontend header (when using Next.js proxy/tRPC) + const headerLogtoId = req.headers["x-logto-id"] as string | undefined; + if (headerLogtoId) { + user = await db.collection("users").findOne({ logtoId: headerLogtoId }); + + const headerEmail = req.headers["x-logto-email"] as string | undefined; + const headerName = req.headers["x-logto-name"] as string | undefined; + + // If user not found but we have claims in headers (trusted from frontend), create/sync them + if (!user) { + // Proceed if we at least have an email or if the ID is sufficient for a basic record + if (headerLogtoId) { + user = await syncUserFromLogto(db, { + sub: headerLogtoId, + email: headerEmail, + name: headerName, + }); + } + } else if (headerName && !user.name) { + // Update if local name is missing but header has one + await db.collection("users").updateOne( + { _id: user._id }, + { $set: { name: headerName, email: headerEmail || user.email } } + ); + user.name = headerName; + if (headerEmail) user.email = headerEmail; + } + } + + // Try explicit user ID (development) const headerUserId = req.headers["x-user-id"] as string | undefined; - if (headerUserId) { + if (!user && headerUserId) { user = await getUserById(db, headerUserId); } } diff --git a/packages/kal-backend/src/middleware/api-key-middleware.ts b/packages/kal-backend/src/middleware/api-key-middleware.ts new file mode 100644 index 0000000..3278536 --- /dev/null +++ b/packages/kal-backend/src/middleware/api-key-middleware.ts @@ -0,0 +1,146 @@ +import { createHash } from "crypto"; + +import type { NextFunction, Request, Response } from "express"; +import type { ApiKey, User } from "kal-shared"; +import type { Db } from "mongodb"; + +import { getDB } from "../lib/db.js"; + +import { checkRateLimit, getRateLimitHeaders } from "./rate-limit.js"; + +/** + * Hash an API key using SHA-256 for secure storage/lookup + */ +export function hashApiKey(key: string): string { + return createHash("sha256").update(key).digest("hex"); +} + +/** + * Generate a new API key with the kal_ prefix + */ +export function generateApiKey(): string { + const randomBytes = new Uint8Array(32); + crypto.getRandomValues(randomBytes); + const hex = Array.from(randomBytes) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); + return `kal_${hex}`; +} + +/** + * Validate an API key and return the associated user + */ +export async function validateApiKeyAndGetUser( + db: Db, + apiKey: string +): Promise<{ valid: boolean; user?: User; error?: string; statusCode?: number }> { + const hashedKey = hashApiKey(apiKey); + + // Find the API key + const keyDoc = await db.collection("api_keys").findOne({ + key: hashedKey, + isRevoked: false, + }); + + if (!keyDoc) { + return { valid: false, error: "Invalid API key", statusCode: 401 }; + } + + // Check expiration + if (keyDoc.expiresAt && new Date(keyDoc.expiresAt) < new Date()) { + return { valid: false, error: "API key has expired", statusCode: 401 }; + } + + // Get the user + const { ObjectId } = await import("mongodb"); + const user = await db.collection("users").findOne({ + _id: new ObjectId(keyDoc.userId) as unknown as string, + }); + + if (!user) { + return { valid: false, error: "User not found", statusCode: 401 }; + } + + // Update last used timestamp (fire and forget) + db.collection("api_keys").updateOne( + { _id: keyDoc._id } as any, // eslint-disable-line @typescript-eslint/no-explicit-any + { $set: { lastUsedAt: new Date() } } + ).catch(console.error); + + return { valid: true, user }; +} + +/** + * Express middleware to validate API key for REST endpoints + */ +export function validateApiKeyMiddleware( + req: Request, + res: Response, + next: NextFunction +): void { + const apiKey = req.headers["x-api-key"] as string | undefined; + + if (!apiKey) { + res.status(401).json({ + success: false, + error: "API key required. Provide X-API-Key header.", + docs: "/api-docs", + }); + return; + } + + if (!apiKey.startsWith("kal_")) { + res.status(401).json({ + success: false, + error: "Invalid API key format", + }); + return; + } + + const db = getDB(); + + validateApiKeyAndGetUser(db, apiKey) + .then(async (result) => { + if (!result.valid || !result.user) { + res.status(result.statusCode || 401).json({ + success: false, + error: result.error, + }); + return; + } + + // Check rate limits + const rateResult = await checkRateLimit( + db, + result.user._id.toString(), + result.user.tier + ); + + // Add rate limit headers + const headers = getRateLimitHeaders(rateResult, result.user.tier); + Object.entries(headers).forEach(([key, value]) => { + res.setHeader(key, value); + }); + + if (rateResult.limited) { + res.status(429).json({ + success: false, + error: "Rate limit exceeded", + retryAfter: rateResult.retryAfter, + }); + return; + } + + // Attach user to request for downstream use + (req as Request & { apiUser?: User }).apiUser = result.user; + + next(); + }) + .catch((error) => { + console.error("API key validation error:", error); + res.status(500).json({ + success: false, + error: "Internal server error", + }); + }); +} diff --git a/packages/kal-backend/src/routers/api-keys.ts b/packages/kal-backend/src/routers/api-keys.ts new file mode 100644 index 0000000..b5968b6 --- /dev/null +++ b/packages/kal-backend/src/routers/api-keys.ts @@ -0,0 +1,179 @@ +import type { ApiKey, ApiKeyExpiration, ApiKeyPublic, RateLimitUsage } from "kal-shared"; +import { ObjectId } from "mongodb"; +import { z } from "zod"; + +import { protectedProcedure, router } from "../lib/trpc.js"; +import { + generateApiKey, + hashApiKey, +} from "../middleware/api-key-middleware.js"; + +/** + * Calculate expiration date based on expiration type + */ +function calculateExpiresAt(expiration: ApiKeyExpiration): Date | null { + const now = new Date(); + switch (expiration) { + case "1_week": + return new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); + case "1_month": + return new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000); + case "never": + return null; + } +} + +/** + * Convert API key document to public format (hides the actual key) + */ +function toPublic(doc: ApiKey): ApiKeyPublic { + return { + _id: doc._id.toString(), + keyPrefix: doc.keyPrefix, + name: doc.name, + expiration: doc.expiration, + expiresAt: doc.expiresAt, + createdAt: doc.createdAt, + lastUsedAt: doc.lastUsedAt, + isRevoked: doc.isRevoked, + }; +} + +export const apiKeysRouter = router({ + /** + * Get current user info (triggers sync from Logto) + */ + getMe: protectedProcedure.query(async ({ ctx }) => { + return { + _id: ctx.user?._id?.toString() || "", + name: ctx.user?.name || "", + email: ctx.user?.email || "", + tier: ctx.user?.tier || "free", + }; + }), + + /** + * List user's API keys (public info only, never the actual key) + */ + list: protectedProcedure.query(async ({ ctx }) => { + const keys = await ctx.db + .collection("api_keys") + .find({ + userId: ctx.userId, + isRevoked: false, + }) + .sort({ createdAt: -1 }) + .toArray(); + + return keys.map(toPublic); + }), + + /** + * Generate a new API key + * Returns the plain key ONCE - user must save it + */ + generate: protectedProcedure + .input( + z.object({ + name: z.string().min(1).max(50), + expiration: z.enum(["1_week", "1_month", "never"]), + }) + ) + .mutation(async ({ ctx, input }) => { + const plainKey = generateApiKey(); + const hashedKey = hashApiKey(plainKey); + const keyPrefix = plainKey.substring(0, 12) + "..."; + + const now = new Date(); + const expiresAt = calculateExpiresAt(input.expiration); + + const doc: Omit = { + userId: ctx.userId!, + key: hashedKey, + keyPrefix, + name: input.name, + expiration: input.expiration, + expiresAt, + createdAt: now, + lastUsedAt: null, + isRevoked: false, + revokedAt: null, + }; + + const result = await ctx.db.collection("api_keys").insertOne(doc); + + return { + // Return the plain key ONLY on creation + key: plainKey, + keyPublic: { + _id: result.insertedId.toString(), + keyPrefix, + name: input.name, + expiration: input.expiration, + expiresAt, + createdAt: now, + lastUsedAt: null, + isRevoked: false, + } as ApiKeyPublic, + }; + }), + + /** + * Revoke an API key + */ + revoke: protectedProcedure + .input(z.object({ keyId: z.string() })) + .mutation(async ({ ctx, input }) => { + const result = await ctx.db.collection("api_keys").updateOne( + { + _id: new ObjectId(input.keyId), + userId: ctx.userId, + isRevoked: false, + }, + { + $set: { + isRevoked: true, + revokedAt: new Date(), + }, + } + ); + + if (result.matchedCount === 0) { + throw new Error("API key not found or already revoked"); + } + + return { success: true }; + }), + + /** + * Get usage statistics for the dashboard + */ + getUsageStats: protectedProcedure.query(async ({ ctx }) => { + const today = new Date().toISOString().split("T")[0]; + + // Get today's usage + const usage = await ctx.db + .collection("rate_limit_usage") + .findOne({ + userId: ctx.userId, + date: today, + }); + + // Get user's tier info + const user = ctx.user; + + // Count active keys + const activeKeyCount = await ctx.db + .collection("api_keys") + .countDocuments({ + userId: ctx.userId, + isRevoked: false, + }); + + return { + tier: user?.tier || "free", + dailyUsed: usage?.dailyCount || 0, + activeKeyCount, + }; + }), +}); diff --git a/packages/kal-backend/src/routers/api.ts b/packages/kal-backend/src/routers/api.ts index 73a69f6..3611378 100644 --- a/packages/kal-backend/src/routers/api.ts +++ b/packages/kal-backend/src/routers/api.ts @@ -1,11 +1,15 @@ import { Router, type Router as RouterType } from "express"; import { getDB } from "../lib/db.js"; +import { validateApiKeyMiddleware } from "../middleware/api-key-middleware.js"; const router: RouterType = Router(); +// Apply API key validation to all routes +router.use(validateApiKeyMiddleware); + // ============================================ -// Natural Foods - Public REST API +// Natural Foods - Public REST API (requires API key) // ============================================ /** diff --git a/packages/kal-backend/src/routers/index.ts b/packages/kal-backend/src/routers/index.ts index e1a65e1..49d8d1f 100644 --- a/packages/kal-backend/src/routers/index.ts +++ b/packages/kal-backend/src/routers/index.ts @@ -1,11 +1,13 @@ import { router } from "../lib/trpc.js"; +import { apiKeysRouter } from "./api-keys.js"; import { foodRouter } from "./food.js"; import { halalRouter } from "./halal.js"; export const appRouter = router({ food: foodRouter, halal: halalRouter, + apiKeys: apiKeysRouter, }); // Export type for client diff --git a/packages/kal-backend/tsconfig.json b/packages/kal-backend/tsconfig.json index 01fe3a3..5f95f39 100644 --- a/packages/kal-backend/tsconfig.json +++ b/packages/kal-backend/tsconfig.json @@ -6,6 +6,7 @@ "outDir": "./dist", "rootDir": "./src", "declaration": true, + "declarationMap": true, "noEmit": false }, "include": ["src/**/*"], diff --git a/packages/kal-db/migrations/20241225000003_create_api_keys.js b/packages/kal-db/migrations/20241225000003_create_api_keys.js new file mode 100644 index 0000000..625de0a --- /dev/null +++ b/packages/kal-db/migrations/20241225000003_create_api_keys.js @@ -0,0 +1,49 @@ +/** + * Migration: Create api_keys collection + * + * Creates the api_keys collection with indexes for: + * - Fast key lookup (authentication) + * - User key listing + * - Expiration queries + */ + +export const up = async (db, _client) => { + // Create api_keys collection + const collections = await db.listCollections({ name: "api_keys" }).toArray(); + if (collections.length === 0) { + await db.createCollection("api_keys"); + console.log("✅ Created api_keys collection"); + } else { + console.log("ℹ️ api_keys collection already exists"); + } + + // Index for fast key lookup (authentication) - key is hashed + await db.collection("api_keys").createIndex( + { key: 1 }, + { unique: true, name: "key_unique" } + ); + console.log("✅ Created key_unique index"); + + // Index for listing user's keys + await db.collection("api_keys").createIndex( + { userId: 1, isRevoked: 1, createdAt: -1 }, + { name: "user_keys" } + ); + console.log("✅ Created user_keys index"); + + // Index for expiration cleanup queries + await db.collection("api_keys").createIndex( + { expiresAt: 1, isRevoked: 1 }, + { name: "expiration" } + ); + console.log("✅ Created expiration index"); +}; + +export const down = async (db, _client) => { + try { + await db.collection("api_keys").drop(); + console.log("✅ Dropped api_keys collection"); + } catch (e) { + console.log("ℹ️ api_keys collection may not exist, skipping drop"); + } +}; diff --git a/packages/kal-frontend/src/app/callback/route.ts b/packages/kal-frontend/src/app/callback/route.ts index a0269f5..4fbdac1 100644 --- a/packages/kal-frontend/src/app/callback/route.ts +++ b/packages/kal-frontend/src/app/callback/route.ts @@ -1,11 +1,13 @@ -import { handleSignIn } from '@logto/next/server-actions'; -import { redirect } from 'next/navigation'; -import type { NextRequest } from 'next/server'; +import { handleSignIn } from "@logto/next/server-actions"; +import { redirect } from "next/navigation"; +import type { NextRequest } from "next/server"; -import { logtoConfig } from '@/lib/logto'; +import { logtoConfig } from "@/lib/logto"; export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams; await handleSignIn(logtoConfig, searchParams); - redirect('/'); + + // Redirect to login success page, which will then redirect to dashboard + redirect("/login-success"); } diff --git a/packages/kal-frontend/src/app/dashboard/client.tsx b/packages/kal-frontend/src/app/dashboard/client.tsx new file mode 100644 index 0000000..a98d500 --- /dev/null +++ b/packages/kal-frontend/src/app/dashboard/client.tsx @@ -0,0 +1,325 @@ +"use client"; + +import type { ApiKeyExpiration } from "kal-shared"; +import { RATE_LIMITS } from "kal-shared"; +import { useState } from "react"; + +import { AuthUpdater, useAuth } from "@/lib/auth-context"; +import { trpc } from "@/lib/trpc"; + +/** API key as returned from tRPC (dates are serialized as strings) */ +interface SerializedApiKey { + _id: string; + keyPrefix: string; + name: string; + expiration: ApiKeyExpiration; + expiresAt: string | null; + createdAt: string; + lastUsedAt: string | null; + isRevoked: boolean; +} + +interface DashboardClientProps { + logtoId?: string; + email?: string | null; + name?: string | null; +} + +export default function DashboardClient({ logtoId, email, name }: DashboardClientProps) { + return ( + <> + + + + ); +} + +function DashboardContentWrapper({ expectedLogtoId }: { expectedLogtoId?: string }) { + const { logtoId } = useAuth(); + + // Wait until context is updated with the Logto ID + if (expectedLogtoId && logtoId !== expectedLogtoId) { + return ( +
+

Loading session...

+
+ ); + } + + return ; +} + +function DashboardContent() { + const [showGenerateModal, setShowGenerateModal] = useState(false); + const [newKeyName, setNewKeyName] = useState(""); + const [newKeyExpiration, setNewKeyExpiration] = useState<"1_week" | "1_month" | "never">("1_month"); + const [generatedKey, setGeneratedKey] = useState(null); + const [copied, setCopied] = useState(false); + + // Fetch current user info (triggers sync from Logto to MongoDB) + const { data: userInfo, isLoading: isLoadingUser } = trpc.apiKeys.getMe.useQuery(); + + // Fetch API keys + const { data: apiKeys, refetch: refetchKeys } = trpc.apiKeys.list.useQuery(); + + // Fetch usage stats + const { data: stats } = trpc.apiKeys.getUsageStats.useQuery(); + + // Generate API key mutation + const generateMutation = trpc.apiKeys.generate.useMutation({ + onSuccess: (data) => { + setGeneratedKey(data.key); + refetchKeys(); + }, + }); + + // Revoke API key mutation + const revokeMutation = trpc.apiKeys.revoke.useMutation({ + onSuccess: () => { + refetchKeys(); + }, + }); + + const handleGenerate = () => { + if (!newKeyName.trim()) return; + generateMutation.mutate({ + name: newKeyName, + expiration: newKeyExpiration, + }); + }; + + const handleCopyKey = () => { + if (generatedKey) { + navigator.clipboard.writeText(generatedKey); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + }; + + const handleCloseModal = () => { + setShowGenerateModal(false); + setNewKeyName(""); + setNewKeyExpiration("1_month"); + setGeneratedKey(null); + setCopied(false); + }; + + const handleRevoke = (keyId: string, keyName: string) => { + if (confirm(`Are you sure you want to revoke "${keyName}"? This action cannot be undone.`)) { + revokeMutation.mutate({ keyId }); + } + }; + + const tier = stats?.tier || "free"; + const limits = RATE_LIMITS[tier]; + const dailyUsed = stats?.dailyUsed || 0; + const dailyRemaining = Math.max(0, limits.dailyLimit - dailyUsed); + const dailyPercentage = Math.min(100, (dailyUsed / limits.dailyLimit) * 100); + + return ( +
+ {/* Usage Stats Section */} +
+

Your Account

+
+
+
+ Profile +
+
+

+ {isLoadingUser ? "Loading..." : userInfo?.name || "Developer"} +

+

{userInfo?.email}

+
+
+ +
+
+ Current Tier + + {tier === "free" ? "Free" : tier === "tier_1" ? "Tier 1" : "Tier 2"} + +
+
+

{limits.dailyLimit} requests/day

+

{limits.burstLimit} requests/minute

+
+
+ +
+
+ Today's Usage + {dailyUsed} / {limits.dailyLimit} +
+
+
+
+

{dailyRemaining} requests remaining

+
+ +
+
+ Active API Keys + {stats?.activeKeyCount || 0} +
+
+
+
+ + {/* API Keys Section */} +
+
+

API Keys

+ +
+ + {apiKeys && apiKeys.length > 0 ? ( +
+
+ Name + Key + Expires + Last Used + Actions +
+ {apiKeys.map((key: SerializedApiKey) => ( +
+ {key.name} + + {key.keyPrefix} + + + {key.expiresAt + ? new Date(key.expiresAt).toLocaleDateString() + : "Never" + } + + + {key.lastUsedAt + ? new Date(key.lastUsedAt).toLocaleDateString() + : "Never" + } + + + + +
+ ))} +
+ ) : ( +
+

No API keys yet. Generate one to get started!

+
+ )} +
+ + {/* Quick Links */} +
+

Quick Links

+ +
+ + {/* Generate Key Modal */} + {showGenerateModal && ( +
+
e.stopPropagation()}> + {!generatedKey ? ( + <> +

Generate New API Key

+
+ + setNewKeyName(e.target.value)} + maxLength={50} + /> +
+
+ +
+ + + +
+
+
+ + +
+ + ) : ( + <> +

🎉 API Key Generated!

+
+ ⚠️ Save this key now! +

This is the only time you'll see this key. Copy it and store it securely.

+
+
+ {generatedKey} + +
+
+ +
+ + )} +
+
+ )} +
+ ); +} diff --git a/packages/kal-frontend/src/app/dashboard/page.tsx b/packages/kal-frontend/src/app/dashboard/page.tsx new file mode 100644 index 0000000..3aa700e --- /dev/null +++ b/packages/kal-frontend/src/app/dashboard/page.tsx @@ -0,0 +1,45 @@ +import { getLogtoContext } from "@logto/next/server-actions"; +import { redirect } from "next/navigation"; + +import DashboardClient from "./client"; + +import { logtoConfig } from "@/lib/logto"; + +export const metadata = { + title: "Dashboard - Kal API", + description: "Manage your API keys and view usage statistics", +}; + +export default async function DashboardPage() { + const { isAuthenticated, claims } = await getLogtoContext(logtoConfig); + + // Redirect to home if not authenticated + if (!isAuthenticated) { + redirect("/"); + } + + return ( +
+
+
+
+

Dashboard

+

+ Welcome back, {claims?.name || claims?.username || claims?.email || "Developer"} +

+
+ + ← Back to Home + +
+ + +
+
+ ); +} + diff --git a/packages/kal-frontend/src/app/globals.css b/packages/kal-frontend/src/app/globals.css index 0b81de1..3b9e1b6 100644 --- a/packages/kal-frontend/src/app/globals.css +++ b/packages/kal-frontend/src/app/globals.css @@ -77,6 +77,17 @@ html { font-size: 0.875rem; } +.dashboard-link { + color: var(--accent-green); + text-decoration: none; + font-weight: 500; + transition: color 0.2s; +} + +.dashboard-link:hover { + color: var(--accent-green-hover); +} + /* Auth Buttons */ .sign-in-btn, .sign-out-btn { @@ -107,3 +118,548 @@ html { background-color: var(--bg-elevated); color: var(--text-primary); } + +/* Login Success Page */ +.login-success-page { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; +} + +.login-success-card { + background: var(--bg-surface); + border: 1px solid var(--border-color); + border-radius: 1rem; + padding: 3rem; + text-align: center; + max-width: 400px; + width: 100%; +} + +.success-icon { + width: 80px; + height: 80px; + border-radius: 50%; + background: var(--accent-green); + color: white; + font-size: 2.5rem; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 1.5rem; + animation: scaleIn 0.3s ease-out; +} + +@keyframes scaleIn { + from { + transform: scale(0); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } +} + +.login-success-card h1 { + font-size: 1.75rem; + font-weight: 700; + margin-bottom: 0.5rem; + color: var(--accent-green); +} + +.welcome-text { + font-size: 1.125rem; + color: var(--text-secondary); + margin-bottom: 1rem; +} + +.welcome-text strong { + color: var(--text-primary); +} + +.redirect-text { + font-size: 0.875rem; + color: var(--text-muted); + margin-bottom: 1.5rem; +} + +/* Dashboard Styles */ +.dashboard-page { + min-height: 100vh; + padding: 2rem; +} + +.dashboard-container { + max-width: 1200px; + margin: 0 auto; +} + +.dashboard-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 2rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid var(--border-color); +} + +.dashboard-header h1 { + font-size: 2rem; + font-weight: 700; + margin-bottom: 0.5rem; +} + +.user-greeting { + color: var(--text-secondary); +} + +.back-link { + color: var(--text-secondary); + text-decoration: none; + transition: color 0.2s; +} + +.back-link:hover { + color: var(--accent-green); +} + +.dashboard-content { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.dashboard-section { + background: var(--bg-surface); + border: 1px solid var(--border-color); + border-radius: 0.75rem; + padding: 1.5rem; +} + +.dashboard-section h2 { + font-size: 1.25rem; + font-weight: 600; + margin-bottom: 1rem; +} + +.profile-info { + margin-top: 0.5rem; +} + +.profile-name { + font-size: 1.25rem; + font-weight: 600; + color: var(--text-primary); + margin: 0; +} + +.profile-email { + font-size: 0.875rem; + color: var(--text-secondary); + margin: 0.25rem 0 0 0; +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.section-header h2 { + margin-bottom: 0; +} + +/* Stats Grid */ +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1rem; +} + +.stat-card { + background: var(--bg-elevated); + border: 1px solid var(--border-color); + border-radius: 0.5rem; + padding: 1rem; +} + +.stat-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; +} + +.stat-label { + color: var(--text-secondary); + font-size: 0.875rem; +} + +.stat-value { + font-size: 1.25rem; + font-weight: 600; + color: var(--accent-green); +} + +.stat-details { + font-size: 0.875rem; + color: var(--text-secondary); +} + +.stat-details p { + margin: 0.25rem 0; +} + +.stat-remaining { + font-size: 0.75rem; + color: var(--text-muted); + margin-top: 0.5rem; +} + +/* Tier Badges */ +.tier-badge { + padding: 0.25rem 0.75rem; + border-radius: 1rem; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; +} + +.tier-free { + background: rgba(156, 163, 175, 0.2); + color: #9ca3af; +} + +.tier-tier_1 { + background: rgba(59, 130, 246, 0.2); + color: #3b82f6; +} + +.tier-tier_2 { + background: rgba(168, 85, 247, 0.2); + color: #a855f7; +} + +/* Progress Bar */ +.progress-bar { + height: 8px; + background: var(--bg-primary); + border-radius: 4px; + overflow: hidden; +} + +.progress-fill { + height: 100%; + background: var(--accent-green); + border-radius: 4px; + transition: width 0.3s ease; +} + +/* API Keys Table */ +.api-keys-table { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.table-header, +.table-row { + display: grid; + grid-template-columns: 2fr 2fr 1fr 1fr 1fr; + gap: 1rem; + padding: 0.75rem 1rem; + align-items: center; +} + +.table-header { + background: var(--bg-elevated); + border-radius: 0.5rem; + font-size: 0.75rem; + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.table-row { + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 0.5rem; +} + +.key-name { + font-weight: 500; +} + +.key-prefix code { + background: var(--bg-elevated); + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + font-size: 0.8rem; +} + +.key-expiry, +.key-last-used { + font-size: 0.875rem; + color: var(--text-secondary); +} + +/* Buttons */ +.btn-primary { + background: var(--accent-green); + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 0.375rem; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s; +} + +.btn-primary:hover:not(:disabled) { + background: var(--accent-green-hover); +} + +.btn-primary:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-secondary { + background: transparent; + color: var(--text-secondary); + border: 1px solid var(--border-color); + padding: 0.5rem 1rem; + border-radius: 0.375rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} + +.btn-secondary:hover { + background: var(--bg-elevated); + color: var(--text-primary); +} + +.btn-danger { + background: transparent; + color: #ef4444; + border: 1px solid #ef4444; + padding: 0.25rem 0.75rem; + border-radius: 0.25rem; + font-size: 0.75rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} + +.btn-danger:hover:not(:disabled) { + background: #ef4444; + color: white; +} + +.btn-danger:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Empty State */ +.empty-state { + text-align: center; + padding: 2rem; + color: var(--text-secondary); +} + +/* Quick Links */ +.quick-links { + display: flex; + gap: 1rem; + flex-wrap: wrap; +} + +.quick-link { + display: flex; + align-items: center; + gap: 0.5rem; + background: var(--bg-elevated); + border: 1px solid var(--border-color); + padding: 0.75rem 1.25rem; + border-radius: 0.5rem; + color: var(--text-primary); + text-decoration: none; + transition: all 0.2s; +} + +.quick-link:hover { + border-color: var(--accent-green); + color: var(--accent-green); +} + +.link-icon { + font-size: 1.25rem; +} + +/* Modal */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.modal { + background: var(--bg-surface); + border: 1px solid var(--border-color); + border-radius: 0.75rem; + padding: 1.5rem; + max-width: 480px; + width: 90%; +} + +.modal h3 { + font-size: 1.25rem; + font-weight: 600; + margin-bottom: 1rem; +} + +.form-group { + margin-bottom: 1rem; +} + +.form-group label { + display: block; + font-size: 0.875rem; + color: var(--text-secondary); + margin-bottom: 0.5rem; +} + +.form-group input { + width: 100%; + padding: 0.75rem; + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 0.375rem; + color: var(--text-primary); + font-size: 1rem; +} + +.form-group input:focus { + outline: none; + border-color: var(--accent-green); +} + +.expiration-options { + display: flex; + gap: 0.5rem; +} + +.exp-option { + flex: 1; + padding: 0.5rem; + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 0.375rem; + color: var(--text-secondary); + cursor: pointer; + transition: all 0.2s; +} + +.exp-option:hover { + border-color: var(--accent-green); +} + +.exp-option.active { + background: var(--accent-green); + border-color: var(--accent-green); + color: white; +} + +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 0.75rem; + margin-top: 1.5rem; +} + +.warning-box { + background: rgba(234, 179, 8, 0.1); + border: 1px solid rgba(234, 179, 8, 0.3); + border-radius: 0.5rem; + padding: 1rem; + margin-bottom: 1rem; +} + +.warning-box strong { + color: #eab308; +} + +.warning-box p { + margin-top: 0.5rem; + font-size: 0.875rem; + color: var(--text-secondary); +} + +.key-display { + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 0.5rem; + padding: 1rem; + display: flex; + align-items: center; + gap: 1rem; +} + +.key-display code { + flex: 1; + word-break: break-all; + font-size: 0.8rem; + color: var(--accent-green); +} + +.copy-btn { + background: var(--accent-green); + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 0.375rem; + font-weight: 500; + cursor: pointer; + white-space: nowrap; +} + +.copy-btn:hover { + background: var(--accent-green-hover); +} + +/* Responsive */ +@media (max-width: 768px) { + .table-header, + .table-row { + grid-template-columns: 1fr 1fr; + gap: 0.5rem; + } + + .key-expiry, + .key-last-used { + display: none; + } + + .stats-grid { + grid-template-columns: 1fr; + } + + .dashboard-header { + flex-direction: column; + gap: 1rem; + } +} diff --git a/packages/kal-frontend/src/app/layout.tsx b/packages/kal-frontend/src/app/layout.tsx index 7652df1..05bbd1d 100644 --- a/packages/kal-frontend/src/app/layout.tsx +++ b/packages/kal-frontend/src/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; +import { AuthProvider } from "@/lib/auth-context"; import { TRPCProvider } from "@/lib/trpc-provider"; const inter = Inter({ subsets: ["latin"] }); @@ -19,7 +20,9 @@ export default function RootLayout({ return ( - {children} + + {children} + ); diff --git a/packages/kal-frontend/src/app/login-success/client.tsx b/packages/kal-frontend/src/app/login-success/client.tsx new file mode 100644 index 0000000..58e32f9 --- /dev/null +++ b/packages/kal-frontend/src/app/login-success/client.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; + +interface LoginSuccessClientProps { + userName: string; +} + +export default function LoginSuccessClient({ userName }: LoginSuccessClientProps) { + const router = useRouter(); + const [countdown, setCountdown] = useState(3); + + useEffect(() => { + if (countdown === 0) { + router.push("/dashboard"); + } + }, [countdown, router]); + + useEffect(() => { + const timer = setInterval(() => { + setCountdown((prev) => (prev > 0 ? prev - 1 : 0)); + }, 1000); + + return () => clearInterval(timer); + }, []); + + return ( +
+
+
+

Login Successful!

+

+ Welcome back, {userName} +

+

+ Redirecting to dashboard in {countdown}... +

+ +
+
+ ); +} diff --git a/packages/kal-frontend/src/app/login-success/page.tsx b/packages/kal-frontend/src/app/login-success/page.tsx new file mode 100644 index 0000000..601997a --- /dev/null +++ b/packages/kal-frontend/src/app/login-success/page.tsx @@ -0,0 +1,26 @@ +import { getLogtoContext } from "@logto/next/server-actions"; +import { redirect } from "next/navigation"; + +import LoginSuccessClient from "./client"; + +import { logtoConfig } from "@/lib/logto"; + +export const metadata = { + title: "Login Successful - Kal", + description: "You have successfully logged in", +}; + +export default async function LoginSuccessPage() { + const { isAuthenticated, claims } = await getLogtoContext(logtoConfig); + + // If not authenticated, redirect to home + if (!isAuthenticated) { + redirect("/"); + } + + return ( + + ); +} diff --git a/packages/kal-frontend/src/components/navbar.tsx b/packages/kal-frontend/src/components/navbar.tsx index 512aaa9..b3bba83 100644 --- a/packages/kal-frontend/src/components/navbar.tsx +++ b/packages/kal-frontend/src/components/navbar.tsx @@ -15,6 +15,7 @@ export default async function Navbar() {
{isAuthenticated ? (
+ Dashboard {claims?.email || claims?.sub} { diff --git a/packages/kal-frontend/src/lib/auth-context.tsx b/packages/kal-frontend/src/lib/auth-context.tsx new file mode 100644 index 0000000..cb22251 --- /dev/null +++ b/packages/kal-frontend/src/lib/auth-context.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { createContext, useContext, type ReactNode, useState, useEffect, useCallback } from "react"; + +interface AuthContextType { + logtoId: string | null; + email: string | null; + name: string | null; + setAuth: (data: { logtoId: string | null; email?: string | null; name?: string | null }) => void; +} + +const AuthContext = createContext({ + logtoId: null, + email: null, + name: null, + setAuth: () => {}, +}); + +export function useAuth() { + return useContext(AuthContext); +} + +export function AuthProvider({ + children, + logtoId: initialLogtoId, +}: { + children: ReactNode; + logtoId: string | null; +}) { + const [auth, setInternalAuth] = useState({ + logtoId: initialLogtoId, + email: null as string | null, + name: null as string | null, + }); + + const setAuth = useCallback((data: { logtoId: string | null; email?: string | null; name?: string | null }) => { + setInternalAuth((prev) => ({ + ...prev, + ...data, + })); + }, []); + + return ( + + {children} + + ); +} + +export function AuthUpdater({ + logtoId, + email, + name +}: { + logtoId: string | undefined; + email?: string | null; + name?: string | null; +}) { + const { setAuth } = useAuth(); + + useEffect(() => { + if (logtoId) { + setAuth({ logtoId, email, name }); + } + }, [logtoId, email, name, setAuth]); + + return null; +} diff --git a/packages/kal-frontend/src/lib/trpc-provider.tsx b/packages/kal-frontend/src/lib/trpc-provider.tsx index a9352ba..6906890 100644 --- a/packages/kal-frontend/src/lib/trpc-provider.tsx +++ b/packages/kal-frontend/src/lib/trpc-provider.tsx @@ -2,8 +2,9 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { httpBatchLink } from "@trpc/client"; -import { useState } from "react"; +import { useRef, useState } from "react"; +import { useAuth } from "./auth-context"; import { trpc } from "./trpc"; function getBaseUrl() { @@ -14,17 +15,33 @@ function getBaseUrl() { } export function TRPCProvider({ children }: { children: React.ReactNode }) { + const { logtoId, email, name } = useAuth(); + + // Use a ref to access the current auth context in the headers callback + const authRef = useRef({ logtoId, email, name }); + authRef.current = { logtoId, email, name }; + const [queryClient] = useState(() => new QueryClient()); const [trpcClient] = useState(() => trpc.createClient({ links: [ httpBatchLink({ url: `${getBaseUrl()}/trpc`, + fetch(url, options) { + return fetch(url, { + ...options, + credentials: "include", + }); + }, headers() { - // TODO: Add Logto auth token - return { - "x-user-id": "dev-user-123", // Temporary for development - }; + const { logtoId, email, name } = authRef.current; + const headers: Record = {}; + + if (logtoId) headers["x-logto-id"] = logtoId; + if (email) headers["x-logto-email"] = email; + if (name) headers["x-logto-name"] = name; + + return headers; }, }), ], diff --git a/packages/kal-shared/src/types/index.ts b/packages/kal-shared/src/types/index.ts index 4447885..11c4c3b 100644 --- a/packages/kal-shared/src/types/index.ts +++ b/packages/kal-shared/src/types/index.ts @@ -47,6 +47,37 @@ export interface LogtoUserInfo { picture?: string; } +// =================== +// API Keys +// =================== +export type ApiKeyExpiration = '1_week' | '1_month' | 'never'; + +export interface ApiKey { + _id: string; + userId: string; // Reference to user + key: string; // The actual API key (hashed in DB) + keyPrefix: string; // First 8 chars for display (e.g., "kal_a1b2...") + name: string; // User-defined name for the key + expiration: ApiKeyExpiration; + expiresAt: Date | null; // null = never expires + createdAt: Date; + lastUsedAt: Date | null; + isRevoked: boolean; + revokedAt: Date | null; +} + +// For frontend display (never expose full key after creation) +export interface ApiKeyPublic { + _id: string; + keyPrefix: string; + name: string; + expiration: ApiKeyExpiration; + expiresAt: Date | null; + createdAt: Date; + lastUsedAt: Date | null; + isRevoked: boolean; +} + export interface FoodEntry { From c9fdc9e9d04489bf4adc7788c3a27788f6c65e39 Mon Sep 17 00:00:00 2001 From: zen0 Date: Fri, 26 Dec 2025 00:12:02 +0800 Subject: [PATCH 3/3] chore: clear the .env.example --- .env.example | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index b9e61a1..16a6778 100644 --- a/.env.example +++ b/.env.example @@ -14,8 +14,8 @@ LOGTO_ENDPOINT=http://localhost:3001 LOGTO_ADMIN_ENDPOINT=http://localhost:3002 # Logto App Credentials (from Admin Console) -LOGTO_APP_ID=8hr723agl0aw142cbag0g -LOGTO_APP_SECRET=0mMmpWv4g3HklArGNM1XuSREJyRMjFyf +LOGTO_APP_ID= +LOGTO_APP_SECRET= # Logto Database (Postgres - Local) LOGTO_DB_USER=logto @@ -32,7 +32,7 @@ FRONTEND_PORT=3000 FRONTEND_URL=http://localhost:3000 # Session Secret (for express-session) -SESSION_SECRET=JdD5Kts7wv3xco9dHKPmYjhHaQmYM3sQ +SESSION_SECRET= # ============================================ # PRODUCTION (VPS 72.62.74.47) - Copy to .env.production @@ -45,8 +45,8 @@ SESSION_SECRET=JdD5Kts7wv3xco9dHKPmYjhHaQmYM3sQ # MONGODB_DATABASE=kalori_db # Logto Production (logto-auth on Coolify) -# LOGTO_ENDPOINT=http://logto-dkgs8kco00440g4cg4goksoc.72.62.74.47.sslip.io -# LOGTO_ADMIN_ENDPOINT=http://logto-admin-dkgs8kco00440g4cg4goksoc.72.62.74.47.sslip.io +# LOGTO_ENDPOINT= +# LOGTO_ADMIN_ENDPOINT= # LOGTO_DB_USER=zen0 # LOGTO_DB_NAME=logto # LOGTO_DB_PASSWORD=