create-theokit 0.2.3 → 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.
- package/package.json +5 -1
- package/templates/default/.env.example +5 -0
- package/templates/default/README.md.tmpl +71 -57
- package/templates/default/_gitignore +2 -3
- package/templates/default/app/layout.tsx +51 -82
- package/templates/default/app/page.tsx +169 -271
- package/templates/default/app.ts +23 -0
- package/templates/default/package.json.tmpl +10 -23
- package/templates/default/server/agents/assistant.agent.ts +46 -0
- package/templates/default/server/controllers/tasks.controller.ts +70 -0
- package/templates/default/server/filters/http-error.filter.ts +20 -0
- package/templates/default/server/guards/auth.guard.ts +47 -0
- package/templates/default/server/interceptors/timing.interceptor.ts +14 -0
- package/templates/default/server/middleware/logger.middleware.ts +12 -0
- package/templates/default/server/store.ts +46 -0
- package/templates/default/server/toolboxes/task.tools.ts +58 -0
- package/templates/default/tsconfig.json +7 -7
- package/templates/api-only/.nvmrc +0 -1
- package/templates/api-only/README.md.tmpl +0 -78
- package/templates/api-only/_gitignore +0 -5
- package/templates/api-only/app/page.tsx +0 -3
- package/templates/api-only/index.html +0 -12
- package/templates/api-only/package.json.tmpl +0 -28
- package/templates/api-only/public/.gitkeep +0 -0
- package/templates/api-only/public/favicon.ico +0 -0
- package/templates/api-only/server/routes/health.ts +0 -5
- package/templates/api-only/server/routes/users.ts +0 -27
- package/templates/api-only/server/routes/webhooks/echo.ts +0 -34
- package/templates/api-only/theo.config.ts +0 -3
- package/templates/api-only/tsconfig.json +0 -15
- package/templates/dashboard/.nvmrc +0 -1
- package/templates/dashboard/README.md.tmpl +0 -76
- package/templates/dashboard/_gitignore +0 -5
- package/templates/dashboard/app/about/page.tsx +0 -3
- package/templates/dashboard/app/dashboard/layout.tsx +0 -10
- package/templates/dashboard/app/dashboard/page.tsx +0 -3
- package/templates/dashboard/app/layout.tsx +0 -14
- package/templates/dashboard/app/page.tsx +0 -8
- package/templates/dashboard/index.html +0 -12
- package/templates/dashboard/package.json.tmpl +0 -28
- package/templates/dashboard/public/.gitkeep +0 -0
- package/templates/dashboard/public/favicon.ico +0 -0
- package/templates/dashboard/server/crons/cleanup-conversations.ts +0 -59
- package/templates/dashboard/server/routes/health.ts +0 -5
- package/templates/dashboard/theo.config.ts +0 -3
- package/templates/dashboard/tsconfig.json +0 -15
- package/templates/default/.nvmrc +0 -1
- package/templates/default/index.html +0 -12
- package/templates/default/public/.gitkeep +0 -0
- package/templates/default/public/favicon.ico +0 -0
- package/templates/default/server/crons/cleanup-conversations.ts +0 -59
- package/templates/default/server/routes/chat.ts +0 -69
- package/templates/default/server/routes/health.ts +0 -5
- package/templates/default/theo.config.ts +0 -3
- package/templates/default/types/jobs.d.ts +0 -25
- package/templates/postgres/.env.example +0 -5
- package/templates/postgres/.nvmrc +0 -1
- package/templates/postgres/README.md.tmpl +0 -83
- package/templates/postgres/_gitignore +0 -5
- package/templates/postgres/app/layout.tsx +0 -14
- package/templates/postgres/app/page.tsx +0 -8
- package/templates/postgres/db/index.ts +0 -7
- package/templates/postgres/db/schema.ts +0 -8
- package/templates/postgres/drizzle.config.ts +0 -10
- package/templates/postgres/index.html +0 -12
- package/templates/postgres/package.json.tmpl +0 -36
- package/templates/postgres/public/.gitkeep +0 -0
- package/templates/postgres/public/favicon.ico +0 -0
- package/templates/postgres/server/context.ts +0 -5
- package/templates/postgres/server/jobs/log-message.ts +0 -26
- package/templates/postgres/server/routes/health.ts +0 -5
- package/templates/postgres/server/routes/users.ts +0 -22
- package/templates/postgres/theo.config.ts +0 -3
- package/templates/postgres/tsconfig.json +0 -15
- package/templates/saas/.env.example +0 -7
- package/templates/saas/.nvmrc +0 -1
- package/templates/saas/README.md.tmpl +0 -103
- package/templates/saas/_gitignore +0 -5
- package/templates/saas/app/layout.tsx +0 -5
- package/templates/saas/app/page.tsx +0 -104
- package/templates/saas/db/index.ts +0 -6
- package/templates/saas/db/schema.ts +0 -20
- package/templates/saas/drizzle.config.ts +0 -10
- package/templates/saas/index.html +0 -12
- package/templates/saas/package.json.tmpl +0 -38
- package/templates/saas/public/.gitkeep +0 -0
- package/templates/saas/public/favicon.ico +0 -0
- package/templates/saas/server/context.ts +0 -37
- package/templates/saas/server/routes/agent.ts +0 -49
- package/templates/saas/server/routes/billing/stripe-webhook.ts +0 -49
- package/templates/saas/server/routes/login.ts +0 -25
- package/templates/saas/server/routes/logout.ts +0 -10
- package/templates/saas/server/routes/me.ts +0 -10
- package/templates/saas/theo.config.ts +0 -5
- package/templates/saas/tsconfig.json +0 -15
- package/templates/services/agent-node/Dockerfile.tmpl +0 -20
- package/templates/services/agent-node/README.md +0 -38
- package/templates/services/agent-node/package.json.tmpl +0 -18
- package/templates/services/agent-node/src/index.ts +0 -58
- package/templates/services/agent-node/tsconfig.json +0 -13
- package/templates/services/agent-python/Dockerfile.tmpl +0 -20
- package/templates/services/agent-python/README.md +0 -37
- package/templates/services/agent-python/main.py +0 -77
- package/templates/services/agent-python/pyproject.toml.tmpl +0 -16
|
@@ -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,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,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,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.2.2",
|
|
18
|
-
"@theokit/ui": "^0.13.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,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"]
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
# Agent service (Node / Hono)
|
|
2
|
-
|
|
3
|
-
This is a TheoKit polyglot sidecar generated by `create-theokit --backend node`.
|
|
4
|
-
|
|
5
|
-
## Requirements
|
|
6
|
-
|
|
7
|
-
- **Node.js 22+** (matches TheoKit's floor)
|
|
8
|
-
- **pnpm** or **npm** (TheoKit's CLI prefers pnpm)
|
|
9
|
-
|
|
10
|
-
## Run it
|
|
11
|
-
|
|
12
|
-
The TheoKit app boots this service automatically via `pnpm dev` (per `theo.config.ts > services`).
|
|
13
|
-
|
|
14
|
-
To run standalone:
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
cd services/agent-node
|
|
18
|
-
pnpm install
|
|
19
|
-
pnpm dev
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
## Endpoints
|
|
23
|
-
|
|
24
|
-
- `GET /health` — healthcheck (returns `{"status":"ok"}`)
|
|
25
|
-
- `POST /echo` — example: `{ "message": "..." }` → `{ "echo": "..." }`
|
|
26
|
-
|
|
27
|
-
## What is wired
|
|
28
|
-
|
|
29
|
-
- Native fetch handler via Hono (works on TheoCloud and local)
|
|
30
|
-
- JSON-line stdout logs
|
|
31
|
-
- W3C `traceparent` propagation from incoming headers
|
|
32
|
-
- TheoKit injects `THEOKIT_SERVICE_NAME` + `THEOKIT_SERVICE_PORT` env vars
|
|
33
|
-
|
|
34
|
-
## Add a tool the TheoKit agent can call
|
|
35
|
-
|
|
36
|
-
Add another `app.post('/tool/<name>', ...)` handler. From the TheoKit TS side,
|
|
37
|
-
`services.worker.<name>({...})` is auto-typed when an OpenAPI URL is reachable
|
|
38
|
-
(Hey API integration — Phase 5).
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "{{name}}-agent-node",
|
|
3
|
-
"private": true,
|
|
4
|
-
"type": "module",
|
|
5
|
-
"scripts": {
|
|
6
|
-
"dev": "tsx watch src/index.ts",
|
|
7
|
-
"start": "tsx src/index.ts",
|
|
8
|
-
"build": "echo 'no build step; tsx runs TS directly'"
|
|
9
|
-
},
|
|
10
|
-
"dependencies": {
|
|
11
|
-
"hono": "^4.6.0",
|
|
12
|
-
"@hono/node-server": "^1.13.0"
|
|
13
|
-
},
|
|
14
|
-
"devDependencies": {
|
|
15
|
-
"tsx": "^4.19.0",
|
|
16
|
-
"typescript": "^5.7.0"
|
|
17
|
-
}
|
|
18
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TheoKit polyglot agent service — Hono sidecar.
|
|
3
|
-
*
|
|
4
|
-
* Conforms to the Like-Vercel runtime contract (ADR-0015):
|
|
5
|
-
* - Fetch-handler entry (native fetch handler from Hono)
|
|
6
|
-
* - GET /health convention (200 / 503)
|
|
7
|
-
* - JSON-line stdout logs
|
|
8
|
-
* - W3C traceparent propagation
|
|
9
|
-
* - Env vars at runtime
|
|
10
|
-
*
|
|
11
|
-
* Generated by `create-theokit --backend node`.
|
|
12
|
-
*/
|
|
13
|
-
import { Hono } from 'hono'
|
|
14
|
-
import { serve } from '@hono/node-server'
|
|
15
|
-
|
|
16
|
-
const SERVICE_NAME = process.env.THEOKIT_SERVICE_NAME ?? 'agent-node'
|
|
17
|
-
const SERVICE_PORT = Number.parseInt(process.env.THEOKIT_SERVICE_PORT ?? '8002', 10)
|
|
18
|
-
|
|
19
|
-
function log(
|
|
20
|
-
level: 'info' | 'warn' | 'error',
|
|
21
|
-
message: string,
|
|
22
|
-
extra: Record<string, unknown> = {},
|
|
23
|
-
) {
|
|
24
|
-
// eslint-disable-next-line no-console -- structured stdout per ADR-0015 invariant #5
|
|
25
|
-
console.log(
|
|
26
|
-
JSON.stringify({
|
|
27
|
-
timestamp: new Date().toISOString(),
|
|
28
|
-
level,
|
|
29
|
-
message,
|
|
30
|
-
service: SERVICE_NAME,
|
|
31
|
-
...extra,
|
|
32
|
-
}),
|
|
33
|
-
)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const app = new Hono()
|
|
37
|
-
|
|
38
|
-
// ADR-0015 invariant #6 — W3C traceparent propagation
|
|
39
|
-
app.use(async (c, next) => {
|
|
40
|
-
const tp = c.req.header('traceparent')
|
|
41
|
-
if (tp) {
|
|
42
|
-
log('info', 'request', { traceparent: tp, path: c.req.path })
|
|
43
|
-
}
|
|
44
|
-
await next()
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
// ADR-0015 invariant #4 — healthcheck convention
|
|
48
|
-
app.get('/health', (c) => c.json({ status: 'ok' }))
|
|
49
|
-
|
|
50
|
-
// Example endpoint — TheoKit proxies /api/worker/* to this service.
|
|
51
|
-
app.post('/echo', async (c) => {
|
|
52
|
-
const body = (await c.req.json()) as { message: string }
|
|
53
|
-
return c.json({ echo: body.message })
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
serve({ fetch: app.fetch, port: SERVICE_PORT }, () => {
|
|
57
|
-
log('info', `agent-node listening on :${String(SERVICE_PORT)}`)
|
|
58
|
-
})
|