@gjermundgaraba/effect-actions 0.20.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 -46
- package/dist/Action.mjs +87 -42
- package/dist/ActionCli.d.mts +69 -33
- package/dist/ActionCli.mjs +51 -32
- package/dist/ActionHttp.d.mts +69 -35
- package/dist/ActionHttp.mjs +75 -23
- package/dist/ActionMcp.d.mts +56 -22
- package/dist/ActionMcp.mjs +94 -69
- package/dist/ActionRpc.d.mts +222 -55
- package/dist/ActionRpc.mjs +299 -46
- package/dist/ActionToolkit.d.mts +22 -5
- package/dist/ActionToolkit.mjs +36 -12
- package/dist/Authentication.d.mts +42 -10
- package/dist/Authentication.mjs +37 -30
- package/dist/Event.d.mts +370 -0
- package/dist/Event.mjs +103 -0
- package/dist/Testing.mjs +1 -1
- package/dist/Webhook.d.mts +146 -0
- package/dist/Webhook.mjs +72 -0
- package/dist/authentication/provider.d.mts +35 -10
- package/dist/authentication/provider.mjs +1 -1
- package/dist/authentication/refusal.mjs +2 -2
- package/dist/cli/commands.d.mts +9 -5
- package/dist/cli/commands.mjs +84 -33
- package/dist/contract/ast.mjs +4 -1
- package/dist/contract/authorizer.d.mts +8 -0
- package/dist/contract/authorizer.mjs +20 -0
- package/dist/contract/call.d.mts +9 -6
- package/dist/contract/call.mjs +48 -6
- package/dist/contract/callback.d.mts +7 -0
- package/dist/contract/callback.mjs +27 -0
- package/dist/contract/caller.d.mts +13 -2
- package/dist/contract/caller.mjs +22 -3
- 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.mjs +25 -9
- package/dist/contract/digest.mjs +6 -0
- package/dist/contract/errors.d.mts +9 -3
- package/dist/contract/errors.mjs +1 -1
- package/dist/contract/feed.d.mts +127 -0
- package/dist/contract/feed.mjs +249 -0
- 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 -47
- package/dist/contract/implementation.mjs +92 -58
- 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/names.mjs +14 -4
- 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 +14 -5
- 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 -12
- package/dist/http/client.mjs +51 -12
- 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 +1 -1
- 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 -97
- package/dist/mcp/client.mjs +59 -132
- package/dist/mcp/events.mjs +142 -0
- package/dist/mcp/progress.mjs +34 -0
- package/dist/mcp/protocol.mjs +39 -13
- 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 +13 -5
- package/dist/testing/memory.d.mts +1 -1
- package/dist/testing/memory.mjs +3 -3
- 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 +52 -60
- 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 -52
- 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 +21 -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 +108 -47
- 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 +10 -4
package/dist/Event.mjs
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Anyone, assertCaller } from "./contract/caller.mjs";
|
|
2
|
+
import { assertEventName, assertOnce } from "./contract/names.mjs";
|
|
3
|
+
import { codecOf } from "./contract/codec.mjs";
|
|
4
|
+
import { authorizerOf } from "./contract/authorizer.mjs";
|
|
5
|
+
import { Events, layerMemory, makeEvents } from "./contract/hub.mjs";
|
|
6
|
+
import { Gap } from "./contract/occurrence.mjs";
|
|
7
|
+
import { EventImplementation, sourcesOf } from "./contract/feed.mjs";
|
|
8
|
+
import { Array as Array$1, Data, Effect, Match, Predicate, Schema } from "effect";
|
|
9
|
+
// src/contract/Event.ts
|
|
10
|
+
const TypeId = "~@gjermundgaraba/effect-actions/Event";
|
|
11
|
+
/** Whether `u` is an event type `make` defined: a binding's list tells events from actions by it. */
|
|
12
|
+
const isEvent = (u) => Predicate.hasProperty(u, TypeId);
|
|
13
|
+
function make(name, options) {
|
|
14
|
+
assertEventName(name);
|
|
15
|
+
const { caller } = options;
|
|
16
|
+
assertCaller(caller);
|
|
17
|
+
if (options.payload === void 0) throw new Error(`Missing payload: event "${name}" (declare what each occurrence carries)`);
|
|
18
|
+
return {
|
|
19
|
+
[TypeId]: TypeId,
|
|
20
|
+
caller,
|
|
21
|
+
name,
|
|
22
|
+
description: options.description,
|
|
23
|
+
input: codecOf(options.input ?? {}),
|
|
24
|
+
payload: codecOf(options.payload)
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** The server serves no event type of that name, or no longer does. */
|
|
28
|
+
var NotServed = class extends Schema.TaggedError()("NotServed", {}) {};
|
|
29
|
+
/**
|
|
30
|
+
* The server serves the event type, but not as asked: `feature` names what it does not offer,
|
|
31
|
+
* such as `deliveryMode`, and `value` which one, such as `push`.
|
|
32
|
+
*/
|
|
33
|
+
var Unsupported = class extends Schema.TaggedError()("Unsupported", {
|
|
34
|
+
feature: Schema.String,
|
|
35
|
+
value: Schema.optional(Schema.String)
|
|
36
|
+
}) {};
|
|
37
|
+
/**
|
|
38
|
+
* Why a webhook's callback endpoint failed, in SEP-3415's categories: it refused the connection
|
|
39
|
+
* or was unreachable, timed out, failed TLS, answered a status from 300 to 499 (`http_4xx`, as
|
|
40
|
+
* the categories name no 3xx) or a 5xx, or did not echo the verification challenge.
|
|
41
|
+
*/
|
|
42
|
+
const CallbackCategory = Schema.Literals([
|
|
43
|
+
"connection_refused",
|
|
44
|
+
"timeout",
|
|
45
|
+
"tls_error",
|
|
46
|
+
"http_4xx",
|
|
47
|
+
"http_5xx",
|
|
48
|
+
"challenge_failed"
|
|
49
|
+
]);
|
|
50
|
+
/** A webhook subscription's callback endpoint failed its verification, or was unreachable. */
|
|
51
|
+
var CallbackFailed = class extends Schema.TaggedError()("CallbackFailed", { category: CallbackCategory }) {};
|
|
52
|
+
/** No webhook subscription matches the one an unsubscribe names. */
|
|
53
|
+
var NotSubscribed = class extends Schema.TaggedError()("NotSubscribed", {}) {};
|
|
54
|
+
const Reason = Schema.Union([
|
|
55
|
+
NotServed,
|
|
56
|
+
Unsupported,
|
|
57
|
+
CallbackFailed,
|
|
58
|
+
NotSubscribed
|
|
59
|
+
]);
|
|
60
|
+
const sentenceOf = Match.typeTags()({
|
|
61
|
+
NotServed: () => "is not served",
|
|
62
|
+
Unsupported: ({ feature, value }) => `is not served with this ${feature}${value === void 0 ? "" : `: ${value}`}`,
|
|
63
|
+
CallbackFailed: ({ category }) => `could not reach its webhook's callback: ${category}`,
|
|
64
|
+
NotSubscribed: () => "has no such webhook subscription"
|
|
65
|
+
});
|
|
66
|
+
/**
|
|
67
|
+
* A subscription that cannot start or go on: `reason` says why, so a subscriber branches on it
|
|
68
|
+
* with `Effect.catchReason("SubscriptionError", "NotServed", ...)`; `message` names the event and
|
|
69
|
+
* the reason. A refusal is `Action.Unauthenticated` or `Action.Forbidden`, and input that does not
|
|
70
|
+
* decode `Action.InvalidInput`, as for an action.
|
|
71
|
+
*/
|
|
72
|
+
var SubscriptionError = class SubscriptionError extends Schema.TaggedError()("SubscriptionError", {
|
|
73
|
+
event: Schema.String,
|
|
74
|
+
reason: Reason,
|
|
75
|
+
message: Schema.String
|
|
76
|
+
}, { httpApiStatus: 409 }) {
|
|
77
|
+
/** The error for `event` failing with `reason`, its message built from both. */
|
|
78
|
+
static of(event, reason) {
|
|
79
|
+
return new SubscriptionError({
|
|
80
|
+
event,
|
|
81
|
+
reason,
|
|
82
|
+
message: `Event "${event}" ${sentenceOf(reason)}`
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* A watch delivered by webhook, which `Webhook.delivery` makes: the server POSTs each occurrence
|
|
88
|
+
* to the route `Webhook.layerReceiver` serves in the watching process, reached under `baseUrl`,
|
|
89
|
+
* for as long as the watch is read, each grant asking for `ttl`.
|
|
90
|
+
*/
|
|
91
|
+
var WebhookDelivery = class extends Data.TaggedClass("WebhookDelivery") {};
|
|
92
|
+
/** Every authenticated subscriber may subscribe to a protected event type. */
|
|
93
|
+
const allowAll = () => Effect.void;
|
|
94
|
+
function implement(events, sources, options) {
|
|
95
|
+
const listed = Array$1.ensure(events);
|
|
96
|
+
assertOnce("event", listed);
|
|
97
|
+
const authorizer = authorizerOf({
|
|
98
|
+
singular: "event",
|
|
99
|
+
module: "Event"
|
|
100
|
+
}, listed.some((event) => event.caller !== Anyone), options?.authorize);
|
|
101
|
+
return new EventImplementation(listed, sourcesOf(listed, !Array.isArray(events), sources), authorizer);
|
|
102
|
+
}
|
|
103
|
+
export { CallbackCategory, CallbackFailed, Events, Gap, NotServed, NotSubscribed, SubscriptionError, Unsupported, WebhookDelivery, allowAll, implement, isEvent, layerMemory, make, makeEvents };
|
package/dist/Testing.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { clientOf } from "./testing/memory.mjs";
|
|
|
2
2
|
import { Cause, Console, Context, Effect, ErrorReporter, FileSystem, Layer, Option, Path } from "effect";
|
|
3
3
|
import { Etag, HttpEffect, HttpPlatform, HttpRouter } from "effect/http";
|
|
4
4
|
// src/testing/Testing.ts
|
|
5
|
+
const isReported = (cause) => cause.reasons.some((reason) => Cause.isFailReason(reason) && !ErrorReporter.isIgnored(reason.error) || Cause.isDieReason(reason) && !ErrorReporter.isIgnored(reason.defect));
|
|
5
6
|
function layer(routes) {
|
|
6
7
|
if (!Layer.isLayer(routes)) return clientOf(routes);
|
|
7
8
|
return Layer.unwrap(Effect.gen(function* () {
|
|
@@ -12,5 +13,4 @@ function layer(routes) {
|
|
|
12
13
|
return clientOf(HttpEffect.toWebHandlerWith(context)(Effect.tapCause(app, (cause) => isReported(cause) ? Effect.logError(cause).pipe(Effect.provideService(Console.Console, globalThis.console)) : Effect.void)));
|
|
13
14
|
}));
|
|
14
15
|
}
|
|
15
|
-
const isReported = (cause) => cause.reasons.some((reason) => Cause.isFailReason(reason) && !ErrorReporter.isIgnored(reason.error) || Cause.isDieReason(reason) && !ErrorReporter.isIgnored(reason.defect));
|
|
16
16
|
export { layer };
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { WebhookDelivery } from "./Event.mjs";
|
|
2
|
+
import { Receiving } from "./contract/receiving.mjs";
|
|
3
|
+
import { Webhooks } from "./contract/webhooks.mjs";
|
|
4
|
+
import { Duration, Layer } from "effect";
|
|
5
|
+
import { HttpClient, HttpRouter } from "effect/http";
|
|
6
|
+
import { Dns } from "effect/net";
|
|
7
|
+
import { KeyValueStore } from "effect/persistence";
|
|
8
|
+
/** How `layer` grants, verifies and retries webhook subscriptions. */
|
|
9
|
+
export interface Options {
|
|
10
|
+
/**
|
|
11
|
+
* The longest lifetime a subscription is granted, and what one that suggests none, or asks for
|
|
12
|
+
* no expiry with `ttlMs: null`, is granted. Defaults to 1 hour; a suggestion under 1 minute is
|
|
13
|
+
* granted 1 minute. Finite, as every grant expires: `Duration.infinity` is refused as the layer
|
|
14
|
+
* is made.
|
|
15
|
+
*/
|
|
16
|
+
readonly maxTtl?: Duration.Input | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Callbacks trusted without the verification handshake or the address check, each an https
|
|
19
|
+
* URL whose origin a callback has exactly and whose path it starts with, segment by segment:
|
|
20
|
+
* `["https://hooks.example.com/"]`. A callback on a private network is delivered to only when
|
|
21
|
+
* one of them trusts it.
|
|
22
|
+
*/
|
|
23
|
+
readonly allowedCallbacks?: readonly string[] | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* How long a subscription's deliveries are retried from its first failure, each wait as long as
|
|
26
|
+
* it has been failing, from 1 second to 5 minutes, before it suspends until its next refresh,
|
|
27
|
+
* which delivers it again with a fresh `retryFor`. Defaults to 1 hour.
|
|
28
|
+
*
|
|
29
|
+
* **Gotchas**
|
|
30
|
+
*
|
|
31
|
+
* - The failures are timed from the subscription's `failedSince`, which `persist` keeps: a restart goes on with the waits and the budget it reached, and a refresh of a subscription still delivering does not extend it.
|
|
32
|
+
*/
|
|
33
|
+
readonly retryFor?: Duration.Input | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Keep each subscription in the application's `KeyValueStore`, which the layer then needs, so
|
|
36
|
+
* a restart goes on delivering from where it reached: each surface restores the subscriptions
|
|
37
|
+
* of the events it serves as it builds.
|
|
38
|
+
*
|
|
39
|
+
* **Gotchas**
|
|
40
|
+
*
|
|
41
|
+
* - Each subscriber's identity is kept by its authentication's `schema`, encoded at each grant: a surface offering a protected event by webhook refuses to start without it, or where two authentications with different schemas serve one event.
|
|
42
|
+
* - A restored delivery runs in the layer's context and that identity alone, until the subscriber's next refresh: an authorizer or a source reading a service middleware or the surface's layer provides fails, each read reported and retried every 5 seconds.
|
|
43
|
+
* - A subscriber refused as its subscription is restored is POSTed `terminated`; an authorizer or a source failing its first read with no refusal is retried, as later.
|
|
44
|
+
* - A subscription suspended stays suspended when restored, until its subscriber's refresh delivers again.
|
|
45
|
+
* - Secrets and identities are kept as JSON, as given, so give the layer a store only the server reads.
|
|
46
|
+
* - A kept subscription whose input or identity no longer decodes, as after a schema change, is reported at each build and never restored, until its subscriber unsubscribes it.
|
|
47
|
+
* - A store whose index cannot be read as the layer builds fails the build; a write that fails later is reported and tried again with the next, and pending writes get 5 seconds when the layer closes.
|
|
48
|
+
* - A callback's verification is not kept: a restored subscription delivers on, and a new one to the same callback after a restart is verified again.
|
|
49
|
+
*/
|
|
50
|
+
readonly persist?: boolean | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Whether a callback's name is resolved through `Dns` for its address check. `false` checks a
|
|
53
|
+
* literal address alone and leaves a name to the platform's egress, and the layer then needs no
|
|
54
|
+
* `Dns`: for a host with no resolver of its own, such as Cloudflare Workers. Defaults to `true`.
|
|
55
|
+
*
|
|
56
|
+
* **Gotchas**
|
|
57
|
+
*
|
|
58
|
+
* - With `false`, a callback named for a private or loopback address is POSTed to wherever the platform's egress reaches: give it only where that egress reaches no network of yours.
|
|
59
|
+
*/
|
|
60
|
+
readonly resolveCallbacks?: boolean | undefined;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Webhook delivery for the MCP endpoints and HTTP routes it is provided to, as SEP-3415 defines
|
|
64
|
+
* it: a subscriber names a callback URL and a secret, and each occurrence of a protected event
|
|
65
|
+
* it may see is POSTed there, signed as Standard Webhooks signs, until the subscription expires,
|
|
66
|
+
* is unsubscribed, or is refused. One layer holds the subscriptions of every surface it is
|
|
67
|
+
* provided to, so a subscription made over MCP is the one HTTP refreshes. It needs an
|
|
68
|
+
* `HttpClient` for the deliveries, `Dns` for the address check unless `resolveCallbacks` is
|
|
69
|
+
* `false`, and with `persist` a `KeyValueStore`. Each POST carries the
|
|
70
|
+
* headers `webhook-id`, `webhook-timestamp`, `webhook-signature` and `x-mcp-subscription-id`, and
|
|
71
|
+
* one JSON body: an occurrence `{ eventId, name, timestamp, data, cursor }`, whose `webhook-id`
|
|
72
|
+
* is its `eventId`, or an envelope, `{ type: "verification", challenge }` to echo,
|
|
73
|
+
* `{ type: "gap", cursor }` where occurrences were missed, or `{ type: "terminated", error }`
|
|
74
|
+
* where the subscription ended. Guide: `docs/Webhook.md` in this package.
|
|
75
|
+
*
|
|
76
|
+
* **Gotchas**
|
|
77
|
+
*
|
|
78
|
+
* - Only a protected event whose descriptor has `principal` is offered webhook delivery: elsewhere `events/list` lists no `webhook`, and a subscribe is `Unsupported`.
|
|
79
|
+
* - A `requestScoped` MCP endpoint offers it too, its subscriptions built once with the endpoint: on a host with no lasting process, leave this layer out, as `ActionHttp.layer` says, since each instance keeps its own subscriptions, lost as it ends.
|
|
80
|
+
* - A callback resolving to an address that is not globally reachable, a loopback or a private one included, is never POSTed to unless `allowedCallbacks` trusts it: a local receiver needs a public name, a trusted URL, or a test's `Dns.layerStatic`; an IPv6-only server behind NAT64 cannot deliver to an IPv4-only callback.
|
|
81
|
+
* - Each POST is checked against the address it resolves to, but fetch connects to the name again, so a name rebinding between the check and the connection is not caught: front the server with an egress proxy that refuses private addresses where that matters.
|
|
82
|
+
* - Deliveries run with fetch's `redirect: "manual"`, and a 3xx answer is a failed attempt; an `HttpClient` that is not fetch's follows redirects as it does.
|
|
83
|
+
* - A failed attempt's category is `connection_refused` for any transport failure, never `tls_error`, which no platform client reports apart.
|
|
84
|
+
* - A delivery's 2xx answer is a success, its body never read; a verification's answer is read as far as 4 KiB, and one longer fails its challenge.
|
|
85
|
+
* - A callback verified for a subscriber is not challenged again for that subscriber's other subscriptions to it while one is granted, as SEP-3415's soft state allows.
|
|
86
|
+
* - Deliveries are one at a time, in order: one failing holds back those after it, for `retryFor`, then the subscription suspends until a refresh, which delivers it again.
|
|
87
|
+
* - An occurrence given up, its body over 256 KiB or its POST answered `410` or `413`, is POSTed as a `gap` at its cursor; one whose admission fails with no refusal is reported and read again, from before it, every 5 seconds.
|
|
88
|
+
* - A refresh restarts its subscription's reading under the refreshed subscriber, from where delivery reached: the subscription hook set up again once the old one is released, and a delivery in flight sent again, which the subscriber drops by its `eventId`.
|
|
89
|
+
* - A refresh its authorizer refuses ends the subscription it names, POSTing `terminated`, though each delivery, admitted as the subscriber of the last grant, would still pass.
|
|
90
|
+
* - A refresh with a new secret signs each POST with both secrets for 5 minutes, `webhook-signature` holding two, so a receiver may switch over; after that, with the new one alone.
|
|
91
|
+
* - A delivery runs in the subscribe request's context without the request: an authorizer or a source reading `HttpServerRequest` fails in a delivery, so read what it needs in middleware, which provides it as a service.
|
|
92
|
+
* - `allowedCallbacks` throws when `layer` is called for an entry that is not an https URL.
|
|
93
|
+
*/
|
|
94
|
+
export declare function layer(options: Options & {
|
|
95
|
+
readonly persist?: false | undefined;
|
|
96
|
+
readonly resolveCallbacks: false;
|
|
97
|
+
}): Layer.Layer<Webhooks, never, HttpClient.HttpClient>;
|
|
98
|
+
export declare function layer(options: Options & {
|
|
99
|
+
readonly resolveCallbacks: false;
|
|
100
|
+
}): Layer.Layer<Webhooks, never, HttpClient.HttpClient | KeyValueStore.KeyValueStore>;
|
|
101
|
+
export declare function layer(options?: Options & {
|
|
102
|
+
readonly persist?: false | undefined;
|
|
103
|
+
}): Layer.Layer<Webhooks, never, HttpClient.HttpClient | Dns.Dns>;
|
|
104
|
+
export declare function layer(options: Options): Layer.Layer<Webhooks, never, HttpClient.HttpClient | Dns.Dns | KeyValueStore.KeyValueStore>;
|
|
105
|
+
/** Where a watch by webhook asks the server to POST, and the lifetime each grant asks for. */
|
|
106
|
+
export type DeliveryOptions = ConstructorParameters<typeof WebhookDelivery>[0];
|
|
107
|
+
/**
|
|
108
|
+
* A watch's `mode` delivering by webhook: `client.watch(event, input, { mode:
|
|
109
|
+
* Webhook.delivery({ baseUrl }) })` requires this process's `layerReceiver`, keeps a place there,
|
|
110
|
+
* subscribes the receiver's `path` under `baseUrl` with that place's id and secret, reads what
|
|
111
|
+
* the server POSTs there, refreshes the subscription when half its grant is left, also through a
|
|
112
|
+
* reconnect, and unsubscribes when the watch ends. Over MCP and HTTP; elsewhere the watch fails
|
|
113
|
+
* with `Event.SubscriptionError`, reason `Unsupported`.
|
|
114
|
+
*
|
|
115
|
+
* **Gotchas**
|
|
116
|
+
*
|
|
117
|
+
* - The receiver's route must be reached at `baseUrl` and its `path`: served elsewhere, or by another `layerReceiver` than the one the watch is given, the subscribe fails with `CallbackFailed`.
|
|
118
|
+
* - A `baseUrl` that is not an absolute https URL fails the watch with a `SchemaError` before it takes a place at the receiver.
|
|
119
|
+
* - A server refuses a callback that is not globally reachable, so `baseUrl` is a public name of this process, such as a tunnel's in development.
|
|
120
|
+
* - The URL subscribed carries a `watch` query parameter, one per watch, so watches never share a subscription: a proxy in front of the route must pass the query on.
|
|
121
|
+
* - A restarted process subscribes afresh from the cursor `resume` kept, so the server sends again what the last process accepted and never handed on; the old subscription lapses at its grant, each delivery to it answered 410.
|
|
122
|
+
*/
|
|
123
|
+
export declare const delivery: (options: DeliveryOptions) => WebhookDelivery;
|
|
124
|
+
/** Where `layerReceiver` serves its route. */
|
|
125
|
+
export interface ReceiverOptions {
|
|
126
|
+
/** The route's path, `/hooks/builds`, which each watch's `delivery` appends to its `baseUrl`. */
|
|
127
|
+
readonly path: `/${string}`;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The receiving end of watches by webhook in this process: a `POST` route at `path`, and the
|
|
131
|
+
* registry each watch keeps its place in, under an id its delivery URL names and a secret of its
|
|
132
|
+
* own. Each POST is verified as Standard Webhooks signs it, against the secret of the watch it
|
|
133
|
+
* names, its timestamp within 5 minutes, and handed to that watch; a verification is answered
|
|
134
|
+
* with its challenge, and a `webhook-id` among the watch's last 1,000 is acknowledged and dropped. Build it once,
|
|
135
|
+
* the watches given it as the route's layer is (`Layer.provideMerge`).
|
|
136
|
+
* Guide: `docs/Webhook.md` in this package.
|
|
137
|
+
*
|
|
138
|
+
* **Gotchas**
|
|
139
|
+
*
|
|
140
|
+
* - A POST naming no watch of this receiver is a 410, which the server abandons without retrying: what is in flight to a watch that ended is not delivered, and its subscription lapses at its grant.
|
|
141
|
+
* - A POST its watch's secret did not sign, or out of the timestamp window, is a 401; a signed body that is not a JSON object, a 400.
|
|
142
|
+
* - A watch reads at its consumer's pace: 16 deliveries wait for it, and the next is answered 503, which the server retries.
|
|
143
|
+
* - Two `layerReceiver` layers are two registries: a watch given one, its route served by the other, has its subscribe fail with `CallbackFailed`, every POST a 410.
|
|
144
|
+
* - A body is read whole: limit it with `HttpServerRequest.MaxBodySize` where the route is served, as `ActionHttp.layer`'s gotcha says.
|
|
145
|
+
*/
|
|
146
|
+
export declare const layerReceiver: ({ path }: ReceiverOptions) => Layer.Layer<Receiving, never, HttpRouter.HttpRouter>;
|
package/dist/Webhook.mjs
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { WebhookDelivery } from "./Event.mjs";
|
|
2
|
+
import { Receiving } from "./contract/receiving.mjs";
|
|
3
|
+
import { Webhooks } from "./contract/webhooks.mjs";
|
|
4
|
+
import { makeReceiver } from "./webhook/receiver.mjs";
|
|
5
|
+
import { makeStorage } from "./webhook/stored.mjs";
|
|
6
|
+
import { makeWebhooks } from "./webhook/subscriptions.mjs";
|
|
7
|
+
import { Duration, Effect, Layer } from "effect";
|
|
8
|
+
import { HttpRouter } from "effect/http";
|
|
9
|
+
import { Dns } from "effect/net";
|
|
10
|
+
import { KeyValueStore } from "effect/persistence";
|
|
11
|
+
// src/webhook/Webhook.ts
|
|
12
|
+
const trustedOf = (prefix) => {
|
|
13
|
+
const url = URL.parse(prefix);
|
|
14
|
+
if (url?.protocol !== "https:") throw new Error(`Invalid allowedCallbacks entry: ${prefix} (give an https URL, such as https://hooks.example.com/)`);
|
|
15
|
+
return url;
|
|
16
|
+
};
|
|
17
|
+
const maxTtlOf = (input) => {
|
|
18
|
+
const maxTtl = Duration.fromInputUnsafe(input);
|
|
19
|
+
if (!Duration.isFinite(maxTtl)) throw new Error(`Invalid maxTtl: ${Duration.format(maxTtl)} (every grant expires: give a finite duration, such as "1 hour")`);
|
|
20
|
+
return maxTtl;
|
|
21
|
+
};
|
|
22
|
+
function layer(options = {}) {
|
|
23
|
+
const allowedCallbacks = (options.allowedCallbacks ?? []).map(trustedOf);
|
|
24
|
+
const maxTtl = maxTtlOf(options.maxTtl ?? "1 hour");
|
|
25
|
+
return Layer.effect(Webhooks, Effect.gen(function* () {
|
|
26
|
+
const dns = options.resolveCallbacks === false ? void 0 : yield* Dns.Dns;
|
|
27
|
+
const storage = options.persist === true ? yield* Effect.flatMap(KeyValueStore.KeyValueStore, makeStorage) : void 0;
|
|
28
|
+
return yield* makeWebhooks({
|
|
29
|
+
maxTtl,
|
|
30
|
+
allowedCallbacks,
|
|
31
|
+
retryFor: Duration.fromInputUnsafe(options.retryFor ?? "1 hour"),
|
|
32
|
+
storage,
|
|
33
|
+
dns
|
|
34
|
+
});
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A watch's `mode` delivering by webhook: `client.watch(event, input, { mode:
|
|
39
|
+
* Webhook.delivery({ baseUrl }) })` requires this process's `layerReceiver`, keeps a place there,
|
|
40
|
+
* subscribes the receiver's `path` under `baseUrl` with that place's id and secret, reads what
|
|
41
|
+
* the server POSTs there, refreshes the subscription when half its grant is left, also through a
|
|
42
|
+
* reconnect, and unsubscribes when the watch ends. Over MCP and HTTP; elsewhere the watch fails
|
|
43
|
+
* with `Event.SubscriptionError`, reason `Unsupported`.
|
|
44
|
+
*
|
|
45
|
+
* **Gotchas**
|
|
46
|
+
*
|
|
47
|
+
* - The receiver's route must be reached at `baseUrl` and its `path`: served elsewhere, or by another `layerReceiver` than the one the watch is given, the subscribe fails with `CallbackFailed`.
|
|
48
|
+
* - A `baseUrl` that is not an absolute https URL fails the watch with a `SchemaError` before it takes a place at the receiver.
|
|
49
|
+
* - A server refuses a callback that is not globally reachable, so `baseUrl` is a public name of this process, such as a tunnel's in development.
|
|
50
|
+
* - The URL subscribed carries a `watch` query parameter, one per watch, so watches never share a subscription: a proxy in front of the route must pass the query on.
|
|
51
|
+
* - A restarted process subscribes afresh from the cursor `resume` kept, so the server sends again what the last process accepted and never handed on; the old subscription lapses at its grant, each delivery to it answered 410.
|
|
52
|
+
*/
|
|
53
|
+
const delivery = (options) => new WebhookDelivery(options);
|
|
54
|
+
/**
|
|
55
|
+
* The receiving end of watches by webhook in this process: a `POST` route at `path`, and the
|
|
56
|
+
* registry each watch keeps its place in, under an id its delivery URL names and a secret of its
|
|
57
|
+
* own. Each POST is verified as Standard Webhooks signs it, against the secret of the watch it
|
|
58
|
+
* names, its timestamp within 5 minutes, and handed to that watch; a verification is answered
|
|
59
|
+
* with its challenge, and a `webhook-id` among the watch's last 1,000 is acknowledged and dropped. Build it once,
|
|
60
|
+
* the watches given it as the route's layer is (`Layer.provideMerge`).
|
|
61
|
+
* Guide: `docs/Webhook.md` in this package.
|
|
62
|
+
*
|
|
63
|
+
* **Gotchas**
|
|
64
|
+
*
|
|
65
|
+
* - A POST naming no watch of this receiver is a 410, which the server abandons without retrying: what is in flight to a watch that ended is not delivered, and its subscription lapses at its grant.
|
|
66
|
+
* - A POST its watch's secret did not sign, or out of the timestamp window, is a 401; a signed body that is not a JSON object, a 400.
|
|
67
|
+
* - A watch reads at its consumer's pace: 16 deliveries wait for it, and the next is answered 503, which the server retries.
|
|
68
|
+
* - Two `layerReceiver` layers are two registries: a watch given one, its route served by the other, has its subscribe fail with `CallbackFailed`, every POST a 410.
|
|
69
|
+
* - A body is read whole: limit it with `HttpServerRequest.MaxBodySize` where the route is served, as `ActionHttp.layer`'s gotcha says.
|
|
70
|
+
*/
|
|
71
|
+
const layerReceiver = ({ path }) => Layer.unwrap(Effect.map(makeReceiver(path), ({ receiving, handler }) => Layer.merge(Layer.succeed(Receiving, receiving), HttpRouter.add("POST", path, handler))));
|
|
72
|
+
export { delivery, layer, layerReceiver };
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { Codec } from "../contract/codec.mjs";
|
|
1
2
|
import { Anyone } from "../contract/caller.mjs";
|
|
2
3
|
import { Refusal } from "../contract/errors.mjs";
|
|
4
|
+
import { EventOf, ServingEvents } from "../contract/feed.mjs";
|
|
5
|
+
import { Any as Any$1 } from "../Event.mjs";
|
|
3
6
|
import { BuildServices, Protected, ServedRequest, Serving } from "../contract/implementation.mjs";
|
|
4
|
-
import { Any as Any$
|
|
7
|
+
import { Any as Any$2, Errors } from "../Action.mjs";
|
|
5
8
|
import { Context, Effect } from "effect";
|
|
6
9
|
import { HttpRouter, HttpServerResponse } from "effect/http";
|
|
7
10
|
import { HttpApiMiddleware, HttpApiSecurity } from "effect/http-api";
|
|
@@ -30,6 +33,7 @@ export interface SecurityMiddleware<I, Name extends string = string> {
|
|
|
30
33
|
};
|
|
31
34
|
}
|
|
32
35
|
type Response = Effect.Effect<HttpServerResponse.HttpServerResponse, unknown, unknown>;
|
|
36
|
+
/** What a provider holds for the surfaces serving its descriptor, beside its identity. */
|
|
33
37
|
interface Runtime$1 {
|
|
34
38
|
/** The descriptor the provider was built from, the one a binding naming it must hold. */
|
|
35
39
|
readonly descriptor: unknown;
|
|
@@ -71,6 +75,10 @@ export interface Descriptor<I, A, S extends Security, Name extends string = stri
|
|
|
71
75
|
readonly security: S;
|
|
72
76
|
/** `Options.error` of `make`, as a list. */
|
|
73
77
|
readonly error: E;
|
|
78
|
+
/** `Options.principal` of `make`, the stable id of an identity, where given. */
|
|
79
|
+
principal?(identity: A): string;
|
|
80
|
+
/** `Options.schema` of `make`, how an identity is kept, where given. */
|
|
81
|
+
readonly schema?: Codec | undefined;
|
|
74
82
|
readonly "~provider": Context.Key<Provider<I, Name>, Runtime$1>;
|
|
75
83
|
readonly "~middleware": Context.Key<SecurityMiddleware<I, Name>, unknown>;
|
|
76
84
|
/** An MCP request's verified identity, which a protected tool's call promotes. */
|
|
@@ -81,16 +89,19 @@ export interface Descriptor<I, A, S extends Security, Name extends string = stri
|
|
|
81
89
|
* binding name one.
|
|
82
90
|
*/
|
|
83
91
|
export type Any = Descriptor<unknown, unknown, Security, string, Errors>;
|
|
84
|
-
|
|
85
|
-
type
|
|
86
|
-
|
|
92
|
+
/** The identity an action's `caller` names, or `never` for none. */
|
|
93
|
+
export type Identity<A extends Pick<Any$2, "caller">> = [A] extends [never] ? never : A["caller"] extends Context.Key<infer I, unknown> ? I : never;
|
|
94
|
+
type Covered<A extends Pick<Any$2, "caller">> = Exclude<A["caller"], typeof Anyone> extends Context.Key<infer I, unknown> ? I : never;
|
|
95
|
+
/** The `authentication` a binding of these actions takes: required once one is protected. */
|
|
96
|
+
export type Required<A extends Pick<Any$2, "caller">> = [Protected<A>] extends [never] ? {
|
|
87
97
|
readonly authentication?: Any;
|
|
88
98
|
} : {
|
|
89
99
|
readonly authentication: Descriptor<Covered<Protected<A>>, unknown, Security, string, Errors>;
|
|
90
100
|
};
|
|
101
|
+
/** The provider `Authentication.layer` builds for the descriptor `D`. */
|
|
91
102
|
export type ProviderOf<D> = D extends Descriptor<infer I, unknown, Security, infer Name, Errors> ? Provider<I, Name> : never;
|
|
92
103
|
/** Actions a binding takes without options: public ones, as a protected one names its descriptor. */
|
|
93
|
-
export type PublicOnly<Actions extends ReadonlyArray<Any$
|
|
104
|
+
export type PublicOnly<Actions extends ReadonlyArray<Pick<Any$2, "caller">>> = [Exclude<Actions[number], {
|
|
94
105
|
readonly caller: typeof Anyone;
|
|
95
106
|
}>] extends [never] ? unknown : {
|
|
96
107
|
readonly "Protected actions take options naming their authentication": never;
|
|
@@ -100,19 +111,29 @@ export type DescriptorOf<O> = [O] extends [{
|
|
|
100
111
|
readonly authentication: infer D extends Any;
|
|
101
112
|
}] ? D : undefined;
|
|
102
113
|
/** All protected actions on a surface use the descriptor's single identity key. */
|
|
103
|
-
export type Matching<A extends Any$
|
|
114
|
+
export type Matching<A extends Pick<Any$2, "caller">, D> = unknown extends Covered<Protected<A>> ? unknown : [Exclude<Covered<Protected<A>>, D extends Descriptor<infer I, unknown, Security, string, Errors> ? I : never>] extends [never] ? unknown : {
|
|
104
115
|
readonly "Authentication descriptor does not cover every protected action": never;
|
|
105
116
|
};
|
|
106
117
|
/** Keep public requirements separate, without rescanning all handlers for every action. */
|
|
107
|
-
export type RemoteRequest<App, A extends Any$
|
|
118
|
+
export type RemoteRequest<App, A extends Any$2> = ServedRequest<App, Extract<A, {
|
|
108
119
|
readonly caller: typeof Anyone;
|
|
109
120
|
}>> | Exclude<ServedRequest<App, Protected<A>>, Identity<Protected<A>>>;
|
|
121
|
+
/**
|
|
122
|
+
* The identity a remote layer's authentication provides every request, where `App` serves only
|
|
123
|
+
* protected actions of `A` and event types of `E`: its middleware reading it then owes nothing.
|
|
124
|
+
* None where one served is public, whose requests authenticate no one.
|
|
125
|
+
*/
|
|
126
|
+
export type LayerIdentity<App, A extends Any$2, E extends Any$1> = [Extract<Serving<App, A>, {
|
|
127
|
+
readonly caller: typeof Anyone;
|
|
128
|
+
}> | Extract<ServingEvents<App, E>, {
|
|
129
|
+
readonly caller: typeof Anyone;
|
|
130
|
+
}>] extends [never] ? Identity<Protected<Serving<App, A>> | Protected<ServingEvents<App, E>>> : never;
|
|
110
131
|
/**
|
|
111
132
|
* What a server of the actions `A` of the implementations `App` reads at startup, as
|
|
112
133
|
* `BuildServices` states it, with the caller a builder yields named as the mistake it is: a server
|
|
113
134
|
* has its caller per request.
|
|
114
135
|
*/
|
|
115
|
-
export type ServerBuildServices<App, A extends Any$
|
|
136
|
+
export type ServerBuildServices<App, A extends Any$2> = App extends unknown ? BuildServices<App, A> extends (infer R) ? R extends Identity<Protected<A>> ? {
|
|
116
137
|
readonly "A builder yields the caller, which a server has per request: yield it in a handler or an authorizer": {
|
|
117
138
|
readonly caller: R;
|
|
118
139
|
readonly builderOf: Serving<App, A>["name"];
|
|
@@ -122,8 +143,12 @@ export type ServerBuildServices<App, A extends Any$1> = App extends unknown ? Bu
|
|
|
122
143
|
* What a call of `A` under the descriptor `D` may fail with from its verifier: the errors `D`
|
|
123
144
|
* declares, for a protected action; none for a public one, which nothing verifies.
|
|
124
145
|
*/
|
|
125
|
-
export type VerifierError<A extends Any$
|
|
146
|
+
export type VerifierError<A extends Pick<Any$2, "caller">, D> = A["caller"] extends typeof Anyone ? never : D extends {
|
|
126
147
|
readonly error: infer E extends Errors;
|
|
127
148
|
} ? E[number] : never;
|
|
128
|
-
|
|
149
|
+
/**
|
|
150
|
+
* The provider a surface's startup reads under the descriptor `D`, where the implementations
|
|
151
|
+
* `App` serve a protected action of `A` or a protected event type of `E`.
|
|
152
|
+
*/
|
|
153
|
+
export type ServedProvider<App, A extends Any$2, D, E extends Any$1 = EventOf<App>> = [Protected<Serving<App, A>> | Protected<E>] extends [never] ? never : ProviderOf<D>;
|
|
129
154
|
export { Runtime$1 as Runtime };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Unauthenticated } from "../contract/errors.mjs";
|
|
2
2
|
import { Effect, Option } from "effect";
|
|
3
3
|
// src/authentication/provider.ts
|
|
4
|
-
const providerOf = (auth) => Effect.
|
|
4
|
+
const providerOf = (auth) => Effect.filterOrElse(Effect.service(auth["~provider"]), (provider) => provider.descriptor === auth, () => Effect.die(/* @__PURE__ */ new Error(`Authentication "${auth.name}": the descriptor named here is not the one its provider was built from (build both from one descriptor)`)));
|
|
5
5
|
const promote = (auth, run) => Effect.flatMap(Effect.serviceOption(auth["~verified"]), (actor) => Option.isSome(actor) ? Effect.provideService(run, auth.identity, actor.value) : Effect.fail(new Unauthenticated()));
|
|
6
6
|
export { promote, providerOf };
|
|
@@ -10,9 +10,9 @@ const declaredResponse = (errors, error) => {
|
|
|
10
10
|
const schema = errorSchemas(errors).map(declared).find((each) => Schema.is(each)(error));
|
|
11
11
|
return schema === void 0 ? void 0 : responseOf(schema, error);
|
|
12
12
|
};
|
|
13
|
-
const rfc6750ErrorDescriptionCharacters = /^[\
|
|
13
|
+
const rfc6750ErrorDescriptionCharacters = /^[\u0020\u0021\u0023-\u005B\u005D-\u007E]+$/u;
|
|
14
14
|
const challenge = (scheme, parameters) => {
|
|
15
|
-
const given = parameters.flatMap(([name, value]) => value === void 0 ? [] : [`${name}="${value.
|
|
15
|
+
const given = parameters.flatMap(([name, value]) => value === void 0 ? [] : [`${name}="${value.replaceAll(/["\\]/gu, "\\$&")}"`]);
|
|
16
16
|
return given.length === 0 ? scheme : `${scheme} ${given.join(", ")}`;
|
|
17
17
|
};
|
|
18
18
|
const bearer = (parameters) => challenge("Bearer", parameters);
|
package/dist/cli/commands.d.mts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import "../contract/errors.mjs";
|
|
2
|
+
import "../Event.mjs";
|
|
3
|
+
import "../contract/progress.mjs";
|
|
4
|
+
import "../contract/implementation.mjs";
|
|
5
|
+
import { Each } from "../contract/stream.mjs";
|
|
2
6
|
import { Any } from "../Action.mjs";
|
|
3
|
-
import { Effect, Option, Runtime, Types } from "effect";
|
|
7
|
+
import { Effect, Option, Runtime, Stream, Types } from "effect";
|
|
4
8
|
import { CliError, Command } from "effect/cli";
|
|
5
|
-
type Field<A extends Any> = A["input"]["Encoded"] extends (infer E) ? true extends Types.IsUnion<E> ? never : E extends
|
|
9
|
+
type Field<A extends Any> = A["input"]["Encoded"] extends (infer E) ? true extends Types.IsUnion<E> ? never : E extends readonly unknown[] ? never : E extends object ? string extends keyof E ? never : Extract<keyof E, string> : never : never;
|
|
6
10
|
type StdinField<A extends Any> = A["input"]["Encoded"] extends (infer E) ? Exclude<Field<A>, { [K in keyof E]-?: {} extends Pick<E, K> ? K : never; }[keyof E]> : never;
|
|
7
11
|
/** How one command of `A` is named, takes its input and prints its result. */
|
|
8
12
|
export interface Options<A extends Any> {
|
|
@@ -16,10 +20,10 @@ export interface Options<A extends Any> {
|
|
|
16
20
|
*/
|
|
17
21
|
readonly description?: string;
|
|
18
22
|
/**
|
|
19
|
-
* Human output: the decoded success to text for a person
|
|
20
|
-
* command that selects JSON instead.
|
|
23
|
+
* Human output: the decoded success to text for a person, or of an action answering a stream
|
|
24
|
+
* each element to its line. Adds a `--json` flag to the command that selects JSON instead.
|
|
21
25
|
*/
|
|
22
|
-
|
|
26
|
+
render?(this: void, output: Each<A["success"]>): string;
|
|
23
27
|
/**
|
|
24
28
|
* Input fields taken as positional arguments instead of flags, in this order: `["id"]` takes
|
|
25
29
|
* `get-note 7` for `--id 7`. An optional field is an optional argument, and an array field
|