tinylogs 0.1.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/LICENSE +21 -0
- package/README.md +103 -0
- package/dist/cli.js +635 -0
- package/dist/cli.js.map +7 -0
- package/dist/client/index.d.ts +30 -0
- package/dist/client/index.js +65 -0
- package/dist/client/index.js.map +7 -0
- package/dist/server/index.js +480 -0
- package/dist/server/index.js.map +7 -0
- package/dist/src/types.d.ts +17 -0
- package/dist/ui/bundle.css +2 -0
- package/dist/ui/bundle.css.map +7 -0
- package/dist/ui/bundle.js +2 -0
- package/dist/ui/bundle.js.map +7 -0
- package/dist/ui/index.html +13 -0
- package/package.json +82 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/server/index.ts
|
|
4
|
+
import { createServer } from "node:http";
|
|
5
|
+
|
|
6
|
+
// src/storage/db.ts
|
|
7
|
+
import Database from "better-sqlite3";
|
|
8
|
+
var MIGRATIONS = [
|
|
9
|
+
`CREATE TABLE logs (
|
|
10
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
11
|
+
ts INTEGER NOT NULL,
|
|
12
|
+
service TEXT NOT NULL,
|
|
13
|
+
message TEXT NOT NULL,
|
|
14
|
+
labels TEXT NOT NULL
|
|
15
|
+
);
|
|
16
|
+
CREATE INDEX idx_logs_ts ON logs(ts);
|
|
17
|
+
CREATE INDEX idx_logs_service ON logs(service);
|
|
18
|
+
CREATE TABLE log_labels (
|
|
19
|
+
log_id INTEGER NOT NULL REFERENCES logs(id) ON DELETE CASCADE,
|
|
20
|
+
key TEXT NOT NULL,
|
|
21
|
+
value TEXT NOT NULL
|
|
22
|
+
);
|
|
23
|
+
CREATE INDEX idx_labels_kv ON log_labels(key, value);
|
|
24
|
+
CREATE INDEX idx_labels_logid ON log_labels(log_id);
|
|
25
|
+
CREATE TABLE users (
|
|
26
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
27
|
+
username TEXT UNIQUE NOT NULL,
|
|
28
|
+
password_hash TEXT NOT NULL,
|
|
29
|
+
role TEXT NOT NULL DEFAULT 'admin',
|
|
30
|
+
created_at INTEGER NOT NULL
|
|
31
|
+
);`
|
|
32
|
+
];
|
|
33
|
+
function openDb(path) {
|
|
34
|
+
const db = new Database(path);
|
|
35
|
+
db.pragma("journal_mode = WAL");
|
|
36
|
+
db.pragma("foreign_keys = ON");
|
|
37
|
+
db.exec("CREATE TABLE IF NOT EXISTS schema_migrations (version INTEGER PRIMARY KEY)");
|
|
38
|
+
const current = db.prepare("SELECT COALESCE(MAX(version),0) v FROM schema_migrations").get().v;
|
|
39
|
+
const apply = db.transaction((from) => {
|
|
40
|
+
for (let i = from; i < MIGRATIONS.length; i++) {
|
|
41
|
+
db.exec(MIGRATIONS[i]);
|
|
42
|
+
db.prepare("INSERT INTO schema_migrations (version) VALUES (?)").run(i + 1);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
apply(current);
|
|
46
|
+
return db;
|
|
47
|
+
}
|
|
48
|
+
function insertLog(db, rec) {
|
|
49
|
+
const txn = db.transaction((r) => {
|
|
50
|
+
const info = db.prepare("INSERT INTO logs (ts, service, message, labels) VALUES (?,?,?,?)").run(r.ts, r.service, r.message, JSON.stringify(r.labels ?? {}));
|
|
51
|
+
const logId = Number(info.lastInsertRowid);
|
|
52
|
+
const ins = db.prepare("INSERT INTO log_labels (log_id, key, value) VALUES (?,?,?)");
|
|
53
|
+
for (const [k, v] of Object.entries(r.labels ?? {})) ins.run(logId, k, String(v));
|
|
54
|
+
return logId;
|
|
55
|
+
});
|
|
56
|
+
return txn(rec);
|
|
57
|
+
}
|
|
58
|
+
function dbSizeBytes(db) {
|
|
59
|
+
const pageCount = db.pragma("page_count", { simple: true });
|
|
60
|
+
const pageSize = db.pragma("page_size", { simple: true });
|
|
61
|
+
return pageCount * pageSize;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/storage/retention.ts
|
|
65
|
+
function pruneByAge(db, retentionDays, now) {
|
|
66
|
+
const cutoff = now - retentionDays * 864e5;
|
|
67
|
+
return db.prepare("DELETE FROM logs WHERE ts < ?").run(cutoff).changes;
|
|
68
|
+
}
|
|
69
|
+
function pruneBySize(db, maxSizeMB, batch = 1e3) {
|
|
70
|
+
const maxBytes = maxSizeMB * 1024 * 1024;
|
|
71
|
+
let deleted = 0;
|
|
72
|
+
while (dbSizeBytes(db) > maxBytes) {
|
|
73
|
+
const changes = db.prepare(
|
|
74
|
+
"DELETE FROM logs WHERE id IN (SELECT id FROM logs ORDER BY id ASC LIMIT ?)"
|
|
75
|
+
).run(batch).changes;
|
|
76
|
+
if (changes === 0) break;
|
|
77
|
+
deleted += changes;
|
|
78
|
+
}
|
|
79
|
+
return deleted;
|
|
80
|
+
}
|
|
81
|
+
function runRetention(db, opts) {
|
|
82
|
+
try {
|
|
83
|
+
const now = opts.now ?? Date.now();
|
|
84
|
+
pruneByAge(db, opts.retentionDays, now);
|
|
85
|
+
pruneBySize(db, opts.maxSizeMB);
|
|
86
|
+
db.exec("VACUUM");
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.error("[tinylogs] retention failed:", err.message);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/buffer.ts
|
|
93
|
+
var RingBuffer = class {
|
|
94
|
+
constructor(capacity) {
|
|
95
|
+
this.capacity = capacity;
|
|
96
|
+
}
|
|
97
|
+
items = [];
|
|
98
|
+
push(rec) {
|
|
99
|
+
this.items.push(rec);
|
|
100
|
+
if (this.items.length > this.capacity) this.items.shift();
|
|
101
|
+
}
|
|
102
|
+
snapshot() {
|
|
103
|
+
return this.items.slice();
|
|
104
|
+
}
|
|
105
|
+
get size() {
|
|
106
|
+
return this.items.length;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// src/config.ts
|
|
111
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
112
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
113
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
114
|
+
function resolveConfigPath(flag) {
|
|
115
|
+
if (flag) return flag;
|
|
116
|
+
if (process.env.TINYLOGS_CONFIG) return process.env.TINYLOGS_CONFIG;
|
|
117
|
+
return join(process.cwd(), "tinylogs.config.json");
|
|
118
|
+
}
|
|
119
|
+
function hashToken(token) {
|
|
120
|
+
return createHash("sha256").update(token, "utf8").digest("hex");
|
|
121
|
+
}
|
|
122
|
+
function loadConfig(path) {
|
|
123
|
+
const raw = readFileSync(path, "utf8");
|
|
124
|
+
const cfg = JSON.parse(raw);
|
|
125
|
+
for (const k of ["port", "host", "dbPath", "sessionSecret", "ingestTokenHash"]) {
|
|
126
|
+
if (cfg[k] === void 0) throw new Error(`config missing field: ${k}`);
|
|
127
|
+
}
|
|
128
|
+
return cfg;
|
|
129
|
+
}
|
|
130
|
+
function resolveDbPath(configPath, cfg) {
|
|
131
|
+
if (isAbsolute(cfg.dbPath)) return cfg.dbPath;
|
|
132
|
+
return resolve(dirname(configPath), cfg.dbPath);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/server/app.ts
|
|
136
|
+
import express from "express";
|
|
137
|
+
import { existsSync } from "node:fs";
|
|
138
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
139
|
+
import { fileURLToPath } from "node:url";
|
|
140
|
+
|
|
141
|
+
// src/auth.ts
|
|
142
|
+
import bcrypt from "bcryptjs";
|
|
143
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
144
|
+
function verifyUser(db, username, pw) {
|
|
145
|
+
const row = db.prepare("SELECT password_hash FROM users WHERE username=?").get(username);
|
|
146
|
+
if (!row) {
|
|
147
|
+
bcrypt.compareSync(pw, "$2a$10$0000000000000000000000000000000000000000000000000000");
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
return bcrypt.compareSync(pw, row.password_hash);
|
|
151
|
+
}
|
|
152
|
+
function safeEqualHex(a, b) {
|
|
153
|
+
const ab = Buffer.from(a, "hex");
|
|
154
|
+
const bb = Buffer.from(b, "hex");
|
|
155
|
+
if (ab.length !== bb.length || ab.length === 0) return false;
|
|
156
|
+
return timingSafeEqual(ab, bb);
|
|
157
|
+
}
|
|
158
|
+
function verifyIngestToken(bearer, ingestTokenHash) {
|
|
159
|
+
if (!bearer || !bearer.startsWith("Bearer ")) return false;
|
|
160
|
+
const token = bearer.slice("Bearer ".length);
|
|
161
|
+
return safeEqualHex(hashToken(token), ingestTokenHash);
|
|
162
|
+
}
|
|
163
|
+
function signSession(username, secret) {
|
|
164
|
+
const mac = createHmac("sha256", secret).update(username).digest("hex");
|
|
165
|
+
return `${username}.${mac}`;
|
|
166
|
+
}
|
|
167
|
+
function verifySession(cookieVal, secret) {
|
|
168
|
+
if (!cookieVal) return null;
|
|
169
|
+
const dot = cookieVal.lastIndexOf(".");
|
|
170
|
+
if (dot <= 0) return null;
|
|
171
|
+
const username = cookieVal.slice(0, dot);
|
|
172
|
+
const mac = cookieVal.slice(dot + 1);
|
|
173
|
+
const expected = createHmac("sha256", secret).update(username).digest("hex");
|
|
174
|
+
if (mac.length !== expected.length) return null;
|
|
175
|
+
if (!timingSafeEqual(Buffer.from(mac), Buffer.from(expected))) return null;
|
|
176
|
+
return username;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/server/ingest.ts
|
|
180
|
+
var MAX_MESSAGE = 16384;
|
|
181
|
+
var MAX_LABELS = 50;
|
|
182
|
+
var MAX_KV = 512;
|
|
183
|
+
var MAX_BATCH = 1e3;
|
|
184
|
+
function validateRecord(raw) {
|
|
185
|
+
if (typeof raw !== "object" || raw === null) return { ok: false, error: "record must be an object" };
|
|
186
|
+
if (typeof raw.service !== "string" || raw.service.length === 0) return { ok: false, error: "service required" };
|
|
187
|
+
if (typeof raw.message !== "string" || raw.message.length === 0) return { ok: false, error: "message required" };
|
|
188
|
+
if (raw.message.length > MAX_MESSAGE) return { ok: false, error: "message too long" };
|
|
189
|
+
if (raw.service.length > MAX_KV) return { ok: false, error: "service too long" };
|
|
190
|
+
const labels = {};
|
|
191
|
+
if (raw.labels !== void 0) {
|
|
192
|
+
if (typeof raw.labels !== "object" || raw.labels === null || Array.isArray(raw.labels))
|
|
193
|
+
return { ok: false, error: "labels must be an object" };
|
|
194
|
+
const keys = Object.keys(raw.labels);
|
|
195
|
+
if (keys.length > MAX_LABELS) return { ok: false, error: "too many labels" };
|
|
196
|
+
for (const k of keys) {
|
|
197
|
+
const v = raw.labels[k];
|
|
198
|
+
if (typeof v !== "string") return { ok: false, error: `label ${k} must be a string` };
|
|
199
|
+
if (k.length > MAX_KV || v.length > MAX_KV) return { ok: false, error: `label ${k} too long` };
|
|
200
|
+
labels[k] = v;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const ts = typeof raw.ts === "number" && Number.isFinite(raw.ts) ? raw.ts : Date.now();
|
|
204
|
+
return { ok: true, rec: { ts, service: raw.service, message: raw.message, labels } };
|
|
205
|
+
}
|
|
206
|
+
function registerIngest(app) {
|
|
207
|
+
app.post("/ingest", (req, res) => {
|
|
208
|
+
const deps = app.get("deps");
|
|
209
|
+
if (!verifyIngestToken(req.header("authorization"), deps.cfg.ingestTokenHash)) {
|
|
210
|
+
return res.status(401).json({ error: "invalid token" });
|
|
211
|
+
}
|
|
212
|
+
const body = req.body;
|
|
213
|
+
const records = Array.isArray(body) ? body : [body];
|
|
214
|
+
if (records.length > MAX_BATCH) return res.status(400).json({ error: "batch too large" });
|
|
215
|
+
const validated = [];
|
|
216
|
+
for (const raw of records) {
|
|
217
|
+
const v = validateRecord(raw);
|
|
218
|
+
if (!v.ok) return res.status(400).json({ error: v.error });
|
|
219
|
+
validated.push(v.rec);
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
for (const rec of validated) {
|
|
223
|
+
const id = insertLog(deps.db, rec);
|
|
224
|
+
const stored = { ...rec, id };
|
|
225
|
+
deps.buffer.push(stored);
|
|
226
|
+
deps.broadcast(stored);
|
|
227
|
+
}
|
|
228
|
+
return res.json({ accepted: validated.length });
|
|
229
|
+
} catch (err) {
|
|
230
|
+
console.error("[tinylogs] ingest error:", err.message);
|
|
231
|
+
return res.status(500).json({ error: "internal error" });
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/server/queryRoutes.ts
|
|
237
|
+
import { parse as parseCookie } from "cookie";
|
|
238
|
+
|
|
239
|
+
// src/storage/query.ts
|
|
240
|
+
function rowToRecord(row) {
|
|
241
|
+
return { id: row.id, ts: row.ts, service: row.service, message: row.message, labels: JSON.parse(row.labels) };
|
|
242
|
+
}
|
|
243
|
+
function queryLogs(db, params) {
|
|
244
|
+
const where = [];
|
|
245
|
+
const args = [];
|
|
246
|
+
if (params.service) {
|
|
247
|
+
where.push("logs.service = ?");
|
|
248
|
+
args.push(params.service);
|
|
249
|
+
}
|
|
250
|
+
if (params.q) {
|
|
251
|
+
where.push("logs.message LIKE ? COLLATE NOCASE");
|
|
252
|
+
args.push(`%${params.q}%`);
|
|
253
|
+
}
|
|
254
|
+
if (params.from !== void 0) {
|
|
255
|
+
where.push("logs.ts >= ?");
|
|
256
|
+
args.push(params.from);
|
|
257
|
+
}
|
|
258
|
+
if (params.to !== void 0) {
|
|
259
|
+
where.push("logs.ts < ?");
|
|
260
|
+
args.push(params.to);
|
|
261
|
+
}
|
|
262
|
+
if (params.before !== void 0) {
|
|
263
|
+
where.push("logs.id < ?");
|
|
264
|
+
args.push(params.before);
|
|
265
|
+
}
|
|
266
|
+
for (const l of params.labels ?? []) {
|
|
267
|
+
where.push("logs.id IN (SELECT log_id FROM log_labels WHERE key = ? AND value = ?)");
|
|
268
|
+
args.push(l.key, l.value);
|
|
269
|
+
}
|
|
270
|
+
const limit = Math.min(Math.max(params.limit ?? 200, 1), 1e3);
|
|
271
|
+
const sql = `SELECT id, ts, service, message, labels FROM logs
|
|
272
|
+
${where.length ? "WHERE " + where.join(" AND ") : ""}
|
|
273
|
+
ORDER BY logs.id DESC LIMIT ?`;
|
|
274
|
+
return db.prepare(sql).all(...args, limit).map(rowToRecord);
|
|
275
|
+
}
|
|
276
|
+
function queryLabels(db, opts = {}) {
|
|
277
|
+
const services = db.prepare(
|
|
278
|
+
"SELECT service, COUNT(*) count FROM logs GROUP BY service ORDER BY count DESC"
|
|
279
|
+
).all();
|
|
280
|
+
let labels;
|
|
281
|
+
if (opts.service) {
|
|
282
|
+
labels = db.prepare(
|
|
283
|
+
`SELECT ll.key, ll.value, COUNT(*) count FROM log_labels ll
|
|
284
|
+
JOIN logs ON logs.id = ll.log_id WHERE logs.service = ?
|
|
285
|
+
GROUP BY ll.key, ll.value ORDER BY ll.key, count DESC`
|
|
286
|
+
).all(opts.service);
|
|
287
|
+
} else {
|
|
288
|
+
labels = db.prepare(
|
|
289
|
+
`SELECT key, value, COUNT(*) count FROM log_labels
|
|
290
|
+
GROUP BY key, value ORDER BY key, count DESC`
|
|
291
|
+
).all();
|
|
292
|
+
}
|
|
293
|
+
return { services, labels };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/server/queryRoutes.ts
|
|
297
|
+
function requireSession(app) {
|
|
298
|
+
return (req, res, next) => {
|
|
299
|
+
const deps = app.get("deps");
|
|
300
|
+
const cookies = parseCookie(req.header("cookie") ?? "");
|
|
301
|
+
const user = verifySession(cookies["tl_session"], deps.cfg.sessionSecret);
|
|
302
|
+
if (!user) return res.status(401).json({ error: "unauthorized" });
|
|
303
|
+
req.user = user;
|
|
304
|
+
next();
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
function parseQuery(query) {
|
|
308
|
+
const p = {};
|
|
309
|
+
if (typeof query.service === "string") p.service = query.service;
|
|
310
|
+
if (typeof query.q === "string") p.q = query.q;
|
|
311
|
+
if (query.from !== void 0) p.from = Number(query.from);
|
|
312
|
+
if (query.to !== void 0) p.to = Number(query.to);
|
|
313
|
+
if (query.limit !== void 0) p.limit = Number(query.limit);
|
|
314
|
+
if (query.before !== void 0) p.before = Number(query.before);
|
|
315
|
+
const rawLabels = query.label === void 0 ? [] : Array.isArray(query.label) ? query.label : [query.label];
|
|
316
|
+
p.labels = rawLabels.map((s) => {
|
|
317
|
+
const i = s.indexOf("=");
|
|
318
|
+
return i < 0 ? null : { key: s.slice(0, i), value: s.slice(i + 1) };
|
|
319
|
+
}).filter(Boolean);
|
|
320
|
+
return p;
|
|
321
|
+
}
|
|
322
|
+
function registerQueryRoutes(app) {
|
|
323
|
+
const guard = requireSession(app);
|
|
324
|
+
app.get("/api/logs", guard, (req, res) => {
|
|
325
|
+
const deps = app.get("deps");
|
|
326
|
+
const logs = queryLogs(deps.db, parseQuery(req.query));
|
|
327
|
+
res.json({ logs });
|
|
328
|
+
});
|
|
329
|
+
app.get("/api/labels", guard, (req, res) => {
|
|
330
|
+
const deps = app.get("deps");
|
|
331
|
+
const service = typeof req.query.service === "string" ? req.query.service : void 0;
|
|
332
|
+
res.json(queryLabels(deps.db, { service }));
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// src/server/authRoutes.ts
|
|
337
|
+
import { serialize as serializeCookie } from "cookie";
|
|
338
|
+
function makeLoginLimiter(maxAttempts = 10, windowMs = 5 * 6e4) {
|
|
339
|
+
const hits = /* @__PURE__ */ new Map();
|
|
340
|
+
return (req, res, next) => {
|
|
341
|
+
const now = Date.now();
|
|
342
|
+
const ip = req.ip ?? "unknown";
|
|
343
|
+
const rec = hits.get(ip);
|
|
344
|
+
if (!rec || now > rec.reset) {
|
|
345
|
+
hits.set(ip, { count: 1, reset: now + windowMs });
|
|
346
|
+
return next();
|
|
347
|
+
}
|
|
348
|
+
rec.count += 1;
|
|
349
|
+
if (rec.count > maxAttempts) return res.status(429).json({ error: "too many attempts" });
|
|
350
|
+
next();
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
function registerAuthRoutes(app) {
|
|
354
|
+
const limiter = makeLoginLimiter();
|
|
355
|
+
app.post("/api/login", limiter, (req, res) => {
|
|
356
|
+
const deps = app.get("deps");
|
|
357
|
+
const { username, password } = req.body ?? {};
|
|
358
|
+
if (typeof username !== "string" || typeof password !== "string")
|
|
359
|
+
return res.status(400).json({ error: "username and password required" });
|
|
360
|
+
if (!verifyUser(deps.db, username, password))
|
|
361
|
+
return res.status(401).json({ error: "invalid credentials" });
|
|
362
|
+
const cookie = serializeCookie("tl_session", signSession(username, deps.cfg.sessionSecret), {
|
|
363
|
+
httpOnly: true,
|
|
364
|
+
sameSite: "strict",
|
|
365
|
+
path: "/",
|
|
366
|
+
maxAge: 60 * 60 * 24 * 30
|
|
367
|
+
});
|
|
368
|
+
res.setHeader("Set-Cookie", cookie);
|
|
369
|
+
res.json({ ok: true, username });
|
|
370
|
+
});
|
|
371
|
+
app.post("/api/logout", (_req, res) => {
|
|
372
|
+
res.setHeader("Set-Cookie", serializeCookie("tl_session", "", { httpOnly: true, path: "/", maxAge: 0 }));
|
|
373
|
+
res.json({ ok: true });
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// src/server/app.ts
|
|
378
|
+
function createApp(deps) {
|
|
379
|
+
const app = express();
|
|
380
|
+
app.set("trust proxy", true);
|
|
381
|
+
app.set("deps", deps);
|
|
382
|
+
app.get("/api/health", (_req, res) => res.json({ ok: true }));
|
|
383
|
+
app.use("/ingest", express.json({ limit: "5mb" }));
|
|
384
|
+
app.use("/api", express.json({ limit: "1mb" }));
|
|
385
|
+
registerIngest(app);
|
|
386
|
+
registerQueryRoutes(app);
|
|
387
|
+
registerAuthRoutes(app);
|
|
388
|
+
const here = dirname2(fileURLToPath(import.meta.url));
|
|
389
|
+
const uiDir = [join2(here, "ui"), join2(here, "..", "ui"), join2(process.cwd(), "dist", "ui")].find((d) => existsSync(join2(d, "bundle.js")));
|
|
390
|
+
if (uiDir) app.use(express.static(uiDir));
|
|
391
|
+
return app;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// src/server/stream.ts
|
|
395
|
+
import { parse as parseCookie2 } from "cookie";
|
|
396
|
+
import { WebSocketServer } from "ws";
|
|
397
|
+
function attachWebSocket(server, deps) {
|
|
398
|
+
const wss = new WebSocketServer({ server, path: "/ws" });
|
|
399
|
+
wss.on("connection", (ws, req) => {
|
|
400
|
+
const cookies = parseCookie2(req.headers.cookie ?? "");
|
|
401
|
+
const user = verifySession(cookies["tl_session"], deps.cfg.sessionSecret);
|
|
402
|
+
if (!user) {
|
|
403
|
+
ws.close(4401, "unauthorized");
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
ws.isAlive = true;
|
|
407
|
+
ws.on("pong", () => {
|
|
408
|
+
ws.isAlive = true;
|
|
409
|
+
});
|
|
410
|
+
ws.send(JSON.stringify({ type: "buffer", data: deps.buffer.snapshot() }));
|
|
411
|
+
deps.clients.add(ws);
|
|
412
|
+
ws.on("close", () => deps.clients.delete(ws));
|
|
413
|
+
ws.on("error", () => deps.clients.delete(ws));
|
|
414
|
+
});
|
|
415
|
+
const interval = setInterval(() => {
|
|
416
|
+
for (const ws of wss.clients) {
|
|
417
|
+
if (ws.isAlive === false) {
|
|
418
|
+
ws.terminate();
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
ws.isAlive = false;
|
|
422
|
+
try {
|
|
423
|
+
ws.ping();
|
|
424
|
+
} catch {
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}, 3e4);
|
|
428
|
+
interval.unref();
|
|
429
|
+
wss.on("close", () => clearInterval(interval));
|
|
430
|
+
return wss;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// src/server/index.ts
|
|
434
|
+
async function start(configPath = resolveConfigPath()) {
|
|
435
|
+
const cfg = loadConfig(configPath);
|
|
436
|
+
const db = openDb(resolveDbPath(configPath, cfg));
|
|
437
|
+
const buffer = new RingBuffer(cfg.bufferSize);
|
|
438
|
+
const wsClients = /* @__PURE__ */ new Set();
|
|
439
|
+
const broadcast = (r) => {
|
|
440
|
+
const msg = JSON.stringify({ type: "log", data: r });
|
|
441
|
+
for (const c of wsClients) {
|
|
442
|
+
try {
|
|
443
|
+
if (c.readyState === 1) c.send(msg);
|
|
444
|
+
} catch {
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
const app = createApp({ db, buffer, cfg, broadcast });
|
|
449
|
+
const server = createServer(app);
|
|
450
|
+
attachWebSocket(server, { buffer, cfg, clients: wsClients });
|
|
451
|
+
const retentionTimer = setInterval(
|
|
452
|
+
() => runRetention(db, { retentionDays: cfg.retentionDays, maxSizeMB: cfg.maxSizeMB }),
|
|
453
|
+
6e4
|
|
454
|
+
);
|
|
455
|
+
retentionTimer.unref();
|
|
456
|
+
await new Promise((resolve2) => server.listen(cfg.port, cfg.host, resolve2));
|
|
457
|
+
const addr = server.address();
|
|
458
|
+
const port = typeof addr === "object" && addr ? addr.port : cfg.port;
|
|
459
|
+
console.log(`[tinylogs] listening on http://${cfg.host}:${port}`);
|
|
460
|
+
return {
|
|
461
|
+
port,
|
|
462
|
+
close: () => new Promise((resolve2) => {
|
|
463
|
+
clearInterval(retentionTimer);
|
|
464
|
+
for (const c of wsClients) {
|
|
465
|
+
try {
|
|
466
|
+
c.close();
|
|
467
|
+
} catch {
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
server.close(() => {
|
|
471
|
+
db.close();
|
|
472
|
+
resolve2();
|
|
473
|
+
});
|
|
474
|
+
})
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
export {
|
|
478
|
+
start
|
|
479
|
+
};
|
|
480
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/server/index.ts", "../../src/storage/db.ts", "../../src/storage/retention.ts", "../../src/buffer.ts", "../../src/config.ts", "../../src/server/app.ts", "../../src/auth.ts", "../../src/server/ingest.ts", "../../src/server/queryRoutes.ts", "../../src/storage/query.ts", "../../src/server/authRoutes.ts", "../../src/server/stream.ts"],
|
|
4
|
+
"sourcesContent": ["import { createServer } from 'node:http';\nimport { openDb } from '../storage/db.js';\nimport { runRetention } from '../storage/retention.js';\nimport { RingBuffer } from '../buffer.js';\nimport { loadConfig, resolveConfigPath, resolveDbPath } from '../config.js';\nimport { createApp } from './app.js';\nimport { attachWebSocket } from './stream.js';\nimport type { WebSocket } from 'ws';\nimport type { LogRecord } from '../types.js';\n\nexport async function start(configPath = resolveConfigPath()): Promise<{ port: number; close: () => Promise<void> }> {\n const cfg = loadConfig(configPath);\n const db = openDb(resolveDbPath(configPath, cfg));\n const buffer = new RingBuffer(cfg.bufferSize);\n const wsClients = new Set<WebSocket>();\n const broadcast = (r: LogRecord) => {\n const msg = JSON.stringify({ type: 'log', data: r });\n for (const c of wsClients) { try { if (c.readyState === 1) c.send(msg); } catch {} }\n };\n\n const app = createApp({ db, buffer, cfg, broadcast });\n const server = createServer(app);\n attachWebSocket(server, { buffer, cfg, clients: wsClients });\n\n const retentionTimer = setInterval(\n () => runRetention(db, { retentionDays: cfg.retentionDays, maxSizeMB: cfg.maxSizeMB }),\n 60_000,\n );\n retentionTimer.unref();\n\n await new Promise<void>((resolve) => server.listen(cfg.port, cfg.host, resolve));\n const addr = server.address();\n const port = typeof addr === 'object' && addr ? addr.port : cfg.port;\n console.log(`[tinylogs] listening on http://${cfg.host}:${port}`);\n\n return {\n port,\n close: () => new Promise<void>((resolve) => {\n clearInterval(retentionTimer);\n for (const c of wsClients) { try { c.close(); } catch {} }\n server.close(() => { db.close(); resolve(); });\n }),\n };\n}\n", "import Database from 'better-sqlite3';\nimport type { LogRecord } from '../types.js';\n\nconst MIGRATIONS: string[] = [\n `CREATE TABLE logs (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n ts INTEGER NOT NULL,\n service TEXT NOT NULL,\n message TEXT NOT NULL,\n labels TEXT NOT NULL\n );\n CREATE INDEX idx_logs_ts ON logs(ts);\n CREATE INDEX idx_logs_service ON logs(service);\n CREATE TABLE log_labels (\n log_id INTEGER NOT NULL REFERENCES logs(id) ON DELETE CASCADE,\n key TEXT NOT NULL,\n value TEXT NOT NULL\n );\n CREATE INDEX idx_labels_kv ON log_labels(key, value);\n CREATE INDEX idx_labels_logid ON log_labels(log_id);\n CREATE TABLE users (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n username TEXT UNIQUE NOT NULL,\n password_hash TEXT NOT NULL,\n role TEXT NOT NULL DEFAULT 'admin',\n created_at INTEGER NOT NULL\n );`,\n];\n\nexport function openDb(path: string): Database.Database {\n const db = new Database(path);\n db.pragma('journal_mode = WAL');\n db.pragma('foreign_keys = ON');\n db.exec('CREATE TABLE IF NOT EXISTS schema_migrations (version INTEGER PRIMARY KEY)');\n const current = (db.prepare('SELECT COALESCE(MAX(version),0) v FROM schema_migrations').get() as any).v as number;\n const apply = db.transaction((from: number) => {\n for (let i = from; i < MIGRATIONS.length; i++) {\n db.exec(MIGRATIONS[i]);\n db.prepare('INSERT INTO schema_migrations (version) VALUES (?)').run(i + 1);\n }\n });\n apply(current);\n return db;\n}\n\nexport function insertLog(db: Database.Database, rec: LogRecord): number {\n const txn = db.transaction((r: LogRecord) => {\n const info = db.prepare('INSERT INTO logs (ts, service, message, labels) VALUES (?,?,?,?)')\n .run(r.ts, r.service, r.message, JSON.stringify(r.labels ?? {}));\n const logId = Number(info.lastInsertRowid);\n const ins = db.prepare('INSERT INTO log_labels (log_id, key, value) VALUES (?,?,?)');\n for (const [k, v] of Object.entries(r.labels ?? {})) ins.run(logId, k, String(v));\n return logId;\n });\n return txn(rec);\n}\n\nexport function dbSizeBytes(db: Database.Database): number {\n const pageCount = (db.pragma('page_count', { simple: true }) as number);\n const pageSize = (db.pragma('page_size', { simple: true }) as number);\n return pageCount * pageSize;\n}\n", "import type Database from 'better-sqlite3';\nimport { dbSizeBytes } from './db.js';\n\nexport function pruneByAge(db: Database.Database, retentionDays: number, now: number): number {\n const cutoff = now - retentionDays * 86400000;\n return db.prepare('DELETE FROM logs WHERE ts < ?').run(cutoff).changes;\n}\n\nexport function pruneBySize(db: Database.Database, maxSizeMB: number, batch = 1000): number {\n const maxBytes = maxSizeMB * 1024 * 1024;\n let deleted = 0;\n // Delete oldest rows in batches until file is under the cap or nothing remains.\n while (dbSizeBytes(db) > maxBytes) {\n const changes = db.prepare(\n 'DELETE FROM logs WHERE id IN (SELECT id FROM logs ORDER BY id ASC LIMIT ?)'\n ).run(batch).changes;\n if (changes === 0) break;\n deleted += changes;\n }\n return deleted;\n}\n\nexport function runRetention(\n db: Database.Database,\n opts: { retentionDays: number; maxSizeMB: number; now?: number }\n): void {\n try {\n const now = opts.now ?? Date.now();\n pruneByAge(db, opts.retentionDays, now);\n pruneBySize(db, opts.maxSizeMB);\n db.exec('VACUUM');\n } catch (err) {\n console.error('[tinylogs] retention failed:', (err as Error).message);\n }\n}\n", "import type { LogRecord } from './types.js';\n\nexport class RingBuffer {\n private items: LogRecord[] = [];\n constructor(private capacity: number) {}\n\n push(rec: LogRecord): void {\n this.items.push(rec);\n if (this.items.length > this.capacity) this.items.shift();\n }\n\n snapshot(): LogRecord[] {\n return this.items.slice();\n }\n\n get size(): number {\n return this.items.length;\n }\n}\n", "import { createHash, randomBytes } from 'node:crypto';\nimport { readFileSync, writeFileSync } from 'node:fs';\nimport { dirname, isAbsolute, join, resolve } from 'node:path';\nimport type { Config } from './types.js';\n\nexport const DEFAULTS = {\n port: 4700,\n host: '127.0.0.1',\n dbPath: 'tinylogs.db',\n retentionDays: 14,\n maxSizeMB: 500,\n bufferSize: 2000,\n} as const;\n\nexport function resolveConfigPath(flag?: string): string {\n if (flag) return flag;\n if (process.env.TINYLOGS_CONFIG) return process.env.TINYLOGS_CONFIG;\n return join(process.cwd(), 'tinylogs.config.json');\n}\n\nexport function hashToken(token: string): string {\n return createHash('sha256').update(token, 'utf8').digest('hex');\n}\n\nexport function generateSecrets() {\n const sessionSecret = randomBytes(32).toString('hex');\n const token = randomBytes(24).toString('base64url');\n return { sessionSecret, token, ingestTokenHash: hashToken(token) };\n}\n\nexport function saveConfig(path: string, cfg: Config): void {\n writeFileSync(path, JSON.stringify(cfg, null, 2) + '\\n', { mode: 0o600 });\n}\n\nexport function loadConfig(path: string): Config {\n const raw = readFileSync(path, 'utf8'); // throws if missing\n const cfg = JSON.parse(raw) as Config;\n for (const k of ['port', 'host', 'dbPath', 'sessionSecret', 'ingestTokenHash'] as const) {\n if (cfg[k] === undefined) throw new Error(`config missing field: ${k}`);\n }\n return cfg;\n}\n\nexport function resolveDbPath(configPath: string, cfg: Config): string {\n if (isAbsolute(cfg.dbPath)) return cfg.dbPath;\n return resolve(dirname(configPath), cfg.dbPath);\n}\n", "import express, { type Express } from 'express';\nimport { existsSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type Database from 'better-sqlite3';\nimport type { Config, LogRecord } from '../types.js';\nimport { RingBuffer } from '../buffer.js';\nimport { registerIngest } from './ingest.js';\nimport { registerQueryRoutes } from './queryRoutes.js';\nimport { registerAuthRoutes } from './authRoutes.js';\n\nexport interface AppDeps {\n db: Database.Database;\n buffer: RingBuffer;\n cfg: Config;\n broadcast: (r: LogRecord) => void;\n}\n\nexport function createApp(deps: AppDeps): Express {\n const app = express();\n app.set('trust proxy', true); // so req.ip reflects X-Forwarded-For behind a reverse proxy\n app.set('deps', deps);\n\n app.get('/api/health', (_req, res) => res.json({ ok: true }));\n\n // /ingest gets its own 5MB JSON parser (Task 9 mounts the handler after this).\n app.use('/ingest', express.json({ limit: '5mb' }));\n app.use('/api', express.json({ limit: '1mb' }));\n\n registerIngest(app);\n registerQueryRoutes(app);\n registerAuthRoutes(app);\n\n // Static UI (built to dist/ui). The running entry may be dist/cli.js (here = dist)\n // or dist/server/index.js (here = dist/server), so probe both depths. In dev (tsx\n // from src) neither resolves, so also fall back to <cwd>/dist/ui. Pick the first\n // candidate that actually contains the built bundle. Absent entirely \u2192 skip.\n const here = dirname(fileURLToPath(import.meta.url));\n const uiDir = [join(here, 'ui'), join(here, '..', 'ui'), join(process.cwd(), 'dist', 'ui')]\n .find((d) => existsSync(join(d, 'bundle.js')));\n if (uiDir) app.use(express.static(uiDir));\n\n return app;\n}\n", "import bcrypt from 'bcryptjs';\nimport { createHmac, timingSafeEqual } from 'node:crypto';\nimport type Database from 'better-sqlite3';\nimport { hashToken } from './config.js';\n\nexport function hashPassword(pw: string): string {\n return bcrypt.hashSync(pw, 10);\n}\n\nexport function createUser(db: Database.Database, username: string, pw: string, role = 'admin'): number {\n const info = db.prepare(\n 'INSERT INTO users (username, password_hash, role, created_at) VALUES (?,?,?,?)'\n ).run(username, hashPassword(pw), role, Date.now());\n return Number(info.lastInsertRowid);\n}\n\nexport function verifyUser(db: Database.Database, username: string, pw: string): boolean {\n const row = db.prepare('SELECT password_hash FROM users WHERE username=?').get(username) as any;\n if (!row) { bcrypt.compareSync(pw, '$2a$10$0000000000000000000000000000000000000000000000000000'); return false; }\n return bcrypt.compareSync(pw, row.password_hash);\n}\n\nfunction safeEqualHex(a: string, b: string): boolean {\n const ab = Buffer.from(a, 'hex'); const bb = Buffer.from(b, 'hex');\n if (ab.length !== bb.length || ab.length === 0) return false;\n return timingSafeEqual(ab, bb);\n}\n\nexport function verifyIngestToken(bearer: string | undefined, ingestTokenHash: string): boolean {\n if (!bearer || !bearer.startsWith('Bearer ')) return false;\n const token = bearer.slice('Bearer '.length);\n return safeEqualHex(hashToken(token), ingestTokenHash);\n}\n\nexport function signSession(username: string, secret: string): string {\n const mac = createHmac('sha256', secret).update(username).digest('hex');\n return `${username}.${mac}`;\n}\n\nexport function verifySession(cookieVal: string | undefined, secret: string): string | null {\n if (!cookieVal) return null;\n const dot = cookieVal.lastIndexOf('.');\n if (dot <= 0) return null;\n const username = cookieVal.slice(0, dot);\n const mac = cookieVal.slice(dot + 1);\n const expected = createHmac('sha256', secret).update(username).digest('hex');\n if (mac.length !== expected.length) return null;\n if (!timingSafeEqual(Buffer.from(mac), Buffer.from(expected))) return null;\n return username;\n}\n", "import type { Express } from 'express';\nimport type { AppDeps } from './app.js';\nimport type { LogRecord } from '../types.js';\nimport { verifyIngestToken } from '../auth.js';\nimport { insertLog } from '../storage/db.js';\n\nconst MAX_MESSAGE = 16384;\nconst MAX_LABELS = 50;\nconst MAX_KV = 512;\nconst MAX_BATCH = 1000;\n\nexport function validateRecord(raw: any): { ok: true; rec: LogRecord } | { ok: false; error: string } {\n if (typeof raw !== 'object' || raw === null) return { ok: false, error: 'record must be an object' };\n if (typeof raw.service !== 'string' || raw.service.length === 0) return { ok: false, error: 'service required' };\n if (typeof raw.message !== 'string' || raw.message.length === 0) return { ok: false, error: 'message required' };\n if (raw.message.length > MAX_MESSAGE) return { ok: false, error: 'message too long' };\n if (raw.service.length > MAX_KV) return { ok: false, error: 'service too long' };\n const labels: Record<string, string> = {};\n if (raw.labels !== undefined) {\n if (typeof raw.labels !== 'object' || raw.labels === null || Array.isArray(raw.labels))\n return { ok: false, error: 'labels must be an object' };\n const keys = Object.keys(raw.labels);\n if (keys.length > MAX_LABELS) return { ok: false, error: 'too many labels' };\n for (const k of keys) {\n const v = raw.labels[k];\n if (typeof v !== 'string') return { ok: false, error: `label ${k} must be a string` };\n if (k.length > MAX_KV || v.length > MAX_KV) return { ok: false, error: `label ${k} too long` };\n labels[k] = v;\n }\n }\n const ts = typeof raw.ts === 'number' && Number.isFinite(raw.ts) ? raw.ts : Date.now();\n return { ok: true, rec: { ts, service: raw.service, message: raw.message, labels } };\n}\n\nexport function registerIngest(app: Express): void {\n app.post('/ingest', (req, res) => {\n const deps = app.get('deps') as AppDeps;\n if (!verifyIngestToken(req.header('authorization'), deps.cfg.ingestTokenHash)) {\n return res.status(401).json({ error: 'invalid token' });\n }\n const body = req.body;\n const records = Array.isArray(body) ? body : [body];\n if (records.length > MAX_BATCH) return res.status(400).json({ error: 'batch too large' });\n const validated: LogRecord[] = [];\n for (const raw of records) {\n const v = validateRecord(raw);\n if (!v.ok) return res.status(400).json({ error: v.error });\n validated.push(v.rec);\n }\n try {\n for (const rec of validated) {\n const id = insertLog(deps.db, rec);\n const stored = { ...rec, id };\n deps.buffer.push(stored);\n deps.broadcast(stored);\n }\n return res.json({ accepted: validated.length });\n } catch (err) {\n console.error('[tinylogs] ingest error:', (err as Error).message);\n return res.status(500).json({ error: 'internal error' });\n }\n });\n}\n", "import type { Express, RequestHandler } from 'express';\nimport { parse as parseCookie } from 'cookie';\nimport type { AppDeps } from './app.js';\nimport type { QueryParams } from '../storage/query.js';\nimport { queryLogs, queryLabels } from '../storage/query.js';\nimport { verifySession } from '../auth.js';\n\nexport function requireSession(app: Express): RequestHandler {\n return (req, res, next) => {\n const deps = app.get('deps') as AppDeps;\n const cookies = parseCookie(req.header('cookie') ?? '');\n const user = verifySession(cookies['tl_session'], deps.cfg.sessionSecret);\n if (!user) return res.status(401).json({ error: 'unauthorized' });\n (req as any).user = user;\n next();\n };\n}\n\nexport function parseQuery(query: Record<string, any>): QueryParams {\n const p: QueryParams = {};\n if (typeof query.service === 'string') p.service = query.service;\n if (typeof query.q === 'string') p.q = query.q;\n if (query.from !== undefined) p.from = Number(query.from);\n if (query.to !== undefined) p.to = Number(query.to);\n if (query.limit !== undefined) p.limit = Number(query.limit);\n if (query.before !== undefined) p.before = Number(query.before);\n const rawLabels = query.label === undefined ? [] : Array.isArray(query.label) ? query.label : [query.label];\n p.labels = rawLabels\n .map((s: string) => { const i = s.indexOf('='); return i < 0 ? null : { key: s.slice(0, i), value: s.slice(i + 1) }; })\n .filter(Boolean) as Array<{ key: string; value: string }>;\n return p;\n}\n\nexport function registerQueryRoutes(app: Express): void {\n const guard = requireSession(app);\n app.get('/api/logs', guard, (req, res) => {\n const deps = app.get('deps') as AppDeps;\n const logs = queryLogs(deps.db, parseQuery(req.query as any));\n res.json({ logs });\n });\n app.get('/api/labels', guard, (req, res) => {\n const deps = app.get('deps') as AppDeps;\n const service = typeof req.query.service === 'string' ? req.query.service : undefined;\n res.json(queryLabels(deps.db, { service }));\n });\n}\n", "import type Database from 'better-sqlite3';\nimport type { LogRecord } from '../types.js';\n\nexport interface QueryParams {\n service?: string;\n labels?: Array<{ key: string; value: string }>;\n q?: string;\n from?: number; to?: number;\n limit?: number;\n before?: number;\n}\n\nfunction rowToRecord(row: any): LogRecord {\n return { id: row.id, ts: row.ts, service: row.service, message: row.message, labels: JSON.parse(row.labels) };\n}\n\nexport function queryLogs(db: Database.Database, params: QueryParams): LogRecord[] {\n const where: string[] = [];\n const args: any[] = [];\n if (params.service) { where.push('logs.service = ?'); args.push(params.service); }\n if (params.q) { where.push('logs.message LIKE ? COLLATE NOCASE'); args.push(`%${params.q}%`); }\n if (params.from !== undefined) { where.push('logs.ts >= ?'); args.push(params.from); }\n if (params.to !== undefined) { where.push('logs.ts < ?'); args.push(params.to); }\n if (params.before !== undefined) { where.push('logs.id < ?'); args.push(params.before); }\n for (const l of params.labels ?? []) {\n where.push('logs.id IN (SELECT log_id FROM log_labels WHERE key = ? AND value = ?)');\n args.push(l.key, l.value);\n }\n const limit = Math.min(Math.max(params.limit ?? 200, 1), 1000);\n const sql = `SELECT id, ts, service, message, labels FROM logs\n ${where.length ? 'WHERE ' + where.join(' AND ') : ''}\n ORDER BY logs.id DESC LIMIT ?`;\n return db.prepare(sql).all(...args, limit).map(rowToRecord);\n}\n\nexport interface LabelCount { key: string; value: string; count: number; }\n\nexport function queryLabels(db: Database.Database, opts: { service?: string } = {}) {\n const services = db.prepare(\n 'SELECT service, COUNT(*) count FROM logs GROUP BY service ORDER BY count DESC'\n ).all() as Array<{ service: string; count: number }>;\n\n let labels: LabelCount[];\n if (opts.service) {\n labels = db.prepare(\n `SELECT ll.key, ll.value, COUNT(*) count FROM log_labels ll\n JOIN logs ON logs.id = ll.log_id WHERE logs.service = ?\n GROUP BY ll.key, ll.value ORDER BY ll.key, count DESC`\n ).all(opts.service) as LabelCount[];\n } else {\n labels = db.prepare(\n `SELECT key, value, COUNT(*) count FROM log_labels\n GROUP BY key, value ORDER BY key, count DESC`\n ).all() as LabelCount[];\n }\n return { services, labels };\n}\n", "import type { Express, RequestHandler } from 'express';\nimport { serialize as serializeCookie } from 'cookie';\nimport type { AppDeps } from './app.js';\nimport { verifyUser, signSession } from '../auth.js';\n\nexport function makeLoginLimiter(maxAttempts = 10, windowMs = 5 * 60_000): RequestHandler {\n const hits = new Map<string, { count: number; reset: number }>();\n return (req, res, next) => {\n const now = Date.now();\n const ip = req.ip ?? 'unknown';\n const rec = hits.get(ip);\n if (!rec || now > rec.reset) { hits.set(ip, { count: 1, reset: now + windowMs }); return next(); }\n rec.count += 1;\n if (rec.count > maxAttempts) return res.status(429).json({ error: 'too many attempts' });\n next();\n };\n}\n\nexport function registerAuthRoutes(app: Express): void {\n const limiter = makeLoginLimiter();\n app.post('/api/login', limiter, (req, res) => {\n const deps = app.get('deps') as AppDeps;\n const { username, password } = req.body ?? {};\n if (typeof username !== 'string' || typeof password !== 'string')\n return res.status(400).json({ error: 'username and password required' });\n if (!verifyUser(deps.db, username, password))\n return res.status(401).json({ error: 'invalid credentials' });\n const cookie = serializeCookie('tl_session', signSession(username, deps.cfg.sessionSecret), {\n httpOnly: true, sameSite: 'strict', path: '/', maxAge: 60 * 60 * 24 * 30,\n });\n res.setHeader('Set-Cookie', cookie);\n res.json({ ok: true, username });\n });\n\n app.post('/api/logout', (_req, res) => {\n res.setHeader('Set-Cookie', serializeCookie('tl_session', '', { httpOnly: true, path: '/', maxAge: 0 }));\n res.json({ ok: true });\n });\n}\n", "import type { Server } from 'node:http';\nimport { parse as parseCookie } from 'cookie';\nimport { WebSocketServer, WebSocket } from 'ws';\nimport type { RingBuffer } from '../buffer.js';\nimport type { Config } from '../types.js';\nimport { verifySession } from '../auth.js';\n\nexport function attachWebSocket(\n server: Server,\n deps: { buffer: RingBuffer; cfg: Config; clients: Set<WebSocket> },\n): WebSocketServer {\n const wss = new WebSocketServer({ server, path: '/ws' });\n\n wss.on('connection', (ws, req) => {\n const cookies = parseCookie(req.headers.cookie ?? '');\n const user = verifySession(cookies['tl_session'], deps.cfg.sessionSecret);\n if (!user) { ws.close(4401, 'unauthorized'); return; }\n\n (ws as any).isAlive = true;\n ws.on('pong', () => { (ws as any).isAlive = true; });\n\n ws.send(JSON.stringify({ type: 'buffer', data: deps.buffer.snapshot() }));\n deps.clients.add(ws);\n ws.on('close', () => deps.clients.delete(ws));\n ws.on('error', () => deps.clients.delete(ws));\n });\n\n const interval = setInterval(() => {\n for (const ws of wss.clients) {\n if ((ws as any).isAlive === false) { ws.terminate(); continue; }\n (ws as any).isAlive = false;\n try { ws.ping(); } catch {}\n }\n }, 30_000);\n interval.unref();\n wss.on('close', () => clearInterval(interval));\n\n return wss;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["dirname", "join", "dirname", "join", "parseCookie", "parseCookie", "resolve"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface LogRecord {
|
|
2
|
+
id?: number;
|
|
3
|
+
ts: number;
|
|
4
|
+
service: string;
|
|
5
|
+
message: string;
|
|
6
|
+
labels: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
export interface Config {
|
|
9
|
+
port: number;
|
|
10
|
+
host: string;
|
|
11
|
+
dbPath: string;
|
|
12
|
+
retentionDays: number;
|
|
13
|
+
maxSizeMB: number;
|
|
14
|
+
bufferSize: number;
|
|
15
|
+
sessionSecret: string;
|
|
16
|
+
ingestTokenHash: string;
|
|
17
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
:root{--bg: #0d1117;--panel: #161b22;--border: #30363d;--text: #c9d1d9;--muted: #8b949e;--accent: #58a6ff;--lvl-error: #f85149;--lvl-warn: #d29922;--lvl-info: #3fb950;--lvl-debug: #8b949e;--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace}*{box-sizing:border-box}html,body{margin:0;height:100%;background:var(--bg);color:var(--text);font-family:var(--mono);font-size:13px}#app{height:100%}button{font-family:inherit;cursor:pointer}.login.svelte-nnlff{height:100%;display:grid;place-items:center}form.svelte-nnlff{display:flex;flex-direction:column;gap:.6rem;width:260px;background:var(--panel);border:1px solid var(--border);padding:1.4rem;border-radius:8px}h1.svelte-nnlff{margin:0 0 .4rem;font-size:1.1rem}input.svelte-nnlff{background:var(--bg);border:1px solid var(--border);color:var(--text);padding:.5rem;border-radius:4px;font-family:inherit}button.svelte-nnlff{background:var(--accent);border:none;color:#fff;padding:.5rem;border-radius:4px}.err.svelte-nnlff{color:var(--lvl-error);margin:0;font-size:12px}aside.svelte-17vk837{background:var(--panel);border-right:1px solid var(--border);overflow-y:auto;padding:.5rem}.section.svelte-17vk837{margin-bottom:1rem}.head.svelte-17vk837{text-transform:uppercase;font-size:10px;color:var(--muted);letter-spacing:.05em;padding:.3rem .4rem}button.svelte-17vk837{display:flex;justify-content:space-between;width:100%;gap:.5rem;text-align:left;background:none;border:none;color:var(--text);padding:.25rem .4rem;border-radius:4px;font-size:12px}button.svelte-17vk837:hover{background:#ffffff0d}button.active.svelte-17vk837{background:#58a6ff26;color:var(--accent)}.count.svelte-17vk837{color:var(--muted)}.table.svelte-1ay25z6{overflow-y:auto;padding-bottom:1rem}.row.svelte-1ay25z6{display:grid;grid-template-columns:180px 130px 60px 1fr;gap:.5rem;padding:.15rem .6rem;border-bottom:1px solid rgba(255,255,255,.03);white-space:nowrap}.row.svelte-1ay25z6:hover{background:#ffffff08}.header.svelte-1ay25z6{position:sticky;top:0;background:var(--panel);color:var(--muted);text-transform:uppercase;font-size:10px;letter-spacing:.05em}.ts.svelte-1ay25z6{color:var(--muted)}.svc.svelte-1ay25z6{color:var(--accent);overflow:hidden;text-overflow:ellipsis}.lvl.error.svelte-1ay25z6{color:var(--lvl-error)}.lvl.warn.svelte-1ay25z6{color:var(--lvl-warn)}.lvl.info.svelte-1ay25z6{color:var(--lvl-info)}.lvl.debug.svelte-1ay25z6{color:var(--lvl-debug)}.msg.svelte-1ay25z6{white-space:pre-wrap;word-break:break-word}.tag.svelte-1ay25z6{color:var(--muted);background:#ffffff0d;padding:0 .3rem;border-radius:3px;margin-left:.4rem;font-size:11px}.more.svelte-1ay25z6{display:block;margin:.6rem auto;background:var(--panel);border:1px solid var(--border);color:var(--text);padding:.3rem 1rem;border-radius:4px}footer.svelte-3dji7h{display:flex;align-items:center;gap:.5rem;padding:.3rem .8rem;background:var(--panel);border-top:1px solid var(--border);font-size:12px;color:var(--muted)}.dot.svelte-3dji7h{width:8px;height:8px;border-radius:50%;background:var(--muted)}.dot.connected.svelte-3dji7h{background:var(--lvl-info)}.dot.disconnected.svelte-3dji7h{background:var(--lvl-error)}.dot.connecting.svelte-3dji7h{background:var(--lvl-warn)}.sep.svelte-3dji7h{opacity:.5}.filter.svelte-3dji7h{color:var(--accent)}.app.svelte-y6hlvb{display:grid;height:100%;grid-template-columns:220px 1fr;grid-template-rows:auto 1fr auto;grid-template-areas:"header header" "sidebar table" "status status"}header.svelte-y6hlvb{grid-area:header;display:flex;align-items:center;gap:1rem;padding:.4rem .8rem;background:var(--panel);border-bottom:1px solid var(--border)}.search.svelte-y6hlvb{flex:1;background:var(--bg);border:1px solid var(--border);color:var(--text);padding:.3rem .6rem;border-radius:4px;font-family:inherit}.logout.svelte-y6hlvb{background:none;border:1px solid var(--border);color:var(--muted);padding:.3rem .7rem;border-radius:4px}aside{grid-area:sidebar}.table{grid-area:table}.status.svelte-y6hlvb{grid-area:status}.boot.svelte-1h6z84z{height:100%;display:grid;place-items:center;color:var(--muted)}
|
|
2
|
+
/*# sourceMappingURL=bundle.css.map */
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../ui/app.css", "Login.svelte", "Sidebar.svelte", "LogTable.svelte", "StatusBar.svelte", "Dashboard.svelte", "App.svelte"],
|
|
4
|
+
"sourcesContent": [":root {\n --bg: #0d1117; --panel: #161b22; --border: #30363d; --text: #c9d1d9;\n --muted: #8b949e; --accent: #58a6ff;\n --lvl-error: #f85149; --lvl-warn: #d29922; --lvl-info: #3fb950; --lvl-debug: #8b949e;\n --mono: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace;\n}\n* { box-sizing: border-box; }\nhtml, body { margin: 0; height: 100%; background: var(--bg); color: var(--text);\n font-family: var(--mono); font-size: 13px; }\n#app { height: 100%; }\nbutton { font-family: inherit; cursor: pointer; }\n", null, null, null, null, null, null],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|