@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.
Files changed (152) hide show
  1. package/README.md +23 -0
  2. package/dist/errors.d.ts +36 -3
  3. package/dist/errors.d.ts.map +1 -1
  4. package/dist/errors.js +231 -38
  5. package/dist/errors.js.map +1 -1
  6. package/dist/errors.test.d.ts +2 -0
  7. package/dist/errors.test.d.ts.map +1 -0
  8. package/dist/errors.test.js +189 -0
  9. package/dist/errors.test.js.map +1 -0
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/integrations/anthropic/types.d.ts +1 -1
  14. package/dist/integrations/base/decode-worker-binary-response.d.ts.map +1 -1
  15. package/dist/integrations/base/decode-worker-binary-response.js +2 -5
  16. package/dist/integrations/base/decode-worker-binary-response.js.map +1 -1
  17. package/dist/integrations/base/index.d.ts +1 -1
  18. package/dist/integrations/base/index.d.ts.map +1 -1
  19. package/dist/integrations/base/request-body.d.ts +18 -10
  20. package/dist/integrations/base/request-body.d.ts.map +1 -1
  21. package/dist/integrations/base/request-body.js +131 -215
  22. package/dist/integrations/base/request-body.js.map +1 -1
  23. package/dist/integrations/base/request-body.test.js +306 -164
  24. package/dist/integrations/base/request-body.test.js.map +1 -1
  25. package/dist/integrations/base/rest-api-client-base.d.ts.map +1 -1
  26. package/dist/integrations/base/rest-api-client-base.js +13 -26
  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 +0 -8
  32. package/dist/integrations/base/types.d.ts.map +1 -1
  33. package/dist/integrations/cosmosdb/client.d.ts.map +1 -1
  34. package/dist/integrations/cosmosdb/client.js +2 -5
  35. package/dist/integrations/cosmosdb/client.js.map +1 -1
  36. package/dist/integrations/documentation-resolver.test.js +88 -2
  37. package/dist/integrations/documentation-resolver.test.js.map +1 -1
  38. package/dist/integrations/documentation.test.js +10 -0
  39. package/dist/integrations/documentation.test.js.map +1 -1
  40. package/dist/integrations/dynamodb/client.d.ts +6 -2
  41. package/dist/integrations/dynamodb/client.d.ts.map +1 -1
  42. package/dist/integrations/dynamodb/client.js +85 -15
  43. package/dist/integrations/dynamodb/client.js.map +1 -1
  44. package/dist/integrations/dynamodb/client.test.d.ts +8 -0
  45. package/dist/integrations/dynamodb/client.test.d.ts.map +1 -0
  46. package/dist/integrations/dynamodb/client.test.js +198 -0
  47. package/dist/integrations/dynamodb/client.test.js.map +1 -0
  48. package/dist/integrations/dynamodb/index.d.ts +1 -1
  49. package/dist/integrations/dynamodb/index.d.ts.map +1 -1
  50. package/dist/integrations/dynamodb/index.js.map +1 -1
  51. package/dist/integrations/dynamodb/types.d.ts +27 -1
  52. package/dist/integrations/dynamodb/types.d.ts.map +1 -1
  53. package/dist/integrations/gcs/client.d.ts.map +1 -1
  54. package/dist/integrations/gcs/client.js +2 -5
  55. package/dist/integrations/gcs/client.js.map +1 -1
  56. package/dist/integrations/gsheets/client.d.ts.map +1 -1
  57. package/dist/integrations/gsheets/client.js +2 -5
  58. package/dist/integrations/gsheets/client.js.map +1 -1
  59. package/dist/integrations/index.d.ts +1 -1
  60. package/dist/integrations/index.d.ts.map +1 -1
  61. package/dist/integrations/index.js.map +1 -1
  62. package/dist/integrations/mongodb/client.d.ts.map +1 -1
  63. package/dist/integrations/mongodb/client.js +2 -5
  64. package/dist/integrations/mongodb/client.js.map +1 -1
  65. package/dist/integrations/restapiintegration/client.body-types.test.js +107 -25
  66. package/dist/integrations/restapiintegration/client.body-types.test.js.map +1 -1
  67. package/dist/integrations/restapiintegration/client.test.js +16 -0
  68. package/dist/integrations/restapiintegration/client.test.js.map +1 -1
  69. package/dist/integrations/s3/client.d.ts.map +1 -1
  70. package/dist/integrations/s3/client.js +2 -5
  71. package/dist/integrations/s3/client.js.map +1 -1
  72. package/dist/integrations/salesforce/client.d.ts.map +1 -1
  73. package/dist/integrations/salesforce/client.js +2 -5
  74. package/dist/integrations/salesforce/client.js.map +1 -1
  75. package/dist/integrations/slack/client.d.ts.map +1 -1
  76. package/dist/integrations/slack/client.js +4 -13
  77. package/dist/integrations/slack/client.js.map +1 -1
  78. package/dist/integrations/slack/client.test.js +2 -5
  79. package/dist/integrations/slack/client.test.js.map +1 -1
  80. package/dist/integrations/utils.d.ts +8 -3
  81. package/dist/integrations/utils.d.ts.map +1 -1
  82. package/dist/integrations/utils.js +42 -1
  83. package/dist/integrations/utils.js.map +1 -1
  84. package/dist/runtime/context.d.ts +6 -0
  85. package/dist/runtime/context.d.ts.map +1 -1
  86. package/dist/runtime/context.js +2 -1
  87. package/dist/runtime/context.js.map +1 -1
  88. package/dist/runtime/executor.d.ts +6 -0
  89. package/dist/runtime/executor.d.ts.map +1 -1
  90. package/dist/runtime/executor.js +1 -0
  91. package/dist/runtime/executor.js.map +1 -1
  92. package/dist/types.d.ts +10 -0
  93. package/dist/types.d.ts.map +1 -1
  94. package/package.json +3 -3
  95. package/src/errors.test.ts +257 -0
  96. package/src/errors.ts +289 -46
  97. package/src/index.ts +1 -0
  98. package/src/integrations/anthropic/README.md +12 -59
  99. package/src/integrations/anthropic/types.ts +1 -1
  100. package/src/integrations/base/decode-worker-binary-response.ts +5 -7
  101. package/src/integrations/base/index.ts +0 -5
  102. package/src/integrations/base/request-body.test.ts +358 -195
  103. package/src/integrations/base/request-body.ts +170 -326
  104. package/src/integrations/base/rest-api-client-base.ts +22 -31
  105. package/src/integrations/base/rest-api-integration-client.ts +8 -7
  106. package/src/integrations/base/types.ts +0 -18
  107. package/src/integrations/box/README.md +4 -60
  108. package/src/integrations/cosmosdb/client.ts +8 -7
  109. package/src/integrations/documentation-resolver.test.ts +108 -2
  110. package/src/integrations/documentation.test.ts +12 -0
  111. package/src/integrations/dropbox/README.md +1 -71
  112. package/src/integrations/dropbox/docs.manifest.json +10 -1
  113. package/src/integrations/dropbox/overlays/upload-unsupported.md +3 -0
  114. package/src/integrations/dropbox/overlays/upload.md +89 -0
  115. package/src/integrations/dynamodb/README.md +33 -12
  116. package/src/integrations/dynamodb/client.test.ts +254 -0
  117. package/src/integrations/dynamodb/client.ts +129 -23
  118. package/src/integrations/dynamodb/index.ts +5 -1
  119. package/src/integrations/dynamodb/types.ts +33 -1
  120. package/src/integrations/elasticsearch/README.md +0 -80
  121. package/src/integrations/elasticsearch/docs.manifest.json +10 -1
  122. package/src/integrations/elasticsearch/overlays/bulk-unsupported.md +3 -0
  123. package/src/integrations/elasticsearch/overlays/bulk.md +80 -0
  124. package/src/integrations/gcs/client.ts +8 -7
  125. package/src/integrations/googledrive/README.md +10 -36
  126. package/src/integrations/gsheets/client.ts +8 -7
  127. package/src/integrations/index.ts +1 -0
  128. package/src/integrations/jira/README.md +0 -26
  129. package/src/integrations/jira/docs.manifest.json +10 -1
  130. package/src/integrations/jira/overlays/attachments-unsupported.md +3 -0
  131. package/src/integrations/jira/overlays/attachments.md +30 -0
  132. package/src/integrations/mongodb/client.ts +8 -7
  133. package/src/integrations/restapiintegration/client.body-types.test.ts +135 -26
  134. package/src/integrations/restapiintegration/client.test.ts +23 -0
  135. package/src/integrations/restapiintegration/docs.manifest.json +2 -2
  136. package/src/integrations/restapiintegration/overlays/request-body-types.md +26 -17
  137. package/src/integrations/s3/client.ts +8 -7
  138. package/src/integrations/salesforce/client.ts +9 -7
  139. package/src/integrations/slack/client.test.ts +2 -5
  140. package/src/integrations/slack/client.ts +13 -19
  141. package/src/integrations/stabilityai/README.md +0 -87
  142. package/src/integrations/stabilityai/docs.manifest.json +10 -1
  143. package/src/integrations/stabilityai/overlays/image-uploads-unsupported.md +3 -0
  144. package/src/integrations/stabilityai/overlays/image-uploads.md +120 -0
  145. package/src/integrations/stripe/README.md +0 -201
  146. package/src/integrations/stripe/docs.manifest.json +10 -1
  147. package/src/integrations/stripe/overlays/request-bodies-unsupported.md +3 -0
  148. package/src/integrations/stripe/overlays/request-bodies.md +204 -0
  149. package/src/integrations/utils.ts +51 -1
  150. package/src/runtime/context.ts +9 -0
  151. package/src/runtime/executor.ts +8 -0
  152. package/src/types.ts +11 -0
@@ -0,0 +1,3 @@
1
+ ## Bulk Operations
2
+
3
+ This agent's sdk-api cannot call `/_bulk` through `apiRequest()` because it serializes every body as JSON, and the bulk API requires newline-delimited JSON. Do not generate bulk code until the agent is upgraded; index, update, or delete documents one call at a time as documented above.
@@ -0,0 +1,80 @@
1
+ ## Bulk Operations
2
+
3
+ `/_bulk` expects newline-delimited JSON, so pass `bodyType: "raw"` with the pre-built NDJSON string and set `Content-Type: application/x-ndjson`. Raw mode adds no headers of its own. Without `bodyType`, `apiRequest()` would JSON-encode the string a second time and Elasticsearch would reject it.
4
+
5
+ Bulk operations require the request bodies to be newline-delimited JSON, not a JSON array:
6
+
7
+ ```typescript
8
+ // WRONG - Regular JSON array
9
+ const body = [{ index: { _index: "products" } }, { name: "Product" }];
10
+
11
+ // CORRECT - Newline-delimited JSON
12
+ const body = '{"index":{"_index":"products"}}\n{"name":"Product"}\n';
13
+
14
+ // Or build programmatically
15
+ const operations = [{ index: { _index: "products" } }, { name: "Product" }];
16
+ const body = operations.map(JSON.stringify).join("\n") + "\n";
17
+ ```
18
+
19
+ ```typescript
20
+ const BulkResponseSchema = z.object({
21
+ took: z.number(),
22
+ errors: z.boolean(),
23
+ items: z.array(
24
+ z.object({
25
+ index: z
26
+ .object({
27
+ _index: z.string(),
28
+ _id: z.string(),
29
+ status: z.number(),
30
+ result: z.string().optional(),
31
+ error: z
32
+ .object({
33
+ type: z.string(),
34
+ reason: z.string(),
35
+ })
36
+ .optional(),
37
+ })
38
+ .optional(),
39
+ delete: z
40
+ .object({
41
+ _index: z.string(),
42
+ _id: z.string(),
43
+ status: z.number(),
44
+ result: z.string().optional(),
45
+ })
46
+ .optional(),
47
+ }),
48
+ ),
49
+ });
50
+
51
+ // Bulk index
52
+ const result = await ctx.integrations.es.apiRequest(
53
+ {
54
+ method: "POST",
55
+ path: "/_bulk",
56
+ headers: {
57
+ "Content-Type": "application/x-ndjson",
58
+ },
59
+ bodyType: "raw",
60
+ body:
61
+ [
62
+ { index: { _index: "products", _id: "1" } },
63
+ { name: "Product 1", price: 10 },
64
+ { index: { _index: "products", _id: "2" } },
65
+ { name: "Product 2", price: 20 },
66
+ ]
67
+ .map(JSON.stringify)
68
+ .join("\n") + "\n",
69
+ },
70
+ { response: BulkResponseSchema },
71
+ );
72
+
73
+ if (result.errors) {
74
+ result.items.forEach((item) => {
75
+ if (item.index?.error) {
76
+ console.error(`Failed to index: ${item.index.error.reason}`);
77
+ }
78
+ });
79
+ }
80
+ ```
@@ -9,7 +9,10 @@ import type { z } from "zod";
9
9
 
10
10
  import type { Plugin as GCSPlugin } from "@superblocksteam/types/dist/src/plugins/gcs/v1/plugin_pb";
11
11
 
12
- import { RestApiValidationError } from "../../errors.js";
12
+ import {
13
+ RestApiValidationError,
14
+ restApiValidationErrorFromZodError,
15
+ } from "../../errors.js";
13
16
  import { IntegrationError } from "../../runtime/errors.js";
14
17
  import type { QueryExecutor, TraceMetadata } from "../registry.js";
15
18
  import type { IntegrationConfig, IntegrationClientImpl } from "../types.js";
@@ -71,12 +74,10 @@ export class GCSClientImpl implements GCSClient, IntegrationClientImpl {
71
74
  const parseResult = schema.safeParse(result);
72
75
 
73
76
  if (!parseResult.success) {
74
- throw new RestApiValidationError(
75
- `Result validation failed: ${parseResult.error.message}`,
76
- {
77
- zodError: parseResult.error,
78
- data: result,
79
- },
77
+ throw restApiValidationErrorFromZodError(
78
+ "Result validation failed",
79
+ parseResult.error,
80
+ result,
80
81
  );
81
82
  }
82
83
 
@@ -1,6 +1,6 @@
1
1
  # Google Drive Client
2
2
 
3
- Upload, download, and manage files in Google Drive.
3
+ Download and manage files in Google Drive. File content uploads are not available through this integration; see [File Content Uploads Are Not Available](#file-content-uploads-are-not-available).
4
4
 
5
5
  ## Methods
6
6
 
@@ -162,32 +162,6 @@ const folder = await ctx.integrations.drive.apiRequest(
162
162
  console.log(`Created folder: ${folder.id}`);
163
163
  ```
164
164
 
165
- ### Upload a File
166
-
167
- ```typescript
168
- // For simple uploads (< 5MB)
169
- const uploadedFile = await ctx.integrations.drive.apiRequest(
170
- {
171
- method: "POST",
172
- path: "/upload/drive/v3/files",
173
- params: {
174
- uploadType: "multipart",
175
- },
176
- body: {
177
- metadata: {
178
- name: "report.txt",
179
- parents: [folderId],
180
- },
181
- media: {
182
- mimeType: "text/plain",
183
- body: "File content here",
184
- },
185
- },
186
- },
187
- { response: FileSchema },
188
- );
189
- ```
190
-
191
165
  ### Update File Metadata
192
166
 
193
167
  ```typescript
@@ -300,7 +274,7 @@ All methods accept an optional `metadata` parameter as the last argument for dia
300
274
  ```typescript
301
275
  // WRONG - These methods do not exist
302
276
  await drive.listFiles({ ... });
303
- await drive.uploadFile({ ... });
277
+ await drive.createFolder({ ... });
304
278
 
305
279
  // CORRECT - Use apiRequest
306
280
  await ctx.integrations.drive.apiRequest(
@@ -394,21 +368,22 @@ const sizeBytes = parseInt(file.size, 10);
394
368
  const sizeMB = sizeBytes / (1024 * 1024);
395
369
  ```
396
370
 
397
- ### Upload Path vs Regular Path
371
+ ### Base URL Prefix
398
372
 
399
- The base URL already includes `/drive/v3`, so regular API paths omit this prefix. However, uploads use a different URL root (`/upload/drive/v3/...` instead of `/drive/v3/...`) and require the full path:
373
+ The base URL already includes `/drive/v3`, so regular API paths omit this prefix:
400
374
 
401
375
  ```typescript
402
- // Regular API calls (base URL already has /drive/v3)
376
+ // CORRECT - base URL already has /drive/v3
403
377
  const path = "/files";
404
378
 
405
- // Upload calls use a different URL root — include the full path
406
- const path = "/upload/drive/v3/files";
407
-
408
- // WRONG - Duplicates base URL prefix for regular calls
379
+ // WRONG - duplicates the base URL prefix
409
380
  const path = "/drive/v3/files";
410
381
  ```
411
382
 
383
+ ### File Content Uploads Are Not Available
384
+
385
+ Google Drive serves every content upload endpoint (`uploadType=media`, `multipart`, and `resumable`) from `https://www.googleapis.com/upload/drive/v3`. This integration's base URL is fixed by the Google Drive integration template to `https://www.googleapis.com/drive/v3`, and `apiRequest` cannot target another URL root, so a path such as `/upload/drive/v3/files` resolves to `https://www.googleapis.com/drive/v3/upload/drive/v3/files` and fails. Do not generate content upload code for this integration. Metadata-only creation through `POST /files` (for example, creating a folder) works. If an app must upload file content to Drive, ask the user to configure a REST API integration with base URL `https://www.googleapis.com/upload/drive/v3` using the same OAuth client, then call it through that integration.
386
+
412
387
  ### Folder MIME Type
413
388
 
414
389
  Folders have a special MIME type:
@@ -481,4 +456,3 @@ try {
481
456
  - [Google Drive API Documentation](https://developers.google.com/drive/api/reference/rest/v3)
482
457
  - [Files Resource](https://developers.google.com/drive/api/reference/rest/v3/files)
483
458
  - [Query String Syntax](https://developers.google.com/drive/api/guides/search-files)
484
- - [Upload Files](https://developers.google.com/drive/api/guides/manage-uploads)
@@ -9,7 +9,10 @@ import type { z } from "zod";
9
9
 
10
10
  import type { Plugin as GSheetsPlugin } from "@superblocksteam/types/dist/src/plugins/gsheets/v1/plugin_pb";
11
11
 
12
- import { RestApiValidationError } from "../../errors.js";
12
+ import {
13
+ RestApiValidationError,
14
+ restApiValidationErrorFromZodError,
15
+ } from "../../errors.js";
13
16
  import { IntegrationError } from "../../runtime/errors.js";
14
17
  import type { QueryExecutor, TraceMetadata } from "../registry.js";
15
18
  import type { IntegrationConfig, IntegrationClientImpl } from "../types.js";
@@ -113,12 +116,10 @@ export class GoogleSheetsClientImpl
113
116
  const parseResult = schema.safeParse(result);
114
117
 
115
118
  if (!parseResult.success) {
116
- throw new RestApiValidationError(
117
- `Result validation failed: ${parseResult.error.message}`,
118
- {
119
- zodError: parseResult.error,
120
- data: result,
121
- },
119
+ throw restApiValidationErrorFromZodError(
120
+ "Result validation failed",
121
+ parseResult.error,
122
+ result,
122
123
  );
123
124
  }
124
125
 
@@ -363,6 +363,7 @@ export { MongoDBClientImpl } from "./mongodb/index.js";
363
363
  export type {
364
364
  DynamoDBClient,
365
365
  DynamoDBAttributeValue,
366
+ DynamoDBScanOptions,
366
367
  } from "./dynamodb/index.js";
367
368
  export { DynamoDBClientImpl } from "./dynamodb/index.js";
368
369
 
@@ -259,32 +259,6 @@ const result = await ctx.integrations.jira.apiRequest(
259
259
  );
260
260
  ```
261
261
 
262
- ### Add Attachment
263
-
264
- ```typescript
265
- // Note: Attachments require multipart/form-data
266
- // The body should be the file content
267
- const AttachmentResponseSchema = z.array(
268
- z.object({
269
- id: z.string(),
270
- filename: z.string(),
271
- size: z.number(),
272
- }),
273
- );
274
-
275
- const result = await ctx.integrations.jira.apiRequest(
276
- {
277
- method: "POST",
278
- path: `/rest/api/3/issue/${issueKey}/attachments`,
279
- headers: {
280
- "X-Atlassian-Token": "no-check",
281
- },
282
- body: fileContent, // Multipart form data
283
- },
284
- { response: AttachmentResponseSchema },
285
- );
286
- ```
287
-
288
262
  ## Trace Metadata
289
263
 
290
264
  All methods accept an optional `metadata` parameter as the last argument for diagnostics labeling. See the [root SDK README](../../../README.md#trace-metadata) for details.
@@ -1,5 +1,14 @@
1
1
  {
2
2
  "pluginId": "jira",
3
3
  "base": "README.md",
4
- "overlays": []
4
+ "overlays": [
5
+ {
6
+ "file": "overlays/attachments.md",
7
+ "sdkVersionRange": ">=0.0.7"
8
+ },
9
+ {
10
+ "file": "overlays/attachments-unsupported.md",
11
+ "sdkVersionRange": "<0.0.7"
12
+ }
13
+ ]
5
14
  }
@@ -0,0 +1,3 @@
1
+ ## Add Attachment
2
+
3
+ This agent's sdk-api cannot upload Jira attachments through `apiRequest()` because it serializes every body as JSON. Do not generate attachment upload code until the agent is upgraded.
@@ -0,0 +1,30 @@
1
+ ## Add Attachment
2
+
3
+ Attachments are multipart uploads, so pass `bodyType: "multipartForm"` and the `X-Atlassian-Token: no-check` header.
4
+
5
+ ```typescript
6
+ const AttachmentResponseSchema = z.array(
7
+ z.object({
8
+ id: z.string(),
9
+ filename: z.string(),
10
+ size: z.number(),
11
+ }),
12
+ );
13
+
14
+ const file = input.upload.files[0];
15
+
16
+ const result = await ctx.integrations.jira.apiRequest(
17
+ {
18
+ method: "POST",
19
+ path: `/rest/api/3/issue/${issueKey}/attachments`,
20
+ headers: { "X-Atlassian-Token": "no-check" },
21
+ bodyType: "multipartForm",
22
+ body: {
23
+ file: { value: await file.readContentsAsync("raw"), filename: file.name },
24
+ },
25
+ },
26
+ { response: AttachmentResponseSchema },
27
+ );
28
+ ```
29
+
30
+ Do not set `Content-Type`. The worker generates the multipart boundary.
@@ -9,7 +9,10 @@ import type { z } from "zod";
9
9
 
10
10
  import type { Plugin as MongoDBPlugin } from "@superblocksteam/types/dist/src/plugins/mongodb/v1/plugin_pb";
11
11
 
12
- import { RestApiValidationError } from "../../errors.js";
12
+ import {
13
+ RestApiValidationError,
14
+ restApiValidationErrorFromZodError,
15
+ } from "../../errors.js";
13
16
  import { IntegrationError } from "../../runtime/errors.js";
14
17
  import type { QueryExecutor, TraceMetadata } from "../registry.js";
15
18
  import type { IntegrationConfig, IntegrationClientImpl } from "../types.js";
@@ -115,12 +118,10 @@ export class MongoDBClientImpl implements MongoDBClient, IntegrationClientImpl {
115
118
  const parseResult = schema.safeParse(result);
116
119
 
117
120
  if (!parseResult.success) {
118
- throw new RestApiValidationError(
119
- `Result validation failed: ${parseResult.error.message}`,
120
- {
121
- zodError: parseResult.error,
122
- data: result,
123
- },
121
+ throw restApiValidationErrorFromZodError(
122
+ "Result validation failed",
123
+ parseResult.error,
124
+ result,
124
125
  );
125
126
  }
126
127
 
@@ -1,7 +1,10 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
  import { z } from "zod";
3
3
 
4
- import { RestApiValidationError } from "../../errors.js";
4
+ import {
5
+ REDACTED_BINARY_RESPONSE_DATA,
6
+ RestApiValidationError,
7
+ } from "../../errors.js";
5
8
  import type { IntegrationConfig } from "../types.js";
6
9
  import { RestApiIntegrationPluginClientImpl } from "./client.js";
7
10
 
@@ -94,49 +97,51 @@ describe("RestApiIntegrationPluginClientImpl request body types", () => {
94
97
  );
95
98
  });
96
99
 
97
- it("sends binary bodies as base64 binaryBody with default Content-Type", async () => {
98
- const bytes = Uint8Array.from([137, 80, 78, 71]);
100
+ it("appends the injected Content-Type after caller headers", async () => {
99
101
  const { client, executeQuery } = createClient();
100
102
  await client.apiRequest(
101
103
  {
102
- method: "PUT",
103
- path: "/files/report.png",
104
- body: bytes,
105
- bodyType: "binary",
104
+ method: "POST",
105
+ path: "/charges",
106
+ headers: { Authorization: "Bearer x" },
107
+ body: { amount: 2000 },
108
+ bodyType: "formUrlencoded",
106
109
  },
107
110
  { response: JsonResponseSchema },
108
111
  );
109
112
  expect(executeQuery).toHaveBeenCalledWith(
110
113
  expect.objectContaining({
111
- body: Buffer.from(bytes).toString("base64"),
112
- bodyType: "binaryBody",
113
- headers: [{ key: "Content-Type", value: "application/octet-stream" }],
114
+ headers: [
115
+ { key: "Authorization", value: "Bearer x" },
116
+ { key: "Content-Type", value: "application/x-www-form-urlencoded" },
117
+ ],
114
118
  }),
115
119
  undefined,
116
120
  undefined,
117
121
  );
118
122
  });
119
123
 
120
- it("sends text multipart file parts as formData", async () => {
124
+ it("keeps a caller Content-Type of any case and injects nothing", async () => {
121
125
  const { client, executeQuery } = createClient();
122
126
  await client.apiRequest(
123
127
  {
124
128
  method: "POST",
125
- path: "/upload",
126
- body: {
127
- note: "hello",
128
- file: { value: "contents", filename: "note.txt" },
129
+ path: "/charges",
130
+ headers: {
131
+ "content-type": "application/x-www-form-urlencoded; charset=utf-8",
129
132
  },
130
- bodyType: "multipartForm",
133
+ body: { amount: 2000 },
134
+ bodyType: "formUrlencoded",
131
135
  },
132
136
  { response: JsonResponseSchema },
133
137
  );
134
138
  expect(executeQuery).toHaveBeenCalledWith(
135
139
  expect.objectContaining({
136
- bodyType: "formData",
137
- formData: [
138
- { key: "note", value: "hello" },
139
- { key: "file", value: "contents", file: { filename: "note.txt" } },
140
+ headers: [
141
+ {
142
+ key: "content-type",
143
+ value: "application/x-www-form-urlencoded; charset=utf-8",
144
+ },
140
145
  ],
141
146
  }),
142
147
  undefined,
@@ -144,14 +149,41 @@ describe("RestApiIntegrationPluginClientImpl request body types", () => {
144
149
  );
145
150
  });
146
151
 
147
- it("sends mixed multipart file parts as formDataBase64 with no top-level body", async () => {
152
+ it("sends binary bodies as base64 binaryBody with default Content-Type", async () => {
153
+ const bytes = Uint8Array.from([137, 80, 78, 71]);
154
+ const { client, executeQuery } = createClient();
155
+ await client.apiRequest(
156
+ {
157
+ method: "PUT",
158
+ path: "/files/report.png",
159
+ body: bytes,
160
+ bodyType: "binary",
161
+ },
162
+ { response: JsonResponseSchema },
163
+ );
164
+ expect(executeQuery).toHaveBeenCalledWith(
165
+ expect.objectContaining({
166
+ body: expect.any(String),
167
+ bodyType: "binaryBody",
168
+ headers: [{ key: "Content-Type", value: "application/octet-stream" }],
169
+ }),
170
+ undefined,
171
+ undefined,
172
+ );
173
+ });
174
+
175
+ it("sends multipart bodies as formDataBase64 with base64 file parts and no top-level body", async () => {
148
176
  const bytes = Uint8Array.from([137, 80, 78, 71]);
149
177
  const { client, executeQuery } = createClient();
150
178
  await client.apiRequest(
151
179
  {
152
180
  method: "POST",
153
181
  path: "/upload",
154
- body: { note: "mixed", image: { value: bytes, filename: "b.png" } },
182
+ body: {
183
+ note: "mixed",
184
+ file: { value: "contents", filename: "note.txt" },
185
+ image: { value: bytes, filename: "b.png" },
186
+ },
155
187
  bodyType: "multipartForm",
156
188
  },
157
189
  { response: JsonResponseSchema },
@@ -161,11 +193,14 @@ describe("RestApiIntegrationPluginClientImpl request body types", () => {
161
193
  bodyType: "formDataBase64",
162
194
  formData: [
163
195
  { key: "note", value: "mixed" },
164
- {
196
+ expect.objectContaining({
197
+ key: "file",
198
+ file: { filename: "note.txt" },
199
+ }),
200
+ expect.objectContaining({
165
201
  key: "image",
166
- value: Buffer.from(bytes).toString("base64"),
167
202
  file: { filename: "b.png" },
168
- },
203
+ }),
169
204
  ],
170
205
  }),
171
206
  undefined,
@@ -197,7 +232,6 @@ describe("RestApiIntegrationPluginClientImpl request body types", () => {
197
232
 
198
233
  const invalidCases = [
199
234
  { method: "POST", path: "/bad", body: Uint8Array.from([1]) },
200
- { method: "POST", path: "/bad", body: () => undefined },
201
235
  { method: "POST", path: "/bad", bodyType: "raw" },
202
236
  { method: "POST", path: "/bad", bodyType: "formUrlencoded" },
203
237
  { method: "POST", path: "/bad", bodyType: "multipartForm" },
@@ -211,4 +245,79 @@ describe("RestApiIntegrationPluginClientImpl request body types", () => {
211
245
  expect(executeQuery).not.toHaveBeenCalled();
212
246
  }
213
247
  });
248
+
249
+ it("redacts request bodies that contain binary from bodySchema validation errors", async () => {
250
+ const bytes = Uint8Array.from([1, 2, 3, 4]);
251
+ const binaryBodies: {
252
+ body: unknown;
253
+ bodyType: string;
254
+ expectedData: unknown;
255
+ }[] = [
256
+ {
257
+ body: bytes,
258
+ bodyType: "binary",
259
+ expectedData: REDACTED_BINARY_RESPONSE_DATA,
260
+ },
261
+ {
262
+ body: bytes.buffer,
263
+ bodyType: "binary",
264
+ expectedData: REDACTED_BINARY_RESPONSE_DATA,
265
+ },
266
+ {
267
+ body: { file: { value: bytes, filename: "a.bin" }, note: "hello" },
268
+ bodyType: "multipartForm",
269
+ expectedData: {
270
+ file: { value: REDACTED_BINARY_RESPONSE_DATA, filename: "a.bin" },
271
+ note: "hello",
272
+ },
273
+ },
274
+ {
275
+ body: { files: [{ value: bytes.buffer, filename: "a.bin" }] },
276
+ bodyType: "multipartForm",
277
+ expectedData: {
278
+ files: [{ value: REDACTED_BINARY_RESPONSE_DATA, filename: "a.bin" }],
279
+ },
280
+ },
281
+ ];
282
+ for (const { body, bodyType, expectedData } of binaryBodies) {
283
+ const { client, executeQuery } = createClient();
284
+ const error = await callApiRequest(
285
+ client,
286
+ { method: "POST", path: "/upload", body, bodyType },
287
+ { body: z.never(), response: JsonResponseSchema },
288
+ ).catch((caught: unknown) => caught);
289
+ expect(error).toBeInstanceOf(RestApiValidationError);
290
+ if (!(error instanceof RestApiValidationError)) throw error;
291
+ expect(error.details.data).toEqual(expectedData);
292
+ expect(executeQuery).not.toHaveBeenCalled();
293
+ }
294
+
295
+ const { client } = createClient();
296
+ const error = await callApiRequest(
297
+ client,
298
+ { method: "POST", path: "/orders", body: { amount: "not-a-number" } },
299
+ { body: z.object({ amount: z.number() }), response: JsonResponseSchema },
300
+ ).catch((caught: unknown) => caught);
301
+ expect(error).toBeInstanceOf(RestApiValidationError);
302
+ if (!(error instanceof RestApiValidationError)) throw error;
303
+ expect(error.details.data).toEqual({ amount: "not-a-number" });
304
+ });
305
+
306
+ it("does not leak binary bytes into the thrown message or zodError for a literal-schema mismatch", async () => {
307
+ const bytes = Uint8Array.from([137, 80, 78, 71]);
308
+ const { client } = createClient();
309
+ const error = await callApiRequest(
310
+ client,
311
+ { method: "PUT", path: "/upload", body: bytes, bodyType: "binary" },
312
+ {
313
+ body: z.literal("none"),
314
+ response: JsonResponseSchema,
315
+ },
316
+ ).catch((caught: unknown) => caught);
317
+ expect(error).toBeInstanceOf(RestApiValidationError);
318
+ if (!(error instanceof RestApiValidationError)) throw error;
319
+ expect(error.details.data).toBe(REDACTED_BINARY_RESPONSE_DATA);
320
+ expect(error.message).not.toContain("137");
321
+ expect(JSON.stringify(error.details.zodError.issues)).not.toContain("137");
322
+ });
214
323
  });
@@ -284,6 +284,29 @@ describe("RestApiIntegrationPluginClientImpl", () => {
284
284
  }
285
285
  throw new Error("Expected binary response validation to fail");
286
286
  });
287
+
288
+ it("keeps binary bytes out of the thrown message, not just details.zodError", async () => {
289
+ const bytes = [137, 80, 78, 71];
290
+ const { client } = createClient(workerBufferPayload(bytes));
291
+
292
+ let caught: unknown;
293
+ try {
294
+ await callApiRequest(
295
+ client,
296
+ { method: "GET", path: "/file.png", responseType: "binary" },
297
+ { response: z.literal("unexpected") },
298
+ );
299
+ } catch (error) {
300
+ caught = error;
301
+ }
302
+
303
+ expect(caught).toBeInstanceOf(RestApiValidationError);
304
+ if (!(caught instanceof RestApiValidationError)) throw caught;
305
+ expect(caught.message).not.toContain("137");
306
+ expect(JSON.stringify(caught.details.zodError.issues)).not.toContain(
307
+ "137",
308
+ );
309
+ });
287
310
  });
288
311
 
289
312
  describe("request body validation", () => {
@@ -16,11 +16,11 @@
16
16
  },
17
17
  {
18
18
  "file": "overlays/request-body-types.md",
19
- "sdkVersionRange": ">=0.0.5"
19
+ "sdkVersionRange": ">=0.0.7"
20
20
  },
21
21
  {
22
22
  "file": "overlays/request-body-types-unsupported.md",
23
- "sdkVersionRange": "<0.0.5"
23
+ "sdkVersionRange": "<0.0.7"
24
24
  }
25
25
  ]
26
26
  }