create-theokit 0.1.0-alpha.12 → 0.1.0-alpha.14

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-theokit",
3
- "version": "0.1.0-alpha.12",
3
+ "version": "0.1.0-alpha.14",
4
4
  "type": "module",
5
5
  "description": "Scaffold a new TheoKit project",
6
6
  "license": "Apache-2.0",
@@ -0,0 +1,78 @@
1
+ # {{name}}
2
+
3
+ TheoKit API-only project. Backend routes with Zod validation + typed responses — no frontend bundle, no React.
4
+
5
+ > 📚 **Full docs:** https://docs.theokit.dev
6
+
7
+ ## Quick start
8
+
9
+ ```bash
10
+ # 1. Set your provider key if you wire an agent route later
11
+ echo 'OPENROUTER_API_KEY=sk-or-v1-...' > .env
12
+
13
+ # 2. Boot the dev server
14
+ npx theokit dev
15
+
16
+ # 3. Probe the health route
17
+ curl http://localhost:3000/api/health
18
+ ```
19
+
20
+ You should see `{"status":"ok"}`. The server is now serving the routes under `server/routes/`.
21
+
22
+ ## Templates
23
+
24
+ - **default** — TheoUI chat composer + agent route.
25
+ - **dashboard** — nested layouts + sidebar.
26
+ - **api-only** (this one) — server routes without React.
27
+ - **postgres** — Drizzle ORM + migrations.
28
+ - **saas** — full app with auth, billing, sessions.
29
+
30
+ ## What the framework auto-loads
31
+
32
+ - **`.env` → `process.env`**. Edit `.env`; restart the dev server.
33
+ - **`.theo/` build output cleanup** on every `theokit build`.
34
+ - **Route discovery** — every `server/routes/*.ts` becomes a wired endpoint.
35
+
36
+ ## Project structure
37
+
38
+ ```
39
+ server/
40
+ ├── routes/
41
+ │ ├── health.ts GET /api/health — returns {status:"ok"}
42
+ │ └── users.ts CRUD /api/users — Zod-validated body
43
+ theo.config.ts Framework config
44
+ .env Secrets — never committed (.gitignore)
45
+ ```
46
+
47
+ ## Sample requests
48
+
49
+ ```bash
50
+ # Health check
51
+ curl http://localhost:3000/api/health
52
+
53
+ # Create a user (POST with JSON body)
54
+ curl -X POST http://localhost:3000/api/users \
55
+ -H 'Content-Type: application/json' \
56
+ -d '{"name":"Alice","email":"alice@example.com"}'
57
+
58
+ # List users
59
+ curl http://localhost:3000/api/users
60
+ ```
61
+
62
+ ## Common commands
63
+
64
+ | Command | What it does |
65
+ |---|---|
66
+ | `npx theokit dev` | Dev server with HMR + structured logs |
67
+ | `npx theokit build` | Production build → `.theo/` |
68
+ | `npx theokit start` | Serve the production build |
69
+ | `npx theokit routes` | List all routes detected |
70
+ | `npm run typecheck` | TypeScript strict check (no emit) |
71
+
72
+ ## Add a new route
73
+
74
+ Drop a `.ts` file in `server/routes/`. Use `defineRoute` from `theokit/server` for Zod-validated handlers, or export `GET`/`POST` directly.
75
+
76
+ ## License
77
+
78
+ Apply your own. The TheoKit framework is Apache-2.0.
@@ -10,7 +10,7 @@
10
10
  "typecheck": "tsc --noEmit"
11
11
  },
12
12
  "dependencies": {
13
- "theokit": "^0.1.0-alpha.12",
13
+ "theokit": "^0.1.0-alpha.14",
14
14
  "react": "^19.0.0",
15
15
  "react-dom": "^19.0.0"
16
16
  },
@@ -18,5 +18,10 @@
18
18
  "typescript": "^5.7.0",
19
19
  "@types/react": "^19.0.0",
20
20
  "@types/react-dom": "^19.0.0"
21
+ },
22
+ "pnpm": {
23
+ "onlyBuiltDependencies": [
24
+ "esbuild"
25
+ ]
21
26
  }
22
27
  }
@@ -0,0 +1,34 @@
1
+ import { defineWebhook } from 'theokit/server'
2
+ import { createHmac, timingSafeEqual } from 'node:crypto'
3
+ import { z } from 'zod'
4
+
5
+ /**
6
+ * Echo webhook — demonstrates `defineWebhook` HMAC-SHA256 pattern
7
+ * without depending on an external provider (Stripe, GitHub, etc.).
8
+ *
9
+ * Self-test:
10
+ * SECRET=$(openssl rand -base64 32)
11
+ * echo -n '{"message":"hi"}' | openssl dgst -sha256 -hmac "$SECRET"
12
+ * curl -X POST localhost:3000/api/webhooks/echo \
13
+ * -H "x-echo-signature: <hex from above>" \
14
+ * -H "Content-Type: application/json" \
15
+ * -d '{"message":"hi"}'
16
+ */
17
+ const ECHO_SECRET = process.env.ECHO_WEBHOOK_SECRET ?? ''
18
+
19
+ export const POST = defineWebhook({
20
+ verify: ({ rawBody, headers }) => {
21
+ if (ECHO_SECRET === '') return false
22
+ const sig = headers.get('x-echo-signature') ?? ''
23
+ const expected = createHmac('sha256', ECHO_SECRET).update(rawBody).digest('hex')
24
+ try {
25
+ return timingSafeEqual(Buffer.from(sig, 'utf-8'), Buffer.from(expected, 'utf-8'))
26
+ } catch {
27
+ return false
28
+ }
29
+ },
30
+ inputSchema: z.object({ message: z.string() }),
31
+ handler: async ({ input }) => {
32
+ return Response.json({ echoed: input.message, timestamp: new Date().toISOString() })
33
+ },
34
+ })
@@ -0,0 +1,76 @@
1
+ # {{name}}
2
+
3
+ TheoKit dashboard project. Build the app your agent lives in — with nested layouts and a sidebar wired from day one.
4
+
5
+ > 📚 **Full docs:** https://docs.theokit.dev
6
+
7
+ ## Quick start
8
+
9
+ ```bash
10
+ # 1. Set your provider key (OpenRouter recommended — one key, any model)
11
+ echo 'OPENROUTER_API_KEY=sk-or-v1-...' > .env
12
+
13
+ # 2. Boot the dev server
14
+ npx theokit dev
15
+ ```
16
+
17
+ Open the printed URL. The default surface is a dashboard shell with sidebar nav + content area, ready to host your agent panels.
18
+
19
+ ## Templates
20
+
21
+ - **default** — TheoUI chat composer + agent route.
22
+ - **dashboard** (this one) — nested layouts + sidebar nav.
23
+ - **api-only** — server routes without React.
24
+ - **postgres** — Drizzle ORM + migrations.
25
+ - **saas** — full app with auth, billing, sessions.
26
+
27
+ ## What the framework auto-loads
28
+
29
+ - **`.env` → `process.env`**. Edit `.env`; restart the dev server.
30
+ - **`.theo/` build output cleanup** on every `theokit build`.
31
+ - **Tailwind + `@usetheo/ui` styling** auto-configured for the TheoUI surface.
32
+
33
+ ## Project structure
34
+
35
+ ```
36
+ app/ Frontend (file-based routing with nested layouts)
37
+ ├── layout.tsx root wrapper — TheoUI provider + theme
38
+ ├── page.tsx / — dashboard home
39
+ ├── dashboard/
40
+ │ ├── layout.tsx /dashboard/* — sidebar shell
41
+ │ └── page.tsx /dashboard — primary panel
42
+ server/ Backend (explicit routes)
43
+ ├── routes/
44
+ │ └── health.ts GET /api/health
45
+ theo.config.ts Framework config
46
+ tailwind.config.ts Tailwind theme tokens
47
+ .env Secrets — never committed
48
+ ```
49
+
50
+ ## Common commands
51
+
52
+ | Command | What it does |
53
+ |---|---|
54
+ | `npx theokit dev` | Dev server with HMR + devtools overlay |
55
+ | `npx theokit build` | Production build → `.theo/` |
56
+ | `npx theokit start` | Serve the production build |
57
+ | `npx theokit check` | Lint for upgrade-readiness |
58
+ | `npx theokit routes` | List all routes + actions detected |
59
+ | `npm run typecheck` | TypeScript strict check (no emit) |
60
+
61
+ ## Add a new panel
62
+
63
+ ```bash
64
+ mkdir -p app/dashboard/billing
65
+ cat > app/dashboard/billing/page.tsx <<'EOF'
66
+ export default function BillingPage() {
67
+ return <h2>Billing</h2>
68
+ }
69
+ EOF
70
+ ```
71
+
72
+ The route appears at `/dashboard/billing` after HMR.
73
+
74
+ ## License
75
+
76
+ Apply your own. The TheoKit framework is Apache-2.0.
@@ -10,7 +10,7 @@
10
10
  "typecheck": "tsc --noEmit"
11
11
  },
12
12
  "dependencies": {
13
- "theokit": "^0.1.0-alpha.12",
13
+ "theokit": "^0.1.0-alpha.14",
14
14
  "react": "^19.0.0",
15
15
  "react-dom": "^19.0.0"
16
16
  },
@@ -18,5 +18,10 @@
18
18
  "typescript": "^5.7.0",
19
19
  "@types/react": "^19.0.0",
20
20
  "@types/react-dom": "^19.0.0"
21
+ },
22
+ "pnpm": {
23
+ "onlyBuiltDependencies": [
24
+ "esbuild"
25
+ ]
21
26
  }
22
27
  }
@@ -0,0 +1,51 @@
1
+ import { defineCron } from 'theokit/server/cron'
2
+ import { readdir, stat, rm } from 'node:fs/promises'
3
+ import { join, resolve } from 'node:path'
4
+
5
+ /**
6
+ * Daily GC of stale conversation transcripts.
7
+ *
8
+ * The `@usetheo/sdk` Agent persists chat history under
9
+ * `.theokit/agents/<agentId>/messages.jsonl`. With no TTL the directory
10
+ * grows unbounded — production foot-gun. This cron removes any agent
11
+ * directory whose `messages.jsonl` hasn't been touched in 30 days.
12
+ */
13
+ const MAX_AGE_DAYS = 30
14
+ const AGENTS_DIR = '.theokit/agents'
15
+
16
+ export default defineCron({
17
+ name: 'cleanup-conversations',
18
+ schedule: '0 4 * * *', // Daily 04:00 UTC
19
+ handler: async ({ log }) => {
20
+ const root = resolve(process.cwd(), AGENTS_DIR)
21
+ const cutoff = Date.now() - MAX_AGE_DAYS * 24 * 60 * 60 * 1000
22
+ let removed = 0
23
+ let kept = 0
24
+ let entries: Awaited<ReturnType<typeof readdir>>
25
+ try {
26
+ entries = await readdir(root, { withFileTypes: true })
27
+ } catch {
28
+ log.info({ msg: 'No agents dir yet — first run', dir: root })
29
+ return
30
+ }
31
+ for (const entry of entries) {
32
+ if (!entry.isDirectory()) continue
33
+ const agentDir = join(root, entry.name)
34
+ const messagesFile = join(agentDir, 'messages.jsonl')
35
+ try {
36
+ const s = await stat(messagesFile)
37
+ if (s.mtimeMs < cutoff) {
38
+ await rm(agentDir, { recursive: true, force: true })
39
+ removed++
40
+ } else {
41
+ kept++
42
+ }
43
+ } catch {
44
+ // messages.jsonl missing → orphan dir, remove
45
+ await rm(agentDir, { recursive: true, force: true }).catch(() => {})
46
+ removed++
47
+ }
48
+ }
49
+ log.info({ msg: 'cleanup-conversations complete', removed, kept, maxAgeDays: MAX_AGE_DAYS })
50
+ },
51
+ })
@@ -2,6 +2,8 @@
2
2
 
3
3
  TheoKit project. Build the app your agent lives in — routing, auth, real-time, deploy — wired.
4
4
 
5
+ > 📚 **Full docs:** https://docs.theokit.dev
6
+
5
7
  ## Quick start
6
8
 
7
9
  ```bash
@@ -10,7 +10,7 @@
10
10
  "typecheck": "tsc --noEmit"
11
11
  },
12
12
  "dependencies": {
13
- "theokit": "^0.1.0-alpha.12",
13
+ "theokit": "^0.1.0-alpha.14",
14
14
  "@usetheo/sdk": "^1.2.0",
15
15
  "@usetheo/ui": "^0.12.0-next.0",
16
16
  "lucide-react": "^0.469.0",
@@ -25,5 +25,10 @@
25
25
  "@types/react-dom": "^19.0.0",
26
26
  "tailwindcss": "^4.0.0",
27
27
  "@tailwindcss/vite": "^4.0.0"
28
+ },
29
+ "pnpm": {
30
+ "onlyBuiltDependencies": [
31
+ "esbuild"
32
+ ]
28
33
  }
29
34
  }
@@ -0,0 +1,51 @@
1
+ import { defineCron } from 'theokit/server/cron'
2
+ import { readdir, stat, rm } from 'node:fs/promises'
3
+ import { join, resolve } from 'node:path'
4
+
5
+ /**
6
+ * Daily GC of stale conversation transcripts.
7
+ *
8
+ * The `@usetheo/sdk` Agent persists chat history under
9
+ * `.theokit/agents/<agentId>/messages.jsonl`. With no TTL the directory
10
+ * grows unbounded — production foot-gun. This cron removes any agent
11
+ * directory whose `messages.jsonl` hasn't been touched in 30 days.
12
+ */
13
+ const MAX_AGE_DAYS = 30
14
+ const AGENTS_DIR = '.theokit/agents'
15
+
16
+ export default defineCron({
17
+ name: 'cleanup-conversations',
18
+ schedule: '0 4 * * *', // Daily 04:00 UTC
19
+ handler: async ({ log }) => {
20
+ const root = resolve(process.cwd(), AGENTS_DIR)
21
+ const cutoff = Date.now() - MAX_AGE_DAYS * 24 * 60 * 60 * 1000
22
+ let removed = 0
23
+ let kept = 0
24
+ let entries: Awaited<ReturnType<typeof readdir>>
25
+ try {
26
+ entries = await readdir(root, { withFileTypes: true })
27
+ } catch {
28
+ log.info({ msg: 'No agents dir yet — first run', dir: root })
29
+ return
30
+ }
31
+ for (const entry of entries) {
32
+ if (!entry.isDirectory()) continue
33
+ const agentDir = join(root, entry.name)
34
+ const messagesFile = join(agentDir, 'messages.jsonl')
35
+ try {
36
+ const s = await stat(messagesFile)
37
+ if (s.mtimeMs < cutoff) {
38
+ await rm(agentDir, { recursive: true, force: true })
39
+ removed++
40
+ } else {
41
+ kept++
42
+ }
43
+ } catch {
44
+ // messages.jsonl missing → orphan dir, remove
45
+ await rm(agentDir, { recursive: true, force: true }).catch(() => {})
46
+ removed++
47
+ }
48
+ }
49
+ log.info({ msg: 'cleanup-conversations complete', removed, kept, maxAgeDays: MAX_AGE_DAYS })
50
+ },
51
+ })
@@ -0,0 +1,83 @@
1
+ # {{name}}
2
+
3
+ TheoKit project with Postgres + Drizzle ORM wired. Schema-first, migration-aware, typed end-to-end.
4
+
5
+ > 📚 **Full docs:** https://docs.theokit.dev
6
+
7
+ ## Quick start
8
+
9
+ ```bash
10
+ # 0. Provision Postgres
11
+ # Option A — local docker (one-liner):
12
+ docker run --name pg -e POSTGRES_PASSWORD=dev -p 5432:5432 -d postgres:16
13
+ # Option B — hosted: neon.tech, supabase.com, fly.io
14
+
15
+ # 1. Set env
16
+ cat > .env <<'EOF'
17
+ DATABASE_URL=postgres://postgres:dev@localhost:5432/postgres
18
+ OPENROUTER_API_KEY=sk-or-v1-...
19
+ EOF
20
+
21
+ # 2. Generate + apply migrations
22
+ pnpm db:generate
23
+ pnpm db:migrate
24
+
25
+ # 3. Boot the dev server
26
+ npx theokit dev
27
+ ```
28
+
29
+ Open the printed URL. The default surface includes a sample users API backed by Postgres.
30
+
31
+ ## Templates
32
+
33
+ - **default** — TheoUI chat composer + agent route.
34
+ - **dashboard** — nested layouts + sidebar.
35
+ - **api-only** — server routes without React.
36
+ - **postgres** (this one) — Drizzle ORM + migrations.
37
+ - **saas** — full app with auth, billing, sessions.
38
+
39
+ ## What the framework auto-loads
40
+
41
+ - **`.env` → `process.env`**. `DATABASE_URL` must be set before `pnpm db:migrate`.
42
+ - **`.theo/` build output cleanup** on every `theokit build`.
43
+ - **Drizzle schema** under `db/schema.ts` drives migrations + typed query builder.
44
+
45
+ ## Project structure
46
+
47
+ ```
48
+ app/ Frontend
49
+ ├── page.tsx / — sample UI
50
+ server/
51
+ ├── routes/
52
+ │ ├── health.ts GET /api/health
53
+ │ └── users.ts CRUD /api/users — backed by db.users
54
+ db/
55
+ ├── schema.ts Drizzle schema (tables + relations)
56
+ ├── client.ts Drizzle client (used by routes)
57
+ └── migrations/ Generated SQL files (committed)
58
+ drizzle.config.ts Drizzle CLI config
59
+ theo.config.ts Framework config
60
+ .env Secrets — never committed
61
+ ```
62
+
63
+ ## Common commands
64
+
65
+ | Command | What it does |
66
+ |---|---|
67
+ | `npx theokit dev` | Dev server with HMR |
68
+ | `npx theokit build` | Production build |
69
+ | `npx theokit start` | Serve production build |
70
+ | `pnpm db:generate` | Generate SQL migration from `db/schema.ts` |
71
+ | `pnpm db:migrate` | Apply pending migrations to `DATABASE_URL` |
72
+ | `pnpm db:studio` | Open Drizzle Studio UI |
73
+ | `npm run typecheck` | TypeScript strict check |
74
+
75
+ ## Troubleshooting
76
+
77
+ - **`pnpm db:migrate` fails with "ECONNREFUSED"** → check `DATABASE_URL` host:port + Postgres is running (`docker ps` or hosted dashboard).
78
+ - **`relation "users" does not exist`** → you forgot `pnpm db:migrate`. Run it.
79
+ - **Schema change not reflected** → `pnpm db:generate` first, then `pnpm db:migrate`.
80
+
81
+ ## License
82
+
83
+ Apply your own. The TheoKit framework is Apache-2.0.
@@ -14,7 +14,7 @@
14
14
  "db:studio": "drizzle-kit studio"
15
15
  },
16
16
  "dependencies": {
17
- "theokit": "^0.1.0-alpha.12",
17
+ "theokit": "^0.1.0-alpha.14",
18
18
  "react": "^19.0.0",
19
19
  "react-dom": "^19.0.0",
20
20
  "drizzle-orm": "^0.45.0",
@@ -26,5 +26,10 @@
26
26
  "@types/react": "^19.0.0",
27
27
  "@types/react-dom": "^19.0.0",
28
28
  "drizzle-kit": "^0.31.0"
29
+ },
30
+ "pnpm": {
31
+ "onlyBuiltDependencies": [
32
+ "esbuild"
33
+ ]
29
34
  }
30
35
  }
@@ -0,0 +1,27 @@
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({
16
+ name: 'log-message',
17
+ input: z.object({ userId: z.string(), message: z.string() }),
18
+ handler: async ({ input, log }) => {
19
+ // v1.1 EC-9: anchor path to process.cwd() — handler CWD may differ from
20
+ // project root when running via external job runner.
21
+ const auditPath = resolve(process.cwd(), '.theo/audit.log')
22
+ await mkdir(dirname(auditPath), { recursive: true })
23
+ const line = `${new Date().toISOString()} user=${input.userId} msg=${input.message}\n`
24
+ await appendFile(auditPath, line)
25
+ log.info({ msg: 'audit logged', userId: input.userId, path: auditPath })
26
+ },
27
+ })
@@ -0,0 +1,103 @@
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.
@@ -14,7 +14,7 @@
14
14
  "db:studio": "drizzle-kit studio"
15
15
  },
16
16
  "dependencies": {
17
- "theokit": "^0.1.0-alpha.12",
17
+ "theokit": "^0.1.0-alpha.14",
18
18
  "@usetheo/ui": "^0.12.0-next.0",
19
19
  "react": "^19.0.0",
20
20
  "react-dom": "^19.0.0",
@@ -28,5 +28,10 @@
28
28
  "@types/react": "^19.0.0",
29
29
  "@types/react-dom": "^19.0.0",
30
30
  "drizzle-kit": "^0.31.0"
31
+ },
32
+ "pnpm": {
33
+ "onlyBuiltDependencies": [
34
+ "esbuild"
35
+ ]
31
36
  }
32
37
  }
@@ -1,10 +1,20 @@
1
1
  import { defineAgentEndpoint, requireAuth, type AgentEvent } from 'theokit/server'
2
+ import { trackAgentRun } from 'theokit/server/cost'
2
3
  import type { RequestContext } from '../context.js'
3
4
 
4
5
  /**
5
6
  * Protected agent endpoint. `requireAuth` fires BEFORE the stream starts;
6
7
  * unauthorized requests get 401 immediately — no SSE bytes leak.
7
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
+ * `@usetheo/sdk` follow-up (R0.5.11). Devtools tab renders "$0.0000" —
16
+ * indicates "cost tracking not yet calibrated for this model".
17
+ *
8
18
  * Replace the mock generator with your LLM provider call.
9
19
  */
10
20
  export const POST = defineAgentEndpoint<{ message: string }, RequestContext>({
@@ -12,10 +22,28 @@ export const POST = defineAgentEndpoint<{ message: string }, RequestContext>({
12
22
  requireAuth(ctx.session)
13
23
  const body = (await request.json()) as { message?: string }
14
24
  const msg = body.message ?? ''
15
- yield {
16
- type: 'message',
17
- content: `Hello ${ctx.session.email}, you said: "${msg}"`,
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
+ )
18
47
  }
19
- yield { type: 'message', content: '(Replace this mock with your LLM.)' }
20
48
  },
21
49
  })
@@ -0,0 +1,49 @@
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
+ })