vibeoscore 1.0.2

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 (46) hide show
  1. package/.env.example +5 -0
  2. package/README.md +29 -0
  3. package/client.js +257 -0
  4. package/client.ts +334 -0
  5. package/dashboard/dist/assets/index-BnPt1Fii.js +1 -0
  6. package/dashboard/dist/assets/index-CfH00tOL.css +1 -0
  7. package/dashboard/dist/index.html +3 -0
  8. package/lib/blackbox-rf.js +1099 -0
  9. package/lib/blackbox.js +137 -0
  10. package/lib/compression.js +119 -0
  11. package/lib/db.js +106 -0
  12. package/lib/db.ts +113 -0
  13. package/lib/delegation.js +137 -0
  14. package/lib/meta-controller.js +418 -0
  15. package/lib/meta-controller.mjs +499 -0
  16. package/lib/patterns.js +150 -0
  17. package/lib/resolution-tracker.js +486 -0
  18. package/lib/stress.js +84 -0
  19. package/lib/tdd.js +218 -0
  20. package/lib/tier-routing.js +48 -0
  21. package/mcp-server.js +370 -0
  22. package/mcp-server.ts +364 -0
  23. package/middleware/auth.js +75 -0
  24. package/middleware/auth.ts +87 -0
  25. package/middleware/usage-logging.js +29 -0
  26. package/middleware/usage-logging.ts +41 -0
  27. package/nginx-vibetheog-api.conf +64 -0
  28. package/package.json +66 -0
  29. package/routes/admin.js +93 -0
  30. package/routes/admin.ts +107 -0
  31. package/routes/blackbox.js +463 -0
  32. package/routes/compression.js +12 -0
  33. package/routes/delegation.js +30 -0
  34. package/routes/patterns.js +53 -0
  35. package/routes/pricing.js +62 -0
  36. package/routes/stress.js +30 -0
  37. package/routes/tdd.js +68 -0
  38. package/routes/tier-routing.js +31 -0
  39. package/scripts/dashboard-server.mjs +246 -0
  40. package/scripts/deploy-zero-downtime.sh +77 -0
  41. package/scripts/deploy.sh +68 -0
  42. package/scripts/release.mjs +30 -0
  43. package/scripts/seed-master-token.js +29 -0
  44. package/scripts/start-all.mjs +34 -0
  45. package/server.js +88 -0
  46. package/vibeos-api.service +19 -0
package/mcp-server.ts ADDED
@@ -0,0 +1,364 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // SPDX-FileCopyrightText: 2026 vibeOS <https://github.com/DrunkkToys/vibeOS>
3
+
4
+ import http from "node:http"
5
+ import { IncomingMessage, ServerResponse } from "node:http"
6
+ import { parse as parseUrl } from "node:url"
7
+ import { URLSearchParams } from "node:url"
8
+ import { createReadStream, existsSync, statSync } from "node:fs"
9
+ import { extname, join, dirname } from "node:path"
10
+ import { fileURLToPath } from "node:url"
11
+
12
+ const MIME_MAP: Record<string, string> = {
13
+ ".html": "text/html; charset=utf-8",
14
+ ".js": "application/javascript; charset=utf-8",
15
+ ".css": "text/css; charset=utf-8",
16
+ ".json": "application/json; charset=utf-8",
17
+ ".png": "image/png",
18
+ ".ico": "image/x-icon",
19
+ }
20
+
21
+ type Deps = {
22
+ getState: () => unknown
23
+ getSavings: () => unknown
24
+ getTodos: () => unknown
25
+ getSessionMetrics: (sessionId: string) => unknown
26
+ getCurrentSessionId: () => string
27
+ listReports: (params: { type?: string; project?: string; hours?: number; fingerprint?: string }) => unknown
28
+ readReport: (id: string) => unknown
29
+ runDiagnose: () => unknown
30
+ runProject: () => unknown
31
+ runTrinity: (action: string, opts: { slot?: string; level?: string }) => Promise<unknown>
32
+ runResearchAudit: (hours: number) => unknown
33
+ saveReport: (params: { type: string; summary: string; findings: unknown[]; metrics: Record<string, unknown>; narrative: string; tags: unknown[] }) => string | null
34
+ generateSessionCheckout: () => unknown
35
+ }
36
+
37
+ type McpServer = {
38
+ start: (port: number) => Promise<http.Server>
39
+ close: () => Promise<void>
40
+ }
41
+
42
+ function json(res: ServerResponse, statusCode: number, data: unknown): void {
43
+ res.statusCode = statusCode
44
+ res.setHeader("Content-Type", "application/json")
45
+ res.end(JSON.stringify(data))
46
+ }
47
+
48
+ function parseBody(req: IncomingMessage): Promise<Record<string, unknown>> {
49
+ return new Promise((resolve, reject) => {
50
+ let raw = ""
51
+ req.on("data", (chunk: Buffer) => {
52
+ raw += String(chunk || "")
53
+ if (raw.length > 1024 * 1024) {
54
+ reject(new Error("payload too large"))
55
+ }
56
+ })
57
+ req.on("end", () => {
58
+ if (!raw.trim()) {
59
+ resolve({})
60
+ return
61
+ }
62
+ try {
63
+ resolve(JSON.parse(raw))
64
+ } catch {
65
+ reject(new Error("invalid request"))
66
+ }
67
+ })
68
+ req.on("error", reject)
69
+ })
70
+ }
71
+
72
+ const _MCP_FILENAME = fileURLToPath(import.meta.url)
73
+ const _MCP_DIR = dirname(_MCP_FILENAME)
74
+
75
+ function resolveDashboardDir(): string {
76
+ const c = [
77
+ join(_MCP_DIR, "dashboard", "dist"),
78
+ ]
79
+ for (const p of c) { if (existsSync(join(p, "index.html"))) return p }
80
+ return c[0]
81
+ }
82
+
83
+ const DASHBOARD_DIR = resolveDashboardDir()
84
+
85
+ const BACKEND_HEALTH_URL = process.env.VIBEOS_BACKEND_HEALTH_URL || "https://api.vibetheog.com/health"
86
+ const BACKEND_HEALTH_TTL_MS = 5_000
87
+
88
+ let backendHealth: { ok: boolean | null; checkedAt: number } = { ok: null, checkedAt: 0 }
89
+
90
+ async function probeBackendHealth(force = false): Promise<boolean | null> {
91
+ const now = Date.now()
92
+ if (!force && backendHealth.ok !== null && (now - backendHealth.checkedAt) < BACKEND_HEALTH_TTL_MS) {
93
+ return backendHealth.ok
94
+ }
95
+ try {
96
+ const ctl = new AbortController()
97
+ const timer = setTimeout(() => ctl.abort(), 1500)
98
+ const res = await fetch(BACKEND_HEALTH_URL, { signal: ctl.signal })
99
+ clearTimeout(timer)
100
+ backendHealth = { ok: res.ok, checkedAt: now }
101
+ return res.ok
102
+ } catch {
103
+ backendHealth = { ok: false, checkedAt: now }
104
+ return false
105
+ }
106
+ }
107
+
108
+ function sendFile(res: ServerResponse, fp: string): void {
109
+ if (!existsSync(fp)) { res.statusCode = 404; res.setHeader("Content-Type", "text/plain; charset=utf-8"); res.end("not found"); return }
110
+ const ext = extname(fp).toLowerCase(); const mime = MIME_MAP[ext] || "application/octet-stream"; const st = statSync(fp)
111
+ res.statusCode = 200; res.setHeader("Content-Type", mime); res.setHeader("Content-Length", st.size); res.setHeader("Cache-Control", "no-cache")
112
+ const s = createReadStream(fp); s.pipe(res); s.on("error", () => { res.statusCode = 500; res.end() })
113
+ }
114
+
115
+ function serveDashboard(res: ServerResponse, p: string): void {
116
+ const idx = join(DASHBOARD_DIR, "index.html"); let fp = join(DASHBOARD_DIR, p === "/" ? "index.html" : p)
117
+ if (existsSync(fp) && statSync(fp).isFile()) { sendFile(res, fp); return }
118
+ if (existsSync(idx)) { sendFile(res, idx); return }
119
+ res.statusCode = 404; res.setHeader("Content-Type", "text/plain; charset=utf-8"); res.end("not found")
120
+ }
121
+
122
+ export function createMcpServer(deps: Deps): McpServer {
123
+ let server: http.Server | null = null
124
+ let startPromise: Promise<http.Server> | null = null
125
+ let closePromise: Promise<void> | null = null
126
+
127
+ const handler = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {
128
+ try {
129
+ const method = (req.method || "GET").toUpperCase()
130
+ const parsed = parseUrl(req.url || "/", true)
131
+ const path = parsed.pathname || "/"
132
+
133
+ if (method === "GET" && path === "/status") {
134
+ const state = deps.getState() as Record<string, unknown>
135
+ const ok = await probeBackendHealth()
136
+ json(res, 200, { ...state, backend_connected: ok === true, backend_health_url: BACKEND_HEALTH_URL })
137
+ return
138
+ }
139
+ if (method === "GET" && path === "/savings") {
140
+ json(res, 200, deps.getSavings())
141
+ return
142
+ }
143
+ if (method === "GET" && path === "/todos") {
144
+ json(res, 200, deps.getTodos())
145
+ return
146
+ }
147
+ if (method === "GET" && path === "/sessions") {
148
+ const state = deps.getState() as Record<string, unknown> | null
149
+ const sessionsMap = state?.sessions_raw as Record<string, Record<string, unknown>> | undefined || {}
150
+ const sessions = Object.entries(sessionsMap).map(([id, ses]) => ({
151
+ id,
152
+ started: ses?.started || null,
153
+ cost_usd: Number(ses?.cost_usd ?? 0) || 0,
154
+ delegation_savings_usd: Array.isArray(ses?.warns)
155
+ ? (ses.warns as Array<Record<string, unknown>>).reduce((sum, w) => sum + (Number(w?.est_savings_usd ?? 0) || 0), 0)
156
+ : ses?.total_savings_usd || 0,
157
+ cache_savings_usd: Number(ses?.cache_savings_usd ?? 0) || 0,
158
+ warns_count: Array.isArray(ses?.warns) ? ses.warns.length : 0,
159
+ }))
160
+ json(res, 200, { sessions, total_sessions: sessions.length })
161
+ return
162
+ }
163
+ if (method === "GET" && path === "/sessions/current") {
164
+ json(res, 200, deps.getSessionMetrics(deps.getCurrentSessionId()))
165
+ return
166
+ }
167
+ if (method === "GET" && path === "/reports") {
168
+ try {
169
+ const query = parsed.query as Record<string, string | undefined>
170
+ const type = typeof query.type === "string" ? query.type : undefined
171
+ const project = typeof query.project === "string" ? query.project : undefined
172
+ const hoursRaw = query.hours
173
+ const hours = hoursRaw != null ? Number(hoursRaw) : undefined
174
+ const fingerprint = typeof query.fingerprint === "string" ? query.fingerprint : undefined
175
+ const reports = deps.listReports({ type, project, hours: Number.isFinite(hours as number) ? hours : undefined, fingerprint })
176
+ json(res, 200, reports)
177
+ } catch (err: unknown) {
178
+ const error = err as { status?: number }
179
+ if (error?.status === 404) {
180
+ json(res, 404, { error: "not found", status: 404 })
181
+ return
182
+ }
183
+ throw err
184
+ }
185
+ return
186
+ }
187
+ if (method === "GET" && path.startsWith("/reports/")) {
188
+ const id = decodeURIComponent(path.replace(/^\/reports\//, "")).trim()
189
+ const report = deps.readReport(id)
190
+ if (!report) {
191
+ json(res, 404, { error: "not found", status: 404 })
192
+ return
193
+ }
194
+ json(res, 200, report)
195
+ return
196
+ }
197
+ if (method === "GET" && path === "/diagnose") {
198
+ json(res, 200, deps.runDiagnose())
199
+ return
200
+ }
201
+ if (method === "GET" && path === "/project") {
202
+ json(res, 200, deps.runProject())
203
+ return
204
+ }
205
+ if (method === "POST" && path === "/trinity") {
206
+ let body: Record<string, unknown>
207
+ try {
208
+ body = await parseBody(req)
209
+ } catch {
210
+ json(res, 400, { error: "invalid request", status: 400 })
211
+ return
212
+ }
213
+ const action = body?.action as string | undefined
214
+ const slot = body?.slot as string | undefined
215
+ const level = body?.level as string | undefined
216
+ if (!action || typeof action !== "string") {
217
+ json(res, 400, { error: "invalid request", status: 400 })
218
+ return
219
+ }
220
+ const result = await deps.runTrinity(action, { slot, level })
221
+ const txt = typeof result === "string" ? result : JSON.stringify(result)
222
+ const ok = !(txt.startsWith("❌") || txt.toLowerCase().includes("unknown action"))
223
+ json(res, ok ? 200 : 400, ok ? { ok: true, result } : { ok: false, error: txt })
224
+ return
225
+ }
226
+ if (method === "POST" && path === "/research-audit") {
227
+ let body: Record<string, unknown>
228
+ try {
229
+ body = await parseBody(req)
230
+ } catch {
231
+ json(res, 400, { error: "invalid request", status: 400 })
232
+ return
233
+ }
234
+ const hours = Number(body?.hours ?? 24)
235
+ const report = deps.runResearchAudit(Number.isFinite(hours) ? hours : 24)
236
+ json(res, 200, report)
237
+ return
238
+ }
239
+ if (method === "POST" && path === "/reports") {
240
+ let body: Record<string, unknown>
241
+ try {
242
+ body = await parseBody(req)
243
+ } catch {
244
+ json(res, 400, { error: "invalid request", status: 400 })
245
+ return
246
+ }
247
+ if (!body || typeof body !== "object") {
248
+ json(res, 400, { error: "invalid request", status: 400 })
249
+ return
250
+ }
251
+ const id = deps.saveReport({
252
+ type: "manual",
253
+ summary: (body.summary as string) || "",
254
+ findings: (body.findings as unknown[]) || [],
255
+ metrics: (body.metrics as Record<string, unknown>) || {},
256
+ narrative: (body.narrative as string) || "",
257
+ tags: Array.isArray(body.tags) ? body.tags : [],
258
+ })
259
+ if (!id) {
260
+ json(res, 500, { error: "failed to save report", status: 500 })
261
+ return
262
+ }
263
+ json(res, 200, { ok: true, id })
264
+ return
265
+ }
266
+ if (method === "POST" && path === "/sessions/checkout") {
267
+ const result = deps.generateSessionCheckout()
268
+ json(res, 200, result)
269
+ return
270
+ }
271
+ if (method === "GET" && path === "/events") {
272
+ res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "Access-Control-Allow-Origin": "*" })
273
+ const push = async () => { const state = deps.getState() as Record<string, unknown>; const ok = await probeBackendHealth(); res.write(`data: ${JSON.stringify({ status: { ...state, backend_connected: ok === true, backend_health_url: BACKEND_HEALTH_URL }, savings: deps.getSavings(), todos: deps.getTodos() })}\n\n`) }; push()
274
+ const iv = setInterval(push, 1500)
275
+ req.on("close", () => { clearInterval(iv) })
276
+ return
277
+ }
278
+
279
+ if (existsSync(join(DASHBOARD_DIR, "index.html"))) { serveDashboard(res, path); return }
280
+ json(res, 404, { error: "not found", status: 404 })
281
+ } catch (err: unknown) {
282
+ const error = err as { message?: string }
283
+ json(res, 500, { error: error?.message || "internal error", status: 500 })
284
+ }
285
+ }
286
+
287
+ return {
288
+ async start(port: number): Promise<http.Server> {
289
+ if (closePromise) await closePromise
290
+ if (server) return server
291
+ if (startPromise) return startPromise
292
+
293
+ const listen = (listenPort: number): Promise<http.Server> => new Promise((resolve, reject) => {
294
+ const nextServer = http.createServer((req: IncomingMessage, res: ServerResponse) => {
295
+ void handler(req, res)
296
+ })
297
+ const onListening = () => resolve(nextServer)
298
+ const onError = (err: Error) => {
299
+ try { nextServer.close() } catch { }
300
+ reject(err)
301
+ }
302
+ nextServer.once("listening", onListening)
303
+ nextServer.once("error", onError)
304
+ try {
305
+ nextServer.listen(listenPort, "127.0.0.1")
306
+ } catch (err: unknown) {
307
+ onError(err as Error)
308
+ }
309
+ })
310
+
311
+ startPromise = (async () => {
312
+ try {
313
+ server = await listen(port)
314
+ return server
315
+ } catch (err: unknown) {
316
+ const error = err as { code?: string; message: string }
317
+ if (error?.code !== "EADDRINUSE" || port === 0) {
318
+ startPromise = null
319
+ server = null
320
+ console.error(`[vibeOS] MCP server bind failed: ${error.message}`)
321
+ throw err
322
+ }
323
+ try {
324
+ const fallback = await listen(0)
325
+ server = fallback
326
+ const bound = fallback.address()
327
+ const actualPort = typeof bound === "object" && bound ? (bound as { port: number }).port : 0
328
+ console.error(`[vibeOS] MCP server port ${port} busy; fell back to ${actualPort}`)
329
+ return fallback
330
+ } catch (fallbackErr: unknown) {
331
+ const fbError = fallbackErr as { message: string }
332
+ startPromise = null
333
+ server = null
334
+ console.error(`[vibeOS] MCP server bind failed: ${fbError.message}`)
335
+ throw fallbackErr
336
+ }
337
+ } finally {
338
+ startPromise = null
339
+ }
340
+ })()
341
+ return startPromise
342
+ },
343
+
344
+ close(): Promise<void> {
345
+ if (!server) return closePromise || Promise.resolve()
346
+ if (closePromise) return closePromise
347
+ const current = server
348
+ closePromise = new Promise((resolve) => {
349
+ try {
350
+ current.close(() => {
351
+ if (server === current) server = null
352
+ closePromise = null
353
+ resolve()
354
+ })
355
+ } catch {
356
+ if (server === current) server = null
357
+ closePromise = null
358
+ resolve()
359
+ }
360
+ })
361
+ return closePromise
362
+ },
363
+ }
364
+ }
@@ -0,0 +1,75 @@
1
+ import crypto from "node:crypto";
2
+ import { getDb } from "../lib/db.js";
3
+ const MASTER_KEY = process.env.VIBEOS_API_MASTER_KEY;
4
+ export function authMiddleware(fastify) {
5
+ fastify.addHook("onRequest", async (request, reply) => {
6
+ if (request.url.startsWith("/health") || request.url.startsWith("/favicon")) {
7
+ return;
8
+ }
9
+ if (request.url.startsWith("/admin/")) {
10
+ const authHeader = request.headers["authorization"];
11
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
12
+ return reply.code(401).send({ error: "unauthorized", message: "Missing or invalid Authorization header" });
13
+ }
14
+ const providedKey = authHeader.slice(7);
15
+ const keyBuffer = Buffer.from(providedKey);
16
+ const expectedBuffer = Buffer.from(MASTER_KEY);
17
+ if (keyBuffer.length !== expectedBuffer.length || !crypto.timingSafeEqual(keyBuffer, expectedBuffer)) {
18
+ return reply.code(403).send({ error: "forbidden", message: "Invalid master key" });
19
+ }
20
+ request.adminAuth = true;
21
+ return;
22
+ }
23
+ if (request.url.startsWith("/api/v1/")) {
24
+ const authHeader = request.headers["authorization"];
25
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
26
+ return reply.code(401).send({ error: "unauthorized", message: "Missing or invalid Authorization header" });
27
+ }
28
+ const providedToken = authHeader.slice(7);
29
+ const db = getDb();
30
+ const tokenRow = db.prepare(`
31
+ SELECT t.id, t.token, t.seat_id, t.status, t.expires_at, t.label,
32
+ s.status as seat_status
33
+ FROM api_tokens t
34
+ JOIN seats s ON t.seat_id = s.id
35
+ WHERE t.token = ?
36
+ `).get(providedToken);
37
+ if (!tokenRow) {
38
+ return reply.code(401).send({ error: "unauthorized", message: "Invalid API token" });
39
+ }
40
+ if (tokenRow.status === "revoked") {
41
+ return reply.code(403).send({
42
+ error: "forbidden",
43
+ message: "API token has been revoked",
44
+ code: "TOKEN_REVOKED"
45
+ });
46
+ }
47
+ if (tokenRow.status === "expired") {
48
+ return reply.code(403).send({
49
+ error: "forbidden",
50
+ message: "API token has expired",
51
+ code: "TOKEN_EXPIRED"
52
+ });
53
+ }
54
+ if (tokenRow.seat_status !== "active") {
55
+ return reply.code(403).send({
56
+ error: "forbidden",
57
+ message: "License seat is not active. Contact support.",
58
+ code: "SEAT_INACTIVE"
59
+ });
60
+ }
61
+ if (tokenRow.expires_at && new Date(tokenRow.expires_at) < new Date()) {
62
+ db.prepare("UPDATE api_tokens SET status = 'expired', revoked_at = datetime('now') WHERE id = ?").run(tokenRow.id);
63
+ return reply.code(403).send({
64
+ error: "forbidden",
65
+ message: "API token has expired",
66
+ code: "TOKEN_EXPIRED"
67
+ });
68
+ }
69
+ db.prepare("UPDATE api_tokens SET last_used_at = datetime('now') WHERE id = ?").run(tokenRow.id);
70
+ request.tokenId = tokenRow.id;
71
+ request.seatId = tokenRow.seat_id;
72
+ request.tokenLabel = tokenRow.label;
73
+ }
74
+ });
75
+ }
@@ -0,0 +1,87 @@
1
+ import crypto from "node:crypto"
2
+ import { getDb } from "../lib/db.js"
3
+
4
+ const MASTER_KEY = process.env.VIBEOS_API_MASTER_KEY
5
+
6
+ export function authMiddleware(fastify: any) {
7
+ fastify.addHook("onRequest", async (request: any, reply: any) => {
8
+ if (request.url.startsWith("/health") || request.url.startsWith("/favicon")) {
9
+ return
10
+ }
11
+
12
+ if (request.url.startsWith("/admin/")) {
13
+ const authHeader = request.headers["authorization"]
14
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
15
+ return reply.code(401).send({ error: "unauthorized", message: "Missing or invalid Authorization header" })
16
+ }
17
+ const providedKey = authHeader.slice(7)
18
+ const keyBuffer = Buffer.from(providedKey)
19
+ const expectedBuffer = Buffer.from(MASTER_KEY)
20
+ if (keyBuffer.length !== expectedBuffer.length || !crypto.timingSafeEqual(keyBuffer, expectedBuffer)) {
21
+ return reply.code(403).send({ error: "forbidden", message: "Invalid master key" })
22
+ }
23
+ request.adminAuth = true
24
+ return
25
+ }
26
+
27
+ if (request.url.startsWith("/api/v1/")) {
28
+ const authHeader = request.headers["authorization"]
29
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
30
+ return reply.code(401).send({ error: "unauthorized", message: "Missing or invalid Authorization header" })
31
+ }
32
+ const providedToken = authHeader.slice(7)
33
+
34
+ const db = getDb()
35
+ const tokenRow = db.prepare(`
36
+ SELECT t.id, t.token, t.seat_id, t.status, t.expires_at, t.label,
37
+ s.status as seat_status
38
+ FROM api_tokens t
39
+ JOIN seats s ON t.seat_id = s.id
40
+ WHERE t.token = ?
41
+ `).get(providedToken)
42
+
43
+ if (!tokenRow) {
44
+ return reply.code(401).send({ error: "unauthorized", message: "Invalid API token" })
45
+ }
46
+
47
+ if (tokenRow.status === "revoked") {
48
+ return reply.code(403).send({
49
+ error: "forbidden",
50
+ message: "API token has been revoked",
51
+ code: "TOKEN_REVOKED"
52
+ })
53
+ }
54
+
55
+ if (tokenRow.status === "expired") {
56
+ return reply.code(403).send({
57
+ error: "forbidden",
58
+ message: "API token has expired",
59
+ code: "TOKEN_EXPIRED"
60
+ })
61
+ }
62
+
63
+ if (tokenRow.seat_status !== "active") {
64
+ return reply.code(403).send({
65
+ error: "forbidden",
66
+ message: "License seat is not active. Contact support.",
67
+ code: "SEAT_INACTIVE"
68
+ })
69
+ }
70
+
71
+ if (tokenRow.expires_at && new Date(tokenRow.expires_at) < new Date()) {
72
+ db.prepare("UPDATE api_tokens SET status = 'expired', revoked_at = datetime('now') WHERE id = ?").run(tokenRow.id)
73
+ return reply.code(403).send({
74
+ error: "forbidden",
75
+ message: "API token has expired",
76
+ code: "TOKEN_EXPIRED"
77
+ })
78
+ }
79
+
80
+ db.prepare("UPDATE api_tokens SET last_used_at = datetime('now') WHERE id = ?").run(tokenRow.id)
81
+
82
+ request.tokenId = tokenRow.id
83
+ request.seatId = tokenRow.seat_id
84
+ request.tokenLabel = tokenRow.label
85
+ }
86
+ })
87
+ }
@@ -0,0 +1,29 @@
1
+ import { getDb } from "../lib/db.js";
2
+ export function usageLoggingMiddleware(fastify) {
3
+ fastify.addHook("onResponse", async (request, reply) => {
4
+ const urlPath = request.url.split("?")[0];
5
+ if (!urlPath.startsWith("/api/v1/") && !urlPath.startsWith("/admin/"))
6
+ return;
7
+ try {
8
+ const db = getDb();
9
+ const duration = request.hrtime ? Math.round(request.hrtime()[1] / 1e6) : 0;
10
+ const requestBody = request.body ? JSON.stringify(request.body).substring(0, 4096) : null;
11
+ const responseSize = reply.getHeader("content-length") || 0;
12
+ if (request.tokenId) {
13
+ db.prepare([
14
+ "INSERT INTO usage_log (token_id, endpoint, request_body, response_size, latency_ms)",
15
+ "VALUES (?, ?, ?, ?, ?)"
16
+ ].join(" ")).run(request.tokenId, urlPath, requestBody, responseSize, duration);
17
+ }
18
+ else if (request.adminAuth) {
19
+ db.prepare([
20
+ "INSERT INTO admin_audit_log (method, endpoint, request_body, response_status, response_size, latency_ms)",
21
+ "VALUES (?, ?, ?, ?, ?, ?)"
22
+ ].join(" ")).run(request.method, urlPath, requestBody, reply.statusCode, responseSize, duration);
23
+ }
24
+ }
25
+ catch (err) {
26
+ console.error("[vibeOS-api] usage logging error: " + err.message);
27
+ }
28
+ });
29
+ }
@@ -0,0 +1,41 @@
1
+ import { getDb } from "../lib/db.js"
2
+
3
+ export function usageLoggingMiddleware(fastify: any) {
4
+ fastify.addHook("onResponse", async (request: any, reply: any) => {
5
+ const urlPath = request.url.split("?")[0]
6
+ if (!urlPath.startsWith("/api/v1/") && !urlPath.startsWith("/admin/")) return
7
+
8
+ try {
9
+ const db = getDb()
10
+ const duration = request.hrtime ? Math.round(request.hrtime()[1] / 1e6) : 0
11
+ const requestBody = request.body ? JSON.stringify(request.body).substring(0, 4096) : null
12
+ const responseSize = reply.getHeader("content-length") || 0
13
+ if (request.tokenId) {
14
+ db.prepare([
15
+ "INSERT INTO usage_log (token_id, endpoint, request_body, response_size, latency_ms)",
16
+ "VALUES (?, ?, ?, ?, ?)"
17
+ ].join(" ")).run(
18
+ request.tokenId,
19
+ urlPath,
20
+ requestBody,
21
+ responseSize,
22
+ duration
23
+ )
24
+ } else if (request.adminAuth) {
25
+ db.prepare([
26
+ "INSERT INTO admin_audit_log (method, endpoint, request_body, response_status, response_size, latency_ms)",
27
+ "VALUES (?, ?, ?, ?, ?, ?)"
28
+ ].join(" ")).run(
29
+ request.method,
30
+ urlPath,
31
+ requestBody,
32
+ reply.statusCode,
33
+ responseSize,
34
+ duration
35
+ )
36
+ }
37
+ } catch (err: any) {
38
+ console.error("[vibeOS-api] usage logging error: " + err.message)
39
+ }
40
+ })
41
+ }
@@ -0,0 +1,64 @@
1
+ limit_req_zone $binary_remote_addr zone=vibeos_api:10m rate=30r/s;
2
+
3
+ server {
4
+ listen 80;
5
+ server_name api.vibetheog.com;
6
+
7
+ location /.well-known/acme-challenge/ {
8
+ root /var/www/certbot;
9
+ }
10
+
11
+
12
+ # Reject requests with spoofed or missing Host header (SMG3 fix)
13
+ if ($host !~* ^(api\.vibetheog\.com|localhost|127\.0\.0\.1)$) {
14
+ return 444;
15
+ }
16
+
17
+ location / {
18
+ return 301 https://$host$request_uri;
19
+ }
20
+ }
21
+
22
+ server {
23
+ listen 443 ssl http2;
24
+ server_name api.vibetheog.com;
25
+
26
+ ssl_certificate /etc/letsencrypt/live/vibetheog.com/fullchain.pem;
27
+ ssl_certificate_key /etc/letsencrypt/live/vibetheog.com/privkey.pem;
28
+
29
+ client_max_body_size 10M;
30
+
31
+ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
32
+ add_header X-Content-Type-Options "nosniff" always;
33
+ add_header X-Frame-Options "DENY" always;
34
+ add_header X-XSS-Protection "1; mode=block" always;
35
+
36
+ limit_req zone=vibeos_api burst=50 nodelay;
37
+
38
+
39
+ # Reject requests with spoofed or missing Host header (SMG3 fix)
40
+ if ($host !~* ^(api\.vibetheog\.com|localhost|127\.0\.0\.1)$) {
41
+ return 444;
42
+ }
43
+
44
+ location / {
45
+ proxy_pass http://127.0.0.1:3000;
46
+ proxy_http_version 1.1;
47
+ proxy_set_header Upgrade $http_upgrade;
48
+ proxy_set_header Connection 'upgrade';
49
+ proxy_set_header Host $host;
50
+ proxy_set_header X-Real-IP $remote_addr;
51
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
52
+ proxy_set_header X-Forwarded-Proto $scheme;
53
+ proxy_cache_bypass $http_upgrade;
54
+ proxy_read_timeout 30s;
55
+ }
56
+
57
+ location /health {
58
+ limit_req zone=vibeos_api burst=20 nodelay;
59
+ proxy_pass http://127.0.0.1:3000/health;
60
+ proxy_http_version 1.1;
61
+ proxy_set_header Host $host;
62
+ proxy_read_timeout 10s;
63
+ }
64
+ }