@rudderhq/server 0.5.2-canary.2 → 0.6.0
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/bootstrap/register-api-routes.d.ts.map +1 -1
- package/dist/bootstrap/register-api-routes.js +15 -0
- package/dist/bootstrap/register-api-routes.js.map +1 -1
- package/dist/bootstrap/types.d.ts +11 -0
- package/dist/bootstrap/types.d.ts.map +1 -1
- package/dist/bundled-plugins/plugin-linear/dist/worker.js +741 -7
- package/dist/bundled-plugins/plugin-linear/dist/worker.js.map +3 -3
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -1
- package/dist/index.js.map +1 -1
- package/dist/local-runtime.d.ts +7 -0
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +15 -1
- package/dist/local-runtime.js.map +1 -1
- package/dist/middleware/auth.js +2 -2
- package/dist/middleware/auth.js.map +1 -1
- package/dist/middleware/error-handler.d.ts.map +1 -1
- package/dist/middleware/error-handler.js +3 -2
- package/dist/middleware/error-handler.js.map +1 -1
- package/dist/middleware/logger.d.ts +4 -0
- package/dist/middleware/logger.d.ts.map +1 -1
- package/dist/middleware/logger.js +82 -7
- package/dist/middleware/logger.js.map +1 -1
- package/dist/middleware/logger.test.js +94 -1
- package/dist/middleware/logger.test.js.map +1 -1
- package/dist/routes/chats.annotation-routes.d.ts +47 -0
- package/dist/routes/chats.annotation-routes.d.ts.map +1 -0
- package/dist/routes/chats.annotation-routes.js +164 -0
- package/dist/routes/chats.annotation-routes.js.map +1 -0
- package/dist/routes/chats.d.ts.map +1 -1
- package/dist/routes/chats.js +467 -165
- package/dist/routes/chats.js.map +1 -1
- package/dist/routes/chats.stream-routes.d.ts +1 -10
- package/dist/routes/chats.stream-routes.d.ts.map +1 -1
- package/dist/routes/chats.stream-routes.js +168 -110
- package/dist/routes/chats.stream-routes.js.map +1 -1
- package/dist/routes/chats.stream-support.d.ts +79 -0
- package/dist/routes/chats.stream-support.d.ts.map +1 -0
- package/dist/routes/chats.stream-support.js +121 -0
- package/dist/routes/chats.stream-support.js.map +1 -0
- package/dist/routes/issues.comments-attachments.d.ts.map +1 -1
- package/dist/routes/issues.comments-attachments.js +16 -2
- package/dist/routes/issues.comments-attachments.js.map +1 -1
- package/dist/routes/issues.d.ts.map +1 -1
- package/dist/routes/issues.js +21 -4
- package/dist/routes/issues.js.map +1 -1
- package/dist/routes/issues.mutations.d.ts.map +1 -1
- package/dist/routes/issues.mutations.js +20 -33
- package/dist/routes/issues.mutations.js.map +1 -1
- package/dist/routes/managed-mcp-agent-bindings.d.ts +15 -0
- package/dist/routes/managed-mcp-agent-bindings.d.ts.map +1 -0
- package/dist/routes/managed-mcp-agent-bindings.js +66 -0
- package/dist/routes/managed-mcp-agent-bindings.js.map +1 -0
- package/dist/routes/managed-mcp-connections.d.ts +8 -0
- package/dist/routes/managed-mcp-connections.d.ts.map +1 -0
- package/dist/routes/managed-mcp-connections.js +189 -0
- package/dist/routes/managed-mcp-connections.js.map +1 -0
- package/dist/routes/managed-mcp-runtime.d.ts +5 -0
- package/dist/routes/managed-mcp-runtime.d.ts.map +1 -0
- package/dist/routes/managed-mcp-runtime.js +148 -0
- package/dist/routes/managed-mcp-runtime.js.map +1 -0
- package/dist/routes/messenger.d.ts.map +1 -1
- package/dist/routes/messenger.js +1 -1
- package/dist/routes/messenger.js.map +1 -1
- package/dist/services/access.d.ts.map +1 -1
- package/dist/services/access.js +91 -12
- package/dist/services/access.js.map +1 -1
- package/dist/services/agent-run-context.d.ts +1 -0
- package/dist/services/agent-run-context.d.ts.map +1 -1
- package/dist/services/agent-run-context.js +5 -1
- package/dist/services/agent-run-context.js.map +1 -1
- package/dist/services/board-auth.d.ts +2 -2
- package/dist/services/chat-annotation-copy-lineage.d.ts +32 -0
- package/dist/services/chat-annotation-copy-lineage.d.ts.map +1 -0
- package/dist/services/chat-annotation-copy-lineage.js +110 -0
- package/dist/services/chat-annotation-copy-lineage.js.map +1 -0
- package/dist/services/chat-assistant.annotations.d.ts +16 -0
- package/dist/services/chat-assistant.annotations.d.ts.map +1 -0
- package/dist/services/chat-assistant.annotations.js +108 -0
- package/dist/services/chat-assistant.annotations.js.map +1 -0
- package/dist/services/chat-assistant.annotations.test.d.ts +2 -0
- package/dist/services/chat-assistant.annotations.test.d.ts.map +1 -0
- package/dist/services/chat-assistant.annotations.test.js +257 -0
- package/dist/services/chat-assistant.annotations.test.js.map +1 -0
- package/dist/services/chat-assistant.d.ts.map +1 -1
- package/dist/services/chat-assistant.helpers.d.ts +2 -3
- package/dist/services/chat-assistant.helpers.d.ts.map +1 -1
- package/dist/services/chat-assistant.helpers.js +37 -53
- package/dist/services/chat-assistant.helpers.js.map +1 -1
- package/dist/services/chat-assistant.js +61 -5
- package/dist/services/chat-assistant.js.map +1 -1
- package/dist/services/chat-generation-protocol.d.ts +8 -15
- package/dist/services/chat-generation-protocol.d.ts.map +1 -1
- package/dist/services/chat-generation-protocol.js +19 -59
- package/dist/services/chat-generation-protocol.js.map +1 -1
- package/dist/services/chat-generation-protocol.test.js +168 -6
- package/dist/services/chat-generation-protocol.test.js.map +1 -1
- package/dist/services/chat-generation-provenance.d.ts +14 -0
- package/dist/services/chat-generation-provenance.d.ts.map +1 -0
- package/dist/services/chat-generation-provenance.js +71 -0
- package/dist/services/chat-generation-provenance.js.map +1 -0
- package/dist/services/chat-inline-annotation-rendering.d.ts +2 -0
- package/dist/services/chat-inline-annotation-rendering.d.ts.map +1 -0
- package/dist/services/chat-inline-annotation-rendering.js +265 -0
- package/dist/services/chat-inline-annotation-rendering.js.map +1 -0
- package/dist/services/chat-inline-annotation-resolved-labels.d.ts +11 -0
- package/dist/services/chat-inline-annotation-resolved-labels.d.ts.map +1 -0
- package/dist/services/chat-inline-annotation-resolved-labels.js +490 -0
- package/dist/services/chat-inline-annotation-resolved-labels.js.map +1 -0
- package/dist/services/chat-inline-annotation-validation.d.ts +15 -0
- package/dist/services/chat-inline-annotation-validation.d.ts.map +1 -0
- package/dist/services/chat-inline-annotation-validation.js +430 -0
- package/dist/services/chat-inline-annotation-validation.js.map +1 -0
- package/dist/services/chat-inline-annotations.d.ts +18 -0
- package/dist/services/chat-inline-annotations.d.ts.map +1 -0
- package/dist/services/chat-inline-annotations.js +52 -0
- package/dist/services/chat-inline-annotations.js.map +1 -0
- package/dist/services/chat-inline-annotations.test.d.ts +2 -0
- package/dist/services/chat-inline-annotations.test.d.ts.map +1 -0
- package/dist/services/chat-inline-annotations.test.js +1306 -0
- package/dist/services/chat-inline-annotations.test.js.map +1 -0
- package/dist/services/chat-queued-message-materialization.d.ts +126 -0
- package/dist/services/chat-queued-message-materialization.d.ts.map +1 -0
- package/dist/services/chat-queued-message-materialization.js +327 -0
- package/dist/services/chat-queued-message-materialization.js.map +1 -0
- package/dist/services/chat-steer-messages.d.ts +3 -3
- package/dist/services/chat-steer-messages.d.ts.map +1 -1
- package/dist/services/chat-steer-messages.js +50 -80
- package/dist/services/chat-steer-messages.js.map +1 -1
- package/dist/services/chat-work-manifest.d.ts.map +1 -1
- package/dist/services/chat-work-manifest.js +5 -1
- package/dist/services/chat-work-manifest.js.map +1 -1
- package/dist/services/chats.annotation-persistence.d.ts +20 -0
- package/dist/services/chats.annotation-persistence.d.ts.map +1 -0
- package/dist/services/chats.annotation-persistence.js +206 -0
- package/dist/services/chats.annotation-persistence.js.map +1 -0
- package/dist/services/chats.d.ts +129 -96
- package/dist/services/chats.d.ts.map +1 -1
- package/dist/services/chats.helpers.d.ts +2 -2
- package/dist/services/chats.inline-visual-persistence.d.ts +3 -3
- package/dist/services/chats.inline-visual-persistence.d.ts.map +1 -1
- package/dist/services/chats.inline-visual-persistence.js +114 -54
- package/dist/services/chats.inline-visual-persistence.js.map +1 -1
- package/dist/services/chats.js +743 -252
- package/dist/services/chats.js.map +1 -1
- package/dist/services/finance.d.ts +1 -1
- package/dist/services/index.d.ts +8 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +8 -0
- package/dist/services/index.js.map +1 -1
- package/dist/services/integrations/agent-integrations.d.ts +2 -2
- package/dist/services/integrations/custom-integrations.d.ts +3 -1
- package/dist/services/integrations/custom-integrations.d.ts.map +1 -1
- package/dist/services/integrations/custom-integrations.js +13 -2
- package/dist/services/integrations/custom-integrations.js.map +1 -1
- package/dist/services/issues.comments-attachments.d.ts +7 -1
- package/dist/services/issues.comments-attachments.d.ts.map +1 -1
- package/dist/services/issues.comments-attachments.js +13 -1
- package/dist/services/issues.comments-attachments.js.map +1 -1
- package/dist/services/issues.d.ts +6 -0
- package/dist/services/issues.d.ts.map +1 -1
- package/dist/services/issues.js +22 -3
- package/dist/services/issues.js.map +1 -1
- package/dist/services/knowledge-portability/organization-portability.core.d.ts +4 -1
- package/dist/services/knowledge-portability/organization-portability.core.d.ts.map +1 -1
- package/dist/services/knowledge-portability/organization-portability.core.js +17 -3
- package/dist/services/knowledge-portability/organization-portability.core.js.map +1 -1
- package/dist/services/knowledge-portability/organization-portability.export.d.ts.map +1 -1
- package/dist/services/knowledge-portability/organization-portability.export.js +2 -2
- package/dist/services/knowledge-portability/organization-portability.export.js.map +1 -1
- package/dist/services/knowledge-portability/organization-portability.import.d.ts.map +1 -1
- package/dist/services/knowledge-portability/organization-portability.import.js +13 -3
- package/dist/services/knowledge-portability/organization-portability.import.js.map +1 -1
- package/dist/services/knowledge-portability/organization-skills.catalog.d.ts +3 -0
- package/dist/services/knowledge-portability/organization-skills.catalog.d.ts.map +1 -1
- package/dist/services/knowledge-portability/organization-skills.catalog.js +54 -23
- package/dist/services/knowledge-portability/organization-skills.catalog.js.map +1 -1
- package/dist/services/knowledge-portability/organization-skills.d.ts.map +1 -1
- package/dist/services/knowledge-portability/organization-skills.js +627 -124
- package/dist/services/knowledge-portability/organization-skills.js.map +1 -1
- package/dist/services/knowledge-portability/organization-skills.sources.d.ts +1 -0
- package/dist/services/knowledge-portability/organization-skills.sources.d.ts.map +1 -1
- package/dist/services/knowledge-portability/organization-skills.sources.js +53 -10
- package/dist/services/knowledge-portability/organization-skills.sources.js.map +1 -1
- package/dist/services/mcp/authorizer-lock.d.ts +9 -0
- package/dist/services/mcp/authorizer-lock.d.ts.map +1 -0
- package/dist/services/mcp/authorizer-lock.js +15 -0
- package/dist/services/mcp/authorizer-lock.js.map +1 -0
- package/dist/services/mcp/managed-bindings.d.ts +17 -0
- package/dist/services/mcp/managed-bindings.d.ts.map +1 -0
- package/dist/services/mcp/managed-bindings.js +332 -0
- package/dist/services/mcp/managed-bindings.js.map +1 -0
- package/dist/services/mcp/managed-client.d.ts +64 -0
- package/dist/services/mcp/managed-client.d.ts.map +1 -0
- package/dist/services/mcp/managed-client.js +238 -0
- package/dist/services/mcp/managed-client.js.map +1 -0
- package/dist/services/mcp/managed-client.test.d.ts +2 -0
- package/dist/services/mcp/managed-client.test.d.ts.map +1 -0
- package/dist/services/mcp/managed-client.test.js +398 -0
- package/dist/services/mcp/managed-client.test.js.map +1 -0
- package/dist/services/mcp/managed-connections.d.ts +81 -0
- package/dist/services/mcp/managed-connections.d.ts.map +1 -0
- package/dist/services/mcp/managed-connections.js +920 -0
- package/dist/services/mcp/managed-connections.js.map +1 -0
- package/dist/services/mcp/managed-runtime.d.ts +134 -0
- package/dist/services/mcp/managed-runtime.d.ts.map +1 -0
- package/dist/services/mcp/managed-runtime.js +282 -0
- package/dist/services/mcp/managed-runtime.js.map +1 -0
- package/dist/services/mcp/oauth-provider.d.ts +48 -0
- package/dist/services/mcp/oauth-provider.d.ts.map +1 -0
- package/dist/services/mcp/oauth-provider.js +120 -0
- package/dist/services/mcp/oauth-provider.js.map +1 -0
- package/dist/services/mcp/oauth-scope.test.d.ts +2 -0
- package/dist/services/mcp/oauth-scope.test.d.ts.map +1 -0
- package/dist/services/mcp/oauth-scope.test.js +69 -0
- package/dist/services/mcp/oauth-scope.test.js.map +1 -0
- package/dist/services/mcp/oauth-session-gc.d.ts +14 -0
- package/dist/services/mcp/oauth-session-gc.d.ts.map +1 -0
- package/dist/services/mcp/oauth-session-gc.js +34 -0
- package/dist/services/mcp/oauth-session-gc.js.map +1 -0
- package/dist/services/mcp/oauth-session-gc.test.d.ts +2 -0
- package/dist/services/mcp/oauth-session-gc.test.d.ts.map +1 -0
- package/dist/services/mcp/oauth-session-gc.test.js +66 -0
- package/dist/services/mcp/oauth-session-gc.test.js.map +1 -0
- package/dist/services/mcp/oauth.d.ts +61 -0
- package/dist/services/mcp/oauth.d.ts.map +1 -0
- package/dist/services/mcp/oauth.js +1690 -0
- package/dist/services/mcp/oauth.js.map +1 -0
- package/dist/services/mcp/pinned-fetch.d.ts +15 -0
- package/dist/services/mcp/pinned-fetch.d.ts.map +1 -0
- package/dist/services/mcp/pinned-fetch.js +400 -0
- package/dist/services/mcp/pinned-fetch.js.map +1 -0
- package/dist/services/mcp/pinned-fetch.test.d.ts +2 -0
- package/dist/services/mcp/pinned-fetch.test.d.ts.map +1 -0
- package/dist/services/mcp/pinned-fetch.test.js +190 -0
- package/dist/services/mcp/pinned-fetch.test.js.map +1 -0
- package/dist/services/mcp/provider-registry.d.ts +77 -0
- package/dist/services/mcp/provider-registry.d.ts.map +1 -0
- package/dist/services/mcp/provider-registry.js +80 -0
- package/dist/services/mcp/provider-registry.js.map +1 -0
- package/dist/services/mcp/provider-registry.test.d.ts +2 -0
- package/dist/services/mcp/provider-registry.test.d.ts.map +1 -0
- package/dist/services/mcp/provider-registry.test.js +80 -0
- package/dist/services/mcp/provider-registry.test.js.map +1 -0
- package/dist/services/mcp/security-policy.d.ts +48 -0
- package/dist/services/mcp/security-policy.d.ts.map +1 -0
- package/dist/services/mcp/security-policy.js +344 -0
- package/dist/services/mcp/security-policy.js.map +1 -0
- package/dist/services/mcp/security-policy.test.d.ts +2 -0
- package/dist/services/mcp/security-policy.test.d.ts.map +1 -0
- package/dist/services/mcp/security-policy.test.js +226 -0
- package/dist/services/mcp/security-policy.test.js.map +1 -0
- package/dist/services/mcp/tool-discovery.d.ts +49 -0
- package/dist/services/mcp/tool-discovery.d.ts.map +1 -0
- package/dist/services/mcp/tool-discovery.js +198 -0
- package/dist/services/mcp/tool-discovery.js.map +1 -0
- package/dist/services/mcp/tool-discovery.test.d.ts +2 -0
- package/dist/services/mcp/tool-discovery.test.d.ts.map +1 -0
- package/dist/services/mcp/tool-discovery.test.js +168 -0
- package/dist/services/mcp/tool-discovery.test.js.map +1 -0
- package/dist/services/messenger.d.ts +1 -1
- package/dist/services/messenger.d.ts.map +1 -1
- package/dist/services/messenger.js +21 -3
- package/dist/services/messenger.js.map +1 -1
- package/dist/services/plugin-registry.d.ts +3 -3
- package/dist/services/runtime-kernel/heartbeat.core.d.ts +1 -1
- package/dist/services/runtime-kernel/heartbeat.core.d.ts.map +1 -1
- package/dist/services/runtime-kernel/heartbeat.core.js +30 -4
- package/dist/services/runtime-kernel/heartbeat.core.js.map +1 -1
- package/dist/services/runtime-kernel/heartbeat.wakeup.d.ts.map +1 -1
- package/dist/services/runtime-kernel/heartbeat.wakeup.js +38 -23
- package/dist/services/runtime-kernel/heartbeat.wakeup.js.map +1 -1
- package/dist/services/secrets.d.ts +64 -6
- package/dist/services/secrets.d.ts.map +1 -1
- package/dist/services/secrets.js +38 -15
- package/dist/services/secrets.js.map +1 -1
- package/dist/services/side-chats.d.ts +1 -1
- package/dist/services/side-chats.d.ts.map +1 -1
- package/dist/services/side-chats.js +56 -14
- package/dist/services/side-chats.js.map +1 -1
- package/dist/services/workspace-runtime.services.d.ts +2 -2
- package/dist/services/workspace-web-preview.js +1 -1
- package/package.json +16 -13
- package/resources/bundled-skills/rudder-docs/references/operating-practices.md +12 -5
- package/skills/rudder-docs/references/operating-practices.md +12 -5
- package/ui-dist/assets/LibraryLiveSurface-CKOthw54.js +1 -0
- package/ui-dist/assets/{abnfDiagram-VRR7QNED-KGtP7zoC.js → abnfDiagram-VRR7QNED-C6a_wMho.js} +1 -1
- package/ui-dist/assets/{arc-JjnKyiOb.js → arc-CP0En6KA.js} +1 -1
- package/ui-dist/assets/architectureDiagram-ZJ3FMSHR-AHfOdcaZ.js +36 -0
- package/ui-dist/assets/{blockDiagram-677ZJIJ3-D7YzMYrz.js → blockDiagram-677ZJIJ3-Bkl9lp63.js} +1 -1
- package/ui-dist/assets/{c4Diagram-LMCZKHZV-Dpf7UQxl.js → c4Diagram-LMCZKHZV-BpZareVt.js} +1 -1
- package/ui-dist/assets/channel-DqxL_Zcy.js +1 -0
- package/ui-dist/assets/{chunk-2Q5K7J3B-UJ3gsGlQ.js → chunk-2Q5K7J3B-BQAwVuXc.js} +1 -1
- package/ui-dist/assets/{chunk-32BRIVSS-BeHl2j2l.js → chunk-32BRIVSS-YkmfA08F.js} +1 -1
- package/ui-dist/assets/{chunk-5VM5RSS4-STE9fJd0.js → chunk-5VM5RSS4-DvvjwTnG.js} +1 -1
- package/ui-dist/assets/{chunk-EX3LRPZG-Bj8g8j4B.js → chunk-EX3LRPZG-TXD_Yhg5.js} +1 -1
- package/ui-dist/assets/{chunk-JWPE2WC7-0V4Pwta-.js → chunk-JWPE2WC7-CgMlj6XI.js} +1 -1
- package/ui-dist/assets/{chunk-MOJQB5TN-DNPn50gm.js → chunk-MOJQB5TN-BnJjU8Rc.js} +1 -1
- package/ui-dist/assets/{chunk-RYQCIY6F-Befmnv8r.js → chunk-RYQCIY6F-D9B64JKM.js} +1 -1
- package/ui-dist/assets/{chunk-V7JOEXUC-Boh8TjHj.js → chunk-V7JOEXUC-PS09QVpl.js} +1 -1
- package/ui-dist/assets/{chunk-VR4S4FIN-B_yEQSo3.js → chunk-VR4S4FIN-DQXb7pRS.js} +1 -1
- package/ui-dist/assets/{chunk-XXDRQBXY-Cf1GnUFx.js → chunk-XXDRQBXY-B1UdLiF2.js} +1 -1
- package/ui-dist/assets/classDiagram-OUVF2IWQ-DMwjzuJE.js +1 -0
- package/ui-dist/assets/classDiagram-v2-EOCWNBFH-DMwjzuJE.js +1 -0
- package/ui-dist/assets/cose-bilkent-JH36ORCC-lqqmMB3m.js +1 -0
- package/ui-dist/assets/{cynefin-VYW2F7L2-Dv1yELYC.js → cynefin-VYW2F7L2-C9hu0XqZ.js} +1 -1
- package/ui-dist/assets/{cynefinDiagram-TSTJHNR4-CeFwjFoZ.js → cynefinDiagram-TSTJHNR4-Uuhz4c-1.js} +1 -1
- package/ui-dist/assets/{dagre-VKFMJZFB-8idd_4sX.js → dagre-VKFMJZFB-ChDrQYse.js} +1 -1
- package/ui-dist/assets/{diagram-FQU43EPY-BBVua6hM.js → diagram-FQU43EPY-DLtjkVSN.js} +1 -1
- package/ui-dist/assets/{diagram-G47NLZAW-D1zLQZlp.js → diagram-G47NLZAW-DCvEm4D0.js} +1 -1
- package/ui-dist/assets/{diagram-NH7WQ7WH-DE_FTSNv.js → diagram-NH7WQ7WH-Dz5jlsPG.js} +1 -1
- package/ui-dist/assets/{diagram-OA4YK3LP-61VSKBEn.js → diagram-OA4YK3LP-BXmQLO-Q.js} +1 -1
- package/ui-dist/assets/{diagram-WEI45ONY-C-tED6BJ.js → diagram-WEI45ONY-JnMU3xrx.js} +1 -1
- package/ui-dist/assets/{ebnfDiagram-CCIWWBDH-BEB8WGt0.js → ebnfDiagram-CCIWWBDH-Kryuqj4r.js} +1 -1
- package/ui-dist/assets/{erDiagram-Q63AITRT-AN_jm1TZ.js → erDiagram-Q63AITRT-Bn7g2mSW.js} +1 -1
- package/ui-dist/assets/{flowDiagram-23GEKE2U-BwLC6tI7.js → flowDiagram-23GEKE2U-DcjqTK91.js} +4 -4
- package/ui-dist/assets/ganttDiagram-NO4QXBWP-BRkw9-GE.js +292 -0
- package/ui-dist/assets/{gitGraphDiagram-IHSO6WYX-D9HCL9rY.js → gitGraphDiagram-IHSO6WYX-Cexcp6kR.js} +1 -1
- package/ui-dist/assets/graph-Cu7OV4Oi.js +1 -0
- package/ui-dist/assets/{index-BQXnpwke.js → index-ASZMPBYm.js} +5 -5
- package/ui-dist/assets/{index-5OM32_rf.js → index-B17BqCTj.js} +1 -1
- package/ui-dist/assets/{index-Cf0gLy6a.js → index-B9MccU1g.js} +1 -1
- package/ui-dist/assets/{index-BoSJ4Rx1.js → index-BPqbtW_X.js} +1 -1
- package/ui-dist/assets/{index-CpIdZ4-7.js → index-C2HjDa98.js} +1 -1
- package/ui-dist/assets/index-CMPtpj0A.js +1 -0
- package/ui-dist/assets/{index-p0ZGU1_7.js → index-CZXgOYtk.js} +1 -1
- package/ui-dist/assets/{index-74cx3ESJ.js → index-CknAauOP.js} +1 -1
- package/ui-dist/assets/index-Cu1GuH0a.js +1 -0
- package/ui-dist/assets/index-D8PdEImm.css +1 -0
- package/ui-dist/assets/{index-DrVDiJhe.js → index-DB1YX9-p.js} +1 -1
- package/ui-dist/assets/{index-vHMnAQSb.js → index-DBxLZXK7.js} +624 -610
- package/ui-dist/assets/{index-CpEfk5zb.js → index-DOCVOD97.js} +1 -1
- package/ui-dist/assets/{index-DMuWG0YN.js → index-DUbakRAC.js} +1 -1
- package/ui-dist/assets/{index-AT8juaIi.js → index-Dbg1ZwO2.js} +1 -1
- package/ui-dist/assets/index-DwbLsoLt.js +1 -0
- package/ui-dist/assets/index-hCLSotmS.js +1 -0
- package/ui-dist/assets/index-jrhEFc-j.js +1 -0
- package/ui-dist/assets/index-kz8ueXgo.js +1 -0
- package/ui-dist/assets/{infoDiagram-FWYZ7A6U-CyO3CAA1.js → infoDiagram-FWYZ7A6U-DugVGI3K.js} +1 -1
- package/ui-dist/assets/{ishikawaDiagram-FXEZZL3T-BLLZuF1_.js → ishikawaDiagram-FXEZZL3T-CHy06Oev.js} +1 -1
- package/ui-dist/assets/{journeyDiagram-5HDEW3XC-CWzQocvE.js → journeyDiagram-5HDEW3XC-BI0w53rb.js} +1 -1
- package/ui-dist/assets/{kanban-definition-HUTT4EX6-Dz4SFU-I.js → kanban-definition-HUTT4EX6-v_bLwx7B.js} +1 -1
- package/ui-dist/assets/layout-Dwp1s_bo.js +1 -0
- package/ui-dist/assets/{linear-BlzDlAaL.js → linear-DAV4STLZ.js} +1 -1
- package/ui-dist/assets/map-Bs6QpaaQ.js +1 -0
- package/ui-dist/assets/{mermaid.core-BtCydd-w.js → mermaid.core-fVmaL47V.js} +4 -4
- package/ui-dist/assets/{mindmap-definition-LN4V7U3C-BeIkKRBx.js → mindmap-definition-LN4V7U3C-5dWD4dxm.js} +1 -1
- package/ui-dist/assets/{pegDiagram-2B236MQR-BZgpT4TN.js → pegDiagram-2B236MQR-DB2XsDPN.js} +1 -1
- package/ui-dist/assets/{pieDiagram-ENE6RG2P-BRFggcg2.js → pieDiagram-ENE6RG2P-Bc5teLCS.js} +1 -1
- package/ui-dist/assets/{quadrantDiagram-ABIIQ3AL-CibUo3gT.js → quadrantDiagram-ABIIQ3AL-BlfNdETt.js} +1 -1
- package/ui-dist/assets/{railroadDiagram-RFXS5EU6-DINJqhgn.js → railroadDiagram-RFXS5EU6-BbXywkq3.js} +1 -1
- package/ui-dist/assets/{requirementDiagram-TGXJPOKE-NegluQIe.js → requirementDiagram-TGXJPOKE-DU-CapdF.js} +1 -1
- package/ui-dist/assets/{sankeyDiagram-HTMAVEWB-Kq41yHAd.js → sankeyDiagram-HTMAVEWB-Ftvh3MqA.js} +1 -1
- package/ui-dist/assets/{sequenceDiagram-DBY2YBRQ-CngfEI4f.js → sequenceDiagram-DBY2YBRQ-CCtHt9V4.js} +1 -1
- package/ui-dist/assets/{sizeCapture-X5ZJPWSS-DUWRNlme.js → sizeCapture-X5ZJPWSS-x0jnIKHx.js} +1 -1
- package/ui-dist/assets/{stateDiagram-2N3HPSRC-BwHC_Fz4.js → stateDiagram-2N3HPSRC-2qCrydE7.js} +1 -1
- package/ui-dist/assets/stateDiagram-v2-6OUMAXLB-CvGRUgRf.js +1 -0
- package/ui-dist/assets/{swimlanes-5IMT3BWC-Cqp2rT1K.js → swimlanes-5IMT3BWC-6kqJq5Xf.js} +2 -2
- package/ui-dist/assets/swimlanesDiagram-G3AALYLV-EBa7-qSc.js +8 -0
- package/ui-dist/assets/{timeline-definition-FHXFAJF6-BG3GYPCu.js → timeline-definition-FHXFAJF6-BuRz8KHZ.js} +1 -1
- package/ui-dist/assets/{vennDiagram-L72KCM5P-D1IFU27N.js → vennDiagram-L72KCM5P-BKzMZ_Rv.js} +1 -1
- package/ui-dist/assets/{wardleyDiagram-EHGQE667-DboO2kYu.js → wardleyDiagram-EHGQE667-8uXlFtBV.js} +1 -1
- package/ui-dist/assets/{xychartDiagram-FW5EYKEG-vI2BGrXP.js → xychartDiagram-FW5EYKEG-CWKGlpF9.js} +1 -1
- package/ui-dist/index.html +2 -2
- package/ui-dist/assets/architectureDiagram-ZJ3FMSHR-DHyfFgK0.js +0 -36
- package/ui-dist/assets/channel-TMGI9Qt9.js +0 -1
- package/ui-dist/assets/classDiagram-OUVF2IWQ-r5zIpY6G.js +0 -1
- package/ui-dist/assets/classDiagram-v2-EOCWNBFH-r5zIpY6G.js +0 -1
- package/ui-dist/assets/cose-bilkent-JH36ORCC-fSNs9447.js +0 -1
- package/ui-dist/assets/ganttDiagram-NO4QXBWP-CJYhBF1C.js +0 -292
- package/ui-dist/assets/graph-DdBT9W3J.js +0 -1
- package/ui-dist/assets/index-6UIO-gg7.js +0 -1
- package/ui-dist/assets/index-BVkQVMFn.js +0 -1
- package/ui-dist/assets/index-BaAPxkqh.css +0 -1
- package/ui-dist/assets/index-CmCERTmS.js +0 -1
- package/ui-dist/assets/index-Cr4lUIaJ.js +0 -1
- package/ui-dist/assets/index-D0fAqlES.js +0 -1
- package/ui-dist/assets/index-DfNcAyZq.js +0 -1
- package/ui-dist/assets/layout-D3tY9q4Q.js +0 -1
- package/ui-dist/assets/map-Ctg1-a9N.js +0 -1
- package/ui-dist/assets/stateDiagram-v2-6OUMAXLB-C6P3NY96.js +0 -1
- package/ui-dist/assets/swimlanesDiagram-G3AALYLV-BNdDt62J.js +0 -8
|
@@ -0,0 +1,1690 @@
|
|
|
1
|
+
import { auth, OAuthError, OAuthErrorCode, } from "@modelcontextprotocol/client";
|
|
2
|
+
import { activityLog, instanceUserRoles, mcpConnections, mcpOAuthGrants, mcpOAuthSessions, organizationMemberships, organizationSecrets, organizationSecretVersions, } from "@rudderhq/db";
|
|
3
|
+
import { MCP_OAUTH_SESSION_TTL_MS, mcpOAuthCallbackSchema, mcpScopeSelectionSchema, } from "@rudderhq/shared";
|
|
4
|
+
import { and, asc, eq, gt, inArray, isNull, lte, sql, } from "drizzle-orm";
|
|
5
|
+
import { createHash, randomBytes, randomUUID, } from "node:crypto";
|
|
6
|
+
import { HttpError, notFound, unprocessable } from "../../errors.js";
|
|
7
|
+
import { getSecretProvider } from "../../secrets/provider-registry.js";
|
|
8
|
+
import { lockManagedMcpOAuthAuthorizer } from "./authorizer-lock.js";
|
|
9
|
+
import { createManagedMcpClient, resolveMcpHttpCredentials, } from "./managed-client.js";
|
|
10
|
+
import { PersistentMcpOAuthClientProvider, resolveMcpOAuthRedirectUri, } from "./oauth-provider.js";
|
|
11
|
+
import { createSecureMcpFetch } from "./pinned-fetch.js";
|
|
12
|
+
import { MCP_PROVIDER_REGISTRY, resolveCuratedMcpEndpoint, } from "./provider-registry.js";
|
|
13
|
+
import { resolveMcpHttpTarget } from "./security-policy.js";
|
|
14
|
+
function createOAuthAuthorizationSnapshot(connection) {
|
|
15
|
+
if (connection.provider === "custom") {
|
|
16
|
+
throw unprocessable("Managed MCP OAuth only supports curated providers");
|
|
17
|
+
}
|
|
18
|
+
const serverUrl = connection.provider === "supabase"
|
|
19
|
+
? MCP_PROVIDER_REGISTRY.supabase.endpoint
|
|
20
|
+
: resolveCuratedMcpEndpoint({
|
|
21
|
+
provider: connection.provider,
|
|
22
|
+
accessMode: connection.accessMode,
|
|
23
|
+
externalScope: connection.externalScope,
|
|
24
|
+
}).href;
|
|
25
|
+
return {
|
|
26
|
+
serverUrl,
|
|
27
|
+
...(connection.provider === "linear" && connection.accessMode === "read_only"
|
|
28
|
+
? { scope: "read" }
|
|
29
|
+
: {}),
|
|
30
|
+
accessMode: connection.accessMode,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function readOAuthAuthorizationSnapshot(statusMetadata, connection) {
|
|
34
|
+
const value = statusMetadata.authorization;
|
|
35
|
+
if (isRecord(value)
|
|
36
|
+
&& typeof value.serverUrl === "string"
|
|
37
|
+
&& value.serverUrl.length > 0
|
|
38
|
+
&& typeof value.accessMode === "string"
|
|
39
|
+
&& (value.scope === undefined || typeof value.scope === "string")) {
|
|
40
|
+
return {
|
|
41
|
+
serverUrl: value.serverUrl,
|
|
42
|
+
...(typeof value.scope === "string" ? { scope: value.scope } : {}),
|
|
43
|
+
accessMode: value.accessMode,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return createOAuthAuthorizationSnapshot(connection);
|
|
47
|
+
}
|
|
48
|
+
function hashState(state) {
|
|
49
|
+
return createHash("sha256").update(state).digest("hex");
|
|
50
|
+
}
|
|
51
|
+
function isRecord(value) {
|
|
52
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
53
|
+
}
|
|
54
|
+
class ManagedMcpOAuthRefreshTimeoutError extends Error {
|
|
55
|
+
constructor() {
|
|
56
|
+
super("Managed MCP OAuth refresh timed out");
|
|
57
|
+
this.name = "ManagedMcpOAuthRefreshTimeoutError";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function withHardTimeout(promise, timeoutMs) {
|
|
61
|
+
let timer;
|
|
62
|
+
try {
|
|
63
|
+
return await Promise.race([
|
|
64
|
+
promise,
|
|
65
|
+
new Promise((_, reject) => {
|
|
66
|
+
timer = setTimeout(() => reject(new ManagedMcpOAuthRefreshTimeoutError()), timeoutMs);
|
|
67
|
+
}),
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
if (timer)
|
|
72
|
+
clearTimeout(timer);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function waitForLease(delayMs) {
|
|
76
|
+
await new Promise((resolve) => {
|
|
77
|
+
setTimeout(resolve, delayMs);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function publicGrant(row) {
|
|
81
|
+
return {
|
|
82
|
+
id: row.id,
|
|
83
|
+
connectionId: row.connectionId,
|
|
84
|
+
providerSubject: row.providerSubject,
|
|
85
|
+
providerScopes: row.providerScopes,
|
|
86
|
+
externalScopeMetadata: row.externalScopeMetadata,
|
|
87
|
+
status: row.status,
|
|
88
|
+
hasCredentials: Boolean(row.credentialSecretId),
|
|
89
|
+
expiresAt: row.expiresAt,
|
|
90
|
+
lastRefreshedAt: row.lastRefreshedAt,
|
|
91
|
+
revokedAt: row.revokedAt,
|
|
92
|
+
createdAt: row.createdAt,
|
|
93
|
+
updatedAt: row.updatedAt,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function publicConnection(row) {
|
|
97
|
+
return {
|
|
98
|
+
id: row.id,
|
|
99
|
+
orgId: row.orgId,
|
|
100
|
+
name: row.name,
|
|
101
|
+
displayName: row.displayName,
|
|
102
|
+
provider: row.provider,
|
|
103
|
+
transport: row.transport,
|
|
104
|
+
externalScope: row.externalScope,
|
|
105
|
+
accessMode: row.accessMode,
|
|
106
|
+
status: row.status,
|
|
107
|
+
safeConfig: row.safeConfig,
|
|
108
|
+
startupTimeoutMs: row.startupTimeoutMs,
|
|
109
|
+
toolTimeoutMs: row.toolTimeoutMs,
|
|
110
|
+
enabled: row.enabled,
|
|
111
|
+
required: row.required,
|
|
112
|
+
hasCredentials: Boolean(row.credentialSecretId),
|
|
113
|
+
lastDiscoveredAt: row.lastDiscoveredAt,
|
|
114
|
+
activatedAt: row.activatedAt,
|
|
115
|
+
disabledAt: row.disabledAt,
|
|
116
|
+
revokedAt: row.revokedAt,
|
|
117
|
+
createdAt: row.createdAt,
|
|
118
|
+
updatedAt: row.updatedAt,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function oauthActivityValues(input) {
|
|
122
|
+
return {
|
|
123
|
+
orgId: input.orgId,
|
|
124
|
+
actorType: input.actorUserId ? "user" : "system",
|
|
125
|
+
actorId: input.actorUserId ?? "system",
|
|
126
|
+
action: input.action,
|
|
127
|
+
entityType: "mcp_connection",
|
|
128
|
+
entityId: input.connectionId,
|
|
129
|
+
details: input.details,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function tokenExpiry(tokens, now = new Date()) {
|
|
133
|
+
if (typeof tokens?.expires_in !== "number" || !Number.isFinite(tokens.expires_in))
|
|
134
|
+
return null;
|
|
135
|
+
return new Date(now.getTime() + Math.max(0, tokens.expires_in) * 1_000);
|
|
136
|
+
}
|
|
137
|
+
function tokenScopes(tokens) {
|
|
138
|
+
return typeof tokens?.scope === "string"
|
|
139
|
+
? Array.from(new Set(tokens.scope.split(/\s+/u).filter(Boolean)))
|
|
140
|
+
: [];
|
|
141
|
+
}
|
|
142
|
+
function accessToken(material) {
|
|
143
|
+
const value = material.tokens?.access_token;
|
|
144
|
+
if (!value)
|
|
145
|
+
throw unprocessable("Managed MCP OAuth credentials are unavailable");
|
|
146
|
+
return value;
|
|
147
|
+
}
|
|
148
|
+
function oauthRevocationEndpoint(material) {
|
|
149
|
+
const metadata = material?.discoveryState?.authorizationServerMetadata;
|
|
150
|
+
return isRecord(metadata) && typeof metadata.revocation_endpoint === "string"
|
|
151
|
+
? metadata.revocation_endpoint
|
|
152
|
+
: null;
|
|
153
|
+
}
|
|
154
|
+
const SAFE_SCOPE_METADATA_KEYS = new Set([
|
|
155
|
+
"region",
|
|
156
|
+
"scopeKind",
|
|
157
|
+
"status",
|
|
158
|
+
"workspaceName",
|
|
159
|
+
"workspace_name",
|
|
160
|
+
"organizationName",
|
|
161
|
+
"organization_name",
|
|
162
|
+
]);
|
|
163
|
+
const MAX_SCOPE_OPTIONS = 500;
|
|
164
|
+
const MAX_SCOPE_METADATA_BYTES = 4_096;
|
|
165
|
+
function safeScopeMetadata(value) {
|
|
166
|
+
const output = {};
|
|
167
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
168
|
+
if (!SAFE_SCOPE_METADATA_KEYS.has(key))
|
|
169
|
+
continue;
|
|
170
|
+
const safeValue = typeof raw === "string"
|
|
171
|
+
? raw.slice(0, 512)
|
|
172
|
+
: typeof raw === "number" && Number.isFinite(raw)
|
|
173
|
+
? raw
|
|
174
|
+
: typeof raw === "boolean"
|
|
175
|
+
? raw
|
|
176
|
+
: undefined;
|
|
177
|
+
if (safeValue === undefined)
|
|
178
|
+
continue;
|
|
179
|
+
const candidate = { ...output, [key]: safeValue };
|
|
180
|
+
if (Buffer.byteLength(JSON.stringify(candidate), "utf8") > MAX_SCOPE_METADATA_BYTES) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
output[key] = safeValue;
|
|
184
|
+
}
|
|
185
|
+
return output;
|
|
186
|
+
}
|
|
187
|
+
function safeScopeOption(value) {
|
|
188
|
+
return {
|
|
189
|
+
id: value.id.trim().slice(0, 512),
|
|
190
|
+
displayName: value.displayName.trim().slice(0, 240),
|
|
191
|
+
metadata: isRecord(value.metadata) ? safeScopeMetadata(value.metadata) : {},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function sanitizeScopeOptions(values) {
|
|
195
|
+
const output = [];
|
|
196
|
+
const seen = new Set();
|
|
197
|
+
for (const value of values) {
|
|
198
|
+
const safe = safeScopeOption(value);
|
|
199
|
+
if (!safe.id || !safe.displayName || seen.has(safe.id))
|
|
200
|
+
continue;
|
|
201
|
+
seen.add(safe.id);
|
|
202
|
+
output.push(safe);
|
|
203
|
+
if (output.length === MAX_SCOPE_OPTIONS)
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
return output;
|
|
207
|
+
}
|
|
208
|
+
function parseToolJson(result) {
|
|
209
|
+
for (const content of result.content ?? []) {
|
|
210
|
+
if (content.type !== "text")
|
|
211
|
+
continue;
|
|
212
|
+
try {
|
|
213
|
+
return JSON.parse(content.text);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// Ignore non-JSON provider prose; onboarding never persists it.
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
function asProviderScope(value, keys) {
|
|
222
|
+
if (!isRecord(value))
|
|
223
|
+
return null;
|
|
224
|
+
const id = keys.id.map((key) => value[key])
|
|
225
|
+
.find((item) => typeof item === "string" && item.length > 0);
|
|
226
|
+
if (!id)
|
|
227
|
+
return null;
|
|
228
|
+
const displayName = keys.name.map((key) => value[key])
|
|
229
|
+
.find((item) => typeof item === "string" && item.length > 0) ?? id;
|
|
230
|
+
const metadata = Object.fromEntries(["region", "status", "workspaceName", "workspace_name", "organizationName"]
|
|
231
|
+
.flatMap((key) => value[key] === undefined ? [] : [[key, value[key]]]));
|
|
232
|
+
return safeScopeOption({ id, displayName, metadata });
|
|
233
|
+
}
|
|
234
|
+
function providerScopeContainer(provider, value) {
|
|
235
|
+
const definition = MCP_PROVIDER_REGISTRY[provider].scopeIdentity;
|
|
236
|
+
const candidates = [
|
|
237
|
+
...definition.containers.map((key) => value[key]),
|
|
238
|
+
];
|
|
239
|
+
const user = value.user;
|
|
240
|
+
if (provider === "linear" && isRecord(user)) {
|
|
241
|
+
candidates.push(...definition.containers.map((key) => user[key]));
|
|
242
|
+
}
|
|
243
|
+
const bot = value.bot;
|
|
244
|
+
if (provider === "notion" && isRecord(bot)) {
|
|
245
|
+
candidates.push(...definition.containers.map((key) => bot[key]));
|
|
246
|
+
}
|
|
247
|
+
const self = value.self;
|
|
248
|
+
if (provider === "notion" && isRecord(self)) {
|
|
249
|
+
candidates.push(...definition.containers.map((key) => self[key]));
|
|
250
|
+
}
|
|
251
|
+
return candidates.find(isRecord) ?? null;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Official identity tools return an actor plus an explicit workspace or
|
|
255
|
+
* organization. The actor id is never a valid Rudder connection scope.
|
|
256
|
+
*/
|
|
257
|
+
export function parseProviderWorkspaceScope(provider, value) {
|
|
258
|
+
if (!isRecord(value))
|
|
259
|
+
return null;
|
|
260
|
+
const container = providerScopeContainer(provider, value);
|
|
261
|
+
if (container) {
|
|
262
|
+
return asProviderScope(container, {
|
|
263
|
+
id: ["workspace_id", "workspaceId", "organization_id", "organizationId", "id"],
|
|
264
|
+
name: [
|
|
265
|
+
"workspace_name",
|
|
266
|
+
"workspaceName",
|
|
267
|
+
"organization_name",
|
|
268
|
+
"organizationName",
|
|
269
|
+
"name",
|
|
270
|
+
],
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
if (provider === "linear"
|
|
274
|
+
&& typeof value.id === "string"
|
|
275
|
+
&& value.id.length > 0
|
|
276
|
+
&& Array.isArray(value.teams)
|
|
277
|
+
&& value.teams.some((team) => isRecord(team) && typeof team.id === "string")) {
|
|
278
|
+
const authorizationName = typeof value.name === "string" && value.name.length > 0
|
|
279
|
+
? `${value.name}'s Linear authorization`
|
|
280
|
+
: "Linear authorization";
|
|
281
|
+
const opaqueSubjectId = createHash("sha256")
|
|
282
|
+
.update(`linear-authorization-subject:${value.id}`)
|
|
283
|
+
.digest("hex")
|
|
284
|
+
.slice(0, 24);
|
|
285
|
+
return safeScopeOption({
|
|
286
|
+
id: `linear-authorization-subject-${opaqueSubjectId}`,
|
|
287
|
+
displayName: authorizationName,
|
|
288
|
+
metadata: {
|
|
289
|
+
scopeKind: "authorization_subject",
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
const nested = provider === "notion" && isRecord(value.bot) ? value.bot : value;
|
|
294
|
+
return asProviderScope(nested, {
|
|
295
|
+
id: ["workspace_id", "workspaceId", "organization_id", "organizationId"],
|
|
296
|
+
name: [
|
|
297
|
+
"workspace_name",
|
|
298
|
+
"workspaceName",
|
|
299
|
+
"organization_name",
|
|
300
|
+
"organizationName",
|
|
301
|
+
],
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
async function defaultDiscoverProviderScope(input) {
|
|
305
|
+
const curatedOrigin = new URL(MCP_PROVIDER_REGISTRY[input.provider].endpoint).origin;
|
|
306
|
+
const client = await createManagedMcpClient({
|
|
307
|
+
transport: "streamable_http",
|
|
308
|
+
url: input.endpoint,
|
|
309
|
+
credentials: resolveMcpHttpCredentials({ bearerToken: accessToken(input.material) }),
|
|
310
|
+
network: {
|
|
311
|
+
allowedOrigins: input.allowlists.httpOrigins,
|
|
312
|
+
curatedOrigin,
|
|
313
|
+
lookup: input.dnsLookup,
|
|
314
|
+
},
|
|
315
|
+
startupTimeoutMs: input.connection.startupTimeoutMs,
|
|
316
|
+
toolTimeoutMs: input.connection.toolTimeoutMs,
|
|
317
|
+
});
|
|
318
|
+
try {
|
|
319
|
+
if (input.provider === "supabase") {
|
|
320
|
+
const parsed = parseToolJson(await client.callTool("list_projects", {}));
|
|
321
|
+
const rawProjects = Array.isArray(parsed)
|
|
322
|
+
? parsed
|
|
323
|
+
: isRecord(parsed) && Array.isArray(parsed.projects)
|
|
324
|
+
? parsed.projects
|
|
325
|
+
: [];
|
|
326
|
+
const options = rawProjects
|
|
327
|
+
.map((project) => asProviderScope(project, {
|
|
328
|
+
id: ["ref", "id", "project_ref"],
|
|
329
|
+
name: ["name", "display_name", "ref", "id"],
|
|
330
|
+
}))
|
|
331
|
+
.filter((option) => Boolean(option));
|
|
332
|
+
return { options };
|
|
333
|
+
}
|
|
334
|
+
const identity = MCP_PROVIDER_REGISTRY[input.provider].scopeIdentity;
|
|
335
|
+
if (!identity)
|
|
336
|
+
throw new Error("Provider did not define workspace identity discovery");
|
|
337
|
+
const tools = await client.discoverTools();
|
|
338
|
+
const identityTool = identity.toolNames.find((name) => (tools.some((tool) => tool.name === name)));
|
|
339
|
+
if (!identityTool) {
|
|
340
|
+
throw new Error("Provider did not expose a safe workspace identity tool");
|
|
341
|
+
}
|
|
342
|
+
const parsed = parseToolJson(await client.callTool(identityTool, identity.arguments));
|
|
343
|
+
const selected = parseProviderWorkspaceScope(input.provider, parsed);
|
|
344
|
+
if (!selected)
|
|
345
|
+
throw new Error("Provider workspace identity was invalid");
|
|
346
|
+
return { options: [], selected };
|
|
347
|
+
}
|
|
348
|
+
finally {
|
|
349
|
+
await client.close();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
async function prepareOAuthSecret(connectionName, kind, material) {
|
|
353
|
+
const id = randomUUID();
|
|
354
|
+
const prepared = await getSecretProvider("local_encrypted").createVersion({
|
|
355
|
+
value: JSON.stringify(material),
|
|
356
|
+
externalRef: null,
|
|
357
|
+
});
|
|
358
|
+
return {
|
|
359
|
+
id,
|
|
360
|
+
name: `Managed MCP OAuth ${kind} - ${connectionName} - ${id}`,
|
|
361
|
+
externalRef: prepared.externalRef,
|
|
362
|
+
material: prepared.material,
|
|
363
|
+
valueSha256: prepared.valueSha256,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
async function insertOAuthSecret(tx, input) {
|
|
367
|
+
await tx.insert(organizationSecrets).values({
|
|
368
|
+
id: input.secret.id,
|
|
369
|
+
orgId: input.orgId,
|
|
370
|
+
name: input.secret.name,
|
|
371
|
+
provider: "local_encrypted",
|
|
372
|
+
purpose: "managed_mcp_oauth",
|
|
373
|
+
externalRef: input.secret.externalRef,
|
|
374
|
+
latestVersion: 1,
|
|
375
|
+
description: "Encrypted managed MCP OAuth material",
|
|
376
|
+
createdByUserId: input.actorUserId ?? null,
|
|
377
|
+
});
|
|
378
|
+
await tx.insert(organizationSecretVersions).values({
|
|
379
|
+
secretId: input.secret.id,
|
|
380
|
+
version: 1,
|
|
381
|
+
material: input.secret.material,
|
|
382
|
+
valueSha256: input.secret.valueSha256,
|
|
383
|
+
createdByUserId: input.actorUserId ?? null,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
function parseOAuthMaterial(value) {
|
|
387
|
+
try {
|
|
388
|
+
const parsed = JSON.parse(value);
|
|
389
|
+
if (!isRecord(parsed))
|
|
390
|
+
throw new Error("invalid");
|
|
391
|
+
return parsed;
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
throw unprocessable("Managed MCP OAuth credentials are unavailable");
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
export function managedMcpOAuthService(db, options) {
|
|
398
|
+
const grantRefreshSingleFlights = new Map();
|
|
399
|
+
const runAuth = options.oauthAuth ?? auth;
|
|
400
|
+
const secureFetch = createSecureMcpFetch({
|
|
401
|
+
allowedOrigins: options.allowlists.httpOrigins,
|
|
402
|
+
lookup: options.dnsLookup,
|
|
403
|
+
});
|
|
404
|
+
async function resolveLockedOAuthSecret(tx, orgId, credentialSecretId) {
|
|
405
|
+
const secret = await tx.select().from(organizationSecrets)
|
|
406
|
+
.where(and(eq(organizationSecrets.orgId, orgId), eq(organizationSecrets.id, credentialSecretId), eq(organizationSecrets.provider, "local_encrypted"), eq(organizationSecrets.purpose, "managed_mcp_oauth")))
|
|
407
|
+
.for("update")
|
|
408
|
+
.then((rows) => rows[0] ?? null);
|
|
409
|
+
if (!secret)
|
|
410
|
+
return null;
|
|
411
|
+
const version = await tx.select().from(organizationSecretVersions)
|
|
412
|
+
.where(and(eq(organizationSecretVersions.secretId, secret.id), eq(organizationSecretVersions.version, secret.latestVersion)))
|
|
413
|
+
.for("share")
|
|
414
|
+
.then((rows) => rows[0] ?? null);
|
|
415
|
+
if (!version)
|
|
416
|
+
return null;
|
|
417
|
+
return {
|
|
418
|
+
secret,
|
|
419
|
+
material: parseOAuthMaterial(await getSecretProvider("local_encrypted").resolveVersion({
|
|
420
|
+
material: version.material,
|
|
421
|
+
externalRef: secret.externalRef,
|
|
422
|
+
})),
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
async function findConnection(orgId, connectionId) {
|
|
426
|
+
const row = await db.select().from(mcpConnections)
|
|
427
|
+
.where(and(eq(mcpConnections.orgId, orgId), eq(mcpConnections.id, connectionId)))
|
|
428
|
+
.then((rows) => rows[0] ?? null);
|
|
429
|
+
if (!row)
|
|
430
|
+
throw notFound("MCP connection not found");
|
|
431
|
+
return row;
|
|
432
|
+
}
|
|
433
|
+
function assertCurated(row) {
|
|
434
|
+
if (row.provider !== "supabase" && row.provider !== "linear" && row.provider !== "notion") {
|
|
435
|
+
throw unprocessable("Custom MCP connections do not use managed OAuth");
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
async function isValidAuthorizer(orgId, authorizingUserId) {
|
|
439
|
+
if (!authorizingUserId)
|
|
440
|
+
return options.deploymentMode === "local_trusted";
|
|
441
|
+
const [membership, instanceAdmin] = await Promise.all([
|
|
442
|
+
db.select().from(organizationMemberships)
|
|
443
|
+
.where(and(eq(organizationMemberships.orgId, orgId), eq(organizationMemberships.principalType, "user"), eq(organizationMemberships.principalId, authorizingUserId), eq(organizationMemberships.status, "active"), eq(organizationMemberships.membershipRole, "owner")))
|
|
444
|
+
.then((rows) => rows[0] ?? null),
|
|
445
|
+
db.select().from(instanceUserRoles)
|
|
446
|
+
.where(and(eq(instanceUserRoles.userId, authorizingUserId), eq(instanceUserRoles.role, "instance_admin")))
|
|
447
|
+
.then((rows) => rows[0] ?? null),
|
|
448
|
+
]);
|
|
449
|
+
return Boolean(membership || instanceAdmin);
|
|
450
|
+
}
|
|
451
|
+
async function isValidAuthorizerInTransaction(tx, orgId, authorizingUserId) {
|
|
452
|
+
if (!authorizingUserId)
|
|
453
|
+
return options.deploymentMode === "local_trusted";
|
|
454
|
+
const membership = await tx.select({ id: organizationMemberships.id })
|
|
455
|
+
.from(organizationMemberships)
|
|
456
|
+
.where(and(eq(organizationMemberships.orgId, orgId), eq(organizationMemberships.principalType, "user"), eq(organizationMemberships.principalId, authorizingUserId), eq(organizationMemberships.status, "active"), eq(organizationMemberships.membershipRole, "owner")))
|
|
457
|
+
.for("share")
|
|
458
|
+
.then((rows) => rows[0] ?? null);
|
|
459
|
+
if (membership)
|
|
460
|
+
return true;
|
|
461
|
+
return Boolean(await tx.select({ id: instanceUserRoles.id })
|
|
462
|
+
.from(instanceUserRoles)
|
|
463
|
+
.where(and(eq(instanceUserRoles.userId, authorizingUserId), eq(instanceUserRoles.role, "instance_admin")))
|
|
464
|
+
.for("share")
|
|
465
|
+
.then((rows) => rows[0] ?? null));
|
|
466
|
+
}
|
|
467
|
+
async function requireUsableGrant(orgId, connectionId) {
|
|
468
|
+
const outcome = await db.transaction(async (tx) => {
|
|
469
|
+
const connection = await tx.select().from(mcpConnections)
|
|
470
|
+
.where(and(eq(mcpConnections.orgId, orgId), eq(mcpConnections.id, connectionId)))
|
|
471
|
+
.for("update")
|
|
472
|
+
.then((rows) => rows[0] ?? null);
|
|
473
|
+
const grant = await tx.select().from(mcpOAuthGrants)
|
|
474
|
+
.where(and(eq(mcpOAuthGrants.orgId, orgId), eq(mcpOAuthGrants.connectionId, connectionId)))
|
|
475
|
+
.for("update")
|
|
476
|
+
.then((rows) => rows[0] ?? null);
|
|
477
|
+
if (!connection
|
|
478
|
+
|| !grant
|
|
479
|
+
|| grant.status !== "active"
|
|
480
|
+
|| !connection.enabled
|
|
481
|
+
|| (connection.status !== "active" && connection.status !== "selecting_scope")) {
|
|
482
|
+
return { kind: "unusable" };
|
|
483
|
+
}
|
|
484
|
+
if (!await isValidAuthorizerInTransaction(tx, orgId, grant.authorizingUserId)) {
|
|
485
|
+
await transitionGrantToNeedsReauth(tx, grant, "authorizer_no_longer_authorized");
|
|
486
|
+
return { kind: "invalid_authorizer" };
|
|
487
|
+
}
|
|
488
|
+
return { kind: "usable", grant };
|
|
489
|
+
});
|
|
490
|
+
if (outcome.kind === "invalid_authorizer") {
|
|
491
|
+
throw unprocessable("Managed MCP OAuth authorization must be reconnected by an owner");
|
|
492
|
+
}
|
|
493
|
+
if (outcome.kind !== "usable") {
|
|
494
|
+
throw unprocessable("Managed MCP OAuth authorization must be reconnected");
|
|
495
|
+
}
|
|
496
|
+
return outcome.grant;
|
|
497
|
+
}
|
|
498
|
+
async function resolveUsableAccessToken(orgId, connectionId) {
|
|
499
|
+
const outcome = await db.transaction(async (tx) => {
|
|
500
|
+
const connection = await tx.select().from(mcpConnections)
|
|
501
|
+
.where(and(eq(mcpConnections.orgId, orgId), eq(mcpConnections.id, connectionId)))
|
|
502
|
+
.for("update")
|
|
503
|
+
.then((rows) => rows[0] ?? null);
|
|
504
|
+
const grant = await tx.select().from(mcpOAuthGrants)
|
|
505
|
+
.where(and(eq(mcpOAuthGrants.orgId, orgId), eq(mcpOAuthGrants.connectionId, connectionId)))
|
|
506
|
+
.for("update")
|
|
507
|
+
.then((rows) => rows[0] ?? null);
|
|
508
|
+
if (!connection
|
|
509
|
+
|| !grant
|
|
510
|
+
|| grant.status !== "active"
|
|
511
|
+
|| !grant.credentialSecretId
|
|
512
|
+
|| !connection.enabled
|
|
513
|
+
|| (connection.status !== "active" && connection.status !== "selecting_scope")) {
|
|
514
|
+
return { kind: "unusable" };
|
|
515
|
+
}
|
|
516
|
+
if (!await isValidAuthorizerInTransaction(tx, orgId, grant.authorizingUserId)) {
|
|
517
|
+
await transitionGrantToNeedsReauth(tx, grant, "authorizer_no_longer_authorized");
|
|
518
|
+
return { kind: "invalid_authorizer" };
|
|
519
|
+
}
|
|
520
|
+
const resolved = await resolveLockedOAuthSecret(tx, orgId, grant.credentialSecretId);
|
|
521
|
+
if (!resolved) {
|
|
522
|
+
await transitionGrantToNeedsReauth(tx, grant, "credential_unavailable");
|
|
523
|
+
return { kind: "unusable" };
|
|
524
|
+
}
|
|
525
|
+
return { kind: "usable", token: accessToken(resolved.material) };
|
|
526
|
+
});
|
|
527
|
+
if (outcome.kind === "invalid_authorizer") {
|
|
528
|
+
throw unprocessable("Managed MCP OAuth authorization must be reconnected by an owner");
|
|
529
|
+
}
|
|
530
|
+
if (outcome.kind !== "usable") {
|
|
531
|
+
throw unprocessable("Managed MCP OAuth authorization must be reconnected");
|
|
532
|
+
}
|
|
533
|
+
return outcome.token;
|
|
534
|
+
}
|
|
535
|
+
async function discoverScope(input) {
|
|
536
|
+
if (options.discoverProviderScope) {
|
|
537
|
+
return options.discoverProviderScope({
|
|
538
|
+
provider: input.connection.provider,
|
|
539
|
+
connection: input.connection,
|
|
540
|
+
material: input.material,
|
|
541
|
+
endpoint: input.endpoint,
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
return defaultDiscoverProviderScope({
|
|
545
|
+
provider: input.connection.provider,
|
|
546
|
+
connection: input.connection,
|
|
547
|
+
material: input.material,
|
|
548
|
+
endpoint: input.endpoint,
|
|
549
|
+
allowlists: options.allowlists,
|
|
550
|
+
dnsLookup: options.dnsLookup,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
async function bestEffortRevokeMaterial(material, timeoutMs) {
|
|
554
|
+
const revocationEndpoint = oauthRevocationEndpoint(material);
|
|
555
|
+
const tokens = material?.tokens;
|
|
556
|
+
const token = tokens?.refresh_token ?? tokens?.access_token;
|
|
557
|
+
if (!revocationEndpoint || !token)
|
|
558
|
+
return;
|
|
559
|
+
const body = new URLSearchParams({
|
|
560
|
+
token,
|
|
561
|
+
token_type_hint: tokens?.refresh_token ? "refresh_token" : "access_token",
|
|
562
|
+
});
|
|
563
|
+
const clientInformation = material?.clientInformation;
|
|
564
|
+
if (clientInformation?.client_id)
|
|
565
|
+
body.set("client_id", clientInformation.client_id);
|
|
566
|
+
if (clientInformation?.client_secret)
|
|
567
|
+
body.set("client_secret", clientInformation.client_secret);
|
|
568
|
+
try {
|
|
569
|
+
const response = await secureFetch(revocationEndpoint, {
|
|
570
|
+
method: "POST",
|
|
571
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
572
|
+
body,
|
|
573
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
574
|
+
});
|
|
575
|
+
await response.body?.cancel();
|
|
576
|
+
}
|
|
577
|
+
catch {
|
|
578
|
+
// RFC 7009 revocation is best effort. Rudder's local state always wins.
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
async function cleanupExpiredSessions(orgId, limit = 100) {
|
|
582
|
+
const databaseClock = await db.execute(sql `
|
|
583
|
+
select clock_timestamp()::text as value
|
|
584
|
+
`);
|
|
585
|
+
const now = new Date(databaseClock[0].value);
|
|
586
|
+
const boundedLimit = Math.max(1, Math.min(limit, 1_000));
|
|
587
|
+
const candidates = await db.select({
|
|
588
|
+
id: mcpOAuthSessions.id,
|
|
589
|
+
connectionId: mcpOAuthSessions.connectionId,
|
|
590
|
+
orgId: mcpOAuthSessions.orgId,
|
|
591
|
+
}).from(mcpOAuthSessions).where(and(eq(mcpOAuthSessions.status, "authorizing"), isNull(mcpOAuthSessions.consumedAt), lte(mcpOAuthSessions.expiresAt, now), ...(orgId ? [eq(mcpOAuthSessions.orgId, orgId)] : [])))
|
|
592
|
+
.orderBy(asc(mcpOAuthSessions.expiresAt), asc(mcpOAuthSessions.id))
|
|
593
|
+
.limit(boundedLimit);
|
|
594
|
+
const groupedCandidates = new Map();
|
|
595
|
+
for (const candidate of candidates) {
|
|
596
|
+
const key = `${candidate.orgId}:${candidate.connectionId}`;
|
|
597
|
+
const group = groupedCandidates.get(key);
|
|
598
|
+
if (group) {
|
|
599
|
+
group.sessionIds.push(candidate.id);
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
groupedCandidates.set(key, {
|
|
603
|
+
connectionId: candidate.connectionId,
|
|
604
|
+
orgId: candidate.orgId,
|
|
605
|
+
sessionIds: [candidate.id],
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
const keys = Array.from(groupedCandidates.values()).sort((left, right) => (left.connectionId.localeCompare(right.connectionId)));
|
|
610
|
+
let cleaned = 0;
|
|
611
|
+
for (const key of keys) {
|
|
612
|
+
cleaned += await db.transaction(async (tx) => {
|
|
613
|
+
const transactionClock = await tx.execute(sql `
|
|
614
|
+
select clock_timestamp()::text as value
|
|
615
|
+
`);
|
|
616
|
+
const transactionNow = new Date(transactionClock[0].value);
|
|
617
|
+
const connection = await tx.select().from(mcpConnections)
|
|
618
|
+
.where(and(eq(mcpConnections.orgId, key.orgId), eq(mcpConnections.id, key.connectionId)))
|
|
619
|
+
.for("update")
|
|
620
|
+
.then((rows) => rows[0] ?? null);
|
|
621
|
+
if (!connection)
|
|
622
|
+
return 0;
|
|
623
|
+
const expired = await tx.select().from(mcpOAuthSessions)
|
|
624
|
+
.where(and(eq(mcpOAuthSessions.orgId, key.orgId), eq(mcpOAuthSessions.connectionId, key.connectionId), inArray(mcpOAuthSessions.id, key.sessionIds), eq(mcpOAuthSessions.status, "authorizing"), isNull(mcpOAuthSessions.consumedAt), lte(mcpOAuthSessions.expiresAt, transactionNow)))
|
|
625
|
+
.for("update");
|
|
626
|
+
if (expired.length === 0)
|
|
627
|
+
return 0;
|
|
628
|
+
const secretIds = expired
|
|
629
|
+
.map((session) => session.credentialSecretId)
|
|
630
|
+
.filter((id) => Boolean(id));
|
|
631
|
+
await tx.update(mcpOAuthSessions).set({
|
|
632
|
+
status: "expired",
|
|
633
|
+
consumedAt: transactionNow,
|
|
634
|
+
credentialSecretId: null,
|
|
635
|
+
statusMetadata: { reason: "ttl_expired" },
|
|
636
|
+
}).where(inArray(mcpOAuthSessions.id, expired.map((session) => session.id)));
|
|
637
|
+
if (secretIds.length > 0) {
|
|
638
|
+
await tx.delete(organizationSecrets)
|
|
639
|
+
.where(inArray(organizationSecrets.id, secretIds));
|
|
640
|
+
}
|
|
641
|
+
const stillAuthorizing = await tx.select({ id: mcpOAuthSessions.id })
|
|
642
|
+
.from(mcpOAuthSessions)
|
|
643
|
+
.where(and(eq(mcpOAuthSessions.orgId, key.orgId), eq(mcpOAuthSessions.connectionId, key.connectionId), eq(mcpOAuthSessions.status, "authorizing"), isNull(mcpOAuthSessions.consumedAt), gt(mcpOAuthSessions.expiresAt, transactionNow)))
|
|
644
|
+
.for("share")
|
|
645
|
+
.then((rows) => rows[0] ?? null);
|
|
646
|
+
if (connection.status === "authorizing" && !stillAuthorizing) {
|
|
647
|
+
await tx.update(mcpConnections).set({
|
|
648
|
+
status: "error",
|
|
649
|
+
lifecycleRevision: connection.lifecycleRevision + 1,
|
|
650
|
+
updatedAt: transactionNow,
|
|
651
|
+
}).where(eq(mcpConnections.id, connection.id));
|
|
652
|
+
}
|
|
653
|
+
await tx.insert(activityLog).values(oauthActivityValues({
|
|
654
|
+
orgId: key.orgId,
|
|
655
|
+
connectionId: key.connectionId,
|
|
656
|
+
action: "mcp_oauth.sessions_expired",
|
|
657
|
+
details: { count: expired.length },
|
|
658
|
+
}));
|
|
659
|
+
return expired.length;
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
return cleaned;
|
|
663
|
+
}
|
|
664
|
+
async function start(orgId, connectionId, actor) {
|
|
665
|
+
await cleanupExpiredSessions(orgId);
|
|
666
|
+
const connection = await findConnection(orgId, connectionId);
|
|
667
|
+
assertCurated(connection);
|
|
668
|
+
if (options.deploymentMode === "authenticated"
|
|
669
|
+
&& !actor.userId) {
|
|
670
|
+
throw unprocessable("Managed MCP OAuth requires an authenticated authorizing user");
|
|
671
|
+
}
|
|
672
|
+
let redirectUri;
|
|
673
|
+
try {
|
|
674
|
+
redirectUri = resolveMcpOAuthRedirectUri({
|
|
675
|
+
deploymentMode: options.deploymentMode,
|
|
676
|
+
serverPort: options.serverPort,
|
|
677
|
+
authPublicBaseUrl: options.authPublicBaseUrl,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
catch {
|
|
681
|
+
throw unprocessable("Managed MCP OAuth requires a canonical HTTPS auth public base URL");
|
|
682
|
+
}
|
|
683
|
+
const rawState = randomBytes(32).toString("hex");
|
|
684
|
+
let material = {};
|
|
685
|
+
const provider = new PersistentMcpOAuthClientProvider({
|
|
686
|
+
redirectUri,
|
|
687
|
+
state: rawState,
|
|
688
|
+
material,
|
|
689
|
+
save: async (next) => {
|
|
690
|
+
material = structuredClone(next);
|
|
691
|
+
},
|
|
692
|
+
});
|
|
693
|
+
const authorization = createOAuthAuthorizationSnapshot(connection);
|
|
694
|
+
const result = await runAuth(provider, {
|
|
695
|
+
serverUrl: authorization.serverUrl,
|
|
696
|
+
scope: authorization.scope,
|
|
697
|
+
fetchFn: secureFetch,
|
|
698
|
+
});
|
|
699
|
+
if (result !== "REDIRECT" || !provider.authorizationUrl) {
|
|
700
|
+
throw unprocessable("Managed MCP OAuth did not produce an authorization redirect");
|
|
701
|
+
}
|
|
702
|
+
try {
|
|
703
|
+
await resolveMcpHttpTarget(provider.authorizationUrl.href, {
|
|
704
|
+
allowedOrigins: options.allowlists.httpOrigins,
|
|
705
|
+
lookup: options.dnsLookup,
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
catch {
|
|
709
|
+
throw unprocessable("Managed MCP OAuth authorization endpoint is not allowed");
|
|
710
|
+
}
|
|
711
|
+
const sessionSecret = await prepareOAuthSecret(connection.name, "session", material);
|
|
712
|
+
const now = new Date();
|
|
713
|
+
const expiresAt = new Date(now.getTime() + MCP_OAUTH_SESSION_TTL_MS);
|
|
714
|
+
await db.transaction(async (tx) => {
|
|
715
|
+
const locked = await tx.select().from(mcpConnections)
|
|
716
|
+
.where(and(eq(mcpConnections.orgId, orgId), eq(mcpConnections.id, connectionId)))
|
|
717
|
+
.for("update")
|
|
718
|
+
.then((rows) => rows[0] ?? null);
|
|
719
|
+
if (!locked)
|
|
720
|
+
throw notFound("MCP connection not found");
|
|
721
|
+
assertCurated(locked);
|
|
722
|
+
if (locked.lifecycleRevision !== connection.lifecycleRevision) {
|
|
723
|
+
throw unprocessable("Managed MCP OAuth connection changed; start authorization again");
|
|
724
|
+
}
|
|
725
|
+
const priorGrant = await tx.select().from(mcpOAuthGrants)
|
|
726
|
+
.where(and(eq(mcpOAuthGrants.orgId, orgId), eq(mcpOAuthGrants.connectionId, connectionId)))
|
|
727
|
+
.for("update")
|
|
728
|
+
.then((rows) => rows[0] ?? null);
|
|
729
|
+
if (priorGrant?.status === "active") {
|
|
730
|
+
await tx.update(mcpOAuthGrants).set({
|
|
731
|
+
status: "needs_reauth",
|
|
732
|
+
statusMetadata: { reason: "authorization_restarted" },
|
|
733
|
+
credentialSecretId: null,
|
|
734
|
+
refreshLeaseNonce: null,
|
|
735
|
+
refreshLeaseExpiresAt: null,
|
|
736
|
+
updatedAt: now,
|
|
737
|
+
}).where(eq(mcpOAuthGrants.id, priorGrant.id));
|
|
738
|
+
if (priorGrant.credentialSecretId) {
|
|
739
|
+
await tx.delete(organizationSecrets)
|
|
740
|
+
.where(eq(organizationSecrets.id, priorGrant.credentialSecretId));
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
const replacedSessions = await tx.select({
|
|
744
|
+
id: mcpOAuthSessions.id,
|
|
745
|
+
credentialSecretId: mcpOAuthSessions.credentialSecretId,
|
|
746
|
+
}).from(mcpOAuthSessions).where(and(eq(mcpOAuthSessions.orgId, orgId), eq(mcpOAuthSessions.connectionId, connectionId), eq(mcpOAuthSessions.status, "authorizing"), isNull(mcpOAuthSessions.consumedAt))).for("update");
|
|
747
|
+
if (replacedSessions.length > 0) {
|
|
748
|
+
await tx.update(mcpOAuthSessions).set({
|
|
749
|
+
status: "expired",
|
|
750
|
+
consumedAt: now,
|
|
751
|
+
credentialSecretId: null,
|
|
752
|
+
statusMetadata: { reason: "replaced" },
|
|
753
|
+
}).where(inArray(mcpOAuthSessions.id, replacedSessions.map((item) => item.id)));
|
|
754
|
+
}
|
|
755
|
+
const replacedSecretIds = replacedSessions
|
|
756
|
+
.map((item) => item.credentialSecretId)
|
|
757
|
+
.filter((id) => Boolean(id));
|
|
758
|
+
if (replacedSecretIds.length > 0) {
|
|
759
|
+
await tx.delete(organizationSecrets)
|
|
760
|
+
.where(inArray(organizationSecrets.id, replacedSecretIds));
|
|
761
|
+
}
|
|
762
|
+
await insertOAuthSecret(tx, {
|
|
763
|
+
orgId,
|
|
764
|
+
secret: sessionSecret,
|
|
765
|
+
actorUserId: actor.userId,
|
|
766
|
+
});
|
|
767
|
+
await tx.insert(mcpOAuthSessions).values({
|
|
768
|
+
orgId,
|
|
769
|
+
connectionId,
|
|
770
|
+
authorizingUserId: actor.localImplicit ? null : actor.userId ?? null,
|
|
771
|
+
stateHash: hashState(rawState),
|
|
772
|
+
credentialSecretId: sessionSecret.id,
|
|
773
|
+
redirectUri,
|
|
774
|
+
status: "authorizing",
|
|
775
|
+
statusMetadata: { authorization },
|
|
776
|
+
expiresAt,
|
|
777
|
+
});
|
|
778
|
+
await tx.update(mcpConnections).set({
|
|
779
|
+
status: "authorizing",
|
|
780
|
+
enabled: true,
|
|
781
|
+
disabledAt: null,
|
|
782
|
+
revokedAt: null,
|
|
783
|
+
lifecycleRevision: locked.lifecycleRevision + 1,
|
|
784
|
+
updatedAt: now,
|
|
785
|
+
}).where(and(eq(mcpConnections.orgId, orgId), eq(mcpConnections.id, connectionId)));
|
|
786
|
+
await tx.insert(activityLog).values(oauthActivityValues({
|
|
787
|
+
orgId,
|
|
788
|
+
connectionId,
|
|
789
|
+
action: "mcp_oauth.authorization_started",
|
|
790
|
+
actorUserId: actor.userId,
|
|
791
|
+
details: { provider: connection.provider },
|
|
792
|
+
}));
|
|
793
|
+
});
|
|
794
|
+
return {
|
|
795
|
+
connectionId,
|
|
796
|
+
authorizationUrl: provider.authorizationUrl.href,
|
|
797
|
+
expiresAt,
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
async function callback(rawInput) {
|
|
801
|
+
const input = mcpOAuthCallbackSchema.parse(rawInput);
|
|
802
|
+
const stateHash = hashState(input.state);
|
|
803
|
+
const session = await db.select().from(mcpOAuthSessions)
|
|
804
|
+
.where(eq(mcpOAuthSessions.stateHash, stateHash))
|
|
805
|
+
.then((rows) => rows[0] ?? null);
|
|
806
|
+
if (!session) {
|
|
807
|
+
throw unprocessable("Managed MCP OAuth session is invalid or expired");
|
|
808
|
+
}
|
|
809
|
+
const consumed = await db.transaction(async (tx) => {
|
|
810
|
+
const now = new Date();
|
|
811
|
+
const connection = await tx.select().from(mcpConnections)
|
|
812
|
+
.where(and(eq(mcpConnections.orgId, session.orgId), eq(mcpConnections.id, session.connectionId)))
|
|
813
|
+
.for("update")
|
|
814
|
+
.then((rows) => rows[0] ?? null);
|
|
815
|
+
if (!connection)
|
|
816
|
+
return null;
|
|
817
|
+
assertCurated(connection);
|
|
818
|
+
if (connection.status !== "authorizing")
|
|
819
|
+
return null;
|
|
820
|
+
const lockedSession = await tx.select().from(mcpOAuthSessions)
|
|
821
|
+
.where(and(eq(mcpOAuthSessions.id, session.id), eq(mcpOAuthSessions.stateHash, stateHash)))
|
|
822
|
+
.for("update")
|
|
823
|
+
.then((rows) => rows[0] ?? null);
|
|
824
|
+
if (!lockedSession || lockedSession.status !== "authorizing" || lockedSession.consumedAt) {
|
|
825
|
+
return null;
|
|
826
|
+
}
|
|
827
|
+
if (lockedSession.expiresAt <= now) {
|
|
828
|
+
await tx.update(mcpOAuthSessions).set({
|
|
829
|
+
consumedAt: now,
|
|
830
|
+
status: "expired",
|
|
831
|
+
credentialSecretId: null,
|
|
832
|
+
statusMetadata: { reason: "ttl_expired" },
|
|
833
|
+
}).where(eq(mcpOAuthSessions.id, lockedSession.id));
|
|
834
|
+
if (lockedSession.credentialSecretId) {
|
|
835
|
+
await tx.delete(organizationSecrets)
|
|
836
|
+
.where(eq(organizationSecrets.id, lockedSession.credentialSecretId));
|
|
837
|
+
}
|
|
838
|
+
if (connection.status === "authorizing") {
|
|
839
|
+
await tx.update(mcpConnections).set({
|
|
840
|
+
status: "error",
|
|
841
|
+
lifecycleRevision: connection.lifecycleRevision + 1,
|
|
842
|
+
updatedAt: now,
|
|
843
|
+
}).where(eq(mcpConnections.id, connection.id));
|
|
844
|
+
}
|
|
845
|
+
return null;
|
|
846
|
+
}
|
|
847
|
+
if (!lockedSession.credentialSecretId)
|
|
848
|
+
return null;
|
|
849
|
+
const resolvedSessionSecret = await resolveLockedOAuthSecret(tx, lockedSession.orgId, lockedSession.credentialSecretId);
|
|
850
|
+
if (!resolvedSessionSecret)
|
|
851
|
+
return null;
|
|
852
|
+
const consumedRow = await tx.update(mcpOAuthSessions).set({
|
|
853
|
+
consumedAt: now,
|
|
854
|
+
status: input.error ? "error" : "consumed",
|
|
855
|
+
credentialSecretId: null,
|
|
856
|
+
statusMetadata: input.error
|
|
857
|
+
? { ...lockedSession.statusMetadata, reason: "provider_denied" }
|
|
858
|
+
: lockedSession.statusMetadata,
|
|
859
|
+
}).where(and(eq(mcpOAuthSessions.id, lockedSession.id), eq(mcpOAuthSessions.status, "authorizing"))).returning().then((rows) => rows[0] ?? null);
|
|
860
|
+
if (!consumedRow)
|
|
861
|
+
return null;
|
|
862
|
+
await tx.delete(organizationSecrets)
|
|
863
|
+
.where(eq(organizationSecrets.id, lockedSession.credentialSecretId));
|
|
864
|
+
await tx.insert(activityLog).values(oauthActivityValues({
|
|
865
|
+
orgId: consumedRow.orgId,
|
|
866
|
+
connectionId: connection.id,
|
|
867
|
+
action: "mcp_oauth.callback_consumed",
|
|
868
|
+
actorUserId: consumedRow.authorizingUserId,
|
|
869
|
+
details: { provider: connection.provider, providerError: Boolean(input.error) },
|
|
870
|
+
}));
|
|
871
|
+
return {
|
|
872
|
+
session: consumedRow,
|
|
873
|
+
sessionMaterial: resolvedSessionSecret.material,
|
|
874
|
+
connection,
|
|
875
|
+
connectionLifecycleRevision: connection.lifecycleRevision,
|
|
876
|
+
};
|
|
877
|
+
});
|
|
878
|
+
if (!consumed) {
|
|
879
|
+
throw unprocessable("Managed MCP OAuth session is invalid or expired");
|
|
880
|
+
}
|
|
881
|
+
const callbackContext = consumed;
|
|
882
|
+
async function finishFailure(inputFailure) {
|
|
883
|
+
await db.transaction(async (tx) => {
|
|
884
|
+
const locked = await tx.select().from(mcpConnections)
|
|
885
|
+
.where(and(eq(mcpConnections.orgId, callbackContext.session.orgId), eq(mcpConnections.id, callbackContext.connection.id)))
|
|
886
|
+
.for("update")
|
|
887
|
+
.then((rows) => rows[0] ?? null);
|
|
888
|
+
if (!locked
|
|
889
|
+
|| locked.status !== "authorizing"
|
|
890
|
+
|| locked.lifecycleRevision !== callbackContext.connectionLifecycleRevision) {
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
const now = new Date();
|
|
894
|
+
await tx.update(mcpConnections).set({
|
|
895
|
+
status: inputFailure.status,
|
|
896
|
+
...(inputFailure.status === "needs_reauth"
|
|
897
|
+
? { enabled: false, disabledAt: now }
|
|
898
|
+
: {}),
|
|
899
|
+
lifecycleRevision: locked.lifecycleRevision + 1,
|
|
900
|
+
updatedAt: now,
|
|
901
|
+
}).where(eq(mcpConnections.id, locked.id));
|
|
902
|
+
await tx.insert(activityLog).values(oauthActivityValues({
|
|
903
|
+
orgId: callbackContext.session.orgId,
|
|
904
|
+
connectionId: locked.id,
|
|
905
|
+
action: inputFailure.action,
|
|
906
|
+
actorUserId: callbackContext.session.authorizingUserId,
|
|
907
|
+
details: { reason: inputFailure.reason },
|
|
908
|
+
}));
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
if (!await isValidAuthorizer(consumed.session.orgId, consumed.session.authorizingUserId)) {
|
|
912
|
+
await finishFailure({
|
|
913
|
+
status: "needs_reauth",
|
|
914
|
+
action: "mcp_oauth.authorization_rejected",
|
|
915
|
+
reason: "authorizer_no_longer_authorized",
|
|
916
|
+
});
|
|
917
|
+
throw unprocessable("Managed MCP OAuth must be reauthorized by an owner");
|
|
918
|
+
}
|
|
919
|
+
if (input.error) {
|
|
920
|
+
await finishFailure({
|
|
921
|
+
status: "error",
|
|
922
|
+
action: "mcp_oauth.authorization_failed",
|
|
923
|
+
reason: "provider_denied",
|
|
924
|
+
});
|
|
925
|
+
throw unprocessable("Managed MCP OAuth authorization was not completed");
|
|
926
|
+
}
|
|
927
|
+
const authorization = readOAuthAuthorizationSnapshot(consumed.session.statusMetadata, consumed.connection);
|
|
928
|
+
if (consumed.connection.provider === "linear"
|
|
929
|
+
&& authorization.accessMode !== consumed.connection.accessMode) {
|
|
930
|
+
await finishFailure({
|
|
931
|
+
status: "needs_reauth",
|
|
932
|
+
action: "mcp_oauth.authorization_failed",
|
|
933
|
+
reason: "authorization_resource_changed",
|
|
934
|
+
});
|
|
935
|
+
throw unprocessable("Linear access mode changed; start authorization again");
|
|
936
|
+
}
|
|
937
|
+
let grantMaterial;
|
|
938
|
+
try {
|
|
939
|
+
grantMaterial = structuredClone(consumed.sessionMaterial);
|
|
940
|
+
}
|
|
941
|
+
catch {
|
|
942
|
+
await finishFailure({
|
|
943
|
+
status: "error",
|
|
944
|
+
action: "mcp_oauth.authorization_failed",
|
|
945
|
+
reason: "session_credential_unavailable",
|
|
946
|
+
});
|
|
947
|
+
throw unprocessable("Managed MCP OAuth authorization could not be completed");
|
|
948
|
+
}
|
|
949
|
+
async function finishUnpersistedGrantFailure(inputFailure) {
|
|
950
|
+
try {
|
|
951
|
+
await finishFailure(inputFailure);
|
|
952
|
+
}
|
|
953
|
+
finally {
|
|
954
|
+
await bestEffortRevokeMaterial(grantMaterial, Math.min(callbackContext.connection.toolTimeoutMs, 10_000));
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
let scope;
|
|
958
|
+
try {
|
|
959
|
+
const provider = new PersistentMcpOAuthClientProvider({
|
|
960
|
+
redirectUri: consumed.session.redirectUri,
|
|
961
|
+
state: "consumed",
|
|
962
|
+
material: grantMaterial,
|
|
963
|
+
save: async (next) => {
|
|
964
|
+
grantMaterial = structuredClone(next);
|
|
965
|
+
},
|
|
966
|
+
});
|
|
967
|
+
const authResult = await runAuth(provider, {
|
|
968
|
+
serverUrl: authorization.serverUrl,
|
|
969
|
+
authorizationCode: input.code,
|
|
970
|
+
iss: input.iss,
|
|
971
|
+
scope: authorization.scope,
|
|
972
|
+
fetchFn: secureFetch,
|
|
973
|
+
});
|
|
974
|
+
if (authResult !== "AUTHORIZED" || !grantMaterial.tokens?.access_token) {
|
|
975
|
+
throw unprocessable("Managed MCP OAuth token exchange failed");
|
|
976
|
+
}
|
|
977
|
+
scope = await discoverScope({
|
|
978
|
+
connection: consumed.connection,
|
|
979
|
+
material: grantMaterial,
|
|
980
|
+
endpoint: authorization.serverUrl,
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
catch (error) {
|
|
984
|
+
await finishUnpersistedGrantFailure({
|
|
985
|
+
status: isInvalidGrantOAuthError(error) ? "needs_reauth" : "error",
|
|
986
|
+
action: "mcp_oauth.authorization_failed",
|
|
987
|
+
reason: isInvalidGrantOAuthError(error)
|
|
988
|
+
? "invalid_grant"
|
|
989
|
+
: "token_or_scope_exchange_failed",
|
|
990
|
+
});
|
|
991
|
+
if (error instanceof HttpError)
|
|
992
|
+
throw error;
|
|
993
|
+
throw unprocessable("Managed MCP OAuth authorization could not be completed");
|
|
994
|
+
}
|
|
995
|
+
const safeOptions = sanitizeScopeOptions(scope.options);
|
|
996
|
+
const selected = scope.selected ? safeScopeOption(scope.selected) : undefined;
|
|
997
|
+
if (consumed.connection.provider === "supabase" && safeOptions.length === 0) {
|
|
998
|
+
await finishUnpersistedGrantFailure({
|
|
999
|
+
status: "error",
|
|
1000
|
+
action: "mcp_oauth.authorization_failed",
|
|
1001
|
+
reason: "no_selectable_projects",
|
|
1002
|
+
});
|
|
1003
|
+
throw unprocessable("Supabase OAuth did not return any selectable projects");
|
|
1004
|
+
}
|
|
1005
|
+
if (consumed.connection.provider !== "supabase" && !selected) {
|
|
1006
|
+
await finishUnpersistedGrantFailure({
|
|
1007
|
+
status: "error",
|
|
1008
|
+
action: "mcp_oauth.authorization_failed",
|
|
1009
|
+
reason: "workspace_identity_unavailable",
|
|
1010
|
+
});
|
|
1011
|
+
throw unprocessable("Managed MCP OAuth did not identify a provider workspace");
|
|
1012
|
+
}
|
|
1013
|
+
const outcome = await (async () => {
|
|
1014
|
+
try {
|
|
1015
|
+
const grantSecret = await prepareOAuthSecret(consumed.connection.name, "grant", grantMaterial);
|
|
1016
|
+
return await db.transaction(async (tx) => {
|
|
1017
|
+
const now = new Date();
|
|
1018
|
+
await lockManagedMcpOAuthAuthorizer(tx, consumed.session.authorizingUserId);
|
|
1019
|
+
const connection = await tx.select().from(mcpConnections)
|
|
1020
|
+
.where(and(eq(mcpConnections.orgId, consumed.session.orgId), eq(mcpConnections.id, consumed.connection.id)))
|
|
1021
|
+
.for("update")
|
|
1022
|
+
.then((rows) => rows[0] ?? null);
|
|
1023
|
+
if (!connection
|
|
1024
|
+
|| connection.status !== "authorizing"
|
|
1025
|
+
|| connection.lifecycleRevision !== consumed.connectionLifecycleRevision) {
|
|
1026
|
+
return null;
|
|
1027
|
+
}
|
|
1028
|
+
const stillAuthorized = options.deploymentMode === "local_trusted"
|
|
1029
|
+
&& !consumed.session.authorizingUserId
|
|
1030
|
+
? true
|
|
1031
|
+
: Boolean(await tx.select({ id: organizationMemberships.id })
|
|
1032
|
+
.from(organizationMemberships)
|
|
1033
|
+
.where(and(eq(organizationMemberships.orgId, consumed.session.orgId), eq(organizationMemberships.principalType, "user"), eq(organizationMemberships.principalId, consumed.session.authorizingUserId ?? ""), eq(organizationMemberships.status, "active"), eq(organizationMemberships.membershipRole, "owner")))
|
|
1034
|
+
.for("share")
|
|
1035
|
+
.then((rows) => rows[0] ?? null)
|
|
1036
|
+
?? await tx.select({ id: instanceUserRoles.id })
|
|
1037
|
+
.from(instanceUserRoles)
|
|
1038
|
+
.where(and(eq(instanceUserRoles.userId, consumed.session.authorizingUserId ?? ""), eq(instanceUserRoles.role, "instance_admin")))
|
|
1039
|
+
.for("share")
|
|
1040
|
+
.then((rows) => rows[0] ?? null));
|
|
1041
|
+
if (!stillAuthorized) {
|
|
1042
|
+
await tx.update(mcpConnections).set({
|
|
1043
|
+
status: "needs_reauth",
|
|
1044
|
+
enabled: false,
|
|
1045
|
+
disabledAt: now,
|
|
1046
|
+
lifecycleRevision: connection.lifecycleRevision + 1,
|
|
1047
|
+
updatedAt: now,
|
|
1048
|
+
}).where(eq(mcpConnections.id, connection.id));
|
|
1049
|
+
await tx.insert(activityLog).values(oauthActivityValues({
|
|
1050
|
+
orgId: consumed.session.orgId,
|
|
1051
|
+
connectionId: connection.id,
|
|
1052
|
+
action: "mcp_oauth.authorization_rejected",
|
|
1053
|
+
actorUserId: consumed.session.authorizingUserId,
|
|
1054
|
+
details: { reason: "authorizer_no_longer_authorized" },
|
|
1055
|
+
}));
|
|
1056
|
+
return { kind: "reauth" };
|
|
1057
|
+
}
|
|
1058
|
+
await insertOAuthSecret(tx, {
|
|
1059
|
+
orgId: consumed.session.orgId,
|
|
1060
|
+
secret: grantSecret,
|
|
1061
|
+
actorUserId: consumed.session.authorizingUserId,
|
|
1062
|
+
});
|
|
1063
|
+
const existingGrant = await tx.select().from(mcpOAuthGrants)
|
|
1064
|
+
.where(and(eq(mcpOAuthGrants.orgId, consumed.session.orgId), eq(mcpOAuthGrants.connectionId, connection.id)))
|
|
1065
|
+
.for("update")
|
|
1066
|
+
.then((rows) => rows[0] ?? null);
|
|
1067
|
+
const grantValues = {
|
|
1068
|
+
authorizingUserId: consumed.session.authorizingUserId,
|
|
1069
|
+
providerSubject: scope.providerSubject ?? selected?.id ?? null,
|
|
1070
|
+
providerScopes: tokenScopes(grantMaterial.tokens),
|
|
1071
|
+
externalScopeMetadata: selected?.metadata ?? {},
|
|
1072
|
+
credentialSecretId: grantSecret.id,
|
|
1073
|
+
status: "active",
|
|
1074
|
+
statusMetadata: {
|
|
1075
|
+
authorization,
|
|
1076
|
+
...(connection.provider === "supabase"
|
|
1077
|
+
? { scopeOptions: safeOptions }
|
|
1078
|
+
: {}),
|
|
1079
|
+
},
|
|
1080
|
+
expiresAt: tokenExpiry(grantMaterial.tokens, now),
|
|
1081
|
+
lastRefreshedAt: now,
|
|
1082
|
+
refreshLeaseNonce: null,
|
|
1083
|
+
refreshLeaseExpiresAt: null,
|
|
1084
|
+
revokedAt: null,
|
|
1085
|
+
updatedAt: now,
|
|
1086
|
+
};
|
|
1087
|
+
if (existingGrant) {
|
|
1088
|
+
await tx.update(mcpOAuthGrants).set(grantValues)
|
|
1089
|
+
.where(eq(mcpOAuthGrants.id, existingGrant.id));
|
|
1090
|
+
}
|
|
1091
|
+
else {
|
|
1092
|
+
await tx.insert(mcpOAuthGrants).values({
|
|
1093
|
+
orgId: consumed.session.orgId,
|
|
1094
|
+
connectionId: connection.id,
|
|
1095
|
+
...grantValues,
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
if (existingGrant?.credentialSecretId) {
|
|
1099
|
+
await tx.delete(organizationSecrets)
|
|
1100
|
+
.where(eq(organizationSecrets.id, existingGrant.credentialSecretId));
|
|
1101
|
+
}
|
|
1102
|
+
const status = connection.provider === "supabase" ? "selecting_scope" : "active";
|
|
1103
|
+
await tx.update(mcpConnections).set({
|
|
1104
|
+
status,
|
|
1105
|
+
externalScope: selected?.id ?? null,
|
|
1106
|
+
safeConfig: connection.provider === "supabase"
|
|
1107
|
+
? {}
|
|
1108
|
+
: connection.safeConfig,
|
|
1109
|
+
enabled: true,
|
|
1110
|
+
activatedAt: status === "active" ? connection.activatedAt ?? now : null,
|
|
1111
|
+
lifecycleRevision: connection.lifecycleRevision + 1,
|
|
1112
|
+
updatedAt: now,
|
|
1113
|
+
}).where(eq(mcpConnections.id, connection.id));
|
|
1114
|
+
await tx.insert(activityLog).values(oauthActivityValues({
|
|
1115
|
+
orgId: consumed.session.orgId,
|
|
1116
|
+
connectionId: connection.id,
|
|
1117
|
+
action: "mcp_oauth.authorization_completed",
|
|
1118
|
+
actorUserId: consumed.session.authorizingUserId,
|
|
1119
|
+
details: {
|
|
1120
|
+
provider: connection.provider,
|
|
1121
|
+
status,
|
|
1122
|
+
hasExternalScope: Boolean(selected),
|
|
1123
|
+
},
|
|
1124
|
+
}));
|
|
1125
|
+
return {
|
|
1126
|
+
kind: "completed",
|
|
1127
|
+
connectionId: connection.id,
|
|
1128
|
+
orgId: consumed.session.orgId,
|
|
1129
|
+
authorizingUserId: consumed.session.authorizingUserId,
|
|
1130
|
+
status,
|
|
1131
|
+
};
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
catch (error) {
|
|
1135
|
+
await finishUnpersistedGrantFailure({
|
|
1136
|
+
status: isInvalidGrantOAuthError(error) ? "needs_reauth" : "error",
|
|
1137
|
+
action: "mcp_oauth.authorization_failed",
|
|
1138
|
+
reason: "grant_persistence_failed",
|
|
1139
|
+
});
|
|
1140
|
+
if (error instanceof HttpError)
|
|
1141
|
+
throw error;
|
|
1142
|
+
throw unprocessable("Managed MCP OAuth authorization could not be completed");
|
|
1143
|
+
}
|
|
1144
|
+
})();
|
|
1145
|
+
if (!outcome) {
|
|
1146
|
+
await bestEffortRevokeMaterial(grantMaterial, Math.min(consumed.connection.toolTimeoutMs, 10_000));
|
|
1147
|
+
throw unprocessable("Managed MCP OAuth connection changed; start authorization again");
|
|
1148
|
+
}
|
|
1149
|
+
if (outcome.kind === "reauth") {
|
|
1150
|
+
await bestEffortRevokeMaterial(grantMaterial, Math.min(consumed.connection.toolTimeoutMs, 10_000));
|
|
1151
|
+
throw unprocessable("Managed MCP OAuth must be reauthorized by an owner");
|
|
1152
|
+
}
|
|
1153
|
+
if (outcome.status === "active" && options.refreshConnectionTools) {
|
|
1154
|
+
await options.refreshConnectionTools(outcome.orgId, outcome.connectionId, { userId: outcome.authorizingUserId, agentId: null });
|
|
1155
|
+
}
|
|
1156
|
+
return { connectionId: outcome.connectionId, status: outcome.status };
|
|
1157
|
+
}
|
|
1158
|
+
async function getGrantSummary(orgId, connectionId) {
|
|
1159
|
+
return publicGrant(await requireUsableGrant(orgId, connectionId));
|
|
1160
|
+
}
|
|
1161
|
+
async function listScopeOptions(orgId, connectionId) {
|
|
1162
|
+
const connection = await findConnection(orgId, connectionId);
|
|
1163
|
+
if (connection.provider !== "supabase" || connection.status !== "selecting_scope") {
|
|
1164
|
+
throw unprocessable("Managed MCP connection does not have pending scope selection");
|
|
1165
|
+
}
|
|
1166
|
+
const grant = await requireUsableGrant(orgId, connectionId);
|
|
1167
|
+
const optionsValue = grant.statusMetadata.scopeOptions;
|
|
1168
|
+
if (!Array.isArray(optionsValue))
|
|
1169
|
+
return [];
|
|
1170
|
+
return optionsValue
|
|
1171
|
+
.filter((value) => (isRecord(value)
|
|
1172
|
+
&& typeof value.id === "string"
|
|
1173
|
+
&& typeof value.displayName === "string"
|
|
1174
|
+
&& isRecord(value.metadata)))
|
|
1175
|
+
.slice(0, MAX_SCOPE_OPTIONS)
|
|
1176
|
+
.map(safeScopeOption);
|
|
1177
|
+
}
|
|
1178
|
+
async function selectScope(orgId, connectionId, rawSelection, actor) {
|
|
1179
|
+
const selection = mcpScopeSelectionSchema.parse(rawSelection);
|
|
1180
|
+
if (selection.connectionId !== connectionId) {
|
|
1181
|
+
throw unprocessable("Managed MCP scope selection does not match its connection");
|
|
1182
|
+
}
|
|
1183
|
+
if (selection.accessMode !== "read_only" && selection.accessMode !== "read_write") {
|
|
1184
|
+
throw unprocessable("Supabase scope requires read_only or read_write access");
|
|
1185
|
+
}
|
|
1186
|
+
const now = new Date();
|
|
1187
|
+
const result = await db.transaction(async (tx) => {
|
|
1188
|
+
const connection = await tx.select().from(mcpConnections)
|
|
1189
|
+
.where(and(eq(mcpConnections.orgId, orgId), eq(mcpConnections.id, connectionId)))
|
|
1190
|
+
.for("update")
|
|
1191
|
+
.then((rows) => rows[0] ?? null);
|
|
1192
|
+
if (!connection || connection.provider !== "supabase" || connection.status !== "selecting_scope") {
|
|
1193
|
+
throw unprocessable("Managed MCP connection does not have pending scope selection");
|
|
1194
|
+
}
|
|
1195
|
+
const grant = await tx.select().from(mcpOAuthGrants)
|
|
1196
|
+
.where(and(eq(mcpOAuthGrants.orgId, orgId), eq(mcpOAuthGrants.connectionId, connectionId)))
|
|
1197
|
+
.for("update")
|
|
1198
|
+
.then((rows) => rows[0] ?? null);
|
|
1199
|
+
if (!grant || grant.status !== "active" || !grant.credentialSecretId) {
|
|
1200
|
+
throw unprocessable("Managed MCP OAuth authorization must be reconnected");
|
|
1201
|
+
}
|
|
1202
|
+
const authorizerValid = options.deploymentMode === "local_trusted"
|
|
1203
|
+
&& !grant.authorizingUserId
|
|
1204
|
+
? true
|
|
1205
|
+
: Boolean(await tx.select({ id: organizationMemberships.id })
|
|
1206
|
+
.from(organizationMemberships)
|
|
1207
|
+
.where(and(eq(organizationMemberships.orgId, orgId), eq(organizationMemberships.principalType, "user"), eq(organizationMemberships.principalId, grant.authorizingUserId ?? ""), eq(organizationMemberships.status, "active"), eq(organizationMemberships.membershipRole, "owner")))
|
|
1208
|
+
.for("share")
|
|
1209
|
+
.then((rows) => rows[0] ?? null)
|
|
1210
|
+
?? await tx.select({ id: instanceUserRoles.id })
|
|
1211
|
+
.from(instanceUserRoles)
|
|
1212
|
+
.where(and(eq(instanceUserRoles.userId, grant.authorizingUserId ?? ""), eq(instanceUserRoles.role, "instance_admin")))
|
|
1213
|
+
.for("share")
|
|
1214
|
+
.then((rows) => rows[0] ?? null));
|
|
1215
|
+
if (!authorizerValid) {
|
|
1216
|
+
await transitionGrantToNeedsReauth(tx, grant, "authorizer_no_longer_authorized");
|
|
1217
|
+
return { kind: "reauth" };
|
|
1218
|
+
}
|
|
1219
|
+
const optionsValue = grant.statusMetadata.scopeOptions;
|
|
1220
|
+
const optionsList = Array.isArray(optionsValue)
|
|
1221
|
+
? optionsValue
|
|
1222
|
+
.filter((value) => (isRecord(value)
|
|
1223
|
+
&& typeof value.id === "string"
|
|
1224
|
+
&& typeof value.displayName === "string"
|
|
1225
|
+
&& isRecord(value.metadata)))
|
|
1226
|
+
.slice(0, MAX_SCOPE_OPTIONS)
|
|
1227
|
+
.map(safeScopeOption)
|
|
1228
|
+
: [];
|
|
1229
|
+
const selected = optionsList.find((option) => option.id === selection.externalScope);
|
|
1230
|
+
if (!selected)
|
|
1231
|
+
throw unprocessable("Selected Supabase project was not discovered");
|
|
1232
|
+
const { scopeOptions: _consumedScopeOptions, ...grantAuthorizationMetadata } = grant.statusMetadata;
|
|
1233
|
+
const row = await tx.update(mcpConnections).set({
|
|
1234
|
+
externalScope: selected.id,
|
|
1235
|
+
accessMode: selection.accessMode,
|
|
1236
|
+
safeConfig: {
|
|
1237
|
+
featureGroups: MCP_PROVIDER_REGISTRY.supabase.featureGroups,
|
|
1238
|
+
},
|
|
1239
|
+
status: "active",
|
|
1240
|
+
enabled: true,
|
|
1241
|
+
activatedAt: connection.activatedAt ?? now,
|
|
1242
|
+
lifecycleRevision: connection.lifecycleRevision + 1,
|
|
1243
|
+
updatedAt: now,
|
|
1244
|
+
}).where(and(eq(mcpConnections.orgId, orgId), eq(mcpConnections.id, connectionId))).returning().then((rows) => rows[0]);
|
|
1245
|
+
const updatedGrant = await tx.update(mcpOAuthGrants).set({
|
|
1246
|
+
externalScopeMetadata: selected.metadata,
|
|
1247
|
+
statusMetadata: grantAuthorizationMetadata,
|
|
1248
|
+
updatedAt: now,
|
|
1249
|
+
}).where(and(eq(mcpOAuthGrants.orgId, orgId), eq(mcpOAuthGrants.connectionId, connectionId), eq(mcpOAuthGrants.status, "active"))).returning({ id: mcpOAuthGrants.id });
|
|
1250
|
+
if (updatedGrant.length !== 1) {
|
|
1251
|
+
throw unprocessable("Managed MCP OAuth authorization changed during scope selection");
|
|
1252
|
+
}
|
|
1253
|
+
await tx.insert(activityLog).values(oauthActivityValues({
|
|
1254
|
+
orgId,
|
|
1255
|
+
connectionId,
|
|
1256
|
+
action: "mcp_oauth.scope_selected",
|
|
1257
|
+
actorUserId: actor.userId,
|
|
1258
|
+
details: {
|
|
1259
|
+
provider: "supabase",
|
|
1260
|
+
accessMode: selection.accessMode,
|
|
1261
|
+
projectSelected: true,
|
|
1262
|
+
},
|
|
1263
|
+
}));
|
|
1264
|
+
return { kind: "selected", row };
|
|
1265
|
+
});
|
|
1266
|
+
if (result.kind === "reauth") {
|
|
1267
|
+
throw unprocessable("Managed MCP OAuth authorization must be reconnected by an owner");
|
|
1268
|
+
}
|
|
1269
|
+
const updated = result.row;
|
|
1270
|
+
if (options.refreshConnectionTools) {
|
|
1271
|
+
await options.refreshConnectionTools(orgId, connectionId, {
|
|
1272
|
+
userId: actor.userId,
|
|
1273
|
+
agentId: null,
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
return publicConnection(updated);
|
|
1277
|
+
}
|
|
1278
|
+
async function revoke(orgId, connectionId, actor, reason = "connection_disconnect") {
|
|
1279
|
+
const existing = await findConnection(orgId, connectionId);
|
|
1280
|
+
assertCurated(existing);
|
|
1281
|
+
const outcome = await db.transaction(async (tx) => {
|
|
1282
|
+
const now = new Date();
|
|
1283
|
+
const connection = await tx.select().from(mcpConnections)
|
|
1284
|
+
.where(and(eq(mcpConnections.orgId, orgId), eq(mcpConnections.id, connectionId)))
|
|
1285
|
+
.for("update")
|
|
1286
|
+
.then((rows) => rows[0] ?? null);
|
|
1287
|
+
const grant = await tx.select().from(mcpOAuthGrants)
|
|
1288
|
+
.where(and(eq(mcpOAuthGrants.orgId, orgId), eq(mcpOAuthGrants.connectionId, connectionId)))
|
|
1289
|
+
.for("update")
|
|
1290
|
+
.then((rows) => rows[0] ?? null);
|
|
1291
|
+
if (!connection)
|
|
1292
|
+
throw notFound("MCP connection not found");
|
|
1293
|
+
assertCurated(connection);
|
|
1294
|
+
const pendingSessions = await tx.select().from(mcpOAuthSessions)
|
|
1295
|
+
.where(and(eq(mcpOAuthSessions.orgId, orgId), eq(mcpOAuthSessions.connectionId, connectionId), eq(mcpOAuthSessions.status, "authorizing"), isNull(mcpOAuthSessions.consumedAt)))
|
|
1296
|
+
.for("update");
|
|
1297
|
+
let material = null;
|
|
1298
|
+
if (grant?.credentialSecretId) {
|
|
1299
|
+
try {
|
|
1300
|
+
material = (await resolveLockedOAuthSecret(tx, orgId, grant.credentialSecretId))?.material ?? null;
|
|
1301
|
+
}
|
|
1302
|
+
catch {
|
|
1303
|
+
// Local revocation and credential deletion must not depend on readable provider material.
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
if (grant) {
|
|
1307
|
+
await tx.update(mcpOAuthGrants).set({
|
|
1308
|
+
status: "revoked",
|
|
1309
|
+
statusMetadata: { reason },
|
|
1310
|
+
credentialSecretId: null,
|
|
1311
|
+
refreshLeaseNonce: null,
|
|
1312
|
+
refreshLeaseExpiresAt: null,
|
|
1313
|
+
revokedAt: now,
|
|
1314
|
+
updatedAt: now,
|
|
1315
|
+
}).where(eq(mcpOAuthGrants.id, grant.id));
|
|
1316
|
+
if (grant.credentialSecretId) {
|
|
1317
|
+
await tx.delete(organizationSecrets)
|
|
1318
|
+
.where(eq(organizationSecrets.id, grant.credentialSecretId));
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
if (pendingSessions.length > 0) {
|
|
1322
|
+
await tx.update(mcpOAuthSessions).set({
|
|
1323
|
+
status: "expired",
|
|
1324
|
+
statusMetadata: { reason },
|
|
1325
|
+
credentialSecretId: null,
|
|
1326
|
+
consumedAt: now,
|
|
1327
|
+
}).where(inArray(mcpOAuthSessions.id, pendingSessions.map((session) => session.id)));
|
|
1328
|
+
const pendingSecretIds = pendingSessions
|
|
1329
|
+
.map((session) => session.credentialSecretId)
|
|
1330
|
+
.filter((id) => Boolean(id));
|
|
1331
|
+
if (pendingSecretIds.length > 0) {
|
|
1332
|
+
await tx.delete(organizationSecrets).where(and(eq(organizationSecrets.orgId, orgId), inArray(organizationSecrets.id, pendingSecretIds)));
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
const updated = await tx.update(mcpConnections).set({
|
|
1336
|
+
status: "revoked",
|
|
1337
|
+
enabled: false,
|
|
1338
|
+
disabledAt: now,
|
|
1339
|
+
revokedAt: now,
|
|
1340
|
+
lifecycleRevision: connection.lifecycleRevision + 1,
|
|
1341
|
+
updatedAt: now,
|
|
1342
|
+
}).where(and(eq(mcpConnections.orgId, orgId), eq(mcpConnections.id, connectionId))).returning().then((rows) => rows[0]);
|
|
1343
|
+
await tx.insert(activityLog).values(oauthActivityValues({
|
|
1344
|
+
orgId,
|
|
1345
|
+
connectionId,
|
|
1346
|
+
action: "mcp_oauth.revoked",
|
|
1347
|
+
actorUserId: actor.userId,
|
|
1348
|
+
details: {
|
|
1349
|
+
provider: connection.provider,
|
|
1350
|
+
reason,
|
|
1351
|
+
upstreamRevocationAvailable: Boolean(oauthRevocationEndpoint(material)),
|
|
1352
|
+
},
|
|
1353
|
+
}));
|
|
1354
|
+
return {
|
|
1355
|
+
connection: updated,
|
|
1356
|
+
material,
|
|
1357
|
+
};
|
|
1358
|
+
});
|
|
1359
|
+
await bestEffortRevokeMaterial(outcome.material, Math.min(existing.toolTimeoutMs, 10_000));
|
|
1360
|
+
return publicConnection(outcome.connection);
|
|
1361
|
+
}
|
|
1362
|
+
async function transitionGrantToNeedsReauth(tx, grant, reason) {
|
|
1363
|
+
const now = new Date();
|
|
1364
|
+
await tx.update(mcpOAuthGrants).set({
|
|
1365
|
+
status: "needs_reauth",
|
|
1366
|
+
statusMetadata: { reason },
|
|
1367
|
+
credentialSecretId: null,
|
|
1368
|
+
refreshLeaseNonce: null,
|
|
1369
|
+
refreshLeaseExpiresAt: null,
|
|
1370
|
+
updatedAt: now,
|
|
1371
|
+
}).where(and(eq(mcpOAuthGrants.id, grant.id), eq(mcpOAuthGrants.status, "active")));
|
|
1372
|
+
await tx.update(mcpConnections).set({
|
|
1373
|
+
status: "needs_reauth",
|
|
1374
|
+
enabled: false,
|
|
1375
|
+
disabledAt: now,
|
|
1376
|
+
lifecycleRevision: sql `${mcpConnections.lifecycleRevision} + 1`,
|
|
1377
|
+
updatedAt: now,
|
|
1378
|
+
}).where(and(eq(mcpConnections.orgId, grant.orgId), eq(mcpConnections.id, grant.connectionId)));
|
|
1379
|
+
if (grant.credentialSecretId) {
|
|
1380
|
+
await tx.delete(organizationSecrets)
|
|
1381
|
+
.where(eq(organizationSecrets.id, grant.credentialSecretId));
|
|
1382
|
+
}
|
|
1383
|
+
await tx.insert(activityLog).values(oauthActivityValues({
|
|
1384
|
+
orgId: grant.orgId,
|
|
1385
|
+
connectionId: grant.connectionId,
|
|
1386
|
+
action: "mcp_oauth.reauthorization_required",
|
|
1387
|
+
actorUserId: grant.authorizingUserId,
|
|
1388
|
+
details: { reason },
|
|
1389
|
+
}));
|
|
1390
|
+
}
|
|
1391
|
+
async function performGrantRefresh(orgId, connectionId) {
|
|
1392
|
+
const baseline = await db.select({
|
|
1393
|
+
credentialSecretId: mcpOAuthGrants.credentialSecretId,
|
|
1394
|
+
latestVersion: organizationSecrets.latestVersion,
|
|
1395
|
+
}).from(mcpOAuthGrants)
|
|
1396
|
+
.innerJoin(organizationSecrets, eq(organizationSecrets.id, mcpOAuthGrants.credentialSecretId))
|
|
1397
|
+
.where(and(eq(mcpOAuthGrants.orgId, orgId), eq(mcpOAuthGrants.connectionId, connectionId), eq(mcpOAuthGrants.status, "active"), eq(organizationSecrets.orgId, orgId), eq(organizationSecrets.purpose, "managed_mcp_oauth")))
|
|
1398
|
+
.then((rows) => rows[0] ?? null);
|
|
1399
|
+
if (!baseline?.credentialSecretId) {
|
|
1400
|
+
throw unprocessable("Managed MCP OAuth authorization must be reconnected");
|
|
1401
|
+
}
|
|
1402
|
+
const acquisitionStartedAt = Date.now();
|
|
1403
|
+
let claimed = null;
|
|
1404
|
+
while (!claimed) {
|
|
1405
|
+
const result = await db.transaction(async (tx) => {
|
|
1406
|
+
const databaseClock = await tx.execute(sql `
|
|
1407
|
+
select clock_timestamp()::text as value
|
|
1408
|
+
`);
|
|
1409
|
+
const now = new Date(databaseClock[0].value);
|
|
1410
|
+
const connection = await tx.select().from(mcpConnections)
|
|
1411
|
+
.where(and(eq(mcpConnections.orgId, orgId), eq(mcpConnections.id, connectionId)))
|
|
1412
|
+
.for("update")
|
|
1413
|
+
.then((rows) => rows[0] ?? null);
|
|
1414
|
+
const grant = await tx.select().from(mcpOAuthGrants)
|
|
1415
|
+
.where(and(eq(mcpOAuthGrants.orgId, orgId), eq(mcpOAuthGrants.connectionId, connectionId)))
|
|
1416
|
+
.for("update")
|
|
1417
|
+
.then((rows) => rows[0] ?? null);
|
|
1418
|
+
if (!connection
|
|
1419
|
+
|| !grant
|
|
1420
|
+
|| grant.status !== "active"
|
|
1421
|
+
|| !grant.credentialSecretId
|
|
1422
|
+
|| connection.status !== "active"
|
|
1423
|
+
|| !connection.enabled) {
|
|
1424
|
+
return { kind: "needs_reauth" };
|
|
1425
|
+
}
|
|
1426
|
+
assertCurated(connection);
|
|
1427
|
+
const authorization = readOAuthAuthorizationSnapshot(grant.statusMetadata, connection);
|
|
1428
|
+
if (connection.provider === "linear"
|
|
1429
|
+
&& authorization.accessMode !== connection.accessMode) {
|
|
1430
|
+
await transitionGrantToNeedsReauth(tx, grant, "authorization_resource_changed");
|
|
1431
|
+
return { kind: "needs_reauth" };
|
|
1432
|
+
}
|
|
1433
|
+
if (!await isValidAuthorizerInTransaction(tx, orgId, grant.authorizingUserId)) {
|
|
1434
|
+
await transitionGrantToNeedsReauth(tx, grant, "authorizer_no_longer_authorized");
|
|
1435
|
+
return { kind: "needs_reauth" };
|
|
1436
|
+
}
|
|
1437
|
+
const resolved = await resolveLockedOAuthSecret(tx, orgId, grant.credentialSecretId);
|
|
1438
|
+
if (!resolved) {
|
|
1439
|
+
await transitionGrantToNeedsReauth(tx, grant, "credential_unavailable");
|
|
1440
|
+
return { kind: "needs_reauth" };
|
|
1441
|
+
}
|
|
1442
|
+
if (grant.credentialSecretId !== baseline.credentialSecretId
|
|
1443
|
+
|| resolved.secret.latestVersion !== baseline.latestVersion) {
|
|
1444
|
+
return { kind: "refreshed" };
|
|
1445
|
+
}
|
|
1446
|
+
if (grant.refreshLeaseNonce
|
|
1447
|
+
&& grant.refreshLeaseExpiresAt
|
|
1448
|
+
&& grant.refreshLeaseExpiresAt > now) {
|
|
1449
|
+
return {
|
|
1450
|
+
kind: "leased",
|
|
1451
|
+
waitMs: Math.min(50, Math.max(5, grant.refreshLeaseExpiresAt.getTime() - now.getTime())),
|
|
1452
|
+
timeoutMs: Math.min(connection.toolTimeoutMs, 10_000),
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
const timeoutMs = Math.min(connection.toolTimeoutMs, 10_000);
|
|
1456
|
+
const nonce = randomUUID();
|
|
1457
|
+
const leaseExpiresAt = new Date(now.getTime() + timeoutMs + 1_000);
|
|
1458
|
+
await tx.update(mcpOAuthGrants).set({
|
|
1459
|
+
refreshLeaseNonce: nonce,
|
|
1460
|
+
refreshLeaseExpiresAt: leaseExpiresAt,
|
|
1461
|
+
updatedAt: now,
|
|
1462
|
+
}).where(eq(mcpOAuthGrants.id, grant.id));
|
|
1463
|
+
return {
|
|
1464
|
+
kind: "claimed",
|
|
1465
|
+
nonce,
|
|
1466
|
+
timeoutMs,
|
|
1467
|
+
connection,
|
|
1468
|
+
grant: { ...grant, credentialSecretId: grant.credentialSecretId },
|
|
1469
|
+
secret: resolved.secret,
|
|
1470
|
+
material: resolved.material,
|
|
1471
|
+
authorization,
|
|
1472
|
+
};
|
|
1473
|
+
});
|
|
1474
|
+
if (result.kind === "needs_reauth") {
|
|
1475
|
+
throw unprocessable("Managed MCP OAuth authorization must be reconnected");
|
|
1476
|
+
}
|
|
1477
|
+
if (result.kind === "refreshed")
|
|
1478
|
+
return;
|
|
1479
|
+
if (result.kind === "leased") {
|
|
1480
|
+
if (Date.now() - acquisitionStartedAt > result.timeoutMs + 2_000) {
|
|
1481
|
+
throw unprocessable("Managed MCP OAuth token refresh timed out");
|
|
1482
|
+
}
|
|
1483
|
+
await waitForLease(result.waitMs);
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
claimed = result;
|
|
1487
|
+
}
|
|
1488
|
+
let material = claimed.material;
|
|
1489
|
+
const provider = new PersistentMcpOAuthClientProvider({
|
|
1490
|
+
redirectUri: resolveMcpOAuthRedirectUri({
|
|
1491
|
+
deploymentMode: options.deploymentMode,
|
|
1492
|
+
serverPort: options.serverPort,
|
|
1493
|
+
authPublicBaseUrl: options.authPublicBaseUrl,
|
|
1494
|
+
}),
|
|
1495
|
+
state: "refresh",
|
|
1496
|
+
material,
|
|
1497
|
+
save: async (next) => {
|
|
1498
|
+
material = structuredClone(next);
|
|
1499
|
+
},
|
|
1500
|
+
});
|
|
1501
|
+
let authResult = null;
|
|
1502
|
+
let refreshError;
|
|
1503
|
+
const refreshAbortController = new AbortController();
|
|
1504
|
+
const refreshAbortTimer = setTimeout(() => {
|
|
1505
|
+
refreshAbortController.abort(new ManagedMcpOAuthRefreshTimeoutError());
|
|
1506
|
+
}, claimed.timeoutMs);
|
|
1507
|
+
try {
|
|
1508
|
+
authResult = await withHardTimeout(runAuth(provider, {
|
|
1509
|
+
serverUrl: claimed.authorization.serverUrl,
|
|
1510
|
+
scope: claimed.authorization.scope,
|
|
1511
|
+
fetchFn: (input, init) => secureFetch(input, {
|
|
1512
|
+
...init,
|
|
1513
|
+
signal: init?.signal
|
|
1514
|
+
? AbortSignal.any([init.signal, refreshAbortController.signal])
|
|
1515
|
+
: refreshAbortController.signal,
|
|
1516
|
+
}),
|
|
1517
|
+
}), claimed.timeoutMs);
|
|
1518
|
+
}
|
|
1519
|
+
catch (error) {
|
|
1520
|
+
refreshError = error;
|
|
1521
|
+
}
|
|
1522
|
+
finally {
|
|
1523
|
+
clearTimeout(refreshAbortTimer);
|
|
1524
|
+
}
|
|
1525
|
+
const invalidGrant = (isInvalidGrantOAuthError(refreshError)
|
|
1526
|
+
|| (refreshError === undefined
|
|
1527
|
+
&& (authResult !== "AUTHORIZED" || !material.tokens?.access_token)));
|
|
1528
|
+
const prepared = !refreshError && !invalidGrant
|
|
1529
|
+
? await getSecretProvider("local_encrypted").createVersion({
|
|
1530
|
+
value: JSON.stringify(material),
|
|
1531
|
+
externalRef: claimed.secret.externalRef,
|
|
1532
|
+
})
|
|
1533
|
+
: null;
|
|
1534
|
+
const outcome = await db.transaction(async (tx) => {
|
|
1535
|
+
const connection = await tx.select().from(mcpConnections)
|
|
1536
|
+
.where(and(eq(mcpConnections.orgId, orgId), eq(mcpConnections.id, connectionId)))
|
|
1537
|
+
.for("update")
|
|
1538
|
+
.then((rows) => rows[0] ?? null);
|
|
1539
|
+
const grant = await tx.select().from(mcpOAuthGrants)
|
|
1540
|
+
.where(and(eq(mcpOAuthGrants.orgId, orgId), eq(mcpOAuthGrants.connectionId, connectionId)))
|
|
1541
|
+
.for("update")
|
|
1542
|
+
.then((rows) => rows[0] ?? null);
|
|
1543
|
+
if (!connection
|
|
1544
|
+
|| !grant
|
|
1545
|
+
|| connection.status !== "active"
|
|
1546
|
+
|| !connection.enabled
|
|
1547
|
+
|| grant.status !== "active"
|
|
1548
|
+
|| !grant.credentialSecretId) {
|
|
1549
|
+
return { kind: "invalidated" };
|
|
1550
|
+
}
|
|
1551
|
+
if (grant.id !== claimed.grant.id
|
|
1552
|
+
|| grant.credentialSecretId !== claimed.secret.id
|
|
1553
|
+
|| grant.refreshLeaseNonce !== claimed.nonce) {
|
|
1554
|
+
return { kind: "stale" };
|
|
1555
|
+
}
|
|
1556
|
+
const resolved = await resolveLockedOAuthSecret(tx, orgId, grant.credentialSecretId);
|
|
1557
|
+
if (!resolved || resolved.secret.latestVersion !== claimed.secret.latestVersion) {
|
|
1558
|
+
await tx.update(mcpOAuthGrants).set({
|
|
1559
|
+
refreshLeaseNonce: null,
|
|
1560
|
+
refreshLeaseExpiresAt: null,
|
|
1561
|
+
updatedAt: new Date(),
|
|
1562
|
+
}).where(and(eq(mcpOAuthGrants.id, grant.id), eq(mcpOAuthGrants.refreshLeaseNonce, claimed.nonce)));
|
|
1563
|
+
return { kind: "stale" };
|
|
1564
|
+
}
|
|
1565
|
+
if (invalidGrant) {
|
|
1566
|
+
await transitionGrantToNeedsReauth(tx, grant, "invalid_grant");
|
|
1567
|
+
return { kind: "needs_reauth" };
|
|
1568
|
+
}
|
|
1569
|
+
if (refreshError || !prepared) {
|
|
1570
|
+
await tx.update(mcpOAuthGrants).set({
|
|
1571
|
+
refreshLeaseNonce: null,
|
|
1572
|
+
refreshLeaseExpiresAt: null,
|
|
1573
|
+
updatedAt: new Date(),
|
|
1574
|
+
}).where(and(eq(mcpOAuthGrants.id, grant.id), eq(mcpOAuthGrants.refreshLeaseNonce, claimed.nonce)));
|
|
1575
|
+
return { kind: "failed" };
|
|
1576
|
+
}
|
|
1577
|
+
const nextVersion = resolved.secret.latestVersion + 1;
|
|
1578
|
+
await tx.insert(organizationSecretVersions).values({
|
|
1579
|
+
secretId: resolved.secret.id,
|
|
1580
|
+
version: nextVersion,
|
|
1581
|
+
material: prepared.material,
|
|
1582
|
+
valueSha256: prepared.valueSha256,
|
|
1583
|
+
createdByUserId: grant.authorizingUserId,
|
|
1584
|
+
});
|
|
1585
|
+
const secretUpdated = await tx.update(organizationSecrets).set({
|
|
1586
|
+
latestVersion: nextVersion,
|
|
1587
|
+
externalRef: prepared.externalRef,
|
|
1588
|
+
updatedAt: new Date(),
|
|
1589
|
+
}).where(and(eq(organizationSecrets.id, resolved.secret.id), eq(organizationSecrets.latestVersion, resolved.secret.latestVersion))).returning({ id: organizationSecrets.id });
|
|
1590
|
+
if (secretUpdated.length !== 1) {
|
|
1591
|
+
throw new Error("Managed MCP OAuth secret version changed during refresh");
|
|
1592
|
+
}
|
|
1593
|
+
const now = new Date();
|
|
1594
|
+
await tx.update(mcpOAuthGrants).set({
|
|
1595
|
+
providerScopes: tokenScopes(material.tokens),
|
|
1596
|
+
expiresAt: tokenExpiry(material.tokens, now),
|
|
1597
|
+
lastRefreshedAt: now,
|
|
1598
|
+
refreshLeaseNonce: null,
|
|
1599
|
+
refreshLeaseExpiresAt: null,
|
|
1600
|
+
statusMetadata: {},
|
|
1601
|
+
updatedAt: now,
|
|
1602
|
+
}).where(and(eq(mcpOAuthGrants.id, grant.id), eq(mcpOAuthGrants.status, "active"), eq(mcpOAuthGrants.refreshLeaseNonce, claimed.nonce)));
|
|
1603
|
+
await tx.insert(activityLog).values(oauthActivityValues({
|
|
1604
|
+
orgId,
|
|
1605
|
+
connectionId,
|
|
1606
|
+
action: "mcp_oauth.token_refreshed",
|
|
1607
|
+
actorUserId: grant.authorizingUserId,
|
|
1608
|
+
details: { rotated: true },
|
|
1609
|
+
}));
|
|
1610
|
+
return { kind: "refreshed" };
|
|
1611
|
+
});
|
|
1612
|
+
if (outcome.kind === "refreshed")
|
|
1613
|
+
return;
|
|
1614
|
+
if (outcome.kind === "invalidated") {
|
|
1615
|
+
await bestEffortRevokeMaterial(material, claimed.timeoutMs);
|
|
1616
|
+
throw unprocessable("Managed MCP OAuth authorization changed during token refresh");
|
|
1617
|
+
}
|
|
1618
|
+
if (outcome.kind === "needs_reauth") {
|
|
1619
|
+
throw unprocessable("Managed MCP OAuth authorization must be reconnected");
|
|
1620
|
+
}
|
|
1621
|
+
if (outcome.kind === "failed") {
|
|
1622
|
+
throw unprocessable("Managed MCP OAuth token refresh failed");
|
|
1623
|
+
}
|
|
1624
|
+
throw unprocessable("Managed MCP OAuth authorization changed during token refresh");
|
|
1625
|
+
}
|
|
1626
|
+
function createCredential(orgId, connectionId) {
|
|
1627
|
+
return {
|
|
1628
|
+
async token() {
|
|
1629
|
+
return resolveUsableAccessToken(orgId, connectionId);
|
|
1630
|
+
},
|
|
1631
|
+
async refresh() {
|
|
1632
|
+
const key = `${orgId}:${connectionId}`;
|
|
1633
|
+
const existing = grantRefreshSingleFlights.get(key);
|
|
1634
|
+
if (existing)
|
|
1635
|
+
return existing;
|
|
1636
|
+
let refreshPromise;
|
|
1637
|
+
refreshPromise = (async () => {
|
|
1638
|
+
await performGrantRefresh(orgId, connectionId);
|
|
1639
|
+
})().finally(() => {
|
|
1640
|
+
if (grantRefreshSingleFlights.get(key) === refreshPromise) {
|
|
1641
|
+
grantRefreshSingleFlights.delete(key);
|
|
1642
|
+
}
|
|
1643
|
+
});
|
|
1644
|
+
grantRefreshSingleFlights.set(key, refreshPromise);
|
|
1645
|
+
return refreshPromise;
|
|
1646
|
+
},
|
|
1647
|
+
async markNeedsReauth() {
|
|
1648
|
+
await db.transaction(async (tx) => {
|
|
1649
|
+
const connection = await tx.select({ id: mcpConnections.id }).from(mcpConnections)
|
|
1650
|
+
.where(and(eq(mcpConnections.orgId, orgId), eq(mcpConnections.id, connectionId)))
|
|
1651
|
+
.for("update")
|
|
1652
|
+
.then((rows) => rows[0] ?? null);
|
|
1653
|
+
if (!connection)
|
|
1654
|
+
return;
|
|
1655
|
+
const locked = await tx.select().from(mcpOAuthGrants)
|
|
1656
|
+
.where(and(eq(mcpOAuthGrants.orgId, orgId), eq(mcpOAuthGrants.connectionId, connectionId)))
|
|
1657
|
+
.for("update")
|
|
1658
|
+
.then((rows) => rows[0] ?? null);
|
|
1659
|
+
if (locked?.status === "active") {
|
|
1660
|
+
await transitionGrantToNeedsReauth(tx, locked, "upstream_unauthorized");
|
|
1661
|
+
}
|
|
1662
|
+
});
|
|
1663
|
+
},
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
function resolveProviderEndpoint(row) {
|
|
1667
|
+
assertCurated(row);
|
|
1668
|
+
return resolveCuratedMcpEndpoint({
|
|
1669
|
+
provider: row.provider,
|
|
1670
|
+
accessMode: row.accessMode,
|
|
1671
|
+
externalScope: row.externalScope,
|
|
1672
|
+
}).href;
|
|
1673
|
+
}
|
|
1674
|
+
return {
|
|
1675
|
+
start,
|
|
1676
|
+
callback,
|
|
1677
|
+
getGrantSummary,
|
|
1678
|
+
listScopeOptions,
|
|
1679
|
+
selectScope,
|
|
1680
|
+
revoke,
|
|
1681
|
+
createCredential,
|
|
1682
|
+
requireUsableGrant,
|
|
1683
|
+
resolveProviderEndpoint,
|
|
1684
|
+
cleanupExpiredSessions,
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
export function isInvalidGrantOAuthError(error) {
|
|
1688
|
+
return error instanceof OAuthError && error.code === OAuthErrorCode.InvalidGrant;
|
|
1689
|
+
}
|
|
1690
|
+
//# sourceMappingURL=oauth.js.map
|