mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-08 06:54:24 +00:00
fix(gateway): add HSTS header hardening and docs
This commit is contained in:
@@ -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.
|
||||
- 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
|
||||
|
||||
|
||||
30
SECURITY.md
30
SECURITY.md
@@ -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.
|
||||
|
||||
### 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
|
||||
|
||||
**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.
|
||||
|
||||
@@ -2126,6 +2126,8 @@ See [Plugins](/tools/plugin).
|
||||
- `gateway.http.endpoints.responses.maxUrlParts`
|
||||
- `gateway.http.endpoints.responses.files.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
|
||||
|
||||
|
||||
@@ -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**.
|
||||
- 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
|
||||
|
||||
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;
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
OpenClaw stores session transcripts on disk under `~/.openclaw/agents/<agentId>/sessions/*.jsonl`.
|
||||
|
||||
@@ -4,6 +4,7 @@ read_when:
|
||||
- Running OpenClaw behind an identity-aware proxy
|
||||
- Setting up Pomerium, Caddy, or nginx with OAuth in front of OpenClaw
|
||||
- Fixing WebSocket 1008 unauthorized errors with reverse proxy setups
|
||||
- Deciding where to set HSTS and other HTTP hardening headers
|
||||
---
|
||||
|
||||
# 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.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
|
||||
|
||||
### Pomerium
|
||||
|
||||
@@ -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.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.",
|
||||
"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.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.",
|
||||
|
||||
@@ -86,6 +86,8 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"gateway.tls.caPath": "Gateway TLS CA Path",
|
||||
"gateway.http": "Gateway HTTP API",
|
||||
"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.sshTarget": "Remote Gateway SSH Target",
|
||||
"gateway.remote.sshIdentity": "Remote Gateway SSH Identity",
|
||||
|
||||
@@ -255,8 +255,19 @@ export type GatewayHttpEndpointsConfig = {
|
||||
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 = {
|
||||
endpoints?: GatewayHttpEndpointsConfig;
|
||||
securityHeaders?: GatewayHttpSecurityHeadersConfig;
|
||||
};
|
||||
|
||||
export type GatewayNodesConfig = {
|
||||
|
||||
@@ -562,6 +562,12 @@ export const OpenClawSchema = z
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
securityHeaders: z
|
||||
.object({
|
||||
strictTransportSecurity: z.union([z.string(), z.literal(false)]).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
|
||||
@@ -8,9 +8,16 @@ import { readJsonBody } from "./hooks.js";
|
||||
* Content-Security-Policy are intentionally omitted here because some handlers
|
||||
* (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("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) {
|
||||
|
||||
@@ -417,6 +417,7 @@ export function createGatewayHttpServer(opts: {
|
||||
openAiChatCompletionsEnabled: boolean;
|
||||
openResponsesEnabled: boolean;
|
||||
openResponsesConfig?: import("../config/types.gateway.js").GatewayHttpResponsesConfig;
|
||||
strictTransportSecurityHeader?: string;
|
||||
handleHooksRequest: HooksRequestHandler;
|
||||
handlePluginRequest?: HooksRequestHandler;
|
||||
resolvedAuth: ResolvedGatewayAuth;
|
||||
@@ -433,6 +434,7 @@ export function createGatewayHttpServer(opts: {
|
||||
openAiChatCompletionsEnabled,
|
||||
openResponsesEnabled,
|
||||
openResponsesConfig,
|
||||
strictTransportSecurityHeader,
|
||||
handleHooksRequest,
|
||||
handlePluginRequest,
|
||||
resolvedAuth,
|
||||
@@ -447,7 +449,9 @@ export function createGatewayHttpServer(opts: {
|
||||
});
|
||||
|
||||
async function handleRequest(req: IncomingMessage, res: ServerResponse) {
|
||||
setDefaultSecurityHeaders(res);
|
||||
setDefaultSecurityHeaders(res, {
|
||||
strictTransportSecurity: strictTransportSecurityHeader,
|
||||
});
|
||||
|
||||
// Don't interfere with WebSocket upgrades; ws handles the 'upgrade' event.
|
||||
if (String(req.headers.upgrade ?? "").toLowerCase() === "websocket") {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,6 +25,7 @@ export type GatewayRuntimeConfig = {
|
||||
openAiChatCompletionsEnabled: boolean;
|
||||
openResponsesEnabled: boolean;
|
||||
openResponsesConfig?: import("../config/types.gateway.js").GatewayHttpResponsesConfig;
|
||||
strictTransportSecurityHeader?: string;
|
||||
controlUiBasePath: string;
|
||||
controlUiRoot?: string;
|
||||
resolvedAuth: ResolvedGatewayAuth;
|
||||
@@ -78,6 +79,15 @@ export async function resolveGatewayRuntimeConfig(params: {
|
||||
false;
|
||||
const openResponsesConfig = params.cfg.gateway?.http?.endpoints?.responses;
|
||||
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 controlUiRootRaw = params.cfg.gateway?.controlUi?.root;
|
||||
const controlUiRoot =
|
||||
@@ -147,6 +157,7 @@ export async function resolveGatewayRuntimeConfig(params: {
|
||||
openResponsesConfig: openResponsesConfig
|
||||
? { ...openResponsesConfig, enabled: openResponsesEnabled }
|
||||
: undefined,
|
||||
strictTransportSecurityHeader,
|
||||
controlUiBasePath,
|
||||
controlUiRoot,
|
||||
resolvedAuth,
|
||||
|
||||
@@ -41,6 +41,7 @@ export async function createGatewayRuntimeState(params: {
|
||||
openAiChatCompletionsEnabled: boolean;
|
||||
openResponsesEnabled: boolean;
|
||||
openResponsesConfig?: import("../config/types.gateway.js").GatewayHttpResponsesConfig;
|
||||
strictTransportSecurityHeader?: string;
|
||||
resolvedAuth: ResolvedGatewayAuth;
|
||||
/** Optional rate limiter for auth brute-force protection. */
|
||||
rateLimiter?: AuthRateLimiter;
|
||||
@@ -128,6 +129,7 @@ export async function createGatewayRuntimeState(params: {
|
||||
openAiChatCompletionsEnabled: params.openAiChatCompletionsEnabled,
|
||||
openResponsesEnabled: params.openResponsesEnabled,
|
||||
openResponsesConfig: params.openResponsesConfig,
|
||||
strictTransportSecurityHeader: params.strictTransportSecurityHeader,
|
||||
handleHooksRequest,
|
||||
handlePluginRequest,
|
||||
resolvedAuth: params.resolvedAuth,
|
||||
|
||||
@@ -301,6 +301,7 @@ export async function startGatewayServer(
|
||||
openAiChatCompletionsEnabled,
|
||||
openResponsesEnabled,
|
||||
openResponsesConfig,
|
||||
strictTransportSecurityHeader,
|
||||
controlUiBasePath,
|
||||
controlUiRoot: controlUiRootOverride,
|
||||
resolvedAuth,
|
||||
@@ -385,6 +386,7 @@ export async function startGatewayServer(
|
||||
openAiChatCompletionsEnabled,
|
||||
openResponsesEnabled,
|
||||
openResponsesConfig,
|
||||
strictTransportSecurityHeader,
|
||||
resolvedAuth,
|
||||
rateLimiter: authRateLimiter,
|
||||
gatewayTls,
|
||||
|
||||
@@ -66,6 +66,68 @@ async function dispatchRequest(
|
||||
}
|
||||
|
||||
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 () => {
|
||||
const resolvedAuth: ResolvedGatewayAuth = {
|
||||
mode: "token",
|
||||
|
||||
Reference in New Issue
Block a user