Spaces:
Sleeping
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 +11 -0
- .playwright-mcp/page-2026-05-29T16-34-02-219Z.yml +35 -0
- .playwright-mcp/page-2026-05-29T16-34-44-022Z.yml +78 -0
- .playwright-mcp/page-2026-05-29T16-35-07-854Z.yml +78 -0
- DEPLOY.md +285 -0
- Dockerfile.hf +65 -0
- GAP_AUDIT_M1_M9.md +189 -0
- apps/api/package.json +5 -3
- apps/api/src/app.module.ts +4 -0
- apps/api/src/auth/auth.module.ts +17 -0
- apps/api/src/auth/auth.service.ts +79 -0
- apps/api/src/auth/clerk-auth.guard.ts +72 -0
- apps/api/src/auth/current-user.decorator.ts +12 -0
- apps/api/src/main.ts +17 -1
- apps/api/src/missions/missions.controller.ts +20 -15
- apps/api/src/missions/missions.service.ts +22 -29
- apps/api/src/users/users.controller.ts +28 -0
- apps/api/src/users/users.module.ts +10 -0
- apps/api/src/users/users.service.ts +55 -0
- apps/web/.env.local.example +9 -0
- apps/web/middleware.ts +29 -0
- apps/web/package.json +1 -0
- apps/web/src/app/[locale]/layout.tsx +44 -1
- apps/web/src/app/[locale]/page.tsx +44 -19
- apps/web/src/app/[locale]/profile/page.tsx +209 -0
- apps/web/src/app/[locale]/sign-in/[[...sign-in]]/page.tsx +23 -0
- apps/web/src/app/[locale]/sign-up/[[...sign-up]]/page.tsx +23 -0
- apps/web/src/app/layout.tsx +6 -6
- apps/web/src/lib/api.ts +52 -23
- apps/web/src/lib/auth-token.tsx +49 -0
- packages/db/src/migrations/0011_blushing_puma.sql +4 -0
- packages/db/src/migrations/meta/0011_snapshot.json +0 -0
- packages/db/src/migrations/meta/_journal.json +7 -0
- packages/db/src/schema.ts +4 -0
- pnpm-lock.yaml +354 -0
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"mcpServers": {
|
| 3 |
+
"shadcn": {
|
| 4 |
+
"command": "npx",
|
| 5 |
+
"args": [
|
| 6 |
+
"shadcn@latest",
|
| 7 |
+
"mcp"
|
| 8 |
+
]
|
| 9 |
+
}
|
| 10 |
+
}
|
| 11 |
+
}
|
|
@@ -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"
|
|
@@ -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]
|
|
@@ -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]
|
|
@@ -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! 🚀*
|
|
@@ -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"]
|
|
@@ -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 |
+
|
|
@@ -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 |
-
"
|
| 28 |
-
"
|
| 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 |
}
|
|
@@ -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 |
})
|
|
@@ -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 {}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
);
|
|
@@ -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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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) => {
|
|
@@ -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 |
-
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|
|
@@ -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
|
| 100 |
-
const uid = await this.ensureDemoUser(tx as unknown as Database);
|
| 101 |
-
|
| 102 |
const [m] = await tx
|
| 103 |
.insert(missions)
|
| 104 |
.values({
|
| 105 |
-
userId
|
| 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:
|
| 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!
|
| 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 |
-
|
| 177 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
return rows.map((m) => this.toDto(m));
|
| 179 |
}
|
| 180 |
|
| 181 |
-
|
| 182 |
-
|
|
|
|
|
|
|
| 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:
|
| 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);
|
|
@@ -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 |
+
}
|
|
@@ -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 {}
|
|
@@ -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 |
+
}
|
|
@@ -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
|
|
@@ -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 |
+
};
|
|
@@ -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",
|
|
@@ -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 |
-
<
|
|
|
|
| 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 |
}
|
|
@@ -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 |
-
<
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
-
<
|
| 16 |
-
<
|
| 17 |
-
|
|
|
|
|
|
|
| 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 |
}
|
|
@@ -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
|
| 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 |
};
|
|
@@ -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 |
+
}
|
|
@@ -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;
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
@@ -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 |
}
|
|
@@ -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(),
|
|
@@ -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
|