Initial API scaffold: Elysia + Bun + Drizzle + BetterAuth + LangChain
This commit is contained in:
17
.env.example
Normal file
17
.env.example
Normal file
@@ -0,0 +1,17 @@
|
||||
# Database
|
||||
DATABASE_URL=postgresql://postgres:password@localhost:5432/networkapp
|
||||
|
||||
# Server
|
||||
PORT=3000
|
||||
APP_URL=http://localhost:3000
|
||||
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080
|
||||
|
||||
# BetterAuth
|
||||
BETTER_AUTH_SECRET=your-secret-key-here-min-32-chars
|
||||
|
||||
# AI (LangChain)
|
||||
ANTHROPIC_API_KEY=your-anthropic-api-key
|
||||
|
||||
# Email (Resend)
|
||||
RESEND_API_KEY=your-resend-api-key
|
||||
DEFAULT_FROM_EMAIL=onboarding@resend.dev
|
||||
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
18
Dockerfile
Normal file
18
Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
FROM oven/bun:1 AS base
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
FROM base AS install
|
||||
COPY package.json bun.lockb ./
|
||||
RUN bun install --frozen-lockfile --production
|
||||
|
||||
# Production image
|
||||
FROM base AS release
|
||||
COPY --from=install /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
ENV NODE_ENV=production
|
||||
EXPOSE 3000
|
||||
|
||||
USER bun
|
||||
CMD ["bun", "run", "src/index.ts"]
|
||||
128
README.md
Normal file
128
README.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Network App API
|
||||
|
||||
Backend API for The Network App — an AI-powered CRM for wealth management advisors.
|
||||
|
||||
## Stack
|
||||
|
||||
- **Runtime:** Bun
|
||||
- **Framework:** Elysia
|
||||
- **Database:** PostgreSQL + Drizzle ORM
|
||||
- **Auth:** BetterAuth
|
||||
- **AI:** LangChain.js (Anthropic Claude)
|
||||
- **Email:** Resend
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Bun](https://bun.sh/) v1.0+
|
||||
- PostgreSQL 15+
|
||||
|
||||
### Setup
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
2. Copy environment file:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
3. Configure `.env` with your values (database, API keys, etc.)
|
||||
|
||||
4. Push database schema:
|
||||
```bash
|
||||
bun run db:push
|
||||
```
|
||||
|
||||
5. Start development server:
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
API runs at `http://localhost:3000`
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Auth (BetterAuth)
|
||||
- `POST /api/auth/sign-up` — Register
|
||||
- `POST /api/auth/sign-in/email` — Login
|
||||
- `POST /api/auth/sign-out` — Logout
|
||||
- `GET /api/auth/session` — Get current session
|
||||
|
||||
### Clients
|
||||
- `GET /api/clients` — List clients
|
||||
- `GET /api/clients/:id` — Get client
|
||||
- `POST /api/clients` — Create client
|
||||
- `PUT /api/clients/:id` — Update client
|
||||
- `DELETE /api/clients/:id` — Delete client
|
||||
- `POST /api/clients/:id/contacted` — Mark as contacted
|
||||
|
||||
### Emails
|
||||
- `POST /api/emails/generate` — Generate AI email
|
||||
- `POST /api/emails/generate-birthday` — Generate birthday message
|
||||
- `GET /api/emails` — List emails
|
||||
- `GET /api/emails/:id` — Get email
|
||||
- `PUT /api/emails/:id` — Edit draft
|
||||
- `POST /api/emails/:id/send` — Send email
|
||||
- `DELETE /api/emails/:id` — Delete draft
|
||||
|
||||
### Events
|
||||
- `GET /api/events` — List events
|
||||
- `GET /api/events/:id` — Get event
|
||||
- `POST /api/events` — Create event
|
||||
- `PUT /api/events/:id` — Update event
|
||||
- `DELETE /api/events/:id` — Delete event
|
||||
- `POST /api/events/sync/:clientId` — Sync client birthdays/anniversaries
|
||||
|
||||
## Database
|
||||
|
||||
### Generate migrations
|
||||
```bash
|
||||
bun run db:generate
|
||||
```
|
||||
|
||||
### Apply migrations
|
||||
```bash
|
||||
bun run db:migrate
|
||||
```
|
||||
|
||||
### Push schema (development)
|
||||
```bash
|
||||
bun run db:push
|
||||
```
|
||||
|
||||
### Open Drizzle Studio
|
||||
```bash
|
||||
bun run db:studio
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
```bash
|
||||
docker build -t network-app-api .
|
||||
docker run -p 3000:3000 --env-file .env network-app-api
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── index.ts # Entry point
|
||||
├── routes/
|
||||
│ ├── clients.ts # Client CRUD
|
||||
│ ├── emails.ts # AI email generation
|
||||
│ └── events.ts # Event tracking
|
||||
├── services/
|
||||
│ ├── ai.ts # LangChain integration
|
||||
│ └── email.ts # Resend integration
|
||||
├── db/
|
||||
│ ├── schema.ts # Drizzle schema
|
||||
│ └── index.ts # DB connection
|
||||
├── lib/
|
||||
│ └── auth.ts # BetterAuth config
|
||||
└── types/
|
||||
└── index.ts # TypeScript types
|
||||
```
|
||||
347
bun.lock
Normal file
347
bun.lock
Normal file
@@ -0,0 +1,347 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "network-app-api",
|
||||
"dependencies": {
|
||||
"@elysiajs/bearer": "^1.4.2",
|
||||
"@elysiajs/cors": "^1.4.1",
|
||||
"@langchain/anthropic": "^1.3.12",
|
||||
"@langchain/core": "^1.1.17",
|
||||
"better-auth": "^1.4.17",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"elysia": "^1.4.22",
|
||||
"pg-boss": "^12.7.0",
|
||||
"postgres": "^3.4.8",
|
||||
"resend": "^6.8.0",
|
||||
"zod": "^4.3.6",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"@types/pg": "^8.16.0",
|
||||
"drizzle-kit": "^0.31.8",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.71.2", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ=="],
|
||||
|
||||
"@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
|
||||
|
||||
"@better-auth/core": ["@better-auth/core@1.4.17", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.3.5" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.8", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-WSaEQDdUO6B1CzAmissN6j0lx9fM9lcslEYzlApB5UzFaBeAOHNUONTdglSyUs6/idiZBoRvt0t/qMXCgIU8ug=="],
|
||||
|
||||
"@better-auth/telemetry": ["@better-auth/telemetry@1.4.17", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.17" } }, "sha512-R1BC4e/bNjQbXu7lG6ubpgmsPj7IMqky5DvMlzAtnAJWJhh99pMh/n6w5gOHa0cqDZgEAuj75IPTxv+q3YiInA=="],
|
||||
|
||||
"@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="],
|
||||
|
||||
"@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="],
|
||||
|
||||
"@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="],
|
||||
|
||||
"@cfworker/json-schema": ["@cfworker/json-schema@4.1.1", "", {}, "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og=="],
|
||||
|
||||
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
|
||||
|
||||
"@elysiajs/bearer": ["@elysiajs/bearer@1.4.2", "", { "peerDependencies": { "elysia": ">= 1.4.3" } }, "sha512-MK2aCFqnFMqMNSa1e/A6+Ow5uNl5LpKd8K4lCB2LIsyDrI6juxOUHAgqq+esgdSoh3urD1UIMqFC//TsqCQViA=="],
|
||||
|
||||
"@elysiajs/cors": ["@elysiajs/cors@1.4.1", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-lQfad+F3r4mNwsxRKbXyJB8Jg43oAOXjRwn7sKUL6bcOW3KjUqUimTS+woNpO97efpzjtDE0tEjGk9DTw8lqTQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="],
|
||||
|
||||
"@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
||||
|
||||
"@langchain/anthropic": ["@langchain/anthropic@1.3.12", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "zod": "^3.25.76 || ^4" }, "peerDependencies": { "@langchain/core": "1.1.17" } }, "sha512-CeICvFLsMLau5RjGHqUsZKAdEWrBZYebVnufRRtGlkg7nq3BqZSK2zVAAcf37q7e5u3WyPzBTWAHwR2adfpMCA=="],
|
||||
|
||||
"@langchain/core": ["@langchain/core@1.1.17", "", { "dependencies": { "@cfworker/json-schema": "^4.0.2", "ansi-styles": "^5.0.0", "camelcase": "6", "decamelize": "1.2.0", "js-tiktoken": "^1.0.12", "langsmith": ">=0.4.0 <1.0.0", "mustache": "^4.2.0", "p-queue": "^6.6.2", "uuid": "^10.0.0", "zod": "^3.25.76 || ^4" } }, "sha512-g7/kcKbKEwNZSyyT7aT0utxn7wTOtKErqz0cL6VjrV4v/aOb9g+dKcfj17YkSm42YQmJp/rB2IXGc17vQPEBqA=="],
|
||||
|
||||
"@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="],
|
||||
|
||||
"@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="],
|
||||
|
||||
"@sinclair/typebox": ["@sinclair/typebox@0.34.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="],
|
||||
|
||||
"@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="],
|
||||
|
||||
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
|
||||
|
||||
"@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="],
|
||||
|
||||
"@types/pg": ["@types/pg@8.16.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ=="],
|
||||
|
||||
"@types/uuid": ["@types/uuid@10.0.0", "", {}, "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
|
||||
|
||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"better-auth": ["better-auth@1.4.17", "", { "dependencies": { "@better-auth/core": "1.4.17", "@better-auth/telemetry": "1.4.17", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.8", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.3.5" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-VmHGQyKsEahkEs37qguROKg/6ypYpNF13D7v/lkbO7w7Aivz0Bv2h+VyUkH4NzrGY0QBKXi1577mGhDCVwp0ew=="],
|
||||
|
||||
"better-call": ["better-call@1.1.8", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw=="],
|
||||
|
||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
|
||||
|
||||
"camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="],
|
||||
|
||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"console-table-printer": ["console-table-printer@2.15.0", "", { "dependencies": { "simple-wcswidth": "^1.1.2" } }, "sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw=="],
|
||||
|
||||
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||
|
||||
"cron-parser": ["cron-parser@5.5.0", "", { "dependencies": { "luxon": "^3.7.1" } }, "sha512-oML4lKUXxizYswqmxuOCpgFS8BNUJpIu6k/2HVHyaL8Ynnf3wdf9tkns0yRdJLSIjkJ+b0DXHMZEHGpMwjnPww=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="],
|
||||
|
||||
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
|
||||
|
||||
"drizzle-kit": ["drizzle-kit@0.31.8", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg=="],
|
||||
|
||||
"drizzle-orm": ["drizzle-orm@0.45.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA=="],
|
||||
|
||||
"elysia": ["elysia@1.4.22", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.6", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-Q90VCb1RVFxnFaRV0FDoSylESQQLWgLHFmWciQJdX9h3b2cSasji9KWEUvaJuy/L9ciAGg4RAhUVfsXHg5K2RQ=="],
|
||||
|
||||
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
||||
|
||||
"esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="],
|
||||
|
||||
"eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
|
||||
|
||||
"exact-mirror": ["exact-mirror@0.2.6", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-7s059UIx9/tnOKSySzUk5cPGkoILhTE4p6ncf6uIPaQ+9aRBQzQjc9+q85l51+oZ+P6aBxh084pD0CzBQPcFUA=="],
|
||||
|
||||
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
|
||||
|
||||
"fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="],
|
||||
|
||||
"file-type": ["file-type@21.3.0", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA=="],
|
||||
|
||||
"get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="],
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
|
||||
|
||||
"js-tiktoken": ["js-tiktoken@1.0.21", "", { "dependencies": { "base64-js": "^1.5.1" } }, "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g=="],
|
||||
|
||||
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
|
||||
|
||||
"kysely": ["kysely@0.28.10", "", {}, "sha512-ksNxfzIW77OcZ+QWSAPC7yDqUSaIVwkTWnTPNiIy//vifNbwsSgQ57OkkncHxxpcBHM3LRfLAZVEh7kjq5twVA=="],
|
||||
|
||||
"langsmith": ["langsmith@0.4.10", "", { "dependencies": { "@types/uuid": "^10.0.0", "chalk": "^4.1.2", "console-table-printer": "^2.12.1", "p-queue": "^6.6.2", "semver": "^7.6.3", "uuid": "^10.0.0" }, "peerDependencies": { "@opentelemetry/api": "*", "@opentelemetry/exporter-trace-otlp-proto": "*", "@opentelemetry/sdk-trace-base": "*", "openai": "*" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/exporter-trace-otlp-proto", "@opentelemetry/sdk-trace-base", "openai"] }, "sha512-l9QP/a7RXBXdaoAnNx99X+TK8aul8Qe4us1oCybdMgDmYMLT5PAwlJactvSdTlT8NOeSoFThYa2N7ijznBNe9w=="],
|
||||
|
||||
"luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="],
|
||||
|
||||
"memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="],
|
||||
|
||||
"nanostores": ["nanostores@1.1.0", "", {}, "sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA=="],
|
||||
|
||||
"non-error": ["non-error@0.1.0", "", {}, "sha512-TMB1uHiGsHRGv1uYclfhivcnf0/PdFp2pNqRxXjncaAsjYMoisaQJI+SSZCqRq+VliwRTC8tsMQfmrWjDMhkPQ=="],
|
||||
|
||||
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
|
||||
|
||||
"p-finally": ["p-finally@1.0.0", "", {}, "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow=="],
|
||||
|
||||
"p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="],
|
||||
|
||||
"p-timeout": ["p-timeout@3.2.0", "", { "dependencies": { "p-finally": "^1.0.0" } }, "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg=="],
|
||||
|
||||
"pg": ["pg@8.17.2", "", { "dependencies": { "pg-connection-string": "^2.10.1", "pg-pool": "^3.11.0", "pg-protocol": "^1.11.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw=="],
|
||||
|
||||
"pg-boss": ["pg-boss@12.7.0", "", { "dependencies": { "cron-parser": "^5.5.0", "pg": "^8.17.2", "serialize-error": "^13.0.1" }, "bin": { "pg-boss": "dist/cli.js" } }, "sha512-sOr7wHVGILgMrciuDoyc3e1ejvC7Wx1ESJUdsIMwvYmNS5tT8EqJOZgzLnTl5O7sKMweyJsDRiGuG23JoHI/+g=="],
|
||||
|
||||
"pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="],
|
||||
|
||||
"pg-connection-string": ["pg-connection-string@2.10.1", "", {}, "sha512-iNzslsoeSH2/gmDDKiyMqF64DATUCWj3YJ0wP14kqcsf2TUklwimd+66yYojKwZCA7h2yRNLGug71hCBA2a4sw=="],
|
||||
|
||||
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
|
||||
|
||||
"pg-pool": ["pg-pool@3.11.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w=="],
|
||||
|
||||
"pg-protocol": ["pg-protocol@1.11.0", "", {}, "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g=="],
|
||||
|
||||
"pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
|
||||
|
||||
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
|
||||
|
||||
"postgres": ["postgres@3.4.8", "", {}, "sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg=="],
|
||||
|
||||
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
||||
|
||||
"postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="],
|
||||
|
||||
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
|
||||
|
||||
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
|
||||
|
||||
"resend": ["resend@6.8.0", "", { "dependencies": { "svix": "1.84.1" }, "peerDependencies": { "@react-email/render": "*" }, "optionalPeers": ["@react-email/render"] }, "sha512-fDOXGqafQfQXl8nXe93wr93pus8tW7YPpowenE3SmG7dJJf0hH3xUWm3xqacnPvhqjCQTJH9xETg07rmUeSuqQ=="],
|
||||
|
||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||
|
||||
"rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="],
|
||||
|
||||
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"serialize-error": ["serialize-error@13.0.1", "", { "dependencies": { "non-error": "^0.1.0", "type-fest": "^5.4.1" } }, "sha512-bBZaRwLH9PN5HbLCjPId4dP5bNGEtumcErgOX952IsvOhVPrm3/AeK1y0UHA/QaPG701eg0yEnOKsCOC6X/kaA=="],
|
||||
|
||||
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
|
||||
|
||||
"simple-wcswidth": ["simple-wcswidth@1.1.2", "", {}, "sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw=="],
|
||||
|
||||
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
|
||||
|
||||
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
||||
|
||||
"standardwebhooks": ["standardwebhooks@1.0.0", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "fast-sha256": "^1.3.0" } }, "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg=="],
|
||||
|
||||
"strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="],
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"svix": ["svix@1.84.1", "", { "dependencies": { "standardwebhooks": "1.0.0", "uuid": "^10.0.0" } }, "sha512-K8DPPSZaW/XqXiz1kEyzSHYgmGLnhB43nQCMeKjWGCUpLIpAMMM8kx3rVVOSm6Bo6EHyK1RQLPT4R06skM/MlQ=="],
|
||||
|
||||
"tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
|
||||
|
||||
"token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="],
|
||||
|
||||
"ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="],
|
||||
|
||||
"type-fest": ["type-fest@5.4.1", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-xygQcmneDyzsEuKZrFbRMne5HDqMs++aFzefrJTgEIKjQ3rekM+RPfFCVq2Gp1VIDqddoYeppCj4Pcb+RZW0GQ=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="],
|
||||
|
||||
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
||||
|
||||
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
|
||||
|
||||
"chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
|
||||
}
|
||||
}
|
||||
10
drizzle.config.ts
Normal file
10
drizzle.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'drizzle-kit';
|
||||
|
||||
export default defineConfig({
|
||||
schema: './src/db/schema.ts',
|
||||
out: './drizzle',
|
||||
dialect: 'postgresql',
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL!,
|
||||
},
|
||||
});
|
||||
36
package.json
Normal file
36
package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "network-app-api",
|
||||
"version": "0.1.0",
|
||||
"module": "src/index.ts",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "bun run --watch src/index.ts",
|
||||
"start": "bun run src/index.ts",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:push": "drizzle-kit push",
|
||||
"db:studio": "drizzle-kit studio"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"@types/pg": "^8.16.0",
|
||||
"drizzle-kit": "^0.31.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@elysiajs/bearer": "^1.4.2",
|
||||
"@elysiajs/cors": "^1.4.1",
|
||||
"@langchain/anthropic": "^1.3.12",
|
||||
"@langchain/core": "^1.1.17",
|
||||
"better-auth": "^1.4.17",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"elysia": "^1.4.22",
|
||||
"pg-boss": "^12.7.0",
|
||||
"postgres": "^3.4.8",
|
||||
"resend": "^6.8.0",
|
||||
"zod": "^4.3.6"
|
||||
}
|
||||
}
|
||||
16
src/db/index.ts
Normal file
16
src/db/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||
import postgres from 'postgres';
|
||||
import * as schema from './schema';
|
||||
|
||||
const connectionString = process.env.DATABASE_URL!;
|
||||
|
||||
if (!connectionString) {
|
||||
throw new Error('DATABASE_URL environment variable is required');
|
||||
}
|
||||
|
||||
// For query purposes
|
||||
const queryClient = postgres(connectionString);
|
||||
export const db = drizzle(queryClient, { schema });
|
||||
|
||||
// For migrations (uses a different client)
|
||||
export const migrationClient = postgres(connectionString, { max: 1 });
|
||||
163
src/db/schema.ts
Normal file
163
src/db/schema.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { pgTable, text, timestamp, uuid, boolean, jsonb, integer } from 'drizzle-orm/pg-core';
|
||||
import { relations } from 'drizzle-orm';
|
||||
|
||||
// Users table (managed by BetterAuth, but we define it for relations)
|
||||
export const users = pgTable('users', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
email: text('email').notNull().unique(),
|
||||
name: text('name').notNull(),
|
||||
emailVerified: boolean('email_verified').default(false),
|
||||
image: text('image'),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// BetterAuth session table
|
||||
export const sessions = pgTable('sessions', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
||||
token: text('token').notNull().unique(),
|
||||
expiresAt: timestamp('expires_at').notNull(),
|
||||
ipAddress: text('ip_address'),
|
||||
userAgent: text('user_agent'),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// BetterAuth account table (for OAuth providers)
|
||||
export const accounts = pgTable('accounts', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
||||
accountId: text('account_id').notNull(),
|
||||
providerId: text('provider_id').notNull(),
|
||||
accessToken: text('access_token'),
|
||||
refreshToken: text('refresh_token'),
|
||||
accessTokenExpiresAt: timestamp('access_token_expires_at'),
|
||||
refreshTokenExpiresAt: timestamp('refresh_token_expires_at'),
|
||||
scope: text('scope'),
|
||||
idToken: text('id_token'),
|
||||
password: text('password'),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// BetterAuth verification table
|
||||
export const verifications = pgTable('verifications', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
identifier: text('identifier').notNull(),
|
||||
value: text('value').notNull(),
|
||||
expiresAt: timestamp('expires_at').notNull(),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// Clients table
|
||||
export const clients = pgTable('clients', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
||||
|
||||
// Basic info
|
||||
firstName: text('first_name').notNull(),
|
||||
lastName: text('last_name').notNull(),
|
||||
email: text('email'),
|
||||
phone: text('phone'),
|
||||
|
||||
// Address
|
||||
street: text('street'),
|
||||
city: text('city'),
|
||||
state: text('state'),
|
||||
zip: text('zip'),
|
||||
|
||||
// Professional
|
||||
company: text('company'),
|
||||
role: text('role'),
|
||||
industry: text('industry'),
|
||||
|
||||
// Personal
|
||||
birthday: timestamp('birthday'),
|
||||
anniversary: timestamp('anniversary'),
|
||||
interests: jsonb('interests').$type<string[]>().default([]),
|
||||
family: jsonb('family').$type<{ spouse?: string; children?: string[] }>(),
|
||||
notes: text('notes'),
|
||||
|
||||
// Organization
|
||||
tags: jsonb('tags').$type<string[]>().default([]),
|
||||
|
||||
// Tracking
|
||||
lastContactedAt: timestamp('last_contacted_at'),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// Events table (birthdays, anniversaries, follow-ups)
|
||||
export const events = pgTable('events', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
||||
clientId: uuid('client_id').references(() => clients.id, { onDelete: 'cascade' }).notNull(),
|
||||
|
||||
type: text('type').notNull(), // 'birthday' | 'anniversary' | 'followup' | 'custom'
|
||||
title: text('title').notNull(),
|
||||
date: timestamp('date').notNull(),
|
||||
recurring: boolean('recurring').default(false),
|
||||
reminderDays: integer('reminder_days').default(7),
|
||||
lastTriggered: timestamp('last_triggered'),
|
||||
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// Communications table (emails, messages)
|
||||
export const communications = pgTable('communications', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
||||
clientId: uuid('client_id').references(() => clients.id, { onDelete: 'cascade' }).notNull(),
|
||||
|
||||
type: text('type').notNull(), // 'email' | 'birthday' | 'followup'
|
||||
subject: text('subject'),
|
||||
content: text('content').notNull(),
|
||||
aiGenerated: boolean('ai_generated').default(false),
|
||||
aiModel: text('ai_model'), // Which model was used
|
||||
status: text('status').default('draft'), // 'draft' | 'approved' | 'sent'
|
||||
sentAt: timestamp('sent_at'),
|
||||
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// Relations
|
||||
export const usersRelations = relations(users, ({ many }) => ({
|
||||
clients: many(clients),
|
||||
events: many(events),
|
||||
communications: many(communications),
|
||||
sessions: many(sessions),
|
||||
accounts: many(accounts),
|
||||
}));
|
||||
|
||||
export const clientsRelations = relations(clients, ({ one, many }) => ({
|
||||
user: one(users, {
|
||||
fields: [clients.userId],
|
||||
references: [users.id],
|
||||
}),
|
||||
events: many(events),
|
||||
communications: many(communications),
|
||||
}));
|
||||
|
||||
export const eventsRelations = relations(events, ({ one }) => ({
|
||||
user: one(users, {
|
||||
fields: [events.userId],
|
||||
references: [users.id],
|
||||
}),
|
||||
client: one(clients, {
|
||||
fields: [events.clientId],
|
||||
references: [clients.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const communicationsRelations = relations(communications, ({ one }) => ({
|
||||
user: one(users, {
|
||||
fields: [communications.userId],
|
||||
references: [users.id],
|
||||
}),
|
||||
client: one(clients, {
|
||||
fields: [communications.clientId],
|
||||
references: [clients.id],
|
||||
}),
|
||||
}));
|
||||
71
src/index.ts
Normal file
71
src/index.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Elysia } from 'elysia';
|
||||
import { cors } from '@elysiajs/cors';
|
||||
import { auth } from './lib/auth';
|
||||
import { clientRoutes } from './routes/clients';
|
||||
import { emailRoutes } from './routes/emails';
|
||||
import { eventRoutes } from './routes/events';
|
||||
import type { User } from './lib/auth';
|
||||
|
||||
const app = new Elysia()
|
||||
// CORS
|
||||
.use(cors({
|
||||
origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
|
||||
credentials: true,
|
||||
}))
|
||||
|
||||
// Health check
|
||||
.get('/health', () => ({ status: 'ok', timestamp: new Date().toISOString() }))
|
||||
|
||||
// BetterAuth routes (login, register, etc.)
|
||||
.all('/api/auth/*', async ({ request }) => {
|
||||
return auth.handler(request);
|
||||
})
|
||||
|
||||
// Protected routes - require auth
|
||||
.derive(async ({ request, set }): Promise<{ user: User }> => {
|
||||
const session = await auth.api.getSession({
|
||||
headers: request.headers,
|
||||
});
|
||||
|
||||
if (!session?.user) {
|
||||
set.status = 401;
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
return { user: session.user as User };
|
||||
})
|
||||
|
||||
// API routes (all require auth due to derive above)
|
||||
.group('/api', app => app
|
||||
.use(clientRoutes)
|
||||
.use(emailRoutes)
|
||||
.use(eventRoutes)
|
||||
)
|
||||
|
||||
// Error handler
|
||||
.onError(({ code, error, set }) => {
|
||||
if (code === 'VALIDATION') {
|
||||
set.status = 400;
|
||||
return { error: 'Validation error', details: error.message };
|
||||
}
|
||||
|
||||
if (error.message === 'Unauthorized') {
|
||||
set.status = 401;
|
||||
return { error: 'Unauthorized' };
|
||||
}
|
||||
|
||||
if (error.message.includes('not found')) {
|
||||
set.status = 404;
|
||||
return { error: error.message };
|
||||
}
|
||||
|
||||
console.error('Error:', error);
|
||||
set.status = 500;
|
||||
return { error: 'Internal server error' };
|
||||
})
|
||||
|
||||
.listen(process.env.PORT || 3000);
|
||||
|
||||
console.log(`🚀 Network App API running at ${app.server?.hostname}:${app.server?.port}`);
|
||||
|
||||
export type App = typeof app;
|
||||
30
src/lib/auth.ts
Normal file
30
src/lib/auth.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { betterAuth } from 'better-auth';
|
||||
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
|
||||
import { db } from '../db';
|
||||
import * as schema from '../db/schema';
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: drizzleAdapter(db, {
|
||||
provider: 'pg',
|
||||
schema: {
|
||||
user: schema.users,
|
||||
session: schema.sessions,
|
||||
account: schema.accounts,
|
||||
verification: schema.verifications,
|
||||
},
|
||||
}),
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
requireEmailVerification: false, // Enable later for production
|
||||
},
|
||||
session: {
|
||||
expiresIn: 60 * 60 * 24 * 7, // 7 days
|
||||
updateAge: 60 * 60 * 24, // Update session every day
|
||||
},
|
||||
trustedOrigins: [
|
||||
process.env.APP_URL || 'http://localhost:3000',
|
||||
],
|
||||
});
|
||||
|
||||
export type Session = typeof auth.$Infer.Session;
|
||||
export type User = typeof auth.$Infer.Session.user;
|
||||
194
src/routes/clients.ts
Normal file
194
src/routes/clients.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import { Elysia, t } from 'elysia';
|
||||
import { db } from '../db';
|
||||
import { clients } from '../db/schema';
|
||||
import { eq, and, ilike, or, sql } from 'drizzle-orm';
|
||||
import type { User } from '../lib/auth';
|
||||
|
||||
// Validation schemas
|
||||
const clientSchema = t.Object({
|
||||
firstName: t.String({ minLength: 1 }),
|
||||
lastName: t.String({ minLength: 1 }),
|
||||
email: t.Optional(t.String({ format: 'email' })),
|
||||
phone: t.Optional(t.String()),
|
||||
street: t.Optional(t.String()),
|
||||
city: t.Optional(t.String()),
|
||||
state: t.Optional(t.String()),
|
||||
zip: t.Optional(t.String()),
|
||||
company: t.Optional(t.String()),
|
||||
role: t.Optional(t.String()),
|
||||
industry: t.Optional(t.String()),
|
||||
birthday: t.Optional(t.String()), // ISO date string
|
||||
anniversary: t.Optional(t.String()),
|
||||
interests: t.Optional(t.Array(t.String())),
|
||||
family: t.Optional(t.Object({
|
||||
spouse: t.Optional(t.String()),
|
||||
children: t.Optional(t.Array(t.String())),
|
||||
})),
|
||||
notes: t.Optional(t.String()),
|
||||
tags: t.Optional(t.Array(t.String())),
|
||||
});
|
||||
|
||||
const updateClientSchema = t.Partial(clientSchema);
|
||||
|
||||
export const clientRoutes = new Elysia({ prefix: '/clients' })
|
||||
// List clients with optional search
|
||||
.get('/', async ({ query, user }: { query: { search?: string; tag?: string }; user: User }) => {
|
||||
let baseQuery = db.select().from(clients).where(eq(clients.userId, user.id));
|
||||
|
||||
if (query.search) {
|
||||
const searchTerm = `%${query.search}%`;
|
||||
baseQuery = db.select().from(clients).where(
|
||||
and(
|
||||
eq(clients.userId, user.id),
|
||||
or(
|
||||
ilike(clients.firstName, searchTerm),
|
||||
ilike(clients.lastName, searchTerm),
|
||||
ilike(clients.company, searchTerm),
|
||||
ilike(clients.email, searchTerm)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const results = await baseQuery.orderBy(clients.lastName, clients.firstName);
|
||||
|
||||
// Filter by tag in-memory if needed (JSONB filtering)
|
||||
if (query.tag) {
|
||||
return results.filter(c => c.tags?.includes(query.tag!));
|
||||
}
|
||||
|
||||
return results;
|
||||
}, {
|
||||
query: t.Object({
|
||||
search: t.Optional(t.String()),
|
||||
tag: t.Optional(t.String()),
|
||||
}),
|
||||
})
|
||||
|
||||
// Get single client
|
||||
.get('/:id', async ({ params, user }: { params: { id: string }; user: User }) => {
|
||||
const [client] = await db.select()
|
||||
.from(clients)
|
||||
.where(and(eq(clients.id, params.id), eq(clients.userId, user.id)))
|
||||
.limit(1);
|
||||
|
||||
if (!client) {
|
||||
throw new Error('Client not found');
|
||||
}
|
||||
|
||||
return client;
|
||||
}, {
|
||||
params: t.Object({
|
||||
id: t.String({ format: 'uuid' }),
|
||||
}),
|
||||
})
|
||||
|
||||
// Create client
|
||||
.post('/', async ({ body, user }: { body: typeof clientSchema.static; user: User }) => {
|
||||
const [client] = await db.insert(clients)
|
||||
.values({
|
||||
userId: user.id,
|
||||
firstName: body.firstName,
|
||||
lastName: body.lastName,
|
||||
email: body.email,
|
||||
phone: body.phone,
|
||||
street: body.street,
|
||||
city: body.city,
|
||||
state: body.state,
|
||||
zip: body.zip,
|
||||
company: body.company,
|
||||
role: body.role,
|
||||
industry: body.industry,
|
||||
birthday: body.birthday ? new Date(body.birthday) : null,
|
||||
anniversary: body.anniversary ? new Date(body.anniversary) : null,
|
||||
interests: body.interests || [],
|
||||
family: body.family,
|
||||
notes: body.notes,
|
||||
tags: body.tags || [],
|
||||
})
|
||||
.returning();
|
||||
|
||||
return client;
|
||||
}, {
|
||||
body: clientSchema,
|
||||
})
|
||||
|
||||
// Update client
|
||||
.put('/:id', async ({ params, body, user }: { params: { id: string }; body: typeof updateClientSchema.static; user: User }) => {
|
||||
// Build update object, only including provided fields
|
||||
const updateData: Record<string, unknown> = {
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
if (body.firstName !== undefined) updateData.firstName = body.firstName;
|
||||
if (body.lastName !== undefined) updateData.lastName = body.lastName;
|
||||
if (body.email !== undefined) updateData.email = body.email;
|
||||
if (body.phone !== undefined) updateData.phone = body.phone;
|
||||
if (body.street !== undefined) updateData.street = body.street;
|
||||
if (body.city !== undefined) updateData.city = body.city;
|
||||
if (body.state !== undefined) updateData.state = body.state;
|
||||
if (body.zip !== undefined) updateData.zip = body.zip;
|
||||
if (body.company !== undefined) updateData.company = body.company;
|
||||
if (body.role !== undefined) updateData.role = body.role;
|
||||
if (body.industry !== undefined) updateData.industry = body.industry;
|
||||
if (body.birthday !== undefined) updateData.birthday = body.birthday ? new Date(body.birthday) : null;
|
||||
if (body.anniversary !== undefined) updateData.anniversary = body.anniversary ? new Date(body.anniversary) : null;
|
||||
if (body.interests !== undefined) updateData.interests = body.interests;
|
||||
if (body.family !== undefined) updateData.family = body.family;
|
||||
if (body.notes !== undefined) updateData.notes = body.notes;
|
||||
if (body.tags !== undefined) updateData.tags = body.tags;
|
||||
|
||||
const [client] = await db.update(clients)
|
||||
.set(updateData)
|
||||
.where(and(eq(clients.id, params.id), eq(clients.userId, user.id)))
|
||||
.returning();
|
||||
|
||||
if (!client) {
|
||||
throw new Error('Client not found');
|
||||
}
|
||||
|
||||
return client;
|
||||
}, {
|
||||
params: t.Object({
|
||||
id: t.String({ format: 'uuid' }),
|
||||
}),
|
||||
body: updateClientSchema,
|
||||
})
|
||||
|
||||
// Delete client
|
||||
.delete('/:id', async ({ params, user }: { params: { id: string }; user: User }) => {
|
||||
const [deleted] = await db.delete(clients)
|
||||
.where(and(eq(clients.id, params.id), eq(clients.userId, user.id)))
|
||||
.returning({ id: clients.id });
|
||||
|
||||
if (!deleted) {
|
||||
throw new Error('Client not found');
|
||||
}
|
||||
|
||||
return { success: true, id: deleted.id };
|
||||
}, {
|
||||
params: t.Object({
|
||||
id: t.String({ format: 'uuid' }),
|
||||
}),
|
||||
})
|
||||
|
||||
// Mark client as contacted
|
||||
.post('/:id/contacted', async ({ params, user }: { params: { id: string }; user: User }) => {
|
||||
const [client] = await db.update(clients)
|
||||
.set({
|
||||
lastContactedAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(and(eq(clients.id, params.id), eq(clients.userId, user.id)))
|
||||
.returning();
|
||||
|
||||
if (!client) {
|
||||
throw new Error('Client not found');
|
||||
}
|
||||
|
||||
return client;
|
||||
}, {
|
||||
params: t.Object({
|
||||
id: t.String({ format: 'uuid' }),
|
||||
}),
|
||||
});
|
||||
269
src/routes/emails.ts
Normal file
269
src/routes/emails.ts
Normal file
@@ -0,0 +1,269 @@
|
||||
import { Elysia, t } from 'elysia';
|
||||
import { db } from '../db';
|
||||
import { clients, communications } from '../db/schema';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import { generateEmail, generateSubject, generateBirthdayMessage, type AIProvider } from '../services/ai';
|
||||
import { sendEmail } from '../services/email';
|
||||
import type { User } from '../lib/auth';
|
||||
|
||||
export const emailRoutes = new Elysia({ prefix: '/emails' })
|
||||
// Generate email for a client
|
||||
.post('/generate', async ({ body, user }: {
|
||||
body: {
|
||||
clientId: string;
|
||||
purpose: string;
|
||||
provider?: AIProvider;
|
||||
};
|
||||
user: User;
|
||||
}) => {
|
||||
// Get client
|
||||
const [client] = await db.select()
|
||||
.from(clients)
|
||||
.where(and(eq(clients.id, body.clientId), eq(clients.userId, user.id)))
|
||||
.limit(1);
|
||||
|
||||
if (!client) {
|
||||
throw new Error('Client not found');
|
||||
}
|
||||
|
||||
// Generate email content
|
||||
const content = await generateEmail({
|
||||
advisorName: user.name,
|
||||
clientName: client.firstName,
|
||||
interests: client.interests || [],
|
||||
notes: client.notes || '',
|
||||
purpose: body.purpose,
|
||||
provider: body.provider,
|
||||
});
|
||||
|
||||
// Generate subject
|
||||
const subject = await generateSubject(body.purpose, client.firstName, body.provider);
|
||||
|
||||
// Save as draft
|
||||
const [communication] = await db.insert(communications)
|
||||
.values({
|
||||
userId: user.id,
|
||||
clientId: client.id,
|
||||
type: 'email',
|
||||
subject,
|
||||
content,
|
||||
aiGenerated: true,
|
||||
aiModel: body.provider || 'anthropic',
|
||||
status: 'draft',
|
||||
})
|
||||
.returning();
|
||||
|
||||
return communication;
|
||||
}, {
|
||||
body: t.Object({
|
||||
clientId: t.String({ format: 'uuid' }),
|
||||
purpose: t.String({ minLength: 1 }),
|
||||
provider: t.Optional(t.Union([t.Literal('anthropic'), t.Literal('openai')])),
|
||||
}),
|
||||
})
|
||||
|
||||
// Generate birthday message
|
||||
.post('/generate-birthday', async ({ body, user }: {
|
||||
body: {
|
||||
clientId: string;
|
||||
provider?: AIProvider;
|
||||
};
|
||||
user: User;
|
||||
}) => {
|
||||
// Get client
|
||||
const [client] = await db.select()
|
||||
.from(clients)
|
||||
.where(and(eq(clients.id, body.clientId), eq(clients.userId, user.id)))
|
||||
.limit(1);
|
||||
|
||||
if (!client) {
|
||||
throw new Error('Client not found');
|
||||
}
|
||||
|
||||
// Calculate years as client
|
||||
const yearsAsClient = Math.floor(
|
||||
(Date.now() - new Date(client.createdAt).getTime()) / (365.25 * 24 * 60 * 60 * 1000)
|
||||
);
|
||||
|
||||
// Generate message
|
||||
const content = await generateBirthdayMessage({
|
||||
clientName: client.firstName,
|
||||
yearsAsClient,
|
||||
interests: client.interests || [],
|
||||
provider: body.provider,
|
||||
});
|
||||
|
||||
// Save as draft
|
||||
const [communication] = await db.insert(communications)
|
||||
.values({
|
||||
userId: user.id,
|
||||
clientId: client.id,
|
||||
type: 'birthday',
|
||||
subject: `Happy Birthday, ${client.firstName}!`,
|
||||
content,
|
||||
aiGenerated: true,
|
||||
aiModel: body.provider || 'anthropic',
|
||||
status: 'draft',
|
||||
})
|
||||
.returning();
|
||||
|
||||
return communication;
|
||||
}, {
|
||||
body: t.Object({
|
||||
clientId: t.String({ format: 'uuid' }),
|
||||
provider: t.Optional(t.Union([t.Literal('anthropic'), t.Literal('openai')])),
|
||||
}),
|
||||
})
|
||||
|
||||
// List emails (drafts and sent)
|
||||
.get('/', async ({ query, user }: {
|
||||
query: { status?: string; clientId?: string };
|
||||
user: User;
|
||||
}) => {
|
||||
let conditions = [eq(communications.userId, user.id)];
|
||||
|
||||
if (query.status) {
|
||||
conditions.push(eq(communications.status, query.status));
|
||||
}
|
||||
|
||||
if (query.clientId) {
|
||||
conditions.push(eq(communications.clientId, query.clientId));
|
||||
}
|
||||
|
||||
const results = await db.select()
|
||||
.from(communications)
|
||||
.where(and(...conditions))
|
||||
.orderBy(communications.createdAt);
|
||||
|
||||
return results;
|
||||
}, {
|
||||
query: t.Object({
|
||||
status: t.Optional(t.String()),
|
||||
clientId: t.Optional(t.String({ format: 'uuid' })),
|
||||
}),
|
||||
})
|
||||
|
||||
// Get single email
|
||||
.get('/:id', async ({ params, user }: { params: { id: string }; user: User }) => {
|
||||
const [email] = await db.select()
|
||||
.from(communications)
|
||||
.where(and(eq(communications.id, params.id), eq(communications.userId, user.id)))
|
||||
.limit(1);
|
||||
|
||||
if (!email) {
|
||||
throw new Error('Email not found');
|
||||
}
|
||||
|
||||
return email;
|
||||
}, {
|
||||
params: t.Object({
|
||||
id: t.String({ format: 'uuid' }),
|
||||
}),
|
||||
})
|
||||
|
||||
// Update email (edit draft)
|
||||
.put('/:id', async ({ params, body, user }: {
|
||||
params: { id: string };
|
||||
body: { subject?: string; content?: string };
|
||||
user: User;
|
||||
}) => {
|
||||
const updateData: Record<string, unknown> = {};
|
||||
if (body.subject !== undefined) updateData.subject = body.subject;
|
||||
if (body.content !== undefined) updateData.content = body.content;
|
||||
|
||||
const [email] = await db.update(communications)
|
||||
.set(updateData)
|
||||
.where(and(
|
||||
eq(communications.id, params.id),
|
||||
eq(communications.userId, user.id),
|
||||
eq(communications.status, 'draft') // Can only edit drafts
|
||||
))
|
||||
.returning();
|
||||
|
||||
if (!email) {
|
||||
throw new Error('Email not found or already sent');
|
||||
}
|
||||
|
||||
return email;
|
||||
}, {
|
||||
params: t.Object({
|
||||
id: t.String({ format: 'uuid' }),
|
||||
}),
|
||||
body: t.Object({
|
||||
subject: t.Optional(t.String()),
|
||||
content: t.Optional(t.String()),
|
||||
}),
|
||||
})
|
||||
|
||||
// Send email
|
||||
.post('/:id/send', async ({ params, user }: { params: { id: string }; user: User }) => {
|
||||
// Get email
|
||||
const [email] = await db.select({
|
||||
email: communications,
|
||||
client: clients,
|
||||
})
|
||||
.from(communications)
|
||||
.innerJoin(clients, eq(communications.clientId, clients.id))
|
||||
.where(and(
|
||||
eq(communications.id, params.id),
|
||||
eq(communications.userId, user.id),
|
||||
eq(communications.status, 'draft')
|
||||
))
|
||||
.limit(1);
|
||||
|
||||
if (!email) {
|
||||
throw new Error('Email not found or already sent');
|
||||
}
|
||||
|
||||
if (!email.client.email) {
|
||||
throw new Error('Client has no email address');
|
||||
}
|
||||
|
||||
// Send via Resend
|
||||
await sendEmail({
|
||||
to: email.client.email,
|
||||
subject: email.email.subject || 'Message from your advisor',
|
||||
content: email.email.content,
|
||||
});
|
||||
|
||||
// Update status
|
||||
const [updated] = await db.update(communications)
|
||||
.set({
|
||||
status: 'sent',
|
||||
sentAt: new Date(),
|
||||
})
|
||||
.where(eq(communications.id, params.id))
|
||||
.returning();
|
||||
|
||||
// Update client's last contacted
|
||||
await db.update(clients)
|
||||
.set({ lastContactedAt: new Date() })
|
||||
.where(eq(clients.id, email.client.id));
|
||||
|
||||
return updated;
|
||||
}, {
|
||||
params: t.Object({
|
||||
id: t.String({ format: 'uuid' }),
|
||||
}),
|
||||
})
|
||||
|
||||
// Delete draft
|
||||
.delete('/:id', async ({ params, user }: { params: { id: string }; user: User }) => {
|
||||
const [deleted] = await db.delete(communications)
|
||||
.where(and(
|
||||
eq(communications.id, params.id),
|
||||
eq(communications.userId, user.id),
|
||||
eq(communications.status, 'draft') // Can only delete drafts
|
||||
))
|
||||
.returning({ id: communications.id });
|
||||
|
||||
if (!deleted) {
|
||||
throw new Error('Email not found or already sent');
|
||||
}
|
||||
|
||||
return { success: true, id: deleted.id };
|
||||
}, {
|
||||
params: t.Object({
|
||||
id: t.String({ format: 'uuid' }),
|
||||
}),
|
||||
});
|
||||
281
src/routes/events.ts
Normal file
281
src/routes/events.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
import { Elysia, t } from 'elysia';
|
||||
import { db } from '../db';
|
||||
import { events, clients } from '../db/schema';
|
||||
import { eq, and, gte, lte, sql } from 'drizzle-orm';
|
||||
import type { User } from '../lib/auth';
|
||||
|
||||
export const eventRoutes = new Elysia({ prefix: '/events' })
|
||||
// List events with optional filters
|
||||
.get('/', async ({ query, user }: {
|
||||
query: {
|
||||
clientId?: string;
|
||||
type?: string;
|
||||
upcoming?: string; // days ahead
|
||||
};
|
||||
user: User;
|
||||
}) => {
|
||||
let conditions = [eq(events.userId, user.id)];
|
||||
|
||||
if (query.clientId) {
|
||||
conditions.push(eq(events.clientId, query.clientId));
|
||||
}
|
||||
|
||||
if (query.type) {
|
||||
conditions.push(eq(events.type, query.type));
|
||||
}
|
||||
|
||||
let results = await db.select({
|
||||
event: events,
|
||||
client: {
|
||||
id: clients.id,
|
||||
firstName: clients.firstName,
|
||||
lastName: clients.lastName,
|
||||
},
|
||||
})
|
||||
.from(events)
|
||||
.innerJoin(clients, eq(events.clientId, clients.id))
|
||||
.where(and(...conditions))
|
||||
.orderBy(events.date);
|
||||
|
||||
// Filter upcoming events if requested
|
||||
if (query.upcoming) {
|
||||
const daysAhead = parseInt(query.upcoming) || 7;
|
||||
const now = new Date();
|
||||
const future = new Date();
|
||||
future.setDate(future.getDate() + daysAhead);
|
||||
|
||||
results = results.filter(r => {
|
||||
const eventDate = new Date(r.event.date);
|
||||
// For recurring events, check if the month/day falls within range
|
||||
if (r.event.recurring) {
|
||||
const thisYear = new Date(
|
||||
now.getFullYear(),
|
||||
eventDate.getMonth(),
|
||||
eventDate.getDate()
|
||||
);
|
||||
const nextYear = new Date(
|
||||
now.getFullYear() + 1,
|
||||
eventDate.getMonth(),
|
||||
eventDate.getDate()
|
||||
);
|
||||
return (thisYear >= now && thisYear <= future) ||
|
||||
(nextYear >= now && nextYear <= future);
|
||||
}
|
||||
return eventDate >= now && eventDate <= future;
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}, {
|
||||
query: t.Object({
|
||||
clientId: t.Optional(t.String({ format: 'uuid' })),
|
||||
type: t.Optional(t.String()),
|
||||
upcoming: t.Optional(t.String()),
|
||||
}),
|
||||
})
|
||||
|
||||
// Get single event
|
||||
.get('/:id', async ({ params, user }: { params: { id: string }; user: User }) => {
|
||||
const [event] = await db.select({
|
||||
event: events,
|
||||
client: {
|
||||
id: clients.id,
|
||||
firstName: clients.firstName,
|
||||
lastName: clients.lastName,
|
||||
},
|
||||
})
|
||||
.from(events)
|
||||
.innerJoin(clients, eq(events.clientId, clients.id))
|
||||
.where(and(eq(events.id, params.id), eq(events.userId, user.id)))
|
||||
.limit(1);
|
||||
|
||||
if (!event) {
|
||||
throw new Error('Event not found');
|
||||
}
|
||||
|
||||
return event;
|
||||
}, {
|
||||
params: t.Object({
|
||||
id: t.String({ format: 'uuid' }),
|
||||
}),
|
||||
})
|
||||
|
||||
// Create event
|
||||
.post('/', async ({ body, user }: {
|
||||
body: {
|
||||
clientId: string;
|
||||
type: string;
|
||||
title: string;
|
||||
date: string;
|
||||
recurring?: boolean;
|
||||
reminderDays?: number;
|
||||
};
|
||||
user: User;
|
||||
}) => {
|
||||
// Verify client belongs to user
|
||||
const [client] = await db.select()
|
||||
.from(clients)
|
||||
.where(and(eq(clients.id, body.clientId), eq(clients.userId, user.id)))
|
||||
.limit(1);
|
||||
|
||||
if (!client) {
|
||||
throw new Error('Client not found');
|
||||
}
|
||||
|
||||
const [event] = await db.insert(events)
|
||||
.values({
|
||||
userId: user.id,
|
||||
clientId: body.clientId,
|
||||
type: body.type,
|
||||
title: body.title,
|
||||
date: new Date(body.date),
|
||||
recurring: body.recurring ?? false,
|
||||
reminderDays: body.reminderDays ?? 7,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return event;
|
||||
}, {
|
||||
body: t.Object({
|
||||
clientId: t.String({ format: 'uuid' }),
|
||||
type: t.String({ minLength: 1 }),
|
||||
title: t.String({ minLength: 1 }),
|
||||
date: t.String(), // ISO date
|
||||
recurring: t.Optional(t.Boolean()),
|
||||
reminderDays: t.Optional(t.Number({ minimum: 0 })),
|
||||
}),
|
||||
})
|
||||
|
||||
// Update event
|
||||
.put('/:id', async ({ params, body, user }: {
|
||||
params: { id: string };
|
||||
body: {
|
||||
type?: string;
|
||||
title?: string;
|
||||
date?: string;
|
||||
recurring?: boolean;
|
||||
reminderDays?: number;
|
||||
};
|
||||
user: User;
|
||||
}) => {
|
||||
const updateData: Record<string, unknown> = {};
|
||||
|
||||
if (body.type !== undefined) updateData.type = body.type;
|
||||
if (body.title !== undefined) updateData.title = body.title;
|
||||
if (body.date !== undefined) updateData.date = new Date(body.date);
|
||||
if (body.recurring !== undefined) updateData.recurring = body.recurring;
|
||||
if (body.reminderDays !== undefined) updateData.reminderDays = body.reminderDays;
|
||||
|
||||
const [event] = await db.update(events)
|
||||
.set(updateData)
|
||||
.where(and(eq(events.id, params.id), eq(events.userId, user.id)))
|
||||
.returning();
|
||||
|
||||
if (!event) {
|
||||
throw new Error('Event not found');
|
||||
}
|
||||
|
||||
return event;
|
||||
}, {
|
||||
params: t.Object({
|
||||
id: t.String({ format: 'uuid' }),
|
||||
}),
|
||||
body: t.Object({
|
||||
type: t.Optional(t.String()),
|
||||
title: t.Optional(t.String()),
|
||||
date: t.Optional(t.String()),
|
||||
recurring: t.Optional(t.Boolean()),
|
||||
reminderDays: t.Optional(t.Number()),
|
||||
}),
|
||||
})
|
||||
|
||||
// Delete event
|
||||
.delete('/:id', async ({ params, user }: { params: { id: string }; user: User }) => {
|
||||
const [deleted] = await db.delete(events)
|
||||
.where(and(eq(events.id, params.id), eq(events.userId, user.id)))
|
||||
.returning({ id: events.id });
|
||||
|
||||
if (!deleted) {
|
||||
throw new Error('Event not found');
|
||||
}
|
||||
|
||||
return { success: true, id: deleted.id };
|
||||
}, {
|
||||
params: t.Object({
|
||||
id: t.String({ format: 'uuid' }),
|
||||
}),
|
||||
})
|
||||
|
||||
// Sync events from client birthdays/anniversaries
|
||||
.post('/sync/:clientId', async ({ params, user }: { params: { clientId: string }; user: User }) => {
|
||||
// Get client
|
||||
const [client] = await db.select()
|
||||
.from(clients)
|
||||
.where(and(eq(clients.id, params.clientId), eq(clients.userId, user.id)))
|
||||
.limit(1);
|
||||
|
||||
if (!client) {
|
||||
throw new Error('Client not found');
|
||||
}
|
||||
|
||||
const created = [];
|
||||
|
||||
// Create birthday event if client has birthday
|
||||
if (client.birthday) {
|
||||
// Check if birthday event already exists
|
||||
const [existing] = await db.select()
|
||||
.from(events)
|
||||
.where(and(
|
||||
eq(events.clientId, client.id),
|
||||
eq(events.type, 'birthday')
|
||||
))
|
||||
.limit(1);
|
||||
|
||||
if (!existing) {
|
||||
const [event] = await db.insert(events)
|
||||
.values({
|
||||
userId: user.id,
|
||||
clientId: client.id,
|
||||
type: 'birthday',
|
||||
title: `${client.firstName}'s Birthday`,
|
||||
date: client.birthday,
|
||||
recurring: true,
|
||||
reminderDays: 7,
|
||||
})
|
||||
.returning();
|
||||
created.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
// Create anniversary event if client has anniversary
|
||||
if (client.anniversary) {
|
||||
const [existing] = await db.select()
|
||||
.from(events)
|
||||
.where(and(
|
||||
eq(events.clientId, client.id),
|
||||
eq(events.type, 'anniversary')
|
||||
))
|
||||
.limit(1);
|
||||
|
||||
if (!existing) {
|
||||
const [event] = await db.insert(events)
|
||||
.values({
|
||||
userId: user.id,
|
||||
clientId: client.id,
|
||||
type: 'anniversary',
|
||||
title: `${client.firstName}'s Anniversary`,
|
||||
date: client.anniversary,
|
||||
recurring: true,
|
||||
reminderDays: 7,
|
||||
})
|
||||
.returning();
|
||||
created.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
return { created };
|
||||
}, {
|
||||
params: t.Object({
|
||||
clientId: t.String({ format: 'uuid' }),
|
||||
}),
|
||||
});
|
||||
109
src/services/ai.ts
Normal file
109
src/services/ai.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { ChatAnthropic } from '@langchain/anthropic';
|
||||
import { ChatPromptTemplate } from '@langchain/core/prompts';
|
||||
import { StringOutputParser } from '@langchain/core/output_parsers';
|
||||
|
||||
export type AIProvider = 'anthropic' | 'openai';
|
||||
|
||||
// Get model based on provider
|
||||
function getModel(provider: AIProvider = 'anthropic') {
|
||||
if (provider === 'anthropic') {
|
||||
return new ChatAnthropic({
|
||||
modelName: 'claude-sonnet-4-20250514',
|
||||
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
|
||||
});
|
||||
}
|
||||
|
||||
// Add OpenAI support later
|
||||
throw new Error(`Provider ${provider} not yet supported`);
|
||||
}
|
||||
|
||||
// Email generation prompt
|
||||
const emailPrompt = ChatPromptTemplate.fromMessages([
|
||||
['system', `You are a professional wealth advisor writing to a valued client.
|
||||
Maintain a warm but professional tone. Incorporate personal details naturally.
|
||||
Keep emails concise (3-4 paragraphs max).
|
||||
Do not include subject line - just the body.`],
|
||||
['human', `Advisor: {advisorName}
|
||||
Client: {clientName}
|
||||
Their interests: {interests}
|
||||
Recent notes: {notes}
|
||||
Purpose: {purpose}
|
||||
|
||||
Generate a personalized email that feels genuine, not templated.`],
|
||||
]);
|
||||
|
||||
export interface GenerateEmailParams {
|
||||
advisorName: string;
|
||||
clientName: string;
|
||||
interests: string[];
|
||||
notes: string;
|
||||
purpose: string;
|
||||
provider?: AIProvider;
|
||||
}
|
||||
|
||||
export async function generateEmail(params: GenerateEmailParams): Promise<string> {
|
||||
const model = getModel(params.provider);
|
||||
const parser = new StringOutputParser();
|
||||
const chain = emailPrompt.pipe(model).pipe(parser);
|
||||
|
||||
const response = await chain.invoke({
|
||||
advisorName: params.advisorName,
|
||||
clientName: params.clientName,
|
||||
interests: params.interests.join(', ') || 'not specified',
|
||||
notes: params.notes || 'No recent notes',
|
||||
purpose: params.purpose,
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// Birthday message generation
|
||||
const birthdayPrompt = ChatPromptTemplate.fromMessages([
|
||||
['system', `Generate a thoughtful birthday message from a wealth advisor to their client.
|
||||
Should feel personal, not generic. Keep it brief (2-3 sentences) and sincere.`],
|
||||
['human', `Client: {clientName}
|
||||
Years as client: {yearsAsClient}
|
||||
Interests: {interests}
|
||||
|
||||
Generate a warm birthday message.`],
|
||||
]);
|
||||
|
||||
export interface GenerateBirthdayMessageParams {
|
||||
clientName: string;
|
||||
yearsAsClient: number;
|
||||
interests: string[];
|
||||
provider?: AIProvider;
|
||||
}
|
||||
|
||||
export async function generateBirthdayMessage(params: GenerateBirthdayMessageParams): Promise<string> {
|
||||
const model = getModel(params.provider);
|
||||
const parser = new StringOutputParser();
|
||||
const chain = birthdayPrompt.pipe(model).pipe(parser);
|
||||
|
||||
const response = await chain.invoke({
|
||||
clientName: params.clientName,
|
||||
yearsAsClient: params.yearsAsClient.toString(),
|
||||
interests: params.interests.join(', ') || 'not specified',
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// Email subject generation
|
||||
const subjectPrompt = ChatPromptTemplate.fromMessages([
|
||||
['system', `Generate a professional but warm email subject line for a wealth advisor's email.
|
||||
Keep it short (under 50 characters). Do not use quotes.`],
|
||||
['human', `Purpose: {purpose}
|
||||
Client name: {clientName}
|
||||
|
||||
Generate just the subject line, nothing else.`],
|
||||
]);
|
||||
|
||||
export async function generateSubject(purpose: string, clientName: string, provider?: AIProvider): Promise<string> {
|
||||
const model = getModel(provider);
|
||||
const parser = new StringOutputParser();
|
||||
const chain = subjectPrompt.pipe(model).pipe(parser);
|
||||
|
||||
const response = await chain.invoke({ purpose, clientName });
|
||||
return response.trim();
|
||||
}
|
||||
27
src/services/email.ts
Normal file
27
src/services/email.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Resend } from 'resend';
|
||||
|
||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||
|
||||
export interface SendEmailParams {
|
||||
to: string;
|
||||
subject: string;
|
||||
content: string;
|
||||
from?: string;
|
||||
replyTo?: string;
|
||||
}
|
||||
|
||||
export async function sendEmail(params: SendEmailParams) {
|
||||
const { data, error } = await resend.emails.send({
|
||||
from: params.from || process.env.DEFAULT_FROM_EMAIL || 'onboarding@resend.dev',
|
||||
to: params.to,
|
||||
subject: params.subject,
|
||||
text: params.content,
|
||||
replyTo: params.replyTo,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Failed to send email: ${error.message}`);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
28
src/types/index.ts
Normal file
28
src/types/index.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { InferSelectModel, InferInsertModel } from 'drizzle-orm';
|
||||
import type { clients, events, communications, users } from '../db/schema';
|
||||
|
||||
// Database types
|
||||
export type User = InferSelectModel<typeof users>;
|
||||
export type NewUser = InferInsertModel<typeof users>;
|
||||
|
||||
export type Client = InferSelectModel<typeof clients>;
|
||||
export type NewClient = InferInsertModel<typeof clients>;
|
||||
|
||||
export type Event = InferSelectModel<typeof events>;
|
||||
export type NewEvent = InferInsertModel<typeof events>;
|
||||
|
||||
export type Communication = InferSelectModel<typeof communications>;
|
||||
export type NewCommunication = InferInsertModel<typeof communications>;
|
||||
|
||||
// API types
|
||||
export interface ApiResponse<T> {
|
||||
data?: T;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
29
tsconfig.json
Normal file
29
tsconfig.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user