security(cards): fail-secure dev-stub, headers, rate-limit, dsgvo audit
Some checks are pending
CI / validate (push) Waiting to run
Some checks are pending
CI / validate (push) Waiting to run
Behebt live verifiziertes Auth-Bypass auf cardecky-api.mana.how (X-User-Id → founder-Tier) und zieht im selben Patch das fehlende Operations-/Compliance-Fundament nach. * Auth-Middleware fail-secure: opt-in via CARDS_AUTH_DEV_STUB="true" (war opt-out, Default true). Compose-Default flipped auf "false", NODE_ENV="production" für cards-api ergänzt, env-Template dokumentiert. vitest.config.ts + tests/setup.ts aktivieren den Stub gezielt für Test-Suiten. * Security-Headers: Hono secureHeaders() in apps/api, SvelteKit hooks.server.ts mit X-Frame/X-Content-Type/Referrer/ HSTS in apps/web. CSP bewusst ausgelassen — eigener Sprint. * CORS-localhost-Whitelist nur außerhalb Prod. * Rate-Limiting (in-memory sliding window, dependency-frei) auf share.receive 60/min/IP, media.upload 30/min/user, decks.generate + decks.from-image 10/min/user, dsgvo.* 10/min/IP. * Health-Endpoint mit echter DB- und MinIO-Probe; /healthz bleibt Liveness, /healthz/details ist Readiness mit 503 bei Failure. * DSGVO-Honesty: storage_ok + storage_error im Response (statt schluckend console.warn), Account-UI zeigt Fehler-Toast. * Audit-Log: strukturierte JSON-Zeile (kind: "audit") auf stdout für /dsgvo/export, /dsgvo/delete, /me/export, /me/delete. * Bug-Fix: duplizierte case "multiple-choice"-Clause in fsrs.ts. Verifiziert: apps/api 17 Files / 104 Tests grün, apps/web check 0 errors. Deploy auf Mac Mini steht noch aus. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5859e202c5
commit
e1ddbf34b3
21 changed files with 832 additions and 80 deletions
|
|
@ -1,22 +1,44 @@
|
|||
import { Hono } from 'hono';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
import { getDb } from '../db/connection.ts';
|
||||
import { getStorage } from '../services/storage.ts';
|
||||
|
||||
export const healthRoute = new Hono();
|
||||
|
||||
/**
|
||||
* Liveness — antwortet immer 200, solange der Prozess läuft.
|
||||
* Bewusst KEIN Downstream-Probe, damit ein kurzer DB-/MinIO-Hänger
|
||||
* nicht den Container in eine Restart-Schleife zwingt.
|
||||
*/
|
||||
healthRoute.get('/healthz', (c) => c.json({ status: 'ok' }));
|
||||
|
||||
healthRoute.get('/healthz/details', (c) =>
|
||||
c.json({
|
||||
status: 'ok',
|
||||
app: 'cards',
|
||||
version: process.env.CARDS_API_VERSION ?? '0.0.0',
|
||||
uptime_s: Math.floor(process.uptime()),
|
||||
mana_packages: {
|
||||
// In Phase 5 mit @mana/shared-app-tpl ersetzen.
|
||||
'@mana/shared-share-protocol': 'TBD',
|
||||
'@mana/shared-app-tpl': 'TBD',
|
||||
/**
|
||||
* Readiness mit Downstream-Probes. Status 200 wenn DB + MinIO grün,
|
||||
* sonst 503 mit Aufschlüsselung welche Probe fehlgeschlagen ist.
|
||||
* Probes timen sich selbst aus über ein 1s-AbortSignal — eine träge
|
||||
* Abhängigkeit darf das Readiness-Signal nicht hängen lassen.
|
||||
*/
|
||||
healthRoute.get('/healthz/details', async (c) => {
|
||||
const [dbProbe, storageProbe] = await Promise.all([
|
||||
probeDb(),
|
||||
probeStorage(),
|
||||
]);
|
||||
const allOk = dbProbe.ok && storageProbe.ok;
|
||||
return c.json(
|
||||
{
|
||||
status: allOk ? 'ok' : 'degraded',
|
||||
app: 'cards',
|
||||
version: process.env.CARDS_API_VERSION ?? '0.0.0',
|
||||
uptime_s: Math.floor(process.uptime()),
|
||||
checks: {
|
||||
db: dbProbe,
|
||||
storage: storageProbe,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
allOk ? 200 : 503
|
||||
);
|
||||
});
|
||||
|
||||
healthRoute.get('/version', (c) =>
|
||||
c.json({
|
||||
|
|
@ -25,3 +47,31 @@ healthRoute.get('/version', (c) =>
|
|||
build: process.env.CARDS_BUILD_SHA ?? 'dev',
|
||||
})
|
||||
);
|
||||
|
||||
type ProbeResult = { ok: true; latency_ms: number } | { ok: false; error: string };
|
||||
|
||||
async function probeDb(): Promise<ProbeResult> {
|
||||
const t0 = Date.now();
|
||||
try {
|
||||
const db = getDb();
|
||||
await db.execute(sql`SELECT 1`);
|
||||
return { ok: true, latency_ms: Date.now() - t0 };
|
||||
} catch (err) {
|
||||
return { ok: false, error: errorMessage(err) };
|
||||
}
|
||||
}
|
||||
|
||||
async function probeStorage(): Promise<ProbeResult> {
|
||||
const t0 = Date.now();
|
||||
try {
|
||||
await getStorage().ensureBucket();
|
||||
return { ok: true, latency_ms: Date.now() - t0 };
|
||||
} catch (err) {
|
||||
return { ok: false, error: errorMessage(err) };
|
||||
}
|
||||
}
|
||||
|
||||
function errorMessage(err: unknown): string {
|
||||
if (err instanceof Error) return err.message;
|
||||
return String(err);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue