@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
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { Codec } from "./contract/codec.mjs";
|
|
1
2
|
import { Refusal } from "./contract/errors.mjs";
|
|
2
3
|
import { Known, OptionalUnless } from "./contract/implementation.mjs";
|
|
3
4
|
import { Errors } from "./Action.mjs";
|
|
4
|
-
import { Any, Descriptor, Provider, Security } from "./authentication/provider.mjs";
|
|
5
5
|
import { ErrorsOf } from "./contract/declared.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import { Any, Descriptor, Provider, Security } from "./authentication/provider.mjs";
|
|
7
|
+
import { Array, Context, Effect, Layer, Option, Redacted, Schema, Scope } from "effect";
|
|
7
8
|
import { HttpRouter, HttpServerResponse } from "effect/http";
|
|
8
9
|
import { HttpApiSecurity } from "effect/http-api";
|
|
9
10
|
/**
|
|
@@ -13,16 +14,18 @@ import { HttpApiSecurity } from "effect/http-api";
|
|
|
13
14
|
* which holds the header and no request; a route of the host's own is authenticated by
|
|
14
15
|
* `protect`.
|
|
15
16
|
*/
|
|
16
|
-
export declare const bearerTokenOf: (authorization: string | null | undefined) => Option.Option<Redacted.Redacted
|
|
17
|
+
export declare const bearerTokenOf: (authorization: string | null | undefined) => Option.Option<Redacted.Redacted>;
|
|
17
18
|
/** An OAuth protected resource (RFC 9728), as `layer` publishes it. */
|
|
18
19
|
export interface ProtectedResource {
|
|
19
20
|
/**
|
|
20
|
-
*
|
|
21
|
-
* `${url}/mcp
|
|
22
|
-
*
|
|
21
|
+
* OAuth resource identifier: the public URL of the endpoint clients connect to, such as
|
|
22
|
+
* `${url}/mcp`, or its origin, such as `https://api.example.com`, which MCP allows to name one
|
|
23
|
+
* resource for every endpoint it serves. Discovery is served at
|
|
24
|
+
* `/.well-known/oauth-protected-resource` followed by its path and query.
|
|
23
25
|
*
|
|
24
26
|
* **Gotchas**
|
|
25
27
|
*
|
|
28
|
+
* - A client applying RFC 9728 §3.3 strictly finds no authorization server for an origin, since metadata reached from a challenge must name the URL it requested; the official MCP client accepts it, and the endpoint's URL works with both.
|
|
26
29
|
* - Discovery answers 404, and an MCP client finds no authorization server, when a request's path and query differ from this URL's, or when the provider is provided to no served layer.
|
|
27
30
|
* - That it and `authorizationServers` are HTTPS, or loopback HTTP in development, is the deployment's to ensure: nothing checks it.
|
|
28
31
|
*/
|
|
@@ -33,7 +36,7 @@ export interface ProtectedResource {
|
|
|
33
36
|
* Every scope the resource accepts, each an OAuth scope token, which a client requests when a
|
|
34
37
|
* 401 names none.
|
|
35
38
|
*/
|
|
36
|
-
readonly scopesSupported?:
|
|
39
|
+
readonly scopesSupported?: readonly string[];
|
|
37
40
|
/**
|
|
38
41
|
* The scopes every 401 names, each an OAuth scope token: what a client requests when it
|
|
39
42
|
* authenticates, rather than every scope supported.
|
|
@@ -78,8 +81,8 @@ type DeclaredOf<D> = D extends {
|
|
|
78
81
|
* naming scopes. `HttpServerResponse.toWeb` gives it as a web `Response`.
|
|
79
82
|
*/
|
|
80
83
|
export declare const refusalResponse: <D extends Any = never>(error: Refusal | DeclaredOf<D>, options?: RefusalResponseOptions<D>) => HttpServerResponse.HttpServerResponse;
|
|
81
|
-
/** What `make` takes besides the name and the identity
|
|
82
|
-
export interface Options {
|
|
84
|
+
/** What `make` takes besides the name and the identity `A`. */
|
|
85
|
+
export interface Options<A = unknown> {
|
|
83
86
|
/**
|
|
84
87
|
* The one native scheme a caller proves the identity by, Bearer when left out: an API key in a
|
|
85
88
|
* cookie, a header or the query, such as `HttpApiSecurity.apiKey({ in: "cookie", key: "session" })`,
|
|
@@ -100,7 +103,35 @@ export interface Options {
|
|
|
100
103
|
* - A failure that is not the caller's, such as an issuer the verifier cannot reach, belongs here with `{ httpApiStatus: 503 }`: an `Unauthenticated` would have the caller sign in again.
|
|
101
104
|
*/
|
|
102
105
|
readonly error?: Errors | Errors[number];
|
|
106
|
+
/**
|
|
107
|
+
* The stable id of an identity, such as its user id: what tells subscribers apart where a
|
|
108
|
+
* surface keeps a subscription between requests, such as an MCP poll subscription's lease.
|
|
109
|
+
*
|
|
110
|
+
* **Gotchas**
|
|
111
|
+
*
|
|
112
|
+
* - A surface serving a protected event type whose source has a `subscription` hook refuses to start without it, its message naming this option.
|
|
113
|
+
* - Two identities with one principal share those subscriptions, so give each its own: a tenant's id alone merges its users.
|
|
114
|
+
* - Without it no surface offers a protected event by webhook, which a subscription keys by its principal: the listing names no webhook, and a webhook subscription fails with `Event.SubscriptionError`, reason `Unsupported`.
|
|
115
|
+
* - Without it every caller shares one idempotency key space: another caller sending a key with the same input is given the first call's answer, as `Action.Options.idempotencyKey` says.
|
|
116
|
+
*/
|
|
117
|
+
readonly principal?: ((identity: A) => string) | undefined;
|
|
118
|
+
/**
|
|
119
|
+
* The identity's schema, its `Type` the identity: how a surface keeps an identity past a
|
|
120
|
+
* restart, as `Webhook.layer` with `persist` keeps each webhook subscriber's, its JSON in the
|
|
121
|
+
* store.
|
|
122
|
+
*
|
|
123
|
+
* **Gotchas**
|
|
124
|
+
*
|
|
125
|
+
* - `Webhook.layer` with `persist` refuses to serve a protected event by webhook without it, its message naming this option.
|
|
126
|
+
*/
|
|
127
|
+
readonly schema?: Codec | undefined;
|
|
103
128
|
}
|
|
129
|
+
type PrincipalOf<A> = NoInfer<Pick<Options<A>, "principal">>;
|
|
130
|
+
type SchemaOf<O, A> = O extends {
|
|
131
|
+
readonly schema: infer S extends Schema.Top;
|
|
132
|
+
} ? [S["Type"], A] extends [A, S["Type"]] ? unknown : {
|
|
133
|
+
readonly schema: "a schema whose Type is the identity's";
|
|
134
|
+
} : unknown;
|
|
104
135
|
type SecurityOf<O> = O extends {
|
|
105
136
|
readonly security: infer S extends Security;
|
|
106
137
|
} ? S : "security" extends keyof O ? NonNullable<O["security" & keyof O]> | HttpApiSecurity.Http : HttpApiSecurity.Http;
|
|
@@ -111,7 +142,7 @@ type SecurityOf<O> = O extends {
|
|
|
111
142
|
* one name only for one declaration, whose values a server and a client may hold as `layer` says.
|
|
112
143
|
* Guide: `docs/Authentication.md` in this package; in an application: `examples/notes-service/src/identity/`.
|
|
113
144
|
*/
|
|
114
|
-
export declare const make: <const Name extends string, I, A, const O extends Options = {}>(name: Name, identity: Context.Key<I, A>, ...options: OptionalUnless<O, O & NoInfer<Known<O, Options
|
|
145
|
+
export declare const make: <const Name extends string, I, A, const O extends Options<never> = {}>(name: Name, identity: Context.Key<I, A>, ...options: OptionalUnless<O, O & NoInfer<Known<O, Options>> & PrincipalOf<A> & SchemaOf<O, A>>) => Descriptor<I, A, SecurityOf<O>, Name, ErrorsOf<O>>;
|
|
115
146
|
/**
|
|
116
147
|
* A descriptor's verifier: the credential its scheme decodes, as Effect's own decoder gives
|
|
117
148
|
* it, to the identity, or a refusal, or an error the descriptor declares, `E`. Its services
|
|
@@ -154,6 +185,7 @@ type Resource = ProtectedResource | Effect.Effect<ProtectedResource | undefined,
|
|
|
154
185
|
* **Gotchas**
|
|
155
186
|
*
|
|
156
187
|
* - Call it once per descriptor, and provide that value to every layer serving a protected action: nothing else satisfies the provider they require, neither the identity nor another descriptor's provider, which type-checks under the same name and identity, then dies when the layer builds; only a client may hold another descriptor of the name, such as one of a cookie name the server decides at startup.
|
|
188
|
+
* - Over `ActionRpc` a verifier reading `HttpServerRequest` reads one holding the message's headers alone, a socket's upgrade headers before its own, at `/` with no query or body, on every protocol: one judging the URL or query sees none of the client's.
|
|
157
189
|
*/
|
|
158
190
|
export declare function layer<const Name extends string, I, A, const S extends Security, E extends Errors, R, EX = never, RX = never, P extends Resource = undefined>(authentication: Descriptor<I, A, S, Name, E>, verify: Verify<NoInfer<A>, NoInfer<S>, R, NoInfer<E[number]["Type"]>> | Effect.Effect<Verify<NoInfer<A>, NoInfer<S>, R, NoInfer<E[number]["Type"]>>, EX, RX>, options?: {
|
|
159
191
|
/** The OAuth protected resource to publish: its rules are on `LayerOptions.protectedResource`. */
|
package/dist/Authentication.mjs
CHANGED
|
@@ -2,11 +2,11 @@ import { Unauthenticated, builtIns, scopeToken } from "./contract/errors.mjs";
|
|
|
2
2
|
import { assertDistinctTags, errorList } from "./contract/declared.mjs";
|
|
3
3
|
import { providerOf } from "./authentication/provider.mjs";
|
|
4
4
|
import { answer, answerStepUp, bearer, challenge, declaredResponse, isRefusal, isStepUp, plain } from "./authentication/refusal.mjs";
|
|
5
|
-
import { Context, Effect, Layer, Match, Option, Predicate, Redacted } from "effect";
|
|
5
|
+
import { Context, Effect, Function, Layer, Match, Option, Predicate, Redacted } from "effect";
|
|
6
6
|
import { HttpEffect, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
|
|
7
7
|
import { HttpApiBuilder, HttpApiMiddleware, HttpApiSecurity } from "effect/http-api";
|
|
8
8
|
// src/authentication/Authentication.ts
|
|
9
|
-
const bearerAuthorizationHeader = /^[ \t]*Bearer +(
|
|
9
|
+
const bearerAuthorizationHeader = /^[ \t]*Bearer +(?<token>.*[^ \t])[ \t]*$/iu;
|
|
10
10
|
/**
|
|
11
11
|
* The bearer token of an `Authorization` header, `authorization`, or none, read as Effect's
|
|
12
12
|
* `HttpApiSecurity.bearer` reads it for routes and MCP: the scheme matched case-insensitively,
|
|
@@ -14,7 +14,7 @@ const bearerAuthorizationHeader = /^[ \t]*Bearer +(.*[^ \t])[ \t]*$/i;
|
|
|
14
14
|
* which holds the header and no request; a route of the host's own is authenticated by
|
|
15
15
|
* `protect`.
|
|
16
16
|
*/
|
|
17
|
-
const bearerTokenOf = (authorization) => Option.map(Option.fromNullishOr(bearerAuthorizationHeader.exec(authorization ?? "")?.[
|
|
17
|
+
const bearerTokenOf = (authorization) => Option.map(Option.fromNullishOr(bearerAuthorizationHeader.exec(authorization ?? "")?.groups?.["token"]), (token) => Redacted.make(token));
|
|
18
18
|
const metadataUrlOf = (options) => {
|
|
19
19
|
const resource = new URL(options.resource);
|
|
20
20
|
const url = new URL(resource);
|
|
@@ -43,10 +43,13 @@ const assertPublishableResource = (oauth, options) => {
|
|
|
43
43
|
if (resource === null) throw new Error(`A protected resource is an absolute URL: ${JSON.stringify(options.resource)} is not one; give the public URL of the endpoint clients connect to, such as https://api.example.com/mcp, read from configuration`);
|
|
44
44
|
if (resource.href.includes("#")) throw new Error(`A protected resource has no fragment: ${options.resource}; drop the "#", which no request URL carries, so its discovery would never answer`);
|
|
45
45
|
};
|
|
46
|
-
const withNoStoreAndChallenge = (response,
|
|
46
|
+
const withNoStoreAndChallenge = (response, wwwAuthenticate) => HttpServerResponse.setHeaders(response, {
|
|
47
47
|
...response.headers["cache-control"] === void 0 ? { "cache-control": "no-store" } : {},
|
|
48
|
-
...
|
|
48
|
+
...wwwAuthenticate !== void 0 && response.status === 401 && response.headers["www-authenticate"] === void 0 ? { "www-authenticate": wwwAuthenticate } : {}
|
|
49
49
|
});
|
|
50
|
+
const isBearer = (security) => Predicate.isTagged(security, "Http") && security.scheme.toLowerCase() === "bearer";
|
|
51
|
+
const openApiComponentKey = /^[\w.-]+$/u;
|
|
52
|
+
const nonBearerChallengeOf = (security, name) => Match.value(security).pipe(Match.tag("Http", ({ scheme }) => challenge(scheme, [])), Match.tag("Basic", () => challenge("Basic", [["realm", name]])), Match.tag("ApiKey", Function.constUndefined), Match.exhaustive);
|
|
50
53
|
/**
|
|
51
54
|
* The response `layer` answers a refusal with, for a caller the router never routes, such as a
|
|
52
55
|
* Node `upgrade` handler admitting a socket, which holds a header and no request: read its token
|
|
@@ -60,15 +63,29 @@ const refusalResponse = (error, options) => {
|
|
|
60
63
|
const oauth = authentication === void 0 || isBearer(authentication.security);
|
|
61
64
|
assertPublishableResource(oauth, options?.protectedResource);
|
|
62
65
|
const named = namedOf(options?.protectedResource);
|
|
63
|
-
const
|
|
66
|
+
const wwwAuthenticate = authentication !== void 0 && !oauth ? nonBearerChallengeOf(authentication.security, authentication.name) : bearerChallengeOf(named, presentsBearerToken(options?.authorization));
|
|
64
67
|
if (!isRefusal(error)) {
|
|
65
68
|
const response = declaredResponse(authentication?.error ?? [], error);
|
|
66
69
|
if (response === void 0) throw new Error("Not a refusal, nor an error the authentication declares: pass the descriptor declaring it as `authentication`, or answer the error yourself");
|
|
67
|
-
return withNoStoreAndChallenge(response,
|
|
70
|
+
return withNoStoreAndChallenge(response, wwwAuthenticate);
|
|
68
71
|
}
|
|
69
|
-
return withNoStoreAndChallenge(oauth ? answer(error, named.metadataUrl) : plain(error),
|
|
72
|
+
return withNoStoreAndChallenge(oauth ? answer(error, named.metadataUrl) : plain(error), wwwAuthenticate);
|
|
73
|
+
};
|
|
74
|
+
const mayResolveToDiscoveryPath = /\.well-known|[\t\n\r]/u;
|
|
75
|
+
const corsPreflight = (request) => {
|
|
76
|
+
const requested = request.headers["access-control-request-headers"];
|
|
77
|
+
return HttpServerResponse.empty({
|
|
78
|
+
status: 204,
|
|
79
|
+
headers: {
|
|
80
|
+
"access-control-allow-origin": "*",
|
|
81
|
+
"access-control-allow-methods": "GET, HEAD, OPTIONS",
|
|
82
|
+
...requested === void 0 ? {} : {
|
|
83
|
+
"access-control-allow-headers": requested,
|
|
84
|
+
vary: "Access-Control-Request-Headers"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
70
88
|
};
|
|
71
|
-
const mayResolveToDiscoveryPath = /\.well-known|[\t\n\r]/;
|
|
72
89
|
const discoveryMiddleware = (options) => {
|
|
73
90
|
const discoveryUrl = metadataUrlOf(options);
|
|
74
91
|
const target = discoveryUrl.href.slice(discoveryUrl.origin.length);
|
|
@@ -79,20 +96,6 @@ const discoveryMiddleware = (options) => {
|
|
|
79
96
|
scopes_supported: options.scopesSupported,
|
|
80
97
|
resource_name: options.resourceName
|
|
81
98
|
}, { headers: { "access-control-allow-origin": "*" } });
|
|
82
|
-
const corsPreflight = (request) => {
|
|
83
|
-
const requested = request.headers["access-control-request-headers"];
|
|
84
|
-
return HttpServerResponse.empty({
|
|
85
|
-
status: 204,
|
|
86
|
-
headers: {
|
|
87
|
-
"access-control-allow-origin": "*",
|
|
88
|
-
"access-control-allow-methods": "GET, HEAD, OPTIONS",
|
|
89
|
-
...requested === void 0 ? {} : {
|
|
90
|
-
"access-control-allow-headers": requested,
|
|
91
|
-
vary: "Access-Control-Request-Headers"
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
};
|
|
96
99
|
return (next) => Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) => {
|
|
97
100
|
const targeted = mayResolveToDiscoveryPath.test(request.url) && URL.canParse(request.url, discoveryUrl.origin) && new URL(request.url, discoveryUrl.origin).href.slice(discoveryUrl.origin.length) === target;
|
|
98
101
|
if (targeted && (request.method === "GET" || request.method === "HEAD")) return Effect.succeed(metadata);
|
|
@@ -109,9 +112,6 @@ const resourceAnswersOf = (options) => {
|
|
|
109
112
|
published: options === void 0 ? void 0 : discoveryMiddleware(options)
|
|
110
113
|
};
|
|
111
114
|
};
|
|
112
|
-
const isBearer = (security) => Predicate.isTagged(security, "Http") && security.scheme.toLowerCase() === "bearer";
|
|
113
|
-
const openApiComponentKey = /^[\w.-]+$/;
|
|
114
|
-
const nonBearerChallengeOf = (security, name) => Match.value(security).pipe(Match.tag("Http", ({ scheme }) => challenge(scheme, [])), Match.tag("Basic", () => challenge("Basic", [["realm", name]])), Match.tag("ApiKey", () => void 0), Match.exhaustive);
|
|
115
115
|
/**
|
|
116
116
|
* Browser-safe declaration shared by clients and remote surfaces: the `identity` it
|
|
117
117
|
* authenticates, and the scheme a caller proves it by, `Options.security`. The literal name
|
|
@@ -122,6 +122,8 @@ const nonBearerChallengeOf = (security, name) => Match.value(security).pipe(Matc
|
|
|
122
122
|
const make = (name, identity, ...options) => {
|
|
123
123
|
const security = options[0]?.security ?? HttpApiSecurity.bearer;
|
|
124
124
|
const error = errorList(options[0]?.error);
|
|
125
|
+
const principal = options[0]?.principal;
|
|
126
|
+
const schema = options[0]?.schema;
|
|
125
127
|
if (!openApiComponentKey.test(name)) throw new Error(`Invalid authentication name: ${JSON.stringify(name)}, not an OpenAPI key: use only letters, digits, "_", "." and "-"`);
|
|
126
128
|
assertDistinctTags(`Authentication "${name}"`, [...builtIns, ...error]);
|
|
127
129
|
if (!Predicate.isTagged(security, "Http") && !Predicate.isTagged(security, "ApiKey") && !Predicate.isTagged(security, "Basic")) throw new Error("Authentication takes one native HttpApiSecurity scheme as `security`: omit it for Bearer, or give one scheme, such as `HttpApiSecurity.basic`, never a record of them");
|
|
@@ -130,6 +132,8 @@ const make = (name, identity, ...options) => {
|
|
|
130
132
|
identity,
|
|
131
133
|
security,
|
|
132
134
|
error,
|
|
135
|
+
...principal === void 0 ? {} : { principal },
|
|
136
|
+
...schema === void 0 ? {} : { schema },
|
|
133
137
|
"~provider": Context.Service(`effect-actions/Authentication/Provider/${name}`),
|
|
134
138
|
"~middleware": HttpApiMiddleware.Service()(`effect-actions/Authentication/Security/${name}`, { security: { [name]: security } }),
|
|
135
139
|
"~verified": Context.Service(`effect-actions/Authentication/Verified/${name}`)
|
|
@@ -139,7 +143,7 @@ const presented = (credential) => Redacted.isRedacted(credential) ? Redacted.val
|
|
|
139
143
|
function layer(authentication, verify, options = {}) {
|
|
140
144
|
const { security } = authentication;
|
|
141
145
|
const oauth = isBearer(security);
|
|
142
|
-
const protectedResource = options
|
|
146
|
+
const { protectedResource } = options;
|
|
143
147
|
assertPublishableResource(oauth, protectedResource);
|
|
144
148
|
const resource = Effect.isEffect(protectedResource) ? Effect.map(protectedResource, (built) => {
|
|
145
149
|
assertPublishableResource(oauth, built);
|
|
@@ -153,17 +157,20 @@ function layer(authentication, verify, options = {}) {
|
|
|
153
157
|
if (!Predicate.isFunction(verifier)) return yield* Effect.die(/* @__PURE__ */ new Error("Missing verify: pass a verify function, or an Effect building one"));
|
|
154
158
|
const authenticate = (credential) => presented(credential) ? verifier(credential) : Effect.fail(missing);
|
|
155
159
|
const noStoreOnFailure = HttpEffect.appendPreResponseHandler((_request, response) => Effect.succeed(HttpServerResponse.setHeader(response, "cache-control", "no-store")));
|
|
156
|
-
const challengeOf = (credential) =>
|
|
160
|
+
const challengeOf = (credential) => {
|
|
161
|
+
if (!oauth) return nonBearerChallengeOf(security, authentication.name);
|
|
162
|
+
return presented(credential) ? invalid : anonymous;
|
|
163
|
+
};
|
|
157
164
|
const refuse = (error) => isRefusal(error) ? Effect.succeed(oauth ? answer(error, metadataUrl) : plain(error)) : Effect.suspend(() => {
|
|
158
165
|
const response = declaredResponse(authentication.error, error);
|
|
159
166
|
return response === void 0 ? Effect.die(error) : Effect.succeed(response);
|
|
160
167
|
});
|
|
161
168
|
const verified = (credential, route, slot) => {
|
|
162
|
-
const
|
|
169
|
+
const wwwAuthenticate = challengeOf(credential);
|
|
163
170
|
return Effect.matchEffect(authenticate(credential), {
|
|
164
171
|
onFailure: refuse,
|
|
165
172
|
onSuccess: (actor) => Effect.provideService(route, slot, actor)
|
|
166
|
-
}).pipe(Effect.onError(() => noStoreOnFailure), HttpEffect.withPreResponseHandler((_request, response) => Effect.succeed(withNoStoreAndChallenge(response,
|
|
173
|
+
}).pipe(Effect.onError(() => noStoreOnFailure), HttpEffect.withPreResponseHandler((_request, response) => Effect.succeed(withNoStoreAndChallenge(response, wwwAuthenticate))));
|
|
167
174
|
};
|
|
168
175
|
return {
|
|
169
176
|
descriptor: authentication,
|
package/dist/Event.d.mts
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { Codec, CodecOf, Fields, NoInput } from "./contract/codec.mjs";
|
|
2
|
+
import { Refusal } from "./contract/errors.mjs";
|
|
3
|
+
import { Gap, OccurrenceSchema } from "./contract/occurrence.mjs";
|
|
4
|
+
import { AnyEventImplementation, EventImplementation } from "./contract/feed.mjs";
|
|
5
|
+
import { Listed, LogStorage } from "./contract/log.mjs";
|
|
6
|
+
import { Events, KeyOptions, MakeEventsOptions, RevisionStorage, layerMemory, makeEvents } from "./contract/hub.mjs";
|
|
7
|
+
import { Protected } from "./contract/implementation.mjs";
|
|
8
|
+
import { Authorizer, Deferred as Deferred$1 } from "./contract/authorizer.mjs";
|
|
9
|
+
import { Options as Options$1 } from "./Action.mjs";
|
|
10
|
+
import { DateTime, Duration, Effect, Schema, Scope, Stream } from "effect";
|
|
11
|
+
declare const TypeId = "~@gjermundgaraba/effect-actions/Event";
|
|
12
|
+
/** What `make` needs to define an event type. */
|
|
13
|
+
export interface Options {
|
|
14
|
+
/**
|
|
15
|
+
* Who may subscribe: `Action.Anyone`, or the identity service a subscriber must have, as an
|
|
16
|
+
* action's `caller` says; the rules of an identity are on `Action.Options.caller`.
|
|
17
|
+
*/
|
|
18
|
+
readonly caller: Options$1["caller"];
|
|
19
|
+
/** What happens when the event fires; a model and a CLI user read it beside the event's name. */
|
|
20
|
+
readonly description: string;
|
|
21
|
+
/**
|
|
22
|
+
* The subscription's arguments, a schema or struct fields, as an action's `input` is: they may
|
|
23
|
+
* filter the occurrences, shape them, or configure the subscription. Omit for an event without
|
|
24
|
+
* arguments. Its rules are `Action.Options.input`'s.
|
|
25
|
+
*/
|
|
26
|
+
readonly input?: Codec | Fields | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* What each occurrence carries, a schema or struct fields: an object, since MCP sends it as an
|
|
29
|
+
* occurrence's `data`.
|
|
30
|
+
*/
|
|
31
|
+
readonly payload: Codec | Fields;
|
|
32
|
+
}
|
|
33
|
+
type OnlyOptions<O> = { readonly [K in Exclude<O extends unknown ? keyof O : never, keyof Options>]: never; };
|
|
34
|
+
type Rules<O> = Options extends O ? unknown : OnlyOptions<O>;
|
|
35
|
+
type InputOf<O> = O extends {
|
|
36
|
+
readonly input?: infer I;
|
|
37
|
+
} ? CodecOf<Extract<Exclude<I, undefined>, Codec | Fields>> | ([undefined] extends [I] ? typeof NoInput : never) : typeof NoInput;
|
|
38
|
+
type PayloadOf<O> = O extends {
|
|
39
|
+
readonly payload: infer P extends Codec | Fields;
|
|
40
|
+
} ? CodecOf<P> : never;
|
|
41
|
+
/**
|
|
42
|
+
* A pure contract for something that happens, bound to its source by `implement`: subscribers
|
|
43
|
+
* receive its occurrences, each carrying a `payload`, filtered and shaped by the subscription's
|
|
44
|
+
* `input`. Its `name`, `input` and `payload` are public: reuse them, as in
|
|
45
|
+
* `Schema.Array(NoteCreated.payload)`.
|
|
46
|
+
*/
|
|
47
|
+
export interface Event<Name extends string, Input extends Codec, Payload extends Codec, Caller extends Options["caller"] = Options["caller"]> {
|
|
48
|
+
readonly [TypeId]: typeof TypeId;
|
|
49
|
+
readonly caller: Caller;
|
|
50
|
+
readonly name: Name;
|
|
51
|
+
readonly description: string;
|
|
52
|
+
/** The subscription's arguments as a schema, a `Schema.Struct` where fields were given. */
|
|
53
|
+
readonly input: Input;
|
|
54
|
+
/** What each occurrence carries, as a schema. */
|
|
55
|
+
readonly payload: Payload;
|
|
56
|
+
}
|
|
57
|
+
/** Any event type, with its schemas erased. */
|
|
58
|
+
export type Any = Event<string, Codec, Codec>;
|
|
59
|
+
/** Whether `u` is an event type `make` defined: a binding's list tells events from actions by it. */
|
|
60
|
+
export declare const isEvent: (u: unknown) => u is Any;
|
|
61
|
+
/**
|
|
62
|
+
* Define an event type: what a subscription takes, what each occurrence carries, and who may
|
|
63
|
+
* subscribe. A surface serving it names it by `name`.
|
|
64
|
+
* Guide: `docs/Event.md` in this package; in an application: `examples/build-events/src/builds/`.
|
|
65
|
+
*
|
|
66
|
+
* **Gotchas**
|
|
67
|
+
*
|
|
68
|
+
* - A name is lowercase segments joined by dots, `note.created`: its form is in the message `Invalid event name` throws; it is unique among the events each surface serves, apart from action names.
|
|
69
|
+
* - Surfaces match an implementation to a contract by object identity, never by name, as for actions.
|
|
70
|
+
*/
|
|
71
|
+
export declare function make<const Name extends string, const O extends Options>(name: Name, options: O & NoInfer<Rules<O>>): Event<Name, InputOf<O>, PayloadOf<O>, O["caller"]>;
|
|
72
|
+
/**
|
|
73
|
+
* One occurrence as a source produces it: its `data`, and optionally the upstream's own id, which
|
|
74
|
+
* subscribers deduplicate by, and when it happened. Without them, a log source's occurrence
|
|
75
|
+
* takes its cursor as its id and the time it was appended, the same on every read; a check's
|
|
76
|
+
* takes a generated id and the time it is read.
|
|
77
|
+
*/
|
|
78
|
+
export interface Emitted<E extends Any> {
|
|
79
|
+
readonly data: E["payload"]["Type"];
|
|
80
|
+
/**
|
|
81
|
+
* A stable id: give the upstream's own, such as a message or delivery id, so a retry or a replay repeats it.
|
|
82
|
+
*
|
|
83
|
+
* **Gotchas**
|
|
84
|
+
*
|
|
85
|
+
* - An id given again for another occurrence is dropped as a duplicate by a `watch` and by `Webhook.layerReceiver`, each keeping its last 1,000: an id from a counter that restarts with the process repeats, so leave it out rather than give one.
|
|
86
|
+
*/
|
|
87
|
+
readonly eventId?: string | undefined;
|
|
88
|
+
readonly timestamp?: DateTime.Utc | undefined;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* What a check answers for a cursor: the occurrences since it, and the cursor after them.
|
|
92
|
+
*
|
|
93
|
+
* **Gotchas**
|
|
94
|
+
*
|
|
95
|
+
* - A cursor the upstream no longer serves, such as one older than its retention, is answered with `truncated: true` and a fresh cursor: the subscriber then knows it missed occurrences.
|
|
96
|
+
* - A `nextPoll` under a second is read as a second, and one over 5 minutes, `Duration.infinity` included, as 5 minutes, by the server's loop and by a polling client alike.
|
|
97
|
+
*/
|
|
98
|
+
export interface Page<E extends Any> {
|
|
99
|
+
readonly events: ReadonlyArray<Emitted<E>>;
|
|
100
|
+
readonly cursor: string;
|
|
101
|
+
readonly truncated?: boolean | undefined;
|
|
102
|
+
/** More occurrences are ready: the subscriber asks again without waiting. */
|
|
103
|
+
readonly hasMore?: boolean | undefined;
|
|
104
|
+
/** How long a subscriber waits before it asks again; defaults to 5 seconds. */
|
|
105
|
+
readonly nextPoll?: Duration.Input | undefined;
|
|
106
|
+
}
|
|
107
|
+
interface Hooked<E extends Any, R> {
|
|
108
|
+
/**
|
|
109
|
+
* What a subscription needs set up upstream while it lasts, such as joining a channel: a scoped
|
|
110
|
+
* Effect, whose finalizer tears it down, run as the subscriber. A stream's lasts as long as the
|
|
111
|
+
* stream, a poll subscription's until `lease` after its last poll, or, watched in process, until
|
|
112
|
+
* the watch ends, and a webhook subscription's until it is unsubscribed or lapses, set up again
|
|
113
|
+
* as `persist` restores it.
|
|
114
|
+
*
|
|
115
|
+
* **Gotchas**
|
|
116
|
+
*
|
|
117
|
+
* - Write it so it may run twice for one subscription: a lease does not outlive the process, so a poll after a restart sets it up again.
|
|
118
|
+
* - A poll subscription's runs in its first poll's context, that request included, held until its lease ends, where a webhook subscription's runs without the request: read what it needs from the request in middleware, which provides it as a service.
|
|
119
|
+
*/
|
|
120
|
+
readonly subscription?: ((input: E["input"]["Type"]) => Effect.Effect<void, never, R | Scope.Scope>) | undefined;
|
|
121
|
+
/** How long a poll subscription's `subscription` lasts after its last poll; defaults to 5 minutes. */
|
|
122
|
+
readonly lease?: Duration.Input | undefined;
|
|
123
|
+
/**
|
|
124
|
+
* The key a subscriber of `input` is reached by, run as the subscriber, such as the room it
|
|
125
|
+
* follows: `Events.notify` and `Events.publish` given a key reach the subscribers of that key
|
|
126
|
+
* alone, and those it answers `undefined` for, who follow every key. Without it, every
|
|
127
|
+
* announcement reaches every subscriber.
|
|
128
|
+
*
|
|
129
|
+
* **Gotchas**
|
|
130
|
+
*
|
|
131
|
+
* - An occurrence a log's `emit` gives without a `key` reaches every subscriber, whatever key it follows: give each one its key.
|
|
132
|
+
*/
|
|
133
|
+
readonly key?: ((input: E["input"]["Type"]) => string | undefined | Effect.Effect<string | undefined, never, R>) | undefined;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* An event type read from an upstream that can be asked what happened since a cursor, such as a
|
|
137
|
+
* change feed or a history API: one function answers every subscriber, whatever the surface.
|
|
138
|
+
*
|
|
139
|
+
* **Gotchas**
|
|
140
|
+
*
|
|
141
|
+
* - An occurrence without `eventId` takes a fresh id at each read, and a page's occurrences before its last are read again from the cursor asked after a reconnect mid-page: give the upstream's ids, or a subscriber receives them again under ids no `watch` drops.
|
|
142
|
+
*/
|
|
143
|
+
export interface Check<E extends Any, R = never> extends Hooked<E, R> {
|
|
144
|
+
/**
|
|
145
|
+
* What happened since `cursor`, as the subscriber's `input` selects it, run as the subscriber.
|
|
146
|
+
* A `null` cursor is a subscriber with no position: answer nothing, so it starts after the last
|
|
147
|
+
* occurrence. An event whose occurrence is the whole value, such as a status, is a `Current`.
|
|
148
|
+
*/
|
|
149
|
+
readonly check: (input: E["input"]["Type"], cursor: string | null) => Effect.Effect<Page<E>, Refusal, R>;
|
|
150
|
+
readonly log?: never;
|
|
151
|
+
readonly current?: never;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* An event type whose occurrence is the whole value, such as a status or a list small enough to
|
|
155
|
+
* send whole: a subscriber receives the value as it is first, then again after each
|
|
156
|
+
* `Events.notify` that reaches it, never a change it missed one by one. The library keeps a
|
|
157
|
+
* revision per key, which each such notify moves, as the cursor and each occurrence's id.
|
|
158
|
+
*
|
|
159
|
+
* **Gotchas**
|
|
160
|
+
*
|
|
161
|
+
* - A notify without a change sends the value again, under a new id: notify after a write that changed it.
|
|
162
|
+
* - The revision is read before `current`, so a value written between them is sent again at the next read, never missed: write the value, then notify.
|
|
163
|
+
*/
|
|
164
|
+
export interface Current<E extends Any, R = never> extends Hooked<E, R> {
|
|
165
|
+
/** The value as it is, as the subscriber's `input` selects it, run as the subscriber. */
|
|
166
|
+
readonly current: (input: E["input"]["Type"]) => Effect.Effect<E["payload"]["Type"], Refusal, R>;
|
|
167
|
+
readonly check?: never;
|
|
168
|
+
readonly log?: never;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* An event type whose occurrences the library keeps as they are announced, for an upstream with
|
|
172
|
+
* no history of its own: `Events.publish` appends each, and `log.emit`, where given, a stream of
|
|
173
|
+
* them. Each subscriber receives the occurrences its `match` accepts, shaped by its `transform`.
|
|
174
|
+
*
|
|
175
|
+
* **Gotchas**
|
|
176
|
+
*
|
|
177
|
+
* - A cursor is only good where the log that minted it lives, for as long as it keeps its occurrences: after a restart, or once they are dropped, a subscriber resuming from it receives `truncated` and a fresh cursor.
|
|
178
|
+
* - A subscriber reading slower than occurrences arrive falls behind the log like a resumed one: it receives `truncated`, never a backlog held for it alone.
|
|
179
|
+
* - An `emit` stream is consumed from startup, in the server's scope, by one build at a time, until it ends, even past its build's release: a failure ends it and is reported, an end is logged, and the next held build's stream, if any, takes over; the log then holds what was published.
|
|
180
|
+
*/
|
|
181
|
+
export interface Log<E extends Any, R = never> extends Hooked<E, R> {
|
|
182
|
+
/** What the log keeps, and the stream feeding it besides `Events.publish`; `{}` for none. */
|
|
183
|
+
readonly log: LogOptions<E>;
|
|
184
|
+
/** Whether a subscriber of `input` receives an occurrence carrying `data`; defaults to every one. */
|
|
185
|
+
readonly match?: ((input: E["input"]["Type"], data: E["payload"]["Type"]) => boolean | Effect.Effect<boolean, never, R>) | undefined;
|
|
186
|
+
/** The data a subscriber of `input` receives in place of `data`, such as one with fields removed. */
|
|
187
|
+
readonly transform?: ((input: E["input"]["Type"], data: E["payload"]["Type"]) => E["payload"]["Type"] | Effect.Effect<E["payload"]["Type"], never, R>) | undefined;
|
|
188
|
+
readonly check?: never;
|
|
189
|
+
readonly current?: never;
|
|
190
|
+
}
|
|
191
|
+
/** What a log source keeps, and what feeds it besides `Events.publish`. */
|
|
192
|
+
export interface LogOptions<E extends Any> {
|
|
193
|
+
/**
|
|
194
|
+
* A stream of occurrences, such as a message bus's, appended as they arrive from startup, each
|
|
195
|
+
* reaching the subscribers of its `key`, as `Events.publish` given one does.
|
|
196
|
+
*/
|
|
197
|
+
readonly emit?: Stream.Stream<Emitted<E> & KeyOptions> | undefined;
|
|
198
|
+
/**
|
|
199
|
+
* How long an occurrence is kept for subscribers resuming from a cursor; defaults to 10 minutes.
|
|
200
|
+
* The first build attached to its log sets it, and later builds share that log.
|
|
201
|
+
*/
|
|
202
|
+
readonly maxAge?: Duration.Input | undefined;
|
|
203
|
+
/**
|
|
204
|
+
* How many occurrences are kept at most, of every key together; defaults to 10,000.
|
|
205
|
+
*
|
|
206
|
+
* **Gotchas**
|
|
207
|
+
*
|
|
208
|
+
* - A busy key drops a quiet one's occurrences too: a subscriber of the quiet key resuming after them receives a gap, though none of its own was dropped.
|
|
209
|
+
*/
|
|
210
|
+
readonly maxEvents?: number | undefined;
|
|
211
|
+
}
|
|
212
|
+
/** Where an event type's occurrences come from: exactly one of `check`, `current` or `log`. */
|
|
213
|
+
export type Source<E extends Any, R = never> = Check<E, R> | Current<E, R> | Log<E, R>;
|
|
214
|
+
/**
|
|
215
|
+
* One occurrence as a subscriber receives it: `eventId`, the source's id or one generated, which
|
|
216
|
+
* a subscriber deduplicates by; `cursor`, where to resume after it, `null` for an event type that
|
|
217
|
+
* cannot be resumed.
|
|
218
|
+
*/
|
|
219
|
+
export type Occurrence<E extends Any> = OccurrenceSchema<E>["Type"];
|
|
220
|
+
/** What a subscriber receives: occurrences, and gaps where it missed some. */
|
|
221
|
+
export type Received<E extends Any> = Occurrence<E> | Gap;
|
|
222
|
+
declare const NotServed_base: Schema.Class<NotServed, Schema.TaggedStruct<"NotServed", {}>, import("effect/Cause").YieldableError & {
|
|
223
|
+
readonly brand: unique symbol;
|
|
224
|
+
}>;
|
|
225
|
+
/** The server serves no event type of that name, or no longer does. */
|
|
226
|
+
export declare class NotServed extends NotServed_base {}
|
|
227
|
+
declare const Unsupported_base: Schema.Class<Unsupported, Schema.TaggedStruct<"Unsupported", {
|
|
228
|
+
readonly feature: Schema.String;
|
|
229
|
+
readonly value: Schema.optional<Schema.String>;
|
|
230
|
+
}>, import("effect/Cause").YieldableError & {
|
|
231
|
+
readonly brand: unique symbol;
|
|
232
|
+
}>;
|
|
233
|
+
/**
|
|
234
|
+
* The server serves the event type, but not as asked: `feature` names what it does not offer,
|
|
235
|
+
* such as `deliveryMode`, and `value` which one, such as `push`.
|
|
236
|
+
*/
|
|
237
|
+
export declare class Unsupported extends Unsupported_base {}
|
|
238
|
+
/**
|
|
239
|
+
* Why a webhook's callback endpoint failed, in SEP-3415's categories: it refused the connection
|
|
240
|
+
* or was unreachable, timed out, failed TLS, answered a status from 300 to 499 (`http_4xx`, as
|
|
241
|
+
* the categories name no 3xx) or a 5xx, or did not echo the verification challenge.
|
|
242
|
+
*/
|
|
243
|
+
export declare const CallbackCategory: Schema.Literals<readonly ["connection_refused", "timeout", "tls_error", "http_4xx", "http_5xx", "challenge_failed"]>;
|
|
244
|
+
export type CallbackCategory = typeof CallbackCategory.Type;
|
|
245
|
+
declare const CallbackFailed_base: Schema.Class<CallbackFailed, Schema.TaggedStruct<"CallbackFailed", {
|
|
246
|
+
readonly category: Schema.Literals<readonly ["connection_refused", "timeout", "tls_error", "http_4xx", "http_5xx", "challenge_failed"]>;
|
|
247
|
+
}>, import("effect/Cause").YieldableError & {
|
|
248
|
+
readonly brand: unique symbol;
|
|
249
|
+
}>;
|
|
250
|
+
/** A webhook subscription's callback endpoint failed its verification, or was unreachable. */
|
|
251
|
+
export declare class CallbackFailed extends CallbackFailed_base {}
|
|
252
|
+
declare const NotSubscribed_base: Schema.Class<NotSubscribed, Schema.TaggedStruct<"NotSubscribed", {}>, import("effect/Cause").YieldableError & {
|
|
253
|
+
readonly brand: unique symbol;
|
|
254
|
+
}>;
|
|
255
|
+
/** No webhook subscription matches the one an unsubscribe names. */
|
|
256
|
+
export declare class NotSubscribed extends NotSubscribed_base {}
|
|
257
|
+
declare const Reason: Schema.Union<readonly [typeof NotServed, typeof Unsupported, typeof CallbackFailed, typeof NotSubscribed]>;
|
|
258
|
+
declare const SubscriptionError_base: Schema.Class<SubscriptionError, Schema.TaggedStruct<"SubscriptionError", {
|
|
259
|
+
readonly event: Schema.String;
|
|
260
|
+
readonly reason: Schema.Union<readonly [typeof NotServed, typeof Unsupported, typeof CallbackFailed, typeof NotSubscribed]>;
|
|
261
|
+
readonly message: Schema.String;
|
|
262
|
+
}>, import("effect/Cause").YieldableError & {
|
|
263
|
+
readonly brand: unique symbol;
|
|
264
|
+
}>;
|
|
265
|
+
/**
|
|
266
|
+
* A subscription that cannot start or go on: `reason` says why, so a subscriber branches on it
|
|
267
|
+
* with `Effect.catchReason("SubscriptionError", "NotServed", ...)`; `message` names the event and
|
|
268
|
+
* the reason. A refusal is `Action.Unauthenticated` or `Action.Forbidden`, and input that does not
|
|
269
|
+
* decode `Action.InvalidInput`, as for an action.
|
|
270
|
+
*/
|
|
271
|
+
export declare class SubscriptionError extends SubscriptionError_base {
|
|
272
|
+
/** The error for `event` failing with `reason`, its message built from both. */
|
|
273
|
+
static of(event: string, reason: (typeof Reason)["Type"]): SubscriptionError;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Where a watch keeps its cursor: what it kept under a key, and keeping a new one; a failure `E`
|
|
277
|
+
* of either ends the watch with it.
|
|
278
|
+
*/
|
|
279
|
+
export interface CursorStore<E = never> {
|
|
280
|
+
readonly get: (key: string) => Effect.Effect<string | undefined, E>;
|
|
281
|
+
readonly set: (key: string, cursor: string) => Effect.Effect<void, E>;
|
|
282
|
+
}
|
|
283
|
+
declare const WebhookDelivery_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => Readonly<A> & {
|
|
284
|
+
readonly _tag: "WebhookDelivery";
|
|
285
|
+
} & import("effect/Pipeable").Pipeable;
|
|
286
|
+
/**
|
|
287
|
+
* A watch delivered by webhook, which `Webhook.delivery` makes: the server POSTs each occurrence
|
|
288
|
+
* to the route `Webhook.layerReceiver` serves in the watching process, reached under `baseUrl`,
|
|
289
|
+
* for as long as the watch is read, each grant asking for `ttl`.
|
|
290
|
+
*/
|
|
291
|
+
export declare class WebhookDelivery extends WebhookDelivery_base<{
|
|
292
|
+
/**
|
|
293
|
+
* The https URL the server reaches the watching process at: its `layerReceiver` route's path
|
|
294
|
+
* is appended to it, `https://notifier.example.com` with `/hooks/builds` delivering to
|
|
295
|
+
* `https://notifier.example.com/hooks/builds`.
|
|
296
|
+
*/
|
|
297
|
+
readonly baseUrl: string;
|
|
298
|
+
/** The lifetime each subscribe asks for; omitted, the server's longest. */
|
|
299
|
+
readonly ttl?: Duration.Input | undefined;
|
|
300
|
+
}> {}
|
|
301
|
+
/**
|
|
302
|
+
* How a client watches an event type; `E` is what its `resume` store fails with.
|
|
303
|
+
*
|
|
304
|
+
* **Gotchas**
|
|
305
|
+
*
|
|
306
|
+
* - Each run of a watch's stream starts from these options, a retry or a repeat included: to go on where a failed run stopped, give `resume`, which every run reads.
|
|
307
|
+
* - `resume` keeps an occurrence's cursor once the next is pulled, so a consumer stopped while handling one receives it again: deduplicate by `eventId` where that matters.
|
|
308
|
+
*/
|
|
309
|
+
export interface WatchOptions<E = never> {
|
|
310
|
+
/** Where to start: a cursor an occurrence or a gap carried; defaults to now. */
|
|
311
|
+
readonly cursor?: string | null | undefined;
|
|
312
|
+
/**
|
|
313
|
+
* `"stream"` holds one subscription open, `"poll"` asks again and again, and a
|
|
314
|
+
* `Webhook.delivery` has the server POST each occurrence to a receiver this process serves,
|
|
315
|
+
* which the watch then requires; left out, a watch streams where its surface can and polls
|
|
316
|
+
* where the server serves the event by poll alone.
|
|
317
|
+
*/
|
|
318
|
+
readonly mode?: "stream" | "poll" | WebhookDelivery | undefined;
|
|
319
|
+
/**
|
|
320
|
+
* Where to keep the cursor between watches, such as across restarts: it starts from the cursor
|
|
321
|
+
* kept under `key`, when no `cursor` is given, and keeps each new one there. Effect's
|
|
322
|
+
* `KeyValueStore` is one: `{ store: yield* KeyValueStore.KeyValueStore, key: "builds" }`.
|
|
323
|
+
*/
|
|
324
|
+
readonly resume?: {
|
|
325
|
+
readonly store: CursorStore<E>;
|
|
326
|
+
readonly key: string;
|
|
327
|
+
} | undefined;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Authorization for protected event types: it refuses, and fails with nothing else. It runs for
|
|
331
|
+
* every subscription of a protected event type, before its source.
|
|
332
|
+
*/
|
|
333
|
+
export type Authorize<E extends Any, R = never> = (event: E) => Effect.Effect<void, Refusal, R>;
|
|
334
|
+
/** Every authenticated subscriber may subscribe to a protected event type. */
|
|
335
|
+
export declare const allowAll: Authorize<Any>;
|
|
336
|
+
type Target = Any | readonly Any[];
|
|
337
|
+
type EventsOf<T extends Target> = T extends readonly Any[] ? T[number] : Extract<T, Any>;
|
|
338
|
+
type TakesNoAuthorize<T extends Target> = [Protected<EventsOf<T>>] extends [never] ? unknown : "A protected event takes { authorize }: your rule, or Event.allowAll";
|
|
339
|
+
type NamesOf<T extends Target> = EventsOf<T>["name"];
|
|
340
|
+
type SourcesOf<T extends Target, R, RequestServices> = T extends Any ? Check<T, R> | Current<T, R> | Log<T, R> : { readonly [K in keyof RequestServices]: K extends NamesOf<T> ? Check<Extract<EventsOf<T>, {
|
|
341
|
+
readonly name: K;
|
|
342
|
+
}>, RequestServices[K]> | Current<Extract<EventsOf<T>, {
|
|
343
|
+
readonly name: K;
|
|
344
|
+
}>, RequestServices[K]> | Log<Extract<EventsOf<T>, {
|
|
345
|
+
readonly name: K;
|
|
346
|
+
}>, RequestServices[K]> : never; };
|
|
347
|
+
type Implemented<T extends Target, RequestServices, R, EX, RX, RA, EAX, RAX> = EventImplementation<EventsOf<T>, { readonly [K in NamesOf<T>]: Exclude<T extends readonly Any[] ? RequestServices[K & keyof RequestServices] : R, Scope.Scope>; }, Deferred$1<EX>, Deferred$1<Exclude<RX, Scope.Scope>> | Events, Exclude<RA, Scope.Scope>, Deferred$1<EAX>, Deferred$1<Exclude<RAX, Scope.Scope>>>;
|
|
348
|
+
/**
|
|
349
|
+
* Bind sources to event types, behind authorization: one event type and its source, or a list
|
|
350
|
+
* and a record of sources keyed by event name, given directly or built by an Effect whose
|
|
351
|
+
* services are startup requirements, while what a source yields per subscriber is a request-time
|
|
352
|
+
* requirement. `authorize` is required when the list holds a protected event type. Every surface
|
|
353
|
+
* serving events takes the implementation it returns, beside action implementations in one list.
|
|
354
|
+
*
|
|
355
|
+
* **Gotchas**
|
|
356
|
+
*
|
|
357
|
+
* - The builder runs once per layer graph, however many surfaces serve it, as the first event type a surface serves attaches; a log source's `emit` starts being consumed once its event type is served or watched, and an event type no surface serves starts nothing.
|
|
358
|
+
* - `check`, `current`, `match`, `transform`, `key` and `subscription` run as the subscriber: what they yield, such as the caller, is resolved per subscription.
|
|
359
|
+
* - A source holds exactly one of `check`, `current` or `log`, checked as a surface serving its event type builds, which fails otherwise: a source of an event type no surface serves is not checked.
|
|
360
|
+
* - Serving or watching an event type needs `Event.Events`, which its source attaches to as it builds: provide `Event.layerMemory` once, around the handlers announcing and the surfaces serving.
|
|
361
|
+
* - An occurrence whose data breaks the event's payload is reported, and every surface sends a `Gap` in its place, a poll's page `truncated`: the rest of its page is still delivered.
|
|
362
|
+
* - A read failing with no refusal is reported once, and the subscription reads on from where it reached: a stream a server holds, a webhook's and `Action.client`'s `watch` after 5 seconds, a client's poll, which a server answers with an error, after its reconnect backoff, from 1 second doubling to 30; a refusal ends the subscription.
|
|
363
|
+
*/
|
|
364
|
+
export declare function implement<const T extends Target, RequestServices extends (T extends readonly Any[] ? { readonly [K in NamesOf<T>]: unknown; } : never), R = never, EX = never, RX = never, RA = never, EAX = never, RAX = never>(events: T, sources: SourcesOf<T, R, RequestServices> | Effect.Effect<SourcesOf<T, R, RequestServices>, EX, RX>, options: [Protected<EventsOf<T>>] extends [never] ? {
|
|
365
|
+
readonly authorize?: "A public-only target takes no authorize";
|
|
366
|
+
} | undefined : {
|
|
367
|
+
readonly authorize: Authorizer<Protected<EventsOf<T>>, RA, EAX, RAX>;
|
|
368
|
+
}): Implemented<T, RequestServices, R, EX, RX, RA, EAX, RAX>;
|
|
369
|
+
export declare function implement<const T extends Target, RequestServices extends (T extends readonly Any[] ? { readonly [K in NamesOf<T>]: unknown; } : never), R = never, EX = never, RX = never>(events: T & TakesNoAuthorize<T>, sources: SourcesOf<T, R, RequestServices> | Effect.Effect<SourcesOf<T, R, RequestServices>, EX, RX>): Implemented<T, RequestServices, R, EX, RX, never, never, never>;
|
|
370
|
+
export { type AnyEventImplementation as AnyImplementation, Events, Gap, type EventImplementation as Implementation, type KeyOptions, type Listed, type LogStorage, type MakeEventsOptions, type RevisionStorage, layerMemory, makeEvents };
|