@gjermundgaraba/effect-actions 0.19.0 → 0.21.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/README.md +19 -41
- package/dist/Action.d.mts +140 -48
- package/dist/Action.mjs +88 -44
- package/dist/ActionCli.d.mts +70 -35
- package/dist/ActionCli.mjs +53 -34
- package/dist/ActionHttp.d.mts +70 -38
- package/dist/ActionHttp.mjs +76 -25
- package/dist/ActionMcp.d.mts +56 -24
- package/dist/ActionMcp.mjs +104 -61
- package/dist/ActionRpc.d.mts +222 -57
- package/dist/ActionRpc.mjs +301 -49
- package/dist/ActionToolkit.d.mts +22 -7
- package/dist/ActionToolkit.mjs +37 -14
- package/dist/Authentication.d.mts +42 -12
- package/dist/Authentication.mjs +38 -32
- package/dist/Event.d.mts +370 -0
- package/dist/Event.mjs +103 -0
- package/dist/Testing.d.mts +1 -3
- package/dist/Testing.mjs +2 -3
- package/dist/Webhook.d.mts +146 -0
- package/dist/Webhook.mjs +72 -0
- package/dist/authentication/provider.d.mts +35 -12
- package/dist/authentication/provider.mjs +2 -3
- package/dist/authentication/refusal.mjs +3 -4
- package/dist/cli/commands.d.mts +9 -7
- package/dist/cli/commands.mjs +85 -35
- package/dist/contract/ast.mjs +5 -3
- package/dist/contract/authorizer.d.mts +8 -0
- package/dist/contract/authorizer.mjs +20 -0
- package/dist/contract/call.d.mts +9 -8
- package/dist/contract/call.mjs +49 -8
- package/dist/contract/callback.d.mts +7 -0
- package/dist/contract/callback.mjs +27 -0
- package/dist/contract/caller.d.mts +12 -3
- package/dist/contract/caller.mjs +23 -5
- package/dist/contract/canonical.mjs +10 -0
- package/dist/contract/codec.d.mts +9 -0
- package/dist/contract/codec.mjs +9 -0
- package/dist/contract/codes.mjs +81 -0
- package/dist/contract/declared.d.mts +0 -2
- package/dist/contract/declared.mjs +26 -11
- package/dist/contract/digest.mjs +6 -0
- package/dist/contract/errors.d.mts +9 -5
- package/dist/contract/errors.mjs +2 -3
- package/dist/contract/feed.d.mts +127 -0
- package/dist/contract/feed.mjs +249 -0
- package/dist/contract/hints.mjs +1 -2
- package/dist/contract/hub.d.mts +94 -0
- package/dist/contract/hub.mjs +133 -0
- package/dist/contract/idempotency.d.mts +47 -0
- package/dist/contract/idempotency.mjs +99 -0
- package/dist/contract/implementation.d.mts +73 -51
- package/dist/contract/implementation.mjs +94 -60
- package/dist/contract/input-check.d.mts +15 -0
- package/dist/contract/input-check.mjs +16 -0
- package/dist/contract/lease.mjs +28 -0
- package/dist/contract/listed.d.mts +4 -0
- package/dist/contract/listed.mjs +22 -0
- package/dist/contract/log.d.mts +41 -0
- package/dist/contract/log.mjs +144 -0
- package/dist/contract/media.d.mts +1 -3
- package/dist/contract/media.mjs +1 -2
- package/dist/contract/names.mjs +15 -6
- package/dist/contract/occurrence.d.mts +29 -0
- package/dist/contract/occurrence.mjs +45 -0
- package/dist/contract/pages.d.mts +13 -0
- package/dist/contract/pages.mjs +39 -0
- package/dist/contract/progress.d.mts +26 -0
- package/dist/contract/progress.mjs +72 -0
- package/dist/contract/push.d.mts +20 -0
- package/dist/contract/push.mjs +96 -0
- package/dist/contract/receiving.d.mts +29 -0
- package/dist/contract/receiving.mjs +42 -0
- package/dist/contract/recent.mjs +12 -0
- package/dist/contract/reported.mjs +16 -0
- package/dist/contract/serving.mjs +63 -0
- package/dist/contract/status.mjs +15 -7
- package/dist/contract/stream.d.mts +34 -0
- package/dist/contract/stream.mjs +31 -0
- package/dist/contract/subscribe.d.mts +45 -0
- package/dist/contract/subscribe.mjs +50 -0
- package/dist/contract/transient.mjs +28 -0
- package/dist/contract/watch.d.mts +18 -0
- package/dist/contract/watch.mjs +105 -0
- package/dist/contract/webhooks.d.mts +42 -0
- package/dist/contract/webhooks.mjs +53 -0
- package/dist/http/client.d.mts +37 -14
- package/dist/http/client.mjs +52 -14
- package/dist/http/events.d.mts +56 -0
- package/dist/http/events.mjs +102 -0
- package/dist/http/placed.d.mts +36 -0
- package/dist/http/placed.mjs +71 -0
- package/dist/http/schema-errors.mjs +2 -3
- package/dist/http/streamed.d.mts +21 -0
- package/dist/http/streamed.mjs +94 -0
- package/dist/http/watch.mjs +50 -0
- package/dist/mcp/bridge.mjs +283 -0
- package/dist/mcp/client.d.mts +30 -99
- package/dist/mcp/client.mjs +60 -134
- package/dist/mcp/events.mjs +142 -0
- package/dist/mcp/progress.mjs +34 -0
- package/dist/mcp/protocol.mjs +40 -15
- package/dist/mcp/reply.d.mts +79 -0
- package/dist/mcp/reply.mjs +99 -0
- package/dist/mcp/request.d.mts +40 -0
- package/dist/mcp/request.mjs +51 -0
- package/dist/mcp/watch.mjs +115 -0
- package/dist/rpc/watch.mjs +39 -0
- package/dist/stdio/console.mjs +14 -7
- package/dist/testing/memory.d.mts +1 -3
- package/dist/testing/memory.mjs +4 -5
- package/dist/webhook/address.mjs +47 -0
- package/dist/webhook/attempt.mjs +65 -0
- package/dist/webhook/receiver.mjs +67 -0
- package/dist/webhook/signing.mjs +28 -0
- package/dist/webhook/stored.mjs +57 -0
- package/dist/webhook/subscriptions.mjs +342 -0
- package/docs/Action-failures.md +2 -2
- package/docs/Action-streams.md +115 -0
- package/docs/Action.md +58 -124
- package/docs/ActionCli-failures.md +1 -2
- package/docs/ActionCli.md +83 -88
- package/docs/ActionHttp-failures.md +3 -3
- package/docs/ActionHttp.md +90 -70
- package/docs/ActionMcp-failures.md +2 -1
- package/docs/ActionMcp.md +120 -92
- package/docs/ActionRpc-events.md +126 -0
- package/docs/ActionRpc-failures.md +1 -2
- package/docs/ActionRpc.md +108 -79
- package/docs/ActionToolkit-failures.md +1 -2
- package/docs/ActionToolkit.md +33 -90
- package/docs/Authentication-advanced.md +57 -61
- package/docs/Authentication-failures.md +5 -4
- package/docs/Authentication.md +23 -58
- package/docs/Event-failures.md +15 -0
- package/docs/Event.md +281 -0
- package/docs/README.md +51 -82
- package/docs/Testing-failures.md +1 -5
- package/docs/Testing.md +126 -86
- package/docs/Webhook-failures.md +8 -0
- package/docs/Webhook.md +319 -0
- package/docs/setup-failures.md +2 -3
- package/docs/setup.md +32 -53
- package/examples/README.md +12 -37
- package/examples/assistant/README.md +21 -26
- package/examples/assistant/src/agent.ts +66 -24
- package/examples/assistant/src/cli.ts +7 -4
- package/examples/assistant/src/commands.ts +9 -4
- package/examples/assistant/src/identity/Identity.ts +5 -4
- package/examples/assistant/src/identity/authorize.ts +13 -5
- package/examples/assistant/src/identity/handlers.ts +3 -2
- package/examples/assistant/src/identity/permissions.ts +9 -3
- package/examples/assistant/src/platform/tasks-file.ts +15 -11
- package/examples/assistant/src/scripted.ts +7 -4
- package/examples/assistant/src/tasks/Tasks.ts +20 -6
- package/examples/assistant/src/tasks/handlers.ts +9 -5
- package/examples/assistant/src/tasks/store.ts +38 -11
- package/examples/assistant/src/toolkit.ts +11 -7
- package/examples/assistant/test/commands.test.ts +53 -27
- package/examples/assistant/test/identity/identity.test.ts +27 -11
- package/examples/assistant/test/platform/tasks-file.test.ts +39 -23
- package/examples/assistant/test/tasks/tasks.test.ts +62 -34
- package/examples/assistant/test/toolkit.test.ts +104 -75
- package/examples/build-events/README.md +83 -0
- package/examples/build-events/src/Api.ts +27 -0
- package/examples/build-events/src/Rpc.ts +26 -0
- package/examples/build-events/src/builds/Builds.ts +108 -0
- package/examples/build-events/src/builds/events.ts +54 -0
- package/examples/build-events/src/builds/handlers.ts +58 -0
- package/examples/build-events/src/builds/log.ts +30 -0
- package/examples/build-events/src/builds/status.ts +28 -0
- package/examples/build-events/src/builds/store.ts +130 -0
- package/examples/build-events/src/cli.ts +18 -0
- package/examples/build-events/src/commands.ts +32 -0
- package/examples/build-events/src/deploys/Deploys.ts +44 -0
- package/examples/build-events/src/deploys/approvers.ts +55 -0
- package/examples/build-events/src/deploys/events.ts +32 -0
- package/examples/build-events/src/deploys/handlers.ts +38 -0
- package/examples/build-events/src/identity/Identity.ts +29 -0
- package/examples/build-events/src/identity/members.ts +7 -0
- package/examples/build-events/src/identity/verifier.ts +22 -0
- package/examples/build-events/src/notifier/Notices.ts +26 -0
- package/examples/build-events/src/notifier/routes.ts +71 -0
- package/examples/build-events/src/notifier.ts +48 -0
- package/examples/build-events/src/routes.ts +71 -0
- package/examples/build-events/src/server.ts +62 -0
- package/examples/build-events/test/builds/events.test.ts +68 -0
- package/examples/build-events/test/builds/log.test.ts +71 -0
- package/examples/build-events/test/builds/progress.test.ts +57 -0
- package/examples/build-events/test/builds/status.test.ts +42 -0
- package/examples/build-events/test/commands.test.ts +103 -0
- package/examples/build-events/test/deploys/events.test.ts +52 -0
- package/examples/build-events/test/http.test.ts +95 -0
- package/examples/build-events/test/notifier.test.ts +80 -0
- package/examples/build-events/test/routes.test.ts +96 -0
- package/examples/build-events/test/rpc.test.ts +84 -0
- package/examples/build-events/test/support/as.ts +8 -0
- package/examples/build-events/test/support/memory.ts +43 -0
- package/examples/build-events/test/webhooks.test.ts +143 -0
- package/examples/kitchen-sink/README.md +178 -166
- package/examples/kitchen-sink/src/Actions.ts +11 -3
- package/examples/kitchen-sink/src/Api.ts +12 -6
- package/examples/kitchen-sink/src/Rpc.ts +7 -5
- package/examples/kitchen-sink/src/admin/Admin.ts +21 -10
- package/examples/kitchen-sink/src/admin/authenticate.ts +8 -4
- package/examples/kitchen-sink/src/admin/authorize.ts +13 -5
- package/examples/kitchen-sink/src/admin/grants.ts +5 -3
- package/examples/kitchen-sink/src/admin/handlers.ts +9 -5
- package/examples/kitchen-sink/src/agent.ts +14 -6
- package/examples/kitchen-sink/src/assistant/Assistant.ts +21 -15
- package/examples/kitchen-sink/src/assistant/conversations.ts +14 -9
- package/examples/kitchen-sink/src/assistant/demo-model.ts +2 -2
- package/examples/kitchen-sink/src/assistant/handlers.ts +42 -22
- package/examples/kitchen-sink/src/attachments/Attachments.ts +6 -6
- package/examples/kitchen-sink/src/attachments/download.ts +7 -6
- package/examples/kitchen-sink/src/attachments/handlers.ts +9 -4
- package/examples/kitchen-sink/src/attachments/store.ts +25 -18
- package/examples/kitchen-sink/src/browser.ts +10 -4
- package/examples/kitchen-sink/src/cli.ts +28 -12
- package/examples/kitchen-sink/src/config.ts +1 -1
- package/examples/kitchen-sink/src/identity/Identity.ts +24 -10
- package/examples/kitchen-sink/src/identity/authenticate.ts +19 -11
- package/examples/kitchen-sink/src/identity/authorize.ts +14 -4
- package/examples/kitchen-sink/src/identity/directory.ts +16 -7
- package/examples/kitchen-sink/src/identity/handlers.ts +1 -0
- package/examples/kitchen-sink/src/identity/members.ts +7 -2
- package/examples/kitchen-sink/src/identity/quota.ts +23 -20
- package/examples/kitchen-sink/src/issues/Issues.ts +91 -15
- package/examples/kitchen-sink/src/issues/assistant.ts +4 -2
- package/examples/kitchen-sink/src/issues/events.ts +46 -0
- package/examples/kitchen-sink/src/issues/export.ts +35 -0
- package/examples/kitchen-sink/src/issues/guide.ts +4 -3
- package/examples/kitchen-sink/src/issues/handlers.ts +20 -7
- package/examples/kitchen-sink/src/issues/store.ts +156 -67
- package/examples/kitchen-sink/src/issues/write-limit.ts +44 -18
- package/examples/kitchen-sink/src/memory.ts +11 -2
- package/examples/kitchen-sink/src/ops.ts +10 -5
- package/examples/kitchen-sink/src/request-policy.ts +25 -9
- package/examples/kitchen-sink/src/routes.ts +67 -27
- package/examples/kitchen-sink/src/sdk.ts +22 -12
- package/examples/kitchen-sink/src/seed.ts +15 -9
- package/examples/kitchen-sink/src/server.ts +24 -10
- package/examples/kitchen-sink/src/stdio.ts +9 -3
- package/examples/kitchen-sink/src/workspaces/Workspaces.ts +10 -7
- package/examples/kitchen-sink/src/workspaces/handlers.ts +3 -2
- package/examples/kitchen-sink/src/workspaces/registry.ts +16 -10
- package/examples/kitchen-sink/src/workspaces/tenant.ts +12 -5
- package/examples/kitchen-sink/test/admin/admin.test.ts +38 -23
- package/examples/kitchen-sink/test/assistant/assistant.test.ts +471 -299
- package/examples/kitchen-sink/test/attachments/attachments.test.ts +135 -94
- package/examples/kitchen-sink/test/identity/identity.test.ts +110 -82
- package/examples/kitchen-sink/test/identity/session.test.ts +65 -43
- package/examples/kitchen-sink/test/issues/assistant.test.ts +50 -31
- package/examples/kitchen-sink/test/issues/events.test.ts +273 -0
- package/examples/kitchen-sink/test/issues/export.test.ts +120 -0
- package/examples/kitchen-sink/test/issues/issues.test.ts +105 -75
- package/examples/kitchen-sink/test/request-policy.test.ts +130 -33
- package/examples/kitchen-sink/test/routes.test.ts +102 -57
- package/examples/kitchen-sink/test/support/as.ts +9 -0
- package/examples/kitchen-sink/test/support/scripted.ts +6 -5
- package/examples/kitchen-sink/test/support/services.ts +17 -0
- package/examples/live-rpc/README.md +33 -35
- package/examples/live-rpc/src/Rpc.ts +9 -5
- package/examples/live-rpc/src/browser.ts +39 -11
- package/examples/live-rpc/src/chat/Chat.ts +30 -16
- package/examples/live-rpc/src/chat/authorize.ts +2 -1
- package/examples/live-rpc/src/chat/events.ts +74 -0
- package/examples/live-rpc/src/chat/handlers.ts +13 -5
- package/examples/live-rpc/src/chat/room.ts +9 -5
- package/examples/live-rpc/src/chat/store.ts +51 -14
- package/examples/live-rpc/src/client.ts +22 -5
- package/examples/live-rpc/src/identity/Identity.ts +3 -3
- package/examples/live-rpc/src/identity/verifier.ts +8 -4
- package/examples/live-rpc/src/page.ts +58 -12
- package/examples/live-rpc/src/routes.ts +14 -10
- package/examples/live-rpc/src/server.ts +15 -5
- package/examples/live-rpc/test/browser.test.ts +127 -50
- package/examples/live-rpc/test/chat/chat.test.ts +215 -23
- package/examples/live-rpc/test/routes.test.ts +130 -46
- package/examples/live-rpc/test/server.test.ts +94 -33
- package/examples/mcp-tools/README.md +24 -38
- package/examples/mcp-tools/src/platform/fake-farm.ts +63 -24
- package/examples/mcp-tools/src/platform/print-farm.ts +59 -29
- package/examples/mcp-tools/src/printers/Printers.ts +18 -7
- package/examples/mcp-tools/src/printers/farm.ts +15 -5
- package/examples/mcp-tools/src/printers/handlers.ts +12 -6
- package/examples/mcp-tools/src/routes.ts +8 -6
- package/examples/mcp-tools/src/server.ts +12 -5
- package/examples/mcp-tools/src/stdio.ts +3 -2
- package/examples/mcp-tools/test/printers/printers.test.ts +132 -89
- package/examples/mcp-tools/test/routes.test.ts +140 -81
- package/examples/notes-service/README.md +30 -45
- package/examples/notes-service/src/Api.ts +1 -0
- package/examples/notes-service/src/browser.ts +15 -14
- package/examples/notes-service/src/cli.ts +8 -3
- package/examples/notes-service/src/commands.ts +46 -20
- package/examples/notes-service/src/identity/Identity.ts +2 -2
- package/examples/notes-service/src/identity/authorize.ts +6 -2
- package/examples/notes-service/src/identity/handlers.ts +3 -2
- package/examples/notes-service/src/identity/users.ts +2 -1
- package/examples/notes-service/src/identity/verifier.ts +12 -6
- package/examples/notes-service/src/notes/Notes.ts +46 -22
- package/examples/notes-service/src/notes/handlers.ts +18 -9
- package/examples/notes-service/src/notes/store.ts +70 -43
- package/examples/notes-service/src/routes.ts +7 -5
- package/examples/notes-service/src/server.ts +12 -5
- package/examples/notes-service/src/status/Status.ts +1 -1
- package/examples/notes-service/src/status/handlers.ts +5 -3
- package/examples/notes-service/src/stdio.ts +7 -3
- package/examples/notes-service/src/worker.ts +6 -3
- package/examples/notes-service/test/commands.test.ts +164 -91
- package/examples/notes-service/test/notes/as.ts +3 -1
- package/examples/notes-service/test/notes/idempotency.test.ts +97 -67
- package/examples/notes-service/test/notes/in-process.test.ts +26 -15
- package/examples/notes-service/test/notes/notes.test.ts +49 -31
- package/examples/notes-service/test/routes.test.ts +106 -71
- package/examples/notes-service/test/worker.test.ts +21 -13
- package/examples/quickstart/README.md +2 -6
- package/examples/quickstart/src/Api.ts +1 -0
- package/examples/quickstart/src/client.ts +6 -3
- package/examples/quickstart/src/greeting/Greeting.ts +1 -1
- package/examples/quickstart/src/greeting/handlers.ts +5 -2
- package/examples/quickstart/src/routes.ts +3 -2
- package/examples/quickstart/src/server.ts +6 -2
- package/examples/quickstart/test/routes.test.ts +3 -2
- package/package.json +19 -17
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import "../contract/progress.mjs";
|
|
2
|
+
import "../Action.mjs";
|
|
3
|
+
import { Effect, Schema, Stream } from "effect";
|
|
4
|
+
import { HttpClientError } from "effect/http";
|
|
5
|
+
declare const StatusError_base: Schema.Class<StatusError, Schema.TaggedStruct<"StatusError", {
|
|
6
|
+
readonly status: Schema.Int;
|
|
7
|
+
readonly body: Schema.String;
|
|
8
|
+
}>, import("effect/Cause").YieldableError & {
|
|
9
|
+
readonly brand: unique symbol;
|
|
10
|
+
}>;
|
|
11
|
+
/** The endpoint answered with an HTTP status other than 200, and a body no declared error decodes. */
|
|
12
|
+
export declare class StatusError extends StatusError_base {}
|
|
13
|
+
declare const ProtocolError_base: Schema.Class<ProtocolError, Schema.TaggedStruct<"ProtocolError", {
|
|
14
|
+
readonly code: Schema.Finite;
|
|
15
|
+
readonly message: Schema.String;
|
|
16
|
+
}>, import("effect/Cause").YieldableError & {
|
|
17
|
+
readonly brand: unique symbol;
|
|
18
|
+
}>;
|
|
19
|
+
/**
|
|
20
|
+
* The endpoint answered with a JSON-RPC error, such as the native server's for an unknown tool.
|
|
21
|
+
*/
|
|
22
|
+
export declare class ProtocolError extends ProtocolError_base {}
|
|
23
|
+
declare const NoReply_base: Schema.Class<NoReply, Schema.TaggedStruct<"NoReply", {
|
|
24
|
+
readonly body: Schema.String;
|
|
25
|
+
}>, import("effect/Cause").YieldableError & {
|
|
26
|
+
readonly brand: unique symbol;
|
|
27
|
+
}>;
|
|
28
|
+
/** The endpoint answered 200 without a JSON-RPC reply to read, as JSON or as an event stream. */
|
|
29
|
+
export declare class NoReply extends NoReply_base {}
|
|
30
|
+
declare const UndeclaredError_base: Schema.Class<UndeclaredError, Schema.TaggedStruct<"UndeclaredError", {
|
|
31
|
+
readonly text: Schema.String;
|
|
32
|
+
}>, import("effect/Cause").YieldableError & {
|
|
33
|
+
readonly brand: unique symbol;
|
|
34
|
+
}>;
|
|
35
|
+
/** The tool returned an error whose text decodes as none of the action's declared errors. */
|
|
36
|
+
export declare class UndeclaredError extends UndeclaredError_base {}
|
|
37
|
+
declare const NoStructuredContent_base: Schema.Class<NoStructuredContent, Schema.TaggedStruct<"NoStructuredContent", {
|
|
38
|
+
readonly body: Schema.String;
|
|
39
|
+
}>, import("effect/Cause").YieldableError & {
|
|
40
|
+
readonly brand: unique symbol;
|
|
41
|
+
}>;
|
|
42
|
+
/** The tool returned a result without the structured content its action's success needs. */
|
|
43
|
+
export declare class NoStructuredContent extends NoStructuredContent_base {}
|
|
44
|
+
declare const FrameTooLarge_base: Schema.Class<FrameTooLarge, Schema.TaggedStruct<"FrameTooLarge", {
|
|
45
|
+
readonly maxEventSize: Schema.Int;
|
|
46
|
+
}>, import("effect/Cause").YieldableError & {
|
|
47
|
+
readonly brand: unique symbol;
|
|
48
|
+
}>;
|
|
49
|
+
/**
|
|
50
|
+
* The endpoint's event stream held more than `maxEventSize` characters with no frame ending, the
|
|
51
|
+
* most a client holds while it waits for one.
|
|
52
|
+
*
|
|
53
|
+
* **Gotchas**
|
|
54
|
+
*
|
|
55
|
+
* - The limit is on what is held unended, as Effect's SSE parser checks it after each chunk: a frame larger than it that ends within the chunk carrying it is read.
|
|
56
|
+
*/
|
|
57
|
+
export declare class FrameTooLarge extends FrameTooLarge_base {}
|
|
58
|
+
declare const McpCallError_base: Schema.Class<McpCallError, Schema.TaggedStruct<"McpCallError", {
|
|
59
|
+
readonly method: Schema.String;
|
|
60
|
+
readonly target: Schema.String;
|
|
61
|
+
readonly reason: Schema.Union<readonly [typeof StatusError, typeof ProtocolError, typeof NoReply, typeof UndeclaredError, typeof NoStructuredContent, typeof FrameTooLarge]>;
|
|
62
|
+
readonly message: Schema.String;
|
|
63
|
+
}>, import("effect/Cause").YieldableError & {
|
|
64
|
+
readonly brand: unique symbol;
|
|
65
|
+
}>;
|
|
66
|
+
/**
|
|
67
|
+
* An answer a client cannot read as the contract's: `method` is the request, `tools/call` or an
|
|
68
|
+
* `events/*` one, `target` the tool or event it named, and `reason` says what was wrong, with what
|
|
69
|
+
* the endpoint sent, so a caller branches on it with
|
|
70
|
+
* `Effect.catchReason("McpCallError", "StatusError", ...)`. `message` names all three.
|
|
71
|
+
* Structured content that does not decode as the success is a `SchemaError` instead, and a
|
|
72
|
+
* request that fails an `HttpClientError`.
|
|
73
|
+
*
|
|
74
|
+
* **Gotchas**
|
|
75
|
+
*
|
|
76
|
+
* - `returned an error: Invalid parameters for tool …`, an `UndeclaredError`, from a call its types allowed: the endpoint serves another contract under that name; give `client` the served contract value.
|
|
77
|
+
*/
|
|
78
|
+
export declare class McpCallError extends McpCallError_base {}
|
|
79
|
+
export {};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { progressOf } from "./progress.mjs";
|
|
2
|
+
import { Effect, Match, Option, Predicate, Schema, Stream } from "effect";
|
|
3
|
+
import { Sse } from "effect/encoding";
|
|
4
|
+
// src/mcp/reply.ts
|
|
5
|
+
/** The endpoint answered with an HTTP status other than 200, and a body no declared error decodes. */
|
|
6
|
+
var StatusError = class extends Schema.TaggedError()("StatusError", {
|
|
7
|
+
status: Schema.Int,
|
|
8
|
+
body: Schema.String
|
|
9
|
+
}) {};
|
|
10
|
+
/**
|
|
11
|
+
* The endpoint answered with a JSON-RPC error, such as the native server's for an unknown tool.
|
|
12
|
+
*/
|
|
13
|
+
var ProtocolError = class extends Schema.TaggedError()("ProtocolError", {
|
|
14
|
+
code: Schema.Finite,
|
|
15
|
+
message: Schema.String
|
|
16
|
+
}) {};
|
|
17
|
+
/** The endpoint answered 200 without a JSON-RPC reply to read, as JSON or as an event stream. */
|
|
18
|
+
var NoReply = class extends Schema.TaggedError()("NoReply", { body: Schema.String }) {};
|
|
19
|
+
/** The tool returned an error whose text decodes as none of the action's declared errors. */
|
|
20
|
+
var UndeclaredError = class extends Schema.TaggedError()("UndeclaredError", { text: Schema.String }) {};
|
|
21
|
+
/** The tool returned a result without the structured content its action's success needs. */
|
|
22
|
+
var NoStructuredContent = class extends Schema.TaggedError()("NoStructuredContent", { body: Schema.String }) {};
|
|
23
|
+
/**
|
|
24
|
+
* The endpoint's event stream held more than `maxEventSize` characters with no frame ending, the
|
|
25
|
+
* most a client holds while it waits for one.
|
|
26
|
+
*
|
|
27
|
+
* **Gotchas**
|
|
28
|
+
*
|
|
29
|
+
* - The limit is on what is held unended, as Effect's SSE parser checks it after each chunk: a frame larger than it that ends within the chunk carrying it is read.
|
|
30
|
+
*/
|
|
31
|
+
var FrameTooLarge = class extends Schema.TaggedError()("FrameTooLarge", { maxEventSize: Schema.Int }) {};
|
|
32
|
+
const Reason = Schema.Union([
|
|
33
|
+
StatusError,
|
|
34
|
+
ProtocolError,
|
|
35
|
+
NoReply,
|
|
36
|
+
UndeclaredError,
|
|
37
|
+
NoStructuredContent,
|
|
38
|
+
FrameTooLarge
|
|
39
|
+
]);
|
|
40
|
+
const sentenceOf = Match.typeTags()({
|
|
41
|
+
StatusError: ({ status, body }) => `answered ${status}: ${body}`,
|
|
42
|
+
ProtocolError: ({ code, message }) => `failed with ${code}: ${message}`,
|
|
43
|
+
NoReply: ({ body }) => `had no reply: ${body}`,
|
|
44
|
+
UndeclaredError: ({ text }) => `returned an error: ${text}`,
|
|
45
|
+
NoStructuredContent: ({ body }) => `returned no structured content: ${body}`,
|
|
46
|
+
FrameTooLarge: ({ maxEventSize }) => `sent more than ${maxEventSize} characters of an event stream with no frame ending`
|
|
47
|
+
});
|
|
48
|
+
/**
|
|
49
|
+
* An answer a client cannot read as the contract's: `method` is the request, `tools/call` or an
|
|
50
|
+
* `events/*` one, `target` the tool or event it named, and `reason` says what was wrong, with what
|
|
51
|
+
* the endpoint sent, so a caller branches on it with
|
|
52
|
+
* `Effect.catchReason("McpCallError", "StatusError", ...)`. `message` names all three.
|
|
53
|
+
* Structured content that does not decode as the success is a `SchemaError` instead, and a
|
|
54
|
+
* request that fails an `HttpClientError`.
|
|
55
|
+
*
|
|
56
|
+
* **Gotchas**
|
|
57
|
+
*
|
|
58
|
+
* - `returned an error: Invalid parameters for tool …`, an `UndeclaredError`, from a call its types allowed: the endpoint serves another contract under that name; give `client` the served contract value.
|
|
59
|
+
*/
|
|
60
|
+
var McpCallError = class extends Schema.TaggedError()("McpCallError", {
|
|
61
|
+
method: Schema.String,
|
|
62
|
+
target: Schema.String,
|
|
63
|
+
reason: Reason,
|
|
64
|
+
message: Schema.String
|
|
65
|
+
}) {};
|
|
66
|
+
const callError = (method, target, reason) => new McpCallError({
|
|
67
|
+
method,
|
|
68
|
+
target,
|
|
69
|
+
reason,
|
|
70
|
+
message: `MCP ${method} "${target}" ${sentenceOf(reason)}`
|
|
71
|
+
});
|
|
72
|
+
const isEventStream = (response) => (response.headers["content-type"] ?? "").startsWith("text/event-stream");
|
|
73
|
+
/**
|
|
74
|
+
* The data of each frame of the event stream `response` is, as it arrives: more than the parser
|
|
75
|
+
* holds with no frame ending fails `method` on `name` with `FrameTooLarge`.
|
|
76
|
+
*/
|
|
77
|
+
const framesOf = (method, name, response) => Stream.suspend(() => {
|
|
78
|
+
const data = [];
|
|
79
|
+
const parser = Sse.makeParser((event) => {
|
|
80
|
+
if (Predicate.isTagged(event, "Event")) data.push(event.data);
|
|
81
|
+
});
|
|
82
|
+
return Stream.decodeText(response.stream).pipe(Stream.mapEffect((chunk) => {
|
|
83
|
+
const error = parser.feed(chunk);
|
|
84
|
+
return error === void 0 ? Effect.succeed(data.splice(0)) : Effect.fail(callError(method, name, new FrameTooLarge({ maxEventSize: error.reason.maxEventSize })));
|
|
85
|
+
}), Stream.flattenIterable);
|
|
86
|
+
});
|
|
87
|
+
/**
|
|
88
|
+
* The messages of `response`, read as it arrives: the data of each frame of an event stream, or
|
|
89
|
+
* the body itself as one. Each `notifications/progress` frame hands its value to `sink` and is
|
|
90
|
+
* left out, so a long call holds the frames its reply is read from, not every value it reported.
|
|
91
|
+
* A stateless request's notifications are its own.
|
|
92
|
+
*/
|
|
93
|
+
const bodyOf = (method, name, response, sink) => isEventStream(response) ? Stream.runCollect(Stream.filterEffect(framesOf(method, name, response), (data) => Option.match(progressOf(data), {
|
|
94
|
+
onNone: () => Effect.succeed(true),
|
|
95
|
+
onSome: (value) => sink === void 0 ? Effect.succeed(false) : Effect.as(sink.progress(value), false)
|
|
96
|
+
}))) : Effect.map(response.text, (text) => [text]);
|
|
97
|
+
/** Fails with the error of `errors` that `text` decodes as, or with `otherwise`. */
|
|
98
|
+
const failWith = (errors, text, otherwise) => Schema.decodeEffect(Schema.fromJsonString(Schema.toCodecJson(Schema.Union(errors))))(text).pipe(Effect.mapError(() => otherwise), Effect.flatMap(Effect.fail));
|
|
99
|
+
export { FrameTooLarge, McpCallError, NoReply, NoStructuredContent, ProtocolError, StatusError, UndeclaredError, bodyOf, callError, failWith, framesOf, isEventStream };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { Headers, HttpClientRequest } from "effect/http";
|
|
3
|
+
/**
|
|
4
|
+
* An MCP request's parameters, as `request` takes them: JSON, with any `_meta`, leaving
|
|
5
|
+
* out an `undefined` one, as a caller may. A helper passing on whole params types them as this,
|
|
6
|
+
* and a tool's `arguments` as `Schema.Json`, never `unknown`.
|
|
7
|
+
*/
|
|
8
|
+
export interface Params {
|
|
9
|
+
readonly _meta?: {
|
|
10
|
+
readonly [key: string]: Schema.Json;
|
|
11
|
+
};
|
|
12
|
+
readonly [key: string]: Schema.Json | undefined;
|
|
13
|
+
}
|
|
14
|
+
/** Where a `request` goes, and with what headers. */
|
|
15
|
+
export interface RequestOptions {
|
|
16
|
+
/**
|
|
17
|
+
* The endpoint, resolved by the `HttpClient` that sends the request: relative under `Testing.layer`.
|
|
18
|
+
* Defaults to `/mcp`, as `client`'s does; converting to a web `Request` needs an absolute
|
|
19
|
+
* one.
|
|
20
|
+
*/
|
|
21
|
+
readonly url?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Headers sent beside the routing ones `request` fills in, such as `Authorization`; a routing
|
|
24
|
+
* header wins over a given one of its name.
|
|
25
|
+
*/
|
|
26
|
+
readonly headers?: Headers.Input;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* One stateless MCP request of `method` with `params`, as `layerHttp` serves it,
|
|
30
|
+
* as the native request value: the JSON-RPC envelope, the 2026-07-28 headers (`Accept`,
|
|
31
|
+
* `MCP-Protocol-Version`, `MCP-Method`), `mcp-name` from
|
|
32
|
+
* `params.uri` for `resources/read` and `params.name` otherwise, and the client metadata in
|
|
33
|
+
* `_meta` are filled in, under any `_meta` given, such as a `progressToken`; the protocol
|
|
34
|
+
* version is always the request's own. The test sends it: `HttpClient.execute` answers the
|
|
35
|
+
* response as the endpoint sent it, whatever its status, and `HttpClientRequest.toWebResult`
|
|
36
|
+
* gives the web `Request` a web handler or `fetch` takes, once `url` is absolute. It is for a test
|
|
37
|
+
* asserting on what `client` decodes away, such as `tools/list`, a refusal's challenge, or
|
|
38
|
+
* a call its types would not send.
|
|
39
|
+
*/
|
|
40
|
+
export declare const request: (method: string, params?: Params, { headers, url }?: RequestOptions) => HttpClientRequest.HttpClientRequest;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { defaultPath, httpProtocol } from "./protocol.mjs";
|
|
2
|
+
import { Predicate } from "effect";
|
|
3
|
+
import { HttpClientRequest } from "effect/http";
|
|
4
|
+
// src/mcp/request.ts
|
|
5
|
+
/** The routing headers and JSON-RPC body of the request `request` builds. */
|
|
6
|
+
const statelessRequest = (method, params) => {
|
|
7
|
+
const { protocolVersion } = httpProtocol;
|
|
8
|
+
const name = method === "resources/read" ? params["uri"] : params["name"];
|
|
9
|
+
return {
|
|
10
|
+
headers: {
|
|
11
|
+
accept: "application/json, text/event-stream",
|
|
12
|
+
"mcp-protocol-version": protocolVersion,
|
|
13
|
+
"mcp-method": method,
|
|
14
|
+
...Predicate.isString(name) ? { "mcp-name": name } : {}
|
|
15
|
+
},
|
|
16
|
+
body: {
|
|
17
|
+
jsonrpc: "2.0",
|
|
18
|
+
id: 1,
|
|
19
|
+
method,
|
|
20
|
+
params: {
|
|
21
|
+
...params,
|
|
22
|
+
_meta: {
|
|
23
|
+
"io.modelcontextprotocol/clientCapabilities": {},
|
|
24
|
+
"io.modelcontextprotocol/clientInfo": {
|
|
25
|
+
name: "effect-actions",
|
|
26
|
+
version: "0"
|
|
27
|
+
},
|
|
28
|
+
...params._meta,
|
|
29
|
+
"io.modelcontextprotocol/protocolVersion": protocolVersion
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* One stateless MCP request of `method` with `params`, as `layerHttp` serves it,
|
|
37
|
+
* as the native request value: the JSON-RPC envelope, the 2026-07-28 headers (`Accept`,
|
|
38
|
+
* `MCP-Protocol-Version`, `MCP-Method`), `mcp-name` from
|
|
39
|
+
* `params.uri` for `resources/read` and `params.name` otherwise, and the client metadata in
|
|
40
|
+
* `_meta` are filled in, under any `_meta` given, such as a `progressToken`; the protocol
|
|
41
|
+
* version is always the request's own. The test sends it: `HttpClient.execute` answers the
|
|
42
|
+
* response as the endpoint sent it, whatever its status, and `HttpClientRequest.toWebResult`
|
|
43
|
+
* gives the web `Request` a web handler or `fetch` takes, once `url` is absolute. It is for a test
|
|
44
|
+
* asserting on what `client` decodes away, such as `tools/list`, a refusal's challenge, or
|
|
45
|
+
* a call its types would not send.
|
|
46
|
+
*/
|
|
47
|
+
const request = (method, params = {}, { headers = {}, url = defaultPath } = {}) => {
|
|
48
|
+
const { headers: routingHeaders, body } = statelessRequest(method, params);
|
|
49
|
+
return HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.setHeaders(routingHeaders), HttpClientRequest.bodyJsonUnsafe(body));
|
|
50
|
+
};
|
|
51
|
+
export { request, statelessRequest };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { InvalidInput } from "../contract/errors.mjs";
|
|
2
|
+
import { isTransientStatus } from "../contract/status.mjs";
|
|
3
|
+
import { Checked, Gap, untaggedOccurrenceOf } from "../contract/occurrence.mjs";
|
|
4
|
+
import { defaultNextPoll } from "../contract/feed.mjs";
|
|
5
|
+
import { reasonOf } from "../contract/codes.mjs";
|
|
6
|
+
import { Receiving, webhookFed } from "../contract/receiving.mjs";
|
|
7
|
+
import { Failed, readOn } from "../contract/push.mjs";
|
|
8
|
+
import { WebhookCallback, WebhookTarget } from "../contract/subscribe.mjs";
|
|
9
|
+
import { failingWhenSilent, failingWhenUnanswered, isTransient } from "../contract/transient.mjs";
|
|
10
|
+
import { Active, Failed as Failed$1, Heartbeat, Occurred, PollEvents, SubscribeEvents, Terminated, UnsubscribeEvents, WireError, codes } from "./events.mjs";
|
|
11
|
+
import { McpCallError, NoReply, ProtocolError, StatusError, bodyOf, callError, failWith, framesOf, isEventStream } from "./reply.mjs";
|
|
12
|
+
import { request } from "./request.mjs";
|
|
13
|
+
import { Duration, Effect, Match, Option, Predicate, Schema, Stream } from "effect";
|
|
14
|
+
// src/mcp/watch.ts
|
|
15
|
+
/** Reads one message as a request's reply: its result, by `success`, or its error. */
|
|
16
|
+
const replyOf = (success) => Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Union([Schema.Struct({ result: success }), Schema.Struct({ error: WireError })])));
|
|
17
|
+
const pollReplyOf = replyOf(PollEvents.successSchema);
|
|
18
|
+
const Frame = Schema.Union([
|
|
19
|
+
Schema.Struct({ error: WireError }),
|
|
20
|
+
Schema.Struct({
|
|
21
|
+
method: Schema.String,
|
|
22
|
+
params: Schema.optional(Schema.Json)
|
|
23
|
+
}),
|
|
24
|
+
Schema.Struct({ result: Schema.Unknown })
|
|
25
|
+
]);
|
|
26
|
+
const decodeFrame = Schema.decodeUnknownOption(Schema.fromJsonString(Frame));
|
|
27
|
+
const reading = (rpc, read) => {
|
|
28
|
+
const decode = Schema.decodeUnknownOption(rpc.payloadSchema);
|
|
29
|
+
return (params) => Option.map(decode(params), read);
|
|
30
|
+
};
|
|
31
|
+
const subscribeReplyOf = replyOf(SubscribeEvents.successSchema);
|
|
32
|
+
const unsubscribeReplyOf = replyOf(UnsubscribeEvents.successSchema);
|
|
33
|
+
const encodeCallback = Schema.encodeSync(WebhookCallback);
|
|
34
|
+
const encodeTarget = Schema.encodeSync(WebhookTarget);
|
|
35
|
+
const transientReason = (reason) => Match.value(reason).pipe(Match.tag("StatusError", ({ status }) => isTransientStatus(status)), Match.tag("ProtocolError", ({ code }) => code === codes.internal), Match.orElse(() => false));
|
|
36
|
+
const isMcpCallError = Schema.is(McpCallError);
|
|
37
|
+
const transient = Predicate.or(isTransient, Predicate.compose(isMcpCallError, ({ reason }) => transientReason(reason)));
|
|
38
|
+
/**
|
|
39
|
+
* The transport of one subscription over an MCP endpoint served by `layerHttp`: `events/poll`
|
|
40
|
+
* for each page, and `events/stream` read as it arrives, each failing as the endpoint answered.
|
|
41
|
+
*/
|
|
42
|
+
const transportOf = ({ http, url, event, refusedWith }, input) => {
|
|
43
|
+
const { name } = event;
|
|
44
|
+
const untagged = untaggedOccurrenceOf(event);
|
|
45
|
+
const occurrenceOf = (wire) => untagged({
|
|
46
|
+
...wire,
|
|
47
|
+
cursor: wire.cursor ?? null
|
|
48
|
+
});
|
|
49
|
+
const failed = (method, error) => reasonOf(name, error) ?? (error.code === codes.invalidParams ? new InvalidInput({ message: error.message }) : callError(method, name, new ProtocolError(error)));
|
|
50
|
+
const sent = (method, params) => Effect.filterOrElse(http.execute(request(method, {
|
|
51
|
+
name,
|
|
52
|
+
arguments: input,
|
|
53
|
+
...params
|
|
54
|
+
}, { url })), (response) => response.status === 200, (response) => Effect.flatMap(response.text, (body) => failWith(refusedWith, body, callError(method, name, new StatusError({
|
|
55
|
+
status: response.status,
|
|
56
|
+
body
|
|
57
|
+
})))));
|
|
58
|
+
/** The result of one `method` request, or what its error reply fails with. */
|
|
59
|
+
const answered = (method, params, decodeReply) => Effect.gen(function* () {
|
|
60
|
+
const messages = yield* bodyOf(method, name, yield* sent(method, params));
|
|
61
|
+
const reply = messages.flatMap((message) => Option.toArray(decodeReply(message))).at(-1);
|
|
62
|
+
if (reply === void 0) return yield* callError(method, name, new NoReply({ body: messages.join("\n") }));
|
|
63
|
+
if ("error" in reply) return yield* failed(method, reply.error);
|
|
64
|
+
return reply.result;
|
|
65
|
+
}).pipe(failingWhenUnanswered);
|
|
66
|
+
const poll = (cursor) => Effect.gen(function* () {
|
|
67
|
+
const result = yield* answered("events/poll", { cursor }, pollReplyOf);
|
|
68
|
+
return {
|
|
69
|
+
events: yield* Effect.forEach(result.events, occurrenceOf),
|
|
70
|
+
cursor: result.cursor ?? null,
|
|
71
|
+
truncated: result.truncated === true,
|
|
72
|
+
hasMore: result.hasMore === true,
|
|
73
|
+
nextPoll: result.nextPollMs === void 0 ? defaultNextPoll : Duration.millis(result.nextPollMs)
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
const readers = new Map(Object.entries({
|
|
77
|
+
[Active._tag]: reading(Active, ({ cursor = null, truncated }) => Effect.succeed([truncated === true ? Gap.make({ cursor }) : Checked.make({ cursor })])),
|
|
78
|
+
[Occurred._tag]: reading(Occurred, (params) => Effect.map(occurrenceOf(params), (occurrence) => [occurrence])),
|
|
79
|
+
[Heartbeat._tag]: reading(Heartbeat, ({ cursor = null }) => Effect.succeed([Checked.make({ cursor })])),
|
|
80
|
+
[Failed$1._tag]: reading(Failed$1, () => Effect.succeed([Failed.make({})])),
|
|
81
|
+
[Terminated._tag]: reading(Terminated, ({ error }) => Effect.fail(failed("events/stream", error)))
|
|
82
|
+
}));
|
|
83
|
+
const framed = (data) => Option.match(decodeFrame(data), {
|
|
84
|
+
onNone: () => Effect.succeed([]),
|
|
85
|
+
onSome: (frame) => {
|
|
86
|
+
if ("error" in frame) return Effect.fail(failed("events/stream", frame.error));
|
|
87
|
+
if (!("method" in frame)) return Effect.succeed([]);
|
|
88
|
+
const reader = readers.get(frame.method);
|
|
89
|
+
if (reader === void 0) return Effect.succeed([]);
|
|
90
|
+
return Option.getOrElse(reader(frame.params), () => Effect.as(Effect.logWarning(`MCP event "${name}" sent a frame that does not decode: ${data}`), []));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
const read = (response) => framesOf("events/stream", name, response).pipe(failingWhenSilent, Stream.mapEffect(framed), Stream.flattenIterable);
|
|
94
|
+
const stream = (cursor) => Stream.unwrap(Effect.map(failingWhenUnanswered(sent("events/stream", { cursor })), (response) => isEventStream(response) ? read(response) : Stream.fromIterableEffect(failingWhenUnanswered(Effect.flatMap(response.text, framed))))).pipe(readOn("MCP", name));
|
|
95
|
+
return {
|
|
96
|
+
name,
|
|
97
|
+
poll,
|
|
98
|
+
stream,
|
|
99
|
+
webhook: {
|
|
100
|
+
receiver: Effect.service(Receiving),
|
|
101
|
+
subscribe: ({ delivery, cursor, ttlMs }) => answered("events/subscribe", {
|
|
102
|
+
delivery: {
|
|
103
|
+
mode: "webhook",
|
|
104
|
+
...encodeCallback(delivery)
|
|
105
|
+
},
|
|
106
|
+
cursor,
|
|
107
|
+
...ttlMs === void 0 ? {} : { ttlMs }
|
|
108
|
+
}, subscribeReplyOf),
|
|
109
|
+
unsubscribe: ({ delivery }) => Effect.asVoid(answered("events/unsubscribe", { delivery: encodeTarget(delivery) }, unsubscribeReplyOf)),
|
|
110
|
+
read: webhookFed(event)
|
|
111
|
+
},
|
|
112
|
+
transient
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
export { transportOf };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { NotServed, SubscriptionError, Unsupported } from "../Event.mjs";
|
|
2
|
+
import { readOn } from "../contract/push.mjs";
|
|
3
|
+
import { isTransientReason, receivedFrom } from "../contract/transient.mjs";
|
|
4
|
+
import { Cause, Effect, Match, Predicate, Schema, Stream } from "effect";
|
|
5
|
+
import { HttpClientError } from "effect/http";
|
|
6
|
+
import { RpcClientError } from "effect/rpc";
|
|
7
|
+
// src/rpc/watch.ts
|
|
8
|
+
const isRpcClientError = Schema.is(RpcClientError.RpcClientError);
|
|
9
|
+
const isAnsweredReason = Schema.is(Schema.Union([
|
|
10
|
+
Schema.instanceOf(HttpClientError.TransportError),
|
|
11
|
+
Schema.instanceOf(HttpClientError.StatusCodeError),
|
|
12
|
+
Schema.instanceOf(HttpClientError.DecodeError)
|
|
13
|
+
]));
|
|
14
|
+
const cutResponses = ["HTTP response ended before RPC request completed", "Received empty HTTP response"];
|
|
15
|
+
/** Whether a watch's failure over an rpc protocol is one a reconnect may outlast. */
|
|
16
|
+
const isRpcTransient = Predicate.or(Cause.isTimeoutError, (error) => isRpcClientError(error) && Match.value(error.reason).pipe(Match.tag("SocketReadError", "SocketWriteError", "SocketOpenError", () => true), Match.tag("SocketCloseError", ({ code }) => code < 4e3 || code > 4999), Match.tag("HttpError", ({ cause }) => isAnsweredReason(cause) && isTransientReason(cause)), Match.tag("RpcClientDefect", ({ message }) => cutResponses.some((fragment) => message.includes(fragment))), Match.orElse(() => false)));
|
|
17
|
+
const isUnknownTag = (tag) => {
|
|
18
|
+
const message = `Unknown request tag: ${tag}`;
|
|
19
|
+
return (cause) => cause.reasons.some((reason) => Cause.isDieReason(reason) && (Predicate.hasProperty(reason.defect, "message") ? reason.defect.message === message : reason.defect === message));
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* The transport of one subscription to `event` over its stream rpc: the stream alone, since an
|
|
23
|
+
* rpc serves no poll, failing as `NotServed` where the server serves no such rpc, and once it goes
|
|
24
|
+
* silent where `silenceEnds`.
|
|
25
|
+
*/
|
|
26
|
+
const transportOf = (event, stream, silenceEnds) => {
|
|
27
|
+
const notServed = Stream.fail(SubscriptionError.of(event.name, new NotServed()));
|
|
28
|
+
const unknownTag = isUnknownTag(event.name);
|
|
29
|
+
return {
|
|
30
|
+
name: event.name,
|
|
31
|
+
poll: () => Effect.fail(SubscriptionError.of(event.name, new Unsupported({
|
|
32
|
+
feature: "deliveryMode",
|
|
33
|
+
value: "poll"
|
|
34
|
+
}))),
|
|
35
|
+
stream: (cursor) => stream(cursor).pipe(Stream.catchCauseIf(unknownTag, () => notServed), silenceEnds ? receivedFrom("RPC", event.name) : readOn("RPC", event.name)),
|
|
36
|
+
transient: isRpcTransient
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
export { isRpcTransient, transportOf };
|
package/dist/stdio/console.mjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Clock, Console, Effect, Function, Predicate } from "effect";
|
|
2
|
-
|
|
2
|
+
// src/stdio/console.ts
|
|
3
3
|
const stderrConsole = (console, clock) => {
|
|
4
4
|
const counts = /* @__PURE__ */ new Map();
|
|
5
5
|
const timers = /* @__PURE__ */ new Map();
|
|
6
|
-
const write = (...args) =>
|
|
6
|
+
const write = (...args) => {
|
|
7
|
+
console.error(...args);
|
|
8
|
+
};
|
|
7
9
|
const elapsed = (label, data) => {
|
|
8
10
|
const start = timers.get(label);
|
|
9
11
|
if (start !== void 0) {
|
|
@@ -32,7 +34,9 @@ const stderrConsole = (console, clock) => {
|
|
|
32
34
|
counts.delete(label);
|
|
33
35
|
},
|
|
34
36
|
debug: write,
|
|
35
|
-
dir: (item) =>
|
|
37
|
+
dir: (item) => {
|
|
38
|
+
write("%O", item);
|
|
39
|
+
},
|
|
36
40
|
dirxml: write,
|
|
37
41
|
error: write,
|
|
38
42
|
group,
|
|
@@ -40,7 +44,9 @@ const stderrConsole = (console, clock) => {
|
|
|
40
44
|
groupEnd: Function.constVoid,
|
|
41
45
|
info: write,
|
|
42
46
|
log: write,
|
|
43
|
-
table: (data) =>
|
|
47
|
+
table: (data) => {
|
|
48
|
+
write(data);
|
|
49
|
+
},
|
|
44
50
|
time: (label = "default") => {
|
|
45
51
|
if (!timers.has(label)) timers.set(label, clock.monotonicTimeNanosUnsafe());
|
|
46
52
|
},
|
|
@@ -48,9 +54,11 @@ const stderrConsole = (console, clock) => {
|
|
|
48
54
|
elapsed(label, []);
|
|
49
55
|
timers.delete(label);
|
|
50
56
|
},
|
|
51
|
-
timeLog: (label = "default", ...data) =>
|
|
57
|
+
timeLog: (label = "default", ...data) => {
|
|
58
|
+
elapsed(label, data);
|
|
59
|
+
},
|
|
52
60
|
trace: (...args) => {
|
|
53
|
-
const { stack = "" } = /* @__PURE__ */ new Error();
|
|
61
|
+
const { stack = "" } = /* @__PURE__ */ new Error("console.trace");
|
|
54
62
|
const [_header, _traceFrame, ...callerFrames] = stack.split("\n");
|
|
55
63
|
const callerStack = callerFrames.join("\n");
|
|
56
64
|
writeLabelKeepingFormat("Trace", args);
|
|
@@ -60,5 +68,4 @@ const stderrConsole = (console, clock) => {
|
|
|
60
68
|
};
|
|
61
69
|
};
|
|
62
70
|
const withStderrConsole = (effect) => Effect.provideServiceEffect(effect, Console.Console, Effect.zipWith(Effect.service(Console.Console), Effect.service(Clock.Clock), stderrConsole));
|
|
63
|
-
//#endregion
|
|
64
71
|
export { withStderrConsole };
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { Layer } from "effect";
|
|
2
2
|
import { HttpClient, HttpRouter, HttpServer } from "effect/http";
|
|
3
|
-
//#region src/testing/memory.d.ts
|
|
4
3
|
/** What in-memory routes may leave to the host: the router, the platform, nothing per request. */
|
|
5
|
-
export type Served = HttpRouter.HttpRouter | HttpRouter.Request<"Requires", never> | HttpRouter.Request<"GlobalRequires", never> | HttpRouter.Request<"Error",
|
|
6
|
-
//#endregion
|
|
4
|
+
export type Served = HttpRouter.HttpRouter | HttpRouter.Request<"Requires", never> | HttpRouter.Request<"GlobalRequires", never> | HttpRouter.Request<"Error", unknown> | HttpRouter.Request<"GlobalError", unknown> | Layer.Success<typeof HttpServer.layerServices>;
|
package/dist/testing/memory.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Effect, Layer, Predicate, Stream } from "effect";
|
|
2
2
|
import { FetchHttpClient, HttpBody, HttpClient, HttpClientRequest } from "effect/http";
|
|
3
|
-
|
|
3
|
+
// src/testing/memory.ts
|
|
4
4
|
const relativeUrlOrigin = "http://localhost/";
|
|
5
5
|
const withWebStreamBodyInCurrentContext = (request) => {
|
|
6
|
-
const body = request
|
|
6
|
+
const { body } = request;
|
|
7
7
|
if (!Predicate.isTagged(body, "Stream")) return Effect.succeed(request);
|
|
8
8
|
return Effect.map(Stream.toReadableStreamEffect(body.stream), (readable) => HttpClientRequest.setBody(request, HttpBody.raw(readable, {
|
|
9
9
|
contentType: body.contentType,
|
|
@@ -11,10 +11,10 @@ const withWebStreamBodyInCurrentContext = (request) => {
|
|
|
11
11
|
})).pipe(HttpClientRequest.updateHeaders(() => request.headers)));
|
|
12
12
|
};
|
|
13
13
|
const clientOf = (handler) => {
|
|
14
|
-
const fetch = (input, init) => {
|
|
14
|
+
const fetch = async (input, init) => {
|
|
15
15
|
const request = new Request(input, init);
|
|
16
16
|
if (!request.headers.has("host")) request.headers.set("host", new URL(request.url).host);
|
|
17
|
-
return handler(request);
|
|
17
|
+
return await handler(request);
|
|
18
18
|
};
|
|
19
19
|
return Layer.effect(HttpClient.HttpClient, Effect.map(HttpClient.HttpClient, (native) => HttpClient.makeWith((request) => Effect.flatMap(request, (mapped) => {
|
|
20
20
|
const resolved = URL.canParse(mapped.url) ? null : URL.parse(mapped.url, relativeUrlOrigin);
|
|
@@ -22,5 +22,4 @@ const clientOf = (handler) => {
|
|
|
22
22
|
return Effect.flatMap(withWebStreamBodyInCurrentContext(sent), (ready) => native.postprocess(Effect.succeed(ready)).pipe(Effect.provideService(FetchHttpClient.Fetch, fetch)));
|
|
23
23
|
}), native.preprocess))).pipe(Layer.provide(Layer.fresh(FetchHttpClient.layer)));
|
|
24
24
|
};
|
|
25
|
-
//#endregion
|
|
26
25
|
export { clientOf };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Effect, Function, Result } from "effect";
|
|
2
|
+
import { Host, IpNetwork, NetAddress } from "effect/net";
|
|
3
|
+
// src/webhook/address.ts
|
|
4
|
+
const notGlobal = [
|
|
5
|
+
"0.0.0.0/8",
|
|
6
|
+
"10.0.0.0/8",
|
|
7
|
+
"100.64.0.0/10",
|
|
8
|
+
"127.0.0.0/8",
|
|
9
|
+
"169.254.0.0/16",
|
|
10
|
+
"172.16.0.0/12",
|
|
11
|
+
"192.0.0.0/24",
|
|
12
|
+
"192.0.2.0/24",
|
|
13
|
+
"192.88.99.0/24",
|
|
14
|
+
"192.168.0.0/16",
|
|
15
|
+
"198.18.0.0/15",
|
|
16
|
+
"198.51.100.0/24",
|
|
17
|
+
"203.0.113.0/24",
|
|
18
|
+
"224.0.0.0/4",
|
|
19
|
+
"240.0.0.0/4",
|
|
20
|
+
"::/127",
|
|
21
|
+
"::ffff:0:0/96",
|
|
22
|
+
"64:ff9b::/96",
|
|
23
|
+
"64:ff9b:1::/48",
|
|
24
|
+
"100::/64",
|
|
25
|
+
"2001::/23",
|
|
26
|
+
"2001:db8::/32",
|
|
27
|
+
"2002::/16",
|
|
28
|
+
"3fff::/20",
|
|
29
|
+
"5f00::/16",
|
|
30
|
+
"fc00::/7",
|
|
31
|
+
"fe80::/10",
|
|
32
|
+
"ff00::/8"
|
|
33
|
+
].map(IpNetwork.fromStringUnsafe);
|
|
34
|
+
/** Whether `address` is globally reachable, in no special-purpose range. */
|
|
35
|
+
const isGlobal = (address) => !notGlobal.some((network) => IpNetwork.contains(network, address));
|
|
36
|
+
/**
|
|
37
|
+
* Checks the host of `url`, failing unless every address it names is globally reachable: a
|
|
38
|
+
* literal address is checked as it is, a name through `dns`, or passed where there is none, and a
|
|
39
|
+
* scoped IPv6 literal refused.
|
|
40
|
+
*/
|
|
41
|
+
const checkedHost = (url, dns) => {
|
|
42
|
+
const host = Result.getOrUndefined(Host.hostFromString(url.hostname.replace(/^\[(?<address>.*)\]$/u, "$<address>")));
|
|
43
|
+
if (!NetAddress.isIpAddress(host) && !Host.isDomainName(host)) return Effect.fail(void 0);
|
|
44
|
+
const addresses = NetAddress.isIpAddress(host) ? Effect.succeed([host]) : dns?.lookup(host) ?? Effect.succeed([]);
|
|
45
|
+
return Effect.flatMap(Effect.mapError(addresses, Function.constUndefined), (resolved) => resolved.every(isGlobal) ? Effect.void : Effect.fail(void 0));
|
|
46
|
+
};
|
|
47
|
+
export { checkedHost };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { signature } from "./signing.mjs";
|
|
2
|
+
import { Clock, Data, Duration, Effect, Option, Schema, Stream } from "effect";
|
|
3
|
+
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http";
|
|
4
|
+
// src/webhook/attempt.ts
|
|
5
|
+
/**
|
|
6
|
+
* One attempt of a delivery that failed: `category` is SEP-3415's for it, and `final` says a
|
|
7
|
+
* retry would not help, as for a `410 Gone` or a `413 Payload Too Large`.
|
|
8
|
+
*/
|
|
9
|
+
var AttemptFailed = class extends Data.TaggedError("AttemptFailed") {};
|
|
10
|
+
const timeout = Duration.seconds(5);
|
|
11
|
+
const statusCategory = (status) => status >= 500 ? "http_5xx" : "http_4xx";
|
|
12
|
+
const outbound = (effect) => Effect.flatMap(Effect.serviceOption(FetchHttpClient.RequestInit), (init) => effect.pipe(Effect.provideService(FetchHttpClient.RequestInit, {
|
|
13
|
+
...Option.getOrUndefined(init),
|
|
14
|
+
redirect: "manual"
|
|
15
|
+
}), Effect.provideService(HttpClient.TracerPropagationEnabled, false)));
|
|
16
|
+
const sent = ({ url, subscriptionId, webhookId, body, keys }, checked, answer) => Effect.gen(function* () {
|
|
17
|
+
const unreached = new AttemptFailed({
|
|
18
|
+
category: "connection_refused",
|
|
19
|
+
final: false
|
|
20
|
+
});
|
|
21
|
+
const timestamp = Math.floor((yield* Clock.currentTimeMillis) / 1e3);
|
|
22
|
+
const signed = yield* signature(keys, webhookId, timestamp, body);
|
|
23
|
+
const client = HttpClient.withScope(yield* HttpClient.HttpClient);
|
|
24
|
+
const request = HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders({
|
|
25
|
+
"webhook-id": webhookId,
|
|
26
|
+
"webhook-timestamp": String(timestamp),
|
|
27
|
+
"webhook-signature": signed,
|
|
28
|
+
"x-mcp-subscription-id": subscriptionId
|
|
29
|
+
}), HttpClientRequest.bodyText(body, "application/json"));
|
|
30
|
+
const answered = Effect.flatMap(Effect.mapError(client.execute(request), () => unreached), (response) => response.status >= 200 && response.status < 300 ? answer(response) : Effect.fail(new AttemptFailed({
|
|
31
|
+
category: statusCategory(response.status),
|
|
32
|
+
final: response.status === 410 || response.status === 413
|
|
33
|
+
})));
|
|
34
|
+
return yield* Effect.andThen(Effect.mapError(checked, () => unreached), outbound(answered)).pipe(Effect.timeoutOrElse({
|
|
35
|
+
duration: timeout,
|
|
36
|
+
orElse: () => Effect.fail(new AttemptFailed({
|
|
37
|
+
category: "timeout",
|
|
38
|
+
final: false
|
|
39
|
+
}))
|
|
40
|
+
}), Effect.scoped);
|
|
41
|
+
});
|
|
42
|
+
/**
|
|
43
|
+
* POSTs `body` to the callback, signed with a fresh timestamp, once `checked`, its address check,
|
|
44
|
+
* passes, and succeeds at a 2xx without reading its body. The timeout bounds the check and the
|
|
45
|
+
* POST together.
|
|
46
|
+
*/
|
|
47
|
+
const attempt = (posting, checked) => sent(posting, checked, () => Effect.void);
|
|
48
|
+
const Challenged = Schema.fromJsonString(Schema.Struct({ challenge: Schema.String }));
|
|
49
|
+
const answerLimit = 4096;
|
|
50
|
+
const textOf = (response) => Effect.suspend(() => {
|
|
51
|
+
const decoder = new TextDecoder();
|
|
52
|
+
return response.stream.pipe(Stream.scan(() => ({
|
|
53
|
+
bytes: 0,
|
|
54
|
+
text: ""
|
|
55
|
+
}), ({ bytes, text }, chunk) => ({
|
|
56
|
+
bytes: bytes + chunk.length,
|
|
57
|
+
text: text + decoder.decode(chunk, { stream: true })
|
|
58
|
+
})), Stream.takeUntil(({ bytes }) => bytes > answerLimit), Stream.runLast, Effect.map((last) => Option.map(Option.filter(last, ({ bytes }) => bytes <= answerLimit), ({ text }) => text + decoder.decode())), Effect.orElseSucceed(() => Option.none()));
|
|
59
|
+
});
|
|
60
|
+
/**
|
|
61
|
+
* `attempt` of a verification: the challenge its 2xx answer echoes, where its first 4 KiB hold
|
|
62
|
+
* one.
|
|
63
|
+
*/
|
|
64
|
+
const challenged = (posting, checked) => sent(posting, checked, (response) => Effect.map(textOf(response), (text) => Option.getOrUndefined(Option.map(Option.flatMap(text, Schema.decodeUnknownOption(Challenged)), ({ challenge }) => challenge))));
|
|
65
|
+
export { AttemptFailed, attempt, challenged };
|