Hetansh Waghela commited on
Commit
f69e867
·
1 Parent(s): 70299ab

feat: Full Clerk Google OAuth + user profiles + combined API/Worker deploy

Browse files

- Pull remote hero page redesign with AutopilotSimulator
- Integrate Clerk auth into new wide layout (maxWidth: 1200)
- Add conditional hero CTAs: Get Started (sign in) vs Start Mission
- Create AuthTokenProvider for Clerk JWT in API requests
- Add email, avatarUrl, phone, fullName to users table (migration 0011)
- Create AuthModule with ClerkAuthGuard, CurrentUser decorator, AuthService
- Auto-create/link users on first API call via Google OAuth
- Scope all missions to authenticated user (no more demo user)
- Create UsersModule with /users/me, /users/me/missions endpoints
- Create Sarkari Calm profile page with editable phone/language
- Combine API + Inngest Worker in one HF Spaces container
- Update Dockerfile.hf for combined deployment
- Add DEMO_MODE env flag for local dev without Clerk keys
- Verify pnpm build + pnpm typecheck pass

.mcp.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "mcpServers": {
3
+ "shadcn": {
4
+ "command": "npx",
5
+ "args": [
6
+ "shadcn@latest",
7
+ "mcp"
8
+ ]
9
+ }
10
+ }
11
+ }
.playwright-mcp/page-2026-05-29T16-34-02-219Z.yml ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ - generic [active] [ref=e1]:
2
+ - banner [ref=e2]:
3
+ - generic [ref=e3]:
4
+ - link "CourtMitra MissionOS" [ref=e4] [cursor=pointer]:
5
+ - /url: /en
6
+ - generic [ref=e5]: CourtMitra
7
+ - generic [ref=e6]: MissionOS
8
+ - navigation [ref=e7]:
9
+ - link "Ledger" [ref=e8] [cursor=pointer]:
10
+ - /url: /en/ledger
11
+ - generic "Language switcher" [ref=e10]:
12
+ - button "EN" [ref=e11] [cursor=pointer]
13
+ - button "हिंदी" [ref=e12] [cursor=pointer]
14
+ - main [ref=e13]:
15
+ - generic [ref=e14]:
16
+ - generic [ref=e16]:
17
+ - generic [ref=e17]: CourtMitra · Phase 1
18
+ - heading "Fight for your rights — automatically" [level=1] [ref=e18]
19
+ - paragraph [ref=e19]: Describe your problem once. CourtMitra builds the evidence file, finds the right authority, drafts and sends the complaint, and follows up until you get a resolution.
20
+ - generic [ref=e20]:
21
+ - button "Start a new mission" [ref=e21] [cursor=pointer]
22
+ - link "See what others resolved" [ref=e22] [cursor=pointer]:
23
+ - /url: /en/ledger
24
+ - generic [ref=e23]:
25
+ - generic [ref=e24]:
26
+ - heading "My Missions" [level=2] [ref=e25]
27
+ - button "+ Start a Mission" [ref=e26] [cursor=pointer]
28
+ - generic [ref=e27]:
29
+ - textbox "Search missions…" [ref=e28]
30
+ - generic [ref=e29]:
31
+ - button "All" [ref=e30] [cursor=pointer]
32
+ - button "consumer" [ref=e31] [cursor=pointer]
33
+ - button "payment" [ref=e32] [cursor=pointer]
34
+ - button "cyber fraud" [ref=e33] [cursor=pointer]
35
+ - region "Notifications alt+T"
.playwright-mcp/page-2026-05-29T16-34-44-022Z.yml ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ - generic [active] [ref=e1]:
2
+ - banner [ref=e2]:
3
+ - generic [ref=e3]:
4
+ - link "CourtMitra MissionOS" [ref=e4] [cursor=pointer]:
5
+ - /url: /en
6
+ - generic [ref=e5]: CourtMitra
7
+ - generic [ref=e6]: MissionOS
8
+ - navigation [ref=e7]:
9
+ - link "Ledger" [ref=e8] [cursor=pointer]:
10
+ - /url: /en/ledger
11
+ - generic "Language switcher" [ref=e10]:
12
+ - button "EN" [ref=e11] [cursor=pointer]
13
+ - button "हिंदी" [ref=e12] [cursor=pointer]
14
+ - main [ref=e13]:
15
+ - generic [ref=e14]:
16
+ - generic [ref=e15]:
17
+ - generic [ref=e16]:
18
+ - link "← All missions" [ref=e17] [cursor=pointer]:
19
+ - /url: /
20
+ - heading "Test smoke mission" [level=1] [ref=e18]
21
+ - generic [ref=e19]:
22
+ - generic [ref=e20]: consumer
23
+ - generic [ref=e21]: created
24
+ - generic [ref=e22]:
25
+ - button "⬤ Timeline 1" [ref=e23] [cursor=pointer]:
26
+ - generic [ref=e24]: ⬤
27
+ - text: Timeline
28
+ - generic [ref=e25]: "1"
29
+ - button "Refresh" [ref=e26] [cursor=pointer]
30
+ - navigation "Journey progress" [ref=e28]:
31
+ - button "1 Describe" [ref=e30] [cursor=pointer]:
32
+ - generic [ref=e31]: "1"
33
+ - generic [ref=e32]: Describe
34
+ - button "2 Facts" [disabled] [ref=e35]:
35
+ - generic [ref=e36]: "2"
36
+ - generic [ref=e37]: Facts
37
+ - button "3 Route" [disabled] [ref=e40]:
38
+ - generic [ref=e41]: "3"
39
+ - generic [ref=e42]: Route
40
+ - button "4 Action" [disabled] [ref=e45]:
41
+ - generic [ref=e46]: "4"
42
+ - generic [ref=e47]: Action
43
+ - button "5 Follow-up" [disabled] [ref=e50]:
44
+ - generic [ref=e51]: "5"
45
+ - generic [ref=e52]: Follow-up
46
+ - button "6 Outcome" [disabled] [ref=e55]:
47
+ - generic [ref=e56]: "6"
48
+ - generic [ref=e57]: Outcome
49
+ - generic [ref=e59]:
50
+ - generic [ref=e61]: Mission created — let's gather the details.
51
+ - generic [ref=e62]:
52
+ - generic [ref=e63]: Your next step
53
+ - text: Describe what happened and upload any evidence.
54
+ - generic [ref=e64]:
55
+ - generic [ref=e65]: "System will:"
56
+ - text: Run intake to structure your grievance.
57
+ - generic [ref=e66]:
58
+ - tablist [ref=e67]:
59
+ - tab "Describe" [selected] [ref=e68] [cursor=pointer]
60
+ - tab "Facts" [ref=e69] [cursor=pointer]
61
+ - tab "Route" [ref=e70] [cursor=pointer]
62
+ - tab "Action" [ref=e71] [cursor=pointer]
63
+ - tab "Follow-up" [ref=e72] [cursor=pointer]
64
+ - tab "Outcome" [ref=e73] [cursor=pointer]
65
+ - tabpanel "Describe" [ref=e74]:
66
+ - heading "Describe your problem" [level=2] [ref=e75]
67
+ - paragraph [ref=e76]: Tell us what happened in your own words.
68
+ - generic [ref=e77]:
69
+ - generic [ref=e78]:
70
+ - generic [ref=e79]: Title
71
+ - generic [ref=e80]: Test smoke mission
72
+ - generic [ref=e82]:
73
+ - generic [ref=e83]: Domain
74
+ - generic [ref=e84]: consumer
75
+ - generic [ref=e85]: Created 29 May 2026, 6:26 pm
76
+ - generic [ref=e86]: Legal information & workflow assistance only — not a lawyer-client relationship. CourtMitra does not guarantee outcomes.
77
+ - region "Notifications alt+T"
78
+ - alert [ref=e87]
.playwright-mcp/page-2026-05-29T16-35-07-854Z.yml ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ - generic [active] [ref=e1]:
2
+ - banner [ref=e2]:
3
+ - generic [ref=e3]:
4
+ - link "CourtMitra MissionOS" [ref=e4] [cursor=pointer]:
5
+ - /url: /hi
6
+ - generic [ref=e5]: CourtMitra
7
+ - generic [ref=e6]: MissionOS
8
+ - navigation [ref=e7]:
9
+ - link "समाधान खाता" [ref=e8] [cursor=pointer]:
10
+ - /url: /hi/ledger
11
+ - generic "Language switcher" [ref=e10]:
12
+ - button "EN" [ref=e11] [cursor=pointer]
13
+ - button "हिंदी" [ref=e12] [cursor=pointer]
14
+ - main [ref=e13]:
15
+ - generic [ref=e14]:
16
+ - generic [ref=e15]:
17
+ - generic [ref=e16]:
18
+ - link "← All missions" [ref=e17] [cursor=pointer]:
19
+ - /url: /
20
+ - heading "Test smoke mission" [level=1] [ref=e18]
21
+ - generic [ref=e19]:
22
+ - generic [ref=e20]: consumer
23
+ - generic [ref=e21]: created
24
+ - generic [ref=e22]:
25
+ - button "⬤ समयरेखा 1" [ref=e23] [cursor=pointer]:
26
+ - generic [ref=e24]: ⬤
27
+ - text: समयरेखा
28
+ - generic [ref=e25]: "1"
29
+ - button "Refresh" [ref=e26] [cursor=pointer]
30
+ - navigation "Journey progress" [ref=e28]:
31
+ - button "1 विवरण" [ref=e30] [cursor=pointer]:
32
+ - generic [ref=e31]: "1"
33
+ - generic [ref=e32]: विवरण
34
+ - button "2 तथ्य" [disabled] [ref=e35]:
35
+ - generic [ref=e36]: "2"
36
+ - generic [ref=e37]: तथ्य
37
+ - button "3 मार्ग" [disabled] [ref=e40]:
38
+ - generic [ref=e41]: "3"
39
+ - generic [ref=e42]: मार्ग
40
+ - button "4 कार्रवाई" [disabled] [ref=e45]:
41
+ - generic [ref=e46]: "4"
42
+ - generic [ref=e47]: कार्रवाई
43
+ - button "5 अनुवर्ती" [disabled] [ref=e50]:
44
+ - generic [ref=e51]: "5"
45
+ - generic [ref=e52]: अनुवर्ती
46
+ - button "6 परिणाम" [disabled] [ref=e55]:
47
+ - generic [ref=e56]: "6"
48
+ - generic [ref=e57]: परिणाम
49
+ - generic [ref=e59]:
50
+ - generic [ref=e61]: Mission created — let's gather the details.
51
+ - generic [ref=e62]:
52
+ - generic [ref=e63]: Your next step
53
+ - text: Describe what happened and upload any evidence.
54
+ - generic [ref=e64]:
55
+ - generic [ref=e65]: "System will:"
56
+ - text: Run intake to structure your grievance.
57
+ - generic [ref=e66]:
58
+ - tablist [ref=e67]:
59
+ - tab "विवरण" [selected] [ref=e68] [cursor=pointer]
60
+ - tab "तथ्य" [ref=e69] [cursor=pointer]
61
+ - tab "मार्ग" [ref=e70] [cursor=pointer]
62
+ - tab "कार्रवाई" [ref=e71] [cursor=pointer]
63
+ - tab "अनुवर्ती" [ref=e72] [cursor=pointer]
64
+ - tab "परिणाम" [ref=e73] [cursor=pointer]
65
+ - tabpanel "विवरण" [ref=e74]:
66
+ - heading "अपनी समस्या बताएं" [level=2] [ref=e75]
67
+ - paragraph [ref=e76]: हमें बताएं कि क्या हुआ, अपने शब्दों में।
68
+ - generic [ref=e77]:
69
+ - generic [ref=e78]:
70
+ - generic [ref=e79]: Title
71
+ - generic [ref=e80]: Test smoke mission
72
+ - generic [ref=e82]:
73
+ - generic [ref=e83]: Domain
74
+ - generic [ref=e84]: consumer
75
+ - generic [ref=e85]: Created 29 May 2026, 6:26 pm
76
+ - generic [ref=e86]: केवल कानूनी जानकारी और कार्यप्रवाह सहायता — यह वकील-मुवक्किल संबंध नहीं है। कोर्टमित्र परिणामों की गारंटी नहीं देता।
77
+ - region "Notifications alt+T"
78
+ - alert [ref=e87]
DEPLOY.md ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Deployment Guide — CourtMitra MissionOS
2
+
3
+ > **Target:** Vercel (frontend) + Hugging Face Spaces (backend + worker) + Google OAuth (Clerk) + Neon (Postgres)
4
+
5
+ ---
6
+
7
+ ## Honest Time Estimate
8
+
9
+ | Milestone | Time |
10
+ |---|---|
11
+ | Clerk setup (Google OAuth) + env vars | **10 min** |
12
+ | Neon Postgres + run migrations | **10 min** |
13
+ | Frontend on Vercel | **5 min** |
14
+ | API + Worker on Hugging Face Spaces | **20–30 min** |
15
+ | Wire frontend → backend + smoke test | **10 min** |
16
+ | **Running frontend + API + Worker + DB + Google Auth** | **~1 hour** (if everything goes smoothly) |
17
+
18
+ ---
19
+
20
+ ## Prerequisites
21
+
22
+ - GitHub repo pushed and public (or HF Pro for private Spaces)
23
+ - Accounts (all have generous free tiers):
24
+ - [Vercel](https://vercel.com)
25
+ - [Clerk](https://clerk.com)
26
+ - [Hugging Face](https://huggingface.co)
27
+ - [Neon](https://neon.tech)
28
+ - [Inngest](https://inngest.com) (optional — for background workflows)
29
+ - OpenRouter API key (or OpenCode / OpenAI key) for LLM calls
30
+
31
+ ---
32
+
33
+ ## Step 1: Clerk + Google OAuth (10 min)
34
+
35
+ 1. Go to [dashboard.clerk.com](https://dashboard.clerk.com) → Create Application.
36
+ 2. In the **Social Connections** section, enable **Google**.
37
+ - For development, use Clerk's shared OAuth credentials (just toggle it on).
38
+ - For production, add your own Google Client ID/Secret.
39
+ 3. Copy your **Publishable Key** and **Secret Key** from Clerk Dashboard → API Keys.
40
+ 4. Set these in Vercel (Step 3) and locally:
41
+ ```
42
+ NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
43
+ CLERK_SECRET_KEY=sk_test_...
44
+ NEXT_PUBLIC_CLERK_SIGN_IN_URL=/en/sign-in
45
+ NEXT_PUBLIC_CLERK_SIGN_UP_URL=/en/sign-up
46
+ ```
47
+ 5. That's it — the frontend has Clerk wired with `SignInButton`, `UserButton`, and `<Show when="signed-in">` components.
48
+ 6. **Backend Clerk verification:** The API verifies Clerk JWTs from the `Authorization: Bearer <token>` header. On first API call, it auto-creates a user in the DB and links their Google identity.
49
+
50
+ ---
51
+
52
+ ## Step 2: Neon Postgres (10 min)
53
+
54
+ 1. Go to [neon.tech](https://neon.tech) → New Project → copy the **connection string**.
55
+ 2. Save it as `DATABASE_URL` and `DIRECT_DATABASE_URL`.
56
+ 3. **Run migrations** against the Neon DB:
57
+ ```bash
58
+ # At repo root
59
+ export DATABASE_URL="postgres://..."
60
+ export DIRECT_DATABASE_URL="postgres://..."
61
+ pnpm db:migrate
62
+ pnpm db:seed
63
+ ```
64
+
65
+ ---
66
+
67
+ ## Step 3: Frontend → Vercel (5 min)
68
+
69
+ 1. Push this repo to GitHub.
70
+ 2. In Vercel → **Add New Project** → import the repo.
71
+ 3. Set these **Environment Variables** in the Vercel dashboard:
72
+ ```
73
+ NEXT_PUBLIC_API_BASE_URL=https://your-hf-space.hf.space/api
74
+ NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
75
+ CLERK_SECRET_KEY=sk_test_...
76
+ NEXT_PUBLIC_CLERK_SIGN_IN_URL=/en/sign-in
77
+ NEXT_PUBLIC_CLERK_SIGN_UP_URL=/en/sign-up
78
+ ```
79
+ 4. Deploy. Vercel will auto-detect Next.js in `apps/web`.
80
+
81
+ > **Tip:** `next.config.mjs` already has `output: "standalone"`, so Vercel optimizes automatically.
82
+
83
+ ---
84
+
85
+ ## Step 4: Backend → Hugging Face Spaces (25 min)
86
+
87
+ ### 4a. Create the Space
88
+
89
+ 1. Go to [huggingface.co/spaces](https://huggingface.co/spaces) → **Create new Space**.
90
+ 2. **Space name:** `courtmitra-api`
91
+ 3. **SDK:** `Docker`
92
+ 4. **Visibility:** Public (free tier)
93
+ 5. **Hardware:** Free CPU
94
+
95
+ ### 4b. Deploy the Dockerfile
96
+
97
+ The repo root has `Dockerfile.hf` which builds the monorepo and runs the NestJS API + Inngest Worker in ONE container.
98
+
99
+ Push it to HF (or link your GitHub repo in HF Space settings):
100
+
101
+ ```bash
102
+ git add Dockerfile.hf
103
+ git commit -m "Add HF Spaces Dockerfile (API + Worker combined)"
104
+ git push origin main
105
+ ```
106
+
107
+ Then in your HF Space → **Settings** → **Repository** → link your GitHub repo.
108
+
109
+ ### 4c. Set Environment Variables
110
+
111
+ In your HF Space → **Settings** → **Variables & Secrets**:
112
+
113
+ ```
114
+ PORT=7860
115
+ API_PORT=7860
116
+ DATABASE_URL=postgres://... (Neon)
117
+ DIRECT_DATABASE_URL=postgres://... (Neon)
118
+ AI_PROVIDER=openrouter
119
+ OPENROUTER_API_KEY=sk-or-v1-...
120
+ AI_MODEL=google/gemini-2.0-flash-exp:free
121
+ AI_VISION_MODEL=google/gemini-2.0-flash-exp:free
122
+ APP_BASE_URL=https://your-hf-space.hf.space
123
+ API_BASE_URL=https://your-hf-space.hf.space
124
+ WEBHOOK_BASE_URL=https://your-hf-space.hf.space
125
+ LOCAL_STORAGE_DIR=/app/.storage
126
+ JWT_SECRET=hackathon-jwt-secret-change-me
127
+ PII_HASH_SALT=hackathon-salt
128
+ TOKEN_ENCRYPTION_KEY=hackathon-enc-key-min-32-chars!!
129
+ CLERK_SECRET_KEY=sk_test_...
130
+ CLERK_JWT_KEY=... (optional — for networkless JWT verification)
131
+ ```
132
+
133
+ Optional (for email actions):
134
+ ```
135
+ RESEND_API_KEY=re_...
136
+ RESEND_FROM=noreply@yourdomain.com
137
+ ```
138
+
139
+ Optional (for Inngest background workflows):
140
+ ```
141
+ INNGEST_EVENT_KEY=... (from Inngest Cloud)
142
+ INNGEST_SIGNING_KEY=... (from Inngest Cloud)
143
+ INNGEST_BASE_URL=https://api.inngest.com
144
+ ```
145
+
146
+ Optional (for local dev without Clerk):
147
+ ```
148
+ DEMO_MODE=true
149
+ ```
150
+
151
+ ### 4d. Verify the API
152
+
153
+ Once the Space builds, visit:
154
+ - `https://your-space.hf.space/api/admin/health` → should return `{"status":"ok"}`
155
+ - `https://your-space.hf.space/api/inngest` → should return the Inngest registration JSON
156
+
157
+ ---
158
+
159
+ ## Step 5: Wire Frontend → Backend
160
+
161
+ Update your Vercel environment variable:
162
+ ```
163
+ NEXT_PUBLIC_API_BASE_URL=https://your-hf-space.hf.space/api
164
+ ```
165
+ Redeploy the frontend (or Vercel will auto-redeploy on save).
166
+
167
+ ---
168
+
169
+ ## Architecture (Combined Container)
170
+
171
+ ```
172
+ ┌─────────────────────────────────────────────────┐
173
+ │ Hugging Face Spaces — Port 7860 │
174
+ │ ┌─────────────────────────────────────────┐ │
175
+ │ │ Fastify HTTP Server (NestJS) │ │
176
+ │ │ ┌─────────────┐ ┌────────────────┐ │ │
177
+ │ │ │ API Routes │ │ /api/inngest │ │ │
178
+ │ │ │ /api/* │ │ (Worker Fns) │ │ │
179
+ │ │ │ │ │ │ │ │
180
+ │ │ │ - missions │ │ - intake │ │ │
181
+ │ │ │ - actions │ │ - evidence │ │ │
182
+ │ │ │ - users │ │ - route-plan │ │ │
183
+ │ │ │ - evidence │ │ - execute │ │ │
184
+ │ │ └─────────────┘ └────────────────┘ │ │
185
+ │ └─────────────────────────────────────────┘ │
186
+ │ ↓ │
187
+ │ Neon Postgres (Cloud) │
188
+ └─────────────────────────────────────────────────┘
189
+ ```
190
+
191
+ ---
192
+
193
+ ## Quick Smoke Test
194
+
195
+ After both are deployed:
196
+
197
+ 1. Open your Vercel URL → click **Sign In** → choose **Google**.
198
+ 2. After signing in, you should see your avatar in the top-right corner.
199
+ 3. Click **Get Started** (hero CTA) → describe a problem.
200
+ 4. The API call should reach HF Spaces, create a user in the DB, and create a mission.
201
+ 5. Visit `/{locale}/profile` → you should see your profile and the mission you just created.
202
+ 6. Check `https://your-hf-space.hf.space/api/admin/health` to confirm backend is alive.
203
+
204
+ ---
205
+
206
+ ## New Features
207
+
208
+ | Feature | Status |
209
+ |---|---|
210
+ | Google OAuth via Clerk | ✅ Frontend + Backend verified |
211
+ | User auto-creation on first API call | ✅ Creates `users` + `userIdentities` rows |
212
+ | Profile page (`/{locale}/profile`) | ✅ Shows avatar, name, email, editable phone + language |
213
+ | My Missions list on profile | ✅ Lists user's own missions |
214
+ | Mission ownership | ✅ Users only see their own missions |
215
+ | Combined API + Worker container | ✅ One HF Space serves both |
216
+ | Demo mode | ✅ `DEMO_MODE=true` bypasses auth for local dev |
217
+
218
+ ---
219
+
220
+ ## Known Limitations for Hackathon Demo
221
+
222
+ | Feature | Status in 1-hour deploy |
223
+ |---|---|
224
+ | Frontend + Google OAuth on Vercel | ✅ Works |
225
+ | API + Worker + DB on HF + Neon | ✅ Works |
226
+ | Mission CRUD (user-scoped) | ✅ Works |
227
+ | Evidence upload | ✅ Works (if storage configured) |
228
+ | Background AI workflows | ⚠️ Needs Inngest Cloud keys for production webhooks |
229
+ | Email sending via Resend | ⚠️ Needs Resend key + verified domain |
230
+ | WhatsApp / Telegram channels | ❌ Needs Meta Business verification (days) |
231
+ | Portal automation / eCourts | ❌ Phase 2 only |
232
+
233
+ ---
234
+
235
+ ## Troubleshooting
236
+
237
+ **HF Space build fails:**
238
+ - Check **Factory Logs** in HF. If `pnpm` is missing, the Dockerfile installs it via `corepack`.
239
+ - If out of disk: HF free tier has ~50GB disk. The Dockerfile removes frontend packages (`next`, `react`) from the API deploy to save space.
240
+
241
+ **API returns 401 Unauthorized:**
242
+ - Make sure `CLERK_SECRET_KEY` is set in HF Space env vars.
243
+ - Make sure the frontend passes `Authorization: Bearer <token>` header. Check browser DevTools Network tab.
244
+
245
+ **API returns 500:**
246
+ - Verify `DATABASE_URL` is set and migrations ran.
247
+ - Verify `OPENROUTER_API_KEY` is set (some endpoints call LLMs).
248
+
249
+ **CORS errors from frontend:**
250
+ - The API has `app.enableCors({ origin: true })` which reflects the request origin. If blocked, add your Vercel domain to `origin: ["https://your-vercel.app"]` in `apps/api/src/main.ts`.
251
+
252
+ **Clerk sign-in fails:**
253
+ - Make sure `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY` are set in Vercel.
254
+ - In Clerk Dashboard → **Social Connections** → **Google**, ensure it's enabled.
255
+ - Add your Vercel domain to Clerk Dashboard → **URLs** → **Allowed Origins**.
256
+
257
+ **Inngest events not firing:**
258
+ - Make sure `INNGEST_EVENT_KEY` and `INNGEST_SIGNING_KEY` are correct.
259
+ - Check the Inngest Cloud dashboard for failed runs.
260
+ - HF free-tier Spaces sleep after inactivity. Use a paid Space or keep-alive ping for reliable background processing.
261
+
262
+ ---
263
+
264
+ ## Local Dev
265
+
266
+ ```bash
267
+ # 1. Start local Postgres + Inngest
268
+ pnpm infra:up
269
+
270
+ # 2. Copy env and fill in keys
271
+ cp infra/.env.example .env
272
+
273
+ # 3. Run everything
274
+ pnpm dev
275
+ ```
276
+
277
+ - Web: http://localhost:3000
278
+ - API: http://localhost:3001/api
279
+ - Inngest Dev Server: http://localhost:8288
280
+
281
+ **Note:** For local dev without Clerk, set `DEMO_MODE=true` in `.env`. The API will skip JWT verification and use a demo user.
282
+
283
+ ---
284
+
285
+ *Good luck at the hackathon! 🚀*
Dockerfile.hf ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CourtMitra API + Worker — NestJS + Fastify + Inngest on Hugging Face Spaces
2
+ # HF expects a web service on PORT (default 7860).
3
+ # This container runs BOTH the API and the Inngest worker in one process.
4
+
5
+ # ── Stage 1: builder ────────────────────────────────────────────────────────
6
+ FROM node:23-alpine AS builder
7
+ RUN corepack enable && corepack prepare pnpm@10 --activate
8
+ WORKDIR /app
9
+
10
+ # Workspace manifests
11
+ COPY pnpm-lock.yaml pnpm-workspace.yaml package.json tsconfig.base.json ./
12
+ COPY apps/api/package.json apps/api/
13
+ COPY apps/worker/package.json apps/worker/
14
+ COPY packages/adapters/package.json packages/adapters/
15
+ COPY packages/contracts/package.json packages/contracts/
16
+ COPY packages/db/package.json packages/db/
17
+ COPY packages/domain/package.json packages/domain/
18
+ COPY packages/ports/package.json packages/ports/
19
+ COPY packages/prompts/package.json packages/prompts/
20
+
21
+ RUN pnpm install --frozen-lockfile
22
+
23
+ # Source — API + Worker + all packages
24
+ COPY apps/api/src apps/api/src
25
+ COPY apps/worker/src apps/worker/src
26
+ COPY packages/adapters/src packages/adapters/src
27
+ COPY packages/contracts/src packages/contracts/src
28
+ COPY packages/db/src packages/db/src
29
+ COPY packages/domain/src packages/domain/src
30
+ COPY packages/ports/src packages/ports/src
31
+ COPY packages/prompts/src packages/prompts/src
32
+ COPY packages/db/drizzle.config.ts packages/db/drizzle.config.ts
33
+ COPY apps/api/tsconfig.json apps/api/tsconfig.json
34
+ COPY apps/worker/tsconfig.json apps/worker/tsconfig.json
35
+
36
+ # Deploy flat production node_modules for the API
37
+ RUN pnpm --filter @courtmitra/api deploy --prod --legacy /deploy
38
+
39
+ # Remove leaked frontend-only packages to save space
40
+ RUN rm -rf /deploy/node_modules/.pnpm/next@* \
41
+ /deploy/node_modules/.pnpm/@next+* \
42
+ /deploy/node_modules/.pnpm/typescript@* \
43
+ /deploy/node_modules/.pnpm/caniuse-lite@* \
44
+ /deploy/node_modules/.pnpm/react@* \
45
+ /deploy/node_modules/.pnpm/react-dom@* \
46
+ /deploy/node_modules/.pnpm/web-streams-polyfill@*
47
+
48
+ # ── Stage 2: runner ─────────────────────────────────────────────────────────
49
+ FROM node:23-alpine AS runner
50
+ RUN apk add --no-cache tini curl
51
+ WORKDIR /app
52
+
53
+ COPY --from=builder /deploy ./
54
+
55
+ # HF provides PORT env var (default 7860)
56
+ EXPOSE 7860
57
+ ENV API_PORT=7860
58
+ ENV PORT=7860
59
+
60
+ # Healthcheck for HF
61
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
62
+ CMD curl -f http://localhost:7860/api/admin/health || exit 1
63
+
64
+ ENTRYPOINT ["/sbin/tini", "--"]
65
+ CMD ["npx", "tsx", "src/main.ts"]
GAP_AUDIT_M1_M9.md ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CourtMitra M1-M9 Gap Audit
2
+
3
+ ## Audit Date: 2026-05-29
4
+ ## Commit Under Review: f892836 "Implement CourtMitra autopilot roadmap"
5
+
6
+ ---
7
+
8
+ ### M0 — Core Automation Loop
9
+ **Status:** Mostly complete. Intake, evidence, facts, legal grounding, route plan, action preview/consent, email send, reply ingestion, follow-up, resolution, ledger all have code paths. Tests green.
10
+
11
+ ---
12
+
13
+ ### M1 — Rate-Limit & Action Executor Idempotency
14
+ **Current:**
15
+ - `checkRateLimit` lives in `action-execution-policy.ts` and is called from `executeAction`.
16
+ - Source is `submissions` table with `status = "sent"` in a rolling window.
17
+
18
+ **Gaps / Bugs:**
19
+ 1. **Single source of truth risk.** The comment says "counted from `submissions`/`action_attempts`", but only `submissions` is queried. If the post-send DB transaction rolls back, the email was sent but `submissions` row is missing → under-count on retry.
20
+ 2. **Idempotency gap.** `canExecute` allows `status === "executing"`. If the DB transaction fails after `emailAdapter.submit` succeeds, Inngest retries and the email is sent again because nothing blocks a second execution of an "executing" action.
21
+ 3. **Env vars undocumented.** `ACTION_RATE_WINDOW_MINUTES` and `ACTION_RATE_MAX_PER_WINDOW` are not in `HANDOFF.md`.
22
+
23
+ **Fix plan:**
24
+ - Count from `action_attempts` (status = "succeeded") as a fallback / dual source.
25
+ - In `executeAction`, query existing attempts before calling the adapter. If a non-failed attempt exists and the prior attempt has a `startedAt` older than a short threshold, assume send succeeded and reconcile DB state without re-sending.
26
+
27
+ ---
28
+
29
+ ### M2 — Escalation Deadline Timezone
30
+ **Current:**
31
+ - `deadline-checker` cron runs hourly.
32
+ - `escalation-policy.ts` `addDays()` uses `new Date(from.getTime() + days * 24 * 60 * 60 * 1000)`.
33
+ - `action-executor.ts` sets `currentDeadlineAt: addDays(new Date(), deadlineDays)`.
34
+
35
+ **Gaps / Bugs:**
36
+ 1. **Timezone not Asia/Kolkata.** `addDays` operates on server-local UTC time. If the server is not in IST, deadlines drift by 5.5 hours, causing the hourly checker to fire too early or too late.
37
+ 2. **State rank mismatch.** `resolution-verify.ts` uses its own `STATE_RANK` map with different numeric values (`resolution_claimed: 17`, `resolved: 18`) compared to the canonical map in other workers (`resolution_claimed: 18`, `resolved: 19`). This can cause state regression or incorrect transitions.
38
+
39
+ **Fix plan:**
40
+ - Create a `deadlineInKolkata(from, days)` helper that computes the deadline in `Asia/Kolkata` timezone.
41
+ - Align `resolution-verify.ts` STATE_RANK with the canonical map used by `route-plan.ts`, `reply-classify.ts`, etc.
42
+
43
+ ---
44
+
45
+ ### M3 — Autopilot Triggers & Resolution Flow
46
+ **Current:**
47
+ - `autopilot-tick` Inngest function listens to `mission.autopilot_tick`.
48
+ - Emitted by `route-plan.ts` (after plan ready) and `reply-classify.ts` (after reply classified).
49
+ - `runAutopilotTick` uses a ToolLoopAgent with tools: `halt`, `draftFirstAction`, `openParallelTracks`, `requestResolution`, `waitForUser`.
50
+
51
+ **Gaps / Bugs:**
52
+ 1. **Auto-claims resolution without user consent.** The `requestResolution` tool creates a `resolutionOutcomes` row, sets mission state to `resolution_claimed`, and emits `mission.resolution_claimed` — all without explicit user approval. Phase 1 spec requires user-confirmed resolution.
53
+ 2. **Missing triggers.** Autopilot does NOT tick on:
54
+ - `mission.deadline_breached` (should evaluate next step)
55
+ - `action_executed` / `action_execution_failed` (should decide follow-up)
56
+ - `followup.due` (should draft escalation)
57
+ 3. **No mode-gated behavior.** `autopilotMode` on the mission row is read but never used. The `requestResolution` tool should be blocked in "moderate" mode.
58
+
59
+ **Fix plan:**
60
+ - Change `requestResolution` to emit a `resolution.proposed` snapshot and wait for user confirmation, instead of auto-claiming.
61
+ - Add autopilot tick emission in `escalation.ts` (deadline breached) and after action execution.
62
+ - Respect `autopilotMode`: only auto-claim resolution in an explicit "aggressive" mode; "moderate" should always wait for user.
63
+
64
+ ---
65
+
66
+ ### M4 — Parallel Tracks (routeGraph AND/OR, parent_action_id)
67
+ **Current:**
68
+ - Schema has `actions.parentActionId` and `actions.trackLabel`.
69
+ - `draftConsentGatedAction` accepts `parentActionId` and `trackLabel`.
70
+ - `openParallelTracks` tool in autopilot.ts drafts up to 2 actions with different authorities.
71
+ - `resolution-verify.ts` supersedes dangling `proposed`/`preview_ready`/`approved` actions on resolution.
72
+
73
+ **Gaps / Bugs:**
74
+ 1. **`parentActionId` never set.** In `openParallelTracks`, each track action is drafted with `trackLabel` but `parentActionId` is always `null`. There is no "parent" action that spawned the parallel tracks, so the link is unused.
75
+ 2. **Route plan has no AND/OR encoding.** The generated `RoutePlanStepDto[]` is linear. The `routeGraph` in route_rules has `nodes`/`edges`, but the LLM-generated plan steps do not encode which steps can run in parallel.
76
+ 3. **Escalation policy ignores tracks.** `getNextEscalationTier` advances linearly; it doesn't respect track boundaries. If two parallel tracks are open, escalating one should not automatically advance the other.
77
+
78
+ **Fix plan:**
79
+ - When `openParallelTracks` opens tracks, create a synthetic "parent" action (or use the first action) and set `parentActionId` on subsequent tracks.
80
+ - Add `parallelGroup` field to `RoutePlanStepDto` so the plan can encode parallelizable steps.
81
+ - Update `getNextEscalationTier` to accept an optional `trackLabel` filter so each track escalates independently.
82
+
83
+ ---
84
+
85
+ ### M5 — Settlement Flow
86
+ **Current:**
87
+ - `reply-classify.v2` prompt extracts settlement offers.
88
+ - `decideSettlementNextAction` in `settlement-policy.ts` recommends accept/counter/escalate/wait.
89
+ - `reply-classify.ts` auto-drafts a counter-offer action if policy says "counter".
90
+
91
+ **Gaps / Bugs:**
92
+ 1. **No settlement acceptance action.** If the user wants to accept a settlement, there's no UI action or API endpoint to generate an acceptance email.
93
+ 2. **No integration tests for settlement end-to-end.** The unit tests for `settlement-policy.ts` exist, but there's no test covering the reply-classify → settlement policy → draft action flow.
94
+ 3. **Counter-offer authority may be wrong.** `from` (the raw reply email) is used as the authority/destination, but it could be a no-reply address.
95
+
96
+ **Fix plan:**
97
+ - Add a settlement-accept endpoint stub and UI button.
98
+ - Add an integration test for the settlement classification → counter-offer flow.
99
+ - Document the counter-offer authority caveat.
100
+
101
+ ---
102
+
103
+ ### M6 — RTI Seeds & Prompts
104
+ **Current:**
105
+ - `legal-data.ts` has RTI Act 2005 chunks (Sections 6, 7, 19, 19(6)).
106
+ - `route-rules-data.ts` has `governmentRtiRouteRule` with RTI0→RTI1→RTI2 steps.
107
+ - Schema supports `issueDomain = "government"`.
108
+
109
+ **Gaps / Bugs:**
110
+ 1. **Intake prompt missing government domain.** `intake.v1` prompt only lists `"consumer" | "payment" | "cyber_fraud"`, so LLM will never classify a government/RTI grievance correctly.
111
+ 2. **No RTI-specific prompt.** There is no dedicated `rti-application.v1` or `rti-packet.v1` prompt for generating RTI application text.
112
+ 3. **No E2E test.** The test suite has no fixture for a government-domain mission.
113
+
114
+ **Fix plan:**
115
+ - Update `intake.v1` prompt to include `"government"` domain.
116
+ - Add `rti-packet.v1` prompt stub.
117
+ - Add a government mission fixture to the seed/integration test data.
118
+
119
+ ---
120
+
121
+ ### M7 — Voice / Indic Intake
122
+ **Current:**
123
+ - `evidence.ts` routes audio to ASR adapter, stores transcript in `transcripts` table.
124
+ - `transcripts` schema exists (Module 2 migration).
125
+ - Redacted text is fed into claim extraction.
126
+
127
+ **Gaps / Bugs:**
128
+ 1. **Original transcript lost.** `evidence.ts` passes `redactedText` as `rawText` to the claim extraction flow. The original unredacted transcript is stored in `transcripts.text` but never preserved separately for audit.
129
+ 2. **Missing unique constraint / idempotency.** `db.insert(schema.transcripts).values(...).onConflictDoNothing()` has no explicit target constraint. On Inngest retry, duplicate rows could accumulate if the table has no unique index on `evidenceItemId`.
130
+ 3. **No language persistence for intake.** ASR returns `language`, but it's only stored in `transcripts.language`. It's not propagated to the mission's `facts` or used to trigger translation.
131
+
132
+ **Fix plan:**
133
+ - Store both `originalText` and `redactedText` in the ASR result / evidence flow.
134
+ - Add a unique index on `transcripts(evidenceItemId)` (or ensure one exists) and specify the conflict target.
135
+ - Propagate detected language to mission facts.
136
+
137
+ ---
138
+
139
+ ### M8 — Portal Assist
140
+ **Current:**
141
+ - `portal-assist.ts` contracts define `PortalAssistState`, `PortalAssistSession`, etc.
142
+ - Schema has `portal_assist_sessions` table.
143
+ - No Inngest function, no API controller, no UI panel.
144
+
145
+ **Gaps / Bugs:**
146
+ 1. **No web session viewer.** The mission page has no portal-assist panel.
147
+ 2. **No screenshot/receipt flow.** There is no endpoint to upload a receipt after a portal submission.
148
+ 3. **No honest `receipt_verified` path.** The `ProofState` enum includes `receipt_verified`, but no code path sets it. Portal actions are always `ready_for_user_submission` or `user_confirmed_external_submission`.
149
+
150
+ **Fix plan:**
151
+ - Add `POST /api/missions/:id/portal-receipt` endpoint that accepts a receipt file and updates the action's `proofState` to `receipt_verified`.
152
+ - Add a minimal portal-assist panel in the mission UI.
153
+ - Document that portal automation is manual-packet only until Browserbase/Stagehand is configured.
154
+
155
+ ---
156
+
157
+ ### M9 — Swarm / Social / DPDP
158
+ **Current:**
159
+ - Swarm and social contracts exist (`swarm.ts`, `social.ts`).
160
+ - Schema has `entities`, `swarms`, `swarm_members`, `social_accounts`, `social_posts` tables.
161
+ - `copy-ready-fallback.ts` social adapter exists.
162
+ - DPDP controller exists in API.
163
+
164
+ **Gaps / Bugs:**
165
+ 1. **No swarm UI on mission page.** `SwarmWithEntity` is imported but never rendered.
166
+ 2. **Social publisher not consent-gated.** `social.ts` contract has `consentGrantId`, but there's no check in the adapter or API that a social post has a valid consent grant before publishing.
167
+ 3. **DPDP consent flow incomplete.** No evidence that the first-contact consent notice is actually delivered to WhatsApp/Telegram users.
168
+
169
+ **Fix plan:**
170
+ - Add a swarm membership opt-in stub to the mission UI.
171
+ - Add a `canPublishSocial` guard that validates `consentGrantId` + action preview hash before any social adapter call.
172
+ - Document DPDP first-contact delivery in the intake flow.
173
+
174
+ ---
175
+
176
+ ## Summary of Priority Fixes
177
+
178
+ | Priority | Module | Fix |
179
+ |----------|--------|-----|
180
+ | P0 | M1 | Dual-source rate limit (submissions + attempts); executor attempt guard |
181
+ | P0 | M2 | Asia/Kolkata deadline helper; align STATE_RANK |
182
+ | P0 | M3 | Stop autopilot auto-claiming resolution; add missing tick triggers |
183
+ | P1 | M4 | Set parentActionId in openParallelTracks; add parallelGroup to steps |
184
+ | P1 | M6 | Add "government" to intake.v1 prompt |
185
+ | P1 | M7 | Preserve original transcript; fix transcript conflict target |
186
+ | P1 | M8 | Add receipt_verified endpoint stub |
187
+ | P1 | M9 | Add social publish consent guard |
188
+ | P2 | M5 | Settlement integration test |
189
+
apps/api/package.json CHANGED
@@ -11,6 +11,7 @@
11
  "typecheck": "tsc --noEmit"
12
  },
13
  "dependencies": {
 
14
  "@courtmitra/adapters": "workspace:*",
15
  "@courtmitra/contracts": "workspace:*",
16
  "@courtmitra/db": "workspace:*",
@@ -19,15 +20,16 @@
19
  "@nestjs/common": "^11.0.0",
20
  "@nestjs/core": "^11.0.0",
21
  "@nestjs/platform-fastify": "^11.0.0",
 
22
  "fastify": "^5.8.5",
23
  "inngest": "^4.4.0",
24
- "drizzle-orm": "^0.45.2",
25
  "reflect-metadata": "^0.2.2",
26
  "rxjs": "^7.8.1",
27
- "zod": "^4.4.3",
28
- "tsx": "^4.19.2"
29
  },
30
  "devDependencies": {
 
31
  "@types/node": "^22.10.2",
32
  "typescript": "^5.7.2"
33
  }
 
11
  "typecheck": "tsc --noEmit"
12
  },
13
  "dependencies": {
14
+ "@clerk/clerk-sdk-node": "^5.1.6",
15
  "@courtmitra/adapters": "workspace:*",
16
  "@courtmitra/contracts": "workspace:*",
17
  "@courtmitra/db": "workspace:*",
 
20
  "@nestjs/common": "^11.0.0",
21
  "@nestjs/core": "^11.0.0",
22
  "@nestjs/platform-fastify": "^11.0.0",
23
+ "drizzle-orm": "^0.45.2",
24
  "fastify": "^5.8.5",
25
  "inngest": "^4.4.0",
 
26
  "reflect-metadata": "^0.2.2",
27
  "rxjs": "^7.8.1",
28
+ "tsx": "^4.19.2",
29
+ "zod": "^4.4.3"
30
  },
31
  "devDependencies": {
32
+ "@types/express": "^5.0.6",
33
  "@types/node": "^22.10.2",
34
  "typescript": "^5.7.2"
35
  }
apps/api/src/app.module.ts CHANGED
@@ -1,5 +1,6 @@
1
  import { Module } from "@nestjs/common";
2
  import { DbModule } from "./db/db.module.js";
 
3
  import { MissionsModule } from "./missions/missions.module.js";
4
  import { InngestModule } from "./workflow/inngest.module.js";
5
  import { ConversationsModule } from "./conversations/conversations.module.js";
@@ -16,12 +17,14 @@ import { PortalAssistModule } from "./portal-assist/portal-assist.module.js";
16
  import { ResolverModule } from "./resolver/resolver.module.js";
17
  import { CaseLookupModule } from "./case-lookup/case-lookup.module.js";
18
  import { ComplianceModule } from "./compliance/compliance.module.js";
 
19
  import { CapabilitiesController } from "./capabilities/capabilities.controller.js";
20
  import { HealthController } from "./health.controller.js";
21
 
22
  @Module({
23
  imports: [
24
  DbModule,
 
25
  InngestModule,
26
  ConversationsModule,
27
  MissionsModule,
@@ -38,6 +41,7 @@ import { HealthController } from "./health.controller.js";
38
  ResolverModule,
39
  CaseLookupModule,
40
  ComplianceModule,
 
41
  ],
42
  controllers: [CapabilitiesController, HealthController],
43
  })
 
1
  import { Module } from "@nestjs/common";
2
  import { DbModule } from "./db/db.module.js";
3
+ import { AuthModule } from "./auth/auth.module.js";
4
  import { MissionsModule } from "./missions/missions.module.js";
5
  import { InngestModule } from "./workflow/inngest.module.js";
6
  import { ConversationsModule } from "./conversations/conversations.module.js";
 
17
  import { ResolverModule } from "./resolver/resolver.module.js";
18
  import { CaseLookupModule } from "./case-lookup/case-lookup.module.js";
19
  import { ComplianceModule } from "./compliance/compliance.module.js";
20
+ import { UsersModule } from "./users/users.module.js";
21
  import { CapabilitiesController } from "./capabilities/capabilities.controller.js";
22
  import { HealthController } from "./health.controller.js";
23
 
24
  @Module({
25
  imports: [
26
  DbModule,
27
+ AuthModule,
28
  InngestModule,
29
  ConversationsModule,
30
  MissionsModule,
 
41
  ResolverModule,
42
  CaseLookupModule,
43
  ComplianceModule,
44
+ UsersModule,
45
  ],
46
  controllers: [CapabilitiesController, HealthController],
47
  })
apps/api/src/auth/auth.module.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Global, Module } from "@nestjs/common";
2
+ import { APP_GUARD } from "@nestjs/core";
3
+ import { AuthService } from "./auth.service.js";
4
+ import { ClerkAuthGuard } from "./clerk-auth.guard.js";
5
+
6
+ @Global()
7
+ @Module({
8
+ providers: [
9
+ AuthService,
10
+ {
11
+ provide: APP_GUARD,
12
+ useClass: ClerkAuthGuard,
13
+ },
14
+ ],
15
+ exports: [AuthService],
16
+ })
17
+ export class AuthModule {}
apps/api/src/auth/auth.service.ts ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable, Inject, UnauthorizedException } from "@nestjs/common";
2
+ import { eq } from "drizzle-orm";
3
+ import { users, userIdentities, type Database } from "@courtmitra/db";
4
+ import { DB } from "../db/db.module.js";
5
+
6
+ export interface ClerkUser {
7
+ id: string;
8
+ emailAddresses: { emailAddress: string }[];
9
+ firstName: string | null;
10
+ lastName: string | null;
11
+ imageUrl: string | null;
12
+ }
13
+
14
+ @Injectable()
15
+ export class AuthService {
16
+ constructor(@Inject(DB) private readonly db: Database) {}
17
+
18
+ /**
19
+ * Find or create a user from Clerk OAuth data.
20
+ * Maps Clerk userId -> our users table via userIdentities.
21
+ */
22
+ async findOrCreateUser(clerkUser: ClerkUser): Promise<{ id: string; email: string | null }> {
23
+ const providerSubject = clerkUser.id;
24
+
25
+ // Try to find existing identity
26
+ const existingIdentity = await this.db
27
+ .select({ userId: userIdentities.userId })
28
+ .from(userIdentities)
29
+ .where(eq(userIdentities.provider, "google"))
30
+ .innerJoin(users, eq(users.id, userIdentities.userId))
31
+ .limit(1);
32
+
33
+ if (existingIdentity[0]) {
34
+ const user = await this.db
35
+ .select()
36
+ .from(users)
37
+ .where(eq(users.id, existingIdentity[0].userId))
38
+ .limit(1);
39
+ if (user[0]) {
40
+ return { id: user[0].id, email: user[0].email ?? null };
41
+ }
42
+ }
43
+
44
+ // Create new user
45
+ const email = clerkUser.emailAddresses[0]?.emailAddress ?? null;
46
+ const fullName = [clerkUser.firstName, clerkUser.lastName].filter(Boolean).join(" ") || null;
47
+
48
+ const [userRow] = await this.db
49
+ .insert(users)
50
+ .values({
51
+ displayName: fullName ?? email ?? "User",
52
+ email,
53
+ avatarUrl: clerkUser.imageUrl,
54
+ fullName,
55
+ })
56
+ .returning();
57
+
58
+ if (!userRow) {
59
+ throw new UnauthorizedException("Failed to create user");
60
+ }
61
+
62
+ // Create identity link
63
+ await this.db.insert(userIdentities).values({
64
+ userId: userRow.id,
65
+ provider: "google",
66
+ providerSubject,
67
+ isVerified: true,
68
+ metadata: { source: "clerk_oauth" },
69
+ });
70
+
71
+ return { id: userRow.id, email };
72
+ }
73
+
74
+ /** Look up a user by their internal DB id. */
75
+ async findUserById(id: string) {
76
+ const rows = await this.db.select().from(users).where(eq(users.id, id)).limit(1);
77
+ return rows[0] ?? null;
78
+ }
79
+ }
apps/api/src/auth/clerk-auth.guard.ts ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from "@nestjs/common";
2
+ import { verifyToken } from "@clerk/clerk-sdk-node";
3
+ import { AuthService } from "./auth.service.js";
4
+
5
+ @Injectable()
6
+ export class ClerkAuthGuard implements CanActivate {
7
+ constructor(private readonly authService: AuthService) {}
8
+
9
+ async canActivate(context: ExecutionContext): Promise<boolean> {
10
+ const request = context.switchToHttp().getRequest<{ headers: { authorization?: string }; userId?: string; clerkUserId?: string }>();
11
+ const authHeader = request.headers.authorization;
12
+
13
+ if (!authHeader?.startsWith("Bearer ")) {
14
+ // Demo mode fallback — allow through with a demo user context
15
+ // This lets local dev work without Clerk keys
16
+ if (process.env.DEMO_MODE === "true") {
17
+ (request as any).clerkUserId = "demo";
18
+ (request as any).userId = "demo";
19
+ return true;
20
+ }
21
+ throw new UnauthorizedException("Missing Authorization header");
22
+ }
23
+
24
+ const token = authHeader.replace("Bearer ", "");
25
+
26
+ try {
27
+ // Verify the Clerk JWT
28
+ const verified = await verifyToken(token, {
29
+ jwtKey: process.env.CLERK_JWT_KEY,
30
+ authorizedParties: process.env.CLERK_AUTHORIZED_PARTIES?.split(",") ?? [],
31
+ });
32
+
33
+ const clerkUserId = verified.sub;
34
+
35
+ // Find or create the user in our DB
36
+ const user = await this.authService.findOrCreateUser({
37
+ id: clerkUserId,
38
+ emailAddresses: [{ emailAddress: verified.email as string }],
39
+ firstName: (verified as any).first_name ?? null,
40
+ lastName: (verified as any).last_name ?? null,
41
+ imageUrl: (verified as any).image_url ?? null,
42
+ });
43
+
44
+ // Attach to request for controllers
45
+ (request as any).clerkUserId = clerkUserId;
46
+ (request as any).userId = user.id;
47
+
48
+ return true;
49
+ } catch (err) {
50
+ // If no CLERK_JWT_KEY set, allow through with clerk user id from token payload (dev mode)
51
+ if (!process.env.CLERK_JWT_KEY && process.env.DEMO_MODE !== "false") {
52
+ try {
53
+ const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64").toString());
54
+ const clerkUserId = payload.sub;
55
+ const user = await this.authService.findOrCreateUser({
56
+ id: clerkUserId,
57
+ emailAddresses: [{ emailAddress: payload.email ?? "" }],
58
+ firstName: payload.first_name ?? null,
59
+ lastName: payload.last_name ?? null,
60
+ imageUrl: payload.image_url ?? null,
61
+ });
62
+ (request as any).clerkUserId = clerkUserId;
63
+ (request as any).userId = user.id;
64
+ return true;
65
+ } catch {
66
+ throw new UnauthorizedException("Invalid token");
67
+ }
68
+ }
69
+ throw new UnauthorizedException("Invalid or expired token");
70
+ }
71
+ }
72
+ }
apps/api/src/auth/current-user.decorator.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createParamDecorator, ExecutionContext } from "@nestjs/common";
2
+
3
+ /**
4
+ * Injects the DB userId from the request (set by ClerkAuthGuard).
5
+ * Falls back to "demo" user id in local dev when no auth is present.
6
+ */
7
+ export const CurrentUser = createParamDecorator(
8
+ (_data: unknown, ctx: ExecutionContext): string => {
9
+ const request = ctx.switchToHttp().getRequest<{ userId?: string }>();
10
+ return request.userId ?? "demo";
11
+ }
12
+ );
apps/api/src/main.ts CHANGED
@@ -2,6 +2,8 @@ import "reflect-metadata";
2
  import { NestFactory } from "@nestjs/core";
3
  import { FastifyAdapter, type NestFastifyApplication } from "@nestjs/platform-fastify";
4
  import { AppModule } from "./app.module.js";
 
 
5
 
6
  async function bootstrap() {
7
  const app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter(), {
@@ -10,9 +12,23 @@ async function bootstrap() {
10
  app.setGlobalPrefix("api");
11
  app.enableCors({ origin: true });
12
 
13
- const port = Number(process.env.API_PORT ?? 3001);
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  await app.listen(port, "0.0.0.0");
15
  console.log(`[api] listening on http://localhost:${port}/api`);
 
16
  }
17
 
18
  bootstrap().catch((err) => {
 
2
  import { NestFactory } from "@nestjs/core";
3
  import { FastifyAdapter, type NestFastifyApplication } from "@nestjs/platform-fastify";
4
  import { AppModule } from "./app.module.js";
5
+ import { serve } from "inngest/node";
6
+ import { inngest as workerInngest, functions } from "../../worker/src/inngest.js";
7
 
8
  async function bootstrap() {
9
  const app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter(), {
 
12
  app.setGlobalPrefix("api");
13
  app.enableCors({ origin: true });
14
 
15
+ // Mount Inngest worker handler on /api/inngest (combined container)
16
+ const inngestHandler = serve({ client: workerInngest, functions });
17
+ const fastify = app.getHttpAdapter().getInstance();
18
+
19
+ fastify.route({
20
+ method: ["GET", "POST", "PUT"],
21
+ url: "/api/inngest",
22
+ handler: async (request: any, reply: any) => {
23
+ reply.hijack();
24
+ inngestHandler(request.raw, reply.raw);
25
+ },
26
+ });
27
+
28
+ const port = Number(process.env.API_PORT ?? process.env.PORT ?? 3001);
29
  await app.listen(port, "0.0.0.0");
30
  console.log(`[api] listening on http://localhost:${port}/api`);
31
+ console.log(`[worker] inngest endpoint on http://localhost:${port}/api/inngest`);
32
  }
33
 
34
  bootstrap().catch((err) => {
apps/api/src/missions/missions.controller.ts CHANGED
@@ -1,13 +1,14 @@
1
- import { Body, Controller, Get, Headers, Inject, NotFoundException, Param, Patch, Post, Query } from "@nestjs/common";
2
  import { CreateMissionInput, UpdateMissionDomainInput } from "@courtmitra/contracts";
3
  import { parseOrThrow } from "../common/zod-validation.pipe.js";
4
  import { MissionsService } from "./missions.service.js";
5
  import { DB } from "../db/db.module.js";
6
  import { schema, type Database } from "@courtmitra/db";
7
  import { eq, desc } from "drizzle-orm";
 
 
8
 
9
- // Explicit @Inject tokens: the tsx/esbuild runtime does not emit decorator
10
- // metadata, so Nest cannot inject by type. Project convention for the API.
11
  @Controller("missions")
12
  export class MissionsController {
13
  constructor(
@@ -16,39 +17,43 @@ export class MissionsController {
16
  ) {}
17
 
18
  @Post()
19
- async create(@Body() body: unknown, @Headers("idempotency-key") idempotencyKey?: string) {
 
 
 
 
20
  const input = parseOrThrow(CreateMissionInput, body);
21
- return this.missions.create(input, idempotencyKey);
22
  }
23
 
24
  @Get()
25
- async list() {
26
- return this.missions.list();
27
  }
28
 
29
  @Get(":id")
30
- async get(@Param("id") id: string) {
31
- return this.missions.get(id);
32
  }
33
 
34
  @Get(":id/snapshot")
35
- async snapshot(@Param("id") id: string) {
36
  return this.missions.latestSnapshot(id);
37
  }
38
 
39
  @Get(":id/timeline")
40
- async timeline(@Param("id") id: string, @Query("cursor") cursor?: string) {
41
- return this.missions.timeline(id, cursor);
42
  }
43
 
44
  @Patch(":id/domain")
45
- async updateDomain(@Param("id") id: string, @Body() body: unknown) {
46
  const input = parseOrThrow(UpdateMissionDomainInput, body);
47
- return this.missions.updateDomain(id, input);
48
  }
49
 
50
  @Get(":id/public-page")
51
- async publicPage(@Param("id") id: string) {
52
  const [row] = await this.db
53
  .select({ slug: schema.publicCasePages.slug })
54
  .from(schema.publicCasePages)
 
1
+ import { Body, Controller, Get, Headers, Inject, NotFoundException, Param, Patch, Post, Query, UseGuards } from "@nestjs/common";
2
  import { CreateMissionInput, UpdateMissionDomainInput } from "@courtmitra/contracts";
3
  import { parseOrThrow } from "../common/zod-validation.pipe.js";
4
  import { MissionsService } from "./missions.service.js";
5
  import { DB } from "../db/db.module.js";
6
  import { schema, type Database } from "@courtmitra/db";
7
  import { eq, desc } from "drizzle-orm";
8
+ import { CurrentUser } from "../auth/current-user.decorator.js";
9
+ import { ClerkAuthGuard } from "../auth/clerk-auth.guard.js";
10
 
11
+ @UseGuards(ClerkAuthGuard)
 
12
  @Controller("missions")
13
  export class MissionsController {
14
  constructor(
 
17
  ) {}
18
 
19
  @Post()
20
+ async create(
21
+ @Body() body: unknown,
22
+ @CurrentUser() userId: string,
23
+ @Headers("idempotency-key") idempotencyKey?: string,
24
+ ) {
25
  const input = parseOrThrow(CreateMissionInput, body);
26
+ return this.missions.create(userId, input, idempotencyKey);
27
  }
28
 
29
  @Get()
30
+ async list(@CurrentUser() userId: string) {
31
+ return this.missions.list(userId);
32
  }
33
 
34
  @Get(":id")
35
+ async get(@Param("id") id: string, @CurrentUser() userId: string) {
36
+ return this.missions.get(id, userId);
37
  }
38
 
39
  @Get(":id/snapshot")
40
+ async snapshot(@Param("id") id: string, @CurrentUser() userId: string) {
41
  return this.missions.latestSnapshot(id);
42
  }
43
 
44
  @Get(":id/timeline")
45
+ async timeline(@Param("id") id: string, @CurrentUser() userId: string, @Query("cursor") cursor?: string) {
46
+ return this.missions.timeline(id, userId, cursor);
47
  }
48
 
49
  @Patch(":id/domain")
50
+ async updateDomain(@Param("id") id: string, @CurrentUser() userId: string, @Body() body: unknown) {
51
  const input = parseOrThrow(UpdateMissionDomainInput, body);
52
+ return this.missions.updateDomain(id, userId, input);
53
  }
54
 
55
  @Get(":id/public-page")
56
+ async publicPage(@Param("id") id: string, @CurrentUser() userId: string) {
57
  const [row] = await this.db
58
  .select({ slug: schema.publicCasePages.slug })
59
  .from(schema.publicCasePages)
apps/api/src/missions/missions.service.ts CHANGED
@@ -7,7 +7,6 @@ import {
7
  missionEvents,
8
  missionStatusSnapshots,
9
  idempotencyKeys,
10
- users,
11
  conversations,
12
  messages,
13
  } from "@courtmitra/db";
@@ -23,9 +22,6 @@ import { WorkflowService } from "../workflow/inngest.module.js";
23
 
24
  type MissionRow = typeof missions.$inferSelect;
25
 
26
- /** Day-1 demo identity: until auth lands (Day 2), browser missions attach to a single demo user. */
27
- const DEMO_USER_NAME = "Demo User";
28
-
29
  function sha256Json(value: unknown): string {
30
  return createHash("sha256").update(JSON.stringify(value), "utf8").digest("hex");
31
  }
@@ -71,14 +67,6 @@ export class MissionsService {
71
  };
72
  }
73
 
74
- /** Get-or-create the Day-1 demo user inside a transaction. */
75
- private async ensureDemoUser(tx: Database): Promise<string> {
76
- const existing = await tx.select().from(users).where(eq(users.displayName, DEMO_USER_NAME)).limit(1);
77
- if (existing[0]) return existing[0].id;
78
- const [created] = await tx.insert(users).values({ displayName: DEMO_USER_NAME }).returning();
79
- return created!.id;
80
- }
81
-
82
  /**
83
  * Create a mission. The mission row, its `mission.created` event and the
84
  * initial status snapshot are written in ONE transaction (PHASE_1 §3.5).
@@ -86,7 +74,7 @@ export class MissionsService {
86
  * After creation, creates a synthetic conversation + message and triggers
87
  * the intake worker so the mission is processed automatically.
88
  */
89
- async create(input: CreateMissionInput, idempotencyKey?: string): Promise<Mission> {
90
  // Check idempotency first, before any transaction
91
  if (idempotencyKey) {
92
  const prior = await this.db.select().from(idempotencyKeys).where(eq(idempotencyKeys.key, idempotencyKey)).limit(1);
@@ -96,13 +84,11 @@ export class MissionsService {
96
  }
97
  }
98
 
99
- const { mission, userId } = await this.db.transaction(async (tx) => {
100
- const uid = await this.ensureDemoUser(tx as unknown as Database);
101
-
102
  const [m] = await tx
103
  .insert(missions)
104
  .values({
105
- userId: uid,
106
  title: input.title,
107
  issueDomain: input.issueDomain,
108
  state: "created",
@@ -117,7 +103,7 @@ export class MissionsService {
117
  missionId: m!.id,
118
  eventType: "mission.created",
119
  actorType: "user",
120
- actorId: uid,
121
  payload: { issueDomain: input.issueDomain, title: input.title, source: "browser" },
122
  });
123
 
@@ -133,11 +119,10 @@ export class MissionsService {
133
  });
134
  }
135
 
136
- return { mission: m!, userId: uid };
137
  });
138
 
139
  // After the transaction, create a synthetic conversation + message and trigger intake.
140
- // This is done outside the transaction so the intake worker can read the mission immediately.
141
  const messageBody = input.description
142
  ? `${input.title}\n\n${input.description}`
143
  : input.title;
@@ -173,13 +158,21 @@ export class MissionsService {
173
  return this.toDto(mission);
174
  }
175
 
176
- async list(): Promise<Mission[]> {
177
- const rows = await this.db.select().from(missions).orderBy(desc(missions.createdAt)).limit(100);
 
 
 
 
 
 
178
  return rows.map((m) => this.toDto(m));
179
  }
180
 
181
- async get(id: string): Promise<Mission> {
182
- const rows = await this.db.select().from(missions).where(eq(missions.id, id)).limit(1);
 
 
183
  if (!rows[0]) throw new NotFoundException(`mission ${id} not found`);
184
  return this.toDto(rows[0]);
185
  }
@@ -210,8 +203,8 @@ export class MissionsService {
210
  };
211
  }
212
 
213
- async updateDomain(id: string, input: UpdateMissionDomainInput): Promise<Mission> {
214
- const mission = await this.get(id);
215
  const [updated] = await this.db
216
  .update(missions)
217
  .set({ issueDomain: input.issueDomain, updatedAt: new Date() })
@@ -222,15 +215,15 @@ export class MissionsService {
222
  missionId: id,
223
  eventType: "mission.domain_changed",
224
  actorType: "user",
225
- actorId: mission.userId,
226
  payload: { from: mission.issueDomain, to: input.issueDomain },
227
  });
228
  return this.toDto(updated);
229
  }
230
 
231
  /** Cursor-paginated mission timeline (PHASE_1 §7), newest-first by createdAt. */
232
- async timeline(missionId: string, cursor?: string, limit = 50): Promise<{ items: MissionEvent[]; nextCursor: string | null }> {
233
- await this.get(missionId);
234
  const where = cursor
235
  ? and(eq(missionEvents.missionId, missionId), lt(missionEvents.createdAt, new Date(cursor)))
236
  : eq(missionEvents.missionId, missionId);
 
7
  missionEvents,
8
  missionStatusSnapshots,
9
  idempotencyKeys,
 
10
  conversations,
11
  messages,
12
  } from "@courtmitra/db";
 
22
 
23
  type MissionRow = typeof missions.$inferSelect;
24
 
 
 
 
25
  function sha256Json(value: unknown): string {
26
  return createHash("sha256").update(JSON.stringify(value), "utf8").digest("hex");
27
  }
 
67
  };
68
  }
69
 
 
 
 
 
 
 
 
 
70
  /**
71
  * Create a mission. The mission row, its `mission.created` event and the
72
  * initial status snapshot are written in ONE transaction (PHASE_1 §3.5).
 
74
  * After creation, creates a synthetic conversation + message and triggers
75
  * the intake worker so the mission is processed automatically.
76
  */
77
+ async create(userId: string, input: CreateMissionInput, idempotencyKey?: string): Promise<Mission> {
78
  // Check idempotency first, before any transaction
79
  if (idempotencyKey) {
80
  const prior = await this.db.select().from(idempotencyKeys).where(eq(idempotencyKeys.key, idempotencyKey)).limit(1);
 
84
  }
85
  }
86
 
87
+ const { mission } = await this.db.transaction(async (tx) => {
 
 
88
  const [m] = await tx
89
  .insert(missions)
90
  .values({
91
+ userId,
92
  title: input.title,
93
  issueDomain: input.issueDomain,
94
  state: "created",
 
103
  missionId: m!.id,
104
  eventType: "mission.created",
105
  actorType: "user",
106
+ actorId: userId,
107
  payload: { issueDomain: input.issueDomain, title: input.title, source: "browser" },
108
  });
109
 
 
119
  });
120
  }
121
 
122
+ return { mission: m! };
123
  });
124
 
125
  // After the transaction, create a synthetic conversation + message and trigger intake.
 
126
  const messageBody = input.description
127
  ? `${input.title}\n\n${input.description}`
128
  : input.title;
 
158
  return this.toDto(mission);
159
  }
160
 
161
+ /** List missions for a specific user, newest first. */
162
+ async list(userId: string): Promise<Mission[]> {
163
+ const rows = await this.db
164
+ .select()
165
+ .from(missions)
166
+ .where(eq(missions.userId, userId))
167
+ .orderBy(desc(missions.createdAt))
168
+ .limit(100);
169
  return rows.map((m) => this.toDto(m));
170
  }
171
 
172
+ /** Get a mission IF it belongs to the requesting user. */
173
+ async get(id: string, userId?: string): Promise<Mission> {
174
+ const where = userId ? and(eq(missions.id, id), eq(missions.userId, userId)) : eq(missions.id, id);
175
+ const rows = await this.db.select().from(missions).where(where).limit(1);
176
  if (!rows[0]) throw new NotFoundException(`mission ${id} not found`);
177
  return this.toDto(rows[0]);
178
  }
 
203
  };
204
  }
205
 
206
+ async updateDomain(id: string, userId: string, input: UpdateMissionDomainInput): Promise<Mission> {
207
+ const mission = await this.get(id, userId);
208
  const [updated] = await this.db
209
  .update(missions)
210
  .set({ issueDomain: input.issueDomain, updatedAt: new Date() })
 
215
  missionId: id,
216
  eventType: "mission.domain_changed",
217
  actorType: "user",
218
+ actorId: userId,
219
  payload: { from: mission.issueDomain, to: input.issueDomain },
220
  });
221
  return this.toDto(updated);
222
  }
223
 
224
  /** Cursor-paginated mission timeline (PHASE_1 §7), newest-first by createdAt. */
225
+ async timeline(missionId: string, userId: string, cursor?: string, limit = 50): Promise<{ items: MissionEvent[]; nextCursor: string | null }> {
226
+ await this.get(missionId, userId);
227
  const where = cursor
228
  ? and(eq(missionEvents.missionId, missionId), lt(missionEvents.createdAt, new Date(cursor)))
229
  : eq(missionEvents.missionId, missionId);
apps/api/src/users/users.controller.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Body, Controller, Get, Inject, Patch, UseGuards } from "@nestjs/common";
2
+ import { UsersService } from "./users.service.js";
3
+ import { CurrentUser } from "../auth/current-user.decorator.js";
4
+ import { ClerkAuthGuard } from "../auth/clerk-auth.guard.js";
5
+
6
+ @UseGuards(ClerkAuthGuard)
7
+ @Controller("users")
8
+ export class UsersController {
9
+ constructor(@Inject(UsersService) private readonly usersService: UsersService) {}
10
+
11
+ @Get("me")
12
+ async me(@CurrentUser() userId: string) {
13
+ return this.usersService.getProfile(userId);
14
+ }
15
+
16
+ @Patch("me")
17
+ async updateMe(
18
+ @CurrentUser() userId: string,
19
+ @Body() body: { phone?: string; fullName?: string; preferredLanguage?: string },
20
+ ) {
21
+ return this.usersService.updateProfile(userId, body);
22
+ }
23
+
24
+ @Get("me/missions")
25
+ async myMissions(@CurrentUser() userId: string) {
26
+ return this.usersService.listUserMissions(userId);
27
+ }
28
+ }
apps/api/src/users/users.module.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Module } from "@nestjs/common";
2
+ import { UsersController } from "./users.controller.js";
3
+ import { UsersService } from "./users.service.js";
4
+
5
+ @Module({
6
+ controllers: [UsersController],
7
+ providers: [UsersService],
8
+ exports: [UsersService],
9
+ })
10
+ export class UsersModule {}
apps/api/src/users/users.service.ts ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable, Inject, NotFoundException } from "@nestjs/common";
2
+ import { eq, desc } from "drizzle-orm";
3
+ import { users, missions, type Database } from "@courtmitra/db";
4
+ import { DB } from "../db/db.module.js";
5
+
6
+ @Injectable()
7
+ export class UsersService {
8
+ constructor(@Inject(DB) private readonly db: Database) {}
9
+
10
+ async getProfile(userId: string) {
11
+ const rows = await this.db.select().from(users).where(eq(users.id, userId)).limit(1);
12
+ if (!rows[0]) throw new NotFoundException("User not found");
13
+ const u = rows[0];
14
+ return {
15
+ id: u.id,
16
+ displayName: u.displayName,
17
+ email: u.email,
18
+ avatarUrl: u.avatarUrl,
19
+ phone: u.phone,
20
+ fullName: u.fullName,
21
+ preferredLanguage: u.preferredLanguage,
22
+ };
23
+ }
24
+
25
+ async updateProfile(userId: string, data: { phone?: string; fullName?: string; preferredLanguage?: string }) {
26
+ const [updated] = await this.db
27
+ .update(users)
28
+ .set({
29
+ phone: data.phone,
30
+ fullName: data.fullName,
31
+ preferredLanguage: data.preferredLanguage,
32
+ updatedAt: new Date(),
33
+ })
34
+ .where(eq(users.id, userId))
35
+ .returning();
36
+ if (!updated) throw new NotFoundException("User not found");
37
+ return this.getProfile(userId);
38
+ }
39
+
40
+ async listUserMissions(userId: string) {
41
+ const rows = await this.db
42
+ .select()
43
+ .from(missions)
44
+ .where(eq(missions.userId, userId))
45
+ .orderBy(desc(missions.createdAt))
46
+ .limit(100);
47
+ return rows.map((m) => ({
48
+ id: m.id,
49
+ title: m.title,
50
+ state: m.state,
51
+ issueDomain: m.issueDomain,
52
+ createdAt: m.createdAt.toISOString(),
53
+ }));
54
+ }
55
+ }
apps/web/.env.local.example ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # Clerk (Google OAuth enabled in dashboard)
2
+ NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
3
+ CLERK_SECRET_KEY=sk_test_...
4
+ NEXT_PUBLIC_CLERK_SIGN_IN_URL=/en/sign-in
5
+ NEXT_PUBLIC_CLERK_SIGN_UP_URL=/en/sign-up
6
+
7
+ # Backend API (HF Spaces or local)
8
+ NEXT_PUBLIC_API_BASE_URL=http://localhost:3001/api
9
+ INTERNAL_API_BASE_URL=http://localhost:3001/api
apps/web/middleware.ts ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
2
+ import createMiddleware from "next-intl/middleware";
3
+ import { routing } from "./src/i18n/routing";
4
+
5
+ const intlMiddleware = createMiddleware(routing);
6
+
7
+ const isProtectedRoute = createRouteMatcher([
8
+ "/:locale/ledger",
9
+ "/:locale/missions(.*)",
10
+ ]);
11
+
12
+ export default clerkMiddleware(async (auth, req) => {
13
+ // Protect specific routes
14
+ if (isProtectedRoute(req)) {
15
+ await auth.protect();
16
+ }
17
+
18
+ // Always run next-intl middleware for locale handling
19
+ return intlMiddleware(req);
20
+ });
21
+
22
+ export const config = {
23
+ matcher: [
24
+ // Skip Next.js internals and all static files
25
+ "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
26
+ // Always run for API routes
27
+ "/(api|trpc)(.*)",
28
+ ],
29
+ };
apps/web/package.json CHANGED
@@ -9,6 +9,7 @@
9
  "typecheck": "tsc --noEmit"
10
  },
11
  "dependencies": {
 
12
  "@courtmitra/contracts": "workspace:*",
13
  "@hookform/resolvers": "^5.4.0",
14
  "@radix-ui/react-label": "^2.1.8",
 
9
  "typecheck": "tsc --noEmit"
10
  },
11
  "dependencies": {
12
+ "@clerk/nextjs": "^7.4.2",
13
  "@courtmitra/contracts": "workspace:*",
14
  "@hookform/resolvers": "^5.4.0",
15
  "@radix-ui/react-label": "^2.1.8",
apps/web/src/app/[locale]/layout.tsx CHANGED
@@ -7,7 +7,9 @@ import { routing } from "@/i18n/routing";
7
  import { Toaster } from "@/components/ui/sonner";
8
  import { TooltipProvider } from "@/components/ui/tooltip";
9
  import { LocaleSwitcher } from "@/components/app/LocaleSwitcher";
 
10
  import Link from "next/link";
 
11
 
12
  export const metadata: Metadata = {
13
  title: "CourtMitra MissionOS",
@@ -41,7 +43,8 @@ export default async function LocaleLayout({
41
 
42
  return (
43
  <NextIntlClientProvider locale={locale} messages={messages}>
44
- <TooltipProvider>
 
45
  {/* ── Top navigation bar ─────────────────────────────── */}
46
  <header
47
  style={{
@@ -135,6 +138,45 @@ export default async function LocaleLayout({
135
  />
136
 
137
  <LocaleSwitcher />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  </nav>
139
  </div>
140
  </header>
@@ -153,6 +195,7 @@ export default async function LocaleLayout({
153
  }}
154
  />
155
  </TooltipProvider>
 
156
  </NextIntlClientProvider>
157
  );
158
  }
 
7
  import { Toaster } from "@/components/ui/sonner";
8
  import { TooltipProvider } from "@/components/ui/tooltip";
9
  import { LocaleSwitcher } from "@/components/app/LocaleSwitcher";
10
+ import { Show, SignInButton, UserButton } from "@clerk/nextjs";
11
  import Link from "next/link";
12
+ import { AuthTokenProvider } from "@/lib/auth-token";
13
 
14
  export const metadata: Metadata = {
15
  title: "CourtMitra MissionOS",
 
43
 
44
  return (
45
  <NextIntlClientProvider locale={locale} messages={messages}>
46
+ <AuthTokenProvider>
47
+ <TooltipProvider>
48
  {/* ── Top navigation bar ─────────────────────────────── */}
49
  <header
50
  style={{
 
138
  />
139
 
140
  <LocaleSwitcher />
141
+
142
+ {/* Divider */}
143
+ <span
144
+ style={{
145
+ width: 1,
146
+ height: 20,
147
+ background: "var(--border)",
148
+ margin: "0 4px",
149
+ display: "inline-block",
150
+ }}
151
+ />
152
+
153
+ {/* Auth */}
154
+ <Show when="signed-out">
155
+ <SignInButton mode="modal">
156
+ <button
157
+ style={{
158
+ fontSize: 14,
159
+ fontWeight: 500,
160
+ color: "var(--primary)",
161
+ padding: "6px 10px",
162
+ borderRadius: 6,
163
+ background: "none",
164
+ border: "none",
165
+ cursor: "pointer",
166
+ whiteSpace: "nowrap",
167
+ }}
168
+ >
169
+ {locale === "hi" ? "साइन इन" : "Sign In"}
170
+ </button>
171
+ </SignInButton>
172
+ </Show>
173
+ <Show when="signed-in">
174
+ <UserButton>
175
+ <UserButton.MenuItems>
176
+ <UserButton.Link label="Profile" labelIcon={<span>👤</span>} href={`/${locale}/profile`} />
177
+ </UserButton.MenuItems>
178
+ </UserButton>
179
+ </Show>
180
  </nav>
181
  </div>
182
  </header>
 
195
  }}
196
  />
197
  </TooltipProvider>
198
+ </AuthTokenProvider>
199
  </NextIntlClientProvider>
200
  );
201
  }
apps/web/src/app/[locale]/page.tsx CHANGED
@@ -4,8 +4,10 @@ import { useEffect, useState } from "react";
4
  import { useRouter } from "next/navigation";
5
  import Link from "next/link";
6
  import { useTranslations, useLocale } from "next-intl";
 
7
  import type { IssueDomain, Mission, MissionState } from "@courtmitra/contracts";
8
- import { api } from "@/lib/api";
 
9
  import {
10
  Dialog,
11
  DialogContent,
@@ -15,15 +17,15 @@ import {
15
  } from "@/components/ui/dialog";
16
  import { stateToStage } from "@/components/mission";
17
  import { AutopilotSimulator, TemplateId } from "@/components/app/AutopilotSimulator";
18
- import {
19
- ShoppingBag,
20
- CreditCard,
21
- ShieldAlert,
22
- Briefcase,
23
- Home as HomeIcon,
24
- Building2,
25
- Smartphone,
26
- HeartPulse,
27
  ArrowRight,
28
  TrendingUp,
29
  Clock,
@@ -421,6 +423,15 @@ export default function MissionBoard() {
421
  // Autopilot Simulator template state
422
  const [activeTemplate, setActiveTemplate] = useState<TemplateId>("upi");
423
 
 
 
 
 
 
 
 
 
 
424
  async function refresh() {
425
  setLoadingMissions(true);
426
  try {
@@ -535,15 +546,29 @@ export default function MissionBoard() {
535
 
536
  {/* CTAs */}
537
  <div className="flex flex-wrap gap-3.5 mb-8">
538
- <button
539
- onClick={() => setDialogOpen(true)}
540
- className="bg-primary text-white border-none rounded-xl px-7 py-3.5 font-bold cursor-pointer transition-all duration-200 hover:bg-primary-hover shadow-lg hover:-translate-y-0.5 text-[15px] animate-pulse-border"
541
- style={{
542
- boxShadow: "0 10px 20px -5px rgba(29, 53, 87, 0.25)"
543
- }}
544
- >
545
- {tLanding("ctaStart")}
546
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
547
  <Link
548
  href={`/${locale}/ledger`}
549
  className="inline-flex items-center justify-center bg-card text-foreground border border-border rounded-xl px-7 py-3.5 font-bold text-[15px] text-decoration-none transition-all duration-200 hover:bg-surface-elevated hover:-translate-y-0.5"
 
4
  import { useRouter } from "next/navigation";
5
  import Link from "next/link";
6
  import { useTranslations, useLocale } from "next-intl";
7
+ import { Show, SignInButton } from "@clerk/nextjs";
8
  import type { IssueDomain, Mission, MissionState } from "@courtmitra/contracts";
9
+ import { api, setAuthTokenGetter } from "@/lib/api";
10
+ import { useAuthToken } from "@/lib/auth-token";
11
  import {
12
  Dialog,
13
  DialogContent,
 
17
  } from "@/components/ui/dialog";
18
  import { stateToStage } from "@/components/mission";
19
  import { AutopilotSimulator, TemplateId } from "@/components/app/AutopilotSimulator";
20
+ import {
21
+ ShoppingBag,
22
+ CreditCard,
23
+ ShieldAlert,
24
+ Briefcase,
25
+ Home as HomeIcon,
26
+ Building2,
27
+ Smartphone,
28
+ HeartPulse,
29
  ArrowRight,
30
  TrendingUp,
31
  Clock,
 
423
  // Autopilot Simulator template state
424
  const [activeTemplate, setActiveTemplate] = useState<TemplateId>("upi");
425
 
426
+ const { getToken } = useAuthToken();
427
+
428
+ // Register auth token getter with API client
429
+ useEffect(() => {
430
+ if (getToken) {
431
+ setAuthTokenGetter(getToken);
432
+ }
433
+ }, [getToken]);
434
+
435
  async function refresh() {
436
  setLoadingMissions(true);
437
  try {
 
546
 
547
  {/* CTAs */}
548
  <div className="flex flex-wrap gap-3.5 mb-8">
549
+ <Show when="signed-in">
550
+ <button
551
+ onClick={() => setDialogOpen(true)}
552
+ className="bg-primary text-white border-none rounded-xl px-7 py-3.5 font-bold cursor-pointer transition-all duration-200 hover:bg-primary-hover shadow-lg hover:-translate-y-0.5 text-[15px] animate-pulse-border"
553
+ style={{
554
+ boxShadow: "0 10px 20px -5px rgba(29, 53, 87, 0.25)"
555
+ }}
556
+ >
557
+ {tLanding("ctaStart")}
558
+ </button>
559
+ </Show>
560
+ <Show when="signed-out">
561
+ <SignInButton mode="modal" fallbackRedirectUrl={`/${locale}`}>
562
+ <button
563
+ className="bg-primary text-white border-none rounded-xl px-7 py-3.5 font-bold cursor-pointer transition-all duration-200 hover:bg-primary-hover shadow-lg hover:-translate-y-0.5 text-[15px] animate-pulse-border"
564
+ style={{
565
+ boxShadow: "0 10px 20px -5px rgba(29, 53, 87, 0.25)"
566
+ }}
567
+ >
568
+ {locale === "hi" ? "शुरू करें" : "Get Started"}
569
+ </button>
570
+ </SignInButton>
571
+ </Show>
572
  <Link
573
  href={`/${locale}/ledger`}
574
  className="inline-flex items-center justify-center bg-card text-foreground border border-border rounded-xl px-7 py-3.5 font-bold text-[15px] text-decoration-none transition-all duration-200 hover:bg-surface-elevated hover:-translate-y-0.5"
apps/web/src/app/[locale]/profile/page.tsx ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { useUser } from "@clerk/nextjs";
5
+ import { useLocale } from "next-intl";
6
+ import { useRouter } from "next/navigation";
7
+ import { api, setAuthTokenGetter } from "@/lib/api";
8
+ import { useAuthToken } from "@/lib/auth-token";
9
+ import { Button } from "@/components/ui/button";
10
+ import { Input } from "@/components/ui/input";
11
+ import { Label } from "@/components/ui/label";
12
+ import { ArrowLeft, Save, User, Phone, Globe, Mail } from "lucide-react";
13
+ import type { UserProfile, UserMissionSummary } from "@/lib/api";
14
+
15
+ export default function ProfilePage() {
16
+ const locale = useLocale();
17
+ const router = useRouter();
18
+ const { user } = useUser();
19
+ const { getToken } = useAuthToken();
20
+
21
+ const [profile, setProfile] = useState<UserProfile | null>(null);
22
+ const [missions, setMissions] = useState<UserMissionSummary[]>([]);
23
+ const [loading, setLoading] = useState(true);
24
+ const [saving, setSaving] = useState(false);
25
+
26
+ const [phone, setPhone] = useState("");
27
+ const [fullName, setFullName] = useState("");
28
+ const [preferredLanguage, setPreferredLanguage] = useState("en");
29
+
30
+ useEffect(() => {
31
+ if (getToken) setAuthTokenGetter(getToken);
32
+ }, [getToken]);
33
+
34
+ useEffect(() => {
35
+ async function load() {
36
+ try {
37
+ const [p, m] = await Promise.all([api.getProfile(), api.getUserMissions()]);
38
+ setProfile(p);
39
+ setMissions(m.items);
40
+ setPhone(p.phone ?? "");
41
+ setFullName(p.fullName ?? "");
42
+ setPreferredLanguage(p.preferredLanguage ?? "en");
43
+ } catch (e) {
44
+ console.error("Failed to load profile", e);
45
+ } finally {
46
+ setLoading(false);
47
+ }
48
+ }
49
+ void load();
50
+ }, []);
51
+
52
+ async function handleSave() {
53
+ setSaving(true);
54
+ try {
55
+ const updated = await api.updateProfile({
56
+ phone: phone || undefined,
57
+ fullName: fullName || undefined,
58
+ preferredLanguage,
59
+ });
60
+ setProfile(updated);
61
+ } finally {
62
+ setSaving(false);
63
+ }
64
+ }
65
+
66
+ if (loading) {
67
+ return (
68
+ <div className="w-full min-h-screen bg-background flex items-center justify-center">
69
+ <div className="animate-pulse text-foreground-muted font-sans">Loading profile…</div>
70
+ </div>
71
+ );
72
+ }
73
+
74
+ return (
75
+ <div className="w-full min-h-screen bg-background">
76
+ <div className="max-w-3xl mx-auto px-5 py-10">
77
+ {/* Back button */}
78
+ <button
79
+ onClick={() => router.push(`/${locale}`)}
80
+ className="flex items-center gap-2 text-sm font-medium text-foreground-muted hover:text-foreground transition-colors mb-8 cursor-pointer"
81
+ >
82
+ <ArrowLeft className="w-4 h-4" />
83
+ {locale === "hi" ? "वापस" : "Back"}
84
+ </button>
85
+
86
+ {/* Header */}
87
+ <div className="mb-10">
88
+ <h1 className="text-3xl font-bold text-foreground font-serif tracking-tight mb-2">
89
+ {locale === "hi" ? "प्रोफ़ाइल" : "Profile"}
90
+ </h1>
91
+ <p className="text-foreground-muted font-sans">
92
+ {locale === "hi" ? "अपनी जानकारी और मिशन देखें" : "View your info and missions"}
93
+ </p>
94
+ </div>
95
+
96
+ {/* Identity Card */}
97
+ <div className="bg-card border border-border rounded-2xl p-6 mb-8">
98
+ <div className="flex items-center gap-4 mb-6">
99
+ {user?.imageUrl ? (
100
+ <img
101
+ src={user.imageUrl}
102
+ alt="avatar"
103
+ className="w-16 h-16 rounded-full border-2 border-border object-cover"
104
+ />
105
+ ) : (
106
+ <div className="w-16 h-16 rounded-full bg-surface-sunken flex items-center justify-center border-2 border-border">
107
+ <User className="w-7 h-7 text-foreground-muted" />
108
+ </div>
109
+ )}
110
+ <div>
111
+ <div className="text-lg font-bold text-foreground font-serif">
112
+ {profile?.fullName || user?.fullName || profile?.displayName || "User"}
113
+ </div>
114
+ <div className="flex items-center gap-1.5 text-sm text-foreground-muted font-sans">
115
+ <Mail className="w-3.5 h-3.5" />
116
+ {profile?.email || user?.primaryEmailAddress?.emailAddress || "—"}
117
+ </div>
118
+ </div>
119
+ </div>
120
+
121
+ {/* Editable fields */}
122
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-5">
123
+ <div className="space-y-2">
124
+ <Label className="flex items-center gap-1.5 text-sm font-medium text-foreground-muted font-sans">
125
+ <Phone className="w-3.5 h-3.5" />
126
+ {locale === "hi" ? "फ़ोन" : "Phone"}
127
+ </Label>
128
+ <Input
129
+ value={phone}
130
+ onChange={(e) => setPhone(e.target.value)}
131
+ placeholder="+91 98765 43210"
132
+ className="bg-background border-border font-sans"
133
+ />
134
+ </div>
135
+
136
+ <div className="space-y-2">
137
+ <Label className="flex items-center gap-1.5 text-sm font-medium text-foreground-muted font-sans">
138
+ <Globe className="w-3.5 h-3.5" />
139
+ {locale === "hi" ? "भाषा" : "Language"}
140
+ </Label>
141
+ <select
142
+ value={preferredLanguage}
143
+ onChange={(e) => setPreferredLanguage(e.target.value)}
144
+ className="w-full h-10 px-3 rounded-md border border-border bg-background text-sm font-sans focus:outline-none focus:ring-2 focus:ring-primary-ring"
145
+ >
146
+ <option value="en">English</option>
147
+ <option value="hi">हिन्दी</option>
148
+ </select>
149
+ </div>
150
+ </div>
151
+
152
+ <div className="mt-6 flex justify-end">
153
+ <Button
154
+ onClick={handleSave}
155
+ disabled={saving}
156
+ className="bg-primary text-white hover:bg-primary-hover font-sans"
157
+ >
158
+ <Save className="w-4 h-4 mr-2" />
159
+ {saving ? (locale === "hi" ? "सहेज रहा है…" : "Saving…") : (locale === "hi" ? "सहेजें" : "Save Profile")}
160
+ </Button>
161
+ </div>
162
+ </div>
163
+
164
+ {/* My Missions */}
165
+ <div>
166
+ <h2 className="text-xl font-bold text-foreground font-serif mb-4">
167
+ {locale === "hi" ? "मेरे मिशन" : "My Missions"}
168
+ </h2>
169
+
170
+ {missions.length === 0 ? (
171
+ <div className="bg-card border border-border rounded-2xl p-8 text-center">
172
+ <p className="text-foreground-muted font-sans mb-4">
173
+ {locale === "hi" ? "अभी तक कोई मिशन नहीं।" : "No missions yet."}
174
+ </p>
175
+ <button
176
+ onClick={() => router.push(`/${locale}`)}
177
+ className="text-primary font-medium hover:underline font-sans cursor-pointer"
178
+ >
179
+ {locale === "hi" ? "पहला मिशन शुरू करें" : "Start your first mission"}
180
+ </button>
181
+ </div>
182
+ ) : (
183
+ <div className="space-y-3">
184
+ {missions.map((m) => (
185
+ <button
186
+ key={m.id}
187
+ onClick={() => router.push(`/${locale}/missions/${m.id}`)}
188
+ className="w-full text-left bg-card border border-border rounded-xl p-4 hover:bg-surface-elevated transition-colors cursor-pointer"
189
+ >
190
+ <div className="flex items-center justify-between mb-1">
191
+ <span className="font-medium text-foreground font-sans text-sm truncate pr-4">
192
+ {m.title || "Untitled Mission"}
193
+ </span>
194
+ <span className="text-[10px] font-bold uppercase tracking-wider px-2 py-1 rounded-full bg-surface-sunken text-foreground-muted font-sans whitespace-nowrap">
195
+ {m.state}
196
+ </span>
197
+ </div>
198
+ <div className="text-xs text-foreground-muted font-sans">
199
+ {m.issueDomain} · {new Date(m.createdAt).toLocaleDateString(locale === "hi" ? "hi-IN" : "en-IN")}
200
+ </div>
201
+ </button>
202
+ ))}
203
+ </div>
204
+ )}
205
+ </div>
206
+ </div>
207
+ </div>
208
+ );
209
+ }
apps/web/src/app/[locale]/sign-in/[[...sign-in]]/page.tsx ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { SignIn } from "@clerk/nextjs";
2
+ import type { Metadata } from "next";
3
+
4
+ export const metadata: Metadata = {
5
+ title: "Sign In — CourtMitra",
6
+ };
7
+
8
+ export default function SignInPage() {
9
+ return (
10
+ <div
11
+ style={{
12
+ display: "flex",
13
+ alignItems: "center",
14
+ justifyContent: "center",
15
+ minHeight: "100vh",
16
+ padding: 24,
17
+ background: "var(--background)",
18
+ }}
19
+ >
20
+ <SignIn routing="hash" signUpUrl="/en/sign-up" />
21
+ </div>
22
+ );
23
+ }
apps/web/src/app/[locale]/sign-up/[[...sign-up]]/page.tsx ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { SignUp } from "@clerk/nextjs";
2
+ import type { Metadata } from "next";
3
+
4
+ export const metadata: Metadata = {
5
+ title: "Sign Up — CourtMitra",
6
+ };
7
+
8
+ export default function SignUpPage() {
9
+ return (
10
+ <div
11
+ style={{
12
+ display: "flex",
13
+ alignItems: "center",
14
+ justifyContent: "center",
15
+ minHeight: "100vh",
16
+ padding: 24,
17
+ background: "var(--background)",
18
+ }}
19
+ >
20
+ <SignUp routing="hash" signInUrl="/en/sign-in" />
21
+ </div>
22
+ );
23
+ }
apps/web/src/app/layout.tsx CHANGED
@@ -1,3 +1,4 @@
 
1
  import type { Metadata } from "next";
2
  import type { ReactNode } from "react";
3
  import "./globals.css";
@@ -7,13 +8,12 @@ export const metadata: Metadata = {
7
  description: "Citizen grievance / legal-action autopilot for India.",
8
  };
9
 
10
- // Root layout — minimal HTML shell.
11
- // Fonts come from the @import in globals.css (Noto Sans + Noto Serif).
12
- // All i18n, nav, and providers live in app/[locale]/layout.tsx.
13
  export default function RootLayout({ children }: { children: ReactNode }) {
14
  return (
15
- <html lang="en" suppressHydrationWarning>
16
- <body suppressHydrationWarning>{children}</body>
17
- </html>
 
 
18
  );
19
  }
 
1
+ import { ClerkProvider } from "@clerk/nextjs";
2
  import type { Metadata } from "next";
3
  import type { ReactNode } from "react";
4
  import "./globals.css";
 
8
  description: "Citizen grievance / legal-action autopilot for India.",
9
  };
10
 
 
 
 
11
  export default function RootLayout({ children }: { children: ReactNode }) {
12
  return (
13
+ <ClerkProvider>
14
+ <html lang="en" suppressHydrationWarning>
15
+ <body suppressHydrationWarning>{children}</body>
16
+ </html>
17
+ </ClerkProvider>
18
  );
19
  }
apps/web/src/lib/api.ts CHANGED
@@ -21,10 +21,30 @@ const API_BASE = (typeof window === "undefined" ? process.env.INTERNAL_API_BASE_
21
  || process.env.NEXT_PUBLIC_API_BASE_URL
22
  || "http://localhost:3001/api";
23
 
 
 
 
 
 
 
 
24
  async function http<T>(path: string, init?: RequestInit): Promise<T> {
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  const res = await fetch(`${API_BASE}${path}`, {
26
  ...init,
27
- headers: { "content-type": "application/json", ...(init?.headers ?? {}) },
28
  cache: "no-store",
29
  });
30
  if (!res.ok) {
@@ -38,8 +58,6 @@ async function http<T>(path: string, init?: RequestInit): Promise<T> {
38
  // Evidence endpoint response types — use contract types (invariant #6).
39
  // ---------------------------------------------------------------------------
40
 
41
- // Re-export contract types for consumers (EvidenceClaimDto has no `id` field,
42
- // so we extend it with the DB id here at the API boundary).
43
  export type { ParsedEvidenceDto, EvidenceClaimDto };
44
 
45
  export interface EvidenceClaimWithId extends EvidenceClaimDto {
@@ -52,7 +70,6 @@ export interface EvidenceItem {
52
  sourceChannel: string | null;
53
  captureDate: string | null;
54
  piiLevel: string;
55
- /** URL pointing to GET /api/files/:key — or null if no file attached */
56
  fileUrl: string | null;
57
  parsed: ParsedEvidenceDto | null;
58
  claims: EvidenceClaimWithId[];
@@ -62,7 +79,6 @@ export interface EvidenceListResponse {
62
  items: EvidenceItem[];
63
  }
64
 
65
- // Re-export RoutePlanDto for consumers.
66
  export type { RoutePlanDto };
67
 
68
  // ---------------------------------------------------------------------------
@@ -73,9 +89,30 @@ export interface FactsResponse {
73
  facts: Record<string, unknown> | null;
74
  }
75
 
76
- /** Convenience: typed view of the `intake` sub-object within mission.facts */
77
  export type IntakeFacts = Partial<IntakeResult> & Record<string, unknown>;
78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  // ---------------------------------------------------------------------------
80
  // API surface
81
  // ---------------------------------------------------------------------------
@@ -93,16 +130,9 @@ export const api = {
93
  body: JSON.stringify(input),
94
  }),
95
 
96
- /** GET /api/missions/:id/facts — returns mission.facts jsonb or null */
97
  getFacts: (id: string) => http<FactsResponse>(`/missions/${id}/facts`),
98
-
99
- /** GET /api/missions/:id/evidence — list of evidence items with parsed outputs + claims */
100
  getEvidence: (id: string) => http<EvidenceListResponse>(`/missions/${id}/evidence`),
101
 
102
- /**
103
- * POST /api/missions/:id/facts/confirm
104
- * Confirms (and optionally edits) extracted facts; advances snapshot to facts_confirmed.
105
- */
106
  confirmFacts: (id: string, facts: Record<string, unknown>) =>
107
  http<{ ok: boolean; state: string }>(`/missions/${id}/facts/confirm`, {
108
  method: "POST",
@@ -110,16 +140,8 @@ export const api = {
110
  body: JSON.stringify({ facts }),
111
  }),
112
 
113
- /**
114
- * GET /api/missions/:id/route-plan
115
- * Returns the latest non-superseded RoutePlanDto, or throws 404 if not ready.
116
- */
117
  getRoutePlan: (id: string) => http<RoutePlanDto>(`/missions/${id}/route-plan`),
118
 
119
- /**
120
- * POST /api/missions/:id/route-plan
121
- * Triggers route plan generation; returns 202 Accepted.
122
- */
123
  triggerRoutePlan: (id: string) =>
124
  http<{ missionId: string }>(`/missions/${id}/route-plan`, {
125
  method: "POST",
@@ -127,7 +149,6 @@ export const api = {
127
  body: JSON.stringify({}),
128
  }),
129
 
130
- // --- Action endpoints ---
131
  previewAction: (missionId: string, input: CreateActionPreviewInput) =>
132
  http<ActionPreviewDto>(`/missions/${missionId}/actions/preview`, {
133
  method: "POST",
@@ -163,7 +184,6 @@ export const api = {
163
  },
164
  ),
165
 
166
- // --- Day 5: Resolution ---
167
  confirmResolution: (missionId: string, input: ConfirmResolutionInput) =>
168
  http<ResolutionOutcomeDto>(`/missions/${missionId}/resolution/confirm`, {
169
  method: "POST",
@@ -182,4 +202,13 @@ export const api = {
182
 
183
  getPublicPage: (missionId: string) =>
184
  http<{ slug: string; url: string }>(`/missions/${missionId}/public-page`),
 
 
 
 
 
 
 
 
 
185
  };
 
21
  || process.env.NEXT_PUBLIC_API_BASE_URL
22
  || "http://localhost:3001/api";
23
 
24
+ /** Global token getter — set by AuthTokenProvider on client mount. */
25
+ let getAuthToken: (() => Promise<string | null>) | null = null;
26
+
27
+ export function setAuthTokenGetter(fn: () => Promise<string | null>) {
28
+ getAuthToken = fn;
29
+ }
30
+
31
  async function http<T>(path: string, init?: RequestInit): Promise<T> {
32
+ const headers: Record<string, string> = {
33
+ "content-type": "application/json",
34
+ ...(init?.headers as Record<string, string> ?? {}),
35
+ };
36
+
37
+ // Add Clerk JWT if available (client-side only)
38
+ if (typeof window !== "undefined" && getAuthToken) {
39
+ const token = await getAuthToken();
40
+ if (token) {
41
+ headers["Authorization"] = `Bearer ${token}`;
42
+ }
43
+ }
44
+
45
  const res = await fetch(`${API_BASE}${path}`, {
46
  ...init,
47
+ headers,
48
  cache: "no-store",
49
  });
50
  if (!res.ok) {
 
58
  // Evidence endpoint response types — use contract types (invariant #6).
59
  // ---------------------------------------------------------------------------
60
 
 
 
61
  export type { ParsedEvidenceDto, EvidenceClaimDto };
62
 
63
  export interface EvidenceClaimWithId extends EvidenceClaimDto {
 
70
  sourceChannel: string | null;
71
  captureDate: string | null;
72
  piiLevel: string;
 
73
  fileUrl: string | null;
74
  parsed: ParsedEvidenceDto | null;
75
  claims: EvidenceClaimWithId[];
 
79
  items: EvidenceItem[];
80
  }
81
 
 
82
  export type { RoutePlanDto };
83
 
84
  // ---------------------------------------------------------------------------
 
89
  facts: Record<string, unknown> | null;
90
  }
91
 
 
92
  export type IntakeFacts = Partial<IntakeResult> & Record<string, unknown>;
93
 
94
+ // ---------------------------------------------------------------------------
95
+ // Profile types
96
+ // ---------------------------------------------------------------------------
97
+
98
+ export interface UserProfile {
99
+ id: string;
100
+ displayName: string | null;
101
+ email: string | null;
102
+ avatarUrl: string | null;
103
+ phone: string | null;
104
+ fullName: string | null;
105
+ preferredLanguage: string | null;
106
+ }
107
+
108
+ export interface UserMissionSummary {
109
+ id: string;
110
+ title: string | null;
111
+ state: string;
112
+ issueDomain: string | null;
113
+ createdAt: string;
114
+ }
115
+
116
  // ---------------------------------------------------------------------------
117
  // API surface
118
  // ---------------------------------------------------------------------------
 
130
  body: JSON.stringify(input),
131
  }),
132
 
 
133
  getFacts: (id: string) => http<FactsResponse>(`/missions/${id}/facts`),
 
 
134
  getEvidence: (id: string) => http<EvidenceListResponse>(`/missions/${id}/evidence`),
135
 
 
 
 
 
136
  confirmFacts: (id: string, facts: Record<string, unknown>) =>
137
  http<{ ok: boolean; state: string }>(`/missions/${id}/facts/confirm`, {
138
  method: "POST",
 
140
  body: JSON.stringify({ facts }),
141
  }),
142
 
 
 
 
 
143
  getRoutePlan: (id: string) => http<RoutePlanDto>(`/missions/${id}/route-plan`),
144
 
 
 
 
 
145
  triggerRoutePlan: (id: string) =>
146
  http<{ missionId: string }>(`/missions/${id}/route-plan`, {
147
  method: "POST",
 
149
  body: JSON.stringify({}),
150
  }),
151
 
 
152
  previewAction: (missionId: string, input: CreateActionPreviewInput) =>
153
  http<ActionPreviewDto>(`/missions/${missionId}/actions/preview`, {
154
  method: "POST",
 
184
  },
185
  ),
186
 
 
187
  confirmResolution: (missionId: string, input: ConfirmResolutionInput) =>
188
  http<ResolutionOutcomeDto>(`/missions/${missionId}/resolution/confirm`, {
189
  method: "POST",
 
202
 
203
  getPublicPage: (missionId: string) =>
204
  http<{ slug: string; url: string }>(`/missions/${missionId}/public-page`),
205
+
206
+ // --- Profile endpoints ---
207
+ getProfile: () => http<UserProfile>("/users/me"),
208
+ updateProfile: (data: { phone?: string; fullName?: string; preferredLanguage?: string }) =>
209
+ http<UserProfile>("/users/me", {
210
+ method: "PATCH",
211
+ body: JSON.stringify(data),
212
+ }),
213
+ getUserMissions: () => http<{ items: UserMissionSummary[] }>("/users/me/missions"),
214
  };
apps/web/src/lib/auth-token.tsx ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useAuth } from "@clerk/nextjs";
4
+ import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
5
+
6
+ interface AuthTokenContextType {
7
+ getToken: () => Promise<string | null>;
8
+ isLoaded: boolean;
9
+ }
10
+
11
+ const AuthTokenContext = createContext<AuthTokenContextType>({
12
+ getToken: async () => null,
13
+ isLoaded: false,
14
+ });
15
+
16
+ export function useAuthToken() {
17
+ return useContext(AuthTokenContext);
18
+ }
19
+
20
+ /** Provides a getToken function that the API client can call on every request. */
21
+ export function AuthTokenProvider({ children }: { children: ReactNode }) {
22
+ const { getToken: clerkGetToken, isLoaded } = useAuth();
23
+ const [token, setToken] = useState<string | null>(null);
24
+
25
+ // Refresh token periodically (Clerk tokens expire every ~1 min)
26
+ useEffect(() => {
27
+ if (!isLoaded) return;
28
+
29
+ const refresh = async () => {
30
+ const t = await clerkGetToken();
31
+ setToken(t);
32
+ };
33
+
34
+ refresh();
35
+ const interval = setInterval(refresh, 45000); // refresh every 45s
36
+ return () => clearInterval(interval);
37
+ }, [isLoaded, clerkGetToken]);
38
+
39
+ const getToken = async () => {
40
+ if (token) return token;
41
+ return clerkGetToken();
42
+ };
43
+
44
+ return (
45
+ <AuthTokenContext.Provider value={{ getToken, isLoaded }}>
46
+ {children}
47
+ </AuthTokenContext.Provider>
48
+ );
49
+ }
packages/db/src/migrations/0011_blushing_puma.sql ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ ALTER TABLE "users" ADD COLUMN "email" text;--> statement-breakpoint
2
+ ALTER TABLE "users" ADD COLUMN "avatar_url" text;--> statement-breakpoint
3
+ ALTER TABLE "users" ADD COLUMN "phone" text;--> statement-breakpoint
4
+ ALTER TABLE "users" ADD COLUMN "full_name" text;
packages/db/src/migrations/meta/0011_snapshot.json ADDED
The diff for this file is too large to render. See raw diff
 
packages/db/src/migrations/meta/_journal.json CHANGED
@@ -78,6 +78,13 @@
78
  "when": 1780034770882,
79
  "tag": "0010_amusing_runaways",
80
  "breakpoints": true
 
 
 
 
 
 
 
81
  }
82
  ]
83
  }
 
78
  "when": 1780034770882,
79
  "tag": "0010_amusing_runaways",
80
  "breakpoints": true
81
+ },
82
+ {
83
+ "idx": 11,
84
+ "version": "7",
85
+ "when": 1780245420535,
86
+ "tag": "0011_blushing_puma",
87
+ "breakpoints": true
88
  }
89
  ]
90
  }
packages/db/src/schema.ts CHANGED
@@ -144,6 +144,10 @@ export const localizedTexts = pgTable("localized_texts", {
144
  export const users = pgTable("users", {
145
  id: id(),
146
  displayName: text("display_name"),
 
 
 
 
147
  preferredLanguage: text("preferred_language").default("en"),
148
  riskFlags: jsonb("risk_flags"),
149
  createdAt: createdAt(),
 
144
  export const users = pgTable("users", {
145
  id: id(),
146
  displayName: text("display_name"),
147
+ email: text("email"),
148
+ avatarUrl: text("avatar_url"),
149
+ phone: text("phone"),
150
+ fullName: text("full_name"),
151
  preferredLanguage: text("preferred_language").default("en"),
152
  riskFlags: jsonb("risk_flags"),
153
  createdAt: createdAt(),
pnpm-lock.yaml CHANGED
@@ -33,6 +33,9 @@ importers:
33
 
34
  apps/api:
35
  dependencies:
 
 
 
36
  '@courtmitra/adapters':
37
  specifier: workspace:*
38
  version: link:../../packages/adapters
@@ -79,6 +82,9 @@ importers:
79
  specifier: ^4.4.3
80
  version: 4.4.3
81
  devDependencies:
 
 
 
82
  '@types/node':
83
  specifier: ^22.10.2
84
  version: 22.19.19
@@ -88,6 +94,9 @@ importers:
88
 
89
  apps/web:
90
  dependencies:
 
 
 
91
  '@courtmitra/contracts':
92
  specifier: workspace:*
93
  version: link:../../packages/contracts
@@ -639,6 +648,80 @@ packages:
639
  '@cfworker/json-schema@4.1.1':
640
  resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==}
641
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
642
  '@connectrpc/connect-node@2.1.1':
643
  resolution: {integrity: sha512-s3TfsI1XF+n+1z6MBS9rTnFsxxR4Rw5wmdEnkQINli81ESGxcsfaEet8duzq8LVuuCupmhUsgpRo0Nv9pZkufg==}
644
  engines: {node: '>=20'}
@@ -3288,6 +3371,9 @@ packages:
3288
  '@tailwindcss/postcss@4.3.0':
3289
  resolution: {integrity: sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w==}
3290
 
 
 
 
3291
  '@tokenizer/inflate@0.4.1':
3292
  resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==}
3293
  engines: {node: '>=18'}
@@ -3342,6 +3428,9 @@ packages:
3342
  '@types/aws-lambda@8.10.161':
3343
  resolution: {integrity: sha512-rUYdp+MQwSFocxIOcSsYSF3YYYC/uUpMbCY/mbO21vGqfrEYvNSoPyKYDj6RhXXpPfS0KstW9RwG3qXh9sL7FQ==}
3344
 
 
 
 
3345
  '@types/bunyan@1.8.11':
3346
  resolution: {integrity: sha512-758fRH7umIMk5qt5ELmRMff4mLDlN+xyYzC+dkPTdKwbSkJFvz6xwyScrytPU0QIBbRRwbiE8/BIg8bpajerNQ==}
3347
 
@@ -3360,6 +3449,15 @@ packages:
3360
  '@types/estree@1.0.9':
3361
  resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
3362
 
 
 
 
 
 
 
 
 
 
3363
  '@types/json-schema@7.0.15':
3364
  resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
3365
 
@@ -3393,6 +3491,12 @@ packages:
3393
  '@types/pg@8.20.0':
3394
  resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==}
3395
 
 
 
 
 
 
 
3396
  '@types/react-dom@19.2.3':
3397
  resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
3398
  peerDependencies:
@@ -3404,6 +3508,12 @@ packages:
3404
  '@types/retry@0.12.0':
3405
  resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==}
3406
 
 
 
 
 
 
 
3407
  '@types/set-cookie-parser@2.4.10':
3408
  resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==}
3409
 
@@ -3812,6 +3922,10 @@ packages:
3812
  resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
3813
  engines: {node: '>= 0.6'}
3814
 
 
 
 
 
3815
  cookie@1.1.1:
3816
  resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
3817
  engines: {node: '>=18'}
@@ -3841,6 +3955,9 @@ packages:
3841
  engines: {node: '>=4'}
3842
  hasBin: true
3843
 
 
 
 
3844
  csstype@3.2.3:
3845
  resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
3846
 
@@ -3917,6 +4034,9 @@ packages:
3917
  resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==}
3918
  engines: {node: '>=0.3.1'}
3919
 
 
 
 
3920
  dotenv@17.4.2:
3921
  resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==}
3922
  engines: {node: '>=12'}
@@ -4409,6 +4529,9 @@ packages:
4409
  resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
4410
  engines: {node: '>=10.13.0'}
4411
 
 
 
 
4412
  globals@14.0.0:
4413
  resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
4414
  engines: {node: '>=18'}
@@ -4681,6 +4804,14 @@ packages:
4681
  resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
4682
  engines: {node: '>=10'}
4683
 
 
 
 
 
 
 
 
 
4684
  js-tiktoken@1.0.21:
4685
  resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==}
4686
 
@@ -4872,6 +5003,9 @@ packages:
4872
  long@5.3.2:
4873
  resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
4874
 
 
 
 
4875
  lru-cache@5.1.1:
4876
  resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
4877
 
@@ -4883,6 +5017,10 @@ packages:
4883
  magic-string@0.30.21:
4884
  resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
4885
 
 
 
 
 
4886
  marky@1.3.0:
4887
  resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==}
4888
 
@@ -5022,6 +5160,9 @@ packages:
5022
  sass:
5023
  optional: true
5024
 
 
 
 
5025
  node-addon-api@7.1.1:
5026
  resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
5027
 
@@ -5553,6 +5694,9 @@ packages:
5553
  resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==}
5554
  engines: {node: '>= 18'}
5555
 
 
 
 
5556
  set-cookie-parser@2.7.2:
5557
  resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
5558
 
@@ -5610,6 +5754,13 @@ packages:
5610
  sisteransi@1.0.5:
5611
  resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
5612
 
 
 
 
 
 
 
 
5613
  sonic-boom@4.2.1:
5614
  resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==}
5615
 
@@ -5644,6 +5795,9 @@ packages:
5644
  resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
5645
  engines: {node: '>= 0.8'}
5646
 
 
 
 
5647
  std-env@4.1.0:
5648
  resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==}
5649
 
@@ -5719,6 +5873,16 @@ packages:
5719
  resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
5720
  engines: {node: '>= 0.4'}
5721
 
 
 
 
 
 
 
 
 
 
 
5722
  tagged-tag@1.0.0:
5723
  resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
5724
  engines: {node: '>=20'}
@@ -5807,6 +5971,9 @@ packages:
5807
  resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==}
5808
  engines: {node: '>=6'}
5809
 
 
 
 
5810
  tslib@2.8.1:
5811
  resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
5812
 
@@ -5826,6 +5993,10 @@ packages:
5826
  resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
5827
  engines: {node: '>= 0.8.0'}
5828
 
 
 
 
 
5829
  type-fest@5.6.0:
5830
  resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==}
5831
  engines: {node: '>=20'}
@@ -6576,6 +6747,97 @@ snapshots:
6576
 
6577
  '@cfworker/json-schema@4.1.1': {}
6578
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6579
  '@connectrpc/connect-node@2.1.1(@bufbuild/protobuf@2.12.0)(@connectrpc/connect@2.1.1(@bufbuild/protobuf@2.12.0))':
6580
  dependencies:
6581
  '@bufbuild/protobuf': 2.12.0
@@ -9171,6 +9433,8 @@ snapshots:
9171
  postcss: 8.5.15
9172
  tailwindcss: 4.3.0
9173
 
 
 
9174
  '@tokenizer/inflate@0.4.1':
9175
  dependencies:
9176
  debug: 4.4.3
@@ -9226,6 +9490,11 @@ snapshots:
9226
 
9227
  '@types/aws-lambda@8.10.161': {}
9228
 
 
 
 
 
 
9229
  '@types/bunyan@1.8.11':
9230
  dependencies:
9231
  '@types/node': 22.19.19
@@ -9247,6 +9516,21 @@ snapshots:
9247
 
9248
  '@types/estree@1.0.9': {}
9249
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9250
  '@types/json-schema@7.0.15': {}
9251
 
9252
  '@types/memcached@2.2.10':
@@ -9292,6 +9576,10 @@ snapshots:
9292
  pg-protocol: 1.14.0
9293
  pg-types: 2.2.0
9294
 
 
 
 
 
9295
  '@types/react-dom@19.2.3(@types/react@19.2.15)':
9296
  dependencies:
9297
  '@types/react': 19.2.15
@@ -9302,6 +9590,15 @@ snapshots:
9302
 
9303
  '@types/retry@0.12.0': {}
9304
 
 
 
 
 
 
 
 
 
 
9305
  '@types/set-cookie-parser@2.4.10':
9306
  dependencies:
9307
  '@types/node': 22.19.19
@@ -9715,6 +10012,8 @@ snapshots:
9715
 
9716
  cookie@0.7.2: {}
9717
 
 
 
9718
  cookie@1.1.1: {}
9719
 
9720
  cors@2.8.6:
@@ -9745,6 +10044,8 @@ snapshots:
9745
 
9746
  cssesc@3.0.0: {}
9747
 
 
 
9748
  csstype@3.2.3: {}
9749
 
9750
  data-uri-to-buffer@4.0.1: {}
@@ -9786,6 +10087,11 @@ snapshots:
9786
 
9787
  diff@8.0.4: {}
9788
 
 
 
 
 
 
9789
  dotenv@17.4.2: {}
9790
 
9791
  drizzle-kit@0.31.10:
@@ -10350,6 +10656,8 @@ snapshots:
10350
  dependencies:
10351
  is-glob: 4.0.3
10352
 
 
 
10353
  globals@14.0.0: {}
10354
 
10355
  google-auth-library@10.6.2:
@@ -10574,6 +10882,10 @@ snapshots:
10574
 
10575
  joycon@3.1.1: {}
10576
 
 
 
 
 
10577
  js-tiktoken@1.0.21:
10578
  dependencies:
10579
  base64-js: 1.5.1
@@ -10738,6 +11050,10 @@ snapshots:
10738
 
10739
  long@5.3.2: {}
10740
 
 
 
 
 
10741
  lru-cache@5.1.1:
10742
  dependencies:
10743
  yallist: 3.1.1
@@ -10750,6 +11066,8 @@ snapshots:
10750
  dependencies:
10751
  '@jridgewell/sourcemap-codec': 1.5.5
10752
 
 
 
10753
  marky@1.3.0:
10754
  optional: true
10755
 
@@ -10883,6 +11201,11 @@ snapshots:
10883
  - '@babel/core'
10884
  - babel-plugin-macros
10885
 
 
 
 
 
 
10886
  node-addon-api@7.1.1: {}
10887
 
10888
  node-domexception@1.0.0: {}
@@ -11509,6 +11832,8 @@ snapshots:
11509
  transitivePeerDependencies:
11510
  - supports-color
11511
 
 
 
11512
  set-cookie-parser@2.7.2: {}
11513
 
11514
  set-cookie-parser@3.1.0: {}
@@ -11633,6 +11958,17 @@ snapshots:
11633
 
11634
  sisteransi@1.0.5: {}
11635
 
 
 
 
 
 
 
 
 
 
 
 
11636
  sonic-boom@4.2.1:
11637
  dependencies:
11638
  atomic-sleep: 1.0.0
@@ -11662,6 +11998,8 @@ snapshots:
11662
 
11663
  statuses@2.0.2: {}
11664
 
 
 
11665
  std-env@4.1.0: {}
11666
 
11667
  stdin-discarder@0.2.2: {}
@@ -11721,6 +12059,18 @@ snapshots:
11721
 
11722
  supports-preserve-symlinks-flag@1.0.0: {}
11723
 
 
 
 
 
 
 
 
 
 
 
 
 
11724
  tagged-tag@1.0.0: {}
11725
 
11726
  tailwind-merge@3.6.0: {}
@@ -11797,6 +12147,8 @@ snapshots:
11797
  minimist: 1.2.8
11798
  strip-bom: 3.0.0
11799
 
 
 
11800
  tslib@2.8.1: {}
11801
 
11802
  tsx@4.22.3:
@@ -11820,6 +12172,8 @@ snapshots:
11820
  dependencies:
11821
  prelude-ls: 1.2.1
11822
 
 
 
11823
  type-fest@5.6.0:
11824
  dependencies:
11825
  tagged-tag: 1.0.0
 
33
 
34
  apps/api:
35
  dependencies:
36
+ '@clerk/clerk-sdk-node':
37
+ specifier: ^5.1.6
38
+ version: 5.1.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
39
  '@courtmitra/adapters':
40
  specifier: workspace:*
41
  version: link:../../packages/adapters
 
82
  specifier: ^4.4.3
83
  version: 4.4.3
84
  devDependencies:
85
+ '@types/express':
86
+ specifier: ^5.0.6
87
+ version: 5.0.6
88
  '@types/node':
89
  specifier: ^22.10.2
90
  version: 22.19.19
 
94
 
95
  apps/web:
96
  dependencies:
97
+ '@clerk/nextjs':
98
+ specifier: ^7.4.2
99
+ version: 7.4.2(next@15.5.18(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
100
  '@courtmitra/contracts':
101
  specifier: workspace:*
102
  version: link:../../packages/contracts
 
648
  '@cfworker/json-schema@4.1.1':
649
  resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==}
650
 
651
+ '@clerk/backend@1.34.0':
652
+ resolution: {integrity: sha512-9rZ8hQJVpX5KX2bEpiuVXfpjhojQCiqCWADJDdCI0PCeKxn58Ep0JPYiIcczg4VKUc3a7jve9vXylykG2XajLQ==}
653
+ engines: {node: '>=18.17.0'}
654
+ peerDependencies:
655
+ svix: ^1.62.0
656
+ peerDependenciesMeta:
657
+ svix:
658
+ optional: true
659
+
660
+ '@clerk/backend@3.4.14':
661
+ resolution: {integrity: sha512-0iaMT7k4wDk31QVC3HMaoeVFttblwsCECTHKNQpbRzIyD8j2gHdKEw/FNjffoyqyBqPw869IQlk1YokUlwVAqQ==}
662
+ engines: {node: '>=20.9.0'}
663
+
664
+ '@clerk/clerk-sdk-node@5.1.6':
665
+ resolution: {integrity: sha512-KeF5p0XP0gNoCx+YIHrfrkNNADBz8ZabwPAhOiJZ9Wo14r93WlzRA51IE0Qgteej8IpWwnvKu4/MpiV7FFoLqA==}
666
+ engines: {node: '>=18.17.0'}
667
+ deprecated: 'January 10 2025 marks the end of support for @clerk/clerk-sdk-node as previously announced in our October 2024 deprecation notice. Express users can migrate to the @clerk/express package. For more information, you can find our changelog here: https://clerk.com/changelog/2025-01-10-node-sdk-eol'
668
+
669
+ '@clerk/nextjs@7.4.2':
670
+ resolution: {integrity: sha512-DpqljtLkTtMNsi03XRWYaWYyt0tb4S07HjS9fSmUtN//CKNqM7QAVMZzpgMAZz5yb2QhIVIVF1Uir2c7f72+MA==}
671
+ engines: {node: '>=20.9.0'}
672
+ peerDependencies:
673
+ next: ^15.2.8 || ^15.3.8 || ^15.4.10 || ^15.5.9 || ^15.6.0-0 || ^16.0.10 || ^16.1.0-0
674
+ react: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0
675
+ react-dom: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0
676
+
677
+ '@clerk/react@6.7.2':
678
+ resolution: {integrity: sha512-tgRWWTfW+ejXj6WiZqx7iMbtvh45h0445uEvbSNVCyJBEOaBlCF0/ZfAfXlWAQNbRZ+bPIphEfNUmatadZgGOQ==}
679
+ engines: {node: '>=20.9.0'}
680
+ peerDependencies:
681
+ react: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0
682
+ react-dom: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0
683
+
684
+ '@clerk/shared@2.22.1':
685
+ resolution: {integrity: sha512-AaPMypQXJAQgJGhN8+5VRE6/VKT9D+fI8o1dCvqXjQBcegpZWyK6Tqe9QH54Wl6qVj12Hv4qh2yH5ejEflm2KQ==}
686
+ engines: {node: '>=18.17.0'}
687
+ peerDependencies:
688
+ react: ^18.0.0 || ^19.0.0 || ^19.0.0-0
689
+ react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-0
690
+ peerDependenciesMeta:
691
+ react:
692
+ optional: true
693
+ react-dom:
694
+ optional: true
695
+
696
+ '@clerk/shared@3.47.7':
697
+ resolution: {integrity: sha512-9Yv4MJFEaC7BzV0whxa4txQ4SoMu/3j1LBnI85EBykb5CcfXxIKvNX/9sjMUUySHlTOjsj7XZa5i3W5Dx02K/Q==}
698
+ engines: {node: '>=18.17.0'}
699
+ peerDependencies:
700
+ react: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0
701
+ react-dom: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0
702
+ peerDependenciesMeta:
703
+ react:
704
+ optional: true
705
+ react-dom:
706
+ optional: true
707
+
708
+ '@clerk/shared@4.14.0':
709
+ resolution: {integrity: sha512-StZCFJ2rg0ITE3fYjIN9CKuKq8ZACW6l2+5qGCFPPYd4hZphA+VDselmh/lHPsF1ex/WRVUSHvquykAHR8cQbQ==}
710
+ engines: {node: '>=20.9.0'}
711
+ peerDependencies:
712
+ react: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0
713
+ react-dom: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0
714
+ peerDependenciesMeta:
715
+ react:
716
+ optional: true
717
+ react-dom:
718
+ optional: true
719
+
720
+ '@clerk/types@4.101.20':
721
+ resolution: {integrity: sha512-MHvr1tGL5lwxD6zdzzixkHICbbZz/S1LChONKR52Qeu0yRGChZomPknsgqBMG9J3yNeyhaj0SWa5phQgQUk0lg==}
722
+ engines: {node: '>=18.17.0'}
723
+ deprecated: 'This package is no longer supported. Please import types from @clerk/shared/types instead. See the upgrade guide for more info: https://clerk.com/docs/guides/development/upgrading/upgrade-guides/core-3'
724
+
725
  '@connectrpc/connect-node@2.1.1':
726
  resolution: {integrity: sha512-s3TfsI1XF+n+1z6MBS9rTnFsxxR4Rw5wmdEnkQINli81ESGxcsfaEet8duzq8LVuuCupmhUsgpRo0Nv9pZkufg==}
727
  engines: {node: '>=20'}
 
3371
  '@tailwindcss/postcss@4.3.0':
3372
  resolution: {integrity: sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w==}
3373
 
3374
+ '@tanstack/query-core@5.100.14':
3375
+ resolution: {integrity: sha512-5X41dGpxgeaHISCRW2oYwcSycZeULZzAunaudXT9ov1KOTj9xwt0CH6hbwqP1/z74ZWF7rYFnDpyYH07XFcZew==}
3376
+
3377
  '@tokenizer/inflate@0.4.1':
3378
  resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==}
3379
  engines: {node: '>=18'}
 
3428
  '@types/aws-lambda@8.10.161':
3429
  resolution: {integrity: sha512-rUYdp+MQwSFocxIOcSsYSF3YYYC/uUpMbCY/mbO21vGqfrEYvNSoPyKYDj6RhXXpPfS0KstW9RwG3qXh9sL7FQ==}
3430
 
3431
+ '@types/body-parser@1.19.6':
3432
+ resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
3433
+
3434
  '@types/bunyan@1.8.11':
3435
  resolution: {integrity: sha512-758fRH7umIMk5qt5ELmRMff4mLDlN+xyYzC+dkPTdKwbSkJFvz6xwyScrytPU0QIBbRRwbiE8/BIg8bpajerNQ==}
3436
 
 
3449
  '@types/estree@1.0.9':
3450
  resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
3451
 
3452
+ '@types/express-serve-static-core@5.1.1':
3453
+ resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==}
3454
+
3455
+ '@types/express@5.0.6':
3456
+ resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==}
3457
+
3458
+ '@types/http-errors@2.0.5':
3459
+ resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
3460
+
3461
  '@types/json-schema@7.0.15':
3462
  resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
3463
 
 
3491
  '@types/pg@8.20.0':
3492
  resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==}
3493
 
3494
+ '@types/qs@6.15.1':
3495
+ resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==}
3496
+
3497
+ '@types/range-parser@1.2.7':
3498
+ resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
3499
+
3500
  '@types/react-dom@19.2.3':
3501
  resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
3502
  peerDependencies:
 
3508
  '@types/retry@0.12.0':
3509
  resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==}
3510
 
3511
+ '@types/send@1.2.1':
3512
+ resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==}
3513
+
3514
+ '@types/serve-static@2.2.0':
3515
+ resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==}
3516
+
3517
  '@types/set-cookie-parser@2.4.10':
3518
  resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==}
3519
 
 
3922
  resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
3923
  engines: {node: '>= 0.6'}
3924
 
3925
+ cookie@1.0.2:
3926
+ resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
3927
+ engines: {node: '>=18'}
3928
+
3929
  cookie@1.1.1:
3930
  resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
3931
  engines: {node: '>=18'}
 
3955
  engines: {node: '>=4'}
3956
  hasBin: true
3957
 
3958
+ csstype@3.1.3:
3959
+ resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
3960
+
3961
  csstype@3.2.3:
3962
  resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
3963
 
 
4034
  resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==}
4035
  engines: {node: '>=0.3.1'}
4036
 
4037
+ dot-case@3.0.4:
4038
+ resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
4039
+
4040
  dotenv@17.4.2:
4041
  resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==}
4042
  engines: {node: '>=12'}
 
4529
  resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
4530
  engines: {node: '>=10.13.0'}
4531
 
4532
+ glob-to-regexp@0.4.1:
4533
+ resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
4534
+
4535
  globals@14.0.0:
4536
  resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
4537
  engines: {node: '>=18'}
 
4804
  resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
4805
  engines: {node: '>=10'}
4806
 
4807
+ js-cookie@3.0.5:
4808
+ resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
4809
+ engines: {node: '>=14'}
4810
+
4811
+ js-cookie@3.0.7:
4812
+ resolution: {integrity: sha512-z/wZZgDrkNV1eA0ULjM/F9/50Ya8fbzgKneSpoPsXSGd0KnpdtHfOZWK+GcwLk+EZbS4F9RBhU+K2RgzuDaItw==}
4813
+ engines: {node: '>=20'}
4814
+
4815
  js-tiktoken@1.0.21:
4816
  resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==}
4817
 
 
5003
  long@5.3.2:
5004
  resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
5005
 
5006
+ lower-case@2.0.2:
5007
+ resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
5008
+
5009
  lru-cache@5.1.1:
5010
  resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
5011
 
 
5017
  magic-string@0.30.21:
5018
  resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
5019
 
5020
+ map-obj@4.3.0:
5021
+ resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==}
5022
+ engines: {node: '>=8'}
5023
+
5024
  marky@1.3.0:
5025
  resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==}
5026
 
 
5160
  sass:
5161
  optional: true
5162
 
5163
+ no-case@3.0.4:
5164
+ resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
5165
+
5166
  node-addon-api@7.1.1:
5167
  resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
5168
 
 
5694
  resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==}
5695
  engines: {node: '>= 18'}
5696
 
5697
+ server-only@0.0.1:
5698
+ resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==}
5699
+
5700
  set-cookie-parser@2.7.2:
5701
  resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
5702
 
 
5754
  sisteransi@1.0.5:
5755
  resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
5756
 
5757
+ snake-case@3.0.4:
5758
+ resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==}
5759
+
5760
+ snakecase-keys@8.0.1:
5761
+ resolution: {integrity: sha512-Sj51kE1zC7zh6TDlNNz0/Jn1n5HiHdoQErxO8jLtnyrkJW/M5PrI7x05uDgY3BO7OUQYKCvmeMurW6BPUdwEOw==}
5762
+ engines: {node: '>=18'}
5763
+
5764
  sonic-boom@4.2.1:
5765
  resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==}
5766
 
 
5795
  resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
5796
  engines: {node: '>= 0.8'}
5797
 
5798
+ std-env@3.10.0:
5799
+ resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
5800
+
5801
  std-env@4.1.0:
5802
  resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==}
5803
 
 
5873
  resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
5874
  engines: {node: '>= 0.4'}
5875
 
5876
+ swr@2.3.4:
5877
+ resolution: {integrity: sha512-bYd2lrhc+VarcpkgWclcUi92wYCpOgMws9Sd1hG1ntAu0NEy+14CbotuFjshBU2kt9rYj9TSmDcybpxpeTU1fg==}
5878
+ peerDependencies:
5879
+ react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
5880
+
5881
+ swr@2.4.1:
5882
+ resolution: {integrity: sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==}
5883
+ peerDependencies:
5884
+ react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
5885
+
5886
  tagged-tag@1.0.0:
5887
  resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
5888
  engines: {node: '>=20'}
 
5971
  resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==}
5972
  engines: {node: '>=6'}
5973
 
5974
+ tslib@2.4.1:
5975
+ resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==}
5976
+
5977
  tslib@2.8.1:
5978
  resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
5979
 
 
5993
  resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
5994
  engines: {node: '>= 0.8.0'}
5995
 
5996
+ type-fest@4.41.0:
5997
+ resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
5998
+ engines: {node: '>=16'}
5999
+
6000
  type-fest@5.6.0:
6001
  resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==}
6002
  engines: {node: '>=20'}
 
6747
 
6748
  '@cfworker/json-schema@4.1.1': {}
6749
 
6750
+ '@clerk/backend@1.34.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
6751
+ dependencies:
6752
+ '@clerk/shared': 3.47.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
6753
+ '@clerk/types': 4.101.20(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
6754
+ cookie: 1.0.2
6755
+ snakecase-keys: 8.0.1
6756
+ tslib: 2.8.1
6757
+ transitivePeerDependencies:
6758
+ - react
6759
+ - react-dom
6760
+
6761
+ '@clerk/backend@3.4.14(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
6762
+ dependencies:
6763
+ '@clerk/shared': 4.14.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
6764
+ standardwebhooks: 1.0.0
6765
+ tslib: 2.8.1
6766
+ transitivePeerDependencies:
6767
+ - react
6768
+ - react-dom
6769
+
6770
+ '@clerk/clerk-sdk-node@5.1.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
6771
+ dependencies:
6772
+ '@clerk/backend': 1.34.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
6773
+ '@clerk/shared': 2.22.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
6774
+ '@clerk/types': 4.101.20(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
6775
+ tslib: 2.4.1
6776
+ transitivePeerDependencies:
6777
+ - react
6778
+ - react-dom
6779
+ - svix
6780
+
6781
+ '@clerk/nextjs@7.4.2(next@15.5.18(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
6782
+ dependencies:
6783
+ '@clerk/backend': 3.4.14(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
6784
+ '@clerk/react': 6.7.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
6785
+ '@clerk/shared': 4.14.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
6786
+ next: 15.5.18(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
6787
+ react: 19.2.6
6788
+ react-dom: 19.2.6(react@19.2.6)
6789
+ server-only: 0.0.1
6790
+ tslib: 2.8.1
6791
+
6792
+ '@clerk/react@6.7.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
6793
+ dependencies:
6794
+ '@clerk/shared': 4.14.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
6795
+ react: 19.2.6
6796
+ react-dom: 19.2.6(react@19.2.6)
6797
+ tslib: 2.8.1
6798
+
6799
+ '@clerk/shared@2.22.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
6800
+ dependencies:
6801
+ '@clerk/types': 4.101.20(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
6802
+ dequal: 2.0.3
6803
+ glob-to-regexp: 0.4.1
6804
+ js-cookie: 3.0.5
6805
+ std-env: 3.10.0
6806
+ swr: 2.4.1(react@19.2.6)
6807
+ optionalDependencies:
6808
+ react: 19.2.6
6809
+ react-dom: 19.2.6(react@19.2.6)
6810
+
6811
+ '@clerk/shared@3.47.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
6812
+ dependencies:
6813
+ csstype: 3.1.3
6814
+ dequal: 2.0.3
6815
+ glob-to-regexp: 0.4.1
6816
+ js-cookie: 3.0.7
6817
+ std-env: 3.10.0
6818
+ swr: 2.3.4(react@19.2.6)
6819
+ optionalDependencies:
6820
+ react: 19.2.6
6821
+ react-dom: 19.2.6(react@19.2.6)
6822
+
6823
+ '@clerk/shared@4.14.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
6824
+ dependencies:
6825
+ '@tanstack/query-core': 5.100.14
6826
+ dequal: 2.0.3
6827
+ glob-to-regexp: 0.4.1
6828
+ js-cookie: 3.0.7
6829
+ std-env: 3.10.0
6830
+ optionalDependencies:
6831
+ react: 19.2.6
6832
+ react-dom: 19.2.6(react@19.2.6)
6833
+
6834
+ '@clerk/types@4.101.20(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
6835
+ dependencies:
6836
+ '@clerk/shared': 3.47.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
6837
+ transitivePeerDependencies:
6838
+ - react
6839
+ - react-dom
6840
+
6841
  '@connectrpc/connect-node@2.1.1(@bufbuild/protobuf@2.12.0)(@connectrpc/connect@2.1.1(@bufbuild/protobuf@2.12.0))':
6842
  dependencies:
6843
  '@bufbuild/protobuf': 2.12.0
 
9433
  postcss: 8.5.15
9434
  tailwindcss: 4.3.0
9435
 
9436
+ '@tanstack/query-core@5.100.14': {}
9437
+
9438
  '@tokenizer/inflate@0.4.1':
9439
  dependencies:
9440
  debug: 4.4.3
 
9490
 
9491
  '@types/aws-lambda@8.10.161': {}
9492
 
9493
+ '@types/body-parser@1.19.6':
9494
+ dependencies:
9495
+ '@types/connect': 3.4.38
9496
+ '@types/node': 22.19.19
9497
+
9498
  '@types/bunyan@1.8.11':
9499
  dependencies:
9500
  '@types/node': 22.19.19
 
9516
 
9517
  '@types/estree@1.0.9': {}
9518
 
9519
+ '@types/express-serve-static-core@5.1.1':
9520
+ dependencies:
9521
+ '@types/node': 22.19.19
9522
+ '@types/qs': 6.15.1
9523
+ '@types/range-parser': 1.2.7
9524
+ '@types/send': 1.2.1
9525
+
9526
+ '@types/express@5.0.6':
9527
+ dependencies:
9528
+ '@types/body-parser': 1.19.6
9529
+ '@types/express-serve-static-core': 5.1.1
9530
+ '@types/serve-static': 2.2.0
9531
+
9532
+ '@types/http-errors@2.0.5': {}
9533
+
9534
  '@types/json-schema@7.0.15': {}
9535
 
9536
  '@types/memcached@2.2.10':
 
9576
  pg-protocol: 1.14.0
9577
  pg-types: 2.2.0
9578
 
9579
+ '@types/qs@6.15.1': {}
9580
+
9581
+ '@types/range-parser@1.2.7': {}
9582
+
9583
  '@types/react-dom@19.2.3(@types/react@19.2.15)':
9584
  dependencies:
9585
  '@types/react': 19.2.15
 
9590
 
9591
  '@types/retry@0.12.0': {}
9592
 
9593
+ '@types/send@1.2.1':
9594
+ dependencies:
9595
+ '@types/node': 22.19.19
9596
+
9597
+ '@types/serve-static@2.2.0':
9598
+ dependencies:
9599
+ '@types/http-errors': 2.0.5
9600
+ '@types/node': 22.19.19
9601
+
9602
  '@types/set-cookie-parser@2.4.10':
9603
  dependencies:
9604
  '@types/node': 22.19.19
 
10012
 
10013
  cookie@0.7.2: {}
10014
 
10015
+ cookie@1.0.2: {}
10016
+
10017
  cookie@1.1.1: {}
10018
 
10019
  cors@2.8.6:
 
10044
 
10045
  cssesc@3.0.0: {}
10046
 
10047
+ csstype@3.1.3: {}
10048
+
10049
  csstype@3.2.3: {}
10050
 
10051
  data-uri-to-buffer@4.0.1: {}
 
10087
 
10088
  diff@8.0.4: {}
10089
 
10090
+ dot-case@3.0.4:
10091
+ dependencies:
10092
+ no-case: 3.0.4
10093
+ tslib: 2.8.1
10094
+
10095
  dotenv@17.4.2: {}
10096
 
10097
  drizzle-kit@0.31.10:
 
10656
  dependencies:
10657
  is-glob: 4.0.3
10658
 
10659
+ glob-to-regexp@0.4.1: {}
10660
+
10661
  globals@14.0.0: {}
10662
 
10663
  google-auth-library@10.6.2:
 
10882
 
10883
  joycon@3.1.1: {}
10884
 
10885
+ js-cookie@3.0.5: {}
10886
+
10887
+ js-cookie@3.0.7: {}
10888
+
10889
  js-tiktoken@1.0.21:
10890
  dependencies:
10891
  base64-js: 1.5.1
 
11050
 
11051
  long@5.3.2: {}
11052
 
11053
+ lower-case@2.0.2:
11054
+ dependencies:
11055
+ tslib: 2.8.1
11056
+
11057
  lru-cache@5.1.1:
11058
  dependencies:
11059
  yallist: 3.1.1
 
11066
  dependencies:
11067
  '@jridgewell/sourcemap-codec': 1.5.5
11068
 
11069
+ map-obj@4.3.0: {}
11070
+
11071
  marky@1.3.0:
11072
  optional: true
11073
 
 
11201
  - '@babel/core'
11202
  - babel-plugin-macros
11203
 
11204
+ no-case@3.0.4:
11205
+ dependencies:
11206
+ lower-case: 2.0.2
11207
+ tslib: 2.8.1
11208
+
11209
  node-addon-api@7.1.1: {}
11210
 
11211
  node-domexception@1.0.0: {}
 
11832
  transitivePeerDependencies:
11833
  - supports-color
11834
 
11835
+ server-only@0.0.1: {}
11836
+
11837
  set-cookie-parser@2.7.2: {}
11838
 
11839
  set-cookie-parser@3.1.0: {}
 
11958
 
11959
  sisteransi@1.0.5: {}
11960
 
11961
+ snake-case@3.0.4:
11962
+ dependencies:
11963
+ dot-case: 3.0.4
11964
+ tslib: 2.8.1
11965
+
11966
+ snakecase-keys@8.0.1:
11967
+ dependencies:
11968
+ map-obj: 4.3.0
11969
+ snake-case: 3.0.4
11970
+ type-fest: 4.41.0
11971
+
11972
  sonic-boom@4.2.1:
11973
  dependencies:
11974
  atomic-sleep: 1.0.0
 
11998
 
11999
  statuses@2.0.2: {}
12000
 
12001
+ std-env@3.10.0: {}
12002
+
12003
  std-env@4.1.0: {}
12004
 
12005
  stdin-discarder@0.2.2: {}
 
12059
 
12060
  supports-preserve-symlinks-flag@1.0.0: {}
12061
 
12062
+ swr@2.3.4(react@19.2.6):
12063
+ dependencies:
12064
+ dequal: 2.0.3
12065
+ react: 19.2.6
12066
+ use-sync-external-store: 1.6.0(react@19.2.6)
12067
+
12068
+ swr@2.4.1(react@19.2.6):
12069
+ dependencies:
12070
+ dequal: 2.0.3
12071
+ react: 19.2.6
12072
+ use-sync-external-store: 1.6.0(react@19.2.6)
12073
+
12074
  tagged-tag@1.0.0: {}
12075
 
12076
  tailwind-merge@3.6.0: {}
 
12147
  minimist: 1.2.8
12148
  strip-bom: 3.0.0
12149
 
12150
+ tslib@2.4.1: {}
12151
+
12152
  tslib@2.8.1: {}
12153
 
12154
  tsx@4.22.3:
 
12172
  dependencies:
12173
  prelude-ls: 1.2.1
12174
 
12175
+ type-fest@4.41.0: {}
12176
+
12177
  type-fest@5.6.0:
12178
  dependencies:
12179
  tagged-tag: 1.0.0