@xopcai/xopc 0.0.77 → 0.0.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser-ext/manifest.json +1 -1
- package/dist/extensions/telegram/xopc.extension.json +1 -1
- package/dist/gateway/static/root/assets/{agents-DN3vr8pb.js → agents-Bh_9-1KB.js} +2 -2
- package/dist/gateway/static/root/assets/{agents-DN3vr8pb.js.map → agents-Bh_9-1KB.js.map} +1 -1
- package/dist/gateway/static/root/assets/{apps-page-BUn41aPi.js → apps-page-CB5anZpc.js} +2 -2
- package/dist/gateway/static/root/assets/{apps-page-BUn41aPi.js.map → apps-page-CB5anZpc.js.map} +1 -1
- package/dist/gateway/static/root/assets/{channels-settings-CYMmWDtP.js → channels-settings-Bt1sprhC.js} +2 -2
- package/dist/gateway/static/root/assets/{channels-settings-CYMmWDtP.js.map → channels-settings-Bt1sprhC.js.map} +1 -1
- package/dist/gateway/static/root/assets/{channels-status-swr-sJj4ueTp.js → channels-status-swr-Crgak3fg.js} +2 -2
- package/dist/gateway/static/root/assets/{channels-status-swr-sJj4ueTp.js.map → channels-status-swr-Crgak3fg.js.map} +1 -1
- package/dist/gateway/static/root/assets/{cron-api-CLxnaHdq.js → cron-api-CzJGvQQ7.js} +2 -2
- package/dist/gateway/static/root/assets/{cron-api-CLxnaHdq.js.map → cron-api-CzJGvQQ7.js.map} +1 -1
- package/dist/gateway/static/root/assets/{cron-page-BAQ8xSnJ.js → cron-page-BoNRJNVV.js} +2 -2
- package/dist/gateway/static/root/assets/{cron-page-BAQ8xSnJ.js.map → cron-page-BoNRJNVV.js.map} +1 -1
- package/dist/gateway/static/root/assets/{dist-BfJYxiK5.js → dist-a-eaOUvs.js} +2 -2
- package/dist/gateway/static/root/assets/{dist-BfJYxiK5.js.map → dist-a-eaOUvs.js.map} +1 -1
- package/dist/gateway/static/root/assets/{extension-debug-page-bgvVs-Sy.js → extension-debug-page-D-O1XjAa.js} +2 -2
- package/dist/gateway/static/root/assets/{extension-debug-page-bgvVs-Sy.js.map → extension-debug-page-D-O1XjAa.js.map} +1 -1
- package/dist/gateway/static/root/assets/{extension-page-SG4TVv-u.js → extension-page-B2VpqBTH.js} +2 -2
- package/dist/gateway/static/root/assets/{extension-page-SG4TVv-u.js.map → extension-page-B2VpqBTH.js.map} +1 -1
- package/dist/gateway/static/root/assets/{extension-settings-page-CJZRTsjF.js → extension-settings-page-CmBcQfeO.js} +2 -2
- package/dist/gateway/static/root/assets/{extension-settings-page-CJZRTsjF.js.map → extension-settings-page-CmBcQfeO.js.map} +1 -1
- package/dist/gateway/static/root/assets/{fetch-K_0JRCXU.js → fetch-EGO9T3MN.js} +3 -3
- package/dist/gateway/static/root/assets/{fetch-K_0JRCXU.js.map → fetch-EGO9T3MN.js.map} +1 -1
- package/dist/gateway/static/root/assets/{field-primitives-Z76hyBYS.js → field-primitives-Bh7G1y4D.js} +2 -2
- package/dist/gateway/static/root/assets/{field-primitives-Z76hyBYS.js.map → field-primitives-Bh7G1y4D.js.map} +1 -1
- package/dist/gateway/static/root/assets/{heartbeat-config-api-BqfDabSI.js → heartbeat-config-api-DxpIEZNs.js} +2 -2
- package/dist/gateway/static/root/assets/{heartbeat-config-api-BqfDabSI.js.map → heartbeat-config-api-DxpIEZNs.js.map} +1 -1
- package/dist/gateway/static/root/assets/{index-ChiUhJAs.js → index-Dxy9ZCtC.js} +5 -5
- package/dist/gateway/static/root/assets/{index-ChiUhJAs.js.map → index-Dxy9ZCtC.js.map} +1 -1
- package/dist/gateway/static/root/assets/{logs-page-DrIMhDE2.js → logs-page-Dw58E2GE.js} +2 -2
- package/dist/gateway/static/root/assets/{logs-page-DrIMhDE2.js.map → logs-page-Dw58E2GE.js.map} +1 -1
- package/dist/gateway/static/root/assets/{sessions-page-B-RGO3N0.js → sessions-page-CPkhCy57.js} +2 -2
- package/dist/gateway/static/root/assets/{sessions-page-B-RGO3N0.js.map → sessions-page-CPkhCy57.js.map} +1 -1
- package/dist/gateway/static/root/assets/{settings-form-section-Csvl1iL6.js → settings-form-section-DLZDVMEf.js} +2 -2
- package/dist/gateway/static/root/assets/{settings-form-section-Csvl1iL6.js.map → settings-form-section-DLZDVMEf.js.map} +1 -1
- package/dist/gateway/static/root/assets/settings-page-CVPCa0PE.js +4 -0
- package/dist/gateway/static/root/assets/settings-page-CVPCa0PE.js.map +1 -0
- package/dist/gateway/static/root/assets/{skills-page-dHwx2vh0.js → skills-page-DueZ9Qfg.js} +2 -2
- package/dist/gateway/static/root/assets/{skills-page-dHwx2vh0.js.map → skills-page-DueZ9Qfg.js.map} +1 -1
- package/dist/gateway/static/root/assets/{theme-store-Bl5A2Fd_.js → theme-store-CWPq9gW1.js} +2 -2
- package/dist/gateway/static/root/assets/{theme-store-Bl5A2Fd_.js.map → theme-store-CWPq9gW1.js.map} +1 -1
- package/dist/gateway/static/root/assets/{utils-COYrNFF7.js → utils-Cnix55r9.js} +2 -2
- package/dist/gateway/static/root/assets/{utils-COYrNFF7.js.map → utils-Cnix55r9.js.map} +1 -1
- package/dist/gateway/static/root/assets/{voice-api-key-field-5WZZaxH3.js → voice-api-key-field-BR3Ut06g.js} +2 -2
- package/dist/gateway/static/root/assets/{voice-api-key-field-5WZZaxH3.js.map → voice-api-key-field-BR3Ut06g.js.map} +1 -1
- package/dist/gateway/static/root/index.html +3 -3
- package/dist/package.js +1 -1
- package/dist/src/browser/providers/browser-ext-install.js +23 -4
- package/dist/src/browser/providers/browser-ext-install.js.map +1 -1
- package/dist/src/cli/commands/tunnel.js +4 -5
- package/dist/src/cli/commands/tunnel.js.map +1 -1
- package/dist/src/config/index.js +2 -2
- package/dist/src/config/rules.js +0 -5
- package/dist/src/config/rules.js.map +1 -1
- package/dist/src/config/schema.d.ts +0 -27
- package/dist/src/config/schema.js +4 -18
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/gateway/auth-rate-limit.d.ts +2 -0
- package/dist/src/gateway/auth-rate-limit.js +9 -3
- package/dist/src/gateway/auth-rate-limit.js.map +1 -1
- package/dist/src/gateway/hono/app.js +19 -13
- package/dist/src/gateway/hono/app.js.map +1 -1
- package/dist/src/gateway/hono/lib/config-payload.d.ts +3 -1
- package/dist/src/gateway/hono/lib/config-payload.js +1 -2
- package/dist/src/gateway/hono/lib/config-payload.js.map +1 -1
- package/dist/src/gateway/hono/routes/tunnel.js +32 -30
- package/dist/src/gateway/hono/routes/tunnel.js.map +1 -1
- package/dist/src/gateway/host.d.ts +24 -0
- package/dist/src/gateway/host.js +33 -1
- package/dist/src/gateway/host.js.map +1 -1
- package/dist/src/gateway/index.d.ts +1 -1
- package/dist/src/gateway/index.js +2 -2
- package/dist/src/gateway/runtime-config.js +1 -8
- package/dist/src/gateway/runtime-config.js.map +1 -1
- package/dist/src/gateway/security/audit.js +4 -4
- package/dist/src/gateway/security/audit.js.map +1 -1
- package/dist/src/gateway/server.js +2 -3
- package/dist/src/gateway/server.js.map +1 -1
- package/dist/src/gateway/service/types.d.ts +2 -0
- package/dist/src/gateway/service.d.ts +2 -0
- package/dist/src/gateway/service.js +7 -2
- package/dist/src/gateway/service.js.map +1 -1
- package/dist/src/tunnel/frp-subdomain-host.d.ts +2 -0
- package/dist/src/tunnel/frp-subdomain-host.js +15 -0
- package/dist/src/tunnel/frp-subdomain-host.js.map +1 -0
- package/dist/src/tunnel/gateway-lifecycle.d.ts +0 -4
- package/dist/src/tunnel/gateway-lifecycle.js +9 -11
- package/dist/src/tunnel/gateway-lifecycle.js.map +1 -1
- package/dist/src/tunnel/index.d.ts +2 -4
- package/dist/src/tunnel/index.js +2 -4
- package/dist/src/tunnel/pair-url.js +7 -1
- package/dist/src/tunnel/pair-url.js.map +1 -1
- package/dist/src/tunnel/pairing.d.ts +13 -0
- package/dist/src/tunnel/pairing.js +48 -1
- package/dist/src/tunnel/pairing.js.map +1 -1
- package/dist/src/tunnel/tunnel-config.js +2 -16
- package/dist/src/tunnel/tunnel-config.js.map +1 -1
- package/dist/src/tunnel/tunnel-service.d.ts +1 -10
- package/dist/src/tunnel/tunnel-service.js +7 -60
- package/dist/src/tunnel/tunnel-service.js.map +1 -1
- package/dist/src/tunnel/tunnel-types.d.ts +3 -18
- package/dist/src/tunnel/well-known.d.ts +5 -0
- package/dist/src/tunnel/well-known.js +2 -1
- package/dist/src/tunnel/well-known.js.map +1 -1
- package/package.json +2 -2
- package/dist/gateway/static/root/assets/settings-page-nxAc0ta1.js +0 -4
- package/dist/gateway/static/root/assets/settings-page-nxAc0ta1.js.map +0 -1
- package/dist/src/tunnel/acme-cert-store.d.ts +0 -34
- package/dist/src/tunnel/acme-cert-store.js +0 -184
- package/dist/src/tunnel/acme-cert-store.js.map +0 -1
- package/dist/src/tunnel/acme-client.d.ts +0 -50
- package/dist/src/tunnel/acme-client.js +0 -473
- package/dist/src/tunnel/acme-client.js.map +0 -1
- package/dist/src/tunnel/acme-crypto.d.ts +0 -25
- package/dist/src/tunnel/acme-crypto.js +0 -58
- package/dist/src/tunnel/acme-crypto.js.map +0 -1
- package/dist/src/tunnel/acme-csr.d.ts +0 -5
- package/dist/src/tunnel/acme-csr.js +0 -48
- package/dist/src/tunnel/acme-csr.js.map +0 -1
- package/dist/src/tunnel/tls-server.d.ts +0 -14
- package/dist/src/tunnel/tls-server.js +0 -126
- package/dist/src/tunnel/tls-server.js.map +0 -1
- package/dist/src/tunnel/tunnel-e2e-config.d.ts +0 -11
- package/dist/src/tunnel/tunnel-e2e-config.js +0 -29
- package/dist/src/tunnel/tunnel-e2e-config.js.map +0 -1
|
@@ -111,10 +111,10 @@ function collectGatewayConfigFindings(params) {
|
|
|
111
111
|
});
|
|
112
112
|
if (!loopback && (!params.corsOrigins || params.corsOrigins.length === 0) && params.dangerouslyAllowHostHeaderOriginFallback !== true) findings.push({
|
|
113
113
|
checkId: "gateway.cors.no_explicit_origins",
|
|
114
|
-
severity: "
|
|
115
|
-
title: "No
|
|
116
|
-
detail: "Gateway
|
|
117
|
-
remediation: "
|
|
114
|
+
severity: "info",
|
|
115
|
+
title: "No custom CORS origins — loopback browsers only",
|
|
116
|
+
detail: "Gateway listens on a network address with empty gateway.corsOrigins. localhost and 127.0.0.1 on the gateway port are allowed by default; LAN or remote browser URLs must be added explicitly.",
|
|
117
|
+
remediation: "Add gateway.corsOrigins entries (e.g. http://192.168.x.x:<port>) for non-loopback browser access, or enable gateway.dangerouslyAllowHostHeaderOriginFallback only if you understand the CSRF risk."
|
|
118
118
|
});
|
|
119
119
|
if (!loopback && params.auth.mode !== "none" && params.auth.mode !== "trusted-proxy" && params.rateLimitEnabled === false) findings.push({
|
|
120
120
|
checkId: "gateway.auth.no_rate_limit",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit.js","names":[],"sources":["../../../../src/gateway/security/audit.ts"],"sourcesContent":["import type { Config } from '../../config/schema.js';\nimport type { ResolvedGatewayAuth } from '../auth.js';\nimport { resolveGatewayAuth, assertGatewayAuthConfigured } from '../auth.js';\nimport { isAuthRateLimitGloballyDisabled, isGatewayStrictSecurityEnabled } from '../auth-rate-limit.js';\nimport { assertGatewayRuntimeConfig } from '../runtime-config.js';\nimport { resolveGatewayListenPlan } from '../listen.js';\nimport {\n collectExposureConflicts,\n isRemoteGatewayInsecure,\n isTailnetBindUnavailable,\n} from '../../remote-access/exposure-guards.js';\nimport { createLogger } from '../../utils/logger.js';\n\nconst log = createLogger('SecurityAudit');\n\nexport type SecurityAuditFinding = {\n checkId: string;\n severity: 'critical' | 'warn' | 'info';\n title: string;\n detail: string;\n /** Actionable remediation suggestion (aligned with OpenClaw audit format). */\n remediation?: string;\n};\n\n/** Minimum token length to resist brute-force even with rate limiting. */\nconst MIN_AUDIT_TOKEN_LENGTH = 22;\n\nfunction isLoopbackHost(host: string | undefined): boolean {\n return !host ||\n host === '127.0.0.1' ||\n host === 'localhost' ||\n host === '::1';\n}\n\nfunction normalizeCorsOrigins(cfg: Config): string[] {\n return (cfg.gateway?.corsOrigins ?? [])\n .map((value) => value.trim())\n .filter(Boolean);\n}\n\nfunction resolveAuditInputs(cfg: Config, env: NodeJS.ProcessEnv = process.env): {\n auth: ResolvedGatewayAuth;\n bindHost: string;\n corsOrigins: string[];\n rateLimitEnabled: boolean;\n tlsEnabled: boolean;\n trustedProxies?: string[];\n allowRealIpFallback: boolean;\n dangerouslyAllowHostHeaderOriginFallback: boolean;\n loopback: boolean;\n} {\n const auth = resolveGatewayAuth({ authConfig: cfg.gateway?.auth, env });\n const plan = resolveGatewayListenPlan({ cfg });\n const corsOrigins = normalizeCorsOrigins(cfg);\n const rateLimitEnabled =\n cfg.gateway?.auth?.rateLimit?.enabled !== false &&\n !isAuthRateLimitGloballyDisabled();\n const tlsEnabled =\n cfg.tunnel?.enabled === true ||\n (cfg.gateway?.tailscale?.mode ?? 'off') !== 'off' ||\n cfg.gateway?.tls?.enabled === true;\n const loopback = isLoopbackHost(plan.bindHost);\n\n return {\n auth,\n bindHost: plan.bindHost,\n corsOrigins,\n rateLimitEnabled,\n tlsEnabled,\n trustedProxies: cfg.gateway?.trustedProxies,\n allowRealIpFallback: cfg.gateway?.allowRealIpFallback === true,\n dangerouslyAllowHostHeaderOriginFallback:\n cfg.gateway?.dangerouslyAllowHostHeaderOriginFallback === true,\n loopback,\n };\n}\n\n/**\n * Pure gateway config audit (no logging). Shared by startup audit and `xopc doctor`.\n */\nexport function collectGatewayConfigFindings(params: {\n auth: ResolvedGatewayAuth;\n bindHost?: string;\n corsOrigins?: string[];\n rateLimitEnabled?: boolean;\n tlsEnabled?: boolean;\n trustedProxies?: string[];\n allowRealIpFallback?: boolean;\n dangerouslyAllowHostHeaderOriginFallback?: boolean;\n strictSecurityEnabled?: boolean;\n rateLimitConfigured?: boolean;\n}): SecurityAuditFinding[] {\n const findings: SecurityAuditFinding[] = [];\n const loopback = isLoopbackHost(params.bindHost);\n\n if (params.auth.mode === 'none') {\n if (!loopback) {\n findings.push({\n checkId: 'gateway.auth.none_on_network',\n severity: 'critical',\n title: 'Gateway has no authentication on a network-accessible address',\n detail: `Auth mode is \"none\" but gateway binds to ${params.bindHost}. ` +\n 'Any host on the network can access the gateway without credentials.',\n remediation: 'Set gateway.auth.mode to \"token\" and configure a strong token ' +\n '(e.g. `openssl rand -hex 32`).',\n });\n } else {\n findings.push({\n checkId: 'gateway.auth.none_loopback',\n severity: 'warn',\n title: 'Gateway authentication is disabled',\n detail: 'Auth mode is \"none\". This is acceptable for local development ' +\n 'but should not be used in production.',\n remediation: 'Set gateway.auth.mode to \"token\" for production use.',\n });\n }\n }\n\n if (\n !loopback &&\n params.auth.mode === 'token' &&\n !params.auth.token?.trim()\n ) {\n findings.push({\n checkId: 'gateway.auth.missing_token_on_network',\n severity: 'critical',\n title: 'Network-accessible gateway has no auth token configured',\n detail: 'gateway.auth.mode is \"token\" but no token is configured for a non-loopback bind.',\n remediation: 'Set gateway.auth.token or XOPC_GATEWAY_TOKEN before binding to the network.',\n });\n }\n\n if (\n !loopback &&\n params.auth.mode === 'password' &&\n !params.auth.password?.trim()\n ) {\n findings.push({\n checkId: 'gateway.auth.missing_password_on_network',\n severity: 'critical',\n title: 'Network-accessible gateway has no auth password configured',\n detail: 'gateway.auth.mode is \"password\" but no password is configured for a non-loopback bind.',\n remediation: 'Set gateway.auth.password or XOPC_GATEWAY_PASSWORD before binding to the network.',\n });\n }\n\n if (params.auth.mode === 'token' && params.auth.token) {\n const token = params.auth.token;\n\n if (token.length < MIN_AUDIT_TOKEN_LENGTH) {\n findings.push({\n checkId: 'gateway.auth.short_token',\n severity: 'warn',\n title: 'Gateway token is short',\n detail: `Token length is ${token.length} characters. Short tokens are vulnerable ` +\n 'to brute-force even with rate limiting.',\n remediation: `Use a token of at least ${MIN_AUDIT_TOKEN_LENGTH} characters ` +\n '(e.g. `openssl rand -hex 32`).',\n });\n }\n\n if (/^(.)\\1+$/.test(token)) {\n findings.push({\n checkId: 'gateway.auth.low_entropy_token',\n severity: 'critical',\n title: 'Gateway token has extremely low entropy',\n detail: 'Token consists of a single repeated character, making it trivially guessable.',\n remediation: 'Generate a cryptographically random token: `openssl rand -hex 32`.',\n });\n }\n\n const envToken = process.env.XOPC_GATEWAY_TOKEN;\n if (!envToken) {\n findings.push({\n checkId: 'gateway.auth.auto_generated_token',\n severity: 'info',\n title: 'Gateway token was auto-generated',\n detail: 'No explicit XOPC_GATEWAY_TOKEN set. The token was auto-generated and will ' +\n 'change on each restart.',\n remediation: 'Set XOPC_GATEWAY_TOKEN environment variable for a stable token.',\n });\n }\n }\n\n if (params.corsOrigins?.includes('*')) {\n findings.push({\n checkId: 'gateway.cors.wildcard',\n severity: !loopback ? 'critical' : 'warn',\n title: 'CORS allows all origins',\n detail: 'corsOrigins includes \"*\". Any website can make authenticated API calls ' +\n 'to the gateway if it can obtain the token.',\n remediation: 'Replace \"*\" with explicit allowed origins ' +\n '(e.g. [\"http://localhost:18790\"]).',\n });\n }\n\n if (params.corsOrigins && params.corsOrigins.length > 20) {\n findings.push({\n checkId: 'gateway.cors.excessive_origins',\n severity: 'info',\n title: 'Large number of CORS origins configured',\n detail: `${params.corsOrigins.length} CORS origins configured. Review whether all are necessary.`,\n });\n }\n\n if (\n !loopback &&\n (!params.corsOrigins || params.corsOrigins.length === 0) &&\n params.dangerouslyAllowHostHeaderOriginFallback !== true\n ) {\n findings.push({\n checkId: 'gateway.cors.no_explicit_origins',\n severity: 'critical',\n title: 'No explicit CORS origins on network-accessible gateway',\n detail: 'Gateway is bound to a non-loopback address but no corsOrigins are configured. ' +\n 'Startup guards will refuse to bind until origins are set.',\n remediation:\n 'Set gateway.corsOrigins to the browser URLs that should access the gateway, ' +\n 'or enable gateway.dangerouslyAllowHostHeaderOriginFallback only if you understand the CSRF risk.',\n });\n }\n\n if (\n !loopback &&\n params.auth.mode !== 'none' &&\n params.auth.mode !== 'trusted-proxy' &&\n params.rateLimitEnabled === false\n ) {\n findings.push({\n checkId: 'gateway.auth.no_rate_limit',\n severity: 'warn',\n title: 'No auth rate limiting on network-accessible gateway',\n detail: 'Gateway is bound to a non-loopback address but auth rate limiting is disabled. ' +\n 'This allows unlimited brute-force authentication attempts.',\n remediation: 'Set gateway.auth.rateLimit ' +\n '(e.g. { maxAttempts: 10, windowMs: 60000, lockoutMs: 300000 }).',\n });\n }\n\n if (\n !loopback &&\n params.strictSecurityEnabled === true &&\n params.rateLimitConfigured !== true\n ) {\n findings.push({\n checkId: 'gateway.security.strict_no_rate_limit',\n severity: 'critical',\n title: 'Strict security requires explicit auth rate limit configuration',\n detail: 'gateway.security.strict is enabled on a network-accessible bind but gateway.auth.rateLimit is missing.',\n remediation:\n 'Set gateway.auth.rateLimit (e.g. { maxAttempts: 10, windowMs: 60000, blockDurationMs: 300000 }).',\n });\n }\n\n if (!loopback && !params.tlsEnabled) {\n findings.push({\n checkId: 'gateway.transport.no_tls',\n severity: 'warn',\n title: 'No TLS on network-accessible gateway',\n detail: 'Gateway is bound to a non-loopback address without TLS termination. ' +\n 'Tokens and data are transmitted in plaintext unless a reverse proxy or tunnel handles HTTPS.',\n remediation:\n 'Enable the tunnel feature (`tunnel.enabled`), terminate TLS at a reverse proxy (Caddy/nginx), ' +\n 'or bind to loopback and access via SSH/VPN.',\n });\n }\n\n if (params.bindHost === '0.0.0.0' || params.bindHost === '::') {\n findings.push({\n checkId: 'gateway.bind.all_interfaces',\n severity: 'warn',\n title: 'Gateway binds to all network interfaces',\n detail: 'Binding to all interfaces exposes the gateway on every network interface. ' +\n 'Prefer loopback unless remote access is required.',\n remediation: 'Set gateway.bind to \"loopback\" unless remote access is required.',\n });\n }\n\n if (params.auth.mode === 'trusted-proxy') {\n const trustedProxies = params.trustedProxies ?? [];\n const trustedProxyConfig = params.auth.trustedProxy;\n\n findings.push({\n checkId: 'gateway.trusted_proxy_auth',\n severity: 'critical',\n title: 'Trusted-proxy auth mode enabled',\n detail:\n 'gateway.auth.mode=\"trusted-proxy\" delegates authentication to a reverse proxy. ' +\n 'Ensure your proxy terminates TLS and authenticates users; gateway.trustedProxies ' +\n 'must only list your proxy server IPs.',\n remediation:\n 'Verify: (1) Proxy terminates TLS and authenticates users. ' +\n '(2) gateway.trustedProxies is restricted to proxy IPs only. ' +\n '(3) Direct access to the gateway port is blocked by firewall.',\n });\n\n if (trustedProxies.length === 0) {\n findings.push({\n checkId: 'gateway.trusted_proxy_no_proxies',\n severity: 'critical',\n title: 'Trusted-proxy auth enabled but no trusted proxies configured',\n detail:\n 'gateway.auth.mode=\"trusted-proxy\" but gateway.trustedProxies is empty. ' +\n 'All requests will be rejected and startup guards will fail.',\n remediation: 'Set gateway.trustedProxies to the IP(s) of your reverse proxy.',\n });\n }\n\n if (!trustedProxyConfig?.userHeader) {\n findings.push({\n checkId: 'gateway.trusted_proxy_no_user_header',\n severity: 'critical',\n title: 'Trusted-proxy auth missing userHeader config',\n detail:\n 'gateway.auth.mode=\"trusted-proxy\" but gateway.auth.trustedProxy.userHeader is not configured.',\n remediation:\n 'Set gateway.auth.trustedProxy.userHeader to the header your proxy uses ' +\n '(e.g. \"x-forwarded-user\", \"x-pomerium-claim-email\").',\n });\n }\n\n if (trustedProxyConfig?.allowLoopback === true) {\n findings.push({\n checkId: 'gateway.trusted_proxy_allow_loopback',\n severity: 'warn',\n title: 'Trusted-proxy auth allows loopback proxy sources',\n detail:\n 'gateway.auth.trustedProxy.allowLoopback=true allows loopback-source requests ' +\n 'from configured gateway.trustedProxies entries to satisfy trusted-proxy auth.',\n remediation:\n 'Enable only when a same-host reverse proxy is the intended trust boundary.',\n });\n }\n\n const allowUsers = trustedProxyConfig?.allowUsers ?? [];\n if (allowUsers.length === 0) {\n findings.push({\n checkId: 'gateway.trusted_proxy_no_allowlist',\n severity: 'warn',\n title: 'Trusted-proxy auth allows all authenticated users',\n detail:\n 'gateway.auth.trustedProxy.allowUsers is empty, so any user authenticated by your proxy can access the gateway.',\n remediation:\n 'Consider setting gateway.auth.trustedProxy.allowUsers to restrict access to specific users.',\n });\n }\n\n if (params.allowRealIpFallback === true) {\n findings.push({\n checkId: 'gateway.trusted_proxy_real_ip_fallback',\n severity: 'warn',\n title: 'X-Real-IP fallback is enabled for trusted-proxy client IP resolution',\n detail:\n 'gateway.allowRealIpFallback=true uses X-Real-IP when X-Forwarded-For chain parsing fails.',\n remediation:\n 'Keep gateway.allowRealIpFallback=false unless your trusted proxy only sets X-Real-IP.',\n });\n }\n }\n\n return findings;\n}\n\nexport function collectExposureAuditFindings(cfg: Config): SecurityAuditFinding[] {\n const findings: SecurityAuditFinding[] = [];\n const tailscaleMode = cfg.gateway?.tailscale?.mode ?? 'off';\n const bindMode = cfg.gateway?.bind ?? 'loopback';\n\n for (const conflict of collectExposureConflicts(cfg)) {\n findings.push({\n checkId: `gateway.exposure.${conflict.code}`,\n severity: 'critical',\n title: 'Remote exposure configuration conflict',\n detail: conflict.message,\n remediation: 'Adjust gateway.tailscale and tunnel settings so only one auto-exposure path is active.',\n });\n }\n\n if (tailscaleMode === 'funnel' && cfg.gateway?.auth?.mode !== 'password') {\n findings.push({\n checkId: 'gateway.tailscale.funnel_without_password',\n severity: 'critical',\n title: 'Tailscale Funnel requires password auth',\n detail: 'gateway.tailscale.mode=funnel exposes the gateway on the public internet and requires gateway.auth.mode=password.',\n remediation: 'Set gateway.auth.mode to password and configure gateway.auth.password.',\n });\n }\n\n if (tailscaleMode !== 'off' && bindMode !== 'loopback') {\n findings.push({\n checkId: 'gateway.tailscale.serve_with_non_loopback_bind',\n severity: 'critical',\n title: 'Tailscale exposure requires loopback bind',\n detail: `Tailscale ${tailscaleMode} is enabled but gateway.bind=${bindMode}.`,\n remediation: 'Set gateway.bind to loopback when using Tailscale Serve or Funnel.',\n });\n }\n\n if (isTailnetBindUnavailable(cfg)) {\n findings.push({\n checkId: 'gateway.bind.tailnet_ip_unavailable',\n severity: 'warn',\n title: 'Tailnet bind requested but Tailscale IP unavailable',\n detail: 'gateway.bind=tailnet but no Tailscale IPv4 (100.x) was detected; gateway falls back to loopback.',\n remediation: 'Install and connect Tailscale, or use gateway.tailscale.mode=serve instead.',\n });\n }\n\n if (isRemoteGatewayInsecure(cfg)) {\n findings.push({\n checkId: 'gateway.remote.insecure_url',\n severity: 'warn',\n title: 'Remote gateway URL uses plaintext HTTP',\n detail: 'gateway.mode=remote points to a non-loopback http:// URL without TLS.',\n remediation: 'Use https://, SSH tunnel to loopback, or Tailscale Serve.',\n });\n }\n\n if (tailscaleMode === 'funnel') {\n findings.push({\n checkId: 'gateway.tailscale.funnel_public',\n severity: 'critical',\n title: 'Tailscale Funnel exposes gateway to the public internet',\n detail: 'Funnel publishes HTTPS endpoints reachable from the public internet.',\n remediation: 'Prefer Tailscale Serve for tailnet-only access, or use FRP with consent for controlled public exposure.',\n });\n }\n\n return findings;\n}\n\n/** Findings from fail-closed startup guards (`assertGatewayRuntimeConfig`). */\nexport function collectGatewayStartupGuardFindings(\n cfg: Config,\n env: NodeJS.ProcessEnv = process.env,\n): SecurityAuditFinding[] {\n try {\n const auth = resolveGatewayAuth({ authConfig: cfg.gateway?.auth, env });\n assertGatewayAuthConfigured(auth);\n assertGatewayRuntimeConfig({\n cfg,\n auth,\n port: cfg.gateway?.port ?? 18790,\n });\n return [];\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return [{\n checkId: 'gateway.runtime_config.blocked',\n severity: 'critical',\n title: 'Gateway startup guards would reject this configuration',\n detail: message,\n remediation: 'Fix the configuration issue above, then run `xopc gateway` again.',\n }];\n }\n}\n\nfunction mergeFindings(findings: SecurityAuditFinding[]): SecurityAuditFinding[] {\n const byId = new Map<string, SecurityAuditFinding>();\n const severityRank = { critical: 3, warn: 2, info: 1 } as const;\n\n for (const finding of findings) {\n const existing = byId.get(finding.checkId);\n if (!existing || severityRank[finding.severity] > severityRank[existing.severity]) {\n byId.set(finding.checkId, finding);\n }\n }\n return [...byId.values()];\n}\n\n/**\n * Full gateway security findings for doctor / CLI audit (config + startup guards).\n */\nexport function collectGatewaySecurityFindings(\n cfg: Config,\n env: NodeJS.ProcessEnv = process.env,\n): SecurityAuditFinding[] {\n const inputs = resolveAuditInputs(cfg, env);\n const configFindings = collectGatewayConfigFindings({\n auth: inputs.auth,\n bindHost: inputs.bindHost,\n corsOrigins: inputs.corsOrigins,\n rateLimitEnabled: inputs.rateLimitEnabled,\n tlsEnabled: inputs.tlsEnabled,\n trustedProxies: inputs.trustedProxies,\n allowRealIpFallback: inputs.allowRealIpFallback,\n dangerouslyAllowHostHeaderOriginFallback: inputs.dangerouslyAllowHostHeaderOriginFallback,\n strictSecurityEnabled: isGatewayStrictSecurityEnabled(cfg),\n rateLimitConfigured: cfg.gateway?.auth?.rateLimit !== undefined,\n });\n const startupFindings = collectGatewayStartupGuardFindings(cfg, env);\n const exposureFindings = collectExposureAuditFindings(cfg);\n return mergeFindings([...configFindings, ...startupFindings, ...exposureFindings]);\n}\n\nfunction emitFindings(findings: SecurityAuditFinding[]): void {\n for (const finding of findings) {\n const logData = {\n checkId: finding.checkId,\n detail: finding.detail,\n ...(finding.remediation ? { remediation: finding.remediation } : {}),\n };\n switch (finding.severity) {\n case 'critical':\n log.error(logData, `Security audit: ${finding.title}`);\n break;\n case 'warn':\n log.warn(logData, `Security audit: ${finding.title}`);\n break;\n case 'info':\n log.info(logData, `Security audit: ${finding.title}`);\n break;\n }\n }\n}\n\n/**\n * Audit the gateway configuration at startup and log security findings.\n */\nexport function auditGatewayConfig(params: {\n auth: ResolvedGatewayAuth;\n bindHost?: string;\n corsOrigins?: string[];\n rateLimitEnabled?: boolean;\n tlsEnabled?: boolean;\n trustedProxies?: string[];\n allowRealIpFallback?: boolean;\n dangerouslyAllowHostHeaderOriginFallback?: boolean;\n strictSecurityEnabled?: boolean;\n rateLimitConfigured?: boolean;\n}): SecurityAuditFinding[] {\n const findings = collectGatewayConfigFindings(params);\n emitFindings(findings);\n return findings;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"audit.js","names":[],"sources":["../../../../src/gateway/security/audit.ts"],"sourcesContent":["import type { Config } from '../../config/schema.js';\nimport type { ResolvedGatewayAuth } from '../auth.js';\nimport { resolveGatewayAuth, assertGatewayAuthConfigured } from '../auth.js';\nimport { isAuthRateLimitGloballyDisabled, isGatewayStrictSecurityEnabled } from '../auth-rate-limit.js';\nimport { assertGatewayRuntimeConfig } from '../runtime-config.js';\nimport { resolveGatewayListenPlan } from '../listen.js';\nimport {\n collectExposureConflicts,\n isRemoteGatewayInsecure,\n isTailnetBindUnavailable,\n} from '../../remote-access/exposure-guards.js';\nimport { createLogger } from '../../utils/logger.js';\n\nconst log = createLogger('SecurityAudit');\n\nexport type SecurityAuditFinding = {\n checkId: string;\n severity: 'critical' | 'warn' | 'info';\n title: string;\n detail: string;\n /** Actionable remediation suggestion (aligned with OpenClaw audit format). */\n remediation?: string;\n};\n\n/** Minimum token length to resist brute-force even with rate limiting. */\nconst MIN_AUDIT_TOKEN_LENGTH = 22;\n\nfunction isLoopbackHost(host: string | undefined): boolean {\n return !host ||\n host === '127.0.0.1' ||\n host === 'localhost' ||\n host === '::1';\n}\n\nfunction normalizeCorsOrigins(cfg: Config): string[] {\n return (cfg.gateway?.corsOrigins ?? [])\n .map((value) => value.trim())\n .filter(Boolean);\n}\n\nfunction resolveAuditInputs(cfg: Config, env: NodeJS.ProcessEnv = process.env): {\n auth: ResolvedGatewayAuth;\n bindHost: string;\n corsOrigins: string[];\n rateLimitEnabled: boolean;\n tlsEnabled: boolean;\n trustedProxies?: string[];\n allowRealIpFallback: boolean;\n dangerouslyAllowHostHeaderOriginFallback: boolean;\n loopback: boolean;\n} {\n const auth = resolveGatewayAuth({ authConfig: cfg.gateway?.auth, env });\n const plan = resolveGatewayListenPlan({ cfg });\n const corsOrigins = normalizeCorsOrigins(cfg);\n const rateLimitEnabled =\n cfg.gateway?.auth?.rateLimit?.enabled !== false &&\n !isAuthRateLimitGloballyDisabled();\n const tlsEnabled =\n cfg.tunnel?.enabled === true ||\n (cfg.gateway?.tailscale?.mode ?? 'off') !== 'off' ||\n cfg.gateway?.tls?.enabled === true;\n const loopback = isLoopbackHost(plan.bindHost);\n\n return {\n auth,\n bindHost: plan.bindHost,\n corsOrigins,\n rateLimitEnabled,\n tlsEnabled,\n trustedProxies: cfg.gateway?.trustedProxies,\n allowRealIpFallback: cfg.gateway?.allowRealIpFallback === true,\n dangerouslyAllowHostHeaderOriginFallback:\n cfg.gateway?.dangerouslyAllowHostHeaderOriginFallback === true,\n loopback,\n };\n}\n\n/**\n * Pure gateway config audit (no logging). Shared by startup audit and `xopc doctor`.\n */\nexport function collectGatewayConfigFindings(params: {\n auth: ResolvedGatewayAuth;\n bindHost?: string;\n corsOrigins?: string[];\n rateLimitEnabled?: boolean;\n tlsEnabled?: boolean;\n trustedProxies?: string[];\n allowRealIpFallback?: boolean;\n dangerouslyAllowHostHeaderOriginFallback?: boolean;\n strictSecurityEnabled?: boolean;\n rateLimitConfigured?: boolean;\n}): SecurityAuditFinding[] {\n const findings: SecurityAuditFinding[] = [];\n const loopback = isLoopbackHost(params.bindHost);\n\n if (params.auth.mode === 'none') {\n if (!loopback) {\n findings.push({\n checkId: 'gateway.auth.none_on_network',\n severity: 'critical',\n title: 'Gateway has no authentication on a network-accessible address',\n detail: `Auth mode is \"none\" but gateway binds to ${params.bindHost}. ` +\n 'Any host on the network can access the gateway without credentials.',\n remediation: 'Set gateway.auth.mode to \"token\" and configure a strong token ' +\n '(e.g. `openssl rand -hex 32`).',\n });\n } else {\n findings.push({\n checkId: 'gateway.auth.none_loopback',\n severity: 'warn',\n title: 'Gateway authentication is disabled',\n detail: 'Auth mode is \"none\". This is acceptable for local development ' +\n 'but should not be used in production.',\n remediation: 'Set gateway.auth.mode to \"token\" for production use.',\n });\n }\n }\n\n if (\n !loopback &&\n params.auth.mode === 'token' &&\n !params.auth.token?.trim()\n ) {\n findings.push({\n checkId: 'gateway.auth.missing_token_on_network',\n severity: 'critical',\n title: 'Network-accessible gateway has no auth token configured',\n detail: 'gateway.auth.mode is \"token\" but no token is configured for a non-loopback bind.',\n remediation: 'Set gateway.auth.token or XOPC_GATEWAY_TOKEN before binding to the network.',\n });\n }\n\n if (\n !loopback &&\n params.auth.mode === 'password' &&\n !params.auth.password?.trim()\n ) {\n findings.push({\n checkId: 'gateway.auth.missing_password_on_network',\n severity: 'critical',\n title: 'Network-accessible gateway has no auth password configured',\n detail: 'gateway.auth.mode is \"password\" but no password is configured for a non-loopback bind.',\n remediation: 'Set gateway.auth.password or XOPC_GATEWAY_PASSWORD before binding to the network.',\n });\n }\n\n if (params.auth.mode === 'token' && params.auth.token) {\n const token = params.auth.token;\n\n if (token.length < MIN_AUDIT_TOKEN_LENGTH) {\n findings.push({\n checkId: 'gateway.auth.short_token',\n severity: 'warn',\n title: 'Gateway token is short',\n detail: `Token length is ${token.length} characters. Short tokens are vulnerable ` +\n 'to brute-force even with rate limiting.',\n remediation: `Use a token of at least ${MIN_AUDIT_TOKEN_LENGTH} characters ` +\n '(e.g. `openssl rand -hex 32`).',\n });\n }\n\n if (/^(.)\\1+$/.test(token)) {\n findings.push({\n checkId: 'gateway.auth.low_entropy_token',\n severity: 'critical',\n title: 'Gateway token has extremely low entropy',\n detail: 'Token consists of a single repeated character, making it trivially guessable.',\n remediation: 'Generate a cryptographically random token: `openssl rand -hex 32`.',\n });\n }\n\n const envToken = process.env.XOPC_GATEWAY_TOKEN;\n if (!envToken) {\n findings.push({\n checkId: 'gateway.auth.auto_generated_token',\n severity: 'info',\n title: 'Gateway token was auto-generated',\n detail: 'No explicit XOPC_GATEWAY_TOKEN set. The token was auto-generated and will ' +\n 'change on each restart.',\n remediation: 'Set XOPC_GATEWAY_TOKEN environment variable for a stable token.',\n });\n }\n }\n\n if (params.corsOrigins?.includes('*')) {\n findings.push({\n checkId: 'gateway.cors.wildcard',\n severity: !loopback ? 'critical' : 'warn',\n title: 'CORS allows all origins',\n detail: 'corsOrigins includes \"*\". Any website can make authenticated API calls ' +\n 'to the gateway if it can obtain the token.',\n remediation: 'Replace \"*\" with explicit allowed origins ' +\n '(e.g. [\"http://localhost:18790\"]).',\n });\n }\n\n if (params.corsOrigins && params.corsOrigins.length > 20) {\n findings.push({\n checkId: 'gateway.cors.excessive_origins',\n severity: 'info',\n title: 'Large number of CORS origins configured',\n detail: `${params.corsOrigins.length} CORS origins configured. Review whether all are necessary.`,\n });\n }\n\n if (\n !loopback &&\n (!params.corsOrigins || params.corsOrigins.length === 0) &&\n params.dangerouslyAllowHostHeaderOriginFallback !== true\n ) {\n findings.push({\n checkId: 'gateway.cors.no_explicit_origins',\n severity: 'info',\n title: 'No custom CORS origins — loopback browsers only',\n detail:\n 'Gateway listens on a network address with empty gateway.corsOrigins. ' +\n 'localhost and 127.0.0.1 on the gateway port are allowed by default; ' +\n 'LAN or remote browser URLs must be added explicitly.',\n remediation:\n 'Add gateway.corsOrigins entries (e.g. http://192.168.x.x:<port>) for non-loopback browser access, ' +\n 'or enable gateway.dangerouslyAllowHostHeaderOriginFallback only if you understand the CSRF risk.',\n });\n }\n\n if (\n !loopback &&\n params.auth.mode !== 'none' &&\n params.auth.mode !== 'trusted-proxy' &&\n params.rateLimitEnabled === false\n ) {\n findings.push({\n checkId: 'gateway.auth.no_rate_limit',\n severity: 'warn',\n title: 'No auth rate limiting on network-accessible gateway',\n detail: 'Gateway is bound to a non-loopback address but auth rate limiting is disabled. ' +\n 'This allows unlimited brute-force authentication attempts.',\n remediation: 'Set gateway.auth.rateLimit ' +\n '(e.g. { maxAttempts: 10, windowMs: 60000, lockoutMs: 300000 }).',\n });\n }\n\n if (\n !loopback &&\n params.strictSecurityEnabled === true &&\n params.rateLimitConfigured !== true\n ) {\n findings.push({\n checkId: 'gateway.security.strict_no_rate_limit',\n severity: 'critical',\n title: 'Strict security requires explicit auth rate limit configuration',\n detail: 'gateway.security.strict is enabled on a network-accessible bind but gateway.auth.rateLimit is missing.',\n remediation:\n 'Set gateway.auth.rateLimit (e.g. { maxAttempts: 10, windowMs: 60000, blockDurationMs: 300000 }).',\n });\n }\n\n if (!loopback && !params.tlsEnabled) {\n findings.push({\n checkId: 'gateway.transport.no_tls',\n severity: 'warn',\n title: 'No TLS on network-accessible gateway',\n detail: 'Gateway is bound to a non-loopback address without TLS termination. ' +\n 'Tokens and data are transmitted in plaintext unless a reverse proxy or tunnel handles HTTPS.',\n remediation:\n 'Enable the tunnel feature (`tunnel.enabled`), terminate TLS at a reverse proxy (Caddy/nginx), ' +\n 'or bind to loopback and access via SSH/VPN.',\n });\n }\n\n if (params.bindHost === '0.0.0.0' || params.bindHost === '::') {\n findings.push({\n checkId: 'gateway.bind.all_interfaces',\n severity: 'warn',\n title: 'Gateway binds to all network interfaces',\n detail: 'Binding to all interfaces exposes the gateway on every network interface. ' +\n 'Prefer loopback unless remote access is required.',\n remediation: 'Set gateway.bind to \"loopback\" unless remote access is required.',\n });\n }\n\n if (params.auth.mode === 'trusted-proxy') {\n const trustedProxies = params.trustedProxies ?? [];\n const trustedProxyConfig = params.auth.trustedProxy;\n\n findings.push({\n checkId: 'gateway.trusted_proxy_auth',\n severity: 'critical',\n title: 'Trusted-proxy auth mode enabled',\n detail:\n 'gateway.auth.mode=\"trusted-proxy\" delegates authentication to a reverse proxy. ' +\n 'Ensure your proxy terminates TLS and authenticates users; gateway.trustedProxies ' +\n 'must only list your proxy server IPs.',\n remediation:\n 'Verify: (1) Proxy terminates TLS and authenticates users. ' +\n '(2) gateway.trustedProxies is restricted to proxy IPs only. ' +\n '(3) Direct access to the gateway port is blocked by firewall.',\n });\n\n if (trustedProxies.length === 0) {\n findings.push({\n checkId: 'gateway.trusted_proxy_no_proxies',\n severity: 'critical',\n title: 'Trusted-proxy auth enabled but no trusted proxies configured',\n detail:\n 'gateway.auth.mode=\"trusted-proxy\" but gateway.trustedProxies is empty. ' +\n 'All requests will be rejected and startup guards will fail.',\n remediation: 'Set gateway.trustedProxies to the IP(s) of your reverse proxy.',\n });\n }\n\n if (!trustedProxyConfig?.userHeader) {\n findings.push({\n checkId: 'gateway.trusted_proxy_no_user_header',\n severity: 'critical',\n title: 'Trusted-proxy auth missing userHeader config',\n detail:\n 'gateway.auth.mode=\"trusted-proxy\" but gateway.auth.trustedProxy.userHeader is not configured.',\n remediation:\n 'Set gateway.auth.trustedProxy.userHeader to the header your proxy uses ' +\n '(e.g. \"x-forwarded-user\", \"x-pomerium-claim-email\").',\n });\n }\n\n if (trustedProxyConfig?.allowLoopback === true) {\n findings.push({\n checkId: 'gateway.trusted_proxy_allow_loopback',\n severity: 'warn',\n title: 'Trusted-proxy auth allows loopback proxy sources',\n detail:\n 'gateway.auth.trustedProxy.allowLoopback=true allows loopback-source requests ' +\n 'from configured gateway.trustedProxies entries to satisfy trusted-proxy auth.',\n remediation:\n 'Enable only when a same-host reverse proxy is the intended trust boundary.',\n });\n }\n\n const allowUsers = trustedProxyConfig?.allowUsers ?? [];\n if (allowUsers.length === 0) {\n findings.push({\n checkId: 'gateway.trusted_proxy_no_allowlist',\n severity: 'warn',\n title: 'Trusted-proxy auth allows all authenticated users',\n detail:\n 'gateway.auth.trustedProxy.allowUsers is empty, so any user authenticated by your proxy can access the gateway.',\n remediation:\n 'Consider setting gateway.auth.trustedProxy.allowUsers to restrict access to specific users.',\n });\n }\n\n if (params.allowRealIpFallback === true) {\n findings.push({\n checkId: 'gateway.trusted_proxy_real_ip_fallback',\n severity: 'warn',\n title: 'X-Real-IP fallback is enabled for trusted-proxy client IP resolution',\n detail:\n 'gateway.allowRealIpFallback=true uses X-Real-IP when X-Forwarded-For chain parsing fails.',\n remediation:\n 'Keep gateway.allowRealIpFallback=false unless your trusted proxy only sets X-Real-IP.',\n });\n }\n }\n\n return findings;\n}\n\nexport function collectExposureAuditFindings(cfg: Config): SecurityAuditFinding[] {\n const findings: SecurityAuditFinding[] = [];\n const tailscaleMode = cfg.gateway?.tailscale?.mode ?? 'off';\n const bindMode = cfg.gateway?.bind ?? 'loopback';\n\n for (const conflict of collectExposureConflicts(cfg)) {\n findings.push({\n checkId: `gateway.exposure.${conflict.code}`,\n severity: 'critical',\n title: 'Remote exposure configuration conflict',\n detail: conflict.message,\n remediation: 'Adjust gateway.tailscale and tunnel settings so only one auto-exposure path is active.',\n });\n }\n\n if (tailscaleMode === 'funnel' && cfg.gateway?.auth?.mode !== 'password') {\n findings.push({\n checkId: 'gateway.tailscale.funnel_without_password',\n severity: 'critical',\n title: 'Tailscale Funnel requires password auth',\n detail: 'gateway.tailscale.mode=funnel exposes the gateway on the public internet and requires gateway.auth.mode=password.',\n remediation: 'Set gateway.auth.mode to password and configure gateway.auth.password.',\n });\n }\n\n if (tailscaleMode !== 'off' && bindMode !== 'loopback') {\n findings.push({\n checkId: 'gateway.tailscale.serve_with_non_loopback_bind',\n severity: 'critical',\n title: 'Tailscale exposure requires loopback bind',\n detail: `Tailscale ${tailscaleMode} is enabled but gateway.bind=${bindMode}.`,\n remediation: 'Set gateway.bind to loopback when using Tailscale Serve or Funnel.',\n });\n }\n\n if (isTailnetBindUnavailable(cfg)) {\n findings.push({\n checkId: 'gateway.bind.tailnet_ip_unavailable',\n severity: 'warn',\n title: 'Tailnet bind requested but Tailscale IP unavailable',\n detail: 'gateway.bind=tailnet but no Tailscale IPv4 (100.x) was detected; gateway falls back to loopback.',\n remediation: 'Install and connect Tailscale, or use gateway.tailscale.mode=serve instead.',\n });\n }\n\n if (isRemoteGatewayInsecure(cfg)) {\n findings.push({\n checkId: 'gateway.remote.insecure_url',\n severity: 'warn',\n title: 'Remote gateway URL uses plaintext HTTP',\n detail: 'gateway.mode=remote points to a non-loopback http:// URL without TLS.',\n remediation: 'Use https://, SSH tunnel to loopback, or Tailscale Serve.',\n });\n }\n\n if (tailscaleMode === 'funnel') {\n findings.push({\n checkId: 'gateway.tailscale.funnel_public',\n severity: 'critical',\n title: 'Tailscale Funnel exposes gateway to the public internet',\n detail: 'Funnel publishes HTTPS endpoints reachable from the public internet.',\n remediation: 'Prefer Tailscale Serve for tailnet-only access, or use FRP with consent for controlled public exposure.',\n });\n }\n\n return findings;\n}\n\n/** Findings from fail-closed startup guards (`assertGatewayRuntimeConfig`). */\nexport function collectGatewayStartupGuardFindings(\n cfg: Config,\n env: NodeJS.ProcessEnv = process.env,\n): SecurityAuditFinding[] {\n try {\n const auth = resolveGatewayAuth({ authConfig: cfg.gateway?.auth, env });\n assertGatewayAuthConfigured(auth);\n assertGatewayRuntimeConfig({\n cfg,\n auth,\n port: cfg.gateway?.port ?? 18790,\n });\n return [];\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return [{\n checkId: 'gateway.runtime_config.blocked',\n severity: 'critical',\n title: 'Gateway startup guards would reject this configuration',\n detail: message,\n remediation: 'Fix the configuration issue above, then run `xopc gateway` again.',\n }];\n }\n}\n\nfunction mergeFindings(findings: SecurityAuditFinding[]): SecurityAuditFinding[] {\n const byId = new Map<string, SecurityAuditFinding>();\n const severityRank = { critical: 3, warn: 2, info: 1 } as const;\n\n for (const finding of findings) {\n const existing = byId.get(finding.checkId);\n if (!existing || severityRank[finding.severity] > severityRank[existing.severity]) {\n byId.set(finding.checkId, finding);\n }\n }\n return [...byId.values()];\n}\n\n/**\n * Full gateway security findings for doctor / CLI audit (config + startup guards).\n */\nexport function collectGatewaySecurityFindings(\n cfg: Config,\n env: NodeJS.ProcessEnv = process.env,\n): SecurityAuditFinding[] {\n const inputs = resolveAuditInputs(cfg, env);\n const configFindings = collectGatewayConfigFindings({\n auth: inputs.auth,\n bindHost: inputs.bindHost,\n corsOrigins: inputs.corsOrigins,\n rateLimitEnabled: inputs.rateLimitEnabled,\n tlsEnabled: inputs.tlsEnabled,\n trustedProxies: inputs.trustedProxies,\n allowRealIpFallback: inputs.allowRealIpFallback,\n dangerouslyAllowHostHeaderOriginFallback: inputs.dangerouslyAllowHostHeaderOriginFallback,\n strictSecurityEnabled: isGatewayStrictSecurityEnabled(cfg),\n rateLimitConfigured: cfg.gateway?.auth?.rateLimit !== undefined,\n });\n const startupFindings = collectGatewayStartupGuardFindings(cfg, env);\n const exposureFindings = collectExposureAuditFindings(cfg);\n return mergeFindings([...configFindings, ...startupFindings, ...exposureFindings]);\n}\n\nfunction emitFindings(findings: SecurityAuditFinding[]): void {\n for (const finding of findings) {\n const logData = {\n checkId: finding.checkId,\n detail: finding.detail,\n ...(finding.remediation ? { remediation: finding.remediation } : {}),\n };\n switch (finding.severity) {\n case 'critical':\n log.error(logData, `Security audit: ${finding.title}`);\n break;\n case 'warn':\n log.warn(logData, `Security audit: ${finding.title}`);\n break;\n case 'info':\n log.info(logData, `Security audit: ${finding.title}`);\n break;\n }\n }\n}\n\n/**\n * Audit the gateway configuration at startup and log security findings.\n */\nexport function auditGatewayConfig(params: {\n auth: ResolvedGatewayAuth;\n bindHost?: string;\n corsOrigins?: string[];\n rateLimitEnabled?: boolean;\n tlsEnabled?: boolean;\n trustedProxies?: string[];\n allowRealIpFallback?: boolean;\n dangerouslyAllowHostHeaderOriginFallback?: boolean;\n strictSecurityEnabled?: boolean;\n rateLimitConfigured?: boolean;\n}): SecurityAuditFinding[] {\n const findings = collectGatewayConfigFindings(params);\n emitFindings(findings);\n return findings;\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { resolveGatewayBindHost, resolveGatewayListenHosts } from "../config/gateway-bind.js";
|
|
2
2
|
import { resolveGatewayListenPlan } from "./listen.js";
|
|
3
|
-
import { getTunnelService } from "../tunnel/tunnel-service.js";
|
|
4
3
|
import { GatewayService } from "./service.js";
|
|
5
4
|
import { createHonoApp } from "./hono/app.js";
|
|
6
5
|
import { serve } from "@hono/node-server";
|
|
@@ -17,7 +16,8 @@ var GatewayServer = class {
|
|
|
17
16
|
enableHotReload: config.enableHotReload,
|
|
18
17
|
deferChannelConnectUntilAfterHttp: true,
|
|
19
18
|
listenBind: config.bind,
|
|
20
|
-
listenCustomBindHost: config.customBindHost
|
|
19
|
+
listenCustomBindHost: config.customBindHost,
|
|
20
|
+
listenPort: config.port
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
23
|
async start() {
|
|
@@ -53,7 +53,6 @@ var GatewayServer = class {
|
|
|
53
53
|
service: this.service,
|
|
54
54
|
token: effectiveToken
|
|
55
55
|
});
|
|
56
|
-
getTunnelService().setGatewayFetch(app.fetch.bind(app));
|
|
57
56
|
const primaryHost = listenHosts[0] ?? bindHost;
|
|
58
57
|
this.server = serve({
|
|
59
58
|
fetch: app.fetch,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","names":[],"sources":["../../../src/gateway/server.ts"],"sourcesContent":["import { serve, type ServerType } from '@hono/node-server';\n\nimport type { GatewayBindMode } from '../config/schema.js';\nimport { resolveGatewayBindHost, resolveGatewayListenHosts } from '../config/gateway-bind.js';\nimport { resolveGatewayListenPlan } from './listen.js';\nimport { GatewayService } from './service.js';\nimport { createHonoApp } from './hono/app.js';\
|
|
1
|
+
{"version":3,"file":"server.js","names":[],"sources":["../../../src/gateway/server.ts"],"sourcesContent":["import { serve, type ServerType } from '@hono/node-server';\n\nimport type { GatewayBindMode } from '../config/schema.js';\nimport { resolveGatewayBindHost, resolveGatewayListenHosts } from '../config/gateway-bind.js';\nimport { resolveGatewayListenPlan } from './listen.js';\nimport { GatewayService } from './service.js';\nimport { createHonoApp } from './hono/app.js';\n\nexport interface GatewayServerConfig {\n port: number;\n /** Resolved listen address (sync plan); re-validated async at start when possible. */\n bindHost: string;\n bind?: GatewayBindMode;\n customBindHost?: string;\n token?: string;\n verbose?: boolean;\n configPath?: string;\n enableHotReload?: boolean;\n}\n\nexport class GatewayServer {\n private server?: ServerType;\n private extraServers: ServerType[] = [];\n private config: GatewayServerConfig;\n private service: GatewayService;\n\n constructor(config: GatewayServerConfig) {\n this.config = config;\n this.service = new GatewayService({\n configPath: config.configPath,\n enableHotReload: config.enableHotReload,\n deferChannelConnectUntilAfterHttp: true,\n listenBind: config.bind,\n listenCustomBindHost: config.customBindHost,\n listenPort: config.port,\n });\n }\n\n async start(): Promise<void> {\n const cfg = this.service.currentConfig;\n const plan = resolveGatewayListenPlan({\n cfg,\n bindOverride: this.config.bind,\n });\n\n let bindHost: string;\n try {\n bindHost = await resolveGatewayBindHost({\n bindMode: plan.bindMode,\n customBindHost: plan.customBindHost ?? this.config.customBindHost,\n });\n } catch (err) {\n bindHost = plan.bindHost;\n if (plan.bindMode === 'custom') {\n throw err;\n }\n }\n\n if (plan.bindMode === 'custom') {\n const expected = plan.customBindHost?.trim();\n if (!expected || bindHost !== expected) {\n throw new Error(\n `gateway bind=custom requested ${expected ?? '(missing)'} but resolved ${bindHost}`,\n );\n }\n }\n\n const listenHosts = await resolveGatewayListenHosts(bindHost);\n console.log(`[GatewayServer] Starting gateway server on ${bindHost}:${this.config.port}...`);\n\n await this.service.start();\n this.service.registerGatewayShutdownForRestart(async () => {\n await this.stop();\n });\n\n const { configureTunnelFromGatewayConfig } = await import('../tunnel/gateway-lifecycle.js');\n await configureTunnelFromGatewayConfig(this.service.currentConfig, { deferWellKnownFetch: true });\n\n const effectiveToken = this.config.token || this.service.getAuthToken();\n const app = createHonoApp({\n service: this.service,\n token: effectiveToken,\n });\n\n const primaryHost = listenHosts[0] ?? bindHost;\n this.server = serve(\n {\n fetch: app.fetch,\n port: this.config.port,\n hostname: primaryHost,\n },\n () => {\n console.log(`[GatewayServer] Gateway server running at http://${primaryHost}:${this.config.port}`);\n void this.service.onHttpListening().catch((err) => {\n console.error('[GatewayServer] Deferred channel startup failed:', err);\n });\n },\n );\n\n for (const aliasHost of listenHosts.slice(1)) {\n const extra = serve({\n fetch: app.fetch,\n port: this.config.port,\n hostname: aliasHost,\n });\n this.extraServers.push(extra);\n }\n }\n\n async close(opts?: { reason?: string; restartExpectedMs?: number | null }): Promise<void> {\n const reason = opts?.reason ?? 'gateway stopping';\n console.log(`[GatewayServer] Closing gateway server: ${reason}`);\n await this.stop();\n }\n\n async stop(): Promise<void> {\n console.log('[GatewayServer] Stopping gateway server...');\n\n const closeServer = async (server: ServerType | undefined) => {\n if (!server) {\n return;\n }\n const forceClose = setTimeout(() => {\n (server as { closeAllConnections?: () => void }).closeAllConnections?.();\n }, 2000);\n await new Promise<void>((resolve) => {\n server.close(() => {\n clearTimeout(forceClose);\n resolve();\n });\n });\n };\n\n await closeServer(this.server);\n this.server = undefined;\n\n for (const extra of this.extraServers) {\n await closeServer(extra);\n }\n this.extraServers = [];\n\n await this.service.stop();\n\n console.log('[GatewayServer] Gateway server stopped');\n }\n\n get isRunning(): boolean {\n return this.server !== undefined;\n }\n\n get serviceInstance(): GatewayService {\n return this.service;\n }\n}\n"],"mappings":";;;;;;AAoBA,IAAa,gBAAb,MAA2B;CACzB;CACA,eAAqC,EAAE;CACvC;CACA;CAEA,YAAY,QAA6B;AACvC,OAAK,SAAS;AACd,OAAK,UAAU,IAAI,eAAe;GAChC,YAAY,OAAO;GACnB,iBAAiB,OAAO;GACxB,mCAAmC;GACnC,YAAY,OAAO;GACnB,sBAAsB,OAAO;GAC7B,YAAY,OAAO;GACpB,CAAC;;CAGJ,MAAM,QAAuB;EAC3B,MAAM,MAAM,KAAK,QAAQ;EACzB,MAAM,OAAO,yBAAyB;GACpC;GACA,cAAc,KAAK,OAAO;GAC3B,CAAC;EAEF,IAAI;AACJ,MAAI;AACF,cAAW,MAAM,uBAAuB;IACtC,UAAU,KAAK;IACf,gBAAgB,KAAK,kBAAkB,KAAK,OAAO;IACpD,CAAC;WACK,KAAK;AACZ,cAAW,KAAK;AAChB,OAAI,KAAK,aAAa,SACpB,OAAM;;AAIV,MAAI,KAAK,aAAa,UAAU;GAC9B,MAAM,WAAW,KAAK,gBAAgB,MAAM;AAC5C,OAAI,CAAC,YAAY,aAAa,SAC5B,OAAM,IAAI,MACR,iCAAiC,YAAY,YAAY,gBAAgB,WAC1E;;EAIL,MAAM,cAAc,MAAM,0BAA0B,SAAS;AAC7D,UAAQ,IAAI,8CAA8C,SAAS,GAAG,KAAK,OAAO,KAAK,KAAK;AAE5F,QAAM,KAAK,QAAQ,OAAO;AAC1B,OAAK,QAAQ,kCAAkC,YAAY;AACzD,SAAM,KAAK,MAAM;IACjB;EAEF,MAAM,EAAE,qCAAqC,MAAM,OAAO;AAC1D,QAAM,iCAAiC,KAAK,QAAQ,eAAe,EAAE,qBAAqB,MAAM,CAAC;EAEjG,MAAM,iBAAiB,KAAK,OAAO,SAAS,KAAK,QAAQ,cAAc;EACvE,MAAM,MAAM,cAAc;GACxB,SAAS,KAAK;GACd,OAAO;GACR,CAAC;EAEF,MAAM,cAAc,YAAY,MAAM;AACtC,OAAK,SAAS,MACZ;GACE,OAAO,IAAI;GACX,MAAM,KAAK,OAAO;GAClB,UAAU;GACX,QACK;AACJ,WAAQ,IAAI,oDAAoD,YAAY,GAAG,KAAK,OAAO,OAAO;AAC7F,QAAK,QAAQ,iBAAiB,CAAC,OAAO,QAAQ;AACjD,YAAQ,MAAM,oDAAoD,IAAI;KACtE;IAEL;AAED,OAAK,MAAM,aAAa,YAAY,MAAM,EAAE,EAAE;GAC5C,MAAM,QAAQ,MAAM;IAClB,OAAO,IAAI;IACX,MAAM,KAAK,OAAO;IAClB,UAAU;IACX,CAAC;AACF,QAAK,aAAa,KAAK,MAAM;;;CAIjC,MAAM,MAAM,MAA8E;EACxF,MAAM,SAAS,MAAM,UAAU;AAC/B,UAAQ,IAAI,2CAA2C,SAAS;AAChE,QAAM,KAAK,MAAM;;CAGnB,MAAM,OAAsB;AAC1B,UAAQ,IAAI,6CAA6C;EAEzD,MAAM,cAAc,OAAO,WAAmC;AAC5D,OAAI,CAAC,OACH;GAEF,MAAM,aAAa,iBAAiB;AACjC,WAAgD,uBAAuB;MACvE,IAAK;AACR,SAAM,IAAI,SAAe,YAAY;AACnC,WAAO,YAAY;AACjB,kBAAa,WAAW;AACxB,cAAS;MACT;KACF;;AAGJ,QAAM,YAAY,KAAK,OAAO;AAC9B,OAAK,SAAS,KAAA;AAEd,OAAK,MAAM,SAAS,KAAK,aACvB,OAAM,YAAY,MAAM;AAE1B,OAAK,eAAe,EAAE;AAEtB,QAAM,KAAK,QAAQ,MAAM;AAEzB,UAAQ,IAAI,yCAAyC;;CAGvD,IAAI,YAAqB;AACvB,SAAO,KAAK,WAAW,KAAA;;CAGzB,IAAI,kBAAkC;AACpC,SAAO,KAAK"}
|
|
@@ -34,6 +34,8 @@ export interface GatewayServiceConfig {
|
|
|
34
34
|
/** CLI `--bind` override for startup security guards. */
|
|
35
35
|
listenBind?: GatewayBindMode;
|
|
36
36
|
listenCustomBindHost?: string;
|
|
37
|
+
/** CLI `--port` override; used for CORS loopback defaults when it differs from config. */
|
|
38
|
+
listenPort?: number;
|
|
37
39
|
enableHotReload?: boolean;
|
|
38
40
|
/**
|
|
39
41
|
* When true (GatewayServer), outbound channel connects that declare
|
|
@@ -285,6 +285,8 @@ export declare class GatewayService {
|
|
|
285
285
|
*/
|
|
286
286
|
invokeGatewayMethod(method: string, params: Record<string, unknown>): Promise<unknown>;
|
|
287
287
|
get currentConfig(): Config;
|
|
288
|
+
/** Effective HTTP listen port (CLI `--port` override or config default). */
|
|
289
|
+
getEffectiveListenPort(): number;
|
|
288
290
|
get cronServiceInstance(): CronService;
|
|
289
291
|
getSkillsApi(lang?: string): {
|
|
290
292
|
catalog: SkillCatalogEntry[];
|
|
@@ -31,6 +31,7 @@ import { AgentService } from "../agent/service.js";
|
|
|
31
31
|
import { ChannelManager } from "../channels/manager.js";
|
|
32
32
|
import { ConfigHotReloader } from "../config/reload.js";
|
|
33
33
|
import "../config/index.js";
|
|
34
|
+
import { resolveEffectiveGatewayPort } from "./host.js";
|
|
34
35
|
import { CronService } from "../cron/service.js";
|
|
35
36
|
import "../cron/index.js";
|
|
36
37
|
import { computeBundledExtensionExtensionsPatch } from "../extensions/bundled-extension-activation.js";
|
|
@@ -122,7 +123,7 @@ var GatewayService = class {
|
|
|
122
123
|
this.auth = resolveGatewayAuth({ authConfig: this.config.gateway?.auth });
|
|
123
124
|
assertGatewayAuthConfigured(this.auth);
|
|
124
125
|
assertGatewayAuthNotKnownWeak(this.auth);
|
|
125
|
-
const gatewayPort = this.
|
|
126
|
+
const gatewayPort = this.getEffectiveListenPort();
|
|
126
127
|
const runtimeConfig = assertGatewayRuntimeConfig({
|
|
127
128
|
cfg: this.config,
|
|
128
129
|
auth: this.auth,
|
|
@@ -378,7 +379,7 @@ var GatewayService = class {
|
|
|
378
379
|
}
|
|
379
380
|
/** After HTTP is listening: exposure auto-start (Tailscale, then FRP tunnel). */
|
|
380
381
|
async runExposureAutoStartIfConfigured() {
|
|
381
|
-
const port = this.
|
|
382
|
+
const port = this.getEffectiveListenPort();
|
|
382
383
|
await getExposureManager().autoStart(this.config, port, this.getAuthToken());
|
|
383
384
|
}
|
|
384
385
|
/**
|
|
@@ -1183,6 +1184,10 @@ var GatewayService = class {
|
|
|
1183
1184
|
get currentConfig() {
|
|
1184
1185
|
return this.config;
|
|
1185
1186
|
}
|
|
1187
|
+
/** Effective HTTP listen port (CLI `--port` override or config default). */
|
|
1188
|
+
getEffectiveListenPort() {
|
|
1189
|
+
return resolveEffectiveGatewayPort(this.config, this.serviceConfig.listenPort);
|
|
1190
|
+
}
|
|
1186
1191
|
get cronServiceInstance() {
|
|
1187
1192
|
return this.cronService;
|
|
1188
1193
|
}
|