vibeoscore 1.0.2 → 1.0.8
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/client.js +1 -0
- package/client.ts +2 -0
- package/lib/logger.js +27 -0
- package/mcp-server.js +5 -4
- package/mcp-server.ts +4 -3
- package/package.json +4 -10
- package/dashboard/dist/assets/index-BnPt1Fii.js +0 -1
- package/dashboard/dist/assets/index-CfH00tOL.css +0 -1
- package/dashboard/dist/index.html +0 -3
- package/lib/blackbox-rf.js +0 -1099
- package/lib/blackbox.js +0 -137
- package/lib/compression.js +0 -119
- package/lib/db.js +0 -106
- package/lib/db.ts +0 -113
- package/lib/delegation.js +0 -137
- package/lib/meta-controller.js +0 -418
- package/lib/meta-controller.mjs +0 -499
- package/lib/patterns.js +0 -150
- package/lib/resolution-tracker.js +0 -486
- package/lib/stress.js +0 -84
- package/lib/tdd.js +0 -218
- package/lib/tier-routing.js +0 -48
- package/middleware/auth.js +0 -75
- package/middleware/auth.ts +0 -87
- package/middleware/usage-logging.js +0 -29
- package/middleware/usage-logging.ts +0 -41
- package/nginx-vibetheog-api.conf +0 -64
- package/routes/admin.js +0 -93
- package/routes/admin.ts +0 -107
- package/routes/blackbox.js +0 -463
- package/routes/compression.js +0 -12
- package/routes/delegation.js +0 -30
- package/routes/patterns.js +0 -53
- package/routes/pricing.js +0 -62
- package/routes/stress.js +0 -30
- package/routes/tdd.js +0 -68
- package/routes/tier-routing.js +0 -31
- package/scripts/dashboard-server.mjs +0 -246
- package/scripts/deploy-zero-downtime.sh +0 -77
- package/scripts/deploy.sh +0 -68
- package/scripts/release.mjs +0 -30
- package/scripts/seed-master-token.js +0 -29
- package/scripts/start-all.mjs +0 -34
- package/server.js +0 -88
- package/vibeos-api.service +0 -19
package/lib/tdd.js
DELETED
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
function extractExports(sourceContent, ext) {
|
|
2
|
-
if (!sourceContent || typeof sourceContent !== "string") return []
|
|
3
|
-
const exports = []
|
|
4
|
-
const seen = new Set()
|
|
5
|
-
const add = (name, type = "function") => {
|
|
6
|
-
if (name && !seen.has(name)) { seen.add(name); exports.push({ name, type }) }
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
switch (ext) {
|
|
10
|
-
case "py": {
|
|
11
|
-
const defRe = /^def\s+([a-zA-Z_]\w*)\s*\(/gm
|
|
12
|
-
const classRe = /^class\s+([a-zA-Z_]\w*)/gm
|
|
13
|
-
let m
|
|
14
|
-
while ((m = defRe.exec(sourceContent)) !== null) add(m[1], "function")
|
|
15
|
-
while ((m = classRe.exec(sourceContent)) !== null) add(m[1], "class")
|
|
16
|
-
break
|
|
17
|
-
}
|
|
18
|
-
case "js": case "mjs": case "jsx": {
|
|
19
|
-
const funcRe = /^(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$]\w*)\s*\(/gm
|
|
20
|
-
const constRe = /^(?:export\s+)?const\s+([a-zA-Z_$]\w*)\s*[:=]\s*(?:async\s+)?(?:\(|function)/gm
|
|
21
|
-
let m
|
|
22
|
-
while ((m = funcRe.exec(sourceContent)) !== null) add(m[1], "function")
|
|
23
|
-
while ((m = constRe.exec(sourceContent)) !== null) add(m[1], "function")
|
|
24
|
-
break
|
|
25
|
-
}
|
|
26
|
-
case "ts": case "tsx": {
|
|
27
|
-
const funcRe = /^(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$]\w*)\s*\(/gm
|
|
28
|
-
const constRe = /^(?:export\s+)?const\s+([a-zA-Z_$]\w*)\s*[:=]\s*(?:async\s+)?(?:\(|function)/gm
|
|
29
|
-
const classRe = /^(?:export\s+)?class\s+([a-zA-Z_$]\w*)/gm
|
|
30
|
-
let m
|
|
31
|
-
while ((m = funcRe.exec(sourceContent)) !== null) add(m[1], "function")
|
|
32
|
-
while ((m = constRe.exec(sourceContent)) !== null) add(m[1], "function")
|
|
33
|
-
while ((m = classRe.exec(sourceContent)) !== null) add(m[1], "class")
|
|
34
|
-
break
|
|
35
|
-
}
|
|
36
|
-
case "go": {
|
|
37
|
-
const funcRe = /^func\s+(?:\([^)]+\)\s+)?([a-zA-Z_]\w*)\s*\(/gm
|
|
38
|
-
let m
|
|
39
|
-
while ((m = funcRe.exec(sourceContent)) !== null) add(m[1], "function")
|
|
40
|
-
break
|
|
41
|
-
}
|
|
42
|
-
case "rs": {
|
|
43
|
-
const fnRe = /^pub\s+(?:async\s+)?fn\s+([a-zA-Z_]\w*)\s*[<(]/gm
|
|
44
|
-
const structRe = /^pub\s+struct\s+([a-zA-Z_]\w*)/gm
|
|
45
|
-
let m
|
|
46
|
-
while ((m = fnRe.exec(sourceContent)) !== null) add(m[1], "function")
|
|
47
|
-
while ((m = structRe.exec(sourceContent)) !== null) add(m[1], "struct")
|
|
48
|
-
break
|
|
49
|
-
}
|
|
50
|
-
case "rb": {
|
|
51
|
-
const defRe = /^\s*def\s+(?:self\.)?([a-zA-Z_]\w*[!?=]?)/gm
|
|
52
|
-
const classRe = /^\s*class\s+([a-zA-Z_]\w*)/gm
|
|
53
|
-
let m
|
|
54
|
-
while ((m = defRe.exec(sourceContent)) !== null) add(m[1], "function")
|
|
55
|
-
while ((m = classRe.exec(sourceContent)) !== null) add(m[1], "class")
|
|
56
|
-
break
|
|
57
|
-
}
|
|
58
|
-
case "java": case "kt": {
|
|
59
|
-
const methodRe = /^\s*(?:public|protected|private|\s)+\s+(?:static\s+)?(?:final\s+)?\w+\s+([a-zA-Z_]\w*)\s*\(/gm
|
|
60
|
-
const funRe = /^\s*(?:fun)\s+([a-zA-Z_]\w*)\s*[<(]/gm
|
|
61
|
-
let m
|
|
62
|
-
while ((m = ext === "kt" ? funRe.exec(sourceContent) : methodRe.exec(sourceContent)) !== null) add(m[1], "function")
|
|
63
|
-
break
|
|
64
|
-
}
|
|
65
|
-
case "sh": {
|
|
66
|
-
const funcRe = /^(?:function\s+)?([a-zA-Z_]\w*)\s*\(\)/gm
|
|
67
|
-
let m
|
|
68
|
-
while ((m = funcRe.exec(sourceContent)) !== null) add(m[1], "function")
|
|
69
|
-
break
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return exports
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function inferFunctionParams(sourceContent, funcName) {
|
|
76
|
-
if (!sourceContent || !funcName) return []
|
|
77
|
-
const patterns = [
|
|
78
|
-
new RegExp(`(?:export\\s+)?(?:async\\s+)?function\\s+${escapeRegex(funcName)}\\s*\\(([^)]*)\\)`, "m"),
|
|
79
|
-
new RegExp(`(?:export\\s+)?const\\s+${escapeRegex(funcName)}\\s*[:=]\\s*(?:async\\s+)?\\(([^)]*)\\)`, "m"),
|
|
80
|
-
new RegExp(`def\\s+${escapeRegex(funcName)}\\s*\\(([^)]*)\\)`, "m"),
|
|
81
|
-
new RegExp(`func\\s+(?:\\([^)]+\\)\\s+)?${escapeRegex(funcName)}\\s*\\(([^)]*)\\)`, "m"),
|
|
82
|
-
new RegExp(`(?:pub\\s+)?fn\\s+${escapeRegex(funcName)}\\s*[<\\(]([^)]*)\\)`, "m"),
|
|
83
|
-
]
|
|
84
|
-
|
|
85
|
-
for (const pat of patterns) {
|
|
86
|
-
const m = sourceContent.match(pat)
|
|
87
|
-
if (m) {
|
|
88
|
-
return m[1].split(",").map(s => {
|
|
89
|
-
const trimmed = s.trim()
|
|
90
|
-
if (!trimmed) return null
|
|
91
|
-
const parts = trimmed.split(":").map(p => p.split("="))
|
|
92
|
-
const name = parts[0][0]?.trim().replace(/^[*&]/, "")
|
|
93
|
-
const type = parts[0][1]?.trim() || inferTypeFromName(name, parts[0][0]?.split("=")[1]?.trim())
|
|
94
|
-
const defaultValue = parts[0][1]?.includes("=") ? parts[0][0].split("=")[1]?.trim() : (parts[1]?.join("=").trim())
|
|
95
|
-
return name ? { name, type: type || "any", defaultValue } : null
|
|
96
|
-
}).filter(Boolean)
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return []
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function inferTypeFromName(paramName, defaultValue) {
|
|
103
|
-
if (!paramName) return "any"
|
|
104
|
-
const name = paramName.toLowerCase()
|
|
105
|
-
|
|
106
|
-
if (defaultValue !== undefined) {
|
|
107
|
-
if (typeof defaultValue === "string") {
|
|
108
|
-
if (defaultValue === "true" || defaultValue === "false") return "boolean"
|
|
109
|
-
if (/^-?\d+(\.\d+)?$/.test(defaultValue)) return "number"
|
|
110
|
-
if (defaultValue.startsWith("[") || defaultValue.startsWith("{")) return defaultValue.startsWith("[") ? "array" : "object"
|
|
111
|
-
return "string"
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (/^(is|has|can|should|will|did|enable|disable|visible|active|open|closed)/.test(name)) return "boolean"
|
|
116
|
-
if (/^(count|index|limit|size|length|max|min|width|height|depth|offset|duration|delay|timeout|interval|rate|threshold|level|score|priority|version|port|year|month|day|hour|minute|second)/.test(name)) return "number"
|
|
117
|
-
if (/^(name|title|label|text|content|body|message|description|summary|path|url|uri|host|port|file|filename|ext|extension|format|type|kind|mode|state|status|color|theme|lang|language|locale|timezone|currency|unit|prefix|suffix|key|token|secret|password|email|phone|address|id|uuid|slug)/.test(name)) return "string"
|
|
118
|
-
if (/^(items|list|array|elements|nodes|children|options|params|args|arguments|values|entries|records|rows|columns|fields|properties|attrs|attributes|headers|tags|categories|labels|classes|styles)/.test(name)) return "array"
|
|
119
|
-
if (/^(obj|config|opts|options|settings|prefs|preferences|context|state|data|info|metadata|meta|props|query|filter|sort|pagination|page|request|response|event|error|result|output|input)/.test(name)) return "object"
|
|
120
|
-
if (/^(fn|cb|callback|handler|listener|middleware|transform|map|reduce|filter|sort|forEach)/.test(name)) return "function"
|
|
121
|
-
|
|
122
|
-
return "any"
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function buildTestSkeleton(language, fileName, exports, options = {}) {
|
|
126
|
-
const { strict = true, quality = true } = options
|
|
127
|
-
const testName = fileName.replace(/\.[^.]+$/, "")
|
|
128
|
-
|
|
129
|
-
const skeletons = {
|
|
130
|
-
py: () => {
|
|
131
|
-
const imports = `import unittest\nfrom ${testName} import ${exports.map(e => e.name).join(", ")}\n`
|
|
132
|
-
const tests = exports.map(exp => {
|
|
133
|
-
if (exp.type === "class") {
|
|
134
|
-
return `\nclass Test${exp.name}(unittest.TestCase):\n def test_init(self):\n """Test ${exp.name} initialization."""\n instance = ${exp.name}()\n self.assertIsNotNone(instance)\n`
|
|
135
|
-
}
|
|
136
|
-
const params = inferFunctionParams("", exp.name)
|
|
137
|
-
const paramStr = params.map(p => p.defaultValue !== undefined ? `${p.name}=${p.defaultValue}` : "None").join(", ") || ""
|
|
138
|
-
return `\n def test_${exp.name}_smoke(self):\n """Smoke test for ${exp.name}."""\n result = ${exp.name}(${paramStr})\n ${strict ? "self.assertIsNotNone(result)" : "pass"}\n`
|
|
139
|
-
}).join("")
|
|
140
|
-
return `${imports}\n${tests}`
|
|
141
|
-
},
|
|
142
|
-
|
|
143
|
-
js: () => {
|
|
144
|
-
const imports = `const { ${exports.map(e => e.name).join(", ")} } = require("./${testName}")\n`
|
|
145
|
-
const tests = exports.map(exp => {
|
|
146
|
-
const params = inferFunctionParams("", exp.name)
|
|
147
|
-
const paramStr = params.map(p => p.defaultValue !== undefined ? `${p.name}=${p.defaultValue}` : "undefined").join(", ") || ""
|
|
148
|
-
return `\ntest("${exp.name} smoke test", () => {\n const result = ${exp.name}(${paramStr})\n ${strict ? "expect(result).toBeDefined()" : "// TODO: add assertions"}\n})`
|
|
149
|
-
}).join("\n")
|
|
150
|
-
return `${imports}\n${tests}`
|
|
151
|
-
},
|
|
152
|
-
|
|
153
|
-
ts: () => {
|
|
154
|
-
const imports = `import { ${exports.map(e => e.name).join(", ")} } from "./${testName}"\n`
|
|
155
|
-
const tests = exports.map(exp => {
|
|
156
|
-
const params = inferFunctionParams("", exp.name)
|
|
157
|
-
const paramStr = params.map(p => p.defaultValue !== undefined ? `${p.name}=${p.defaultValue}` : "undefined").join(", ") || ""
|
|
158
|
-
return `\ntest("${exp.name} smoke test", () => {\n const result = ${exp.name}(${paramStr})\n ${strict ? "expect(result).toBeDefined()" : "// TODO: add assertions"}\n})`
|
|
159
|
-
}).join("\n")
|
|
160
|
-
return `${imports}\n${tests}`
|
|
161
|
-
},
|
|
162
|
-
|
|
163
|
-
go: () => {
|
|
164
|
-
const tests = exports.map(exp => {
|
|
165
|
-
return `\nfunc Test${capitalize(exp.name)}(t *testing.T) {\n\t// TODO: implement test for ${exp.name}\n\tt.Skip("not implemented")\n}`
|
|
166
|
-
}).join("\n")
|
|
167
|
-
return `package main\n\nimport "testing"\n${tests}`
|
|
168
|
-
},
|
|
169
|
-
|
|
170
|
-
sh: () => {
|
|
171
|
-
const tests = exports.map(exp => {
|
|
172
|
-
return `\ntest_${exp.name}() {\n # TODO: implement test for ${exp.name}\n echo "SKIP: test_${exp.name} not implemented"\n}`
|
|
173
|
-
}).join("\n")
|
|
174
|
-
return `#!/usr/bin/env bash\nset -euo pipefail\n\nsource "./${testName}.sh"\n${tests}`
|
|
175
|
-
},
|
|
176
|
-
|
|
177
|
-
rs: () => {
|
|
178
|
-
const tests = exports.map(exp => {
|
|
179
|
-
return `\n#[test]\nfn test_${exp.name}() {\n // TODO: implement test for ${exp.name}\n}`
|
|
180
|
-
}).join("\n")
|
|
181
|
-
return `#[cfg(test)]\nmod tests {\n use super::*;\n${tests}\n}`
|
|
182
|
-
},
|
|
183
|
-
|
|
184
|
-
rb: () => {
|
|
185
|
-
const tests = exports.map(exp => {
|
|
186
|
-
return `\n def test_${exp.name}\n # TODO: implement test for ${exp.name}\n skip "not implemented"\n end`
|
|
187
|
-
}).join("\n")
|
|
188
|
-
return `require "minitest/autorun"\nrequire_relative "${testName}"\n\nclass Test${capitalize(testName)} < Minitest::Test\n${tests}\nend`
|
|
189
|
-
},
|
|
190
|
-
|
|
191
|
-
java: () => {
|
|
192
|
-
const tests = exports.map(exp => {
|
|
193
|
-
return `\n @Test\n void test${capitalize(exp.name)}() {\n // TODO: implement test for ${exp.name}\n }`
|
|
194
|
-
}).join("\n")
|
|
195
|
-
return `import org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\nclass ${capitalize(testName)}Test {\n${tests}\n}`
|
|
196
|
-
},
|
|
197
|
-
|
|
198
|
-
kt: () => {
|
|
199
|
-
const tests = exports.map(exp => {
|
|
200
|
-
return `\n @Test\n fun test${capitalize(exp.name)}() {\n // TODO: implement test for ${exp.name}\n }`
|
|
201
|
-
}).join("\n")
|
|
202
|
-
return `import org.junit.jupiter.api.Test\nimport org.junit.jupiter.api.Assertions.*\n\nclass ${capitalize(testName)}Test {\n${tests}\n}`
|
|
203
|
-
},
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const generator = skeletons[language] || skeletons.js
|
|
207
|
-
return generator()
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function escapeRegex(str) {
|
|
211
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function capitalize(str) {
|
|
215
|
-
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
export { extractExports, inferFunctionParams, inferTypeFromName, buildTestSkeleton }
|
package/lib/tier-routing.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
const FALLBACK_HIGH = /opus|gemini-.*-pro|deepseek\/deepseek-v4-pro|gpt-5|(^|\/)o[134]($|-|\/)/i
|
|
2
|
-
const FALLBACK_MID = /deepseek\/deepseek-v4-flash|claude.*sonnet|gemini-.*-flash|gpt-4o(?!-mini)/i
|
|
3
|
-
|
|
4
|
-
const BASE_EXPLORATORY = new Set([
|
|
5
|
-
"check", "find", "list", "search", "does", "verify", "look", "count",
|
|
6
|
-
"show", "get", "read", "grep", "scan", "detect", "inspect"
|
|
7
|
-
])
|
|
8
|
-
|
|
9
|
-
function classify(model, customRegex = null) {
|
|
10
|
-
const s = String(model || "").toLowerCase()
|
|
11
|
-
const highRe = customRegex?.high ? new RegExp(customRegex.high, "i") : FALLBACK_HIGH
|
|
12
|
-
const midRe = customRegex?.mid ? new RegExp(customRegex.mid, "i") : FALLBACK_MID
|
|
13
|
-
if (highRe.test(s)) return "high"
|
|
14
|
-
if (midRe.test(s)) return "mid"
|
|
15
|
-
return "budget"
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function routeModel(prompt, currentTier, trinityCheap, trinityMedium, learnedExploratory = [], stressScore = 0) {
|
|
19
|
-
const firstWord = prompt.split(/\s+/)[0]?.toLowerCase() || ""
|
|
20
|
-
|
|
21
|
-
const exploratory = new Set([...BASE_EXPLORATORY, ...(learnedExploratory || [])])
|
|
22
|
-
const isExploratory = exploratory.has(firstWord)
|
|
23
|
-
|
|
24
|
-
if (isExploratory && trinityCheap) {
|
|
25
|
-
return { target: trinityCheap, reason: "exploratory_first_word", word: firstWord }
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (currentTier === "high" && trinityMedium) {
|
|
29
|
-
let target = trinityMedium
|
|
30
|
-
|
|
31
|
-
if (trinityCheap && stressScore > 0.5) {
|
|
32
|
-
target = trinityCheap
|
|
33
|
-
return { target, reason: "stress_aware_downgrade", stress_score: stressScore }
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return { target, reason: "medium_fallback" }
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return { target: trinityCheap || null, reason: "default_cheap" }
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function isExploratoryPrompt(prompt, learnedExploratory = []) {
|
|
43
|
-
const firstWord = prompt.split(/\s+/)[0]?.toLowerCase() || ""
|
|
44
|
-
const exploratory = new Set([...BASE_EXPLORATORY, ...(learnedExploratory || [])])
|
|
45
|
-
return { is_exploratory: exploratory.has(firstWord), first_word: firstWord }
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export { classify, routeModel, isExploratoryPrompt, BASE_EXPLORATORY }
|
package/middleware/auth.js
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
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
|
-
}
|
package/middleware/auth.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
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
|
-
}
|
package/nginx-vibetheog-api.conf
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
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
|
-
}
|
package/routes/admin.js
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { getDb } from "../lib/db.js";
|
|
2
|
-
import { randomBytes } from "node:crypto";
|
|
3
|
-
function generateToken() {
|
|
4
|
-
return "vos_" + randomBytes(32).toString("hex");
|
|
5
|
-
}
|
|
6
|
-
export async function adminRoutes(fastify) {
|
|
7
|
-
fastify.post("/admin/seats", async (request, reply) => {
|
|
8
|
-
const { name, email } = request.body || {};
|
|
9
|
-
if (!name) {
|
|
10
|
-
return reply.code(400).send({ error: "name is required" });
|
|
11
|
-
}
|
|
12
|
-
const db = getDb();
|
|
13
|
-
const result = db.prepare("INSERT INTO seats (name, email) VALUES (?, ?)").run(name, email || null);
|
|
14
|
-
const seat = db.prepare("SELECT * FROM seats WHERE id = ?").get(result.lastInsertRowid);
|
|
15
|
-
return { ok: true, seat };
|
|
16
|
-
});
|
|
17
|
-
fastify.get("/admin/seats", async (request, reply) => {
|
|
18
|
-
const db = getDb();
|
|
19
|
-
const seats = db.prepare("SELECT * FROM seats ORDER BY created_at DESC").all();
|
|
20
|
-
return { seats, count: seats.length };
|
|
21
|
-
});
|
|
22
|
-
fastify.patch("/admin/seats/:id", async (request, reply) => {
|
|
23
|
-
const { id } = request.params;
|
|
24
|
-
const { status } = request.body || {};
|
|
25
|
-
if (!status || !["active", "suspended", "cancelled"].includes(status)) {
|
|
26
|
-
return reply.code(400).send({ error: "valid status is required (active, suspended, cancelled)" });
|
|
27
|
-
}
|
|
28
|
-
const db = getDb();
|
|
29
|
-
const result = db.prepare("UPDATE seats SET status = ?, updated_at = datetime('now') WHERE id = ?").run(status, id);
|
|
30
|
-
if (result.changes === 0) {
|
|
31
|
-
return reply.code(404).send({ error: "seat not found" });
|
|
32
|
-
}
|
|
33
|
-
if (status !== "active") {
|
|
34
|
-
db.prepare("UPDATE api_tokens SET status = 'revoked', revoked_at = datetime('now') WHERE seat_id = ? AND status = 'active'").run(id);
|
|
35
|
-
}
|
|
36
|
-
const seat = db.prepare("SELECT * FROM seats WHERE id = ?").get(id);
|
|
37
|
-
return { ok: true, seat };
|
|
38
|
-
});
|
|
39
|
-
fastify.post("/admin/tokens", async (request, reply) => {
|
|
40
|
-
const { seat_id, label, expires_at } = request.body || {};
|
|
41
|
-
if (!seat_id) {
|
|
42
|
-
return reply.code(400).send({ error: "seat_id is required" });
|
|
43
|
-
}
|
|
44
|
-
const db = getDb();
|
|
45
|
-
const seat = db.prepare("SELECT * FROM seats WHERE id = ?").get(seat_id);
|
|
46
|
-
if (!seat) {
|
|
47
|
-
return reply.code(404).send({ error: "seat not found" });
|
|
48
|
-
}
|
|
49
|
-
const token = generateToken();
|
|
50
|
-
const result = db.prepare("INSERT INTO api_tokens (token, seat_id, label, expires_at) VALUES (?, ?, ?, ?)").run(token, seat_id, label || null, expires_at || null);
|
|
51
|
-
const tokenRow = db.prepare("SELECT * FROM api_tokens WHERE id = ?").get(result.lastInsertRowid);
|
|
52
|
-
return { ok: true, token: tokenRow };
|
|
53
|
-
});
|
|
54
|
-
fastify.get("/admin/tokens", async (request, reply) => {
|
|
55
|
-
const db = getDb();
|
|
56
|
-
const tokens = db.prepare(`
|
|
57
|
-
SELECT t.*, s.name as seat_name, s.email as seat_email, s.status as seat_status
|
|
58
|
-
FROM api_tokens t
|
|
59
|
-
JOIN seats s ON t.seat_id = s.id
|
|
60
|
-
ORDER BY t.created_at DESC
|
|
61
|
-
`).all();
|
|
62
|
-
return { tokens, count: tokens.length };
|
|
63
|
-
});
|
|
64
|
-
fastify.patch("/admin/tokens/:id", async (request, reply) => {
|
|
65
|
-
const { id } = request.params;
|
|
66
|
-
const { status } = request.body || {};
|
|
67
|
-
if (!status || !["active", "revoked", "expired"].includes(status)) {
|
|
68
|
-
return reply.code(400).send({ error: "valid status is required (active, revoked, expired)" });
|
|
69
|
-
}
|
|
70
|
-
const db = getDb();
|
|
71
|
-
const update = status === "revoked"
|
|
72
|
-
? "UPDATE api_tokens SET status = ?, revoked_at = datetime('now') WHERE id = ?"
|
|
73
|
-
: "UPDATE api_tokens SET status = ? WHERE id = ?";
|
|
74
|
-
const result = db.prepare(update).run(status, id);
|
|
75
|
-
if (result.changes === 0) {
|
|
76
|
-
return reply.code(404).send({ error: "token not found" });
|
|
77
|
-
}
|
|
78
|
-
const token = db.prepare("SELECT * FROM api_tokens WHERE id = ?").get(id);
|
|
79
|
-
return { ok: true, token };
|
|
80
|
-
});
|
|
81
|
-
fastify.get("/admin/usage", async (request, reply) => {
|
|
82
|
-
const { days: daysRaw } = request.query || {};
|
|
83
|
-
const days = Number(daysRaw);
|
|
84
|
-
if (daysRaw !== undefined && (isNaN(days) || !Number.isFinite(days) || !Number.isInteger(days) || days < 1 || days > 365)) {
|
|
85
|
-
return reply.code(400).send({ error: "days must be a positive integer between 1 and 365" });
|
|
86
|
-
}
|
|
87
|
-
const effectiveDays = days || 30;
|
|
88
|
-
const db = getDb();
|
|
89
|
-
const sql = "SELECT t.token, t.label, s.name as seat_name, COUNT(*) as request_count, AVG(l.latency_ms) as avg_latency_ms, MIN(l.created_at) as first_used, MAX(l.created_at) as last_used FROM usage_log l JOIN api_tokens t ON l.token_id = t.id JOIN seats s ON t.seat_id = s.id WHERE l.created_at >= datetime('now', ?) GROUP BY t.id ORDER BY request_count DESC";
|
|
90
|
-
const usage = db.prepare(sql).all("-" + effectiveDays + " days");
|
|
91
|
-
return { usage, count: usage.length, days: effectiveDays };
|
|
92
|
-
});
|
|
93
|
-
}
|