mirror of
https://github.com/dtzp555-max/ocp.git
synced 2026-07-21 21:15:09 +00:00
160 lines
4.6 KiB
JavaScript
160 lines
4.6 KiB
JavaScript
// keys.mjs — API key management and usage tracking for OCP LAN mode
|
|
import Database from "better-sqlite3";
|
|
import { randomBytes } from "node:crypto";
|
|
import { join } from "node:path";
|
|
import { mkdirSync } from "node:fs";
|
|
import { homedir } from "node:os";
|
|
|
|
const OCP_DIR = join(homedir(), ".ocp");
|
|
mkdirSync(OCP_DIR, { recursive: true });
|
|
const DB_PATH = join(OCP_DIR, "ocp.db");
|
|
|
|
let db;
|
|
|
|
export function getDb() {
|
|
if (!db) {
|
|
db = new Database(DB_PATH);
|
|
db.pragma("journal_mode = WAL");
|
|
db.pragma("foreign_keys = ON");
|
|
initSchema();
|
|
}
|
|
return db;
|
|
}
|
|
|
|
function initSchema() {
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS api_keys (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
key TEXT UNIQUE NOT NULL,
|
|
name TEXT NOT NULL,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
revoked INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS usage_log (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
key_id INTEGER,
|
|
key_name TEXT NOT NULL DEFAULT 'anonymous',
|
|
model TEXT NOT NULL,
|
|
prompt_chars INTEGER NOT NULL DEFAULT 0,
|
|
response_chars INTEGER NOT NULL DEFAULT 0,
|
|
elapsed_ms INTEGER NOT NULL DEFAULT 0,
|
|
success INTEGER NOT NULL DEFAULT 1,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
FOREIGN KEY (key_id) REFERENCES api_keys(id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_usage_created ON usage_log(created_at);
|
|
CREATE INDEX IF NOT EXISTS idx_usage_key ON usage_log(key_id);
|
|
`);
|
|
}
|
|
|
|
// ── Key CRUD ──
|
|
|
|
export function createKey(name) {
|
|
const key = "ocp_" + randomBytes(24).toString("base64url");
|
|
const d = getDb();
|
|
const stmt = d.prepare("INSERT INTO api_keys (key, name) VALUES (?, ?)");
|
|
const result = stmt.run(key, name);
|
|
return { id: result.lastInsertRowid, key, name };
|
|
}
|
|
|
|
export function listKeys() {
|
|
const d = getDb();
|
|
return d.prepare(
|
|
"SELECT id, key, name, created_at, revoked FROM api_keys ORDER BY created_at DESC"
|
|
).all().map(k => ({
|
|
...k,
|
|
keyPreview: k.key.slice(0, 8) + "..." + k.key.slice(-4),
|
|
}));
|
|
}
|
|
|
|
export function revokeKey(idOrName) {
|
|
const d = getDb();
|
|
const stmt = d.prepare(
|
|
"UPDATE api_keys SET revoked = 1 WHERE (id = ? OR name = ?) AND revoked = 0"
|
|
);
|
|
return stmt.run(idOrName, idOrName).changes > 0;
|
|
}
|
|
|
|
export function validateKey(key) {
|
|
const d = getDb();
|
|
const row = d.prepare(
|
|
"SELECT id, name FROM api_keys WHERE key = ? AND revoked = 0"
|
|
).get(key);
|
|
return row || null;
|
|
}
|
|
|
|
// ── Usage recording ──
|
|
|
|
export function recordUsage({ keyId, keyName, model, promptChars, responseChars, elapsedMs, success }) {
|
|
const d = getDb();
|
|
d.prepare(`
|
|
INSERT INTO usage_log (key_id, key_name, model, prompt_chars, response_chars, elapsed_ms, success)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
`).run(keyId ?? null, keyName || "anonymous", model, promptChars, responseChars, elapsedMs, success ? 1 : 0);
|
|
}
|
|
|
|
// ── Usage queries ──
|
|
|
|
export function getUsageByKey({ since, until } = {}) {
|
|
const d = getDb();
|
|
let where = "WHERE 1=1";
|
|
const params = [];
|
|
if (since) { where += " AND created_at >= ?"; params.push(since); }
|
|
if (until) { where += " AND created_at <= ?"; params.push(until); }
|
|
|
|
return d.prepare(`
|
|
SELECT
|
|
key_name,
|
|
COUNT(*) as requests,
|
|
SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as successes,
|
|
SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) as errors,
|
|
SUM(prompt_chars) as total_prompt_chars,
|
|
SUM(response_chars) as total_response_chars,
|
|
SUM(elapsed_ms) as total_elapsed_ms,
|
|
AVG(elapsed_ms) as avg_elapsed_ms,
|
|
MIN(created_at) as first_request,
|
|
MAX(created_at) as last_request
|
|
FROM usage_log
|
|
${where}
|
|
GROUP BY key_name
|
|
ORDER BY requests DESC
|
|
`).all(...params);
|
|
}
|
|
|
|
export function getUsageTimeline({ keyName, hours = 24 } = {}) {
|
|
const d = getDb();
|
|
const since = new Date(Date.now() - hours * 3600000).toISOString();
|
|
let where = "WHERE created_at >= ?";
|
|
const params = [since];
|
|
if (keyName) { where += " AND key_name = ?"; params.push(keyName); }
|
|
|
|
return d.prepare(`
|
|
SELECT
|
|
strftime('%Y-%m-%dT%H:00:00', created_at) as hour,
|
|
COUNT(*) as requests,
|
|
SUM(prompt_chars) as prompt_chars,
|
|
SUM(response_chars) as response_chars,
|
|
AVG(elapsed_ms) as avg_elapsed_ms
|
|
FROM usage_log
|
|
${where}
|
|
GROUP BY hour
|
|
ORDER BY hour
|
|
`).all(...params);
|
|
}
|
|
|
|
export function getRecentUsage(limit = 50) {
|
|
const d = getDb();
|
|
return d.prepare(`
|
|
SELECT key_name, model, prompt_chars, response_chars, elapsed_ms, success, created_at
|
|
FROM usage_log
|
|
ORDER BY created_at DESC
|
|
LIMIT ?
|
|
`).all(limit);
|
|
}
|
|
|
|
export function closeDb() {
|
|
if (db) { db.close(); db = null; }
|
|
}
|