@xopcai/xopc 0.0.129 → 0.0.131
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/icons/icon-128.png +0 -0
- package/dist/browser-ext/icons/icon-16.png +0 -0
- package/dist/browser-ext/icons/icon-32.png +0 -0
- package/dist/browser-ext/icons/icon-48.png +0 -0
- package/dist/browser-ext/manifest.json +1 -1
- package/dist/extensions/telegram/xopc.extension.json +1 -1
- package/dist/gateway/static/root/apple-touch-icon.png +0 -0
- package/dist/gateway/static/root/assets/{agent-avatar-dicebear-DHzfg5D-.js → agent-avatar-dicebear-BJYeTxQ9.js} +1 -1
- package/dist/gateway/static/root/assets/agents-kVsY4Iz_.js +2 -0
- package/dist/gateway/static/root/assets/app-management-settings-panel-Bj8iJ-7d.js +1 -0
- package/dist/gateway/static/root/assets/{appearance-settings-bYeEj3IW.js → appearance-settings-6TjISbHB.js} +1 -1
- package/dist/gateway/static/root/assets/apps-page-DC4YwXLL.js +1 -0
- package/dist/gateway/static/root/assets/{archive-plugin-Dy6lsTHw.js → archive-plugin-2G62raBg.js} +1 -1
- package/dist/gateway/static/root/assets/{attachment-load-qOtBYR6Y.js → attachment-load-kLNJMEJh.js} +1 -1
- package/dist/gateway/static/root/assets/{automation-suggestion-card-BFH-RfaV.js → automation-suggestion-card-CpcZSSD8.js} +1 -1
- package/dist/gateway/static/root/assets/automations-page-MZ8m4bYP.js +4 -0
- package/dist/gateway/static/root/assets/{binary-plugins-DJvi_thc.js → binary-plugins-C_zSrWow.js} +1 -1
- package/dist/gateway/static/root/assets/{block-editor-DlMnizlt.js → block-editor-D_PMrT-l.js} +4 -4
- package/dist/gateway/static/root/assets/browser-settings-page-C89wKfPd.js +8 -0
- package/dist/gateway/static/root/assets/capability-presets-api-CPFtajl7.js +1 -0
- package/dist/gateway/static/root/assets/{capability-presets-settings-panel-Dng8Grpv.js → capability-presets-settings-panel-DWw31-4v.js} +1 -1
- package/dist/gateway/static/root/assets/channel-recipient-api-BUi8syv2.js +1 -0
- package/dist/gateway/static/root/assets/channels-settings-DOYzcMAE.js +1 -0
- package/dist/gateway/static/root/assets/connectors-page-DCE-PgQF.js +2 -0
- package/dist/gateway/static/root/assets/desktop-pet-BYV16wez.js +1 -0
- package/dist/gateway/static/root/assets/{desktop-pet-settings-B-u63N4e.js → desktop-pet-settings-DGIaFqPR.js} +1 -1
- package/dist/gateway/static/root/assets/{directory-picker-path-field-8p1ehIHn.js → directory-picker-path-field-BEhDuShd.js} +1 -1
- package/dist/gateway/static/root/assets/dreaming-settings-BIkgLFDH.js +3 -0
- package/dist/gateway/static/root/assets/{extension-debug-page-DJ1ABK95.js → extension-debug-page-BIjLZMAQ.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-page-CP9P2lGM.js → extension-page-Cxv4Bgj5.js} +1 -1
- package/dist/gateway/static/root/assets/extension-settings-page-DNd0tRFl.js +1 -0
- package/dist/gateway/static/root/assets/fetch-Bl7zMnmd.js +1 -0
- package/dist/gateway/static/root/assets/gateway-config-api-BiqKIkkp.js +1 -0
- package/dist/gateway/static/root/assets/gateway-settings-D_Cvasqi.js +1 -0
- package/dist/gateway/static/root/assets/{gateway-startup-retry-CY5W2Pp4.js → gateway-startup-retry-CPOgTcpv.js} +1 -1
- package/dist/gateway/static/root/assets/{goal-create-dialog-2lPbGfp3.js → goal-create-dialog-DZ68qzpm.js} +1 -1
- package/dist/gateway/static/root/assets/goal-detail-page-DF8QhTHH.js +4 -0
- package/dist/gateway/static/root/assets/goals-config-api-h26_-2Vm.js +1 -0
- package/dist/gateway/static/root/assets/goals-page-BzBKHZJg.js +2 -0
- package/dist/gateway/static/root/assets/goals-settings-BFG1uTI8.js +1 -0
- package/dist/gateway/static/root/assets/heartbeat-settings-5vV8vTva.js +1 -0
- package/dist/gateway/static/root/assets/index-DOetFpTt.js +262 -0
- package/dist/gateway/static/root/assets/index-W4ogAiHt.css +1 -0
- package/dist/gateway/static/root/assets/{keyboard-shortcuts-settings-BrENZzM7.js → keyboard-shortcuts-settings-Dpx1ovO6.js} +1 -1
- package/dist/gateway/static/root/assets/logs-page-3QO3elfx.js +7 -0
- package/dist/gateway/static/root/assets/{markdown-split-BYF4ReGM.js → markdown-split-Ba4EsPkV.js} +1 -1
- package/dist/gateway/static/root/assets/{media-plugins-CpQDz-QJ.js → media-plugins-lA5rvtCq.js} +1 -1
- package/dist/gateway/static/root/assets/{messages-YzjnURPJ.js → messages-Bq4REfIj.js} +2 -2
- package/dist/gateway/static/root/assets/models-hub-panel-BBG87CHZ.js +2 -0
- package/dist/gateway/static/root/assets/notes-page-DrUGE-Hn.js +1 -0
- package/dist/gateway/static/root/assets/notes-workbench-eAIE8iKV.js +2 -0
- package/dist/gateway/static/root/assets/{page-tabs-RnYYOIWO.js → page-tabs-Cy7bX7Ce.js} +1 -1
- package/dist/gateway/static/root/assets/{preview-open-alternatives-CTRt06Ri.js → preview-open-alternatives-B9_iql49.js} +1 -1
- package/dist/gateway/static/root/assets/project-detail-page-Cwct6pCV.js +1 -0
- package/dist/gateway/static/root/assets/projects-page-BXZlzV8t.js +1 -0
- package/dist/gateway/static/root/assets/remote-access-hub-C29EWM0P.js +1 -0
- package/dist/gateway/static/root/assets/{save-bar-controls-c4pZeikq.js → save-bar-controls-nMloZ0mp.js} +1 -1
- package/dist/gateway/static/root/assets/{schedule-field-Cb6R7iuP.js → schedule-field-BuajpVNF.js} +1 -1
- package/dist/gateway/static/root/assets/{schema-form-BqET_hEP.js → schema-form-IeXV_yQk.js} +1 -1
- package/dist/gateway/static/root/assets/sessions-page-DHHVDqsy.js +1 -0
- package/dist/gateway/static/root/assets/settings-advanced-gate-hDvmobsT.js +1 -0
- package/dist/gateway/static/root/assets/{settings-collapsible-section-DuOvqNsI.js → settings-collapsible-section-ByixWwvu.js} +1 -1
- package/dist/gateway/static/root/assets/{settings-form-section-DptSsbWd.js → settings-form-section-C-5-n-jM.js} +1 -1
- package/dist/gateway/static/root/assets/{settings-loading-skeleton-BlVfBCb9.js → settings-loading-skeleton-CTA5Gje4.js} +1 -1
- package/dist/gateway/static/root/assets/settings-page-DzqxeemF.js +1 -0
- package/dist/gateway/static/root/assets/{settings-page-layout-DVt69dJJ.js → settings-page-layout-DgFTf6nM.js} +1 -1
- package/dist/gateway/static/root/assets/setup-status-panel-svpHcJ62.js +1 -0
- package/dist/gateway/static/root/assets/share-preview-page-IOjU69FC.js +2 -0
- package/dist/gateway/static/root/assets/shares-settings-Ddg8gHPN.js +1 -0
- package/dist/gateway/static/root/assets/skill-reload-api-DcNOEJsK.js +1 -0
- package/dist/gateway/static/root/assets/skills-page-DoonnYRs.js +2 -0
- package/dist/gateway/static/root/assets/{system-settings-panel-CW22OkZ4.js → system-settings-panel-BMDD7FXg.js} +1 -1
- package/dist/gateway/static/root/assets/{typed-models-lib-BqpGAKxV.js → typed-models-lib-DayamJ56.js} +1 -1
- package/dist/gateway/static/root/assets/use-channel-catalog-ChYwFiqo.js +1 -0
- package/dist/gateway/static/root/assets/{user-profile-settings-panel-CERVLuaW.js → user-profile-settings-panel-Dd0X3mPv.js} +1 -1
- package/dist/gateway/static/root/assets/{utils-BnmysurA.js → utils-D9v9ZpK4.js} +1 -1
- package/dist/gateway/static/root/assets/voice-api-key-field-QXGvjTwa.js +1 -0
- package/dist/gateway/static/root/assets/work-item-detail-page-CP-5OUYI.js +1 -0
- package/dist/gateway/static/root/assets/{workflow-run-setup-panel-Ci9GSPnS.js → workflow-run-setup-panel-IidqhbUI.js} +2 -2
- package/dist/gateway/static/root/assets/{workflow-start-dialog-BhDj8pqH.js → workflow-start-dialog-BQQkr8YD.js} +1 -1
- package/dist/gateway/static/root/assets/workflows-page-DtVIjVSX.js +18 -0
- package/dist/gateway/static/root/favicon-16x16.png +0 -0
- package/dist/gateway/static/root/favicon-32x32.png +0 -0
- package/dist/gateway/static/root/favicon-icon.svg +23 -1
- package/dist/gateway/static/root/favicon.ico +0 -0
- package/dist/gateway/static/root/favicon.png +0 -0
- package/dist/gateway/static/root/favicon.svg +20 -24
- package/dist/gateway/static/root/index.html +11 -11
- package/dist/gateway/static/root/logo-dark.svg +18 -23
- package/dist/gateway/static/root/logo.svg +18 -24
- package/dist/gateway/static/root/pwa-192x192.png +0 -0
- package/dist/gateway/static/root/pwa-512x512.png +0 -0
- package/dist/gateway/static/root/site.webmanifest +23 -0
- package/dist/package.js +1 -1
- package/dist/src/cli/commands/gateway/probe.js +6 -6
- package/dist/src/cli/commands/gateway/probe.js.map +1 -1
- package/dist/src/cli/commands/onboard.js +2 -2
- package/dist/src/cli/commands/onboard.js.map +1 -1
- package/dist/src/cli/commands/tui.js +11 -4
- package/dist/src/cli/commands/tui.js.map +1 -1
- package/dist/src/cli/utils/gateway-client-options.d.ts +3 -0
- package/dist/src/cli/utils/gateway-client-options.js +2 -1
- package/dist/src/cli/utils/gateway-client-options.js.map +1 -1
- package/dist/src/cli/utils/gateway-client.d.ts +6 -3
- package/dist/src/cli/utils/gateway-client.js +14 -8
- package/dist/src/cli/utils/gateway-client.js.map +1 -1
- package/dist/src/gateway/credential.d.ts +9 -0
- package/dist/src/gateway/credential.js +16 -0
- package/dist/src/gateway/credential.js.map +1 -0
- package/dist/src/gateway/hono/app.d.ts +0 -1
- package/dist/src/gateway/hono/app.js +2 -9
- package/dist/src/gateway/hono/app.js.map +1 -1
- package/dist/src/gateway/hono/middleware/auth.d.ts +0 -2
- package/dist/src/gateway/hono/middleware/auth.js +46 -21
- package/dist/src/gateway/hono/middleware/auth.js.map +1 -1
- package/dist/src/gateway/hono/routes/sessions.js +2 -27
- package/dist/src/gateway/hono/routes/sessions.js.map +1 -1
- package/dist/src/gateway/server.js +1 -5
- package/dist/src/gateway/server.js.map +1 -1
- package/dist/src/mcp/channel-bridge.d.ts +2 -1
- package/dist/src/mcp/channel-bridge.js +10 -6
- package/dist/src/mcp/channel-bridge.js.map +1 -1
- package/dist/src/mcp/channel-server.d.ts +2 -1
- package/dist/src/mcp/channel-server.js +1 -1
- package/dist/src/mcp/channel-server.js.map +1 -1
- package/dist/src/mcp/gateway-http-client.d.ts +3 -2
- package/dist/src/mcp/gateway-http-client.js +9 -7
- package/dist/src/mcp/gateway-http-client.js.map +1 -1
- package/dist/src/tui/backends/gateway-sse-backend.d.ts +3 -2
- package/dist/src/tui/backends/gateway-sse-backend.js +45 -41
- package/dist/src/tui/backends/gateway-sse-backend.js.map +1 -1
- package/dist/src/tui/tui-types.d.ts +3 -2
- package/dist/src/tui/tui-types.js.map +1 -1
- package/dist/src/tui/tui.js +1 -1
- package/dist/src/tui/tui.js.map +1 -1
- package/package.json +5 -2
- package/dist/gateway/static/root/assets/agents-CMxKKFQD.js +0 -2
- package/dist/gateway/static/root/assets/app-management-settings-panel-ptk9c5BN.js +0 -1
- package/dist/gateway/static/root/assets/apps-page-DdZvDf5B.js +0 -1
- package/dist/gateway/static/root/assets/automations-page-B8JTjkCr.js +0 -4
- package/dist/gateway/static/root/assets/browser-settings-page-slHLiZgi.js +0 -8
- package/dist/gateway/static/root/assets/capability-presets-api-D4q7vHVJ.js +0 -1
- package/dist/gateway/static/root/assets/channel-recipient-api-BrW5Kcgc.js +0 -1
- package/dist/gateway/static/root/assets/channels-settings-KIsISXw0.js +0 -1
- package/dist/gateway/static/root/assets/connectors-page-C5fx4Qs9.js +0 -2
- package/dist/gateway/static/root/assets/desktop-pet-Jmdn6q47.js +0 -1
- package/dist/gateway/static/root/assets/dreaming-settings-CCkKP-I4.js +0 -3
- package/dist/gateway/static/root/assets/extension-settings-page-DnhU-tx0.js +0 -1
- package/dist/gateway/static/root/assets/fetch-BMjlW0g1.js +0 -1
- package/dist/gateway/static/root/assets/gateway-config-api-CjxDn46s.js +0 -1
- package/dist/gateway/static/root/assets/gateway-settings-TING7224.js +0 -1
- package/dist/gateway/static/root/assets/goal-detail-page-DHWVRafk.js +0 -4
- package/dist/gateway/static/root/assets/goals-config-api-BqhuxG8P.js +0 -1
- package/dist/gateway/static/root/assets/goals-page-CMk-9ZYN.js +0 -2
- package/dist/gateway/static/root/assets/goals-settings-BHzOAU_l.js +0 -1
- package/dist/gateway/static/root/assets/heartbeat-settings-BQIm0P3V.js +0 -1
- package/dist/gateway/static/root/assets/index-C4k6QSXw.js +0 -257
- package/dist/gateway/static/root/assets/index-EKmpPvZj.css +0 -1
- package/dist/gateway/static/root/assets/logs-page-BiL302Ve.js +0 -2
- package/dist/gateway/static/root/assets/models-hub-panel-CFPpl3Cq.js +0 -2
- package/dist/gateway/static/root/assets/notes-page-BurgcdVZ.js +0 -1
- package/dist/gateway/static/root/assets/notes-workbench-BAELRM_O.js +0 -2
- package/dist/gateway/static/root/assets/project-detail-page-iXX1Ceo2.js +0 -1
- package/dist/gateway/static/root/assets/projects-page-GDVTFAY7.js +0 -1
- package/dist/gateway/static/root/assets/remote-access-hub-BUrbUnwv.js +0 -1
- package/dist/gateway/static/root/assets/sessions-page-CiOreA2k.js +0 -1
- package/dist/gateway/static/root/assets/settings-advanced-gate-C-qvptlu.js +0 -1
- package/dist/gateway/static/root/assets/settings-page-xQx2Pkg5.js +0 -1
- package/dist/gateway/static/root/assets/setup-status-panel-DWB8VdLK.js +0 -1
- package/dist/gateway/static/root/assets/share-preview-page-8m_ACH0z.js +0 -2
- package/dist/gateway/static/root/assets/shares-settings-CgxfhNxI.js +0 -1
- package/dist/gateway/static/root/assets/skill-reload-api-BBFUOxmf.js +0 -1
- package/dist/gateway/static/root/assets/skills-page-bo695odu.js +0 -2
- package/dist/gateway/static/root/assets/url-DFGNSBNT.js +0 -1
- package/dist/gateway/static/root/assets/use-channel-catalog-CPdJ0o0i.js +0 -1
- package/dist/gateway/static/root/assets/voice-api-key-field-Jr3jvPcB.js +0 -1
- package/dist/gateway/static/root/assets/work-item-detail-page-B-8rex4u.js +0 -1
- package/dist/gateway/static/root/assets/workflows-page-Brb3UGvs.js +0 -18
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.js","names":[],"sources":["../../../../src/gateway/hono/app.ts"],"sourcesContent":["import { Hono } from 'hono';\nimport type { Context } from 'hono';\nimport { cors } from 'hono/cors';\nimport { createMiddleware } from 'hono/factory';\nimport { bodyLimit } from 'hono/body-limit';\nimport { getConnInfo } from '@hono/node-server/conninfo';\n\nimport { resolveGatewayEffectiveHost } from '../../config/gateway-bind.js';\nimport { createLogger } from '../../utils/logger.js';\nimport type { GatewayService } from '../service.js';\nimport { resolveAllowedBrowserOrigins, resolveGatewayServiceListenPort } from '../host.js';\nimport { loadTunnelState } from '../../tunnel/tunnel-state.js';\nimport { WORK_ITEM_ATTACHMENT_UPLOAD_BODY_MAX_BYTES } from '../../work-items/index.js';\nimport { maxWebchatAgentRequestBodyBytes } from '../chat-limits.js';\nimport { buildGatewayConsoleCspHeader } from '../security/csp.js';\nimport { checkBrowserOrigin } from '../security/origin-check.js';\nimport { isLoopbackIpAddress, isTrustedProxyAddress } from '../client-ip.js';\nimport { resolveReverseProxyPublicUrl } from '../public-url.js';\nimport { auth } from './middleware/auth.js';\nimport { operatorScopes } from './middleware/scopes.js';\nimport {\n createChannelRateLimitMiddleware,\n createStrictRateLimitMiddleware,\n} from './middleware/strict-rate-limit.js';\nimport { logContextMiddleware } from './middleware/log-context.js';\nimport { logger } from './middleware/logger.js';\nimport { routeErrorMiddleware } from './middleware/route-errors.js';\nimport { registerPublicExtensionAssetRoutes } from './routes/auth-registry-extensions.js';\nimport { registerAuthenticatedRoutes } from './routes/index.js';\nimport { registerPublicGatewayRoutes } from './routes/public-gateway.js';\nimport { resetLazyRouteBundlesForTests } from './routes/lazy-fallback.js';\nimport { prewarmStaticUiCache } from './lib/static-ui.js';\nimport { registerSiteShareMiddleware } from '../../share/site-share-router.js';\nconst log = createLogger('Gateway:App');\n\nexport interface HonoAppConfig {\n service: GatewayService;\n token?: string;\n}\n\n/**\n * Extension sandbox HTML under `/api/extensions/:id/assets/*` ships its own CSP\n * (`frame-ancestors 'self'`). The global gateway middleware must not overwrite it\n * with `frame-ancestors 'none'` / `X-Frame-Options: DENY`, or the console cannot embed iframes.\n */\nexport function isExtensionGatewayUiAssetPath(path: string): boolean {\n return /^\\/api\\/extensions\\/[^/]+\\/assets\\//.test(path);\n}\n\nexport function createHonoApp(config: HonoAppConfig): Hono {\n if (process.env.VITEST) {\n resetLazyRouteBundlesForTests();\n }\n const { service, token } = config;\n const app = new Hono();\n\n const gatewayPort = resolveGatewayServiceListenPort(service);\n\n const resolveBrowserOrigins = (): string[] =>\n resolveAllowedBrowserOrigins({\n configuredOrigins: service.currentConfig.gateway.corsOrigins,\n port: gatewayPort,\n bindHost: resolveGatewayEffectiveHost(service.currentConfig),\n tunnelPublicUrl: loadTunnelState()?.publicUrl,\n reverseProxyPublicUrl: resolveReverseProxyPublicUrl(service.currentConfig),\n });\n\n /**\n * TCP source for the in-flight request, normalized for trusted-proxy checks.\n * Returns undefined when the runtime doesn't expose conninfo (tests, mocks).\n */\n const resolveRequestRemoteAddress = (c: Context): string | undefined => {\n try {\n return getConnInfo(c).remote.address;\n } catch {\n return undefined;\n }\n };\n\n /**\n * A request's TCP source qualifies as a \"trusted proxy hop\" when it's\n * loopback (the user's own machine, where any reverse proxy lives) or\n * listed in `gateway.trustedProxies`. We use this signal to safely\n * auto-allow same-host Origins through CSRF without requiring a manual\n * `corsOrigins` entry for every reverse-proxy hostname.\n */\n const isRequestFromTrustedProxy = (c: Context): boolean => {\n const remote = resolveRequestRemoteAddress(c);\n if (!remote) return false;\n if (isLoopbackIpAddress(remote)) return true;\n return isTrustedProxyAddress(remote, service.currentConfig.gateway?.trustedProxies);\n };\n\n app.use(logContextMiddleware());\n app.use(logger({\n trustedProxies: service.currentConfig.gateway?.trustedProxies,\n allowRealIpFallback: service.currentConfig.gateway?.allowRealIpFallback === true,\n }));\n\n // Site-share middleware runs BEFORE CORS/CSP — it owns the request when the\n // Host header matches `*.<publicHostSuffix>` (default `*.share.xopc.ai`) or\n // the path starts with `/site/:token/`. Otherwise it falls through.\n registerSiteShareMiddleware(app, service);\n app.use(\n cors({\n origin: (origin) => {\n const allowed = resolveBrowserOrigins();\n if (!origin) {\n return allowed[0] ?? `http://127.0.0.1:${gatewayPort}`;\n }\n const normalized = origin.toLowerCase();\n const hit = allowed.find((entry) => entry.toLowerCase() === normalized);\n if (hit) return origin;\n return allowed.includes('*') ? '*' : '';\n },\n allowMethods: ['GET', 'POST', 'PATCH', 'DELETE', 'OPTIONS'],\n allowHeaders: ['Content-Type', 'Authorization', 'Accept', 'X-Session-Id', 'Last-Event-ID'],\n credentials: true,\n maxAge: 86400,\n }),\n );\n\n // Build CSP header once at startup (no inline script hashes needed for SPA)\n const gatewayConsoleCsp = buildGatewayConsoleCspHeader();\n\n // Security headers middleware\n app.use(createMiddleware(async (c, next) => {\n await next();\n if (isExtensionGatewayUiAssetPath(c.req.path)) {\n return;\n }\n c.header('X-Frame-Options', 'DENY');\n c.header('X-Content-Type-Options', 'nosniff');\n c.header('Referrer-Policy', 'strict-origin-when-cross-origin');\n c.header('X-XSS-Protection', '1; mode=block');\n // microphone=(self): allow same-origin chat voice (composer). microphone=() breaks packaged Electron loading the gateway SPA.\n c.header('Permissions-Policy', 'camera=(), microphone=(self), geolocation=()');\n c.header('Content-Security-Policy', gatewayConsoleCsp);\n }));\n\n // Browser Origin check middleware for API routes (CSRF protection).\n // Non-browser requests (no Origin header) pass through — they are\n // authenticated by the token middleware instead.\n const allowHostHeaderOriginFallback =\n service.currentConfig.gateway?.dangerouslyAllowHostHeaderOriginFallback === true;\n app.use('/api/*', createMiddleware(async (c, next) => {\n // Sandboxed extension iframes (no allow-same-origin) send `Origin: null`.\n // `checkBrowserOrigin` rejects that; these routes rely on CSP instead\n // (`registerPublicExtensionAssetRoutes`).\n if (isExtensionGatewayUiAssetPath(c.req.path)) {\n return next();\n }\n\n const origin = c.req.header('origin');\n if (!origin || origin.trim().toLowerCase() === 'null') {\n // Native apps / opaque origins — authenticated via Bearer token\n return next();\n }\n\n const result = checkBrowserOrigin({\n requestHost: c.req.header('host'),\n origin,\n allowedOrigins: resolveBrowserOrigins(),\n allowHostHeaderOriginFallback,\n autoAllowSameHostFromTrustedProxy: isRequestFromTrustedProxy(c),\n isLocalClient: false,\n });\n\n if (!result.ok) {\n log.warn(\n {\n origin,\n requestHost: c.req.header('host'),\n reason: 'reason' in result ? result.reason : 'unknown',\n path: c.req.path,\n method: c.req.method,\n },\n `Browser origin check failed: ${origin} not in allowed list`,\n );\n return c.json({ error: 'Forbidden', message: 'Origin not allowed' }, 403);\n }\n\n return next();\n }));\n\n const DEFAULT_API_BODY_MAX = 1 * 1024 * 1024;\n const SKILL_UPLOAD_BODY_MAX = 10 * 1024 * 1024;\n const NOTE_MEDIA_BODY_MAX = 25 * 1024 * 1024;\n const VOICE_TRANSCRIBE_BODY_MAX = 35 * 1024 * 1024;\n const WEBCHAT_AGENT_BODY_MAX = maxWebchatAgentRequestBodyBytes();\n\n const isNoteMediaUploadRequest = (path: string, method: string, contentType: string | undefined): boolean => {\n if (method !== 'POST') return false;\n if (/^\\/api\\/notes\\/[^/]+\\/media$/.test(path)) return true;\n return path === '/api/notes' && contentType?.includes('multipart/form-data') === true;\n };\n\n const isWorkItemAttachmentUploadRequest = (path: string, method: string, contentType: string | undefined): boolean => {\n if (method !== 'POST' || contentType?.includes('multipart/form-data') !== true) return false;\n return /^\\/api\\/work-items\\/[^/]+\\/attachments$/.test(path)\n || /^\\/api\\/projects\\/[^/]+\\/work-items$/.test(path);\n };\n\n app.use('/api/skills/upload', bodyLimit({\n maxSize: SKILL_UPLOAD_BODY_MAX,\n onError: (c) => {\n log.warn({ path: c.req.path, maxSizeMb: 10 }, 'Request body too large: skills upload exceeds 10MB limit');\n return c.json({ error: 'Skill package too large', maxSize: '10MB' }, 413);\n },\n }));\n\n app.use('/api/*', async (c, next) => {\n const contentType = c.req.header('content-type');\n const maxSize = c.req.path === '/api/agent'\n ? WEBCHAT_AGENT_BODY_MAX\n : c.req.path === '/api/skills/upload'\n ? SKILL_UPLOAD_BODY_MAX\n : c.req.path === '/api/voice/transcribe'\n ? VOICE_TRANSCRIBE_BODY_MAX\n : isNoteMediaUploadRequest(c.req.path, c.req.method, contentType)\n ? NOTE_MEDIA_BODY_MAX\n : isWorkItemAttachmentUploadRequest(c.req.path, c.req.method, contentType)\n ? WORK_ITEM_ATTACHMENT_UPLOAD_BODY_MAX_BYTES\n : DEFAULT_API_BODY_MAX;\n const maxSizeMb = Math.ceil(maxSize / (1024 * 1024));\n return bodyLimit({\n maxSize,\n onError: (ctx) => {\n log.warn({ path: ctx.req.path, maxSizeMb }, `Request body too large: exceeds ${maxSizeMb}MB limit`);\n return ctx.json({ error: 'Request body too large', maxSize: `${maxSizeMb}MB` }, 413);\n },\n })(c, next);\n });\n\n registerPublicGatewayRoutes(app, service);\n\n // Extension UI assets are served without auth: sandboxed iframes (no allow-same-origin)\n // have an opaque origin of `null` and cannot forward the ?token= from the parent HTML URL.\n // Security is enforced by the strict CSP (frame-ancestors 'self') on every response.\n registerPublicExtensionAssetRoutes(app, service);\n\n const authenticated = new Hono();\n authenticated.use(routeErrorMiddleware());\n authenticated.use(\n auth({\n token,\n getGatewayAuth: () => service.currentConfig.gateway?.auth,\n getResolvedAuth: () => {\n if (typeof service.getResolvedAuth === 'function') {\n return service.getResolvedAuth();\n }\n return token ? { mode: 'token', token } : { mode: 'none' };\n },\n getTrustedProxyContext: () => ({\n trustedProxies: service.currentConfig.gateway?.trustedProxies,\n allowRealIpFallback: service.currentConfig.gateway?.allowRealIpFallback === true,\n }),\n }),\n );\n authenticated.use(operatorScopes());\n\n const strictRateLimitMiddleware = createStrictRateLimitMiddleware({\n getTrustedProxyContext: () => ({\n trustedProxies: service.currentConfig.gateway?.trustedProxies,\n allowRealIpFallback: service.currentConfig.gateway?.allowRealIpFallback === true,\n }),\n });\n const channelRateLimitMiddleware = createChannelRateLimitMiddleware({\n getTrustedProxyContext: () => ({\n trustedProxies: service.currentConfig.gateway?.trustedProxies,\n allowRealIpFallback: service.currentConfig.gateway?.allowRealIpFallback === true,\n }),\n });\n\n const sseConfig = {\n service,\n maxSseConnections: service.currentConfig.gateway.maxSseConnections,\n };\n\n registerAuthenticatedRoutes(app, authenticated, {\n service,\n strictRateLimitMiddleware,\n channelRateLimitMiddleware,\n sseConfig,\n });\n\n const prewarm = prewarmStaticUiCache();\n if (prewarm.loaded > 0) {\n log.debug({ loaded: prewarm.loaded, missing: prewarm.missing }, 'Static UI cache prewarmed');\n }\n\n app.route('/', authenticated);\n\n app.notFound((c) => {\n const isApiRoute = c.req.path.startsWith('/api/');\n const fields = { path: c.req.path, method: c.req.method };\n if (isApiRoute) {\n log.warn(fields, 'Route not found');\n } else {\n log.debug(fields, 'Route not found');\n }\n return c.json({ error: 'Not found' }, 404);\n });\n\n app.onError((err, c) => {\n log.error(\n {\n err,\n phase: 'gateway.http.unhandled',\n path: c.req.path,\n method: c.req.method,\n userAgent: c.req.header('user-agent'),\n },\n `Hono error on ${c.req.method} ${c.req.path}: ${err instanceof Error ? err.message : String(err)}`,\n );\n return c.json({ error: 'Internal server error' }, 500);\n });\n\n return app;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"app.js","names":[],"sources":["../../../../src/gateway/hono/app.ts"],"sourcesContent":["import { Hono } from 'hono';\nimport type { Context } from 'hono';\nimport { cors } from 'hono/cors';\nimport { createMiddleware } from 'hono/factory';\nimport { bodyLimit } from 'hono/body-limit';\nimport { getConnInfo } from '@hono/node-server/conninfo';\n\nimport { resolveGatewayEffectiveHost } from '../../config/gateway-bind.js';\nimport { createLogger } from '../../utils/logger.js';\nimport type { GatewayService } from '../service.js';\nimport { resolveAllowedBrowserOrigins, resolveGatewayServiceListenPort } from '../host.js';\nimport { loadTunnelState } from '../../tunnel/tunnel-state.js';\nimport { WORK_ITEM_ATTACHMENT_UPLOAD_BODY_MAX_BYTES } from '../../work-items/index.js';\nimport { maxWebchatAgentRequestBodyBytes } from '../chat-limits.js';\nimport { buildGatewayConsoleCspHeader } from '../security/csp.js';\nimport { checkBrowserOrigin } from '../security/origin-check.js';\nimport { isLoopbackIpAddress, isTrustedProxyAddress } from '../client-ip.js';\nimport { resolveReverseProxyPublicUrl } from '../public-url.js';\nimport { auth } from './middleware/auth.js';\nimport { operatorScopes } from './middleware/scopes.js';\nimport {\n createChannelRateLimitMiddleware,\n createStrictRateLimitMiddleware,\n} from './middleware/strict-rate-limit.js';\nimport { logContextMiddleware } from './middleware/log-context.js';\nimport { logger } from './middleware/logger.js';\nimport { routeErrorMiddleware } from './middleware/route-errors.js';\nimport { registerPublicExtensionAssetRoutes } from './routes/auth-registry-extensions.js';\nimport { registerAuthenticatedRoutes } from './routes/index.js';\nimport { registerPublicGatewayRoutes } from './routes/public-gateway.js';\nimport { resetLazyRouteBundlesForTests } from './routes/lazy-fallback.js';\nimport { prewarmStaticUiCache } from './lib/static-ui.js';\nimport { registerSiteShareMiddleware } from '../../share/site-share-router.js';\nconst log = createLogger('Gateway:App');\n\nexport interface HonoAppConfig {\n service: GatewayService;\n}\n\n/**\n * Extension sandbox HTML under `/api/extensions/:id/assets/*` ships its own CSP\n * (`frame-ancestors 'self'`). The global gateway middleware must not overwrite it\n * with `frame-ancestors 'none'` / `X-Frame-Options: DENY`, or the console cannot embed iframes.\n */\nexport function isExtensionGatewayUiAssetPath(path: string): boolean {\n return /^\\/api\\/extensions\\/[^/]+\\/assets\\//.test(path);\n}\n\nexport function createHonoApp(config: HonoAppConfig): Hono {\n if (process.env.VITEST) {\n resetLazyRouteBundlesForTests();\n }\n const { service } = config;\n const app = new Hono();\n\n const gatewayPort = resolveGatewayServiceListenPort(service);\n\n const resolveBrowserOrigins = (): string[] =>\n resolveAllowedBrowserOrigins({\n configuredOrigins: service.currentConfig.gateway.corsOrigins,\n port: gatewayPort,\n bindHost: resolveGatewayEffectiveHost(service.currentConfig),\n tunnelPublicUrl: loadTunnelState()?.publicUrl,\n reverseProxyPublicUrl: resolveReverseProxyPublicUrl(service.currentConfig),\n });\n\n /**\n * TCP source for the in-flight request, normalized for trusted-proxy checks.\n * Returns undefined when the runtime doesn't expose conninfo (tests, mocks).\n */\n const resolveRequestRemoteAddress = (c: Context): string | undefined => {\n try {\n return getConnInfo(c).remote.address;\n } catch {\n return undefined;\n }\n };\n\n /**\n * A request's TCP source qualifies as a \"trusted proxy hop\" when it's\n * loopback (the user's own machine, where any reverse proxy lives) or\n * listed in `gateway.trustedProxies`. We use this signal to safely\n * auto-allow same-host Origins through CSRF without requiring a manual\n * `corsOrigins` entry for every reverse-proxy hostname.\n */\n const isRequestFromTrustedProxy = (c: Context): boolean => {\n const remote = resolveRequestRemoteAddress(c);\n if (!remote) return false;\n if (isLoopbackIpAddress(remote)) return true;\n return isTrustedProxyAddress(remote, service.currentConfig.gateway?.trustedProxies);\n };\n\n app.use(logContextMiddleware());\n app.use(logger({\n trustedProxies: service.currentConfig.gateway?.trustedProxies,\n allowRealIpFallback: service.currentConfig.gateway?.allowRealIpFallback === true,\n }));\n\n // Site-share middleware runs BEFORE CORS/CSP — it owns the request when the\n // Host header matches `*.<publicHostSuffix>` (default `*.share.xopc.ai`) or\n // the path starts with `/site/:token/`. Otherwise it falls through.\n registerSiteShareMiddleware(app, service);\n app.use(\n cors({\n origin: (origin) => {\n const allowed = resolveBrowserOrigins();\n if (!origin) {\n return allowed[0] ?? `http://127.0.0.1:${gatewayPort}`;\n }\n const normalized = origin.toLowerCase();\n const hit = allowed.find((entry) => entry.toLowerCase() === normalized);\n if (hit) return origin;\n return allowed.includes('*') ? '*' : '';\n },\n allowMethods: ['GET', 'POST', 'PATCH', 'DELETE', 'OPTIONS'],\n allowHeaders: ['Content-Type', 'Authorization', 'Accept', 'X-Session-Id', 'Last-Event-ID'],\n credentials: true,\n maxAge: 86400,\n }),\n );\n\n // Build CSP header once at startup (no inline script hashes needed for SPA)\n const gatewayConsoleCsp = buildGatewayConsoleCspHeader();\n\n // Security headers middleware\n app.use(createMiddleware(async (c, next) => {\n await next();\n if (isExtensionGatewayUiAssetPath(c.req.path)) {\n return;\n }\n c.header('X-Frame-Options', 'DENY');\n c.header('X-Content-Type-Options', 'nosniff');\n c.header('Referrer-Policy', 'strict-origin-when-cross-origin');\n c.header('X-XSS-Protection', '1; mode=block');\n // microphone=(self): allow same-origin chat voice (composer). microphone=() breaks packaged Electron loading the gateway SPA.\n c.header('Permissions-Policy', 'camera=(), microphone=(self), geolocation=()');\n c.header('Content-Security-Policy', gatewayConsoleCsp);\n }));\n\n // Browser Origin check middleware for API routes (CSRF protection).\n // Non-browser requests (no Origin header) pass through — they are\n // authenticated by the token middleware instead.\n const allowHostHeaderOriginFallback =\n service.currentConfig.gateway?.dangerouslyAllowHostHeaderOriginFallback === true;\n app.use('/api/*', createMiddleware(async (c, next) => {\n // Sandboxed extension iframes (no allow-same-origin) send `Origin: null`.\n // `checkBrowserOrigin` rejects that; these routes rely on CSP instead\n // (`registerPublicExtensionAssetRoutes`).\n if (isExtensionGatewayUiAssetPath(c.req.path)) {\n return next();\n }\n\n const origin = c.req.header('origin');\n if (!origin || origin.trim().toLowerCase() === 'null') {\n // Native apps / opaque origins — authenticated via Bearer token\n return next();\n }\n\n const result = checkBrowserOrigin({\n requestHost: c.req.header('host'),\n origin,\n allowedOrigins: resolveBrowserOrigins(),\n allowHostHeaderOriginFallback,\n autoAllowSameHostFromTrustedProxy: isRequestFromTrustedProxy(c),\n isLocalClient: false,\n });\n\n if (!result.ok) {\n log.warn(\n {\n origin,\n requestHost: c.req.header('host'),\n reason: 'reason' in result ? result.reason : 'unknown',\n path: c.req.path,\n method: c.req.method,\n },\n `Browser origin check failed: ${origin} not in allowed list`,\n );\n return c.json({ error: 'Forbidden', message: 'Origin not allowed' }, 403);\n }\n\n return next();\n }));\n\n const DEFAULT_API_BODY_MAX = 1 * 1024 * 1024;\n const SKILL_UPLOAD_BODY_MAX = 10 * 1024 * 1024;\n const NOTE_MEDIA_BODY_MAX = 25 * 1024 * 1024;\n const VOICE_TRANSCRIBE_BODY_MAX = 35 * 1024 * 1024;\n const WEBCHAT_AGENT_BODY_MAX = maxWebchatAgentRequestBodyBytes();\n\n const isNoteMediaUploadRequest = (path: string, method: string, contentType: string | undefined): boolean => {\n if (method !== 'POST') return false;\n if (/^\\/api\\/notes\\/[^/]+\\/media$/.test(path)) return true;\n return path === '/api/notes' && contentType?.includes('multipart/form-data') === true;\n };\n\n const isWorkItemAttachmentUploadRequest = (path: string, method: string, contentType: string | undefined): boolean => {\n if (method !== 'POST' || contentType?.includes('multipart/form-data') !== true) return false;\n return /^\\/api\\/work-items\\/[^/]+\\/attachments$/.test(path)\n || /^\\/api\\/projects\\/[^/]+\\/work-items$/.test(path);\n };\n\n app.use('/api/skills/upload', bodyLimit({\n maxSize: SKILL_UPLOAD_BODY_MAX,\n onError: (c) => {\n log.warn({ path: c.req.path, maxSizeMb: 10 }, 'Request body too large: skills upload exceeds 10MB limit');\n return c.json({ error: 'Skill package too large', maxSize: '10MB' }, 413);\n },\n }));\n\n app.use('/api/*', async (c, next) => {\n const contentType = c.req.header('content-type');\n const maxSize = c.req.path === '/api/agent'\n ? WEBCHAT_AGENT_BODY_MAX\n : c.req.path === '/api/skills/upload'\n ? SKILL_UPLOAD_BODY_MAX\n : c.req.path === '/api/voice/transcribe'\n ? VOICE_TRANSCRIBE_BODY_MAX\n : isNoteMediaUploadRequest(c.req.path, c.req.method, contentType)\n ? NOTE_MEDIA_BODY_MAX\n : isWorkItemAttachmentUploadRequest(c.req.path, c.req.method, contentType)\n ? WORK_ITEM_ATTACHMENT_UPLOAD_BODY_MAX_BYTES\n : DEFAULT_API_BODY_MAX;\n const maxSizeMb = Math.ceil(maxSize / (1024 * 1024));\n return bodyLimit({\n maxSize,\n onError: (ctx) => {\n log.warn({ path: ctx.req.path, maxSizeMb }, `Request body too large: exceeds ${maxSizeMb}MB limit`);\n return ctx.json({ error: 'Request body too large', maxSize: `${maxSizeMb}MB` }, 413);\n },\n })(c, next);\n });\n\n registerPublicGatewayRoutes(app, service);\n\n // Extension UI assets are served without auth: sandboxed iframes (no allow-same-origin)\n // have an opaque origin of `null` and cannot forward the ?token= from the parent HTML URL.\n // Security is enforced by the strict CSP (frame-ancestors 'self') on every response.\n registerPublicExtensionAssetRoutes(app, service);\n\n const authenticated = new Hono();\n authenticated.use(routeErrorMiddleware());\n authenticated.use(\n auth({\n getGatewayAuth: () => service.currentConfig.gateway?.auth,\n getResolvedAuth: () => service.getResolvedAuth(),\n getTrustedProxyContext: () => ({\n trustedProxies: service.currentConfig.gateway?.trustedProxies,\n allowRealIpFallback: service.currentConfig.gateway?.allowRealIpFallback === true,\n }),\n }),\n );\n authenticated.use(operatorScopes());\n\n const strictRateLimitMiddleware = createStrictRateLimitMiddleware({\n getTrustedProxyContext: () => ({\n trustedProxies: service.currentConfig.gateway?.trustedProxies,\n allowRealIpFallback: service.currentConfig.gateway?.allowRealIpFallback === true,\n }),\n });\n const channelRateLimitMiddleware = createChannelRateLimitMiddleware({\n getTrustedProxyContext: () => ({\n trustedProxies: service.currentConfig.gateway?.trustedProxies,\n allowRealIpFallback: service.currentConfig.gateway?.allowRealIpFallback === true,\n }),\n });\n\n const sseConfig = {\n service,\n maxSseConnections: service.currentConfig.gateway.maxSseConnections,\n };\n\n registerAuthenticatedRoutes(app, authenticated, {\n service,\n strictRateLimitMiddleware,\n channelRateLimitMiddleware,\n sseConfig,\n });\n\n const prewarm = prewarmStaticUiCache();\n if (prewarm.loaded > 0) {\n log.debug({ loaded: prewarm.loaded, missing: prewarm.missing }, 'Static UI cache prewarmed');\n }\n\n app.route('/', authenticated);\n\n app.notFound((c) => {\n const isApiRoute = c.req.path.startsWith('/api/');\n const fields = { path: c.req.path, method: c.req.method };\n if (isApiRoute) {\n log.warn(fields, 'Route not found');\n } else {\n log.debug(fields, 'Route not found');\n }\n return c.json({ error: 'Not found' }, 404);\n });\n\n app.onError((err, c) => {\n log.error(\n {\n err,\n phase: 'gateway.http.unhandled',\n path: c.req.path,\n method: c.req.method,\n userAgent: c.req.header('user-agent'),\n },\n `Hono error on ${c.req.method} ${c.req.path}: ${err instanceof Error ? err.message : String(err)}`,\n );\n return c.json({ error: 'Internal server error' }, 500);\n });\n\n return app;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;aAQqD;AAyBrD,MAAM,MAAM,aAAa,cAAc;;;;;;AAWvC,SAAgB,8BAA8B,MAAuB;AACnE,QAAO,sCAAsC,KAAK,KAAK;;AAGzD,SAAgB,cAAc,QAA6B;AACzD,KAAI,QAAQ,IAAI,OACd,gCAA+B;CAEjC,MAAM,EAAE,YAAY;CACpB,MAAM,MAAM,IAAI,MAAM;CAEtB,MAAM,cAAc,gCAAgC,QAAQ;CAE5D,MAAM,8BACJ,6BAA6B;EAC3B,mBAAmB,QAAQ,cAAc,QAAQ;EACjD,MAAM;EACN,UAAU,4BAA4B,QAAQ,cAAc;EAC5D,iBAAiB,iBAAiB,EAAE;EACpC,uBAAuB,6BAA6B,QAAQ,cAAc;EAC3E,CAAC;;;;;CAMJ,MAAM,+BAA+B,MAAmC;AACtE,MAAI;AACF,UAAO,YAAY,EAAE,CAAC,OAAO;UACvB;AACN;;;;;;;;;;CAWJ,MAAM,6BAA6B,MAAwB;EACzD,MAAM,SAAS,4BAA4B,EAAE;AAC7C,MAAI,CAAC,OAAQ,QAAO;AACpB,MAAI,oBAAoB,OAAO,CAAE,QAAO;AACxC,SAAO,sBAAsB,QAAQ,QAAQ,cAAc,SAAS,eAAe;;AAGrF,KAAI,IAAI,sBAAsB,CAAC;AAC/B,KAAI,IAAI,OAAO;EACb,gBAAgB,QAAQ,cAAc,SAAS;EAC/C,qBAAqB,QAAQ,cAAc,SAAS,wBAAwB;EAC7E,CAAC,CAAC;AAKH,6BAA4B,KAAK,QAAQ;AACzC,KAAI,IACF,KAAK;EACH,SAAS,WAAW;GAClB,MAAM,UAAU,uBAAuB;AACvC,OAAI,CAAC,OACH,QAAO,QAAQ,MAAM,oBAAoB;GAE3C,MAAM,aAAa,OAAO,aAAa;AAEvC,OADY,QAAQ,MAAM,UAAU,MAAM,aAAa,KAAK,WACrD,CAAE,QAAO;AAChB,UAAO,QAAQ,SAAS,IAAI,GAAG,MAAM;;EAEvC,cAAc;GAAC;GAAO;GAAQ;GAAS;GAAU;GAAU;EAC3D,cAAc;GAAC;GAAgB;GAAiB;GAAU;GAAgB;GAAgB;EAC1F,aAAa;EACb,QAAQ;EACT,CAAC,CACH;CAGD,MAAM,oBAAoB,8BAA8B;AAGxD,KAAI,IAAI,iBAAiB,OAAO,GAAG,SAAS;AAC1C,QAAM,MAAM;AACZ,MAAI,8BAA8B,EAAE,IAAI,KAAK,CAC3C;AAEF,IAAE,OAAO,mBAAmB,OAAO;AACnC,IAAE,OAAO,0BAA0B,UAAU;AAC7C,IAAE,OAAO,mBAAmB,kCAAkC;AAC9D,IAAE,OAAO,oBAAoB,gBAAgB;AAE7C,IAAE,OAAO,sBAAsB,+CAA+C;AAC9E,IAAE,OAAO,2BAA2B,kBAAkB;GACtD,CAAC;CAKH,MAAM,gCACJ,QAAQ,cAAc,SAAS,6CAA6C;AAC9E,KAAI,IAAI,UAAU,iBAAiB,OAAO,GAAG,SAAS;AAIpD,MAAI,8BAA8B,EAAE,IAAI,KAAK,CAC3C,QAAO,MAAM;EAGf,MAAM,SAAS,EAAE,IAAI,OAAO,SAAS;AACrC,MAAI,CAAC,UAAU,OAAO,MAAM,CAAC,aAAa,KAAK,OAE7C,QAAO,MAAM;EAGf,MAAM,SAAS,mBAAmB;GAChC,aAAa,EAAE,IAAI,OAAO,OAAO;GACjC;GACA,gBAAgB,uBAAuB;GACvC;GACA,mCAAmC,0BAA0B,EAAE;GAC/D,eAAe;GAChB,CAAC;AAEF,MAAI,CAAC,OAAO,IAAI;AACd,OAAI,KACF;IACE;IACA,aAAa,EAAE,IAAI,OAAO,OAAO;IACjC,QAAQ,YAAY,SAAS,OAAO,SAAS;IAC7C,MAAM,EAAE,IAAI;IACZ,QAAQ,EAAE,IAAI;IACf,EACD,gCAAgC,OAAO,sBACxC;AACD,UAAO,EAAE,KAAK;IAAE,OAAO;IAAa,SAAS;IAAsB,EAAE,IAAI;;AAG3E,SAAO,MAAM;GACb,CAAC;CAEH,MAAM,uBAAuB,IAAI,OAAO;CACxC,MAAM,wBAAwB,KAAK,OAAO;CAC1C,MAAM,sBAAsB,KAAK,OAAO;CACxC,MAAM,4BAA4B,KAAK,OAAO;CAC9C,MAAM,yBAAyB,iCAAiC;CAEhE,MAAM,4BAA4B,MAAc,QAAgB,gBAA6C;AAC3G,MAAI,WAAW,OAAQ,QAAO;AAC9B,MAAI,+BAA+B,KAAK,KAAK,CAAE,QAAO;AACtD,SAAO,SAAS,gBAAgB,aAAa,SAAS,sBAAsB,KAAK;;CAGnF,MAAM,qCAAqC,MAAc,QAAgB,gBAA6C;AACpH,MAAI,WAAW,UAAU,aAAa,SAAS,sBAAsB,KAAK,KAAM,QAAO;AACvF,SAAO,0CAA0C,KAAK,KAAK,IACtD,uCAAuC,KAAK,KAAK;;AAGxD,KAAI,IAAI,sBAAsB,UAAU;EACtC,SAAS;EACT,UAAU,MAAM;AACd,OAAI,KAAK;IAAE,MAAM,EAAE,IAAI;IAAM,WAAW;IAAI,EAAE,2DAA2D;AACzG,UAAO,EAAE,KAAK;IAAE,OAAO;IAA2B,SAAS;IAAQ,EAAE,IAAI;;EAE5E,CAAC,CAAC;AAEH,KAAI,IAAI,UAAU,OAAO,GAAG,SAAS;EACnC,MAAM,cAAc,EAAE,IAAI,OAAO,eAAe;EAChD,MAAM,UAAU,EAAE,IAAI,SAAS,eAC3B,yBACA,EAAE,IAAI,SAAS,uBACb,wBACA,EAAE,IAAI,SAAS,0BACb,4BACA,yBAAyB,EAAE,IAAI,MAAM,EAAE,IAAI,QAAQ,YAAY,GAC7D,sBACA,kCAAkC,EAAE,IAAI,MAAM,EAAE,IAAI,QAAQ,YAAY,GACtE,6CACA;EACZ,MAAM,YAAY,KAAK,KAAK,WAAW,OAAO,MAAM;AACpD,SAAO,UAAU;GACf;GACA,UAAU,QAAQ;AAChB,QAAI,KAAK;KAAE,MAAM,IAAI,IAAI;KAAM;KAAW,EAAE,mCAAmC,UAAU,UAAU;AACnG,WAAO,IAAI,KAAK;KAAE,OAAO;KAA0B,SAAS,GAAG,UAAU;KAAK,EAAE,IAAI;;GAEvF,CAAC,CAAC,GAAG,KAAK;GACX;AAEF,6BAA4B,KAAK,QAAQ;AAKzC,oCAAmC,KAAK,QAAQ;CAEhD,MAAM,gBAAgB,IAAI,MAAM;AAChC,eAAc,IAAI,sBAAsB,CAAC;AACzC,eAAc,IACZ,KAAK;EACH,sBAAsB,QAAQ,cAAc,SAAS;EACrD,uBAAuB,QAAQ,iBAAiB;EAChD,+BAA+B;GAC7B,gBAAgB,QAAQ,cAAc,SAAS;GAC/C,qBAAqB,QAAQ,cAAc,SAAS,wBAAwB;GAC7E;EACF,CAAC,CACH;AACD,eAAc,IAAI,gBAAgB,CAAC;AAoBnC,6BAA4B,KAAK,eAAe;EAC9C;EACA,2BApBgC,gCAAgC,EAChE,+BAA+B;GAC7B,gBAAgB,QAAQ,cAAc,SAAS;GAC/C,qBAAqB,QAAQ,cAAc,SAAS,wBAAwB;GAC7E,GACF,CAe0B;EACzB,4BAfiC,iCAAiC,EAClE,+BAA+B;GAC7B,gBAAgB,QAAQ,cAAc,SAAS;GAC/C,qBAAqB,QAAQ,cAAc,SAAS,wBAAwB;GAC7E,GACF,CAU2B;EAC1B,WAAA;GARA;GACA,mBAAmB,QAAQ,cAAc,QAAQ;GAOxC;EACV,CAAC;CAEF,MAAM,UAAU,sBAAsB;AACtC,KAAI,QAAQ,SAAS,EACnB,KAAI,MAAM;EAAE,QAAQ,QAAQ;EAAQ,SAAS,QAAQ;EAAS,EAAE,4BAA4B;AAG9F,KAAI,MAAM,KAAK,cAAc;AAE7B,KAAI,UAAU,MAAM;EAClB,MAAM,aAAa,EAAE,IAAI,KAAK,WAAW,QAAQ;EACjD,MAAM,SAAS;GAAE,MAAM,EAAE,IAAI;GAAM,QAAQ,EAAE,IAAI;GAAQ;AACzD,MAAI,WACF,KAAI,KAAK,QAAQ,kBAAkB;MAEnC,KAAI,MAAM,QAAQ,kBAAkB;AAEtC,SAAO,EAAE,KAAK,EAAE,OAAO,aAAa,EAAE,IAAI;GAC1C;AAEF,KAAI,SAAS,KAAK,MAAM;AACtB,MAAI,MACF;GACE;GACA,OAAO;GACP,MAAM,EAAE,IAAI;GACZ,QAAQ,EAAE,IAAI;GACd,WAAW,EAAE,IAAI,OAAO,aAAa;GACtC,EACD,iBAAiB,EAAE,IAAI,OAAO,GAAG,EAAE,IAAI,KAAK,IAAI,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GACjG;AACD,SAAO,EAAE,KAAK,EAAE,OAAO,yBAAyB,EAAE,IAAI;GACtD;AAEF,QAAO"}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { GatewayAuthConfig } from '../../../config/schema.js';
|
|
2
2
|
import type { ResolvedGatewayAuth } from '../../auth.js';
|
|
3
3
|
export interface AuthConfig {
|
|
4
|
-
token?: string;
|
|
5
4
|
/** Current gateway auth from config (for rate-limit settings); optional. */
|
|
6
5
|
getGatewayAuth?: () => GatewayAuthConfig | undefined;
|
|
7
6
|
getResolvedAuth?: () => ResolvedGatewayAuth;
|
|
@@ -20,5 +19,4 @@ export declare function auth(config?: AuthConfig): import("hono").MiddlewareHand
|
|
|
20
19
|
}, 429, "json">) | (Response & import("hono").TypedResponse<{
|
|
21
20
|
error: string;
|
|
22
21
|
code: string;
|
|
23
|
-
message: string;
|
|
24
22
|
}, 401, "json">)>;
|
|
@@ -14,9 +14,12 @@ import { getConnInfo } from "@hono/node-server/conninfo";
|
|
|
14
14
|
//#region src/gateway/hono/middleware/auth.ts
|
|
15
15
|
init_logger();
|
|
16
16
|
const log = createLogger("Gateway:Auth");
|
|
17
|
-
function
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
function resolveExpectedCredential(auth) {
|
|
18
|
+
return auth.mode === "token" ? auth.token : auth.mode === "password" ? auth.password : void 0;
|
|
19
|
+
}
|
|
20
|
+
function validateCredential(providedCredential, expectedCredential) {
|
|
21
|
+
if (!providedCredential) return false;
|
|
22
|
+
return safeEqualSecret(providedCredential, expectedCredential);
|
|
20
23
|
}
|
|
21
24
|
function extractTokenFromHeader(authHeader) {
|
|
22
25
|
if (!authHeader) return null;
|
|
@@ -97,10 +100,20 @@ function blockedResponse(c, retryAfterSec) {
|
|
|
97
100
|
}, 429);
|
|
98
101
|
}
|
|
99
102
|
function auth(config) {
|
|
100
|
-
const {
|
|
103
|
+
const { getGatewayAuth, getResolvedAuth, getTrustedProxyContext } = config || {};
|
|
101
104
|
return createMiddleware(async (c, next) => {
|
|
102
105
|
const resolvedAuth = getResolvedAuth?.();
|
|
103
|
-
|
|
106
|
+
if (!resolvedAuth) {
|
|
107
|
+
log.error({
|
|
108
|
+
path: c.req.path,
|
|
109
|
+
method: c.req.method
|
|
110
|
+
}, "HTTP auth rejected: gateway auth is unavailable");
|
|
111
|
+
return c.json({
|
|
112
|
+
error: "Unauthorized",
|
|
113
|
+
code: "auth_unavailable"
|
|
114
|
+
}, 401);
|
|
115
|
+
}
|
|
116
|
+
const authMode = resolvedAuth.mode;
|
|
104
117
|
if (authMode === "trusted-proxy") {
|
|
105
118
|
const proxyContext = getTrustedProxyContext?.();
|
|
106
119
|
const trustedProxies = proxyContext?.trustedProxies;
|
|
@@ -157,21 +170,33 @@ function auth(config) {
|
|
|
157
170
|
await next();
|
|
158
171
|
return;
|
|
159
172
|
}
|
|
160
|
-
if (authMode === "none"
|
|
173
|
+
if (authMode === "none") return next();
|
|
174
|
+
const expectedCredential = resolveExpectedCredential(resolvedAuth);
|
|
175
|
+
if (!expectedCredential) {
|
|
176
|
+
log.error({
|
|
177
|
+
path: c.req.path,
|
|
178
|
+
method: c.req.method,
|
|
179
|
+
authMode
|
|
180
|
+
}, "HTTP auth rejected: credential is unavailable");
|
|
181
|
+
return c.json({
|
|
182
|
+
error: "Unauthorized",
|
|
183
|
+
code: "auth_unconfigured"
|
|
184
|
+
}, 401);
|
|
185
|
+
}
|
|
161
186
|
const proxyContext = getTrustedProxyContext?.();
|
|
162
187
|
const clientIp = resolveMiddlewareClientIp(c, proxyContext?.trustedProxies, proxyContext?.allowRealIpFallback);
|
|
163
188
|
const origin = c.req.header("origin");
|
|
164
189
|
const rl = buildRateLimitContext(getGatewayAuth, clientIp, origin);
|
|
165
190
|
const authHeader = extractTokenFromHeader(c.req.header("authorization"));
|
|
166
191
|
const requestPath = new URL(c.req.url).pathname;
|
|
167
|
-
const queryToken = isQueryTokenAllowedPath(requestPath, c.req.method) ? extractTokenFromQuery(c.req.url) : null;
|
|
192
|
+
const queryToken = authMode === "token" && isQueryTokenAllowedPath(requestPath, c.req.method) ? extractTokenFromQuery(c.req.url) : null;
|
|
168
193
|
if (!authHeader && queryToken === null && new URL(c.req.url).searchParams.has("token")) log.warn({
|
|
169
194
|
path: requestPath,
|
|
170
195
|
method: c.req.method,
|
|
171
196
|
clientIp
|
|
172
|
-
}, "
|
|
173
|
-
const
|
|
174
|
-
if (
|
|
197
|
+
}, "Credential in query string rejected: use Authorization header for this endpoint");
|
|
198
|
+
const providedCredential = authHeader || queryToken;
|
|
199
|
+
if (providedCredential && validateCredential(providedCredential, expectedCredential)) {
|
|
175
200
|
recordSuccess(rl);
|
|
176
201
|
await next();
|
|
177
202
|
return;
|
|
@@ -188,23 +213,23 @@ function auth(config) {
|
|
|
188
213
|
}, "Auth rate limit blocked");
|
|
189
214
|
return blockedResponse(c, blocked.retryAfterSec);
|
|
190
215
|
}
|
|
191
|
-
if (!
|
|
216
|
+
if (!providedCredential) {
|
|
192
217
|
log.warn({
|
|
193
218
|
path: c.req.path,
|
|
194
219
|
method: c.req.method,
|
|
195
220
|
clientIp,
|
|
196
|
-
reason: "
|
|
221
|
+
reason: "missing_credential",
|
|
197
222
|
phase: "gateway.http.auth"
|
|
198
|
-
}, "HTTP auth rejected: no
|
|
223
|
+
}, "HTTP auth rejected: no Authorization credential");
|
|
199
224
|
logAuthEvent("auth.failed", {
|
|
200
225
|
ip: clientIp,
|
|
201
226
|
result: "denied",
|
|
202
|
-
reason: "
|
|
227
|
+
reason: "missing_credential"
|
|
203
228
|
});
|
|
204
229
|
return c.json({
|
|
205
230
|
error: "Unauthorized",
|
|
206
|
-
code: "
|
|
207
|
-
message: "Missing authentication
|
|
231
|
+
code: "missing_credential",
|
|
232
|
+
message: "Missing authentication credential"
|
|
208
233
|
}, 401);
|
|
209
234
|
}
|
|
210
235
|
recordFailure(rl);
|
|
@@ -212,18 +237,18 @@ function auth(config) {
|
|
|
212
237
|
path: c.req.path,
|
|
213
238
|
method: c.req.method,
|
|
214
239
|
clientIp,
|
|
215
|
-
reason: "
|
|
240
|
+
reason: "invalid_credential",
|
|
216
241
|
phase: "gateway.http.auth"
|
|
217
|
-
}, "HTTP auth rejected:
|
|
242
|
+
}, "HTTP auth rejected: credential mismatch");
|
|
218
243
|
logAuthEvent("auth.failed", {
|
|
219
244
|
ip: clientIp,
|
|
220
245
|
result: "failure",
|
|
221
|
-
reason: "
|
|
246
|
+
reason: "invalid_credential"
|
|
222
247
|
});
|
|
223
248
|
return c.json({
|
|
224
249
|
error: "Unauthorized",
|
|
225
|
-
code: "
|
|
226
|
-
message: "Invalid authentication
|
|
250
|
+
code: "invalid_credential",
|
|
251
|
+
message: "Invalid authentication credential"
|
|
227
252
|
}, 401);
|
|
228
253
|
});
|
|
229
254
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","names":[],"sources":["../../../../../src/gateway/hono/middleware/auth.ts"],"sourcesContent":["import { createMiddleware } from 'hono/factory';\nimport type { Context } from 'hono';\nimport { getConnInfo } from '@hono/node-server/conninfo';\n\nimport type { GatewayAuthConfig } from '../../../config/schema.js';\nimport type { ResolvedGatewayAuth } from '../../auth.js';\nimport { resolveClientIpFromRequest } from '../../client-ip.js';\nimport {\n authPolicyConfig,\n buckets,\n isAuthRateLimitGloballyDisabled,\n resolveAuthRateLimit,\n resolveAuthTracking,\n type ResolvedAuthRateLimitConfig,\n} from '../../rate-limit/index.js';\nimport { getClientIpFromHeaders } from '../../security/loopback.js';\nimport { safeEqualSecret } from '../../security/secret-equal.js';\nimport { authorizeTrustedProxy } from '../../trusted-proxy.js';\nimport { createLogger, logAuthEvent } from '../../../utils/logger.js';\n\nconst log = createLogger('Gateway:Auth');\n\nexport interface AuthConfig {\n token?: string;\n /** Current gateway auth from config (for rate-limit settings); optional. */\n getGatewayAuth?: () => GatewayAuthConfig | undefined;\n getResolvedAuth?: () => ResolvedGatewayAuth;\n getTrustedProxyContext?: () => {\n trustedProxies?: string[];\n allowRealIpFallback?: boolean;\n };\n}\n\nfunction validateToken(providedToken: string | undefined, expectedToken: string): boolean {\n if (!providedToken) return false;\n return safeEqualSecret(providedToken, expectedToken);\n}\n\nfunction extractTokenFromHeader(authHeader: string | null): string | null {\n if (!authHeader) return null;\n const parts = authHeader.split(' ');\n if (parts.length === 2 && parts[0].toLowerCase() === 'bearer') return parts[1];\n return authHeader;\n}\n\n/**\n * SECURITY: query-string tokens leak into server logs, Referer headers, and\n * browser history. We accept them only where the `Authorization` header cannot\n * be set — SSE/WebSocket (`EventSource`) and `<img>` subresource loads for agent\n * avatars. Note media uses Bearer-authenticated blob fetch in the gateway console.\n */\nfunction extractTokenFromQuery(url: string): string | null {\n return new URL(url).searchParams.get('token');\n}\n\nconst QUERY_TOKEN_ALLOWED_PATHS = new Set(['/api/events', '/api/ws']);\n\nconst AGENT_AVATAR_GET_PATH = /^\\/api\\/agents\\/[^/]+\\/avatar$/;\n\n/** Exported for gateway security tests. */\nexport function isQueryTokenAllowedPath(path: string, method: string): boolean {\n if (QUERY_TOKEN_ALLOWED_PATHS.has(path) || path.startsWith('/api/events')) {\n return true;\n }\n if (method === 'GET' && AGENT_AVATAR_GET_PATH.test(path)) {\n return true;\n }\n return false;\n}\n\nfunction resolveRemoteAddress(c: Context): string | undefined {\n try {\n return getConnInfo(c).remote.address;\n } catch {\n return undefined;\n }\n}\n\nfunction resolveMiddlewareClientIp(\n c: Context,\n trustedProxies?: string[],\n allowRealIpFallback?: boolean,\n): string {\n if (trustedProxies?.length) {\n return resolveClientIpFromRequest({\n remoteAddress: resolveRemoteAddress(c),\n getHeader: (name) => c.req.header(name),\n trustedProxies,\n allowRealIpFallback,\n });\n }\n return getClientIpFromHeaders({\n get: (name: string) => c.req.header(name) ?? undefined,\n });\n}\n\ntype RateLimitContext = {\n active: boolean;\n cfg: ResolvedAuthRateLimitConfig;\n /** `undefined` when the client is exempted (loopback, disabled, etc.). */\n trackingKey: string | undefined;\n};\n\nfunction buildRateLimitContext(\n getGatewayAuth: AuthConfig['getGatewayAuth'],\n clientIp: string,\n origin: string | undefined,\n): RateLimitContext {\n const cfg = resolveAuthRateLimit(getGatewayAuth?.()?.rateLimit);\n const active = cfg.enabled && !isAuthRateLimitGloballyDisabled();\n if (!active) return { active: false, cfg, trackingKey: undefined };\n const tracking = resolveAuthTracking({ clientIp, origin, cfg: authPolicyConfig(cfg) });\n return {\n active: true,\n cfg,\n trackingKey: tracking.exempt ? undefined : tracking.key,\n };\n}\n\nfunction checkBlocked(rl: RateLimitContext): { blocked: false } | { blocked: true; retryAfterSec: number } {\n if (!rl.active || rl.trackingKey === undefined) return { blocked: false };\n return buckets.authFailure(rl.cfg).check(rl.trackingKey);\n}\n\nfunction recordFailure(rl: RateLimitContext): void {\n if (!rl.active || rl.trackingKey === undefined) return;\n buckets.authFailure(rl.cfg).fail(rl.trackingKey);\n}\n\nfunction recordSuccess(rl: RateLimitContext): void {\n if (!rl.active || rl.trackingKey === undefined) return;\n buckets.authFailure(rl.cfg).succeed(rl.trackingKey);\n}\n\nfunction blockedResponse(c: Context, retryAfterSec: number) {\n c.header('Retry-After', String(retryAfterSec));\n return c.json(\n {\n error: 'Too Many Requests',\n code: 'auth_blocked',\n message: 'Too many authentication attempts',\n retryAfter: retryAfterSec,\n },\n 429,\n );\n}\n\nexport function auth(config?: AuthConfig) {\n const { token, getGatewayAuth, getResolvedAuth, getTrustedProxyContext } = config || {};\n\n return createMiddleware(async (c, next) => {\n const resolvedAuth = getResolvedAuth?.();\n const authMode = resolvedAuth?.mode ?? (token ? 'token' : 'none');\n\n if (authMode === 'trusted-proxy') {\n const proxyContext = getTrustedProxyContext?.();\n const trustedProxies = proxyContext?.trustedProxies;\n const trustedProxyConfig = resolvedAuth?.trustedProxy;\n\n const clientIp = resolveMiddlewareClientIp(c, trustedProxies, proxyContext?.allowRealIpFallback);\n const origin = c.req.header('origin');\n const rl = buildRateLimitContext(getGatewayAuth, clientIp, origin);\n\n // Server misconfiguration — not an attack signal. Don't count.\n if (!trustedProxyConfig) {\n log.warn(\n { path: c.req.path, method: c.req.method, clientIp, reason: 'trusted_proxy_config_missing' },\n 'HTTP auth rejected: trusted-proxy config missing',\n );\n return c.json(\n { error: 'Unauthorized', code: 'auth_unconfigured', message: 'Trusted-proxy auth is not configured' },\n 401,\n );\n }\n\n const blocked = checkBlocked(rl);\n if (blocked.blocked) {\n log.warn(\n { clientIp, origin: origin ?? undefined, path: c.req.path, method: c.req.method, retryAfterSec: blocked.retryAfterSec, reason: 'auth_blocked' },\n 'Auth rate limit blocked',\n );\n return blockedResponse(c, blocked.retryAfterSec);\n }\n\n const result = authorizeTrustedProxy({\n remoteAddress: resolveRemoteAddress(c),\n getHeader: (name) => c.req.header(name),\n trustedProxies,\n trustedProxyConfig,\n });\n\n if (result.ok === false) {\n recordFailure(rl);\n log.warn(\n { path: c.req.path, method: c.req.method, clientIp, reason: result.reason },\n `HTTP auth rejected: trusted-proxy validation failed (${result.reason})`,\n );\n return c.json(\n { error: 'Unauthorized', code: 'invalid_proxy_credentials', message: 'Trusted-proxy authentication failed' },\n 401,\n );\n }\n\n recordSuccess(rl);\n await next();\n return;\n }\n\n if (authMode === 'none' || !token) {\n return next();\n }\n\n const proxyContext = getTrustedProxyContext?.();\n const clientIp = resolveMiddlewareClientIp(c, proxyContext?.trustedProxies, proxyContext?.allowRealIpFallback);\n const origin = c.req.header('origin');\n const rl = buildRateLimitContext(getGatewayAuth, clientIp, origin);\n\n const authHeader = extractTokenFromHeader(c.req.header('authorization'));\n const requestPath = new URL(c.req.url).pathname;\n const queryToken = isQueryTokenAllowedPath(requestPath, c.req.method)\n ? extractTokenFromQuery(c.req.url)\n : null;\n\n if (!authHeader && queryToken === null && new URL(c.req.url).searchParams.has('token')) {\n log.warn(\n { path: requestPath, method: c.req.method, clientIp },\n 'Token in query string rejected: use Authorization header for this endpoint',\n );\n }\n\n const providedToken = authHeader || queryToken;\n\n if (providedToken && validateToken(providedToken, token)) {\n recordSuccess(rl);\n await next();\n return;\n }\n\n const blocked = checkBlocked(rl);\n if (blocked.blocked) {\n log.warn(\n { clientIp, origin: origin ?? undefined, path: requestPath, method: c.req.method, retryAfterSec: blocked.retryAfterSec, reason: 'auth_blocked' },\n 'Auth rate limit blocked',\n );\n return blockedResponse(c, blocked.retryAfterSec);\n }\n\n // Missing token is an unauthenticated request, not a brute-force signal —\n // page reloads / SDK cold starts often hit endpoints before the token is\n // attached. Counting this would lock users out of the token-entry path.\n if (!providedToken) {\n log.warn(\n { path: c.req.path, method: c.req.method, clientIp, reason: 'missing_token', phase: 'gateway.http.auth' },\n 'HTTP auth rejected: no Bearer or ?token=',\n );\n void logAuthEvent('auth.failed', {\n ip: clientIp,\n result: 'denied',\n reason: 'missing_token',\n });\n return c.json(\n { error: 'Unauthorized', code: 'missing_token', message: 'Missing authentication token' },\n 401,\n );\n }\n\n recordFailure(rl);\n log.warn(\n { path: c.req.path, method: c.req.method, clientIp, reason: 'invalid_token', phase: 'gateway.http.auth' },\n 'HTTP auth rejected: token mismatch',\n );\n void logAuthEvent('auth.failed', {\n ip: clientIp,\n result: 'failure',\n reason: 'invalid_token',\n });\n return c.json(\n { error: 'Unauthorized', code: 'invalid_token', message: 'Invalid authentication token' },\n 401,\n );\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"auth.js","names":[],"sources":["../../../../../src/gateway/hono/middleware/auth.ts"],"sourcesContent":["import { createMiddleware } from 'hono/factory';\nimport type { Context } from 'hono';\nimport { getConnInfo } from '@hono/node-server/conninfo';\n\nimport type { GatewayAuthConfig } from '../../../config/schema.js';\nimport type { ResolvedGatewayAuth } from '../../auth.js';\nimport { resolveClientIpFromRequest } from '../../client-ip.js';\nimport {\n authPolicyConfig,\n buckets,\n isAuthRateLimitGloballyDisabled,\n resolveAuthRateLimit,\n resolveAuthTracking,\n type ResolvedAuthRateLimitConfig,\n} from '../../rate-limit/index.js';\nimport { getClientIpFromHeaders } from '../../security/loopback.js';\nimport { safeEqualSecret } from '../../security/secret-equal.js';\nimport { authorizeTrustedProxy } from '../../trusted-proxy.js';\nimport { createLogger, logAuthEvent } from '../../../utils/logger.js';\n\nconst log = createLogger('Gateway:Auth');\n\nexport interface AuthConfig {\n /** Current gateway auth from config (for rate-limit settings); optional. */\n getGatewayAuth?: () => GatewayAuthConfig | undefined;\n getResolvedAuth?: () => ResolvedGatewayAuth;\n getTrustedProxyContext?: () => {\n trustedProxies?: string[];\n allowRealIpFallback?: boolean;\n };\n}\n\nfunction resolveExpectedCredential(auth: ResolvedGatewayAuth): string | undefined {\n return auth.mode === 'token' ? auth.token : auth.mode === 'password' ? auth.password : undefined;\n}\n\nfunction validateCredential(providedCredential: string | undefined, expectedCredential: string): boolean {\n if (!providedCredential) return false;\n return safeEqualSecret(providedCredential, expectedCredential);\n}\n\nfunction extractTokenFromHeader(authHeader: string | null): string | null {\n if (!authHeader) return null;\n const parts = authHeader.split(' ');\n if (parts.length === 2 && parts[0].toLowerCase() === 'bearer') return parts[1];\n return authHeader;\n}\n\n/**\n * SECURITY: query-string tokens leak into server logs, Referer headers, and\n * browser history. We accept them only where the `Authorization` header cannot\n * be set — SSE/WebSocket (`EventSource`) and `<img>` subresource loads for agent\n * avatars. Note media uses Bearer-authenticated blob fetch in the gateway console.\n */\nfunction extractTokenFromQuery(url: string): string | null {\n return new URL(url).searchParams.get('token');\n}\n\nconst QUERY_TOKEN_ALLOWED_PATHS = new Set(['/api/events', '/api/ws']);\n\nconst AGENT_AVATAR_GET_PATH = /^\\/api\\/agents\\/[^/]+\\/avatar$/;\n\n/** Exported for gateway security tests. */\nexport function isQueryTokenAllowedPath(path: string, method: string): boolean {\n if (QUERY_TOKEN_ALLOWED_PATHS.has(path) || path.startsWith('/api/events')) {\n return true;\n }\n if (method === 'GET' && AGENT_AVATAR_GET_PATH.test(path)) {\n return true;\n }\n return false;\n}\n\nfunction resolveRemoteAddress(c: Context): string | undefined {\n try {\n return getConnInfo(c).remote.address;\n } catch {\n return undefined;\n }\n}\n\nfunction resolveMiddlewareClientIp(\n c: Context,\n trustedProxies?: string[],\n allowRealIpFallback?: boolean,\n): string {\n if (trustedProxies?.length) {\n return resolveClientIpFromRequest({\n remoteAddress: resolveRemoteAddress(c),\n getHeader: (name) => c.req.header(name),\n trustedProxies,\n allowRealIpFallback,\n });\n }\n return getClientIpFromHeaders({\n get: (name: string) => c.req.header(name) ?? undefined,\n });\n}\n\ntype RateLimitContext = {\n active: boolean;\n cfg: ResolvedAuthRateLimitConfig;\n /** `undefined` when the client is exempted (loopback, disabled, etc.). */\n trackingKey: string | undefined;\n};\n\nfunction buildRateLimitContext(\n getGatewayAuth: AuthConfig['getGatewayAuth'],\n clientIp: string,\n origin: string | undefined,\n): RateLimitContext {\n const cfg = resolveAuthRateLimit(getGatewayAuth?.()?.rateLimit);\n const active = cfg.enabled && !isAuthRateLimitGloballyDisabled();\n if (!active) return { active: false, cfg, trackingKey: undefined };\n const tracking = resolveAuthTracking({ clientIp, origin, cfg: authPolicyConfig(cfg) });\n return {\n active: true,\n cfg,\n trackingKey: tracking.exempt ? undefined : tracking.key,\n };\n}\n\nfunction checkBlocked(rl: RateLimitContext): { blocked: false } | { blocked: true; retryAfterSec: number } {\n if (!rl.active || rl.trackingKey === undefined) return { blocked: false };\n return buckets.authFailure(rl.cfg).check(rl.trackingKey);\n}\n\nfunction recordFailure(rl: RateLimitContext): void {\n if (!rl.active || rl.trackingKey === undefined) return;\n buckets.authFailure(rl.cfg).fail(rl.trackingKey);\n}\n\nfunction recordSuccess(rl: RateLimitContext): void {\n if (!rl.active || rl.trackingKey === undefined) return;\n buckets.authFailure(rl.cfg).succeed(rl.trackingKey);\n}\n\nfunction blockedResponse(c: Context, retryAfterSec: number) {\n c.header('Retry-After', String(retryAfterSec));\n return c.json(\n {\n error: 'Too Many Requests',\n code: 'auth_blocked',\n message: 'Too many authentication attempts',\n retryAfter: retryAfterSec,\n },\n 429,\n );\n}\n\nexport function auth(config?: AuthConfig) {\n const { getGatewayAuth, getResolvedAuth, getTrustedProxyContext } = config || {};\n\n return createMiddleware(async (c, next) => {\n const resolvedAuth = getResolvedAuth?.();\n if (!resolvedAuth) {\n log.error({ path: c.req.path, method: c.req.method }, 'HTTP auth rejected: gateway auth is unavailable');\n return c.json({ error: 'Unauthorized', code: 'auth_unavailable' }, 401);\n }\n const authMode = resolvedAuth.mode;\n\n if (authMode === 'trusted-proxy') {\n const proxyContext = getTrustedProxyContext?.();\n const trustedProxies = proxyContext?.trustedProxies;\n const trustedProxyConfig = resolvedAuth?.trustedProxy;\n\n const clientIp = resolveMiddlewareClientIp(c, trustedProxies, proxyContext?.allowRealIpFallback);\n const origin = c.req.header('origin');\n const rl = buildRateLimitContext(getGatewayAuth, clientIp, origin);\n\n // Server misconfiguration — not an attack signal. Don't count.\n if (!trustedProxyConfig) {\n log.warn(\n { path: c.req.path, method: c.req.method, clientIp, reason: 'trusted_proxy_config_missing' },\n 'HTTP auth rejected: trusted-proxy config missing',\n );\n return c.json(\n { error: 'Unauthorized', code: 'auth_unconfigured', message: 'Trusted-proxy auth is not configured' },\n 401,\n );\n }\n\n const blocked = checkBlocked(rl);\n if (blocked.blocked) {\n log.warn(\n { clientIp, origin: origin ?? undefined, path: c.req.path, method: c.req.method, retryAfterSec: blocked.retryAfterSec, reason: 'auth_blocked' },\n 'Auth rate limit blocked',\n );\n return blockedResponse(c, blocked.retryAfterSec);\n }\n\n const result = authorizeTrustedProxy({\n remoteAddress: resolveRemoteAddress(c),\n getHeader: (name) => c.req.header(name),\n trustedProxies,\n trustedProxyConfig,\n });\n\n if (result.ok === false) {\n recordFailure(rl);\n log.warn(\n { path: c.req.path, method: c.req.method, clientIp, reason: result.reason },\n `HTTP auth rejected: trusted-proxy validation failed (${result.reason})`,\n );\n return c.json(\n { error: 'Unauthorized', code: 'invalid_proxy_credentials', message: 'Trusted-proxy authentication failed' },\n 401,\n );\n }\n\n recordSuccess(rl);\n await next();\n return;\n }\n\n if (authMode === 'none') {\n return next();\n }\n\n const expectedCredential = resolveExpectedCredential(resolvedAuth);\n if (!expectedCredential) {\n log.error({ path: c.req.path, method: c.req.method, authMode }, 'HTTP auth rejected: credential is unavailable');\n return c.json({ error: 'Unauthorized', code: 'auth_unconfigured' }, 401);\n }\n\n const proxyContext = getTrustedProxyContext?.();\n const clientIp = resolveMiddlewareClientIp(c, proxyContext?.trustedProxies, proxyContext?.allowRealIpFallback);\n const origin = c.req.header('origin');\n const rl = buildRateLimitContext(getGatewayAuth, clientIp, origin);\n\n const authHeader = extractTokenFromHeader(c.req.header('authorization'));\n const requestPath = new URL(c.req.url).pathname;\n const queryToken = authMode === 'token' && isQueryTokenAllowedPath(requestPath, c.req.method)\n ? extractTokenFromQuery(c.req.url)\n : null;\n\n if (!authHeader && queryToken === null && new URL(c.req.url).searchParams.has('token')) {\n log.warn(\n { path: requestPath, method: c.req.method, clientIp },\n 'Credential in query string rejected: use Authorization header for this endpoint',\n );\n }\n\n const providedCredential = authHeader || queryToken;\n\n if (providedCredential && validateCredential(providedCredential, expectedCredential)) {\n recordSuccess(rl);\n await next();\n return;\n }\n\n const blocked = checkBlocked(rl);\n if (blocked.blocked) {\n log.warn(\n { clientIp, origin: origin ?? undefined, path: requestPath, method: c.req.method, retryAfterSec: blocked.retryAfterSec, reason: 'auth_blocked' },\n 'Auth rate limit blocked',\n );\n return blockedResponse(c, blocked.retryAfterSec);\n }\n\n // Missing credentials are unauthenticated requests, not brute-force signals —\n // page reloads / SDK cold starts often hit endpoints before credentials are\n // attached. Counting this would lock users out of the token-entry path.\n if (!providedCredential) {\n log.warn(\n { path: c.req.path, method: c.req.method, clientIp, reason: 'missing_credential', phase: 'gateway.http.auth' },\n 'HTTP auth rejected: no Authorization credential',\n );\n void logAuthEvent('auth.failed', {\n ip: clientIp,\n result: 'denied',\n reason: 'missing_credential',\n });\n return c.json(\n { error: 'Unauthorized', code: 'missing_credential', message: 'Missing authentication credential' },\n 401,\n );\n }\n\n recordFailure(rl);\n log.warn(\n { path: c.req.path, method: c.req.method, clientIp, reason: 'invalid_credential', phase: 'gateway.http.auth' },\n 'HTTP auth rejected: credential mismatch',\n );\n void logAuthEvent('auth.failed', {\n ip: clientIp,\n result: 'failure',\n reason: 'invalid_credential',\n });\n return c.json(\n { error: 'Unauthorized', code: 'invalid_credential', message: 'Invalid authentication credential' },\n 401,\n );\n });\n}\n"],"mappings":";;;;;;;;;;;;;;aAkBsE;AAEtE,MAAM,MAAM,aAAa,eAAe;AAYxC,SAAS,0BAA0B,MAA+C;AAChF,QAAO,KAAK,SAAS,UAAU,KAAK,QAAQ,KAAK,SAAS,aAAa,KAAK,WAAW,KAAA;;AAGzF,SAAS,mBAAmB,oBAAwC,oBAAqC;AACvG,KAAI,CAAC,mBAAoB,QAAO;AAChC,QAAO,gBAAgB,oBAAoB,mBAAmB;;AAGhE,SAAS,uBAAuB,YAA0C;AACxE,KAAI,CAAC,WAAY,QAAO;CACxB,MAAM,QAAQ,WAAW,MAAM,IAAI;AACnC,KAAI,MAAM,WAAW,KAAK,MAAM,GAAG,aAAa,KAAK,SAAU,QAAO,MAAM;AAC5E,QAAO;;;;;;;;AAST,SAAS,sBAAsB,KAA4B;AACzD,QAAO,IAAI,IAAI,IAAI,CAAC,aAAa,IAAI,QAAQ;;AAG/C,MAAM,4BAA4B,IAAI,IAAI,CAAC,eAAe,UAAU,CAAC;AAErE,MAAM,wBAAwB;;AAG9B,SAAgB,wBAAwB,MAAc,QAAyB;AAC7E,KAAI,0BAA0B,IAAI,KAAK,IAAI,KAAK,WAAW,cAAc,CACvE,QAAO;AAET,KAAI,WAAW,SAAS,sBAAsB,KAAK,KAAK,CACtD,QAAO;AAET,QAAO;;AAGT,SAAS,qBAAqB,GAAgC;AAC5D,KAAI;AACF,SAAO,YAAY,EAAE,CAAC,OAAO;SACvB;AACN;;;AAIJ,SAAS,0BACP,GACA,gBACA,qBACQ;AACR,KAAI,gBAAgB,OAClB,QAAO,2BAA2B;EAChC,eAAe,qBAAqB,EAAE;EACtC,YAAY,SAAS,EAAE,IAAI,OAAO,KAAK;EACvC;EACA;EACD,CAAC;AAEJ,QAAO,uBAAuB,EAC5B,MAAM,SAAiB,EAAE,IAAI,OAAO,KAAK,IAAI,KAAA,GAC9C,CAAC;;AAUJ,SAAS,sBACP,gBACA,UACA,QACkB;CAClB,MAAM,MAAM,qBAAqB,kBAAkB,EAAE,UAAU;AAE/D,KAAI,EADW,IAAI,WAAW,CAAC,iCAAiC,EACnD,QAAO;EAAE,QAAQ;EAAO;EAAK,aAAa,KAAA;EAAW;CAClE,MAAM,WAAW,oBAAoB;EAAE;EAAU;EAAQ,KAAK,iBAAiB,IAAI;EAAE,CAAC;AACtF,QAAO;EACL,QAAQ;EACR;EACA,aAAa,SAAS,SAAS,KAAA,IAAY,SAAS;EACrD;;AAGH,SAAS,aAAa,IAAqF;AACzG,KAAI,CAAC,GAAG,UAAU,GAAG,gBAAgB,KAAA,EAAW,QAAO,EAAE,SAAS,OAAO;AACzE,QAAO,QAAQ,YAAY,GAAG,IAAI,CAAC,MAAM,GAAG,YAAY;;AAG1D,SAAS,cAAc,IAA4B;AACjD,KAAI,CAAC,GAAG,UAAU,GAAG,gBAAgB,KAAA,EAAW;AAChD,SAAQ,YAAY,GAAG,IAAI,CAAC,KAAK,GAAG,YAAY;;AAGlD,SAAS,cAAc,IAA4B;AACjD,KAAI,CAAC,GAAG,UAAU,GAAG,gBAAgB,KAAA,EAAW;AAChD,SAAQ,YAAY,GAAG,IAAI,CAAC,QAAQ,GAAG,YAAY;;AAGrD,SAAS,gBAAgB,GAAY,eAAuB;AAC1D,GAAE,OAAO,eAAe,OAAO,cAAc,CAAC;AAC9C,QAAO,EAAE,KACP;EACE,OAAO;EACP,MAAM;EACN,SAAS;EACT,YAAY;EACb,EACD,IACD;;AAGH,SAAgB,KAAK,QAAqB;CACxC,MAAM,EAAE,gBAAgB,iBAAiB,2BAA2B,UAAU,EAAE;AAEhF,QAAO,iBAAiB,OAAO,GAAG,SAAS;EACzC,MAAM,eAAe,mBAAmB;AACxC,MAAI,CAAC,cAAc;AACjB,OAAI,MAAM;IAAE,MAAM,EAAE,IAAI;IAAM,QAAQ,EAAE,IAAI;IAAQ,EAAE,kDAAkD;AACxG,UAAO,EAAE,KAAK;IAAE,OAAO;IAAgB,MAAM;IAAoB,EAAE,IAAI;;EAEzE,MAAM,WAAW,aAAa;AAE9B,MAAI,aAAa,iBAAiB;GAChC,MAAM,eAAe,0BAA0B;GAC/C,MAAM,iBAAiB,cAAc;GACrC,MAAM,qBAAqB,cAAc;GAEzC,MAAM,WAAW,0BAA0B,GAAG,gBAAgB,cAAc,oBAAoB;GAChG,MAAM,SAAS,EAAE,IAAI,OAAO,SAAS;GACrC,MAAM,KAAK,sBAAsB,gBAAgB,UAAU,OAAO;AAGlE,OAAI,CAAC,oBAAoB;AACvB,QAAI,KACF;KAAE,MAAM,EAAE,IAAI;KAAM,QAAQ,EAAE,IAAI;KAAQ;KAAU,QAAQ;KAAgC,EAC5F,mDACD;AACD,WAAO,EAAE,KACP;KAAE,OAAO;KAAgB,MAAM;KAAqB,SAAS;KAAwC,EACrG,IACD;;GAGH,MAAM,UAAU,aAAa,GAAG;AAChC,OAAI,QAAQ,SAAS;AACnB,QAAI,KACF;KAAE;KAAU,QAAQ,UAAU,KAAA;KAAW,MAAM,EAAE,IAAI;KAAM,QAAQ,EAAE,IAAI;KAAQ,eAAe,QAAQ;KAAe,QAAQ;KAAgB,EAC/I,0BACD;AACD,WAAO,gBAAgB,GAAG,QAAQ,cAAc;;GAGlD,MAAM,SAAS,sBAAsB;IACnC,eAAe,qBAAqB,EAAE;IACtC,YAAY,SAAS,EAAE,IAAI,OAAO,KAAK;IACvC;IACA;IACD,CAAC;AAEF,OAAI,OAAO,OAAO,OAAO;AACvB,kBAAc,GAAG;AACjB,QAAI,KACF;KAAE,MAAM,EAAE,IAAI;KAAM,QAAQ,EAAE,IAAI;KAAQ;KAAU,QAAQ,OAAO;KAAQ,EAC3E,wDAAwD,OAAO,OAAO,GACvE;AACD,WAAO,EAAE,KACP;KAAE,OAAO;KAAgB,MAAM;KAA6B,SAAS;KAAuC,EAC5G,IACD;;AAGH,iBAAc,GAAG;AACjB,SAAM,MAAM;AACZ;;AAGF,MAAI,aAAa,OACf,QAAO,MAAM;EAGf,MAAM,qBAAqB,0BAA0B,aAAa;AAClE,MAAI,CAAC,oBAAoB;AACvB,OAAI,MAAM;IAAE,MAAM,EAAE,IAAI;IAAM,QAAQ,EAAE,IAAI;IAAQ;IAAU,EAAE,gDAAgD;AAChH,UAAO,EAAE,KAAK;IAAE,OAAO;IAAgB,MAAM;IAAqB,EAAE,IAAI;;EAG1E,MAAM,eAAe,0BAA0B;EAC/C,MAAM,WAAW,0BAA0B,GAAG,cAAc,gBAAgB,cAAc,oBAAoB;EAC9G,MAAM,SAAS,EAAE,IAAI,OAAO,SAAS;EACrC,MAAM,KAAK,sBAAsB,gBAAgB,UAAU,OAAO;EAElE,MAAM,aAAa,uBAAuB,EAAE,IAAI,OAAO,gBAAgB,CAAC;EACxE,MAAM,cAAc,IAAI,IAAI,EAAE,IAAI,IAAI,CAAC;EACvC,MAAM,aAAa,aAAa,WAAW,wBAAwB,aAAa,EAAE,IAAI,OAAO,GACzF,sBAAsB,EAAE,IAAI,IAAI,GAChC;AAEJ,MAAI,CAAC,cAAc,eAAe,QAAQ,IAAI,IAAI,EAAE,IAAI,IAAI,CAAC,aAAa,IAAI,QAAQ,CACpF,KAAI,KACF;GAAE,MAAM;GAAa,QAAQ,EAAE,IAAI;GAAQ;GAAU,EACrD,kFACD;EAGH,MAAM,qBAAqB,cAAc;AAEzC,MAAI,sBAAsB,mBAAmB,oBAAoB,mBAAmB,EAAE;AACpF,iBAAc,GAAG;AACjB,SAAM,MAAM;AACZ;;EAGF,MAAM,UAAU,aAAa,GAAG;AAChC,MAAI,QAAQ,SAAS;AACnB,OAAI,KACF;IAAE;IAAU,QAAQ,UAAU,KAAA;IAAW,MAAM;IAAa,QAAQ,EAAE,IAAI;IAAQ,eAAe,QAAQ;IAAe,QAAQ;IAAgB,EAChJ,0BACD;AACD,UAAO,gBAAgB,GAAG,QAAQ,cAAc;;AAMlD,MAAI,CAAC,oBAAoB;AACvB,OAAI,KACF;IAAE,MAAM,EAAE,IAAI;IAAM,QAAQ,EAAE,IAAI;IAAQ;IAAU,QAAQ;IAAsB,OAAO;IAAqB,EAC9G,kDACD;AACI,gBAAa,eAAe;IAC/B,IAAI;IACJ,QAAQ;IACR,QAAQ;IACT,CAAC;AACF,UAAO,EAAE,KACP;IAAE,OAAO;IAAgB,MAAM;IAAsB,SAAS;IAAqC,EACnG,IACD;;AAGH,gBAAc,GAAG;AACjB,MAAI,KACF;GAAE,MAAM,EAAE,IAAI;GAAM,QAAQ,EAAE,IAAI;GAAQ;GAAU,QAAQ;GAAsB,OAAO;GAAqB,EAC9G,0CACD;AACI,eAAa,eAAe;GAC/B,IAAI;GACJ,QAAQ;GACR,QAAQ;GACT,CAAC;AACF,SAAO,EAAE,KACP;GAAE,OAAO;GAAgB,MAAM;GAAsB,SAAS;GAAqC,EACnG,IACD;GACD"}
|
|
@@ -7,6 +7,7 @@ import { deleteMediaUrisNoLongerReferenced } from "../../../media/session-refere
|
|
|
7
7
|
import { createGatewayRouteLogger, logRouteError } from "../lib/route-logger.js";
|
|
8
8
|
import { computeUserRoundDeleteRange } from "../../../session/user-round-delete.js";
|
|
9
9
|
import { respondStartupUnavailable } from "../lib/startup-unavailable.js";
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
10
11
|
//#region src/gateway/hono/routes/sessions.ts
|
|
11
12
|
init_session_key();
|
|
12
13
|
const log = createGatewayRouteLogger("Sessions");
|
|
@@ -54,16 +55,6 @@ function buildDirectSessionMetadata(params) {
|
|
|
54
55
|
customData: { genericNewChatShell: params.source === "webchat" && params.peerId.startsWith("chat_") }
|
|
55
56
|
};
|
|
56
57
|
}
|
|
57
|
-
function isGenericNewChatShell(session, agentId, projectId) {
|
|
58
|
-
if (session.messageCount !== 0) return false;
|
|
59
|
-
if (session.routing?.agentId !== agentId) return false;
|
|
60
|
-
if (projectId ? session.projectId !== projectId : Boolean(session.projectId)) return false;
|
|
61
|
-
if (session.parentSessionKey || session.workflowRunId || session.workflowDefinitionId) return false;
|
|
62
|
-
if (session.customData?.genericNewChatShell === false) return false;
|
|
63
|
-
const sourceBinding = session.customData?.sourceBinding;
|
|
64
|
-
if (sourceBinding && typeof sourceBinding === "object") return false;
|
|
65
|
-
return Boolean(session.routing?.peerId?.startsWith("chat_"));
|
|
66
|
-
}
|
|
67
58
|
function registerSessionsRoutes(authenticated, deps) {
|
|
68
59
|
const { service } = deps;
|
|
69
60
|
authenticated.get("/api/sidebar/chat-list", async (c) => {
|
|
@@ -158,23 +149,7 @@ function registerSessionsRoutes(authenticated, deps) {
|
|
|
158
149
|
const session = await service.sessions.getSession(sessionKey);
|
|
159
150
|
return c.json({ session }, 201);
|
|
160
151
|
}
|
|
161
|
-
const
|
|
162
|
-
channel,
|
|
163
|
-
includeHidden: true,
|
|
164
|
-
limit: 50,
|
|
165
|
-
sortBy: "updatedAt",
|
|
166
|
-
sortOrder: "desc"
|
|
167
|
-
})).items.find((s) => {
|
|
168
|
-
return isGenericNewChatShell(s, agentId, projectId);
|
|
169
|
-
});
|
|
170
|
-
if (emptySession) {
|
|
171
|
-
const session = await service.sessions.getSession(emptySession.key);
|
|
172
|
-
return c.json({
|
|
173
|
-
session,
|
|
174
|
-
reused: true
|
|
175
|
-
}, 200);
|
|
176
|
-
}
|
|
177
|
-
const chatId = `chat_${Date.now()}`;
|
|
152
|
+
const chatId = `chat_${randomUUID()}`;
|
|
178
153
|
const sessionKey = buildSessionKey({
|
|
179
154
|
agentId,
|
|
180
155
|
source: channel,
|