@dual-os/sdk 0.0.0-stage → 0.8.0-next.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 (214) hide show
  1. package/LICENSE +4 -0
  2. package/README.md +19 -2
  3. package/dist/Action.d.ts +201 -0
  4. package/dist/Action.d.ts.map +1 -0
  5. package/dist/Action.js +172 -0
  6. package/dist/Action.js.map +1 -0
  7. package/dist/AgentChannel.d.ts +1717 -0
  8. package/dist/AgentChannel.d.ts.map +1 -0
  9. package/dist/AgentChannel.js +708 -0
  10. package/dist/AgentChannel.js.map +1 -0
  11. package/dist/AgentChannelConformance.d.ts +1543 -0
  12. package/dist/AgentChannelConformance.d.ts.map +1 -0
  13. package/dist/AgentChannelConformance.js +163 -0
  14. package/dist/AgentChannelConformance.js.map +1 -0
  15. package/dist/AgentChannelRuntime.d.ts +78 -0
  16. package/dist/AgentChannelRuntime.d.ts.map +1 -0
  17. package/dist/AgentChannelRuntime.js +270 -0
  18. package/dist/AgentChannelRuntime.js.map +1 -0
  19. package/dist/AiProvider.d.ts +132 -0
  20. package/dist/AiProvider.d.ts.map +1 -0
  21. package/dist/AiProvider.js +100 -0
  22. package/dist/AiProvider.js.map +1 -0
  23. package/dist/AuthMethod.d.ts +381 -0
  24. package/dist/AuthMethod.d.ts.map +1 -0
  25. package/dist/AuthMethod.js +225 -0
  26. package/dist/AuthMethod.js.map +1 -0
  27. package/dist/AuthMethodBinding.d.ts +77 -0
  28. package/dist/AuthMethodBinding.d.ts.map +1 -0
  29. package/dist/AuthMethodBinding.js +31 -0
  30. package/dist/AuthMethodBinding.js.map +1 -0
  31. package/dist/AuthMethodExecution.d.ts +223 -0
  32. package/dist/AuthMethodExecution.d.ts.map +1 -0
  33. package/dist/AuthMethodExecution.js +277 -0
  34. package/dist/AuthMethodExecution.js.map +1 -0
  35. package/dist/ConnectionResolver.d.ts +219 -0
  36. package/dist/ConnectionResolver.d.ts.map +1 -0
  37. package/dist/ConnectionResolver.js +153 -0
  38. package/dist/ConnectionResolver.js.map +1 -0
  39. package/dist/DefinitionError.d.ts +67 -0
  40. package/dist/DefinitionError.d.ts.map +1 -0
  41. package/dist/DefinitionError.js +82 -0
  42. package/dist/DefinitionError.js.map +1 -0
  43. package/dist/DualRegistry.d.ts +71 -0
  44. package/dist/DualRegistry.d.ts.map +1 -0
  45. package/dist/DualRegistry.js +96 -0
  46. package/dist/DualRegistry.js.map +1 -0
  47. package/dist/DualRuntime.d.ts +279 -0
  48. package/dist/DualRuntime.d.ts.map +1 -0
  49. package/dist/DualRuntime.js +311 -0
  50. package/dist/DualRuntime.js.map +1 -0
  51. package/dist/Files.d.ts +80 -0
  52. package/dist/Files.d.ts.map +1 -0
  53. package/dist/Files.js +66 -0
  54. package/dist/Files.js.map +1 -0
  55. package/dist/Group.d.ts +106 -0
  56. package/dist/Group.d.ts.map +1 -0
  57. package/dist/Group.js +86 -0
  58. package/dist/Group.js.map +1 -0
  59. package/dist/Http.d.ts +342 -0
  60. package/dist/Http.d.ts.map +1 -0
  61. package/dist/Http.js +677 -0
  62. package/dist/Http.js.map +1 -0
  63. package/dist/Mcp.d.ts +109 -0
  64. package/dist/Mcp.d.ts.map +1 -0
  65. package/dist/Mcp.js +176 -0
  66. package/dist/Mcp.js.map +1 -0
  67. package/dist/OAuth2TokenRefresher.d.ts +18 -0
  68. package/dist/OAuth2TokenRefresher.d.ts.map +1 -0
  69. package/dist/OAuth2TokenRefresher.js +19 -0
  70. package/dist/OAuth2TokenRefresher.js.map +1 -0
  71. package/dist/OAuthFlow.d.ts +71 -0
  72. package/dist/OAuthFlow.d.ts.map +1 -0
  73. package/dist/OAuthFlow.js +488 -0
  74. package/dist/OAuthFlow.js.map +1 -0
  75. package/dist/Plugin.d.ts +383 -0
  76. package/dist/Plugin.d.ts.map +1 -0
  77. package/dist/Plugin.js +249 -0
  78. package/dist/Plugin.js.map +1 -0
  79. package/dist/PluginRuntime.d.ts +3186 -0
  80. package/dist/PluginRuntime.d.ts.map +1 -0
  81. package/dist/PluginRuntime.js +331 -0
  82. package/dist/PluginRuntime.js.map +1 -0
  83. package/dist/Runner.d.ts +1 -0
  84. package/dist/Runner.js +124 -0
  85. package/dist/Runner.js.map +1 -0
  86. package/dist/ToolFile.d.ts +23 -0
  87. package/dist/ToolFile.d.ts.map +1 -0
  88. package/dist/ToolFile.js +33 -0
  89. package/dist/ToolFile.js.map +1 -0
  90. package/dist/Trigger.d.ts +358 -0
  91. package/dist/Trigger.d.ts.map +1 -0
  92. package/dist/Trigger.js +287 -0
  93. package/dist/Trigger.js.map +1 -0
  94. package/dist/_virtual/_rolldown/runtime.js +13 -0
  95. package/dist/index.d.ts +24 -0
  96. package/dist/index.js +24 -0
  97. package/dist/internal/auth-context.d.ts +23 -0
  98. package/dist/internal/auth-context.d.ts.map +1 -0
  99. package/dist/internal/auth-context.js +1 -0
  100. package/dist/internal/auth-execution.d.ts +27 -0
  101. package/dist/internal/auth-execution.d.ts.map +1 -0
  102. package/dist/internal/auth-execution.js +66 -0
  103. package/dist/internal/auth-execution.js.map +1 -0
  104. package/dist/internal/auth-runtime.d.ts +5 -0
  105. package/dist/internal/auth-runtime.d.ts.map +1 -0
  106. package/dist/internal/auth-runtime.js +16 -0
  107. package/dist/internal/auth-runtime.js.map +1 -0
  108. package/dist/internal/capabilities.d.ts +24 -0
  109. package/dist/internal/capabilities.d.ts.map +1 -0
  110. package/dist/internal/capabilities.js +22 -0
  111. package/dist/internal/capabilities.js.map +1 -0
  112. package/dist/internal/children.d.ts +34 -0
  113. package/dist/internal/children.d.ts.map +1 -0
  114. package/dist/internal/children.js +53 -0
  115. package/dist/internal/children.js.map +1 -0
  116. package/dist/internal/collection-utils.d.ts +4 -0
  117. package/dist/internal/collection-utils.d.ts.map +1 -0
  118. package/dist/internal/collection-utils.js +6 -0
  119. package/dist/internal/collection-utils.js.map +1 -0
  120. package/dist/internal/dual/actions.d.ts +33 -0
  121. package/dist/internal/dual/actions.d.ts.map +1 -0
  122. package/dist/internal/dual/actions.js +26 -0
  123. package/dist/internal/dual/actions.js.map +1 -0
  124. package/dist/internal/dual/invoke.d.ts +63 -0
  125. package/dist/internal/dual/invoke.d.ts.map +1 -0
  126. package/dist/internal/dual/invoke.js +75 -0
  127. package/dist/internal/dual/invoke.js.map +1 -0
  128. package/dist/internal/dual/registry.d.ts +125 -0
  129. package/dist/internal/dual/registry.d.ts.map +1 -0
  130. package/dist/internal/dual/registry.js +172 -0
  131. package/dist/internal/dual/registry.js.map +1 -0
  132. package/dist/internal/dual/triggers.d.ts +29 -0
  133. package/dist/internal/dual/triggers.d.ts.map +1 -0
  134. package/dist/internal/dual/triggers.js +26 -0
  135. package/dist/internal/dual/triggers.js.map +1 -0
  136. package/dist/internal/handler-context.d.ts +9 -0
  137. package/dist/internal/handler-context.d.ts.map +1 -0
  138. package/dist/internal/handler-context.js +31 -0
  139. package/dist/internal/handler-context.js.map +1 -0
  140. package/dist/internal/identifier.d.ts +6 -0
  141. package/dist/internal/identifier.d.ts.map +1 -0
  142. package/dist/internal/identifier.js +28 -0
  143. package/dist/internal/identifier.js.map +1 -0
  144. package/dist/internal/plugin-handlers.d.ts +38 -0
  145. package/dist/internal/plugin-handlers.d.ts.map +1 -0
  146. package/dist/internal/plugin-handlers.js +52 -0
  147. package/dist/internal/plugin-handlers.js.map +1 -0
  148. package/dist/internal/proto.d.ts +7 -0
  149. package/dist/internal/proto.d.ts.map +1 -0
  150. package/dist/internal/proto.js +10 -0
  151. package/dist/internal/proto.js.map +1 -0
  152. package/dist/internal/schema-runtime.d.ts +6 -0
  153. package/dist/internal/schema-runtime.d.ts.map +1 -0
  154. package/dist/internal/schema-runtime.js +10 -0
  155. package/dist/internal/schema-runtime.js.map +1 -0
  156. package/dist/internal/trigger-runtime-shared.d.ts +41 -0
  157. package/dist/internal/trigger-runtime-shared.d.ts.map +1 -0
  158. package/dist/internal/trigger-runtime-shared.js +132 -0
  159. package/dist/internal/trigger-runtime-shared.js.map +1 -0
  160. package/dist/internal/trigger-runtime.d.ts +12 -0
  161. package/dist/internal/trigger-runtime.d.ts.map +1 -0
  162. package/dist/internal/trigger-runtime.js +95 -0
  163. package/dist/internal/trigger-runtime.js.map +1 -0
  164. package/dist/internal/types.d.ts +7 -0
  165. package/dist/internal/types.d.ts.map +1 -0
  166. package/dist/internal/types.js +1 -0
  167. package/dist/remote-runtime.d.ts +585 -0
  168. package/dist/remote-runtime.d.ts.map +1 -0
  169. package/dist/remote-runtime.js +367 -0
  170. package/dist/remote-runtime.js.map +1 -0
  171. package/package.json +55 -4
  172. package/src/Action.ts +370 -0
  173. package/src/AgentChannel.ts +860 -0
  174. package/src/AgentChannelConformance.ts +124 -0
  175. package/src/AgentChannelRuntime.ts +616 -0
  176. package/src/AiProvider.ts +255 -0
  177. package/src/AuthMethod.ts +954 -0
  178. package/src/AuthMethodBinding.ts +112 -0
  179. package/src/AuthMethodExecution.ts +542 -0
  180. package/src/ConnectionResolver.ts +265 -0
  181. package/src/DefinitionError.ts +115 -0
  182. package/src/DualRegistry.ts +152 -0
  183. package/src/DualRuntime.ts +1081 -0
  184. package/src/Files.ts +89 -0
  185. package/src/Group.ts +292 -0
  186. package/src/Http.ts +1205 -0
  187. package/src/Mcp.ts +299 -0
  188. package/src/OAuth2TokenRefresher.ts +22 -0
  189. package/src/OAuthFlow.ts +874 -0
  190. package/src/Plugin.ts +912 -0
  191. package/src/PluginRuntime.ts +414 -0
  192. package/src/Runner.ts +248 -0
  193. package/src/ToolFile.ts +28 -0
  194. package/src/Trigger.ts +722 -0
  195. package/src/index.ts +158 -0
  196. package/src/internal/auth-context.ts +27 -0
  197. package/src/internal/auth-execution.ts +241 -0
  198. package/src/internal/auth-runtime.ts +20 -0
  199. package/src/internal/capabilities.ts +55 -0
  200. package/src/internal/children.ts +126 -0
  201. package/src/internal/collection-utils.ts +8 -0
  202. package/src/internal/dual/actions.ts +116 -0
  203. package/src/internal/dual/invoke.ts +307 -0
  204. package/src/internal/dual/registry.ts +340 -0
  205. package/src/internal/dual/triggers.ts +128 -0
  206. package/src/internal/handler-context.ts +57 -0
  207. package/src/internal/identifier.ts +27 -0
  208. package/src/internal/plugin-handlers.ts +146 -0
  209. package/src/internal/proto.ts +8 -0
  210. package/src/internal/schema-runtime.ts +15 -0
  211. package/src/internal/trigger-runtime-shared.ts +449 -0
  212. package/src/internal/trigger-runtime.ts +223 -0
  213. package/src/internal/types.ts +12 -0
  214. package/src/remote-runtime.ts +469 -0
package/src/Http.ts ADDED
@@ -0,0 +1,1205 @@
1
+ /**
2
+ * The HTTP runtime of generated API plugins: each operation's contract and call, request encoding,
3
+ * response decoding, continuation links, redirects and uploads.
4
+ *
5
+ * @since 0.0.0
6
+ */
7
+ import {
8
+ Array,
9
+ Boolean,
10
+ DateTime,
11
+ Effect,
12
+ Equal,
13
+ Option,
14
+ Order,
15
+ Predicate,
16
+ Random,
17
+ Record,
18
+ Result,
19
+ Schema,
20
+ SchemaGetter,
21
+ SchemaIssue,
22
+ String,
23
+ flow,
24
+ pipe,
25
+ } from "effect";
26
+ import type { SchemaAST } from "effect";
27
+
28
+ import { Base64, Base64Url } from "effect/encoding";
29
+ import {
30
+ FetchHttpClient,
31
+ HttpBody,
32
+ HttpClient,
33
+ HttpClientError,
34
+ HttpClientRequest,
35
+ } from "effect/http";
36
+ import type { HttpClientResponse } from "effect/http";
37
+
38
+ import * as Action from "./Action.ts";
39
+ import type * as AuthMethod from "./AuthMethod.ts";
40
+ import { Files } from "./Files.ts";
41
+ import { Mcp } from "./Mcp.ts";
42
+ import * as ToolFile from "./ToolFile.ts";
43
+
44
+ function preserveJsonProperties<A, Input>(value: A, input: Input): A {
45
+ if (Equal.equals(value, input)) return value;
46
+ if (Array.isArray(value) && Array.isArray(input)) {
47
+ for (const entry of Array.zip(value, input)) preserveJsonProperties(entry[0], entry[1]);
48
+ } else if (Predicate.isObject(value) && Predicate.isObject(input)) {
49
+ for (const entry of Record.toEntries(input)) {
50
+ if (Record.has(value, entry[0])) {
51
+ preserveJsonProperties(value[entry[0]], entry[1]);
52
+ } else {
53
+ Object.defineProperty(value, entry[0], {
54
+ configurable: true,
55
+ enumerable: true,
56
+ value: entry[1],
57
+ writable: true,
58
+ });
59
+ }
60
+ }
61
+ }
62
+ return value;
63
+ }
64
+
65
+ function decodeResponse<Output extends Schema.Top>(output: Output) {
66
+ return flow(
67
+ Effect.succeed,
68
+ Effect.flatMap(input =>
69
+ pipe(
70
+ Schema.decodeUnknownEffect(output)(input),
71
+ Effect.map(value => preserveJsonProperties(value, input)),
72
+ ),
73
+ ),
74
+ );
75
+ }
76
+
77
+ /**
78
+ * An action's input and output schemas.
79
+ *
80
+ * @since 0.0.0
81
+ * @category models
82
+ */
83
+ export interface Contract {
84
+ readonly input: Schema.Top;
85
+ readonly output: Schema.Top;
86
+ }
87
+
88
+ /**
89
+ * Runs an action: its input in, its output or a failure out.
90
+ *
91
+ * @since 0.0.0
92
+ * @category models
93
+ */
94
+ export type Handler<Methods extends readonly AuthMethod.Any[], Input, Output> = (
95
+ input: Input,
96
+ context: Action.HandlerContext<Methods>,
97
+ ) => Effect.Effect<Output, Action.ActionHandlerFailure, HttpClient.HttpClient>;
98
+
99
+ /**
100
+ * A file input: a ToolFile as a download returns it, or just its bytes with a type and name.
101
+ *
102
+ * @since 0.0.0
103
+ * @category schemas
104
+ */
105
+ export type File = typeof File.Type;
106
+ export const File = Schema.Struct({
107
+ _tag: Schema.optionalKey(Schema.Literal("ToolFile")),
108
+ contentBase64: Schema.Uint8ArrayFromBase64,
109
+ contentType: Schema.optionalKey(Schema.NullOr(Schema.String)),
110
+ filename: Schema.optionalKey(Schema.NullOr(Schema.String)),
111
+ sizeBytes: Schema.optionalKey(Schema.Natural),
112
+ }).annotate({
113
+ description:
114
+ "A ToolFile, such as the result of a download, passed unchanged; or its contentBase64 with an optional contentType and filename.",
115
+ });
116
+
117
+ const signatures: [string, [number, number[]][]][] = [
118
+ ["image/jpeg", [[0, [0xff, 0xd8, 0xff]]]],
119
+ ["image/png", [[0, [0x89, 0x50, 0x4e, 0x47]]]],
120
+ ["image/gif", [[0, [0x47, 0x49, 0x46, 0x38]]]],
121
+ [
122
+ "image/webp",
123
+ [
124
+ [0, [0x52, 0x49, 0x46, 0x46]],
125
+ [8, [0x57, 0x45, 0x42, 0x50]],
126
+ ],
127
+ ],
128
+ ["application/pdf", [[0, [0x25, 0x50, 0x44, 0x46]]]],
129
+ ["application/zip", [[0, [0x50, 0x4b, 0x03, 0x04]]]],
130
+ ];
131
+
132
+ const utf8 = new TextDecoder("utf-8", { fatal: true });
133
+
134
+ function sniff(bytes: Uint8Array) {
135
+ return pipe(
136
+ Array.findFirst(signatures, signature =>
137
+ Array.every(signature[1], part =>
138
+ Array.every(part[1], (byte, index) => bytes[part[0] + index] === byte),
139
+ ),
140
+ ),
141
+ Option.map(signature => signature[0]),
142
+ Option.orElse(() =>
143
+ Option.liftPredicate("text/plain", () =>
144
+ Result.isSuccess(Result.try(() => utf8.decode(bytes))),
145
+ ),
146
+ ),
147
+ Option.getOrNull,
148
+ );
149
+ }
150
+
151
+ function toolFile(bytes: Uint8Array, declared: string | null, filename: string | null) {
152
+ return {
153
+ _tag: "ToolFile",
154
+ contentBase64: Base64.encode(bytes),
155
+ contentType:
156
+ declared === null || pipe(declared, String.startsWith("application/octet-stream"))
157
+ ? (sniff(bytes) ?? declared)
158
+ : declared,
159
+ filename,
160
+ sizeBytes: bytes.byteLength,
161
+ } satisfies ToolFile.File;
162
+ }
163
+
164
+ function invalid(expected: string, value: unknown, options: SchemaAST.ParseOptions) {
165
+ return Effect.fail(new SchemaIssue.InvalidValue({ expected }, value, options));
166
+ }
167
+
168
+ function decodeBytes(text: string, options: SchemaAST.ParseOptions) {
169
+ return Result.match(
170
+ Base64Url.decode(pipe(text, String.replaceAll("+", "-"), String.replaceAll("/", "_"))),
171
+ {
172
+ onFailure: () => invalid("a Base64 or Base64Url string", text, options),
173
+ onSuccess: bytes => Effect.succeed(bytes),
174
+ },
175
+ );
176
+ }
177
+
178
+ type Carrier = "base64" | "base64url" | "data-uri";
179
+
180
+ interface Content {
181
+ bytes: Uint8Array;
182
+ contentType: string | null;
183
+ }
184
+
185
+ function contentOf(carrier: Carrier) {
186
+ return SchemaGetter.transformEffect<Content, string>((text, options) => {
187
+ if (carrier !== "data-uri") {
188
+ return Effect.map(decodeBytes(text, options), bytes => ({ bytes, contentType: null }));
189
+ }
190
+ return Option.match(pipe(text, String.match(/^data:([^;,]*)(?:;[^;,]*)*;base64,(.*)$/su)), {
191
+ onNone: () => invalid("a Base64 data URI", text, options),
192
+ onSome: parts =>
193
+ Effect.map(decodeBytes(Array.getUnsafe(parts, 2), options), bytes => ({
194
+ bytes,
195
+ contentType: pipe(
196
+ Array.getUnsafe(parts, 1),
197
+ Option.liftPredicate(String.isNonEmpty),
198
+ Option.getOrNull,
199
+ ),
200
+ })),
201
+ });
202
+ });
203
+ }
204
+
205
+ function textOf(carrier: Carrier, content: Content) {
206
+ if (carrier === "base64") return Base64.encode(content.bytes);
207
+ if (carrier === "base64url") return Base64Url.encode(content.bytes);
208
+ const contentType = content.contentType ?? sniff(content.bytes) ?? "application/octet-stream";
209
+ return `data:${contentType};base64,${Base64.encode(content.bytes)}`;
210
+ }
211
+
212
+ function fileFrom(carrier: Carrier) {
213
+ return Schema.String.pipe(
214
+ Schema.decodeTo(ToolFile.Schema, {
215
+ decode: SchemaGetter.compose(
216
+ contentOf(carrier),
217
+ SchemaGetter.transform(content => toolFile(content.bytes, content.contentType, null)),
218
+ ),
219
+ encode: SchemaGetter.transformEffect((file: ToolFile.File, options) =>
220
+ Effect.map(decodeBytes(file.contentBase64, options), bytes =>
221
+ textOf(carrier, { bytes, contentType: file.contentType }),
222
+ ),
223
+ ),
224
+ }),
225
+ );
226
+ }
227
+
228
+ function encodedFrom(carrier: Carrier) {
229
+ return File.pipe(
230
+ Schema.decodeTo(Schema.String, {
231
+ decode: SchemaGetter.transform(file =>
232
+ textOf(carrier, { bytes: file.contentBase64, contentType: file.contentType ?? null }),
233
+ ),
234
+ encode: SchemaGetter.compose(
235
+ contentOf(carrier),
236
+ SchemaGetter.transform(content => ({
237
+ contentBase64: content.bytes,
238
+ contentType: content.contentType,
239
+ })),
240
+ ),
241
+ }),
242
+ );
243
+ }
244
+
245
+ /**
246
+ * A file the API returns as a Base64 string inside JSON, decoded into a ToolFile with its content
247
+ * type recognized from the bytes.
248
+ *
249
+ * @since 0.0.0
250
+ * @category schemas
251
+ */
252
+ export const FileFromBase64 = fileFrom("base64");
253
+
254
+ /**
255
+ * {@link FileFromBase64} for an API that writes the file in Base64Url.
256
+ *
257
+ * @since 0.0.0
258
+ * @category schemas
259
+ */
260
+ export const FileFromBase64Url = fileFrom("base64url");
261
+
262
+ /**
263
+ * A {@link File} input the API reads as a Base64 string inside JSON.
264
+ *
265
+ * @since 0.0.0
266
+ * @category schemas
267
+ */
268
+ export const Base64FromFile = encodedFrom("base64");
269
+
270
+ /**
271
+ * {@link Base64FromFile} for an API that reads the file in Base64Url.
272
+ *
273
+ * @since 0.0.0
274
+ * @category schemas
275
+ */
276
+ export const Base64UrlFromFile = encodedFrom("base64url");
277
+
278
+ /**
279
+ * {@link FileFromBase64} for an API that writes the file as a data URI, which also carries its
280
+ * content type.
281
+ *
282
+ * @since 0.0.0
283
+ * @category schemas
284
+ */
285
+ export const FileFromDataUri = fileFrom("data-uri");
286
+
287
+ /**
288
+ * {@link Base64FromFile} for an API that reads the file as a data URI, built from the file's
289
+ * content type.
290
+ *
291
+ * @since 0.0.0
292
+ * @category schemas
293
+ */
294
+ export const DataUriFromFile = encodedFrom("data-uri");
295
+
296
+ type DateTimeText = typeof DateTimeText.Type;
297
+ const DateTimeText = Schema.String.annotate({ format: "date-time" });
298
+
299
+ const rfc3339 = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.\d+)?(?:Z|([+-]\d{2}:\d{2}))$/u;
300
+
301
+ /** The instant of an RFC 3339 date-time with its offset; an impossible date such as February 30 has none. */
302
+ function rfc3339Instant(text: string) {
303
+ return pipe(
304
+ Option.fromNullishOr(rfc3339.exec(text)),
305
+ Option.flatMap(match =>
306
+ Option.filter(DateTime.makeZoned(text, { timeZone: match[2] ?? "+00:00" }), zoned =>
307
+ String.startsWith(match[1] ?? "")(DateTime.formatIsoOffset(zoned)),
308
+ ),
309
+ ),
310
+ Option.map(DateTime.toUtc),
311
+ );
312
+ }
313
+
314
+ function instants<Wire>(
315
+ wire: Schema.Codec<Wire>,
316
+ expected: string,
317
+ read: (value: Wire) => Option.Option<DateTime.Utc>,
318
+ write: (instant: DateTime.Utc) => Wire,
319
+ ) {
320
+ const fromWire = SchemaGetter.transformEffect<string, Wire>((value, options) =>
321
+ Option.match(read(value), {
322
+ onNone: () => invalid(expected, value, options),
323
+ onSome: instant => Effect.succeed(DateTime.formatIso(instant)),
324
+ }),
325
+ );
326
+ const toWire = SchemaGetter.transformEffect<Wire, string>((text, options) =>
327
+ Option.match(rfc3339Instant(text), {
328
+ onNone: () => invalid("an ISO 8601 date-time", text, options),
329
+ onSome: instant => Effect.succeed(write(instant)),
330
+ }),
331
+ );
332
+ return {
333
+ input: pipe(DateTimeText, Schema.decodeTo(wire, { decode: toWire, encode: fromWire })),
334
+ output: pipe(wire, Schema.decodeTo(DateTimeText, { decode: fromWire, encode: toWire })),
335
+ };
336
+ }
337
+
338
+ const unixSeconds = instants(
339
+ Schema.Int,
340
+ "Unix seconds",
341
+ seconds => Option.map(DateTime.make(seconds * 1000), DateTime.toUtc),
342
+ instant => Math.floor(DateTime.toEpochMillis(instant) / 1000),
343
+ );
344
+
345
+ const rfc2822 = instants(
346
+ Schema.String,
347
+ "an RFC 2822 date-time",
348
+ text => Option.map(DateTime.make(text), DateTime.toUtc),
349
+ instant => DateTime.toDateUtc(instant).toUTCString(),
350
+ );
351
+
352
+ /**
353
+ * A date-time the API writes as Unix seconds, decoded into an ISO 8601 UTC string.
354
+ *
355
+ * @since 0.0.0
356
+ * @category schemas
357
+ */
358
+ export const DateTimeFromUnixSeconds = unixSeconds.output;
359
+
360
+ /**
361
+ * An ISO 8601 date-time input the API reads as Unix seconds.
362
+ *
363
+ * @since 0.0.0
364
+ * @category schemas
365
+ */
366
+ export const UnixSecondsFromDateTime = unixSeconds.input;
367
+
368
+ /**
369
+ * A date-time the API writes in RFC 2822 form, decoded into an ISO 8601 UTC string.
370
+ *
371
+ * @since 0.0.0
372
+ * @category schemas
373
+ */
374
+ export const DateTimeFromRfc2822 = rfc2822.output;
375
+
376
+ /**
377
+ * An ISO 8601 date-time input the API reads in RFC 2822 form.
378
+ *
379
+ * @since 0.0.0
380
+ * @category schemas
381
+ */
382
+ export const Rfc2822FromDateTime = rfc2822.input;
383
+
384
+ /**
385
+ * An action output whose response decodes into a different shape, such as a file the API sends as
386
+ * Base64: the action declares the decoded shape and the call decodes the response with `wire`.
387
+ *
388
+ * @since 0.0.0
389
+ * @category models
390
+ */
391
+ export interface Decoded<Wire extends Schema.Top> {
392
+ readonly _tag: "Decoded";
393
+ readonly wire: Wire;
394
+ }
395
+
396
+ /**
397
+ * Declares an output by the response schema it decodes from; see {@link Decoded}.
398
+ *
399
+ * @since 0.0.0
400
+ * @category constructors
401
+ */
402
+ export function decoded<const Wire extends Schema.Top>(wire: Wire): Decoded<Wire> {
403
+ return { _tag: "Decoded", wire };
404
+ }
405
+
406
+ type Request =
407
+ | HttpClientRequest.HttpClientRequest
408
+ | Effect.Effect<HttpClientRequest.HttpClientRequest, Action.ActionHandlerError>;
409
+
410
+ /** `binary` returns a released version's base64 `ToolFile`; `file` stores the body as a file. */
411
+ type Body = "json" | "text" | "binary" | "file" | "void" | "unknown";
412
+
413
+ type GraphqlResponse = typeof GraphqlResponse.Type;
414
+ const GraphqlResponse = Schema.Struct({
415
+ data: Schema.optionalKey(Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown))),
416
+ errors: Schema.optionalKey(
417
+ Schema.Array(
418
+ Schema.StructWithRest(Schema.Struct({ message: Schema.String }), [
419
+ Schema.Record(Schema.String, Schema.Unknown),
420
+ ]),
421
+ ),
422
+ ),
423
+ });
424
+
425
+ /**
426
+ * A response status, media type and decoding mode advertised by an HTTP action.
427
+ *
428
+ * @since 0.4.1
429
+ * @category models
430
+ */
431
+ export interface Variant {
432
+ readonly status?: number;
433
+ readonly contentType: string;
434
+ readonly body: Body;
435
+ readonly headers?: readonly string[];
436
+ }
437
+
438
+ type Media =
439
+ | { readonly contentBase64: Uint8Array; readonly contentType?: string | null }
440
+ | Files.File;
441
+
442
+ type Upload =
443
+ | { readonly mode: "media"; readonly media: Media }
444
+ | { readonly mode: "multipart"; readonly media: Media; readonly metadata: unknown }
445
+ | {
446
+ readonly mode: "start";
447
+ readonly contentType: string;
448
+ readonly metadata?: unknown;
449
+ readonly totalBytes: number;
450
+ }
451
+ | {
452
+ readonly mode: "chunk";
453
+ readonly sessionUrl: string;
454
+ readonly totalBytes: number;
455
+ readonly offset: number;
456
+ readonly media: Media;
457
+ }
458
+ | { readonly mode: "status"; readonly sessionUrl: string; readonly totalBytes: number };
459
+
460
+ function coerce(value: unknown) {
461
+ return Predicate.isObject(value) ? JSON.stringify(value) : `${value}`;
462
+ }
463
+
464
+ /**
465
+ * Encodes a path parameter as one segment.
466
+ *
467
+ * @since 0.0.0
468
+ * @category encoding
469
+ */
470
+ export function path(value: unknown) {
471
+ return encodeURIComponent(coerce(value));
472
+ }
473
+
474
+ /**
475
+ * Encodes a path parameter written inside single quotes, an OData string literal, doubling its
476
+ * quotes: `O'Brien` in `People('{name}')` is sent as `People('O''Brien')`.
477
+ *
478
+ * @since 0.0.0
479
+ * @category encoding
480
+ */
481
+ export function literal(value: unknown) {
482
+ return path(pipe(coerce(value), String.replaceAll("'", "''")));
483
+ }
484
+
485
+ /**
486
+ * Encodes a path parameter whose slashes stay path separators.
487
+ *
488
+ * @since 0.0.0
489
+ * @category encoding
490
+ */
491
+ export function segments(value: unknown) {
492
+ return Array.join(Array.map(String.split(coerce(value), "/"), encodeURIComponent), "/");
493
+ }
494
+
495
+ function deepObject(name: string, value: unknown): [string, string][] {
496
+ if (Array.isArray(value)) {
497
+ return Array.flatMap(value, (item, index) => deepObject(`${name}[${index}]`, item));
498
+ }
499
+ if (Predicate.isObject(value)) {
500
+ return Array.flatMap(Record.toEntries(value), entry =>
501
+ deepObject(`${name}[${entry[0]}]`, entry[1]),
502
+ );
503
+ }
504
+ return value === undefined ? [] : [[name, coerce(value)]];
505
+ }
506
+
507
+ /**
508
+ * Query, header or form entries, repeating array values unless their name is joined, and encoding
509
+ * `deep` names as bracketed keys such as `metadata[plan]` and `expand[0]`. A joined name is
510
+ * comma-separated, or separated by the delimiter it is paired with, such as `["ids", "|"]`.
511
+ * `exploded` object parameters use their property names as query keys (`role=vet&active=true`).
512
+ *
513
+ * @since 0.0.0
514
+ * @category encoding
515
+ */
516
+ export function params(
517
+ fields: Record.ReadonlyRecord<string, unknown>,
518
+ joined: readonly (string | readonly [string, string])[] = [],
519
+ deep: readonly string[] = [],
520
+ exploded: readonly string[] = [],
521
+ ) {
522
+ return Array.flatMap(Record.toEntries(fields), field => {
523
+ if (field[1] === undefined) return [];
524
+ if (Array.contains(deep, field[0])) return deepObject(field[0], field[1]);
525
+ const values = field[1];
526
+ if (
527
+ Array.contains(exploded, field[0]) &&
528
+ Predicate.isObject(values) &&
529
+ !Array.isArray(values)
530
+ ) {
531
+ return Array.filterMap(Record.toEntries(values), entry =>
532
+ entry[1] === undefined
533
+ ? Result.failVoid
534
+ : Result.succeed([entry[0], coerce(entry[1])] as const),
535
+ );
536
+ }
537
+ if (!Array.isArray(values)) return [[field[0], coerce(values)] as const];
538
+ const separator = Array.findFirst(joined, entry => {
539
+ const pair = Predicate.isString(entry) ? ([entry, ","] as const) : entry;
540
+ return pair[0] === field[0] ? Option.some(pair[1]) : Option.none();
541
+ });
542
+ return Option.match(separator, {
543
+ onNone: () => Array.map(values, item => [field[0], coerce(item)] as const),
544
+ onSome: value => [[field[0], Array.join(Array.map(values, coerce), value)] as const],
545
+ });
546
+ });
547
+ }
548
+
549
+ function filePart(bytes: Uint8Array, name: string, contentType: string) {
550
+ return new globalThis.File([new Uint8Array(bytes)], name, { type: contentType });
551
+ }
552
+
553
+ /**
554
+ * Multipart form fields, sending each {@link File} as a file part.
555
+ *
556
+ * @since 0.0.0
557
+ * @category encoding
558
+ */
559
+ export function form(payload: Record.ReadonlyRecord<string, unknown>) {
560
+ return Record.map(payload, (value, name) =>
561
+ Array.map(Array.ensure(value), item => {
562
+ if (item === undefined) return undefined;
563
+ if (!Schema.is(File)(item)) return coerce(item);
564
+ return filePart(
565
+ item.contentBase64,
566
+ item.filename ?? name,
567
+ item.contentType ?? "application/octet-stream",
568
+ );
569
+ }),
570
+ );
571
+ }
572
+
573
+ function fileFailed(error: Files.FileError) {
574
+ return Action.ActionHandlerError.make({ cause: error, message: error.message });
575
+ }
576
+
577
+ function readFile(file: Files.File) {
578
+ return pipe(Files.read(file), Effect.mapError(fileFailed));
579
+ }
580
+
581
+ /**
582
+ * Sends a file as the raw request body.
583
+ *
584
+ * @since 0.1.0
585
+ * @category encoding
586
+ */
587
+ export const withFile = Effect.fnUntraced(function* (
588
+ request: HttpClientRequest.HttpClientRequest,
589
+ file: Files.File,
590
+ ) {
591
+ return HttpClientRequest.bodyUint8Array(request, yield* readFile(file), file.contentType);
592
+ });
593
+
594
+ /**
595
+ * Sends multipart form fields, each {@link Files.File} as a file part.
596
+ *
597
+ * @since 0.1.0
598
+ * @category encoding
599
+ */
600
+ export const withForm = Effect.fnUntraced(function* (
601
+ request: HttpClientRequest.HttpClientRequest,
602
+ payload: Record.ReadonlyRecord<string, unknown>,
603
+ ) {
604
+ return HttpClientRequest.bodyFormDataRecord(
605
+ request,
606
+ Record.fromEntries(
607
+ yield* Effect.forEach(Record.toEntries(payload), field =>
608
+ Effect.map(
609
+ Effect.forEach(
610
+ Array.filter(Array.ensure(field[1]), item => item !== undefined),
611
+ item =>
612
+ Effect.gen(function* () {
613
+ if (!Schema.is(Files.File)(item)) return coerce(item);
614
+ return filePart(yield* readFile(item), item.name, item.contentType);
615
+ }),
616
+ ),
617
+ parts => [field[0], parts] satisfies [string, (string | globalThis.File)[]],
618
+ ),
619
+ ),
620
+ ),
621
+ );
622
+ });
623
+
624
+ function mediaBytes(media: Media) {
625
+ if (Schema.is(Files.File)(media)) {
626
+ return Effect.map(readFile(media), bytes => ({ bytes, contentType: media.contentType }));
627
+ }
628
+ return Effect.succeed({
629
+ bytes: media.contentBase64,
630
+ contentType: Option.getOrNull(Option.fromNullishOr(media.contentType)),
631
+ });
632
+ }
633
+
634
+ function isSafeUrl(value: string, origin?: string) {
635
+ if (!URL.canParse(value)) return false;
636
+ const url = new URL(value);
637
+ return (
638
+ url.protocol === "https:" &&
639
+ url.username === "" &&
640
+ url.password === "" &&
641
+ (origin === undefined || url.origin === origin)
642
+ );
643
+ }
644
+
645
+ function refused(message: string) {
646
+ return Effect.fail(Action.ActionHandlerError.make({ message }));
647
+ }
648
+
649
+ /**
650
+ * Follows a returned next-page link on the operation's own origin: only its path and query are
651
+ * taken, so the connection's host routing applies and credentials never reach another host.
652
+ *
653
+ * @since 0.0.0
654
+ * @category requests
655
+ */
656
+ export function continuation(request: HttpClientRequest.HttpClientRequest, nextLink?: string) {
657
+ if (nextLink === undefined) return Effect.succeed(request);
658
+ const declared = new URL(request.url);
659
+ const next = URL.canParse(nextLink) ? new URL(nextLink) : undefined;
660
+ if (next === undefined || !isSafeUrl(nextLink) || next.pathname !== declared.pathname) {
661
+ return refused("A continuation URL must be an HTTPS link to the same operation path");
662
+ }
663
+ return Effect.succeed(
664
+ HttpClientRequest.make(request.method)(`${declared.origin}${next.pathname}${next.search}`, {
665
+ headers: request.headers,
666
+ }),
667
+ );
668
+ }
669
+
670
+ /**
671
+ * A request to a preauthenticated upload URL, which is sent without the connection's credentials.
672
+ *
673
+ * @since 0.0.0
674
+ * @category requests
675
+ */
676
+ export function preauthenticated(
677
+ method: (url: string) => HttpClientRequest.HttpClientRequest,
678
+ url: string,
679
+ ...steps: readonly ((
680
+ request: HttpClientRequest.HttpClientRequest,
681
+ ) => HttpClientRequest.HttpClientRequest)[]
682
+ ) {
683
+ if (!isSafeUrl(url)) return refused("An upload session requires a preauthenticated HTTPS URL");
684
+ return Effect.succeed(Array.reduce(steps, method(url), (request, step) => step(request)));
685
+ }
686
+
687
+ function failure(
688
+ error:
689
+ | Action.ActionHandlerError
690
+ | Action.HttpClientFailure
691
+ | { readonly cause: unknown; readonly response: HttpClientResponse.HttpClientResponse },
692
+ ) {
693
+ return Schema.is(Action.ActionHandlerError)(error) ? error : Action.fromHttpClientFailure(error);
694
+ }
695
+
696
+ // Like HttpClient.followRedirects, except that a redirect to another origin sends no
697
+ // request header at all: auth methods may set any header, such as GitLab's PRIVATE-TOKEN.
698
+ function redirect(response: HttpClientResponse.HttpClientResponse) {
699
+ const location = response.headers["location"];
700
+ if (response.status < 300 || response.status >= 400 || location === undefined) return undefined;
701
+ const sent = response.request;
702
+ const url = new URL(location, sent.url);
703
+ const request =
704
+ (response.status === 303 && sent.method !== "GET" && sent.method !== "HEAD") ||
705
+ ((response.status === 301 || response.status === 302) && sent.method === "POST")
706
+ ? HttpClientRequest.setBody(HttpClientRequest.setMethod(sent, "GET"), HttpBody.empty)
707
+ : sent;
708
+ return url.origin === new URL(sent.url).origin
709
+ ? HttpClientRequest.setUrl(request, url)
710
+ : HttpClientRequest.make(request.method)(url, { body: request.body });
711
+ }
712
+
713
+ function follow(
714
+ client: HttpClient.HttpClient,
715
+ response: HttpClientResponse.HttpClientResponse,
716
+ redirects: number,
717
+ ): Effect.Effect<HttpClientResponse.HttpClientResponse, HttpClientError.HttpClientError> {
718
+ const next = redirect(response);
719
+ return next === undefined || redirects === 10
720
+ ? Effect.succeed(response)
721
+ : Effect.flatMap(client.execute(next), result => follow(client, result, redirects + 1));
722
+ }
723
+
724
+ /** The file name a `Content-Disposition` header suggests, preferring its encoded form. */
725
+ function dispositionName(header?: string) {
726
+ if (header === undefined) return undefined;
727
+ const encoded = pipe(
728
+ String.match(/(?:^|;)\s*filename\*\s*=\s*UTF-8'[^']*'([^;]+)/iu)(header),
729
+ Option.flatMap(match => Option.fromUndefinedOr(match[1])),
730
+ );
731
+ const decoded = pipe(
732
+ encoded,
733
+ Option.flatMap(value => Option.liftThrowable(() => decodeURIComponent(String.trim(value)))()),
734
+ );
735
+ return Option.getOrElse(decoded, () =>
736
+ pipe(
737
+ String.match(/(?:^|;)\s*filename\s*=\s*(?:"((?:\\.|[^"\\])*)"|([^;]*))/iu)(header),
738
+ Option.flatMap(match =>
739
+ Option.orElse(
740
+ Option.map(Option.fromUndefinedOr(match[1]), name =>
741
+ String.replace(/\\(.)/gu, "$1")(name),
742
+ ),
743
+ () => Option.map(Option.fromUndefinedOr(match[2]), String.trim),
744
+ ),
745
+ ),
746
+ Option.getOrUndefined,
747
+ ),
748
+ );
749
+ }
750
+
751
+ const read = Effect.fnUntraced(function* (
752
+ response: HttpClientResponse.HttpClientResponse,
753
+ body: Body,
754
+ ) {
755
+ if (body === "json") return yield* response.json;
756
+ if (body === "text") return yield* response.text;
757
+ if (body === "void") return {};
758
+ if (body === "unknown") {
759
+ const text = yield* response.text;
760
+ if (String.isEmpty(String.trim(text))) return null;
761
+ return Option.getOrElse(
762
+ Schema.decodeOption(Schema.fromJsonString(Schema.Json))(text),
763
+ () => text,
764
+ );
765
+ }
766
+ const bytes = new Uint8Array(yield* response.arrayBuffer);
767
+ const name = dispositionName(response.headers["content-disposition"]);
768
+ if (body === "file") {
769
+ return yield* pipe(
770
+ Files.write({
771
+ bytes,
772
+ contentType: Option.getOrNull(Option.fromUndefinedOr(response.headers["content-type"])),
773
+ name: name ?? "file",
774
+ }),
775
+ Effect.mapError(fileFailed),
776
+ );
777
+ }
778
+ return toolFile(bytes, response.headers["content-type"] ?? null, name ?? null);
779
+ });
780
+
781
+ /**
782
+ * One API operation as {@link make} returns it: its contract and the call that performs it. The
783
+ * `Context` parameter lets a package name its handler context once, so declaration files print
784
+ * `Endpoint<Context, …>` instead of repeating its auth methods in every action.
785
+ *
786
+ * @since 0.0.0
787
+ * @category models
788
+ */
789
+ export interface Endpoint<Context, Input extends Schema.Top, Output extends Schema.Top> {
790
+ readonly input: Input;
791
+ readonly output: Output;
792
+ readonly run: (
793
+ input: Input["Type"],
794
+ context: Context,
795
+ ) => Effect.Effect<Output["Type"], Action.ActionHandlerFailure, HttpClient.HttpClient>;
796
+ }
797
+
798
+ /**
799
+ * An {@link Endpoint} whose output is the decoded type of the response schema it keeps as `wire`.
800
+ *
801
+ * @since 0.0.0
802
+ * @category models
803
+ */
804
+ export interface DecodedEndpoint<Context, Input extends Schema.Top, Wire extends Schema.Top> {
805
+ readonly input: Input;
806
+ readonly output: Schema.toType<Wire>;
807
+ readonly wire: Wire;
808
+ readonly run: (
809
+ input: Input["Type"],
810
+ context: Context,
811
+ ) => Effect.Effect<Wire["Type"], Action.ActionHandlerFailure, HttpClient.HttpClient>;
812
+ }
813
+
814
+ /**
815
+ * Binds the runtime to a package's auth module: the calls a generated version file makes. Its
816
+ * `authorize` applies a connection's credentials to the client. Its optional `checkResponse` fails
817
+ * a successful response that reports a failed call, such as Slack's `{ ok: false, error }`.
818
+ * `preauthenticatedUploadUrl` restricts credential-free upload requests and every redirect to
819
+ * provider-approved HTTPS URLs, without changing authenticated calls or other providers.
820
+ *
821
+ * @since 0.0.0
822
+ * @category constructors
823
+ */
824
+ export function make<
825
+ const Methods extends readonly AuthMethod.Any[],
826
+ Context extends Action.HandlerContext<Methods> = Action.HandlerContext<Methods>,
827
+ >(auth: {
828
+ readonly authorize: (
829
+ client: HttpClient.HttpClient,
830
+ context: Action.HandlerContext<Methods>,
831
+ ) => HttpClient.HttpClient;
832
+ readonly checkResponse?: (
833
+ response: HttpClientResponse.HttpClientResponse,
834
+ ) => Effect.Effect<void, Action.ActionHandlerError | HttpClientError.HttpClientError>;
835
+ readonly preauthenticatedUploadUrl?: Schema.Schema<string>;
836
+ }) {
837
+ const { authorize, checkResponse = () => Effect.void } = auth;
838
+ const send = Effect.fnUntraced(function* (context: Context | undefined, request: Request) {
839
+ const base = yield* HttpClient.HttpClient;
840
+ const uploadPolicy = context === undefined ? auth.preauthenticatedUploadUrl : undefined;
841
+ const client =
842
+ uploadPolicy === undefined
843
+ ? base
844
+ : HttpClient.mapRequestEffect(base, outgoing =>
845
+ isSafeUrl(outgoing.url) && Schema.is(uploadPolicy)(outgoing.url)
846
+ ? Effect.succeed(outgoing)
847
+ : Effect.fail(
848
+ new HttpClientError.HttpClientError({
849
+ reason: new HttpClientError.InvalidUrlError({
850
+ description: "The upload URL is not an approved provider HTTPS destination",
851
+ request: outgoing,
852
+ }),
853
+ }),
854
+ ),
855
+ );
856
+ const transport = HttpClient.transformResponse(
857
+ client,
858
+ Effect.flatMap(response => follow(client, response, 0)),
859
+ );
860
+ const authorized = context === undefined ? transport : authorize(transport, context);
861
+ const execution = authorized.execute(Effect.isEffect(request) ? yield* request : request);
862
+ const options = yield* Effect.serviceOption(FetchHttpClient.RequestInit);
863
+ const response = yield* Boolean.match(uploadPolicy !== undefined, {
864
+ onFalse: () => execution,
865
+ onTrue: () =>
866
+ Effect.provideService(execution, FetchHttpClient.RequestInit, {
867
+ ...Option.getOrElse(options, () => ({})),
868
+ redirect: "manual",
869
+ }),
870
+ });
871
+ if (response.status < 200 || response.status >= 300) {
872
+ return yield* Effect.fail({ cause: yield* response.text, response });
873
+ }
874
+ yield* checkResponse(response);
875
+ return response;
876
+ });
877
+
878
+ /**
879
+ * One API operation: its action contract and the call that performs it. A {@link Decoded} output
880
+ * declares the decoded shape and hands the call its response schema.
881
+ */
882
+ function endpoint<const Input extends Schema.Top, const Output extends Schema.Top>(
883
+ contract: { readonly input: Input; readonly output: Output },
884
+ run: (
885
+ input: Input["Type"],
886
+ context: Context,
887
+ output: Output,
888
+ ) => Effect.Effect<Output["Type"], Action.ActionHandlerFailure, HttpClient.HttpClient>,
889
+ ): Endpoint<Context, Input, Output>;
890
+ function endpoint<const Input extends Schema.Top, const Wire extends Schema.Top>(
891
+ contract: { readonly input: Input; readonly output: Decoded<Wire> },
892
+ run: (
893
+ input: Input["Type"],
894
+ context: Context,
895
+ output: Wire,
896
+ ) => Effect.Effect<Wire["Type"], Action.ActionHandlerFailure, HttpClient.HttpClient>,
897
+ ): DecodedEndpoint<Context, Input, Wire>;
898
+ function endpoint<const Input extends Schema.Top, const Output extends Schema.Top>(
899
+ contract: { readonly input: Input; readonly output: Output | Decoded<Output> },
900
+ run: (
901
+ input: Input["Type"],
902
+ context: Context,
903
+ output: Output,
904
+ ) => Effect.Effect<Output["Type"], Action.ActionHandlerFailure, HttpClient.HttpClient>,
905
+ ) {
906
+ if (!Schema.isSchema(contract.output)) {
907
+ const wire = contract.output.wire;
908
+ return {
909
+ input: contract.input,
910
+ output: Schema.toType(wire),
911
+ run: (input: Input["Type"], context: Context) => run(input, context, wire),
912
+ wire,
913
+ };
914
+ }
915
+ const output = contract.output;
916
+ return {
917
+ input: contract.input,
918
+ output,
919
+ run: (input: Input["Type"], context: Context) => run(input, context, output),
920
+ };
921
+ }
922
+
923
+ /** Sends a request and decodes its body as the operation's single response. */
924
+ function execute<Output extends Schema.Top>(
925
+ context: Context | undefined,
926
+ output: Output,
927
+ request: Request,
928
+ body: Body,
929
+ ) {
930
+ return pipe(
931
+ send(context, request),
932
+ Effect.flatMap(response => read(response, body)),
933
+ Effect.flatMap(decodeResponse(output)),
934
+ Effect.mapError(failure),
935
+ );
936
+ }
937
+
938
+ /** Sends a request and decodes the declared response variant its status and media type select. */
939
+ const variants = Effect.fnUntraced(function* <Output extends Schema.Top>(
940
+ context: Context | undefined,
941
+ output: Output,
942
+ request: Request,
943
+ declared: readonly Variant[],
944
+ media?: boolean,
945
+ ) {
946
+ const response = yield* send(context, request);
947
+ const exact = Array.flatMap(declared, variant =>
948
+ variant.status === undefined ? [] : [variant.status],
949
+ );
950
+ const contentType = pipe(
951
+ response.headers["content-type"] ?? "",
952
+ String.split(";"),
953
+ Array.headNonEmpty,
954
+ String.trim,
955
+ String.toLowerCase,
956
+ );
957
+ const ordered = Array.sort(
958
+ declared,
959
+ Order.mapInput(Order.Number, (variant: Variant) => {
960
+ if (variant.body === "void" || variant.body === "unknown") return 3;
961
+ if (variant.contentType === "*/*") return 2;
962
+ return pipe(variant.contentType, String.endsWith("/*")) ? 1 : 0;
963
+ }),
964
+ );
965
+ const variant = Array.findFirst(ordered, candidate => {
966
+ const status =
967
+ candidate.status === undefined
968
+ ? !Array.contains(exact, response.status)
969
+ : candidate.status === response.status;
970
+ if (!status) return false;
971
+ if (media !== undefined) {
972
+ return (candidate.body === "binary" || candidate.body === "file") === media;
973
+ }
974
+ if (
975
+ candidate.body === "void" ||
976
+ candidate.body === "unknown" ||
977
+ candidate.contentType === "*/*"
978
+ ) {
979
+ return true;
980
+ }
981
+ if (pipe(candidate.contentType, String.endsWith("/*"))) {
982
+ return pipe(
983
+ contentType,
984
+ String.startsWith(pipe(candidate.contentType, String.slice(0, -1))),
985
+ );
986
+ }
987
+ return contentType === candidate.contentType;
988
+ });
989
+ if (Option.isNone(variant)) {
990
+ return yield* Effect.fail({
991
+ cause: "The API returned an undeclared successful response",
992
+ response,
993
+ });
994
+ }
995
+ const headers = Array.flatMap(variant.value.headers ?? [], name => {
996
+ const value = response.headers[String.toLowerCase(name)];
997
+ return value === undefined ? [] : [[name, value] as const];
998
+ });
999
+ return yield* decodeResponse(output)({
1000
+ body: yield* read(response, variant.value.body),
1001
+ contentType: variant.value.contentType,
1002
+ status: response.status,
1003
+ ...(variant.value.headers === undefined ? {} : { headers: Record.fromEntries(headers) }),
1004
+ });
1005
+ }, Effect.mapError(failure));
1006
+
1007
+ /** Google's media, multipart and resumable upload protocols. */
1008
+ const googleUpload = Effect.fnUntraced(function* <Output extends Schema.Top>(
1009
+ context: Context,
1010
+ output: Output,
1011
+ request: HttpClientRequest.HttpClientRequest,
1012
+ payload: Upload,
1013
+ options: { readonly maxSize?: number; readonly resumable?: string },
1014
+ ) {
1015
+ const client = authorize(yield* HttpClient.HttpClient, context);
1016
+ const media = "media" in payload ? yield* mediaBytes(payload.media) : undefined;
1017
+ const size = "totalBytes" in payload ? payload.totalBytes : (media?.bytes.byteLength ?? 0);
1018
+ if (size < 0 || (options.maxSize !== undefined && size > options.maxSize)) {
1019
+ return yield* refused("The upload size is outside the API's declared limit");
1020
+ }
1021
+ const origin = new URL(request.url).origin;
1022
+ if (payload.mode === "start") {
1023
+ if (options.resumable === undefined) {
1024
+ return yield* refused("This API has no resumable upload protocol");
1025
+ }
1026
+ const response = yield* client.execute(
1027
+ pipe(
1028
+ HttpClientRequest.setUrl(request, options.resumable),
1029
+ HttpClientRequest.setUrlParam("uploadType", "resumable"),
1030
+ HttpClientRequest.setHeaders({
1031
+ "x-upload-content-length": `${size}`,
1032
+ "x-upload-content-type": payload.contentType,
1033
+ }),
1034
+ HttpClientRequest.bodyJsonUnsafe(payload.metadata ?? {}),
1035
+ ),
1036
+ );
1037
+ if (response.status < 200 || response.status >= 300) {
1038
+ return yield* Effect.fail({ cause: yield* response.text, response });
1039
+ }
1040
+ const sessionUrl = response.headers["location"];
1041
+ if (sessionUrl === undefined || !isSafeUrl(sessionUrl, origin)) {
1042
+ return yield* Effect.fail({
1043
+ cause: "The API returned an invalid upload session URL",
1044
+ response,
1045
+ });
1046
+ }
1047
+ return yield* decodeResponse(output)({ nextOffset: 0, sessionUrl, state: "pending" });
1048
+ }
1049
+ if (payload.mode === "chunk" || payload.mode === "status") {
1050
+ if (!isSafeUrl(payload.sessionUrl, origin)) {
1051
+ return yield* refused("The upload session URL must use the API's HTTPS origin");
1052
+ }
1053
+ const bytes = media?.bytes ?? new Uint8Array();
1054
+ const end = payload.mode === "chunk" ? payload.offset + bytes.byteLength : 0;
1055
+ if (
1056
+ payload.mode === "chunk" &&
1057
+ (payload.offset < 0 ||
1058
+ bytes.byteLength === 0 ||
1059
+ end > size ||
1060
+ (end < size && bytes.byteLength % 262_144 !== 0))
1061
+ ) {
1062
+ return yield* refused(
1063
+ "Upload chunks must fit the total size and use 256 KiB multiples except for the final chunk",
1064
+ );
1065
+ }
1066
+ const response = yield* client.execute(
1067
+ pipe(
1068
+ HttpClientRequest.put(payload.sessionUrl),
1069
+ HttpClientRequest.bodyUint8Array(bytes, media?.contentType ?? "application/octet-stream"),
1070
+ HttpClientRequest.setHeader(
1071
+ "content-range",
1072
+ payload.mode === "status"
1073
+ ? `bytes */${size}`
1074
+ : `bytes ${payload.offset}-${end - 1}/${size}`,
1075
+ ),
1076
+ ),
1077
+ );
1078
+ if (response.status === 308) {
1079
+ const range = response.headers["range"];
1080
+ if (range !== undefined && !/^bytes=0-\d+$/u.test(range)) {
1081
+ return yield* Effect.fail({
1082
+ cause: "The API returned an invalid upload range",
1083
+ response,
1084
+ });
1085
+ }
1086
+ const nextOffset = range === undefined ? 0 : Number(pipe(range, String.slice(8))) + 1;
1087
+ if (nextOffset > size) {
1088
+ return yield* Effect.fail({
1089
+ cause: "The API returned an upload offset beyond the total size",
1090
+ response,
1091
+ });
1092
+ }
1093
+ return yield* decodeResponse(output)({
1094
+ nextOffset,
1095
+ sessionUrl: payload.sessionUrl,
1096
+ state: "pending",
1097
+ });
1098
+ }
1099
+ if (response.status < 200 || response.status >= 300) {
1100
+ return yield* Effect.fail({ cause: yield* response.text, response });
1101
+ }
1102
+ return yield* decodeResponse(output)({ resource: yield* response.json, state: "complete" });
1103
+ }
1104
+ const selected = HttpClientRequest.setUrlParam(request, "uploadType", payload.mode);
1105
+ const bytes = media?.bytes ?? new Uint8Array();
1106
+ const contentType = media?.contentType ?? "application/octet-stream";
1107
+ const boundary = `dual-${yield* Random.nextInt}-${yield* Random.nextInt}`;
1108
+ const metadata =
1109
+ payload.mode === "multipart"
1110
+ ? yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(payload.metadata)
1111
+ : "";
1112
+ const body =
1113
+ payload.mode === "media"
1114
+ ? HttpClientRequest.bodyUint8Array(selected, bytes, contentType)
1115
+ : HttpClientRequest.bodyUint8Array(
1116
+ selected,
1117
+ new Uint8Array(
1118
+ yield* Effect.promise(() =>
1119
+ new Blob([
1120
+ `--${boundary}\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n${metadata}\r\n--${boundary}\r\nContent-Type: ${contentType}\r\n\r\n`,
1121
+ new Uint8Array(bytes),
1122
+ `\r\n--${boundary}--\r\n`,
1123
+ ]).arrayBuffer(),
1124
+ ),
1125
+ ),
1126
+ `multipart/related; boundary=${boundary}`,
1127
+ );
1128
+ const response = yield* client.execute(body);
1129
+ if (response.status < 200 || response.status >= 300) {
1130
+ return yield* Effect.fail({ cause: yield* response.text, response });
1131
+ }
1132
+ return yield* decodeResponse(output)({ resource: yield* response.json, state: "complete" });
1133
+ }, Effect.mapError(failure));
1134
+
1135
+ /** Sends a GraphQL operation and decodes its root field, failing with the errors it returns. */
1136
+ const graphql = Effect.fnUntraced(function* <Output extends Schema.Top>(
1137
+ context: Context,
1138
+ output: Output,
1139
+ request: Request,
1140
+ field: string,
1141
+ ) {
1142
+ const response = yield* send(context, request);
1143
+ const body = yield* Schema.decodeUnknownEffect(GraphqlResponse)(yield* response.json);
1144
+ if (body.errors !== undefined && Array.isReadonlyArrayNonEmpty(body.errors)) {
1145
+ return yield* Action.ActionHandlerError.make({
1146
+ details: body.errors,
1147
+ message: Array.join(
1148
+ Array.map(body.errors, error => error.message),
1149
+ "; ",
1150
+ ),
1151
+ });
1152
+ }
1153
+ return yield* decodeResponse(output)(body.data?.[field]);
1154
+ }, Effect.mapError(failure));
1155
+
1156
+ /**
1157
+ * Calls a Model Context Protocol tool in a session with the server. A tool that reports an error
1158
+ * fails the action with its text; otherwise the output is its structured content, or its content
1159
+ * blocks when it declares no output schema.
1160
+ */
1161
+ const mcp = Effect.fnUntraced(
1162
+ function* <Output extends Schema.Top>(
1163
+ context: Context,
1164
+ output: Output,
1165
+ server: string,
1166
+ tool: string,
1167
+ args: unknown,
1168
+ structured: boolean,
1169
+ ) {
1170
+ const connection = yield* Mcp.connect(
1171
+ authorize(yield* HttpClient.HttpClient, context),
1172
+ server,
1173
+ {},
1174
+ );
1175
+ const result = yield* Schema.decodeUnknownEffect(Mcp.CallToolResult)(
1176
+ yield* connection.request("tools/call", { arguments: args, name: tool }),
1177
+ );
1178
+ if (result.isError === true) {
1179
+ return yield* Action.ActionHandlerError.make({
1180
+ details: result.content,
1181
+ message: Array.join(
1182
+ Array.flatMap(result.content, block => Array.fromNullishOr(block.text)),
1183
+ "\n",
1184
+ ),
1185
+ });
1186
+ }
1187
+ return yield* decodeResponse(output)(
1188
+ Boolean.match(structured, {
1189
+ onFalse: () => ({ content: result.content }),
1190
+ onTrue: () => result.structuredContent,
1191
+ }),
1192
+ );
1193
+ },
1194
+ Effect.scoped,
1195
+ Effect.mapError(error =>
1196
+ Schema.is(Mcp.McpError)(error)
1197
+ ? Action.ActionHandlerError.make({ message: error.message })
1198
+ : failure(error),
1199
+ ),
1200
+ );
1201
+
1202
+ return { endpoint, execute, googleUpload, graphql, mcp, variants };
1203
+ }
1204
+
1205
+ export * as Http from "./Http.ts";