@superblocksteam/sdk-api 0.0.12-body-types → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/dist/errors.d.ts +36 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +231 -38
- package/dist/errors.js.map +1 -1
- package/dist/errors.test.d.ts +2 -0
- package/dist/errors.test.d.ts.map +1 -0
- package/dist/errors.test.js +189 -0
- package/dist/errors.test.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/integrations/anthropic/types.d.ts +1 -1
- package/dist/integrations/base/decode-worker-binary-response.d.ts.map +1 -1
- package/dist/integrations/base/decode-worker-binary-response.js +2 -5
- package/dist/integrations/base/decode-worker-binary-response.js.map +1 -1
- package/dist/integrations/base/index.d.ts +1 -1
- package/dist/integrations/base/index.d.ts.map +1 -1
- package/dist/integrations/base/request-body.d.ts +18 -10
- package/dist/integrations/base/request-body.d.ts.map +1 -1
- package/dist/integrations/base/request-body.js +131 -215
- package/dist/integrations/base/request-body.js.map +1 -1
- package/dist/integrations/base/request-body.test.js +306 -164
- package/dist/integrations/base/request-body.test.js.map +1 -1
- package/dist/integrations/base/rest-api-client-base.d.ts.map +1 -1
- package/dist/integrations/base/rest-api-client-base.js +13 -26
- package/dist/integrations/base/rest-api-client-base.js.map +1 -1
- package/dist/integrations/base/rest-api-integration-client.d.ts.map +1 -1
- package/dist/integrations/base/rest-api-integration-client.js +2 -5
- package/dist/integrations/base/rest-api-integration-client.js.map +1 -1
- package/dist/integrations/base/types.d.ts +0 -8
- package/dist/integrations/base/types.d.ts.map +1 -1
- package/dist/integrations/cosmosdb/client.d.ts.map +1 -1
- package/dist/integrations/cosmosdb/client.js +2 -5
- package/dist/integrations/cosmosdb/client.js.map +1 -1
- package/dist/integrations/documentation-resolver.test.js +88 -2
- package/dist/integrations/documentation-resolver.test.js.map +1 -1
- package/dist/integrations/documentation.test.js +10 -0
- package/dist/integrations/documentation.test.js.map +1 -1
- package/dist/integrations/dynamodb/client.d.ts +6 -2
- package/dist/integrations/dynamodb/client.d.ts.map +1 -1
- package/dist/integrations/dynamodb/client.js +85 -15
- package/dist/integrations/dynamodb/client.js.map +1 -1
- package/dist/integrations/dynamodb/client.test.d.ts +8 -0
- package/dist/integrations/dynamodb/client.test.d.ts.map +1 -0
- package/dist/integrations/dynamodb/client.test.js +198 -0
- package/dist/integrations/dynamodb/client.test.js.map +1 -0
- package/dist/integrations/dynamodb/index.d.ts +1 -1
- package/dist/integrations/dynamodb/index.d.ts.map +1 -1
- package/dist/integrations/dynamodb/index.js.map +1 -1
- package/dist/integrations/dynamodb/types.d.ts +27 -1
- package/dist/integrations/dynamodb/types.d.ts.map +1 -1
- package/dist/integrations/gcs/client.d.ts.map +1 -1
- package/dist/integrations/gcs/client.js +2 -5
- package/dist/integrations/gcs/client.js.map +1 -1
- package/dist/integrations/gsheets/client.d.ts.map +1 -1
- package/dist/integrations/gsheets/client.js +2 -5
- package/dist/integrations/gsheets/client.js.map +1 -1
- package/dist/integrations/index.d.ts +1 -1
- package/dist/integrations/index.d.ts.map +1 -1
- package/dist/integrations/index.js.map +1 -1
- package/dist/integrations/mongodb/client.d.ts.map +1 -1
- package/dist/integrations/mongodb/client.js +2 -5
- package/dist/integrations/mongodb/client.js.map +1 -1
- package/dist/integrations/restapiintegration/client.body-types.test.js +107 -25
- package/dist/integrations/restapiintegration/client.body-types.test.js.map +1 -1
- package/dist/integrations/restapiintegration/client.test.js +16 -0
- package/dist/integrations/restapiintegration/client.test.js.map +1 -1
- package/dist/integrations/s3/client.d.ts.map +1 -1
- package/dist/integrations/s3/client.js +2 -5
- package/dist/integrations/s3/client.js.map +1 -1
- package/dist/integrations/salesforce/client.d.ts.map +1 -1
- package/dist/integrations/salesforce/client.js +2 -5
- package/dist/integrations/salesforce/client.js.map +1 -1
- package/dist/integrations/slack/client.d.ts.map +1 -1
- package/dist/integrations/slack/client.js +4 -13
- package/dist/integrations/slack/client.js.map +1 -1
- package/dist/integrations/slack/client.test.js +2 -5
- package/dist/integrations/slack/client.test.js.map +1 -1
- package/dist/integrations/utils.d.ts +8 -3
- package/dist/integrations/utils.d.ts.map +1 -1
- package/dist/integrations/utils.js +42 -1
- package/dist/integrations/utils.js.map +1 -1
- package/dist/runtime/context.d.ts +6 -0
- package/dist/runtime/context.d.ts.map +1 -1
- package/dist/runtime/context.js +2 -1
- package/dist/runtime/context.js.map +1 -1
- package/dist/runtime/executor.d.ts +6 -0
- package/dist/runtime/executor.d.ts.map +1 -1
- package/dist/runtime/executor.js +1 -0
- package/dist/runtime/executor.js.map +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/errors.test.ts +257 -0
- package/src/errors.ts +289 -46
- package/src/index.ts +1 -0
- package/src/integrations/anthropic/README.md +12 -59
- package/src/integrations/anthropic/types.ts +1 -1
- package/src/integrations/base/decode-worker-binary-response.ts +5 -7
- package/src/integrations/base/index.ts +0 -5
- package/src/integrations/base/request-body.test.ts +358 -195
- package/src/integrations/base/request-body.ts +170 -326
- package/src/integrations/base/rest-api-client-base.ts +22 -31
- package/src/integrations/base/rest-api-integration-client.ts +8 -7
- package/src/integrations/base/types.ts +0 -18
- package/src/integrations/box/README.md +4 -60
- package/src/integrations/cosmosdb/client.ts +8 -7
- package/src/integrations/documentation-resolver.test.ts +108 -2
- package/src/integrations/documentation.test.ts +12 -0
- package/src/integrations/dropbox/README.md +1 -71
- package/src/integrations/dropbox/docs.manifest.json +10 -1
- package/src/integrations/dropbox/overlays/upload-unsupported.md +3 -0
- package/src/integrations/dropbox/overlays/upload.md +89 -0
- package/src/integrations/dynamodb/README.md +33 -12
- package/src/integrations/dynamodb/client.test.ts +254 -0
- package/src/integrations/dynamodb/client.ts +129 -23
- package/src/integrations/dynamodb/index.ts +5 -1
- package/src/integrations/dynamodb/types.ts +33 -1
- package/src/integrations/elasticsearch/README.md +0 -80
- package/src/integrations/elasticsearch/docs.manifest.json +10 -1
- package/src/integrations/elasticsearch/overlays/bulk-unsupported.md +3 -0
- package/src/integrations/elasticsearch/overlays/bulk.md +80 -0
- package/src/integrations/gcs/client.ts +8 -7
- package/src/integrations/googledrive/README.md +10 -36
- package/src/integrations/gsheets/client.ts +8 -7
- package/src/integrations/index.ts +1 -0
- package/src/integrations/jira/README.md +0 -26
- package/src/integrations/jira/docs.manifest.json +10 -1
- package/src/integrations/jira/overlays/attachments-unsupported.md +3 -0
- package/src/integrations/jira/overlays/attachments.md +30 -0
- package/src/integrations/mongodb/client.ts +8 -7
- package/src/integrations/restapiintegration/client.body-types.test.ts +135 -26
- package/src/integrations/restapiintegration/client.test.ts +23 -0
- package/src/integrations/restapiintegration/docs.manifest.json +2 -2
- package/src/integrations/restapiintegration/overlays/request-body-types.md +26 -17
- package/src/integrations/s3/client.ts +8 -7
- package/src/integrations/salesforce/client.ts +9 -7
- package/src/integrations/slack/client.test.ts +2 -5
- package/src/integrations/slack/client.ts +13 -19
- package/src/integrations/stabilityai/README.md +0 -87
- package/src/integrations/stabilityai/docs.manifest.json +10 -1
- package/src/integrations/stabilityai/overlays/image-uploads-unsupported.md +3 -0
- package/src/integrations/stabilityai/overlays/image-uploads.md +120 -0
- package/src/integrations/stripe/README.md +0 -201
- package/src/integrations/stripe/docs.manifest.json +10 -1
- package/src/integrations/stripe/overlays/request-bodies-unsupported.md +3 -0
- package/src/integrations/stripe/overlays/request-bodies.md +204 -0
- package/src/integrations/utils.ts +51 -1
- package/src/runtime/context.ts +9 -0
- package/src/runtime/executor.ts +8 -0
- package/src/types.ts +11 -0
|
@@ -3,44 +3,38 @@ import { z } from "zod";
|
|
|
3
3
|
|
|
4
4
|
import type { Property } from "@superblocksteam/types/dist/src/common/v1/plugin_pb";
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { findBinaryDataPath, isBinaryData } from "../../errors.js";
|
|
7
|
+
import { describeOptionValue, validationError } from "../utils.js";
|
|
7
8
|
import { REST_API_BODY_TYPES } from "./types.js";
|
|
8
|
-
import type { BinaryBody } from "./types.js";
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const REQUEST_BODY_SIZE_ERROR_MESSAGE =
|
|
13
|
-
"request bodies must be 20 MiB or smaller because agent requests have a 28 MiB serialized size cap";
|
|
10
|
+
type BinaryBody = ArrayBuffer | ArrayBufferView;
|
|
14
11
|
|
|
15
12
|
type RequestBodyWireType =
|
|
16
13
|
| "jsonBody"
|
|
17
14
|
| "rawBody"
|
|
18
|
-
| "formData"
|
|
19
15
|
| "formDataBase64"
|
|
20
16
|
| "binaryBody";
|
|
21
|
-
type InjectedHeader = { readonly key: string; readonly value: string };
|
|
22
17
|
|
|
23
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Fields safe to spread directly onto the proto `RestApiRequest`. Never
|
|
20
|
+
* carries `contentType`: that is a header directive, not a wire field, and
|
|
21
|
+
* lives on `EncodedRequestBody` instead so a caller cannot spread it onto
|
|
22
|
+
* the wire message by mistake.
|
|
23
|
+
*/
|
|
24
|
+
export type RequestBodyWireFields = {
|
|
24
25
|
readonly body?: string;
|
|
25
|
-
readonly formData?:
|
|
26
|
+
readonly formData?: PartialMessage<Property>[];
|
|
26
27
|
readonly bodyType?: RequestBodyWireType;
|
|
27
|
-
readonly injectedHeaders: readonly InjectedHeader[];
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
readonly kind: "file";
|
|
39
|
-
readonly key: string;
|
|
40
|
-
readonly value: string | BinaryBody;
|
|
41
|
-
readonly filename: string;
|
|
42
|
-
readonly byteLength: number;
|
|
43
|
-
};
|
|
30
|
+
/**
|
|
31
|
+
* The encoder's full result: wire fields plus an optional Content-Type the
|
|
32
|
+
* caller must push onto request headers itself.
|
|
33
|
+
*/
|
|
34
|
+
export type EncodedRequestBody = {
|
|
35
|
+
readonly wire: RequestBodyWireFields;
|
|
36
|
+
readonly contentType?: string;
|
|
37
|
+
};
|
|
44
38
|
|
|
45
39
|
const requestBodyTypeSchema = z.enum(REST_API_BODY_TYPES);
|
|
46
40
|
|
|
@@ -48,51 +42,85 @@ export function encodeRequestBody(input: {
|
|
|
48
42
|
body: unknown;
|
|
49
43
|
bodyType?: unknown;
|
|
50
44
|
headers?: Record<string, string>;
|
|
51
|
-
}):
|
|
45
|
+
}): EncodedRequestBody {
|
|
52
46
|
const bodyTypeResult = requestBodyTypeSchema.safeParse(
|
|
53
|
-
input.bodyType
|
|
47
|
+
input.bodyType === undefined ? "json" : input.bodyType,
|
|
54
48
|
);
|
|
55
49
|
if (!bodyTypeResult.success) {
|
|
56
|
-
throw
|
|
57
|
-
`Unsupported bodyType ${
|
|
58
|
-
|
|
50
|
+
throw validationError(
|
|
51
|
+
`Unsupported bodyType ${describeOptionValue(input.bodyType)} - expected one of: ${REST_API_BODY_TYPES.join(", ")}`,
|
|
52
|
+
["bodyType"],
|
|
59
53
|
);
|
|
60
54
|
}
|
|
61
55
|
|
|
62
56
|
const bodyType = bodyTypeResult.data;
|
|
63
57
|
if (input.body === undefined) {
|
|
64
|
-
if (bodyType === "json") return {
|
|
65
|
-
throw validationError(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
["body"],
|
|
69
|
-
);
|
|
58
|
+
if (bodyType === "json") return { wire: {} };
|
|
59
|
+
throw validationError(`bodyType "${bodyType}" requires a request body`, [
|
|
60
|
+
"body",
|
|
61
|
+
]);
|
|
70
62
|
}
|
|
71
63
|
|
|
72
64
|
switch (bodyType) {
|
|
73
65
|
case "json":
|
|
74
|
-
return encodeJsonBody(input.body);
|
|
66
|
+
return { wire: encodeJsonBody(input.body) };
|
|
75
67
|
case "raw":
|
|
76
|
-
return encodeRawBody(input.body);
|
|
68
|
+
return { wire: encodeRawBody(input.body) };
|
|
77
69
|
case "formUrlencoded":
|
|
78
70
|
return encodeFormUrlencodedBody(input.body, input.headers);
|
|
79
71
|
case "multipartForm":
|
|
80
|
-
return encodeMultipartFormBody(input.body, input.headers);
|
|
72
|
+
return { wire: encodeMultipartFormBody(input.body, input.headers) };
|
|
81
73
|
case "binary":
|
|
82
74
|
return encodeBinaryBody(input.body, input.headers);
|
|
83
|
-
default: {
|
|
84
|
-
const _exhaustive: never = bodyType;
|
|
85
|
-
return _exhaustive;
|
|
86
|
-
}
|
|
87
75
|
}
|
|
88
76
|
}
|
|
89
77
|
|
|
90
|
-
|
|
91
|
-
|
|
78
|
+
// JSON.stringify's own error text is unbounded: a circular-structure message
|
|
79
|
+
// embeds the caller's full property path and constructor names, and a
|
|
80
|
+
// throwing toJSON()/getter contributes an arbitrary caller-authored string.
|
|
81
|
+
// Every other message in this module goes through describeOptionValue's
|
|
82
|
+
// bounded conventions; classify the failure instead of echoing its text.
|
|
83
|
+
function describeJsonSerializationFailure(error: unknown): string {
|
|
84
|
+
if (
|
|
85
|
+
error instanceof TypeError &&
|
|
86
|
+
/circular (structure|reference)/i.test(error.message)
|
|
87
|
+
) {
|
|
88
|
+
return "the body contains a circular reference";
|
|
89
|
+
}
|
|
90
|
+
if (error instanceof TypeError && /BigInt/i.test(error.message)) {
|
|
91
|
+
return "the body contains a BigInt value";
|
|
92
|
+
}
|
|
93
|
+
return "a nested value's toJSON, toString, or getter threw while serializing the body";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function encodeJsonBody(body: unknown): RequestBodyWireFields {
|
|
97
|
+
if (isBinaryData(body)) {
|
|
98
|
+
throw validationError('Binary request bodies require bodyType: "binary"', [
|
|
99
|
+
"body",
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
// The walk itself only ever throws MAX_BINARY_SCAN_DEPTH's RangeError
|
|
103
|
+
// (see errors.ts); anything else (a throwing getter, an unusual toJSON)
|
|
104
|
+
// is left for JSON.stringify to judge below, which reports the same
|
|
105
|
+
// failure as a serialization error. A depth-capped walk fails closed
|
|
106
|
+
// instead: treat the body as containing binary at an unknown location
|
|
107
|
+
// rather than assume none, because JSON.stringify does not throw on a
|
|
108
|
+
// Uint8Array/Buffer/typed array -- the exact shape this check exists to
|
|
109
|
+
// catch -- it silently serializes them into digit-keyed garbage.
|
|
110
|
+
let binaryPath: (string | number)[] | undefined;
|
|
111
|
+
try {
|
|
112
|
+
binaryPath = findBinaryDataPath(body, isBinaryData);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
binaryPath = error instanceof RangeError ? [] : undefined;
|
|
115
|
+
}
|
|
116
|
+
if (binaryPath !== undefined) {
|
|
117
|
+
const location =
|
|
118
|
+
binaryPath.length > 0
|
|
119
|
+
? ` at ${describeOptionValue(binaryPath.join("."))}`
|
|
120
|
+
: "";
|
|
92
121
|
throw validationError(
|
|
93
|
-
|
|
94
|
-
body,
|
|
95
|
-
["body"],
|
|
122
|
+
`bodyType "json" request body contains binary data${location}; send file parts with bodyType: "multipartForm" as { value, filename }, or send bytes alone with bodyType: "binary"`,
|
|
123
|
+
["body", ...binaryPath],
|
|
96
124
|
);
|
|
97
125
|
}
|
|
98
126
|
let serialized: string | undefined;
|
|
@@ -100,87 +128,99 @@ function encodeJsonBody(body: unknown): RequestBodyWirePatch {
|
|
|
100
128
|
serialized = JSON.stringify(body);
|
|
101
129
|
} catch (error) {
|
|
102
130
|
throw validationError(
|
|
103
|
-
`
|
|
104
|
-
body,
|
|
131
|
+
`bodyType "json" request body is not JSON-serializable: ${describeJsonSerializationFailure(error)}`,
|
|
105
132
|
["body"],
|
|
106
133
|
);
|
|
107
134
|
}
|
|
108
135
|
if (serialized === undefined) {
|
|
109
136
|
throw validationError(
|
|
110
|
-
"
|
|
111
|
-
body,
|
|
137
|
+
`bodyType "json" cannot serialize a ${typeof body} request body`,
|
|
112
138
|
["body"],
|
|
113
139
|
);
|
|
114
140
|
}
|
|
115
|
-
|
|
116
|
-
return { body: serialized, bodyType: "jsonBody", injectedHeaders: [] };
|
|
141
|
+
return { body: serialized, bodyType: "jsonBody" };
|
|
117
142
|
}
|
|
118
143
|
|
|
119
|
-
function encodeRawBody(body: unknown):
|
|
144
|
+
function encodeRawBody(body: unknown): RequestBodyWireFields {
|
|
120
145
|
if (typeof body !== "string" || body.length === 0) {
|
|
121
146
|
throw validationError(
|
|
122
147
|
'bodyType "raw" requires a non-empty string request body',
|
|
123
|
-
body,
|
|
124
148
|
["body"],
|
|
125
149
|
);
|
|
126
150
|
}
|
|
127
|
-
|
|
128
|
-
|
|
151
|
+
return { body, bodyType: "rawBody" };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// A field's value may be a scalar or an array; either way, undefined items
|
|
155
|
+
// (an explicitly-undefined optional field, or an undefined array element)
|
|
156
|
+
// are omitted rather than encoded, matching JSON.stringify's default-body
|
|
157
|
+
// semantics. Shared by encodeFormUrlencodedBody and encodeMultipartFormBody.
|
|
158
|
+
function definedArrayValues(value: unknown): unknown[] {
|
|
159
|
+
return (Array.isArray(value) ? value : [value]).filter(
|
|
160
|
+
(item) => item !== undefined,
|
|
161
|
+
);
|
|
129
162
|
}
|
|
130
163
|
|
|
131
164
|
function encodeFormUrlencodedBody(
|
|
132
165
|
body: unknown,
|
|
133
166
|
headers: Record<string, string> | undefined,
|
|
134
|
-
):
|
|
167
|
+
): EncodedRequestBody {
|
|
135
168
|
if (!isPlainRecord(body)) {
|
|
136
169
|
throw validationError(
|
|
137
170
|
'bodyType "formUrlencoded" requires a plain object request body',
|
|
138
|
-
body,
|
|
139
171
|
["body"],
|
|
140
172
|
);
|
|
141
173
|
}
|
|
142
|
-
const
|
|
174
|
+
const pairs: string[] = [];
|
|
143
175
|
for (const [key, value] of Object.entries(body)) {
|
|
144
176
|
if (value === undefined) continue;
|
|
145
177
|
if (key.length === 0) {
|
|
146
178
|
throw validationError(
|
|
147
179
|
"formUrlencoded body keys must be non-empty strings",
|
|
148
|
-
body,
|
|
149
180
|
["body", key],
|
|
150
181
|
);
|
|
151
182
|
}
|
|
152
|
-
|
|
183
|
+
const encodedKey = formUrlencode(key, key);
|
|
184
|
+
for (const item of definedArrayValues(value)) {
|
|
185
|
+
pairs.push(
|
|
186
|
+
`${encodedKey}=${formUrlencode(formUrlencodedPrimitiveToString(key, item), key)}`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
153
189
|
}
|
|
154
|
-
|
|
155
|
-
if (encodedBody.length === 0) {
|
|
190
|
+
if (pairs.length === 0) {
|
|
156
191
|
throw validationError(
|
|
157
192
|
"formUrlencoded body must encode at least one field",
|
|
158
|
-
body,
|
|
159
193
|
["body"],
|
|
160
194
|
);
|
|
161
195
|
}
|
|
162
|
-
validateRequestByteLength(utf8ByteLength(encodedBody), ["body"]);
|
|
163
196
|
return {
|
|
164
|
-
body:
|
|
165
|
-
|
|
166
|
-
injectedHeaders: contentTypeHeaderIfMissing(
|
|
197
|
+
wire: { body: pairs.join("&"), bodyType: "rawBody" },
|
|
198
|
+
contentType: contentTypeIfMissing(
|
|
167
199
|
headers,
|
|
168
200
|
"application/x-www-form-urlencoded",
|
|
169
201
|
),
|
|
170
202
|
};
|
|
171
203
|
}
|
|
172
204
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
205
|
+
// The WASM sdk-api sandbox has no URLSearchParams global, so encode by hand.
|
|
206
|
+
// encodeURIComponent's unreserved set leaves `! ' ( ) ~` unescaped, but the
|
|
207
|
+
// WHATWG application/x-www-form-urlencoded safe set (what URLSearchParams
|
|
208
|
+
// produces) escapes those five; only alphanumerics, `* - . _`, and space
|
|
209
|
+
// (as `+`) stay literal.
|
|
210
|
+
function formUrlencode(value: string, key: string): string {
|
|
211
|
+
try {
|
|
212
|
+
return encodeURIComponent(value)
|
|
213
|
+
.replace(/%20/g, "+")
|
|
214
|
+
.replace(
|
|
215
|
+
/[!'()~]/g,
|
|
216
|
+
(char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`,
|
|
217
|
+
);
|
|
218
|
+
} catch {
|
|
219
|
+
throw validationError(
|
|
220
|
+
`formUrlencoded field ${describeOptionValue(key)} contains a lone surrogate and cannot be percent-encoded`,
|
|
221
|
+
["body", key],
|
|
222
|
+
);
|
|
182
223
|
}
|
|
183
|
-
params.append(key, formUrlencodedPrimitiveToString(key, value));
|
|
184
224
|
}
|
|
185
225
|
|
|
186
226
|
function formUrlencodedPrimitiveToString(key: string, value: unknown): string {
|
|
@@ -189,14 +229,12 @@ function formUrlencodedPrimitiveToString(key: string, value: unknown): string {
|
|
|
189
229
|
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
190
230
|
if (typeof value === "object" && value !== null) {
|
|
191
231
|
throw validationError(
|
|
192
|
-
`formUrlencoded field
|
|
193
|
-
value,
|
|
232
|
+
`formUrlencoded field ${describeOptionValue(key)} is nested; flatten it using bracket notation such as "metadata[order_id]"`,
|
|
194
233
|
["body", key],
|
|
195
234
|
);
|
|
196
235
|
}
|
|
197
236
|
throw validationError(
|
|
198
|
-
`formUrlencoded field
|
|
199
|
-
value,
|
|
237
|
+
`formUrlencoded field ${describeOptionValue(key)} must be a string, finite number, boolean, or array of those values`,
|
|
200
238
|
["body", key],
|
|
201
239
|
);
|
|
202
240
|
}
|
|
@@ -204,96 +242,74 @@ function formUrlencodedPrimitiveToString(key: string, value: unknown): string {
|
|
|
204
242
|
function encodeBinaryBody(
|
|
205
243
|
body: unknown,
|
|
206
244
|
headers: Record<string, string> | undefined,
|
|
207
|
-
):
|
|
208
|
-
if (!
|
|
245
|
+
): EncodedRequestBody {
|
|
246
|
+
if (!isBinaryData(body)) {
|
|
209
247
|
throw validationError(
|
|
210
248
|
'bodyType "binary" requires an ArrayBuffer or ArrayBufferView request body',
|
|
211
|
-
body,
|
|
212
249
|
["body"],
|
|
213
250
|
);
|
|
214
251
|
}
|
|
215
|
-
|
|
216
|
-
|
|
252
|
+
if (body.byteLength === 0) {
|
|
253
|
+
throw validationError("binary request bodies must be non-empty", ["body"]);
|
|
254
|
+
}
|
|
217
255
|
return {
|
|
218
|
-
body:
|
|
219
|
-
|
|
220
|
-
injectedHeaders: contentTypeHeaderIfMissing(
|
|
221
|
-
headers,
|
|
222
|
-
"application/octet-stream",
|
|
223
|
-
),
|
|
256
|
+
wire: { body: toBuffer(body).toString("base64"), bodyType: "binaryBody" },
|
|
257
|
+
contentType: contentTypeIfMissing(headers, "application/octet-stream"),
|
|
224
258
|
};
|
|
225
259
|
}
|
|
226
260
|
|
|
227
261
|
function encodeMultipartFormBody(
|
|
228
262
|
body: unknown,
|
|
229
263
|
headers: Record<string, string> | undefined,
|
|
230
|
-
):
|
|
264
|
+
): RequestBodyWireFields {
|
|
231
265
|
if (hasHeader(headers, "content-type")) {
|
|
232
266
|
throw validationError(
|
|
233
267
|
"multipartForm bodies cannot provide Content-Type because the multipart boundary is generated at execution time",
|
|
234
|
-
body,
|
|
235
268
|
["headers", "Content-Type"],
|
|
236
269
|
);
|
|
237
270
|
}
|
|
238
|
-
if (!isPlainRecord(body)
|
|
239
|
-
throw validationError(
|
|
240
|
-
'bodyType "multipartForm" requires a plain non-empty object request body',
|
|
241
|
-
body,
|
|
242
|
-
["body"],
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
const parts = Object.entries(body)
|
|
246
|
-
.filter(([_key, value]) => value !== undefined)
|
|
247
|
-
.map(([key, value]) => parseMultipartPart(key, value));
|
|
248
|
-
if (parts.length === 0) {
|
|
271
|
+
if (!isPlainRecord(body)) {
|
|
249
272
|
throw validationError(
|
|
250
|
-
"multipartForm
|
|
251
|
-
body,
|
|
273
|
+
'bodyType "multipartForm" requires a plain object request body',
|
|
252
274
|
["body"],
|
|
253
275
|
);
|
|
254
276
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
(part) => part.kind === "file" && isBinaryBody(part.value),
|
|
277
|
+
const formData = Object.entries(body).flatMap(([key, value]) =>
|
|
278
|
+
definedArrayValues(value).map((item) => parseMultipartPart(key, item)),
|
|
258
279
|
);
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
280
|
+
if (formData.length === 0) {
|
|
281
|
+
throw validationError("multipartForm body must encode at least one field", [
|
|
282
|
+
"body",
|
|
283
|
+
]);
|
|
284
|
+
}
|
|
285
|
+
return { bodyType: "formDataBase64", formData };
|
|
264
286
|
}
|
|
265
287
|
|
|
266
|
-
function parseMultipartPart(
|
|
288
|
+
function parseMultipartPart(
|
|
289
|
+
key: string,
|
|
290
|
+
value: unknown,
|
|
291
|
+
): PartialMessage<Property> {
|
|
267
292
|
if (key.length === 0)
|
|
268
|
-
throw validationError(
|
|
269
|
-
"
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
);
|
|
293
|
+
throw validationError("multipartForm body keys must be non-empty strings", [
|
|
294
|
+
"body",
|
|
295
|
+
key,
|
|
296
|
+
]);
|
|
273
297
|
if (
|
|
274
298
|
typeof value === "string" ||
|
|
275
|
-
typeof value === "
|
|
276
|
-
typeof value === "
|
|
299
|
+
typeof value === "boolean" ||
|
|
300
|
+
(typeof value === "number" && Number.isFinite(value))
|
|
277
301
|
) {
|
|
278
|
-
|
|
279
|
-
return {
|
|
280
|
-
kind: "field",
|
|
281
|
-
key,
|
|
282
|
-
value: stringValue,
|
|
283
|
-
byteLength: utf8ByteLength(stringValue),
|
|
284
|
-
};
|
|
302
|
+
return { key, value: String(value) };
|
|
285
303
|
}
|
|
286
|
-
if (
|
|
304
|
+
if (isBinaryData(value)) {
|
|
287
305
|
throw validationError(
|
|
288
|
-
`multipartForm binary field
|
|
289
|
-
value,
|
|
306
|
+
`multipartForm binary field ${describeOptionValue(key)} must be wrapped as { value, filename }`,
|
|
290
307
|
["body", key],
|
|
291
308
|
);
|
|
292
309
|
}
|
|
293
310
|
if (!isPlainRecord(value)) {
|
|
294
311
|
throw validationError(
|
|
295
|
-
`multipartForm field
|
|
296
|
-
value,
|
|
312
|
+
`multipartForm field ${describeOptionValue(key)} must be a primitive value or { value, filename } file part`,
|
|
297
313
|
["body", key],
|
|
298
314
|
);
|
|
299
315
|
}
|
|
@@ -305,8 +321,7 @@ function parseMultipartPart(key: string, value: unknown): MultipartPart {
|
|
|
305
321
|
);
|
|
306
322
|
if (missingKeys.length > 0) {
|
|
307
323
|
throw validationError(
|
|
308
|
-
`multipartForm file part
|
|
309
|
-
value,
|
|
324
|
+
`multipartForm file part ${describeOptionValue(key)} is missing required ${pluralize("key", missingKeys)}: ${missingKeys.join(", ")}`,
|
|
310
325
|
["body", key],
|
|
311
326
|
);
|
|
312
327
|
}
|
|
@@ -314,73 +329,29 @@ function parseMultipartPart(key: string, value: unknown): MultipartPart {
|
|
|
314
329
|
(partKey) => partKey !== "value" && partKey !== "filename",
|
|
315
330
|
);
|
|
316
331
|
if (unexpectedKeys.length > 0) {
|
|
332
|
+
const shown = unexpectedKeys.slice(0, 5).map(describeOptionValue);
|
|
333
|
+
const suffix = unexpectedKeys.length > 5 ? ", ..." : "";
|
|
317
334
|
throw validationError(
|
|
318
|
-
`multipartForm file part
|
|
319
|
-
value,
|
|
335
|
+
`multipartForm file part ${describeOptionValue(key)} has unexpected ${pluralize("key", unexpectedKeys)}: ${shown.join(", ")}${suffix}`,
|
|
320
336
|
["body", key],
|
|
321
337
|
);
|
|
322
338
|
}
|
|
323
339
|
if (typeof value.filename !== "string" || value.filename.length === 0) {
|
|
324
340
|
throw validationError(
|
|
325
|
-
`multipartForm file part
|
|
326
|
-
value,
|
|
341
|
+
`multipartForm file part ${describeOptionValue(key)} requires a non-empty filename`,
|
|
327
342
|
["body", key, "filename"],
|
|
328
343
|
);
|
|
329
344
|
}
|
|
330
|
-
if (typeof value.value !== "string" && !
|
|
345
|
+
if (typeof value.value !== "string" && !isBinaryData(value.value)) {
|
|
331
346
|
throw validationError(
|
|
332
|
-
`multipartForm file part
|
|
333
|
-
value,
|
|
347
|
+
`multipartForm file part ${describeOptionValue(key)} value must be a string or binary body`,
|
|
334
348
|
["body", key, "value"],
|
|
335
349
|
);
|
|
336
350
|
}
|
|
337
|
-
const byteLength = isBinaryBody(value.value)
|
|
338
|
-
? binaryBodyToBytes(value.value).byteLength
|
|
339
|
-
: utf8ByteLength(value.value);
|
|
340
|
-
validateMultipartFilePartByteLength(byteLength, value.value, [
|
|
341
|
-
"body",
|
|
342
|
-
key,
|
|
343
|
-
"value",
|
|
344
|
-
]);
|
|
345
351
|
return {
|
|
346
|
-
kind: "file",
|
|
347
352
|
key,
|
|
348
|
-
value: value.value,
|
|
349
|
-
filename: value.filename,
|
|
350
|
-
byteLength,
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function totalMultipartPartBytes(parts: MultipartPart[]): number {
|
|
355
|
-
return parts.reduce((total, part) => total + part.byteLength, 0);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function multipartPartToWire(
|
|
359
|
-
part: MultipartPart,
|
|
360
|
-
encodeFilePartsAsBase64: boolean,
|
|
361
|
-
): PartialMessage<Property> {
|
|
362
|
-
if (part.kind === "field") return { key: part.key, value: part.value };
|
|
363
|
-
if (!encodeFilePartsAsBase64) {
|
|
364
|
-
if (typeof part.value !== "string") {
|
|
365
|
-
throw validationError(
|
|
366
|
-
"multipartForm binary file parts require formDataBase64 encoding",
|
|
367
|
-
part.value,
|
|
368
|
-
["body", part.key, "value"],
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
return {
|
|
372
|
-
key: part.key,
|
|
373
|
-
value: part.value,
|
|
374
|
-
file: { filename: part.filename },
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
const bytes = isBinaryBody(part.value)
|
|
378
|
-
? binaryBodyToBytes(part.value)
|
|
379
|
-
: textToUtf8Bytes(part.value);
|
|
380
|
-
return {
|
|
381
|
-
key: part.key,
|
|
382
|
-
value: base64Encode(bytes),
|
|
383
|
-
file: { filename: part.filename },
|
|
353
|
+
value: toBuffer(value.value).toString("base64"),
|
|
354
|
+
file: { filename: value.filename },
|
|
384
355
|
};
|
|
385
356
|
}
|
|
386
357
|
|
|
@@ -391,58 +362,17 @@ function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
|
391
362
|
return prototype === Object.prototype || prototype === null;
|
|
392
363
|
}
|
|
393
364
|
|
|
394
|
-
function
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
function binaryBodyToBytes(value: BinaryBody): Uint8Array {
|
|
399
|
-
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
400
|
-
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
365
|
+
function toBuffer(value: string | BinaryBody): Buffer {
|
|
366
|
+
if (typeof value === "string") return Buffer.from(value, "utf8");
|
|
367
|
+
if (value instanceof ArrayBuffer) return Buffer.from(value);
|
|
368
|
+
return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
401
369
|
}
|
|
402
370
|
|
|
403
|
-
function
|
|
404
|
-
byteLength: number,
|
|
405
|
-
data: unknown,
|
|
406
|
-
path: (string | number)[],
|
|
407
|
-
): void {
|
|
408
|
-
if (byteLength === 0)
|
|
409
|
-
throw validationError(
|
|
410
|
-
"binary request bodies must be non-empty",
|
|
411
|
-
data,
|
|
412
|
-
path,
|
|
413
|
-
);
|
|
414
|
-
validateRequestByteLength(byteLength, path, data);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function validateMultipartFilePartByteLength(
|
|
418
|
-
byteLength: number,
|
|
419
|
-
data: string | BinaryBody,
|
|
420
|
-
path: (string | number)[],
|
|
421
|
-
): void {
|
|
422
|
-
if (byteLength <= MAX_BINARY_BODY_BYTES) return;
|
|
423
|
-
throw validationError(
|
|
424
|
-
REQUEST_BODY_SIZE_ERROR_MESSAGE,
|
|
425
|
-
isBinaryBody(data) ? data : { byteLength },
|
|
426
|
-
path,
|
|
427
|
-
);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function validateRequestByteLength(
|
|
431
|
-
byteLength: number,
|
|
432
|
-
path: (string | number)[],
|
|
433
|
-
data: unknown = { byteLength },
|
|
434
|
-
): void {
|
|
435
|
-
if (byteLength <= MAX_BINARY_BODY_BYTES) return;
|
|
436
|
-
throw validationError(REQUEST_BODY_SIZE_ERROR_MESSAGE, data, path);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
function contentTypeHeaderIfMissing(
|
|
371
|
+
function contentTypeIfMissing(
|
|
440
372
|
headers: Record<string, string> | undefined,
|
|
441
373
|
value: string,
|
|
442
|
-
):
|
|
443
|
-
return hasHeader(headers, "content-type")
|
|
444
|
-
? []
|
|
445
|
-
: [{ key: "Content-Type", value }];
|
|
374
|
+
): string | undefined {
|
|
375
|
+
return hasHeader(headers, "content-type") ? undefined : value;
|
|
446
376
|
}
|
|
447
377
|
|
|
448
378
|
function hasHeader(
|
|
@@ -457,92 +387,6 @@ function hasHeader(
|
|
|
457
387
|
);
|
|
458
388
|
}
|
|
459
389
|
|
|
460
|
-
function base64Encode(bytes: Uint8Array): string {
|
|
461
|
-
if (typeof Buffer !== "undefined")
|
|
462
|
-
return Buffer.from(
|
|
463
|
-
bytes.buffer,
|
|
464
|
-
bytes.byteOffset,
|
|
465
|
-
bytes.byteLength,
|
|
466
|
-
).toString("base64");
|
|
467
|
-
const alphabet =
|
|
468
|
-
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
469
|
-
let output = "";
|
|
470
|
-
for (let index = 0; index < bytes.length; index += 3) {
|
|
471
|
-
const first = bytes[index];
|
|
472
|
-
const second = bytes[index + 1];
|
|
473
|
-
const third = bytes[index + 2];
|
|
474
|
-
output += alphabet[first >> 2];
|
|
475
|
-
output += alphabet[((first & 0x03) << 4) | ((second ?? 0) >> 4)];
|
|
476
|
-
output +=
|
|
477
|
-
second === undefined
|
|
478
|
-
? "="
|
|
479
|
-
: alphabet[((second & 0x0f) << 2) | ((third ?? 0) >> 6)];
|
|
480
|
-
output += third === undefined ? "=" : alphabet[third & 0x3f];
|
|
481
|
-
}
|
|
482
|
-
return output;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
function textToUtf8Bytes(value: string): Uint8Array {
|
|
486
|
-
if (typeof TextEncoder !== "undefined")
|
|
487
|
-
return new TextEncoder().encode(value);
|
|
488
|
-
const bytes: number[] = [];
|
|
489
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
490
|
-
const codePoint = value.codePointAt(index);
|
|
491
|
-
if (codePoint === undefined) continue;
|
|
492
|
-
if (codePoint > 0xffff) index += 1;
|
|
493
|
-
appendCodePointAsUtf8(bytes, codePoint);
|
|
494
|
-
}
|
|
495
|
-
return Uint8Array.from(bytes);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
function utf8ByteLength(value: string): number {
|
|
499
|
-
let byteLength = 0;
|
|
500
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
501
|
-
const codePoint = value.codePointAt(index);
|
|
502
|
-
if (codePoint === undefined) continue;
|
|
503
|
-
if (codePoint > 0xffff) index += 1;
|
|
504
|
-
if (codePoint <= 0x7f) byteLength += 1;
|
|
505
|
-
else if (codePoint <= 0x7ff) byteLength += 2;
|
|
506
|
-
else if (codePoint <= 0xffff) byteLength += 3;
|
|
507
|
-
else byteLength += 4;
|
|
508
|
-
}
|
|
509
|
-
return byteLength;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
390
|
function pluralize(word: string, values: readonly unknown[]): string {
|
|
513
391
|
return values.length === 1 ? word : `${word}s`;
|
|
514
392
|
}
|
|
515
|
-
|
|
516
|
-
function appendCodePointAsUtf8(bytes: number[], codePoint: number): void {
|
|
517
|
-
if (codePoint <= 0x7f) bytes.push(codePoint);
|
|
518
|
-
else if (codePoint <= 0x7ff)
|
|
519
|
-
bytes.push(0xc0 | (codePoint >> 6), 0x80 | (codePoint & 0x3f));
|
|
520
|
-
else if (codePoint <= 0xffff)
|
|
521
|
-
bytes.push(
|
|
522
|
-
0xe0 | (codePoint >> 12),
|
|
523
|
-
0x80 | ((codePoint >> 6) & 0x3f),
|
|
524
|
-
0x80 | (codePoint & 0x3f),
|
|
525
|
-
);
|
|
526
|
-
else
|
|
527
|
-
bytes.push(
|
|
528
|
-
0xf0 | (codePoint >> 18),
|
|
529
|
-
0x80 | ((codePoint >> 12) & 0x3f),
|
|
530
|
-
0x80 | ((codePoint >> 6) & 0x3f),
|
|
531
|
-
0x80 | (codePoint & 0x3f),
|
|
532
|
-
);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
function validationError(
|
|
536
|
-
message: string,
|
|
537
|
-
data: unknown,
|
|
538
|
-
path: (string | number)[],
|
|
539
|
-
): RestApiValidationError {
|
|
540
|
-
return new RestApiValidationError(message, {
|
|
541
|
-
data,
|
|
542
|
-
zodError: new z.ZodError([{ code: z.ZodIssueCode.custom, message, path }]),
|
|
543
|
-
});
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
function errorMessage(error: unknown): string {
|
|
547
|
-
return error instanceof Error ? error.message : String(error);
|
|
548
|
-
}
|