@superblocksteam/sdk-api 2.0.105 → 2.0.106-next.1

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 (182) hide show
  1. package/README.md +439 -89
  2. package/dist/api/definition.d.ts +11 -6
  3. package/dist/api/definition.d.ts.map +1 -1
  4. package/dist/api/definition.js +19 -12
  5. package/dist/api/definition.js.map +1 -1
  6. package/dist/api/definition.test.js +39 -15
  7. package/dist/api/definition.test.js.map +1 -1
  8. package/dist/errors.d.ts +1 -1
  9. package/dist/errors.js +1 -1
  10. package/dist/index.d.ts +10 -11
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +9 -5
  13. package/dist/index.js.map +1 -1
  14. package/dist/integrations/base/index.d.ts +2 -1
  15. package/dist/integrations/base/index.d.ts.map +1 -1
  16. package/dist/integrations/base/index.js +1 -0
  17. package/dist/integrations/base/index.js.map +1 -1
  18. package/dist/integrations/base/rest-api-client-base.d.ts +48 -0
  19. package/dist/integrations/base/rest-api-client-base.d.ts.map +1 -0
  20. package/dist/integrations/base/rest-api-client-base.js +98 -0
  21. package/dist/integrations/base/rest-api-client-base.js.map +1 -0
  22. package/dist/integrations/base/rest-api-integration-client.d.ts +10 -20
  23. package/dist/integrations/base/rest-api-integration-client.d.ts.map +1 -1
  24. package/dist/integrations/base/rest-api-integration-client.js +10 -65
  25. package/dist/integrations/base/rest-api-integration-client.js.map +1 -1
  26. package/dist/integrations/box/types.d.ts +1 -1
  27. package/dist/integrations/declarations.d.ts +5 -73
  28. package/dist/integrations/declarations.d.ts.map +1 -1
  29. package/dist/integrations/declarations.js +5 -68
  30. package/dist/integrations/declarations.js.map +1 -1
  31. package/dist/integrations/documentation.test.js +0 -2
  32. package/dist/integrations/documentation.test.js.map +1 -1
  33. package/dist/integrations/googledrive/types.d.ts +1 -1
  34. package/dist/integrations/index.d.ts +1 -11
  35. package/dist/integrations/index.d.ts.map +1 -1
  36. package/dist/integrations/index.js +1 -7
  37. package/dist/integrations/index.js.map +1 -1
  38. package/dist/integrations/registry.d.ts +1 -11
  39. package/dist/integrations/registry.d.ts.map +1 -1
  40. package/dist/integrations/registry.js +0 -29
  41. package/dist/integrations/registry.js.map +1 -1
  42. package/dist/integrations/slack/client.d.ts +13 -9
  43. package/dist/integrations/slack/client.d.ts.map +1 -1
  44. package/dist/integrations/slack/client.js +60 -8
  45. package/dist/integrations/slack/client.js.map +1 -1
  46. package/dist/integrations/slack/client.test.d.ts +11 -0
  47. package/dist/integrations/slack/client.test.d.ts.map +1 -0
  48. package/dist/integrations/slack/client.test.js +368 -0
  49. package/dist/integrations/slack/client.test.js.map +1 -0
  50. package/dist/integrations/slack/index.d.ts +2 -1
  51. package/dist/integrations/slack/index.d.ts.map +1 -1
  52. package/dist/integrations/slack/index.js +1 -0
  53. package/dist/integrations/slack/index.js.map +1 -1
  54. package/dist/integrations/slack/types.d.ts +127 -28
  55. package/dist/integrations/slack/types.d.ts.map +1 -1
  56. package/dist/integrations/slack/types.js +27 -1
  57. package/dist/integrations/slack/types.js.map +1 -1
  58. package/dist/integrations/snowflake/client.d.ts +2 -2
  59. package/dist/integrations/snowflake/client.js +2 -2
  60. package/dist/runtime/context.d.ts +1 -1
  61. package/dist/runtime/executor.d.ts +2 -2
  62. package/dist/types.d.ts +15 -6
  63. package/dist/types.d.ts.map +1 -1
  64. package/package.json +2 -2
  65. package/src/api/definition.test.ts +40 -15
  66. package/src/api/definition.ts +19 -12
  67. package/src/errors.ts +1 -1
  68. package/src/index.ts +13 -33
  69. package/src/integrations/asana/README.md +12 -12
  70. package/src/integrations/base/index.ts +2 -1
  71. package/src/integrations/base/rest-api-client-base.ts +134 -0
  72. package/src/integrations/base/rest-api-integration-client.ts +12 -89
  73. package/src/integrations/bitbucket/README.md +19 -19
  74. package/src/integrations/box/README.md +24 -24
  75. package/src/integrations/box/types.ts +1 -1
  76. package/src/integrations/circleci/README.md +18 -18
  77. package/src/integrations/declarations.ts +5 -105
  78. package/src/integrations/documentation.test.ts +0 -2
  79. package/src/integrations/googledrive/README.md +25 -22
  80. package/src/integrations/googledrive/types.ts +1 -1
  81. package/src/integrations/graphql/README.md +2 -2
  82. package/src/integrations/groq/README.md +8 -8
  83. package/src/integrations/index.ts +0 -51
  84. package/src/integrations/mongodb/README.md +65 -12
  85. package/src/integrations/perplexity/README.md +39 -48
  86. package/src/integrations/registry.ts +1 -39
  87. package/src/integrations/salesforce/README.md +11 -9
  88. package/src/integrations/slack/README.md +62 -19
  89. package/src/integrations/slack/client.test.ts +553 -0
  90. package/src/integrations/slack/client.ts +92 -12
  91. package/src/integrations/slack/index.ts +6 -1
  92. package/src/integrations/slack/types.ts +142 -29
  93. package/src/integrations/snowflake/client.ts +2 -2
  94. package/src/integrations/zoom/README.md +15 -15
  95. package/src/runtime/context.ts +1 -1
  96. package/src/runtime/executor.ts +2 -2
  97. package/src/types.ts +15 -6
  98. package/dist/integrations/couchbase/client.d.ts +0 -36
  99. package/dist/integrations/couchbase/client.d.ts.map +0 -1
  100. package/dist/integrations/couchbase/client.js +0 -148
  101. package/dist/integrations/couchbase/client.js.map +0 -1
  102. package/dist/integrations/couchbase/index.d.ts +0 -8
  103. package/dist/integrations/couchbase/index.d.ts.map +0 -1
  104. package/dist/integrations/couchbase/index.js +0 -7
  105. package/dist/integrations/couchbase/index.js.map +0 -1
  106. package/dist/integrations/couchbase/types.d.ts +0 -100
  107. package/dist/integrations/couchbase/types.d.ts.map +0 -1
  108. package/dist/integrations/couchbase/types.js +0 -5
  109. package/dist/integrations/couchbase/types.js.map +0 -1
  110. package/dist/integrations/kafka/client.d.ts +0 -25
  111. package/dist/integrations/kafka/client.d.ts.map +0 -1
  112. package/dist/integrations/kafka/client.js +0 -124
  113. package/dist/integrations/kafka/client.js.map +0 -1
  114. package/dist/integrations/kafka/index.d.ts +0 -8
  115. package/dist/integrations/kafka/index.d.ts.map +0 -1
  116. package/dist/integrations/kafka/index.js +0 -7
  117. package/dist/integrations/kafka/index.js.map +0 -1
  118. package/dist/integrations/kafka/types.d.ts +0 -113
  119. package/dist/integrations/kafka/types.d.ts.map +0 -1
  120. package/dist/integrations/kafka/types.js +0 -5
  121. package/dist/integrations/kafka/types.js.map +0 -1
  122. package/dist/integrations/kinesis/client.d.ts +0 -31
  123. package/dist/integrations/kinesis/client.d.ts.map +0 -1
  124. package/dist/integrations/kinesis/client.js +0 -101
  125. package/dist/integrations/kinesis/client.js.map +0 -1
  126. package/dist/integrations/kinesis/index.d.ts +0 -8
  127. package/dist/integrations/kinesis/index.d.ts.map +0 -1
  128. package/dist/integrations/kinesis/index.js +0 -7
  129. package/dist/integrations/kinesis/index.js.map +0 -1
  130. package/dist/integrations/kinesis/types.d.ts +0 -97
  131. package/dist/integrations/kinesis/types.d.ts.map +0 -1
  132. package/dist/integrations/kinesis/types.js +0 -7
  133. package/dist/integrations/kinesis/types.js.map +0 -1
  134. package/dist/integrations/python/client.d.ts +0 -42
  135. package/dist/integrations/python/client.d.ts.map +0 -1
  136. package/dist/integrations/python/client.js +0 -89
  137. package/dist/integrations/python/client.js.map +0 -1
  138. package/dist/integrations/python/client.test.d.ts +0 -5
  139. package/dist/integrations/python/client.test.d.ts.map +0 -1
  140. package/dist/integrations/python/client.test.js +0 -214
  141. package/dist/integrations/python/client.test.js.map +0 -1
  142. package/dist/integrations/python/index.d.ts +0 -6
  143. package/dist/integrations/python/index.d.ts.map +0 -1
  144. package/dist/integrations/python/index.js +0 -5
  145. package/dist/integrations/python/index.js.map +0 -1
  146. package/dist/integrations/python/types.d.ts +0 -85
  147. package/dist/integrations/python/types.d.ts.map +0 -1
  148. package/dist/integrations/python/types.js +0 -5
  149. package/dist/integrations/python/types.js.map +0 -1
  150. package/dist/integrations/redis/client.d.ts +0 -43
  151. package/dist/integrations/redis/client.d.ts.map +0 -1
  152. package/dist/integrations/redis/client.js +0 -142
  153. package/dist/integrations/redis/client.js.map +0 -1
  154. package/dist/integrations/redis/index.d.ts +0 -8
  155. package/dist/integrations/redis/index.d.ts.map +0 -1
  156. package/dist/integrations/redis/index.js +0 -7
  157. package/dist/integrations/redis/index.js.map +0 -1
  158. package/dist/integrations/redis/types.d.ts +0 -137
  159. package/dist/integrations/redis/types.d.ts.map +0 -1
  160. package/dist/integrations/redis/types.js +0 -5
  161. package/dist/integrations/redis/types.js.map +0 -1
  162. package/src/integrations/couchbase/README.md +0 -138
  163. package/src/integrations/couchbase/client.ts +0 -225
  164. package/src/integrations/couchbase/index.ts +0 -8
  165. package/src/integrations/couchbase/types.ts +0 -126
  166. package/src/integrations/kafka/README.md +0 -144
  167. package/src/integrations/kafka/client.ts +0 -216
  168. package/src/integrations/kafka/index.ts +0 -14
  169. package/src/integrations/kafka/types.ts +0 -128
  170. package/src/integrations/kinesis/README.md +0 -153
  171. package/src/integrations/kinesis/client.ts +0 -146
  172. package/src/integrations/kinesis/index.ts +0 -14
  173. package/src/integrations/kinesis/types.ts +0 -114
  174. package/src/integrations/python/README.md +0 -566
  175. package/src/integrations/python/client.test.ts +0 -341
  176. package/src/integrations/python/client.ts +0 -136
  177. package/src/integrations/python/index.ts +0 -6
  178. package/src/integrations/python/types.ts +0 -92
  179. package/src/integrations/redis/README.md +0 -200
  180. package/src/integrations/redis/client.ts +0 -208
  181. package/src/integrations/redis/index.ts +0 -8
  182. package/src/integrations/redis/types.ts +0 -167
@@ -0,0 +1,553 @@
1
+ /**
2
+ * Tests for SlackClientImpl.
3
+ *
4
+ * Validates the SlackResponse<T> discriminated union contract:
5
+ * - ok:false from Slack → SlackErrorResponse (no throw)
6
+ * - ok:true from Slack → Zod-validated success data
7
+ * - ok:true but schema mismatch → RestApiValidationError (thrown)
8
+ * - Body validation failures still throw RestApiValidationError
9
+ */
10
+
11
+ import { describe, it, expect, vi } from "vitest";
12
+ import { z } from "zod";
13
+
14
+ import { RestApiValidationError } from "../../errors.js";
15
+ import type { IntegrationConfig } from "../types.js";
16
+ import { SlackClientImpl } from "./client.js";
17
+ import type { SlackErrorResponse } from "./types.js";
18
+
19
+ const TEST_CONFIG: IntegrationConfig = {
20
+ id: "slack-test-id",
21
+ name: "Test Slack",
22
+ pluginId: "slack",
23
+ configuration: {},
24
+ };
25
+
26
+ function createClient(mockResult: unknown) {
27
+ const executeQuery = vi.fn().mockResolvedValue(mockResult);
28
+ const client = new SlackClientImpl(TEST_CONFIG, executeQuery);
29
+ return { client, executeQuery };
30
+ }
31
+
32
+ const ChannelsSchema = z.object({
33
+ channels: z.array(z.object({ id: z.string(), name: z.string() })),
34
+ });
35
+
36
+ const PostMessageSchema = z.object({
37
+ channel: z.string(),
38
+ ts: z.string(),
39
+ });
40
+
41
+ describe("SlackClientImpl", () => {
42
+ // ── Success responses ──────────────────────────────────────────
43
+
44
+ describe("success responses (ok: true)", () => {
45
+ it("returns validated data with ok: true", async () => {
46
+ const { client } = createClient({
47
+ ok: true,
48
+ channels: [{ id: "C123", name: "general" }],
49
+ });
50
+
51
+ const result = await client.apiRequest(
52
+ { method: "GET", path: "/conversations.list" },
53
+ { response: ChannelsSchema },
54
+ );
55
+
56
+ expect(result.ok).toBe(true);
57
+ if (result.ok) {
58
+ expect(result.channels).toHaveLength(1);
59
+ expect(result.channels[0]).toEqual({ id: "C123", name: "general" });
60
+ }
61
+ });
62
+
63
+ it("strips unknown keys via Zod strip mode", async () => {
64
+ const { client } = createClient({
65
+ ok: true,
66
+ channels: [{ id: "C1", name: "test", extra: true }],
67
+ response_metadata: { next_cursor: "" },
68
+ });
69
+
70
+ const result = await client.apiRequest(
71
+ { method: "GET", path: "/conversations.list" },
72
+ { response: ChannelsSchema },
73
+ );
74
+
75
+ expect(result.ok).toBe(true);
76
+ if (result.ok) {
77
+ expect(result).not.toHaveProperty("response_metadata");
78
+ expect(result.channels[0]).not.toHaveProperty("extra");
79
+ }
80
+ });
81
+
82
+ it("validates a post message success response", async () => {
83
+ const { client } = createClient({
84
+ ok: true,
85
+ channel: "C123",
86
+ ts: "1234567890.123456",
87
+ });
88
+
89
+ const result = await client.apiRequest(
90
+ {
91
+ method: "POST",
92
+ path: "/chat.postMessage",
93
+ body: { channel: "C123", text: "hello" },
94
+ },
95
+ { response: PostMessageSchema },
96
+ );
97
+
98
+ expect(result.ok).toBe(true);
99
+ if (result.ok) {
100
+ expect(result.ts).toBe("1234567890.123456");
101
+ expect(result.channel).toBe("C123");
102
+ }
103
+ });
104
+ });
105
+
106
+ // ── Error responses (ok: false) ────────────────────────────────
107
+
108
+ describe("error responses (ok: false)", () => {
109
+ it("returns SlackErrorResponse instead of throwing", async () => {
110
+ const { client } = createClient({
111
+ ok: false,
112
+ error: "channel_not_found",
113
+ });
114
+
115
+ const result = await client.apiRequest(
116
+ {
117
+ method: "POST",
118
+ path: "/chat.postMessage",
119
+ body: { channel: "C000", text: "hi" },
120
+ },
121
+ { response: PostMessageSchema },
122
+ );
123
+
124
+ expect(result.ok).toBe(false);
125
+ if (!result.ok) {
126
+ expect(result.error).toBe("channel_not_found");
127
+ }
128
+ });
129
+
130
+ it("includes needed scopes on missing_scope errors", async () => {
131
+ const { client } = createClient({
132
+ ok: false,
133
+ error: "missing_scope",
134
+ needed: "channels:read",
135
+ provided: "identify,bot",
136
+ });
137
+
138
+ const result = await client.apiRequest(
139
+ { method: "GET", path: "/conversations.list" },
140
+ { response: ChannelsSchema },
141
+ );
142
+
143
+ expect(result.ok).toBe(false);
144
+ if (!result.ok) {
145
+ expect(result.error).toBe("missing_scope");
146
+ expect(result.needed).toBe("channels:read");
147
+ expect(result.provided).toBe("identify,bot");
148
+ }
149
+ });
150
+
151
+ it("throws when ok: false but no error string (invalid Slack error)", async () => {
152
+ const { client } = createClient({ ok: false });
153
+
154
+ // { ok: false } without an error string doesn't match SlackErrorSchema,
155
+ // and also doesn't match the success schema → RestApiValidationError.
156
+ await expect(
157
+ client.apiRequest(
158
+ { method: "GET", path: "/conversations.list" },
159
+ { response: ChannelsSchema },
160
+ ),
161
+ ).rejects.toThrow(RestApiValidationError);
162
+ });
163
+
164
+ it.each(["not_authed", "invalid_auth", "ratelimited"])(
165
+ "handles %s error",
166
+ async (errorCode) => {
167
+ const { client } = createClient({ ok: false, error: errorCode });
168
+
169
+ const result = await client.apiRequest(
170
+ { method: "GET", path: "/auth.test" },
171
+ { response: z.object({}) },
172
+ );
173
+
174
+ expect(result.ok).toBe(false);
175
+ if (!result.ok) {
176
+ expect(result.error).toBe(errorCode);
177
+ }
178
+ },
179
+ );
180
+
181
+ it("omits needed/provided when not present in Slack response", async () => {
182
+ const { client } = createClient({
183
+ ok: false,
184
+ error: "channel_not_found",
185
+ });
186
+
187
+ const result = await client.apiRequest(
188
+ { method: "POST", path: "/chat.postMessage" },
189
+ { response: PostMessageSchema },
190
+ );
191
+
192
+ expect(result.ok).toBe(false);
193
+ if (!result.ok) {
194
+ expect(result).not.toHaveProperty("needed");
195
+ expect(result).not.toHaveProperty("provided");
196
+ }
197
+ });
198
+
199
+ it("throws when needed/provided are non-string (invalid Slack error)", async () => {
200
+ const { client } = createClient({
201
+ ok: false,
202
+ error: "missing_scope",
203
+ needed: 123,
204
+ provided: true,
205
+ });
206
+
207
+ // Zod rejects non-string needed/provided. Since raw response has ok:false,
208
+ // SlackClient rejects it before success payload validation.
209
+ await expect(
210
+ client.apiRequest(
211
+ { method: "GET", path: "/conversations.list" },
212
+ { response: ChannelsSchema },
213
+ ),
214
+ ).rejects.toThrow(RestApiValidationError);
215
+ });
216
+ });
217
+
218
+ // ── Validation errors (still thrown) ───────────────────────────
219
+
220
+ describe("validation errors", () => {
221
+ it("throws RestApiValidationError when ok:true but schema does not match", async () => {
222
+ // Slack says ok:true but response is missing required fields
223
+ const { client } = createClient({
224
+ ok: true,
225
+ // Missing 'channels' field
226
+ });
227
+
228
+ await expect(
229
+ client.apiRequest(
230
+ { method: "GET", path: "/conversations.list" },
231
+ { response: ChannelsSchema },
232
+ ),
233
+ ).rejects.toThrow(RestApiValidationError);
234
+ });
235
+
236
+ it("includes raw data and Zod error in validation error details", async () => {
237
+ const badResponse = { ok: true };
238
+ const { client } = createClient(badResponse);
239
+
240
+ try {
241
+ await client.apiRequest(
242
+ { method: "GET", path: "/conversations.list" },
243
+ { response: ChannelsSchema },
244
+ );
245
+ expect.fail("Expected RestApiValidationError");
246
+ } catch (e) {
247
+ expect(e).toBeInstanceOf(RestApiValidationError);
248
+ const err = e as RestApiValidationError;
249
+ expect(err.details.data).toEqual(badResponse);
250
+ expect(err.details.zodError).toBeDefined();
251
+ }
252
+ });
253
+
254
+ it("throws RestApiValidationError when request body fails validation", async () => {
255
+ const { client } = createClient({ ok: true });
256
+ const BodySchema = z.object({
257
+ channel: z.string(),
258
+ text: z.string(),
259
+ });
260
+
261
+ await expect(
262
+ client.apiRequest(
263
+ {
264
+ method: "POST",
265
+ path: "/chat.postMessage",
266
+ body: { channel: 123 as unknown as string, text: "hello" },
267
+ },
268
+ { body: BodySchema, response: PostMessageSchema },
269
+ ),
270
+ ).rejects.toThrow(RestApiValidationError);
271
+ });
272
+ });
273
+
274
+ // ── Request building ───────────────────────────────────────────
275
+
276
+ describe("request building", () => {
277
+ it("sends correct proto structure to executeQuery", async () => {
278
+ const { client, executeQuery } = createClient({
279
+ ok: true,
280
+ channel: "C123",
281
+ ts: "1234567890.123456",
282
+ });
283
+
284
+ await client.apiRequest(
285
+ {
286
+ method: "POST",
287
+ path: "/chat.postMessage",
288
+ body: { channel: "#general", text: "hello" },
289
+ headers: { "X-Custom": "value" },
290
+ params: { unfurl_links: false },
291
+ },
292
+ { response: PostMessageSchema },
293
+ );
294
+
295
+ expect(executeQuery).toHaveBeenCalledOnce();
296
+ const request = executeQuery.mock.calls[0][0];
297
+ expect(request.openApiAction).toBe("genericHttpRequest");
298
+ expect(request.httpMethod).toBe("POST");
299
+ expect(request.urlPath).toBe("/chat.postMessage");
300
+ expect(request.responseType).toBe("json");
301
+ expect(request.body).toBe(
302
+ JSON.stringify({ channel: "#general", text: "hello" }),
303
+ );
304
+ expect(request.bodyType).toBe("jsonBody");
305
+ expect(request.headers).toEqual([{ key: "X-Custom", value: "value" }]);
306
+ expect(request.params).toEqual([{ key: "unfurl_links", value: "false" }]);
307
+ });
308
+
309
+ it("passes trace metadata to executeQuery", async () => {
310
+ const { client, executeQuery } = createClient({
311
+ ok: true,
312
+ channel: "C1",
313
+ ts: "1.1",
314
+ });
315
+
316
+ await client.apiRequest(
317
+ { method: "POST", path: "/chat.postMessage" },
318
+ { response: PostMessageSchema },
319
+ { label: "slack.postMessage", description: "Post a message" },
320
+ );
321
+
322
+ expect(executeQuery).toHaveBeenCalledWith(expect.any(Object), undefined, {
323
+ label: "slack.postMessage",
324
+ description: "Post a message",
325
+ });
326
+ });
327
+
328
+ it("propagates executeQuery rejections as transport errors", async () => {
329
+ const executeQuery = vi
330
+ .fn()
331
+ .mockRejectedValue(new Error("network timeout"));
332
+ const client = new SlackClientImpl(TEST_CONFIG, executeQuery);
333
+
334
+ await expect(
335
+ client.apiRequest(
336
+ { method: "GET", path: "/auth.test" },
337
+ { response: z.object({}) },
338
+ ),
339
+ ).rejects.toThrow("network timeout");
340
+ });
341
+ });
342
+
343
+ // ── Edge cases ─────────────────────────────────────────────────
344
+
345
+ describe("edge cases", () => {
346
+ it("falls through to schema validation when response has no ok field", async () => {
347
+ const { client } = createClient({ data: [1, 2, 3] });
348
+
349
+ // No ok field → not Slack error, and missing success envelope.
350
+ await expect(
351
+ client.apiRequest(
352
+ { method: "GET", path: "/some.endpoint" },
353
+ { response: ChannelsSchema },
354
+ ),
355
+ ).rejects.toThrow(RestApiValidationError);
356
+ });
357
+
358
+ it("falls through to schema validation when ok is not a boolean", async () => {
359
+ const { client } = createClient({ ok: "false", error: "nope" });
360
+
361
+ // ok is a string, so it is neither a valid Slack error envelope
362
+ // nor a valid success envelope.
363
+ await expect(
364
+ client.apiRequest(
365
+ { method: "GET", path: "/some.endpoint" },
366
+ { response: ChannelsSchema },
367
+ ),
368
+ ).rejects.toThrow(RestApiValidationError);
369
+ });
370
+
371
+ it("handles response surviving JSON round-trip (protobuf simulation)", async () => {
372
+ const original = {
373
+ ok: true,
374
+ channels: [{ id: "C123", name: "general" }],
375
+ nested: { count: 42 },
376
+ };
377
+ const roundTripped = JSON.parse(JSON.stringify(original));
378
+ const { client } = createClient(roundTripped);
379
+
380
+ const result = await client.apiRequest(
381
+ { method: "GET", path: "/conversations.list" },
382
+ { response: ChannelsSchema },
383
+ );
384
+
385
+ expect(result.ok).toBe(true);
386
+ if (result.ok) {
387
+ expect(result.channels[0].id).toBe("C123");
388
+ }
389
+ });
390
+
391
+ it("injects ok: true when schema omits the ok field", async () => {
392
+ const NoOkSchema = z.object({
393
+ channels: z.array(z.object({ id: z.string(), name: z.string() })),
394
+ });
395
+
396
+ const { client } = createClient({
397
+ ok: true,
398
+ channels: [{ id: "C1", name: "test" }],
399
+ });
400
+
401
+ const result = await client.apiRequest(
402
+ { method: "GET", path: "/conversations.list" },
403
+ { response: NoOkSchema },
404
+ );
405
+
406
+ expect(result.ok).toBe(true);
407
+ if (result.ok) {
408
+ expect(result.channels).toHaveLength(1);
409
+ }
410
+ });
411
+
412
+ it("validates payload schema after removing envelope field for strict schemas", async () => {
413
+ const StrictSchema = z
414
+ .object({
415
+ channels: z.array(z.object({ id: z.string(), name: z.string() })),
416
+ })
417
+ .strict();
418
+
419
+ const { client } = createClient({
420
+ ok: true,
421
+ channels: [{ id: "C1", name: "test" }],
422
+ });
423
+
424
+ const result = await client.apiRequest(
425
+ { method: "GET", path: "/conversations.list" },
426
+ { response: StrictSchema },
427
+ );
428
+
429
+ expect(result.ok).toBe(true);
430
+ if (result.ok) {
431
+ expect(result.channels).toHaveLength(1);
432
+ }
433
+ });
434
+
435
+ it("supports passthrough schemas and still enforces literal ok discriminant", async () => {
436
+ const PassthroughSchema = z
437
+ .object({
438
+ channels: z.array(z.object({ id: z.string(), name: z.string() })),
439
+ })
440
+ .passthrough();
441
+
442
+ const { client } = createClient({
443
+ ok: true,
444
+ channels: [{ id: "C1", name: "test" }],
445
+ response_metadata: { next_cursor: "abc" },
446
+ });
447
+
448
+ const result = await client.apiRequest(
449
+ { method: "GET", path: "/conversations.list" },
450
+ { response: PassthroughSchema },
451
+ );
452
+
453
+ expect(result.ok).toBe(true);
454
+ if (result.ok) {
455
+ expect(result.channels).toHaveLength(1);
456
+ expect(result).toHaveProperty("response_metadata");
457
+ }
458
+ });
459
+
460
+ it("throws when ok:false and loose payload schema would otherwise accept", async () => {
461
+ const LooseSchema = z.object({});
462
+ const { client } = createClient({ ok: false });
463
+
464
+ await expect(
465
+ client.apiRequest(
466
+ { method: "GET", path: "/auth.test" },
467
+ { response: LooseSchema },
468
+ ),
469
+ ).rejects.toThrow(RestApiValidationError);
470
+ });
471
+
472
+ it("routes to error branch when response matches both schemas", async () => {
473
+ // Response matches SlackErrorSchema AND a loose success schema.
474
+ // Error branch must win because it's checked first.
475
+ const FlexibleSchema = z.object({
476
+ error: z.string().optional(),
477
+ });
478
+ const { client } = createClient({
479
+ ok: false,
480
+ error: "channel_not_found",
481
+ });
482
+
483
+ const result = await client.apiRequest(
484
+ { method: "POST", path: "/chat.postMessage" },
485
+ { response: FlexibleSchema },
486
+ );
487
+
488
+ expect(result.ok).toBe(false);
489
+ if (!result.ok) {
490
+ expect(result.error).toBe("channel_not_found");
491
+ }
492
+ });
493
+
494
+ it("does not treat ok: 0 as a Slack error (only strict false)", async () => {
495
+ // ok: 0 is not === false, so it is NOT detected as a Slack error.
496
+ // It fails success envelope validation because ok must be literal true.
497
+ const { client } = createClient({
498
+ ok: 0,
499
+ channels: [{ id: "C1", name: "test" }],
500
+ });
501
+
502
+ await expect(
503
+ client.apiRequest(
504
+ { method: "GET", path: "/conversations.list" },
505
+ { response: ChannelsSchema },
506
+ ),
507
+ ).rejects.toThrow(RestApiValidationError);
508
+ });
509
+ });
510
+
511
+ // ── Type narrowing (compile-time verification) ─────────────────
512
+
513
+ describe("type narrowing", () => {
514
+ it("narrows to error response fields when ok is false", async () => {
515
+ const { client } = createClient({
516
+ ok: false,
517
+ error: "missing_scope",
518
+ needed: "channels:read",
519
+ });
520
+
521
+ const result = await client.apiRequest(
522
+ { method: "GET", path: "/conversations.list" },
523
+ { response: ChannelsSchema },
524
+ );
525
+
526
+ // This block exercises the SlackErrorResponse branch
527
+ if (!result.ok) {
528
+ const errorResult: SlackErrorResponse = result;
529
+ expect(errorResult.error).toBe("missing_scope");
530
+ expect(errorResult.needed).toBe("channels:read");
531
+ }
532
+ });
533
+
534
+ it("narrows to success response fields when ok is true", async () => {
535
+ const { client } = createClient({
536
+ ok: true,
537
+ channels: [{ id: "C1", name: "general" }],
538
+ });
539
+
540
+ const result = await client.apiRequest(
541
+ { method: "GET", path: "/conversations.list" },
542
+ { response: ChannelsSchema },
543
+ );
544
+
545
+ // This block exercises the T & { ok: true } branch
546
+ if (result.ok) {
547
+ // TypeScript narrows: result.channels is accessible
548
+ expect(result.channels).toHaveLength(1);
549
+ expect(result.channels[0].name).toBe("general");
550
+ }
551
+ });
552
+ });
553
+ });
@@ -1,23 +1,103 @@
1
1
  /**
2
2
  * Slack client implementation.
3
3
  *
4
- * Uses the proto-generated REST API Integration Plugin type from @superblocksteam/types.
5
- * Slack integrations use the REST API Integration plugin under the hood.
4
+ * Extends RestApiClientBase to inherit shared request infrastructure,
5
+ * then provides a Slack-specific apiRequest() that returns a
6
+ * SlackResponse<T> discriminated union instead of throwing on ok:false.
6
7
  */
7
8
 
8
- import { RestApiIntegrationClient } from "../base/index.js";
9
- import type { SlackClient } from "./types.js";
9
+ import type { z } from "zod";
10
+
11
+ import { RestApiValidationError } from "../../errors.js";
12
+ import { RestApiClientBase } from "../base/rest-api-client-base.js";
13
+ import type { ApiRequestOptions } from "../base/types.js";
14
+ import type { TraceMetadata } from "../registry.js";
15
+ import {
16
+ SlackErrorSchema,
17
+ SlackSuccessEnvelopeSchema,
18
+ type SlackApiRequestSchema,
19
+ type SlackClient,
20
+ type SlackResponse,
21
+ } from "./types.js";
10
22
 
11
23
  /**
12
24
  * Internal implementation of SlackClient.
13
25
  *
14
- * Extends RestApiIntegrationClient to inherit the generic apiRequest() method.
15
- * This implementation communicates with the orchestrator to execute
16
- * Slack API calls. At runtime, the orchestrator handles OAuth authentication
17
- * and API request execution.
26
+ * Extends RestApiClientBase for request building and execution.
27
+ * Uses {@link SlackErrorSchema} to parse Slack error responses via Zod,
28
+ * then falls back to the caller's schema for success responses.
18
29
  */
19
- export class SlackClientImpl
20
- extends RestApiIntegrationClient
21
- implements SlackClient {
22
- // Inherits: name, pluginId, config, executeQuery, apiRequest()
30
+ export class SlackClientImpl extends RestApiClientBase implements SlackClient {
31
+ async apiRequest<TBody, TResponseSchema extends z.AnyZodObject>(
32
+ options: ApiRequestOptions<TBody>,
33
+ schema: SlackApiRequestSchema<TBody, TResponseSchema>,
34
+ metadata?: TraceMetadata,
35
+ ): Promise<SlackResponse<z.output<TResponseSchema>>> {
36
+ const result = await this.executeApiRequest(options, schema.body, metadata);
37
+
38
+ // Slack returns errors as HTTP 200 with { ok: false, error: "..." }.
39
+ // Let Zod parse the error shape — if it matches, return the error branch.
40
+ const errorResult = SlackErrorSchema.safeParse(result);
41
+ if (errorResult.success) {
42
+ return errorResult.data;
43
+ }
44
+
45
+ // Guard: if the raw response has ok: false but didn't match SlackErrorSchema
46
+ // (e.g. missing error string), don't let it fall through to the success
47
+ // schema which might accept it and get stamped with ok: true.
48
+ if (
49
+ typeof result === "object" &&
50
+ result !== null &&
51
+ "ok" in result &&
52
+ (result as Record<string, unknown>).ok === false
53
+ ) {
54
+ throw new RestApiValidationError(
55
+ `Slack returned ok: false but the response did not match the expected error shape: ${errorResult.error.message}`,
56
+ {
57
+ zodError: errorResult.error,
58
+ data: result,
59
+ },
60
+ );
61
+ }
62
+
63
+ // Success responses must carry the Slack envelope discriminant.
64
+ const successEnvelopeResult = SlackSuccessEnvelopeSchema.safeParse(result);
65
+ if (!successEnvelopeResult.success) {
66
+ throw new RestApiValidationError(
67
+ `Slack success response is missing the expected ok: true envelope: ${successEnvelopeResult.error.message}`,
68
+ {
69
+ zodError: successEnvelopeResult.error,
70
+ data: result,
71
+ },
72
+ );
73
+ }
74
+
75
+ // Validate caller-provided payload shape (without the `ok` field).
76
+ // This keeps strict payload schemas compatible with Slack's envelope.
77
+ const payloadCandidate: Record<string, unknown> = {
78
+ ...(result as Record<string, unknown>),
79
+ };
80
+ delete payloadCandidate.ok;
81
+
82
+ const payloadResult = schema.response.safeParse(payloadCandidate);
83
+ if (!payloadResult.success) {
84
+ throw new RestApiValidationError(
85
+ `Response validation failed: ${payloadResult.error.message}`,
86
+ {
87
+ zodError: payloadResult.error,
88
+ data: result,
89
+ },
90
+ );
91
+ }
92
+
93
+ // Defense against `.passthrough()` schemas: strip any leaked `ok` from
94
+ // the parsed payload before re-injecting the literal discriminant.
95
+ const payloadWithoutOk: Record<string, unknown> = { ...payloadResult.data };
96
+ delete payloadWithoutOk.ok;
97
+
98
+ return {
99
+ ...payloadWithoutOk,
100
+ ok: true as const,
101
+ } as z.output<TResponseSchema> & { readonly ok: true };
102
+ }
23
103
  }
@@ -2,5 +2,10 @@
2
2
  * Slack integration module.
3
3
  */
4
4
 
5
- export type { SlackClient } from "./types.js";
5
+ export type {
6
+ SlackClient,
7
+ SlackResponse,
8
+ SlackErrorResponse,
9
+ } from "./types.js";
10
+ export { SlackErrorSchema } from "./types.js";
6
11
  export { SlackClientImpl } from "./client.js";