@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.
- package/LICENSE +4 -0
- package/README.md +19 -2
- package/dist/Action.d.ts +201 -0
- package/dist/Action.d.ts.map +1 -0
- package/dist/Action.js +172 -0
- package/dist/Action.js.map +1 -0
- package/dist/AgentChannel.d.ts +1717 -0
- package/dist/AgentChannel.d.ts.map +1 -0
- package/dist/AgentChannel.js +708 -0
- package/dist/AgentChannel.js.map +1 -0
- package/dist/AgentChannelConformance.d.ts +1543 -0
- package/dist/AgentChannelConformance.d.ts.map +1 -0
- package/dist/AgentChannelConformance.js +163 -0
- package/dist/AgentChannelConformance.js.map +1 -0
- package/dist/AgentChannelRuntime.d.ts +78 -0
- package/dist/AgentChannelRuntime.d.ts.map +1 -0
- package/dist/AgentChannelRuntime.js +270 -0
- package/dist/AgentChannelRuntime.js.map +1 -0
- package/dist/AiProvider.d.ts +132 -0
- package/dist/AiProvider.d.ts.map +1 -0
- package/dist/AiProvider.js +100 -0
- package/dist/AiProvider.js.map +1 -0
- package/dist/AuthMethod.d.ts +381 -0
- package/dist/AuthMethod.d.ts.map +1 -0
- package/dist/AuthMethod.js +225 -0
- package/dist/AuthMethod.js.map +1 -0
- package/dist/AuthMethodBinding.d.ts +77 -0
- package/dist/AuthMethodBinding.d.ts.map +1 -0
- package/dist/AuthMethodBinding.js +31 -0
- package/dist/AuthMethodBinding.js.map +1 -0
- package/dist/AuthMethodExecution.d.ts +223 -0
- package/dist/AuthMethodExecution.d.ts.map +1 -0
- package/dist/AuthMethodExecution.js +277 -0
- package/dist/AuthMethodExecution.js.map +1 -0
- package/dist/ConnectionResolver.d.ts +219 -0
- package/dist/ConnectionResolver.d.ts.map +1 -0
- package/dist/ConnectionResolver.js +153 -0
- package/dist/ConnectionResolver.js.map +1 -0
- package/dist/DefinitionError.d.ts +67 -0
- package/dist/DefinitionError.d.ts.map +1 -0
- package/dist/DefinitionError.js +82 -0
- package/dist/DefinitionError.js.map +1 -0
- package/dist/DualRegistry.d.ts +71 -0
- package/dist/DualRegistry.d.ts.map +1 -0
- package/dist/DualRegistry.js +96 -0
- package/dist/DualRegistry.js.map +1 -0
- package/dist/DualRuntime.d.ts +279 -0
- package/dist/DualRuntime.d.ts.map +1 -0
- package/dist/DualRuntime.js +311 -0
- package/dist/DualRuntime.js.map +1 -0
- package/dist/Files.d.ts +80 -0
- package/dist/Files.d.ts.map +1 -0
- package/dist/Files.js +66 -0
- package/dist/Files.js.map +1 -0
- package/dist/Group.d.ts +106 -0
- package/dist/Group.d.ts.map +1 -0
- package/dist/Group.js +86 -0
- package/dist/Group.js.map +1 -0
- package/dist/Http.d.ts +342 -0
- package/dist/Http.d.ts.map +1 -0
- package/dist/Http.js +677 -0
- package/dist/Http.js.map +1 -0
- package/dist/Mcp.d.ts +109 -0
- package/dist/Mcp.d.ts.map +1 -0
- package/dist/Mcp.js +176 -0
- package/dist/Mcp.js.map +1 -0
- package/dist/OAuth2TokenRefresher.d.ts +18 -0
- package/dist/OAuth2TokenRefresher.d.ts.map +1 -0
- package/dist/OAuth2TokenRefresher.js +19 -0
- package/dist/OAuth2TokenRefresher.js.map +1 -0
- package/dist/OAuthFlow.d.ts +71 -0
- package/dist/OAuthFlow.d.ts.map +1 -0
- package/dist/OAuthFlow.js +488 -0
- package/dist/OAuthFlow.js.map +1 -0
- package/dist/Plugin.d.ts +383 -0
- package/dist/Plugin.d.ts.map +1 -0
- package/dist/Plugin.js +249 -0
- package/dist/Plugin.js.map +1 -0
- package/dist/PluginRuntime.d.ts +3186 -0
- package/dist/PluginRuntime.d.ts.map +1 -0
- package/dist/PluginRuntime.js +331 -0
- package/dist/PluginRuntime.js.map +1 -0
- package/dist/Runner.d.ts +1 -0
- package/dist/Runner.js +124 -0
- package/dist/Runner.js.map +1 -0
- package/dist/ToolFile.d.ts +23 -0
- package/dist/ToolFile.d.ts.map +1 -0
- package/dist/ToolFile.js +33 -0
- package/dist/ToolFile.js.map +1 -0
- package/dist/Trigger.d.ts +358 -0
- package/dist/Trigger.d.ts.map +1 -0
- package/dist/Trigger.js +287 -0
- package/dist/Trigger.js.map +1 -0
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +24 -0
- package/dist/internal/auth-context.d.ts +23 -0
- package/dist/internal/auth-context.d.ts.map +1 -0
- package/dist/internal/auth-context.js +1 -0
- package/dist/internal/auth-execution.d.ts +27 -0
- package/dist/internal/auth-execution.d.ts.map +1 -0
- package/dist/internal/auth-execution.js +66 -0
- package/dist/internal/auth-execution.js.map +1 -0
- package/dist/internal/auth-runtime.d.ts +5 -0
- package/dist/internal/auth-runtime.d.ts.map +1 -0
- package/dist/internal/auth-runtime.js +16 -0
- package/dist/internal/auth-runtime.js.map +1 -0
- package/dist/internal/capabilities.d.ts +24 -0
- package/dist/internal/capabilities.d.ts.map +1 -0
- package/dist/internal/capabilities.js +22 -0
- package/dist/internal/capabilities.js.map +1 -0
- package/dist/internal/children.d.ts +34 -0
- package/dist/internal/children.d.ts.map +1 -0
- package/dist/internal/children.js +53 -0
- package/dist/internal/children.js.map +1 -0
- package/dist/internal/collection-utils.d.ts +4 -0
- package/dist/internal/collection-utils.d.ts.map +1 -0
- package/dist/internal/collection-utils.js +6 -0
- package/dist/internal/collection-utils.js.map +1 -0
- package/dist/internal/dual/actions.d.ts +33 -0
- package/dist/internal/dual/actions.d.ts.map +1 -0
- package/dist/internal/dual/actions.js +26 -0
- package/dist/internal/dual/actions.js.map +1 -0
- package/dist/internal/dual/invoke.d.ts +63 -0
- package/dist/internal/dual/invoke.d.ts.map +1 -0
- package/dist/internal/dual/invoke.js +75 -0
- package/dist/internal/dual/invoke.js.map +1 -0
- package/dist/internal/dual/registry.d.ts +125 -0
- package/dist/internal/dual/registry.d.ts.map +1 -0
- package/dist/internal/dual/registry.js +172 -0
- package/dist/internal/dual/registry.js.map +1 -0
- package/dist/internal/dual/triggers.d.ts +29 -0
- package/dist/internal/dual/triggers.d.ts.map +1 -0
- package/dist/internal/dual/triggers.js +26 -0
- package/dist/internal/dual/triggers.js.map +1 -0
- package/dist/internal/handler-context.d.ts +9 -0
- package/dist/internal/handler-context.d.ts.map +1 -0
- package/dist/internal/handler-context.js +31 -0
- package/dist/internal/handler-context.js.map +1 -0
- package/dist/internal/identifier.d.ts +6 -0
- package/dist/internal/identifier.d.ts.map +1 -0
- package/dist/internal/identifier.js +28 -0
- package/dist/internal/identifier.js.map +1 -0
- package/dist/internal/plugin-handlers.d.ts +38 -0
- package/dist/internal/plugin-handlers.d.ts.map +1 -0
- package/dist/internal/plugin-handlers.js +52 -0
- package/dist/internal/plugin-handlers.js.map +1 -0
- package/dist/internal/proto.d.ts +7 -0
- package/dist/internal/proto.d.ts.map +1 -0
- package/dist/internal/proto.js +10 -0
- package/dist/internal/proto.js.map +1 -0
- package/dist/internal/schema-runtime.d.ts +6 -0
- package/dist/internal/schema-runtime.d.ts.map +1 -0
- package/dist/internal/schema-runtime.js +10 -0
- package/dist/internal/schema-runtime.js.map +1 -0
- package/dist/internal/trigger-runtime-shared.d.ts +41 -0
- package/dist/internal/trigger-runtime-shared.d.ts.map +1 -0
- package/dist/internal/trigger-runtime-shared.js +132 -0
- package/dist/internal/trigger-runtime-shared.js.map +1 -0
- package/dist/internal/trigger-runtime.d.ts +12 -0
- package/dist/internal/trigger-runtime.d.ts.map +1 -0
- package/dist/internal/trigger-runtime.js +95 -0
- package/dist/internal/trigger-runtime.js.map +1 -0
- package/dist/internal/types.d.ts +7 -0
- package/dist/internal/types.d.ts.map +1 -0
- package/dist/internal/types.js +1 -0
- package/dist/remote-runtime.d.ts +585 -0
- package/dist/remote-runtime.d.ts.map +1 -0
- package/dist/remote-runtime.js +367 -0
- package/dist/remote-runtime.js.map +1 -0
- package/package.json +55 -4
- package/src/Action.ts +370 -0
- package/src/AgentChannel.ts +860 -0
- package/src/AgentChannelConformance.ts +124 -0
- package/src/AgentChannelRuntime.ts +616 -0
- package/src/AiProvider.ts +255 -0
- package/src/AuthMethod.ts +954 -0
- package/src/AuthMethodBinding.ts +112 -0
- package/src/AuthMethodExecution.ts +542 -0
- package/src/ConnectionResolver.ts +265 -0
- package/src/DefinitionError.ts +115 -0
- package/src/DualRegistry.ts +152 -0
- package/src/DualRuntime.ts +1081 -0
- package/src/Files.ts +89 -0
- package/src/Group.ts +292 -0
- package/src/Http.ts +1205 -0
- package/src/Mcp.ts +299 -0
- package/src/OAuth2TokenRefresher.ts +22 -0
- package/src/OAuthFlow.ts +874 -0
- package/src/Plugin.ts +912 -0
- package/src/PluginRuntime.ts +414 -0
- package/src/Runner.ts +248 -0
- package/src/ToolFile.ts +28 -0
- package/src/Trigger.ts +722 -0
- package/src/index.ts +158 -0
- package/src/internal/auth-context.ts +27 -0
- package/src/internal/auth-execution.ts +241 -0
- package/src/internal/auth-runtime.ts +20 -0
- package/src/internal/capabilities.ts +55 -0
- package/src/internal/children.ts +126 -0
- package/src/internal/collection-utils.ts +8 -0
- package/src/internal/dual/actions.ts +116 -0
- package/src/internal/dual/invoke.ts +307 -0
- package/src/internal/dual/registry.ts +340 -0
- package/src/internal/dual/triggers.ts +128 -0
- package/src/internal/handler-context.ts +57 -0
- package/src/internal/identifier.ts +27 -0
- package/src/internal/plugin-handlers.ts +146 -0
- package/src/internal/proto.ts +8 -0
- package/src/internal/schema-runtime.ts +15 -0
- package/src/internal/trigger-runtime-shared.ts +449 -0
- package/src/internal/trigger-runtime.ts +223 -0
- package/src/internal/types.ts +12 -0
- 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";
|