@gjermundgaraba/effect-actions 0.18.2 → 0.20.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 +20 -47
- package/dist/Action.d.mts +96 -49
- package/dist/Action.mjs +12 -16
- package/dist/ActionCli.d.mts +82 -44
- package/dist/ActionCli.mjs +54 -35
- package/dist/ActionHttp.d.mts +84 -45
- package/dist/ActionHttp.mjs +3 -4
- package/dist/ActionMcp.d.mts +54 -29
- package/dist/ActionMcp.mjs +33 -15
- package/dist/ActionRpc.d.mts +48 -37
- package/dist/ActionRpc.mjs +3 -4
- package/dist/ActionToolkit.d.mts +32 -15
- package/dist/ActionToolkit.mjs +11 -9
- package/dist/Authentication.d.mts +92 -42
- package/dist/Authentication.mjs +38 -37
- package/dist/Testing.d.mts +23 -27
- package/dist/Testing.mjs +1 -2
- package/dist/authentication/provider.d.mts +1 -6
- package/dist/authentication/provider.mjs +2 -3
- package/dist/authentication/refusal.mjs +3 -3
- package/dist/cli/commands.d.mts +34 -13
- package/dist/cli/commands.mjs +48 -24
- package/dist/contract/ast.mjs +1 -2
- package/dist/contract/call.d.mts +1 -3
- package/dist/contract/call.mjs +1 -2
- package/dist/contract/caller.d.mts +1 -3
- package/dist/contract/caller.mjs +2 -3
- package/dist/contract/declared.d.mts +0 -2
- package/dist/contract/declared.mjs +28 -16
- package/dist/contract/errors.d.mts +42 -19
- package/dist/contract/errors.mjs +43 -19
- package/dist/contract/hints.mjs +4 -4
- package/dist/contract/implementation.d.mts +7 -14
- package/dist/contract/implementation.mjs +31 -19
- package/dist/contract/media.d.mts +7 -5
- package/dist/contract/media.mjs +7 -4
- package/dist/contract/names.mjs +15 -14
- package/dist/contract/status.mjs +4 -12
- package/dist/http/client.d.mts +15 -6
- package/dist/http/client.mjs +2 -3
- package/dist/http/schema-errors.mjs +1 -2
- package/dist/mcp/client.d.mts +21 -10
- package/dist/mcp/client.mjs +8 -11
- package/dist/mcp/protocol.mjs +1 -2
- package/dist/stdio/console.mjs +1 -2
- package/dist/testing/memory.d.mts +1 -3
- package/dist/testing/memory.mjs +1 -2
- package/docs/Action-failures.md +6 -15
- package/docs/Action.md +159 -191
- package/docs/ActionCli-failures.md +12 -92
- package/docs/ActionCli.md +102 -156
- package/docs/ActionHttp-failures.md +14 -76
- package/docs/ActionHttp.md +150 -176
- package/docs/ActionMcp-failures.md +2 -22
- package/docs/ActionMcp.md +79 -170
- package/docs/ActionRpc-failures.md +3 -12
- package/docs/ActionRpc.md +183 -120
- package/docs/ActionToolkit-failures.md +3 -10
- package/docs/ActionToolkit.md +112 -109
- package/docs/Authentication-advanced.md +171 -108
- package/docs/Authentication-failures.md +11 -91
- package/docs/Authentication.md +114 -135
- package/docs/README.md +45 -30
- package/docs/Testing-failures.md +2 -6
- package/docs/Testing.md +126 -159
- package/docs/setup-failures.md +8 -32
- package/docs/setup.md +42 -38
- package/examples/README.md +6 -10
- package/examples/assistant/README.md +20 -38
- package/examples/assistant/src/agent.ts +2 -3
- package/examples/assistant/src/cli.ts +4 -9
- package/examples/assistant/src/commands.ts +6 -4
- package/examples/assistant/src/identity/authorize.ts +3 -3
- package/examples/assistant/src/tasks/Tasks.ts +1 -2
- package/examples/assistant/src/tasks/handlers.ts +3 -3
- package/examples/assistant/src/toolkit.ts +6 -8
- package/examples/assistant/test/commands.test.ts +16 -11
- package/examples/assistant/test/tasks/tasks.test.ts +0 -2
- package/examples/assistant/test/toolkit.test.ts +0 -1
- package/examples/kitchen-sink/README.md +30 -52
- package/examples/kitchen-sink/src/Actions.ts +2 -3
- package/examples/kitchen-sink/src/Api.ts +3 -4
- package/examples/kitchen-sink/src/admin/Admin.ts +1 -1
- package/examples/kitchen-sink/src/admin/authenticate.ts +3 -3
- package/examples/kitchen-sink/src/admin/authorize.ts +1 -2
- package/examples/kitchen-sink/src/agent.ts +1 -1
- package/examples/kitchen-sink/src/assistant/handlers.ts +1 -6
- package/examples/kitchen-sink/src/attachments/Attachments.ts +4 -8
- package/examples/kitchen-sink/src/attachments/download.ts +5 -8
- package/examples/kitchen-sink/src/attachments/handlers.ts +2 -3
- package/examples/kitchen-sink/src/browser.ts +3 -4
- package/examples/kitchen-sink/src/cli.ts +6 -7
- package/examples/kitchen-sink/src/config.ts +2 -0
- package/examples/kitchen-sink/src/identity/Identity.ts +7 -7
- package/examples/kitchen-sink/src/identity/authenticate.ts +3 -5
- package/examples/kitchen-sink/src/identity/authorize.ts +4 -3
- package/examples/kitchen-sink/src/identity/quota.ts +1 -1
- package/examples/kitchen-sink/src/issues/Issues.ts +2 -2
- package/examples/kitchen-sink/src/issues/assistant.ts +3 -3
- package/examples/kitchen-sink/src/issues/guide.ts +2 -3
- package/examples/kitchen-sink/src/ops.ts +3 -6
- package/examples/kitchen-sink/src/request-policy.ts +8 -4
- package/examples/kitchen-sink/src/routes.ts +4 -6
- package/examples/kitchen-sink/src/seed.ts +0 -1
- package/examples/kitchen-sink/src/server.ts +11 -12
- package/examples/kitchen-sink/src/stdio.ts +3 -2
- package/examples/kitchen-sink/src/workspaces/tenant.ts +3 -1
- package/examples/kitchen-sink/test/admin/admin.test.ts +0 -1
- package/examples/kitchen-sink/test/assistant/assistant.test.ts +1 -1
- package/examples/kitchen-sink/test/attachments/attachments.test.ts +1 -2
- package/examples/kitchen-sink/test/identity/identity.test.ts +0 -1
- package/examples/kitchen-sink/test/issues/issues.test.ts +0 -2
- package/examples/kitchen-sink/test/request-policy.test.ts +39 -3
- package/examples/kitchen-sink/test/routes.test.ts +3 -3
- package/examples/live-rpc/README.md +8 -49
- package/examples/live-rpc/src/browser.ts +8 -41
- package/examples/live-rpc/src/chat/Chat.ts +9 -3
- package/examples/live-rpc/src/chat/room.ts +1 -1
- package/examples/live-rpc/src/client.ts +2 -3
- package/examples/live-rpc/src/page.ts +34 -0
- package/examples/live-rpc/src/routes.ts +6 -7
- package/examples/live-rpc/src/server.ts +6 -4
- package/examples/live-rpc/test/browser.test.ts +6 -5
- package/examples/live-rpc/test/routes.test.ts +16 -15
- package/examples/live-rpc/test/server.test.ts +1 -1
- package/examples/mcp-tools/README.md +22 -45
- package/examples/mcp-tools/src/printers/Printers.ts +3 -7
- package/examples/mcp-tools/src/routes.ts +0 -7
- package/examples/mcp-tools/src/server.ts +1 -1
- package/examples/mcp-tools/test/printers/printers.test.ts +2 -3
- package/examples/mcp-tools/test/routes.test.ts +2 -3
- package/examples/notes-service/README.md +9 -43
- package/examples/notes-service/src/cli.ts +0 -3
- package/examples/notes-service/src/commands.ts +7 -17
- package/examples/notes-service/src/identity/authorize.ts +2 -5
- package/examples/notes-service/src/identity/verifier.ts +3 -4
- package/examples/notes-service/src/notes/Notes.ts +2 -6
- package/examples/notes-service/src/notes/store.ts +5 -1
- package/examples/notes-service/src/routes.ts +6 -8
- package/examples/notes-service/src/server.ts +4 -3
- package/examples/notes-service/src/stdio.ts +4 -3
- package/examples/notes-service/test/commands.test.ts +1 -1
- package/examples/notes-service/test/notes/as.ts +6 -0
- package/examples/notes-service/test/notes/idempotency.test.ts +1 -4
- package/examples/notes-service/test/notes/in-process.test.ts +50 -0
- package/examples/notes-service/test/notes/notes.test.ts +3 -42
- package/examples/notes-service/test/routes.test.ts +2 -2
- package/examples/quickstart/README.md +1 -1
- package/examples/quickstart/src/client.ts +1 -0
- package/examples/quickstart/src/server.ts +2 -4
- package/examples/quickstart/test/{greeting/greeting.test.ts → routes.test.ts} +3 -3
- package/package.json +12 -16
- package/docs/Action-in-process.md +0 -93
- package/docs/CONTEXT.md +0 -46
- package/docs/guarantees.md +0 -44
|
@@ -6,23 +6,28 @@ import { ErrorsOf } from "./contract/declared.mjs";
|
|
|
6
6
|
import { Array, Context, Effect, Layer, Option, Redacted, Scope } from "effect";
|
|
7
7
|
import { HttpRouter, HttpServerResponse } from "effect/http";
|
|
8
8
|
import { HttpApiSecurity } from "effect/http-api";
|
|
9
|
-
//#region src/authentication/Authentication.d.ts
|
|
10
9
|
/**
|
|
11
10
|
* The bearer token of an `Authorization` header, `authorization`, or none, read as Effect's
|
|
12
|
-
* `HttpApiSecurity.bearer` reads it for routes and MCP
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
* `HttpApiSecurity.bearer` reads it for routes and MCP: the scheme matched case-insensitively,
|
|
12
|
+
* as RFC 9110 requires, and the token `Redacted`. It is for a caller the router never routes,
|
|
13
|
+
* which holds the header and no request; a route of the host's own is authenticated by
|
|
14
|
+
* `protect`.
|
|
16
15
|
*/
|
|
17
16
|
export declare const bearerTokenOf: (authorization: string | null | undefined) => Option.Option<Redacted.Redacted<string>>;
|
|
18
17
|
/** An OAuth protected resource (RFC 9728), as `layer` publishes it. */
|
|
19
18
|
export interface ProtectedResource {
|
|
20
19
|
/**
|
|
21
|
-
* Exact OAuth resource identifier
|
|
22
|
-
*
|
|
20
|
+
* Exact OAuth resource identifier: the public URL of the endpoint clients connect to, such as
|
|
21
|
+
* `${url}/mcp`. Discovery is served at `/.well-known/oauth-protected-resource` followed by its
|
|
22
|
+
* path and query.
|
|
23
|
+
*
|
|
24
|
+
* **Gotchas**
|
|
25
|
+
*
|
|
26
|
+
* - Discovery answers 404, and an MCP client finds no authorization server, when a request's path and query differ from this URL's, or when the provider is provided to no served layer.
|
|
27
|
+
* - That it and `authorizationServers` are HTTPS, or loopback HTTP in development, is the deployment's to ensure: nothing checks it.
|
|
23
28
|
*/
|
|
24
29
|
readonly resource: string;
|
|
25
|
-
/** Where clients get tokens: nonempty. */
|
|
30
|
+
/** Where clients get tokens, your authorization servers' issuers: nonempty. */
|
|
26
31
|
readonly authorizationServers: Array.NonEmptyReadonlyArray<string>;
|
|
27
32
|
/**
|
|
28
33
|
* Every scope the resource accepts, each an OAuth scope token, which a client requests when a
|
|
@@ -31,21 +36,29 @@ export interface ProtectedResource {
|
|
|
31
36
|
readonly scopesSupported?: ReadonlyArray<string>;
|
|
32
37
|
/**
|
|
33
38
|
* The scopes every 401 names, each an OAuth scope token: what a client requests when it
|
|
34
|
-
* authenticates, rather than every scope supported.
|
|
35
|
-
*
|
|
39
|
+
* authenticates, rather than every scope supported.
|
|
40
|
+
*
|
|
41
|
+
* **Gotchas**
|
|
42
|
+
*
|
|
43
|
+
* - Give it whenever some actions need scopes others do not, or a user who only reads is asked to consent to writes on first login.
|
|
36
44
|
*/
|
|
37
45
|
readonly scopesRequired?: Array.NonEmptyReadonlyArray<string>;
|
|
46
|
+
/** A human-readable name of the resource, published as `resource_name`. */
|
|
38
47
|
readonly resourceName?: string;
|
|
39
48
|
}
|
|
40
49
|
/** What `refusalResponse` answers about. */
|
|
41
50
|
export interface RefusalResponseOptions<D extends Any = Any> {
|
|
42
51
|
/**
|
|
43
|
-
* The descriptor refusing, whose scheme decides the challenge: Bearer's
|
|
44
|
-
* out
|
|
45
|
-
*
|
|
52
|
+
* The descriptor refusing, whose scheme decides the challenge as on its routes: Bearer's when
|
|
53
|
+
* left out, as `protectedResource` and `authorization` shape it; what another scheme gives up
|
|
54
|
+
* is on `Options.security`. An error it declares is sent as its endpoints declare it.
|
|
46
55
|
*/
|
|
47
56
|
readonly authentication?: D | undefined;
|
|
48
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* The OAuth protected resource refusing, the one given to `layer`: every challenge then names
|
|
59
|
+
* its metadata URL, and a 401's its `scopesRequired`. Where `layer` builds the resource from
|
|
60
|
+
* configuration, build this one from the same configuration.
|
|
61
|
+
*/
|
|
49
62
|
readonly protectedResource?: ProtectedResource | undefined;
|
|
50
63
|
/**
|
|
51
64
|
* The request's `Authorization` header, as the web `Headers.get` gives it, which decides whether a
|
|
@@ -57,24 +70,34 @@ type DeclaredOf<D> = D extends {
|
|
|
57
70
|
readonly error: infer E extends Errors;
|
|
58
71
|
} ? E[number]["Type"] : never;
|
|
59
72
|
/**
|
|
60
|
-
* The response `layer` answers a refusal with, for a caller the router never routes, such as
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* and status alone, a 401 naming that scheme, or none for an API key. An error
|
|
67
|
-
* `authentication` declares is sent as its endpoints declare it, its JSON with its status.
|
|
68
|
-
* `HttpServerResponse.toWeb` gives it as a web `Response`.
|
|
73
|
+
* The response `layer` answers a refusal with, for a caller the router never routes, such as a
|
|
74
|
+
* Node `upgrade` handler admitting a socket, which holds a header and no request: read its token
|
|
75
|
+
* with `bearerTokenOf`, verify it with the routes' verifier, and answer a failure with this. It
|
|
76
|
+
* holds the refusal's status and JSON, `Cache-Control: no-store` and the challenge `options`
|
|
77
|
+
* decide: under Bearer an `Unauthenticated`'s, or the `insufficient_scope` of a `Forbidden`
|
|
78
|
+
* naming scopes. `HttpServerResponse.toWeb` gives it as a web `Response`.
|
|
69
79
|
*/
|
|
70
80
|
export declare const refusalResponse: <D extends Any = never>(error: Refusal | DeclaredOf<D>, options?: RefusalResponseOptions<D>) => HttpServerResponse.HttpServerResponse;
|
|
71
81
|
/** What `make` takes besides the name and the identity. */
|
|
72
82
|
export interface Options {
|
|
73
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* The one native scheme a caller proves the identity by, Bearer when left out: an API key in a
|
|
85
|
+
* cookie, a header or the query, such as `HttpApiSecurity.apiKey({ in: "cookie", key: "session" })`,
|
|
86
|
+
* or `HttpApiSecurity.basic`. Only Bearer speaks OAuth: discovery, challenges naming scopes, and
|
|
87
|
+
* step-up.
|
|
88
|
+
*
|
|
89
|
+
* **Gotchas**
|
|
90
|
+
*
|
|
91
|
+
* - One scheme per descriptor, with no fallback: a host accepting two credentials for one identity declares two descriptors and serves them on two bindings or endpoints.
|
|
92
|
+
*/
|
|
74
93
|
readonly security?: Security;
|
|
75
94
|
/**
|
|
76
95
|
* What the verifier may fail with besides a refusal, such as its provider being unreachable:
|
|
77
96
|
* one schema, or a list. Every protected endpoint declares it, so clients decode it.
|
|
97
|
+
*
|
|
98
|
+
* **Gotchas**
|
|
99
|
+
*
|
|
100
|
+
* - A failure that is not the caller's, such as an issuer the verifier cannot reach, belongs here with `{ httpApiStatus: 503 }`: an `Unauthenticated` would have the caller sign in again.
|
|
78
101
|
*/
|
|
79
102
|
readonly error?: Errors | Errors[number];
|
|
80
103
|
}
|
|
@@ -83,20 +106,38 @@ type SecurityOf<O> = O extends {
|
|
|
83
106
|
} ? S : "security" extends keyof O ? NonNullable<O["security" & keyof O]> | HttpApiSecurity.Http : HttpApiSecurity.Http;
|
|
84
107
|
/**
|
|
85
108
|
* Browser-safe declaration shared by clients and remote surfaces: the `identity` it
|
|
86
|
-
* authenticates,
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* holds only letters, digits, `_`, `.` and `-`; reuse one name only for one declaration.
|
|
109
|
+
* authenticates, and the scheme a caller proves it by, `Options.security`. The literal name
|
|
110
|
+
* identifies the provider, like a native Context.Key name, and the scheme's OpenAPI key; reuse
|
|
111
|
+
* one name only for one declaration, whose values a server and a client may hold as `layer` says.
|
|
90
112
|
* Guide: `docs/Authentication.md` in this package; in an application: `examples/notes-service/src/identity/`.
|
|
91
113
|
*/
|
|
92
114
|
export declare const make: <const Name extends string, I, A, const O extends Options = {}>(name: Name, identity: Context.Key<I, A>, ...options: OptionalUnless<O, O & NoInfer<Known<O, Options>>>) => Descriptor<I, A, SecurityOf<O>, Name, ErrorsOf<O>>;
|
|
93
115
|
/**
|
|
94
116
|
* A descriptor's verifier: the credential its scheme decodes, as Effect's own decoder gives
|
|
95
117
|
* it, to the identity, or a refusal, or an error the descriptor declares, `E`. Its services
|
|
96
|
-
* belong to the request
|
|
118
|
+
* belong to the request; one it needs at startup, such as a user directory, is yielded by an
|
|
119
|
+
* Effect that builds the verifier, given to `layer` in its place.
|
|
120
|
+
*
|
|
121
|
+
* **Gotchas**
|
|
122
|
+
*
|
|
123
|
+
* - The library reads no token: its signature, expiry and audience are the verifier's to check, with your authorization server's library; fail a token issued for another resource with `Unauthenticated`.
|
|
124
|
+
* - Only a missing or empty credential is refused before it runs; Basic's reaches it when its username or its password is non-empty, so it may receive an empty username.
|
|
97
125
|
*/
|
|
98
126
|
export type Verify<A, S extends Security, R, E = never> = (credential: HttpApiSecurity.HttpApiSecurity.Type<S>) => Effect.Effect<A, Refusal | E, R>;
|
|
127
|
+
/** What `layer` takes besides the descriptor and the verifier. */
|
|
99
128
|
export interface LayerOptions<EP = never, RP = never> {
|
|
129
|
+
/**
|
|
130
|
+
* Bearer only: the OAuth protected resource (RFC 9728) to publish, or an Effect building it
|
|
131
|
+
* when the provider is built; left out, or built as `undefined`, nothing is published. The
|
|
132
|
+
* provider then serves its discovery and names it in every challenge, so an MCP client given a
|
|
133
|
+
* 401 finds the authorization server; without it the challenge names nowhere to sign in.
|
|
134
|
+
*
|
|
135
|
+
* **Gotchas**
|
|
136
|
+
*
|
|
137
|
+
* - Under another scheme it is a type error, or, for an `Http` scheme the types cannot tell from Bearer, a throw at `layer`.
|
|
138
|
+
* - A literal URL is right in one deployment only: in every other, the published `resource` and each challenge's metadata URL name another server, where an MCP client then looks for its authorization server; give an Effect reading configuration.
|
|
139
|
+
* - Discovery is public: it answers before routing, to any origin, so no authentication and no route middleware covers it; global middleware covers it only when provided after the provider, as `examples/kitchen-sink/src/routes.ts` shows.
|
|
140
|
+
*/
|
|
100
141
|
readonly protectedResource?: ProtectedResource | Effect.Effect<ProtectedResource | undefined, EP, RP>;
|
|
101
142
|
}
|
|
102
143
|
type ResourceOf<S> = [S] extends [HttpApiSecurity.Http] ? unknown : {
|
|
@@ -105,11 +146,17 @@ type ResourceOf<S> = [S] extends [HttpApiSecurity.Http] ? unknown : {
|
|
|
105
146
|
type Resource = ProtectedResource | Effect.Effect<ProtectedResource | undefined, unknown, unknown> | undefined;
|
|
106
147
|
/**
|
|
107
148
|
* The provider of `authentication`: `verify`, or an Effect building it once per layer graph, run on
|
|
108
|
-
* each remote request a protected action receives,
|
|
109
|
-
*
|
|
110
|
-
* OAuth protected resource `protectedResource`, on the
|
|
149
|
+
* each remote request a protected action receives, and on every request presenting a credential
|
|
150
|
+
* to an MCP endpoint that serves one, with the credential the descriptor's scheme decodes. A
|
|
151
|
+
* Bearer scheme may publish the OAuth protected resource `protectedResource`, on the
|
|
152
|
+
* `HttpRouter` it then requires.
|
|
153
|
+
*
|
|
154
|
+
* **Gotchas**
|
|
155
|
+
*
|
|
156
|
+
* - Call it once per descriptor, and provide that value to every layer serving a protected action: nothing else satisfies the provider they require, neither the identity nor another descriptor's provider, which type-checks under the same name and identity, then dies when the layer builds; only a client may hold another descriptor of the name, such as one of a cookie name the server decides at startup.
|
|
111
157
|
*/
|
|
112
158
|
export declare function layer<const Name extends string, I, A, const S extends Security, E extends Errors, R, EX = never, RX = never, P extends Resource = undefined>(authentication: Descriptor<I, A, S, Name, E>, verify: Verify<NoInfer<A>, NoInfer<S>, R, NoInfer<E[number]["Type"]>> | Effect.Effect<Verify<NoInfer<A>, NoInfer<S>, R, NoInfer<E[number]["Type"]>>, EX, RX>, options?: {
|
|
159
|
+
/** The OAuth protected resource to publish: its rules are on `LayerOptions.protectedResource`. */
|
|
113
160
|
readonly protectedResource?: P;
|
|
114
161
|
} & ResourceOf<S>): Layer.Layer<Provider<I, Name>, EX | Effect.Error<P>, ([P] extends [undefined] ? never : HttpRouter.HttpRouter) | Exclude<RX | Effect.Services<P>, Scope.Scope> | HttpRouter.Request.From<"Requires", Exclude<R, HttpRouter.Provided>>>;
|
|
115
162
|
type Provides<I> = unknown extends I ? never : I;
|
|
@@ -127,16 +174,19 @@ export type Protection<I, Name extends string> = HttpRouter.Middleware<{
|
|
|
127
174
|
layerRequires: Provider<I, Name>;
|
|
128
175
|
}>;
|
|
129
176
|
/**
|
|
130
|
-
* Native router middleware authenticating a route of the host's own, such as an export, a
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
177
|
+
* Native router middleware authenticating a route of the host's own, such as an export, a page
|
|
178
|
+
* frame or a WebSocket upgrade, as its actions' routes are: the credential its scheme decodes,
|
|
179
|
+
* verified by `authentication`'s provider, which its layer requires, and the identity provided
|
|
180
|
+
* to the route. A missing or invalid credential, and a refusal the route fails with, such as a
|
|
181
|
+
* scope check's `Forbidden`, are answered as on an action route: status, JSON and challenge,
|
|
182
|
+
* stepping up under Bearer.
|
|
183
|
+
*
|
|
184
|
+
* **Gotchas**
|
|
185
|
+
*
|
|
186
|
+
* - No authorizer runs for its routes: each states its own authorization, such as by running the `authorize` of the action it mirrors.
|
|
187
|
+
* - Every response to a request it covers is `Cache-Control: no-store` unless the route states its own caching; a route that does says `private` unless its answer is the same for every caller, or a cache in front of the server may serve it to anyone.
|
|
188
|
+
* - A failure that enclosing middleware serializes is `no-store` whatever the route set: only a response the route itself returns keeps its caching.
|
|
189
|
+
* - An erased descriptor, `Any`, provides no service the types can name: its identity is `unknown`, which would discharge every request service the route owes.
|
|
139
190
|
*/
|
|
140
191
|
export declare const protect: <I, A, S extends Security, Name extends string, E extends Errors>(authentication: Descriptor<I, A, S, Name, E>) => Protection<I, Name>;
|
|
141
|
-
//#endregion
|
|
142
192
|
export type { Any, Descriptor, Provider };
|
package/dist/Authentication.mjs
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import { Unauthenticated, scopeToken } from "./contract/errors.mjs";
|
|
2
|
-
import {
|
|
1
|
+
import { Unauthenticated, builtIns, scopeToken } from "./contract/errors.mjs";
|
|
2
|
+
import { assertDistinctTags, errorList } from "./contract/declared.mjs";
|
|
3
3
|
import { providerOf } from "./authentication/provider.mjs";
|
|
4
4
|
import { answer, answerStepUp, bearer, challenge, declaredResponse, isRefusal, isStepUp, plain } from "./authentication/refusal.mjs";
|
|
5
5
|
import { Context, Effect, Layer, Match, Option, Predicate, Redacted } from "effect";
|
|
6
6
|
import { HttpEffect, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
|
|
7
7
|
import { HttpApiBuilder, HttpApiMiddleware, HttpApiSecurity } from "effect/http-api";
|
|
8
|
-
|
|
8
|
+
// src/authentication/Authentication.ts
|
|
9
9
|
const bearerAuthorizationHeader = /^[ \t]*Bearer +(.*[^ \t])[ \t]*$/i;
|
|
10
10
|
/**
|
|
11
11
|
* The bearer token of an `Authorization` header, `authorization`, or none, read as Effect's
|
|
12
|
-
* `HttpApiSecurity.bearer` reads it for routes and MCP
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* `HttpApiSecurity.bearer` reads it for routes and MCP: the scheme matched case-insensitively,
|
|
13
|
+
* as RFC 9110 requires, and the token `Redacted`. It is for a caller the router never routes,
|
|
14
|
+
* which holds the header and no request; a route of the host's own is authenticated by
|
|
15
|
+
* `protect`.
|
|
16
16
|
*/
|
|
17
17
|
const bearerTokenOf = (authorization) => Option.map(Option.fromNullishOr(bearerAuthorizationHeader.exec(authorization ?? "")?.[1]), (token) => Redacted.make(token));
|
|
18
18
|
const metadataUrlOf = (options) => {
|
|
@@ -33,28 +33,27 @@ const bearerChallengeOf = (named, presented) => bearer([
|
|
|
33
33
|
]);
|
|
34
34
|
const assertPublishableResource = (oauth, options) => {
|
|
35
35
|
if (options === void 0) return;
|
|
36
|
-
if (!oauth) throw new Error("A protected resource is published only for a Bearer scheme");
|
|
36
|
+
if (!oauth) throw new Error("A protected resource is published only for a Bearer scheme: drop `protectedResource`, or authenticate with a Bearer descriptor");
|
|
37
37
|
if (Effect.isEffect(options)) return;
|
|
38
38
|
for (const key of ["scopesSupported", "scopesRequired"]) {
|
|
39
39
|
const invalid = options[key]?.find((scope) => !scopeToken.test(scope));
|
|
40
|
-
if (invalid !== void 0) throw new Error(`Invalid scope in ${key}: "${invalid}"`);
|
|
40
|
+
if (invalid !== void 0) throw new Error(`Invalid scope in ${key}: "${invalid}", not an OAuth scope token: give each scope as its own element, of printable ASCII without a space, '"' or '\\'`);
|
|
41
41
|
}
|
|
42
|
-
|
|
42
|
+
const resource = URL.parse(options.resource);
|
|
43
|
+
if (resource === null) throw new Error(`A protected resource is an absolute URL: ${JSON.stringify(options.resource)} is not one; give the public URL of the endpoint clients connect to, such as https://api.example.com/mcp, read from configuration`);
|
|
44
|
+
if (resource.href.includes("#")) throw new Error(`A protected resource has no fragment: ${options.resource}; drop the "#", which no request URL carries, so its discovery would never answer`);
|
|
43
45
|
};
|
|
44
46
|
const withNoStoreAndChallenge = (response, challenge) => HttpServerResponse.setHeaders(response, {
|
|
45
47
|
...response.headers["cache-control"] === void 0 ? { "cache-control": "no-store" } : {},
|
|
46
48
|
...challenge !== void 0 && response.status === 401 && response.headers["www-authenticate"] === void 0 ? { "www-authenticate": challenge } : {}
|
|
47
49
|
});
|
|
48
50
|
/**
|
|
49
|
-
* The response `layer` answers a refusal with, for a caller the router never routes, such as
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* and status alone, a 401 naming that scheme, or none for an API key. An error
|
|
56
|
-
* `authentication` declares is sent as its endpoints declare it, its JSON with its status.
|
|
57
|
-
* `HttpServerResponse.toWeb` gives it as a web `Response`.
|
|
51
|
+
* The response `layer` answers a refusal with, for a caller the router never routes, such as a
|
|
52
|
+
* Node `upgrade` handler admitting a socket, which holds a header and no request: read its token
|
|
53
|
+
* with `bearerTokenOf`, verify it with the routes' verifier, and answer a failure with this. It
|
|
54
|
+
* holds the refusal's status and JSON, `Cache-Control: no-store` and the challenge `options`
|
|
55
|
+
* decide: under Bearer an `Unauthenticated`'s, or the `insufficient_scope` of a `Forbidden`
|
|
56
|
+
* naming scopes. `HttpServerResponse.toWeb` gives it as a web `Response`.
|
|
58
57
|
*/
|
|
59
58
|
const refusalResponse = (error, options) => {
|
|
60
59
|
const authentication = options?.authentication;
|
|
@@ -64,7 +63,7 @@ const refusalResponse = (error, options) => {
|
|
|
64
63
|
const challenge = authentication !== void 0 && !oauth ? nonBearerChallengeOf(authentication.security, authentication.name) : bearerChallengeOf(named, presentsBearerToken(options?.authorization));
|
|
65
64
|
if (!isRefusal(error)) {
|
|
66
65
|
const response = declaredResponse(authentication?.error ?? [], error);
|
|
67
|
-
if (response === void 0) throw new Error("Not a refusal, nor an error the authentication declares");
|
|
66
|
+
if (response === void 0) throw new Error("Not a refusal, nor an error the authentication declares: pass the descriptor declaring it as `authentication`, or answer the error yourself");
|
|
68
67
|
return withNoStoreAndChallenge(response, challenge);
|
|
69
68
|
}
|
|
70
69
|
return withNoStoreAndChallenge(oauth ? answer(error, named.metadataUrl) : plain(error), challenge);
|
|
@@ -115,18 +114,17 @@ const openApiComponentKey = /^[\w.-]+$/;
|
|
|
115
114
|
const nonBearerChallengeOf = (security, name) => Match.value(security).pipe(Match.tag("Http", ({ scheme }) => challenge(scheme, [])), Match.tag("Basic", () => challenge("Basic", [["realm", name]])), Match.tag("ApiKey", () => void 0), Match.exhaustive);
|
|
116
115
|
/**
|
|
117
116
|
* Browser-safe declaration shared by clients and remote surfaces: the `identity` it
|
|
118
|
-
* authenticates,
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* holds only letters, digits, `_`, `.` and `-`; reuse one name only for one declaration.
|
|
117
|
+
* authenticates, and the scheme a caller proves it by, `Options.security`. The literal name
|
|
118
|
+
* identifies the provider, like a native Context.Key name, and the scheme's OpenAPI key; reuse
|
|
119
|
+
* one name only for one declaration, whose values a server and a client may hold as `layer` says.
|
|
122
120
|
* Guide: `docs/Authentication.md` in this package; in an application: `examples/notes-service/src/identity/`.
|
|
123
121
|
*/
|
|
124
122
|
const make = (name, identity, ...options) => {
|
|
125
123
|
const security = options[0]?.security ?? HttpApiSecurity.bearer;
|
|
126
124
|
const error = errorList(options[0]?.error);
|
|
127
|
-
if (!openApiComponentKey.test(name)) throw new Error(`Invalid authentication name: ${JSON.stringify(name)}, not an OpenAPI key`);
|
|
128
|
-
|
|
129
|
-
if (!Predicate.isTagged(security, "Http") && !Predicate.isTagged(security, "ApiKey") && !Predicate.isTagged(security, "Basic")) throw new Error("Authentication takes one native HttpApiSecurity scheme");
|
|
125
|
+
if (!openApiComponentKey.test(name)) throw new Error(`Invalid authentication name: ${JSON.stringify(name)}, not an OpenAPI key: use only letters, digits, "_", "." and "-"`);
|
|
126
|
+
assertDistinctTags(`Authentication "${name}"`, [...builtIns, ...error]);
|
|
127
|
+
if (!Predicate.isTagged(security, "Http") && !Predicate.isTagged(security, "ApiKey") && !Predicate.isTagged(security, "Basic")) throw new Error("Authentication takes one native HttpApiSecurity scheme as `security`: omit it for Bearer, or give one scheme, such as `HttpApiSecurity.basic`, never a record of them");
|
|
130
128
|
return {
|
|
131
129
|
name,
|
|
132
130
|
identity,
|
|
@@ -178,16 +176,19 @@ function layer(authentication, verify, options = {}) {
|
|
|
178
176
|
}));
|
|
179
177
|
}
|
|
180
178
|
/**
|
|
181
|
-
* Native router middleware authenticating a route of the host's own, such as an export, a
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
179
|
+
* Native router middleware authenticating a route of the host's own, such as an export, a page
|
|
180
|
+
* frame or a WebSocket upgrade, as its actions' routes are: the credential its scheme decodes,
|
|
181
|
+
* verified by `authentication`'s provider, which its layer requires, and the identity provided
|
|
182
|
+
* to the route. A missing or invalid credential, and a refusal the route fails with, such as a
|
|
183
|
+
* scope check's `Forbidden`, are answered as on an action route: status, JSON and challenge,
|
|
184
|
+
* stepping up under Bearer.
|
|
185
|
+
*
|
|
186
|
+
* **Gotchas**
|
|
187
|
+
*
|
|
188
|
+
* - No authorizer runs for its routes: each states its own authorization, such as by running the `authorize` of the action it mirrors.
|
|
189
|
+
* - Every response to a request it covers is `Cache-Control: no-store` unless the route states its own caching; a route that does says `private` unless its answer is the same for every caller, or a cache in front of the server may serve it to anyone.
|
|
190
|
+
* - A failure that enclosing middleware serializes is `no-store` whatever the route set: only a response the route itself returns keeps its caching.
|
|
191
|
+
* - An erased descriptor, `Any`, provides no service the types can name: its identity is `unknown`, which would discharge every request service the route owes.
|
|
190
192
|
*/
|
|
191
193
|
const protect = (authentication) => HttpRouter.middleware()(Effect.map(providerOf(authentication), (runtime) => (route) => runtime.route(route)));
|
|
192
|
-
//#endregion
|
|
193
194
|
export { bearerTokenOf, layer, make, protect, refusalResponse };
|
package/dist/Testing.d.mts
CHANGED
|
@@ -1,35 +1,31 @@
|
|
|
1
1
|
import { Served } from "./testing/memory.mjs";
|
|
2
2
|
import { Layer } from "effect";
|
|
3
3
|
import { HttpClient, HttpRouter } from "effect/http";
|
|
4
|
-
//#region src/testing/Testing.d.ts
|
|
5
4
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `
|
|
10
|
-
* the routes' platform services provided, as `layer(routes)` provides them itself. It is the
|
|
11
|
-
* client `layer(routes)` gives, without building anything: the test owns the handler, and
|
|
12
|
-
* disposes of it.
|
|
5
|
+
* The native `HttpClient`, answered by `handler` instead of the network: a web handler the test
|
|
6
|
+
* serves and disposes of, which other tests and raw web `Request`s may share, such as
|
|
7
|
+
* `HttpRouter.toWebHandler(routes.pipe(Layer.provide(HttpServer.layerServices))).handler`. It is
|
|
8
|
+
* the client `layer(routes)` gives, building nothing; most tests want that one, below.
|
|
13
9
|
*/
|
|
14
10
|
export declare function layer(handler: (request: Request) => Promise<Response>): Layer.Layer<HttpClient.HttpClient>;
|
|
15
11
|
/**
|
|
16
|
-
* The native `HttpClient`, answered in memory by `routes` instead of the network
|
|
17
|
-
*
|
|
18
|
-
* released with its scope,
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* the
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
12
|
+
* The native `HttpClient`, answered in memory by `routes` instead of the network, for
|
|
13
|
+
* `ActionHttp.client`, `ActionMcp.client` and `HttpClient.execute`. The routes are built with this
|
|
14
|
+
* layer and released with its scope, keep their real authentication, so a client sends its
|
|
15
|
+
* caller's credential, and require what they require under `HttpRouter.serve`: a builder's
|
|
16
|
+
* services, and a per-request service no middleware of theirs provides, a global middleware's
|
|
17
|
+
* included. Guide: `docs/Testing.md` in this package; in an application:
|
|
18
|
+
* `examples/notes-service/test/`. A type error at `it.effect`, such as one naming `Scope`:
|
|
19
|
+
* `docs/Testing-failures.md`.
|
|
20
|
+
*
|
|
21
|
+
* **Gotchas**
|
|
22
|
+
*
|
|
23
|
+
* - Every request on its client reaches the routes, whatever its host: a relative URL resolves against `http://localhost`, and a client given a `baseUrl`, a remote `ActionCli` command's included, runs unchanged.
|
|
24
|
+
* - The client is the layer's own: the program's other HTTP clients get none of its requests, so a fake upstream is another `HttpClient`, provided to the service that calls out.
|
|
25
|
+
* - A service the test reads too is provided around it once, with `Layer.provideMerge`: provided to the routes and again to the test, it is two instances, and the test reads one the handlers never changed.
|
|
26
|
+
* - Requests run in the context it is built in: a `ConfigProvider` or the test clock provided around the test reaches middleware and handlers.
|
|
27
|
+
* - No request is logged: only the cause of a defect, of a result that does not encode or of a failure a route leaves unhandled, as an error on the process console, even under `it.effect`'s `TestConsole`; a client reads it as an empty 500.
|
|
28
|
+
* - It never requires `FileSystem`, `Path`, `HttpPlatform` or `Etag.Generator`: one provided around it is the routes', and `HttpServer.layerServices`' defaults stand in for the rest, whose `FileSystem` is a no-op, so a file the routes read is `NotFound` until `NodeFileSystem.layer` is provided around it.
|
|
29
|
+
* - It answers HTTP requests and cannot upgrade a WebSocket: give an rpc layer `RpcServer.layerProtocolHttp` in the test, and its client `RpcClient.layerProtocolHttp` over this layer.
|
|
33
30
|
*/
|
|
34
|
-
export declare function layer<A, E, R>(routes: Layer.Layer<A, E, R>): Layer.Layer<HttpClient.HttpClient, E, Exclude<HttpRouter.Request.Without<R> | HttpRouter.Request.Only<"Requires", R> | HttpRouter.Request.Only<"GlobalRequires", R>, Served>>;
|
|
35
|
-
//#endregion
|
|
31
|
+
export declare function layer<A, E, R>(routes: Layer.Layer<A, E, R>): Layer.Layer<HttpClient.HttpClient, E, Exclude<HttpRouter.Request.Without<R> | HttpRouter.Request.Only<"Requires", R> | HttpRouter.Request.Only<"GlobalRequires", R>, Served>>;
|
package/dist/Testing.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { clientOf } from "./testing/memory.mjs";
|
|
2
2
|
import { Cause, Console, Context, Effect, ErrorReporter, FileSystem, Layer, Option, Path } from "effect";
|
|
3
3
|
import { Etag, HttpEffect, HttpPlatform, HttpRouter } from "effect/http";
|
|
4
|
-
|
|
4
|
+
// src/testing/Testing.ts
|
|
5
5
|
function layer(routes) {
|
|
6
6
|
if (!Layer.isLayer(routes)) return clientOf(routes);
|
|
7
7
|
return Layer.unwrap(Effect.gen(function* () {
|
|
@@ -13,5 +13,4 @@ function layer(routes) {
|
|
|
13
13
|
}));
|
|
14
14
|
}
|
|
15
15
|
const isReported = (cause) => cause.reasons.some((reason) => Cause.isFailReason(reason) && !ErrorReporter.isIgnored(reason.error) || Cause.isDieReason(reason) && !ErrorReporter.isIgnored(reason.defect));
|
|
16
|
-
//#endregion
|
|
17
16
|
export { layer };
|
|
@@ -5,7 +5,6 @@ import { 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/authentication/provider.d.ts
|
|
9
8
|
/** One native security scheme: Bearer unless a descriptor names another. */
|
|
10
9
|
export type Security = HttpApiSecurity.HttpApiSecurity;
|
|
11
10
|
/** What a native scheme decodes: a `Redacted` token or key, or Basic credentials. */
|
|
@@ -70,10 +69,7 @@ export interface Descriptor<I, A, S extends Security, Name extends string = stri
|
|
|
70
69
|
readonly name: Name;
|
|
71
70
|
readonly identity: Context.Key<I, A>;
|
|
72
71
|
readonly security: S;
|
|
73
|
-
/**
|
|
74
|
-
* What its verifier may fail with besides a refusal, as a list: every protected endpoint
|
|
75
|
-
* declares it, and every remote surface sends it, over HTTP with its status.
|
|
76
|
-
*/
|
|
72
|
+
/** `Options.error` of `make`, as a list. */
|
|
77
73
|
readonly error: E;
|
|
78
74
|
readonly "~provider": Context.Key<Provider<I, Name>, Runtime$1>;
|
|
79
75
|
readonly "~middleware": Context.Key<SecurityMiddleware<I, Name>, unknown>;
|
|
@@ -130,5 +126,4 @@ export type VerifierError<A extends Any$1, D> = A["caller"] extends typeof Anyon
|
|
|
130
126
|
readonly error: infer E extends Errors;
|
|
131
127
|
} ? E[number] : never;
|
|
132
128
|
export type ServedProvider<App, A extends Any$1, D> = [Protected<Serving<App, A>>] extends [never] ? never : ProviderOf<D>;
|
|
133
|
-
//#endregion
|
|
134
129
|
export { Runtime$1 as Runtime };
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Unauthenticated } from "../contract/errors.mjs";
|
|
2
2
|
import { Effect, Option } from "effect";
|
|
3
|
-
|
|
4
|
-
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
|
|
3
|
+
// src/authentication/provider.ts
|
|
4
|
+
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 descriptor named here is not the one its provider was built from (build both from one descriptor)`)));
|
|
5
5
|
const promote = (auth, run) => Effect.flatMap(Effect.serviceOption(auth["~verified"]), (actor) => Option.isSome(actor) ? Effect.provideService(run, auth.identity, actor.value) : Effect.fail(new Unauthenticated()));
|
|
6
|
-
//#endregion
|
|
7
6
|
export { promote, providerOf };
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { Forbidden, Refusal, Unauthenticated } from "../contract/errors.mjs";
|
|
2
2
|
import { declared, httpApiStatusOf } from "../contract/status.mjs";
|
|
3
|
+
import { errorSchemas } from "../contract/declared.mjs";
|
|
3
4
|
import { Cause, Context, Effect, Exit, Option, Predicate, Ref, Schema } from "effect";
|
|
4
5
|
import { HttpServerResponse } from "effect/http";
|
|
5
|
-
|
|
6
|
+
// src/authentication/refusal.ts
|
|
6
7
|
const responseOf = (schema, error) => HttpServerResponse.jsonUnsafe(Schema.encodeUnknownSync(Schema.toCodecJson(schema))(error), { status: httpApiStatusOf(schema.ast) ?? 422 });
|
|
7
8
|
/** `error` answered as an endpoint declaring `errors` answers it, if one of them declares it. */
|
|
8
9
|
const declaredResponse = (errors, error) => {
|
|
9
|
-
const schema = errors.
|
|
10
|
+
const schema = errorSchemas(errors).map(declared).find((each) => Schema.is(each)(error));
|
|
10
11
|
return schema === void 0 ? void 0 : responseOf(schema, error);
|
|
11
12
|
};
|
|
12
13
|
const rfc6750ErrorDescriptionCharacters = /^[\x20\x21\x23-\x5B\x5D-\x7E]+$/;
|
|
@@ -42,5 +43,4 @@ const answerStepUp = (route, metadataUrl) => Effect.gen(function* () {
|
|
|
42
43
|
const diedOrInterruptedAfterRefusal = Exit.isFailure(exit) && (Cause.hasDies(exit.cause) || Cause.hasInterrupts(exit.cause));
|
|
43
44
|
return Option.isSome(refused) && !diedOrInterruptedAfterRefusal ? answer(refused.value, metadataUrl) : yield* exit;
|
|
44
45
|
});
|
|
45
|
-
//#endregion
|
|
46
46
|
export { answer, answerStepUp, bearer, challenge, declaredResponse, isRefusal, isStepUp, plain, recordStepUp };
|
package/dist/cli/commands.d.mts
CHANGED
|
@@ -2,38 +2,60 @@ import "../contract/errors.mjs";
|
|
|
2
2
|
import { Any } from "../Action.mjs";
|
|
3
3
|
import { Effect, Option, Runtime, Types } from "effect";
|
|
4
4
|
import { CliError, Command } from "effect/cli";
|
|
5
|
-
//#region src/cli/commands.d.ts
|
|
6
5
|
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
6
|
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
7
|
/** How one command of `A` is named, takes its input and prints its result. */
|
|
9
8
|
export interface Options<A extends Any> {
|
|
10
9
|
/** Override the command name. The action name, in kebab case, is used by default. */
|
|
11
10
|
readonly name?: string;
|
|
12
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* The command's help, where the CLI's command differs from the action, such as one rendering
|
|
13
|
+
* a summary: `"List the open issues, one per line."`. The action's `description` is used by
|
|
14
|
+
* default, and stays what HTTP, MCP and RPC describe. With `stdin`, the paragraph naming the
|
|
15
|
+
* field stdin fills follows it.
|
|
16
|
+
*/
|
|
17
|
+
readonly description?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Human output: the decoded success to text for a person. Adds a `--json` flag to the
|
|
20
|
+
* command that selects JSON instead.
|
|
21
|
+
*/
|
|
13
22
|
readonly render?: (output: A["success"]["Type"]) => string;
|
|
14
23
|
/**
|
|
15
|
-
* Input fields taken as positional arguments instead of flags, in this order
|
|
16
|
-
* optional field is an optional argument,
|
|
24
|
+
* Input fields taken as positional arguments instead of flags, in this order: `["id"]` takes
|
|
25
|
+
* `get-note 7` for `--id 7`. An optional field is an optional argument, and an array field
|
|
26
|
+
* takes every argument left, one element each.
|
|
27
|
+
*
|
|
28
|
+
* **Gotchas**
|
|
29
|
+
*
|
|
30
|
+
* - A field listed here has no flag: `--<field>` is `Unrecognized flag`, so pass its value as an argument.
|
|
17
31
|
*/
|
|
18
32
|
readonly positional?: ReadonlyArray<Field<A>>;
|
|
19
33
|
/**
|
|
20
|
-
* A short name per input field's flag, such as `{ limit: "n" }` for `-n
|
|
21
|
-
* `Param.withAlias` takes it.
|
|
34
|
+
* A short name per input field's flag, such as `{ limit: "n" }` for `-n 5` beside
|
|
35
|
+
* `--limit 5`, given as `Param.withAlias` takes it.
|
|
22
36
|
*/
|
|
23
37
|
readonly aliases?: { readonly [K in Field<A>]?: string; };
|
|
24
38
|
/**
|
|
25
39
|
* 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.
|
|
40
|
+
* leave in the process list and the shell's history, or a document. The value is all of
|
|
41
|
+
* stdin until it closes, without the line endings that end it.
|
|
42
|
+
*
|
|
43
|
+
* **Gotchas**
|
|
44
|
+
*
|
|
45
|
+
* - A terminal is never read: run interactively, the command fails with `InvalidInput`.
|
|
46
|
+
* - The command waits without output until stdin closes: a parent process holding the pipe, or `ssh` without `-n`, keeps it open, so redirect stdin from a file or `/dev/null`.
|
|
27
47
|
*/
|
|
28
48
|
readonly stdin?: StdinField<A>;
|
|
29
49
|
}
|
|
30
50
|
/**
|
|
31
51
|
* 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
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
52
|
+
* action's failure as its `cause` and `reason`, and whose `userMessage` is its JSON, or for a
|
|
53
|
+
* failure no schema encodes, as `command` says. A type only.
|
|
54
|
+
*
|
|
55
|
+
* **Gotchas**
|
|
56
|
+
*
|
|
57
|
+
* - `Command.run` has printed it before a recovery runs, so a recovery decides what follows, such as the exit code, not the text: for text of your own, provide `CliOutput.layer(Object.assign({}, CliOutput.defaultFormatter(), { formatError }))`, or pass `renderErrors: false` to `Command.run` and print every failure, the parser's too.
|
|
58
|
+
* - A run also fails with Effect CLI's own errors, such as a flag that does not parse, and only a `UserError` has a `userMessage`: match `Effect.catchTag("UserError", …)` before reading one.
|
|
37
59
|
*/
|
|
38
60
|
export declare class UserError<out E = unknown> extends CliError.UserError {
|
|
39
61
|
readonly cause: E;
|
|
@@ -44,5 +66,4 @@ export declare class UserError<out E = unknown> extends CliError.UserError {
|
|
|
44
66
|
get reason(): E;
|
|
45
67
|
get [Runtime.errorExitCode](): number;
|
|
46
68
|
}
|
|
47
|
-
//#endregion
|
|
48
69
|
export {};
|