| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259 |
- // KirinCode Cloud Server v0.4.0
- // Enterprise Usage Tracking + JWT Auth + SQLite
- import { Elysia, t } from "elysia"
- import { jwt } from "@elysiajs/jwt"
- import { cors } from "@elysiajs/cors"
- import { Database } from "bun:sqlite"
- import { hash, compare } from "bcryptjs"
- import { randomUUID } from "node:crypto"
- const JWT_SECRET = process.env.JWT_SECRET || "kirincode-cloud-secret-change-in-production"
- const PORT = parseInt(process.env.PORT || "3000")
- const DB_PATH = process.env.DB_PATH || "kirincode.db"
- // Initialize SQLite
- const db = new Database(DB_PATH)
- db.run("PRAGMA journal_mode=WAL")
- db.run("PRAGMA foreign_keys=ON")
- // Run schema
- const schema = await Bun.file("./src/db/schema.sql").text()
- db.run(schema)
- // Helper: calculate cost
- function calculateCost(provider: string, model: string, inputTokens: number, outputTokens: number, cacheTokens: number = 0): number {
- const row = db.query("SELECT input_price_per_1m, output_price_per_1m, cache_price_per_1m FROM pricing WHERE provider = ? AND model = ?").get(provider, model) as any
- if (!row) return 0
- const inputCost = (inputTokens / 1_000_000) * row.input_price_per_1m
- const outputCost = (outputTokens / 1_000_000) * row.output_price_per_1m
- const cacheCost = (cacheTokens / 1_000_000) * (row.cache_price_per_1m || 0)
- return Math.round((inputCost + outputCost + cacheCost) * 1e6) / 1e6
- }
- const app = new Elysia()
- .use(cors())
- .use(jwt({ name: "jwt", secret: JWT_SECRET }))
- // --- Auth Middleware ---
- .derive(async ({ jwt, headers, set }) => {
- const auth = headers["authorization"]
- if (!auth?.startsWith("Bearer ")) return { userId: null, userRole: null, userTeamId: null }
- try {
- const payload = await jwt.verify(auth.slice(7)) as any
- const user = db.query("SELECT role, team_id FROM users WHERE id = ?").get(payload.sub) as any
- return { userId: payload.sub as string, userRole: user?.role || null, userTeamId: user?.team_id || null }
- } catch {
- return { userId: null, userRole: null, userTeamId: null }
- }
- })
- // --- Public Routes ---
- .post("/api/v1/auth/register", async ({ body, set }) => {
- const existing = db.query("SELECT id FROM users WHERE email = ?").get(body.email)
- if (existing) { set.status = 409; return { error: "Email already registered" } }
- const id = randomUUID()
- const passwordHash = await hash(body.password, 10)
- db.run("INSERT INTO users (id, email, password_hash, display_name) VALUES (?, ?, ?, ?)", [id, body.email, passwordHash, body.display_name || body.email.split("@")[0]])
- const token = await app.decorator.jwt.sign({ sub: id })
- return { token, user: { id, email: body.email, display_name: body.display_name } }
- }, { body: t.Object({ email: t.String(), password: t.String(), display_name: t.Optional(t.String()) }) })
- .post("/api/v1/auth/login", async ({ body, set }) => {
- const user = db.query("SELECT id, email, password_hash, display_name, role, team_id FROM users WHERE email = ?").get(body.email) as any
- if (!user || !(await compare(body.password, user.password_hash))) {
- set.status = 401; return { error: "Invalid credentials" }
- }
- const token = await app.decorator.jwt.sign({ sub: user.id })
- return { token, user: { id: user.id, email: user.email, display_name: user.display_name, role: user.role, team_id: user.team_id } }
- }, { body: t.Object({ email: t.String(), password: t.String() }) })
- .post("/api/v1/auth/refresh", async ({ jwt, headers }) => {
- const auth = headers["authorization"]?.slice(7)
- if (!auth) return { error: "No token" }
- try {
- const payload = await jwt.verify(auth) as any
- const token = await app.decorator.jwt.sign({ sub: payload.sub })
- return { token }
- } catch { return { error: "Invalid token" } }
- })
- // --- Protected Routes ---
- .guard({ beforeHandle: ({ userId, set }) => { if (!userId) { set.status = 401; return "Unauthorized" } } })
- // --- Sessions ---
- .get("/api/v1/sessions", async ({ userId }) => {
- const sessions = db.query("SELECT id, title, agent, created_at, updated_at FROM sessions WHERE user_id = ? ORDER BY updated_at DESC LIMIT 50").all(userId)
- return { data: sessions }
- })
- .post("/api/v1/sessions", async ({ userId, body }) => {
- const id = randomUUID()
- db.run("INSERT INTO sessions (id, user_id, title, agent) VALUES (?, ?, ?, ?)", [id, userId, body.title || "New Chat", body.agent || "build"])
- return { data: { id, title: body.title, agent: body.agent } }
- }, { body: t.Object({ title: t.Optional(t.String()), agent: t.Optional(t.String()) }) })
- .post("/api/v1/sessions/:id/prompt", async ({ userId, params, body, set }) => {
- const session = db.query("SELECT id FROM sessions WHERE id = ? AND user_id = ?").get(params.id, userId)
- if (!session) { set.status = 404; return { error: "Session not found" } }
- const msgId = randomUUID()
- const text = (body.parts || []).filter((p: any) => p.type === "text").map((p: any) => p.text).join("\n")
- db.run("INSERT INTO messages (id, session_id, role, content) VALUES (?, ?, ?, ?)", [msgId, params.id, "user", text])
- db.run("UPDATE sessions SET updated_at = datetime('now') WHERE id = ?", [params.id])
- // AI response placeholder — records usage with estimated tokens
- const inputTokens = Math.ceil(text.length / 4)
- const outputTokens = Math.ceil(text.length / 2)
- const provider = body.model?.providerID || "anthropic"
- const model = body.model?.modelID || "claude-sonnet-4-5"
- const cost = calculateCost(provider, model, inputTokens, outputTokens)
- const user = db.query("SELECT team_id FROM users WHERE id = ?").get(userId) as any
- db.run(
- "INSERT INTO usage_events (user_id, team_id, provider, model, session_id, input_tokens, output_tokens, cost_usd) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
- [userId, user?.team_id || null, provider, model, params.id, inputTokens, outputTokens, cost]
- )
- const aiMsgId = randomUUID()
- db.run("INSERT INTO messages (id, session_id, role, content) VALUES (?, ?, ?, ?)", [aiMsgId, params.id, "assistant", `[AI response — ${outputTokens} tokens, $${cost}]`])
- const msgs = db.query("SELECT id, role, content, created_at FROM messages WHERE session_id = ? ORDER BY created_at").all(params.id)
- return { data: { messages: msgs, usage: { input_tokens: inputTokens, output_tokens: outputTokens, cost_usd: cost } } }
- })
- .get("/api/v1/sessions/:id/messages", async ({ userId, params }) => {
- return { data: db.query("SELECT id, role, content, created_at FROM messages WHERE session_id = (SELECT id FROM sessions WHERE id = ? AND user_id = ?) ORDER BY created_at").all(params.id, userId) }
- })
- // --- Admin: Usage Stats ---
- .guard({ beforeHandle: ({ userId, userRole, set }) => {
- if (!userId || !["admin", "team_admin"].includes(userRole || "")) { set.status = 403; return "Forbidden: admin access required" }
- }})
- .get("/api/v1/admin/usage/team", async ({ userTeamId, userRole, userId, query }) => {
- const teamId = userRole === "admin" ? query.team_id : userTeamId
- const from = query.from || "1970-01-01"
- const to = query.to || "2099-12-31"
- const rows = db.query(`
- SELECT date(created_at) as day, provider, model,
- SUM(input_tokens) as input_tokens,
- SUM(output_tokens) as output_tokens,
- COUNT(*) as request_count,
- SUM(cost_usd) as total_cost
- FROM usage_events
- WHERE team_id = ? AND created_at BETWEEN ? AND ?
- GROUP BY 1, 2, 3
- ORDER BY 1 DESC
- `).all(teamId, from, to)
- return { data: rows }
- })
- .get("/api/v1/admin/usage/users", async ({ userTeamId, userRole, query }) => {
- const teamId = userRole === "admin" ? query.team_id : userTeamId
- const from = query.from || "1970-01-01"
- const to = query.to || "2099-12-31"
- const rows = db.query(`
- SELECT u.display_name, u.email, ue.user_id,
- SUM(ue.input_tokens) as input_tokens,
- SUM(ue.output_tokens) as output_tokens,
- COUNT(*) as request_count,
- SUM(ue.cost_usd) as total_cost
- FROM usage_events ue
- JOIN users u ON u.id = ue.user_id
- WHERE ue.team_id = ? AND ue.created_at BETWEEN ? AND ?
- GROUP BY 1, 2, 3
- ORDER BY total_cost DESC
- `).all(teamId, from, to)
- return { data: rows }
- })
- .get("/api/v1/admin/usage/minute", async ({ userTeamId, userRole, userId, query }) => {
- const teamId = userRole === "admin" ? query.team_id : userTeamId
- const targetUser = query.user_id || userId
- const from = query.from || "1970-01-01"
- const to = query.to || "2099-12-31"
- const rows = db.query(`
- SELECT created_at as minute,
- input_tokens, output_tokens, provider, model, cost_usd
- FROM usage_events
- WHERE user_id = ? AND team_id = ? AND created_at BETWEEN ? AND ?
- ORDER BY created_at DESC
- LIMIT 1000
- `).all(targetUser, teamId, from, to)
- return { data: rows }
- })
- .get("/api/v1/admin/usage/today", async ({ userTeamId, userRole }) => {
- const teamId = userTeamId
- const rows = db.query(`
- SELECT
- SUM(input_tokens) as input_tokens,
- SUM(output_tokens) as output_tokens,
- COUNT(*) as request_count,
- SUM(cost_usd) as total_cost,
- COUNT(DISTINCT user_id) as active_users
- FROM usage_events
- WHERE team_id = ? AND date(created_at) = date('now')
- `).get(teamId)
- return { data: rows }
- })
- .get("/api/v1/admin/teams", async ({ userRole, set }) => {
- if (userRole !== "admin") { set.status = 403; return { error: "Admin only" } }
- return { data: db.query("SELECT id, name, owner_id, created_at FROM teams ORDER BY created_at").all() }
- })
- .post("/api/v1/admin/teams", async ({ userRole, body, set }) => {
- if (userRole !== "admin") { set.status = 403; return { error: "Admin only" } }
- const id = randomUUID()
- db.run("INSERT INTO teams (id, name, owner_id) VALUES (?, ?, ?)", [id, body.name, body.owner_id])
- db.run("INSERT INTO team_members (team_id, user_id, role) VALUES (?, ?, 'team_admin')", [id, body.owner_id])
- db.run("UPDATE users SET team_id = ?, role = 'team_admin' WHERE id = ?", [id, body.owner_id])
- return { data: { id, name: body.name } }
- }, { body: t.Object({ name: t.String(), owner_id: t.String() }) })
- // --- Memory ---
- .get("/api/v1/memory", async ({ userId }) => {
- const rows = db.query("SELECT id, category, content, created_at FROM memory_entries WHERE user_id = ? ORDER BY created_at DESC LIMIT 100").all(userId)
- return { data: rows }
- })
- .post("/api/v1/memory", async ({ userId, body }) => {
- if (!db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='memory_entries'").get()) {
- db.run("CREATE TABLE memory_entries (id TEXT PRIMARY KEY, user_id TEXT, category TEXT, content TEXT, created_at TEXT DEFAULT (datetime('now')))")
- }
- const id = randomUUID()
- db.run("INSERT INTO memory_entries (id, user_id, category, content) VALUES (?, ?, ?, ?)", [id, userId, body.category, body.content])
- return { data: { id, category: body.category } }
- })
- // --- Skills ---
- .get("/api/v1/skills", async ({ userId }) => {
- if (!db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='skills'").get()) {
- db.run("CREATE TABLE skills (id TEXT PRIMARY KEY, user_id TEXT, name TEXT, description TEXT, content TEXT, created_at TEXT DEFAULT (datetime('now')))")
- }
- return { data: db.query("SELECT id, name, description, created_at FROM skills WHERE user_id = ? ORDER BY created_at DESC").all(userId) }
- })
- .post("/api/v1/skills", async ({ userId, body }) => {
- if (!db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='skills'").get()) {
- db.run("CREATE TABLE skills (id TEXT PRIMARY KEY, user_id TEXT, name TEXT, description TEXT, content TEXT, created_at TEXT DEFAULT (datetime('now')))")
- }
- const id = randomUUID()
- db.run("INSERT INTO skills (id, user_id, name, description, content) VALUES (?, ?, ?, ?, ?)", [id, userId, body.name, body.description, body.content])
- return { data: { id, name: body.name } }
- })
- // --- Health ---
- .get("/api/v1/health", () => ({
- status: "ok",
- version: "0.4.0",
- db: db.query("SELECT COUNT(*) as users FROM users").get(),
- }))
- .listen(PORT)
- console.log(`🦄 KirinCode Cloud Server v0.4.0 — http://localhost:${PORT}`)
- console.log(` DB: ${DB_PATH} (${db.query("SELECT COUNT(*) as c FROM users").get() || 0} users)`)
- console.log(` API: http://localhost:${PORT}/api/v1/health`)
- export type App = typeof app
|