@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/cli/commands.mjs
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { literalValues, members, unsuspended } from "../contract/ast.mjs";
|
|
2
2
|
import { InvalidInput } from "../contract/errors.mjs";
|
|
3
|
-
import { assertDistinct } from "../contract/names.mjs";
|
|
4
3
|
import { errorSchemas } from "../contract/declared.mjs";
|
|
4
|
+
import { assertDistinct } from "../contract/names.mjs";
|
|
5
|
+
import { elementOf } from "../contract/stream.mjs";
|
|
6
|
+
import { elementsOf } from "../contract/implementation.mjs";
|
|
5
7
|
import { decodeInput } from "../contract/call.mjs";
|
|
8
|
+
import { ReceivedOf } from "../contract/occurrence.mjs";
|
|
9
|
+
import { SubscriptionError } from "../Event.mjs";
|
|
6
10
|
import { withStderrConsole } from "../stdio/console.mjs";
|
|
7
11
|
import { Console, Effect, Option, Predicate, Record, Runtime, Schema, SchemaAST, Stdio, Stream } from "effect";
|
|
8
12
|
import { CliError, Command, Flag, Param } from "effect/cli";
|
|
9
13
|
// src/cli/commands.ts
|
|
10
14
|
const kebab = (name) => {
|
|
11
|
-
const kebabbed = name.
|
|
15
|
+
const kebabbed = name.replaceAll(/(?<upper>[A-Z]+)(?<word>[A-Z][a-z])/gu, "$<upper>-$<word>").replaceAll(/(?<lower>[a-z0-9])(?<upper>[A-Z])/gu, "$<lower>-$<upper>").replaceAll("_", "-").replaceAll(/^-+|-+$/gu, "").toLowerCase();
|
|
12
16
|
return kebabbed === "" ? name : kebabbed;
|
|
13
17
|
};
|
|
14
18
|
const renderedCommandJsonFlag = Flag.Boolean("json").pipe(Flag.withDefault(false), Flag.withDescription("Print machine-readable JSON"));
|
|
@@ -20,7 +24,13 @@ const jsonKinds = [
|
|
|
20
24
|
"array",
|
|
21
25
|
"object"
|
|
22
26
|
];
|
|
23
|
-
const jsonKindOf = (value) =>
|
|
27
|
+
const jsonKindOf = (value) => {
|
|
28
|
+
if (value === null) return "null";
|
|
29
|
+
if (Predicate.isBoolean(value)) return "boolean";
|
|
30
|
+
if (Predicate.isNumber(value)) return "number";
|
|
31
|
+
if (Predicate.isString(value)) return "string";
|
|
32
|
+
return Array.isArray(value) ? "array" : "object";
|
|
33
|
+
};
|
|
24
34
|
const acceptedJsonKindsOf = (ast) => members(ast).flatMap((member) => {
|
|
25
35
|
if (SchemaAST.isLiteral(member)) return [jsonKindOf(member.literal)];
|
|
26
36
|
if (SchemaAST.isEnum(member)) return member.enums.map(([, value]) => jsonKindOf(value));
|
|
@@ -51,8 +61,8 @@ const valueParam = (kind, name, field) => {
|
|
|
51
61
|
return Param.String(kind, name).pipe(Param.withSchema(jsonOfAcceptedKindOrText(encoded)), Param.withMetavar("value"));
|
|
52
62
|
};
|
|
53
63
|
const withoutOptionalAbsence = (ast) => {
|
|
54
|
-
const
|
|
55
|
-
return
|
|
64
|
+
const present = SchemaAST.isUnion(ast) ? ast.types.filter((member) => !SchemaAST.isNull(member) && !SchemaAST.isUndefined(member)) : [];
|
|
65
|
+
return present.length === 1 && present[0] !== void 0 ? present[0] : ast;
|
|
56
66
|
};
|
|
57
67
|
const optionalAddedNull = (plain) => {
|
|
58
68
|
const types = members(plain);
|
|
@@ -60,7 +70,7 @@ const optionalAddedNull = (plain) => {
|
|
|
60
70
|
};
|
|
61
71
|
const repeatedElementOf = (ast) => {
|
|
62
72
|
const encoded = unsuspended(ast);
|
|
63
|
-
if (!SchemaAST.isArrays(encoded)) return
|
|
73
|
+
if (!SchemaAST.isArrays(encoded)) return;
|
|
64
74
|
const [element, ...after] = encoded.rest;
|
|
65
75
|
if (element === void 0 || after.length > 0 || !encoded.elements.every((first) => first === element) || !acceptedJsonKindsOf(element).every((kind) => kind === "string" || kind === "number")) return;
|
|
66
76
|
return {
|
|
@@ -69,26 +79,30 @@ const repeatedElementOf = (ast) => {
|
|
|
69
79
|
};
|
|
70
80
|
};
|
|
71
81
|
const noElementOccurrence = "[]";
|
|
82
|
+
const namedFieldsOf = (ast) => {
|
|
83
|
+
const fields = SchemaAST.isDeclaration(ast) ? ast.typeParameters[0] : ast;
|
|
84
|
+
return new Map(fields !== void 0 && SchemaAST.isObjects(fields) ? fields.propertySignatures.map((property) => [property.name, property.type]) : []);
|
|
85
|
+
};
|
|
86
|
+
const elementFieldsOf = (array) => {
|
|
87
|
+
if (!SchemaAST.isArrays(array)) return;
|
|
88
|
+
const [element, ...others] = [...array.elements, ...array.rest];
|
|
89
|
+
if (element === void 0 || others.some((other) => other !== element)) return;
|
|
90
|
+
const fields = namedFieldsOf(unsuspended(element));
|
|
91
|
+
return fields.size === 0 ? void 0 : Array.from(fields, ([name, field]) => SchemaAST.isOptional(field) ? `${String(name)}?` : String(name));
|
|
92
|
+
};
|
|
72
93
|
const jsonHintOf = (encoded) => {
|
|
73
94
|
const value = unsuspended(encoded);
|
|
74
95
|
const kinds = new Set(acceptedJsonKindsOf(value));
|
|
75
|
-
if (kinds.size !== 1) return
|
|
96
|
+
if (kinds.size !== 1) return;
|
|
76
97
|
if (kinds.has("object")) return "A JSON object.";
|
|
77
|
-
if (!kinds.has("array")) return
|
|
98
|
+
if (!kinds.has("array")) return;
|
|
78
99
|
const fields = elementFieldsOf(value);
|
|
79
100
|
return fields === void 0 ? "A JSON array." : `A JSON array of {${fields.join(", ")}}.`;
|
|
80
101
|
};
|
|
81
|
-
const elementFieldsOf = (array) => {
|
|
82
|
-
if (!SchemaAST.isArrays(array)) return void 0;
|
|
83
|
-
const [element, ...others] = [...array.elements, ...array.rest];
|
|
84
|
-
if (element === void 0 || others.some((other) => other !== element)) return void 0;
|
|
85
|
-
const fields = namedFieldsOf(unsuspended(element));
|
|
86
|
-
return fields.size === 0 ? void 0 : Array.from(fields, ([name, field]) => SchemaAST.isOptional(field) ? `${String(name)}?` : String(name));
|
|
87
|
-
};
|
|
88
102
|
const withHint = (description, hint) => {
|
|
89
103
|
if (hint === void 0 || description === void 0) return description ?? hint;
|
|
90
104
|
const stated = description.trimEnd();
|
|
91
|
-
return /[.!?:]
|
|
105
|
+
return /[.!?:]$/u.test(stated) ? `${stated} ${hint}` : `${stated}. ${hint}`;
|
|
92
106
|
};
|
|
93
107
|
const fieldParam = (kind, name, encoded, plain) => {
|
|
94
108
|
const optional = SchemaAST.isOptional(encoded);
|
|
@@ -97,7 +111,7 @@ const fieldParam = (kind, name, encoded, plain) => {
|
|
|
97
111
|
if (repeats !== void 0) {
|
|
98
112
|
const literals = stringChoicesOf(unsuspended(repeats.element));
|
|
99
113
|
const elements = literals === void 0 ? valueParam(kind, name, repeats.element) : Param.Literals(kind, name, [...literals, noElementOccurrence]);
|
|
100
|
-
const listed = (values) => Option.some(values.filter((
|
|
114
|
+
const listed = (values) => Option.some(values.filter((given) => given !== noElementOccurrence));
|
|
101
115
|
return {
|
|
102
116
|
param: optional ? Param.variadic(elements).pipe(Param.map((values) => values.length === 0 ? Option.none() : listed(values))) : Param.variadic(elements, { min: repeats.min }).pipe(Param.map(listed)),
|
|
103
117
|
repeats: true,
|
|
@@ -116,13 +130,9 @@ const fieldParam = (kind, name, encoded, plain) => {
|
|
|
116
130
|
hint: isSwitch ? "Left out, it sends false." : jsonHintOf(encoded)
|
|
117
131
|
};
|
|
118
132
|
};
|
|
119
|
-
const namedFieldsOf = (ast) => {
|
|
120
|
-
const fields = SchemaAST.isDeclaration(ast) ? ast.typeParameters[0] : ast;
|
|
121
|
-
return new Map(fields !== void 0 && SchemaAST.isObjects(fields) ? fields.propertySignatures.map((property) => [property.name, property.type]) : []);
|
|
122
|
-
};
|
|
123
133
|
const isTransformedAsWhole = (ast) => (SchemaAST.isDeclaration(ast) ? ast.typeParameters[0] : ast)?.encoding !== void 0;
|
|
124
|
-
const ownOrSuspendedDescriptionOf = (ast) => ast.context?.annotations?.description ?? SchemaAST.resolveDescription(ast) ?? describedBeforeLastCheck(ast) ?? (SchemaAST.isSuspend(ast) ? ownOrSuspendedDescriptionOf(ast.thunk()) : void 0);
|
|
125
134
|
const describedBeforeLastCheck = (ast) => [ast.annotations, ...(ast.checks ?? []).map((check) => check.annotations)].map((annotations) => annotations?.["description"]).findLast(Predicate.isString);
|
|
135
|
+
const ownOrSuspendedDescriptionOf = (ast) => ast.context?.annotations?.description ?? SchemaAST.resolveDescription(ast) ?? describedBeforeLastCheck(ast) ?? (SchemaAST.isSuspend(ast) ? ownOrSuspendedDescriptionOf(ast.thunk()) : void 0);
|
|
126
136
|
const describedThroughAbsence = (ast) => ownOrSuspendedDescriptionOf(ast) ?? ownOrSuspendedDescriptionOf(withoutOptionalAbsence(ast));
|
|
127
137
|
const describedByElement = (ast) => {
|
|
128
138
|
const array = unsuspended(withoutOptionalAbsence(ast));
|
|
@@ -191,7 +201,7 @@ var UserError = class extends CliError.UserError {
|
|
|
191
201
|
}
|
|
192
202
|
};
|
|
193
203
|
const ownStringProperty = (error, key) => {
|
|
194
|
-
if (!Predicate.hasProperty(error, key)) return
|
|
204
|
+
if (!Predicate.hasProperty(error, key)) return;
|
|
195
205
|
const value = error[key];
|
|
196
206
|
return Predicate.isString(value) ? value : void 0;
|
|
197
207
|
};
|
|
@@ -228,7 +238,7 @@ const stdinTextOf = (field) => Effect.gen(function* () {
|
|
|
228
238
|
const stdio = yield* Effect.serviceOption(Stdio.Stdio);
|
|
229
239
|
if (Option.isNone(stdio)) return yield* Effect.die(/* @__PURE__ */ new Error("Stdio is not provided (provide NodeServices.layer, or Stdio.layerTest in a test)"));
|
|
230
240
|
if (yield* stdio.value.stdinIsTerminal) return yield* unreadableStdinField(field, `${field} is read from stdin, which is a terminal: pipe it in`);
|
|
231
|
-
return (yield* stdio.value.stdin.pipe(Stream.decodeText(), Stream.mkString, Effect.mapError((error) => unreadableStdinField(field, `stdin cannot be read: ${error.message}`)))).replace(/[\r\n]
|
|
241
|
+
return (yield* stdio.value.stdin.pipe(Stream.decodeText(), Stream.mkString, Effect.mapError((error) => unreadableStdinField(field, `stdin cannot be read: ${error.message}`)))).replace(/[\r\n]+$/u, "");
|
|
232
242
|
});
|
|
233
243
|
const inputConfig = (action, positional, aliases, stdin) => {
|
|
234
244
|
const codec = Schema.toCodecJson(action.input);
|
|
@@ -275,6 +285,24 @@ const inputConfig = (action, positional, aliases, stdin) => {
|
|
|
275
285
|
};
|
|
276
286
|
};
|
|
277
287
|
const aliasFlag = (alias) => alias.length === 1 ? `-${alias}` : `--${alias}`;
|
|
288
|
+
const progressLinesOf = (progress) => {
|
|
289
|
+
if (progress === void 0) return;
|
|
290
|
+
const encode = Schema.encodeUnknownEffect(Schema.fromJsonString(Schema.toCodecJson(progress)));
|
|
291
|
+
return (value) => Effect.flatMap(Effect.orDie(encode(value)), Console.log);
|
|
292
|
+
};
|
|
293
|
+
const printedOf = (action) => {
|
|
294
|
+
const element = elementOf(action.success);
|
|
295
|
+
if (element === void 0) return (value, render, print) => Effect.flatMap(printedOutput(action, value, render), Option.match({
|
|
296
|
+
onNone: () => Effect.void,
|
|
297
|
+
onSome: (line) => Effect.sync(() => {
|
|
298
|
+
print(line);
|
|
299
|
+
})
|
|
300
|
+
}));
|
|
301
|
+
const encode = Schema.encodeUnknownEffect(Schema.fromJsonString(Schema.toCodecJson(element)));
|
|
302
|
+
return (value, render, print) => Stream.runForEach(elementsOf(Effect.succeed(value)), (each) => Effect.map(Effect.orDie(encode(each)), (json) => {
|
|
303
|
+
print(render === void 0 ? json : render(each));
|
|
304
|
+
}));
|
|
305
|
+
};
|
|
278
306
|
const command = (action, execute, options, errors) => {
|
|
279
307
|
const name = options?.name ?? kebab(action.name);
|
|
280
308
|
const description = options?.description ?? action.description;
|
|
@@ -293,7 +321,7 @@ const command = (action, execute, options, errors) => {
|
|
|
293
321
|
"render's --json"
|
|
294
322
|
]],
|
|
295
323
|
...Object.entries(options?.aliases ?? {}).flatMap(([field, alias]) => {
|
|
296
|
-
const bare = Predicate.isString(alias) ? alias.replace(
|
|
324
|
+
const bare = Predicate.isString(alias) ? alias.replace(/^-+/u, "") : void 0;
|
|
297
325
|
return bare === void 0 ? [] : [[
|
|
298
326
|
bare,
|
|
299
327
|
aliasFlag(bare),
|
|
@@ -302,7 +330,7 @@ const command = (action, execute, options, errors) => {
|
|
|
302
330
|
})
|
|
303
331
|
], ([key]) => key, {
|
|
304
332
|
labelOf: ([, label]) => label,
|
|
305
|
-
claimantOf: (
|
|
333
|
+
claimantOf: (claim) => claim[2],
|
|
306
334
|
fix: render === void 0 ? "rename the field or the alias" : "rename the field or the alias, or drop `render`"
|
|
307
335
|
});
|
|
308
336
|
const args = positional.map(({ param }) => param);
|
|
@@ -310,11 +338,12 @@ const command = (action, execute, options, errors) => {
|
|
|
310
338
|
...input,
|
|
311
339
|
...Object.fromEntries(positional.map(({ field }, index) => [field, values[index] ?? Option.none()]))
|
|
312
340
|
});
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
341
|
+
const progressLines = progressLinesOf(action.progress);
|
|
342
|
+
const printed = printedOf(action);
|
|
343
|
+
const run = (parsed, rendered) => Console.consoleWith((stdout) => decode(parsed).pipe(Effect.flatMap((input) => execute(input, progressLines)), Effect.flatMap((value) => printed(value, rendered, (line) => {
|
|
344
|
+
stdout.log(line);
|
|
345
|
+
})), failing, withStderrConsole));
|
|
346
|
+
const projected = render === void 0 ? Command.make(name, {
|
|
318
347
|
input: flags,
|
|
319
348
|
args
|
|
320
349
|
}, ({ input, args: values }) => run(parsedFields(input, values), void 0)) : Command.make(name, {
|
|
@@ -322,6 +351,28 @@ const command = (action, execute, options, errors) => {
|
|
|
322
351
|
args,
|
|
323
352
|
json: renderedCommandJsonFlag
|
|
324
353
|
}, ({ input, args: values, json }) => run(parsedFields(input, values), json ? void 0 : render));
|
|
325
|
-
return stdinHelp === void 0 ? Command.withDescription(
|
|
354
|
+
return stdinHelp === void 0 ? Command.withDescription(projected, description) : projected.pipe(Command.withDescription(`${description}\n\n${stdinHelp}`), Command.withShortDescription(description));
|
|
355
|
+
};
|
|
356
|
+
/**
|
|
357
|
+
* The command watching `event`, named after it, with one flag per field of its input as an
|
|
358
|
+
* action's command has, and `--cursor` to resume from: it prints each occurrence and gap `watch`
|
|
359
|
+
* yields as one line of compact JSON on stdout, until it is interrupted or the watch fails.
|
|
360
|
+
*/
|
|
361
|
+
const watchCommand = (event, watch, errors) => {
|
|
362
|
+
const { flags, decode } = inputConfig(event, [], {}, void 0);
|
|
363
|
+
const encode = Schema.encodeEffect(Schema.fromJsonString(Schema.toCodecJson(ReceivedOf(event))));
|
|
364
|
+
const failing = failingAsDerived([...errors, SubscriptionError]);
|
|
365
|
+
assertDistinct("flag", [...Object.keys(flags).map((field) => [kebab(field), `field ${field}`]), ["cursor", "--cursor"]], ([key]) => key, {
|
|
366
|
+
labelOf: ([key]) => `--${key}`,
|
|
367
|
+
claimantOf: ([, claimant]) => claimant,
|
|
368
|
+
fix: "rename the field"
|
|
369
|
+
});
|
|
370
|
+
const cursor = Flag.String("cursor").pipe(Flag.withDescription("Where to resume: a cursor an earlier line carried; omitted, from now"), Flag.optional);
|
|
371
|
+
return Command.make(event.name, {
|
|
372
|
+
input: flags,
|
|
373
|
+
cursor
|
|
374
|
+
}, ({ input, cursor: from }) => Console.consoleWith((stdout) => decode(input).pipe(Effect.flatMap((value) => Stream.runForEach(watch(value, Option.getOrUndefined(from)), (received) => Effect.flatMap(Effect.orDie(encode(received)), (line) => Effect.sync(() => {
|
|
375
|
+
stdout.log(line);
|
|
376
|
+
})))), failing, withStderrConsole))).pipe(Command.withDescription(event.description));
|
|
326
377
|
};
|
|
327
|
-
export { UserError, command, failingAsDerived, kebab };
|
|
378
|
+
export { UserError, command, failingAsDerived, kebab, watchCommand };
|
package/dist/contract/ast.mjs
CHANGED
|
@@ -5,5 +5,8 @@ const members = (ast) => {
|
|
|
5
5
|
const resolved = unsuspended(ast);
|
|
6
6
|
return SchemaAST.isUnion(resolved) ? resolved.types.flatMap(members) : [resolved];
|
|
7
7
|
};
|
|
8
|
-
const literalValues = (ast) =>
|
|
8
|
+
const literalValues = (ast) => {
|
|
9
|
+
if (SchemaAST.isLiteral(ast)) return [ast.literal];
|
|
10
|
+
return SchemaAST.isEnum(ast) ? ast.enums.map(([, value]) => value) : [void 0];
|
|
11
|
+
};
|
|
9
12
|
export { literalValues, members, unsuspended };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Refusal } from "./errors.mjs";
|
|
2
|
+
import "./implementation.mjs";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
/** `T`, read only once it is inferred: an implementation's build channels. */
|
|
5
|
+
type Deferred$1<T> = [T][T extends unknown ? 0 : never];
|
|
6
|
+
/** What `implement` takes as `authorize` for contracts `C`: the rule, or an Effect building it. */
|
|
7
|
+
export type Authorizer<C, RA, EAX, RAX> = ((contract: C) => Effect.Effect<void, Refusal, RA>) | Effect.Effect<(contract: C) => Effect.Effect<void, Refusal, RA>, EAX, RAX>;
|
|
8
|
+
export { Deferred$1 as Deferred };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { memoized } from "./implementation.mjs";
|
|
2
|
+
import { Effect, Predicate } from "effect";
|
|
3
|
+
// src/contract/authorizer.ts
|
|
4
|
+
/**
|
|
5
|
+
* The memoized authorizer of an implementation: the `authorize` given where it holds a protected
|
|
6
|
+
* contract, which then requires one, and a rule allowing every call where it holds none, which
|
|
7
|
+
* then takes none.
|
|
8
|
+
*/
|
|
9
|
+
const authorizerOf = (holding, holdsProtected, authorize) => {
|
|
10
|
+
const { singular, module } = holding;
|
|
11
|
+
if (holdsProtected && authorize === void 0) throw new Error(`Protected ${singular}s require authorize, or ${module}.allowAll`);
|
|
12
|
+
if (!holdsProtected && authorize !== void 0) throw new Error(`A public-only target takes no authorize (remove it, or list a protected ${singular})`);
|
|
13
|
+
const checked = (rule) => {
|
|
14
|
+
if (!Predicate.isFunction(rule)) throw new Error(`Missing authorize: not a function (pass your rule, ${module}.allowAll, or an Effect returning one)`);
|
|
15
|
+
return rule;
|
|
16
|
+
};
|
|
17
|
+
const given = authorize ?? (() => Effect.void);
|
|
18
|
+
return memoized(Effect.isEffect(given) ? Effect.map(given, checked) : Effect.succeed(checked(given)));
|
|
19
|
+
};
|
|
20
|
+
export { authorizerOf };
|
package/dist/contract/call.d.mts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import "./codec.mjs";
|
|
1
2
|
import "./errors.mjs";
|
|
3
|
+
import { ProgressArgs } from "./progress.mjs";
|
|
2
4
|
import "./implementation.mjs";
|
|
3
5
|
import { Any } from "../Action.mjs";
|
|
4
|
-
import { Effect, Schema } from "effect";
|
|
6
|
+
import { Effect, Schema, Stream } from "effect";
|
|
5
7
|
/**
|
|
6
|
-
* A call of `A` answering `R
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `
|
|
8
|
+
* A call of `A` answering `R`, taking `{ onProgress }` after its input where `A` declares
|
|
9
|
+
* `progress`. It may leave its input out when `{}` is a valid encoded input, such as for an
|
|
10
|
+
* action declared without `input` or one whose every field has a decoding default, and then
|
|
11
|
+
* sends the input `{}` decodes to. Every client shares this rule: `ActionHttp.client`,
|
|
12
|
+
* `ActionRpc.client`, `ActionMcp.client` and `Action.client`.
|
|
10
13
|
*/
|
|
11
|
-
export type Call<A extends Any, R> = {} extends A["input"]["Encoded"] ? (...input: [] | [input: A["input"]["Type"]]) => R : (input: A["input"]["Type"]) => R;
|
|
14
|
+
export type Call<A extends Any, R> = {} extends A["input"]["Encoded"] ? (...input: [] | [input: A["input"]["Type"], ...ProgressArgs<A>]) => R : (input: A["input"]["Type"], ...options: ProgressArgs<A>) => R;
|
package/dist/contract/call.mjs
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { InvalidInput } from "./errors.mjs";
|
|
2
|
-
import { Effect, Schema } from "effect";
|
|
2
|
+
import { Effect, Predicate, Schema, Stream } from "effect";
|
|
3
3
|
// src/contract/call.ts
|
|
4
|
+
const isCallOptions = (options) => Predicate.hasProperty(options, "onProgress") && Predicate.isFunction(options.onProgress);
|
|
5
|
+
/** The `onProgress` a call's arguments give after its input, where they give one. */
|
|
6
|
+
const onProgressOf = (args) => {
|
|
7
|
+
const [, options] = args;
|
|
8
|
+
return isCallOptions(options) ? options.onProgress : void 0;
|
|
9
|
+
};
|
|
4
10
|
/**
|
|
5
11
|
* How HTTP, RPC, MCP and a command decode a request's input: reporting every issue, and refusing a
|
|
6
12
|
* field the input does not declare.
|
|
@@ -14,9 +20,45 @@ const decodeInput = (action) => {
|
|
|
14
20
|
const decode = Schema.decodeUnknownEffect(Schema.toCodecJson(action.input), inputParseOptions);
|
|
15
21
|
return (encoded) => Effect.mapError(decode(encoded), InvalidInput.fromSchemaError);
|
|
16
22
|
};
|
|
17
|
-
const inputOf = (action, args) => args.length === 0 ? Schema.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
const inputOf = (action, args) => args.length === 0 ? Schema.decodeEffect(Schema.toCodecJson(action.input))({}) : Effect.succeed(args[0]);
|
|
24
|
+
/**
|
|
25
|
+
* Values of `schema` as they cross a network: each encoded to its JSON, then decoded back from
|
|
26
|
+
* that JSON's text, which carries `-0` as `0`, every issue reported, answering the JSON sent and
|
|
27
|
+
* the value received.
|
|
28
|
+
*/
|
|
29
|
+
const jsonRoundTrip = (schema) => {
|
|
30
|
+
const codec = Schema.toCodecJson(schema);
|
|
31
|
+
const encode = Schema.encodeUnknownEffect(codec, { errors: "all" });
|
|
32
|
+
const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(codec), { errors: "all" });
|
|
33
|
+
return (value) => Effect.flatMap(encode(value), (json) => Effect.map(decode(JSON.stringify(json)), (received) => ({
|
|
34
|
+
json,
|
|
35
|
+
value: received
|
|
36
|
+
})));
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* A watch's input as every client sends it: read as `inputOf` reads an action's, then through
|
|
40
|
+
* its JSON, dropping undeclared fields, or `InvalidInput` reporting every issue.
|
|
41
|
+
*/
|
|
42
|
+
const watchInput = (event, input) => inputOf(event, input === void 0 ? [] : [input]).pipe(Effect.flatMap(jsonRoundTrip(event.input)), Effect.mapError(InvalidInput.fromSchemaError));
|
|
43
|
+
/** Send the input's JSON, for a client handing it to its protocol as it is. */
|
|
44
|
+
const sendingJson = (action) => Schema.encodeUnknownEffect(Schema.toCodecJson(action.input), { errors: "all" });
|
|
45
|
+
/** Send the input itself once its JSON encodes, for a client encoding it by the action's schema. */
|
|
46
|
+
const sendingValue = (action) => {
|
|
47
|
+
const encode = sendingJson(action);
|
|
48
|
+
return (input) => Effect.as(encode(input), input);
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* A client's method of `action`, its input checked by `check` before `send` sends what the check
|
|
52
|
+
* answers, with the call's `onProgress` where it gave one, taking decoded values, for `send` to
|
|
53
|
+
* use where the action declares `progress`.
|
|
54
|
+
*/
|
|
55
|
+
const checked = (action, check, send) => {
|
|
56
|
+
const encode = check(action);
|
|
57
|
+
return (...args) => inputOf(action, args).pipe(Effect.flatMap(encode), Effect.mapError(InvalidInput.fromSchemaError), Effect.flatMap((input) => send(input, onProgressOf(args))));
|
|
58
|
+
};
|
|
59
|
+
/** A client's method of `action` answering a stream, its input checked as `checked` checks it. */
|
|
60
|
+
const checkedStream = (action, check, send) => {
|
|
61
|
+
const method = checked(action, check, (input, onProgress) => Effect.succeed(send(input, onProgress)));
|
|
62
|
+
return (...args) => Stream.unwrap(method(...args));
|
|
21
63
|
};
|
|
22
|
-
export { checked, decodeInput, inputOf, inputParseOptions };
|
|
64
|
+
export { checked, checkedStream, decodeInput, inputOf, inputParseOptions, jsonRoundTrip, onProgressOf, sendingJson, sendingValue, watchInput };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
/** A Standard Webhooks signing secret: as written, `whsec_` and base64, and the key it holds. */
|
|
3
|
+
export declare const SigningSecret: Schema.Struct<{
|
|
4
|
+
readonly text: Schema.String;
|
|
5
|
+
readonly key: Schema.Uint8Array;
|
|
6
|
+
}>;
|
|
7
|
+
export type SigningSecret = typeof SigningSecret.Type;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Effect, Option, Schema, SchemaIssue, SchemaTransformation } from "effect";
|
|
2
|
+
// src/contract/callback.ts
|
|
3
|
+
/** A webhook's callback: an https URL, which each delivery is POSTed to, decoded as a `URL`. */
|
|
4
|
+
const CallbackUrl = Schema.String.annotate({ description: "The https URL each delivery is POSTed to." }).pipe(Schema.decodeTo(Schema.URL, SchemaTransformation.urlFromString), Schema.check(Schema.makeFilter((url) => url.protocol === "https:" || "an https URL")));
|
|
5
|
+
/** A Standard Webhooks signing secret: as written, `whsec_` and base64, and the key it holds. */
|
|
6
|
+
const SigningSecret = Schema.Struct({
|
|
7
|
+
text: Schema.String,
|
|
8
|
+
key: Schema.Uint8Array
|
|
9
|
+
});
|
|
10
|
+
const secretPrefix = "whsec_";
|
|
11
|
+
const decodeBase64 = Schema.decodeUnknownOption(Schema.Uint8ArrayFromBase64);
|
|
12
|
+
const keyOf = (text) => text.startsWith(secretPrefix) ? Option.filter(decodeBase64(text.slice(6)), (key) => key.length >= 24 && key.length <= 64) : Option.none();
|
|
13
|
+
/**
|
|
14
|
+
* A webhook's signing secret, as Standard Webhooks writes one: `whsec_` and the base64 of 24 to
|
|
15
|
+
* 64 bytes, decoded as the `SigningSecret` it is.
|
|
16
|
+
*/
|
|
17
|
+
const WebhookSecret = Schema.String.annotate({ description: "whsec_ and the base64 of 24 to 64 random bytes, which signs each delivery." }).pipe(Schema.decodeTo(SigningSecret, SchemaTransformation.transformEffect({
|
|
18
|
+
decode: (text, options) => Option.match(keyOf(text), {
|
|
19
|
+
onNone: () => Effect.fail(new SchemaIssue.InvalidValue({ message: "whsec_ and the base64 of 24 to 64 random bytes" }, text, options)),
|
|
20
|
+
onSome: (key) => Effect.succeed({
|
|
21
|
+
text,
|
|
22
|
+
key
|
|
23
|
+
})
|
|
24
|
+
}),
|
|
25
|
+
encode: ({ text }) => Effect.succeed(text)
|
|
26
|
+
})));
|
|
27
|
+
export { CallbackUrl, SigningSecret, WebhookSecret };
|
|
@@ -1,7 +1,18 @@
|
|
|
1
|
-
import "
|
|
1
|
+
import { Codec } from "./codec.mjs";
|
|
2
|
+
import { Any } from "../Action.mjs";
|
|
3
|
+
import { Context, Effect } from "effect";
|
|
2
4
|
/**
|
|
3
5
|
* The caller of a public action, `caller: Action.Anyone`: anyone, signed in or not, and no
|
|
4
6
|
* authorizer runs for it. Registered globally, so two copies of the package agree on it, as they
|
|
5
7
|
* agree on a name.
|
|
6
8
|
*/
|
|
7
|
-
export declare const Anyone: unique symbol;
|
|
9
|
+
export declare const Anyone: unique symbol;
|
|
10
|
+
/** What names one caller of a surface: its authentication's `principal`, where it has one. */
|
|
11
|
+
export interface Keyed {
|
|
12
|
+
readonly principal?: (identity: Any["input"]["Type"]) => string;
|
|
13
|
+
readonly schema?: Codec | undefined;
|
|
14
|
+
}
|
|
15
|
+
/** A surface's authentication that names each caller: its `principal` given. */
|
|
16
|
+
export interface Named extends Keyed {
|
|
17
|
+
readonly principal: (identity: Any["input"]["Type"]) => string;
|
|
18
|
+
}
|
package/dist/contract/caller.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Context, Effect, Function } from "effect";
|
|
1
2
|
// src/contract/caller.ts
|
|
2
3
|
/**
|
|
3
4
|
* The caller of a public action, `caller: Action.Anyone`: anyone, signed in or not, and no
|
|
@@ -5,7 +6,25 @@
|
|
|
5
6
|
* agree on a name.
|
|
6
7
|
*/
|
|
7
8
|
const Anyone = Symbol.for("@gjermundgaraba/effect-actions/Anyone");
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* The authentication of the surface the running call came through, which names its caller where
|
|
11
|
+
* it has a `principal`: none in process, as `Action.client` calls.
|
|
12
|
+
*/
|
|
13
|
+
const CallerKeyed = Context.Reference("effect-actions/CallerKeyed", { defaultValue: Function.constUndefined });
|
|
14
|
+
/** Whether `keyed` names each caller, its `principal` given. */
|
|
15
|
+
const isNamed = (keyed) => keyed?.principal !== void 0;
|
|
16
|
+
/**
|
|
17
|
+
* The principal of the caller of `called`, as `keyed` names it: a poll's subscriber, for its
|
|
18
|
+
* `subscription` hook, and whose idempotency keys a call's are. None where `called` is public or
|
|
19
|
+
* `keyed` has no `principal`: every caller is then one.
|
|
20
|
+
*/
|
|
21
|
+
const principalOf = (called, keyed) => keyed?.principal === void 0 || called.caller === Anyone ? Effect.undefined : Effect.map(Effect.service(called.caller), (identity) => keyed.principal?.(identity));
|
|
22
|
+
const assertAuthentication = (contracts, authentication, noun = "action") => {
|
|
23
|
+
for (const contract of contracts) if (contract.caller !== Anyone && (authentication === void 0 || contract.caller.key !== authentication.identity.key)) throw new Error(`Protected ${noun} '${contract.name}' requires its matching authentication descriptor (give it the authentication whose identity the ${noun}'s caller names)`);
|
|
24
|
+
};
|
|
25
|
+
/** Refuses a declared `caller` that is neither `Anyone` nor an identity service. */
|
|
26
|
+
const assertCaller = (caller) => {
|
|
27
|
+
if (caller !== Anyone && !Context.isKey(caller)) throw new Error("Missing caller: declare Action.Anyone or an identity service key");
|
|
28
|
+
if (caller !== Anyone && Context.isReference(caller)) throw new Error("Invalid caller: an identity is a Context.Service, not a Context.Reference");
|
|
10
29
|
};
|
|
11
|
-
export { Anyone, assertAuthentication };
|
|
30
|
+
export { Anyone, CallerKeyed, assertAuthentication, assertCaller, isNamed, principalOf };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Effect, Predicate, Schema } from "effect";
|
|
2
|
+
// src/contract/canonical.ts
|
|
3
|
+
/**
|
|
4
|
+
* `value`, an encoded input, as JSON written one way whatever the order of its objects' keys:
|
|
5
|
+
* what makes two inputs one, for a subscription's key and an idempotency key's input alike.
|
|
6
|
+
*/
|
|
7
|
+
const canonical = (value) => JSON.stringify(value, (_key, nested) => Predicate.isObject(nested) && !Array.isArray(nested) ? Object.fromEntries(Object.entries(nested).toSorted(([a], [b]) => a < b ? -1 : 1)) : nested);
|
|
8
|
+
/** `input`, encoded to JSON by `schema`, as `canonical` writes it. */
|
|
9
|
+
const canonicalInput = (schema, input) => Effect.map(Effect.orDie(Schema.encodeUnknownEffect(Schema.toCodecJson(schema))(input)), canonical);
|
|
10
|
+
export { canonical, canonicalInput };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
/** A schema a contract holds: one with no services to decode or encode. */
|
|
3
|
+
export type Codec = Schema.Codec<unknown>;
|
|
4
|
+
/** Struct fields, given in place of a schema. */
|
|
5
|
+
export type Fields = Readonly<Record<string, Codec>>;
|
|
6
|
+
/** The input of a contract declared without one: an empty object. */
|
|
7
|
+
export declare const NoInput: Schema.$Record<Schema.String, Schema.Never>;
|
|
8
|
+
/** The schema a contract holds for `S`, given as a schema or as struct fields. */
|
|
9
|
+
export type CodecOf<S extends Codec | Fields> = S extends Codec ? S : S extends Schema.Struct.Fields ? keyof S extends never ? typeof NoInput : Extract<Schema.Struct<S>, Codec> : never;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
// src/contract/codec.ts
|
|
3
|
+
/** The input of a contract declared without one: an empty object. */
|
|
4
|
+
const NoInput = Schema.Record(Schema.String, Schema.Never);
|
|
5
|
+
const codecOf = (schema) => {
|
|
6
|
+
if (Schema.isSchema(schema)) return schema;
|
|
7
|
+
return Reflect.ownKeys(schema).length === 0 ? NoInput : Schema.Struct(schema);
|
|
8
|
+
};
|
|
9
|
+
export { NoInput, codecOf };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Forbidden, Refusal } from "./errors.mjs";
|
|
2
|
+
import { CallbackCategory, CallbackFailed, NotServed, NotSubscribed, SubscriptionError, Unsupported } from "../Event.mjs";
|
|
3
|
+
import { Option, Schema, SchemaTransformation, Struct } from "effect";
|
|
4
|
+
// src/contract/codes.ts
|
|
5
|
+
/** The error codes SEP-3415 defines, each naming one reason. */
|
|
6
|
+
const eventCodes = {
|
|
7
|
+
notFound: -32023,
|
|
8
|
+
forbidden: -32024,
|
|
9
|
+
unsupported: -32026,
|
|
10
|
+
callbackEndpoint: -32027
|
|
11
|
+
};
|
|
12
|
+
const sent = {
|
|
13
|
+
NotSubscribed: {
|
|
14
|
+
code: eventCodes.notFound,
|
|
15
|
+
data: Schema.Struct({ kind: Schema.Literal("subscription") }).pipe(Schema.decodeTo(Schema.instanceOf(NotSubscribed), SchemaTransformation.transform({
|
|
16
|
+
decode: () => new NotSubscribed(),
|
|
17
|
+
encode: () => ({ kind: "subscription" })
|
|
18
|
+
})))
|
|
19
|
+
},
|
|
20
|
+
NotServed: {
|
|
21
|
+
code: eventCodes.notFound,
|
|
22
|
+
data: Schema.UndefinedOr(Schema.Json).pipe(Schema.decodeTo(Schema.instanceOf(NotServed), SchemaTransformation.transform({
|
|
23
|
+
decode: () => new NotServed(),
|
|
24
|
+
encode: () => ({ kind: "event" })
|
|
25
|
+
})))
|
|
26
|
+
},
|
|
27
|
+
Unsupported: {
|
|
28
|
+
code: eventCodes.unsupported,
|
|
29
|
+
data: Schema.Struct(Struct.omit(Unsupported.fields, ["_tag"])).pipe(Schema.decodeTo(Schema.instanceOf(Unsupported), SchemaTransformation.transform({
|
|
30
|
+
decode: (fields) => new Unsupported(fields),
|
|
31
|
+
encode: ({ feature, value }) => ({
|
|
32
|
+
feature,
|
|
33
|
+
value
|
|
34
|
+
})
|
|
35
|
+
})))
|
|
36
|
+
},
|
|
37
|
+
CallbackFailed: {
|
|
38
|
+
code: eventCodes.callbackEndpoint,
|
|
39
|
+
data: Schema.Struct({ reason: CallbackCategory }).pipe(Schema.decodeTo(Schema.instanceOf(CallbackFailed), SchemaTransformation.transform({
|
|
40
|
+
decode: ({ reason }) => new CallbackFailed({ category: reason }),
|
|
41
|
+
encode: ({ category }) => ({ reason: category })
|
|
42
|
+
})))
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const encodeRefusal = Schema.encodeUnknownSync(Schema.toCodecJson(Refusal));
|
|
46
|
+
const decodeRefusal = Schema.decodeUnknownOption(Schema.toCodecJson(Refusal));
|
|
47
|
+
/**
|
|
48
|
+
* The SEP-3415 error `error` is sent as: a refusal's `-32024`, its message and the refusal as
|
|
49
|
+
* its data, or its reason's code and data beside its message.
|
|
50
|
+
*/
|
|
51
|
+
const sentError = (error) => {
|
|
52
|
+
if (Schema.is(Refusal)(error)) return {
|
|
53
|
+
code: eventCodes.forbidden,
|
|
54
|
+
message: error.message,
|
|
55
|
+
data: encodeRefusal(error)
|
|
56
|
+
};
|
|
57
|
+
const { code, data } = sent[error.reason._tag];
|
|
58
|
+
return {
|
|
59
|
+
code,
|
|
60
|
+
message: error.message,
|
|
61
|
+
data: Schema.encodeUnknownSync(Schema.toCodecJson(data))(error.reason)
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* The refusal a SEP-3415 error carries, as `sentError` writes one: its `data` decoded, or
|
|
66
|
+
* `Forbidden` with its message where `data` is no refusal.
|
|
67
|
+
*/
|
|
68
|
+
const refusalOf = (error) => Option.getOrElse(decodeRefusal(error.data), () => new Forbidden({ message: error.message }));
|
|
69
|
+
/**
|
|
70
|
+
* What a SEP-3415 error of the event `name` names, as a subscription's request fails with one and
|
|
71
|
+
* its termination carries one: a reason of `Event.SubscriptionError`, or the refusal; `undefined`
|
|
72
|
+
* for a code SEP-3415 does not define, or one whose `data` does not say what it names.
|
|
73
|
+
*/
|
|
74
|
+
const reasonOf = (name, error) => {
|
|
75
|
+
if (error.code === eventCodes.forbidden) return refusalOf(error);
|
|
76
|
+
for (const { code, data } of Object.values(sent)) {
|
|
77
|
+
const reason = code === error.code ? Schema.decodeUnknownOption(data)(error.data) : Option.none();
|
|
78
|
+
if (Option.isSome(reason)) return SubscriptionError.of(name, reason.value);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
export { eventCodes, reasonOf, refusalOf, sentError };
|
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import { literalValues, members } from "./ast.mjs";
|
|
2
1
|
import { Anyone, assertAuthentication } from "./caller.mjs";
|
|
2
|
+
import { literalValues, members } from "./ast.mjs";
|
|
3
3
|
import { builtIns } from "./errors.mjs";
|
|
4
|
-
import { assertOnce } from "./names.mjs";
|
|
5
4
|
import { httpApiStatusOf } from "./status.mjs";
|
|
6
5
|
import { Predicate, Schema, SchemaAST } from "effect";
|
|
7
6
|
// src/contract/declared.ts
|
|
8
|
-
const errorList = (error) =>
|
|
7
|
+
const errorList = (error) => {
|
|
8
|
+
if (error === void 0) return [];
|
|
9
|
+
return Schema.isSchema(error) ? [error] : error;
|
|
10
|
+
};
|
|
9
11
|
const ownsNothing = (ast) => httpApiStatusOf(ast) === void 0 && ast.checks === void 0 && ast.encoding === void 0;
|
|
10
|
-
const schemasOf = (ast) =>
|
|
12
|
+
const schemasOf = (ast) => {
|
|
13
|
+
if (!ownsNothing(ast)) return [ast];
|
|
14
|
+
if (SchemaAST.isSuspend(ast)) return schemasOf(ast.thunk());
|
|
15
|
+
return SchemaAST.isUnion(ast) ? ast.types.flatMap(schemasOf) : [ast];
|
|
16
|
+
};
|
|
11
17
|
/** `errors` as the schemas a call decodes a failure with, in order, each once. */
|
|
12
18
|
const errorSchemas = (errors) => {
|
|
13
19
|
const schemas = /* @__PURE__ */ new Map();
|
|
@@ -24,6 +30,14 @@ const projectedErrors = (action, surface = []) => errorSchemas([
|
|
|
24
30
|
* binding's and, where `action` is protected, its authentication's.
|
|
25
31
|
*/
|
|
26
32
|
const bindingErrors = (action, binding) => projectedErrors(action, action.caller === Anyone || binding.authentication === void 0 ? binding.error : [...binding.error, ...binding.authentication.error]);
|
|
33
|
+
/**
|
|
34
|
+
* Every error a binding's subscription to `event` declares: the built-in ones, the binding's and,
|
|
35
|
+
* where `event` is protected, its authentication's. An event type declares none of its own.
|
|
36
|
+
*/
|
|
37
|
+
const eventErrors = (event, binding) => bindingErrors({
|
|
38
|
+
error: [],
|
|
39
|
+
caller: event.caller
|
|
40
|
+
}, binding);
|
|
27
41
|
const encodedTags = (ast) => members(SchemaAST.toEncoded(ast)).flatMap((member) => {
|
|
28
42
|
const tag = SchemaAST.isObjects(member) ? member.propertySignatures.find((property) => property.name === "_tag")?.type : void 0;
|
|
29
43
|
return tag === void 0 ? [] : members(tag).flatMap(literalValues).filter(Predicate.isString);
|
|
@@ -38,17 +52,19 @@ const assertDistinctTags = (what, errors) => {
|
|
|
38
52
|
}
|
|
39
53
|
};
|
|
40
54
|
/**
|
|
41
|
-
* The checks `ActionHttp.make` and `ActionRpc.make` share:
|
|
42
|
-
*
|
|
55
|
+
* The checks `ActionHttp.make` and `ActionRpc.make` share: the descriptor every protected action
|
|
56
|
+
* and event type names, and each call's and each subscription's errors distinct by `_tag`.
|
|
57
|
+
* Returns the binding's `error`, as a list, and `authentication`.
|
|
43
58
|
*/
|
|
44
|
-
const bindingOf = (what, actions, options) => {
|
|
45
|
-
assertOnce("action", actions);
|
|
59
|
+
const bindingOf = (what, actions, events, options) => {
|
|
46
60
|
const bound = {
|
|
47
61
|
error: errorList(options.error),
|
|
48
62
|
authentication: options.authentication
|
|
49
63
|
};
|
|
50
64
|
assertAuthentication(actions, bound.authentication);
|
|
65
|
+
assertAuthentication(events, bound.authentication, "event");
|
|
51
66
|
for (const action of actions) assertDistinctTags(`${what} of "${action.name}"`, bindingErrors(action, bound));
|
|
67
|
+
for (const event of events) assertDistinctTags(`${what} of event "${event.name}"`, eventErrors(event, bound));
|
|
52
68
|
return bound;
|
|
53
69
|
};
|
|
54
|
-
export { assertDistinctTags, bindingErrors, bindingOf, errorList, errorSchemas, projectedErrors };
|
|
70
|
+
export { assertDistinctTags, bindingErrors, bindingOf, errorList, errorSchemas, eventErrors, projectedErrors };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
// src/contract/digest.ts
|
|
3
|
+
const encoder = new TextEncoder();
|
|
4
|
+
/** The SHA-256 of `text`, in hex. */
|
|
5
|
+
const sha256Hex = (text) => Effect.promise(async () => Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(text))), (byte) => byte.toString(16).padStart(2, "0")).join(""));
|
|
6
|
+
export { sha256Hex };
|