@superblocksteam/sdk-api 0.0.13 → 0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -610
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -1
- package/dist/errors.js.map +1 -1
- package/dist/integrations/base/decode-worker-binary-response.d.ts.map +1 -1
- package/dist/integrations/base/decode-worker-binary-response.js +22 -1
- package/dist/integrations/base/decode-worker-binary-response.js.map +1 -1
- package/dist/integrations/base/decode-worker-binary-response.test.js +15 -2
- package/dist/integrations/base/decode-worker-binary-response.test.js.map +1 -1
- package/dist/integrations/base/index.d.ts +1 -1
- package/dist/integrations/base/index.d.ts.map +1 -1
- package/dist/integrations/base/rest-api-integration-client.d.ts +2 -2
- package/dist/integrations/base/rest-api-integration-client.d.ts.map +1 -1
- package/dist/integrations/base/rest-api-integration-client.js +3 -1
- package/dist/integrations/base/rest-api-integration-client.js.map +1 -1
- package/dist/integrations/base/types.d.ts +23 -6
- package/dist/integrations/base/types.d.ts.map +1 -1
- package/dist/integrations/restapiintegration/client.test.js +42 -32
- package/dist/integrations/restapiintegration/client.test.js.map +1 -1
- package/package.json +1 -1
- package/src/errors.ts +4 -1
- package/src/integrations/base/decode-worker-binary-response.test.ts +25 -5
- package/src/integrations/base/decode-worker-binary-response.ts +26 -1
- package/src/integrations/base/index.ts +1 -0
- package/src/integrations/base/rest-api-integration-client.ts +6 -3
- package/src/integrations/base/types.ts +25 -6
- package/src/integrations/lakebase/README.md +1 -3
- package/src/integrations/postgres/README.md +1 -3
- package/src/integrations/restapiintegration/client.test.ts +51 -38
- package/src/integrations/restapiintegration/overlays/response-types-binary.md +4 -8
- package/src/integrations/snowflakepostgres/README.md +1 -3
|
@@ -7,6 +7,18 @@ import {
|
|
|
7
7
|
|
|
8
8
|
export { REDACTED_BINARY_RESPONSE_DATA };
|
|
9
9
|
|
|
10
|
+
// Node's decoder accepts missing and misplaced padding. A worker payload is
|
|
11
|
+
// a canonical padded string, so reject anything else before decoding.
|
|
12
|
+
const BASE64 =
|
|
13
|
+
/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
14
|
+
|
|
15
|
+
function decodeBase64(data: string): Uint8Array | undefined {
|
|
16
|
+
if (!BASE64.test(data)) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
return new Uint8Array(Buffer.from(data, "base64"));
|
|
20
|
+
}
|
|
21
|
+
|
|
10
22
|
const workerBinaryResponseSchema = z.union([
|
|
11
23
|
z.instanceof(Uint8Array).transform((value) => new Uint8Array(value)),
|
|
12
24
|
z
|
|
@@ -15,10 +27,23 @@ const workerBinaryResponseSchema = z.union([
|
|
|
15
27
|
type: z.literal("Buffer"),
|
|
16
28
|
})
|
|
17
29
|
.transform((value, context) => {
|
|
30
|
+
if (typeof value.data === "string") {
|
|
31
|
+
const bytes = decodeBase64(value.data);
|
|
32
|
+
if (!bytes) {
|
|
33
|
+
context.addIssue({
|
|
34
|
+
code: z.ZodIssueCode.custom,
|
|
35
|
+
message: "data must be base64",
|
|
36
|
+
path: ["data"],
|
|
37
|
+
});
|
|
38
|
+
return z.NEVER;
|
|
39
|
+
}
|
|
40
|
+
return bytes;
|
|
41
|
+
}
|
|
42
|
+
|
|
18
43
|
if (!Array.isArray(value.data)) {
|
|
19
44
|
context.addIssue({
|
|
20
45
|
code: z.ZodIssueCode.custom,
|
|
21
|
-
message: "data must be an array of bytes",
|
|
46
|
+
message: "data must be an array of bytes or a base64 string",
|
|
22
47
|
path: ["data"],
|
|
23
48
|
});
|
|
24
49
|
return z.NEVER;
|
|
@@ -9,6 +9,7 @@ import { RestApiClientBase } from "./rest-api-client-base.js";
|
|
|
9
9
|
import type {
|
|
10
10
|
ApiRequestOptions,
|
|
11
11
|
ApiRequestSchema,
|
|
12
|
+
BinaryApiRequestSchema,
|
|
12
13
|
RestApiResponseType,
|
|
13
14
|
SupportsApiRequest,
|
|
14
15
|
} from "./types.js";
|
|
@@ -30,7 +31,7 @@ export abstract class RestApiIntegrationClient
|
|
|
30
31
|
options: ApiRequestOptions<TBody> & {
|
|
31
32
|
responseType: "binary";
|
|
32
33
|
},
|
|
33
|
-
schema?:
|
|
34
|
+
schema?: BinaryApiRequestSchema<TBody>,
|
|
34
35
|
metadata?: TraceMetadata,
|
|
35
36
|
): Promise<Uint8Array>;
|
|
36
37
|
async apiRequest<TBody>(
|
|
@@ -42,7 +43,7 @@ export abstract class RestApiIntegrationClient
|
|
|
42
43
|
): Promise<unknown>;
|
|
43
44
|
async apiRequest<TBody, TResponse>(
|
|
44
45
|
options: ApiRequestOptions<TBody>,
|
|
45
|
-
schema?: ApiRequestSchema<TBody, TResponse>,
|
|
46
|
+
schema?: ApiRequestSchema<TBody, TResponse> | BinaryApiRequestSchema<TBody>,
|
|
46
47
|
metadata?: TraceMetadata,
|
|
47
48
|
): Promise<TResponse | Uint8Array | unknown> {
|
|
48
49
|
if ((options.responseType ?? "json") === "json" && !schema?.response) {
|
|
@@ -67,7 +68,9 @@ export abstract class RestApiIntegrationClient
|
|
|
67
68
|
metadata,
|
|
68
69
|
);
|
|
69
70
|
|
|
70
|
-
|
|
71
|
+
// Binary bodies are validated while decoding. A caller's schema cannot
|
|
72
|
+
// check them: the sandbox's Uint8Array is not the SDK's constructor.
|
|
73
|
+
if (options.responseType === "binary" || !schema?.response) {
|
|
71
74
|
return result;
|
|
72
75
|
}
|
|
73
76
|
|
|
@@ -61,10 +61,10 @@ export interface ApiRequestOptions<TBody = unknown> {
|
|
|
61
61
|
* How the response body should be decoded. Defaults to `"json"`.
|
|
62
62
|
*
|
|
63
63
|
* Use `"text"` for endpoints that return non-JSON payloads such as
|
|
64
|
-
* XML. Use `"binary"` for PDFs and other byte payloads.
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* `
|
|
64
|
+
* XML. Use `"binary"` for PDFs and other byte payloads. Text responses
|
|
65
|
+
* may omit the response schema or use one matching the decoded value,
|
|
66
|
+
* e.g. `z.string()`. Binary responses never take a response schema; the
|
|
67
|
+
* decoded `Uint8Array` is already validated.
|
|
68
68
|
*/
|
|
69
69
|
responseType?: RestApiResponseType;
|
|
70
70
|
}
|
|
@@ -83,6 +83,24 @@ export interface ApiRequestSchema<TBody = unknown, TResponse = unknown> {
|
|
|
83
83
|
response?: z.ZodSchema<TResponse>;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Schemas accepted by `apiRequest()` with `responseType: "binary"`.
|
|
88
|
+
*
|
|
89
|
+
* There is no response schema: the SDK decodes and validates the bytes
|
|
90
|
+
* itself. API code runs in a sandbox whose `Uint8Array` is a different
|
|
91
|
+
* constructor from the SDK's, so `z.instanceof(Uint8Array)` would reject
|
|
92
|
+
* every binary response. A `response` passed at runtime is ignored.
|
|
93
|
+
*/
|
|
94
|
+
export interface BinaryApiRequestSchema<TBody = unknown> {
|
|
95
|
+
/**
|
|
96
|
+
* Optional Zod schema for request body validation.
|
|
97
|
+
* When omitted, `options.body` is sent without validation.
|
|
98
|
+
*/
|
|
99
|
+
body?: z.ZodSchema<TBody>;
|
|
100
|
+
|
|
101
|
+
response?: never;
|
|
102
|
+
}
|
|
103
|
+
|
|
86
104
|
/**
|
|
87
105
|
* Interface for integration clients that support generic API requests.
|
|
88
106
|
*
|
|
@@ -153,7 +171,8 @@ export interface SupportsApiRequest {
|
|
|
153
171
|
* Execute a generic API request that returns binary data.
|
|
154
172
|
*
|
|
155
173
|
* @param options - Request configuration; `responseType` must be "binary"
|
|
156
|
-
* @param schema - Optional Zod
|
|
174
|
+
* @param schema - Optional Zod schema for request body validation. Binary
|
|
175
|
+
* responses do not take a response schema.
|
|
157
176
|
* @param metadata - Optional trace metadata for observability (label, description)
|
|
158
177
|
* @returns The decoded body as a `Uint8Array`
|
|
159
178
|
*
|
|
@@ -171,7 +190,7 @@ export interface SupportsApiRequest {
|
|
|
171
190
|
options: ApiRequestOptions<TBody> & {
|
|
172
191
|
responseType: "binary";
|
|
173
192
|
},
|
|
174
|
-
schema?:
|
|
193
|
+
schema?: BinaryApiRequestSchema<TBody>,
|
|
175
194
|
metadata?: TraceMetadata,
|
|
176
195
|
): Promise<Uint8Array>;
|
|
177
196
|
|
|
@@ -162,9 +162,7 @@ The `query()` method requires a Zod schema for runtime validation:
|
|
|
162
162
|
// WRONG - Missing schema parameter
|
|
163
163
|
const users = await ctx.integrations.db.query(
|
|
164
164
|
"SELECT * FROM users",
|
|
165
|
-
[
|
|
166
|
-
/* params */
|
|
167
|
-
], // This is wrong - params are 3rd argument
|
|
165
|
+
[/* params */], // This is wrong - params are 3rd argument
|
|
168
166
|
);
|
|
169
167
|
|
|
170
168
|
// CORRECT - Schema is the second parameter
|
|
@@ -292,9 +292,7 @@ The `query()` method requires a Zod schema for runtime validation. This is by de
|
|
|
292
292
|
// WRONG - Missing schema parameter
|
|
293
293
|
const users = await ctx.integrations.db.query(
|
|
294
294
|
"SELECT * FROM users",
|
|
295
|
-
[
|
|
296
|
-
/* params */
|
|
297
|
-
], // This is wrong - params are 3rd argument
|
|
295
|
+
[/* params */], // This is wrong - params are 3rd argument
|
|
298
296
|
);
|
|
299
297
|
|
|
300
298
|
// CORRECT - Schema is the second parameter
|
|
@@ -227,50 +227,55 @@ describe("RestApiIntegrationPluginClientImpl", () => {
|
|
|
227
227
|
expect(executeQuery).toHaveBeenCalled();
|
|
228
228
|
});
|
|
229
229
|
|
|
230
|
-
it("
|
|
230
|
+
it("returns decoded bytes and ignores a response schema passed at runtime", async () => {
|
|
231
|
+
// Models sandboxed API code: its Uint8Array is a different constructor,
|
|
232
|
+
// so a caller's z.instanceof(Uint8Array) rejects every SDK-decoded body.
|
|
233
|
+
const crossRealmInstanceof = z.custom<Uint8Array>(() => false);
|
|
231
234
|
const { client } = createClient(workerBufferPayload(PDF_BYTES));
|
|
232
235
|
|
|
233
|
-
const result = await
|
|
236
|
+
const result = await callApiRequest(
|
|
237
|
+
client,
|
|
234
238
|
{ method: "GET", path: "/file.pdf", responseType: "binary" },
|
|
235
|
-
{
|
|
236
|
-
response: z
|
|
237
|
-
.instanceof(Uint8Array)
|
|
238
|
-
.refine((bytes) => bytes[0] === 0x25)
|
|
239
|
-
.transform((bytes) => bytes.slice(1)),
|
|
240
|
-
},
|
|
239
|
+
{ response: crossRealmInstanceof },
|
|
241
240
|
);
|
|
242
241
|
|
|
243
|
-
expect(
|
|
242
|
+
expect(result).toBeInstanceOf(Uint8Array);
|
|
243
|
+
expect(result instanceof Uint8Array && Array.from(result)).toEqual(
|
|
244
|
+
PDF_BYTES,
|
|
245
|
+
);
|
|
244
246
|
});
|
|
245
247
|
|
|
246
|
-
it("
|
|
247
|
-
const { client } = createClient(
|
|
248
|
+
it("still validates the request body for binary responses", async () => {
|
|
249
|
+
const { client, executeQuery } = createClient(
|
|
250
|
+
workerBufferPayload(PDF_BYTES),
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const dynamicBody: unknown = JSON.parse('{"pages":"many"}');
|
|
248
254
|
|
|
249
255
|
await expect(
|
|
250
256
|
client.apiRequest(
|
|
251
|
-
{ method: "GET", path: "/file.pdf", responseType: "binary" },
|
|
252
257
|
{
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
258
|
+
method: "POST",
|
|
259
|
+
path: "/render.pdf",
|
|
260
|
+
body: dynamicBody,
|
|
261
|
+
responseType: "binary",
|
|
256
262
|
},
|
|
263
|
+
{ body: z.object({ pages: z.number() }) },
|
|
257
264
|
),
|
|
258
265
|
).rejects.toThrow(RestApiValidationError);
|
|
266
|
+
expect(executeQuery).not.toHaveBeenCalled();
|
|
259
267
|
});
|
|
260
268
|
|
|
261
|
-
it("redacts binary data from
|
|
262
|
-
const bytes = Array.from({ length: 4096 }, () => 0);
|
|
269
|
+
it("redacts binary data from malformed-body errors", async () => {
|
|
270
|
+
const bytes = [...Array.from({ length: 4096 }, () => 0), 256];
|
|
263
271
|
const { client } = createClient(workerBufferPayload(bytes));
|
|
264
272
|
|
|
265
273
|
try {
|
|
266
|
-
await client.apiRequest(
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
.refine((value) => value.byteLength < 1024),
|
|
272
|
-
},
|
|
273
|
-
);
|
|
274
|
+
await client.apiRequest({
|
|
275
|
+
method: "GET",
|
|
276
|
+
path: "/file.pdf",
|
|
277
|
+
responseType: "binary",
|
|
278
|
+
});
|
|
274
279
|
} catch (error) {
|
|
275
280
|
expect(error).toBeInstanceOf(RestApiValidationError);
|
|
276
281
|
if (!(error instanceof RestApiValidationError)) {
|
|
@@ -282,20 +287,20 @@ describe("RestApiIntegrationPluginClientImpl", () => {
|
|
|
282
287
|
});
|
|
283
288
|
return;
|
|
284
289
|
}
|
|
285
|
-
throw new Error("Expected binary
|
|
290
|
+
throw new Error("Expected malformed binary decoding to fail");
|
|
286
291
|
});
|
|
287
292
|
|
|
288
293
|
it("keeps binary bytes out of the thrown message, not just details.zodError", async () => {
|
|
289
|
-
const bytes = [137, 80, 78, 71];
|
|
294
|
+
const bytes = [137, 80, 78, 71, 256];
|
|
290
295
|
const { client } = createClient(workerBufferPayload(bytes));
|
|
291
296
|
|
|
292
297
|
let caught: unknown;
|
|
293
298
|
try {
|
|
294
|
-
await
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
);
|
|
299
|
+
await client.apiRequest({
|
|
300
|
+
method: "GET",
|
|
301
|
+
path: "/file.png",
|
|
302
|
+
responseType: "binary",
|
|
303
|
+
});
|
|
299
304
|
} catch (error) {
|
|
300
305
|
caught = error;
|
|
301
306
|
}
|
|
@@ -461,24 +466,32 @@ describe("RestApiIntegrationPluginClientImpl", () => {
|
|
|
461
466
|
await wrongBinary;
|
|
462
467
|
});
|
|
463
468
|
|
|
464
|
-
it("rejects
|
|
469
|
+
it("rejects response schemas on binary requests but accepts body schemas", async () => {
|
|
465
470
|
const { client: binaryClient } = createClient(
|
|
466
471
|
workerBufferPayload(PDF_BYTES),
|
|
467
472
|
);
|
|
468
473
|
|
|
469
|
-
// @ts-expect-error binary
|
|
470
|
-
binaryClient.apiRequest(
|
|
474
|
+
// @ts-expect-error binary responses do not take a response schema
|
|
475
|
+
const withResponse = binaryClient.apiRequest(
|
|
471
476
|
{
|
|
472
477
|
method: "GET",
|
|
473
478
|
path: "/file.pdf",
|
|
474
479
|
responseType: "binary",
|
|
475
480
|
},
|
|
481
|
+
{ response: z.instanceof(Uint8Array) },
|
|
482
|
+
);
|
|
483
|
+
await withResponse;
|
|
484
|
+
|
|
485
|
+
const withBody: Promise<Uint8Array> = binaryClient.apiRequest(
|
|
476
486
|
{
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
487
|
+
method: "POST",
|
|
488
|
+
path: "/render.pdf",
|
|
489
|
+
body: { pages: 1 },
|
|
490
|
+
responseType: "binary",
|
|
480
491
|
},
|
|
492
|
+
{ body: z.object({ pages: z.number() }) },
|
|
481
493
|
);
|
|
494
|
+
expect(Array.from(await withBody)).toEqual(PDF_BYTES);
|
|
482
495
|
});
|
|
483
496
|
|
|
484
497
|
it("rejects response types outside the public SDK contract", async () => {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
- `"binary"` for PDFs and other byte payloads. The result is a `Uint8Array`
|
|
6
6
|
- `"text"` for XML, CSV, HTML, and other decoded strings
|
|
7
7
|
|
|
8
|
-
With `responseType: "text"`
|
|
8
|
+
With `responseType: "text"` the response schema may be omitted, and the raw string is returned typed `unknown`. With `responseType: "binary"` never pass a response schema. The SDK decodes and validates the bytes and returns a `Uint8Array`:
|
|
9
9
|
|
|
10
10
|
```typescript
|
|
11
11
|
const xml = await ctx.integrations.legacyApi.apiRequest({
|
|
@@ -23,7 +23,7 @@ const pdf = await ctx.integrations.legacyApi.apiRequest({
|
|
|
23
23
|
// pdf is a Uint8Array
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
To validate after decoding, pass a schema that matches the decoded
|
|
26
|
+
To validate a text response after decoding, pass a schema that matches the decoded string:
|
|
27
27
|
|
|
28
28
|
```typescript
|
|
29
29
|
const xml = await ctx.integrations.legacyApi.apiRequest(
|
|
@@ -31,14 +31,10 @@ const xml = await ctx.integrations.legacyApi.apiRequest(
|
|
|
31
31
|
{ response: z.string() },
|
|
32
32
|
);
|
|
33
33
|
// xml is typed string
|
|
34
|
-
|
|
35
|
-
const pdf = await ctx.integrations.legacyApi.apiRequest(
|
|
36
|
-
{ method: "GET", path: "/file.pdf", responseType: "binary" },
|
|
37
|
-
{ response: z.instanceof(Uint8Array) },
|
|
38
|
-
);
|
|
39
|
-
// pdf is typed Uint8Array
|
|
40
34
|
```
|
|
41
35
|
|
|
36
|
+
Do not use `z.instanceof(Uint8Array)` anywhere in an API, including the response or output schema. API code runs in a sandbox where the bytes the SDK returns are not instances of the API code's `Uint8Array`, so that check always fails. Check binary content directly in `run()` instead, for example `if (pdf.byteLength === 0) throw new Error("Empty PDF")`.
|
|
37
|
+
|
|
42
38
|
Convert binary data to a JSON-safe representation before returning it from an SDK API:
|
|
43
39
|
|
|
44
40
|
```typescript
|
|
@@ -137,9 +137,7 @@ The `query()` method requires a Zod schema for runtime validation:
|
|
|
137
137
|
// WRONG - Missing schema parameter
|
|
138
138
|
const users = await ctx.integrations.db.query(
|
|
139
139
|
"SELECT * FROM users",
|
|
140
|
-
[
|
|
141
|
-
/* params */
|
|
142
|
-
], // This is wrong - params are 3rd argument
|
|
140
|
+
[/* params */], // This is wrong - params are 3rd argument
|
|
143
141
|
);
|
|
144
142
|
|
|
145
143
|
// CORRECT - Schema is the second parameter
|