# nexuscrm-api-host
> Transport primitives for NexusCRM API hosts and dialect route composition.

Part of the **NexusCRM platform**, split by release boundary into L0 platform repos + L1 product packs + L2 tenant delivery. This repository is an **L0 platform transport module**.

## What this is
This is the transport concern of the platform — a kernel-free transport substrate. It owns route registration and dialect route composition, middleware contracts, request-parameter parsing, response/error projection, websocket primitives, origin/CORS configuration, dialect transport hooks, system version discovery, per-IP rate limiting, trusted internal bearer-token auth, security headers, and the independently runnable app-blind Gateway edge. It is HTTP/websocket plumbing with no business logic of its own.

## Role in the L0 / L1 / L2 architecture
This is an L0 concern module: platform **mechanism** for serving and composing transport, not engine services, persistence, or product behavior. Kernel services, authorization decisions, and product dialect behavior remain outside this repo and are injected through explicit ports. It is the thin transport layer; everything above it provides the actual behavior it exposes.

## What belongs here
- Route registration and dialect route composition
- Middleware contracts and generic HTTP middleware (rate limit, trusted internal bearer token, security headers)
- Request-parameter parsing
- Response / error projection
- Websocket transport primitives
- Origin / CORS and redirect-URI configuration
- Dialect transport hooks
- System version discovery for mounted dialects
- Runtime gRPC server and client envelope, compatibility, deadline, fail-fast
  admission, drain, semantic mapping, and injected-authority interception

## What does NOT belong here
- Engine services
- Persistence
- Controlplane lifecycle
- Backend service locator / tenant boot
- Salesforce or any product business behavior

## Public surface (key packages)
- `dialect` — dialect transport-binding contract
- `dialecttransport` — gin-free dialect route binder and registry
- `middleware` — authenticated middleware, internal-token auth, rate limiting, security headers
- `hostruntime` — host-runtime protocol types (auth, permissions, request lineage, websocket session)
- `requestparams` — HTTP request-parameter parsing helpers
- `websocket` — websocket transport handler
- `origin` — browser origin and redirect-URI runtime configuration
- `system` — system version discovery routes for mounted dialects
- `runtimegrpc` — app-blind Runtime gRPC host policy and TLS-only Identity +
  capability clients used by independently deployed L1 services
- `processhttp` — request/response-only process liveness, readiness, version,
  capability discovery, HTTP listener lifecycle, drain ordering, and
  transport-complete shutdown; upgraded/hijacked sessions require a distinct
  lifecycle owner
- `gateway` — immutable exact-route edge proxy, coarse admission, deadlines,
  correlation, metrics, TLS lifecycle, and fail-closed upstream transport
- `cmd/gateway` — independently runnable generic Gateway process

## Dependency rules
- May depend on: `nexuscrm-contract` (optional) plus transport libraries (gin, gorilla/websocket)
- Must NOT depend on: `backend`, the engine modules, or `controlplane`
- A Runtime server using `runtimegrpc` must keep every gRPC compressor package
  and every `encoding.RegisterCompressor` call out of its production command
  dependency closure. gRPC selects/decompresses registered request encodings
  before a unary interceptor can reject them; the host policy rejects the
  header and prevents handler-selected response compression, while the command
  closure is the authoritative structural guard for request decompression.
- Identity exchange is opt-in through `runtimegrpc.NewPolicyWithIdentity` and
  requires an explicit host attempt cap. Its end-user bearer uses the dedicated
  sensitive `x-nexuscrm-end-user-authorization` metadata key; the standard
  `authorization` key remains owned by workload authentication. The sensitive
  key is removed before Authority and handler metadata and never enters the
  semantic protobuf.
- The private Catalog/Platform surface is opt-in per neutral capability through
  `runtimegrpc.WithPlatformCapabilityMaxAdmissionLease`. The released method
  registry assigns every RPC one closed method class (identity exchange,
  delegated user data, or workload consumer) and one operational resource
  class (short unary, heavy work, file stream, or Catalog watch). The injected
  Authority receives those host-selected facts through context; this package
  does not decide workload grants, AppInstallation scope, or SecurityEpoch.
  Workload-consumer and identity methods reject Runtime delegation metadata;
  all non-identity methods reject the reserved end-user bearer key.
- Enabling a long-lived or expensive Platform capability requires an explicit
  isolated pool through `runtimegrpc.WithResourceClassAdmissionLimit`.
  Existing methods continue sharing the released aggregate pool unless the
  host opts into a resource-class pool. `AdmissionSnapshot` remains the
  aggregate compatibility view, while `ResourceAdmissionSnapshot` exposes one
  effective pool.
- `runtimegrpc.RuntimePlatformClient` uses one TLS-only private target for the
  Catalog, Search, Flow, Report, Files, Command, ApplicationResult,
  Permissions, and Approval services. Catalog, Command, and ApplicationResult
  calls use workload authority only; user-data capabilities exchange an
  end-user bearer for one least-privilege delegation before a fresh data
  attempt. ApplicationResult reads preserve the exact Command ID and
  ExecutionRef digest semantic correlation and never infer a terminal result
  from transport failure. Permission Authoring exposes one delegated Snapshot,
  atomic five-family Save, and request-bound Status path; reconciliation keeps
  the original Save intent so a terminal for another aggregate or revision is
  rejected. Reserved credentials are sanitized per method class.
  Catalog watches and Files streams expose explicit `Recv`, `Send`,
  `CloseAndRecv`, and concurrent `Cancel` ownership, validate the released
  semantic state machines, and never hide an arbitrary body reader or consumer
  callback behind an RPC attempt.
- Record-access is opt-in through `runtimegrpc.NewPolicyWithOptions` and
  `runtimegrpc.WithRecordAccessMaxAdmissionLease`. The value is an operational
  admission lease, not a semantic deadline or production SLO: an admitted call
  keeps its caller-owned context unchanged, while an over-lease request is
  fail-fast `RESOURCE_EXHAUSTED` before Authority or handler work.
- `runtimegrpc.RuntimeRecordAccessClient` requires an explicit
  `RuntimeClientConfig.AttemptTimeout` and derives a fresh child deadline for
  each Identity and Records RPC. The parent context continues to own the full
  L1 route budget; callers must configure the per-RPC timeout within the
  Runtime host's operational record-access admission lease.
- Related-record counts are separately opt-in through
  `runtimegrpc.WithRelatedCountMaxAdmissionLease`; the generated client sends
  the complete bounded batch in one Runtime RPC. Product route grammar and
  count response projection remain outside this L0 transport package.
- Recycle-bin listing is separately opt-in through
  `runtimegrpc.WithRecycleBinMaxAdmissionLease`; the generated client exchanges
  verified identity and sends one bounded neutral listing RPC. Product route
  aliases and restore/purge mutations remain outside this transport package.

## How it is consumed
```go
require github.com/pingcap-inc/nexuscrm-api-host v0.2.0
```
Consumed by the backend to host API routes and compose dialect transports.

### Refreshed bearer response boundary

Hosts that opt into `hostruntime.SessionRenewingAuthenticator` must install the
response guard as their outermost HTTP handler so Gin's explicit and implicit
response commits cannot be changed by later middleware:

```go
server := &http.Server{
	Handler: middleware.ProtectRefreshedTokenResponses(router),
}
```

The auth middleware falls back to legacy validation when the guard is absent or
the response has already committed. Routes that hijack the connection must put
`middleware.WithoutSessionRenewal()` before `RequireAuth` or `OptionalAuth`;
request `Connection` and `Upgrade` headers alone never disable renewal.

### Strong Permission State admission

The Permission State path is additive and inactive until a host explicitly
constructs it. Existing `SessionAuthenticator`, `PermissionProvider`,
`RequireAuth`, `OptionalAuth`, `PermissionLoader`, and `PermissionCache`
consumers keep their current behavior.

A strong host supplies both
`hostruntime.AuthorizingSessionAdmitter` and
`hostruntime.EffectivePermissionSnapshotProvider`. Admission returns the
current authoritative user/profile `Subject` plus a minimum Permission State
version. The provider returns one immutable aggregate of system and custom
permissions for that subject. Object and field permissions remain outside this
bounded aggregate.

```go
cache, err := middleware.NewPermissionStateCache(
	middleware.DefaultPermissionStateCacheTTL,
)
if err != nil {
	return err
}
strongAuth, err := middleware.NewAuthorizingSessionMiddleware(
	middleware.AuthorizingSessionMiddlewareOptions{
		Admitter:          authorizingSessions,
		SnapshotProvider:  effectiveSnapshots,
		SnapshotCache:     cache,
		SessionWriter:     sessionWriter,
		PermissionContext: permissionContext,
		RequestContext:    requestContext,
		FailureResponder:  failureResponder,
	},
)
if err != nil {
	return err
}
router.Use(strongAuth.RequireAuth())
```

Construction fails when either strong capability, the configured cache, or a
required context writer is missing. There is intentionally no adapter from the
legacy two-call `PermissionProvider`, no type assertion from strong middleware
to legacy auth, and no fallback after strong admission begins. A snapshot must
be in the admitted floor's lineage and at or above its revision before the
session is installed.

`OptionalAuth` continues anonymously for a missing/malformed bearer or the
explicit `hostruntime.ErrAuthorizingCredentialRejected` result. Every other
error after a bearer reaches admission—including an unknown adapter error,
unavailable authority, invalid floor, stale snapshot, or incomparable
lineage—aborts with `503 AUTHORITY_UNAVAILABLE`.

The cache coalesces same-subject loads and publishes versions monotonically.
TTL expiry cannot publish a lower revision. A new global lineage clears all
subject entries, fences older in-flight loads, and retires the old lineage for
the process lifetime. The complete subject/floor/snapshot tuple is available
through `hostruntime.AuthorizingPermissionStateFromContext`.

### Strong WebSocket reauthorization

An HTTP admission at WebSocket upgrade time is not authority for later
subscription or delivery decisions. Hosts can opt into the additive
`NewAuthorizingWebSocketHandler` constructor with a
`hostruntime.WebSocketAuthorizationLeaseFactory`.

The factory receives the already admitted bearer and initial Permission State
once, before upgrade. It returns a connection-scoped lease that re-establishes
current credential, subject and Permission State authority before every
subscribe, unsubscribe, disconnect and matching-channel delivery. The factory
must never log the bearer and should retain only the minimum opaque credential
handle required for later strong re-admission.

```go
handler, err := websocket.NewAuthorizingWebSocketHandler(
	websocket.AuthorizingWebSocketHandlerOptions{
		SessionManager:         sessions,
		FeatureGate:            features,
		AuthorizationFactory:   websocketAuthorization,
		ReauthorizationTimeout: 5 * time.Second,
		WriteTimeout:           5 * time.Second,
	},
)
if err != nil {
	return err
}
```

Each lease result must be a complete `AuthorizingPermissionState` for the same
user and must not regress the connection's last admitted version. A changed
lineage, changed user, stale/partial state, rejected credential, authority
failure or timeout closes the connection before the protected mutation or
delivery. Gate acquisition, lease opening and reauthorization have host-enforced
return boundaries even if an implementation ignores cancellation; implementations
must still honor cancellation to release their own resources. Protected writes
use the configured write deadline, and connection cleanup never waits for an
authorized action before closing the underlying socket. Credential rejection
uses WebSocket policy-violation close code 1008; other authority failures use
internal-error close code 1011. The legacy
`NewWebSocketHandler` constructor remains unchanged for source compatibility
and does not claim strong long-lived authorization. A credential rejected while
the lease is being opened returns HTTP 401 before upgrade; every other open
failure returns HTTP 503.

### Browser session-control proof

First-party browser hosts can add an independent, session-scoped control proof
for safe lost-response recovery and logout. The transport keeps the proof
opaque: the consumer generates it, stores only the durable verifier it needs,
and validates it through `hostruntime.SessionControlAuthenticator`.

Install admission before authenticated, login, or revoke routes, using only the
static first-party frontend origins. Do not pass the broader CORS or OAuth
Connected App allowlist:

```go
sessionControl, err := middleware.NewSessionControlAdmission("https://app.example.com")
if err != nil {
	return err
}
router.Use(sessionControl)

server := &http.Server{
	Handler: middleware.ProtectSessionCredentialResponses(router),
}
```

At interactive login, persist a verifier for a cryptographically random opaque
proof and issue the proof with
`middleware.IssueSessionControlCookie(c, proof, durableExpiry)`. The fixed
cookie name is `__Host-nexuscrm-session-control`; api-host always emits it as
host-only, `Secure`, `HttpOnly`, `SameSite=Strict`, and `Path=/`, with matching
`Expires` and `Max-Age`. The proof stays stable across bearer renewals. A
control-aware authenticator that validates the proof and renews or recovers the
session returns the new durable expiry in
`hostruntime.SessionControlValidation.ProofCookieExpiresAt`; the protected
response boundary then refreshes that same cookie. Missing or invalid proofs,
strict current-bearer validation, and non-renewing responses must return a zero
expiry and cannot refresh it.

`middleware.SessionControlRequestAdmitted(c.Request.Context())` identifies an
initial cookie-less login only after the request passes the first-party browser
checks. `IssueSessionControlCookie` rejects requests without that marker.

Browser requests that intend to use the ambient proof send exactly:

```text
X-NexusCRM-Session-Control-Intent: use
```

Admission also requires `Sec-Fetch-Site: same-origin` or `same-site`, rejects
duplicate intent/source/cookie values, and validates every supplied `Origin`
or `Referer` against the configured first-party origins. Unsafe methods require
an exact allowed `Origin`; `same-site` cross-origin requests require it for all
methods. A same-origin safe request may omit `Origin` and `Referer` because its
fetch metadata independently establishes the browser relationship. Malformed,
quoted, shorter-than-32-byte, or larger-than-256-byte proofs are withheld. A
request that supplies valid first-party intent and source evidence but an
invalid or ambiguous proof fails authentication before control-aware, strict,
or legacy-renewal validation.

`middleware.SessionControlProof(c.Request.Context())` exposes only an admitted
proof to a login/revoke adapter. Clear it with
`middleware.ClearSessionControlCookie(c)` only after durable revocation
succeeds. `RequireAuth` and `OptionalAuth` prefer
`hostruntime.SessionControlAuthenticator` when renewal is transport-safe and
session-control admission succeeds. An admitted request without a proof cookie
calls the control-aware seam with an empty proof and cannot fall back to legacy
renewal. Requests that do not opt into session-control admission, and consumers
that do not implement the control-aware interface, retain
`SessionRenewingAuthenticator` behavior, while
`WithoutSessionRenewal()` continues to use strict current-bearer validation.

The cookie is a second browser-held bearer, not copy-resistant proof of
possession. Its value must never be logged or accepted from a request header,
and its authenticator must reject an empty, unknown, or mismatched proof without
returning a replacement bearer or proof-cookie expiry. Rejecting the proof as
renewal/recovery authority does not require rejecting a still-current bearer:
the authenticator may return strict current-bearer claims with both replacement
fields empty. External OAuth sessions should not receive the cookie or opt into
the intent header. Cross-origin deployments must explicitly allow the fixed
intent header in their first-party CORS policy.
Loopback development origins (`http://localhost` or `http://127.0.0.1`) are
accepted by admission, but the cookie remains `Secure`; local browser tooling
must use a browser that treats loopback as a trustworthy context or terminate
local HTTPS rather than weakening production cookie attributes.

The response guard reserves both `X-Refreshed-Token` and the control cookie,
removes downstream attempts to overwrite either credential, preserves unrelated
cookies, and commits credential-bearing responses with `Cache-Control: no-store`.
`ProtectRefreshedTokenResponses` remains a compatibility alias for
existing hosts.

### Generic Gateway process

`cmd/gateway` loads one versioned JSON route artifact at startup and never
reloads, discovers, adds application-level retries, or falls back to another distribution. Product
paths and AppService origins are deployment inputs; none are compiled into L0.
The original `gateway.nexuscrm.io/v1` grammar accepts exact paths only. The
`gateway.nexuscrm.io/v2` grammar also accepts one opaque terminal segment such
as `/api/v1/items/:id`. The `gateway.nexuscrm.io/v3` grammar accepts multiple
bounded opaque segments such as `/api/v1/items/:object/:id`. Every configured
shape still requires the exact segment count and fixed segments; prefix,
wildcard, and regex routing remain unsupported. Overlapping exact and template
routes are rejected when the immutable generation is loaded.
The process requires edge TLS. Deployment chooses an explicit `http` or `https`
upstream origin and owns the internal network policy for that hop.

```json
{
  "apiVersion": "gateway.nexuscrm.io/v1",
  "revision": "test-stack-r1",
  "routes": [{
    "name": "bounded-read",
    "path": "/api/v1/items/access",
    "methods": ["POST"],
    "upstream": "https://appservice.example.test",
    "timeoutMillis": 5000,
    "maxRequestBytes": 65536
  }]
}
```

Required environment:

- `GATEWAY_ROUTE_CONFIG`: path to that immutable JSON artifact
- `GATEWAY_LISTEN_ADDRESS`: TLS listener address, for example `:8443`
- `GATEWAY_TLS_CERT_FILE` and `GATEWAY_TLS_KEY_FILE`: operator-supplied edge TLS
- `GATEWAY_MAX_IN_FLIGHT`: fail-fast process concurrency admission
- `GATEWAY_SHUTDOWN_TIMEOUT`: optional graceful drain bound; defaults to the
  longest configured route deadline plus the downstream response-write bound,
  and cannot be configured below that complete-response lifecycle

The operational surface is `/livez`, `/readyz`, `/version`, `/capabilities`,
and `/metrics`. `SIGTERM` first removes readiness and new admission, then
`http.Server.Shutdown` drains complete HTTP responses. This Gateway is a
request/response edge: protocol upgrades are rejected because hijacked
connections are outside `http.Server.Shutdown` ownership. The runtime-only image is
built by compiling `./cmd/gateway` and passing that binary to
`Dockerfile.gateway` as `gateway-binary`; deployment owns the immutable image digest and route
artifact. The source-free authority implementation lives in the reusable
`trusted-publish-gateway-image.yml` workflow. The active `main` caller and owner
policy both pin its reviewed foundation merge SHA
`c2cedc3d50ef39e79e93b9dd0d0f34c22592d1d6`; the AWS role binds that same
exact `job_workflow_ref`. The policy is `active` only because all three facts
identify the same immutable authority. Every push to `main` publishes
`ghcr.io/pingcap-inc/nexuscrm-api-host-gateway:<full-main-SHA>` and records the
resulting image, maximum-mode BuildKit provenance, SBOM, owner-signed SLSA v1
provenance, owner signing-key identity, and
their digests in the workflow summary. A run builds under a run-scoped
`pending-*` tag; that tag is disposable publication workspace, not a release
identity or an L2 input. The repository's exact `main` publisher exchanges its
GitHub OIDC identity for a repository-scoped AWS role and signs the immutable
image digest with its own asymmetric KMS key. It also requests a separate
GitHub-signed OIDC JWT whose custom audience contains that exact image digest.
The JWT binds the immutable repository ID, repository visibility, source
SHA/ref, caller workflow name/ref/SHA, exact reusable job workflow ref/SHA,
event, runner environment, and run identity.
Source SHA and workflow SHA remain separate GitHub-owned facts: the former
identifies the triggering revision, while the latter identifies the workflow
file revision. The verifier records both and never assumes they are equal.
That JWT is embedded in the KMS-signed Cosign payload. No long-lived AWS
credential or private signing key is stored in GitHub. The source-free signer also
derives a canonical SLSA v1 predicate from the versioned owner policy and
GitHub-owned run identity and signs it with the same KMS key. It never promotes
candidate-authored BuildKit predicates into release authority. The signer
locally checks its output, then a fresh job with neither OIDC nor KMS authority
runs the same complete `verify-release` command used by downstream admission,
including independent validation of the retained JWT signature against GitHub's
OIDC JWKS. Only a final source-free job with package-write authority may create
the exact-SHA tag after that verifier succeeds. Signer-authored annotations
alone are never workflow proof. A failed run may
therefore leave abandoned pending candidates, and a rerun may rebuild them, but
it never overwrites or retroactively signs an existing exact-SHA tag. The first
successfully committed exact-SHA tag is the single release discovery fact. L2
resolves that tag once, then pins the image digest, exact owner source commit,
[`release/gateway-image-signing-policy.json`](release/gateway-image-signing-policy.json),
its declared public-key digest, GitHub OIDC key digest and identity receipt,
owner-signed provenance receipt, and SBOM digest rather than either mutable
alias. The referenced
[`release/gateway-image-signing-public-key.pem`](release/gateway-image-signing-public-key.pem)
is the owner-key verifier input. L2 checks those source-owned bytes, verifies
the GitHub-signed identity evidence from the same payload against the
source-declared GitHub JWKS endpoint, and persists the matched OIDC key digest
in its immutable admission receipt. It needs registry and HTTPS access but no
AWS access; it never signs an L0-owned Gateway image.

The trusted publisher uses separate validation, candidate-builder, signer,
pre-publication verifier, release-commit, and existing-release verification
jobs, each on a fresh runner, as hard capability boundaries. The validation job executes tests and repository checks but has no
package-write or OIDC permission. The candidate job owns fixed dependency
resolution, binary/image construction, and the disposable pending tag; it does
not execute product tests or repository scripts, has no `id-token` permission,
and cannot assume the KMS role. The signer job alone has
`id-token: write`. It never checks out, compiles, interprets, or executes
repository code; the source-owned policy and public key are fetched as bounded
data and checked against constants owned by the workflow. It runs no product
tests, private dependency download, Go toolchain, repository script, or Docker
image build. Before obtaining AWS credentials it
independently resolves the candidate tag from the registry and rechecks its
digest, source revision, and the presence of supplemental BuildKit provenance
and SBOM data. It requests and validates digest-bound GitHub identity evidence,
then generates the canonical provenance from source-owned and
GitHub-owned facts; candidate outputs do not supply its builder, source, or run
claims. Only that isolated job may attest and sign. A fresh no-authority verifier
executes the exact downstream release policy. A final job can write packages but
has no source checkout, OIDC, or KMS capability; it commits the stable source-SHA
tag only after verification succeeds. Step ordering within
one runner is not treated as credential isolation because GitHub permissions
are job-scoped. Reruns for an existing stable tag use a separate job with no OIDC
or KMS capability; that job may execute the source-owned downstream verifier but
can only revalidate, never sign or mutate the release.

The versioned owner policy is the sole public configuration source for the AWS
role, immutable KMS key ARN, region, OIDC identity, verifier key and algorithm.
The publisher does not read repository variables for those facts; any variables
created while bootstrapping the owner key are obsolete and may be removed.
Before a new digest is signed, the publisher requires an enabled
`ECC_NIST_P256` / `SIGN_VERIFY` KMS
key, proves its exported public key is the key committed beside the policy, and
uses a content-hash-pinned Cosign binary. Existing stable releases can be
verified from the committed public key plus GitHub's OIDC JWKS without
obtaining AWS credentials. The short-lived JWT need not still be current during
historical verification because its audience is bound to one immutable digest;
only the signer's pre-sign identity check requires a current token. Key
rotation is therefore a reviewed source change; old source commits retain the
verification material for old releases. The external AWS ownership boundary and
administrator readback are described in
[`docs/gateway-image-signing-owner-facts.md`](docs/gateway-image-signing-owner-facts.md).

After checking out this repository at the image's exact source commit, a
downstream consumer with registry read access and the content-pinned Cosign
binary can reproduce the complete signature policy without AWS credentials:

```bash
./scripts/gateway-image-signing-policy.sh verify-release \
  "$SOURCE_COMMIT" "$IMAGE_DIGEST"
```

The command emits a machine-readable identity receipt and rejects a different
image repository, source commit, source ref, publisher workflow/SHA, immutable
repository ID, event, runner environment, KMS key, owner-policy digest, public
key, OIDC signature/key, or image digest. Set `GATEWAY_OIDC_JWKS_FILE` to an
independently fetched JWKS document when the admission system must persist and
pin the exact verifier bytes used for a release decision.

## Release & versioning
Published private module, semver tags (`v0.2.0`). Breaking changes are rare and bump minor/major; consumers pin a version.
