@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.
Files changed (31) hide show
  1. package/README.md +35 -610
  2. package/dist/errors.d.ts.map +1 -1
  3. package/dist/errors.js +2 -1
  4. package/dist/errors.js.map +1 -1
  5. package/dist/integrations/base/decode-worker-binary-response.d.ts.map +1 -1
  6. package/dist/integrations/base/decode-worker-binary-response.js +22 -1
  7. package/dist/integrations/base/decode-worker-binary-response.js.map +1 -1
  8. package/dist/integrations/base/decode-worker-binary-response.test.js +15 -2
  9. package/dist/integrations/base/decode-worker-binary-response.test.js.map +1 -1
  10. package/dist/integrations/base/index.d.ts +1 -1
  11. package/dist/integrations/base/index.d.ts.map +1 -1
  12. package/dist/integrations/base/rest-api-integration-client.d.ts +2 -2
  13. package/dist/integrations/base/rest-api-integration-client.d.ts.map +1 -1
  14. package/dist/integrations/base/rest-api-integration-client.js +3 -1
  15. package/dist/integrations/base/rest-api-integration-client.js.map +1 -1
  16. package/dist/integrations/base/types.d.ts +23 -6
  17. package/dist/integrations/base/types.d.ts.map +1 -1
  18. package/dist/integrations/restapiintegration/client.test.js +42 -32
  19. package/dist/integrations/restapiintegration/client.test.js.map +1 -1
  20. package/package.json +1 -1
  21. package/src/errors.ts +4 -1
  22. package/src/integrations/base/decode-worker-binary-response.test.ts +25 -5
  23. package/src/integrations/base/decode-worker-binary-response.ts +26 -1
  24. package/src/integrations/base/index.ts +1 -0
  25. package/src/integrations/base/rest-api-integration-client.ts +6 -3
  26. package/src/integrations/base/types.ts +25 -6
  27. package/src/integrations/lakebase/README.md +1 -3
  28. package/src/integrations/postgres/README.md +1 -3
  29. package/src/integrations/restapiintegration/client.test.ts +51 -38
  30. package/src/integrations/restapiintegration/overlays/response-types-binary.md +4 -8
  31. 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;
@@ -10,6 +10,7 @@ export { REST_API_BODY_TYPES } from "./types.js";
10
10
  export type {
11
11
  ApiRequestOptions,
12
12
  ApiRequestSchema,
13
+ BinaryApiRequestSchema,
13
14
  RestApiBodyType,
14
15
  RestApiResponseType,
15
16
  SupportsApiRequest,
@@ -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?: ApiRequestSchema<TBody, Uint8Array>,
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
- if (!schema?.response) {
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. When
65
- * requesting a text or binary response, omit the response schema (or
66
- * use one matching the decoded value, e.g. `z.string()` or
67
- * `z.instanceof(Uint8Array)`).
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 schemas for request body and decoded response validation
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?: ApiRequestSchema<TBody, Uint8Array>,
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("accepts response schemas that transform bytes to bytes", async () => {
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 client.apiRequest(
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(Array.from(result)).toEqual(PDF_BYTES.slice(1));
242
+ expect(result).toBeInstanceOf(Uint8Array);
243
+ expect(result instanceof Uint8Array && Array.from(result)).toEqual(
244
+ PDF_BYTES,
245
+ );
244
246
  });
245
247
 
246
- it("throws RestApiValidationError when the schema rejects normalized bytes", async () => {
247
- const { client } = createClient(workerBufferPayload(PDF_BYTES));
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
- response: z
254
- .instanceof(Uint8Array)
255
- .refine((bytes) => bytes.byteLength > 64),
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 response validation errors", async () => {
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
- { method: "GET", path: "/file.pdf", responseType: "binary" },
268
- {
269
- response: z
270
- .instanceof(Uint8Array)
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 response validation to fail");
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 callApiRequest(
295
- client,
296
- { method: "GET", path: "/file.png", responseType: "binary" },
297
- { response: z.literal("unexpected") },
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 binary response schemas that transform bytes to another type", () => {
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 response schemas must output Uint8Array
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
- response: z
478
- .instanceof(Uint8Array)
479
- .transform((bytes) => bytes.byteLength),
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"` or `responseType: "binary"` the response schema may be omitted. Text returns the raw string typed `unknown`. Binary returns a `Uint8Array`:
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 value:
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