diff --git a/.env.example b/.env.example
index c20fca2..16a6778 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=
+LOGTO_APP_SECRET=
# 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=
# ============================================
# PRODUCTION (VPS 72.62.74.47) - Copy to .env.production
@@ -38,8 +45,8 @@ FRONTEND_PORT=3000
# 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=
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/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/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/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/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..0869b45 100644
--- a/packages/kal-backend/src/index.ts
+++ b/packages/kal-backend/src/index.ts
@@ -1,11 +1,16 @@
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 { logtoConfig, validateLogtoConfig } from "./lib/logto.js";
+import { apiRouter } from "./routers/api.js";
+import { appRouter } from "./routers/index.js";
const PORT = process.env.BACKEND_PORT || 3000;
@@ -87,9 +92,35 @@ async function main() {
const app = express();
- // Middleware
- app.use(cors());
+ // Core middleware
+ 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());
+
+ // 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..32ad951 100644
--- a/packages/kal-backend/src/lib/context.ts
+++ b/packages/kal-backend/src/lib/context.ts
@@ -1,15 +1,126 @@
-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 & { 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(
+ { logtoId: claims.sub },
+ {
+ $set: {
+ email: claims.email || "",
+ name: displayName,
+ 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,
+ 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 (!user && 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/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/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-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 31cd1f0..3611378 100644
--- a/packages/kal-backend/src/routers/api.ts
+++ b/packages/kal-backend/src/routers/api.ts
@@ -1,15 +1,20 @@
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);
+
// ============================================
-// Malaysian Food Database - Public REST API
+// Natural Foods - Public REST API (requires API key)
// ============================================
/**
* 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 +30,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 +60,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 +78,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 +116,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 +131,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 +164,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 +184,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 +384,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..49d8d1f 100644
--- a/packages/kal-backend/src/routers/index.ts
+++ b/packages/kal-backend/src/routers/index.ts
@@ -1,9 +1,15 @@
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
export type AppRouter = typeof appRouter;
+
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/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/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-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 */}
+
setActiveEndpoint(activeEndpoint === endpoint.id ? null : endpoint.id)}
+ className="w-full px-5 py-4 flex items-center justify-between hover:bg-[#1a1a1a] transition-colors text-left"
+ >
+
+
+ {endpoint.method}
+
+ {endpoint.path}
+
+
+
+
+
+
+ {/* Expanded content */}
+ {activeEndpoint === endpoint.id && (
+
+
{endpoint.description}
+
+ {/* Parameters */}
+ {endpoint.params && (
+
+
Parameters
+
+
+
+
+ Name
+ Type
+ Required
+ Description
+
+
+
+ {endpoint.params.map((param) => (
+
+ {param.name}
+ {param.type}
+
+ {param.required ? (
+ Yes
+ ) : (
+ No
+ )}
+
+ {param.description}
+
+ ))}
+
+
+
+
+ )}
+
+ {/* Try it */}
+
+
Try it
+
+
+ {endpoint.example}
+
+ tryEndpoint(endpoint.example)}
+ disabled={loading}
+ className={`px-4 py-2 ${accentColor} text-black font-semibold rounded-lg hover:opacity-80 transition-colors disabled:opacity-50`}
+ >
+ {loading ? "..." : "Run"}
+
+
+
+
+ {/* 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}
+
+
+
onSignIn()}
+ className="px-4 py-2 bg-[#10b981] text-black font-semibold rounded-lg hover:bg-[#0d9668] transition-colors flex items-center gap-2 text-sm"
+ >
+
+
+
+ Sign in to view
+
+
+
+ );
+ };
+
+ 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) */}
+
+
+
+ {/* Logo */}
+
+
+
Kal
+
+
+ {/* Desktop Navigation */}
+
+
+ {/* Auth Buttons */}
+
+ {isAuthenticated ? (
+ <>
+ {userEmail}
+ onSignOut()}
+ className="px-3 py-1.5 text-sm text-content-secondary hover:text-content-primary transition-colors"
+ >
+ Sign Out
+
+ >
+ ) : (
+ onSignIn()} size="sm">
+ Sign In →
+
+ )}
+
+
+ {/* Mobile Menu Button */}
+
setMobileMenuOpen(!mobileMenuOpen)}
+ className="md:hidden p-2 text-content-secondary hover:text-content-primary"
+ >
+
+ {mobileMenuOpen ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {/* Mobile Menu */}
+ {mobileMenuOpen && (
+
+ )}
+
+
+
+ {/* 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 */}
-
-
-
- {/* 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 */}
-
setActiveEndpoint(activeEndpoint === endpoint.id ? null : endpoint.id)}
- className="w-full px-5 py-4 flex items-center justify-between hover:bg-[#1a1a1a] transition-colors text-left"
- >
-
-
- {endpoint.method}
-
- {endpoint.path}
-
-
-
-
-
-
- {/* Expanded content */}
- {activeEndpoint === endpoint.id && (
-
-
{endpoint.description}
-
- {/* Parameters */}
- {endpoint.params && (
-
-
Parameters
-
-
-
-
- Name
- Type
- Required
- Description
-
-
-
- {endpoint.params.map((param) => (
-
- {param.name}
- {param.type}
-
- {param.required ? (
- Yes
- ) : (
- No
- )}
-
- {param.description}
-
- ))}
-
-
-
-
- )}
-
- {/* Try it */}
-
-
Try it
-
-
- {endpoint.example}
-
- tryEndpoint(endpoint.example)}
- disabled={loading}
- className="px-4 py-2 bg-[#10b981] text-black font-semibold rounded-lg hover:bg-[#34d399] transition-colors disabled:opacity-50"
- >
- {loading ? "..." : "Run"}
-
-
-
-
- {/* 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..4fbdac1
--- /dev/null
+++ b/packages/kal-frontend/src/app/callback/route.ts
@@ -0,0 +1,13 @@
+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 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 (
+
+ );
+ }
+
+ 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
+ setShowGenerateModal(true)}
+ >
+ + Generate New Key
+
+
+
+ {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"
+ }
+
+
+ handleRevoke(key._id, key.name)}
+ disabled={revokeMutation.isPending}
+ >
+ Revoke
+
+
+
+ ))}
+
+ ) : (
+
+
No API keys yet. Generate one to get started!
+
+ )}
+
+
+ {/* Quick Links */}
+
+
+ {/* Generate Key Modal */}
+ {showGenerateModal && (
+
+
e.stopPropagation()}>
+ {!generatedKey ? (
+ <>
+
Generate New API Key
+
+ Key Name
+ setNewKeyName(e.target.value)}
+ maxLength={50}
+ />
+
+
+
Expiration
+
+ setNewKeyExpiration("1_week")}
+ >
+ 1 Week
+
+ setNewKeyExpiration("1_month")}
+ >
+ 1 Month
+
+ setNewKeyExpiration("never")}
+ >
+ Never
+
+
+
+
+
+ Cancel
+
+
+ {generateMutation.isPending ? "Generating..." : "Generate Key"}
+
+
+ >
+ ) : (
+ <>
+
🎉 API Key Generated!
+
+
⚠️ Save this key now!
+
This is the only time you'll see this key. Copy it and store it securely.
+
+
+ {generatedKey}
+
+ {copied ? "✓ Copied!" : "Copy"}
+
+
+
+
+ Done
+
+
+ >
+ )}
+
+
+ )}
+
+ );
+}
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 (
+
+
+
+ );
+}
+
diff --git a/packages/kal-frontend/src/app/globals.css b/packages/kal-frontend/src/app/globals.css
index 2ddc6b4..3b9e1b6 100644
--- a/packages/kal-frontend/src/app/globals.css
+++ b/packages/kal-frontend/src/app/globals.css
@@ -42,3 +42,624 @@ 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;
+}
+
+.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 {
+ 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);
+}
+
+/* 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 799ffc3..05bbd1d 100644
--- a/packages/kal-frontend/src/app/layout.tsx
+++ b/packages/kal-frontend/src/app/layout.tsx
@@ -1,6 +1,8 @@
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"] });
@@ -18,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}...
+
+
router.push("/dashboard")}
+ >
+ Go to Dashboard Now
+
+
+
+ );
+}
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/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 */}
+
+ setActiveTab("natural")}
+ className={`px-6 py-3 rounded-xl font-medium transition-all duration-200
+ ${activeTab === "natural"
+ ? "bg-accent text-dark shadow-lg shadow-accent/20"
+ : "bg-dark-surface text-content-secondary border border-dark-border hover:border-accent/30"
+ }`}
+ >
+ 🍚 Natural Foods
+
+ setActiveTab("halal")}
+ className={`px-6 py-3 rounded-xl font-medium transition-all duration-200
+ ${activeTab === "halal"
+ ? "bg-emerald-500 text-white shadow-lg shadow-emerald-500/20"
+ : "bg-dark-surface text-content-secondary border border-dark-border hover:border-emerald-500/30"
+ }`}
+ >
+ Halal Certified
+
+
+
+ {/* 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 && (
setSelectedCategory("")}
@@ -169,7 +284,7 @@ export default function SearchPage() {
>
All
- {categories.map((cat) => (
+ {naturalCategories.map((cat) => (
setSelectedCategory(cat)}
@@ -186,6 +301,37 @@ export default function SearchPage() {
)}
+ {/* Brand Filter (Halal Foods) */}
+ {activeTab === "halal" && !isSearchMode && halalBrands && halalBrands.length > 0 && (
+
+ setSelectedBrand("")}
+ className={`px-4 py-2 rounded-full text-sm font-medium transition-all
+ ${
+ selectedBrand === ""
+ ? "bg-emerald-500 text-white"
+ : "bg-dark-surface text-content-secondary border border-dark-border hover:border-emerald-500/30"
+ }`}
+ >
+ All Brands
+
+ {halalBrands.map((brand) => (
+ setSelectedBrand(brand)}
+ className={`px-4 py-2 rounded-full text-sm font-medium transition-all
+ ${
+ selectedBrand === brand
+ ? "bg-emerald-500 text-white"
+ : "bg-dark-surface text-content-secondary border border-dark-border hover:border-emerald-500/30"
+ }`}
+ >
+ {brand}
+
+ ))}
+
+ )}
+
{/* Results count */}
{!isLoading && foods.length > 0 && (
@@ -211,75 +357,105 @@ export default function SearchPage() {
) : foods.length > 0 ? (
<>
- {foods.map((food) => (
-
- setSelectedFood(
- selectedFood?._id === food._id ? null : food
- )
- }
- className={`text-left p-6 rounded-xl border transition-all duration-200
- hover:scale-[1.01]
- ${
- selectedFood?._id === food._id
- ? "bg-dark-elevated border-accent/50"
- : "bg-dark-surface border-dark-border hover:border-accent/30"
- }`}
- >
-
-
-
- {food.name}
-
- {food.category && (
-
- {food.category}
-
- )}
-
-
- {food.calories} cal
-
-
-
- Serving: {food.serving}
-
-
- {/* Macro breakdown */}
-
-
-
-
- P: {food.protein}g
+ {foods.map((food) => {
+ const isHalalFood = "brand" in food;
+ return (
+
+ setSelectedFood(
+ selectedFood?._id === food._id ? null : food
+ )
+ }
+ className={`text-left p-6 rounded-xl border transition-all duration-200
+ hover:scale-[1.01]
+ ${
+ selectedFood?._id === food._id
+ ? isHalalFood
+ ? "bg-dark-elevated border-emerald-500/50"
+ : "bg-dark-elevated border-accent/50"
+ : "bg-dark-surface border-dark-border hover:border-accent/30"
+ }`}
+ >
+
+
+
+ {food.name}
+
+
+ {isHalalFood && (food as HalalFood).brand && (
+
+ {(food as HalalFood).brand}
+
+ )}
+ {food.category && (
+
+ {food.category}
+
+ )}
+
+
+
+ {food.calories} cal
-
-
-
- C: {food.carbs}g
-
-
-
-
-
- F: {food.fat}g
-
+
+ {/* Halal certification badge */}
+ {isHalalFood && (food as HalalFood).halalCertifier && (
+
+
+ {(food as HalalFood).halalCertifier} Certified
+ {(food as HalalFood).halalCertYear && ` (${(food as HalalFood).halalCertYear})`}
+
+
+ )}
+
+
+ Serving: {food.serving}
+
+
+ {/* Macro breakdown */}
+
+
+
+
+ P: {food.protein}g
+
+
+
+
+
+ C: {food.carbs}g
+
+
+
-
-
- ))}
+
+ );
+ })}
{/* Load more trigger (only for pagination mode) */}
{!isSearchMode && (
- {isFetchingNextPage ? (
+ {currentIsFetchingNextPage ? (
- ) : 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..b3bba83
--- /dev/null
+++ b/packages/kal-frontend/src/components/navbar.tsx
@@ -0,0 +1,38 @@
+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 (
+
+
+
+ {isAuthenticated ? (
+
+
Dashboard
+
{claims?.email || claims?.sub}
+
{
+ 'use server';
+ await signOut(logtoConfig);
+ }}
+ />
+
+ ) : (
+
{
+ 'use server';
+ await signIn(logtoConfig);
+ }}
+ />
+ )}
+
+
+ );
+}
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 (
+ onSignIn()}
+ className="sign-in-btn"
+ >
+ Sign In
+
+ );
+}
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 (
+ onSignOut()}
+ className="sign-out-btn"
+ >
+ Sign Out
+
+ );
+}
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/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..6906890 100644
--- a/packages/kal-frontend/src/lib/trpc-provider.tsx
+++ b/packages/kal-frontend/src/lib/trpc-provider.tsx
@@ -1,8 +1,10 @@
"use client";
-import { useState } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { httpBatchLink } from "@trpc/client";
+import { useRef, useState } from "react";
+
+import { useAuth } from "./auth-context";
import { trpc } from "./trpc";
function getBaseUrl() {
@@ -13,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-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..11c4c3b 100644
--- a/packages/kal-shared/src/types/index.ts
+++ b/packages/kal-shared/src/types/index.ts
@@ -1,12 +1,85 @@
+// ===================
+// 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;
+}
+
+// ===================
+// 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 {
_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"]
},