fix(gateway): add HSTS header hardening and docs

This commit is contained in:
Peter Steinberger
2026-02-23 19:47:09 +00:00
parent c88915b721
commit 9af3ec92a5
16 changed files with 275 additions and 2 deletions

View File

@@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai
- Providers/Vercel AI Gateway: accept Claude shorthand model refs (`vercel-ai-gateway/claude-*`) by normalizing to canonical Anthropic-routed model ids. (#23985) Thanks @sallyom, @markbooch, and @vincentkoc. - Providers/Vercel AI Gateway: accept Claude shorthand model refs (`vercel-ai-gateway/claude-*`) by normalizing to canonical Anthropic-routed model ids. (#23985) Thanks @sallyom, @markbooch, and @vincentkoc.
- Docs/Prompt caching: add a dedicated prompt-caching reference covering `cacheRetention`, per-agent `params` merge precedence, Bedrock/OpenRouter behavior, and cache-ttl + heartbeat tuning. Thanks @svenssonaxel. - Docs/Prompt caching: add a dedicated prompt-caching reference covering `cacheRetention`, per-agent `params` merge precedence, Bedrock/OpenRouter behavior, and cache-ttl + heartbeat tuning. Thanks @svenssonaxel.
- Gateway/HTTP security headers: add optional `gateway.http.securityHeaders.strictTransportSecurity` support to emit `Strict-Transport-Security` for direct HTTPS deployments, with runtime wiring, validation, tests, and hardening docs.
### Breaking ### Breaking

View File

@@ -30,6 +30,36 @@ For full reporting instructions see our [Trust page](https://trust.openclaw.ai).
Reports without reproduction steps, demonstrated impact, and remediation advice will be deprioritized. Given the volume of AI-generated scanner findings, we must ensure we're receiving vetted reports from researchers who understand the issues. Reports without reproduction steps, demonstrated impact, and remediation advice will be deprioritized. Given the volume of AI-generated scanner findings, we must ensure we're receiving vetted reports from researchers who understand the issues.
### Report Acceptance Gate (Triage Fast Path)
For fastest triage, include all of the following:
- Exact vulnerable path (`file`, function, and line range) on a current revision.
- Tested version details (OpenClaw version and/or commit SHA).
- Reproducible PoC against latest `main` or latest released version.
- Demonstrated impact tied to OpenClaw's documented trust boundaries.
- Scope check explaining why the report is **not** covered by the Out of Scope section below.
Reports that miss these requirements may be closed as `invalid` or `no-action`.
### Common False-Positive Patterns
These are frequently reported but are typically closed with no code change:
- Prompt-injection-only chains without a boundary bypass (prompt injection is out of scope).
- Operator-intended local features (for example TUI local `!` shell) presented as remote injection.
- Reports that assume per-user multi-tenant authorization on a shared gateway host/config.
- Missing HSTS findings on default local/loopback deployments.
- Slack webhook signature findings when HTTP mode already uses signing-secret verification.
- Discord inbound webhook signature findings for paths not used by this repo's Discord integration.
- Scanner-only claims against stale/nonexistent paths, or claims without a working repro.
### Duplicate Report Handling
- Search existing advisories before filing.
- Include likely duplicate GHSA IDs in your report when applicable.
- Maintainers may close lower-quality/later duplicates in favor of the earliest high-quality canonical report.
## Security & Trust ## Security & Trust
**Jamieson O'Reilly** ([@theonejvo](https://twitter.com/theonejvo)) is Security & Trust at OpenClaw. Jamieson is the founder of [Dvuln](https://dvuln.com) and brings extensive experience in offensive security, penetration testing, and security program development. **Jamieson O'Reilly** ([@theonejvo](https://twitter.com/theonejvo)) is Security & Trust at OpenClaw. Jamieson is the founder of [Dvuln](https://dvuln.com) and brings extensive experience in offensive security, penetration testing, and security program development.

View File

@@ -2126,6 +2126,8 @@ See [Plugins](/tools/plugin).
- `gateway.http.endpoints.responses.maxUrlParts` - `gateway.http.endpoints.responses.maxUrlParts`
- `gateway.http.endpoints.responses.files.urlAllowlist` - `gateway.http.endpoints.responses.files.urlAllowlist`
- `gateway.http.endpoints.responses.images.urlAllowlist` - `gateway.http.endpoints.responses.images.urlAllowlist`
- Optional response hardening header:
- `gateway.http.securityHeaders.strictTransportSecurity` (set only for HTTPS origins you control; see [Trusted Proxy Auth](/gateway/trusted-proxy-auth#tls-termination-and-hsts))
### Multi-instance isolation ### Multi-instance isolation

View File

@@ -38,6 +38,40 @@ OpenClaw assumes the host and config boundary are trusted:
- Running one Gateway for multiple mutually untrusted/adversarial operators is **not a recommended setup**. - Running one Gateway for multiple mutually untrusted/adversarial operators is **not a recommended setup**.
- For mixed-trust teams, split trust boundaries with separate gateways (or at minimum separate OS users/hosts). - For mixed-trust teams, split trust boundaries with separate gateways (or at minimum separate OS users/hosts).
## Trust boundary matrix
Use this as the quick model when triaging risk:
| Boundary or control | What it means | Common misread |
| ------------------------------------------- | ------------------------------------------------- | ----------------------------------------------------------------------------- |
| `gateway.auth` (token/password/device auth) | Authenticates callers to gateway APIs | "Needs per-message signatures on every frame to be secure" |
| `sessionKey` | Routing key for context/session selection | "Session key is a user auth boundary" |
| Prompt/content guardrails | Reduce model abuse risk | "Prompt injection alone proves auth bypass" |
| `canvas.eval` / browser evaluate | Intentional operator capability when enabled | "Any JS eval primitive is automatically a vuln in this trust model" |
| Local TUI `!` shell | Explicit operator-triggered local execution | "Local shell convenience command is remote injection" |
| Node pairing and node commands | Operator-level remote execution on paired devices | "Remote device control should be treated as untrusted user access by default" |
## Not vulnerabilities by design
These patterns are commonly reported and are usually closed as no-action unless a real boundary bypass is shown:
- Prompt-injection-only chains without a policy/auth/sandbox bypass.
- Claims that assume hostile multi-tenant operation on one shared host/config.
- Localhost-only deployment findings (for example HSTS on loopback-only gateway).
- Discord inbound webhook signature findings for inbound paths that do not exist in this repo.
- "Missing per-user authorization" findings that treat `sessionKey` as an auth token.
## Researcher preflight checklist
Before opening a GHSA, verify all of these:
1. Repro still works on latest `main` or latest release.
2. Report includes exact code path (`file`, function, line range) and tested version/commit.
3. Impact crosses a documented trust boundary (not just prompt injection).
4. Claim is not listed in [Out of Scope](https://github.com/openclaw/openclaw/blob/main/SECURITY.md#out-of-scope).
5. Existing advisories were checked for duplicates (reuse canonical GHSA when applicable).
6. Deployment assumptions are explicit (loopback/local vs exposed, trusted vs untrusted operators).
## Hardened baseline in 60 seconds ## Hardened baseline in 60 seconds
Use this baseline first, then selectively re-enable tools per trusted agent: Use this baseline first, then selectively re-enable tools per trusted agent:
@@ -202,6 +236,14 @@ Bad reverse proxy behavior (append/preserve untrusted forwarding headers):
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
``` ```
## HSTS and origin notes
- OpenClaw gateway is local/loopback first. If you terminate TLS at a reverse proxy, set HSTS on the proxy-facing HTTPS domain there.
- If the gateway itself terminates HTTPS, you can set `gateway.http.securityHeaders.strictTransportSecurity` to emit the HSTS header from OpenClaw responses.
- Detailed deployment guidance is in [Trusted Proxy Auth](/gateway/trusted-proxy-auth#tls-termination-and-hsts).
- For non-loopback Control UI deployments, explicitly configure `gateway.controlUi.allowedOrigins` instead of relying on permissive defaults.
- Treat DNS rebinding and proxy-host header behavior as deployment hardening concerns; keep `trustedProxies` tight and avoid exposing the gateway directly to the public internet.
## Local session logs live on disk ## Local session logs live on disk
OpenClaw stores session transcripts on disk under `~/.openclaw/agents/<agentId>/sessions/*.jsonl`. OpenClaw stores session transcripts on disk under `~/.openclaw/agents/<agentId>/sessions/*.jsonl`.

View File

@@ -4,6 +4,7 @@ read_when:
- Running OpenClaw behind an identity-aware proxy - Running OpenClaw behind an identity-aware proxy
- Setting up Pomerium, Caddy, or nginx with OAuth in front of OpenClaw - Setting up Pomerium, Caddy, or nginx with OAuth in front of OpenClaw
- Fixing WebSocket 1008 unauthorized errors with reverse proxy setups - Fixing WebSocket 1008 unauthorized errors with reverse proxy setups
- Deciding where to set HSTS and other HTTP hardening headers
--- ---
# Trusted Proxy Auth # Trusted Proxy Auth
@@ -75,6 +76,52 @@ If `gateway.bind` is `loopback`, include a loopback proxy address in
| `gateway.auth.trustedProxy.requiredHeaders` | No | Additional headers that must be present for the request to be trusted | | `gateway.auth.trustedProxy.requiredHeaders` | No | Additional headers that must be present for the request to be trusted |
| `gateway.auth.trustedProxy.allowUsers` | No | Allowlist of user identities. Empty means allow all authenticated users. | | `gateway.auth.trustedProxy.allowUsers` | No | Allowlist of user identities. Empty means allow all authenticated users. |
## TLS termination and HSTS
Use one TLS termination point and apply HSTS there.
### Recommended pattern: proxy TLS termination
When your reverse proxy handles HTTPS for `https://control.example.com`, set
`Strict-Transport-Security` at the proxy for that domain.
- Good fit for internet-facing deployments.
- Keeps certificate + HTTP hardening policy in one place.
- OpenClaw can stay on loopback HTTP behind the proxy.
Example header value:
```text
Strict-Transport-Security: max-age=31536000; includeSubDomains
```
### Gateway TLS termination
If OpenClaw itself serves HTTPS directly (no TLS-terminating proxy), set:
```json5
{
gateway: {
tls: { enabled: true },
http: {
securityHeaders: {
strictTransportSecurity: "max-age=31536000; includeSubDomains",
},
},
},
}
```
`strictTransportSecurity` accepts a string header value, or `false` to disable explicitly.
### Rollout guidance
- Start with a short max age first (for example `max-age=300`) while validating traffic.
- Increase to long-lived values (for example `max-age=31536000`) only after confidence is high.
- Add `includeSubDomains` only if every subdomain is HTTPS-ready.
- Use preload only if you intentionally meet preload requirements for your full domain set.
- Loopback-only local development does not benefit from HSTS.
## Proxy Setup Examples ## Proxy Setup Examples
### Pomerium ### Pomerium

View File

@@ -119,6 +119,10 @@ export const FIELD_HELP: Record<string, string> = {
"Gateway HTTP API configuration grouping endpoint toggles and transport-facing API exposure controls. Keep only required endpoints enabled to reduce attack surface.", "Gateway HTTP API configuration grouping endpoint toggles and transport-facing API exposure controls. Keep only required endpoints enabled to reduce attack surface.",
"gateway.http.endpoints": "gateway.http.endpoints":
"HTTP endpoint feature toggles under the gateway API surface for compatibility routes and optional integrations. Enable endpoints intentionally and monitor access patterns after rollout.", "HTTP endpoint feature toggles under the gateway API surface for compatibility routes and optional integrations. Enable endpoints intentionally and monitor access patterns after rollout.",
"gateway.http.securityHeaders":
"Optional HTTP response security headers applied by the gateway process itself. Prefer setting these at your reverse proxy when TLS terminates there.",
"gateway.http.securityHeaders.strictTransportSecurity":
"Value for the Strict-Transport-Security response header. Set only on HTTPS origins that you fully control; use false to explicitly disable.",
"gateway.remote.url": "Remote Gateway WebSocket URL (ws:// or wss://).", "gateway.remote.url": "Remote Gateway WebSocket URL (ws:// or wss://).",
"gateway.remote.token": "gateway.remote.token":
"Bearer token used to authenticate this client to a remote gateway in token-auth deployments. Store via secret/env substitution and rotate alongside remote gateway auth changes.", "Bearer token used to authenticate this client to a remote gateway in token-auth deployments. Store via secret/env substitution and rotate alongside remote gateway auth changes.",

View File

@@ -86,6 +86,8 @@ export const FIELD_LABELS: Record<string, string> = {
"gateway.tls.caPath": "Gateway TLS CA Path", "gateway.tls.caPath": "Gateway TLS CA Path",
"gateway.http": "Gateway HTTP API", "gateway.http": "Gateway HTTP API",
"gateway.http.endpoints": "Gateway HTTP Endpoints", "gateway.http.endpoints": "Gateway HTTP Endpoints",
"gateway.http.securityHeaders": "Gateway HTTP Security Headers",
"gateway.http.securityHeaders.strictTransportSecurity": "Strict Transport Security Header",
"gateway.remote.url": "Remote Gateway URL", "gateway.remote.url": "Remote Gateway URL",
"gateway.remote.sshTarget": "Remote Gateway SSH Target", "gateway.remote.sshTarget": "Remote Gateway SSH Target",
"gateway.remote.sshIdentity": "Remote Gateway SSH Identity", "gateway.remote.sshIdentity": "Remote Gateway SSH Identity",

View File

@@ -255,8 +255,19 @@ export type GatewayHttpEndpointsConfig = {
responses?: GatewayHttpResponsesConfig; responses?: GatewayHttpResponsesConfig;
}; };
export type GatewayHttpSecurityHeadersConfig = {
/**
* Value for the Strict-Transport-Security response header.
* Set to false to disable explicitly.
*
* Example: "max-age=31536000; includeSubDomains"
*/
strictTransportSecurity?: string | false;
};
export type GatewayHttpConfig = { export type GatewayHttpConfig = {
endpoints?: GatewayHttpEndpointsConfig; endpoints?: GatewayHttpEndpointsConfig;
securityHeaders?: GatewayHttpSecurityHeadersConfig;
}; };
export type GatewayNodesConfig = { export type GatewayNodesConfig = {

View File

@@ -562,6 +562,12 @@ export const OpenClawSchema = z
}) })
.strict() .strict()
.optional(), .optional(),
securityHeaders: z
.object({
strictTransportSecurity: z.union([z.string(), z.literal(false)]).optional(),
})
.strict()
.optional(),
}) })
.strict() .strict()
.optional(), .optional(),

View File

@@ -8,9 +8,16 @@ import { readJsonBody } from "./hooks.js";
* Content-Security-Policy are intentionally omitted here because some handlers * Content-Security-Policy are intentionally omitted here because some handlers
* (canvas host, A2UI) serve content that may be loaded inside frames. * (canvas host, A2UI) serve content that may be loaded inside frames.
*/ */
export function setDefaultSecurityHeaders(res: ServerResponse) { export function setDefaultSecurityHeaders(
res: ServerResponse,
opts?: { strictTransportSecurity?: string },
) {
res.setHeader("X-Content-Type-Options", "nosniff"); res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader("Referrer-Policy", "no-referrer"); res.setHeader("Referrer-Policy", "no-referrer");
const strictTransportSecurity = opts?.strictTransportSecurity;
if (typeof strictTransportSecurity === "string" && strictTransportSecurity.length > 0) {
res.setHeader("Strict-Transport-Security", strictTransportSecurity);
}
} }
export function sendJson(res: ServerResponse, status: number, body: unknown) { export function sendJson(res: ServerResponse, status: number, body: unknown) {

View File

@@ -417,6 +417,7 @@ export function createGatewayHttpServer(opts: {
openAiChatCompletionsEnabled: boolean; openAiChatCompletionsEnabled: boolean;
openResponsesEnabled: boolean; openResponsesEnabled: boolean;
openResponsesConfig?: import("../config/types.gateway.js").GatewayHttpResponsesConfig; openResponsesConfig?: import("../config/types.gateway.js").GatewayHttpResponsesConfig;
strictTransportSecurityHeader?: string;
handleHooksRequest: HooksRequestHandler; handleHooksRequest: HooksRequestHandler;
handlePluginRequest?: HooksRequestHandler; handlePluginRequest?: HooksRequestHandler;
resolvedAuth: ResolvedGatewayAuth; resolvedAuth: ResolvedGatewayAuth;
@@ -433,6 +434,7 @@ export function createGatewayHttpServer(opts: {
openAiChatCompletionsEnabled, openAiChatCompletionsEnabled,
openResponsesEnabled, openResponsesEnabled,
openResponsesConfig, openResponsesConfig,
strictTransportSecurityHeader,
handleHooksRequest, handleHooksRequest,
handlePluginRequest, handlePluginRequest,
resolvedAuth, resolvedAuth,
@@ -447,7 +449,9 @@ export function createGatewayHttpServer(opts: {
}); });
async function handleRequest(req: IncomingMessage, res: ServerResponse) { async function handleRequest(req: IncomingMessage, res: ServerResponse) {
setDefaultSecurityHeaders(res); setDefaultSecurityHeaders(res, {
strictTransportSecurity: strictTransportSecurityHeader,
});
// Don't interfere with WebSocket upgrades; ws handles the 'upgrade' event. // Don't interfere with WebSocket upgrades; ws handles the 'upgrade' event.
if (String(req.headers.upgrade ?? "").toLowerCase() === "websocket") { if (String(req.headers.upgrade ?? "").toLowerCase() === "websocket") {

View File

@@ -189,4 +189,44 @@ describe("resolveGatewayRuntimeConfig", () => {
); );
}); });
}); });
describe("HTTP security headers", () => {
it("resolves strict transport security header from config", async () => {
const result = await resolveGatewayRuntimeConfig({
cfg: {
gateway: {
bind: "loopback",
auth: { mode: "none" },
http: {
securityHeaders: {
strictTransportSecurity: " max-age=31536000; includeSubDomains ",
},
},
},
},
port: 18789,
});
expect(result.strictTransportSecurityHeader).toBe("max-age=31536000; includeSubDomains");
});
it("does not set strict transport security when explicitly disabled", async () => {
const result = await resolveGatewayRuntimeConfig({
cfg: {
gateway: {
bind: "loopback",
auth: { mode: "none" },
http: {
securityHeaders: {
strictTransportSecurity: false,
},
},
},
},
port: 18789,
});
expect(result.strictTransportSecurityHeader).toBeUndefined();
});
});
}); });

View File

@@ -25,6 +25,7 @@ export type GatewayRuntimeConfig = {
openAiChatCompletionsEnabled: boolean; openAiChatCompletionsEnabled: boolean;
openResponsesEnabled: boolean; openResponsesEnabled: boolean;
openResponsesConfig?: import("../config/types.gateway.js").GatewayHttpResponsesConfig; openResponsesConfig?: import("../config/types.gateway.js").GatewayHttpResponsesConfig;
strictTransportSecurityHeader?: string;
controlUiBasePath: string; controlUiBasePath: string;
controlUiRoot?: string; controlUiRoot?: string;
resolvedAuth: ResolvedGatewayAuth; resolvedAuth: ResolvedGatewayAuth;
@@ -78,6 +79,15 @@ export async function resolveGatewayRuntimeConfig(params: {
false; false;
const openResponsesConfig = params.cfg.gateway?.http?.endpoints?.responses; const openResponsesConfig = params.cfg.gateway?.http?.endpoints?.responses;
const openResponsesEnabled = params.openResponsesEnabled ?? openResponsesConfig?.enabled ?? false; const openResponsesEnabled = params.openResponsesEnabled ?? openResponsesConfig?.enabled ?? false;
const strictTransportSecurityConfig =
params.cfg.gateway?.http?.securityHeaders?.strictTransportSecurity;
const strictTransportSecurityHeader =
strictTransportSecurityConfig === false
? undefined
: typeof strictTransportSecurityConfig === "string" &&
strictTransportSecurityConfig.trim().length > 0
? strictTransportSecurityConfig.trim()
: undefined;
const controlUiBasePath = normalizeControlUiBasePath(params.cfg.gateway?.controlUi?.basePath); const controlUiBasePath = normalizeControlUiBasePath(params.cfg.gateway?.controlUi?.basePath);
const controlUiRootRaw = params.cfg.gateway?.controlUi?.root; const controlUiRootRaw = params.cfg.gateway?.controlUi?.root;
const controlUiRoot = const controlUiRoot =
@@ -147,6 +157,7 @@ export async function resolveGatewayRuntimeConfig(params: {
openResponsesConfig: openResponsesConfig openResponsesConfig: openResponsesConfig
? { ...openResponsesConfig, enabled: openResponsesEnabled } ? { ...openResponsesConfig, enabled: openResponsesEnabled }
: undefined, : undefined,
strictTransportSecurityHeader,
controlUiBasePath, controlUiBasePath,
controlUiRoot, controlUiRoot,
resolvedAuth, resolvedAuth,

View File

@@ -41,6 +41,7 @@ export async function createGatewayRuntimeState(params: {
openAiChatCompletionsEnabled: boolean; openAiChatCompletionsEnabled: boolean;
openResponsesEnabled: boolean; openResponsesEnabled: boolean;
openResponsesConfig?: import("../config/types.gateway.js").GatewayHttpResponsesConfig; openResponsesConfig?: import("../config/types.gateway.js").GatewayHttpResponsesConfig;
strictTransportSecurityHeader?: string;
resolvedAuth: ResolvedGatewayAuth; resolvedAuth: ResolvedGatewayAuth;
/** Optional rate limiter for auth brute-force protection. */ /** Optional rate limiter for auth brute-force protection. */
rateLimiter?: AuthRateLimiter; rateLimiter?: AuthRateLimiter;
@@ -128,6 +129,7 @@ export async function createGatewayRuntimeState(params: {
openAiChatCompletionsEnabled: params.openAiChatCompletionsEnabled, openAiChatCompletionsEnabled: params.openAiChatCompletionsEnabled,
openResponsesEnabled: params.openResponsesEnabled, openResponsesEnabled: params.openResponsesEnabled,
openResponsesConfig: params.openResponsesConfig, openResponsesConfig: params.openResponsesConfig,
strictTransportSecurityHeader: params.strictTransportSecurityHeader,
handleHooksRequest, handleHooksRequest,
handlePluginRequest, handlePluginRequest,
resolvedAuth: params.resolvedAuth, resolvedAuth: params.resolvedAuth,

View File

@@ -301,6 +301,7 @@ export async function startGatewayServer(
openAiChatCompletionsEnabled, openAiChatCompletionsEnabled,
openResponsesEnabled, openResponsesEnabled,
openResponsesConfig, openResponsesConfig,
strictTransportSecurityHeader,
controlUiBasePath, controlUiBasePath,
controlUiRoot: controlUiRootOverride, controlUiRoot: controlUiRootOverride,
resolvedAuth, resolvedAuth,
@@ -385,6 +386,7 @@ export async function startGatewayServer(
openAiChatCompletionsEnabled, openAiChatCompletionsEnabled,
openResponsesEnabled, openResponsesEnabled,
openResponsesConfig, openResponsesConfig,
strictTransportSecurityHeader,
resolvedAuth, resolvedAuth,
rateLimiter: authRateLimiter, rateLimiter: authRateLimiter,
gatewayTls, gatewayTls,

View File

@@ -66,6 +66,68 @@ async function dispatchRequest(
} }
describe("gateway plugin HTTP auth boundary", () => { describe("gateway plugin HTTP auth boundary", () => {
test("applies default security headers and optional strict transport security", async () => {
const resolvedAuth: ResolvedGatewayAuth = {
mode: "none",
token: undefined,
password: undefined,
allowTailscale: false,
};
await withTempConfig({
cfg: { gateway: { trustedProxies: [] } },
prefix: "openclaw-plugin-http-security-headers-test-",
run: async () => {
const withoutHsts = createGatewayHttpServer({
canvasHost: null,
clients: new Set(),
controlUiEnabled: false,
controlUiBasePath: "/__control__",
openAiChatCompletionsEnabled: false,
openResponsesEnabled: false,
handleHooksRequest: async () => false,
resolvedAuth,
});
const withoutHstsResponse = createResponse();
await dispatchRequest(
withoutHsts,
createRequest({ path: "/missing" }),
withoutHstsResponse.res,
);
expect(withoutHstsResponse.setHeader).toHaveBeenCalledWith(
"X-Content-Type-Options",
"nosniff",
);
expect(withoutHstsResponse.setHeader).toHaveBeenCalledWith(
"Referrer-Policy",
"no-referrer",
);
expect(withoutHstsResponse.setHeader).not.toHaveBeenCalledWith(
"Strict-Transport-Security",
expect.any(String),
);
const withHsts = createGatewayHttpServer({
canvasHost: null,
clients: new Set(),
controlUiEnabled: false,
controlUiBasePath: "/__control__",
openAiChatCompletionsEnabled: false,
openResponsesEnabled: false,
strictTransportSecurityHeader: "max-age=31536000; includeSubDomains",
handleHooksRequest: async () => false,
resolvedAuth,
});
const withHstsResponse = createResponse();
await dispatchRequest(withHsts, createRequest({ path: "/missing" }), withHstsResponse.res);
expect(withHstsResponse.setHeader).toHaveBeenCalledWith(
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains",
);
},
});
});
test("requires gateway auth for /api/channels/* plugin routes and allows authenticated pass-through", async () => { test("requires gateway auth for /api/channels/* plugin routes and allows authenticated pass-through", async () => {
const resolvedAuth: ResolvedGatewayAuth = { const resolvedAuth: ResolvedGatewayAuth = {
mode: "token", mode: "token",