@superblocksteam/sdk-api 0.0.7 → 0.0.8

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 (43) hide show
  1. package/dist/integrations/base/index.d.ts +1 -1
  2. package/dist/integrations/base/index.d.ts.map +1 -1
  3. package/dist/integrations/base/rest-api-client-base.d.ts +1 -1
  4. package/dist/integrations/base/rest-api-client-base.d.ts.map +1 -1
  5. package/dist/integrations/base/rest-api-client-base.js +23 -3
  6. package/dist/integrations/base/rest-api-client-base.js.map +1 -1
  7. package/dist/integrations/base/rest-api-integration-client.d.ts +9 -3
  8. package/dist/integrations/base/rest-api-integration-client.d.ts.map +1 -1
  9. package/dist/integrations/base/rest-api-integration-client.js +31 -3
  10. package/dist/integrations/base/rest-api-integration-client.js.map +1 -1
  11. package/dist/integrations/base/types.d.ts +69 -3
  12. package/dist/integrations/base/types.d.ts.map +1 -1
  13. package/dist/integrations/base/types.js +23 -1
  14. package/dist/integrations/base/types.js.map +1 -1
  15. package/dist/integrations/documentation-resolver.test.js +167 -1
  16. package/dist/integrations/documentation-resolver.test.js.map +1 -1
  17. package/dist/integrations/documentation.d.ts +1 -0
  18. package/dist/integrations/documentation.d.ts.map +1 -1
  19. package/dist/integrations/documentation.js +31 -8
  20. package/dist/integrations/documentation.js.map +1 -1
  21. package/dist/integrations/postgres/client.d.ts +0 -7
  22. package/dist/integrations/postgres/client.d.ts.map +1 -1
  23. package/dist/integrations/restapiintegration/client.test.d.ts +12 -0
  24. package/dist/integrations/restapiintegration/client.test.d.ts.map +1 -0
  25. package/dist/integrations/restapiintegration/client.test.js +236 -0
  26. package/dist/integrations/restapiintegration/client.test.js.map +1 -0
  27. package/dist/integrations/slack/types.d.ts +2 -2
  28. package/dist/integrations/slack/types.d.ts.map +1 -1
  29. package/package.json +6 -6
  30. package/src/integrations/base/index.ts +1 -0
  31. package/src/integrations/base/rest-api-client-base.ts +27 -3
  32. package/src/integrations/base/rest-api-integration-client.ts +54 -5
  33. package/src/integrations/base/types.ts +77 -3
  34. package/src/integrations/documentation-resolver.test.ts +190 -1
  35. package/src/integrations/documentation.ts +63 -11
  36. package/src/integrations/graphql/docs.manifest.json +6 -1
  37. package/src/integrations/graphql/overlays/dynamic-headers.md +34 -0
  38. package/src/integrations/postgres/client.ts +1 -1
  39. package/src/integrations/restapiintegration/client.test.ts +359 -0
  40. package/src/integrations/restapiintegration/docs.manifest.json +10 -1
  41. package/src/integrations/restapiintegration/overlays/response-types-unsupported.md +5 -0
  42. package/src/integrations/restapiintegration/overlays/response-types.md +26 -0
  43. package/src/integrations/slack/types.ts +2 -2
@@ -17,6 +17,7 @@ import type { Plugin as RestApiIntegrationPlugin } from "@superblocksteam/types/
17
17
  import { RestApiValidationError } from "../../errors.js";
18
18
  import type { QueryExecutor, TraceMetadata } from "../registry.js";
19
19
  import type { IntegrationConfig, IntegrationClientImpl } from "../types.js";
20
+ import { REST_API_RESPONSE_TYPES } from "./types.js";
20
21
  import type { ApiRequestOptions } from "./types.js";
21
22
 
22
23
  export type RestApiRequest = PartialMessage<RestApiIntegrationPlugin>;
@@ -58,7 +59,7 @@ export abstract class RestApiClientBase implements IntegrationClientImpl {
58
59
  * Returns the raw (unvalidated) response from the orchestrator.
59
60
  * Subclasses call this, then apply their own response handling.
60
61
  *
61
- * @param options - HTTP method, path, body, params, headers
62
+ * @param options - Request configuration (see {@link ApiRequestOptions})
62
63
  * @param bodySchema - Optional Zod schema for body validation
63
64
  * @param metadata - Optional trace metadata for observability
64
65
  * @returns Raw response from the orchestrator
@@ -96,13 +97,31 @@ export abstract class RestApiClientBase implements IntegrationClientImpl {
96
97
  }
97
98
  }
98
99
 
100
+ // The RestApiResponseType union is erased in the bundled JS user APIs
101
+ // run as, so values outside it (including wire values the orchestrator
102
+ // implements but the SDK deliberately does not expose, like "auto")
103
+ // must also be rejected at runtime, before the request is issued.
104
+ const responseTypeResult = z
105
+ .enum(REST_API_RESPONSE_TYPES)
106
+ .safeParse(options.responseType ?? "json");
107
+ if (!responseTypeResult.success) {
108
+ throw new RestApiValidationError(
109
+ `Unsupported responseType ${JSON.stringify(options.responseType)} — expected one of: ${REST_API_RESPONSE_TYPES.join(", ")}`,
110
+ {
111
+ zodError: responseTypeResult.error,
112
+ data: options.responseType,
113
+ },
114
+ );
115
+ }
116
+ const responseType = responseTypeResult.data;
117
+
99
118
  const request: RestApiRequest = {
100
119
  openApiAction: "genericHttpRequest",
101
120
  httpMethod: options.method.toUpperCase(),
102
121
  urlPath: options.path,
103
122
  headers,
104
123
  params,
105
- responseType: "json",
124
+ responseType,
106
125
  };
107
126
 
108
127
  if (options.body !== undefined) {
@@ -116,11 +135,16 @@ export abstract class RestApiClientBase implements IntegrationClientImpl {
116
135
  metadata,
117
136
  );
118
137
 
138
+ // Neither `null` nor `undefined` is a legitimate decode result for the
139
+ // exposed response types: JSON parses to a value the schema sees, and
140
+ // an empty text body decodes to "". Either value only arises from a
141
+ // broken execution contract, so both throw. (If "auto" is ever exposed,
142
+ // `null` becomes legitimate — it parses a literal `null` JSON body.)
119
143
  if (result === null || result === undefined) {
120
144
  const nonNullResult = z.object({}).safeParse(result);
121
145
  if (!nonNullResult.success) {
122
146
  throw new RestApiValidationError(
123
- `Integration query returned ${String(result)} — expected a JSON response object`,
147
+ `Integration query returned ${String(result)} for responseType "${responseType}" — expected a response value`,
124
148
  {
125
149
  zodError: nonNullResult.error,
126
150
  data: result,
@@ -3,10 +3,13 @@
3
3
  *
4
4
  * Extends RestApiClientBase with an apiRequest() that validates the
5
5
  * full response against a caller-supplied Zod schema and throws
6
- * RestApiValidationError on mismatch.
6
+ * RestApiValidationError on mismatch. When no response schema is
7
+ * provided, the raw result is returned without validation — useful for
8
+ * non-JSON responses (e.g. XML), which can alternatively be validated
9
+ * with a schema matching the decoded value (e.g. z.string()).
7
10
  */
8
11
 
9
- import type { z } from "zod";
12
+ import { z } from "zod";
10
13
 
11
14
  import { RestApiValidationError } from "../../errors.js";
12
15
  import type { TraceMetadata } from "../registry.js";
@@ -14,6 +17,7 @@ import { RestApiClientBase } from "./rest-api-client-base.js";
14
17
  import type {
15
18
  ApiRequestOptions,
16
19
  ApiRequestSchema,
20
+ RestApiResponseType,
17
21
  SupportsApiRequest,
18
22
  } from "./types.js";
19
23
 
@@ -34,10 +38,55 @@ export abstract class RestApiIntegrationClient
34
38
  response: z.ZodSchema<TResponse>;
35
39
  },
36
40
  metadata?: TraceMetadata,
37
- ): Promise<TResponse> {
38
- const result = await this.executeApiRequest(options, schema.body, metadata);
41
+ ): Promise<TResponse>;
42
+ async apiRequest<TBody>(
43
+ options: ApiRequestOptions<TBody> & {
44
+ responseType: Exclude<RestApiResponseType, "json">;
45
+ },
46
+ schema?: ApiRequestSchema<TBody, unknown>,
47
+ metadata?: TraceMetadata,
48
+ ): Promise<unknown>;
49
+ async apiRequest<TBody, TResponse>(
50
+ options: ApiRequestOptions<TBody>,
51
+ schema?: ApiRequestSchema<TBody, TResponse>,
52
+ metadata?: TraceMetadata,
53
+ ): Promise<TResponse | unknown> {
54
+ // The overloads make schema-less JSON unrepresentable in TypeScript,
55
+ // but user API code executes as bundled JS with no type enforcement —
56
+ // enforce the same contract at runtime, before issuing the request.
57
+ if ((options.responseType ?? "json") === "json" && !schema?.response) {
58
+ throw new RestApiValidationError(
59
+ 'apiRequest() with responseType "json" requires a response schema — provide one, or set responseType: "text" to receive a non-JSON payload unvalidated',
60
+ {
61
+ zodError: new z.ZodError([
62
+ {
63
+ code: z.ZodIssueCode.custom,
64
+ message: "response schema is required for JSON responses",
65
+ path: ["response"],
66
+ },
67
+ ]),
68
+ data: undefined,
69
+ },
70
+ );
71
+ }
72
+
73
+ const result = await this.executeApiRequest(
74
+ options,
75
+ schema?.body,
76
+ metadata,
77
+ );
78
+
79
+ // Without a response schema the raw result is returned as-is (typed
80
+ // unknown). Non-JSON responses (e.g. responseType "text" returning
81
+ // XML) can either omit the schema or pass one matching the decoded
82
+ // value, such as z.string(). Request-body validation is opt-in:
83
+ // executeApiRequest only validates options.body when schema.body is
84
+ // provided (unchanged from the previous contract, where schema.body
85
+ // was already optional).
86
+ if (!schema?.response) {
87
+ return result;
88
+ }
39
89
 
40
- // Response schema is REQUIRED - always validate
41
90
  const responseParseResult = schema.response.safeParse(result);
42
91
 
43
92
  if (!responseParseResult.success) {
@@ -9,6 +9,32 @@ import type { TraceMetadata } from "../registry.js";
9
9
  // Re-export for backwards compatibility
10
10
  export type { TraceMetadata };
11
11
 
12
+ /**
13
+ * How the orchestrator should decode the HTTP response body.
14
+ *
15
+ * - `"json"` (default): parse the response as JSON; fails on non-JSON bodies
16
+ * - `"text"`: return the decoded response body as a string (XML, CSV, HTML, ...)
17
+ *
18
+ * The orchestrator also implements `"auto"`, `"binary"`, and `"raw"` wire
19
+ * values; they are deliberately not exposed here (widening this union later
20
+ * is backward-compatible, narrowing it is not):
21
+ *
22
+ * - `"auto"` returns parsed JSON whenever the body happens to parse, which
23
+ * would let JSON responses bypass schema validation via the schema-less
24
+ * overload
25
+ * - `"binary"` would freeze the worker's internal Buffer JSON encoding
26
+ * (`{ type: "Buffer", data: number[] }`, ~4x payload inflation) into the
27
+ * public SDK contract
28
+ * - `"raw"` is rejected by the orchestrator for non-streaming requests
29
+ *
30
+ * The runtime tuple backs the request-time guard in `executeApiRequest`:
31
+ * the type-level union is erased in the bundled JS user APIs run as, so
32
+ * values outside it must also be rejected at runtime.
33
+ */
34
+ export const REST_API_RESPONSE_TYPES = ["json", "text"] as const;
35
+
36
+ export type RestApiResponseType = (typeof REST_API_RESPONSE_TYPES)[number];
37
+
12
38
  /**
13
39
  * Options for making a generic REST API request.
14
40
  */
@@ -38,6 +64,15 @@ export interface ApiRequestOptions<TBody = unknown> {
38
64
  * Optional HTTP headers
39
65
  */
40
66
  headers?: Record<string, string>;
67
+
68
+ /**
69
+ * How the response body should be decoded. Defaults to `"json"`.
70
+ *
71
+ * Use `"text"` for endpoints that return non-JSON payloads such as
72
+ * XML. When requesting a text response, omit the response schema (or
73
+ * use one matching the decoded string, e.g. `z.string()`).
74
+ */
75
+ responseType?: RestApiResponseType;
41
76
  }
42
77
 
43
78
  /**
@@ -46,7 +81,7 @@ export interface ApiRequestOptions<TBody = unknown> {
46
81
  export interface ApiRequestSchema<TBody = unknown, TResponse = unknown> {
47
82
  /**
48
83
  * Optional Zod schema for request body validation.
49
- * Required if body is provided in options.
84
+ * When omitted, `options.body` is sent without validation.
50
85
  */
51
86
  body?: z.ZodSchema<TBody>;
52
87
 
@@ -60,14 +95,18 @@ export interface ApiRequestSchema<TBody = unknown, TResponse = unknown> {
60
95
  /**
61
96
  * Interface for integration clients that support generic API requests.
62
97
  *
63
- * All API requests require response schema validation for type safety.
98
+ * Providing a response schema gives type-safe, validated results. Omitting
99
+ * it returns the raw response as `unknown`. For non-JSON payloads (e.g.
100
+ * `responseType: "text"` for XML), either omit the schema or supply one
101
+ * matching the decoded value (e.g. `z.string()` for text) — object schemas
102
+ * only make sense for JSON-shaped results.
64
103
  */
65
104
  export interface SupportsApiRequest {
66
105
  /**
67
106
  * Execute a generic API request with type-safe validation.
68
107
  *
69
108
  * @param options - Request configuration including method, path, params, and body
70
- * @param schema - Zod schemas for request body and response validation (response schema REQUIRED)
109
+ * @param schema - Zod schemas for request body and response validation
71
110
  * @param metadata - Optional trace metadata for observability (label, description)
72
111
  * @returns Validated response data
73
112
  *
@@ -103,4 +142,39 @@ export interface SupportsApiRequest {
103
142
  },
104
143
  metadata?: TraceMetadata,
105
144
  ): Promise<TResponse>;
145
+
146
+ /**
147
+ * Execute a generic API request without response validation.
148
+ *
149
+ * This overload requires an explicit non-JSON `responseType` — JSON
150
+ * responses must always be consumed through the schema overload above,
151
+ * so "unvalidated JSON" is unrepresentable. The decoded non-JSON
152
+ * response is returned as-is, typed `unknown`.
153
+ *
154
+ * Note: request-body validation is opt-in — it runs only when
155
+ * `schema.body` is provided. Omitting `schema` sends `options.body`
156
+ * without validation.
157
+ *
158
+ * @param options - Request configuration; `responseType` must be "text"
159
+ * @param schema - Optional Zod schema for request body validation
160
+ * @param metadata - Optional trace metadata for observability (label, description)
161
+ * @returns The raw response from the integration
162
+ *
163
+ * @example
164
+ * ```typescript
165
+ * const xml = await ctx.integrations.legacyApi.apiRequest({
166
+ * method: 'GET',
167
+ * path: '/report.xml',
168
+ * responseType: 'text',
169
+ * });
170
+ * // xml is the raw XML string
171
+ * ```
172
+ */
173
+ apiRequest<TBody>(
174
+ options: ApiRequestOptions<TBody> & {
175
+ responseType: Exclude<RestApiResponseType, "json">;
176
+ },
177
+ schema?: ApiRequestSchema<TBody, unknown>,
178
+ metadata?: TraceMetadata,
179
+ ): Promise<unknown>;
106
180
  }
@@ -659,7 +659,27 @@ describe("resolveIntegrationDocumentation", () => {
659
659
  }
660
660
  });
661
661
 
662
- it("throws when overlay entry has empty versionRange", async () => {
662
+ it("throws when overlay entry has neither versionRange nor sdkVersionRange", async () => {
663
+ const integrationsDirectory = createPluginDocsFixture("dropbox", {
664
+ "README.md": "base",
665
+ "docs.manifest.json": JSON.stringify({
666
+ overlays: [{ file: "overlays/01.md" }],
667
+ }),
668
+ });
669
+
670
+ try {
671
+ await expect(
672
+ resolveIntegrationDocumentation("dropbox", {
673
+ pluginVersion: "0.4.0",
674
+ integrationsDirectory,
675
+ }),
676
+ ).rejects.toThrowError(/Invalid overlay entry/);
677
+ } finally {
678
+ rmSync(integrationsDirectory, { recursive: true, force: true });
679
+ }
680
+ });
681
+
682
+ it("throws when overlay entry has empty versionRange and no sdkVersionRange", async () => {
663
683
  const integrationsDirectory = createPluginDocsFixture("dropbox", {
664
684
  "README.md": "base",
665
685
  "docs.manifest.json": JSON.stringify({
@@ -680,6 +700,175 @@ describe("resolveIntegrationDocumentation", () => {
680
700
  });
681
701
  });
682
702
 
703
+ describe("sdkVersionRange", () => {
704
+ it("applies overlay when sdkVersion matches sdkVersionRange", async () => {
705
+ const integrationsDirectory = createPluginDocsFixture("graphql", {
706
+ "README.md": "base-graphql-docs",
707
+ "docs.manifest.json": JSON.stringify({
708
+ overlays: [
709
+ { file: "overlays/headers.md", sdkVersionRange: ">=0.0.2" },
710
+ ],
711
+ }),
712
+ "overlays/headers.md": "dynamic-headers-docs",
713
+ });
714
+
715
+ try {
716
+ const docs = await resolveIntegrationDocumentation("graphql", {
717
+ sdkVersion: "0.0.2",
718
+ integrationsDirectory,
719
+ });
720
+ expect(docs).toBe("base-graphql-docs\n\ndynamic-headers-docs");
721
+ } finally {
722
+ rmSync(integrationsDirectory, { recursive: true, force: true });
723
+ }
724
+ });
725
+
726
+ it("skips overlay when sdkVersion does not match sdkVersionRange", async () => {
727
+ const integrationsDirectory = createPluginDocsFixture("graphql", {
728
+ "README.md": "base-graphql-docs",
729
+ "docs.manifest.json": JSON.stringify({
730
+ overlays: [
731
+ { file: "overlays/headers.md", sdkVersionRange: ">=0.0.2" },
732
+ ],
733
+ }),
734
+ "overlays/headers.md": "dynamic-headers-docs",
735
+ });
736
+
737
+ try {
738
+ const docs = await resolveIntegrationDocumentation("graphql", {
739
+ sdkVersion: "0.0.1",
740
+ integrationsDirectory,
741
+ });
742
+ expect(docs).toBe("base-graphql-docs");
743
+ } finally {
744
+ rmSync(integrationsDirectory, { recursive: true, force: true });
745
+ }
746
+ });
747
+
748
+ it("skips overlay when sdkVersion is not provided", async () => {
749
+ const integrationsDirectory = createPluginDocsFixture("graphql", {
750
+ "README.md": "base-graphql-docs",
751
+ "docs.manifest.json": JSON.stringify({
752
+ overlays: [
753
+ { file: "overlays/headers.md", sdkVersionRange: ">=0.0.2" },
754
+ ],
755
+ }),
756
+ "overlays/headers.md": "dynamic-headers-docs",
757
+ });
758
+
759
+ try {
760
+ const docs = await resolveIntegrationDocumentation("graphql", {
761
+ pluginVersion: "0.0.10",
762
+ integrationsDirectory,
763
+ });
764
+ expect(docs).toBe("base-graphql-docs");
765
+ } finally {
766
+ rmSync(integrationsDirectory, { recursive: true, force: true });
767
+ }
768
+ });
769
+
770
+ it("requires both versionRange and sdkVersionRange to match when both are specified", async () => {
771
+ const integrationsDirectory = createPluginDocsFixture("graphql", {
772
+ "README.md": "base",
773
+ "docs.manifest.json": JSON.stringify({
774
+ overlays: [
775
+ {
776
+ file: "overlays/both.md",
777
+ versionRange: ">=0.0.10",
778
+ sdkVersionRange: ">=0.0.2",
779
+ },
780
+ ],
781
+ }),
782
+ "overlays/both.md": "both-match-overlay",
783
+ });
784
+
785
+ try {
786
+ // Both match
787
+ expect(
788
+ await resolveIntegrationDocumentation("graphql", {
789
+ pluginVersion: "0.0.10",
790
+ sdkVersion: "0.0.2",
791
+ integrationsDirectory,
792
+ }),
793
+ ).toBe("base\n\nboth-match-overlay");
794
+
795
+ // Only plugin matches
796
+ expect(
797
+ await resolveIntegrationDocumentation("graphql", {
798
+ pluginVersion: "0.0.10",
799
+ sdkVersion: "0.0.1",
800
+ integrationsDirectory,
801
+ }),
802
+ ).toBe("base");
803
+
804
+ // Only sdk matches
805
+ expect(
806
+ await resolveIntegrationDocumentation("graphql", {
807
+ pluginVersion: "0.0.9",
808
+ sdkVersion: "0.0.2",
809
+ integrationsDirectory,
810
+ }),
811
+ ).toBe("base");
812
+ } finally {
813
+ rmSync(integrationsDirectory, { recursive: true, force: true });
814
+ }
815
+ });
816
+
817
+ it("allows overlay with only sdkVersionRange (no versionRange)", async () => {
818
+ const integrationsDirectory = createPluginDocsFixture("graphql", {
819
+ "README.md": "base",
820
+ "docs.manifest.json": JSON.stringify({
821
+ overlays: [
822
+ { file: "overlays/sdk-only.md", sdkVersionRange: ">=0.0.2" },
823
+ ],
824
+ }),
825
+ "overlays/sdk-only.md": "sdk-gated-overlay",
826
+ });
827
+
828
+ try {
829
+ // No pluginVersion needed — only sdkVersion matters
830
+ const docs = await resolveIntegrationDocumentation("graphql", {
831
+ sdkVersion: "0.0.3",
832
+ integrationsDirectory,
833
+ });
834
+ expect(docs).toBe("base\n\nsdk-gated-overlay");
835
+ } finally {
836
+ rmSync(integrationsDirectory, { recursive: true, force: true });
837
+ }
838
+ });
839
+ });
840
+
841
+ describe("restapiintegration responseType gating (real docs)", () => {
842
+ // These resolve the actual shipped docs, not a fixture: the overlay
843
+ // gating is the discoverability guard for the responseType feature
844
+ // (agents whose worker sdk-api predates it must never be told the
845
+ // option exists), so the real manifest content is the contract.
846
+
847
+ it("documents responseType for agents whose sdk-api supports it", async () => {
848
+ const docs = await resolveIntegrationDocumentation("restapiintegration", {
849
+ sdkVersion: "0.0.3",
850
+ });
851
+
852
+ expect(docs).toContain('responseType: "text"');
853
+ expect(docs).not.toContain("does not support the `responseType`");
854
+ });
855
+
856
+ it("notes non-support instead of documenting responseType on older agents", async () => {
857
+ const docs = await resolveIntegrationDocumentation("restapiintegration", {
858
+ sdkVersion: "0.0.2",
859
+ });
860
+
861
+ expect(docs).toContain("does not support the `responseType`");
862
+ expect(docs).not.toContain('responseType: "text"');
863
+ });
864
+
865
+ it("stays silent about responseType when the agent reports no sdk-api version", async () => {
866
+ const docs = await resolveIntegrationDocumentation("restapiintegration");
867
+
868
+ expect(docs).not.toContain("responseType");
869
+ });
870
+ });
871
+
683
872
  it("blocks overlay paths outside the plugin directory", async () => {
684
873
  const integrationsDirectory = createPluginDocsFixture("dropbox", {
685
874
  "README.md": "base",
@@ -11,7 +11,13 @@ const DOC_DIRECTORY_ALIASES = {
11
11
 
12
12
  interface DocumentationOverlay {
13
13
  file: string;
14
- versionRange: string;
14
+ versionRange?: string;
15
+ /** When set, checks against the sdk-api version ("javascriptsdkapi") running
16
+ * in the orchestrator instead of the plugin-specific version. Use this to
17
+ * gate overlays on sdk-api features that ship independently of plugin
18
+ * version bumps. Both versionRange and sdkVersionRange can be specified
19
+ * together — the overlay is applied only when all specified ranges match. */
20
+ sdkVersionRange?: string;
15
21
  }
16
22
 
17
23
  interface DocumentationManifest {
@@ -32,6 +38,7 @@ type OverlayOperation =
32
38
 
33
39
  export interface ResolveIntegrationDocumentationOptions {
34
40
  pluginVersion?: SemVer;
41
+ sdkVersion?: SemVer;
35
42
  integrationsDirectory?: string;
36
43
  }
37
44
 
@@ -82,16 +89,17 @@ async function parseDocumentationManifest(
82
89
  typeof overlay !== "object" ||
83
90
  overlay === null ||
84
91
  !("file" in overlay) ||
85
- !("versionRange" in overlay)
92
+ (!("versionRange" in overlay) && !("sdkVersionRange" in overlay))
86
93
  ) {
87
94
  throw new Error(
88
95
  `Invalid overlay entry at index ${index} in ${manifestPath}.`,
89
96
  );
90
97
  }
91
98
 
92
- const { file, versionRange } = overlay as {
99
+ const { file, versionRange, sdkVersionRange } = overlay as {
93
100
  file: unknown;
94
101
  versionRange: unknown;
102
+ sdkVersionRange: unknown;
95
103
  };
96
104
 
97
105
  if (typeof file !== "string" || file.trim().length === 0) {
@@ -99,18 +107,35 @@ async function parseDocumentationManifest(
99
107
  `Invalid "file" for overlay index ${index} in ${manifestPath}.`,
100
108
  );
101
109
  }
102
- if (
103
- typeof versionRange !== "string" ||
104
- versionRange.trim().length === 0
105
- ) {
110
+
111
+ const hasVersionRange =
112
+ typeof versionRange === "string" && versionRange.trim().length > 0;
113
+ const hasSdkVersionRange =
114
+ typeof sdkVersionRange === "string" &&
115
+ sdkVersionRange.trim().length > 0;
116
+
117
+ if ("versionRange" in overlay && !hasVersionRange) {
106
118
  throw new Error(
107
119
  `Invalid "versionRange" for overlay index ${index} in ${manifestPath}.`,
108
120
  );
109
121
  }
122
+ if ("sdkVersionRange" in overlay && !hasSdkVersionRange) {
123
+ throw new Error(
124
+ `Invalid "sdkVersionRange" for overlay index ${index} in ${manifestPath}.`,
125
+ );
126
+ }
127
+ if (!hasVersionRange && !hasSdkVersionRange) {
128
+ throw new Error(
129
+ `Overlay at index ${index} in ${manifestPath} must specify at least one of "versionRange" or "sdkVersionRange".`,
130
+ );
131
+ }
110
132
 
111
133
  return {
112
134
  file,
113
- versionRange,
135
+ ...(hasVersionRange ? { versionRange: versionRange as string } : {}),
136
+ ...(hasSdkVersionRange
137
+ ? { sdkVersionRange: sdkVersionRange as string }
138
+ : {}),
114
139
  };
115
140
  },
116
141
  );
@@ -380,11 +405,38 @@ function ensureOverlayPathIsWithinPluginDir(
380
405
  }
381
406
  }
382
407
 
408
+ function overlayMatchesVersions(
409
+ overlay: DocumentationOverlay,
410
+ pluginVersion: SemVer | undefined,
411
+ sdkVersion: SemVer | undefined,
412
+ ): boolean {
413
+ if (
414
+ overlay.versionRange &&
415
+ (!pluginVersion ||
416
+ !versionMatchesRange(pluginVersion, overlay.versionRange))
417
+ ) {
418
+ return false;
419
+ }
420
+
421
+ if (
422
+ overlay.sdkVersionRange &&
423
+ (!sdkVersion || !versionMatchesRange(sdkVersion, overlay.sdkVersionRange))
424
+ ) {
425
+ return false;
426
+ }
427
+
428
+ return true;
429
+ }
430
+
383
431
  export async function resolveIntegrationDocumentation(
384
432
  pluginId: string,
385
433
  options: ResolveIntegrationDocumentationOptions = {},
386
434
  ): Promise<string> {
387
- const { pluginVersion, integrationsDirectory = INTEGRATIONS_DIR } = options;
435
+ const {
436
+ pluginVersion,
437
+ sdkVersion,
438
+ integrationsDirectory = INTEGRATIONS_DIR,
439
+ } = options;
388
440
 
389
441
  const pluginDirectory = resolve(
390
442
  integrationsDirectory,
@@ -397,13 +449,13 @@ export async function resolveIntegrationDocumentation(
397
449
  const baseDocumentationPath = resolve(pluginDirectory, baseFileName);
398
450
  const baseDocumentation = await readFile(baseDocumentationPath, "utf8");
399
451
 
400
- if (!manifest || !pluginVersion) {
452
+ if (!manifest) {
401
453
  return baseDocumentation;
402
454
  }
403
455
 
404
456
  let resolvedDocumentation = baseDocumentation;
405
457
  for (const overlay of manifest.overlays) {
406
- if (!versionMatchesRange(pluginVersion, overlay.versionRange)) {
458
+ if (!overlayMatchesVersions(overlay, pluginVersion, sdkVersion)) {
407
459
  continue;
408
460
  }
409
461
 
@@ -1,5 +1,10 @@
1
1
  {
2
2
  "pluginId": "graphql",
3
3
  "base": "README.md",
4
- "overlays": []
4
+ "overlays": [
5
+ {
6
+ "file": "overlays/dynamic-headers.md",
7
+ "sdkVersionRange": ">=0.0.2"
8
+ }
9
+ ]
5
10
  }
@@ -0,0 +1,34 @@
1
+ ## @replace: Methods
2
+
3
+ | Method | Description |
4
+ | ---------------------------------------------------------------- | ---------------------------------------------------------- |
5
+ | `query<T>(query, schema, variables?, metadata?, headers?)` | Execute a GraphQL query with required schema validation |
6
+ | `mutation<T>(mutation, schema, variables?, metadata?, headers?)` | Execute a GraphQL mutation with required schema validation |
7
+
8
+ ## @replace: Trace Metadata
9
+
10
+ All methods accept an optional `metadata` parameter for diagnostics labeling. See the [root SDK README](../../../README.md#trace-metadata) for details.
11
+
12
+ ## Dynamic Headers
13
+
14
+ Static headers (e.g. a fixed `X-API-Version`) and auth headers (e.g. Bearer tokens, API keys) should be configured on the GraphQL integration in the Superblocks UI so they apply to every call automatically.
15
+
16
+ For values that change per request — for example a bearer token derived from the API's input or from `ctx.env` — pass an optional `headers` map as the final argument to `query()` or `mutation()`:
17
+
18
+ ```typescript
19
+ const MeResponseSchema = z.object({
20
+ data: z.object({
21
+ me: z.object({ id: z.string(), email: z.string() }),
22
+ }),
23
+ });
24
+
25
+ const result = await ctx.integrations.graphql.query(
26
+ `query { me { id email } }`,
27
+ { response: MeResponseSchema },
28
+ undefined, // no variables
29
+ undefined, // no trace metadata
30
+ { Authorization: `Bearer ${ctx.env.UPSTREAM_TOKEN}` },
31
+ );
32
+ ```
33
+
34
+ Since both `variables` and `metadata` accept plain objects, pass `undefined` for any of them that you do not need.
@@ -20,7 +20,7 @@ import type { PostgresClient } from "./types.js";
20
20
  * PostgreSQL request type derived from proto definition.
21
21
  * Using PartialMessage allows optional fields.
22
22
  */
23
- export type PostgresRequest = PartialMessage<PostgresPlugin>;
23
+ type PostgresRequest = PartialMessage<PostgresPlugin>;
24
24
 
25
25
  /**
26
26
  * Internal implementation of PostgresClient.