fix(api): share base-domain API host
Some checks failed
Architecture Governance / architecture (push) Has been cancelled

Tenant subdomains route through api.<base-domain>; nginx forwards the exact storefront host derived from the validated browser origin.
This commit is contained in:
2026-08-20 16:12:37 +04:00
parent e5949c3967
commit 9cd56586fb
15 changed files with 175 additions and 65 deletions

View File

@@ -6,28 +6,27 @@ and the backend. It supersedes any fixed `api.dexarmarket.ru` or same-origin
## 1. Deterministic hostname rule
The frontend prefixes the **complete** storefront hostname with `api.`:
The frontend uses one API hostname per **base domain**:
| Storefront | API origin | Bootstrap |
|---|---|---|
| `example.com` | `https://api.example.com` | `https://api.example.com/bootstrap` |
| `store1.example.com` | `https://api.store1.example.com` | `https://api.store1.example.com/bootstrap` |
| `www.example.com` | `https://api.www.example.com` | `https://api.www.example.com/bootstrap` |
| `store1.example.com` | `https://api.example.com` | `https://api.example.com/bootstrap` |
| `www.example.com` | `https://api.example.com` | `https://api.example.com/bootstrap` |
Bootstrap, auth, legacy routes, and `/api/...` routes all use this origin.
Localhost is the only exception and continues through the local `/api` proxy.
## 2. Backend changes required
For every request received publicly on `api.<storefront-host>`:
For every request received publicly on the shared `api.<base-domain>`:
1. Behind the trusted project nginx, use `X-Storefront-Host`. nginx deliberately
sends the same storefront value as upstream `Host` for compatibility with the
currently live backend and preserves the public API hostname in
`X-Forwarded-Host`.
2. Without that trusted proxy, normalize the request `Host`: lowercase, remove
the port, remove exactly one leading `api.` label when present, and retain
every remaining label.
1. Behind the trusted project nginx, use `X-Storefront-Host`. nginx derives it
from a validated browser `Origin`, sends the same value as upstream `Host`,
and preserves the shared public API hostname in `X-Forwarded-Host`.
2. Do not infer a subdomain tenant from the API `Host`: `store1.example.com` and
`example.com` intentionally share `api.example.com`. Non-browser clients must
provide tenant context through their authenticated server-to-server contract.
3. Resolve that normalized storefront hostname through the tenant-domain
registry. Do not infer a tenant from only the first label.
4. Reject unknown, disabled, or unverified domains with `403` before reading
@@ -41,21 +40,18 @@ For every request received publicly on `api.<storefront-host>`:
Pseudo-code:
```text
if request.remoteAddress is trustedProxy:
storefrontHost = lower(stripPort(request.header["X-Storefront-Host"]))
else:
requestHost = lower(stripPort(request.host))
storefrontHost = removeAtMostOnePrefix(requestHost, "api.")
require request.remoteAddress is trustedProxy
storefrontHost = lower(stripPort(request.header["X-Storefront-Host"]))
tenant = registry.findVerifiedDomain(storefrontHost) ?? forbidden()
request.tenant = tenant
```
## 3. CORS contract
For API host `api.<storefront-host>`, allow exactly:
For API host `api.<base-domain>`, echo the exact validated storefront origin:
```http
Access-Control-Allow-Origin: https://<storefront-host>
Access-Control-Allow-Origin: https://<base-domain or registered subdomain>
Access-Control-Allow-Credentials: true
Vary: Origin
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
@@ -77,15 +73,14 @@ scripts/deploy/configure-api-domain.sh \
--upstream https://127.0.0.1:445
```
It creates `api.gorbushka.market`, issues/renews its certificate, configures
CORS, and proxies all paths to the backend. Upstream receives
`Host: gorbushka.market`, `X-Forwarded-Host: api.gorbushka.market`, and
`X-Storefront-Host: gorbushka.market`; the script then reloads nginx and verifies
that `/bootstrap` returns a JSON object.
It creates the shared `api.gorbushka.market`, issues/renews its certificate,
configures CORS for `gorbushka.market` and its subdomains, and proxies all paths
to the backend. A request from `store1.gorbushka.market` reaches upstream with
`Host` and `X-Storefront-Host` set to `store1.gorbushka.market`, while
`X-Forwarded-Host` remains `api.gorbushka.market`.
For `store1.example.com`, both DNS and TLS must exist for
`api.store1.example.com`. A certificate for `*.example.com` does **not** cover
that two-label-deep hostname.
`store1.example.com` requires no additional API DNS or certificate; it uses the
same `api.example.com` certificate as the root storefront.
## 5. CI/CD contract
@@ -118,7 +113,7 @@ curl -i -X OPTIONS https://api.example.com/bootstrap \
```
- Frontend bundle contains no fixed marketplace API hostname.
- Root and nested storefronts call their matching `api.<full-hostname>`.
- Unknown API hosts return `403`, not the default tenant.
- Root and nested storefronts call the same `api.<base-domain>`.
- Unknown storefront domains return `403`, not the default tenant.
- `/bootstrap` returns JSON and the correct tenant.
- API responses never return the Angular `index.html` fallback.