@zapier/zapier-sdk 0.5.2 → 0.6.0

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 (115) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +107 -83
  3. package/dist/index.cjs +320 -50
  4. package/dist/index.d.mts +409 -340
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +0 -1
  7. package/dist/index.mjs +320 -50
  8. package/dist/plugins/api/index.js +1 -1
  9. package/dist/plugins/findFirstAuthentication/index.d.ts.map +1 -1
  10. package/dist/plugins/findFirstAuthentication/index.js +1 -0
  11. package/dist/plugins/findUniqueAuthentication/index.d.ts.map +1 -1
  12. package/dist/plugins/findUniqueAuthentication/index.js +1 -0
  13. package/dist/plugins/getAction/index.d.ts.map +1 -1
  14. package/dist/plugins/getAction/index.js +1 -0
  15. package/dist/plugins/getAction/index.test.js +1 -1
  16. package/dist/plugins/getApp/index.d.ts +6 -3
  17. package/dist/plugins/getApp/index.d.ts.map +1 -1
  18. package/dist/plugins/getApp/index.js +8 -18
  19. package/dist/plugins/getApp/index.test.js +2 -0
  20. package/dist/plugins/getAuthentication/index.d.ts.map +1 -1
  21. package/dist/plugins/getAuthentication/index.js +1 -0
  22. package/dist/plugins/getAuthentication/index.test.js +12 -1
  23. package/dist/plugins/getProfile/index.d.ts.map +1 -1
  24. package/dist/plugins/getProfile/index.js +1 -0
  25. package/dist/plugins/listActions/index.d.ts +5 -3
  26. package/dist/plugins/listActions/index.d.ts.map +1 -1
  27. package/dist/plugins/listActions/index.js +6 -6
  28. package/dist/plugins/listActions/index.test.js +26 -74
  29. package/dist/plugins/listActions/schemas.d.ts +4 -4
  30. package/dist/plugins/listApps/index.d.ts.map +1 -1
  31. package/dist/plugins/listApps/index.js +1 -0
  32. package/dist/plugins/listApps/schemas.d.ts +2 -2
  33. package/dist/plugins/listAuthentications/index.d.ts +4 -2
  34. package/dist/plugins/listAuthentications/index.d.ts.map +1 -1
  35. package/dist/plugins/listAuthentications/index.js +9 -12
  36. package/dist/plugins/listAuthentications/index.test.js +33 -40
  37. package/dist/plugins/listAuthentications/schemas.d.ts +4 -4
  38. package/dist/plugins/listInputFields/index.d.ts +3 -1
  39. package/dist/plugins/listInputFields/index.d.ts.map +1 -1
  40. package/dist/plugins/listInputFields/index.js +5 -5
  41. package/dist/plugins/listInputFields/index.test.js +10 -8
  42. package/dist/plugins/listInputFields/schemas.d.ts +4 -4
  43. package/dist/plugins/lockVersion/index.d.ts +24 -0
  44. package/dist/plugins/lockVersion/index.d.ts.map +1 -0
  45. package/dist/plugins/lockVersion/index.js +72 -0
  46. package/dist/plugins/lockVersion/index.test.d.ts +2 -0
  47. package/dist/plugins/lockVersion/index.test.d.ts.map +1 -0
  48. package/dist/plugins/lockVersion/index.test.js +129 -0
  49. package/dist/plugins/lockVersion/schemas.d.ts +10 -0
  50. package/dist/plugins/lockVersion/schemas.d.ts.map +1 -0
  51. package/dist/plugins/lockVersion/schemas.js +6 -0
  52. package/dist/plugins/manifest/index.d.ts +24 -0
  53. package/dist/plugins/manifest/index.d.ts.map +1 -0
  54. package/dist/plugins/manifest/index.js +119 -0
  55. package/dist/plugins/manifest/index.test.d.ts +2 -0
  56. package/dist/plugins/manifest/index.test.d.ts.map +1 -0
  57. package/dist/plugins/manifest/index.test.js +331 -0
  58. package/dist/plugins/manifest/schemas.d.ts +64 -0
  59. package/dist/plugins/manifest/schemas.d.ts.map +1 -0
  60. package/dist/plugins/manifest/schemas.js +25 -0
  61. package/dist/plugins/registry/index.d.ts +9 -1
  62. package/dist/plugins/registry/index.d.ts.map +1 -1
  63. package/dist/plugins/registry/index.js +68 -3
  64. package/dist/plugins/request/index.d.ts.map +1 -1
  65. package/dist/plugins/request/index.js +1 -0
  66. package/dist/plugins/request/index.test.js +6 -1
  67. package/dist/plugins/request/schemas.d.ts +4 -4
  68. package/dist/plugins/runAction/index.d.ts +2 -0
  69. package/dist/plugins/runAction/index.d.ts.map +1 -1
  70. package/dist/plugins/runAction/index.js +5 -5
  71. package/dist/plugins/runAction/index.test.js +9 -8
  72. package/dist/plugins/runAction/schemas.d.ts +4 -4
  73. package/dist/sdk.d.ts +3 -3
  74. package/dist/sdk.d.ts.map +1 -1
  75. package/dist/sdk.js +18 -7
  76. package/dist/sdk.test.js +1 -1
  77. package/dist/types/plugin.d.ts +10 -2
  78. package/dist/types/plugin.d.ts.map +1 -1
  79. package/dist/types/sdk.d.ts +13 -2
  80. package/dist/types/sdk.d.ts.map +1 -1
  81. package/package.json +1 -1
  82. package/src/index.ts +0 -2
  83. package/src/plugins/api/index.ts +1 -1
  84. package/src/plugins/findFirstAuthentication/index.ts +1 -0
  85. package/src/plugins/findUniqueAuthentication/index.ts +1 -0
  86. package/src/plugins/getAction/index.test.ts +1 -1
  87. package/src/plugins/getAction/index.ts +1 -0
  88. package/src/plugins/getApp/index.test.ts +2 -0
  89. package/src/plugins/getApp/index.ts +12 -24
  90. package/src/plugins/getAuthentication/index.test.ts +13 -3
  91. package/src/plugins/getAuthentication/index.ts +1 -0
  92. package/src/plugins/getProfile/index.ts +1 -0
  93. package/src/plugins/listActions/index.test.ts +30 -89
  94. package/src/plugins/listActions/index.ts +13 -9
  95. package/src/plugins/listApps/index.ts +1 -0
  96. package/src/plugins/listAuthentications/index.test.ts +38 -47
  97. package/src/plugins/listAuthentications/index.ts +21 -18
  98. package/src/plugins/listInputFields/index.test.ts +12 -9
  99. package/src/plugins/listInputFields/index.ts +10 -6
  100. package/src/plugins/lockVersion/index.test.ts +176 -0
  101. package/src/plugins/lockVersion/index.ts +112 -0
  102. package/src/plugins/lockVersion/schemas.ts +9 -0
  103. package/src/plugins/manifest/index.test.ts +439 -0
  104. package/src/plugins/manifest/index.ts +171 -0
  105. package/src/plugins/manifest/schemas.ts +53 -0
  106. package/src/plugins/registry/index.ts +89 -8
  107. package/src/plugins/request/index.test.ts +8 -4
  108. package/src/plugins/request/index.ts +1 -0
  109. package/src/plugins/runAction/index.test.ts +9 -8
  110. package/src/plugins/runAction/index.ts +13 -7
  111. package/src/sdk.test.ts +1 -1
  112. package/src/sdk.ts +22 -7
  113. package/src/types/plugin.ts +14 -2
  114. package/src/types/sdk.ts +15 -1
  115. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,439 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { manifestPlugin, loadManifestFromFile } from "./index";
3
+ import { createSdk } from "../../sdk";
4
+ import type { Manifest } from "./schemas";
5
+
6
+ // Mock fs and path modules
7
+ vi.mock("fs", () => ({
8
+ readFileSync: vi.fn(),
9
+ }));
10
+
11
+ vi.mock("path", () => ({
12
+ resolve: vi.fn((path) => `/resolved/${path}`),
13
+ }));
14
+
15
+ import { readFileSync } from "fs";
16
+ import { resolve } from "path";
17
+ import type { ApiClient } from "../../api";
18
+
19
+ const mockReadFileSync = vi.mocked(readFileSync);
20
+ const mockResolve = vi.mocked(resolve);
21
+
22
+ describe("manifestPlugin", () => {
23
+ const mockManifest = {
24
+ apps: {
25
+ slack: {
26
+ implementationName: "SlackCLIAPI",
27
+ version: "1.21.1",
28
+ },
29
+ "google-sheets": {
30
+ implementationName: "GoogleSheetsCLIAPI",
31
+ version: "2.0.0",
32
+ },
33
+ },
34
+ };
35
+
36
+ const mockManifestContent = JSON.stringify(mockManifest);
37
+
38
+ let mockApiClient: ApiClient;
39
+ let mockSdk: any;
40
+
41
+ beforeEach(() => {
42
+ vi.clearAllMocks();
43
+ vi.spyOn(console, "warn").mockImplementation(() => {});
44
+
45
+ mockApiClient = {
46
+ get: vi.fn().mockResolvedValue({
47
+ count: 1,
48
+ next: null,
49
+ previous: null,
50
+ results: [
51
+ {
52
+ selected_api: "SlackCLIAPI@1.29.0",
53
+ app_id: null,
54
+ service_id: null,
55
+ auth_type: "oauth",
56
+ auth_fields: [
57
+ {
58
+ key: "access_token",
59
+ required: true,
60
+ type: "unicode",
61
+ computed: true,
62
+ },
63
+ ],
64
+ is_deprecated: false,
65
+ is_private_only: false,
66
+ is_invite_only: false,
67
+ is_beta: false,
68
+ is_premium: false,
69
+ is_hidden: false,
70
+ name: "Slack (1.29.0)",
71
+ name_clean: "Slack",
72
+ version: "1.29.0",
73
+ slug: null,
74
+ images: {
75
+ url_16x16:
76
+ "https://zapier-images.imgix.net/storage/services/6cf3f5a461feadfba7abc93c4c395b33_2.png?auto=format%2Ccompress&fit=crop&h=16&ixlib=python-3.0.0&q=50&w=16",
77
+ url_32x32:
78
+ "https://zapier-images.imgix.net/storage/services/6cf3f5a461feadfba7abc93c4c395b33_2.png?auto=format%2Ccompress&fit=crop&h=32&ixlib=python-3.0.0&q=50&w=32",
79
+ url_64x64:
80
+ "https://zapier-images.imgix.net/storage/services/6cf3f5a461feadfba7abc93c4c395b33_2.png?auto=format%2Ccompress&fit=crop&h=64&ixlib=python-3.0.0&q=50&w=64",
81
+ url_128x128:
82
+ "https://zapier-images.imgix.net/storage/services/6cf3f5a461feadfba7abc93c4c395b33_2.png?auto=format%2Ccompress&fit=crop&h=128&ixlib=python-3.0.0&q=50&w=128",
83
+ },
84
+ primary_color: null,
85
+ secondary_color: null,
86
+ classification: "third-party",
87
+ current_implementation: "SlackCLIAPI@1.30.0",
88
+ is_adoptable: true,
89
+ is_usable: true,
90
+ update_cta_level: "info",
91
+ update_cta_message:
92
+ "This version is legacy. Consider updating to the latest version for better support.",
93
+ badges: [
94
+ {
95
+ label: "Legacy",
96
+ color: "primary",
97
+ tooltip_markdown:
98
+ "This version is legacy. Consider updating to the latest version for better support.",
99
+ },
100
+ ],
101
+ },
102
+ ],
103
+ }),
104
+ } as Partial<ApiClient> as ApiClient;
105
+
106
+ mockSdk = {
107
+ listApps: vi.fn().mockReturnValue({
108
+ items: vi.fn().mockReturnValue(
109
+ [
110
+ {
111
+ title: "Slack",
112
+ key: "slack",
113
+ current_implementation_id: "SlackCLIAPI@1.30.0",
114
+ description: "Team communication platform",
115
+ },
116
+ ][Symbol.iterator](),
117
+ ),
118
+ }),
119
+ };
120
+ });
121
+
122
+ afterEach(() => {
123
+ vi.restoreAllMocks();
124
+ });
125
+
126
+ const apiPlugin = () => ({
127
+ context: {
128
+ api: mockApiClient,
129
+ },
130
+ });
131
+
132
+ const listAppsMockPlugin = () => ({
133
+ listApps: mockSdk.listApps,
134
+ context: {
135
+ meta: {
136
+ listApps: {
137
+ inputSchema: {} as any,
138
+ },
139
+ },
140
+ },
141
+ });
142
+
143
+ function createTestSdk(options: any = {}) {
144
+ return createSdk(options)
145
+ .addPlugin(apiPlugin)
146
+ .addPlugin(listAppsMockPlugin)
147
+ .addPlugin(manifestPlugin);
148
+ }
149
+
150
+ describe("plugin initialization", () => {
151
+ it("should provide manifest context with direct manifest", () => {
152
+ const sdk = createTestSdk({ manifest: mockManifest });
153
+ const context = sdk.getContext();
154
+
155
+ expect(context.manifest).toEqual(mockManifest);
156
+
157
+ expect(context.getVersionedImplementationId).toBeInstanceOf(Function);
158
+ expect(context.getImplementation).toBeInstanceOf(Function);
159
+ });
160
+
161
+ it("should provide manifest context with manifestPath", () => {
162
+ mockReadFileSync.mockReturnValue(mockManifestContent);
163
+
164
+ const sdk = createTestSdk({ manifestPath: "manifest.json" });
165
+ const context = sdk.getContext();
166
+
167
+ expect(mockResolve).toHaveBeenCalledWith("manifest.json");
168
+ expect(mockReadFileSync).toHaveBeenCalledWith(
169
+ "/resolved/manifest.json",
170
+ "utf8",
171
+ );
172
+ expect(context.manifest).toEqual(mockManifest);
173
+ });
174
+
175
+ it("should provide null manifest when no manifest or path provided", () => {
176
+ const sdk = createTestSdk();
177
+ const context = sdk.getContext();
178
+
179
+ expect(context.manifest).toBeNull();
180
+ });
181
+
182
+ it("should prioritize direct manifest over manifestPath", () => {
183
+ mockReadFileSync.mockReturnValue(
184
+ '{"apps": {"other": {"implementationName": "Other", "version": "1.0.0"}}}',
185
+ );
186
+
187
+ const sdk = createTestSdk({
188
+ manifest: mockManifest,
189
+ manifestPath: "manifest.json",
190
+ });
191
+ const context = sdk.getContext();
192
+
193
+ expect(context.manifest).toEqual(mockManifest);
194
+ expect(mockReadFileSync).not.toHaveBeenCalled();
195
+ });
196
+ });
197
+
198
+ describe("getVersionedImplementationId function", () => {
199
+ it("should return versioned implementation ID when app exists in manifest", async () => {
200
+ const sdk = createTestSdk({ manifest: mockManifest });
201
+ const context = sdk.getContext();
202
+
203
+ const result = await context.getVersionedImplementationId("slack");
204
+ expect(result).toBe("SlackCLIAPI@1.21.1");
205
+ });
206
+
207
+ it("should fetch and return versioned implementation ID when app not in manifest", async () => {
208
+ const sdk = createTestSdk();
209
+ const context = sdk.getContext();
210
+
211
+ const result = await context.getVersionedImplementationId("slack");
212
+ expect(result).toBe("SlackCLIAPI@1.30.0");
213
+ });
214
+
215
+ it("should return null when app does not exist", async () => {
216
+ mockSdk.listApps = vi.fn().mockReturnValue({
217
+ items: vi.fn().mockReturnValue([][Symbol.iterator]()),
218
+ });
219
+
220
+ const sdk = createTestSdk();
221
+ const context = sdk.getContext();
222
+
223
+ const result = await context.getVersionedImplementationId("nonexistent");
224
+ expect(result).toBeNull();
225
+ });
226
+ });
227
+
228
+ describe("getImplementation function", () => {
229
+ it("should return app from manifest when available", async () => {
230
+ const sdk = createTestSdk({ manifest: mockManifest });
231
+ const context = sdk.getContext();
232
+
233
+ const result = await context.getImplementation("slack");
234
+ expect(result).toBeDefined();
235
+ expect(mockApiClient.get).toHaveBeenCalledWith(
236
+ "/api/v4/implementations/",
237
+ {
238
+ searchParams: {
239
+ selected_apis: "SlackCLIAPI@1.21.1",
240
+ },
241
+ },
242
+ );
243
+ });
244
+
245
+ it("should fetch app from API when not in manifest", async () => {
246
+ const sdk = createTestSdk();
247
+ const context = sdk.getContext();
248
+
249
+ const result = await context.getImplementation("slack");
250
+ expect(result).toBeDefined();
251
+ expect(result?.title).toBe("Slack");
252
+ expect(result?.current_implementation_id).toBe("SlackCLIAPI@1.30.0");
253
+ });
254
+
255
+ it("should return null when app cannot be fetched", async () => {
256
+ mockSdk.listApps = vi.fn().mockReturnValue({
257
+ items: vi.fn().mockReturnValue([][Symbol.iterator]()),
258
+ });
259
+
260
+ const sdk = createTestSdk();
261
+ const context = sdk.getContext();
262
+
263
+ const result = await context.getImplementation("nonexistent");
264
+ expect(result).toBeNull();
265
+ });
266
+ });
267
+
268
+ describe("manifest parsing", () => {
269
+ it("should parse valid manifest content", () => {
270
+ const sdk = createTestSdk({ manifest: mockManifest });
271
+ const context = sdk.getContext();
272
+
273
+ expect(context.manifest).toEqual(mockManifest);
274
+ });
275
+
276
+ it("should handle manifest with missing version", () => {
277
+ const manifestWithoutVersion: Manifest = {
278
+ apps: {
279
+ slack: {
280
+ implementationName: "SlackCLIAPI",
281
+ },
282
+ },
283
+ };
284
+
285
+ const sdk = createTestSdk({ manifest: manifestWithoutVersion });
286
+ const context = sdk.getContext();
287
+
288
+ expect(context.manifest).toEqual(manifestWithoutVersion);
289
+ });
290
+
291
+ it("should handle empty manifest", () => {
292
+ const sdk = createTestSdk({ manifest: {} });
293
+ const context = sdk.getContext();
294
+
295
+ expect(context.manifest).toEqual({});
296
+ });
297
+ });
298
+
299
+ describe("file loading", () => {
300
+ it("should load manifest from file successfully", () => {
301
+ mockReadFileSync.mockReturnValue(mockManifestContent);
302
+
303
+ const sdk = createTestSdk({ manifestPath: "manifest.json" });
304
+ const context = sdk.getContext();
305
+
306
+ expect(mockResolve).toHaveBeenCalledWith("manifest.json");
307
+ expect(mockReadFileSync).toHaveBeenCalledWith(
308
+ "/resolved/manifest.json",
309
+ "utf8",
310
+ );
311
+ expect(context.manifest).toEqual(mockManifest);
312
+ });
313
+
314
+ it("should handle file read errors gracefully", () => {
315
+ mockReadFileSync.mockImplementation(() => {
316
+ throw new Error("File not found");
317
+ });
318
+
319
+ const sdk = createTestSdk({ manifestPath: "nonexistent.json" });
320
+ const context = sdk.getContext();
321
+
322
+ expect(context.manifest).toBeNull();
323
+ expect(console.warn).toHaveBeenCalledWith(
324
+ expect.stringContaining(
325
+ "Failed to load manifest from nonexistent.json",
326
+ ),
327
+ expect.any(Error),
328
+ );
329
+ });
330
+
331
+ it("should handle invalid JSON gracefully", () => {
332
+ mockReadFileSync.mockReturnValue("invalid json content");
333
+
334
+ const sdk = createTestSdk({ manifestPath: "invalid.json" });
335
+ const context = sdk.getContext();
336
+
337
+ expect(context.manifest).toBeNull();
338
+ expect(console.warn).toHaveBeenCalledWith(
339
+ expect.stringContaining(
340
+ "Failed to parse manifest from /resolved/invalid.json",
341
+ ),
342
+ expect.any(SyntaxError),
343
+ );
344
+ });
345
+
346
+ it("should handle JSON without apps property", () => {
347
+ mockReadFileSync.mockReturnValue('{"other": "data"}');
348
+
349
+ const sdk = createTestSdk({ manifestPath: "no-apps.json" });
350
+ const context = sdk.getContext();
351
+
352
+ expect(context.manifest).toBeNull();
353
+ });
354
+
355
+ it("should handle JSON with invalid apps format", () => {
356
+ mockReadFileSync.mockReturnValue('{"apps": "not an object"}');
357
+
358
+ const sdk = createTestSdk({ manifestPath: "invalid-apps.json" });
359
+ const context = sdk.getContext();
360
+
361
+ expect(context.manifest).toBeNull();
362
+ });
363
+ });
364
+
365
+ describe("integration with SDK", () => {
366
+ it("should work with createSdk", async () => {
367
+ const sdk = createSdk({ manifest: mockManifest })
368
+ .addPlugin(apiPlugin)
369
+ .addPlugin(listAppsMockPlugin)
370
+ .addPlugin(manifestPlugin);
371
+ const context = sdk.getContext();
372
+
373
+ expect(context.manifest).toEqual(mockManifest);
374
+ const versionedId = await context.getVersionedImplementationId("slack");
375
+ expect(versionedId).toBe("SlackCLIAPI@1.21.1");
376
+ });
377
+ });
378
+ });
379
+
380
+ describe("loadManifestFromFile", () => {
381
+ const mockManifestContent = JSON.stringify({
382
+ apps: {
383
+ slack: {
384
+ implementationName: "SlackCLIAPI",
385
+ version: "1.21.1",
386
+ },
387
+ "google-sheets": {
388
+ implementationName: "GoogleSheetsCLIAPI",
389
+ version: "2.0.0",
390
+ },
391
+ },
392
+ });
393
+
394
+ beforeEach(() => {
395
+ vi.clearAllMocks();
396
+ });
397
+
398
+ it("should load and parse manifest from file", () => {
399
+ mockReadFileSync.mockReturnValue(mockManifestContent);
400
+
401
+ const result = loadManifestFromFile("manifest.json");
402
+
403
+ expect(mockResolve).toHaveBeenCalledWith("manifest.json");
404
+ expect(mockReadFileSync).toHaveBeenCalledWith(
405
+ "/resolved/manifest.json",
406
+ "utf8",
407
+ );
408
+ expect(result).toEqual({
409
+ apps: {
410
+ slack: {
411
+ implementationName: "SlackCLIAPI",
412
+ version: "1.21.1",
413
+ },
414
+ "google-sheets": {
415
+ implementationName: "GoogleSheetsCLIAPI",
416
+ version: "2.0.0",
417
+ },
418
+ },
419
+ });
420
+ });
421
+
422
+ it("should handle file read errors", () => {
423
+ mockReadFileSync.mockImplementation(() => {
424
+ throw new Error("File not found");
425
+ });
426
+
427
+ const result = loadManifestFromFile("nonexistent.json");
428
+
429
+ expect(result).toBeNull();
430
+ });
431
+
432
+ it("should handle invalid JSON content", () => {
433
+ mockReadFileSync.mockReturnValue("invalid json");
434
+
435
+ const result = loadManifestFromFile("invalid.json");
436
+
437
+ expect(result).toBeNull();
438
+ });
439
+ });
@@ -0,0 +1,171 @@
1
+ import { readFileSync } from "fs";
2
+ import { resolve } from "path";
3
+ import type {
4
+ GetImplementation,
5
+ GetManifestEntry,
6
+ GetVersionedImplementationId,
7
+ Manifest,
8
+ } from "./schemas";
9
+ import { ManifestSchema, ManifestPluginOptionsSchema } from "./schemas";
10
+ import type { GetSdkType, Plugin } from "../../types/plugin";
11
+ import { z } from "zod";
12
+ import { ApiClient } from "../../api";
13
+ import { ListAppsPluginProvides } from "../listApps";
14
+ import { ImplementationsResponse } from "../../api/types";
15
+ import {
16
+ normalizeImplementationToAppItem,
17
+ splitVersionedKey,
18
+ } from "../../utils/domain-utils";
19
+
20
+ export type ManifestPluginOptions = z.infer<typeof ManifestPluginOptionsSchema>;
21
+
22
+ export interface ManifestPluginProvides {
23
+ context: {
24
+ manifest: Manifest | null;
25
+ getVersionedImplementationId: GetVersionedImplementationId;
26
+ getManifestEntry: GetManifestEntry;
27
+ getImplementation: GetImplementation;
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Parse manifest content from a string
33
+ */
34
+ function parseManifestContent(
35
+ content: string,
36
+ source: string,
37
+ ): Manifest | null {
38
+ try {
39
+ const parsed = JSON.parse(content);
40
+
41
+ if (parsed?.apps && typeof parsed?.apps === "object") {
42
+ const result = ManifestSchema.safeParse(parsed);
43
+ if (result.success) {
44
+ return result.data;
45
+ }
46
+ console.warn(`⚠️ Invalid manifest format in ${source}: ${result.error}`);
47
+ }
48
+
49
+ return null;
50
+ } catch (error) {
51
+ console.warn(`⚠️ Failed to parse manifest from ${source}:`, error);
52
+ return null;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Load manifest from a file path synchronously
58
+ * Supports local files (Node.js only)
59
+ */
60
+ export function loadManifestFromFile(filePath: string): Manifest | null {
61
+ try {
62
+ // Resolve relative paths relative to current working directory
63
+ const resolvedPath = resolve(filePath);
64
+ const content = readFileSync(resolvedPath, "utf8");
65
+ return parseManifestContent(content, resolvedPath);
66
+ } catch (error) {
67
+ console.warn(`⚠️ Failed to load manifest from ${filePath}:`, error);
68
+ return null;
69
+ }
70
+ }
71
+
72
+ const emitWarning = (appKey: string) => {
73
+ console.warn(
74
+ `\n${"⚠️".padEnd(3)} ${"WARNING".padEnd(8)} No manifest version found for '${appKey}'`,
75
+ );
76
+ console.warn(
77
+ ` ${"↳".padEnd(3)} Using a manifest ensures version locking and prevents unexpected behavior due to version changes.`,
78
+ );
79
+ console.warn(
80
+ ` ${"↳".padEnd(3)} Generate/update the manifest with: \`zapier-sdk update-manifest ${appKey}\`\n`,
81
+ );
82
+ };
83
+
84
+ export const manifestPlugin: Plugin<
85
+ GetSdkType<ListAppsPluginProvides>,
86
+ { api: ApiClient },
87
+ ManifestPluginProvides
88
+ > = (params) => {
89
+ const { sdk, context } = params;
90
+ const { api, options } = context;
91
+ const { manifestPath = ".zapierrc", manifest } = options || {};
92
+
93
+ const resolvedManifest = (() => {
94
+ // If manifest is provided directly, use it
95
+ if (manifest) {
96
+ return manifest;
97
+ }
98
+ // If manifestPath is provided, load from file
99
+ if (manifestPath) {
100
+ return loadManifestFromFile(manifestPath);
101
+ }
102
+ return null;
103
+ })();
104
+
105
+ const getManifestEntry = (appKey: string) => {
106
+ return resolvedManifest?.apps?.[appKey] || null;
107
+ };
108
+
109
+ const getImplementation = async (appKey: string) => {
110
+ let selectedApi = null;
111
+ const manifestImplementation = resolvedManifest?.apps?.[appKey];
112
+ const [versionlessAppKey, version] = splitVersionedKey(appKey);
113
+
114
+ // Use versioned app key if provided
115
+ if (version) {
116
+ selectedApi = `${versionlessAppKey}@${version}`;
117
+ // Otherwise, use manifest entry if available
118
+ } else if (manifestImplementation) {
119
+ selectedApi = `${manifestImplementation.implementationName}@${manifestImplementation.version || "latest"}`;
120
+ }
121
+
122
+ if (selectedApi) {
123
+ const searchParams = {
124
+ selected_apis: selectedApi,
125
+ };
126
+ const implementationData: ImplementationsResponse = await api.get(
127
+ "/api/v4/implementations/",
128
+ {
129
+ searchParams,
130
+ },
131
+ );
132
+ const implementationResults = implementationData.results[0];
133
+ if (!implementationResults) return null;
134
+ return normalizeImplementationToAppItem(implementationResults);
135
+ }
136
+ emitWarning(appKey);
137
+
138
+ const appsIterator = sdk.listApps({ appKeys: [appKey] }).items();
139
+
140
+ const apps = [];
141
+ for await (const app of appsIterator) {
142
+ apps.push(app);
143
+ break; // Only need the first result
144
+ }
145
+
146
+ if (apps.length === 0) {
147
+ return null;
148
+ }
149
+ const app = apps[0];
150
+ return app;
151
+ };
152
+
153
+ const getVersionedImplementationId = async (appKey: string) => {
154
+ const manifestEntry = getManifestEntry(appKey);
155
+ if (manifestEntry) {
156
+ return `${manifestEntry.implementationName}@${manifestEntry.version || "latest"}`;
157
+ }
158
+ const implementation = await getImplementation(appKey);
159
+ if (!implementation) return null;
160
+ return implementation.current_implementation_id;
161
+ };
162
+
163
+ return {
164
+ context: {
165
+ manifest: resolvedManifest,
166
+ getVersionedImplementationId,
167
+ getManifestEntry,
168
+ getImplementation,
169
+ },
170
+ };
171
+ };
@@ -0,0 +1,53 @@
1
+ import { z } from "zod";
2
+ import { AppItem } from "../../types/domain";
3
+
4
+ export type ManifestEntry = {
5
+ implementationName: string;
6
+ version?: string;
7
+ };
8
+
9
+ export type GetManifestEntry = (appKey: string) => ManifestEntry | null;
10
+
11
+ export type GetVersionedImplementationId = (
12
+ appKey: string,
13
+ ) => Promise<string | null>;
14
+
15
+ export type GetImplementation = (appKey: string) => Promise<AppItem | null>;
16
+
17
+ export type Manifest = {
18
+ apps: Record<string, ManifestEntry>;
19
+ };
20
+
21
+ /**
22
+ * Manifest schema for version locking
23
+ * Maps app keys to their locked version information
24
+ */
25
+ export const ManifestSchema = z
26
+ .object({
27
+ apps: z.record(
28
+ z.string(),
29
+ z.object({
30
+ implementationName: z
31
+ .string()
32
+ .describe(
33
+ "Base implementation name without version (e.g., 'SlackCLIAPI')",
34
+ ),
35
+ version: z.string().describe("Version string (e.g., '1.21.1')"),
36
+ }),
37
+ ),
38
+ })
39
+ .describe("Manifest mapping app keys to version information");
40
+
41
+ export const ManifestPluginOptionsSchema = z.object({
42
+ manifestPath: z.string().optional().describe("Path to manifest file"),
43
+ manifest: z
44
+ .record(
45
+ z.string(),
46
+ z.object({
47
+ implementationName: z.string(),
48
+ version: z.string().optional(),
49
+ }),
50
+ )
51
+ .optional()
52
+ .describe("Direct manifest object"),
53
+ });