@superblocksteam/sdk-api 0.0.12 → 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.
Files changed (125) hide show
  1. package/dist/errors.d.ts +36 -3
  2. package/dist/errors.d.ts.map +1 -1
  3. package/dist/errors.js +244 -8
  4. package/dist/errors.js.map +1 -1
  5. package/dist/errors.test.d.ts +2 -0
  6. package/dist/errors.test.d.ts.map +1 -0
  7. package/dist/errors.test.js +189 -0
  8. package/dist/errors.test.js.map +1 -0
  9. package/dist/integrations/anthropic/types.d.ts +1 -1
  10. package/dist/integrations/base/decode-worker-binary-response.d.ts.map +1 -1
  11. package/dist/integrations/base/decode-worker-binary-response.js +2 -5
  12. package/dist/integrations/base/decode-worker-binary-response.js.map +1 -1
  13. package/dist/integrations/base/index.d.ts +2 -1
  14. package/dist/integrations/base/index.d.ts.map +1 -1
  15. package/dist/integrations/base/index.js +1 -0
  16. package/dist/integrations/base/index.js.map +1 -1
  17. package/dist/integrations/base/request-body.d.ts +29 -0
  18. package/dist/integrations/base/request-body.d.ts.map +1 -0
  19. package/dist/integrations/base/request-body.js +238 -0
  20. package/dist/integrations/base/request-body.js.map +1 -0
  21. package/dist/integrations/base/request-body.test.d.ts +2 -0
  22. package/dist/integrations/base/request-body.test.d.ts.map +1 -0
  23. package/dist/integrations/base/request-body.test.js +466 -0
  24. package/dist/integrations/base/request-body.test.js.map +1 -0
  25. package/dist/integrations/base/rest-api-client-base.d.ts.map +1 -1
  26. package/dist/integrations/base/rest-api-client-base.js +15 -14
  27. package/dist/integrations/base/rest-api-client-base.js.map +1 -1
  28. package/dist/integrations/base/rest-api-integration-client.d.ts.map +1 -1
  29. package/dist/integrations/base/rest-api-integration-client.js +2 -5
  30. package/dist/integrations/base/rest-api-integration-client.js.map +1 -1
  31. package/dist/integrations/base/types.d.ts +32 -5
  32. package/dist/integrations/base/types.d.ts.map +1 -1
  33. package/dist/integrations/base/types.js +7 -0
  34. package/dist/integrations/base/types.js.map +1 -1
  35. package/dist/integrations/cosmosdb/client.d.ts.map +1 -1
  36. package/dist/integrations/cosmosdb/client.js +2 -5
  37. package/dist/integrations/cosmosdb/client.js.map +1 -1
  38. package/dist/integrations/documentation-resolver.test.js +106 -1
  39. package/dist/integrations/documentation-resolver.test.js.map +1 -1
  40. package/dist/integrations/documentation.test.js +10 -0
  41. package/dist/integrations/documentation.test.js.map +1 -1
  42. package/dist/integrations/dynamodb/client.d.ts.map +1 -1
  43. package/dist/integrations/dynamodb/client.js +2 -5
  44. package/dist/integrations/dynamodb/client.js.map +1 -1
  45. package/dist/integrations/gcs/client.d.ts.map +1 -1
  46. package/dist/integrations/gcs/client.js +2 -5
  47. package/dist/integrations/gcs/client.js.map +1 -1
  48. package/dist/integrations/gsheets/client.d.ts.map +1 -1
  49. package/dist/integrations/gsheets/client.js +2 -5
  50. package/dist/integrations/gsheets/client.js.map +1 -1
  51. package/dist/integrations/mongodb/client.d.ts.map +1 -1
  52. package/dist/integrations/mongodb/client.js +2 -5
  53. package/dist/integrations/mongodb/client.js.map +1 -1
  54. package/dist/integrations/restapiintegration/client.body-types.test.d.ts +2 -0
  55. package/dist/integrations/restapiintegration/client.body-types.test.d.ts.map +1 -0
  56. package/dist/integrations/restapiintegration/client.body-types.test.js +227 -0
  57. package/dist/integrations/restapiintegration/client.body-types.test.js.map +1 -0
  58. package/dist/integrations/restapiintegration/client.test.js +16 -0
  59. package/dist/integrations/restapiintegration/client.test.js.map +1 -1
  60. package/dist/integrations/s3/client.d.ts.map +1 -1
  61. package/dist/integrations/s3/client.js +2 -5
  62. package/dist/integrations/s3/client.js.map +1 -1
  63. package/dist/integrations/salesforce/client.d.ts.map +1 -1
  64. package/dist/integrations/salesforce/client.js +2 -5
  65. package/dist/integrations/salesforce/client.js.map +1 -1
  66. package/dist/integrations/slack/client.d.ts.map +1 -1
  67. package/dist/integrations/slack/client.js +4 -13
  68. package/dist/integrations/slack/client.js.map +1 -1
  69. package/dist/integrations/slack/client.test.js +20 -0
  70. package/dist/integrations/slack/client.test.js.map +1 -1
  71. package/dist/integrations/utils.d.ts +8 -3
  72. package/dist/integrations/utils.d.ts.map +1 -1
  73. package/dist/integrations/utils.js +42 -1
  74. package/dist/integrations/utils.js.map +1 -1
  75. package/package.json +3 -3
  76. package/src/errors.test.ts +257 -0
  77. package/src/errors.ts +304 -10
  78. package/src/integrations/anthropic/README.md +12 -59
  79. package/src/integrations/anthropic/types.ts +1 -1
  80. package/src/integrations/base/decode-worker-binary-response.ts +5 -7
  81. package/src/integrations/base/index.ts +2 -0
  82. package/src/integrations/base/request-body.test.ts +565 -0
  83. package/src/integrations/base/request-body.ts +392 -0
  84. package/src/integrations/base/rest-api-client-base.ts +24 -19
  85. package/src/integrations/base/rest-api-integration-client.ts +8 -7
  86. package/src/integrations/base/types.ts +41 -5
  87. package/src/integrations/box/README.md +4 -60
  88. package/src/integrations/cosmosdb/client.ts +8 -7
  89. package/src/integrations/documentation-resolver.test.ts +130 -1
  90. package/src/integrations/documentation.test.ts +12 -0
  91. package/src/integrations/dropbox/README.md +1 -71
  92. package/src/integrations/dropbox/docs.manifest.json +10 -1
  93. package/src/integrations/dropbox/overlays/upload-unsupported.md +3 -0
  94. package/src/integrations/dropbox/overlays/upload.md +89 -0
  95. package/src/integrations/dynamodb/client.ts +8 -7
  96. package/src/integrations/elasticsearch/README.md +0 -80
  97. package/src/integrations/elasticsearch/docs.manifest.json +10 -1
  98. package/src/integrations/elasticsearch/overlays/bulk-unsupported.md +3 -0
  99. package/src/integrations/elasticsearch/overlays/bulk.md +80 -0
  100. package/src/integrations/gcs/client.ts +8 -7
  101. package/src/integrations/googledrive/README.md +10 -36
  102. package/src/integrations/gsheets/client.ts +8 -7
  103. package/src/integrations/jira/README.md +0 -26
  104. package/src/integrations/jira/docs.manifest.json +10 -1
  105. package/src/integrations/jira/overlays/attachments-unsupported.md +3 -0
  106. package/src/integrations/jira/overlays/attachments.md +30 -0
  107. package/src/integrations/mongodb/client.ts +8 -7
  108. package/src/integrations/restapiintegration/client.body-types.test.ts +323 -0
  109. package/src/integrations/restapiintegration/client.test.ts +23 -0
  110. package/src/integrations/restapiintegration/docs.manifest.json +8 -0
  111. package/src/integrations/restapiintegration/overlays/request-body-types-unsupported.md +7 -0
  112. package/src/integrations/restapiintegration/overlays/request-body-types.md +115 -0
  113. package/src/integrations/s3/client.ts +8 -7
  114. package/src/integrations/salesforce/client.ts +9 -7
  115. package/src/integrations/slack/client.test.ts +30 -0
  116. package/src/integrations/slack/client.ts +13 -19
  117. package/src/integrations/stabilityai/README.md +0 -87
  118. package/src/integrations/stabilityai/docs.manifest.json +10 -1
  119. package/src/integrations/stabilityai/overlays/image-uploads-unsupported.md +3 -0
  120. package/src/integrations/stabilityai/overlays/image-uploads.md +120 -0
  121. package/src/integrations/stripe/README.md +0 -201
  122. package/src/integrations/stripe/docs.manifest.json +10 -1
  123. package/src/integrations/stripe/overlays/request-bodies-unsupported.md +3 -0
  124. package/src/integrations/stripe/overlays/request-bodies.md +204 -0
  125. package/src/integrations/utils.ts +51 -1
@@ -0,0 +1,392 @@
1
+ import type { PartialMessage } from "@bufbuild/protobuf";
2
+ import { z } from "zod";
3
+
4
+ import type { Property } from "@superblocksteam/types/dist/src/common/v1/plugin_pb";
5
+
6
+ import { findBinaryDataPath, isBinaryData } from "../../errors.js";
7
+ import { describeOptionValue, validationError } from "../utils.js";
8
+ import { REST_API_BODY_TYPES } from "./types.js";
9
+
10
+ type BinaryBody = ArrayBuffer | ArrayBufferView;
11
+
12
+ type RequestBodyWireType =
13
+ | "jsonBody"
14
+ | "rawBody"
15
+ | "formDataBase64"
16
+ | "binaryBody";
17
+
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 = {
25
+ readonly body?: string;
26
+ readonly formData?: PartialMessage<Property>[];
27
+ readonly bodyType?: RequestBodyWireType;
28
+ };
29
+
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
+ };
38
+
39
+ const requestBodyTypeSchema = z.enum(REST_API_BODY_TYPES);
40
+
41
+ export function encodeRequestBody(input: {
42
+ body: unknown;
43
+ bodyType?: unknown;
44
+ headers?: Record<string, string>;
45
+ }): EncodedRequestBody {
46
+ const bodyTypeResult = requestBodyTypeSchema.safeParse(
47
+ input.bodyType === undefined ? "json" : input.bodyType,
48
+ );
49
+ if (!bodyTypeResult.success) {
50
+ throw validationError(
51
+ `Unsupported bodyType ${describeOptionValue(input.bodyType)} - expected one of: ${REST_API_BODY_TYPES.join(", ")}`,
52
+ ["bodyType"],
53
+ );
54
+ }
55
+
56
+ const bodyType = bodyTypeResult.data;
57
+ if (input.body === undefined) {
58
+ if (bodyType === "json") return { wire: {} };
59
+ throw validationError(`bodyType "${bodyType}" requires a request body`, [
60
+ "body",
61
+ ]);
62
+ }
63
+
64
+ switch (bodyType) {
65
+ case "json":
66
+ return { wire: encodeJsonBody(input.body) };
67
+ case "raw":
68
+ return { wire: encodeRawBody(input.body) };
69
+ case "formUrlencoded":
70
+ return encodeFormUrlencodedBody(input.body, input.headers);
71
+ case "multipartForm":
72
+ return { wire: encodeMultipartFormBody(input.body, input.headers) };
73
+ case "binary":
74
+ return encodeBinaryBody(input.body, input.headers);
75
+ }
76
+ }
77
+
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
+ : "";
121
+ throw validationError(
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],
124
+ );
125
+ }
126
+ let serialized: string | undefined;
127
+ try {
128
+ serialized = JSON.stringify(body);
129
+ } catch (error) {
130
+ throw validationError(
131
+ `bodyType "json" request body is not JSON-serializable: ${describeJsonSerializationFailure(error)}`,
132
+ ["body"],
133
+ );
134
+ }
135
+ if (serialized === undefined) {
136
+ throw validationError(
137
+ `bodyType "json" cannot serialize a ${typeof body} request body`,
138
+ ["body"],
139
+ );
140
+ }
141
+ return { body: serialized, bodyType: "jsonBody" };
142
+ }
143
+
144
+ function encodeRawBody(body: unknown): RequestBodyWireFields {
145
+ if (typeof body !== "string" || body.length === 0) {
146
+ throw validationError(
147
+ 'bodyType "raw" requires a non-empty string request body',
148
+ ["body"],
149
+ );
150
+ }
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
+ );
162
+ }
163
+
164
+ function encodeFormUrlencodedBody(
165
+ body: unknown,
166
+ headers: Record<string, string> | undefined,
167
+ ): EncodedRequestBody {
168
+ if (!isPlainRecord(body)) {
169
+ throw validationError(
170
+ 'bodyType "formUrlencoded" requires a plain object request body',
171
+ ["body"],
172
+ );
173
+ }
174
+ const pairs: string[] = [];
175
+ for (const [key, value] of Object.entries(body)) {
176
+ if (value === undefined) continue;
177
+ if (key.length === 0) {
178
+ throw validationError(
179
+ "formUrlencoded body keys must be non-empty strings",
180
+ ["body", key],
181
+ );
182
+ }
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
+ }
189
+ }
190
+ if (pairs.length === 0) {
191
+ throw validationError(
192
+ "formUrlencoded body must encode at least one field",
193
+ ["body"],
194
+ );
195
+ }
196
+ return {
197
+ wire: { body: pairs.join("&"), bodyType: "rawBody" },
198
+ contentType: contentTypeIfMissing(
199
+ headers,
200
+ "application/x-www-form-urlencoded",
201
+ ),
202
+ };
203
+ }
204
+
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
+ );
223
+ }
224
+ }
225
+
226
+ function formUrlencodedPrimitiveToString(key: string, value: unknown): string {
227
+ if (typeof value === "string" || typeof value === "boolean")
228
+ return String(value);
229
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
230
+ if (typeof value === "object" && value !== null) {
231
+ throw validationError(
232
+ `formUrlencoded field ${describeOptionValue(key)} is nested; flatten it using bracket notation such as "metadata[order_id]"`,
233
+ ["body", key],
234
+ );
235
+ }
236
+ throw validationError(
237
+ `formUrlencoded field ${describeOptionValue(key)} must be a string, finite number, boolean, or array of those values`,
238
+ ["body", key],
239
+ );
240
+ }
241
+
242
+ function encodeBinaryBody(
243
+ body: unknown,
244
+ headers: Record<string, string> | undefined,
245
+ ): EncodedRequestBody {
246
+ if (!isBinaryData(body)) {
247
+ throw validationError(
248
+ 'bodyType "binary" requires an ArrayBuffer or ArrayBufferView request body',
249
+ ["body"],
250
+ );
251
+ }
252
+ if (body.byteLength === 0) {
253
+ throw validationError("binary request bodies must be non-empty", ["body"]);
254
+ }
255
+ return {
256
+ wire: { body: toBuffer(body).toString("base64"), bodyType: "binaryBody" },
257
+ contentType: contentTypeIfMissing(headers, "application/octet-stream"),
258
+ };
259
+ }
260
+
261
+ function encodeMultipartFormBody(
262
+ body: unknown,
263
+ headers: Record<string, string> | undefined,
264
+ ): RequestBodyWireFields {
265
+ if (hasHeader(headers, "content-type")) {
266
+ throw validationError(
267
+ "multipartForm bodies cannot provide Content-Type because the multipart boundary is generated at execution time",
268
+ ["headers", "Content-Type"],
269
+ );
270
+ }
271
+ if (!isPlainRecord(body)) {
272
+ throw validationError(
273
+ 'bodyType "multipartForm" requires a plain object request body',
274
+ ["body"],
275
+ );
276
+ }
277
+ const formData = Object.entries(body).flatMap(([key, value]) =>
278
+ definedArrayValues(value).map((item) => parseMultipartPart(key, item)),
279
+ );
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 };
286
+ }
287
+
288
+ function parseMultipartPart(
289
+ key: string,
290
+ value: unknown,
291
+ ): PartialMessage<Property> {
292
+ if (key.length === 0)
293
+ throw validationError("multipartForm body keys must be non-empty strings", [
294
+ "body",
295
+ key,
296
+ ]);
297
+ if (
298
+ typeof value === "string" ||
299
+ typeof value === "boolean" ||
300
+ (typeof value === "number" && Number.isFinite(value))
301
+ ) {
302
+ return { key, value: String(value) };
303
+ }
304
+ if (isBinaryData(value)) {
305
+ throw validationError(
306
+ `multipartForm binary field ${describeOptionValue(key)} must be wrapped as { value, filename }`,
307
+ ["body", key],
308
+ );
309
+ }
310
+ if (!isPlainRecord(value)) {
311
+ throw validationError(
312
+ `multipartForm field ${describeOptionValue(key)} must be a primitive value or { value, filename } file part`,
313
+ ["body", key],
314
+ );
315
+ }
316
+ const keys = Object.keys(value).filter(
317
+ (partKey) => value[partKey] !== undefined,
318
+ );
319
+ const missingKeys = ["value", "filename"].filter(
320
+ (requiredKey) => !keys.includes(requiredKey),
321
+ );
322
+ if (missingKeys.length > 0) {
323
+ throw validationError(
324
+ `multipartForm file part ${describeOptionValue(key)} is missing required ${pluralize("key", missingKeys)}: ${missingKeys.join(", ")}`,
325
+ ["body", key],
326
+ );
327
+ }
328
+ const unexpectedKeys = keys.filter(
329
+ (partKey) => partKey !== "value" && partKey !== "filename",
330
+ );
331
+ if (unexpectedKeys.length > 0) {
332
+ const shown = unexpectedKeys.slice(0, 5).map(describeOptionValue);
333
+ const suffix = unexpectedKeys.length > 5 ? ", ..." : "";
334
+ throw validationError(
335
+ `multipartForm file part ${describeOptionValue(key)} has unexpected ${pluralize("key", unexpectedKeys)}: ${shown.join(", ")}${suffix}`,
336
+ ["body", key],
337
+ );
338
+ }
339
+ if (typeof value.filename !== "string" || value.filename.length === 0) {
340
+ throw validationError(
341
+ `multipartForm file part ${describeOptionValue(key)} requires a non-empty filename`,
342
+ ["body", key, "filename"],
343
+ );
344
+ }
345
+ if (typeof value.value !== "string" && !isBinaryData(value.value)) {
346
+ throw validationError(
347
+ `multipartForm file part ${describeOptionValue(key)} value must be a string or binary body`,
348
+ ["body", key, "value"],
349
+ );
350
+ }
351
+ return {
352
+ key,
353
+ value: toBuffer(value.value).toString("base64"),
354
+ file: { filename: value.filename },
355
+ };
356
+ }
357
+
358
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
359
+ if (typeof value !== "object" || value === null || Array.isArray(value))
360
+ return false;
361
+ const prototype = Object.getPrototypeOf(value);
362
+ return prototype === Object.prototype || prototype === null;
363
+ }
364
+
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);
369
+ }
370
+
371
+ function contentTypeIfMissing(
372
+ headers: Record<string, string> | undefined,
373
+ value: string,
374
+ ): string | undefined {
375
+ return hasHeader(headers, "content-type") ? undefined : value;
376
+ }
377
+
378
+ function hasHeader(
379
+ headers: Record<string, string> | undefined,
380
+ headerName: string,
381
+ ): boolean {
382
+ return (
383
+ !!headers &&
384
+ Object.keys(headers).some(
385
+ (key) => key.toLowerCase() === headerName.toLowerCase(),
386
+ )
387
+ );
388
+ }
389
+
390
+ function pluralize(word: string, values: readonly unknown[]): string {
391
+ return values.length === 1 ? word : `${word}s`;
392
+ }
@@ -4,10 +4,15 @@ import { z } from "zod";
4
4
  import type { Property } from "@superblocksteam/types/dist/src/common/v1/plugin_pb";
5
5
  import type { Plugin as RestApiIntegrationPlugin } from "@superblocksteam/types/dist/src/plugins/restapiintegration/v1/plugin_pb";
6
6
 
7
- import { RestApiValidationError } from "../../errors.js";
7
+ import {
8
+ RestApiValidationError,
9
+ restApiValidationErrorFromZodError,
10
+ } from "../../errors.js";
8
11
  import type { QueryExecutor, TraceMetadata } from "../registry.js";
9
12
  import type { IntegrationConfig, IntegrationClientImpl } from "../types.js";
13
+ import { describeOptionValue, validationError } from "../utils.js";
10
14
  import { decodeWorkerBinaryResponse } from "./decode-worker-binary-response.js";
15
+ import { encodeRequestBody } from "./request-body.js";
11
16
  import { REST_API_RESPONSE_TYPES } from "./types.js";
12
17
  import type { ApiRequestOptions } from "./types.js";
13
18
 
@@ -45,12 +50,10 @@ export abstract class RestApiClientBase implements IntegrationClientImpl {
45
50
  if (options.body !== undefined && bodySchema) {
46
51
  const bodyParseResult = bodySchema.safeParse(options.body);
47
52
  if (!bodyParseResult.success) {
48
- throw new RestApiValidationError(
49
- `Request body validation failed: ${bodyParseResult.error.message}`,
50
- {
51
- zodError: bodyParseResult.error,
52
- data: options.body,
53
- },
53
+ throw restApiValidationErrorFromZodError(
54
+ "Request body validation failed",
55
+ bodyParseResult.error,
56
+ options.body,
54
57
  );
55
58
  }
56
59
  }
@@ -73,16 +76,22 @@ export abstract class RestApiClientBase implements IntegrationClientImpl {
73
76
  .enum(REST_API_RESPONSE_TYPES)
74
77
  .safeParse(options.responseType ?? "json");
75
78
  if (!responseTypeResult.success) {
76
- throw new RestApiValidationError(
77
- `Unsupported responseType ${JSON.stringify(options.responseType)} - expected one of: ${REST_API_RESPONSE_TYPES.join(", ")}`,
78
- {
79
- zodError: responseTypeResult.error,
80
- data: options.responseType,
81
- },
79
+ throw validationError(
80
+ `Unsupported responseType ${describeOptionValue(options.responseType)} - expected one of: ${REST_API_RESPONSE_TYPES.join(", ")}`,
81
+ ["responseType"],
82
82
  );
83
83
  }
84
84
  const responseType = responseTypeResult.data;
85
85
 
86
+ const { wire: bodyWire, contentType } = encodeRequestBody({
87
+ body: options.body,
88
+ bodyType: options.bodyType,
89
+ headers: options.headers,
90
+ });
91
+ if (contentType !== undefined) {
92
+ headers.push(this.createParam("Content-Type", contentType));
93
+ }
94
+
86
95
  const request: RestApiRequest = {
87
96
  openApiAction: "genericHttpRequest",
88
97
  httpMethod: options.method.toUpperCase(),
@@ -90,14 +99,10 @@ export abstract class RestApiClientBase implements IntegrationClientImpl {
90
99
  headers,
91
100
  params,
92
101
  responseType,
102
+ ...bodyWire,
93
103
  };
94
104
 
95
- if (options.body !== undefined) {
96
- request.body = JSON.stringify(options.body);
97
- request.bodyType = "jsonBody";
98
- }
99
-
100
- const result = await this.executeQuery({ ...request }, undefined, metadata);
105
+ const result = await this.executeQuery(request, undefined, metadata);
101
106
 
102
107
  if (result === null || result === undefined) {
103
108
  throw new RestApiValidationError(
@@ -1,6 +1,9 @@
1
1
  import { z } from "zod";
2
2
 
3
- import { RestApiValidationError } from "../../errors.js";
3
+ import {
4
+ RestApiValidationError,
5
+ restApiValidationErrorFromZodError,
6
+ } from "../../errors.js";
4
7
  import type { TraceMetadata } from "../registry.js";
5
8
  import { RestApiClientBase } from "./rest-api-client-base.js";
6
9
  import type {
@@ -71,12 +74,10 @@ export abstract class RestApiIntegrationClient
71
74
  const responseParseResult = schema.response.safeParse(result);
72
75
 
73
76
  if (!responseParseResult.success) {
74
- throw new RestApiValidationError(
75
- `Response validation failed: ${responseParseResult.error.message}`,
76
- {
77
- data: result,
78
- zodError: responseParseResult.error,
79
- },
77
+ throw restApiValidationErrorFromZodError(
78
+ "Response validation failed",
79
+ responseParseResult.error,
80
+ result,
80
81
  );
81
82
  }
82
83
 
@@ -8,6 +8,16 @@ export const REST_API_RESPONSE_TYPES = ["binary", "json", "text"] as const;
8
8
 
9
9
  export type RestApiResponseType = (typeof REST_API_RESPONSE_TYPES)[number];
10
10
 
11
+ export const REST_API_BODY_TYPES = [
12
+ "json",
13
+ "raw",
14
+ "formUrlencoded",
15
+ "multipartForm",
16
+ "binary",
17
+ ] as const;
18
+
19
+ export type RestApiBodyType = (typeof REST_API_BODY_TYPES)[number];
20
+
11
21
  export interface ApiRequestOptions<TBody = unknown> {
12
22
  /**
13
23
  * HTTP method for the request.
@@ -25,6 +35,18 @@ export interface ApiRequestOptions<TBody = unknown> {
25
35
  */
26
36
  body?: TBody;
27
37
 
38
+ /**
39
+ * How the request body should be encoded. Defaults to `"json"`.
40
+ *
41
+ * Use `"raw"` for caller-encoded string bodies, `"formUrlencoded"` for
42
+ * `application/x-www-form-urlencoded` payloads, `"multipartForm"` for
43
+ * multipart form fields and file parts, and `"binary"` for byte payloads.
44
+ * Worker wire values such as `"jsonBody"`, `"rawBody"`, `"formData"`,
45
+ * `"fileForm"`, `"binaryBody"`, and `"formDataBase64"` are not public SDK
46
+ * `bodyType` values.
47
+ */
48
+ bodyType?: RestApiBodyType;
49
+
28
50
  /**
29
51
  * Optional query parameters
30
52
  */
@@ -64,16 +86,20 @@ export interface ApiRequestSchema<TBody = unknown, TResponse = unknown> {
64
86
  /**
65
87
  * Interface for integration clients that support generic API requests.
66
88
  *
67
- * Providing a response schema gives type-safe, validated results. Omitting
68
- * it returns the decoded response: `Uint8Array` for `responseType: "binary"`,
69
- * or `unknown` for `responseType: "text"`. JSON still requires a schema.
70
- * Object schemas only make sense for JSON-shaped results.
89
+ * Providing a response schema gives type-safe, validated results. Omitting it
90
+ * returns the decoded response: `Uint8Array` for `responseType: "binary"`, or
91
+ * `unknown` for `responseType: "text"`. JSON still requires a schema. Object
92
+ * schemas only make sense for JSON-shaped results.
93
+ *
94
+ * Request bodies default to JSON. Set `bodyType` to one of `"json"`, `"raw"`,
95
+ * `"formUrlencoded"`, `"multipartForm"`, or `"binary"` when the endpoint
96
+ * expects a different request encoding.
71
97
  */
72
98
  export interface SupportsApiRequest {
73
99
  /**
74
100
  * Execute a generic API request with type-safe validation.
75
101
  *
76
- * @param options - Request configuration including method, path, params, and body
102
+ * @param options - Request configuration including method, path, params, body, and bodyType
77
103
  * @param schema - Zod schemas for request body and response validation
78
104
  * @param metadata - Optional trace metadata for observability (label, description)
79
105
  * @returns Validated response data
@@ -101,6 +127,16 @@ export interface SupportsApiRequest {
101
127
  * { label: 'slack.postMessage', description: 'Post a message to #alerts' }
102
128
  * );
103
129
  * // result is typed as { ts: string; channel: string }
130
+ *
131
+ * await ctx.integrations.stripe.apiRequest(
132
+ * {
133
+ * method: 'POST',
134
+ * path: '/v1/charges',
135
+ * bodyType: 'formUrlencoded',
136
+ * body: { amount: 2000, currency: 'usd' },
137
+ * },
138
+ * { response: StripeChargeSchema }
139
+ * );
104
140
  * ```
105
141
  */
106
142
  apiRequest<TBody, TResponse>(
@@ -1,6 +1,6 @@
1
1
  # Box Client
2
2
 
3
- Upload, download, and manage files in Box.
3
+ Download and manage files in Box. File uploads are not available through this integration; see [File Uploads Are Not Available](#file-uploads-are-not-available).
4
4
 
5
5
  ## Methods
6
6
 
@@ -163,37 +163,6 @@ const content = await ctx.integrations.box.apiRequest(
163
163
  );
164
164
  ```
165
165
 
166
- ### Upload a File
167
-
168
- ```typescript
169
- const UploadResponseSchema = z.object({
170
- entries: z.array(FileInfoSchema),
171
- total_count: z.number(),
172
- });
173
-
174
- // For files < 50MB, use direct upload
175
- const result = await ctx.integrations.box.apiRequest(
176
- {
177
- method: "POST",
178
- path: "/files/content",
179
- headers: {
180
- "Content-Type": "multipart/form-data",
181
- },
182
- body: {
183
- attributes: JSON.stringify({
184
- name: "new-file.txt",
185
- parent: { id: folderId },
186
- }),
187
- file: fileContent,
188
- },
189
- },
190
- { response: UploadResponseSchema },
191
- );
192
-
193
- const uploadedFile = result.entries[0];
194
- console.log(`Uploaded: ${uploadedFile.name} (${uploadedFile.id})`);
195
- ```
196
-
197
166
  ### Create a Folder
198
167
 
199
168
  ```typescript
@@ -399,7 +368,7 @@ All methods accept an optional `metadata` parameter as the last argument for dia
399
368
  ```typescript
400
369
  // WRONG - These methods do not exist
401
370
  await box.listFiles({ ... });
402
- await box.uploadFile({ ... });
371
+ await box.createFolder({ ... });
403
372
 
404
373
  // CORRECT - Use apiRequest
405
374
  await ctx.integrations.box.apiRequest(
@@ -457,20 +426,9 @@ const result = await ctx.integrations.box.apiRequest(
457
426
  );
458
427
  ```
459
428
 
460
- ### Upload Returns Array
461
-
462
- File uploads return an array in `entries`:
429
+ ### File Uploads Are Not Available
463
430
 
464
- ```typescript
465
- // Response structure
466
- const schema = z.object({
467
- entries: z.array(FileInfoSchema), // Array even for single file
468
- total_count: z.number(),
469
- });
470
-
471
- // Access the uploaded file
472
- const uploadedFile = result.entries[0];
473
- ```
431
+ Box serves every upload endpoint (`/files/content`, `/files/upload_sessions`) from `https://upload.box.com/api/2.0`. This integration's base URL is fixed by the Box integration template to `https://api.box.com/2.0` and `apiRequest` cannot target another host, so uploads through `ctx.integrations.box` fail. Do not generate upload code for this integration. If an app must upload to Box, ask the user to configure a REST API integration with base URL `https://upload.box.com/api/2.0` using the same OAuth client, then call it through that integration.
474
432
 
475
433
  ### If-Match Header for Updates
476
434
 
@@ -521,20 +479,6 @@ async function getAllItems(box: BoxClient, folderId: string) {
521
479
  }
522
480
  ```
523
481
 
524
- ### File Size Limits
525
-
526
- Different upload methods for different file sizes:
527
-
528
- ```typescript
529
- // < 50MB - Use /files/content (simple upload)
530
- // > 50MB - Use chunked upload session
531
-
532
- // Chunked upload for large files:
533
- // 1. POST /files/upload_sessions - Create session
534
- // 2. PUT /files/upload_sessions/:id - Upload parts
535
- // 3. POST /files/upload_sessions/:id/commit - Finalize
536
- ```
537
-
538
482
  ### Shared Link Access Levels
539
483
 
540
484
  ```typescript