@superblocksteam/sdk-api 0.0.12 → 2.0.0-SNAPSHOT.1d540461e97d05b4
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/dist/errors.d.ts +28 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +187 -8
- 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/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 +2 -1
- package/dist/integrations/base/index.d.ts.map +1 -1
- package/dist/integrations/base/index.js +1 -0
- package/dist/integrations/base/index.js.map +1 -1
- package/dist/integrations/base/request-body.d.ts +29 -0
- package/dist/integrations/base/request-body.d.ts.map +1 -0
- package/dist/integrations/base/request-body.js +217 -0
- package/dist/integrations/base/request-body.js.map +1 -0
- package/dist/integrations/base/request-body.test.d.ts +2 -0
- package/dist/integrations/base/request-body.test.d.ts.map +1 -0
- package/dist/integrations/base/request-body.test.js +448 -0
- package/dist/integrations/base/request-body.test.js.map +1 -0
- package/dist/integrations/base/rest-api-client-base.d.ts.map +1 -1
- package/dist/integrations/base/rest-api-client-base.js +15 -14
- 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 +32 -5
- package/dist/integrations/base/types.d.ts.map +1 -1
- package/dist/integrations/base/types.js +7 -0
- package/dist/integrations/base/types.js.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 +53 -1
- 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.map +1 -1
- package/dist/integrations/dynamodb/client.js +2 -5
- package/dist/integrations/dynamodb/client.js.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/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.d.ts +2 -0
- package/dist/integrations/restapiintegration/client.body-types.test.d.ts.map +1 -0
- package/dist/integrations/restapiintegration/client.body-types.test.js +227 -0
- package/dist/integrations/restapiintegration/client.body-types.test.js.map +1 -0
- 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 +20 -0
- 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/package.json +6 -3
- package/src/errors.test.ts +257 -0
- package/src/errors.ts +214 -10
- 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 +2 -0
- package/src/integrations/base/request-body.test.ts +535 -0
- package/src/integrations/base/request-body.ts +368 -0
- package/src/integrations/base/rest-api-client-base.ts +24 -19
- package/src/integrations/base/rest-api-integration-client.ts +8 -7
- package/src/integrations/base/types.ts +41 -5
- package/src/integrations/cosmosdb/client.ts +8 -7
- package/src/integrations/documentation-resolver.test.ts +65 -1
- package/src/integrations/documentation.test.ts +12 -0
- package/src/integrations/dynamodb/client.ts +8 -7
- package/src/integrations/gcs/client.ts +8 -7
- package/src/integrations/gsheets/client.ts +8 -7
- 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 +323 -0
- package/src/integrations/restapiintegration/client.test.ts +23 -0
- package/src/integrations/restapiintegration/docs.manifest.json +8 -0
- package/src/integrations/restapiintegration/overlays/request-body-types-unsupported.md +7 -0
- package/src/integrations/restapiintegration/overlays/request-body-types.md +115 -0
- package/src/integrations/s3/client.ts +8 -7
- package/src/integrations/salesforce/client.ts +9 -7
- package/src/integrations/slack/client.test.ts +30 -0
- package/src/integrations/slack/client.ts +13 -19
- package/src/integrations/utils.ts +51 -1
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
|
|
9
9
|
import type { z } from "zod";
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
RestApiValidationError,
|
|
13
|
+
restApiValidationErrorFromZodError,
|
|
14
|
+
} from "../../errors.js";
|
|
12
15
|
import { IntegrationError } from "../../runtime/errors.js";
|
|
13
16
|
import type { QueryExecutor, TraceMetadata } from "../registry.js";
|
|
14
17
|
import type { IntegrationConfig, IntegrationClientImpl } from "../types.js";
|
|
@@ -85,12 +88,10 @@ export class DynamoDBClientImpl
|
|
|
85
88
|
const parseResult = schema.safeParse(result);
|
|
86
89
|
|
|
87
90
|
if (!parseResult.success) {
|
|
88
|
-
throw
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
data: result,
|
|
93
|
-
},
|
|
91
|
+
throw restApiValidationErrorFromZodError(
|
|
92
|
+
"Result validation failed",
|
|
93
|
+
parseResult.error,
|
|
94
|
+
result,
|
|
94
95
|
);
|
|
95
96
|
}
|
|
96
97
|
|
|
@@ -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 {
|
|
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
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
data: result,
|
|
79
|
-
},
|
|
77
|
+
throw restApiValidationErrorFromZodError(
|
|
78
|
+
"Result validation failed",
|
|
79
|
+
parseResult.error,
|
|
80
|
+
result,
|
|
80
81
|
);
|
|
81
82
|
}
|
|
82
83
|
|
|
@@ -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 {
|
|
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
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
data: result,
|
|
121
|
-
},
|
|
119
|
+
throw restApiValidationErrorFromZodError(
|
|
120
|
+
"Result validation failed",
|
|
121
|
+
parseResult.error,
|
|
122
|
+
result,
|
|
122
123
|
);
|
|
123
124
|
}
|
|
124
125
|
|
|
@@ -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,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 {
|
|
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
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
data: result,
|
|
123
|
-
},
|
|
121
|
+
throw restApiValidationErrorFromZodError(
|
|
122
|
+
"Result validation failed",
|
|
123
|
+
parseResult.error,
|
|
124
|
+
result,
|
|
124
125
|
);
|
|
125
126
|
}
|
|
126
127
|
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
REDACTED_BINARY_RESPONSE_DATA,
|
|
6
|
+
RestApiValidationError,
|
|
7
|
+
} from "../../errors.js";
|
|
8
|
+
import type { IntegrationConfig } from "../types.js";
|
|
9
|
+
import { RestApiIntegrationPluginClientImpl } from "./client.js";
|
|
10
|
+
|
|
11
|
+
const TEST_CONFIG: IntegrationConfig = {
|
|
12
|
+
id: "restapi-test-id",
|
|
13
|
+
name: "Test REST API",
|
|
14
|
+
pluginId: "restapiintegration",
|
|
15
|
+
configuration: {},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const JsonResponseSchema = z.object({ ok: z.boolean() });
|
|
19
|
+
|
|
20
|
+
function createClient() {
|
|
21
|
+
const executeQuery = vi.fn().mockResolvedValue({ ok: true });
|
|
22
|
+
const client = new RestApiIntegrationPluginClientImpl(
|
|
23
|
+
TEST_CONFIG,
|
|
24
|
+
executeQuery,
|
|
25
|
+
);
|
|
26
|
+
return { client, executeQuery };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function callApiRequest(
|
|
30
|
+
client: RestApiIntegrationPluginClientImpl,
|
|
31
|
+
...args: unknown[]
|
|
32
|
+
): Promise<unknown> {
|
|
33
|
+
return Reflect.apply(client.apiRequest, client, args);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("RestApiIntegrationPluginClientImpl request body types", () => {
|
|
37
|
+
it("preserves omitted and explicit JSON body wire behavior", async () => {
|
|
38
|
+
for (const bodyType of [undefined, "json"] as const) {
|
|
39
|
+
const { client, executeQuery } = createClient();
|
|
40
|
+
await client.apiRequest(
|
|
41
|
+
{ method: "POST", path: "/payments", body: { amount: 42 }, bodyType },
|
|
42
|
+
{ response: JsonResponseSchema },
|
|
43
|
+
);
|
|
44
|
+
expect(executeQuery).toHaveBeenCalledWith(
|
|
45
|
+
expect.objectContaining({
|
|
46
|
+
body: JSON.stringify({ amount: 42 }),
|
|
47
|
+
bodyType: "jsonBody",
|
|
48
|
+
}),
|
|
49
|
+
undefined,
|
|
50
|
+
undefined,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("sends raw string bodies as rawBody", async () => {
|
|
56
|
+
const { client, executeQuery } = createClient();
|
|
57
|
+
await client.apiRequest(
|
|
58
|
+
{
|
|
59
|
+
method: "POST",
|
|
60
|
+
path: "/xml",
|
|
61
|
+
body: "<note>hello</note>",
|
|
62
|
+
bodyType: "raw",
|
|
63
|
+
},
|
|
64
|
+
{ response: JsonResponseSchema },
|
|
65
|
+
);
|
|
66
|
+
expect(executeQuery).toHaveBeenCalledWith(
|
|
67
|
+
expect.objectContaining({
|
|
68
|
+
body: "<note>hello</note>",
|
|
69
|
+
bodyType: "rawBody",
|
|
70
|
+
}),
|
|
71
|
+
undefined,
|
|
72
|
+
undefined,
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("sends formUrlencoded bodies as rawBody with injected Content-Type", async () => {
|
|
77
|
+
const { client, executeQuery } = createClient();
|
|
78
|
+
await client.apiRequest(
|
|
79
|
+
{
|
|
80
|
+
method: "POST",
|
|
81
|
+
path: "/charges",
|
|
82
|
+
body: { amount: 2000, currency: "usd", "metadata[order_id]": "6735" },
|
|
83
|
+
bodyType: "formUrlencoded",
|
|
84
|
+
},
|
|
85
|
+
{ response: JsonResponseSchema },
|
|
86
|
+
);
|
|
87
|
+
expect(executeQuery).toHaveBeenCalledWith(
|
|
88
|
+
expect.objectContaining({
|
|
89
|
+
body: "amount=2000¤cy=usd&metadata%5Border_id%5D=6735",
|
|
90
|
+
bodyType: "rawBody",
|
|
91
|
+
headers: [
|
|
92
|
+
{ key: "Content-Type", value: "application/x-www-form-urlencoded" },
|
|
93
|
+
],
|
|
94
|
+
}),
|
|
95
|
+
undefined,
|
|
96
|
+
undefined,
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("appends the injected Content-Type after caller headers", async () => {
|
|
101
|
+
const { client, executeQuery } = createClient();
|
|
102
|
+
await client.apiRequest(
|
|
103
|
+
{
|
|
104
|
+
method: "POST",
|
|
105
|
+
path: "/charges",
|
|
106
|
+
headers: { Authorization: "Bearer x" },
|
|
107
|
+
body: { amount: 2000 },
|
|
108
|
+
bodyType: "formUrlencoded",
|
|
109
|
+
},
|
|
110
|
+
{ response: JsonResponseSchema },
|
|
111
|
+
);
|
|
112
|
+
expect(executeQuery).toHaveBeenCalledWith(
|
|
113
|
+
expect.objectContaining({
|
|
114
|
+
headers: [
|
|
115
|
+
{ key: "Authorization", value: "Bearer x" },
|
|
116
|
+
{ key: "Content-Type", value: "application/x-www-form-urlencoded" },
|
|
117
|
+
],
|
|
118
|
+
}),
|
|
119
|
+
undefined,
|
|
120
|
+
undefined,
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("keeps a caller Content-Type of any case and injects nothing", async () => {
|
|
125
|
+
const { client, executeQuery } = createClient();
|
|
126
|
+
await client.apiRequest(
|
|
127
|
+
{
|
|
128
|
+
method: "POST",
|
|
129
|
+
path: "/charges",
|
|
130
|
+
headers: {
|
|
131
|
+
"content-type": "application/x-www-form-urlencoded; charset=utf-8",
|
|
132
|
+
},
|
|
133
|
+
body: { amount: 2000 },
|
|
134
|
+
bodyType: "formUrlencoded",
|
|
135
|
+
},
|
|
136
|
+
{ response: JsonResponseSchema },
|
|
137
|
+
);
|
|
138
|
+
expect(executeQuery).toHaveBeenCalledWith(
|
|
139
|
+
expect.objectContaining({
|
|
140
|
+
headers: [
|
|
141
|
+
{
|
|
142
|
+
key: "content-type",
|
|
143
|
+
value: "application/x-www-form-urlencoded; charset=utf-8",
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
}),
|
|
147
|
+
undefined,
|
|
148
|
+
undefined,
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
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 () => {
|
|
176
|
+
const bytes = Uint8Array.from([137, 80, 78, 71]);
|
|
177
|
+
const { client, executeQuery } = createClient();
|
|
178
|
+
await client.apiRequest(
|
|
179
|
+
{
|
|
180
|
+
method: "POST",
|
|
181
|
+
path: "/upload",
|
|
182
|
+
body: {
|
|
183
|
+
note: "mixed",
|
|
184
|
+
file: { value: "contents", filename: "note.txt" },
|
|
185
|
+
image: { value: bytes, filename: "b.png" },
|
|
186
|
+
},
|
|
187
|
+
bodyType: "multipartForm",
|
|
188
|
+
},
|
|
189
|
+
{ response: JsonResponseSchema },
|
|
190
|
+
);
|
|
191
|
+
expect(executeQuery).toHaveBeenCalledWith(
|
|
192
|
+
expect.objectContaining({
|
|
193
|
+
bodyType: "formDataBase64",
|
|
194
|
+
formData: [
|
|
195
|
+
{ key: "note", value: "mixed" },
|
|
196
|
+
expect.objectContaining({
|
|
197
|
+
key: "file",
|
|
198
|
+
file: { filename: "note.txt" },
|
|
199
|
+
}),
|
|
200
|
+
expect.objectContaining({
|
|
201
|
+
key: "image",
|
|
202
|
+
file: { filename: "b.png" },
|
|
203
|
+
}),
|
|
204
|
+
],
|
|
205
|
+
}),
|
|
206
|
+
undefined,
|
|
207
|
+
undefined,
|
|
208
|
+
);
|
|
209
|
+
expect(executeQuery.mock.calls[0][0]).not.toHaveProperty("body");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("rejects invalid bodyType and invalid bodies before executeQuery", async () => {
|
|
213
|
+
for (const bodyType of [
|
|
214
|
+
"form",
|
|
215
|
+
"jsonBody",
|
|
216
|
+
"rawBody",
|
|
217
|
+
"formData",
|
|
218
|
+
"fileForm",
|
|
219
|
+
"binaryBody",
|
|
220
|
+
"formDataBase64",
|
|
221
|
+
]) {
|
|
222
|
+
const { client, executeQuery } = createClient();
|
|
223
|
+
await expect(
|
|
224
|
+
callApiRequest(
|
|
225
|
+
client,
|
|
226
|
+
{ method: "POST", path: "/bad", body: {}, bodyType },
|
|
227
|
+
{ response: JsonResponseSchema },
|
|
228
|
+
),
|
|
229
|
+
).rejects.toThrow(RestApiValidationError);
|
|
230
|
+
expect(executeQuery).not.toHaveBeenCalled();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const invalidCases = [
|
|
234
|
+
{ method: "POST", path: "/bad", body: Uint8Array.from([1]) },
|
|
235
|
+
{ method: "POST", path: "/bad", bodyType: "raw" },
|
|
236
|
+
{ method: "POST", path: "/bad", bodyType: "formUrlencoded" },
|
|
237
|
+
{ method: "POST", path: "/bad", bodyType: "multipartForm" },
|
|
238
|
+
{ method: "POST", path: "/bad", bodyType: "binary" },
|
|
239
|
+
];
|
|
240
|
+
for (const options of invalidCases) {
|
|
241
|
+
const { client, executeQuery } = createClient();
|
|
242
|
+
await expect(
|
|
243
|
+
callApiRequest(client, options, { response: JsonResponseSchema }),
|
|
244
|
+
).rejects.toThrow(RestApiValidationError);
|
|
245
|
+
expect(executeQuery).not.toHaveBeenCalled();
|
|
246
|
+
}
|
|
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
|
+
});
|
|
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", () => {
|
|
@@ -13,6 +13,14 @@
|
|
|
13
13
|
{
|
|
14
14
|
"file": "overlays/response-types-unsupported.md",
|
|
15
15
|
"sdkVersionRange": "<0.0.3"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"file": "overlays/request-body-types.md",
|
|
19
|
+
"sdkVersionRange": ">=0.0.7"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"file": "overlays/request-body-types-unsupported.md",
|
|
23
|
+
"sdkVersionRange": "<0.0.7"
|
|
16
24
|
}
|
|
17
25
|
]
|
|
18
26
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
## Request Body Types
|
|
2
|
+
|
|
3
|
+
This organization's agent does not support the `bodyType` option on `apiRequest()` yet. Its worker runs an sdk-api version that always serializes request bodies as JSON, so raw strings, binary uploads, multipart/form-data, and application/x-www-form-urlencoded payloads cannot be sent from SDK APIs until the agent is upgraded.
|
|
4
|
+
|
|
5
|
+
Do not generate code that passes a `bodyType` value before that upgrade. It may typecheck against a newer local copy of the SDK but fail at runtime on this agent.
|
|
6
|
+
|
|
7
|
+
When the user needs a non-JSON request body, tell them the required action: an administrator must upgrade this organization's Superblocks agent (On-Premise Agent / OPA) to a newer version that supports the `bodyType` option. Until that upgrade happens, there is no code-level workaround.
|