@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.
Files changed (155) hide show
  1. package/README.md +20 -47
  2. package/dist/Action.d.mts +96 -49
  3. package/dist/Action.mjs +12 -16
  4. package/dist/ActionCli.d.mts +82 -44
  5. package/dist/ActionCli.mjs +54 -35
  6. package/dist/ActionHttp.d.mts +84 -45
  7. package/dist/ActionHttp.mjs +3 -4
  8. package/dist/ActionMcp.d.mts +54 -29
  9. package/dist/ActionMcp.mjs +33 -15
  10. package/dist/ActionRpc.d.mts +48 -37
  11. package/dist/ActionRpc.mjs +3 -4
  12. package/dist/ActionToolkit.d.mts +32 -15
  13. package/dist/ActionToolkit.mjs +11 -9
  14. package/dist/Authentication.d.mts +92 -42
  15. package/dist/Authentication.mjs +38 -37
  16. package/dist/Testing.d.mts +23 -27
  17. package/dist/Testing.mjs +1 -2
  18. package/dist/authentication/provider.d.mts +1 -6
  19. package/dist/authentication/provider.mjs +2 -3
  20. package/dist/authentication/refusal.mjs +3 -3
  21. package/dist/cli/commands.d.mts +34 -13
  22. package/dist/cli/commands.mjs +48 -24
  23. package/dist/contract/ast.mjs +1 -2
  24. package/dist/contract/call.d.mts +1 -3
  25. package/dist/contract/call.mjs +1 -2
  26. package/dist/contract/caller.d.mts +1 -3
  27. package/dist/contract/caller.mjs +2 -3
  28. package/dist/contract/declared.d.mts +0 -2
  29. package/dist/contract/declared.mjs +28 -16
  30. package/dist/contract/errors.d.mts +42 -19
  31. package/dist/contract/errors.mjs +43 -19
  32. package/dist/contract/hints.mjs +4 -4
  33. package/dist/contract/implementation.d.mts +7 -14
  34. package/dist/contract/implementation.mjs +31 -19
  35. package/dist/contract/media.d.mts +7 -5
  36. package/dist/contract/media.mjs +7 -4
  37. package/dist/contract/names.mjs +15 -14
  38. package/dist/contract/status.mjs +4 -12
  39. package/dist/http/client.d.mts +15 -6
  40. package/dist/http/client.mjs +2 -3
  41. package/dist/http/schema-errors.mjs +1 -2
  42. package/dist/mcp/client.d.mts +21 -10
  43. package/dist/mcp/client.mjs +8 -11
  44. package/dist/mcp/protocol.mjs +1 -2
  45. package/dist/stdio/console.mjs +1 -2
  46. package/dist/testing/memory.d.mts +1 -3
  47. package/dist/testing/memory.mjs +1 -2
  48. package/docs/Action-failures.md +6 -15
  49. package/docs/Action.md +159 -191
  50. package/docs/ActionCli-failures.md +12 -92
  51. package/docs/ActionCli.md +102 -156
  52. package/docs/ActionHttp-failures.md +14 -76
  53. package/docs/ActionHttp.md +150 -176
  54. package/docs/ActionMcp-failures.md +2 -22
  55. package/docs/ActionMcp.md +79 -170
  56. package/docs/ActionRpc-failures.md +3 -12
  57. package/docs/ActionRpc.md +183 -120
  58. package/docs/ActionToolkit-failures.md +3 -10
  59. package/docs/ActionToolkit.md +112 -109
  60. package/docs/Authentication-advanced.md +171 -108
  61. package/docs/Authentication-failures.md +11 -91
  62. package/docs/Authentication.md +114 -135
  63. package/docs/README.md +45 -30
  64. package/docs/Testing-failures.md +2 -6
  65. package/docs/Testing.md +126 -159
  66. package/docs/setup-failures.md +8 -32
  67. package/docs/setup.md +42 -38
  68. package/examples/README.md +6 -10
  69. package/examples/assistant/README.md +20 -38
  70. package/examples/assistant/src/agent.ts +2 -3
  71. package/examples/assistant/src/cli.ts +4 -9
  72. package/examples/assistant/src/commands.ts +6 -4
  73. package/examples/assistant/src/identity/authorize.ts +3 -3
  74. package/examples/assistant/src/tasks/Tasks.ts +1 -2
  75. package/examples/assistant/src/tasks/handlers.ts +3 -3
  76. package/examples/assistant/src/toolkit.ts +6 -8
  77. package/examples/assistant/test/commands.test.ts +16 -11
  78. package/examples/assistant/test/tasks/tasks.test.ts +0 -2
  79. package/examples/assistant/test/toolkit.test.ts +0 -1
  80. package/examples/kitchen-sink/README.md +30 -52
  81. package/examples/kitchen-sink/src/Actions.ts +2 -3
  82. package/examples/kitchen-sink/src/Api.ts +3 -4
  83. package/examples/kitchen-sink/src/admin/Admin.ts +1 -1
  84. package/examples/kitchen-sink/src/admin/authenticate.ts +3 -3
  85. package/examples/kitchen-sink/src/admin/authorize.ts +1 -2
  86. package/examples/kitchen-sink/src/agent.ts +1 -1
  87. package/examples/kitchen-sink/src/assistant/handlers.ts +1 -6
  88. package/examples/kitchen-sink/src/attachments/Attachments.ts +4 -8
  89. package/examples/kitchen-sink/src/attachments/download.ts +5 -8
  90. package/examples/kitchen-sink/src/attachments/handlers.ts +2 -3
  91. package/examples/kitchen-sink/src/browser.ts +3 -4
  92. package/examples/kitchen-sink/src/cli.ts +6 -7
  93. package/examples/kitchen-sink/src/config.ts +2 -0
  94. package/examples/kitchen-sink/src/identity/Identity.ts +7 -7
  95. package/examples/kitchen-sink/src/identity/authenticate.ts +3 -5
  96. package/examples/kitchen-sink/src/identity/authorize.ts +4 -3
  97. package/examples/kitchen-sink/src/identity/quota.ts +1 -1
  98. package/examples/kitchen-sink/src/issues/Issues.ts +2 -2
  99. package/examples/kitchen-sink/src/issues/assistant.ts +3 -3
  100. package/examples/kitchen-sink/src/issues/guide.ts +2 -3
  101. package/examples/kitchen-sink/src/ops.ts +3 -6
  102. package/examples/kitchen-sink/src/request-policy.ts +8 -4
  103. package/examples/kitchen-sink/src/routes.ts +4 -6
  104. package/examples/kitchen-sink/src/seed.ts +0 -1
  105. package/examples/kitchen-sink/src/server.ts +11 -12
  106. package/examples/kitchen-sink/src/stdio.ts +3 -2
  107. package/examples/kitchen-sink/src/workspaces/tenant.ts +3 -1
  108. package/examples/kitchen-sink/test/admin/admin.test.ts +0 -1
  109. package/examples/kitchen-sink/test/assistant/assistant.test.ts +1 -1
  110. package/examples/kitchen-sink/test/attachments/attachments.test.ts +1 -2
  111. package/examples/kitchen-sink/test/identity/identity.test.ts +0 -1
  112. package/examples/kitchen-sink/test/issues/issues.test.ts +0 -2
  113. package/examples/kitchen-sink/test/request-policy.test.ts +39 -3
  114. package/examples/kitchen-sink/test/routes.test.ts +3 -3
  115. package/examples/live-rpc/README.md +8 -49
  116. package/examples/live-rpc/src/browser.ts +8 -41
  117. package/examples/live-rpc/src/chat/Chat.ts +9 -3
  118. package/examples/live-rpc/src/chat/room.ts +1 -1
  119. package/examples/live-rpc/src/client.ts +2 -3
  120. package/examples/live-rpc/src/page.ts +34 -0
  121. package/examples/live-rpc/src/routes.ts +6 -7
  122. package/examples/live-rpc/src/server.ts +6 -4
  123. package/examples/live-rpc/test/browser.test.ts +6 -5
  124. package/examples/live-rpc/test/routes.test.ts +16 -15
  125. package/examples/live-rpc/test/server.test.ts +1 -1
  126. package/examples/mcp-tools/README.md +22 -45
  127. package/examples/mcp-tools/src/printers/Printers.ts +3 -7
  128. package/examples/mcp-tools/src/routes.ts +0 -7
  129. package/examples/mcp-tools/src/server.ts +1 -1
  130. package/examples/mcp-tools/test/printers/printers.test.ts +2 -3
  131. package/examples/mcp-tools/test/routes.test.ts +2 -3
  132. package/examples/notes-service/README.md +9 -43
  133. package/examples/notes-service/src/cli.ts +0 -3
  134. package/examples/notes-service/src/commands.ts +7 -17
  135. package/examples/notes-service/src/identity/authorize.ts +2 -5
  136. package/examples/notes-service/src/identity/verifier.ts +3 -4
  137. package/examples/notes-service/src/notes/Notes.ts +2 -6
  138. package/examples/notes-service/src/notes/store.ts +5 -1
  139. package/examples/notes-service/src/routes.ts +6 -8
  140. package/examples/notes-service/src/server.ts +4 -3
  141. package/examples/notes-service/src/stdio.ts +4 -3
  142. package/examples/notes-service/test/commands.test.ts +1 -1
  143. package/examples/notes-service/test/notes/as.ts +6 -0
  144. package/examples/notes-service/test/notes/idempotency.test.ts +1 -4
  145. package/examples/notes-service/test/notes/in-process.test.ts +50 -0
  146. package/examples/notes-service/test/notes/notes.test.ts +3 -42
  147. package/examples/notes-service/test/routes.test.ts +2 -2
  148. package/examples/quickstart/README.md +1 -1
  149. package/examples/quickstart/src/client.ts +1 -0
  150. package/examples/quickstart/src/server.ts +2 -4
  151. package/examples/quickstart/test/{greeting/greeting.test.ts → routes.test.ts} +3 -3
  152. package/package.json +12 -16
  153. package/docs/Action-in-process.md +0 -93
  154. package/docs/CONTEXT.md +0 -46
  155. 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, which a test holds equal. It is for a
13
- * caller the router never routes, which holds the header and no request; a route of the host's
14
- * own is authenticated by `protect`. The scheme is matched case-insensitively, as RFC 9110
15
- * requires, and the token is `Redacted`, as Effect's own decoder gives it.
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; its path and query select the discovery path. It has no
22
- * fragment, which no request URL carries, so its discovery would never answer (RFC 9728).
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. A `Forbidden` naming scopes asks for
35
- * more when a call needs them.
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, below, when left
44
- * out; another HTTP scheme's or Basic's 401 names that scheme, an API key's none, and nothing
45
- * steps up under it. An error it declares is sent as its endpoints declare it.
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
- /** The OAuth protected resource refusing: the one given to `layer`. */
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
- * a Node `upgrade` handler admitting a socket: its status, its JSON, `Cache-Control: no-store`
62
- * and its challenge, an `Unauthenticated`'s `Bearer`, naming `invalid_token` when
63
- * `authorization` presented a bearer token, or the `insufficient_scope` of a `Forbidden`
64
- * naming scopes. Given the protected resource `layer` was, every challenge names its metadata
65
- * URL and a 401's its `scopesRequired`. Under a descriptor of another scheme, it is the JSON
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
- /** The one native scheme a caller proves the identity by: Bearer when left out. */
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, by the one native scheme `security`, Bearer unless it names another, such
87
- * as `HttpApiSecurity.apiKey({ in: "cookie", key: "session" })`. The literal name
88
- * identifies the provider, like a native Context.Key name, and the scheme's OpenAPI key, so it
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, with the credential the descriptor's
109
- * scheme decodes. A Bearer scheme answers each 401 with its challenge, and may publish the
110
- * OAuth protected resource `protectedResource`, on the `HttpRouter` it then requires.
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
- * page frame or a WebSocket upgrade, with `authentication`'s provider, as its actions' routes
132
- * are: the credential its scheme decodes, verified by the same verifier, and the identity
133
- * provided to the route. It answers as an action route: a missing or invalid credential with
134
- * the refusal, every response to the request it authenticates `no-store` unless it states its
135
- * own caching, and a refusal the route fails with, such as a scope check's `Forbidden`, as its
136
- * status, JSON and challenge, stepping up under Bearer. The layer requires the provider,
137
- * `layer(authentication, ...)`. An erased descriptor, `Any`, provides no service the types can
138
- * name: its identity is `unknown`, which would discharge every request service the route owes.
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 };
@@ -1,18 +1,18 @@
1
- import { Unauthenticated, scopeToken } from "./contract/errors.mjs";
2
- import { assertOwnTags, errorList } from "./contract/declared.mjs";
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
- //#region src/authentication/Authentication.ts
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, which a test holds equal. It is for a
13
- * caller the router never routes, which holds the header and no request; a route of the host's
14
- * own is authenticated by `protect`. The scheme is matched case-insensitively, as RFC 9110
15
- * requires, and the token is `Redacted`, as Effect's own decoder gives it.
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
- if (new URL(options.resource).href.includes("#")) throw new Error(`A protected resource has no fragment: ${options.resource}`);
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
- * a Node `upgrade` handler admitting a socket: its status, its JSON, `Cache-Control: no-store`
51
- * and its challenge, an `Unauthenticated`'s `Bearer`, naming `invalid_token` when
52
- * `authorization` presented a bearer token, or the `insufficient_scope` of a `Forbidden`
53
- * naming scopes. Given the protected resource `layer` was, every challenge names its metadata
54
- * URL and a 401's its `scopesRequired`. Under a descriptor of another scheme, it is the JSON
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, by the one native scheme `security`, Bearer unless it names another, such
119
- * as `HttpApiSecurity.apiKey({ in: "cookie", key: "session" })`. The literal name
120
- * identifies the provider, like a native Context.Key name, and the scheme's OpenAPI key, so it
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
- assertOwnTags(`Authentication "${name}"`, error);
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
- * page frame or a WebSocket upgrade, with `authentication`'s provider, as its actions' routes
183
- * are: the credential its scheme decodes, verified by the same verifier, and the identity
184
- * provided to the route. It answers as an action route: a missing or invalid credential with
185
- * the refusal, every response to the request it authenticates `no-store` unless it states its
186
- * own caching, and a refusal the route fails with, such as a scope check's `Forbidden`, as its
187
- * status, JSON and challenge, stepping up under Bearer. The layer requires the provider,
188
- * `layer(authentication, ...)`. An erased descriptor, `Any`, provides no service the types can
189
- * name: its identity is `unknown`, which would discharge every request service the route owes.
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 };
@@ -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
- * For a web handler the test already owns; most tests want `layer(routes)`, below, with
7
- * `ActionHttp.client` and `ActionMcp.client`. The native `HttpClient`, answered by `handler`
8
- * instead of the network: a web handler the test serves, which other tests may share, such as
9
- * `HttpRouter.toWebHandler(routes.pipe(Layer.provide(HttpServer.layerServices))).handler`,
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: provide
17
- * it to `ActionHttp.client` and to `ActionMcp.client`. The routes are built with this layer and
18
- * released with its scope, without request logs: only the cause of a defect, of a result that
19
- * does not encode or of a failure a route of your own leaves unhandled is logged, as an error,
20
- * where a server's request logger would show it, on the process console even under `it.effect`
21
- * and its `TestConsole`. What they still require is this layer's, as
22
- * under `HttpRouter.serve`: a builder's services, and a per-request service no middleware of
23
- * theirs provides, including one a global middleware reads. The routes keep their real
24
- * authentication: a client sends its caller's credential. Provided around it, the test
25
- * program shares them. It never requires
26
- * the platform services, `FileSystem`, `Path`, `HttpPlatform` and `Etag.Generator`: one
27
- * provided around it is the routes' too, and `HttpServer.layerServices`' defaults stand in for
28
- * the rest, whose `FileSystem` is a no-op. A relative URL resolves against `http://localhost`.
29
- * The client is the layer's own: the program's other HTTP clients get none of its requests,
30
- * and it none of theirs.
31
- * Guide: `docs/Testing.md` in this package; in an application: `examples/notes-service/test/`.
32
- * A type error at `it.effect`, such as one naming `Scope`: `docs/Testing-failures.md`.
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
- //#region src/testing/Testing.ts
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
- //#region 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 binding's descriptor is not its provider's; build both from one descriptor`)));
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
- //#region src/authentication/refusal.ts
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.flatMap(declared).find((each) => Schema.is(each)(error));
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 };
@@ -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
- /** Human output. Adds a `--json` flag to the command that selects JSON instead. */
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. An
16
- * optional field is an optional argument, so it follows every required one.
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`, given as
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 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)`.
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 {};