- API writes pass token:<name> for Bearer requests and "user" for sessions. - X-Requested-With: sttracker on every non-Bearer write, login and register included. - JSON-body routes reject non-JSON content types with 415. - A malformed stt_session cookie counts as no cookie (401), not 500. - DELETE me/tokens/:id is 404 for unknown or foreign ids; admin/invites is admin-only. - Bun.serve development mode only with NODE_ENV=development or --hot. - OpenAPI documents multipart fields. Co-Authored-By: Claude Opus 5.5 <[email protected]>
69 lines
2.7 KiB
TypeScript
69 lines
2.7 KiB
TypeScript
import { mkdir } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { backupHandler } from "@sttracker/core/backup";
|
|
import { syncHandler } from "@sttracker/core/connections";
|
|
import { startScheduler, startWorker } from "@sttracker/core/jobs";
|
|
import { loadPlugins } from "@sttracker/core/registry";
|
|
import { createDb } from "@sttracker/db/db";
|
|
import { migrateToLatest } from "@sttracker/db/migrate";
|
|
import index from "../../web/index.html";
|
|
import { createApp } from "./app.ts";
|
|
import { ensureBackupScheduled } from "./boot.ts";
|
|
import { loadConfig } from "./config.ts";
|
|
|
|
const BACKUP_CHECK_MS = 60 * 60 * 1000;
|
|
|
|
const config = loadConfig();
|
|
await mkdir(join(config.dataDir, "plugins"), { recursive: true });
|
|
const db = await createDb(config.databaseUrl);
|
|
await migrateToLatest(db);
|
|
const registry = await loadPlugins(config.pluginDirs);
|
|
|
|
const ensureBackup = () => ensureBackupScheduled(db).catch((e) => console.error("[backup] scheduling failed", e));
|
|
const runBackup = backupHandler({ db, dataDir: config.dataDir });
|
|
const worker = startWorker(db, {
|
|
sync: syncHandler({ db, secret: config.secret, registry }),
|
|
backup: async () => {
|
|
try {
|
|
await runBackup();
|
|
} finally {
|
|
// The finishing job still holds lockKey "backup" until the worker marks it done; queue the next one shortly after.
|
|
setTimeout(() => void ensureBackup(), 5_000);
|
|
}
|
|
},
|
|
});
|
|
const scheduler = startScheduler(db);
|
|
await ensureBackup();
|
|
// Safety net: re-queue the daily backup if a run's follow-up was missed (e.g. it failed for good).
|
|
const backupTimer = setInterval(() => void ensureBackup(), BACKUP_CHECK_MS);
|
|
|
|
const app = createApp({ db, registry, secret: config.secret, dataDir: config.dataDir, publicUrl: config.publicUrl });
|
|
const server = Bun.serve({
|
|
port: config.port,
|
|
// Production unless asked: dev mode adds HMR and verbose error pages.
|
|
// `bun --hot` puts the flag in execArgv, not Bun.argv.
|
|
development: process.env.NODE_ENV === "development" || process.execArgv.includes("--hot"),
|
|
routes: {
|
|
"/api/*": (req, srv) => app.fetch(req, srv),
|
|
"/ingest/*": (req, srv) => app.fetch(req, srv),
|
|
"/*": index,
|
|
},
|
|
fetch: (req, srv) => app.fetch(req, srv),
|
|
});
|
|
console.log(`STTracker listening on ${server.url} (${db.dialect}, ${registry.list().length} plugin(s))`);
|
|
|
|
let stopping = false;
|
|
async function shutdown(signal: string): Promise<void> {
|
|
if (stopping) return;
|
|
stopping = true;
|
|
console.log(`${signal}: shutting down`);
|
|
clearInterval(backupTimer);
|
|
scheduler.stop();
|
|
await server.stop();
|
|
await worker.stop();
|
|
await db.close();
|
|
process.exit(0);
|
|
}
|
|
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
process.on("SIGINT", () => void shutdown("SIGINT"));
|