Unauthenticated (@Public()) endpoints β
Every route that opts out of the global AppGuard is listed here with the reason it is allowed to, and the control that stands in for authentication. A route not on this list must not carry @Public().
This page is the SOC 2 artifact for CC6.1 / CC6.6 / CC6.7: an auditor can re-check it against grep -rn "@Public()" apps/agri-backend/src without reading the code. Last full sweep: 1 Sep 2026 (ENG-1403).
Rules β
@Public()at class level makes every route in the controller public. Prefer method level β class-level decorators are how most of the ENG-1403 findings happened.- A route that a machine calls is not a reason to use
@Public(). Use@ApiKeyAllowed()+ApiKeyGuard(x-api-key, routed byAppGuard), or an HMAC signature guard likeVapiSignatureGuard. - Adding a
@Public()route means adding a row here in the same PR. - Route paths that still read
public/...while being authenticated are kept only because installed mobile builds and cached PWA bundles pin bare paths (see API versioning). The path is not the control.
Justified public routes β
| Route | Why public | Control |
|---|---|---|
GET / (app.controller.ts) | liveness ping | returns a constant |
GET /health, GET /health/ready | platform probes | no data returned |
POST /auth/verify-token, /session-login, /session-verify | pre-authentication by definition | Firebase token/cookie verification is the check |
POST /auth/register | signup | RateLimitGuard; gated further by ENG-1400 |
POST /impersonate/token | the operator's browser holds no session yet | operator link TTL 300s and single-use (jti consumed in Redis); the 30-day demo token is reusable by design and re-checked by assertDemoOnlyTarget on every exchange |
GET /public/reports/:shareToken | share-by-link | randomUUID() token (122 bits), unique sparse index; findByShareToken requires isPubliclyShared: true, so unsharing revokes at once. No expiry β accepted risk |
POST /account/cancel-deletion-token | emailed link, the user may be signed out | Zod-validated token with a service-side expiry |
POST /signup-invites/preview | invite landing page renders before signup | RateLimitGuard, token-scoped |
GET /public/legal, GET /public/legal/:slug | published legal documents | static content |
GET + POST /webhooks/messaging/whatsapp | Meta calls it | signature verification; mismatch returns invalid_signature |
POST /vapi/server-setting, POST /vapi/tools/* | Vapi calls it | VapiSignatureGuard (HMAC) |
POST /integrations/fieldx/login, /register, /merge | partner pre-authentication | RateLimitGuard |
GET /companies/:id/public | company name on the login / invite screen | .select('_id name') β nothing else can leave |
Known exception, tracked β
| Route | Status |
|---|---|
GET /public/attachments/:id | @Public() with no tenancy check. Owned by ENG-664, not closed here |
Accepted residual risks β
GET /phone-numbers/findAllis authenticated but not company-scoped.PhoneNumberhas nocompanyId; numbers are a platform-wide pool allocated by country (CompanyService.findPhoneIdForLanguage), and the route exists to render a picker over the unallocated pool. Any authenticated user can therefore enumerate every number. Every phone-number mutation is super-admin (AdminGuard). GivingPhoneNumberan owner plus a provisioning flow is a separate ticket.- Report share tokens never expire. Revocation works (unshare), entropy is fine; only the unbounded lifetime is outstanding.
- There is no HTTP access log. The backend registers no
nestjs-pinoautoLogging or request middleware, so Loki cannot answer "is this route being called?" β during ENG-1403 that had to be inferred from downstream signals (queue activity, repo-wide grep). SOC 2 CC7.2 wants request-level monitoring; tracked separately.
Closed by ENG-1403 (1 Sep 2026) β
Deleted β no callers in any client, and no production traffic:
POST /public/parse-callβ superseded by the internalCallingServicepathPOST /public/process-audioβ unmetered transcription spend per anonymous requestPOST /call-logs,PUT /call-logs/:idβ zero jobs on thecall-log.receivequeue in the 30 days to 1 Sep 2026 (that route was its only producer)GET /call-logs/by-call-sid/:callSidβ returned a transcript to anyone holding a call SID, a value that appears in Vapi, Twilio and our own logsPOST /public/push/testβ unauthenticatedsendToAllwith a caller-supplied title, body and URL: a phishing channel carrying our own branding
Authenticated:
POST /public/push/subscribe,/unsubscribeβ theuserIdnow comes from the token, never the body, and a caller may only drop their own endpointPOST /public/process-voice-noteβuserId/companyIdin the body are overwritten from the tokenGET /phone-numbers/*β reads authenticated, mutations super-adminGET /leaderboardβ@Public(), andgetLeaderboardStats()returned the raw user document for every user of every tenant (email, phone,firebaseUid,memberships,platformRoles). Not exploitable today:LeaderboardModuleis commented out ofAppModule(ENG-402), so the route is not mounted β production boot logs never map/leaderboard. Hardened anyway, so re-enabling the module cannot reintroduce the leak: authenticated,@CompanyRequired(), scoped to the caller's company before aggregation, and projected to{_id, firstName, lastName, photoURL}byLeaderboardEntrySchema
Regression coverage: apps/agri-backend/src/public-endpoints.authz.integration.spec.ts (18 route assertions) and apps/agri-backend/src/leaderboard/leaderboard.schema.spec.ts (the projection, asserted at the schema because the route is not mounted).