@xopcai/xopc 0.0.88 → 0.0.90
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/README.md +8 -1
- package/README.zh-CN.md +8 -1
- package/dist/browser-ext/manifest.json +1 -1
- package/dist/extensions/telegram/xopc.extension.json +1 -1
- package/dist/gateway/static/root/assets/agents-cPvvYLXo.js +222 -0
- package/dist/gateway/static/root/assets/apps-page-Bk1_P5FJ.js +1 -0
- package/dist/gateway/static/root/assets/channels-settings-CZoeQwHz.js +1 -0
- package/dist/gateway/static/root/assets/{channels-status-swr-DIsl75Y3.js → channels-status-swr-BrtH2VzC.js} +1 -1
- package/dist/gateway/static/root/assets/circle-check-C23XjkUj.js +1 -0
- package/dist/gateway/static/root/assets/cron-api-CyqbgfHM.js +1 -0
- package/dist/gateway/static/root/assets/cron-dreaming-jobs-Ip703-qM.js +2 -0
- package/dist/gateway/static/root/assets/cron-page-BpLdiQN8.js +1 -0
- package/dist/gateway/static/root/assets/dist-BpAiK86n.js +1 -0
- package/dist/gateway/static/root/assets/{extension-debug-page-BVJohZoZ.js → extension-debug-page-D6Ak0STa.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-page-BT2tmElC.js → extension-page-Q0P3d6DW.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-settings-page-BSS47c2j.js → extension-settings-page-CL55LwU_.js} +1 -1
- package/dist/gateway/static/root/assets/eye-DAfL1U7M.js +1 -0
- package/dist/gateway/static/root/assets/{fetch-BaFNUtkE.js → fetch-Dqa9iTWl.js} +1 -1
- package/dist/gateway/static/root/assets/{field-primitives-QwYEq6Hz.js → field-primitives-HUR6JElP.js} +1 -1
- package/dist/gateway/static/root/assets/{heartbeat-config-api-BVSidEDJ.js → heartbeat-config-api-DusckjUX.js} +1 -1
- package/dist/gateway/static/root/assets/{index-qNrVJp-y.js → index-BYcGfwcE.js} +97 -97
- package/dist/gateway/static/root/assets/index-V7MQ7834.css +1 -0
- package/dist/gateway/static/root/assets/{logs-page-DDonPVLn.js → logs-page-_HcZ2fgK.js} +1 -1
- package/dist/gateway/static/root/assets/sessions-page-iezSMjho.js +1 -0
- package/dist/gateway/static/root/assets/{settings-form-section-B8N3A3Zo.js → settings-form-section-a0qGVOlr.js} +1 -1
- package/dist/gateway/static/root/assets/settings-page-C9_nYQwM.js +3 -0
- package/dist/gateway/static/root/assets/{share-preview-page-Q7KqkO-u.js → share-preview-page-DExl7CJy.js} +1 -1
- package/dist/gateway/static/root/assets/skills-page-BlgGD93t.js +2 -0
- package/dist/gateway/static/root/assets/{theme-store-BbRc5ugR.js → theme-store-C0Ehmdo5.js} +1 -1
- package/dist/gateway/static/root/assets/url-fxyYANfA.js +3 -0
- package/dist/gateway/static/root/assets/{utils-CxDGduqK.js → utils-DRQryzdn.js} +1 -1
- package/dist/gateway/static/root/assets/voice-api-key-field-D0viACE2.js +1 -0
- package/dist/gateway/static/root/assets/workflow-page.utils-DnG8JBhV.js +1 -0
- package/dist/gateway/static/root/assets/workflows-page-BvMobnJP.js +27 -0
- package/dist/gateway/static/root/index.html +7 -6
- package/dist/package.js +1 -1
- package/dist/src/agent/agent-manager.d.ts +2 -0
- package/dist/src/agent/agent-manager.js +1 -0
- package/dist/src/agent/agent-manager.js.map +1 -1
- package/dist/src/agent/service.js +2 -1
- package/dist/src/agent/service.js.map +1 -1
- package/dist/src/agent/service.types.d.ts +3 -1
- package/dist/src/agent/skills/marketplace/adapters/skillhub/adapter.js +20 -18
- package/dist/src/agent/skills/marketplace/adapters/skillhub/adapter.js.map +1 -1
- package/dist/src/agent/tools/cronjob-tool.d.ts +6 -0
- package/dist/src/agent/tools/cronjob-tool.js +76 -10
- package/dist/src/agent/tools/cronjob-tool.js.map +1 -1
- package/dist/src/agent/tools/edit.d.ts +5 -1
- package/dist/src/agent/tools/edit.js +7 -5
- package/dist/src/agent/tools/edit.js.map +1 -1
- package/dist/src/agent/tools/factory.d.ts +3 -0
- package/dist/src/agent/tools/factory.js +4 -25
- package/dist/src/agent/tools/factory.js.map +1 -1
- package/dist/src/agent/tools/workflow-tool.d.ts +6 -28
- package/dist/src/agent/tools/workflow-tool.js +60 -260
- package/dist/src/agent/tools/workflow-tool.js.map +1 -1
- package/dist/src/agent/tools/write.d.ts +5 -1
- package/dist/src/agent/tools/write.js +7 -5
- package/dist/src/agent/tools/write.js.map +1 -1
- package/dist/src/agent/workflow/agent-progress.js +2 -0
- package/dist/src/agent/workflow/agent-progress.js.map +1 -1
- package/dist/src/agent/workflow/builtins/client-proposal.d.ts +12 -0
- package/dist/src/agent/workflow/builtins/client-proposal.js +155 -0
- package/dist/src/agent/workflow/builtins/client-proposal.js.map +1 -0
- package/dist/src/agent/workflow/builtins/competitor-scan.d.ts +12 -0
- package/dist/src/agent/workflow/builtins/competitor-scan.js +150 -0
- package/dist/src/agent/workflow/builtins/competitor-scan.js.map +1 -0
- package/dist/src/agent/workflow/builtins/content-draft.d.ts +13 -0
- package/dist/src/agent/workflow/builtins/content-draft.js +146 -0
- package/dist/src/agent/workflow/builtins/content-draft.js.map +1 -0
- package/dist/src/agent/workflow/builtins/content-repurpose.d.ts +11 -0
- package/dist/src/agent/workflow/builtins/content-repurpose.js +137 -0
- package/dist/src/agent/workflow/builtins/content-repurpose.js.map +1 -0
- package/dist/src/agent/workflow/builtins/decision-compare.d.ts +13 -0
- package/dist/src/agent/workflow/builtins/decision-compare.js +173 -0
- package/dist/src/agent/workflow/builtins/decision-compare.js.map +1 -0
- package/dist/src/agent/workflow/builtins/inbox-triage.d.ts +11 -0
- package/dist/src/agent/workflow/builtins/inbox-triage.js +148 -0
- package/dist/src/agent/workflow/builtins/inbox-triage.js.map +1 -0
- package/dist/src/agent/workflow/builtins/index.d.ts +10 -1
- package/dist/src/agent/workflow/builtins/index.js +46 -1
- package/dist/src/agent/workflow/builtins/index.js.map +1 -1
- package/dist/src/agent/workflow/builtins/meeting-prep.d.ts +12 -0
- package/dist/src/agent/workflow/builtins/meeting-prep.js +144 -0
- package/dist/src/agent/workflow/builtins/meeting-prep.js.map +1 -0
- package/dist/src/agent/workflow/builtins/offer-design.d.ts +12 -0
- package/dist/src/agent/workflow/builtins/offer-design.js +161 -0
- package/dist/src/agent/workflow/builtins/offer-design.js.map +1 -0
- package/dist/src/agent/workflow/builtins/weekly-review.d.ts +12 -0
- package/dist/src/agent/workflow/builtins/weekly-review.js +131 -0
- package/dist/src/agent/workflow/builtins/weekly-review.js.map +1 -0
- package/dist/src/agent/workflow/step-labels.js +2 -2
- package/dist/src/agent/workflow/step-labels.js.map +1 -1
- package/dist/src/agent/workflow/subagent-runner.js +3 -1
- package/dist/src/agent/workflow/subagent-runner.js.map +1 -1
- package/dist/src/agent/workflow/types.d.ts +4 -0
- package/dist/src/agent/workflow/workflow-child-tools.d.ts +4 -0
- package/dist/src/agent/workflow/workflow-child-tools.js +21 -0
- package/dist/src/agent/workflow/workflow-child-tools.js.map +1 -0
- package/dist/src/auth/credentials.d.ts +14 -2
- package/dist/src/auth/credentials.js +38 -13
- package/dist/src/auth/credentials.js.map +1 -1
- package/dist/src/auth/oauth/types.d.ts +16 -0
- package/dist/src/chat-commands/agent-edit.d.ts +4 -0
- package/dist/src/chat-commands/agent-edit.js +136 -0
- package/dist/src/chat-commands/agent-edit.js.map +1 -0
- package/dist/src/chat-commands/index.d.ts +1 -0
- package/dist/src/chat-commands/index.js +3 -1
- package/dist/src/chat-commands/index.js.map +1 -1
- package/dist/src/cli/bin.js +2 -0
- package/dist/src/cli/bin.js.map +1 -1
- package/dist/src/cli/commands/auth.js +6 -0
- package/dist/src/cli/commands/auth.js.map +1 -1
- package/dist/src/cli/commands/cron.js +42 -3
- package/dist/src/cli/commands/cron.js.map +1 -1
- package/dist/src/cli/commands/doctor/checks/session-integrity.js +79 -56
- package/dist/src/cli/commands/doctor/checks/session-integrity.js.map +1 -1
- package/dist/src/cli/commands/onboard/model.js +6 -0
- package/dist/src/cli/commands/onboard/model.js.map +1 -1
- package/dist/src/cli/commands/update.js +86 -79
- package/dist/src/cli/commands/update.js.map +1 -1
- package/dist/src/commands/agents.config.d.ts +3 -2
- package/dist/src/commands/agents.config.js +5 -2
- package/dist/src/commands/agents.config.js.map +1 -1
- package/dist/src/config/agent-typed-models.d.ts +2 -7
- package/dist/src/config/agent-typed-models.js +3 -14
- package/dist/src/config/agent-typed-models.js.map +1 -1
- package/dist/src/config/localized-text.d.ts +6 -0
- package/dist/src/config/localized-text.js +42 -0
- package/dist/src/config/localized-text.js.map +1 -0
- package/dist/src/config/models-json.d.ts +6 -6
- package/dist/src/config/schema.d.ts +6 -21
- package/dist/src/config/schema.js +4 -4
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/cron/executor.d.ts +4 -0
- package/dist/src/cron/executor.js +169 -5
- package/dist/src/cron/executor.js.map +1 -1
- package/dist/src/cron/job-content.js +2 -1
- package/dist/src/cron/job-content.js.map +1 -1
- package/dist/src/cron/types.d.ts +28 -1
- package/dist/src/cron/validation.d.ts +80 -0
- package/dist/src/cron/validation.js +30 -4
- package/dist/src/cron/validation.js.map +1 -1
- package/dist/src/cron/workflow-run-completion.d.ts +23 -0
- package/dist/src/cron/workflow-run-completion.js +72 -0
- package/dist/src/cron/workflow-run-completion.js.map +1 -0
- package/dist/src/extensions/update.d.ts +51 -0
- package/dist/src/extensions/update.js +260 -0
- package/dist/src/extensions/update.js.map +1 -0
- package/dist/src/gateway/agents-admin.d.ts +15 -8
- package/dist/src/gateway/agents-admin.js +77 -28
- package/dist/src/gateway/agents-admin.js.map +1 -1
- package/dist/src/gateway/gateway-workflow-host.types.d.ts +17 -0
- package/dist/src/gateway/gateway-workflow-host.types.js +1 -0
- package/dist/src/gateway/heartbeat/service.js +1 -1
- package/dist/src/gateway/hono/lib/config-payload.d.ts +5 -0
- package/dist/src/gateway/hono/lib/config-payload.js +2 -1
- package/dist/src/gateway/hono/lib/config-payload.js.map +1 -1
- package/dist/src/gateway/hono/middleware/auth.d.ts +2 -0
- package/dist/src/gateway/hono/middleware/auth.js +12 -7
- package/dist/src/gateway/hono/middleware/auth.js.map +1 -1
- package/dist/src/gateway/hono/oauth-async.js +40 -15
- package/dist/src/gateway/hono/oauth-async.js.map +1 -1
- package/dist/src/gateway/hono/oauth.js +31 -6
- package/dist/src/gateway/hono/oauth.js.map +1 -1
- package/dist/src/gateway/hono/routes/agents.js +55 -12
- package/dist/src/gateway/hono/routes/agents.js.map +1 -1
- package/dist/src/gateway/hono/routes/config-patch/agents.js +1 -1
- package/dist/src/gateway/hono/routes/models.js +11 -5
- package/dist/src/gateway/hono/routes/models.js.map +1 -1
- package/dist/src/gateway/hono/routes/update.js +55 -107
- package/dist/src/gateway/hono/routes/update.js.map +1 -1
- package/dist/src/gateway/hono/routes/workflows.js +72 -191
- package/dist/src/gateway/hono/routes/workflows.js.map +1 -1
- package/dist/src/gateway/server.js +2 -0
- package/dist/src/gateway/server.js.map +1 -1
- package/dist/src/gateway/service.d.ts +5 -0
- package/dist/src/gateway/service.js +24 -3
- package/dist/src/gateway/service.js.map +1 -1
- package/dist/src/heartbeat/index.js +1 -1
- package/dist/src/infra/brew.d.ts +4 -0
- package/dist/src/infra/brew.js +20 -0
- package/dist/src/infra/brew.js.map +1 -0
- package/dist/src/infra/package-json.d.ts +2 -0
- package/dist/src/infra/package-json.js +23 -0
- package/dist/src/infra/package-json.js.map +1 -0
- package/dist/src/infra/package-update-steps.d.ts +35 -0
- package/dist/src/infra/package-update-steps.js +304 -0
- package/dist/src/infra/package-update-steps.js.map +1 -0
- package/dist/src/infra/path-env.d.ts +11 -0
- package/dist/src/infra/path-env.js +90 -0
- package/dist/src/infra/path-env.js.map +1 -0
- package/dist/src/infra/path-prepend.d.ts +7 -0
- package/dist/src/infra/path-prepend.js +44 -0
- package/dist/src/infra/path-prepend.js.map +1 -0
- package/dist/src/infra/stable-node-path.d.ts +2 -0
- package/dist/src/infra/stable-node-path.js +28 -0
- package/dist/src/infra/stable-node-path.js.map +1 -0
- package/dist/src/infra/update-global.d.ts +30 -23
- package/dist/src/infra/update-global.js +113 -64
- package/dist/src/infra/update-global.js.map +1 -1
- package/dist/src/infra/update-log.d.ts +1 -0
- package/dist/src/infra/update-log.js +12 -0
- package/dist/src/infra/update-log.js.map +1 -0
- package/dist/src/infra/update-restart.d.ts +20 -0
- package/dist/src/infra/update-restart.js +165 -0
- package/dist/src/infra/update-restart.js.map +1 -0
- package/dist/src/infra/update-runner.d.ts +89 -1
- package/dist/src/infra/update-runner.js +604 -173
- package/dist/src/infra/update-runner.js.map +1 -1
- package/dist/src/infra/update-startup.d.ts +3 -0
- package/dist/src/infra/update-startup.js +8 -4
- package/dist/src/infra/update-startup.js.map +1 -1
- package/dist/src/providers/index.d.ts +8 -0
- package/dist/src/providers/index.js +51 -12
- package/dist/src/providers/index.js.map +1 -1
- package/dist/src/routing/resolve-route.d.ts +3 -1
- package/dist/src/routing/resolve-route.js.map +1 -1
- package/dist/src/session/store.d.ts +5 -3
- package/dist/src/session/store.js +66 -20
- package/dist/src/session/store.js.map +1 -1
- package/dist/src/share/site-share-config.d.ts +3 -2
- package/dist/src/share/site-share-config.js.map +1 -1
- package/dist/src/utils/logger/stats.d.ts +1 -1
- package/dist/src/workflows/domain/command.d.ts +2 -1
- package/dist/src/workflows/domain/definition-utils.d.ts +14 -0
- package/dist/src/workflows/domain/definition-utils.js +50 -0
- package/dist/src/workflows/domain/definition-utils.js.map +1 -0
- package/dist/src/workflows/domain/event.d.ts +3 -0
- package/dist/src/workflows/domain/index.d.ts +2 -0
- package/dist/src/workflows/domain/index.js +3 -1
- package/dist/src/workflows/domain/run.d.ts +60 -0
- package/dist/src/workflows/domain/run.js.map +1 -1
- package/dist/src/workflows/domain/validation.d.ts +19 -0
- package/dist/src/workflows/domain/validation.js +66 -0
- package/dist/src/workflows/domain/validation.js.map +1 -0
- package/dist/src/workflows/engine/projector.js +17 -0
- package/dist/src/workflows/engine/projector.js.map +1 -1
- package/dist/src/workflows/engine/workflow-engine.d.ts +2 -1
- package/dist/src/workflows/engine/workflow-engine.js +128 -0
- package/dist/src/workflows/engine/workflow-engine.js.map +1 -1
- package/dist/src/workflows/index.d.ts +4 -0
- package/dist/src/workflows/index.js +9 -2
- package/dist/src/workflows/service/run-view-to-snapshot.d.ts +4 -0
- package/dist/src/workflows/service/run-view-to-snapshot.js +63 -0
- package/dist/src/workflows/service/run-view-to-snapshot.js.map +1 -0
- package/dist/src/workflows/service/workflow-run-service.d.ts +37 -0
- package/dist/src/workflows/service/workflow-run-service.js +282 -0
- package/dist/src/workflows/service/workflow-run-service.js.map +1 -0
- package/dist/src/workflows/service/workflow-run-service.types.d.ts +47 -0
- package/dist/src/workflows/service/workflow-run-service.types.js +1 -0
- package/dist/src/workflows/service/workflow-session-bridge.d.ts +29 -0
- package/dist/src/workflows/service/workflow-session-bridge.js +177 -0
- package/dist/src/workflows/service/workflow-session-bridge.js.map +1 -0
- package/dist/src/workflows/service/workflow-session-key.d.ts +3 -0
- package/dist/src/workflows/service/workflow-session-key.js +21 -0
- package/dist/src/workflows/service/workflow-session-key.js.map +1 -0
- package/dist/src/workflows/store/run-store.js +1 -0
- package/dist/src/workflows/store/run-store.js.map +1 -1
- package/package.json +1 -1
- package/dist/gateway/static/root/assets/agents-CRxETUZx.js +0 -222
- package/dist/gateway/static/root/assets/apps-page-wKWf3l57.js +0 -1
- package/dist/gateway/static/root/assets/channels-settings-DDbqVNkx.js +0 -1
- package/dist/gateway/static/root/assets/copy-SxMW6Xpc.js +0 -1
- package/dist/gateway/static/root/assets/cron-api-N9hvuRrn.js +0 -1
- package/dist/gateway/static/root/assets/cron-dreaming-jobs-DueM3rBz.js +0 -2
- package/dist/gateway/static/root/assets/cron-page-tlNGNxhP.js +0 -1
- package/dist/gateway/static/root/assets/dist-CJwfHYvT.js +0 -1
- package/dist/gateway/static/root/assets/index-CqZzHNEg.css +0 -1
- package/dist/gateway/static/root/assets/sessions-page-DKt-Wmib.js +0 -1
- package/dist/gateway/static/root/assets/settings-page-DcJjvvw4.js +0 -3
- package/dist/gateway/static/root/assets/skills-page-DuJ4BTO3.js +0 -2
- package/dist/gateway/static/root/assets/url-D6jvVYIA.js +0 -7
- package/dist/gateway/static/root/assets/voice-api-key-field-CTyHz7L_.js +0 -1
- package/dist/gateway/static/root/assets/workflows-page-GacJ41Fv.js +0 -27
|
@@ -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 } from '../../../utils/logger.js';\n\nconst log = createLogger('Hono: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 for SSE/WebSocket connections where\n * the `Authorization` header cannot be set by `EventSource`. For normal REST\n * requests prefer the `Authorization: Bearer <token>` header.\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\nfunction isQueryTokenAllowedPath(path: string): boolean {\n return QUERY_TOKEN_ALLOWED_PATHS.has(path) || path.startsWith('/api/events');\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) ? extractTokenFromQuery(c.req.url) : 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' },\n 'HTTP auth rejected: no Bearer or ?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' },\n 'HTTP auth rejected: token mismatch',\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 } from '../../../utils/logger.js';\n\nconst log = createLogger('Hono: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. For normal REST requests prefer the `Authorization: Bearer <token>`\n * header.\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 // `<img src>` cannot send Bearer tokens; gateway console loads custom avatars here.\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' },\n 'HTTP auth rejected: no Bearer or ?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' },\n 'HTTP auth rejected: token mismatch',\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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"}
|
|
@@ -45,7 +45,7 @@ const SESSION_TTL_MS = 600 * 1e3;
|
|
|
45
45
|
setInterval(() => {
|
|
46
46
|
const now = Date.now();
|
|
47
47
|
for (const [id, session] of oauthSessions.entries()) if (now > session.expiresAt) {
|
|
48
|
-
|
|
48
|
+
cancelOAuthSession(session, "OAuth flow expired");
|
|
49
49
|
oauthSessions.delete(id);
|
|
50
50
|
log.debug({ sessionId: id }, "Cleaned up expired OAuth session");
|
|
51
51
|
}
|
|
@@ -53,6 +53,14 @@ setInterval(() => {
|
|
|
53
53
|
function generateSessionId() {
|
|
54
54
|
return `oauth_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
|
|
55
55
|
}
|
|
56
|
+
function cancelOAuthSession(session, message = "OAuth flow cancelled") {
|
|
57
|
+
if (session.abortController) session.abortController.abort();
|
|
58
|
+
if (session.manualCodeResolve) session.manualCodeResolve("");
|
|
59
|
+
session.manualCodeResolve = void 0;
|
|
60
|
+
session.manualCodeReject = void 0;
|
|
61
|
+
session.status = "cancelled";
|
|
62
|
+
session.message = message;
|
|
63
|
+
}
|
|
56
64
|
function createOAuthAsyncHandler(service) {
|
|
57
65
|
const oauth = new Hono();
|
|
58
66
|
/**
|
|
@@ -142,14 +150,7 @@ function createOAuthAsyncHandler(service) {
|
|
|
142
150
|
const sessionId = c.req.param("sessionId");
|
|
143
151
|
const session = oauthSessions.get(sessionId);
|
|
144
152
|
if (!session) return c.json({ error: "Session not found" }, 404);
|
|
145
|
-
|
|
146
|
-
if (session.manualCodeReject) {
|
|
147
|
-
session.manualCodeReject(/* @__PURE__ */ new Error("OAuth cancelled by user"));
|
|
148
|
-
session.manualCodeReject = void 0;
|
|
149
|
-
session.manualCodeResolve = void 0;
|
|
150
|
-
}
|
|
151
|
-
session.status = "cancelled";
|
|
152
|
-
session.message = "OAuth flow cancelled";
|
|
153
|
+
cancelOAuthSession(session);
|
|
153
154
|
return c.json({
|
|
154
155
|
ok: true,
|
|
155
156
|
payload: { message: "OAuth flow cancelled" }
|
|
@@ -162,8 +163,7 @@ function createOAuthAsyncHandler(service) {
|
|
|
162
163
|
oauth.delete("/:sessionId", (c) => {
|
|
163
164
|
const sessionId = c.req.param("sessionId");
|
|
164
165
|
if (oauthSessions.has(sessionId)) {
|
|
165
|
-
|
|
166
|
-
if (session.abortController) session.abortController.abort();
|
|
166
|
+
cancelOAuthSession(oauthSessions.get(sessionId));
|
|
167
167
|
oauthSessions.delete(sessionId);
|
|
168
168
|
}
|
|
169
169
|
return c.json({ ok: true });
|
|
@@ -197,6 +197,14 @@ async function runOAuthFlow(session, oauthProvider, _service) {
|
|
|
197
197
|
session.message = "Complete authorization in browser";
|
|
198
198
|
}
|
|
199
199
|
},
|
|
200
|
+
onDeviceCode: (info) => {
|
|
201
|
+
session.status = "waiting_auth";
|
|
202
|
+
session.authUrl = info.verificationUri;
|
|
203
|
+
session.deviceCode = info.userCode;
|
|
204
|
+
session.verificationUri = info.verificationUri;
|
|
205
|
+
session.instructions = `Enter code ${info.userCode}`;
|
|
206
|
+
session.message = `Open ${info.verificationUri} and enter code ${info.userCode}`;
|
|
207
|
+
},
|
|
200
208
|
onPrompt: async (prompt) => {
|
|
201
209
|
session.status = "waiting_code";
|
|
202
210
|
session.deviceCode = prompt.deviceCode;
|
|
@@ -221,13 +229,30 @@ async function runOAuthFlow(session, oauthProvider, _service) {
|
|
|
221
229
|
if (manualCodePromise) return manualCodePromise;
|
|
222
230
|
return "";
|
|
223
231
|
},
|
|
232
|
+
onSelect: async (prompt) => {
|
|
233
|
+
const browserOption = prompt.options.find((option) => option.id === "browser");
|
|
234
|
+
const firstOption = prompt.options[0];
|
|
235
|
+
const selectedOption = browserOption ?? firstOption;
|
|
236
|
+
if (!selectedOption) throw new Error("OAuth login did not provide any selectable auth method");
|
|
237
|
+
log.debug({
|
|
238
|
+
sessionId: session.id,
|
|
239
|
+
provider: session.provider,
|
|
240
|
+
selected: selectedOption.id
|
|
241
|
+
}, "Selected OAuth auth method");
|
|
242
|
+
return selectedOption.id;
|
|
243
|
+
},
|
|
224
244
|
signal: abortController.signal
|
|
225
245
|
};
|
|
226
246
|
try {
|
|
227
247
|
const credentials = await oauthProvider.login(callbacks);
|
|
228
248
|
setOAuthCredentialsToCache(session.provider, credentials);
|
|
229
|
-
|
|
230
|
-
|
|
249
|
+
await new CredentialResolver().saveOAuthToken(session.provider, {
|
|
250
|
+
access: oauthProvider.getApiKey(credentials),
|
|
251
|
+
refresh: credentials.refresh,
|
|
252
|
+
expiresAt: credentials.expires,
|
|
253
|
+
scope: Array.isArray(credentials.scope) ? credentials.scope.filter((value) => typeof value === "string") : void 0,
|
|
254
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
255
|
+
});
|
|
231
256
|
session.status = "completed";
|
|
232
257
|
session.credentials = credentials;
|
|
233
258
|
session.message = "OAuth login successful";
|
|
@@ -236,9 +261,9 @@ async function runOAuthFlow(session, oauthProvider, _service) {
|
|
|
236
261
|
provider: session.provider
|
|
237
262
|
}, "OAuth login completed");
|
|
238
263
|
} catch (err) {
|
|
239
|
-
if (abortController.signal.aborted) {
|
|
264
|
+
if (abortController.signal.aborted || session.status === "cancelled") {
|
|
240
265
|
session.status = "cancelled";
|
|
241
|
-
session.message
|
|
266
|
+
session.message ??= "OAuth flow cancelled by user";
|
|
242
267
|
} else {
|
|
243
268
|
session.status = "failed";
|
|
244
269
|
session.error = formatOAuthAsyncError(err);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth-async.js","names":["googleGeminiCliOAuthProvider","googleAntigravityOAuthProvider"],"sources":["../../../../src/gateway/hono/oauth-async.ts"],"sourcesContent":["/**\n * Async OAuth Handler\n * \n * Provides non-blocking OAuth flow with session-based state management.\n * This allows OAuth flows that require user interaction (browser login) \n * without blocking the HTTP request.\n */\n\nimport { Hono } from 'hono';\nimport type { GatewayService } from '../service.js';\nimport { \n type OAuthProviderInterface, \n type OAuthLoginCallbacks,\n type OAuthCredentials \n} from '../../auth/oauth/types.js';\nimport {\n kimiCodingOAuthProvider,\n minimaxOAuthProvider,\n minimaxCnOAuthProvider,\n anthropicOAuthProvider,\n githubCopilotOAuthProvider,\n googleGeminiCliOAuthProvider,\n googleAntigravityOAuthProvider,\n openaiCodexOAuthProvider,\n} from '../../auth/oauth/index.js';\nimport { createLogger } from '../../utils/logger.js';\nimport { CredentialResolver } from '../../auth/credentials.js';\n\nconst log = createLogger('OAuthAsync');\n\n/** User-facing message when undici/fetch fails (often DNS, firewall, or wrong machine for localhost callback). */\nfunction formatOAuthAsyncError(err: unknown): string {\n\tconst base = err instanceof Error ? err.message : 'OAuth login failed';\n\tconst cause =\n\t\terr instanceof Error && err.cause instanceof Error\n\t\t\t? err.cause.message\n\t\t\t: err instanceof Error && typeof err.cause === 'string'\n\t\t\t\t? err.cause\n\t\t\t\t: '';\n\tconst detail = cause ? ` (${cause})` : '';\n\tif (/^fetch failed$/i.test(base) || base.includes('fetch failed')) {\n\t\treturn (\n\t\t\t`Network request failed${detail}. If the browser opened on another device, the redirect goes to that device's localhost — ` +\n\t\t\t`copy the full URL from the browser address bar after sign-in (starts with http://127.0.0.1 or http://localhost) and paste it below. ` +\n\t\t\t`Otherwise check VPN/proxy/DNS/firewall access to Google OAuth.`\n\t\t);\n\t}\n\treturn base;\n}\n\n// Static OAuth providers map\nconst OAUTH_PROVIDERS: Record<string, OAuthProviderInterface> = {\n 'kimi-coding': kimiCodingOAuthProvider,\n 'minimax': minimaxOAuthProvider,\n 'minimax-cn': minimaxCnOAuthProvider,\n 'anthropic': anthropicOAuthProvider,\n 'github-copilot': githubCopilotOAuthProvider,\n 'google-gemini-cli': googleGeminiCliOAuthProvider,\n 'google-antigravity': googleAntigravityOAuthProvider,\n 'openai-codex': openaiCodexOAuthProvider,\n};\n\n// OAuth session state\ninterface OAuthSession {\n id: string;\n provider: string;\n status: 'pending' | 'waiting_auth' | 'waiting_code' | 'completed' | 'failed' | 'cancelled';\n authUrl?: string;\n instructions?: string;\n deviceCode?: string;\n verificationUri?: string;\n message?: string;\n error?: string;\n credentials?: OAuthCredentials;\n createdAt: number;\n expiresAt: number;\n abortController?: AbortController;\n manualCodeResolve?: (code: string) => void;\n manualCodeReject?: (error: Error) => void;\n}\n\n// In-memory session store (could be moved to Redis for production)\nconst oauthSessions = new Map<string, OAuthSession>();\nconst SESSION_TTL_MS = 10 * 60 * 1000; // 10 minutes\n\n// Clean up expired sessions periodically\nsetInterval(() => {\n const now = Date.now();\n for (const [id, session] of oauthSessions.entries()) {\n if (now > session.expiresAt) {\n if (session.abortController) {\n session.abortController.abort();\n }\n oauthSessions.delete(id);\n log.debug({ sessionId: id }, 'Cleaned up expired OAuth session');\n }\n }\n}, 60 * 1000);\n\nfunction generateSessionId(): string {\n return `oauth_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;\n}\n\nexport function createOAuthAsyncHandler(service: GatewayService) {\n const oauth = new Hono();\n\n /**\n * POST /api/auth/oauth-async/start\n * Start async OAuth flow - returns immediately with session ID\n */\n oauth.post('/start', async (c) => {\n const { provider } = await c.req.json().catch(() => ({}));\n \n if (!provider) {\n return c.json({ error: 'Provider is required' }, 400);\n }\n\n const oauthProvider = OAUTH_PROVIDERS[provider];\n if (!oauthProvider) {\n return c.json({ error: `Unknown OAuth provider: ${provider}` }, 400);\n }\n\n const sessionId = generateSessionId();\n const session: OAuthSession = {\n id: sessionId,\n provider,\n status: 'pending',\n createdAt: Date.now(),\n expiresAt: Date.now() + SESSION_TTL_MS,\n };\n\n oauthSessions.set(sessionId, session);\n\n // Start OAuth flow in background\n runOAuthFlow(session, oauthProvider, service).catch(err => {\n log.error({ sessionId, provider, error: err }, 'Background OAuth flow failed');\n session.status = 'failed';\n session.error = err instanceof Error ? err.message : 'OAuth flow failed';\n });\n\n return c.json({ \n ok: true, \n payload: { \n sessionId,\n provider,\n status: session.status,\n } \n });\n });\n\n /**\n * GET /api/auth/oauth-async/:sessionId/status\n * Check OAuth session status\n */\n oauth.get('/:sessionId/status', (c) => {\n const sessionId = c.req.param('sessionId');\n const session = oauthSessions.get(sessionId);\n\n if (!session) {\n return c.json({ error: 'Session not found' }, 404);\n }\n\n return c.json({ \n ok: true, \n payload: { \n sessionId: session.id,\n provider: session.provider,\n status: session.status,\n authUrl: session.authUrl,\n instructions: session.instructions,\n deviceCode: session.deviceCode,\n verificationUri: session.verificationUri,\n message: session.message,\n error: session.error,\n expiresAt: session.expiresAt,\n } \n });\n });\n\n /**\n * POST /api/auth/oauth-async/:sessionId/code\n * Submit manual authorization code\n */\n oauth.post('/:sessionId/code', async (c) => {\n const sessionId = c.req.param('sessionId');\n const { code } = await c.req.json().catch(() => ({}));\n \n if (!code) {\n return c.json({ error: 'Code is required' }, 400);\n }\n\n const session = oauthSessions.get(sessionId);\n if (!session) {\n return c.json({ error: 'Session not found' }, 404);\n }\n\n if (session.status !== 'waiting_code' || !session.manualCodeResolve) {\n return c.json({ error: 'Session is not waiting for code' }, 400);\n }\n\n // Resolve the manual code promise\n session.manualCodeResolve(code);\n session.manualCodeResolve = undefined;\n session.manualCodeReject = undefined;\n\n return c.json({ \n ok: true, \n payload: { \n message: 'Code submitted, processing...',\n } \n });\n });\n\n /**\n * POST /api/auth/oauth-async/:sessionId/cancel\n * Cancel OAuth flow\n */\n oauth.post('/:sessionId/cancel', async (c) => {\n const sessionId = c.req.param('sessionId');\n const session = oauthSessions.get(sessionId);\n\n if (!session) {\n return c.json({ error: 'Session not found' }, 404);\n }\n\n if (session.abortController) {\n session.abortController.abort();\n }\n\n if (session.manualCodeReject) {\n session.manualCodeReject(new Error('OAuth cancelled by user'));\n session.manualCodeReject = undefined;\n session.manualCodeResolve = undefined;\n }\n\n session.status = 'cancelled';\n session.message = 'OAuth flow cancelled';\n\n return c.json({ \n ok: true, \n payload: { \n message: 'OAuth flow cancelled',\n } \n });\n });\n\n /**\n * DELETE /api/auth/oauth-async/:sessionId\n * Clean up OAuth session\n */\n oauth.delete('/:sessionId', (c) => {\n const sessionId = c.req.param('sessionId');\n \n if (oauthSessions.has(sessionId)) {\n const session = oauthSessions.get(sessionId)!;\n if (session.abortController) {\n session.abortController.abort();\n }\n oauthSessions.delete(sessionId);\n }\n\n return c.json({ ok: true });\n });\n\n return oauth;\n}\n\n/**\n * Run OAuth flow in background\n */\nasync function runOAuthFlow(\n session: OAuthSession,\n oauthProvider: OAuthProviderInterface,\n _service: GatewayService\n): Promise<void> {\n const abortController = new AbortController();\n session.abortController = abortController;\n\n let manualCodePromise: Promise<string> | null = null;\n let manualCodeResolve: ((code: string) => void) | undefined;\n let manualCodeReject: ((error: Error) => void) | undefined;\n\n const callbacks: OAuthLoginCallbacks = {\n onAuth: (auth: { url: string; instructions?: string }) => {\n session.authUrl = auth.url;\n session.instructions = auth.instructions;\n \n if (oauthProvider.usesCallbackServer) {\n // For callback server providers, prepare for manual code input\n session.status = 'waiting_code';\n session.message = 'Complete authorization in browser, or paste the redirect URL below';\n manualCodePromise = new Promise((resolve, reject) => {\n manualCodeResolve = resolve;\n manualCodeReject = reject;\n });\n session.manualCodeResolve = manualCodeResolve;\n session.manualCodeReject = manualCodeReject;\n } else {\n session.status = 'waiting_auth';\n session.message = 'Complete authorization in browser';\n }\n },\n onPrompt: async (prompt: { message: string; deviceCode?: string; verificationUri?: string }) => {\n session.status = 'waiting_code';\n session.deviceCode = prompt.deviceCode;\n session.verificationUri = prompt.verificationUri;\n session.message = prompt.message;\n \n // For device code flow, wait for manual input\n manualCodePromise = new Promise((resolve, reject) => {\n manualCodeResolve = resolve;\n manualCodeReject = reject;\n });\n session.manualCodeResolve = manualCodeResolve;\n session.manualCodeReject = manualCodeReject;\n \n // Return empty for now, will be resolved by manual code submission\n return '';\n },\n onProgress: (message: string) => {\n log.debug({ sessionId: session.id, message }, 'OAuth progress');\n session.message = message;\n },\n onManualCodeInput: async () => {\n // Return the manual code promise for callback server providers\n if (manualCodePromise) {\n return manualCodePromise;\n }\n return '';\n },\n signal: abortController.signal,\n };\n\n try {\n const credentials = await oauthProvider.login(callbacks);\n \n // Save credentials to cache\n setOAuthCredentialsToCache(session.provider, credentials);\n\n // Get API key from OAuth credentials\n const apiKey = oauthProvider.getApiKey(credentials);\n\n // Save API key to credential system\n const resolver = new CredentialResolver();\n await resolver.saveApiKey(session.provider, apiKey, { profileName: 'default' });\n\n session.status = 'completed';\n session.credentials = credentials;\n session.message = 'OAuth login successful';\n \n log.info({ sessionId: session.id, provider: session.provider }, 'OAuth login completed');\n } catch (err) {\n if (abortController.signal.aborted) {\n session.status = 'cancelled';\n session.message = 'OAuth flow cancelled by user';\n } else {\n session.status = 'failed';\n session.error = formatOAuthAsyncError(err);\n log.error({ sessionId: session.id, provider: session.provider, error: err }, 'OAuth login failed');\n }\n } finally {\n session.abortController = undefined;\n session.manualCodeResolve = undefined;\n session.manualCodeReject = undefined;\n }\n}\n\n// Simple in-memory cache for OAuth credentials\nconst oauthCredentialsCache: Map<string, OAuthCredentials> = new Map();\n\nexport function getOAuthCredentialsFromCache(provider: string): OAuthCredentials | undefined {\n return oauthCredentialsCache.get(provider);\n}\n\nexport function setOAuthCredentialsToCache(provider: string, creds: OAuthCredentials): void {\n oauthCredentialsCache.set(provider, creds);\n}\n\nexport function deleteOAuthCredentialsFromCache(provider: string): void {\n oauthCredentialsCache.delete(provider);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"oauth-async.js","names":["googleGeminiCliOAuthProvider","googleAntigravityOAuthProvider"],"sources":["../../../../src/gateway/hono/oauth-async.ts"],"sourcesContent":["/**\n * Async OAuth Handler\n * \n * Provides non-blocking OAuth flow with session-based state management.\n * This allows OAuth flows that require user interaction (browser login) \n * without blocking the HTTP request.\n */\n\nimport { Hono } from 'hono';\nimport type { GatewayService } from '../service.js';\nimport { \n type OAuthProviderInterface, \n type OAuthLoginCallbacks,\n type OAuthCredentials \n} from '../../auth/oauth/types.js';\nimport {\n kimiCodingOAuthProvider,\n minimaxOAuthProvider,\n minimaxCnOAuthProvider,\n anthropicOAuthProvider,\n githubCopilotOAuthProvider,\n googleGeminiCliOAuthProvider,\n googleAntigravityOAuthProvider,\n openaiCodexOAuthProvider,\n} from '../../auth/oauth/index.js';\nimport { createLogger } from '../../utils/logger.js';\nimport { CredentialResolver } from '../../auth/credentials.js';\n\nconst log = createLogger('OAuthAsync');\n\n/** User-facing message when undici/fetch fails (often DNS, firewall, or wrong machine for localhost callback). */\nfunction formatOAuthAsyncError(err: unknown): string {\n\tconst base = err instanceof Error ? err.message : 'OAuth login failed';\n\tconst cause =\n\t\terr instanceof Error && err.cause instanceof Error\n\t\t\t? err.cause.message\n\t\t\t: err instanceof Error && typeof err.cause === 'string'\n\t\t\t\t? err.cause\n\t\t\t\t: '';\n\tconst detail = cause ? ` (${cause})` : '';\n\tif (/^fetch failed$/i.test(base) || base.includes('fetch failed')) {\n\t\treturn (\n\t\t\t`Network request failed${detail}. If the browser opened on another device, the redirect goes to that device's localhost — ` +\n\t\t\t`copy the full URL from the browser address bar after sign-in (starts with http://127.0.0.1 or http://localhost) and paste it below. ` +\n\t\t\t`Otherwise check VPN/proxy/DNS/firewall access to Google OAuth.`\n\t\t);\n\t}\n\treturn base;\n}\n\n// Static OAuth providers map\nconst OAUTH_PROVIDERS: Record<string, OAuthProviderInterface> = {\n 'kimi-coding': kimiCodingOAuthProvider,\n 'minimax': minimaxOAuthProvider,\n 'minimax-cn': minimaxCnOAuthProvider,\n 'anthropic': anthropicOAuthProvider,\n 'github-copilot': githubCopilotOAuthProvider,\n 'google-gemini-cli': googleGeminiCliOAuthProvider,\n 'google-antigravity': googleAntigravityOAuthProvider,\n 'openai-codex': openaiCodexOAuthProvider,\n};\n\n// OAuth session state\ninterface OAuthSession {\n id: string;\n provider: string;\n status: 'pending' | 'waiting_auth' | 'waiting_code' | 'completed' | 'failed' | 'cancelled';\n authUrl?: string;\n instructions?: string;\n deviceCode?: string;\n verificationUri?: string;\n message?: string;\n error?: string;\n credentials?: OAuthCredentials;\n createdAt: number;\n expiresAt: number;\n abortController?: AbortController;\n manualCodeResolve?: (code: string) => void;\n manualCodeReject?: (error: Error) => void;\n}\n\n// In-memory session store (could be moved to Redis for production)\nconst oauthSessions = new Map<string, OAuthSession>();\nconst SESSION_TTL_MS = 10 * 60 * 1000; // 10 minutes\n\n// Clean up expired sessions periodically\nsetInterval(() => {\n const now = Date.now();\n for (const [id, session] of oauthSessions.entries()) {\n if (now > session.expiresAt) {\n cancelOAuthSession(session, 'OAuth flow expired');\n oauthSessions.delete(id);\n log.debug({ sessionId: id }, 'Cleaned up expired OAuth session');\n }\n }\n}, 60 * 1000);\n\nfunction generateSessionId(): string {\n return `oauth_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;\n}\n\nfunction cancelOAuthSession(session: OAuthSession, message = 'OAuth flow cancelled'): void {\n if (session.abortController) {\n session.abortController.abort();\n }\n\n if (session.manualCodeResolve) {\n session.manualCodeResolve('');\n }\n\n session.manualCodeResolve = undefined;\n session.manualCodeReject = undefined;\n session.status = 'cancelled';\n session.message = message;\n}\n\nexport function createOAuthAsyncHandler(service: GatewayService) {\n const oauth = new Hono();\n\n /**\n * POST /api/auth/oauth-async/start\n * Start async OAuth flow - returns immediately with session ID\n */\n oauth.post('/start', async (c) => {\n const { provider } = await c.req.json().catch(() => ({}));\n \n if (!provider) {\n return c.json({ error: 'Provider is required' }, 400);\n }\n\n const oauthProvider = OAUTH_PROVIDERS[provider];\n if (!oauthProvider) {\n return c.json({ error: `Unknown OAuth provider: ${provider}` }, 400);\n }\n\n const sessionId = generateSessionId();\n const session: OAuthSession = {\n id: sessionId,\n provider,\n status: 'pending',\n createdAt: Date.now(),\n expiresAt: Date.now() + SESSION_TTL_MS,\n };\n\n oauthSessions.set(sessionId, session);\n\n // Start OAuth flow in background\n runOAuthFlow(session, oauthProvider, service).catch(err => {\n log.error({ sessionId, provider, error: err }, 'Background OAuth flow failed');\n session.status = 'failed';\n session.error = err instanceof Error ? err.message : 'OAuth flow failed';\n });\n\n return c.json({ \n ok: true, \n payload: { \n sessionId,\n provider,\n status: session.status,\n } \n });\n });\n\n /**\n * GET /api/auth/oauth-async/:sessionId/status\n * Check OAuth session status\n */\n oauth.get('/:sessionId/status', (c) => {\n const sessionId = c.req.param('sessionId');\n const session = oauthSessions.get(sessionId);\n\n if (!session) {\n return c.json({ error: 'Session not found' }, 404);\n }\n\n return c.json({ \n ok: true, \n payload: { \n sessionId: session.id,\n provider: session.provider,\n status: session.status,\n authUrl: session.authUrl,\n instructions: session.instructions,\n deviceCode: session.deviceCode,\n verificationUri: session.verificationUri,\n message: session.message,\n error: session.error,\n expiresAt: session.expiresAt,\n } \n });\n });\n\n /**\n * POST /api/auth/oauth-async/:sessionId/code\n * Submit manual authorization code\n */\n oauth.post('/:sessionId/code', async (c) => {\n const sessionId = c.req.param('sessionId');\n const { code } = await c.req.json().catch(() => ({}));\n \n if (!code) {\n return c.json({ error: 'Code is required' }, 400);\n }\n\n const session = oauthSessions.get(sessionId);\n if (!session) {\n return c.json({ error: 'Session not found' }, 404);\n }\n\n if (session.status !== 'waiting_code' || !session.manualCodeResolve) {\n return c.json({ error: 'Session is not waiting for code' }, 400);\n }\n\n // Resolve the manual code promise\n session.manualCodeResolve(code);\n session.manualCodeResolve = undefined;\n session.manualCodeReject = undefined;\n\n return c.json({ \n ok: true, \n payload: { \n message: 'Code submitted, processing...',\n } \n });\n });\n\n /**\n * POST /api/auth/oauth-async/:sessionId/cancel\n * Cancel OAuth flow\n */\n oauth.post('/:sessionId/cancel', async (c) => {\n const sessionId = c.req.param('sessionId');\n const session = oauthSessions.get(sessionId);\n\n if (!session) {\n return c.json({ error: 'Session not found' }, 404);\n }\n\n cancelOAuthSession(session);\n\n return c.json({ \n ok: true, \n payload: { \n message: 'OAuth flow cancelled',\n } \n });\n });\n\n /**\n * DELETE /api/auth/oauth-async/:sessionId\n * Clean up OAuth session\n */\n oauth.delete('/:sessionId', (c) => {\n const sessionId = c.req.param('sessionId');\n \n if (oauthSessions.has(sessionId)) {\n const session = oauthSessions.get(sessionId)!;\n cancelOAuthSession(session);\n oauthSessions.delete(sessionId);\n }\n\n return c.json({ ok: true });\n });\n\n return oauth;\n}\n\n/**\n * Run OAuth flow in background\n */\nasync function runOAuthFlow(\n session: OAuthSession,\n oauthProvider: OAuthProviderInterface,\n _service: GatewayService\n): Promise<void> {\n const abortController = new AbortController();\n session.abortController = abortController;\n\n let manualCodePromise: Promise<string> | null = null;\n let manualCodeResolve: ((code: string) => void) | undefined;\n let manualCodeReject: ((error: Error) => void) | undefined;\n\n const callbacks: OAuthLoginCallbacks = {\n onAuth: (auth: { url: string; instructions?: string }) => {\n session.authUrl = auth.url;\n session.instructions = auth.instructions;\n \n if (oauthProvider.usesCallbackServer) {\n // For callback server providers, prepare for manual code input\n session.status = 'waiting_code';\n session.message = 'Complete authorization in browser, or paste the redirect URL below';\n manualCodePromise = new Promise((resolve, reject) => {\n manualCodeResolve = resolve;\n manualCodeReject = reject;\n });\n session.manualCodeResolve = manualCodeResolve;\n session.manualCodeReject = manualCodeReject;\n } else {\n session.status = 'waiting_auth';\n session.message = 'Complete authorization in browser';\n }\n },\n onDeviceCode: (info) => {\n session.status = 'waiting_auth';\n session.authUrl = info.verificationUri;\n session.deviceCode = info.userCode;\n session.verificationUri = info.verificationUri;\n session.instructions = `Enter code ${info.userCode}`;\n session.message = `Open ${info.verificationUri} and enter code ${info.userCode}`;\n },\n onPrompt: async (prompt: { message: string; deviceCode?: string; verificationUri?: string }) => {\n session.status = 'waiting_code';\n session.deviceCode = prompt.deviceCode;\n session.verificationUri = prompt.verificationUri;\n session.message = prompt.message;\n \n // For device code flow, wait for manual input\n manualCodePromise = new Promise((resolve, reject) => {\n manualCodeResolve = resolve;\n manualCodeReject = reject;\n });\n session.manualCodeResolve = manualCodeResolve;\n session.manualCodeReject = manualCodeReject;\n \n // Return empty for now, will be resolved by manual code submission\n return '';\n },\n onProgress: (message: string) => {\n log.debug({ sessionId: session.id, message }, 'OAuth progress');\n session.message = message;\n },\n onManualCodeInput: async () => {\n // Return the manual code promise for callback server providers\n if (manualCodePromise) {\n return manualCodePromise;\n }\n return '';\n },\n onSelect: async (prompt) => {\n const browserOption = prompt.options.find((option) => option.id === 'browser');\n const firstOption = prompt.options[0];\n const selectedOption = browserOption ?? firstOption;\n if (!selectedOption) {\n throw new Error('OAuth login did not provide any selectable auth method');\n }\n log.debug(\n { sessionId: session.id, provider: session.provider, selected: selectedOption.id },\n 'Selected OAuth auth method',\n );\n return selectedOption.id;\n },\n signal: abortController.signal,\n };\n\n try {\n const credentials = await oauthProvider.login(callbacks);\n \n // Save credentials to cache and persist them as first-class OAuth credentials.\n setOAuthCredentialsToCache(session.provider, credentials);\n\n const resolver = new CredentialResolver();\n await resolver.saveOAuthToken(session.provider, {\n access: oauthProvider.getApiKey(credentials),\n refresh: credentials.refresh,\n expiresAt: credentials.expires,\n scope: Array.isArray(credentials.scope) ? credentials.scope.filter((value): value is string => typeof value === 'string') : undefined,\n createdAt: new Date().toISOString(),\n });\n\n session.status = 'completed';\n session.credentials = credentials;\n session.message = 'OAuth login successful';\n \n log.info({ sessionId: session.id, provider: session.provider }, 'OAuth login completed');\n } catch (err) {\n if (abortController.signal.aborted || session.status === 'cancelled') {\n session.status = 'cancelled';\n session.message ??= 'OAuth flow cancelled by user';\n } else {\n session.status = 'failed';\n session.error = formatOAuthAsyncError(err);\n log.error({ sessionId: session.id, provider: session.provider, error: err }, 'OAuth login failed');\n }\n } finally {\n session.abortController = undefined;\n session.manualCodeResolve = undefined;\n session.manualCodeReject = undefined;\n }\n}\n\n// Simple in-memory cache for OAuth credentials\nconst oauthCredentialsCache: Map<string, OAuthCredentials> = new Map();\n\nexport function getOAuthCredentialsFromCache(provider: string): OAuthCredentials | undefined {\n return oauthCredentialsCache.get(provider);\n}\n\nexport function setOAuthCredentialsToCache(provider: string, creds: OAuthCredentials): void {\n oauthCredentialsCache.set(provider, creds);\n}\n\nexport function deleteOAuthCredentialsFromCache(provider: string): void {\n oauthCredentialsCache.delete(provider);\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CredentialResolver, init_credentials } from "../../auth/credentials.js";
|
|
2
2
|
import { anthropicOAuthProvider } from "../../auth/oauth/anthropic.js";
|
|
3
|
-
import { init_providers, isProviderConfigured } from "../../providers/index.js";
|
|
3
|
+
import { getProviderAuthState, init_providers, isProviderConfigured } from "../../providers/index.js";
|
|
4
4
|
import { minimaxOAuthProvider } from "../../auth/oauth/minimax.js";
|
|
5
5
|
import { minimaxCnOAuthProvider } from "../../auth/oauth/minimax-cn.js";
|
|
6
6
|
import { kimiCodingOAuthProvider } from "../../auth/oauth/kimi-coding.js";
|
|
@@ -61,6 +61,15 @@ function createOAuthHandler(_service) {
|
|
|
61
61
|
instructions: auth.instructions
|
|
62
62
|
};
|
|
63
63
|
},
|
|
64
|
+
onDeviceCode: (info) => {
|
|
65
|
+
authResult = {
|
|
66
|
+
url: info.verificationUri,
|
|
67
|
+
instructions: `Enter code ${info.userCode}`,
|
|
68
|
+
message: `Open ${info.verificationUri} and enter code ${info.userCode}`,
|
|
69
|
+
deviceCode: info.userCode,
|
|
70
|
+
verificationUri: info.verificationUri
|
|
71
|
+
};
|
|
72
|
+
},
|
|
64
73
|
onPrompt: async (prompt) => {
|
|
65
74
|
authResult = {
|
|
66
75
|
message: prompt.message,
|
|
@@ -75,11 +84,19 @@ function createOAuthHandler(_service) {
|
|
|
75
84
|
onManualCodeInput: async () => {
|
|
76
85
|
manualCodeRequested = true;
|
|
77
86
|
return "";
|
|
87
|
+
},
|
|
88
|
+
onSelect: async (prompt) => {
|
|
89
|
+
return prompt.options.find((option) => option.id === "browser")?.id ?? prompt.options[0]?.id;
|
|
78
90
|
}
|
|
79
91
|
});
|
|
80
92
|
setOAuthCredentialsToCache(provider, credentials);
|
|
81
|
-
|
|
82
|
-
|
|
93
|
+
await new CredentialResolver().saveOAuthToken(provider, {
|
|
94
|
+
access: oauthProvider.getApiKey(credentials),
|
|
95
|
+
refresh: credentials.refresh,
|
|
96
|
+
expiresAt: credentials.expires,
|
|
97
|
+
scope: Array.isArray(credentials.scope) ? credentials.scope.filter((value) => typeof value === "string") : void 0,
|
|
98
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
99
|
+
});
|
|
83
100
|
return c.json({
|
|
84
101
|
ok: true,
|
|
85
102
|
payload: {
|
|
@@ -107,12 +124,15 @@ function createOAuthHandler(_service) {
|
|
|
107
124
|
oauth.get("/:provider", async (c) => {
|
|
108
125
|
const provider = c.req.param("provider");
|
|
109
126
|
const credentials = getOAuthCredentialsFromCache(provider);
|
|
127
|
+
const authState = await getProviderAuthState(provider);
|
|
110
128
|
const configured = await isProviderConfigured(provider);
|
|
111
129
|
return c.json({
|
|
112
130
|
ok: true,
|
|
113
131
|
payload: {
|
|
114
132
|
configured: configured || !!credentials,
|
|
115
|
-
|
|
133
|
+
authMode: authState.authMode,
|
|
134
|
+
authStatus: authState.authStatus,
|
|
135
|
+
expiresAt: authState.expiresAt ?? credentials?.expires
|
|
116
136
|
}
|
|
117
137
|
});
|
|
118
138
|
});
|
|
@@ -121,8 +141,13 @@ function createOAuthHandler(_service) {
|
|
|
121
141
|
* Revoke OAuth credentials
|
|
122
142
|
*/
|
|
123
143
|
oauth.delete("/:provider", async (c) => {
|
|
124
|
-
|
|
125
|
-
|
|
144
|
+
const provider = c.req.param("provider");
|
|
145
|
+
deleteOAuthCredentialsFromCache(provider);
|
|
146
|
+
await new CredentialResolver().deleteProviderCredential(provider);
|
|
147
|
+
return c.json({
|
|
148
|
+
ok: true,
|
|
149
|
+
payload: { disconnected: provider }
|
|
150
|
+
});
|
|
126
151
|
});
|
|
127
152
|
/**
|
|
128
153
|
* GET /api/auth/oauth
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth.js","names":["googleGeminiCliOAuthProvider","googleAntigravityOAuthProvider"],"sources":["../../../../src/gateway/hono/oauth.ts"],"sourcesContent":["/**\n * OAuth HTTP Handler\n * \n * Provides HTTP endpoints for OAuth login flow.\n */\n\nimport { Hono } from 'hono';\nimport type { GatewayService } from '../service.js';\nimport { \n type OAuthProviderInterface, \n type OAuthLoginCallbacks,\n type OAuthCredentials \n} from '../../auth/oauth/types.js';\nimport {\n kimiCodingOAuthProvider,\n minimaxOAuthProvider,\n minimaxCnOAuthProvider,\n anthropicOAuthProvider,\n githubCopilotOAuthProvider,\n googleGeminiCliOAuthProvider,\n googleAntigravityOAuthProvider,\n openaiCodexOAuthProvider,\n} from '../../auth/oauth/index.js';\nimport { CredentialResolver } from '../../auth/credentials.js';\nimport { isProviderConfigured } from '../../providers/index.js';\n\n// Static OAuth providers map\nconst OAUTH_PROVIDERS: Record<string, OAuthProviderInterface> = {\n 'kimi-coding': kimiCodingOAuthProvider,\n 'minimax': minimaxOAuthProvider,\n 'minimax-cn': minimaxCnOAuthProvider,\n 'anthropic': anthropicOAuthProvider,\n 'github-copilot': githubCopilotOAuthProvider,\n 'google-gemini-cli': googleGeminiCliOAuthProvider,\n 'google-antigravity': googleAntigravityOAuthProvider,\n 'openai-codex': openaiCodexOAuthProvider,\n};\n\n// Simple in-memory cache for OAuth credentials\nconst oauthCredentialsCache: Map<string, OAuthCredentials> = new Map();\n\nfunction getOAuthCredentialsFromCache(provider: string): OAuthCredentials | undefined {\n return oauthCredentialsCache.get(provider);\n}\n\nfunction setOAuthCredentialsToCache(provider: string, creds: OAuthCredentials): void {\n oauthCredentialsCache.set(provider, creds);\n}\n\nfunction deleteOAuthCredentialsFromCache(provider: string): void {\n oauthCredentialsCache.delete(provider);\n}\n\n/** No-op: OAuth tokens live on disk under auth paths; cache is populated during login. */\nexport function loadOAuthCredentialsToCache(_service: GatewayService): void {}\n\nexport function createOAuthHandler(_service: GatewayService) {\n const oauth = new Hono();\n\n /**\n * POST /api/auth/oauth/start\n * Start OAuth flow for a provider\n */\n oauth.post('/start', async (c) => {\n const { provider } = await c.req.json().catch(() => ({}));\n \n if (!provider) {\n return c.json({ error: 'Provider is required' }, 400);\n }\n\n const oauthProvider = OAUTH_PROVIDERS[provider];\n if (!oauthProvider) {\n return c.json({ error: `Unknown OAuth provider: ${provider}` }, 400);\n }\n\n try {\n let authResult: any = null;\n let manualCodeRequested = false;\n \n const callbacks: OAuthLoginCallbacks = {\n onAuth: (auth: { url: string; instructions?: string }) => {\n authResult = { url: auth.url, instructions: auth.instructions };\n },\n onPrompt: async (prompt: { message: string; deviceCode?: string; verificationUri?: string }) => {\n authResult = { \n message: prompt.message, \n deviceCode: prompt.deviceCode,\n verificationUri: prompt.verificationUri,\n };\n return prompt.deviceCode || '';\n },\n onProgress: (message: string) => {\n console.log('OAuth progress:', message);\n },\n onManualCodeInput: async () => {\n // For callback server providers, signal that manual code input is needed\n manualCodeRequested = true;\n // Return empty - frontend will handle manual input\n return '';\n },\n };\n\n const credentials = await oauthProvider.login(callbacks);\n setOAuthCredentialsToCache(provider, credentials);\n\n
|
|
1
|
+
{"version":3,"file":"oauth.js","names":["googleGeminiCliOAuthProvider","googleAntigravityOAuthProvider"],"sources":["../../../../src/gateway/hono/oauth.ts"],"sourcesContent":["/**\n * OAuth HTTP Handler\n * \n * Provides HTTP endpoints for OAuth login flow.\n */\n\nimport { Hono } from 'hono';\nimport type { GatewayService } from '../service.js';\nimport { \n type OAuthProviderInterface, \n type OAuthLoginCallbacks,\n type OAuthCredentials \n} from '../../auth/oauth/types.js';\nimport {\n kimiCodingOAuthProvider,\n minimaxOAuthProvider,\n minimaxCnOAuthProvider,\n anthropicOAuthProvider,\n githubCopilotOAuthProvider,\n googleGeminiCliOAuthProvider,\n googleAntigravityOAuthProvider,\n openaiCodexOAuthProvider,\n} from '../../auth/oauth/index.js';\nimport { CredentialResolver } from '../../auth/credentials.js';\nimport { getProviderAuthState, isProviderConfigured } from '../../providers/index.js';\n\n// Static OAuth providers map\nconst OAUTH_PROVIDERS: Record<string, OAuthProviderInterface> = {\n 'kimi-coding': kimiCodingOAuthProvider,\n 'minimax': minimaxOAuthProvider,\n 'minimax-cn': minimaxCnOAuthProvider,\n 'anthropic': anthropicOAuthProvider,\n 'github-copilot': githubCopilotOAuthProvider,\n 'google-gemini-cli': googleGeminiCliOAuthProvider,\n 'google-antigravity': googleAntigravityOAuthProvider,\n 'openai-codex': openaiCodexOAuthProvider,\n};\n\n// Simple in-memory cache for OAuth credentials\nconst oauthCredentialsCache: Map<string, OAuthCredentials> = new Map();\n\nfunction getOAuthCredentialsFromCache(provider: string): OAuthCredentials | undefined {\n return oauthCredentialsCache.get(provider);\n}\n\nfunction setOAuthCredentialsToCache(provider: string, creds: OAuthCredentials): void {\n oauthCredentialsCache.set(provider, creds);\n}\n\nfunction deleteOAuthCredentialsFromCache(provider: string): void {\n oauthCredentialsCache.delete(provider);\n}\n\n/** No-op: OAuth tokens live on disk under auth paths; cache is populated during login. */\nexport function loadOAuthCredentialsToCache(_service: GatewayService): void {}\n\nexport function createOAuthHandler(_service: GatewayService) {\n const oauth = new Hono();\n\n /**\n * POST /api/auth/oauth/start\n * Start OAuth flow for a provider\n */\n oauth.post('/start', async (c) => {\n const { provider } = await c.req.json().catch(() => ({}));\n \n if (!provider) {\n return c.json({ error: 'Provider is required' }, 400);\n }\n\n const oauthProvider = OAUTH_PROVIDERS[provider];\n if (!oauthProvider) {\n return c.json({ error: `Unknown OAuth provider: ${provider}` }, 400);\n }\n\n try {\n let authResult: any = null;\n let manualCodeRequested = false;\n \n const callbacks: OAuthLoginCallbacks = {\n onAuth: (auth: { url: string; instructions?: string }) => {\n authResult = { url: auth.url, instructions: auth.instructions };\n },\n onDeviceCode: (info) => {\n authResult = {\n url: info.verificationUri,\n instructions: `Enter code ${info.userCode}`,\n message: `Open ${info.verificationUri} and enter code ${info.userCode}`,\n deviceCode: info.userCode,\n verificationUri: info.verificationUri,\n };\n },\n onPrompt: async (prompt: { message: string; deviceCode?: string; verificationUri?: string }) => {\n authResult = { \n message: prompt.message, \n deviceCode: prompt.deviceCode,\n verificationUri: prompt.verificationUri,\n };\n return prompt.deviceCode || '';\n },\n onProgress: (message: string) => {\n console.log('OAuth progress:', message);\n },\n onManualCodeInput: async () => {\n // For callback server providers, signal that manual code input is needed\n manualCodeRequested = true;\n // Return empty - frontend will handle manual input\n return '';\n },\n onSelect: async (prompt) => {\n const browserOption = prompt.options.find((option) => option.id === 'browser');\n return browserOption?.id ?? prompt.options[0]?.id;\n },\n };\n\n const credentials = await oauthProvider.login(callbacks);\n setOAuthCredentialsToCache(provider, credentials);\n\n const resolver = new CredentialResolver();\n await resolver.saveOAuthToken(provider, {\n access: oauthProvider.getApiKey(credentials),\n refresh: credentials.refresh,\n expiresAt: credentials.expires,\n scope: Array.isArray(credentials.scope) ? credentials.scope.filter((value): value is string => typeof value === 'string') : undefined,\n createdAt: new Date().toISOString(),\n });\n\n return c.json({ \n ok: true, \n payload: { \n success: true,\n provider,\n message: 'OAuth login successful',\n expires: credentials.expires,\n authUrl: authResult?.url,\n deviceCode: authResult?.deviceCode,\n verificationUri: authResult?.verificationUri,\n instructions: authResult?.instructions,\n usesCallbackServer: oauthProvider.usesCallbackServer ?? false,\n manualCodeRequested,\n } \n });\n } catch (err) {\n console.error('OAuth login error:', err);\n return c.json({ \n error: err instanceof Error ? err.message : 'OAuth login failed' \n }, 500);\n }\n });\n\n /**\n * GET /api/auth/oauth/:provider\n * Check OAuth status for a provider\n */\n oauth.get('/:provider', async (c) => {\n const provider = c.req.param('provider');\n const credentials = getOAuthCredentialsFromCache(provider);\n const authState = await getProviderAuthState(provider);\n const configured = await isProviderConfigured(provider);\n\n return c.json({ \n ok: true, \n payload: { \n configured: configured || !!credentials,\n authMode: authState.authMode,\n authStatus: authState.authStatus,\n expiresAt: authState.expiresAt ?? credentials?.expires,\n } \n });\n });\n\n /**\n * DELETE /api/auth/oauth/:provider\n * Revoke OAuth credentials\n */\n oauth.delete('/:provider', async (c) => {\n const provider = c.req.param('provider');\n \n deleteOAuthCredentialsFromCache(provider);\n const resolver = new CredentialResolver();\n await resolver.deleteProviderCredential(provider);\n\n return c.json({ ok: true, payload: { disconnected: provider } });\n });\n\n /**\n * GET /api/auth/oauth\n * List available OAuth providers\n */\n oauth.get('/', (c) => {\n const result = Object.entries(OAUTH_PROVIDERS).map(([id, p]) => ({\n id,\n name: p.name,\n }));\n\n return c.json({ ok: true, payload: { providers: result } });\n });\n\n return oauth;\n}\n"],"mappings":"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"}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { init_agent_scope, normalizeAgentId } from "../../../agent/agent-scope.js";
|
|
2
|
+
import { init_localized_text, normalizeLocalizedText } from "../../../config/localized-text.js";
|
|
2
3
|
import { init_schema, parseModelRef } from "../../../config/schema.js";
|
|
3
4
|
import { init_providers, isProviderConfigured, resolveModel } from "../../../providers/index.js";
|
|
4
5
|
import { getVoiceModelsConfig } from "../../../config/voice.js";
|
|
5
|
-
import { agentModelFallbacksToArray, agentModelRefToString } from "../lib/agent-model.js";
|
|
6
6
|
import { deleteAgentAvatarFile, finalizeCreateAgentDirs, listAgentProfileFiles, listGatewayAgents, prepareCreateAgent, prepareCreateAgentsBatch, prepareDeleteAgent, prepareUpdateAgent, readAgentAvatarFile, readAgentProfileFile, runAfterDeletePurge, writeAgentAvatarFromBase64, writeAgentProfileFile } from "../../agents-admin.js";
|
|
7
7
|
import { resolveImageGenerationCapabilities, resolveImageUnderstandingCapabilities } from "../../image-capabilities.js";
|
|
8
|
+
import { agentModelFallbacksToArray, agentModelRefToString } from "../lib/agent-model.js";
|
|
8
9
|
//#region src/gateway/hono/routes/agents.ts
|
|
9
10
|
init_schema();
|
|
11
|
+
init_localized_text();
|
|
10
12
|
init_providers();
|
|
11
13
|
init_agent_scope();
|
|
12
14
|
function parseProfileFiles(raw) {
|
|
@@ -22,14 +24,29 @@ function parseProfileFiles(raw) {
|
|
|
22
24
|
function isParseError(value) {
|
|
23
25
|
return typeof value === "object" && value !== null && "error" in value && typeof value.error === "string";
|
|
24
26
|
}
|
|
27
|
+
function parseLocalizedText(raw, fieldName) {
|
|
28
|
+
if (raw === void 0) return;
|
|
29
|
+
if (typeof raw === "string") return raw;
|
|
30
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return { error: `${fieldName} must be a string or locale map` };
|
|
31
|
+
const localized = {};
|
|
32
|
+
for (const [locale, text] of Object.entries(raw)) {
|
|
33
|
+
if (typeof text !== "string") return { error: `${fieldName}.${locale} must be a string` };
|
|
34
|
+
localized[locale] = text;
|
|
35
|
+
}
|
|
36
|
+
return normalizeLocalizedText(localized);
|
|
37
|
+
}
|
|
25
38
|
function parseCreateAgentBody(raw) {
|
|
26
39
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return { error: "each agent must be an object" };
|
|
27
40
|
const body = raw;
|
|
28
|
-
const
|
|
41
|
+
const parsedName = parseLocalizedText(body.name, "name");
|
|
42
|
+
if (isParseError(parsedName)) return parsedName;
|
|
43
|
+
const parsedDescription = parseLocalizedText(body.description, "description");
|
|
44
|
+
if (isParseError(parsedDescription)) return parsedDescription;
|
|
45
|
+
const name = parsedName ?? "";
|
|
29
46
|
const workspace = typeof body.workspace === "string" ? body.workspace : "";
|
|
30
47
|
const model = typeof body.model === "string" ? body.model : void 0;
|
|
31
48
|
const agentDir = typeof body.agentDir === "string" ? body.agentDir : void 0;
|
|
32
|
-
const description =
|
|
49
|
+
const description = parsedDescription;
|
|
33
50
|
const id = typeof body.id === "string" ? body.id : void 0;
|
|
34
51
|
const toolsDisable = Array.isArray(body.toolsDisable) ? body.toolsDisable.map((x) => String(x).trim()).filter(Boolean) : void 0;
|
|
35
52
|
let profileFiles;
|
|
@@ -38,6 +55,7 @@ function parseCreateAgentBody(raw) {
|
|
|
38
55
|
if (isParseError(parsed)) return parsed;
|
|
39
56
|
profileFiles = parsed;
|
|
40
57
|
}
|
|
58
|
+
const cloneFrom = typeof body.cloneFrom === "string" ? body.cloneFrom : void 0;
|
|
41
59
|
return {
|
|
42
60
|
name,
|
|
43
61
|
workspace,
|
|
@@ -46,14 +64,15 @@ function parseCreateAgentBody(raw) {
|
|
|
46
64
|
...id !== void 0 ? { id } : {},
|
|
47
65
|
...description !== void 0 ? { description } : {},
|
|
48
66
|
...toolsDisable !== void 0 ? { toolsDisable } : {},
|
|
49
|
-
...profileFiles !== void 0 ? { profileFiles } : {}
|
|
67
|
+
...profileFiles !== void 0 ? { profileFiles } : {},
|
|
68
|
+
...cloneFrom !== void 0 ? { cloneFrom } : {}
|
|
50
69
|
};
|
|
51
70
|
}
|
|
52
71
|
function registerAgentsRoutes(authenticated, deps) {
|
|
53
72
|
const { service, strictRateLimitMiddleware } = deps;
|
|
54
73
|
authenticated.get("/api/agents", async (c) => {
|
|
55
74
|
const cfg = service.currentConfig;
|
|
56
|
-
const payload = await listGatewayAgents(cfg);
|
|
75
|
+
const payload = await listGatewayAgents(cfg, { locale: c.req.query("locale") || c.req.header("Accept-Language")?.split(",")[0]?.trim() });
|
|
57
76
|
return c.json({
|
|
58
77
|
ok: true,
|
|
59
78
|
payload
|
|
@@ -104,7 +123,7 @@ function registerAgentsRoutes(authenticated, deps) {
|
|
|
104
123
|
}, finalized.status ?? 400);
|
|
105
124
|
agentIds.push(item.agentId);
|
|
106
125
|
}
|
|
107
|
-
const agentsPayload = await listGatewayAgents(cfg);
|
|
126
|
+
const agentsPayload = await listGatewayAgents(cfg, { locale: c.req.query("locale") || c.req.header("Accept-Language")?.split(",")[0]?.trim() });
|
|
108
127
|
return c.json({
|
|
109
128
|
ok: true,
|
|
110
129
|
payload: {
|
|
@@ -139,12 +158,16 @@ function registerAgentsRoutes(authenticated, deps) {
|
|
|
139
158
|
ok: false,
|
|
140
159
|
error: { message: save.error ?? "save failed" }
|
|
141
160
|
}, 500);
|
|
142
|
-
const finalized = await finalizeCreateAgentDirs(service.currentConfig, agentId, {
|
|
161
|
+
const finalized = await finalizeCreateAgentDirs(service.currentConfig, agentId, {
|
|
162
|
+
...parsed.profileFiles !== void 0 ? { profileFiles: parsed.profileFiles } : {},
|
|
163
|
+
...parsed.cloneFrom ? { cloneFrom: parsed.cloneFrom } : {}
|
|
164
|
+
});
|
|
143
165
|
if (finalized.ok === false) return c.json({
|
|
144
166
|
ok: false,
|
|
145
167
|
error: { message: finalized.error }
|
|
146
168
|
}, finalized.status ?? 400);
|
|
147
|
-
const
|
|
169
|
+
const locale = c.req.query("locale") || c.req.header("Accept-Language")?.split(",")[0]?.trim();
|
|
170
|
+
const agentsPayload = await listGatewayAgents(service.currentConfig, { locale });
|
|
148
171
|
return c.json({
|
|
149
172
|
ok: true,
|
|
150
173
|
payload: {
|
|
@@ -166,9 +189,27 @@ function registerAgentsRoutes(authenticated, deps) {
|
|
|
166
189
|
}
|
|
167
190
|
const skillsPatch = body.skills === null ? null : Array.isArray(body.skills) ? body.skills.map((x) => String(x).trim()).filter(Boolean) : void 0;
|
|
168
191
|
const toolsDisablePatch = body.toolsDisable === null ? null : Array.isArray(body.toolsDisable) ? body.toolsDisable.map((x) => String(x).trim()).filter(Boolean) : void 0;
|
|
169
|
-
|
|
192
|
+
let namePatch;
|
|
193
|
+
if (Object.hasOwn(body, "name")) {
|
|
194
|
+
const parsedName = parseLocalizedText(body.name, "name");
|
|
195
|
+
if (isParseError(parsedName)) return c.json({
|
|
196
|
+
ok: false,
|
|
197
|
+
error: { message: parsedName.error }
|
|
198
|
+
}, 400);
|
|
199
|
+
namePatch = parsedName;
|
|
200
|
+
}
|
|
201
|
+
let descriptionPatch;
|
|
202
|
+
if (Object.hasOwn(body, "description")) if (body.description === null) descriptionPatch = null;
|
|
203
|
+
else {
|
|
204
|
+
const parsedDescription = parseLocalizedText(body.description, "description");
|
|
205
|
+
if (isParseError(parsedDescription)) return c.json({
|
|
206
|
+
ok: false,
|
|
207
|
+
error: { message: parsedDescription.error }
|
|
208
|
+
}, 400);
|
|
209
|
+
descriptionPatch = parsedDescription;
|
|
210
|
+
}
|
|
170
211
|
const prep = prepareUpdateAgent(service.currentConfig, id, {
|
|
171
|
-
name:
|
|
212
|
+
name: namePatch,
|
|
172
213
|
...descriptionPatch !== void 0 ? { description: descriptionPatch } : {},
|
|
173
214
|
workspace: typeof body.workspace === "string" ? body.workspace : void 0,
|
|
174
215
|
model: body.model === null ? null : typeof body.model === "string" ? body.model : void 0,
|
|
@@ -186,7 +227,8 @@ function registerAgentsRoutes(authenticated, deps) {
|
|
|
186
227
|
ok: false,
|
|
187
228
|
error: { message: save.error ?? "save failed" }
|
|
188
229
|
}, 500);
|
|
189
|
-
const
|
|
230
|
+
const locale = c.req.query("locale") || c.req.header("Accept-Language")?.split(",")[0]?.trim();
|
|
231
|
+
const agentsPayload = await listGatewayAgents(service.currentConfig, { locale });
|
|
190
232
|
return c.json({
|
|
191
233
|
ok: true,
|
|
192
234
|
payload: agentsPayload
|
|
@@ -207,7 +249,8 @@ function registerAgentsRoutes(authenticated, deps) {
|
|
|
207
249
|
error: { message: save.error ?? "save failed" }
|
|
208
250
|
}, 500);
|
|
209
251
|
if (purge) await runAfterDeletePurge(service.currentConfig, agentId);
|
|
210
|
-
const
|
|
252
|
+
const locale = c.req.query("locale") || c.req.header("Accept-Language")?.split(",")[0]?.trim();
|
|
253
|
+
const agentsPayload = await listGatewayAgents(service.currentConfig, { locale });
|
|
211
254
|
return c.json({
|
|
212
255
|
ok: true,
|
|
213
256
|
payload: {
|