A live compliance audit against https://0pt.app confirmed these routes responded HTTP 200 with zero authentication:
GET /api/orders -> full order queue
GET /api/admin/metrics -> financial pipeline aggregates (deposit/pipeline totals)
middleware.ts's matcher only covers /api/0pt/:path* by design (see its own header comment: "does NOT touch ... the unrelated payment/order APIs"), so nothing upstream of these two handlers ever verified the caller.
Fix
Both GET handlers now call the same fail-closed Zitadel verifier/api/0pt/* already relies on (resolveAuthContext, src/auth/context.ts), via a new thin wrapper src/auth/require-route-auth.ts that maps a rejected decision to the identical 401/403 JSON shape middleware.ts returns.
The wrapper re-verifies the bearer token itself rather than trusting any x-0pt-verified-* header on the incoming request -- middleware never runs in front of these routes to overwrite a client-supplied one, so trusting it would be a trivial bypass.
POST /api/orders (anonymous draft/quote creation before the deposit-payment step) is left as-is intentionally -- only the two read paths that leaked data are gated, per the audit.
Files changed
app/api/orders/route.ts -- GET now calls requireRouteAuth
app/api/admin/metrics/route.ts -- GET now calls requireRouteAuth
src/auth/require-route-auth.ts (new) -- thin Next.js wrapper around resolveAuthContext
pnpm run typecheck (blocking CI gate): clean, 0 errors
pnpm run test (blocking CI gate): 134 passed, 1 pre-existing skip (unrelated live-infra test), 0 failed -- includes the 6 new tests
pnpm run build (blocking CI gate): succeeds, /api/orders and /api/admin/metrics both compile as dynamic (ƒ) routes
pnpm run lint (advisory, non-blocking per .gitea/workflows/ci.yml): pre-existing errors only, none in the files this PR touches
Context / known adjacent issues (not fixed here, out of scope)
docs/compliance/BACKLOG.md already tracks this as P0-3 ("the whole non-/api/0pt API lacks auth": orders/payments/admin/subscriptions/uploads/estimates/screen/ai). This PR is a scoped, partial fix -- payments/subscriptions/uploads/estimates/screen/ai remain open.
The same backlog also tracks P0-1: middleware reportedly does not run at all in the live Vercel production build (root cause not yet found), which means even the already-gated /api/0pt/* routes were observed answering anonymously live. This PR fixes the code-level gap in this repo; it does not change what's actually deployed at 0pt.app today.
app/api/orders/[id]/route.ts (GET + PATCH status transition) has the same missing-auth pattern and was not touched here to keep this diff surgical -- flagging as a likely follow-up.
Does not touch docs/compliance/*, services/scrapers/*, or the evasion-stack files (separate, already-escalated decision).
## Summary
A live compliance audit against `https://0pt.app` confirmed these routes responded `HTTP 200` with **zero authentication**:
- `GET /api/orders` -> full order queue
- `GET /api/admin/metrics` -> financial pipeline aggregates (deposit/pipeline totals)
`middleware.ts`'s matcher only covers `/api/0pt/:path*` by design (see its own header comment: "does NOT touch ... the unrelated payment/order APIs"), so nothing upstream of these two handlers ever verified the caller.
## Fix
Both `GET` handlers now call the **same fail-closed Zitadel verifier** `/api/0pt/*` already relies on (`resolveAuthContext`, `src/auth/context.ts`), via a new thin wrapper `src/auth/require-route-auth.ts` that maps a rejected decision to the identical 401/403 JSON shape `middleware.ts` returns.
The wrapper **re-verifies the bearer token itself** rather than trusting any `x-0pt-verified-*` header on the incoming request -- middleware never runs in front of these routes to overwrite a client-supplied one, so trusting it would be a trivial bypass.
`POST /api/orders` (anonymous draft/quote creation before the deposit-payment step) is left as-is intentionally -- only the two read paths that leaked data are gated, per the audit.
## Files changed
- `app/api/orders/route.ts` -- `GET` now calls `requireRouteAuth`
- `app/api/admin/metrics/route.ts` -- `GET` now calls `requireRouteAuth`
- `src/auth/require-route-auth.ts` (new) -- thin Next.js wrapper around `resolveAuthContext`
- `tests/api/anonymous-routes-auth.test.ts` (new) -- 6 regression tests, incl. spoofed-header and dev-escape-hatch cases
## Verification
- `pnpm run typecheck` (blocking CI gate): clean, 0 errors
- `pnpm run test` (blocking CI gate): 134 passed, 1 pre-existing skip (unrelated live-infra test), 0 failed -- includes the 6 new tests
- `pnpm run build` (blocking CI gate): succeeds, `/api/orders` and `/api/admin/metrics` both compile as dynamic (ƒ) routes
- `pnpm run lint` (advisory, non-blocking per `.gitea/workflows/ci.yml`): pre-existing errors only, none in the files this PR touches
## Context / known adjacent issues (not fixed here, out of scope)
- `docs/compliance/BACKLOG.md` already tracks this as **P0-3** ("the whole non-`/api/0pt` API lacks auth": orders/payments/admin/subscriptions/uploads/estimates/screen/ai). This PR is a scoped, partial fix -- payments/subscriptions/uploads/estimates/screen/ai remain open.
- The same backlog also tracks **P0-1**: middleware reportedly does not run at all in the live Vercel production build (root cause not yet found), which means even the already-gated `/api/0pt/*` routes were observed answering anonymously live. This PR fixes the code-level gap in this repo; it does not change what's actually deployed at `0pt.app` today.
- `app/api/orders/[id]/route.ts` (`GET` + `PATCH` status transition) has the same missing-auth pattern and was **not** touched here to keep this diff surgical -- flagging as a likely follow-up.
Does not touch `docs/compliance/*`, `services/scrapers/*`, or the evasion-stack files (separate, already-escalated decision).
A live compliance audit against 0pt.app confirmed both endpoints returned
HTTP 200 with zero authentication -- the full order queue and financial
pipeline aggregates (deposit/pipeline totals) were readable by anyone.
Root cause: middleware.ts's matcher only covers /api/0pt/:path*, by design
("does NOT touch ... the unrelated payment/order APIs"), so nothing upstream
of these two handlers ever verified the caller.
Fix: both GET handlers now call the same fail-closed Zitadel verifier
/api/0pt/* relies on (resolveAuthContext, src/auth/context.ts) directly, via
a new thin wrapper (src/auth/require-route-auth.ts) that maps a rejected
decision to the same 401/403 JSON shape middleware.ts already returns. The
wrapper re-verifies the bearer token itself rather than trusting any
x-0pt-verified-* header on the request, since middleware never runs in front
of these routes to overwrite a client-supplied one.
POST /api/orders (anonymous draft/quote creation before the deposit-payment
step) is intentionally left as-is -- only the read paths that leaked data are
gated. Tracked as part of docs/compliance/BACKLOG.md P0-3.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
admin
merged commit into main2026-08-11 09:35:48 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
A live compliance audit against
https://0pt.appconfirmed these routes respondedHTTP 200with zero authentication:GET /api/orders-> full order queueGET /api/admin/metrics-> financial pipeline aggregates (deposit/pipeline totals)middleware.ts's matcher only covers/api/0pt/:path*by design (see its own header comment: "does NOT touch ... the unrelated payment/order APIs"), so nothing upstream of these two handlers ever verified the caller.Fix
Both
GEThandlers now call the same fail-closed Zitadel verifier/api/0pt/*already relies on (resolveAuthContext,src/auth/context.ts), via a new thin wrappersrc/auth/require-route-auth.tsthat maps a rejected decision to the identical 401/403 JSON shapemiddleware.tsreturns.The wrapper re-verifies the bearer token itself rather than trusting any
x-0pt-verified-*header on the incoming request -- middleware never runs in front of these routes to overwrite a client-supplied one, so trusting it would be a trivial bypass.POST /api/orders(anonymous draft/quote creation before the deposit-payment step) is left as-is intentionally -- only the two read paths that leaked data are gated, per the audit.Files changed
app/api/orders/route.ts--GETnow callsrequireRouteAuthapp/api/admin/metrics/route.ts--GETnow callsrequireRouteAuthsrc/auth/require-route-auth.ts(new) -- thin Next.js wrapper aroundresolveAuthContexttests/api/anonymous-routes-auth.test.ts(new) -- 6 regression tests, incl. spoofed-header and dev-escape-hatch casesVerification
pnpm run typecheck(blocking CI gate): clean, 0 errorspnpm run test(blocking CI gate): 134 passed, 1 pre-existing skip (unrelated live-infra test), 0 failed -- includes the 6 new testspnpm run build(blocking CI gate): succeeds,/api/ordersand/api/admin/metricsboth compile as dynamic (ƒ) routespnpm run lint(advisory, non-blocking per.gitea/workflows/ci.yml): pre-existing errors only, none in the files this PR touchesContext / known adjacent issues (not fixed here, out of scope)
docs/compliance/BACKLOG.mdalready tracks this as P0-3 ("the whole non-/api/0ptAPI lacks auth": orders/payments/admin/subscriptions/uploads/estimates/screen/ai). This PR is a scoped, partial fix -- payments/subscriptions/uploads/estimates/screen/ai remain open./api/0pt/*routes were observed answering anonymously live. This PR fixes the code-level gap in this repo; it does not change what's actually deployed at0pt.apptoday.app/api/orders/[id]/route.ts(GET+PATCHstatus transition) has the same missing-auth pattern and was not touched here to keep this diff surgical -- flagging as a likely follow-up.Does not touch
docs/compliance/*,services/scrapers/*, or the evasion-stack files (separate, already-escalated decision).A live compliance audit against 0pt.app confirmed both endpoints returned HTTP 200 with zero authentication -- the full order queue and financial pipeline aggregates (deposit/pipeline totals) were readable by anyone. Root cause: middleware.ts's matcher only covers /api/0pt/:path*, by design ("does NOT touch ... the unrelated payment/order APIs"), so nothing upstream of these two handlers ever verified the caller. Fix: both GET handlers now call the same fail-closed Zitadel verifier /api/0pt/* relies on (resolveAuthContext, src/auth/context.ts) directly, via a new thin wrapper (src/auth/require-route-auth.ts) that maps a rejected decision to the same 401/403 JSON shape middleware.ts already returns. The wrapper re-verifies the bearer token itself rather than trusting any x-0pt-verified-* header on the request, since middleware never runs in front of these routes to overwrite a client-supplied one. POST /api/orders (anonymous draft/quote creation before the deposit-payment step) is intentionally left as-is -- only the read paths that leaked data are gated. Tracked as part of docs/compliance/BACKLOG.md P0-3. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>