create-theokit 0.4.0-beta.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/package.json +8 -4
  2. package/templates/default/.env.example +5 -0
  3. package/templates/default/README.md.tmpl +71 -57
  4. package/templates/default/_gitignore +2 -3
  5. package/templates/default/app/layout.tsx +51 -82
  6. package/templates/default/app/page.tsx +169 -271
  7. package/templates/default/app.ts +23 -0
  8. package/templates/default/package.json.tmpl +10 -23
  9. package/templates/default/server/agents/assistant.agent.ts +46 -0
  10. package/templates/default/server/controllers/tasks.controller.ts +70 -0
  11. package/templates/default/server/filters/http-error.filter.ts +20 -0
  12. package/templates/default/server/guards/auth.guard.ts +47 -0
  13. package/templates/default/server/interceptors/timing.interceptor.ts +14 -0
  14. package/templates/default/server/middleware/logger.middleware.ts +12 -0
  15. package/templates/default/server/store.ts +46 -0
  16. package/templates/default/server/toolboxes/task.tools.ts +58 -0
  17. package/templates/default/tsconfig.json +7 -7
  18. package/LICENSE +0 -201
  19. package/templates/api-only/.nvmrc +0 -1
  20. package/templates/api-only/README.md.tmpl +0 -78
  21. package/templates/api-only/_gitignore +0 -5
  22. package/templates/api-only/app/page.tsx +0 -3
  23. package/templates/api-only/index.html +0 -12
  24. package/templates/api-only/package.json.tmpl +0 -28
  25. package/templates/api-only/public/.gitkeep +0 -0
  26. package/templates/api-only/public/favicon.ico +0 -0
  27. package/templates/api-only/server/routes/health.ts +0 -5
  28. package/templates/api-only/server/routes/users.ts +0 -27
  29. package/templates/api-only/server/routes/webhooks/echo.ts +0 -34
  30. package/templates/api-only/theo.config.ts +0 -3
  31. package/templates/api-only/tsconfig.json +0 -15
  32. package/templates/dashboard/.nvmrc +0 -1
  33. package/templates/dashboard/README.md.tmpl +0 -76
  34. package/templates/dashboard/_gitignore +0 -5
  35. package/templates/dashboard/app/about/page.tsx +0 -3
  36. package/templates/dashboard/app/dashboard/layout.tsx +0 -10
  37. package/templates/dashboard/app/dashboard/page.tsx +0 -3
  38. package/templates/dashboard/app/layout.tsx +0 -14
  39. package/templates/dashboard/app/page.tsx +0 -8
  40. package/templates/dashboard/index.html +0 -12
  41. package/templates/dashboard/package.json.tmpl +0 -28
  42. package/templates/dashboard/public/.gitkeep +0 -0
  43. package/templates/dashboard/public/favicon.ico +0 -0
  44. package/templates/dashboard/server/crons/cleanup-conversations.ts +0 -59
  45. package/templates/dashboard/server/routes/health.ts +0 -5
  46. package/templates/dashboard/theo.config.ts +0 -3
  47. package/templates/dashboard/tsconfig.json +0 -15
  48. package/templates/default/.nvmrc +0 -1
  49. package/templates/default/index.html +0 -12
  50. package/templates/default/public/.gitkeep +0 -0
  51. package/templates/default/public/favicon.ico +0 -0
  52. package/templates/default/server/crons/cleanup-conversations.ts +0 -59
  53. package/templates/default/server/routes/chat.ts +0 -69
  54. package/templates/default/server/routes/health.ts +0 -5
  55. package/templates/default/theo.config.ts +0 -3
  56. package/templates/default/types/jobs.d.ts +0 -25
  57. package/templates/postgres/.env.example +0 -5
  58. package/templates/postgres/.nvmrc +0 -1
  59. package/templates/postgres/README.md.tmpl +0 -83
  60. package/templates/postgres/_gitignore +0 -5
  61. package/templates/postgres/app/layout.tsx +0 -14
  62. package/templates/postgres/app/page.tsx +0 -8
  63. package/templates/postgres/db/index.ts +0 -7
  64. package/templates/postgres/db/schema.ts +0 -8
  65. package/templates/postgres/drizzle.config.ts +0 -10
  66. package/templates/postgres/index.html +0 -12
  67. package/templates/postgres/package.json.tmpl +0 -36
  68. package/templates/postgres/public/.gitkeep +0 -0
  69. package/templates/postgres/public/favicon.ico +0 -0
  70. package/templates/postgres/server/context.ts +0 -5
  71. package/templates/postgres/server/jobs/log-message.ts +0 -26
  72. package/templates/postgres/server/routes/health.ts +0 -5
  73. package/templates/postgres/server/routes/users.ts +0 -22
  74. package/templates/postgres/theo.config.ts +0 -3
  75. package/templates/postgres/tsconfig.json +0 -15
  76. package/templates/saas/.env.example +0 -7
  77. package/templates/saas/.nvmrc +0 -1
  78. package/templates/saas/README.md.tmpl +0 -103
  79. package/templates/saas/_gitignore +0 -5
  80. package/templates/saas/app/layout.tsx +0 -5
  81. package/templates/saas/app/page.tsx +0 -104
  82. package/templates/saas/db/index.ts +0 -6
  83. package/templates/saas/db/schema.ts +0 -20
  84. package/templates/saas/drizzle.config.ts +0 -10
  85. package/templates/saas/index.html +0 -12
  86. package/templates/saas/package.json.tmpl +0 -38
  87. package/templates/saas/public/.gitkeep +0 -0
  88. package/templates/saas/public/favicon.ico +0 -0
  89. package/templates/saas/server/context.ts +0 -37
  90. package/templates/saas/server/routes/agent.ts +0 -49
  91. package/templates/saas/server/routes/billing/stripe-webhook.ts +0 -49
  92. package/templates/saas/server/routes/login.ts +0 -25
  93. package/templates/saas/server/routes/logout.ts +0 -10
  94. package/templates/saas/server/routes/me.ts +0 -10
  95. package/templates/saas/theo.config.ts +0 -5
  96. package/templates/saas/tsconfig.json +0 -15
  97. package/templates/services/agent-node/Dockerfile.tmpl +0 -20
  98. package/templates/services/agent-node/README.md +0 -38
  99. package/templates/services/agent-node/package.json.tmpl +0 -18
  100. package/templates/services/agent-node/src/index.ts +0 -58
  101. package/templates/services/agent-node/tsconfig.json +0 -13
  102. package/templates/services/agent-python/Dockerfile.tmpl +0 -20
  103. package/templates/services/agent-python/README.md +0 -37
  104. package/templates/services/agent-python/main.py +0 -77
  105. package/templates/services/agent-python/pyproject.toml.tmpl +0 -16
@@ -1,5 +0,0 @@
1
- import { db } from '../db/index.js'
2
-
3
- export function createContext() {
4
- return { db }
5
- }
@@ -1,26 +0,0 @@
1
- import { defineJob } from 'theokit/server/jobs'
2
- import { z } from 'zod'
3
- import { appendFile, mkdir } from 'node:fs/promises'
4
- import { resolve, dirname } from 'node:path'
5
-
6
- /**
7
- * Background job demonstrating `defineJob` + `ctx.queue.enqueue` pattern.
8
- *
9
- * Triggered from `server/routes/users.ts` POST handler via:
10
- * await ctx.queue.enqueue('log-message', { userId, message })
11
- *
12
- * Per ADR-0003 (transactional outbox), enqueue is deferred until the
13
- * route handler commits successfully — handler throws → 0 jobs dispatched.
14
- */
15
- export default defineJob('log-message', {
16
- input: z.object({ userId: z.string(), message: z.string() }),
17
- handler: async ({ input, log }) => {
18
- // v1.1 EC-9: anchor path to process.cwd() — handler CWD may differ from
19
- // project root when running via external job runner.
20
- const auditPath = resolve(process.cwd(), '.theo/audit.log')
21
- await mkdir(dirname(auditPath), { recursive: true })
22
- const line = `${new Date().toISOString()} user=${input.userId} msg=${input.message}\n`
23
- await appendFile(auditPath, line)
24
- log.info({ msg: 'audit logged', userId: input.userId, path: auditPath })
25
- },
26
- })
@@ -1,5 +0,0 @@
1
- import { defineRoute } from 'theokit/server'
2
-
3
- export const GET = defineRoute({
4
- handler: () => ({ ok: true }),
5
- })
@@ -1,22 +0,0 @@
1
- import { defineRoute } from 'theokit/server'
2
- import { z } from 'zod'
3
- import { users } from '../../db/schema.js'
4
-
5
- export const GET = defineRoute({
6
- handler: async ({ ctx }) => {
7
- const allUsers = await (ctx as any).db.select().from(users)
8
- return { users: allUsers }
9
- },
10
- })
11
-
12
- export const POST = defineRoute({
13
- body: z.object({
14
- name: z.string().min(1),
15
- email: z.string().email(),
16
- }),
17
- status: 201,
18
- handler: async ({ body, ctx }) => {
19
- const [user] = await (ctx as any).db.insert(users).values(body).returning()
20
- return user
21
- },
22
- })
@@ -1,3 +0,0 @@
1
- import { defineConfig } from 'theokit'
2
-
3
- export default defineConfig({})
@@ -1,15 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "strict": true,
7
- "noEmit": true,
8
- "esModuleInterop": true,
9
- "skipLibCheck": true,
10
- "jsx": "react-jsx",
11
- "isolatedModules": true,
12
- "resolveJsonModule": true
13
- },
14
- "include": ["app/**/*.ts", "app/**/*.tsx", "server/**/*.ts"]
15
- }
@@ -1,7 +0,0 @@
1
- # Production server REFUSES TO BOOT with this placeholder value.
2
- # Replace before deploying:
3
- # openssl rand -hex 32
4
- SECRET=CHANGE_ME_TO_RANDOM_32_PLUS_CHARS_FOR_REAL
5
-
6
- # Drizzle/postgres connection string
7
- DATABASE_URL=postgres://user:pass@localhost:5432/saas_dev
@@ -1 +0,0 @@
1
- 22.12
@@ -1,103 +0,0 @@
1
- # {{name}}
2
-
3
- TheoKit SaaS template — auth, sessions, billing-ready, and an agent route. The full stack for shipping an account-aware product on day one.
4
-
5
- > 📚 **Full docs:** https://docs.theokit.dev
6
-
7
- ## Quick start
8
-
9
- ```bash
10
- # 0. Provision Postgres (sessions + users)
11
- docker run --name pg -e POSTGRES_PASSWORD=dev -p 5432:5432 -d postgres:16
12
- # Or use a hosted Postgres (neon.tech / supabase.com)
13
-
14
- # 1. Set env (generate a strong session secret)
15
- cat > .env <<EOF
16
- DATABASE_URL=postgres://postgres:dev@localhost:5432/postgres
17
- SESSION_SECRET=$(openssl rand -base64 32)
18
- OPENROUTER_API_KEY=sk-or-v1-...
19
- EOF
20
-
21
- # 2. Migrate the schema
22
- pnpm db:generate
23
- pnpm db:migrate
24
-
25
- # 3. Boot the dev server
26
- npx theokit dev
27
- ```
28
-
29
- ## Sample auth flow
30
-
31
- ```bash
32
- # Register
33
- curl -X POST http://localhost:3000/api/register \
34
- -H 'Content-Type: application/json' \
35
- -d '{"email":"alice@example.com","password":"strong-passphrase"}'
36
-
37
- # Login (saves session cookie)
38
- curl -X POST http://localhost:3000/api/login \
39
- -H 'Content-Type: application/json' \
40
- -d '{"email":"alice@example.com","password":"strong-passphrase"}' \
41
- -c cookies.txt
42
-
43
- # Authenticated request
44
- curl http://localhost:3000/api/me -b cookies.txt
45
- ```
46
-
47
- ## Templates
48
-
49
- - **default** — TheoUI chat composer + agent route.
50
- - **dashboard** — nested layouts + sidebar.
51
- - **api-only** — server routes without React.
52
- - **postgres** — Drizzle ORM + migrations.
53
- - **saas** (this one) — full app with auth, billing, sessions.
54
-
55
- ## What the framework auto-loads
56
-
57
- - **`.env` → `process.env`**. `SESSION_SECRET`, `DATABASE_URL`, `OPENROUTER_API_KEY` all required.
58
- - **Encrypted sessions** (AES-256-GCM) via `SESSION_SECRET`.
59
- - **`.theo/` build output cleanup** on every `theokit build`.
60
-
61
- ## Project structure
62
-
63
- ```
64
- app/ Frontend
65
- ├── page.tsx / — landing
66
- server/
67
- ├── routes/
68
- │ ├── login.ts POST /api/login — sets session cookie
69
- │ ├── logout.ts POST /api/logout — clears session
70
- │ ├── me.ts GET /api/me — requireAuth() guarded
71
- │ └── agent.ts POST /api/agent — agent SSE, requireAuth()
72
- db/
73
- ├── schema.ts users + sessions tables (Drizzle)
74
- └── migrations/ generated SQL (committed)
75
- drizzle.config.ts Drizzle config
76
- theo.config.ts Framework config
77
- .env Secrets — never committed
78
- ```
79
-
80
- ## Common commands
81
-
82
- | Command | What it does |
83
- |---|---|
84
- | `npx theokit dev` | Dev server with HMR + auth |
85
- | `npx theokit build` | Production build |
86
- | `npx theokit start` | Serve production build |
87
- | `pnpm db:generate` | Generate SQL migration |
88
- | `pnpm db:migrate` | Apply migrations |
89
- | `npm run typecheck` | TypeScript strict check |
90
-
91
- ## Adding billing
92
-
93
- Stripe is the canonical path — add `STRIPE_SECRET_KEY` to `.env`, mount `server/routes/billing/webhook.ts` via `defineWebhook`, and wire plan checks into `requireAuth()`.
94
-
95
- ## Troubleshooting
96
-
97
- - **`SESSION_SECRET must be at least 32 bytes`** → regenerate with `openssl rand -base64 32`.
98
- - **`relation "users" does not exist`** → run `pnpm db:migrate` first.
99
- - **`401 Unauthorized` on `/api/me`** → login first; cookie must be sent (`-b cookies.txt`).
100
-
101
- ## License
102
-
103
- Apply your own. The TheoKit framework is Apache-2.0.
@@ -1,5 +0,0 @@
1
- node_modules
2
- .theo
3
- dist
4
- .env
5
- .env.local
@@ -1,5 +0,0 @@
1
- import type { PropsWithChildren } from 'react'
2
-
3
- export default function Layout({ children }: PropsWithChildren) {
4
- return <>{children}</>
5
- }
@@ -1,104 +0,0 @@
1
- 'use client'
2
-
3
- import { useEffect, useState } from 'react'
4
- import { AgentComposer, AgentTimeline, type AgentEvent as AgentRow } from '@theokit/ui'
5
- import { useAgentStream } from 'theokit/client'
6
-
7
- interface Me {
8
- userId: string
9
- email: string
10
- }
11
-
12
- export default function Page() {
13
- const [me, setMe] = useState<Me | null>(null)
14
- const [email, setEmail] = useState('demo@example.com')
15
- const [composer, setComposer] = useState('')
16
-
17
- const { events, send, status } = useAgentStream<{ message: string }>('/api/agent')
18
-
19
- async function refreshMe() {
20
- const res = await fetch('/api/me')
21
- setMe(res.ok ? ((await res.json()) as Me) : null)
22
- }
23
-
24
- useEffect(() => {
25
- refreshMe()
26
- }, [])
27
-
28
- async function login() {
29
- await fetch('/api/login', {
30
- method: 'POST',
31
- headers: { 'content-type': 'application/json' },
32
- body: JSON.stringify({ email, password: 'demo' }),
33
- })
34
- refreshMe()
35
- }
36
- async function logout() {
37
- await fetch('/api/logout', { method: 'POST' })
38
- refreshMe()
39
- }
40
-
41
- if (!me) {
42
- return (
43
- <main
44
- style={{ padding: 24, display: 'flex', flexDirection: 'column', gap: 8, maxWidth: 360 }}
45
- >
46
- <h1>Sign in</h1>
47
- <input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="email" />
48
- <button type="button" onClick={login}>
49
- Sign in (demo)
50
- </button>
51
- </main>
52
- )
53
- }
54
-
55
- const rows: AgentRow[] = events.map((e, i) => ({
56
- id: `e-${i}`,
57
- type: e.type === 'tool_call' ? 'tool' : 'command',
58
- label:
59
- e.type === 'message'
60
- ? e.content
61
- : e.type === 'tool_call'
62
- ? `tool: ${e.name}`
63
- : e.type === 'error'
64
- ? `error: ${e.message}`
65
- : e.type,
66
- status: e.type === 'error' ? 'failed' : 'success',
67
- timestamp: new Date().toISOString(),
68
- }))
69
-
70
- return (
71
- <main
72
- style={{ display: 'flex', flexDirection: 'column', height: '100vh', padding: 24, gap: 16 }}
73
- >
74
- <header style={{ display: 'flex', justifyContent: 'space-between' }}>
75
- <div>
76
- <h1>SaaS Agent</h1>
77
- <p style={{ color: '#888', fontSize: 14 }}>
78
- Signed in as {me.email} · status: <code>{status}</code>
79
- </p>
80
- </div>
81
- <button type="button" onClick={logout}>
82
- Sign out
83
- </button>
84
- </header>
85
- <section style={{ flex: 1, overflowY: 'auto' }}>
86
- <AgentTimeline events={rows} />
87
- </section>
88
- <footer>
89
- <AgentComposer
90
- value={composer}
91
- onValueChange={setComposer}
92
- onSubmit={() => {
93
- const v = composer.trim()
94
- if (v) {
95
- send({ message: v })
96
- setComposer('')
97
- }
98
- }}
99
- placeholder="Ask your agent…"
100
- />
101
- </footer>
102
- </main>
103
- )
104
- }
@@ -1,6 +0,0 @@
1
- import { drizzle } from 'drizzle-orm/postgres-js'
2
- import postgres from 'postgres'
3
- import * as schema from './schema.js'
4
-
5
- const client = postgres(process.env.DATABASE_URL ?? '')
6
- export const db = drizzle(client, { schema })
@@ -1,20 +0,0 @@
1
- import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
2
-
3
- export const users = pgTable('users', {
4
- id: uuid('id').primaryKey().defaultRandom(),
5
- email: text('email').notNull().unique(),
6
- passwordHash: text('password_hash').notNull(),
7
- createdAt: timestamp('created_at').defaultNow().notNull(),
8
- })
9
-
10
- export const sessions = pgTable('sessions', {
11
- id: uuid('id').primaryKey().defaultRandom(),
12
- userId: uuid('user_id')
13
- .references(() => users.id)
14
- .notNull(),
15
- expiresAt: timestamp('expires_at').notNull(),
16
- createdAt: timestamp('created_at').defaultNow().notNull(),
17
- })
18
-
19
- export type User = typeof users.$inferSelect
20
- export type Session = typeof sessions.$inferSelect
@@ -1,10 +0,0 @@
1
- import type { Config } from 'drizzle-kit'
2
-
3
- export default {
4
- schema: './db/schema.ts',
5
- out: './drizzle',
6
- dialect: 'postgresql',
7
- dbCredentials: {
8
- url: process.env.DATABASE_URL ?? '',
9
- },
10
- } satisfies Config
@@ -1,12 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>SaaS Starter</title>
7
- </head>
8
- <body>
9
- <div id="root"></div>
10
- <script type="module" src="/@theo/entry-client"></script>
11
- </body>
12
- </html>
@@ -1,38 +0,0 @@
1
- {
2
- "name": "{{name}}",
3
- "version": "0.1.0",
4
- "private": true,
5
- "type": "module",
6
- "scripts": {
7
- "dev": "theokit dev",
8
- "build": "theokit build",
9
- "start": "theokit start",
10
- "typecheck": "tsc --noEmit",
11
- "db:push": "drizzle-kit push",
12
- "db:generate": "drizzle-kit generate",
13
- "db:migrate": "drizzle-kit migrate",
14
- "db:studio": "drizzle-kit studio"
15
- },
16
- "dependencies": {
17
- "theokit": "^0.4.0-beta.0",
18
- "@theokit/ui": "^0.14.0",
19
- "react": "^19.0.0",
20
- "react-dom": "^19.0.0",
21
- "react-router": "^7.0.0",
22
- "drizzle-orm": "^0.45.0",
23
- "postgres": "^3.4.0",
24
- "zod": "^3.24.0"
25
- },
26
- "devDependencies": {
27
- "@types/node": "^22.10.0",
28
- "typescript": "^5.7.0",
29
- "@types/react": "^19.0.0",
30
- "@types/react-dom": "^19.0.0",
31
- "drizzle-kit": "^0.31.0"
32
- },
33
- "pnpm": {
34
- "onlyBuiltDependencies": [
35
- "esbuild"
36
- ]
37
- }
38
- }
File without changes
Binary file
@@ -1,37 +0,0 @@
1
- import { assertProductionSecret, createSessionManager, type SessionManager } from 'theokit/server'
2
- import type { IncomingMessage, ServerResponse } from 'node:http'
3
- import { db } from '../db/index.js'
4
-
5
- export interface UserSession {
6
- userId: string
7
- email: string
8
- }
9
-
10
- const SECRET = process.env.SECRET ?? 'CHANGE_ME_TO_RANDOM_32_PLUS_CHARS_FOR_REAL'
11
-
12
- // EC-2: dev warns + prod refuses to boot if SECRET is a placeholder.
13
- // Replace .env.example secret with `openssl rand -hex 32` before deploying.
14
- assertProductionSecret(SECRET)
15
-
16
- const sessions: SessionManager<UserSession> = createSessionManager({
17
- secret: SECRET,
18
- cookieName: 'theo_session',
19
- })
20
-
21
- export interface RequestContext {
22
- sessions: SessionManager<UserSession>
23
- session: UserSession | null
24
- res: ServerResponse
25
- db: typeof db
26
- }
27
-
28
- export async function createContext({
29
- request,
30
- response,
31
- }: {
32
- request: IncomingMessage
33
- response: ServerResponse
34
- }): Promise<RequestContext> {
35
- const session = await sessions.getSession(request)
36
- return { sessions, session, res: response, db }
37
- }
@@ -1,49 +0,0 @@
1
- import { defineAgentEndpoint, requireAuth, type AgentEvent } from 'theokit/server'
2
- import { trackAgentRun } from 'theokit/server/cost'
3
- import type { RequestContext } from '../context.js'
4
-
5
- /**
6
- * Protected agent endpoint. `requireAuth` fires BEFORE the stream starts;
7
- * unauthorized requests get 401 immediately — no SSE bytes leak.
8
- *
9
- * Observability: wraps the run with `trackAgentRun` to surface per-user
10
- * cost + token usage to the configured `UsageStorageAdapter` (configure via
11
- * `theo.config.ts > cost.storage`). Also feeds the devtools `Agents` tab
12
- * (when running in dev).
13
- *
14
- * NOTE: `costUsd: 0` is a v1 stub. Pricing table integration is a
15
- * `@theokit/sdk` follow-up (R0.5.11). Devtools tab renders "$0.0000" —
16
- * indicates "cost tracking not yet calibrated for this model".
17
- *
18
- * Replace the mock generator with your LLM provider call.
19
- */
20
- export const POST = defineAgentEndpoint<{ message: string }, RequestContext>({
21
- async *handler({ ctx, request }): AsyncGenerator<AgentEvent> {
22
- requireAuth(ctx.session)
23
- const body = (await request.json()) as { message?: string }
24
- const msg = body.message ?? ''
25
- try {
26
- yield {
27
- type: 'message',
28
- content: `Hello ${ctx.session.email}, you said: "${msg}"`,
29
- }
30
- yield { type: 'message', content: '(Replace this mock with your LLM.)' }
31
- } finally {
32
- // Always emit observability — even on stream error / abort.
33
- // `storage` resolved from theo.config.ts > cost.storage (undefined =
34
- // no-op; configure to enable persistence + devtools tab visibility).
35
- // To enable persistent cost tracking: wire `cost: { storage }` into
36
- // `theo.config.ts` and forward via context. Demo passes `undefined`
37
- // (no-op storage; still fires devtools dispatcher in dev mode).
38
- await trackAgentRun(
39
- {
40
- userId: ctx.session.email,
41
- model: 'mock/echo',
42
- tokens: { input: msg.length, output: 0 }, // crude — real impl uses tokenizer
43
- costUsd: 0, // v1 stub
44
- },
45
- { storage: undefined },
46
- )
47
- }
48
- },
49
- })
@@ -1,49 +0,0 @@
1
- import { defineWebhook } from 'theokit/server'
2
- import { createHmac, timingSafeEqual } from 'node:crypto'
3
- import { z } from 'zod'
4
-
5
- /**
6
- * Stripe webhook receiver.
7
- *
8
- * Verifies `Stripe-Signature` header per Stripe's documented HMAC-SHA256
9
- * scheme (https://stripe.com/docs/webhooks/signatures). Real impl would
10
- * handle `checkout.session.completed`, `invoice.paid`, etc.
11
- *
12
- * Setup:
13
- * 1. Create webhook endpoint in Stripe Dashboard pointing to /api/billing/stripe-webhook
14
- * 2. Copy signing secret → `.env` STRIPE_WEBHOOK_SECRET
15
- * 3. Test locally: `stripe listen --forward-to localhost:3000/api/billing/stripe-webhook`
16
- */
17
- const STRIPE_SECRET = process.env.STRIPE_WEBHOOK_SECRET ?? ''
18
-
19
- export const POST = defineWebhook({
20
- verify: ({ rawBody, headers }) => {
21
- if (STRIPE_SECRET === '') return false
22
- const sigHeader = headers.get('stripe-signature') ?? ''
23
- // Stripe format: `t=<unix-ts>,v1=<hash>` (potentially also v0)
24
- const parts: Record<string, string> = {}
25
- for (const pair of sigHeader.split(',')) {
26
- const [k, v] = pair.split('=')
27
- if (k && v) parts[k.trim()] = v.trim()
28
- }
29
- const t = parts['t']
30
- const v1 = parts['v1']
31
- if (!t || !v1) return false
32
- const signedPayload = `${t}.${rawBody}`
33
- const expected = createHmac('sha256', STRIPE_SECRET).update(signedPayload).digest('hex')
34
- try {
35
- return timingSafeEqual(Buffer.from(v1, 'utf-8'), Buffer.from(expected, 'utf-8'))
36
- } catch {
37
- return false
38
- }
39
- },
40
- inputSchema: z.object({ type: z.string(), data: z.unknown() }),
41
- handler: async ({ input, log }) => {
42
- log.info({ msg: 'stripe webhook received', type: input.type })
43
- // TODO: dispatch by input.type:
44
- // - 'checkout.session.completed' → activate subscription
45
- // - 'invoice.paid' → extend access
46
- // - 'invoice.payment_failed' → notify user + retry plan
47
- return Response.json({ received: true })
48
- },
49
- })
@@ -1,25 +0,0 @@
1
- import { defineRoute } from 'theokit/server'
2
- import { z } from 'zod'
3
- import type { RequestContext } from '../context.js'
4
-
5
- export const POST = defineRoute<
6
- z.ZodUndefined,
7
- z.ZodObject<{ email: z.ZodString; password: z.ZodString }>,
8
- z.ZodUndefined,
9
- RequestContext
10
- >({
11
- body: z.object({
12
- email: z.string().email(),
13
- password: z.string().min(1),
14
- }),
15
- handler: async ({ body, ctx }) => {
16
- // DEMO ONLY — replace with bcrypt/argon2 password comparison against
17
- // the users table. This stub accepts any password.
18
- const userId = `u-${body.email}`
19
- await ctx.sessions.createSession(ctx.res, {
20
- userId,
21
- email: body.email,
22
- })
23
- return { ok: true, email: body.email }
24
- },
25
- })
@@ -1,10 +0,0 @@
1
- import { defineRoute } from 'theokit/server'
2
- import type { z } from 'zod'
3
- import type { RequestContext } from '../context.js'
4
-
5
- export const POST = defineRoute<z.ZodUndefined, z.ZodUndefined, z.ZodUndefined, RequestContext>({
6
- handler: ({ ctx }) => {
7
- ctx.sessions.destroySession(ctx.res)
8
- return { ok: true }
9
- },
10
- })
@@ -1,10 +0,0 @@
1
- import { defineRoute, requireAuth } from 'theokit/server'
2
- import type { z } from 'zod'
3
- import type { RequestContext } from '../context.js'
4
-
5
- export const GET = defineRoute<z.ZodUndefined, z.ZodUndefined, z.ZodUndefined, RequestContext>({
6
- handler: ({ ctx }) => {
7
- requireAuth(ctx.session)
8
- return { userId: ctx.session.userId, email: ctx.session.email }
9
- },
10
- })
@@ -1,5 +0,0 @@
1
- import { defineConfig } from 'theokit'
2
-
3
- export default defineConfig({
4
- ui: { theme: 'violet-forge', fonts: 'bundled' },
5
- })
@@ -1,15 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "lib": ["ES2022", "DOM", "DOM.Iterable"],
5
- "module": "ESNext",
6
- "moduleResolution": "bundler",
7
- "strict": true,
8
- "jsx": "react-jsx",
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "noEmit": true,
12
- "allowImportingTsExtensions": false
13
- },
14
- "include": ["app", "server", "db"]
15
- }
@@ -1,20 +0,0 @@
1
- FROM node:22-alpine
2
-
3
- WORKDIR /app
4
-
5
- # Install pnpm (or use npm if pnpm not available in image)
6
- RUN corepack enable && corepack prepare pnpm@latest --activate || true
7
-
8
- # Copy manifest and install
9
- COPY package.json ./
10
- RUN pnpm install --frozen-lockfile || npm install
11
-
12
- # Copy source
13
- COPY . .
14
-
15
- EXPOSE 8002
16
-
17
- HEALTHCHECK --interval=10s --timeout=5s --retries=3 \
18
- CMD wget --spider -q http://localhost:8002/health || exit 1
19
-
20
- CMD ["pnpm", "start"]