- make quick/check/mutation from cj/security-baseline (Makefile unchanged, settings in baseline.mk), pre-commit, gitleaks, Semgrep rules, ESLint security rules, Forgejo workflow - tests/security: generic baseline tests plus app config and role tests against createApp() with an in-memory database - extract createApp() from src/server/index.ts; LB_DATABASE_PATH for tests - fix gaps found by the tests: reject unknown fields on project create, answer malformed JSON with 400 instead of 500, set nosniff on the API - npm audit fix (next 16.4.1 and others, no high/critical left) - clear ESLint errors, safeFileName without control-character regex - per-project ownership stays an open, documented gap (idorGap -> TODO) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
75 lines
2.4 KiB
TypeScript
75 lines
2.4 KiB
TypeScript
import { createApp } from "./app.js";
|
|
import { userService } from "./composition/auth.js";
|
|
import { createLogger, toErrorMeta } from "../shared/logging/logger.js";
|
|
import { closeDefaultDatabase } from "../db/client.js";
|
|
|
|
const logger = createLogger("api");
|
|
const app = createApp();
|
|
const port = Number(process.env.PORT || 3000);
|
|
const heartbeatIntervalMs = 5 * 60 * 1000;
|
|
const shutdownTimeoutMs = 15 * 1000;
|
|
|
|
process.on("uncaughtException", (error) => {
|
|
logger.error("uncaught exception, exiting", toErrorMeta(error));
|
|
process.exit(1);
|
|
});
|
|
|
|
process.on("unhandledRejection", (reason) => {
|
|
logger.error("unhandled rejection, exiting", toErrorMeta(reason));
|
|
process.exit(1);
|
|
});
|
|
|
|
setInterval(() => {
|
|
const memory = process.memoryUsage();
|
|
logger.verbose("heartbeat", {
|
|
uptimeSeconds: Math.round(process.uptime()),
|
|
rssMb: Math.round(memory.rss / 1024 / 1024),
|
|
heapUsedMb: Math.round(memory.heapUsed / 1024 / 1024),
|
|
});
|
|
}, heartbeatIntervalMs).unref();
|
|
|
|
userService
|
|
.ensureBootstrapAdmin()
|
|
.catch((error) => {
|
|
logger.error("bootstrap admin setup failed", toErrorMeta(error));
|
|
process.exit(1);
|
|
})
|
|
.then(() => {
|
|
const server = app.listen(port, () => {
|
|
logger.info("server started", { port });
|
|
});
|
|
|
|
let shuttingDown = false;
|
|
|
|
function shutdown(signal: NodeJS.Signals): void {
|
|
if (shuttingDown) return;
|
|
shuttingDown = true;
|
|
logger.info("shutting down", { signal });
|
|
|
|
// Falls ein Request haengt oder ein Socket nicht freigegeben wird, soll der
|
|
// Prozess trotzdem enden, statt auf das SIGKILL des Containers zu warten.
|
|
const forceExit = setTimeout(() => {
|
|
logger.error("shutdown timed out, forcing exit");
|
|
process.exit(1);
|
|
}, shutdownTimeoutMs);
|
|
forceExit.unref();
|
|
|
|
server.close((error) => {
|
|
if (error) logger.error("http server close failed", toErrorMeta(error));
|
|
try {
|
|
closeDefaultDatabase();
|
|
} catch (closeError) {
|
|
logger.error("database close failed", toErrorMeta(closeError));
|
|
}
|
|
logger.info("shutdown complete");
|
|
process.exit(error ? 1 : 0);
|
|
});
|
|
|
|
// Ohne das warten offene Keep-Alive-Verbindungen (z. B. die des
|
|
// Next.js-Servers) den Timeout oben aus; laufende Requests laufen weiter.
|
|
server.closeIdleConnections();
|
|
}
|
|
|
|
process.on("SIGTERM", shutdown);
|
|
process.on("SIGINT", shutdown);
|
|
});
|