skybridge 0.0.0-dev.e9a7859 → 0.0.0-dev.e9ac102
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -14
- package/dist/cli/build-helpers.d.ts +6 -2
- package/dist/cli/build-helpers.js +39 -14
- package/dist/cli/build-helpers.js.map +1 -1
- package/dist/cli/build-helpers.test.js +73 -5
- package/dist/cli/build-helpers.test.js.map +1 -1
- package/dist/cli/build-steps.d.ts +2 -0
- package/dist/cli/build-steps.js +73 -0
- package/dist/cli/build-steps.js.map +1 -0
- package/dist/cli/build-steps.test.js +52 -0
- package/dist/cli/build-steps.test.js.map +1 -0
- package/dist/cli/detect-port.js +0 -1
- package/dist/cli/detect-port.js.map +1 -1
- package/dist/cli/header.d.ts +1 -1
- package/dist/cli/resolve-skybridge-config.d.ts +5 -0
- package/dist/cli/{resolve-views-dir.js → resolve-skybridge-config.js} +4 -3
- package/dist/cli/resolve-skybridge-config.js.map +1 -0
- package/dist/cli/run-plain.d.ts +18 -0
- package/dist/cli/run-plain.js +90 -0
- package/dist/cli/run-plain.js.map +1 -0
- package/dist/cli/telemetry.js +2 -0
- package/dist/cli/telemetry.js.map +1 -1
- package/dist/cli/use-execute-steps.d.ts +1 -1
- package/dist/cli/use-nodemon.d.ts +25 -1
- package/dist/cli/use-nodemon.js +96 -63
- package/dist/cli/use-nodemon.js.map +1 -1
- package/dist/cli/use-typescript-check.d.ts +8 -2
- package/dist/cli/use-typescript-check.js +70 -67
- package/dist/cli/use-typescript-check.js.map +1 -1
- package/dist/commands/build.d.ts +0 -2
- package/dist/commands/build.js +2 -61
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/dev.d.ts +1 -0
- package/dist/commands/dev.js +38 -6
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/start.js +2 -2
- package/dist/commands/start.js.map +1 -1
- package/dist/context-warnings.d.ts +5 -0
- package/dist/context-warnings.js +31 -0
- package/dist/context-warnings.js.map +1 -0
- package/dist/context-warnings.test.js +28 -0
- package/dist/context-warnings.test.js.map +1 -0
- package/dist/server/app.d.ts +195 -0
- package/dist/server/app.js +268 -0
- package/dist/server/app.js.map +1 -0
- package/dist/server/app.test.js +196 -0
- package/dist/server/app.test.js.map +1 -0
- package/dist/server/auth/discovery.d.ts +2 -1
- package/dist/server/auth/discovery.js +2 -2
- package/dist/server/auth/discovery.js.map +1 -1
- package/dist/server/auth/index.d.ts +35 -5
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/auth/providers/auth0.d.ts +25 -3
- package/dist/server/auth/providers/auth0.js +13 -8
- package/dist/server/auth/providers/auth0.js.map +1 -1
- package/dist/server/auth/providers/auth0.test.js +5 -3
- package/dist/server/auth/providers/auth0.test.js.map +1 -1
- package/dist/server/auth/providers/authplane.d.ts +49 -0
- package/dist/server/auth/providers/authplane.js +57 -0
- package/dist/server/auth/providers/authplane.js.map +1 -0
- package/dist/server/auth/providers/authplane.test.js +143 -0
- package/dist/server/auth/providers/authplane.test.js.map +1 -0
- package/dist/server/auth/providers/clerk.d.ts +40 -3
- package/dist/server/auth/providers/clerk.js +4 -1
- package/dist/server/auth/providers/clerk.js.map +1 -1
- package/dist/server/auth/providers/clerk.test.js +5 -3
- package/dist/server/auth/providers/clerk.test.js.map +1 -1
- package/dist/server/auth/providers/custom.d.ts +24 -6
- package/dist/server/auth/providers/custom.js +33 -11
- package/dist/server/auth/providers/custom.js.map +1 -1
- package/dist/server/auth/providers/custom.test.js +71 -17
- package/dist/server/auth/providers/custom.test.js.map +1 -1
- package/dist/server/auth/providers/descope.d.ts +32 -3
- package/dist/server/auth/providers/descope.js +12 -2
- package/dist/server/auth/providers/descope.js.map +1 -1
- package/dist/server/auth/providers/descope.test.js +42 -18
- package/dist/server/auth/providers/descope.test.js.map +1 -1
- package/dist/server/auth/providers/stytch.d.ts +16 -3
- package/dist/server/auth/providers/stytch.js +4 -1
- package/dist/server/auth/providers/stytch.js.map +1 -1
- package/dist/server/auth/providers/verify-spy.d.ts +12 -0
- package/dist/server/auth/providers/verify-spy.js +21 -0
- package/dist/server/auth/providers/verify-spy.js.map +1 -0
- package/dist/server/auth/providers/workos.d.ts +28 -5
- package/dist/server/auth/providers/workos.js +6 -2
- package/dist/server/auth/providers/workos.js.map +1 -1
- package/dist/server/auth/security-schemes.d.ts +23 -0
- package/dist/server/auth/security-schemes.js +64 -0
- package/dist/server/auth/security-schemes.js.map +1 -0
- package/dist/server/auth/security-schemes.test.d.ts +1 -0
- package/dist/server/auth/security-schemes.test.js +99 -0
- package/dist/server/auth/security-schemes.test.js.map +1 -0
- package/dist/server/auth/setup.d.ts +3 -1
- package/dist/server/auth/setup.js +66 -26
- package/dist/server/auth/setup.js.map +1 -1
- package/dist/server/auth/setup.test.js +349 -20
- package/dist/server/auth/setup.test.js.map +1 -1
- package/dist/server/auth/verify.d.ts +42 -6
- package/dist/server/auth/verify.js +27 -3
- package/dist/server/auth/verify.js.map +1 -1
- package/dist/server/auth/verify.test.js +65 -0
- package/dist/server/auth/verify.test.js.map +1 -1
- package/dist/server/auth-extra.test-d.js +130 -0
- package/dist/server/auth-extra.test-d.js.map +1 -0
- package/dist/server/auth.d.ts +43 -6
- package/dist/server/auth.js +4 -5
- package/dist/server/auth.js.map +1 -1
- package/dist/server/build-manifest.test.js +7 -6
- package/dist/server/build-manifest.test.js.map +1 -1
- package/dist/server/content-helpers.d.ts +1 -1
- package/dist/server/content-helpers.js.map +1 -1
- package/dist/server/events.d.ts +113 -0
- package/dist/server/events.js +318 -0
- package/dist/server/events.js.map +1 -0
- package/dist/server/events.test.d.ts +1 -0
- package/dist/server/events.test.js +172 -0
- package/dist/server/events.test.js.map +1 -0
- package/dist/server/express.d.ts +23 -3
- package/dist/server/express.js +43 -32
- package/dist/server/express.js.map +1 -1
- package/dist/server/express.test.js +221 -103
- package/dist/server/express.test.js.map +1 -1
- package/dist/server/file-ref.d.ts +1 -1
- package/dist/server/file-ref.js +1 -1
- package/dist/server/file-ref.js.map +1 -1
- package/dist/server/form-input.d.ts +120 -0
- package/dist/server/form-input.js +89 -0
- package/dist/server/form-input.js.map +1 -0
- package/dist/server/host.d.ts +2 -0
- package/dist/server/host.js +11 -0
- package/dist/server/host.js.map +1 -0
- package/dist/server/host.test.d.ts +1 -0
- package/dist/server/host.test.js +13 -0
- package/dist/server/host.test.js.map +1 -0
- package/dist/server/index.d.ts +18 -9
- package/dist/server/index.js +11 -3
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp-errors.test.d.ts +1 -0
- package/dist/server/mcp-errors.test.js +41 -0
- package/dist/server/mcp-errors.test.js.map +1 -0
- package/dist/server/middleware.d.ts +35 -10
- package/dist/server/middleware.js +33 -0
- package/dist/server/middleware.js.map +1 -1
- package/dist/server/middleware.test-d.js +1 -1
- package/dist/server/middleware.test-d.js.map +1 -1
- package/dist/server/middleware.test.js +242 -134
- package/dist/server/middleware.test.js.map +1 -1
- package/dist/server/register-tool.test.d.ts +1 -0
- package/dist/server/register-tool.test.js +230 -0
- package/dist/server/register-tool.test.js.map +1 -0
- package/dist/server/server.d.ts +373 -128
- package/dist/server/server.js +441 -274
- package/dist/server/server.js.map +1 -1
- package/dist/server/skills-integration.test.d.ts +1 -0
- package/dist/server/skills-integration.test.js +86 -0
- package/dist/server/skills-integration.test.js.map +1 -0
- package/dist/server/skills.d.ts +27 -0
- package/dist/server/skills.js +188 -0
- package/dist/server/skills.js.map +1 -0
- package/dist/server/skills.test.d.ts +1 -0
- package/dist/server/skills.test.js +243 -0
- package/dist/server/skills.test.js.map +1 -0
- package/dist/server/templateHelper.d.ts +0 -3
- package/dist/server/templateHelper.js.map +1 -1
- package/dist/server/templates.generated.js +4 -8
- package/dist/server/templates.generated.js.map +1 -1
- package/dist/server/tool-extra.test.d.ts +1 -0
- package/dist/server/tool-extra.test.js +167 -0
- package/dist/server/tool-extra.test.js.map +1 -0
- package/dist/server/view-resource-resolution.test.js +18 -13
- package/dist/server/view-resource-resolution.test.js.map +1 -1
- package/dist/server/viewsDevServer.js +4 -1
- package/dist/server/viewsDevServer.js.map +1 -1
- package/dist/test/utils.d.ts +9 -11
- package/dist/test/utils.js +220 -201
- package/dist/test/utils.js.map +1 -1
- package/dist/test/view.test.js +44 -40
- package/dist/test/view.test.js.map +1 -1
- package/dist/views/index.d.ts +10 -0
- package/dist/views/index.js +3 -0
- package/dist/views/index.js.map +1 -0
- package/dist/{web/plugin → views}/scan-views.js +1 -1
- package/dist/views/scan-views.js.map +1 -0
- package/dist/views/scan-views.test.d.ts +1 -0
- package/dist/views/scan-views.test.js.map +1 -0
- package/dist/views/validate-view.d.ts +1 -0
- package/dist/{web/plugin → views}/validate-view.js +2 -2
- package/dist/views/validate-view.js.map +1 -0
- package/dist/views/validate-view.test.d.ts +1 -0
- package/dist/views/validate-view.test.js.map +1 -0
- package/dist/web/bridges/adaptor.d.ts +14 -3
- package/dist/web/bridges/adaptor.js +181 -17
- package/dist/web/bridges/adaptor.js.map +1 -1
- package/dist/web/bridges/adaptor.test.js +95 -11
- package/dist/web/bridges/adaptor.test.js.map +1 -1
- package/dist/web/bridges/apps-sdk/bridge.js +1 -1
- package/dist/web/bridges/apps-sdk/bridge.js.map +1 -1
- package/dist/web/bridges/apps-sdk/types.d.ts +2 -3
- package/dist/web/bridges/apps-sdk/types.js.map +1 -1
- package/dist/web/bridges/apps-sdk/use-apps-sdk-context.d.ts +4 -4
- package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js +4 -4
- package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js.map +1 -1
- package/dist/web/bridges/get-adaptor.test.js +2 -2
- package/dist/web/bridges/get-adaptor.test.js.map +1 -1
- package/dist/web/bridges/mcp-app/bridge.d.ts +1 -1
- package/dist/web/bridges/mcp-app/bridge.js +15 -6
- package/dist/web/bridges/mcp-app/bridge.js.map +1 -1
- package/dist/web/bridges/mcp-app/bridge.test.js +2 -2
- package/dist/web/bridges/mcp-app/bridge.test.js.map +1 -1
- package/dist/web/bridges/mcp-app/types.d.ts +19 -2
- package/dist/web/bridges/mcp-app/types.js.map +1 -1
- package/dist/web/bridges/mcp-app/use-mcp-app-context.d.ts +2 -2
- package/dist/web/bridges/mcp-app/use-mcp-app-context.js +1 -1
- package/dist/web/bridges/mcp-app/use-mcp-app-context.js.map +1 -1
- package/dist/web/bridges/mcp-app/use-mcp-app-context.test.js +1 -1
- package/dist/web/bridges/mcp-app/use-mcp-app-context.test.js.map +1 -1
- package/dist/web/bridges/mcp-app/view-tools.test.js +1 -1
- package/dist/web/bridges/mcp-app/view-tools.test.js.map +1 -1
- package/dist/web/bridges/types.d.ts +74 -16
- package/dist/web/bridges/types.js +1 -1
- package/dist/web/bridges/types.js.map +1 -1
- package/dist/web/components/modal-provider.d.ts +1 -1
- package/dist/web/create-store.test.js +2 -2
- package/dist/web/create-store.test.js.map +1 -1
- package/dist/web/data-llm.d.ts +1 -1
- package/dist/web/data-llm.js +3 -2
- package/dist/web/data-llm.js.map +1 -1
- package/dist/web/data-llm.test.js +17 -2
- package/dist/web/data-llm.test.js.map +1 -1
- package/dist/web/generate-helpers.d.ts +23 -19
- package/dist/web/generate-helpers.js +21 -17
- package/dist/web/generate-helpers.js.map +1 -1
- package/dist/web/generate-helpers.test-d.js +3 -3
- package/dist/web/generate-helpers.test-d.js.map +1 -1
- package/dist/web/generate-helpers.test.js +2 -2
- package/dist/web/generate-helpers.test.js.map +1 -1
- package/dist/web/hooks/index.d.ts +6 -1
- package/dist/web/hooks/index.js +6 -1
- package/dist/web/hooks/index.js.map +1 -1
- package/dist/web/hooks/openai-extensions.test.d.ts +1 -0
- package/dist/web/hooks/openai-extensions.test.js +107 -0
- package/dist/web/hooks/openai-extensions.test.js.map +1 -0
- package/dist/web/hooks/test/utils.d.ts +2 -1
- package/dist/web/hooks/test/utils.js +1 -1
- package/dist/web/hooks/test/utils.js.map +1 -1
- package/dist/web/hooks/use-call-tool.d.ts +2 -2
- package/dist/web/hooks/use-call-tool.js +15 -10
- package/dist/web/hooks/use-call-tool.js.map +1 -1
- package/dist/web/hooks/use-call-tool.test.js +15 -4
- package/dist/web/hooks/use-call-tool.test.js.map +1 -1
- package/dist/web/hooks/use-deep-link.d.ts +22 -0
- package/dist/web/hooks/use-deep-link.js +34 -0
- package/dist/web/hooks/use-deep-link.js.map +1 -0
- package/dist/web/hooks/use-display-mode.d.ts +1 -1
- package/dist/web/hooks/use-display-mode.js +1 -1
- package/dist/web/hooks/use-display-mode.js.map +1 -1
- package/dist/web/hooks/use-display-mode.test.js +1 -1
- package/dist/web/hooks/use-display-mode.test.js.map +1 -1
- package/dist/web/hooks/use-download.d.ts +1 -3
- package/dist/web/hooks/use-download.js +1 -2
- package/dist/web/hooks/use-download.js.map +1 -1
- package/dist/web/hooks/use-download.test.js +7 -7
- package/dist/web/hooks/use-download.test.js.map +1 -1
- package/dist/web/hooks/use-file-resource.d.ts +37 -0
- package/dist/web/hooks/use-file-resource.js +98 -0
- package/dist/web/hooks/use-file-resource.js.map +1 -0
- package/dist/web/hooks/use-file-resource.test.d.ts +1 -0
- package/dist/web/hooks/use-file-resource.test.js +65 -0
- package/dist/web/hooks/use-file-resource.test.js.map +1 -0
- package/dist/web/hooks/use-files.test.js +1 -1
- package/dist/web/hooks/use-files.test.js.map +1 -1
- package/dist/web/hooks/use-host.d.ts +44 -0
- package/dist/web/hooks/use-host.js +45 -0
- package/dist/web/hooks/use-host.js.map +1 -0
- package/dist/web/hooks/use-host.test.d.ts +1 -0
- package/dist/web/hooks/use-host.test.js +59 -0
- package/dist/web/hooks/use-host.test.js.map +1 -0
- package/dist/web/hooks/use-model-context.d.ts +44 -0
- package/dist/web/hooks/use-model-context.js +63 -0
- package/dist/web/hooks/use-model-context.js.map +1 -0
- package/dist/web/hooks/use-open-external.test.js +2 -2
- package/dist/web/hooks/use-open-external.test.js.map +1 -1
- package/dist/web/hooks/use-open-file.d.ts +20 -0
- package/dist/web/hooks/use-open-file.js +25 -0
- package/dist/web/hooks/use-open-file.js.map +1 -0
- package/dist/web/hooks/use-register-view-tool.d.ts +3 -4
- package/dist/web/hooks/use-register-view-tool.js +1 -1
- package/dist/web/hooks/use-register-view-tool.js.map +1 -1
- package/dist/web/hooks/use-request-close.test.js +1 -1
- package/dist/web/hooks/use-request-close.test.js.map +1 -1
- package/dist/web/hooks/use-request-modal.test.js +1 -1
- package/dist/web/hooks/use-request-modal.test.js.map +1 -1
- package/dist/web/hooks/use-request-size.d.ts +1 -1
- package/dist/web/hooks/use-request-size.js +1 -1
- package/dist/web/hooks/use-request-size.js.map +1 -1
- package/dist/web/hooks/use-request-size.test.js +1 -1
- package/dist/web/hooks/use-request-size.test.js.map +1 -1
- package/dist/web/hooks/use-send-follow-up-message.d.ts +8 -2
- package/dist/web/hooks/use-send-follow-up-message.js +6 -0
- package/dist/web/hooks/use-send-follow-up-message.js.map +1 -1
- package/dist/web/hooks/use-set-open-in-app-url.test.js +2 -8
- package/dist/web/hooks/use-set-open-in-app-url.test.js.map +1 -1
- package/dist/web/hooks/use-tool-info.d.ts +1 -20
- package/dist/web/hooks/use-tool-info.js +0 -1
- package/dist/web/hooks/use-tool-info.js.map +1 -1
- package/dist/web/hooks/use-tool-info.test-d.js +0 -5
- package/dist/web/hooks/use-tool-info.test-d.js.map +1 -1
- package/dist/web/hooks/use-tool-info.test.js +2 -7
- package/dist/web/hooks/use-tool-info.test.js.map +1 -1
- package/dist/web/hooks/use-user.d.ts +3 -2
- package/dist/web/hooks/use-user.js +4 -3
- package/dist/web/hooks/use-user.js.map +1 -1
- package/dist/web/hooks/use-user.test.js +5 -3
- package/dist/web/hooks/use-user.test.js.map +1 -1
- package/dist/web/hooks/use-view-state.js +19 -9
- package/dist/web/hooks/use-view-state.js.map +1 -1
- package/dist/web/hooks/use-view-state.test.js +33 -2
- package/dist/web/hooks/use-view-state.test.js.map +1 -1
- package/dist/web/hooks/use-viewport.d.ts +20 -0
- package/dist/web/hooks/use-viewport.js +21 -0
- package/dist/web/hooks/use-viewport.js.map +1 -0
- package/dist/web/hooks/use-viewport.test.d.ts +1 -0
- package/dist/web/hooks/{use-layout.test.js → use-viewport.test.js} +12 -32
- package/dist/web/hooks/use-viewport.test.js.map +1 -0
- package/dist/web/mount-view.d.ts +3 -2
- package/dist/web/mount-view.js +5 -8
- package/dist/web/mount-view.js.map +1 -1
- package/package.json +34 -29
- package/dist/cli/resolve-views-dir.d.ts +0 -1
- package/dist/cli/resolve-views-dir.js.map +0 -1
- package/dist/web/hooks/use-layout.d.ts +0 -24
- package/dist/web/hooks/use-layout.js +0 -25
- package/dist/web/hooks/use-layout.js.map +0 -1
- package/dist/web/hooks/use-layout.test.js.map +0 -1
- package/dist/web/plugin/data-llm.test.js +0 -81
- package/dist/web/plugin/data-llm.test.js.map +0 -1
- package/dist/web/plugin/plugin.d.ts +0 -33
- package/dist/web/plugin/plugin.js +0 -189
- package/dist/web/plugin/plugin.js.map +0 -1
- package/dist/web/plugin/scan-views.js.map +0 -1
- package/dist/web/plugin/scan-views.test.js.map +0 -1
- package/dist/web/plugin/transform-data-llm.d.ts +0 -12
- package/dist/web/plugin/transform-data-llm.js +0 -96
- package/dist/web/plugin/transform-data-llm.js.map +0 -1
- package/dist/web/plugin/transform-data-llm.test.js +0 -81
- package/dist/web/plugin/transform-data-llm.test.js.map +0 -1
- package/dist/web/plugin/validate-view.d.ts +0 -1
- package/dist/web/plugin/validate-view.js.map +0 -1
- package/dist/web/plugin/validate-view.test.js.map +0 -1
- /package/dist/{web/hooks/use-layout.test.d.ts → cli/build-steps.test.d.ts} +0 -0
- /package/dist/{web/plugin/data-llm.test.d.ts → context-warnings.test.d.ts} +0 -0
- /package/dist/{web/plugin/scan-views.test.d.ts → server/app.test.d.ts} +0 -0
- /package/dist/{web/plugin/validate-view.test.d.ts → server/auth/providers/authplane.test.d.ts} +0 -0
- /package/dist/{web/plugin/transform-data-llm.test.d.ts → server/auth-extra.test-d.d.ts} +0 -0
- /package/dist/{web/plugin → views}/scan-views.d.ts +0 -0
- /package/dist/{web/plugin → views}/scan-views.test.js +0 -0
- /package/dist/{web/plugin → views}/validate-view.test.js +0 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { type McpServer as SdkMcpServer, type StandardSchemaV1, type StandardSchemaWithJSON } from "@modelcontextprotocol/server";
|
|
2
|
+
/**
|
|
3
|
+
* A webhook subscription, as passed to `onSubscribe`. Whatever delivers the
|
|
4
|
+
* events POSTs them to `url`, signed with `secret`, until `expiresAt`.
|
|
5
|
+
*/
|
|
6
|
+
export interface EventSubscription {
|
|
7
|
+
/** Stable across refreshes: the same user subscribing to the same event with the same arguments and callback gets the same `id`. */
|
|
8
|
+
id: string;
|
|
9
|
+
/** Callback URL deliveries are POSTed to. */
|
|
10
|
+
url: string;
|
|
11
|
+
/** Standard Webhooks signing secret (`whsec_…`). A refresh may carry a new one. */
|
|
12
|
+
secret: string;
|
|
13
|
+
/** When the subscription lapses unless the host refreshes it. */
|
|
14
|
+
expiresAt: Date;
|
|
15
|
+
}
|
|
16
|
+
/** An event occurrence, as passed to {@link deliverEvent}. */
|
|
17
|
+
export interface EventOccurrence {
|
|
18
|
+
/** Event type name, as registered with `registerEvent`. */
|
|
19
|
+
name: string;
|
|
20
|
+
/** Stable identifier the host uses to drop duplicates, ideally the upstream's. Reuse it when retrying. */
|
|
21
|
+
id: string;
|
|
22
|
+
/** When the event happened. Defaults to now. */
|
|
23
|
+
timestamp?: Date;
|
|
24
|
+
/** Payload matching the event's `payloadSchema`. */
|
|
25
|
+
data: unknown;
|
|
26
|
+
}
|
|
27
|
+
/** Outcome of {@link deliverEvent}. Retry with the same event `id` when `retryable`. */
|
|
28
|
+
export type DeliveryResult = {
|
|
29
|
+
ok: true;
|
|
30
|
+
} | {
|
|
31
|
+
ok: false;
|
|
32
|
+
reason: string;
|
|
33
|
+
retryable: boolean;
|
|
34
|
+
status?: number;
|
|
35
|
+
};
|
|
36
|
+
type EventSchema = Record<string, StandardSchemaWithJSON> | StandardSchemaWithJSON;
|
|
37
|
+
export type InferEventSchema<T extends EventSchema> = T extends StandardSchemaWithJSON ? StandardSchemaV1.InferOutput<T> : T extends Record<string, StandardSchemaWithJSON> ? {
|
|
38
|
+
[K in keyof T as undefined extends StandardSchemaV1.InferOutput<T[K]> ? never : K]: StandardSchemaV1.InferOutput<T[K]>;
|
|
39
|
+
} & {
|
|
40
|
+
[K in keyof T as undefined extends StandardSchemaV1.InferOutput<T[K]> ? K : never]?: StandardSchemaV1.InferOutput<T[K]>;
|
|
41
|
+
} : never;
|
|
42
|
+
/** The `registerEvent` config: how the event type is described in `events/list`. */
|
|
43
|
+
export interface EventConfig<TInput extends EventSchema> {
|
|
44
|
+
/** Stable, specific name such as `comment.created`. */
|
|
45
|
+
name: string;
|
|
46
|
+
/** What the event is and when it fires. */
|
|
47
|
+
description?: string;
|
|
48
|
+
/** Subscription arguments (filters): a Zod shape or a Standard Schema object. */
|
|
49
|
+
inputSchema?: TInput;
|
|
50
|
+
/** Shape of the delivered `data`: a Zod shape or a Standard Schema object. */
|
|
51
|
+
payloadSchema: EventSchema;
|
|
52
|
+
_meta?: Record<string, unknown>;
|
|
53
|
+
}
|
|
54
|
+
/** What `registerEvent` does when the host subscribes and unsubscribes. */
|
|
55
|
+
export interface EventHooks<TArguments, TExtra> {
|
|
56
|
+
/**
|
|
57
|
+
* Runs on every `events/subscribe`, including the host's refreshes, once
|
|
58
|
+
* the callback URL is verified. Hand the subscription to whatever delivers
|
|
59
|
+
* the events, keyed by `subscription.id`. Throw (e.g. a `ProtocolError`) to
|
|
60
|
+
* reject it, for instance when the user may not access what the arguments
|
|
61
|
+
* point to.
|
|
62
|
+
*/
|
|
63
|
+
onSubscribe(args: TArguments, context: {
|
|
64
|
+
subscription: EventSubscription;
|
|
65
|
+
extra: TExtra;
|
|
66
|
+
}): void | Promise<void>;
|
|
67
|
+
/** Runs on `events/unsubscribe`. Stop delivering to `subscription.id`. */
|
|
68
|
+
onUnsubscribe(args: TArguments, context: {
|
|
69
|
+
subscription: Pick<EventSubscription, "id">;
|
|
70
|
+
extra: TExtra;
|
|
71
|
+
}): void | Promise<void>;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* @experimental POST one event to a subscription handed out by `onSubscribe`,
|
|
75
|
+
* as MCP Events expects: the event envelope, Standard Webhooks signature
|
|
76
|
+
* headers, a 10 second deadline and, outside development, no requests to
|
|
77
|
+
* private addresses. Pass the old and new secrets during a rotation so both
|
|
78
|
+
* sign the delivery. A subscription past `expiresAt` is not posted to.
|
|
79
|
+
* Retrying is up to the caller. API may change.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```ts
|
|
83
|
+
* const result = await deliverEvent(subscription, {
|
|
84
|
+
* name: "comment.created",
|
|
85
|
+
* id: comment.id,
|
|
86
|
+
* data: { documentId: comment.documentId, excerpt: comment.text.slice(0, 280) },
|
|
87
|
+
* });
|
|
88
|
+
* if (!result.ok && result.retryable) {
|
|
89
|
+
* await queue.retryLater(subscription, comment);
|
|
90
|
+
* }
|
|
91
|
+
* ```
|
|
92
|
+
*
|
|
93
|
+
* @see https://docs.skybridge.tech/api-reference/register-event
|
|
94
|
+
*/
|
|
95
|
+
export declare function deliverEvent(subscription: Pick<EventSubscription, "id" | "url"> & {
|
|
96
|
+
secret: string | string[];
|
|
97
|
+
expiresAt: Date | string;
|
|
98
|
+
}, event: EventOccurrence): Promise<DeliveryResult>;
|
|
99
|
+
/**
|
|
100
|
+
* The event types registered on one server, and the `events/*` handlers that
|
|
101
|
+
* serve them.
|
|
102
|
+
*
|
|
103
|
+
* @internal
|
|
104
|
+
*/
|
|
105
|
+
export declare class EventRegistry {
|
|
106
|
+
private readonly server;
|
|
107
|
+
private readonly events;
|
|
108
|
+
constructor(server: Pick<SdkMcpServer["server"], "setRequestHandler">);
|
|
109
|
+
add(config: EventConfig<EventSchema>, hooks: EventHooks<never, never>): void;
|
|
110
|
+
private install;
|
|
111
|
+
private resolve;
|
|
112
|
+
}
|
|
113
|
+
export {};
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import http from "node:http";
|
|
3
|
+
import https from "node:https";
|
|
4
|
+
import { INVALID_PARAMS, ProtocolError, } from "@modelcontextprotocol/server";
|
|
5
|
+
import { globalHttpAgent, globalHttpsAgent } from "request-filtering-agent";
|
|
6
|
+
import { Webhook } from "standardwebhooks";
|
|
7
|
+
import { z } from "zod/v4";
|
|
8
|
+
const EVENTS_ERROR = {
|
|
9
|
+
NotFound: -32011,
|
|
10
|
+
Forbidden: -32012,
|
|
11
|
+
Unsupported: -32014,
|
|
12
|
+
CallbackEndpointError: -32015,
|
|
13
|
+
};
|
|
14
|
+
const MAX_PAYLOAD_BYTES = 256 * 1024;
|
|
15
|
+
const DELIVERY_TIMEOUT_MS = 10_000;
|
|
16
|
+
const VERIFICATION_CACHE_MS = 60 * 60_000;
|
|
17
|
+
const MIN_TTL_MS = 60_000;
|
|
18
|
+
const DEFAULT_TTL_MS = 30 * 60_000;
|
|
19
|
+
const MAX_TTL_MS = 24 * 60 * 60_000;
|
|
20
|
+
function toStandardSchema(schema) {
|
|
21
|
+
return "~standard" in schema
|
|
22
|
+
? schema
|
|
23
|
+
: z.object(schema);
|
|
24
|
+
}
|
|
25
|
+
async function validate(schema, value) {
|
|
26
|
+
const result = await schema["~standard"].validate(value);
|
|
27
|
+
if (result.issues) {
|
|
28
|
+
throw new ProtocolError(INVALID_PARAMS, `Invalid arguments: ${result.issues.map((issue) => issue.message).join(", ")}`);
|
|
29
|
+
}
|
|
30
|
+
return result.value;
|
|
31
|
+
}
|
|
32
|
+
function canonicalJson(value) {
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
35
|
+
}
|
|
36
|
+
if (value && typeof value === "object") {
|
|
37
|
+
return `{${Object.keys(value)
|
|
38
|
+
.sort()
|
|
39
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
|
|
40
|
+
.join(",")}}`;
|
|
41
|
+
}
|
|
42
|
+
return JSON.stringify(value);
|
|
43
|
+
}
|
|
44
|
+
function subscriberOf(ctx) {
|
|
45
|
+
const authInfo = ctx.http?.authInfo;
|
|
46
|
+
const subject = authInfo?.extra?.subject;
|
|
47
|
+
const subscriber = typeof subject === "string" ? subject : authInfo?.clientId;
|
|
48
|
+
if (!subscriber) {
|
|
49
|
+
throw new ProtocolError(EVENTS_ERROR.Forbidden, "Webhook subscriptions require an authenticated caller");
|
|
50
|
+
}
|
|
51
|
+
return subscriber;
|
|
52
|
+
}
|
|
53
|
+
function isValidSecret(secret) {
|
|
54
|
+
const encoded = secret.startsWith("whsec_") ? secret.slice(6) : "";
|
|
55
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(encoded)) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
const length = Buffer.from(encoded, "base64").length;
|
|
59
|
+
return length >= 24 && length <= 64;
|
|
60
|
+
}
|
|
61
|
+
const insecureCallbacksAllowed = () => process.env.NODE_ENV !== "production";
|
|
62
|
+
function checkCallbackUrl(raw) {
|
|
63
|
+
let url;
|
|
64
|
+
try {
|
|
65
|
+
url = new URL(raw);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
throw new ProtocolError(INVALID_PARAMS, "delivery.url is not a valid URL");
|
|
69
|
+
}
|
|
70
|
+
const allowed = url.protocol === "https:" ||
|
|
71
|
+
(url.protocol === "http:" && insecureCallbacksAllowed());
|
|
72
|
+
if (!allowed) {
|
|
73
|
+
throw new ProtocolError(INVALID_PARAMS, "delivery.url must use https");
|
|
74
|
+
}
|
|
75
|
+
return url.href;
|
|
76
|
+
}
|
|
77
|
+
function post(url, headers, body) {
|
|
78
|
+
const signal = AbortSignal.timeout(DELIVERY_TIMEOUT_MS);
|
|
79
|
+
const failure = (error) => ({
|
|
80
|
+
ok: false,
|
|
81
|
+
reason: signal.aborted
|
|
82
|
+
? "timeout"
|
|
83
|
+
: error?.code?.startsWith("ERR_TLS") || error?.code?.includes("CERT")
|
|
84
|
+
? "tls_error"
|
|
85
|
+
: "connection_refused",
|
|
86
|
+
});
|
|
87
|
+
const [client, agent] = url.startsWith("https:")
|
|
88
|
+
? [https, globalHttpsAgent]
|
|
89
|
+
: [http, globalHttpAgent];
|
|
90
|
+
return new Promise((resolve) => {
|
|
91
|
+
const request = client.request(url, {
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers: { ...headers, "content-length": Buffer.byteLength(body) },
|
|
94
|
+
signal,
|
|
95
|
+
...(!insecureCallbacksAllowed() && { agent }),
|
|
96
|
+
}, (response) => {
|
|
97
|
+
const status = response.statusCode ?? 0;
|
|
98
|
+
const chunks = [];
|
|
99
|
+
let size = 0;
|
|
100
|
+
response.on("data", (chunk) => {
|
|
101
|
+
size += chunk.length;
|
|
102
|
+
if (size <= 64 * 1024) {
|
|
103
|
+
chunks.push(chunk);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
response.on("end", () => {
|
|
107
|
+
if (status >= 200 && status < 300) {
|
|
108
|
+
resolve({ ok: true, body: Buffer.concat(chunks).toString() });
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
resolve({
|
|
112
|
+
ok: false,
|
|
113
|
+
status,
|
|
114
|
+
reason: status >= 500 ? "http_5xx" : "http_4xx",
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
response.on("error", (error) => resolve(failure(error)));
|
|
119
|
+
response.on("close", () => resolve(failure()));
|
|
120
|
+
});
|
|
121
|
+
request.on("error", (error) => resolve(failure(error)));
|
|
122
|
+
request.end(body);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
function signedPost(subscription, messageId, body) {
|
|
126
|
+
const signedAt = new Date();
|
|
127
|
+
const secrets = [subscription.secret].flat();
|
|
128
|
+
return post(subscription.url, {
|
|
129
|
+
"content-type": "application/json",
|
|
130
|
+
"webhook-id": messageId,
|
|
131
|
+
"webhook-timestamp": String(Math.floor(signedAt.getTime() / 1000)),
|
|
132
|
+
"webhook-signature": secrets
|
|
133
|
+
.map((secret) => new Webhook(secret).sign(messageId, signedAt, body))
|
|
134
|
+
.join(" "),
|
|
135
|
+
"x-mcp-subscription-id": subscription.id,
|
|
136
|
+
}, body);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* @experimental POST one event to a subscription handed out by `onSubscribe`,
|
|
140
|
+
* as MCP Events expects: the event envelope, Standard Webhooks signature
|
|
141
|
+
* headers, a 10 second deadline and, outside development, no requests to
|
|
142
|
+
* private addresses. Pass the old and new secrets during a rotation so both
|
|
143
|
+
* sign the delivery. A subscription past `expiresAt` is not posted to.
|
|
144
|
+
* Retrying is up to the caller. API may change.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```ts
|
|
148
|
+
* const result = await deliverEvent(subscription, {
|
|
149
|
+
* name: "comment.created",
|
|
150
|
+
* id: comment.id,
|
|
151
|
+
* data: { documentId: comment.documentId, excerpt: comment.text.slice(0, 280) },
|
|
152
|
+
* });
|
|
153
|
+
* if (!result.ok && result.retryable) {
|
|
154
|
+
* await queue.retryLater(subscription, comment);
|
|
155
|
+
* }
|
|
156
|
+
* ```
|
|
157
|
+
*
|
|
158
|
+
* @see https://docs.skybridge.tech/api-reference/register-event
|
|
159
|
+
*/
|
|
160
|
+
export async function deliverEvent(subscription, event) {
|
|
161
|
+
if (!(new Date(subscription.expiresAt).getTime() > Date.now())) {
|
|
162
|
+
return { ok: false, reason: "expired", retryable: false };
|
|
163
|
+
}
|
|
164
|
+
const body = JSON.stringify({
|
|
165
|
+
eventId: event.id,
|
|
166
|
+
name: event.name,
|
|
167
|
+
timestamp: (event.timestamp ?? new Date()).toISOString(),
|
|
168
|
+
data: event.data,
|
|
169
|
+
cursor: null,
|
|
170
|
+
});
|
|
171
|
+
if (Buffer.byteLength(body) > MAX_PAYLOAD_BYTES) {
|
|
172
|
+
throw new Error(`Event "${event.name}" payload exceeds ${MAX_PAYLOAD_BYTES} bytes; send a summary and expose a tool to fetch the rest.`);
|
|
173
|
+
}
|
|
174
|
+
const outcome = await signedPost(subscription, event.id, body);
|
|
175
|
+
if (outcome.ok) {
|
|
176
|
+
return { ok: true };
|
|
177
|
+
}
|
|
178
|
+
const status = outcome.status ?? 0;
|
|
179
|
+
return {
|
|
180
|
+
...outcome,
|
|
181
|
+
retryable: status !== 410 && status !== 413 && (status < 300 || status >= 400),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const verifiedCallbacks = new Map();
|
|
185
|
+
async function verifyCallback(subscriber, subscription) {
|
|
186
|
+
const key = `${subscriber}\n${subscription.url}`;
|
|
187
|
+
const now = Date.now();
|
|
188
|
+
if ((verifiedCallbacks.get(key) ?? 0) > now) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const challenge = randomBytes(24).toString("base64url");
|
|
192
|
+
const outcome = await signedPost(subscription, `msg_verification_${randomBytes(12).toString("hex")}`, JSON.stringify({ type: "verification", challenge }));
|
|
193
|
+
let echoed;
|
|
194
|
+
if (outcome.ok) {
|
|
195
|
+
try {
|
|
196
|
+
echoed = JSON.parse(outcome.body)?.challenge;
|
|
197
|
+
}
|
|
198
|
+
catch { }
|
|
199
|
+
}
|
|
200
|
+
const matched = typeof echoed === "string" &&
|
|
201
|
+
echoed.length === challenge.length &&
|
|
202
|
+
timingSafeEqual(Buffer.from(echoed), Buffer.from(challenge));
|
|
203
|
+
if (!matched) {
|
|
204
|
+
throw new ProtocolError(EVENTS_ERROR.CallbackEndpointError, "Callback endpoint verification failed", { reason: outcome.ok ? "challenge_failed" : outcome.reason });
|
|
205
|
+
}
|
|
206
|
+
for (const [cached, until] of verifiedCallbacks) {
|
|
207
|
+
if (until <= now) {
|
|
208
|
+
verifiedCallbacks.delete(cached);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
verifiedCallbacks.set(key, now + VERIFICATION_CACHE_MS);
|
|
212
|
+
}
|
|
213
|
+
const SubscriptionKeySchema = z.object({
|
|
214
|
+
name: z.string(),
|
|
215
|
+
arguments: z.record(z.string(), z.unknown()).optional(),
|
|
216
|
+
delivery: z.object({ url: z.string() }),
|
|
217
|
+
});
|
|
218
|
+
const SubscribeParamsSchema = SubscriptionKeySchema.extend({
|
|
219
|
+
delivery: z.object({
|
|
220
|
+
mode: z.string().optional(),
|
|
221
|
+
url: z.string(),
|
|
222
|
+
secret: z.string(),
|
|
223
|
+
}),
|
|
224
|
+
cursor: z.string().nullable().optional(),
|
|
225
|
+
ttlMs: z.number().nullable().optional(),
|
|
226
|
+
});
|
|
227
|
+
/**
|
|
228
|
+
* The event types registered on one server, and the `events/*` handlers that
|
|
229
|
+
* serve them.
|
|
230
|
+
*
|
|
231
|
+
* @internal
|
|
232
|
+
*/
|
|
233
|
+
export class EventRegistry {
|
|
234
|
+
server;
|
|
235
|
+
events = new Map();
|
|
236
|
+
constructor(server) {
|
|
237
|
+
this.server = server;
|
|
238
|
+
}
|
|
239
|
+
add(config, hooks) {
|
|
240
|
+
if (this.events.has(config.name)) {
|
|
241
|
+
throw new Error(`Event "${config.name}" is already registered.`);
|
|
242
|
+
}
|
|
243
|
+
if (this.events.size === 0) {
|
|
244
|
+
this.install();
|
|
245
|
+
}
|
|
246
|
+
this.events.set(config.name, {
|
|
247
|
+
name: config.name,
|
|
248
|
+
description: config.description,
|
|
249
|
+
inputSchema: toStandardSchema(config.inputSchema ?? {}),
|
|
250
|
+
payloadSchema: toStandardSchema(config.payloadSchema),
|
|
251
|
+
_meta: config._meta,
|
|
252
|
+
hooks,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
install() {
|
|
256
|
+
this.server.setRequestHandler("events/list", { params: z.object({ cursor: z.string().optional() }).optional() }, () => ({
|
|
257
|
+
events: [...this.events.values()].map((event) => ({
|
|
258
|
+
name: event.name,
|
|
259
|
+
...(event.description && { description: event.description }),
|
|
260
|
+
delivery: ["webhook"],
|
|
261
|
+
inputSchema: event.inputSchema["~standard"].jsonSchema.input({
|
|
262
|
+
target: "draft-2020-12",
|
|
263
|
+
}),
|
|
264
|
+
payloadSchema: event.payloadSchema["~standard"].jsonSchema.output({
|
|
265
|
+
target: "draft-2020-12",
|
|
266
|
+
}),
|
|
267
|
+
...(event._meta && { _meta: event._meta }),
|
|
268
|
+
})),
|
|
269
|
+
}));
|
|
270
|
+
this.server.setRequestHandler("events/subscribe", { params: SubscribeParamsSchema }, async (params, ctx) => {
|
|
271
|
+
if (params.delivery.mode && params.delivery.mode !== "webhook") {
|
|
272
|
+
throw new ProtocolError(EVENTS_ERROR.Unsupported, `Unsupported delivery mode: ${params.delivery.mode}`, { feature: "deliveryMode", value: params.delivery.mode });
|
|
273
|
+
}
|
|
274
|
+
if (!isValidSecret(params.delivery.secret)) {
|
|
275
|
+
throw new ProtocolError(INVALID_PARAMS, "delivery.secret must be whsec_ followed by base64 of 24 to 64 bytes");
|
|
276
|
+
}
|
|
277
|
+
const { event, subscriber, args, id, url } = await this.resolve(params, ctx);
|
|
278
|
+
const ttl = params.ttlMs === null ? MAX_TTL_MS : (params.ttlMs ?? DEFAULT_TTL_MS);
|
|
279
|
+
const subscription = {
|
|
280
|
+
id,
|
|
281
|
+
url,
|
|
282
|
+
secret: params.delivery.secret,
|
|
283
|
+
expiresAt: new Date(Date.now() + Math.min(Math.max(ttl, MIN_TTL_MS), MAX_TTL_MS)),
|
|
284
|
+
};
|
|
285
|
+
await verifyCallback(subscriber, subscription);
|
|
286
|
+
await event.hooks.onSubscribe(args, { subscription, extra: ctx });
|
|
287
|
+
return {
|
|
288
|
+
id,
|
|
289
|
+
refreshBefore: subscription.expiresAt.toISOString(),
|
|
290
|
+
cursor: null,
|
|
291
|
+
truncated: false,
|
|
292
|
+
};
|
|
293
|
+
});
|
|
294
|
+
this.server.setRequestHandler("events/unsubscribe", { params: SubscriptionKeySchema }, async (params, ctx) => {
|
|
295
|
+
const { event, args, id } = await this.resolve(params, ctx);
|
|
296
|
+
await event.hooks.onUnsubscribe(args, {
|
|
297
|
+
subscription: { id },
|
|
298
|
+
extra: ctx,
|
|
299
|
+
});
|
|
300
|
+
return {};
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
async resolve(params, ctx) {
|
|
304
|
+
const event = this.events.get(params.name);
|
|
305
|
+
if (!event) {
|
|
306
|
+
throw new ProtocolError(EVENTS_ERROR.NotFound, `Unknown event: ${params.name}`, { kind: "event" });
|
|
307
|
+
}
|
|
308
|
+
const url = checkCallbackUrl(params.delivery.url);
|
|
309
|
+
const subscriber = subscriberOf(ctx);
|
|
310
|
+
const rawArguments = params.arguments ?? {};
|
|
311
|
+
const args = await validate(event.inputSchema, rawArguments);
|
|
312
|
+
const hash = createHash("sha256")
|
|
313
|
+
.update(canonicalJson([subscriber, url, event.name, rawArguments]))
|
|
314
|
+
.digest("hex");
|
|
315
|
+
return { event, subscriber, args, url, id: `sub_${hash.slice(0, 32)}` };
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/server/events.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash, randomBytes, timingSafeEqual } from \"node:crypto\";\nimport http from \"node:http\";\nimport https from \"node:https\";\nimport {\n INVALID_PARAMS,\n ProtocolError,\n type McpServer as SdkMcpServer,\n type ServerContext,\n type StandardSchemaV1,\n type StandardSchemaWithJSON,\n} from \"@modelcontextprotocol/server\";\nimport { globalHttpAgent, globalHttpsAgent } from \"request-filtering-agent\";\nimport { Webhook } from \"standardwebhooks\";\nimport { z } from \"zod/v4\";\n\nconst EVENTS_ERROR = {\n NotFound: -32011,\n Forbidden: -32012,\n Unsupported: -32014,\n CallbackEndpointError: -32015,\n} as const;\n\nconst MAX_PAYLOAD_BYTES = 256 * 1024;\nconst DELIVERY_TIMEOUT_MS = 10_000;\nconst VERIFICATION_CACHE_MS = 60 * 60_000;\nconst MIN_TTL_MS = 60_000;\nconst DEFAULT_TTL_MS = 30 * 60_000;\nconst MAX_TTL_MS = 24 * 60 * 60_000;\n\n/**\n * A webhook subscription, as passed to `onSubscribe`. Whatever delivers the\n * events POSTs them to `url`, signed with `secret`, until `expiresAt`.\n */\nexport interface EventSubscription {\n /** Stable across refreshes: the same user subscribing to the same event with the same arguments and callback gets the same `id`. */\n id: string;\n /** Callback URL deliveries are POSTed to. */\n url: string;\n /** Standard Webhooks signing secret (`whsec_…`). A refresh may carry a new one. */\n secret: string;\n /** When the subscription lapses unless the host refreshes it. */\n expiresAt: Date;\n}\n\n/** An event occurrence, as passed to {@link deliverEvent}. */\nexport interface EventOccurrence {\n /** Event type name, as registered with `registerEvent`. */\n name: string;\n /** Stable identifier the host uses to drop duplicates, ideally the upstream's. Reuse it when retrying. */\n id: string;\n /** When the event happened. Defaults to now. */\n timestamp?: Date;\n /** Payload matching the event's `payloadSchema`. */\n data: unknown;\n}\n\n/** Outcome of {@link deliverEvent}. Retry with the same event `id` when `retryable`. */\nexport type DeliveryResult =\n | { ok: true }\n | { ok: false; reason: string; retryable: boolean; status?: number };\n\ntype EventSchema =\n | Record<string, StandardSchemaWithJSON>\n | StandardSchemaWithJSON;\n\nexport type InferEventSchema<T extends EventSchema> =\n T extends StandardSchemaWithJSON\n ? StandardSchemaV1.InferOutput<T>\n : T extends Record<string, StandardSchemaWithJSON>\n ? {\n [K in keyof T as undefined extends StandardSchemaV1.InferOutput<T[K]>\n ? never\n : K]: StandardSchemaV1.InferOutput<T[K]>;\n } & {\n [K in keyof T as undefined extends StandardSchemaV1.InferOutput<T[K]>\n ? K\n : never]?: StandardSchemaV1.InferOutput<T[K]>;\n }\n : never;\n\n/** The `registerEvent` config: how the event type is described in `events/list`. */\nexport interface EventConfig<TInput extends EventSchema> {\n /** Stable, specific name such as `comment.created`. */\n name: string;\n /** What the event is and when it fires. */\n description?: string;\n /** Subscription arguments (filters): a Zod shape or a Standard Schema object. */\n inputSchema?: TInput;\n /** Shape of the delivered `data`: a Zod shape or a Standard Schema object. */\n payloadSchema: EventSchema;\n _meta?: Record<string, unknown>;\n}\n\n/** What `registerEvent` does when the host subscribes and unsubscribes. */\nexport interface EventHooks<TArguments, TExtra> {\n /**\n * Runs on every `events/subscribe`, including the host's refreshes, once\n * the callback URL is verified. Hand the subscription to whatever delivers\n * the events, keyed by `subscription.id`. Throw (e.g. a `ProtocolError`) to\n * reject it, for instance when the user may not access what the arguments\n * point to.\n */\n onSubscribe(\n args: TArguments,\n context: { subscription: EventSubscription; extra: TExtra },\n ): void | Promise<void>;\n /** Runs on `events/unsubscribe`. Stop delivering to `subscription.id`. */\n onUnsubscribe(\n args: TArguments,\n context: { subscription: Pick<EventSubscription, \"id\">; extra: TExtra },\n ): void | Promise<void>;\n}\n\ntype RegisteredEvent = {\n name: string;\n description?: string;\n inputSchema: StandardSchemaWithJSON;\n payloadSchema: StandardSchemaWithJSON;\n _meta?: Record<string, unknown>;\n hooks: EventHooks<Record<string, unknown>, ServerContext>;\n};\n\nfunction toStandardSchema(schema: EventSchema): StandardSchemaWithJSON {\n return \"~standard\" in schema\n ? (schema as StandardSchemaWithJSON)\n : z.object(schema as unknown as z.ZodRawShape);\n}\n\nasync function validate(\n schema: StandardSchemaWithJSON,\n value: unknown,\n): Promise<Record<string, unknown>> {\n const result = await schema[\"~standard\"].validate(value);\n if (result.issues) {\n throw new ProtocolError(\n INVALID_PARAMS,\n `Invalid arguments: ${result.issues.map((issue) => issue.message).join(\", \")}`,\n );\n }\n return result.value as Record<string, unknown>;\n}\n\nfunction canonicalJson(value: unknown): string {\n if (Array.isArray(value)) {\n return `[${value.map(canonicalJson).join(\",\")}]`;\n }\n if (value && typeof value === \"object\") {\n return `{${Object.keys(value)\n .sort()\n .map(\n (key) =>\n `${JSON.stringify(key)}:${canonicalJson((value as Record<string, unknown>)[key])}`,\n )\n .join(\",\")}}`;\n }\n return JSON.stringify(value);\n}\n\nfunction subscriberOf(ctx: ServerContext): string {\n const authInfo = ctx.http?.authInfo;\n const subject = authInfo?.extra?.subject;\n const subscriber = typeof subject === \"string\" ? subject : authInfo?.clientId;\n if (!subscriber) {\n throw new ProtocolError(\n EVENTS_ERROR.Forbidden,\n \"Webhook subscriptions require an authenticated caller\",\n );\n }\n return subscriber;\n}\n\nfunction isValidSecret(secret: string): boolean {\n const encoded = secret.startsWith(\"whsec_\") ? secret.slice(6) : \"\";\n if (!/^[A-Za-z0-9+/]+={0,2}$/.test(encoded)) {\n return false;\n }\n const length = Buffer.from(encoded, \"base64\").length;\n return length >= 24 && length <= 64;\n}\n\nconst insecureCallbacksAllowed = () => process.env.NODE_ENV !== \"production\";\n\nfunction checkCallbackUrl(raw: string): string {\n let url: URL;\n try {\n url = new URL(raw);\n } catch {\n throw new ProtocolError(INVALID_PARAMS, \"delivery.url is not a valid URL\");\n }\n const allowed =\n url.protocol === \"https:\" ||\n (url.protocol === \"http:\" && insecureCallbacksAllowed());\n if (!allowed) {\n throw new ProtocolError(INVALID_PARAMS, \"delivery.url must use https\");\n }\n return url.href;\n}\n\ntype PostOutcome =\n | { ok: true; body: string }\n | { ok: false; status?: number; reason: string };\n\nfunction post(\n url: string,\n headers: Record<string, string>,\n body: string,\n): Promise<PostOutcome> {\n const signal = AbortSignal.timeout(DELIVERY_TIMEOUT_MS);\n const failure = (error?: NodeJS.ErrnoException): PostOutcome => ({\n ok: false,\n reason: signal.aborted\n ? \"timeout\"\n : error?.code?.startsWith(\"ERR_TLS\") || error?.code?.includes(\"CERT\")\n ? \"tls_error\"\n : \"connection_refused\",\n });\n const [client, agent] = url.startsWith(\"https:\")\n ? [https, globalHttpsAgent]\n : [http, globalHttpAgent];\n return new Promise((resolve) => {\n const request = client.request(\n url,\n {\n method: \"POST\",\n headers: { ...headers, \"content-length\": Buffer.byteLength(body) },\n signal,\n ...(!insecureCallbacksAllowed() && { agent }),\n },\n (response) => {\n const status = response.statusCode ?? 0;\n const chunks: Buffer[] = [];\n let size = 0;\n response.on(\"data\", (chunk: Buffer) => {\n size += chunk.length;\n if (size <= 64 * 1024) {\n chunks.push(chunk);\n }\n });\n response.on(\"end\", () => {\n if (status >= 200 && status < 300) {\n resolve({ ok: true, body: Buffer.concat(chunks).toString() });\n } else {\n resolve({\n ok: false,\n status,\n reason: status >= 500 ? \"http_5xx\" : \"http_4xx\",\n });\n }\n });\n response.on(\"error\", (error) => resolve(failure(error)));\n response.on(\"close\", () => resolve(failure()));\n },\n );\n request.on(\"error\", (error) => resolve(failure(error)));\n request.end(body);\n });\n}\n\nfunction signedPost(\n subscription: { id: string; url: string; secret: string | string[] },\n messageId: string,\n body: string,\n): Promise<PostOutcome> {\n const signedAt = new Date();\n const secrets = [subscription.secret].flat();\n return post(\n subscription.url,\n {\n \"content-type\": \"application/json\",\n \"webhook-id\": messageId,\n \"webhook-timestamp\": String(Math.floor(signedAt.getTime() / 1000)),\n \"webhook-signature\": secrets\n .map((secret) => new Webhook(secret).sign(messageId, signedAt, body))\n .join(\" \"),\n \"x-mcp-subscription-id\": subscription.id,\n },\n body,\n );\n}\n\n/**\n * @experimental POST one event to a subscription handed out by `onSubscribe`,\n * as MCP Events expects: the event envelope, Standard Webhooks signature\n * headers, a 10 second deadline and, outside development, no requests to\n * private addresses. Pass the old and new secrets during a rotation so both\n * sign the delivery. A subscription past `expiresAt` is not posted to.\n * Retrying is up to the caller. API may change.\n *\n * @example\n * ```ts\n * const result = await deliverEvent(subscription, {\n * name: \"comment.created\",\n * id: comment.id,\n * data: { documentId: comment.documentId, excerpt: comment.text.slice(0, 280) },\n * });\n * if (!result.ok && result.retryable) {\n * await queue.retryLater(subscription, comment);\n * }\n * ```\n *\n * @see https://docs.skybridge.tech/api-reference/register-event\n */\nexport async function deliverEvent(\n subscription: Pick<EventSubscription, \"id\" | \"url\"> & {\n secret: string | string[];\n expiresAt: Date | string;\n },\n event: EventOccurrence,\n): Promise<DeliveryResult> {\n if (!(new Date(subscription.expiresAt).getTime() > Date.now())) {\n return { ok: false, reason: \"expired\", retryable: false };\n }\n const body = JSON.stringify({\n eventId: event.id,\n name: event.name,\n timestamp: (event.timestamp ?? new Date()).toISOString(),\n data: event.data,\n cursor: null,\n });\n if (Buffer.byteLength(body) > MAX_PAYLOAD_BYTES) {\n throw new Error(\n `Event \"${event.name}\" payload exceeds ${MAX_PAYLOAD_BYTES} bytes; send a summary and expose a tool to fetch the rest.`,\n );\n }\n const outcome = await signedPost(subscription, event.id, body);\n if (outcome.ok) {\n return { ok: true };\n }\n const status = outcome.status ?? 0;\n return {\n ...outcome,\n retryable:\n status !== 410 && status !== 413 && (status < 300 || status >= 400),\n };\n}\n\nconst verifiedCallbacks = new Map<string, number>();\n\nasync function verifyCallback(\n subscriber: string,\n subscription: EventSubscription,\n): Promise<void> {\n const key = `${subscriber}\\n${subscription.url}`;\n const now = Date.now();\n if ((verifiedCallbacks.get(key) ?? 0) > now) {\n return;\n }\n const challenge = randomBytes(24).toString(\"base64url\");\n const outcome = await signedPost(\n subscription,\n `msg_verification_${randomBytes(12).toString(\"hex\")}`,\n JSON.stringify({ type: \"verification\", challenge }),\n );\n let echoed: unknown;\n if (outcome.ok) {\n try {\n echoed = JSON.parse(outcome.body)?.challenge;\n } catch {}\n }\n const matched =\n typeof echoed === \"string\" &&\n echoed.length === challenge.length &&\n timingSafeEqual(Buffer.from(echoed), Buffer.from(challenge));\n if (!matched) {\n throw new ProtocolError(\n EVENTS_ERROR.CallbackEndpointError,\n \"Callback endpoint verification failed\",\n { reason: outcome.ok ? \"challenge_failed\" : outcome.reason },\n );\n }\n for (const [cached, until] of verifiedCallbacks) {\n if (until <= now) {\n verifiedCallbacks.delete(cached);\n }\n }\n verifiedCallbacks.set(key, now + VERIFICATION_CACHE_MS);\n}\n\nconst SubscriptionKeySchema = z.object({\n name: z.string(),\n arguments: z.record(z.string(), z.unknown()).optional(),\n delivery: z.object({ url: z.string() }),\n});\n\nconst SubscribeParamsSchema = SubscriptionKeySchema.extend({\n delivery: z.object({\n mode: z.string().optional(),\n url: z.string(),\n secret: z.string(),\n }),\n cursor: z.string().nullable().optional(),\n ttlMs: z.number().nullable().optional(),\n});\n\n/**\n * The event types registered on one server, and the `events/*` handlers that\n * serve them.\n *\n * @internal\n */\nexport class EventRegistry {\n private readonly events = new Map<string, RegisteredEvent>();\n\n constructor(\n private readonly server: Pick<SdkMcpServer[\"server\"], \"setRequestHandler\">,\n ) {}\n\n add(config: EventConfig<EventSchema>, hooks: EventHooks<never, never>): void {\n if (this.events.has(config.name)) {\n throw new Error(`Event \"${config.name}\" is already registered.`);\n }\n if (this.events.size === 0) {\n this.install();\n }\n this.events.set(config.name, {\n name: config.name,\n description: config.description,\n inputSchema: toStandardSchema(config.inputSchema ?? {}),\n payloadSchema: toStandardSchema(config.payloadSchema),\n _meta: config._meta,\n hooks,\n });\n }\n\n private install(): void {\n this.server.setRequestHandler(\n \"events/list\",\n { params: z.object({ cursor: z.string().optional() }).optional() },\n () => ({\n events: [...this.events.values()].map((event) => ({\n name: event.name,\n ...(event.description && { description: event.description }),\n delivery: [\"webhook\"],\n inputSchema: event.inputSchema[\"~standard\"].jsonSchema.input({\n target: \"draft-2020-12\",\n }),\n payloadSchema: event.payloadSchema[\"~standard\"].jsonSchema.output({\n target: \"draft-2020-12\",\n }),\n ...(event._meta && { _meta: event._meta }),\n })),\n }),\n );\n\n this.server.setRequestHandler(\n \"events/subscribe\",\n { params: SubscribeParamsSchema },\n async (params, ctx) => {\n if (params.delivery.mode && params.delivery.mode !== \"webhook\") {\n throw new ProtocolError(\n EVENTS_ERROR.Unsupported,\n `Unsupported delivery mode: ${params.delivery.mode}`,\n { feature: \"deliveryMode\", value: params.delivery.mode },\n );\n }\n if (!isValidSecret(params.delivery.secret)) {\n throw new ProtocolError(\n INVALID_PARAMS,\n \"delivery.secret must be whsec_ followed by base64 of 24 to 64 bytes\",\n );\n }\n const { event, subscriber, args, id, url } = await this.resolve(\n params,\n ctx,\n );\n const ttl =\n params.ttlMs === null ? MAX_TTL_MS : (params.ttlMs ?? DEFAULT_TTL_MS);\n const subscription: EventSubscription = {\n id,\n url,\n secret: params.delivery.secret,\n expiresAt: new Date(\n Date.now() + Math.min(Math.max(ttl, MIN_TTL_MS), MAX_TTL_MS),\n ),\n };\n await verifyCallback(subscriber, subscription);\n await event.hooks.onSubscribe(args, { subscription, extra: ctx });\n return {\n id,\n refreshBefore: subscription.expiresAt.toISOString(),\n cursor: null,\n truncated: false,\n };\n },\n );\n\n this.server.setRequestHandler(\n \"events/unsubscribe\",\n { params: SubscriptionKeySchema },\n async (params, ctx) => {\n const { event, args, id } = await this.resolve(params, ctx);\n await event.hooks.onUnsubscribe(args, {\n subscription: { id },\n extra: ctx,\n });\n return {};\n },\n );\n }\n\n private async resolve(\n params: z.infer<typeof SubscriptionKeySchema>,\n ctx: ServerContext,\n ) {\n const event = this.events.get(params.name);\n if (!event) {\n throw new ProtocolError(\n EVENTS_ERROR.NotFound,\n `Unknown event: ${params.name}`,\n { kind: \"event\" },\n );\n }\n const url = checkCallbackUrl(params.delivery.url);\n const subscriber = subscriberOf(ctx);\n const rawArguments = params.arguments ?? {};\n const args = await validate(event.inputSchema, rawArguments);\n const hash = createHash(\"sha256\")\n .update(canonicalJson([subscriber, url, event.name, rawArguments]))\n .digest(\"hex\");\n return { event, subscriber, args, url, id: `sub_${hash.slice(0, 32)}` };\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { createHmac, randomBytes } from "node:crypto";
|
|
2
|
+
import http from "node:http";
|
|
3
|
+
import net, {} from "node:net";
|
|
4
|
+
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { deliverEvent, Skybridge } from "./index.js";
|
|
8
|
+
async function startReceiver() {
|
|
9
|
+
const received = [];
|
|
10
|
+
const state = { dropResponses: false };
|
|
11
|
+
const server = http.createServer((req, res) => {
|
|
12
|
+
let body = "";
|
|
13
|
+
req.on("data", (chunk) => {
|
|
14
|
+
body += chunk;
|
|
15
|
+
});
|
|
16
|
+
req.on("end", () => {
|
|
17
|
+
received.push({ headers: req.headers, body });
|
|
18
|
+
if (state.dropResponses) {
|
|
19
|
+
res.writeHead(200, { "content-length": "1000" });
|
|
20
|
+
res.write("partial");
|
|
21
|
+
setTimeout(() => res.socket?.destroy(), 20);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const parsed = JSON.parse(body);
|
|
25
|
+
res.setHeader("content-type", "application/json");
|
|
26
|
+
res.end(parsed.type === "verification"
|
|
27
|
+
? JSON.stringify({ challenge: parsed.challenge })
|
|
28
|
+
: "{}");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
32
|
+
const { port } = server.address();
|
|
33
|
+
return { url: `http://127.0.0.1:${port}/hooks`, received, server, state };
|
|
34
|
+
}
|
|
35
|
+
let cleanup;
|
|
36
|
+
afterEach(async () => {
|
|
37
|
+
await cleanup?.();
|
|
38
|
+
cleanup = undefined;
|
|
39
|
+
});
|
|
40
|
+
describe("MCP events", () => {
|
|
41
|
+
it("verifies the callback, hands the subscription to the hooks, and delivers signed events", async () => {
|
|
42
|
+
const receiver = await startReceiver();
|
|
43
|
+
const subscriptions = new Map();
|
|
44
|
+
const app = new Skybridge({
|
|
45
|
+
name: "t",
|
|
46
|
+
version: "0.0.1",
|
|
47
|
+
handler: (server) => server.registerEvent({
|
|
48
|
+
name: "comment.created",
|
|
49
|
+
inputSchema: { documentId: z.string() },
|
|
50
|
+
payloadSchema: { documentId: z.string(), excerpt: z.string() },
|
|
51
|
+
}, {
|
|
52
|
+
onSubscribe: (_args, { subscription }) => {
|
|
53
|
+
subscriptions.set(subscription.id, subscription);
|
|
54
|
+
},
|
|
55
|
+
onUnsubscribe: (_args, { subscription }) => {
|
|
56
|
+
subscriptions.delete(subscription.id);
|
|
57
|
+
},
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
61
|
+
const send = clientTransport.send.bind(clientTransport);
|
|
62
|
+
clientTransport.send = (message) => send(message, {
|
|
63
|
+
authInfo: {
|
|
64
|
+
token: "t",
|
|
65
|
+
clientId: "chatgpt",
|
|
66
|
+
scopes: [],
|
|
67
|
+
extra: { subject: "user-1" },
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
const instance = await app.createServerInstance();
|
|
71
|
+
await instance.connect(serverTransport);
|
|
72
|
+
const client = new Client({ name: "c", version: "0.0.1" });
|
|
73
|
+
await client.connect(clientTransport);
|
|
74
|
+
cleanup = async () => {
|
|
75
|
+
await client.close();
|
|
76
|
+
await instance.close();
|
|
77
|
+
receiver.server.close();
|
|
78
|
+
};
|
|
79
|
+
expect(instance.getCapabilities().events).toEqual({});
|
|
80
|
+
const list = await client.request({ method: "events/list", params: {} }, z.object({
|
|
81
|
+
events: z.array(z.object({ name: z.string(), delivery: z.array(z.string()) })),
|
|
82
|
+
}));
|
|
83
|
+
expect(list.events).toEqual([
|
|
84
|
+
expect.objectContaining({
|
|
85
|
+
name: "comment.created",
|
|
86
|
+
delivery: ["webhook"],
|
|
87
|
+
}),
|
|
88
|
+
]);
|
|
89
|
+
const secret = `whsec_${randomBytes(32).toString("base64")}`;
|
|
90
|
+
const subscription = await client.request({
|
|
91
|
+
method: "events/subscribe",
|
|
92
|
+
params: {
|
|
93
|
+
name: "comment.created",
|
|
94
|
+
arguments: { documentId: "doc-1" },
|
|
95
|
+
delivery: { mode: "webhook", url: receiver.url, secret },
|
|
96
|
+
},
|
|
97
|
+
}, z.object({ id: z.string(), refreshBefore: z.string() }));
|
|
98
|
+
expect(receiver.received.map(({ body }) => JSON.parse(body).type)).toEqual([
|
|
99
|
+
"verification",
|
|
100
|
+
]);
|
|
101
|
+
const stored = subscriptions.get(subscription.id);
|
|
102
|
+
expect(stored).toMatchObject({ url: receiver.url, secret });
|
|
103
|
+
if (!stored) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
await expect(deliverEvent(stored, {
|
|
107
|
+
name: "comment.created",
|
|
108
|
+
id: "evt_1",
|
|
109
|
+
data: { documentId: "doc-1", excerpt: "hello" },
|
|
110
|
+
})).resolves.toEqual({ ok: true });
|
|
111
|
+
expect(receiver.received).toHaveLength(2);
|
|
112
|
+
const [, delivery] = receiver.received;
|
|
113
|
+
expect(JSON.parse(delivery?.body ?? "")).toMatchObject({
|
|
114
|
+
eventId: "evt_1",
|
|
115
|
+
name: "comment.created",
|
|
116
|
+
data: { documentId: "doc-1", excerpt: "hello" },
|
|
117
|
+
});
|
|
118
|
+
const headers = delivery?.headers ?? {};
|
|
119
|
+
expect(headers["x-mcp-subscription-id"]).toBe(subscription.id);
|
|
120
|
+
const expected = createHmac("sha256", Buffer.from(secret.slice(6), "base64"))
|
|
121
|
+
.update(`${headers["webhook-id"]}.${headers["webhook-timestamp"]}.${delivery?.body}`)
|
|
122
|
+
.digest("base64");
|
|
123
|
+
expect(headers["webhook-signature"]).toBe(`v1,${expected}`);
|
|
124
|
+
receiver.state.dropResponses = true;
|
|
125
|
+
const delivered = deliverEvent(stored, {
|
|
126
|
+
name: "comment.created",
|
|
127
|
+
id: "evt_2",
|
|
128
|
+
data: { documentId: "doc-1", excerpt: "dropped" },
|
|
129
|
+
});
|
|
130
|
+
await expect(Promise.race([
|
|
131
|
+
delivered,
|
|
132
|
+
new Promise((resolve) => setTimeout(() => resolve("hung"), 2_000)),
|
|
133
|
+
])).resolves.toMatchObject({ ok: false, retryable: true });
|
|
134
|
+
await client.request({
|
|
135
|
+
method: "events/unsubscribe",
|
|
136
|
+
params: {
|
|
137
|
+
name: "comment.created",
|
|
138
|
+
arguments: { documentId: "doc-1" },
|
|
139
|
+
delivery: { url: receiver.url },
|
|
140
|
+
},
|
|
141
|
+
}, z.object({}));
|
|
142
|
+
expect(subscriptions.size).toBe(0);
|
|
143
|
+
let connections = 0;
|
|
144
|
+
const privateTarget = net.createServer((socket) => {
|
|
145
|
+
connections += 1;
|
|
146
|
+
socket.destroy();
|
|
147
|
+
});
|
|
148
|
+
await new Promise((resolve) => privateTarget.listen(0, "127.0.0.1", resolve));
|
|
149
|
+
const { port: privatePort } = privateTarget.address();
|
|
150
|
+
const nodeEnv = process.env.NODE_ENV;
|
|
151
|
+
process.env.NODE_ENV = "production";
|
|
152
|
+
try {
|
|
153
|
+
await expect(client.request({
|
|
154
|
+
method: "events/subscribe",
|
|
155
|
+
params: {
|
|
156
|
+
name: "comment.created",
|
|
157
|
+
arguments: { documentId: "doc-1" },
|
|
158
|
+
delivery: {
|
|
159
|
+
url: `https://127.0.0.1:${privatePort}/hooks`,
|
|
160
|
+
secret,
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
}, z.object({}))).rejects.toMatchObject({ code: -32015 });
|
|
164
|
+
expect(connections).toBe(0);
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
process.env.NODE_ENV = nodeEnv;
|
|
168
|
+
privateTarget.close();
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
//# sourceMappingURL=events.test.js.map
|