Written directly, in the same spirit as this project's "Honest limitations" — what is real, what is not, and what was never claimed.
- The webhook route (
/api/webhooks/razorpay) verifies an HMAC signature before anything else touches the request body (verifyWebhookSignature,src/domain/webhooks/verify-signature.ts) — the raw bytes, not a re-serialized copy, checked before any parsing or persistence. This is the real perimeter for the one route that accepts external, untrusted input claiming to be a payment event. - A replay window (
src/domain/webhooks/replay-window.ts) rejects a correctly-signed but stale delivery. - Idempotency is a database UNIQUE constraint in the same transaction as the
write (
webhook_events.event_id), not a lock in a separate store that could disagree with what actually got written — seesrc/ports/kv.ts's own docstring on exactly why that distinction matters. - Exactly one file reads
process.env(src/config/env.ts), enforced by an ESLint boundary rule, so no adapter can read a credential the capability banner doesn't already know about and report honestly. RAZORPAY_KEY_IDstarting withrzp_live_fails to boot. Not a warning — a hard refusal, so this project structurally cannot move real money by accident, in any deployment./api/batchesand/api/simulate, the two unauthenticated public routes that do real work per call, are rate-limited per client IP (5 batches / 5 minutes, 20 simulations / 5 minutes —src/app/rate-limit.ts, verified live: 6 real requests against the running instance, 5 accepted, the 6th a real429). Added after a strict outside review named the absence of this directly.- A pre-commit hook and a CI job both scan every diff for secret-shaped
strings (
scripts/scan-secrets.mjs) before anything reaches the remote.
npm audit was actually run against this exact dependency tree: 0
vulnerabilities, at every severity level, as of the commit this file was
added in. Wired into CI (.github/workflows/ci.yml's audit job, failing the
build on any high/critical finding) so this stays a checked fact rather than a
one-time snapshot that silently goes stale.
- No authentication on any route. The dashboard, the audit ledger, the
queue page, the operator queue, and the batch/simulate endpoints are all open
to anyone who can reach the instance. An auth layer was considered and
deliberately not built for this submission —
docs/SETUP.mdsets out what structurally bounds the damage instead (dry-run by default, a hard refusal of live keys, per-route rate limits, batch replays forced to dry-run) and what genuinely remains exposed. For a hackathon demo instance this is the point — a reviewer should not need credentials to see it work. For any real deployment, this would need a real auth layer before anything else on this list matters. - The operator queue has no authentication, so
assigneeis self-asserted.PATCH /api/escalations/<id>is rate-limited (120/60s per client IP) and its state transitions are safe against concurrency — each is one conditional UPDATE, so two callers cannot both claim one item. What it cannot do is tell you who acted: the assignee is a string the caller types, recorded as'unattributed'when omitted. For a demo instance that is the point, and the audit trail says plainly that nobody put their name to it. For anything real, this is the route that most needs authentication first, because it is the one that writes human-attributed outcomes into a customer's history. - No OWASP-style review, no penetration test.
npm auditis now real and wired into CI (see above), but that covers known dependency CVEs only — it says nothing about this project's own code. - The rate limits above are per-IP via
x-forwarded-for, which a motivated attacker can spoof or route around (a botnet, or simply omitting the header behind a proxy that doesn't set it, falls into one shared bucket). This raises the bar for casual abuse of a demo instance; it is not a defense against a determined one. - The webhook route's own rate limit is deliberately generous, and that is a
real residual. It is rate-limited now (300 requests / 60s per client IP,
app/api/webhooks/razorpay/route.ts), which closes the volumetric-flooding hole that existed when only/api/batchesand/api/simulatewere limited. But 300/60s is set high on purpose — a real burst of legitimate Razorpay deliveries for one merchant must never be dropped — so it stops casual abuse of a public URL, not a determined flood. A flood of correctly signed duplicate deliveries under that ceiling still lands on the database and the connection pool;docs/LOAD_TEST.mdmeasured what that ceiling actually is. /api/healthpublishes less than it used to, and the reason is not flattering. Unauthenticated, it was returning each port'starget— which on a configured instance meant a real Supabase database hostname, a real Upstash hostname, and the Groq model id, to anyone who asked. That is free reconnaissance and no part of what a health endpoint is for. SettingRECLAIM_PUBLIC_INSTANCEnow withholdstarget; adapter names,liveflags, and the human-readablereasonstill come through, so the endpoint still answers the question it exists to answer. Locally the full table is genuinely useful, which is why the flag is opt-in rather than keyed offNODE_ENV— the documented demo path runs a real production build./api/dev/*was named "dev" and gated by nothing. Both routes (audit-count,audit-actions) answered on a production build, unauthenticated, and?eventIds=was an unbounded list fanned into a SQL= ANY($1)— parameterised, so never an injection, but an arbitrarily long array is cheap to send and not cheap to answer. Now:RECLAIM_PUBLIC_INSTANCEturns them off entirely (404, not 403 — a route that is off should not confirm it exists), and the list is capped at 500 on every instance either way (src/app/dev-route-guard.ts,tests/unit/dev-route-guard.test.ts). They still exist because the reason they exist is real:scripts/replay.tsmust not open a second connection to a single-process embedded database.- No secrets manager. Real credentials live in a gitignored
.env, scanned for on every commit, but that is a development-time safeguard, not a production secrets-management story (rotation, access auditing, a vault).
- Real authentication on every route that is not the public webhook endpoint. Still the first thing, and still not done.
- A Python dependency audit (
pip-audit) wired into CI the waynpm auditalready is. The npm half of this list item was done and the item was never updated, which is exactly the kind of stale claim this file should not make; the Python half genuinely is still open. - A real penetration test or at minimum a structured OWASP Top 10 pass, by someone other than the person who wrote the code.
- Moving IP-based limiting to something that survives a spoofed or absent
x-forwarded-forheader (a real reverse proxy or WAF setting that header itself, not trusting a client-supplied one). Rate limiting the webhook route itself was the other half of this item and is now done — see above. - A real secrets manager and a documented rotation policy for the Razorpay webhook secret and API keys.