Route Tenant Coverage
Source of truth:
src/middleware/setup/routeManifest.ts(ROUTE_MANIFEST). This doc is a human-readable companion; the audit gatenpm run audit-tenant-coverageenforces source ↔ manifest consistency.PR-F0: each manifest prefix now also carries a typed
RoutePolicy(auth/lifecycle/rateProfile) insrc/middleware/setup/routePolicy.ts, audited bynpm run audit-route-policy.
Classification semantics
| Classification | Behavior | Use when |
|---|---|---|
public |
No identity required, no tenant filtering | Health/probes, redirect shims, swagger UI |
system |
Elevated identity required (admin/ops); auth still enforced at handler level | Tenant lifecycle admin, ops metrics |
tenant_required |
Central tenantIsolation middleware populates req.tenantContext from verified sources only (Bearer JWT against JWT_SECRET, configured resolveTenant callback, or trustedTenants fast-path). The x-tenant-id header alone does NOT populate it — disableHeaderExtraction: true is a permanent security invariant frozen by npm run audit-tenant-isolation-invariant. |
Any route that operates on tenant-scoped data |
demo |
Bypasses tenant isolation entirely | Demo-mode fallback routes |
PR 4B + PR 2C-Auth + F6 Stage 5 contract
- Strict production mode (today). The production central gate mounts
tenantIsolationwithstrictMode: true, disableHeaderExtraction: true. Atenant_requiredroute without verified tenant context returns403with codeTENANT_REQUIREDbefore its handler runs. The middleware option default remains permissive only as an explicit compatibility seam for isolated harnesses; production must pass strict mode deliberately. disableHeaderExtraction: trueis a permanent security invariant. It means the un-verifiedx-tenant-idheader does NOT populatereq.tenantContext, closing the header-impersonation surface against directreq.tenantContextconsumers (mcpPolicies.ts) AND against theextractIdentityContextreq.tenantContextbridge added in PR 2C-Auth. The flag is frozen bynpm run audit-tenant-isolation-invariant: anytenantIsolation(...)callsite undersrc/that omits or sets itfalsefails CI. The flag should not flip unless an upstream gateway verifies the header — Preston-Test does not have one.- PR 2C-Auth wired the JWT path.
optionalAuthMiddlewareis mounted globally on/api/*ahead of the central gate, so a Bearer JWT againstJWT_SECRETpopulatesreq.user.tenantId. Inside the gate,tenantIsolation's built-in JWT extraction populatesreq.tenantContextfrom the same JWT.extractIdentityContextreadsreq.auth → req.user → req.tenantContextin that order; the bridge (third source) was added in PR 2C-Auth and inherits the verified-source-only invariant above. - Handler identity fallbacks.
extractIdentityContextstill supportsSYSTEM_IDENTITYfor non-request and explicitly permissive harness contexts, but productiontenant_requiredrequests can no longer reach that fallback anonymously. Existing handler-local 401 checks remain defense in depth and preserve their behavior on deferred or locally mounted paths that do not use the production central gate. - Unknown-route safety default.
classifyRoute()returns'system'(NOT'public') for unmatched paths and emits a one-time-per-pathlogger.error, capped at 1024 entries to bound memory/log volume under attacker-driven path enumeration. The CI audit gate is the primary safety net; the noisy runtime default is the backstop.
Identity & audit propagation matrix
Scope of this section. The classification table above answers "is this route tenant-scoped?" (isolation). This section answers a second, orthogonal SOC-2 question: "does the handler attribute the acting identity, and does that actor reach the audit trail?" (attribution). A route can be tenant-isolated at the middleware layer yet still fail to record who performed a write.
Baseline. Every
/api/*route passes throughoptionalAuthMiddleware(global) + the centraltenantIsolationgate, so a verified Bearer JWT makesreq.user.tenantIdavailable everywhere. Attribution is therefore about whether the handler/service consumes that identity (viaextractIdentityContext(req)) and threads it into audit rows — not about whether auth ran.States. ✅ propagates — handler calls
extractIdentityContextand/or attributes the actor in audit. 🔶 tenant-scoped only — handler readsreq.user.tenantId/requireTenantIdfor data scoping but does not attribute the actor per write. ⛔ gap — handler delegates to a service without passing tenant/actor; noextractIdentityContext, no audit actor (middleware still authenticates the caller, so this is an attribution gap, not an open door).Methodology. Per-route-file scan (point-in-time, 2026-05-29) for
extractIdentityContext,SYSTEM_IDENTITY, audit emission (auditService/AuditService/AuditLogRepository/safeAudit), and tenant reads (req.user?.tenantId/requireTenantId/tenantContext). Regenerate with:for f in src/routes/**/*.ts; do grep -cE 'extractIdentityContext|SYSTEM_IDENTITY' "$f"; done(see the grep block in the PR that introduced this section).
| Route family | Actor source | Per-write audit actor | State |
|---|---|---|---|
/api/ai/proxy (+ Agent/BI/MCP/Mapping/Quality sub-routers) |
verified req.user via verifiedUserId/verifiedIdentity (F6 sub-project B; no SYSTEM_IDENTITY fallback — anonymous demo callers are diverted by the F2 gate before identity is read) |
partial (MCP yes) | ✅ |
/api/mcp |
extractIdentityContext + req.user.tenantId |
yes | ✅ |
/api/finance-central |
extractIdentityContext (§21 Kerry C5) |
yes (AuditLogRepository) |
✅ |
/api/workflow-central |
extractIdentityContext (§21 Kerry C5) |
yes (safeAudit) |
✅ |
/api/hubspot |
extractIdentityContext |
yes | ✅ |
/api/governance/approvals |
extractIdentityContext + 401 gate |
yes | ✅ |
/api/cost-transparency |
extractIdentityContext |
n/a (read surface) | ✅ |
/api/nl-action-gate |
extractIdentityContext |
via dispatch service | ✅ |
/api/actions |
extractIdentityContext |
n/a | ✅ |
/api/sync-error-assist |
extractIdentityContext + tenant resolve |
via service | ✅ |
/api/help |
extractIdentityContext |
n/a (read surface) | ✅ |
/api/reconciliation-center |
extractIdentityContext (401 on SYSTEM_IDENTITY) |
yes (resolve records actor) | ✅ |
/api/lineage |
extractIdentityContext (401 on SYSTEM_IDENTITY / missing / synthetic userId) |
n/a (read surface) | ✅ |
/api/configurations, /api/integrations |
requireTenantId / req.user.tenantId |
no | 🔶 |
/api/settings, /api/dashboard, /api/mappings, /api/templates |
req.user.tenantId (Mapping sub-router uses extractIdentityContext) |
no | 🔶 |
/api/compliance |
req.user.permissions (authenticated) |
audit emits, actor not threaded per write | 🔶 |
/api/payment-central (+ processors/invoices/gl/dunning/reconciliation) |
none — getPaymentService() called without tenant/actor |
no | ⛔ |
/api/supplier-central, /api/customer-central, /api/quality-central, /api/payout-central, /api/installer-central, /api/service-central, /api/inventory-central, /api/contract-central, /api/portal-central |
none at handler | no | ⛔ |
/api/sync-central, /api/sync-orchestrator, /api/suitecentral/*, /api/squire/suitecentral/* |
none at handler | no | ⛔ |
Genuine gaps (next increment)
The ⛔ rows are the live attribution gaps. They are tenant-authenticated (the central gate runs) but their handlers delegate to operator/sync services without passing extractIdentityContext(req) or writing an actor-bearing audit row — so a write through these surfaces is not attributable to a user. FinanceCentral and WorkflowCentral already closed this (PR §21 / Kerry C5); the remaining central-operator families should replicate that pattern (thread extractIdentityContext → service → audit row). This is deliberately not bundled here: each family touches a distinct operator service and is the same magnitude as the FC/WC work, so wiring them belongs in scoped per-family PRs, tracked against this matrix. Closing them is what lets a SOC-2 reviewer answer "is multi-tenant audit complete?" with yes.
Public
| Path | Notes |
|---|---|
/health |
liveness probe |
/ready |
readiness probe |
/api/metrics |
Prometheus scrape; gated by METRICS_SCRAPE_TOKEN since PR #952 (timing-safe Bearer; unset → 403 unless NODE_ENV is explicitly development/test or HOSTED_DEMO — tenant IDs + per-tenant AI cost appear as metric labels). /api/metrics/review (reviewer-evidence JSON) stays open |
/api/ai |
PR 1B 301 redirect shim → /api/ai/proxy |
/api/download |
static downloads |
/docs |
docs router |
/api-docs |
swagger UI |
/api/connector-metadata |
sub-route of bare /api mount (connectorCredentialRouter); global connector catalog, no auth |
System
| Path | Notes |
|---|---|
/api/admin/tenants |
tenant lifecycle admin |
/api/admin/tenants/:tenantId/suitecentral |
PR-A6: platform-admin SuiteCentral control plane for a named tenant; authMiddleware + requirePlatformAdmin. Tenant comes from the path, deliberately NOT tenant_required (the central gate must not re-scope it to the admin's own tenant) |
/api/admin/suitecentral/allowed-hosts |
PR-A6: platform-global SuiteCentral egress allowlist; authMiddleware + requirePlatformAdmin. Not tenant-scoped — no tenant is accepted or read |
/metrics |
gated by ENABLE_METRICS + authMiddleware |
/api/disaster-recovery |
ops-only |
/api/disaster-recovery/dashboard |
ops-only |
/api/statistics |
single-endpoint diagnostic mounted in src/index.ts behind route-level REQUIRED authMiddleware (PR #952 — it returns cross-tenant aggregate config metadata); reads global configService state, not tenant-scoped |
/api/full-pipeline-demo |
platform-admin ERP write family; authMiddleware + requirePlatformAdmin + shared erp-write limiter via mountFullPipelineDemoRoutes. POST /execute additionally requires tenantId + user.id claims |
Demo
| Path | Notes |
|---|---|
/api/ai-demo |
|
/api/data-migration |
demo migration playground |
Tenant-required
| Path | Notes |
|---|---|
/api/ai/proxy |
AI provider proxy; governance + tenant scoping |
/api/settings |
|
/api/mcp |
|
/api/mappings |
|
/api/mappings/templates |
|
/api/templates |
|
/api/dashboard |
|
/api/dashboard/api/mappings |
legacy double-/api/ prefix; mirrors RouteSetup.ts mount |
/api/dashboard/mappings |
|
/api/dashboard/mappings/templates |
|
/api/dashboard/templates |
|
/api/integrations |
|
/api/upload |
|
/api/testing |
|
/api/fixtures |
|
/api/baselines |
|
/api/persistence |
|
/api/predictive-analytics |
|
/api/executive |
|
/api/agents |
|
/api/context |
|
/api/embedded/host-bootstrap |
|
/api/embedded/context |
|
/api/embedded/sessions |
|
/api/governance/approvals |
HITL queue; Codex-5.4 401 gate inside router |
/api/actions |
|
/api/documents |
|
/api/feature-flags |
|
/api/roi-dashboard |
|
/api/suitecentral/sync |
|
/api/suitecentral/netsuite/sync |
|
/api/squire/suitecentral/netsuite/sync |
|
/api/suitecentral/prod |
PR-A6: tenant-admin SuiteCentral control plane (replaced the deleted legacy router); authMiddleware + requireSuiteCentralTenantAdmin; target tenant from the verified JWT claim only |
/api/payment-central |
centralAuthMiddleware + tenantStatusGate already in place |
/api/supplier-central |
|
/api/customer-central |
|
/api/quality-central |
|
/api/payout-central |
|
/api/installer-central |
|
/api/service-central |
|
/api/inventory-central |
|
/api/finance-central |
|
/api/contract-central |
|
/api/portal-central |
|
/api/workflow-central |
|
/api/reconciliation-center |
PR 11 durable exception queue; 401 when extractIdentityContext resolves to SYSTEM_IDENTITY |
/api/lineage |
PR 12 record-level lineage; 401 on SYSTEM_IDENTITY / missing / synthetic operator userId |
/api/shipstation |
|
/api/hubspot |
|
/api/sync-central |
tenantStatusGate wired (kill switch; identity via global optionalAuthMiddleware) |
/api/sync-orchestrator |
tenantStatusGate wired (kill switch; identity via global optionalAuthMiddleware) |
/api/automation-libraries |
tenantStatusGate wired (kill switch; identity via global optionalAuthMiddleware) |
/api/nl-action-gate |
|
/api/mdm |
|
/api/compliance |
|
/api/sync-error-assist |
pathless mount via syncErrorAssistRoutes; routes defined as absolute /api/sync-error-assist/* inside the router |
/api/cost-transparency |
cost dashboard + anomaly; /api/cost-transparency/health is a static unauthenticated probe |
/api/ai-config |
pathless mount via aiConfigRouter; routes defined as absolute /api/ai-config/* inside the router |
/api/help |
mounted in src/index.ts; help.ts reads identity via extractIdentityContext |
/api/connector-credentials |
sub-route of bare /api mount (connectorCredentialRouter); per-tenant credentials, requireAuth |
/api/test-connection |
sub-route of bare /api mount (connectorTestRouter); tests tenant connector credentials |
/api/configurations |
sub-route of root / mount (configurationRouter); integration configuration CRUD |
/api/enterprise |
sub-route of root / mount (enterpriseFeaturesRouter); /api/enterprise/* surface incl. activity, approvals, golden-set, governance |