@gjermundgaraba/effect-actions 0.10.0 → 0.12.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 +103 -43
- package/dist/Action.d.mts +31 -118
- package/dist/Action.mjs +15 -53
- package/dist/ActionCli.d.mts +60 -28
- package/dist/ActionCli.mjs +54 -48
- package/dist/ActionHttp.d.mts +18 -67
- package/dist/ActionHttp.mjs +20 -70
- package/dist/ActionMcp.d.mts +25 -31
- package/dist/ActionMcp.mjs +140 -74
- package/dist/ActionRpc.d.mts +147 -0
- package/dist/ActionRpc.mjs +113 -0
- package/dist/ActionToolkit.d.mts +5 -8
- package/dist/ActionToolkit.mjs +3 -3
- package/dist/Authentication.d.mts +15 -15
- package/dist/Authentication.mjs +42 -96
- package/dist/Testing.d.mts +32 -25
- package/dist/Testing.mjs +45 -30
- package/dist/{internal/authentication.d.mts → authentication/provider.d.mts} +41 -9
- package/dist/authentication/provider.mjs +12 -0
- package/dist/authentication/refusal.mjs +36 -0
- package/dist/{internal/cli.d.mts → cli/commands.d.mts} +16 -12
- package/dist/cli/commands.mjs +302 -0
- package/dist/{internal → contract}/call.d.mts +2 -2
- package/dist/contract/call.mjs +10 -0
- package/dist/{internal → contract}/errors.d.mts +13 -10
- package/dist/{internal → contract}/errors.mjs +9 -14
- package/dist/{internal → contract}/implementation.d.mts +37 -21
- package/dist/{internal → contract}/implementation.mjs +18 -49
- package/dist/contract/media.d.mts +13 -0
- package/dist/contract/media.mjs +18 -0
- package/dist/contract/rules.d.mts +25 -0
- package/dist/{internal/actions.mjs → contract/rules.mjs} +6 -42
- package/dist/{internal → http}/client.d.mts +7 -9
- package/dist/http/client.mjs +17 -0
- package/dist/http/declared.mjs +20 -0
- package/dist/http/schema-errors.mjs +9 -0
- package/dist/mcp/protocol.d.mts +17 -0
- package/dist/mcp/protocol.mjs +149 -0
- package/dist/stdio/console.mjs +64 -0
- package/dist/{internal → testing}/memory.d.mts +1 -1
- package/dist/{internal → testing}/memory.mjs +5 -17
- package/dist/toolkit/tools.mjs +29 -0
- package/docs/Action-failures.md +47 -0
- package/docs/Action-in-process.md +93 -0
- package/docs/Action.md +206 -424
- package/docs/ActionCli-failures.md +102 -0
- package/docs/ActionCli.md +206 -292
- package/docs/ActionHttp-failures.md +83 -0
- package/docs/ActionHttp.md +202 -281
- package/docs/ActionMcp-failures.md +31 -0
- package/docs/ActionMcp.md +229 -193
- package/docs/ActionRpc-failures.md +21 -0
- package/docs/ActionRpc.md +178 -0
- package/docs/ActionToolkit-failures.md +15 -0
- package/docs/ActionToolkit.md +100 -117
- package/docs/Authentication-advanced.md +139 -0
- package/docs/Authentication-failures.md +99 -0
- package/docs/Authentication.md +142 -392
- package/docs/CONTEXT.md +39 -23
- package/docs/README.md +71 -35
- package/docs/Testing-failures.md +20 -0
- package/docs/Testing.md +186 -151
- package/docs/guarantees.md +35 -95
- package/docs/setup-failures.md +34 -0
- package/docs/setup.md +88 -87
- package/examples/README.md +68 -0
- package/examples/assistant/README.md +92 -0
- package/examples/assistant/src/agent.ts +92 -0
- package/examples/assistant/src/cli.ts +34 -0
- package/examples/assistant/src/commands.ts +25 -0
- package/examples/assistant/src/identity/Identity.ts +33 -0
- package/examples/assistant/src/identity/authorize.ts +24 -0
- package/examples/assistant/src/identity/handlers.ts +10 -0
- package/examples/assistant/src/identity/members.ts +9 -0
- package/examples/assistant/src/identity/permissions.ts +23 -0
- package/examples/assistant/src/scripted.ts +23 -0
- package/examples/assistant/src/tasks/Tasks.ts +63 -0
- package/examples/assistant/src/tasks/handlers.ts +25 -0
- package/examples/assistant/src/tasks/store.ts +96 -0
- package/examples/assistant/src/toolkit.ts +30 -0
- package/examples/assistant/test/commands.test.ts +57 -0
- package/examples/assistant/test/identity/identity.test.ts +43 -0
- package/examples/assistant/test/tasks/store.test.ts +48 -0
- package/examples/assistant/test/tasks/tasks.test.ts +75 -0
- package/examples/assistant/test/toolkit.test.ts +121 -0
- package/examples/kitchen-sink/README.md +317 -0
- package/examples/kitchen-sink/src/Actions.ts +19 -0
- package/examples/kitchen-sink/src/Api.ts +24 -0
- package/examples/kitchen-sink/src/Rpc.ts +12 -0
- package/examples/kitchen-sink/src/admin/Admin.ts +59 -0
- package/examples/kitchen-sink/src/admin/authenticate.ts +18 -0
- package/examples/kitchen-sink/src/admin/authorize.ts +27 -0
- package/examples/kitchen-sink/src/admin/grants.ts +22 -0
- package/examples/kitchen-sink/src/admin/handlers.ts +21 -0
- package/examples/kitchen-sink/src/admin/keys.ts +8 -0
- package/examples/kitchen-sink/src/agent.ts +47 -0
- package/examples/kitchen-sink/src/assistant/Assistant.ts +82 -0
- package/examples/kitchen-sink/src/assistant/conversations.ts +71 -0
- package/examples/kitchen-sink/src/assistant/demo-model.ts +33 -0
- package/examples/kitchen-sink/src/assistant/handlers.ts +146 -0
- package/examples/kitchen-sink/src/attachments/Attachments.ts +48 -0
- package/examples/kitchen-sink/src/attachments/download.ts +49 -0
- package/examples/kitchen-sink/src/attachments/handlers.ts +38 -0
- package/examples/kitchen-sink/src/attachments/store.ts +58 -0
- package/examples/kitchen-sink/src/browser.ts +33 -0
- package/examples/kitchen-sink/src/cli.ts +85 -0
- package/examples/kitchen-sink/src/config.ts +18 -0
- package/examples/kitchen-sink/src/identity/Identity.ts +61 -0
- package/examples/kitchen-sink/src/identity/authenticate.ts +87 -0
- package/examples/kitchen-sink/src/identity/authorize.ts +16 -0
- package/examples/kitchen-sink/src/identity/directory.ts +52 -0
- package/examples/kitchen-sink/src/identity/handlers.ts +9 -0
- package/examples/kitchen-sink/src/identity/members.ts +13 -0
- package/examples/kitchen-sink/src/identity/quota.ts +62 -0
- package/examples/kitchen-sink/src/issues/Issues.ts +109 -0
- package/examples/kitchen-sink/src/issues/assistant.ts +23 -0
- package/examples/kitchen-sink/src/issues/guide.ts +37 -0
- package/examples/kitchen-sink/src/issues/handlers.ts +37 -0
- package/examples/kitchen-sink/src/issues/store.ts +96 -0
- package/examples/kitchen-sink/src/issues/write-limit.ts +41 -0
- package/examples/kitchen-sink/src/memory.ts +30 -0
- package/examples/kitchen-sink/src/ops.ts +42 -0
- package/examples/kitchen-sink/src/request-policy.ts +63 -0
- package/examples/kitchen-sink/src/routes.ts +120 -0
- package/examples/kitchen-sink/src/sdk.ts +46 -0
- package/examples/kitchen-sink/src/seed.ts +65 -0
- package/examples/kitchen-sink/src/server.ts +62 -0
- package/examples/kitchen-sink/src/stdio.ts +34 -0
- package/examples/kitchen-sink/src/workspaces/Workspaces.ts +28 -0
- package/examples/kitchen-sink/src/workspaces/handlers.ts +23 -0
- package/examples/kitchen-sink/src/workspaces/registry.ts +46 -0
- package/examples/kitchen-sink/src/workspaces/tenant.ts +17 -0
- package/examples/kitchen-sink/test/admin/admin.test.ts +67 -0
- package/examples/kitchen-sink/test/assistant/assistant.test.ts +435 -0
- package/examples/kitchen-sink/test/attachments/attachments.test.ts +161 -0
- package/examples/kitchen-sink/test/identity/identity.test.ts +137 -0
- package/examples/kitchen-sink/test/identity/session.test.ts +110 -0
- package/examples/kitchen-sink/test/issues/assistant.test.ts +75 -0
- package/examples/kitchen-sink/test/issues/issues.test.ts +135 -0
- package/examples/kitchen-sink/test/request-policy.test.ts +125 -0
- package/examples/kitchen-sink/test/routes.test.ts +123 -0
- package/examples/kitchen-sink/test/support/scripted.ts +40 -0
- package/examples/live-rpc/README.md +107 -0
- package/examples/live-rpc/src/Rpc.ts +8 -0
- package/examples/live-rpc/src/browser.ts +98 -0
- package/examples/live-rpc/src/chat/Chat.ts +75 -0
- package/examples/live-rpc/src/chat/authorize.ts +16 -0
- package/examples/live-rpc/src/chat/handlers.ts +50 -0
- package/examples/live-rpc/src/chat/room.ts +23 -0
- package/examples/live-rpc/src/chat/store.ts +54 -0
- package/examples/live-rpc/src/client.ts +37 -0
- package/examples/live-rpc/src/identity/Identity.ts +19 -0
- package/examples/live-rpc/src/identity/members.ts +8 -0
- package/examples/live-rpc/src/identity/verifier.ts +23 -0
- package/examples/live-rpc/src/routes.ts +22 -0
- package/examples/live-rpc/src/server.ts +43 -0
- package/examples/live-rpc/test/browser.test.ts +105 -0
- package/examples/live-rpc/test/chat/chat.test.ts +69 -0
- package/examples/live-rpc/test/routes.test.ts +99 -0
- package/examples/live-rpc/test/server.test.ts +75 -0
- package/examples/mcp-tools/README.md +136 -0
- package/examples/mcp-tools/src/instructions.ts +12 -0
- package/examples/mcp-tools/src/printers/Printers.ts +119 -0
- package/examples/mcp-tools/src/printers/fake.ts +115 -0
- package/examples/mcp-tools/src/printers/farm.ts +136 -0
- package/examples/mcp-tools/src/printers/handlers.ts +29 -0
- package/examples/mcp-tools/src/routes.ts +52 -0
- package/examples/mcp-tools/src/server.ts +30 -0
- package/examples/mcp-tools/src/stdio.ts +26 -0
- package/examples/mcp-tools/test/printers/printers.test.ts +189 -0
- package/examples/mcp-tools/test/routes.test.ts +202 -0
- package/examples/notes-service/README.md +108 -0
- package/examples/notes-service/src/Api.ts +13 -0
- package/examples/notes-service/src/browser.ts +41 -0
- package/examples/notes-service/src/cli.ts +22 -0
- package/examples/notes-service/src/commands.ts +89 -0
- package/examples/notes-service/src/identity/Identity.ts +31 -0
- package/examples/notes-service/src/identity/authorize.ts +18 -0
- package/examples/notes-service/src/identity/handlers.ts +10 -0
- package/examples/notes-service/src/identity/users.ts +9 -0
- package/examples/notes-service/src/identity/verifier.ts +36 -0
- package/examples/notes-service/src/notes/Notes.ts +91 -0
- package/examples/notes-service/src/notes/handlers.ts +33 -0
- package/examples/notes-service/src/notes/store.ts +73 -0
- package/examples/notes-service/src/routes.ts +45 -0
- package/examples/notes-service/src/server.ts +26 -0
- package/examples/notes-service/src/status/Status.ts +10 -0
- package/examples/notes-service/src/status/handlers.ts +14 -0
- package/examples/notes-service/src/stdio.ts +25 -0
- package/examples/notes-service/test/commands.test.ts +134 -0
- package/examples/notes-service/test/notes/notes.test.ts +103 -0
- package/examples/notes-service/test/routes.test.ts +84 -0
- package/examples/quickstart/README.md +31 -0
- package/examples/quickstart/src/Api.ts +5 -0
- package/examples/quickstart/src/client.ts +13 -0
- package/examples/quickstart/src/greeting/Greeting.ts +12 -0
- package/examples/quickstart/src/greeting/handlers.ts +6 -0
- package/examples/quickstart/src/routes.ts +11 -0
- package/examples/quickstart/src/server.ts +16 -0
- package/examples/quickstart/test/greeting/greeting.test.ts +18 -0
- package/package.json +32 -14
- package/dist/internal/actions.d.mts +0 -9
- package/dist/internal/authentication.mjs +0 -11
- package/dist/internal/call.mjs +0 -12
- package/dist/internal/cli.mjs +0 -328
- package/dist/internal/client.mjs +0 -25
- package/dist/internal/console.mjs +0 -77
- package/dist/internal/declared.d.mts +0 -13
- package/dist/internal/declared.mjs +0 -34
- package/dist/internal/mcp.d.mts +0 -11
- package/dist/internal/mcp.mjs +0 -52
- package/dist/internal/refusal.mjs +0 -67
- package/dist/internal/schema-errors.mjs +0 -20
- package/dist/internal/tools.mjs +0 -44
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { Anyone } from "
|
|
2
|
-
import { Refusal } from "
|
|
3
|
-
import { Protected, ServedRequest, Serving } from "
|
|
1
|
+
import { Anyone } from "../contract/rules.mjs";
|
|
2
|
+
import { Refusal } from "../contract/errors.mjs";
|
|
3
|
+
import { BuildServices, Protected, ServedRequest, Serving } from "../contract/implementation.mjs";
|
|
4
4
|
import { Action_d_exports, Any as Any$1, Errors } from "../Action.mjs";
|
|
5
5
|
import { Context, Effect } from "effect";
|
|
6
6
|
import { HttpRouter, HttpServerResponse } from "effect/http";
|
|
7
7
|
import { HttpApiMiddleware, HttpApiSecurity } from "effect/http-api";
|
|
8
|
-
//#region src/
|
|
8
|
+
//#region src/authentication/provider.d.ts
|
|
9
9
|
/** One native security scheme: Bearer unless a descriptor names another. */
|
|
10
10
|
export type Security = HttpApiSecurity.HttpApiSecurity;
|
|
11
11
|
/** What a native scheme decodes: a `Redacted` token or key, or Basic credentials. */
|
|
@@ -34,6 +34,12 @@ type Response = Effect.Effect<HttpServerResponse.HttpServerResponse, unknown, un
|
|
|
34
34
|
interface Runtime$1 {
|
|
35
35
|
/** The descriptor the provider was built from, the one a binding naming it must hold. */
|
|
36
36
|
readonly descriptor: unknown;
|
|
37
|
+
/**
|
|
38
|
+
* The verifier itself, for a surface answering refusals its own way, such as RPC: the
|
|
39
|
+
* credential to the identity, or a refusal, or an error the descriptor declares. An empty
|
|
40
|
+
* credential, as the native decoder gives an absent one, is refused without reaching it.
|
|
41
|
+
*/
|
|
42
|
+
readonly authenticate: (credential: Credential) => Effect.Effect<unknown, unknown, unknown>;
|
|
37
43
|
/** The native security middleware's implementation, keyed by its scheme. */
|
|
38
44
|
readonly middleware: Readonly<Record<string, (route: Response, options: {
|
|
39
45
|
readonly credential: Credential;
|
|
@@ -43,6 +49,11 @@ interface Runtime$1 {
|
|
|
43
49
|
* answered with its challenge under Bearer; as it is under another scheme.
|
|
44
50
|
*/
|
|
45
51
|
readonly stepUp: HttpApiMiddleware.HttpApiMiddleware<never, never, never>;
|
|
52
|
+
/**
|
|
53
|
+
* An MCP endpoint's request: decoded here, and a protected tool's call promotes what it
|
|
54
|
+
* verified. Where `optional`, a request presenting no credential, which the scheme decodes as
|
|
55
|
+
* empty, passes signed out.
|
|
56
|
+
*/
|
|
46
57
|
readonly http: <E, R>(route: Effect.Effect<HttpServerResponse.HttpServerResponse, E, R>, optional: boolean) => Effect.Effect<HttpServerResponse.HttpServerResponse, E, R | HttpRouter.Provided>;
|
|
47
58
|
/**
|
|
48
59
|
* A route of the host's own: the request is decoded here, the route gets the identity
|
|
@@ -61,7 +72,7 @@ export interface Descriptor<I, A, S extends Security, Name extends string = stri
|
|
|
61
72
|
readonly security: S;
|
|
62
73
|
/**
|
|
63
74
|
* What its verifier may fail with besides a refusal, as a list: every protected endpoint
|
|
64
|
-
* declares it, and every remote surface sends it with its status.
|
|
75
|
+
* declares it, and every remote surface sends it, over HTTP with its status.
|
|
65
76
|
*/
|
|
66
77
|
readonly error: E;
|
|
67
78
|
readonly "~provider": Context.Key<Provider<I, Name>, Runtime$1>;
|
|
@@ -69,12 +80,12 @@ export interface Descriptor<I, A, S extends Security, Name extends string = stri
|
|
|
69
80
|
/** An MCP request's verified identity, which a protected tool's call promotes. */
|
|
70
81
|
readonly "~verified": Context.Key<never, unknown>;
|
|
71
82
|
}
|
|
72
|
-
export type Any = Descriptor<unknown, unknown, Security, string, Errors>;
|
|
73
|
-
export type Identity<A extends Any$1> = [A] extends [never] ? never : A["caller"] extends Context.Key<infer I, unknown> ? I : never;
|
|
74
83
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
84
|
+
* Any descriptor `make` returns, named so a package emitting declarations can export one, and a
|
|
85
|
+
* binding name one.
|
|
77
86
|
*/
|
|
87
|
+
export type Any = Descriptor<unknown, unknown, Security, string, Errors>;
|
|
88
|
+
export type Identity<A extends Any$1> = [A] extends [never] ? never : A["caller"] extends Context.Key<infer I, unknown> ? I : never;
|
|
78
89
|
type Covered<A extends Any$1> = Exclude<A["caller"], typeof Anyone> extends Context.Key<infer I, unknown> ? I : never;
|
|
79
90
|
export type Required<A extends Any$1> = [Protected<A>] extends [never] ? {
|
|
80
91
|
readonly authentication?: Any;
|
|
@@ -82,6 +93,16 @@ export type Required<A extends Any$1> = [Protected<A>] extends [never] ? {
|
|
|
82
93
|
readonly authentication: Descriptor<Covered<Protected<A>>, unknown, Security, string, Errors>;
|
|
83
94
|
};
|
|
84
95
|
export type ProviderOf<D> = D extends Descriptor<infer I, unknown, Security, infer Name, Errors> ? Provider<I, Name> : never;
|
|
96
|
+
/** Actions a binding takes without options: public ones, as a protected one names its descriptor. */
|
|
97
|
+
export type PublicOnly<Actions extends ReadonlyArray<Any$1>> = [Exclude<Actions[number], {
|
|
98
|
+
readonly caller: typeof Anyone;
|
|
99
|
+
}>] extends [never] ? unknown : {
|
|
100
|
+
readonly "Protected actions take options naming their authentication": never;
|
|
101
|
+
};
|
|
102
|
+
/** The descriptor options `O` name, where they always name one. */
|
|
103
|
+
export type DescriptorOf<O> = [O] extends [{
|
|
104
|
+
readonly authentication: infer D extends Any;
|
|
105
|
+
}] ? D : undefined;
|
|
85
106
|
/** All protected actions on a surface use the descriptor's single identity key. */
|
|
86
107
|
export type Matching<A extends Any$1, D> = unknown extends Covered<Protected<A>> ? unknown : [Exclude<Covered<Protected<A>>, D extends Descriptor<infer I, unknown, Security, string, Errors> ? I : never>] extends [never] ? unknown : {
|
|
87
108
|
readonly "Authentication descriptor does not cover every protected action": never;
|
|
@@ -90,6 +111,17 @@ export type Matching<A extends Any$1, D> = unknown extends Covered<Protected<A>>
|
|
|
90
111
|
export type RemoteRequest<App, A extends Any$1> = ServedRequest<App, Extract<A, {
|
|
91
112
|
readonly caller: typeof Anyone;
|
|
92
113
|
}>> | Exclude<ServedRequest<App, Protected<A>>, Identity<Protected<A>>>;
|
|
114
|
+
/**
|
|
115
|
+
* What a server of the actions `A` of the implementations `App` reads at startup, as
|
|
116
|
+
* `BuildServices` states it, with the caller a builder yields named as the mistake it is: a server
|
|
117
|
+
* has its caller per request.
|
|
118
|
+
*/
|
|
119
|
+
export type ServerBuildServices<App, A extends Any$1> = App extends unknown ? BuildServices<App, A> extends (infer R) ? R extends Identity<Protected<A>> ? {
|
|
120
|
+
readonly "A builder yields the caller, which a server has per request: yield it in a handler or an authorizer": {
|
|
121
|
+
readonly caller: R;
|
|
122
|
+
readonly builderOf: Serving<App, A>["name"];
|
|
123
|
+
};
|
|
124
|
+
} : R : never : never;
|
|
93
125
|
/**
|
|
94
126
|
* What a call of `A` under the descriptor `D` may fail with from its verifier: the errors `D`
|
|
95
127
|
* declares, for a protected action; none for a public one, which nothing verifies.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Unauthenticated } from "../contract/errors.mjs";
|
|
2
|
+
import { Anyone } from "../contract/rules.mjs";
|
|
3
|
+
import { Effect, Option } from "effect";
|
|
4
|
+
//#region src/authentication/provider.ts
|
|
5
|
+
const withVerifierErrors = (action, errors, verifierErrors) => action.caller === Anyone || verifierErrors === void 0 ? errors : [...errors, ...verifierErrors];
|
|
6
|
+
const assertAuthentication = (actions, auth) => {
|
|
7
|
+
for (const action of actions) if (action.caller !== Anyone && (auth === void 0 || action.caller.key !== auth.identity.key)) throw new Error(`Protected action '${action.name}' requires its matching authentication descriptor`);
|
|
8
|
+
};
|
|
9
|
+
const providerOf = (auth) => Effect.flatMap(Effect.service(auth["~provider"]), (provider) => provider.descriptor === auth ? Effect.succeed(provider) : Effect.die(/* @__PURE__ */ new Error(`Authentication "${auth.name}": the binding's descriptor is not its provider's; build both from one descriptor`)));
|
|
10
|
+
const promote = (auth, run) => Effect.flatMap(Effect.serviceOption(auth["~verified"]), (actor) => Option.isSome(actor) ? Effect.provideService(run, auth.identity, actor.value) : Effect.fail(new Unauthenticated()));
|
|
11
|
+
//#endregion
|
|
12
|
+
export { assertAuthentication, promote, providerOf, withVerifierErrors };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Refusal, statuses } from "../contract/errors.mjs";
|
|
2
|
+
import { Cause, Context, Effect, Exit, Option, Predicate, Ref, Schema } from "effect";
|
|
3
|
+
import { HttpServerResponse } from "effect/http";
|
|
4
|
+
//#region src/authentication/refusal.ts
|
|
5
|
+
const rfc6750ErrorDescriptionCharacters = /^[\x20\x21\x23-\x5B\x5D-\x7E]+$/;
|
|
6
|
+
const challenge = (scheme, parameters) => {
|
|
7
|
+
const given = parameters.flatMap(([name, value]) => value === void 0 ? [] : [`${name}="${value.replace(/["\\]/g, "\\$&")}"`]);
|
|
8
|
+
return given.length === 0 ? scheme : `${scheme} ${given.join(", ")}`;
|
|
9
|
+
};
|
|
10
|
+
const bearer = (parameters) => challenge("Bearer", parameters);
|
|
11
|
+
const encode = Schema.encodeSync(Refusal);
|
|
12
|
+
const isRefusal = Schema.is(Refusal);
|
|
13
|
+
const insufficientScopeChallenge = (error, metadataUrl) => Predicate.isTagged(error, "Forbidden") && error.scopes !== void 0 ? bearer([
|
|
14
|
+
["error", "insufficient_scope"],
|
|
15
|
+
["scope", error.scopes.join(" ")],
|
|
16
|
+
["resource_metadata", metadataUrl],
|
|
17
|
+
["error_description", rfc6750ErrorDescriptionCharacters.test(error.message) ? error.message : void 0]
|
|
18
|
+
]) : void 0;
|
|
19
|
+
const plain = (error) => HttpServerResponse.jsonUnsafe(encode(error), { status: statuses[error._tag] });
|
|
20
|
+
const answer = (error, metadataUrl) => {
|
|
21
|
+
const response = plain(error);
|
|
22
|
+
const scoped = insufficientScopeChallenge(error, metadataUrl);
|
|
23
|
+
return scoped === void 0 ? response : HttpServerResponse.setHeader(response, "www-authenticate", scoped);
|
|
24
|
+
};
|
|
25
|
+
var SteppedUpRefusal = class extends Context.Service()("effect-actions/SteppedUp") {};
|
|
26
|
+
const isStepUp = (error) => isRefusal(error) && (!Predicate.isTagged(error, "Forbidden") || error.scopes !== void 0);
|
|
27
|
+
const recordStepUp = (call) => Effect.tapError(call, (error) => isStepUp(error) ? Effect.flatMap(Effect.serviceOption(SteppedUpRefusal), (slot) => Option.isSome(slot) ? Ref.set(slot.value, Option.some(error)) : Effect.void) : Effect.void);
|
|
28
|
+
const answerStepUp = (route, metadataUrl) => Effect.gen(function* () {
|
|
29
|
+
const slot = yield* Ref.make(Option.none());
|
|
30
|
+
const exit = yield* Effect.exit(Effect.provideService(route, SteppedUpRefusal, slot));
|
|
31
|
+
const refused = yield* Ref.get(slot);
|
|
32
|
+
const diedOrInterruptedAfterRefusal = Exit.isFailure(exit) && (Cause.hasDies(exit.cause) || Cause.hasInterrupts(exit.cause));
|
|
33
|
+
return Option.isSome(refused) && !diedOrInterruptedAfterRefusal ? answer(refused.value, metadataUrl) : yield* exit;
|
|
34
|
+
});
|
|
35
|
+
//#endregion
|
|
36
|
+
export { answer, answerStepUp, bearer, challenge, isRefusal, isStepUp, plain, recordStepUp };
|
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
import "
|
|
1
|
+
import "../contract/errors.mjs";
|
|
2
2
|
import { Action_d_exports, Any } from "../Action.mjs";
|
|
3
3
|
import { Effect, Runtime, Types } from "effect";
|
|
4
4
|
import { CliError, Command } from "effect/cli";
|
|
5
|
-
//#region src/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*/
|
|
10
|
-
export type Field<A extends Any> = A["input"]["Encoded"] extends (infer E) ? true extends Types.IsUnion<E> ? never : E extends ReadonlyArray<unknown> ? never : E extends object ? string extends keyof E ? never : Extract<keyof E, string> : never : never;
|
|
11
|
-
/** How one command is named, takes its input and prints its result. */
|
|
5
|
+
//#region src/cli/commands.d.ts
|
|
6
|
+
type Field<A extends Any> = A["input"]["Encoded"] extends (infer E) ? true extends Types.IsUnion<E> ? never : E extends ReadonlyArray<unknown> ? never : E extends object ? string extends keyof E ? never : Extract<keyof E, string> : never : never;
|
|
7
|
+
type StdinField<A extends Any> = A["input"]["Encoded"] extends (infer E) ? Exclude<Field<A>, { [K in keyof E]-?: {} extends Pick<E, K> ? K : never; }[keyof E]> : never;
|
|
8
|
+
/** How one command of `A` is named, takes its input and prints its result. */
|
|
12
9
|
export interface Options<A extends Any> {
|
|
13
10
|
/** Override the command name. The action name, in kebab case, is used by default. */
|
|
14
11
|
readonly name?: string;
|
|
@@ -24,12 +21,19 @@ export interface Options<A extends Any> {
|
|
|
24
21
|
* `Param.withAlias` takes it.
|
|
25
22
|
*/
|
|
26
23
|
readonly aliases?: { readonly [K in Field<A>]?: string; };
|
|
24
|
+
/**
|
|
25
|
+
* A required field read from stdin instead of a flag, such as a secret, which a flag would
|
|
26
|
+
* leave in the process list and the shell's history.
|
|
27
|
+
*/
|
|
28
|
+
readonly stdin?: StdinField<A>;
|
|
27
29
|
}
|
|
28
30
|
/**
|
|
29
|
-
* What a command fails with when its action fails: Effect CLI's
|
|
30
|
-
* failure
|
|
31
|
-
*
|
|
32
|
-
* `Runtime.errorExitCode`, 1 by default.
|
|
31
|
+
* What a command fails with when its action fails: Effect CLI's `UserError`, typed with the
|
|
32
|
+
* action's failure as its `cause` and `reason`, and whose message is its JSON, which `Command.run`
|
|
33
|
+
* prints on stderr and marks reported, so `runMain` does not print it again. The process exits with
|
|
34
|
+
* the cause's `Runtime.errorExitCode`, 1 by default. A type only: after `Command.run`, match the
|
|
35
|
+
* reason by its tag, `Effect.catchReason("UserError", "UserNotFound", f)`. A command of your own
|
|
36
|
+
* fails with one through `toUserError`; any other failure is reported as a crash, `ERROR (#n)`.
|
|
33
37
|
*/
|
|
34
38
|
export declare class UserError<out E = unknown> extends CliError.UserError {
|
|
35
39
|
readonly cause: E;
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { InvalidInput } from "../contract/errors.mjs";
|
|
2
|
+
import { assertDistinct, literalValues, members, projectedErrors, unsuspended } from "../contract/rules.mjs";
|
|
3
|
+
import { withStderrConsole } from "../stdio/console.mjs";
|
|
4
|
+
import { Console, Effect, Option, Predicate, Record, Runtime, Schema, SchemaAST, Stdio, Stream, flow } from "effect";
|
|
5
|
+
import { CliError, Command, Flag, Param } from "effect/cli";
|
|
6
|
+
//#region src/cli/commands.ts
|
|
7
|
+
const kebab = (name) => {
|
|
8
|
+
const kebabbed = name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/_/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
|
|
9
|
+
return kebabbed === "" ? name : kebabbed;
|
|
10
|
+
};
|
|
11
|
+
const renderedCommandJsonFlag = Flag.Boolean("json").pipe(Flag.withDefault(false), Flag.withDescription("Print machine-readable JSON"));
|
|
12
|
+
const jsonKinds = [
|
|
13
|
+
"null",
|
|
14
|
+
"boolean",
|
|
15
|
+
"number",
|
|
16
|
+
"string",
|
|
17
|
+
"array",
|
|
18
|
+
"object"
|
|
19
|
+
];
|
|
20
|
+
const jsonKindOf = (value) => value === null ? "null" : Predicate.isBoolean(value) ? "boolean" : Predicate.isNumber(value) ? "number" : Predicate.isString(value) ? "string" : Array.isArray(value) ? "array" : "object";
|
|
21
|
+
const acceptedJsonKindsOf = (ast) => members(ast).flatMap((member) => {
|
|
22
|
+
if (SchemaAST.isLiteral(member)) return [jsonKindOf(member.literal)];
|
|
23
|
+
if (SchemaAST.isEnum(member)) return member.enums.map(([, value]) => jsonKindOf(value));
|
|
24
|
+
if (SchemaAST.isNull(member)) return ["null"];
|
|
25
|
+
if (SchemaAST.isBoolean(member)) return ["boolean"];
|
|
26
|
+
if (SchemaAST.isNumber(member)) return ["number"];
|
|
27
|
+
if (SchemaAST.isString(member) || SchemaAST.isTemplateLiteral(member)) return ["string"];
|
|
28
|
+
if (SchemaAST.isArrays(member)) return ["array"];
|
|
29
|
+
if (SchemaAST.isObjects(member)) return ["object"];
|
|
30
|
+
return jsonKinds;
|
|
31
|
+
});
|
|
32
|
+
const jsonOfAcceptedKindOrText = (encoded) => {
|
|
33
|
+
const accepted = new Set(acceptedJsonKindsOf(encoded));
|
|
34
|
+
return Schema.Union([Schema.fromJsonString(Schema.Json).check(Schema.makeFilter((json) => accepted.has(jsonKindOf(json)))), Schema.String]);
|
|
35
|
+
};
|
|
36
|
+
const stringChoicesOf = (ast) => {
|
|
37
|
+
const accepted = members(ast).flatMap(literalValues);
|
|
38
|
+
return accepted.every(Predicate.isString) ? accepted : void 0;
|
|
39
|
+
};
|
|
40
|
+
const takesTextAsIs = (encoded) => stringChoicesOf(encoded) !== void 0 || SchemaAST.isString(encoded) || SchemaAST.isTemplateLiteral(encoded);
|
|
41
|
+
const valueParam = (kind, name, field) => {
|
|
42
|
+
const encoded = unsuspended(field);
|
|
43
|
+
if (takesTextAsIs(encoded)) {
|
|
44
|
+
const literals = stringChoicesOf(encoded);
|
|
45
|
+
return literals === void 0 ? Param.String(kind, name) : Param.Literals(kind, name, literals);
|
|
46
|
+
}
|
|
47
|
+
if (SchemaAST.isBoolean(encoded)) return Param.Boolean(kind, name);
|
|
48
|
+
return Param.String(kind, name).pipe(Param.withSchema(jsonOfAcceptedKindOrText(encoded)), Param.withMetavar("value"));
|
|
49
|
+
};
|
|
50
|
+
const withoutOptionalAbsence = (ast) => {
|
|
51
|
+
const members = SchemaAST.isUnion(ast) ? ast.types.filter((member) => !SchemaAST.isNull(member) && !SchemaAST.isUndefined(member)) : [];
|
|
52
|
+
return members.length === 1 && members[0] !== void 0 ? members[0] : ast;
|
|
53
|
+
};
|
|
54
|
+
const optionalAddedNull = (plain) => {
|
|
55
|
+
const types = members(plain);
|
|
56
|
+
return types.some(SchemaAST.isUndefined) && !types.some(SchemaAST.isNull);
|
|
57
|
+
};
|
|
58
|
+
const repeatedElementOf = (ast) => {
|
|
59
|
+
const encoded = unsuspended(ast);
|
|
60
|
+
if (!SchemaAST.isArrays(encoded)) return void 0;
|
|
61
|
+
const [element, ...after] = encoded.rest;
|
|
62
|
+
if (element === void 0 || after.length > 0 || !encoded.elements.every((first) => first === element) || !acceptedJsonKindsOf(element).every((kind) => kind === "string" || kind === "number")) return;
|
|
63
|
+
return {
|
|
64
|
+
element,
|
|
65
|
+
min: encoded.elements.length
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
const noElementOccurrence = "[]";
|
|
69
|
+
const jsonHintOf = (encoded) => {
|
|
70
|
+
const value = unsuspended(encoded);
|
|
71
|
+
const kinds = new Set(acceptedJsonKindsOf(value));
|
|
72
|
+
if (kinds.size !== 1) return void 0;
|
|
73
|
+
if (kinds.has("object")) return "A JSON object.";
|
|
74
|
+
if (!kinds.has("array")) return void 0;
|
|
75
|
+
const fields = elementFieldsOf(value);
|
|
76
|
+
return fields === void 0 ? "A JSON array." : `A JSON array of {${fields.join(", ")}}.`;
|
|
77
|
+
};
|
|
78
|
+
const elementFieldsOf = (array) => {
|
|
79
|
+
if (!SchemaAST.isArrays(array)) return void 0;
|
|
80
|
+
const [element, ...others] = [...array.elements, ...array.rest];
|
|
81
|
+
if (element === void 0 || others.some((other) => other !== element)) return void 0;
|
|
82
|
+
const fields = namedFieldsOf(unsuspended(element));
|
|
83
|
+
return fields.size === 0 ? void 0 : Array.from(fields, ([name, field]) => SchemaAST.isOptional(field) ? `${String(name)}?` : String(name));
|
|
84
|
+
};
|
|
85
|
+
const withHint = (description, hint) => {
|
|
86
|
+
if (hint === void 0 || description === void 0) return description ?? hint;
|
|
87
|
+
const stated = description.trimEnd();
|
|
88
|
+
return /[.!?:]$/.test(stated) ? `${stated} ${hint}` : `${stated}. ${hint}`;
|
|
89
|
+
};
|
|
90
|
+
const fieldParam = (kind, name, encoded, plain) => {
|
|
91
|
+
const optional = SchemaAST.isOptional(encoded);
|
|
92
|
+
const value = optional && plain !== void 0 && optionalAddedNull(plain) ? withoutOptionalAbsence(encoded) : encoded;
|
|
93
|
+
const repeats = repeatedElementOf(value);
|
|
94
|
+
if (repeats !== void 0) {
|
|
95
|
+
const literals = stringChoicesOf(unsuspended(repeats.element));
|
|
96
|
+
const elements = literals === void 0 ? valueParam(kind, name, repeats.element) : Param.Literals(kind, name, [...literals, noElementOccurrence]);
|
|
97
|
+
const listed = (values) => Option.some(values.filter((value) => value !== noElementOccurrence));
|
|
98
|
+
return {
|
|
99
|
+
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)),
|
|
100
|
+
repeats: true,
|
|
101
|
+
hint: kind === Param.flagKind ? "Repeat for each value." : void 0
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (optional) return {
|
|
105
|
+
param: Param.optional(valueParam(kind, name, value)),
|
|
106
|
+
repeats: false,
|
|
107
|
+
hint: kind === Param.flagKind && SchemaAST.isBoolean(unsuspended(value)) ? `--no-${name} sends false.` : jsonHintOf(value)
|
|
108
|
+
};
|
|
109
|
+
const isSwitch = kind === Param.flagKind && SchemaAST.isBoolean(unsuspended(encoded));
|
|
110
|
+
return {
|
|
111
|
+
param: isSwitch ? Flag.Boolean(name).pipe(Flag.withDefault(false), Flag.map(Option.some)) : valueParam(kind, name, encoded).pipe(Param.map(Option.some)),
|
|
112
|
+
repeats: false,
|
|
113
|
+
hint: isSwitch ? "Left out, it sends false." : jsonHintOf(encoded)
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
const namedFieldsOf = (ast) => {
|
|
117
|
+
const fields = SchemaAST.isDeclaration(ast) ? ast.typeParameters[0] : ast;
|
|
118
|
+
return new Map(fields !== void 0 && SchemaAST.isObjects(fields) ? fields.propertySignatures.map((property) => [property.name, property.type]) : []);
|
|
119
|
+
};
|
|
120
|
+
const isTransformedAsWhole = (ast) => (SchemaAST.isDeclaration(ast) ? ast.typeParameters[0] : ast)?.encoding !== void 0;
|
|
121
|
+
const ownOrSuspendedDescriptionOf = (ast) => ast.context?.annotations?.description ?? SchemaAST.resolveDescription(ast) ?? describedBeforeLastCheck(ast) ?? (SchemaAST.isSuspend(ast) ? ownOrSuspendedDescriptionOf(ast.thunk()) : void 0);
|
|
122
|
+
const describedBeforeLastCheck = (ast) => [ast.annotations, ...(ast.checks ?? []).map((check) => check.annotations)].map((annotations) => annotations?.["description"]).findLast(Predicate.isString);
|
|
123
|
+
const describedThroughAbsence = (ast) => ownOrSuspendedDescriptionOf(ast) ?? ownOrSuspendedDescriptionOf(withoutOptionalAbsence(ast));
|
|
124
|
+
const describedByElement = (ast) => {
|
|
125
|
+
const array = unsuspended(withoutOptionalAbsence(ast));
|
|
126
|
+
const element = SchemaAST.isArrays(array) ? array.rest[0] : void 0;
|
|
127
|
+
return element === void 0 ? void 0 : describedThroughAbsence(element);
|
|
128
|
+
};
|
|
129
|
+
const fieldParams = (encoded, input, positional, aliases) => {
|
|
130
|
+
const plainEncodedFields = namedFieldsOf(SchemaAST.toEncoded(input));
|
|
131
|
+
const declaredFieldsUnlessRenamed = namedFieldsOf(isTransformedAsWhole(input) ? encoded : SchemaAST.toType(input));
|
|
132
|
+
return encoded.propertySignatures.map((property) => {
|
|
133
|
+
const field = String(property.name);
|
|
134
|
+
const declaredField = declaredFieldsUnlessRenamed.get(property.name);
|
|
135
|
+
const kind = positional.includes(field) ? Param.argumentKind : Param.flagKind;
|
|
136
|
+
const { param, repeats, hint } = fieldParam(kind, kebab(field), property.type, plainEncodedFields.get(property.name));
|
|
137
|
+
const alias = kind === Param.flagKind ? aliases[field] : void 0;
|
|
138
|
+
const aliased = alias === void 0 ? param : Param.withAlias(param, alias);
|
|
139
|
+
const documented = declaredField ?? property.type;
|
|
140
|
+
const description = describedThroughAbsence(documented) ?? describedThroughAbsence(property.type) ?? (repeats ? describedByElement(documented) ?? describedByElement(property.type) : void 0);
|
|
141
|
+
const help = withHint(description, hint);
|
|
142
|
+
return {
|
|
143
|
+
field,
|
|
144
|
+
param: help === void 0 ? aliased : Param.withDescription(aliased, help),
|
|
145
|
+
optional: SchemaAST.isOptional(property.type),
|
|
146
|
+
repeats,
|
|
147
|
+
hint,
|
|
148
|
+
description
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
const positionalOrder = (params, positional) => {
|
|
153
|
+
assertDistinct("positional argument", positional, (field) => field);
|
|
154
|
+
const ordered = positional.map((field) => {
|
|
155
|
+
const found = params.find((param) => param.field === field);
|
|
156
|
+
if (found === void 0) throw new Error(`Not an input field: ${field}`);
|
|
157
|
+
return found;
|
|
158
|
+
});
|
|
159
|
+
const firstOptional = ordered.findIndex((param) => param.optional);
|
|
160
|
+
const misplaced = ordered.slice(firstOptional === -1 ? ordered.length : firstOptional).find((param) => !param.optional);
|
|
161
|
+
if (misplaced !== void 0) throw new Error(`Required positional argument after an optional one: ${misplaced.field}`);
|
|
162
|
+
const repeatedBeforeLast = ordered.slice(0, -1).find((param) => param.repeats);
|
|
163
|
+
if (repeatedBeforeLast !== void 0) throw new Error(`Repeated positional argument before another one: ${repeatedBeforeLast.field}`);
|
|
164
|
+
return ordered;
|
|
165
|
+
};
|
|
166
|
+
const wholeInputFlag = (encoded) => Flag.String("input").pipe(Flag.withSchema(jsonOfAcceptedKindOrText(encoded)), Flag.withDescription("Whole action input as JSON; omitted, {}"), Flag.optional);
|
|
167
|
+
const printedOutput = (action, value, render) => Effect.map(Effect.orDie(Schema.encodeEffect(Schema.toCodecJson(action.success))(value)), (encoded) => render === void 0 && SchemaAST.isVoid(action.success.ast) ? Option.none() : Option.some(render === void 0 ? JSON.stringify(encoded, null, 2) : render(value)));
|
|
168
|
+
/**
|
|
169
|
+
* What a command fails with when its action fails: Effect CLI's `UserError`, typed with the
|
|
170
|
+
* action's failure as its `cause` and `reason`, and whose message is its JSON, which `Command.run`
|
|
171
|
+
* prints on stderr and marks reported, so `runMain` does not print it again. The process exits with
|
|
172
|
+
* the cause's `Runtime.errorExitCode`, 1 by default. A type only: after `Command.run`, match the
|
|
173
|
+
* reason by its tag, `Effect.catchReason("UserError", "UserNotFound", f)`. A command of your own
|
|
174
|
+
* fails with one through `toUserError`; any other failure is reported as a crash, `ERROR (#n)`.
|
|
175
|
+
*/
|
|
176
|
+
var UserError = class extends CliError.UserError {
|
|
177
|
+
/**
|
|
178
|
+
* The action's failure, as Effect names an error's nested one, so the native
|
|
179
|
+
* `Effect.catchReason("UserError", "UserNotFound", f)` and `catchReasons` match it by tag.
|
|
180
|
+
*/
|
|
181
|
+
get reason() {
|
|
182
|
+
return this.cause;
|
|
183
|
+
}
|
|
184
|
+
get [Runtime.errorExitCode]() {
|
|
185
|
+
return Runtime.getErrorExitCode(this.cause);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
const ownStringProperty = (error, key) => {
|
|
189
|
+
if (!Predicate.hasProperty(error, key)) return void 0;
|
|
190
|
+
const value = error[key];
|
|
191
|
+
return Predicate.isString(value) ? value : void 0;
|
|
192
|
+
};
|
|
193
|
+
const describedWithoutFields = (error, seen = /* @__PURE__ */ new Set()) => {
|
|
194
|
+
const name = ownStringProperty(error, "_tag") ?? (error instanceof Error ? error.name : "Error");
|
|
195
|
+
const message = Predicate.isString(error) ? error : ownStringProperty(error, "message") ?? "";
|
|
196
|
+
const named = message === "" ? name : `${name}: ${message}`;
|
|
197
|
+
seen.add(error);
|
|
198
|
+
return Predicate.hasProperty(error, "cause") && error.cause !== void 0 && !seen.has(error.cause) ? `${named}: ${describedWithoutFields(error.cause, seen)}` : named;
|
|
199
|
+
};
|
|
200
|
+
const userErrorOf = (errors) => {
|
|
201
|
+
const encode = Schema.encodeUnknownEffect(Schema.fromJsonString(Schema.toCodecJson(Schema.Union(errors))));
|
|
202
|
+
return (error) => encode(error, { onExcessProperty: "error" }).pipe(Effect.catch(() => encode(error)), Effect.orElseSucceed(() => describedWithoutFields(error)), Effect.flatMap((userMessage) => Effect.fail(new UserError({
|
|
203
|
+
cause: error,
|
|
204
|
+
userMessage
|
|
205
|
+
}))));
|
|
206
|
+
};
|
|
207
|
+
const failingAsDerived = (actions, surfaceErrorsOf) => {
|
|
208
|
+
const failure = userErrorOf([...new Set(actions.flatMap((action) => projectedErrors(action, surfaceErrorsOf(action))))]);
|
|
209
|
+
return (self) => Effect.catchIf(self, (error) => !CliError.isCliError(error), failure);
|
|
210
|
+
};
|
|
211
|
+
const unreadableStdinField = (field, message) => new InvalidInput({
|
|
212
|
+
message,
|
|
213
|
+
issues: [{
|
|
214
|
+
path: [field],
|
|
215
|
+
message
|
|
216
|
+
}]
|
|
217
|
+
});
|
|
218
|
+
const fieldValueFromText = (field) => {
|
|
219
|
+
const encoded = unsuspended(field);
|
|
220
|
+
return takesTextAsIs(encoded) ? (text) => text : Schema.decodeSync(jsonOfAcceptedKindOrText(encoded));
|
|
221
|
+
};
|
|
222
|
+
const stdinTextOf = (field) => Effect.gen(function* () {
|
|
223
|
+
const stdio = yield* Effect.serviceOption(Stdio.Stdio);
|
|
224
|
+
if (Option.isNone(stdio)) return yield* Effect.die(/* @__PURE__ */ new Error("Stdio is not provided"));
|
|
225
|
+
if (yield* stdio.value.stdinIsTerminal) return yield* unreadableStdinField(field, `${field} is read from stdin, which is a terminal: pipe it in`);
|
|
226
|
+
return (yield* stdio.value.stdin.pipe(Stream.decodeText(), Stream.mkString, Effect.mapError((error) => unreadableStdinField(field, `stdin cannot be read: ${error.message}`)))).replace(/[\r\n]+$/, "");
|
|
227
|
+
});
|
|
228
|
+
const inputConfig = (action, positional, aliases, stdin) => {
|
|
229
|
+
const codec = Schema.toCodecJson(action.input);
|
|
230
|
+
const encoded = unsuspended(SchemaAST.toEncoded(codec.ast));
|
|
231
|
+
const decodeRefusingUndeclared = Schema.decodeUnknownEffect(codec, { onExcessProperty: "error" });
|
|
232
|
+
const decode = flow(decodeRefusingUndeclared, Effect.mapError(InvalidInput.fromSchemaError));
|
|
233
|
+
if (SchemaAST.isObjects(encoded) && encoded.indexSignatures.every((signature) => SchemaAST.isNever(signature.type))) {
|
|
234
|
+
const params = fieldParams(encoded, unsuspended(action.input.ast), positional, aliases);
|
|
235
|
+
const fromStdin = Option.map(Option.fromUndefinedOr(stdin), (field) => {
|
|
236
|
+
const property = encoded.propertySignatures.find(({ name }) => name === field);
|
|
237
|
+
if (property === void 0 || SchemaAST.isOptional(property.type)) throw new Error(`Not a required input field: ${field}`);
|
|
238
|
+
if (positional.includes(field)) throw new Error(`Both positional and stdin: ${field}`);
|
|
239
|
+
const description = params.find((param) => param.field === field)?.description;
|
|
240
|
+
return {
|
|
241
|
+
field,
|
|
242
|
+
value: fieldValueFromText(property.type),
|
|
243
|
+
help: `Reads ${kebab(field)} from stdin${description === void 0 ? "." : `: ${description}`}`
|
|
244
|
+
};
|
|
245
|
+
});
|
|
246
|
+
const flagged = new Set(params.map(({ field }) => field).filter((field) => !positional.includes(field) && field !== stdin));
|
|
247
|
+
const stray = Object.keys(aliases).find((field) => !flagged.has(field));
|
|
248
|
+
if (stray !== void 0) throw new Error(`Not a flag's input field: ${stray}`);
|
|
249
|
+
return {
|
|
250
|
+
flags: Object.fromEntries(params.flatMap(({ field, param }) => flagged.has(field) ? [[field, param]] : [])),
|
|
251
|
+
positional: positionalOrder(params, positional),
|
|
252
|
+
decode: (parsed) => Option.match(fromStdin, {
|
|
253
|
+
onNone: () => decode(Record.getSomes(parsed)),
|
|
254
|
+
onSome: ({ field, value }) => Effect.flatMap(stdinTextOf(field), (text) => decode({
|
|
255
|
+
...Record.getSomes(parsed),
|
|
256
|
+
[field]: value(text)
|
|
257
|
+
}))
|
|
258
|
+
}),
|
|
259
|
+
stdinHelp: Option.getOrUndefined(Option.map(fromStdin, ({ help }) => help))
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
if (positional.length > 0) throw new Error(`Positional arguments need named input fields: ${positional.join(", ")}`);
|
|
263
|
+
if (Object.keys(aliases).length > 0) throw new Error(`Aliases need named input fields: ${Object.keys(aliases).join(", ")}`);
|
|
264
|
+
if (stdin !== void 0) throw new Error(`Stdin needs named input fields: ${stdin}`);
|
|
265
|
+
return {
|
|
266
|
+
flags: { input: wholeInputFlag(encoded) },
|
|
267
|
+
positional: [],
|
|
268
|
+
decode: (parsed) => decode(Option.getOrElse(parsed["input"] ?? Option.none(), () => ({}))),
|
|
269
|
+
stdinHelp: void 0
|
|
270
|
+
};
|
|
271
|
+
};
|
|
272
|
+
const command = (action, execute, options, errors = []) => {
|
|
273
|
+
const name = options?.name ?? kebab(action.name);
|
|
274
|
+
const render = options?.render;
|
|
275
|
+
const { flags, positional, decode, stdinHelp } = inputConfig(action, options?.positional ?? [], options?.aliases ?? {}, options?.stdin);
|
|
276
|
+
const failure = userErrorOf(projectedErrors(action, errors));
|
|
277
|
+
assertDistinct("flag", [
|
|
278
|
+
...Object.keys(flags).map((field) => [`--${kebab(field)}`, `field ${field}`]),
|
|
279
|
+
...render === void 0 ? [] : [["--json", "render's --json"]],
|
|
280
|
+
...Object.entries(options?.aliases ?? {}).flatMap(([field, alias]) => Predicate.isString(alias) ? [[`--${alias.replace(/^-+/, "")}`, `alias of field ${field}`]] : [])
|
|
281
|
+
], ([flag]) => flag, ([, claimant]) => claimant);
|
|
282
|
+
const args = positional.map(({ param }) => param);
|
|
283
|
+
const parsedFields = (input, values) => ({
|
|
284
|
+
...input,
|
|
285
|
+
...Object.fromEntries(positional.map(({ field }, index) => [field, values[index] ?? Option.none()]))
|
|
286
|
+
});
|
|
287
|
+
const run = (parsed, rendered) => decode(parsed).pipe(Effect.flatMap(execute), Effect.catch(failure), Effect.flatMap((value) => printedOutput(action, value, rendered)), withStderrConsole, Effect.flatMap(Option.match({
|
|
288
|
+
onNone: () => Effect.void,
|
|
289
|
+
onSome: Console.log
|
|
290
|
+
})));
|
|
291
|
+
const command = render === void 0 ? Command.make(name, {
|
|
292
|
+
input: flags,
|
|
293
|
+
args
|
|
294
|
+
}, ({ input, args: values }) => run(parsedFields(input, values), void 0)) : Command.make(name, {
|
|
295
|
+
input: flags,
|
|
296
|
+
args,
|
|
297
|
+
json: renderedCommandJsonFlag
|
|
298
|
+
}, ({ input, args: values, json }) => run(parsedFields(input, values), json ? void 0 : render));
|
|
299
|
+
return stdinHelp === void 0 ? Command.withDescription(command, action.description) : command.pipe(Command.withDescription(`${action.description}\n\n${stdinHelp}`), Command.withShortDescription(action.description));
|
|
300
|
+
};
|
|
301
|
+
//#endregion
|
|
302
|
+
export { UserError, command, failingAsDerived, kebab };
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import "./implementation.mjs";
|
|
2
2
|
import { Action_d_exports, Any } from "../Action.mjs";
|
|
3
3
|
import { Effect, Schema } from "effect";
|
|
4
|
-
//#region src/
|
|
4
|
+
//#region src/contract/call.d.ts
|
|
5
5
|
/**
|
|
6
6
|
* A call of `A` answering `R`. It may leave its input out when `{}` is a valid encoded input,
|
|
7
7
|
* such as for an action declared without `input` or one whose every field has a decoding
|
|
8
8
|
* default, and then sends the input `{}` decodes to. Every client shares this rule:
|
|
9
|
-
* `ActionHttp.client`, `Testing.mcpClient` and `Action.client`.
|
|
9
|
+
* `ActionHttp.client`, `ActionRpc.client`, `Testing.mcpClient` and `Action.client`.
|
|
10
10
|
*/
|
|
11
11
|
export type Call<A extends Any, R> = {} extends A["input"]["Encoded"] ? (...input: [] | [input: A["input"]["Type"]]) => R : (input: A["input"]["Type"]) => R;
|
|
12
12
|
//#endregion
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { InvalidInput } from "./errors.mjs";
|
|
2
|
+
import { Effect, Schema } from "effect";
|
|
3
|
+
//#region src/contract/call.ts
|
|
4
|
+
const inputOf = (action, args) => args.length === 0 ? Schema.decodeUnknownEffect(Schema.toCodecJson(action.input))({}) : Effect.succeed(args[0]);
|
|
5
|
+
const checked = (action, send) => {
|
|
6
|
+
const encode = Schema.encodeUnknownEffect(Schema.toCodecJson(action.input), { errors: "all" });
|
|
7
|
+
return (...args) => inputOf(action, args).pipe(Effect.tap(encode), Effect.mapError(InvalidInput.fromSchemaError), Effect.flatMap(send));
|
|
8
|
+
};
|
|
9
|
+
//#endregion
|
|
10
|
+
export { checked, inputOf };
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
|
-
//#region src/
|
|
3
|
-
/**
|
|
4
|
-
* One thing wrong with an input: where, as the keys and indexes leading to it from the input's
|
|
5
|
-
* root, none for the input itself, and what the schema expects there.
|
|
6
|
-
*/
|
|
2
|
+
//#region src/contract/errors.d.ts
|
|
7
3
|
declare const Issue: Schema.Struct<{
|
|
8
4
|
readonly path: Schema.$Array<Schema.Union<readonly [Schema.String, Schema.Finite]>>;
|
|
9
5
|
readonly message: Schema.String;
|
|
10
6
|
}>;
|
|
11
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* One thing wrong with an input, as `InvalidInput.issues` lists it: where, as the keys and
|
|
9
|
+
* indexes leading to it from the input's root, none for the input itself, and what the schema
|
|
10
|
+
* expects there.
|
|
11
|
+
*/
|
|
12
12
|
type Issue = (typeof Issue)["Type"];
|
|
13
13
|
declare const InvalidInput_base: Schema.Class<InvalidInput, Schema.TaggedStruct<"InvalidInput", {
|
|
14
14
|
readonly message: Schema.withConstructorDefault<Schema.String>;
|
|
@@ -24,7 +24,7 @@ declare const InvalidInput_base: Schema.Class<InvalidInput, Schema.TaggedStruct<
|
|
|
24
24
|
* failing with it for input that decodes but cannot be served may name its own.
|
|
25
25
|
*/
|
|
26
26
|
export declare class InvalidInput extends InvalidInput_base {
|
|
27
|
-
private readonly "~
|
|
27
|
+
private readonly "~declared in the action's error, or built in";
|
|
28
28
|
/**
|
|
29
29
|
* The `InvalidInput` for input that did not decode, as every surface answers it: the
|
|
30
30
|
* schema's message, and each of its issues by path, for code that decodes input of its own,
|
|
@@ -40,7 +40,7 @@ declare const Unauthenticated_base: Schema.Class<Unauthenticated, Schema.TaggedS
|
|
|
40
40
|
}>, import("effect/Cause").YieldableError>;
|
|
41
41
|
/** The caller is not authenticated: authentication or an implementation's `authorize` refuses. */
|
|
42
42
|
export declare class Unauthenticated extends Unauthenticated_base {
|
|
43
|
-
private readonly "~
|
|
43
|
+
private readonly "~declared in the action's error, or built in";
|
|
44
44
|
}
|
|
45
45
|
declare const Forbidden_base: Schema.Class<Forbidden, Schema.TaggedStruct<"Forbidden", {
|
|
46
46
|
readonly message: Schema.withConstructorDefault<Schema.String>;
|
|
@@ -49,10 +49,13 @@ declare const Forbidden_base: Schema.Class<Forbidden, Schema.TaggedStruct<"Forbi
|
|
|
49
49
|
/**
|
|
50
50
|
* The caller may not run this action: an implementation's `authorize` refuses. `scopes`,
|
|
51
51
|
* when given, are every OAuth scope the call needs: a refusal naming them is answered with
|
|
52
|
-
* the `insufficient_scope` challenge an OAuth client re-authorizes on
|
|
52
|
+
* the `insufficient_scope` challenge an OAuth client re-authorizes on, so name them only where
|
|
53
|
+
* re-authorizing can grant them. Over HTTP it is a 403 either way. Over MCP, from an authorizer
|
|
54
|
+
* or a handler, it is a 403 only when it names scopes on an endpoint under Bearer, and
|
|
55
|
+
* otherwise a tool result with `isError: true`, in a 200 response.
|
|
53
56
|
*/
|
|
54
57
|
export declare class Forbidden extends Forbidden_base {
|
|
55
|
-
private readonly "~
|
|
58
|
+
private readonly "~declared in the action's error, or built in";
|
|
56
59
|
}
|
|
57
60
|
/**
|
|
58
61
|
* What authentication or an implementation's `authorize` refuses a caller with, instead of
|
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
import { Effect, Predicate, Schema, SchemaIssue } from "effect";
|
|
2
|
-
//#region src/
|
|
3
|
-
|
|
4
|
-
const message = (fallback) => Schema.String.pipe(Schema.withConstructorDefault(Effect.succeed(fallback)));
|
|
5
|
-
/** Each built-in error's HTTP status, by tag: its annotation, and what a refusal answers. */
|
|
2
|
+
//#region src/contract/errors.ts
|
|
3
|
+
const defaultedMessage = (fallback) => Schema.String.pipe(Schema.withConstructorDefault(Effect.succeed(fallback)));
|
|
6
4
|
const statuses = {
|
|
7
5
|
InvalidInput: 400,
|
|
8
6
|
Unauthenticated: 401,
|
|
9
7
|
Forbidden: 403
|
|
10
8
|
};
|
|
11
|
-
/**
|
|
12
|
-
* One thing wrong with an input: where, as the keys and indexes leading to it from the input's
|
|
13
|
-
* root, none for the input itself, and what the schema expects there.
|
|
14
|
-
*/
|
|
15
9
|
const Issue = Schema.Struct({
|
|
16
10
|
path: Schema.Array(Schema.Union([Schema.String, Schema.Finite])),
|
|
17
11
|
message: Schema.String
|
|
@@ -24,7 +18,7 @@ const standardIssues = SchemaIssue.makeFormatterStandardSchemaV1();
|
|
|
24
18
|
* failing with it for input that decodes but cannot be served may name its own.
|
|
25
19
|
*/
|
|
26
20
|
var InvalidInput = class InvalidInput extends Schema.TaggedError()("InvalidInput", {
|
|
27
|
-
message:
|
|
21
|
+
message: defaultedMessage("The input does not match the action's input."),
|
|
28
22
|
issues: Schema.Array(Issue).pipe(Schema.withConstructorDefault(Effect.succeed([])))
|
|
29
23
|
}, { httpApiStatus: statuses.InvalidInput }) {
|
|
30
24
|
/**
|
|
@@ -47,20 +41,21 @@ var InvalidInput = class InvalidInput extends Schema.TaggedError()("InvalidInput
|
|
|
47
41
|
});
|
|
48
42
|
};
|
|
49
43
|
/** The caller is not authenticated: authentication or an implementation's `authorize` refuses. */
|
|
50
|
-
var Unauthenticated = class extends Schema.TaggedError()("Unauthenticated", { message:
|
|
51
|
-
/** An OAuth scope token (RFC 6749 §3.3): printable ASCII but space, `"` and `\`. */
|
|
44
|
+
var Unauthenticated = class extends Schema.TaggedError()("Unauthenticated", { message: defaultedMessage("Authentication is required.") }, { httpApiStatus: statuses.Unauthenticated }) {};
|
|
52
45
|
const scopeToken = /^[\x21\x23-\x5B\x5D-\x7E]+$/;
|
|
53
46
|
const ScopeToken = Schema.String.check(Schema.isPattern(scopeToken));
|
|
54
47
|
/**
|
|
55
48
|
* The caller may not run this action: an implementation's `authorize` refuses. `scopes`,
|
|
56
49
|
* when given, are every OAuth scope the call needs: a refusal naming them is answered with
|
|
57
|
-
* the `insufficient_scope` challenge an OAuth client re-authorizes on
|
|
50
|
+
* the `insufficient_scope` challenge an OAuth client re-authorizes on, so name them only where
|
|
51
|
+
* re-authorizing can grant them. Over HTTP it is a 403 either way. Over MCP, from an authorizer
|
|
52
|
+
* or a handler, it is a 403 only when it names scopes on an endpoint under Bearer, and
|
|
53
|
+
* otherwise a tool result with `isError: true`, in a 200 response.
|
|
58
54
|
*/
|
|
59
55
|
var Forbidden = class extends Schema.TaggedError()("Forbidden", {
|
|
60
|
-
message:
|
|
56
|
+
message: defaultedMessage("Not allowed."),
|
|
61
57
|
scopes: Schema.optionalKey(Schema.NonEmptyArray(ScopeToken))
|
|
62
58
|
}, { httpApiStatus: statuses.Forbidden }) {};
|
|
63
|
-
/** The refusals, as schemas. */
|
|
64
59
|
const refusals = [Unauthenticated, Forbidden];
|
|
65
60
|
/**
|
|
66
61
|
* What authentication or an implementation's `authorize` refuses a caller with, instead of
|